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));
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(),
);
});
}),
);

View File

@@ -56,6 +56,7 @@ export type NodeListNode = {
permissions?: Record<string, boolean>;
paired?: boolean;
connected?: boolean;
connectedAtMs?: number;
};
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");
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 () => {

View File

@@ -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),
},