feat: render nodes status as table

This commit is contained in:
Peter Steinberger
2026-01-21 03:11:27 +00:00
parent caf9dec89c
commit e6287270d9
4 changed files with 63 additions and 27 deletions

View File

@@ -56,32 +56,57 @@ export function registerNodesStatusCommands(nodes: Command) {
defaultRuntime.log(JSON.stringify(result, null, 2)); defaultRuntime.log(JSON.stringify(result, null, 2));
return; 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 nodes = parseNodeList(result);
const pairedCount = nodes.filter((n) => Boolean(n.paired)).length; const pairedCount = nodes.filter((n) => Boolean(n.paired)).length;
const connectedCount = nodes.filter((n) => Boolean(n.connected)).length; const connectedCount = nodes.filter((n) => Boolean(n.connected)).length;
defaultRuntime.log( defaultRuntime.log(
`Known: ${nodes.length} · Paired: ${pairedCount} · Connected: ${connectedCount}`, `Known: ${nodes.length} · Paired: ${pairedCount} · Connected: ${connectedCount}`,
); );
for (const n of nodes) { if (nodes.length === 0) return;
const name = n.displayName || n.nodeId;
const ip = n.remoteIp ? ` · ${n.remoteIp}` : ""; const rows = nodes.map((n) => {
const device = n.deviceFamily ? ` · device: ${n.deviceFamily}` : ""; const name = n.displayName?.trim() ? n.displayName.trim() : n.nodeId;
const hw = n.modelIdentifier ? ` · hw: ${n.modelIdentifier}` : ""; const device = (() => {
const perms = formatPermissions(n.permissions); if (n.deviceFamily && n.modelIdentifier) {
const permsText = perms ? ` · perms: ${perms}` : ""; return `${n.deviceFamily} (${n.modelIdentifier})`;
const versions = formatNodeVersions(n); }
const versionText = versions ? ` · ${versions}` : ""; return n.deviceFamily ?? n.modelIdentifier ?? "";
const caps = })();
Array.isArray(n.caps) && n.caps.length > 0 const caps = Array.isArray(n.caps)
? `[${n.caps.map(String).filter(Boolean).sort().join(",")}]` ? n.caps.map(String).filter(Boolean).sort().join(", ")
: Array.isArray(n.caps) : "?";
? "[]" const paired = n.paired ? ok("paired") : warn("unpaired");
: "?"; const connected = n.connected ? ok("connected") : muted("disconnected");
const pairing = n.paired ? "paired" : "unpaired";
defaultRuntime.log( return {
`- ${name} · ${n.nodeId}${ip}${device}${hw}${permsText}${versionText} · ${pairing} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`, 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(),
);
}); });
}), }),
); );

View File

@@ -56,6 +56,7 @@ export type NodeListNode = {
permissions?: Record<string, boolean>; permissions?: Record<string, boolean>;
paired?: boolean; paired?: boolean;
connected?: boolean; connected?: boolean;
connectedAtMs?: number;
}; };
export type PendingRequest = { export type PendingRequest = {

View File

@@ -95,10 +95,12 @@ describe("cli program (nodes basics)", () => {
const output = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n"); 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("Known: 1 · Paired: 1 · Connected: 1");
expect(output).toContain("iOS Node"); expect(output).toContain("iOS Node");
expect(output).toContain("device: iPad"); expect(output).toContain("Device");
expect(output).toContain("hw: iPad16,6"); expect(output).toContain("iPad (iPad16,6)");
expect(output).toContain("Status");
expect(output).toContain("paired"); 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 () => { 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"); 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("Known: 1 · Paired: 0 · Connected: 1");
expect(output).toContain("Peter's Tab S10 Ultra"); expect(output).toContain("Peter's Tab S10");
expect(output).toContain("device: Android"); expect(output).toContain("Ultra");
expect(output).toContain("hw: samsung SM-X926B"); 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("unpaired");
expect(output).toContain("connected"); 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 () => { it("runs nodes describe and calls node.describe", async () => {

View File

@@ -258,6 +258,7 @@ export const nodeHandlers: GatewayRequestHandlers = {
caps, caps,
commands, commands,
permissions: live?.permissions ?? paired?.permissions, permissions: live?.permissions ?? paired?.permissions,
connectedAtMs: live?.connectedAtMs,
paired: Boolean(paired), paired: Boolean(paired),
connected: Boolean(live), connected: Boolean(live),
}; };
@@ -320,6 +321,7 @@ export const nodeHandlers: GatewayRequestHandlers = {
caps, caps,
commands, commands,
permissions: live?.permissions, permissions: live?.permissions,
connectedAtMs: live?.connectedAtMs,
paired: Boolean(paired), paired: Boolean(paired),
connected: Boolean(live), connected: Boolean(live),
}, },