import type { Command } from "commander"; import { callGateway, randomIdempotencyKey } from "../../gateway/call.js"; import { resolveNodeFromNodeList } from "../../shared/node-resolve.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js"; import { withProgress } from "../progress.js"; import { parseNodeList, parsePairingList } from "./format.js"; import type { NodeListNode, NodesRpcOpts } from "./types.js"; export const nodesCallOpts = (cmd: Command, defaults?: { timeoutMs?: number }) => cmd .option("--url ", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)") .option("--token ", "Gateway token (if required)") .option("--timeout ", "Timeout in ms", String(defaults?.timeoutMs ?? 10_000)) .option("--json", "Output JSON", false); export const callGatewayCli = async ( method: string, opts: NodesRpcOpts, params?: unknown, callOpts?: { transportTimeoutMs?: number }, ) => withProgress( { label: `Nodes ${method}`, indeterminate: true, enabled: opts.json !== true, }, async () => await callGateway({ url: opts.url, token: opts.token, method, params, timeoutMs: callOpts?.transportTimeoutMs ?? Number(opts.timeout ?? 10_000), clientName: GATEWAY_CLIENT_NAMES.CLI, mode: GATEWAY_CLIENT_MODES.CLI, }), ); export function buildNodeInvokeParams(params: { nodeId: string; command: string; params?: Record; timeoutMs?: number; idempotencyKey?: string; }): Record { const invokeParams: Record = { nodeId: params.nodeId, command: params.command, params: params.params, idempotencyKey: params.idempotencyKey ?? randomIdempotencyKey(), }; if (typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)) { invokeParams.timeoutMs = params.timeoutMs; } return invokeParams; } export function unauthorizedHintForMessage(message: string): string | null { const haystack = message.toLowerCase(); if ( haystack.includes("unauthorizedclient") || haystack.includes("bridge client is not authorized") || haystack.includes("unsigned bridge clients are not allowed") ) { return [ "peekaboo bridge rejected the client.", "sign the peekaboo CLI (TeamID Y5PE65HELJ) or launch the host with", "PEEKABOO_ALLOW_UNSIGNED_SOCKET_CLIENTS=1 for local dev.", ].join(" "); } return null; } export async function resolveNodeId(opts: NodesRpcOpts, query: string) { return (await resolveNode(opts, query)).nodeId; } export async function resolveNode(opts: NodesRpcOpts, query: string): Promise { let nodes: NodeListNode[] = []; try { const res = await callGatewayCli("node.list", opts, {}); nodes = parseNodeList(res); } catch { const res = await callGatewayCli("node.pair.list", opts, {}); const { paired } = parsePairingList(res); nodes = paired.map((n) => ({ nodeId: n.nodeId, displayName: n.displayName, platform: n.platform, version: n.version, remoteIp: n.remoteIp, })); } return resolveNodeFromNodeList(nodes, query); }