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