@linkedclaw/cli 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * SubprocessHandler bridges the ProviderHandler interface to
3
+ * a user child process via stdin/stdout JSON-lines. Each inbound event gets
4
+ * an `id`; handler must respond with exactly one `{id, result}` or `{id, error}`.
5
+ *
6
+ * Wire protocol (one JSON object per line):
7
+ * CLI → child stdin: {id, type, ...event fields}
8
+ * child stdout → CLI: {id, result: {...}} | {id, error: {code, message}}
9
+ *
10
+ * Anything the child writes to stderr is passed through for operator visibility.
11
+ */
12
+
13
+ import { spawn, type ChildProcess } from "node:child_process";
14
+ import type { Readable, Writable } from "node:stream";
15
+ import { randomUUID } from "node:crypto";
16
+ import { createInterface } from "node:readline";
17
+ import type {
18
+ BroadcastExecuteEvent,
19
+ BroadcastExecuteResult,
20
+ BroadcastOfferDecision,
21
+ BroadcastOfferEvent,
22
+ InvokeEvent,
23
+ InvokeHandlerResult,
24
+ ProviderHandler,
25
+ SessionAcceptDecision,
26
+ SessionCreateEvent,
27
+ SessionEndInboundEvent,
28
+ SessionMessageEvent,
29
+ SessionReply,
30
+ } from "@linkedclaw/provider-runtime";
31
+
32
+ interface Pending {
33
+ resolve: (value: unknown) => void;
34
+ reject: (err: Error) => void;
35
+ timer?: ReturnType<typeof setTimeout>;
36
+ }
37
+
38
+ export interface SubprocessHandlerOptions {
39
+ /** Shell-style command. Uses /bin/sh -c on posix, cmd /c on windows. */
40
+ cmd: string;
41
+ /** Working directory for the child. */
42
+ cwd?: string;
43
+ /** Extra env for the child. */
44
+ env?: NodeJS.ProcessEnv;
45
+ /** Per-event default timeout (ms). Core already enforces its own timeouts. */
46
+ requestTimeoutMs?: number;
47
+ }
48
+
49
+ export class SubprocessHandler implements ProviderHandler {
50
+ private readonly child: ChildProcess;
51
+ private readonly stdin: Writable;
52
+ private readonly pending = new Map<string, Pending>();
53
+ private readonly requestTimeoutMs: number;
54
+
55
+ constructor(opts: SubprocessHandlerOptions) {
56
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 600_000;
57
+ const shell = process.platform === "win32" ? "cmd.exe" : "/bin/sh";
58
+ const shellArgs = process.platform === "win32" ? ["/c", opts.cmd] : ["-c", opts.cmd];
59
+ this.child = spawn(shell, shellArgs, {
60
+ stdio: ["pipe", "pipe", "inherit"],
61
+ cwd: opts.cwd,
62
+ env: { ...process.env, ...opts.env },
63
+ });
64
+ if (!this.child.stdin || !this.child.stdout) {
65
+ throw new Error("subprocess handler: child stdin/stdout not available");
66
+ }
67
+ this.stdin = this.child.stdin;
68
+ const rl = createInterface({ input: this.child.stdout as Readable });
69
+ rl.on("line", (line) => this.handleLine(line));
70
+ this.child.on("exit", (code, signal) => this.handleExit(code, signal));
71
+ }
72
+
73
+ // ───── ProviderHandler impl ─────
74
+
75
+ async onSessionCreate(evt: SessionCreateEvent): Promise<SessionAcceptDecision> {
76
+ return this.request<SessionAcceptDecision>(evt);
77
+ }
78
+
79
+ async onSessionMessage(evt: SessionMessageEvent): Promise<SessionReply> {
80
+ const r = await this.request<unknown>(evt);
81
+ if (r === null || r === undefined) return null;
82
+ if (typeof r === "string") return r;
83
+ if (typeof r === "object") {
84
+ const obj = r as Record<string, unknown>;
85
+ if (typeof obj["content"] === "string") return obj["content"] as string;
86
+ if ("payload" in obj) return obj as { payload: unknown };
87
+ }
88
+ return null;
89
+ }
90
+
91
+ async onSessionEnd(evt: SessionEndInboundEvent): Promise<void> {
92
+ try {
93
+ await this.request<unknown>(evt);
94
+ } catch {
95
+ // session is ending anyway; ignore handler errors here
96
+ }
97
+ }
98
+
99
+ async onInvoke(evt: InvokeEvent): Promise<InvokeHandlerResult> {
100
+ return this.request<InvokeHandlerResult>(evt);
101
+ }
102
+
103
+ async onBroadcastOffer(evt: BroadcastOfferEvent): Promise<BroadcastOfferDecision> {
104
+ return this.request<BroadcastOfferDecision>(evt);
105
+ }
106
+
107
+ async onBroadcastExecute(evt: BroadcastExecuteEvent): Promise<BroadcastExecuteResult> {
108
+ return this.request<BroadcastExecuteResult>(evt);
109
+ }
110
+
111
+ // ───── shutdown ─────
112
+
113
+ async close(): Promise<void> {
114
+ if (this.child.exitCode !== null) return;
115
+ this.stdin.end();
116
+ try {
117
+ this.child.kill("SIGTERM");
118
+ } catch {
119
+ // already gone
120
+ }
121
+ for (const p of this.pending.values()) {
122
+ p.reject(new Error("handler_closed"));
123
+ if (p.timer) clearTimeout(p.timer);
124
+ }
125
+ this.pending.clear();
126
+ }
127
+
128
+ // ───── internals ─────
129
+
130
+ private request<T>(frame: { type: string } & object): Promise<T> {
131
+ const id = randomUUID();
132
+ const line = JSON.stringify({ id, ...frame }) + "\n";
133
+ return new Promise<T>((resolve, reject) => {
134
+ const timer = setTimeout(() => {
135
+ this.pending.delete(id);
136
+ reject(new Error(`handler_timeout: no response for ${frame.type} after ${this.requestTimeoutMs}ms`));
137
+ }, this.requestTimeoutMs);
138
+ if (typeof (timer as unknown as { unref?: () => void }).unref === "function") {
139
+ (timer as unknown as { unref: () => void }).unref();
140
+ }
141
+ this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });
142
+ this.stdin.write(line, (err) => {
143
+ if (err) {
144
+ this.pending.delete(id);
145
+ clearTimeout(timer);
146
+ reject(err);
147
+ }
148
+ });
149
+ });
150
+ }
151
+
152
+ private handleLine(line: string): void {
153
+ const trimmed = line.trim();
154
+ if (!trimmed) return;
155
+ let msg: unknown;
156
+ try {
157
+ msg = JSON.parse(trimmed);
158
+ } catch {
159
+ process.stderr.write(`handler wrote non-JSON line: ${trimmed}\n`);
160
+ return;
161
+ }
162
+ if (typeof msg !== "object" || msg === null) return;
163
+ const m = msg as { id?: unknown; result?: unknown; error?: unknown };
164
+ if (typeof m.id !== "string") return;
165
+ const p = this.pending.get(m.id);
166
+ if (!p) return;
167
+ this.pending.delete(m.id);
168
+ if (p.timer) clearTimeout(p.timer);
169
+ if (m.error !== undefined) {
170
+ const err = m.error as { code?: string; message?: string };
171
+ p.reject(Object.assign(new Error(err.message ?? "handler_error"), { code: err.code }));
172
+ } else {
173
+ p.resolve(m.result);
174
+ }
175
+ }
176
+
177
+ private handleExit(code: number | null, signal: NodeJS.Signals | null): void {
178
+ const reason = signal ?? `exit ${code}`;
179
+ for (const p of this.pending.values()) {
180
+ p.reject(new Error(`handler_crashed (${reason})`));
181
+ if (p.timer) clearTimeout(p.timer);
182
+ }
183
+ this.pending.clear();
184
+ }
185
+ }
package/src/output.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { ApiError, LinkedClawError, NetworkError } from "./errors.js";
2
+
3
+ export interface PrintOptions {
4
+ human?: boolean;
5
+ }
6
+
7
+ export function printResult(value: unknown, opts: PrintOptions = {}): void {
8
+ if (opts.human) {
9
+ if (typeof value === "string") process.stdout.write(value + "\n");
10
+ else process.stdout.write(JSON.stringify(value, null, 2) + "\n");
11
+ return;
12
+ }
13
+ process.stdout.write(JSON.stringify(value) + "\n");
14
+ }
15
+
16
+ export function printError(err: unknown): void {
17
+ const payload: Record<string, unknown> =
18
+ err instanceof ApiError
19
+ ? { error: err.code, status: err.status, path: err.path, detail: err.detail }
20
+ : err instanceof NetworkError
21
+ ? { error: err.code, message: err.message }
22
+ : err instanceof LinkedClawError
23
+ ? { error: err.code, message: err.message }
24
+ : { error: "internal_error", message: err instanceof Error ? err.message : String(err) };
25
+ process.stderr.write(JSON.stringify(payload) + "\n");
26
+ }
27
+
28
+ export async function runCommand(fn: () => Promise<unknown> | unknown, opts: PrintOptions = {}): Promise<void> {
29
+ try {
30
+ const out = await fn();
31
+ if (out !== undefined) printResult(out, opts);
32
+ } catch (err) {
33
+ printError(err);
34
+ process.exitCode = 1;
35
+ }
36
+ }
37
+
38
+ export async function readLine(prompt: string): Promise<string> {
39
+ const { createInterface } = await import("node:readline/promises");
40
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
41
+ try {
42
+ return (await rl.question(prompt)).trim();
43
+ } finally {
44
+ rl.close();
45
+ }
46
+ }
47
+
48
+ export async function readStdin(): Promise<string> {
49
+ if (process.stdin.isTTY) {
50
+ process.stderr.write("reading from stdin — press Ctrl-D when done\n");
51
+ }
52
+ const chunks: Buffer[] = [];
53
+ for await (const chunk of process.stdin) {
54
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
55
+ }
56
+ return Buffer.concat(chunks).toString("utf8");
57
+ }
package/src/types.ts ADDED
@@ -0,0 +1,90 @@
1
+ // CLI-local request/response types. Only types consumed by commands are here.
2
+ // Response/listing types are typed as Record<string,unknown> in the SDK methods;
3
+ // these interfaces are used for request body construction and CLI flag parsing.
4
+
5
+ export type EntityId = string;
6
+
7
+ export type PricingModel =
8
+ | "free"
9
+ | "per_session"
10
+ | "per_message"
11
+ | "per_call"
12
+ | "per_task";
13
+
14
+ export type VerifyMethod = "none" | "custom" | "output_schema";
15
+
16
+ export interface CreateAgentRequest {
17
+ slug: string;
18
+ name: string;
19
+ description: string;
20
+ capabilities: string[];
21
+ pricing_model?: PricingModel;
22
+ price_credits?: number;
23
+ status?: "online" | "offline" | "disabled";
24
+ verify_method?: VerifyMethod;
25
+ verify_config?: Record<string, unknown>;
26
+ min_requester_reputation?: number;
27
+ min_sessions?: number;
28
+ require_payment?: boolean;
29
+ slot_fulfillment?: string[];
30
+ fork_policy?: string | null;
31
+ }
32
+
33
+ export type UpdateAgentRequest = Partial<Omit<CreateAgentRequest, "slug">>;
34
+
35
+ export interface EndSessionRequest {
36
+ message_count?: number | null;
37
+ final_output?: string | null;
38
+ }
39
+
40
+ export interface InvokeRequest {
41
+ capability: string;
42
+ input: Record<string, unknown>;
43
+ max_credits?: number;
44
+ manifest_id?: EntityId;
45
+ manifest?: TaskManifest;
46
+ timeout_seconds?: number;
47
+ referred_by?: EntityId;
48
+ }
49
+
50
+ export interface TaskManifest {
51
+ intention: string;
52
+ [key: string]: unknown;
53
+ }
54
+
55
+ export interface CreateBroadcastRequest {
56
+ capability: string;
57
+ instruction: string;
58
+ target_providers: number;
59
+ credits_per_provider: number;
60
+ deadline?: string | null;
61
+ task_params?: Record<string, unknown>;
62
+ result_schema?: Record<string, unknown> | null;
63
+ acceptance_criteria?: Record<string, unknown> | null;
64
+ partition_type?: string;
65
+ payment_type?: string;
66
+ required_stake?: number;
67
+ referred_by?: EntityId;
68
+ }
69
+
70
+ export interface AcceptBroadcastRequest {
71
+ agent_id: EntityId;
72
+ slot_key?: string | null;
73
+ }
74
+
75
+ export interface BroadcastSubmitRequest {
76
+ result_data: string;
77
+ result_payload?: Record<string, unknown> | null;
78
+ proof?: Array<{ type: string; value: string; label?: string }> | null;
79
+ }
80
+
81
+ // Re-export provider-runtime types needed by commands
82
+ export type {
83
+ ProviderHandler,
84
+ BroadcastOfferDecision,
85
+ BroadcastExecuteResult,
86
+ InvokeHandlerResult,
87
+ SessionAcceptDecision,
88
+ SessionReply,
89
+ } from "@linkedclaw/provider-runtime";
90
+ export type { MessageType } from "@linkedclaw/provider";
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const BIN = path.resolve(__dirname, "../dist/bin.js");
9
+
10
+ beforeAll(() => {
11
+ if (!existsSync(BIN)) {
12
+ throw new Error(`bin not built — run 'pnpm -r --filter @linkedclaw/cli build' first (${BIN})`);
13
+ }
14
+ });
15
+
16
+ function run(args: string[]): { code: number | null; stdout: string; stderr: string } {
17
+ const r = spawnSync("node", [BIN, ...args], { encoding: "utf8" });
18
+ return { code: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
19
+ }
20
+
21
+ describe("cli help", () => {
22
+ it("--help lists every top-level verb from §3.9", () => {
23
+ const { code, stdout } = run(["--help"]);
24
+ expect(code).toBe(0);
25
+ for (const verb of [
26
+ "provider", "broadcast", "search", "hire", "send", "end",
27
+ "invoke", "receipt", "trust", "credits", "register", "login", "whoami",
28
+ ]) {
29
+ expect(stdout).toContain(verb);
30
+ }
31
+ });
32
+
33
+ it("hire --help shows --message and --interactive", () => {
34
+ const { code, stdout } = run(["hire", "--help"]);
35
+ expect(code).toBe(0);
36
+ expect(stdout).toContain("--message");
37
+ expect(stdout).toContain("--interactive");
38
+ });
39
+
40
+ it("register --help shows --no-browser and --cloud-url", () => {
41
+ const { code, stdout } = run(["register", "--help"]);
42
+ expect(code).toBe(0);
43
+ expect(stdout).toContain("--no-browser");
44
+ expect(stdout).toContain("--cloud-url");
45
+ });
46
+
47
+ it("provider --help shows all six subcommands", () => {
48
+ const { code, stdout } = run(["provider", "--help"]);
49
+ expect(code).toBe(0);
50
+ for (const sub of ["register", "update", "listings", "run", "pick", "submit"]) {
51
+ expect(stdout).toContain(sub);
52
+ }
53
+ });
54
+
55
+ it("broadcast --help shows all six subcommands", () => {
56
+ const { code, stdout } = run(["broadcast", "--help"]);
57
+ expect(code).toBe(0);
58
+ for (const sub of ["create", "get", "list", "available", "accept", "submit"]) {
59
+ expect(stdout).toContain(sub);
60
+ }
61
+ });
62
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ const sendSpy = vi.fn().mockResolvedValue({ ok: true });
4
+ const eventsSpy = vi.fn().mockResolvedValue({ events: [{}, {}] });
5
+ const hireSpy = vi.fn().mockResolvedValue({ session: { session_id: "s1" }, activated: true });
6
+ const endSessionSpy = vi.fn().mockResolvedValue({ ok: true });
7
+
8
+ vi.mock("../src/context.js", () => ({
9
+ buildContext: () => ({
10
+ cfg: { apiKey: "k", cloudUrl: "http://x" },
11
+ requesterFlows: { hire: hireSpy, send: sendSpy, search: vi.fn(), invoke: vi.fn() },
12
+ consumer: { getSessionEvents: eventsSpy, endSession: endSessionSpy, invoke: vi.fn() },
13
+ providerClient: {},
14
+ }),
15
+ }));
16
+
17
+ vi.mock("../src/output.js", () => ({
18
+ runCommand: async (fn: () => Promise<unknown>) => fn(),
19
+ readStdin: async () => "",
20
+ printError: () => {},
21
+ printResult: () => {},
22
+ }));
23
+
24
+ describe("hire --message", () => {
25
+ it("calls requesterFlows.send once with message and seq=1", async () => {
26
+ const { Command } = await import("commander");
27
+ const program = new Command();
28
+ program.exitOverride();
29
+ const { registerRequesterCommands } = await import("../src/commands/requester.js");
30
+ registerRequesterCommands(program);
31
+
32
+ await program.parseAsync([
33
+ "node", "cli", "hire", "agent-1",
34
+ "--capability", "test-cap",
35
+ "--message", "hello",
36
+ ]);
37
+
38
+ expect(hireSpy).toHaveBeenCalled();
39
+ expect(sendSpy).toHaveBeenCalledWith("s1", "hello", 1);
40
+ });
41
+ });
42
+
43
+ describe("hire --interactive flag", () => {
44
+ it("--interactive option is registered on the hire command", async () => {
45
+ const { Command } = await import("commander");
46
+ const program = new Command();
47
+ const { registerRequesterCommands } = await import("../src/commands/requester.js");
48
+ registerRequesterCommands(program);
49
+
50
+ const hireCmd = program.commands.find((c) => c.name() === "hire");
51
+ expect(hireCmd).toBeDefined();
52
+ const interactiveOpt = hireCmd!.options.find((o) => o.long === "--interactive");
53
+ expect(interactiveOpt).toBeDefined();
54
+ });
55
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ const eventsSpy = vi.fn();
4
+
5
+ vi.mock("../src/context.js", () => ({
6
+ buildContext: () => ({
7
+ cfg: { apiKey: "k", cloudUrl: "http://x" },
8
+ requesterFlows: { hire: vi.fn(), send: vi.fn(), search: vi.fn(), invoke: vi.fn() },
9
+ consumer: { getSessionEvents: eventsSpy, endSession: vi.fn(), invoke: vi.fn() },
10
+ providerClient: {},
11
+ }),
12
+ }));
13
+
14
+ vi.mock("../src/output.js", () => ({
15
+ runCommand: async (fn: () => Promise<unknown>) => fn(),
16
+ readStdin: async () => "",
17
+ printError: () => {},
18
+ printResult: () => {},
19
+ }));
20
+
21
+ describe("recv subcommand", () => {
22
+ it("registers with --since, --wait, --human options", async () => {
23
+ const { Command } = await import("commander");
24
+ const program = new Command();
25
+ const { registerRequesterCommands } = await import("../src/commands/requester.js");
26
+ registerRequesterCommands(program);
27
+
28
+ const recvCmd = program.commands.find((c) => c.name() === "recv");
29
+ expect(recvCmd).toBeDefined();
30
+ expect(recvCmd!.options.find((o) => o.long === "--since")).toBeDefined();
31
+ expect(recvCmd!.options.find((o) => o.long === "--wait")).toBeDefined();
32
+ expect(recvCmd!.options.find((o) => o.long === "--human")).toBeDefined();
33
+ });
34
+
35
+ it("calls getSessionEvents with offset=0 by default", async () => {
36
+ eventsSpy.mockResolvedValueOnce({ events: [{ event_seq: 1 }], next_offset: 1 });
37
+ const { Command } = await import("commander");
38
+ const program = new Command();
39
+ program.exitOverride();
40
+ const { registerRequesterCommands } = await import("../src/commands/requester.js");
41
+ registerRequesterCommands(program);
42
+
43
+ await program.parseAsync(["node", "cli", "recv", "ses_1"]);
44
+
45
+ expect(eventsSpy).toHaveBeenCalledWith("ses_1", { offset: 0 });
46
+ });
47
+
48
+ it("forwards --since as offset", async () => {
49
+ eventsSpy.mockResolvedValueOnce({ events: [{ event_seq: 7 }], next_offset: 7 });
50
+ const { Command } = await import("commander");
51
+ const program = new Command();
52
+ program.exitOverride();
53
+ const { registerRequesterCommands } = await import("../src/commands/requester.js");
54
+ registerRequesterCommands(program);
55
+
56
+ await program.parseAsync(["node", "cli", "recv", "ses_2", "--since", "5"]);
57
+
58
+ expect(eventsSpy).toHaveBeenCalledWith("ses_2", { offset: 5 });
59
+ });
60
+
61
+ it("polls repeatedly under --wait until events arrive", async () => {
62
+ eventsSpy.mockReset();
63
+ eventsSpy
64
+ .mockResolvedValueOnce({ events: [], next_offset: 0 })
65
+ .mockResolvedValueOnce({ events: [], next_offset: 0 })
66
+ .mockResolvedValueOnce({ events: [{ event_seq: 1 }], next_offset: 1 });
67
+
68
+ vi.useFakeTimers();
69
+ const { Command } = await import("commander");
70
+ const program = new Command();
71
+ program.exitOverride();
72
+ const { registerRequesterCommands } = await import("../src/commands/requester.js");
73
+ registerRequesterCommands(program);
74
+
75
+ const promise = program.parseAsync(["node", "cli", "recv", "ses_3", "--wait", "10"]);
76
+ await vi.advanceTimersByTimeAsync(1500);
77
+ await vi.advanceTimersByTimeAsync(1500);
78
+ await promise;
79
+ vi.useRealTimers();
80
+
81
+ expect(eventsSpy).toHaveBeenCalledTimes(3);
82
+ });
83
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ const writeSpy = vi.fn();
4
+ const readFileSpy = vi.fn().mockReturnValue({});
5
+
6
+ vi.mock("../src/config.js", () => ({
7
+ configPath: () => "/tmp/cli-test-config.yaml",
8
+ configDir: () => "/tmp",
9
+ readFileConfig: readFileSpy,
10
+ writeFileConfig: writeSpy,
11
+ resolveConfig: () => ({ apiKey: "", cloudUrl: "http://localhost:17221" }),
12
+ }));
13
+
14
+ vi.mock("../src/output.js", () => ({
15
+ runCommand: async (fn: () => Promise<unknown>) => fn(),
16
+ readLine: async () => "test-api-key-abc123",
17
+ readStdin: async () => "test-api-key-abc123",
18
+ printError: () => {},
19
+ printResult: () => {},
20
+ }));
21
+
22
+ const openSpy = vi.fn().mockResolvedValue(undefined);
23
+ vi.mock("open", () => ({ default: openSpy }));
24
+
25
+ describe("register --browser", () => {
26
+ it("opens portal URL and persists pasted key via writeFileConfig", async () => {
27
+ openSpy.mockResolvedValueOnce(undefined);
28
+
29
+ const { Command } = await import("commander");
30
+ const program = new Command();
31
+ program.exitOverride();
32
+ const { registerAuthCommands } = await import("../src/commands/auth.js");
33
+ registerAuthCommands(program);
34
+
35
+ await program.parseAsync(["node", "cli", "register", "--cloud-url", "http://test:17221"]);
36
+
37
+ expect(openSpy).toHaveBeenCalledWith("http://test:17221/register");
38
+ expect(writeSpy).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "test-api-key-abc123" }));
39
+ });
40
+
41
+ it("falls through to URL print when open() throws (headless)", async () => {
42
+ openSpy.mockRejectedValueOnce(new Error("no DISPLAY"));
43
+ writeSpy.mockClear();
44
+
45
+ const { Command } = await import("commander");
46
+ const program = new Command();
47
+ program.exitOverride();
48
+ const { registerAuthCommands } = await import("../src/commands/auth.js");
49
+ registerAuthCommands(program);
50
+
51
+ await program.parseAsync(["node", "cli", "register", "--cloud-url", "http://test:17221"]);
52
+
53
+ expect(writeSpy).toHaveBeenCalled();
54
+ });
55
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["dist", "node_modules", "test"]
14
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/bin.ts"],
5
+ format: ["esm"],
6
+ platform: "node",
7
+ dts: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ target: "node20",
11
+ splitting: false,
12
+ shims: false,
13
+ banner: { js: "#!/usr/bin/env node" },
14
+ // Bundle the four SDK packages so `npm install -g @linkedclaw/cli` works without
15
+ // separate installs.
16
+ noExternal: [
17
+ "@linkedclaw/consumer",
18
+ "@linkedclaw/consumer-runtime",
19
+ "@linkedclaw/provider",
20
+ "@linkedclaw/provider-runtime",
21
+ ],
22
+ // ws is CJS and requires Node built-ins dynamically; keep it external so Node
23
+ // resolves it natively at runtime (ws is a direct dep for this reason).
24
+ external: ["ws"],
25
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["test/**/*.test.ts"],
6
+ environment: "node",
7
+ },
8
+ });