feat: polish nodes cli output

This commit is contained in:
Peter Steinberger
2026-01-21 03:17:56 +00:00
parent aec622fe63
commit aae4b2952f
8 changed files with 177 additions and 76 deletions

View File

@@ -1,13 +1,28 @@
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { isRich, theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js"; import { runCommandWithRuntime } from "../cli-utils.js";
import { unauthorizedHintForMessage } from "./rpc.js"; import { unauthorizedHintForMessage } from "./rpc.js";
export function getNodesTheme() {
const rich = isRich();
const color = (fn: (value: string) => string) => (value: string) => (rich ? fn(value) : value);
return {
rich,
heading: color(theme.heading),
ok: color(theme.success),
warn: color(theme.warn),
muted: color(theme.muted),
error: color(theme.error),
};
}
export function runNodesCommand(label: string, action: () => Promise<void>) { export function runNodesCommand(label: string, action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action, (err) => { return runCommandWithRuntime(defaultRuntime, action, (err) => {
const message = String(err); const message = String(err);
defaultRuntime.error(`nodes ${label} failed: ${message}`); const { error, warn } = getNodesTheme();
defaultRuntime.error(error(`nodes ${label} failed: ${message}`));
const hint = unauthorizedHintForMessage(message); const hint = unauthorizedHintForMessage(message);
if (hint) defaultRuntime.error(hint); if (hint) defaultRuntime.error(warn(hint));
defaultRuntime.exit(1); defaultRuntime.exit(1);
}); });
} }

View File

@@ -9,9 +9,10 @@ import {
writeBase64ToFile, writeBase64ToFile,
} from "../nodes-camera.js"; } from "../nodes-camera.js";
import { parseDurationMs } from "../parse-duration.js"; import { parseDurationMs } from "../parse-duration.js";
import { runNodesCommand } from "./cli-utils.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js"; import type { NodesRpcOpts } from "./types.js";
import { renderTable } from "../../terminal/table.js";
const parseFacing = (value: string): CameraFacing => { const parseFacing = (value: string): CameraFacing => {
const v = String(value ?? "") const v = String(value ?? "")
@@ -52,16 +53,30 @@ export function registerNodesCameraCommands(nodes: Command) {
} }
if (devices.length === 0) { if (devices.length === 0) {
defaultRuntime.log("No cameras reported."); const { muted } = getNodesTheme();
defaultRuntime.log(muted("No cameras reported."));
return; return;
} }
for (const device of devices) { const { heading, muted } = getNodesTheme();
const id = typeof device.id === "string" ? device.id : ""; const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const name = typeof device.name === "string" ? device.name : "Unknown Camera"; const rows = devices.map((device) => ({
const position = typeof device.position === "string" ? device.position : "unspecified"; Name: typeof device.name === "string" ? device.name : "Unknown Camera",
defaultRuntime.log(`${name} (${position})${id ? `${id}` : ""}`); Position: typeof device.position === "string" ? device.position : muted("unspecified"),
} ID: typeof device.id === "string" ? device.id : "",
}));
defaultRuntime.log(heading("Cameras"));
defaultRuntime.log(
renderTable({
width: tableWidth,
columns: [
{ key: "Name", header: "Name", minWidth: 14, flex: true },
{ key: "Position", header: "Position", minWidth: 10 },
{ key: "ID", header: "ID", minWidth: 10, flex: true },
],
rows,
}).trimEnd(),
);
}); });
}), }),
{ timeoutMs: 60_000 }, { timeoutMs: 60_000 },

View File

@@ -6,7 +6,7 @@ import { writeBase64ToFile } from "../nodes-camera.js";
import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../nodes-canvas.js"; import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../nodes-canvas.js";
import { parseTimeoutMs } from "../nodes-run.js"; import { parseTimeoutMs } from "../nodes-run.js";
import { buildA2UITextJsonl, validateA2UIJsonl } from "./a2ui-jsonl.js"; import { buildA2UITextJsonl, validateA2UIJsonl } from "./a2ui-jsonl.js";
import { runNodesCommand } from "./cli-utils.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js"; import type { NodesRpcOpts } from "./types.js";
@@ -121,7 +121,10 @@ export function registerNodesCanvasCommands(nodes: Command) {
params.placement = placement; params.placement = placement;
} }
await invokeCanvas(opts, "canvas.present", params); await invokeCanvas(opts, "canvas.present", params);
if (!opts.json) defaultRuntime.log("canvas present ok"); if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas present ok"));
}
}); });
}), }),
); );
@@ -135,7 +138,10 @@ export function registerNodesCanvasCommands(nodes: Command) {
.action(async (opts: NodesRpcOpts) => { .action(async (opts: NodesRpcOpts) => {
await runNodesCommand("canvas hide", async () => { await runNodesCommand("canvas hide", async () => {
await invokeCanvas(opts, "canvas.hide", undefined); await invokeCanvas(opts, "canvas.hide", undefined);
if (!opts.json) defaultRuntime.log("canvas hide ok"); if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas hide ok"));
}
}); });
}), }),
); );
@@ -150,7 +156,10 @@ export function registerNodesCanvasCommands(nodes: Command) {
.action(async (url: string, opts: NodesRpcOpts) => { .action(async (url: string, opts: NodesRpcOpts) => {
await runNodesCommand("canvas navigate", async () => { await runNodesCommand("canvas navigate", async () => {
await invokeCanvas(opts, "canvas.navigate", { url }); await invokeCanvas(opts, "canvas.navigate", { url });
if (!opts.json) defaultRuntime.log("canvas navigate ok"); if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas navigate ok"));
}
}); });
}), }),
); );
@@ -179,7 +188,10 @@ export function registerNodesCanvasCommands(nodes: Command) {
? (raw as { payload?: { result?: string } }).payload ? (raw as { payload?: { result?: string } }).payload
: undefined; : undefined;
if (payload?.result) defaultRuntime.log(payload.result); if (payload?.result) defaultRuntime.log(payload.result);
else defaultRuntime.log("canvas eval ok"); else {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas eval ok"));
}
}); });
}), }),
); );
@@ -213,8 +225,11 @@ export function registerNodesCanvasCommands(nodes: Command) {
} }
await invokeCanvas(opts, "canvas.a2ui.pushJSONL", { jsonl }); await invokeCanvas(opts, "canvas.a2ui.pushJSONL", { jsonl });
if (!opts.json) { if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log( defaultRuntime.log(
`canvas a2ui push ok (v0.8, ${messageCount} message${messageCount === 1 ? "" : "s"})`, ok(
`canvas a2ui push ok (v0.8, ${messageCount} message${messageCount === 1 ? "" : "s"})`,
),
); );
} }
}); });
@@ -230,7 +245,10 @@ export function registerNodesCanvasCommands(nodes: Command) {
.action(async (opts: NodesRpcOpts) => { .action(async (opts: NodesRpcOpts) => {
await runNodesCommand("canvas a2ui reset", async () => { await runNodesCommand("canvas a2ui reset", async () => {
await invokeCanvas(opts, "canvas.a2ui.reset", undefined); await invokeCanvas(opts, "canvas.a2ui.reset", undefined);
if (!opts.json) defaultRuntime.log("canvas a2ui reset ok"); if (!opts.json) {
const { ok } = getNodesTheme();
defaultRuntime.log(ok("canvas a2ui reset ok"));
}
}); });
}), }),
); );

View File

@@ -2,7 +2,7 @@ import type { Command } from "commander";
import { randomIdempotencyKey } from "../../gateway/call.js"; import { randomIdempotencyKey } from "../../gateway/call.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { parseEnvPairs, parseTimeoutMs } from "../nodes-run.js"; import { parseEnvPairs, parseTimeoutMs } from "../nodes-run.js";
import { runNodesCommand } from "./cli-utils.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId, unauthorizedHintForMessage } from "./rpc.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId, unauthorizedHintForMessage } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js"; import type { NodesRpcOpts } from "./types.js";
@@ -21,7 +21,8 @@ export function registerNodesInvokeCommands(nodes: Command) {
const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
const command = String(opts.command ?? "").trim(); const command = String(opts.command ?? "").trim();
if (!nodeId || !command) { if (!nodeId || !command) {
defaultRuntime.error("--node and --command required"); const { error } = getNodesTheme();
defaultRuntime.error(error("--node and --command required"));
defaultRuntime.exit(1); defaultRuntime.exit(1);
return; return;
} }
@@ -108,16 +109,21 @@ export function registerNodesInvokeCommands(nodes: Command) {
if (stdout) process.stdout.write(stdout); if (stdout) process.stdout.write(stdout);
if (stderr) process.stderr.write(stderr); if (stderr) process.stderr.write(stderr);
if (timedOut) { if (timedOut) {
defaultRuntime.error("run timed out"); const { error } = getNodesTheme();
defaultRuntime.error(error("run timed out"));
defaultRuntime.exit(1); defaultRuntime.exit(1);
return; return;
} }
if (exitCode !== null && exitCode !== 0) { if (exitCode !== null && exitCode !== 0) {
const hint = unauthorizedHintForMessage(`${stderr}\n${stdout}`); const hint = unauthorizedHintForMessage(`${stderr}\n${stdout}`);
if (hint) defaultRuntime.error(hint); if (hint) {
const { warn } = getNodesTheme();
defaultRuntime.error(warn(hint));
}
} }
if (exitCode !== null && exitCode !== 0 && !success) { if (exitCode !== null && exitCode !== 0 && !success) {
defaultRuntime.error(`run exit ${exitCode}`); const { error } = getNodesTheme();
defaultRuntime.error(error(`run exit ${exitCode}`));
defaultRuntime.exit(1); defaultRuntime.exit(1);
return; return;
} }

View File

@@ -1,7 +1,7 @@
import type { Command } from "commander"; import type { Command } from "commander";
import { randomIdempotencyKey } from "../../gateway/call.js"; import { randomIdempotencyKey } from "../../gateway/call.js";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { runNodesCommand } from "./cli-utils.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js"; import type { NodesRpcOpts } from "./types.js";
@@ -49,7 +49,8 @@ export function registerNodesNotifyCommand(nodes: Command) {
defaultRuntime.log(JSON.stringify(result, null, 2)); defaultRuntime.log(JSON.stringify(result, null, 2));
return; return;
} }
defaultRuntime.log("notify ok"); const { ok } = getNodesTheme();
defaultRuntime.log(ok("notify ok"));
}); });
}), }),
); );

View File

@@ -1,9 +1,10 @@
import type { Command } from "commander"; import type { Command } from "commander";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { formatAge, parsePairingList } from "./format.js"; import { formatAge, parsePairingList } from "./format.js";
import { runNodesCommand } from "./cli-utils.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js"; import type { NodesRpcOpts } from "./types.js";
import { renderTable } from "../../terminal/table.js";
export function registerNodesPairingCommands(nodes: Command) { export function registerNodesPairingCommands(nodes: Command) {
nodesCallOpts( nodesCallOpts(
@@ -19,16 +20,37 @@ export function registerNodesPairingCommands(nodes: Command) {
return; return;
} }
if (pending.length === 0) { if (pending.length === 0) {
defaultRuntime.log("No pending pairing requests."); const { muted } = getNodesTheme();
defaultRuntime.log(muted("No pending pairing requests."));
return; return;
} }
for (const r of pending) { const { heading, warn, muted } = getNodesTheme();
const name = r.displayName || r.nodeId; const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const repair = r.isRepair ? " (repair)" : ""; const now = Date.now();
const ip = r.remoteIp ? ` · ${r.remoteIp}` : ""; const rows = pending.map((r) => ({
const age = typeof r.ts === "number" ? ` · ${formatAge(Date.now() - r.ts)} ago` : ""; Request: r.requestId,
defaultRuntime.log(`- ${r.requestId}: ${name}${repair}${ip}${age}`); Node: r.displayName?.trim() ? r.displayName.trim() : r.nodeId,
} IP: r.remoteIp ?? "",
Requested:
typeof r.ts === "number"
? `${formatAge(Math.max(0, now - r.ts))} ago`
: muted("unknown"),
Repair: r.isRepair ? warn("yes") : "",
}));
defaultRuntime.log(heading("Pending"));
defaultRuntime.log(
renderTable({
width: tableWidth,
columns: [
{ key: "Request", header: "Request", minWidth: 8 },
{ key: "Node", header: "Node", minWidth: 14, flex: true },
{ key: "IP", header: "IP", minWidth: 10 },
{ key: "Requested", header: "Requested", minWidth: 12 },
{ key: "Repair", header: "Repair", minWidth: 6 },
],
rows,
}).trimEnd(),
);
}); });
}), }),
); );
@@ -86,7 +108,8 @@ export function registerNodesPairingCommands(nodes: Command) {
defaultRuntime.log(JSON.stringify(result, null, 2)); defaultRuntime.log(JSON.stringify(result, null, 2));
return; return;
} }
defaultRuntime.log(`node rename ok: ${nodeId} -> ${name}`); const { ok } = getNodesTheme();
defaultRuntime.log(ok(`node rename ok: ${nodeId} -> ${name}`));
}); });
}), }),
); );

View File

@@ -1,11 +1,10 @@
import type { Command } from "commander"; import type { Command } from "commander";
import { defaultRuntime } from "../../runtime.js"; import { defaultRuntime } from "../../runtime.js";
import { formatAge, formatPermissions, parseNodeList, parsePairingList } from "./format.js"; import { formatAge, formatPermissions, parseNodeList, parsePairingList } from "./format.js";
import { runNodesCommand } from "./cli-utils.js"; import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js"; import type { NodesRpcOpts } from "./types.js";
import { renderTable } from "../../terminal/table.js"; import { renderTable } from "../../terminal/table.js";
import { isRich, theme } from "../../terminal/theme.js";
function formatVersionLabel(raw: string) { function formatVersionLabel(raw: string) {
const trimmed = raw.trim(); const trimmed = raw.trim();
@@ -56,11 +55,9 @@ 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, warn, muted } = getNodesTheme();
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 tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const now = Date.now();
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;
@@ -71,24 +68,30 @@ export function registerNodesStatusCommands(nodes: Command) {
const rows = nodes.map((n) => { const rows = nodes.map((n) => {
const name = n.displayName?.trim() ? n.displayName.trim() : n.nodeId; const name = n.displayName?.trim() ? n.displayName.trim() : n.nodeId;
const device = (() => { const perms = formatPermissions(n.permissions);
if (n.deviceFamily && n.modelIdentifier) { const versions = formatNodeVersions(n);
return `${n.deviceFamily} (${n.modelIdentifier})`; const detailParts = [
} n.deviceFamily ? `device: ${n.deviceFamily}` : null,
return n.deviceFamily ?? n.modelIdentifier ?? ""; n.modelIdentifier ? `hw: ${n.modelIdentifier}` : null,
})(); perms ? `perms: ${perms}` : null,
versions,
].filter(Boolean) as string[];
const caps = Array.isArray(n.caps) const caps = Array.isArray(n.caps)
? n.caps.map(String).filter(Boolean).sort().join(", ") ? n.caps.map(String).filter(Boolean).sort().join(", ")
: "?"; : "?";
const paired = n.paired ? ok("paired") : warn("unpaired"); const paired = n.paired ? ok("paired") : warn("unpaired");
const connected = n.connected ? ok("connected") : muted("disconnected"); const connected = n.connected ? ok("connected") : muted("disconnected");
const since =
typeof n.connectedAtMs === "number"
? ` (${formatAge(Math.max(0, now - n.connectedAtMs))} ago)`
: "";
return { return {
Node: name, Node: name,
ID: n.nodeId, ID: n.nodeId,
IP: n.remoteIp ?? "", IP: n.remoteIp ?? "",
Device: device, Detail: detailParts.join(" · "),
Status: `${paired} · ${connected}`, Status: `${paired} · ${connected}${since}`,
Caps: caps, Caps: caps,
}; };
}); });
@@ -100,9 +103,9 @@ export function registerNodesStatusCommands(nodes: Command) {
{ key: "Node", header: "Node", minWidth: 14, flex: true }, { key: "Node", header: "Node", minWidth: 14, flex: true },
{ key: "ID", header: "ID", minWidth: 10 }, { key: "ID", header: "ID", minWidth: 10 },
{ key: "IP", header: "IP", minWidth: 10 }, { key: "IP", header: "IP", minWidth: 10 },
{ key: "Device", header: "Device", minWidth: 14, flex: true }, { key: "Detail", header: "Detail", minWidth: 18, flex: true },
{ key: "Status", header: "Status", minWidth: 16 }, { key: "Status", header: "Status", minWidth: 18 },
{ key: "Caps", header: "Caps", minWidth: 10, flex: true }, { key: "Caps", header: "Caps", minWidth: 12, flex: true },
], ],
rows, rows,
}).trimEnd(), }).trimEnd(),
@@ -133,6 +136,7 @@ export function registerNodesStatusCommands(nodes: Command) {
: {}; : {};
const displayName = typeof obj.displayName === "string" ? obj.displayName : nodeId; const displayName = typeof obj.displayName === "string" ? obj.displayName : nodeId;
const connected = Boolean(obj.connected); const connected = Boolean(obj.connected);
const paired = Boolean(obj.paired);
const caps = Array.isArray(obj.caps) ? obj.caps.map(String).filter(Boolean).sort() : null; const caps = Array.isArray(obj.caps) ? obj.caps.map(String).filter(Boolean).sort() : null;
const commands = Array.isArray(obj.commands) const commands = Array.isArray(obj.commands)
? obj.commands.map(String).filter(Boolean).sort() ? obj.commands.map(String).filter(Boolean).sort()
@@ -150,18 +154,38 @@ export function registerNodesStatusCommands(nodes: Command) {
}, },
); );
const parts: string[] = ["Node:", displayName, nodeId]; const { heading, ok, warn, muted } = getNodesTheme();
if (ip) parts.push(ip); const status = `${paired ? ok("paired") : warn("unpaired")} · ${
if (family) parts.push(`device: ${family}`); connected ? ok("connected") : muted("disconnected")
if (model) parts.push(`hw: ${model}`); }`;
if (perms) parts.push(`perms: ${perms}`); const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
if (versions) parts.push(versions); const rows = [
parts.push(connected ? "connected" : "disconnected"); { Field: "ID", Value: nodeId },
parts.push(`caps: ${caps ? `[${caps.join(",")}]` : "?"}`); displayName ? { Field: "Name", Value: displayName } : null,
defaultRuntime.log(parts.join(" · ")); ip ? { Field: "IP", Value: ip } : null,
defaultRuntime.log("Commands:"); family ? { Field: "Device", Value: family } : null,
model ? { Field: "Model", Value: model } : null,
perms ? { Field: "Perms", Value: perms } : null,
versions ? { Field: "Version", Value: versions } : null,
{ Field: "Status", Value: status },
{ Field: "Caps", Value: caps ? caps.join(", ") : "?" },
].filter(Boolean) as Array<{ Field: string; Value: string }>;
defaultRuntime.log(heading("Node"));
defaultRuntime.log(
renderTable({
width: tableWidth,
columns: [
{ key: "Field", header: "Field", minWidth: 8 },
{ key: "Value", header: "Value", minWidth: 24, flex: true },
],
rows,
}).trimEnd(),
);
defaultRuntime.log("");
defaultRuntime.log(heading("Commands"));
if (commands.length === 0) { if (commands.length === 0) {
defaultRuntime.log("- (none reported)"); defaultRuntime.log(muted("- (none reported)"));
return; return;
} }
for (const c of commands) defaultRuntime.log(`- ${c}`); for (const c of commands) defaultRuntime.log(`- ${c}`);
@@ -182,10 +206,7 @@ export function registerNodesStatusCommands(nodes: Command) {
} }
const { pending, paired } = parsePairingList(result); const { pending, paired } = parsePairingList(result);
defaultRuntime.log(`Pending: ${pending.length} · Paired: ${paired.length}`); defaultRuntime.log(`Pending: ${pending.length} · Paired: ${paired.length}`);
const rich = isRich(); const { heading, muted, warn } = getNodesTheme();
const heading = (text: string) => (rich ? theme.heading(text) : text);
const muted = (text: string) => (rich ? theme.muted(text) : text);
const warn = (text: string) => (rich ? theme.warn(text) : text);
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const now = Date.now(); const now = Date.now();

View File

@@ -95,12 +95,14 @@ 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"); expect(output).toContain("Detail");
expect(output).toContain("iPad (iPad16,6)"); expect(output).toContain("device: iPad");
expect(output).toContain("hw: iPad16,6");
expect(output).toContain("Status"); expect(output).toContain("Status");
expect(output).toContain("paired"); expect(output).toContain("paired");
expect(output).toContain("Caps"); expect(output).toContain("Caps");
expect(output).toContain("camera, canvas"); expect(output).toContain("camera");
expect(output).toContain("canvas");
}); });
it("runs nodes status and shows unpaired nodes", async () => { it("runs nodes status and shows unpaired nodes", async () => {
@@ -125,12 +127,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: 0 · Connected: 1"); expect(output).toContain("Known: 1 · Paired: 0 · Connected: 1");
expect(output).toContain("Peter's Tab S10"); expect(output).toContain("Peter's Tab");
expect(output).toContain("Ultra"); expect(output).toContain("S10 Ultra");
expect(output).toContain("Device"); expect(output).toContain("Detail");
expect(output).toContain("Android"); expect(output).toContain("device: Android");
expect(output).toContain("SM-"); expect(output).toContain("hw: samsung");
expect(output).toContain("X926B"); expect(output).toContain("SM-X926B");
expect(output).toContain("Status"); expect(output).toContain("Status");
expect(output).toContain("unpaired"); expect(output).toContain("unpaired");
expect(output).toContain("connected"); expect(output).toContain("connected");
@@ -184,7 +186,7 @@ describe("cli program (nodes basics)", () => {
); );
const out = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n"); const out = runtime.log.mock.calls.map((c) => String(c[0] ?? "")).join("\n");
expect(out).toContain("Commands:"); expect(out).toContain("Commands");
expect(out).toContain("canvas.eval"); expect(out).toContain("canvas.eval");
}); });