feat: render nodes status as table
This commit is contained in:
@@ -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(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user