Download src/cli/nodes-cli/register.status.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 25.3 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/nodes-cli/register.status.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/cli/nodes-cli/register.status.ts
-
curl -L -o register.status.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/nodes-cli/register.status.ts
25.3 kB
| // Node status/list/describe commands and paired-node display formatting. | |
| import { formatByteSize } from "@openclaw/normalization-core"; | |
| import { | |
| normalizeLowercaseStringOrEmpty, | |
| normalizeOptionalLowercaseString, | |
| normalizeOptionalString, | |
| } from "@openclaw/normalization-core/string-coerce"; | |
| import type { Command } from "commander"; | |
| import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js"; | |
| import { getTerminalTableWidth, renderTable } from "../../../packages/terminal-core/src/table.js"; | |
| import { formatErrorMessage } from "../../infra/errors.js"; | |
| import { formatTimeAgo } from "../../infra/format-time/format-relative.ts"; | |
| import { defaultRuntime } from "../../runtime.js"; | |
| import { isNodeHostStats } from "../../shared/node-host-stats.js"; | |
| import { shortenHomeInString } from "../../utils.js"; | |
| import { formatPairingApproveCommand } from "../pairing-command-format.js"; | |
| import { parseDurationMs } from "../parse-duration.js"; | |
| import { formatConnectionFlagReminder, getNodesTheme, runNodesCommand } from "./cli-utils.js"; | |
| import { formatPermissions, parseNodeList, parsePairingList } from "./format.js"; | |
| import { renderPendingPairingRequestsTable } from "./pairing-render.js"; | |
| import { | |
| callNodesGatewayCli, | |
| callNodeDiagnosticsGatewayCli, | |
| nodesCallOpts, | |
| resolveNodeDiagnosticsId, | |
| } from "./rpc.js"; | |
| import type { NodeListNode, NodesRpcOpts, PairedNode } from "./types.js"; | |
| type PairedNodeListRow = PairedNode & Partial<NodeListNode>; | |
| type NodeApprovalState = NonNullable<NodeListNode["approvalState"]>; | |
| function formatNodeStatsBytes(bytes: number): string { | |
| return formatByteSize(bytes, { | |
| style: "legacy-binary", | |
| maxUnit: "tera", | |
| separator: " ", | |
| fractionDigits: (value, unit) => (value < 10 && unit !== "byte" ? 1 : 0), | |
| }); | |
| } | |
| function formatNodeHostStats(stats: unknown, connected: boolean, now: number): string | null { | |
| if (!isNodeHostStats(stats)) { | |
| return null; | |
| } | |
| const totalMemory = formatNodeStatsBytes(stats.memoryTotalBytes); | |
| const usedMemory = formatNodeStatsBytes(stats.memoryTotalBytes - stats.memoryFreeBytes); | |
| const memoryUnit = totalMemory.slice(totalMemory.lastIndexOf(" ")); | |
| const usedLabel = usedMemory.endsWith(memoryUnit) | |
| ? usedMemory.slice(0, -memoryUnit.length) | |
| : usedMemory; | |
| const summary = [ | |
| stats.loadAverage ? `load ${stats.loadAverage[0].toFixed(1)}/${stats.cpuCount}` : null, | |
| `mem ${usedLabel}/${totalMemory}`, | |
| stats.diskAvailableBytes !== undefined | |
| ? `disk ${formatNodeStatsBytes(stats.diskAvailableBytes)} free` | |
| : null, | |
| ] | |
| .filter(Boolean) | |
| .join(" · "); | |
| return connected | |
| ? summary | |
| : `${summary} (last known ${formatTimeAgo(Math.max(0, now - stats.updatedAtMs))})`; | |
| } | |
| function formatVersionLabel(raw: string) { | |
| const trimmed = raw.trim(); | |
| if (!trimmed) { | |
| return raw; | |
| } | |
| if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("v")) { | |
| return trimmed; | |
| } | |
| return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed; | |
| } | |
| function resolveNodeVersions(node: { | |
| platform?: string; | |
| version?: string; | |
| coreVersion?: string; | |
| uiVersion?: string; | |
| }) { | |
| const core = normalizeOptionalString(node.coreVersion); | |
| const ui = normalizeOptionalString(node.uiVersion); | |
| if (core || ui) { | |
| return { core, ui }; | |
| } | |
| const legacy = node.version?.trim(); | |
| if (!legacy) { | |
| return { core: undefined, ui: undefined }; | |
| } | |
| const platform = normalizeOptionalLowercaseString(node.platform) ?? ""; | |
| // Legacy nodes reported one version field; headless hosts use it as core, mobile nodes as UI. | |
| const headless = | |
| platform === "darwin" || platform === "linux" || platform === "win32" || platform === "windows"; | |
| return headless ? { core: legacy, ui: undefined } : { core: undefined, ui: legacy }; | |
| } | |
| function formatNodeVersions(node: { | |
| platform?: string; | |
| version?: string; | |
| coreVersion?: string; | |
| uiVersion?: string; | |
| }) { | |
| const { core, ui } = resolveNodeVersions(node); | |
| const parts: string[] = []; | |
| if (core) { | |
| parts.push(`core ${formatVersionLabel(core)}`); | |
| } | |
| if (ui) { | |
| parts.push(`ui ${formatVersionLabel(ui)}`); | |
| } | |
| return parts.length > 0 ? parts.join(" · ") : null; | |
| } | |
| function isWindowsNodePlatform(platform?: string): boolean { | |
| const normalized = normalizeOptionalLowercaseString(platform) ?? ""; | |
| return normalized === "win32" || normalized === "windows"; | |
| } | |
| function formatPathEnv(raw?: string, platform?: string): string | null { | |
| if (typeof raw !== "string") { | |
| return null; | |
| } | |
| const trimmed = raw.trim(); | |
| if (!trimmed) { | |
| return null; | |
| } | |
| const delimiter = isWindowsNodePlatform(platform) ? ";" : ":"; | |
| const parts = trimmed.split(delimiter).filter(Boolean); | |
| const display = | |
| parts.length <= 3 | |
| ? trimmed | |
| : `${parts.slice(0, 2).join(delimiter)}${delimiter}…${delimiter}${parts.slice(-1)[0]}`; | |
| return shortenHomeInString(display); | |
| } | |
| function formatClientLabel(node: { clientId?: string; clientMode?: string }): string | null { | |
| const clientId = node.clientId?.trim(); | |
| const clientMode = node.clientMode?.trim(); | |
| if (clientId && clientMode) { | |
| return `${clientId}/${clientMode}`; | |
| } | |
| return clientId || clientMode || null; | |
| } | |
| function formatNodeTerminalLabel(node: { nodeId: string; displayName?: string }): string { | |
| const label = node.displayName?.trim() ? node.displayName.trim() : node.nodeId; | |
| return sanitizeTerminalText(label); | |
| } | |
| function formatNodeTimeAgo(now: number, timestamp: unknown): string | null { | |
| return typeof timestamp === "number" && Number.isFinite(timestamp) | |
| ? formatTimeAgo(Math.max(0, now - timestamp)) | |
| : null; | |
| } | |
| function formatNodeApprovalState(raw: unknown): NodeApprovalState | null { | |
| return raw === "approved" || | |
| raw === "pending-approval" || | |
| raw === "pending-reapproval" || | |
| raw === "unapproved" | |
| ? raw | |
| : null; | |
| } | |
| function formatApprovalStateLabel(state: NodeApprovalState): string { | |
| if (state === "pending-approval") { | |
| return "approval pending"; | |
| } | |
| if (state === "pending-reapproval") { | |
| return "reapproval pending"; | |
| } | |
| return state; | |
| } | |
| function isPendingApprovalState( | |
| state: NodeApprovalState | null, | |
| ): state is "pending-approval" | "pending-reapproval" { | |
| return state === "pending-approval" || state === "pending-reapproval"; | |
| } | |
| function parseSinceMs(raw: string | undefined, label: string): number | undefined { | |
| if (raw === undefined) { | |
| return undefined; | |
| } | |
| try { | |
| return parseDurationMs(raw); | |
| } catch (err) { | |
| throw new Error(`${label}: ${formatErrorMessage(err)}`, { cause: err }); | |
| } | |
| } | |
| function matchesNodeConnectionFilter( | |
| node: NodeListNode, | |
| connectedOnly: boolean, | |
| sinceMs: number | undefined, | |
| now: number, | |
| ): boolean { | |
| if (connectedOnly && !node.connected) { | |
| return false; | |
| } | |
| // Older gateways lack the recorded lastConnectedAtMs field. | |
| const lastConnectedAtMs = node.lastConnectedAtMs ?? node.connectedAtMs; | |
| return ( | |
| sinceMs === undefined || | |
| (typeof lastConnectedAtMs === "number" && now - lastConnectedAtMs <= sinceMs) | |
| ); | |
| } | |
| function mergePairedNodeWithEffectiveNode( | |
| paired: PairedNode | undefined, | |
| effective: NodeListNode, | |
| ): PairedNodeListRow { | |
| return { | |
| ...paired, | |
| ...effective, | |
| createdAtMs: paired?.createdAtMs, | |
| // node.list can record a connection newer than the separate pairing snapshot. | |
| lastConnectedAtMs: | |
| effective.lastConnectedAtMs ?? paired?.lastConnectedAtMs ?? effective.connectedAtMs, | |
| displayName: effective.displayName ?? paired?.displayName, | |
| platform: effective.platform ?? paired?.platform, | |
| version: effective.version ?? paired?.version, | |
| coreVersion: effective.coreVersion ?? paired?.coreVersion, | |
| uiVersion: effective.uiVersion ?? paired?.uiVersion, | |
| remoteIp: effective.remoteIp ?? paired?.remoteIp, | |
| permissions: effective.permissions ?? paired?.permissions, | |
| approvedAtMs: effective.approvedAtMs ?? paired?.approvedAtMs, | |
| }; | |
| } | |
| function mergePairedNodesWithEffectiveNodes( | |
| paired: PairedNode[], | |
| effectiveNodes: NodeListNode[] | null, | |
| ): PairedNodeListRow[] { | |
| if (effectiveNodes === null) { | |
| return paired; | |
| } | |
| const pairedById = new Map(paired.map((node) => [node.nodeId, node])); | |
| const seen = new Set<string>(); | |
| const rows: PairedNodeListRow[] = []; | |
| for (const effective of effectiveNodes) { | |
| const pairedNode = pairedById.get(effective.nodeId); | |
| if (!pairedNode && effective.paired !== true) { | |
| continue; | |
| } | |
| seen.add(effective.nodeId); | |
| rows.push(mergePairedNodeWithEffectiveNode(pairedNode, effective)); | |
| } | |
| for (const node of paired) { | |
| if (!seen.has(node.nodeId)) { | |
| rows.push(node); | |
| } | |
| } | |
| return rows; | |
| } | |
| async function tryReadNodeList(opts: NodesRpcOpts): Promise<NodeListNode[] | null> { | |
| try { | |
| return parseNodeList(await callNodeDiagnosticsGatewayCli("node.list", opts, {})); | |
| } catch (error) { | |
| // Best-effort enrichment may degrade to pairing-only rows, but never | |
| // silently: without this notice the table looks authoritative while | |
| // omitting connected/commands state. Stderr keeps --json output clean. | |
| defaultRuntime.error( | |
| getNodesTheme().muted( | |
| `live node view unavailable (${formatErrorMessage(error)}); showing paired-only data`, | |
| ), | |
| ); | |
| return null; | |
| } | |
| } | |
| /** Register node status, describe, and paired-node list commands. */ | |
| export function registerNodesStatusCommands(nodes: Command) { | |
| nodesCallOpts( | |
| nodes | |
| .command("status") | |
| .description("List known nodes with connection status and capabilities") | |
| .option("--connected", "Only show connected nodes") | |
| .option("--last-connected <duration>", "Only show nodes connected within duration (e.g. 24h)") | |
| .action(async (opts: NodesRpcOpts) => { | |
| await runNodesCommand("status", async () => { | |
| const connectedOnly = Boolean(opts.connected); | |
| const sinceMs = parseSinceMs(opts.lastConnected, "Invalid --last-connected"); | |
| const result = await callNodeDiagnosticsGatewayCli("node.list", opts, {}); | |
| const obj: Record<string, unknown> = | |
| typeof result === "object" && result !== null ? result : {}; | |
| const { ok, warn, muted } = getNodesTheme(); | |
| const tableWidth = getTerminalTableWidth(); | |
| const now = Date.now(); | |
| const nodesLocal = parseNodeList(result); | |
| const filtered = nodesLocal.filter((node) => | |
| matchesNodeConnectionFilter(node, connectedOnly, sinceMs, now), | |
| ); | |
| if (opts.json) { | |
| const ts = typeof obj.ts === "number" ? obj.ts : Date.now(); | |
| defaultRuntime.writeJson({ ...obj, ts, nodes: filtered }); | |
| return; | |
| } | |
| const pairedCount = filtered.filter((n) => Boolean(n.paired)).length; | |
| const connectedCount = filtered.filter((n) => Boolean(n.connected)).length; | |
| const filteredLabel = | |
| filtered.length !== nodesLocal.length ? ` (of ${nodesLocal.length})` : ""; | |
| defaultRuntime.log( | |
| `Known: ${filtered.length}${filteredLabel} · Paired: ${pairedCount} · Connected: ${connectedCount}`, | |
| ); | |
| if (filtered.length === 0) { | |
| return; | |
| } | |
| const rows = filtered.map((n) => { | |
| const perms = formatPermissions(n.permissions); | |
| const versions = formatNodeVersions(n); | |
| const pathEnv = formatPathEnv(n.pathEnv, n.platform); | |
| const client = formatClientLabel(n); | |
| const lastActive = formatNodeTimeAgo(now, n.lastActiveAtMs); | |
| const detailParts = [ | |
| client ? `client: ${client}` : null, | |
| n.deviceFamily ? `device: ${n.deviceFamily}` : null, | |
| n.modelIdentifier ? `hw: ${n.modelIdentifier}` : null, | |
| perms ? `perms: ${perms}` : null, | |
| versions, | |
| formatNodeHostStats(n.hostStats, Boolean(n.connected), now), | |
| pathEnv ? `path: ${pathEnv}` : null, | |
| lastActive ? `input: ${lastActive}${n.active ? " (active)" : ""}` : null, | |
| ] | |
| .filter(Boolean) | |
| .map((part) => sanitizeTerminalText(String(part))); | |
| const caps = Array.isArray(n.caps) | |
| ? sanitizeTerminalText(n.caps.map(String).filter(Boolean).toSorted().join(", ")) | |
| : "?"; | |
| const paired = n.paired ? ok("paired") : warn("unpaired"); | |
| const connected = n.connected ? ok("connected") : muted("disconnected"); | |
| const approvalState = formatNodeApprovalState(n.approvalState); | |
| const approval = | |
| approvalState === "approved" | |
| ? ok("approved") | |
| : isPendingApprovalState(approvalState) | |
| ? warn(formatApprovalStateLabel(approvalState)) | |
| : approvalState === "unapproved" | |
| ? warn("unapproved") | |
| : null; | |
| const since = | |
| typeof n.connectedAtMs === "number" | |
| ? ` (${formatTimeAgo(Math.max(0, now - n.connectedAtMs))})` | |
| : ""; | |
| return { | |
| Node: formatNodeTerminalLabel(n), | |
| ID: sanitizeTerminalText(n.nodeId), | |
| IP: sanitizeTerminalText(n.remoteIp ?? ""), | |
| Detail: detailParts.join(" · "), | |
| Status: `${paired} · ${connected}${since}${approval ? ` · ${approval}` : ""}`, | |
| 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: "Detail", header: "Detail", minWidth: 18, flex: true }, | |
| { key: "Status", header: "Status", minWidth: 18 }, | |
| { key: "Caps", header: "Caps", minWidth: 12, flex: true }, | |
| ], | |
| rows, | |
| }).trimEnd(), | |
| ); | |
| for (const node of filtered) { | |
| const approvalState = formatNodeApprovalState(node.approvalState); | |
| const requestId = normalizeOptionalString(node.pendingRequestId); | |
| if (isPendingApprovalState(approvalState) && requestId) { | |
| const approveCommand = formatPairingApproveCommand("nodes", requestId, { | |
| timeout: opts.timeout, | |
| }); | |
| const action = approvalState === "pending-reapproval" ? "Reapproval" : "Approval"; | |
| defaultRuntime.log( | |
| warn( | |
| `${action} pending for ${formatNodeTerminalLabel(node)}. Run ${sanitizeTerminalText(approveCommand)}`, | |
| ), | |
| ); | |
| const connectionReminder = formatConnectionFlagReminder(opts); | |
| if (connectionReminder) { | |
| defaultRuntime.log(warn(connectionReminder)); | |
| } | |
| } | |
| } | |
| }); | |
| }), | |
| ); | |
| nodesCallOpts( | |
| nodes | |
| .command("describe") | |
| .description("Describe a node (capabilities + supported invoke commands)") | |
| .requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP") | |
| .action(async (opts: NodesRpcOpts) => { | |
| await runNodesCommand("describe", async () => { | |
| const nodeId = await resolveNodeDiagnosticsId(opts, opts.node ?? ""); | |
| const result = await callNodeDiagnosticsGatewayCli("node.describe", opts, { | |
| nodeId, | |
| }); | |
| if (opts.json) { | |
| defaultRuntime.writeJson(result); | |
| return; | |
| } | |
| const obj: Record<string, unknown> = | |
| typeof result === "object" && result !== null ? result : {}; | |
| const displayName = typeof obj.displayName === "string" ? obj.displayName : nodeId; | |
| const connected = Boolean(obj.connected); | |
| const paired = Boolean(obj.paired); | |
| const caps = Array.isArray(obj.caps) | |
| ? obj.caps.map(String).filter(Boolean).toSorted() | |
| : null; | |
| const commands = Array.isArray(obj.commands) | |
| ? obj.commands.map(String).filter(Boolean).toSorted() | |
| : []; | |
| const perms = formatPermissions(obj.permissions); | |
| const approvalState = formatNodeApprovalState(obj.approvalState); | |
| const pendingRequestId = normalizeOptionalString(obj.pendingRequestId); | |
| const pendingCaps = Array.isArray(obj.pendingDeclaredCaps) | |
| ? obj.pendingDeclaredCaps.map(String).filter(Boolean).toSorted() | |
| : null; | |
| const pendingCommands = Array.isArray(obj.pendingDeclaredCommands) | |
| ? obj.pendingDeclaredCommands.map(String).filter(Boolean).toSorted() | |
| : []; | |
| const pendingPerms = formatPermissions(obj.pendingDeclaredPermissions); | |
| const approveCommand = | |
| isPendingApprovalState(approvalState) && pendingRequestId | |
| ? formatPairingApproveCommand("nodes", pendingRequestId, { timeout: opts.timeout }) | |
| : null; | |
| const connectionReminder = approveCommand ? formatConnectionFlagReminder(opts) : null; | |
| const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null; | |
| const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null; | |
| const client = formatClientLabel(obj as { clientId?: string; clientMode?: string }); | |
| const ip = typeof obj.remoteIp === "string" ? obj.remoteIp : null; | |
| const pathEnv = typeof obj.pathEnv === "string" ? obj.pathEnv : null; | |
| const versions = formatNodeVersions( | |
| obj as { | |
| platform?: string; | |
| version?: string; | |
| coreVersion?: string; | |
| uiVersion?: string; | |
| }, | |
| ); | |
| const lastActive = formatNodeTimeAgo(Date.now(), obj.lastActiveAtMs); | |
| const stats = formatNodeHostStats(obj.hostStats, connected, Date.now()); | |
| const { heading, ok, warn, muted } = getNodesTheme(); | |
| const status = `${paired ? ok("paired") : warn("unpaired")} · ${ | |
| connected ? ok("connected") : muted("disconnected") | |
| }`; | |
| const tableWidth = getTerminalTableWidth(); | |
| const rows = [ | |
| { Field: "ID", Value: sanitizeTerminalText(nodeId) }, | |
| displayName ? { Field: "Name", Value: sanitizeTerminalText(displayName) } : null, | |
| client ? { Field: "Client", Value: sanitizeTerminalText(client) } : null, | |
| ip ? { Field: "IP", Value: sanitizeTerminalText(ip) } : null, | |
| family ? { Field: "Device", Value: sanitizeTerminalText(family) } : null, | |
| model ? { Field: "Model", Value: sanitizeTerminalText(model) } : null, | |
| perms ? { Field: "Perms", Value: sanitizeTerminalText(perms) } : null, | |
| versions ? { Field: "Version", Value: sanitizeTerminalText(versions) } : null, | |
| stats ? { Field: "Stats", Value: stats } : null, | |
| pathEnv ? { Field: "PATH", Value: sanitizeTerminalText(pathEnv) } : null, | |
| lastActive | |
| ? { | |
| Field: "Last input", | |
| Value: `${lastActive}${obj.active === true ? " (active node)" : ""}`, | |
| } | |
| : null, | |
| { Field: "Status", Value: status }, | |
| approvalState | |
| ? { Field: "Approval", Value: formatApprovalStateLabel(approvalState) } | |
| : null, | |
| pendingRequestId | |
| ? { Field: "Pending request", Value: sanitizeTerminalText(pendingRequestId) } | |
| : null, | |
| pendingCaps | |
| ? { Field: "Pending caps", Value: sanitizeTerminalText(pendingCaps.join(", ")) } | |
| : null, | |
| pendingPerms | |
| ? { Field: "Pending perms", Value: sanitizeTerminalText(pendingPerms) } | |
| : null, | |
| approveCommand | |
| ? { | |
| Field: approvalState === "pending-reapproval" ? "Reapprove" : "Approve", | |
| Value: sanitizeTerminalText(approveCommand), | |
| } | |
| : null, | |
| approveCommand && connectionReminder | |
| ? { Field: "Connection reminder", Value: connectionReminder } | |
| : null, | |
| { Field: "Caps", Value: caps ? sanitizeTerminalText(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) { | |
| defaultRuntime.log(muted("- (none effective)")); | |
| } else { | |
| for (const c of commands) { | |
| defaultRuntime.log(`- ${sanitizeTerminalText(c)}`); | |
| } | |
| } | |
| if (pendingCommands.length > 0) { | |
| defaultRuntime.log(""); | |
| defaultRuntime.log(heading("Pending commands")); | |
| for (const command of pendingCommands) { | |
| defaultRuntime.log(`- ${sanitizeTerminalText(command)}`); | |
| } | |
| } | |
| }); | |
| }), | |
| ); | |
| nodesCallOpts( | |
| nodes | |
| .command("list") | |
| .description("List pending and paired nodes") | |
| .option("--connected", "Only show connected nodes") | |
| .option("--last-connected <duration>", "Only show nodes connected within duration (e.g. 24h)") | |
| .action(async (opts: NodesRpcOpts) => { | |
| await runNodesCommand("list", async () => { | |
| const connectedOnly = Boolean(opts.connected); | |
| const sinceMs = parseSinceMs(opts.lastConnected, "Invalid --last-connected"); | |
| const result = await callNodesGatewayCli("node.pair.list", opts, {}); | |
| const { pending, paired } = parsePairingList(result); | |
| const { heading, muted, warn } = getNodesTheme(); | |
| const tableWidth = getTerminalTableWidth(); | |
| const now = Date.now(); | |
| const hasFilters = connectedOnly || sinceMs !== undefined; | |
| // Pending requests carry no connection state to filter on; hiding | |
| // them under --connected printed "Pending: 0" while requests waited. | |
| const pendingRows = pending; | |
| const effectiveNodes = hasFilters | |
| ? parseNodeList(await callNodeDiagnosticsGatewayCli("node.list", opts, {})) | |
| : await tryReadNodeList(opts); | |
| const effectivePairedRows = mergePairedNodesWithEffectiveNodes(paired, effectiveNodes); | |
| const filteredPaired = effectivePairedRows.filter((node) => | |
| matchesNodeConnectionFilter(node, connectedOnly, sinceMs, now), | |
| ); | |
| const filteredLabel = | |
| hasFilters && filteredPaired.length !== effectivePairedRows.length | |
| ? ` (of ${effectivePairedRows.length})` | |
| : ""; | |
| if (opts.json) { | |
| defaultRuntime.writeJson({ | |
| pending: pendingRows, | |
| // Current gateways emit no token, but the permissive parser keeps | |
| // unknown fields; strip so an older gateway's legacy node token | |
| // never reaches JSON output. | |
| paired: filteredPaired.map((row) => { | |
| const { token: _token, ...rest } = row as { token?: unknown }; | |
| return rest; | |
| }), | |
| }); | |
| return; | |
| } | |
| defaultRuntime.log( | |
| `Pending: ${pendingRows.length} · Paired: ${filteredPaired.length}${filteredLabel}`, | |
| ); | |
| if (pendingRows.length > 0) { | |
| const rendered = renderPendingPairingRequestsTable({ | |
| pending: pendingRows, | |
| now, | |
| tableWidth, | |
| theme: { heading, warn, muted }, | |
| }); | |
| defaultRuntime.log(""); | |
| defaultRuntime.log(rendered.heading); | |
| defaultRuntime.log(rendered.table); | |
| } | |
| if (filteredPaired.length > 0) { | |
| const pairedTableRows = filteredPaired.map((n) => ({ | |
| Node: formatNodeTerminalLabel(n), | |
| Id: sanitizeTerminalText(n.nodeId), | |
| IP: sanitizeTerminalText(n.remoteIp ?? ""), | |
| LastConnect: | |
| formatNodeTimeAgo(now, n.lastConnectedAtMs ?? n.connectedAtMs) ?? muted("unknown"), | |
| })); | |
| defaultRuntime.log(""); | |
| defaultRuntime.log(heading("Paired")); | |
| 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: "LastConnect", header: "Last Connect", minWidth: 14 }, | |
| ], | |
| rows: pairedTableRows, | |
| }).trimEnd(), | |
| ); | |
| } | |
| }); | |
| }), | |
| ); | |
| } | |