2026-04-05 21:09:33 -05:00
|
|
|
/**
|
|
|
|
|
* Shared CLI utilities.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-04-05 22:44:45 -05:00
|
|
|
import { createTrustGraphSocket, type BaseApi } from "@trustgraph/client";
|
2026-06-02 00:22:04 -05:00
|
|
|
import { Duration, Effect } from "effect";
|
2026-06-06 10:33:10 -05:00
|
|
|
import * as O from "effect/Option";
|
2026-06-02 00:22:04 -05:00
|
|
|
import * as S from "effect/Schema";
|
2026-06-06 10:33:10 -05:00
|
|
|
import * as Command from "effect/unstable/cli/Command";
|
|
|
|
|
import * as Flag from "effect/unstable/cli/Flag";
|
2026-04-05 21:09:33 -05:00
|
|
|
|
|
|
|
|
export interface CliOpts {
|
|
|
|
|
gateway: string;
|
2026-04-05 22:44:45 -05:00
|
|
|
user: string;
|
2026-04-05 21:09:33 -05:00
|
|
|
token?: string;
|
|
|
|
|
flow: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 10:33:10 -05:00
|
|
|
export const rootCommand = Command.make("tg").pipe(
|
|
|
|
|
Command.withDescription("TrustGraph CLI - interact with TrustGraph services"),
|
|
|
|
|
Command.withSharedFlags({
|
|
|
|
|
gateway: Flag.string("gateway").pipe(
|
|
|
|
|
Flag.withAlias("g"),
|
|
|
|
|
Flag.withDescription("Gateway WebSocket URL"),
|
|
|
|
|
Flag.withDefault("ws://localhost:8088/api/v1/rpc"),
|
|
|
|
|
),
|
|
|
|
|
user: Flag.string("user").pipe(
|
|
|
|
|
Flag.withAlias("u"),
|
|
|
|
|
Flag.withDescription("User identifier"),
|
|
|
|
|
Flag.withDefault("cli"),
|
|
|
|
|
),
|
|
|
|
|
token: Flag.string("token").pipe(
|
|
|
|
|
Flag.withDescription("Authentication token"),
|
|
|
|
|
Flag.optional,
|
|
|
|
|
),
|
|
|
|
|
flow: Flag.string("flow").pipe(
|
|
|
|
|
Flag.withAlias("f"),
|
|
|
|
|
Flag.withDescription("Flow ID"),
|
|
|
|
|
Flag.withDefault("default"),
|
|
|
|
|
),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
export const getOpts = Effect.gen(function* () {
|
|
|
|
|
const opts = yield* rootCommand;
|
|
|
|
|
const base = {
|
|
|
|
|
gateway: opts.gateway,
|
|
|
|
|
user: opts.user,
|
|
|
|
|
flow: opts.flow,
|
|
|
|
|
};
|
|
|
|
|
const token = O.getOrUndefined(opts.token);
|
|
|
|
|
return token === undefined ? base : { ...base, token } satisfies CliOpts;
|
|
|
|
|
});
|
2026-04-05 21:09:33 -05:00
|
|
|
|
2026-06-02 00:22:04 -05:00
|
|
|
export class CliCommandError extends S.TaggedErrorClass<CliCommandError>()(
|
|
|
|
|
"CliCommandError",
|
|
|
|
|
{
|
|
|
|
|
message: S.String,
|
|
|
|
|
operation: S.String,
|
|
|
|
|
},
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
export function cliCommandError(operation: string, error: unknown): CliCommandError {
|
|
|
|
|
const message = typeof error === "object" && error !== null && "message" in error
|
|
|
|
|
? String(error.message)
|
|
|
|
|
: String(error);
|
|
|
|
|
return CliCommandError.make({ operation, message });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const writeLine = (line: string) =>
|
|
|
|
|
Effect.sync(() => {
|
|
|
|
|
process.stdout.write(`${line}\n`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const writeJson = (value: unknown) =>
|
|
|
|
|
S.encodeUnknownEffect(S.UnknownFromJsonString)(value).pipe(
|
|
|
|
|
Effect.mapError((error) => cliCommandError("write-json", error)),
|
|
|
|
|
Effect.flatMap(writeLine),
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-05 22:44:45 -05:00
|
|
|
/**
|
|
|
|
|
* Create a BaseApi socket client and wait for the connection to be established.
|
|
|
|
|
* The client auto-connects; we listen for the first "connected/authenticated"
|
|
|
|
|
* state before handing it back to the caller.
|
|
|
|
|
*/
|
2026-06-02 00:22:04 -05:00
|
|
|
export function createSocketEffect(opts: CliOpts): Effect.Effect<BaseApi, CliCommandError> {
|
2026-04-05 22:44:45 -05:00
|
|
|
const socket = createTrustGraphSocket(opts.user, opts.token, opts.gateway);
|
|
|
|
|
|
2026-06-02 00:22:04 -05:00
|
|
|
return Effect.callback<void, CliCommandError>((resume) => {
|
2026-04-05 22:44:45 -05:00
|
|
|
const unsub = socket.onConnectionStateChange((state) => {
|
2026-06-02 00:22:04 -05:00
|
|
|
if (state.status === "authenticated" || state.status === "unauthenticated") {
|
2026-04-05 22:44:45 -05:00
|
|
|
unsub();
|
2026-06-02 00:22:04 -05:00
|
|
|
resume(Effect.void);
|
2026-04-05 22:44:45 -05:00
|
|
|
} else if (state.status === "failed") {
|
|
|
|
|
unsub();
|
2026-06-02 00:22:04 -05:00
|
|
|
resume(Effect.fail(cliCommandError("connect", state.lastError ?? "WebSocket connection failed")));
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-02 00:22:04 -05:00
|
|
|
return Effect.sync(() => {
|
|
|
|
|
unsub();
|
|
|
|
|
});
|
|
|
|
|
}).pipe(
|
|
|
|
|
Effect.timeout(Duration.seconds(15)),
|
|
|
|
|
Effect.catchTag("TimeoutError", () =>
|
|
|
|
|
Effect.fail(cliCommandError("connect", "Timed out waiting for WebSocket connection")),
|
|
|
|
|
),
|
|
|
|
|
Effect.as(socket),
|
|
|
|
|
);
|
2026-04-05 21:09:33 -05:00
|
|
|
}
|
2026-06-02 00:22:04 -05:00
|
|
|
|
2026-06-06 10:33:10 -05:00
|
|
|
export const withSocket = Effect.fn("withSocket")(function* <A, E, R>(
|
2026-06-02 00:22:04 -05:00
|
|
|
use: (socket: BaseApi, opts: CliOpts) => Effect.Effect<A, E, R>,
|
2026-06-06 10:33:10 -05:00
|
|
|
) {
|
|
|
|
|
const opts = yield* getOpts;
|
|
|
|
|
return yield* Effect.acquireUseRelease(
|
|
|
|
|
createSocketEffect(opts),
|
|
|
|
|
(socket) => use(socket, opts),
|
2026-06-02 00:22:04 -05:00
|
|
|
(socket) =>
|
|
|
|
|
Effect.sync(() => {
|
|
|
|
|
socket.close();
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-06-06 10:33:10 -05:00
|
|
|
});
|