diff --git a/src/cli/nodes-cli/register.status.ts b/src/cli/nodes-cli/register.status.ts index c092c121f..629857224 100644 --- a/src/cli/nodes-cli/register.status.ts +++ b/src/cli/nodes-cli/register.status.ts @@ -56,32 +56,57 @@ export function registerNodesStatusCommands(nodes: Command) { defaultRuntime.log(JSON.stringify(result, null, 2)); return; } + const rich = isRich(); + const ok = (text: string) => (rich ? theme.success(text) : text); + const warn = (text: string) => (rich ? theme.warn(text) : text); + const muted = (text: string) => (rich ? theme.muted(text) : text); + const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); const nodes = parseNodeList(result); const pairedCount = nodes.filter((n) => Boolean(n.paired)).length; const connectedCount = nodes.filter((n) => Boolean(n.connected)).length; defaultRuntime.log( `Known: ${nodes.length} · Paired: ${pairedCount} · 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 perms = formatPermissions(n.permissions); - const permsText = perms ? ` · perms: ${perms}` : ""; - const versions = formatNodeVersions(n); - const versionText = versions ? ` · ${versions}` : ""; - const caps = - Array.isArray(n.caps) && n.caps.length > 0 - ? `[${n.caps.map(String).filter(Boolean).sort().join(",")}]` - : Array.isArray(n.caps) - ? "[]" - : "?"; - const pairing = n.paired ? "paired" : "unpaired"; - defaultRuntime.log( - `- ${name} · ${n.nodeId}${ip}${device}${hw}${permsText}${versionText} · ${pairing} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`, - ); - } + if (nodes.length === 0) return; + + const rows = nodes.map((n) => { + const name = n.displayName?.trim() ? n.displayName.trim() : n.nodeId; + const device = (() => { + if (n.deviceFamily && n.modelIdentifier) { + return `${n.deviceFamily} (${n.modelIdentifier})`; + } + return n.deviceFamily ?? n.modelIdentifier ?? ""; + })(); + const caps = Array.isArray(n.caps) + ? n.caps.map(String).filter(Boolean).sort().join(", ") + : "?"; + const paired = n.paired ? ok("paired") : warn("unpaired"); + const connected = n.connected ? ok("connected") : muted("disconnected"); + + return { + Node: name, + ID: n.nodeId, + IP: n.remoteIp ?? "", + Device: device, + Status: `${paired} · ${connected}`, + Caps: caps, + }; + }); + + defaultRuntime.log( + renderTable({ + width: tableWidth, + columns: [ + { key: "Node", header: "Node", minWidth: 14, flex: true }, + { key: "ID", header: "ID", minWidth: 10 }, + { key: "IP", header: "IP", minWidth: 10 }, + { key: "Device", header: "Device", minWidth: 14, flex: true }, + { key: "Status", header: "Status", minWidth: 16 }, + { key: "Caps", header: "Caps", minWidth: 10, flex: true }, + ], + rows, + }).trimEnd(), + ); }); }), ); diff --git a/src/cli/nodes-cli/types.ts b/src/cli/nodes-cli/types.ts index d7d247aa7..fbcf128df 100644 --- a/src/cli/nodes-cli/types.ts +++ b/src/cli/nodes-cli/types.ts @@ -56,6 +56,7 @@ export type NodeListNode = { permissions?: Record; paired?: boolean; connected?: boolean; + connectedAtMs?: number; }; export type PendingRequest = { diff --git a/src/cli/program.nodes-basic.test.ts b/src/cli/program.nodes-basic.test.ts index 65f1b4681..722b2d06c 100644 --- a/src/cli/program.nodes-basic.test.ts +++ b/src/cli/program.nodes-basic.test.ts @@ -95,10 +95,12 @@ describe("cli program (nodes basics)", () => { const output = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n"); expect(output).toContain("Known: 1 · Paired: 1 · Connected: 1"); expect(output).toContain("iOS Node"); - expect(output).toContain("device: iPad"); - expect(output).toContain("hw: iPad16,6"); + expect(output).toContain("Device"); + expect(output).toContain("iPad (iPad16,6)"); + expect(output).toContain("Status"); expect(output).toContain("paired"); - expect(output).toContain("caps: [camera,canvas]"); + expect(output).toContain("Caps"); + expect(output).toContain("camera, canvas"); }); it("runs nodes status and shows unpaired nodes", async () => { @@ -123,12 +125,18 @@ describe("cli program (nodes basics)", () => { const output = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n"); expect(output).toContain("Known: 1 · Paired: 0 · Connected: 1"); - expect(output).toContain("Peter's Tab S10 Ultra"); - expect(output).toContain("device: Android"); - expect(output).toContain("hw: samsung SM-X926B"); + expect(output).toContain("Peter's Tab S10"); + expect(output).toContain("Ultra"); + expect(output).toContain("Device"); + expect(output).toContain("Android"); + expect(output).toContain("SM-"); + expect(output).toContain("X926B"); + expect(output).toContain("Status"); expect(output).toContain("unpaired"); expect(output).toContain("connected"); - expect(output).toContain("caps: [camera,canvas]"); + expect(output).toContain("Caps"); + expect(output).toContain("camera"); + expect(output).toContain("canvas"); }); it("runs nodes describe and calls node.describe", async () => { diff --git a/src/gateway/server-methods/nodes.ts b/src/gateway/server-methods/nodes.ts index b112d4a9c..ecebab964 100644 --- a/src/gateway/server-methods/nodes.ts +++ b/src/gateway/server-methods/nodes.ts @@ -258,6 +258,7 @@ export const nodeHandlers: GatewayRequestHandlers = { caps, commands, permissions: live?.permissions ?? paired?.permissions, + connectedAtMs: live?.connectedAtMs, paired: Boolean(paired), connected: Boolean(live), }; @@ -320,6 +321,7 @@ export const nodeHandlers: GatewayRequestHandlers = { caps, commands, permissions: live?.permissions, + connectedAtMs: live?.connectedAtMs, paired: Boolean(paired), connected: Boolean(live), },