trustgraph/ts/packages/cli/src/commands/util.ts

97 lines
2.8 KiB
TypeScript
Raw Normal View History

2026-04-05 21:09:33 -05:00
/**
* Shared CLI utilities.
*/
import type { Command } from "commander";
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";
import * as S from "effect/Schema";
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;
}
export function getOpts(cmd: Command): CliOpts {
// Walk up to root command to get global options
let root = cmd;
2026-05-12 08:06:58 -05:00
while (root.parent !== null) root = root.parent;
2026-04-05 21:09:33 -05:00
return root.opts() as CliOpts;
}
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
export function createSocket(opts: CliOpts): Promise<BaseApi> {
return Effect.runPromise(createSocketEffect(opts));
}
export const withSocket = <A, E, R>(
cmd: Command,
use: (socket: BaseApi, opts: CliOpts) => Effect.Effect<A, E, R>,
) =>
Effect.acquireUseRelease(
createSocketEffect(getOpts(cmd)),
(socket) => use(socket, getOpts(cmd)),
(socket) =>
Effect.sync(() => {
socket.close();
}),
);