diff --git a/src/cli/nodes-cli.ts b/src/cli/nodes-cli.ts index 1bfa931b9..33e4e0ec7 100644 --- a/src/cli/nodes-cli.ts +++ b/src/cli/nodes-cli.ts @@ -32,6 +32,9 @@ type NodeListNode = { platform?: string; version?: string; remoteIp?: string; + deviceFamily?: string; + modelIdentifier?: string; + caps?: string[]; connected?: boolean; }; @@ -177,6 +180,44 @@ export function registerNodesCli(program: Command) { .command("nodes") .description("Manage gateway-owned node pairing"); + nodesCallOpts( + nodes + .command("status") + .description("List paired nodes with connection status and capabilities") + .action(async (opts: NodesRpcOpts) => { + try { + const result = (await callGatewayCli("node.list", opts, {})) as unknown; + if (opts.json) { + defaultRuntime.log(JSON.stringify(result, null, 2)); + return; + } + const nodes = parseNodeList(result); + const connectedCount = nodes.filter((n) => Boolean(n.connected)).length; + defaultRuntime.log( + `Paired: ${nodes.length} · Connected: ${connectedCount}`, + ); + for (const n of nodes) { + const name = n.displayName || n.nodeId; + const ip = n.remoteIp ? ` · ${n.remoteIp}` : ""; + const device = n.deviceFamily ? ` · device: ${n.deviceFamily}` : ""; + const hw = n.modelIdentifier ? ` · hw: ${n.modelIdentifier}` : ""; + const caps = + Array.isArray(n.caps) && n.caps.length > 0 + ? `[${n.caps.map(String).filter(Boolean).sort().join(",")}]` + : Array.isArray(n.caps) + ? "[]" + : "?"; + defaultRuntime.log( + `- ${name} · ${n.nodeId}${ip}${device}${hw} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`, + ); + } + } catch (err) { + defaultRuntime.error(`nodes status failed: ${String(err)}`); + defaultRuntime.exit(1); + } + }), + ); + nodesCallOpts( nodes .command("list") diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index 181de82dc..214e9c583 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -62,6 +62,37 @@ describe("cli program", () => { expect(runtime.log).toHaveBeenCalledWith("Pending: 0 · Paired: 0"); }); + it("runs nodes status and calls node.list", async () => { + callGateway.mockResolvedValue({ + ts: Date.now(), + nodes: [ + { + nodeId: "ios-node", + displayName: "iOS Node", + remoteIp: "192.168.0.88", + deviceFamily: "iPad", + modelIdentifier: "iPad16,6", + caps: ["canvas", "camera"], + connected: true, + }, + ], + }); + const program = buildProgram(); + runtime.log.mockClear(); + await program.parseAsync(["nodes", "status"], { from: "user" }); + + expect(callGateway).toHaveBeenCalledWith( + expect.objectContaining({ method: "node.list", params: {} }), + ); + + const output = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n"); + expect(output).toContain("Paired: 1 · Connected: 1"); + expect(output).toContain("iOS Node"); + expect(output).toContain("device: iPad"); + expect(output).toContain("hw: iPad16,6"); + expect(output).toContain("caps: [camera,canvas]"); + }); + it("runs nodes approve and calls node.pair.approve", async () => { callGateway.mockResolvedValue({ requestId: "r1",