diff --git a/src/cli/nodes-cli/cli-utils.ts b/src/cli/nodes-cli/cli-utils.ts new file mode 100644 index 000000000..45a7569ca --- /dev/null +++ b/src/cli/nodes-cli/cli-utils.ts @@ -0,0 +1,13 @@ +import { defaultRuntime } from "../../runtime.js"; +import { runCommandWithRuntime } from "../cli-utils.js"; +import { unauthorizedHintForMessage } from "./rpc.js"; + +export function runNodesCommand(label: string, action: () => Promise) { + return runCommandWithRuntime(defaultRuntime, action, (err) => { + const message = String(err); + defaultRuntime.error(`nodes ${label} failed: ${message}`); + const hint = unauthorizedHintForMessage(message); + if (hint) defaultRuntime.error(hint); + defaultRuntime.exit(1); + }); +} diff --git a/src/cli/nodes-cli/register.camera.ts b/src/cli/nodes-cli/register.camera.ts index e3a8b4bfb..adb3af2ad 100644 --- a/src/cli/nodes-cli/register.camera.ts +++ b/src/cli/nodes-cli/register.camera.ts @@ -9,6 +9,7 @@ import { writeBase64ToFile, } from "../nodes-camera.js"; import { parseDurationMs } from "../parse-duration.js"; +import { runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -29,7 +30,7 @@ export function registerNodesCameraCommands(nodes: Command) { .description("List available cameras on a node") .requiredOption("--node ", "Node id, name, or IP") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("camera list", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const raw = (await callGatewayCli("node.invoke", opts, { nodeId, @@ -61,10 +62,7 @@ export function registerNodesCameraCommands(nodes: Command) { const position = typeof device.position === "string" ? device.position : "unspecified"; defaultRuntime.log(`${name} (${position})${id ? ` — ${id}` : ""}`); } - } catch (err) { - defaultRuntime.error(`nodes camera list failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), { timeoutMs: 60_000 }, ); @@ -81,7 +79,7 @@ export function registerNodesCameraCommands(nodes: Command) { .option("--delay-ms ", "Delay before capture in ms (macOS default 2000)") .option("--invoke-timeout ", "Node invoke timeout in ms (default 20000)", "20000") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("camera snap", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const facingOpt = String(opts.facing ?? "both") .trim() @@ -153,10 +151,7 @@ export function registerNodesCameraCommands(nodes: Command) { return; } defaultRuntime.log(results.map((r) => `MEDIA:${r.path}`).join("\n")); - } catch (err) { - defaultRuntime.error(`nodes camera snap failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), { timeoutMs: 60_000 }, ); @@ -176,7 +171,7 @@ export function registerNodesCameraCommands(nodes: Command) { .option("--no-audio", "Disable audio capture") .option("--invoke-timeout ", "Node invoke timeout in ms (default 90000)", "90000") .action(async (opts: NodesRpcOpts & { audio?: boolean }) => { - try { + await runNodesCommand("camera clip", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const facing = parseFacing(String(opts.facing ?? "front")); const durationMs = parseDurationMs(String(opts.duration ?? "3000")); @@ -230,10 +225,7 @@ export function registerNodesCameraCommands(nodes: Command) { return; } defaultRuntime.log(`MEDIA:${filePath}`); - } catch (err) { - defaultRuntime.error(`nodes camera clip failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), { timeoutMs: 90_000 }, ); diff --git a/src/cli/nodes-cli/register.canvas.ts b/src/cli/nodes-cli/register.canvas.ts index df880f16e..77a3667ac 100644 --- a/src/cli/nodes-cli/register.canvas.ts +++ b/src/cli/nodes-cli/register.canvas.ts @@ -6,6 +6,7 @@ import { writeBase64ToFile } from "../nodes-camera.js"; import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "../nodes-canvas.js"; import { parseTimeoutMs } from "../nodes-run.js"; import { buildA2UITextJsonl, validateA2UIJsonl } from "./a2ui-jsonl.js"; +import { runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -39,7 +40,7 @@ export function registerNodesCanvasCommands(nodes: Command) { .option("--quality <0-1>", "JPEG quality (optional)") .option("--invoke-timeout ", "Node invoke timeout in ms (default 20000)", "20000") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("canvas snapshot", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const formatOpt = String(opts.format ?? "jpg") .trim() @@ -85,10 +86,7 @@ export function registerNodesCanvasCommands(nodes: Command) { return; } defaultRuntime.log(`MEDIA:${filePath}`); - } catch (err) { - defaultRuntime.error(`nodes canvas snapshot failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), { timeoutMs: 60_000 }, ); @@ -105,7 +103,7 @@ export function registerNodesCanvasCommands(nodes: Command) { .option("--height ", "Placement height") .option("--invoke-timeout ", "Node invoke timeout in ms") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("canvas present", async () => { const placement = { x: opts.x ? Number.parseFloat(opts.x) : undefined, y: opts.y ? Number.parseFloat(opts.y) : undefined, @@ -124,10 +122,7 @@ export function registerNodesCanvasCommands(nodes: Command) { } await invokeCanvas(opts, "canvas.present", params); if (!opts.json) defaultRuntime.log("canvas present ok"); - } catch (err) { - defaultRuntime.error(`nodes canvas present failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -138,13 +133,10 @@ export function registerNodesCanvasCommands(nodes: Command) { .requiredOption("--node ", "Node id, name, or IP") .option("--invoke-timeout ", "Node invoke timeout in ms") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("canvas hide", async () => { await invokeCanvas(opts, "canvas.hide", undefined); if (!opts.json) defaultRuntime.log("canvas hide ok"); - } catch (err) { - defaultRuntime.error(`nodes canvas hide failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -156,13 +148,10 @@ export function registerNodesCanvasCommands(nodes: Command) { .requiredOption("--node ", "Node id, name, or IP") .option("--invoke-timeout ", "Node invoke timeout in ms") .action(async (url: string, opts: NodesRpcOpts) => { - try { + await runNodesCommand("canvas navigate", async () => { await invokeCanvas(opts, "canvas.navigate", { url }); if (!opts.json) defaultRuntime.log("canvas navigate ok"); - } catch (err) { - defaultRuntime.error(`nodes canvas navigate failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -175,7 +164,7 @@ export function registerNodesCanvasCommands(nodes: Command) { .requiredOption("--node ", "Node id, name, or IP") .option("--invoke-timeout ", "Node invoke timeout in ms") .action(async (jsArg: string | undefined, opts: NodesRpcOpts) => { - try { + await runNodesCommand("canvas eval", async () => { const js = opts.js ?? jsArg; if (!js) throw new Error("missing --js or "); const raw = await invokeCanvas(opts, "canvas.eval", { @@ -191,10 +180,7 @@ export function registerNodesCanvasCommands(nodes: Command) { : undefined; if (payload?.result) defaultRuntime.log(payload.result); else defaultRuntime.log("canvas eval ok"); - } catch (err) { - defaultRuntime.error(`nodes canvas eval failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -209,7 +195,7 @@ export function registerNodesCanvasCommands(nodes: Command) { .requiredOption("--node ", "Node id, name, or IP") .option("--invoke-timeout ", "Node invoke timeout in ms") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("canvas a2ui push", async () => { const hasJsonl = Boolean(opts.jsonl); const hasText = typeof opts.text === "string"; if (hasJsonl === hasText) { @@ -231,10 +217,7 @@ export function registerNodesCanvasCommands(nodes: Command) { `canvas a2ui push ok (v0.8, ${messageCount} message${messageCount === 1 ? "" : "s"})`, ); } - } catch (err) { - defaultRuntime.error(`nodes canvas a2ui push failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -245,13 +228,10 @@ export function registerNodesCanvasCommands(nodes: Command) { .requiredOption("--node ", "Node id, name, or IP") .option("--invoke-timeout ", "Node invoke timeout in ms") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("canvas a2ui reset", async () => { await invokeCanvas(opts, "canvas.a2ui.reset", undefined); if (!opts.json) defaultRuntime.log("canvas a2ui reset ok"); - } catch (err) { - defaultRuntime.error(`nodes canvas a2ui reset failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); } diff --git a/src/cli/nodes-cli/register.invoke.ts b/src/cli/nodes-cli/register.invoke.ts index 02aaa32f8..82768b1a1 100644 --- a/src/cli/nodes-cli/register.invoke.ts +++ b/src/cli/nodes-cli/register.invoke.ts @@ -2,6 +2,7 @@ import type { Command } from "commander"; import { randomIdempotencyKey } from "../../gateway/call.js"; import { defaultRuntime } from "../../runtime.js"; import { parseEnvPairs, parseTimeoutMs } from "../nodes-run.js"; +import { runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId, unauthorizedHintForMessage } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -16,7 +17,7 @@ export function registerNodesInvokeCommands(nodes: Command) { .option("--invoke-timeout ", "Node invoke timeout in ms (default 15000)", "15000") .option("--idempotency-key ", "Idempotency key (optional)") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("invoke", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const command = String(opts.command ?? "").trim(); if (!nodeId || !command) { @@ -41,10 +42,7 @@ export function registerNodesInvokeCommands(nodes: Command) { const result = await callGatewayCli("node.invoke", opts, invokeParams); defaultRuntime.log(JSON.stringify(result, null, 2)); - } catch (err) { - defaultRuntime.error(`nodes invoke failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), { timeoutMs: 30_000 }, ); @@ -65,7 +63,7 @@ export function registerNodesInvokeCommands(nodes: Command) { .option("--invoke-timeout ", "Node invoke timeout in ms (default 30000)", "30000") .argument("", "Command and args") .action(async (command: string[], opts: NodesRpcOpts) => { - try { + await runNodesCommand("run", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); if (!Array.isArray(command) || command.length === 0) { throw new Error("command required"); @@ -123,12 +121,7 @@ export function registerNodesInvokeCommands(nodes: Command) { defaultRuntime.exit(1); return; } - } catch (err) { - defaultRuntime.error(`nodes run failed: ${String(err)}`); - const hint = unauthorizedHintForMessage(String(err)); - if (hint) defaultRuntime.error(hint); - defaultRuntime.exit(1); - } + }); }), { timeoutMs: 35_000 }, ); diff --git a/src/cli/nodes-cli/register.location.ts b/src/cli/nodes-cli/register.location.ts index 24934551f..f7cdafd5b 100644 --- a/src/cli/nodes-cli/register.location.ts +++ b/src/cli/nodes-cli/register.location.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import { randomIdempotencyKey } from "../../gateway/call.js"; import { defaultRuntime } from "../../runtime.js"; +import { runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -20,7 +21,7 @@ export function registerNodesLocationCommands(nodes: Command) { .option("--location-timeout ", "Location fix timeout (ms)", "10000") .option("--invoke-timeout ", "Node invoke timeout in ms (default 20000)", "20000") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("location get", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const maxAgeMs = opts.maxAge ? Number.parseInt(String(opts.maxAge), 10) : undefined; const desiredAccuracyRaw = @@ -73,10 +74,7 @@ export function registerNodesLocationCommands(nodes: Command) { return; } defaultRuntime.log(JSON.stringify(payload)); - } catch (err) { - defaultRuntime.error(`nodes location get failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), { timeoutMs: 30_000 }, ); diff --git a/src/cli/nodes-cli/register.notify.ts b/src/cli/nodes-cli/register.notify.ts index e3944de65..c0e453190 100644 --- a/src/cli/nodes-cli/register.notify.ts +++ b/src/cli/nodes-cli/register.notify.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import { randomIdempotencyKey } from "../../gateway/call.js"; import { defaultRuntime } from "../../runtime.js"; +import { runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -17,7 +18,7 @@ export function registerNodesNotifyCommand(nodes: Command) { .option("--delivery ", "Delivery mode", "system") .option("--invoke-timeout ", "Node invoke timeout in ms (default 15000)", "15000") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("notify", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const title = String(opts.title ?? "").trim(); const body = String(opts.body ?? "").trim(); @@ -49,10 +50,7 @@ export function registerNodesNotifyCommand(nodes: Command) { return; } defaultRuntime.log("notify ok"); - } catch (err) { - defaultRuntime.error(`nodes notify failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); } diff --git a/src/cli/nodes-cli/register.pairing.ts b/src/cli/nodes-cli/register.pairing.ts index 56f6c6f67..a95a2c9ee 100644 --- a/src/cli/nodes-cli/register.pairing.ts +++ b/src/cli/nodes-cli/register.pairing.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import { defaultRuntime } from "../../runtime.js"; import { formatAge, parsePairingList } from "./format.js"; +import { runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -10,7 +11,7 @@ export function registerNodesPairingCommands(nodes: Command) { .command("pending") .description("List pending pairing requests") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("pending", async () => { const result = (await callGatewayCli("node.pair.list", opts, {})) as unknown; const { pending } = parsePairingList(result); if (opts.json) { @@ -28,10 +29,7 @@ export function registerNodesPairingCommands(nodes: Command) { const age = typeof r.ts === "number" ? ` · ${formatAge(Date.now() - r.ts)} ago` : ""; defaultRuntime.log(`- ${r.requestId}: ${name}${repair}${ip}${age}`); } - } catch (err) { - defaultRuntime.error(`nodes pending failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -41,15 +39,12 @@ export function registerNodesPairingCommands(nodes: Command) { .description("Approve a pending pairing request") .argument("", "Pending request id") .action(async (requestId: string, opts: NodesRpcOpts) => { - try { + await runNodesCommand("approve", async () => { const result = await callGatewayCli("node.pair.approve", opts, { requestId, }); defaultRuntime.log(JSON.stringify(result, null, 2)); - } catch (err) { - defaultRuntime.error(`nodes approve failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -59,15 +54,12 @@ export function registerNodesPairingCommands(nodes: Command) { .description("Reject a pending pairing request") .argument("", "Pending request id") .action(async (requestId: string, opts: NodesRpcOpts) => { - try { + await runNodesCommand("reject", async () => { const result = await callGatewayCli("node.pair.reject", opts, { requestId, }); defaultRuntime.log(JSON.stringify(result, null, 2)); - } catch (err) { - defaultRuntime.error(`nodes reject failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -78,7 +70,7 @@ export function registerNodesPairingCommands(nodes: Command) { .requiredOption("--node ", "Node id, name, or IP") .requiredOption("--name ", "New display name") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("rename", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const name = String(opts.name ?? "").trim(); if (!nodeId || !name) { @@ -95,10 +87,7 @@ export function registerNodesPairingCommands(nodes: Command) { return; } defaultRuntime.log(`node rename ok: ${nodeId} -> ${name}`); - } catch (err) { - defaultRuntime.error(`nodes rename failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); } diff --git a/src/cli/nodes-cli/register.screen.ts b/src/cli/nodes-cli/register.screen.ts index 1bf578ad0..4a50f539b 100644 --- a/src/cli/nodes-cli/register.screen.ts +++ b/src/cli/nodes-cli/register.screen.ts @@ -7,6 +7,7 @@ import { writeScreenRecordToFile, } from "../nodes-screen.js"; import { parseDurationMs } from "../parse-duration.js"; +import { runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -27,7 +28,7 @@ export function registerNodesScreenCommands(nodes: Command) { .option("--out ", "Output path") .option("--invoke-timeout ", "Node invoke timeout in ms (default 120000)", "120000") .action(async (opts: NodesRpcOpts & { out?: string }) => { - try { + await runNodesCommand("screen record", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const durationMs = parseDurationMs(opts.duration ?? ""); const screenIndex = Number.parseInt(String(opts.screen ?? "0"), 10); @@ -77,10 +78,7 @@ export function registerNodesScreenCommands(nodes: Command) { return; } defaultRuntime.log(`MEDIA:${written.path}`); - } catch (err) { - defaultRuntime.error(`nodes screen record failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), { timeoutMs: 180_000 }, ); diff --git a/src/cli/nodes-cli/register.status.ts b/src/cli/nodes-cli/register.status.ts index c7940f0cb..3371a09bc 100644 --- a/src/cli/nodes-cli/register.status.ts +++ b/src/cli/nodes-cli/register.status.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import { defaultRuntime } from "../../runtime.js"; import { formatAge, formatPermissions, parseNodeList, parsePairingList } from "./format.js"; +import { runNodesCommand } from "./cli-utils.js"; import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js"; import type { NodesRpcOpts } from "./types.js"; @@ -47,7 +48,7 @@ export function registerNodesStatusCommands(nodes: Command) { .command("status") .description("List known nodes with connection status and capabilities") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("status", async () => { const result = (await callGatewayCli("node.list", opts, {})) as unknown; if (opts.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); @@ -79,10 +80,7 @@ export function registerNodesStatusCommands(nodes: Command) { `- ${name} · ${n.nodeId}${ip}${device}${hw}${permsText}${versionText} · ${pairing} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`, ); } - } catch (err) { - defaultRuntime.error(`nodes status failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -92,7 +90,7 @@ export function registerNodesStatusCommands(nodes: Command) { .description("Describe a node (capabilities + supported invoke commands)") .requiredOption("--node ", "Node id, name, or IP") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("describe", async () => { const nodeId = await resolveNodeId(opts, String(opts.node ?? "")); const result = (await callGatewayCli("node.describe", opts, { nodeId, @@ -140,10 +138,7 @@ export function registerNodesStatusCommands(nodes: Command) { return; } for (const c of commands) defaultRuntime.log(`- ${c}`); - } catch (err) { - defaultRuntime.error(`nodes describe failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); @@ -152,7 +147,7 @@ export function registerNodesStatusCommands(nodes: Command) { .command("list") .description("List pending and paired nodes") .action(async (opts: NodesRpcOpts) => { - try { + await runNodesCommand("list", async () => { const result = (await callGatewayCli("node.pair.list", opts, {})) as unknown; if (opts.json) { defaultRuntime.log(JSON.stringify(result, null, 2)); @@ -178,10 +173,7 @@ export function registerNodesStatusCommands(nodes: Command) { defaultRuntime.log(`- ${n.nodeId}: ${name}${ip}`); } } - } catch (err) { - defaultRuntime.error(`nodes list failed: ${String(err)}`); - defaultRuntime.exit(1); - } + }); }), ); }