





















@@ -7,14 +7,17 @@ import {
77normalizeOptionalLowercaseString,
88normalizeOptionalString,
99} from "../../shared/string-coerce.js";
10+import { sanitizeTerminalText } from "../../terminal/safe-text.js";
1011import { getTerminalTableWidth, renderTable } from "../../terminal/table.js";
1112import { shortenHomeInString } from "../../utils.js";
1213import { parseDurationMs } from "../parse-duration.js";
1314import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
1415import { formatPermissions, parseNodeList, parsePairingList } from "./format.js";
1516import { renderPendingPairingRequestsTable } from "./pairing-render.js";
1617import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
17-import type { NodesRpcOpts } from "./types.js";
18+import type { NodeListNode, NodesRpcOpts, PairedNode } from "./types.js";
19+20+type PairedNodeListRow = PairedNode & Partial<NodeListNode>;
18211922function formatVersionLabel(raw: string) {
2023const trimmed = raw.trim();
@@ -88,6 +91,11 @@ function formatClientLabel(node: { clientId?: string; clientMode?: string }): st
8891return clientId || clientMode || null;
8992}
909394+function formatNodeTerminalLabel(node: { nodeId: string; displayName?: string }): string {
95+const label = node.displayName?.trim() ? node.displayName.trim() : node.nodeId;
96+return sanitizeTerminalText(label);
97+}
98+9199function parseSinceMs(raw: unknown, label: string): number | undefined {
92100if (raw === undefined || raw === null) {
93101return undefined;
@@ -111,6 +119,67 @@ function parseSinceMs(raw: unknown, label: string): number | undefined {
111119}
112120}
113121122+function mergePairedNodeWithEffectiveNode(
123+paired: PairedNode | undefined,
124+effective: NodeListNode,
125+): PairedNodeListRow {
126+return {
127+ ...paired,
128+ ...effective,
129+token: paired?.token,
130+createdAtMs: paired?.createdAtMs,
131+lastConnectedAtMs: paired?.lastConnectedAtMs ?? effective.connectedAtMs,
132+displayName: effective.displayName ?? paired?.displayName,
133+platform: effective.platform ?? paired?.platform,
134+version: effective.version ?? paired?.version,
135+coreVersion: effective.coreVersion ?? paired?.coreVersion,
136+uiVersion: effective.uiVersion ?? paired?.uiVersion,
137+remoteIp: effective.remoteIp ?? paired?.remoteIp,
138+permissions: effective.permissions ?? paired?.permissions,
139+approvedAtMs: effective.approvedAtMs ?? paired?.approvedAtMs,
140+};
141+}
142+143+function mergePairedNodesWithEffectiveNodes(
144+paired: PairedNode[],
145+effectiveNodes: NodeListNode[] | null,
146+): PairedNodeListRow[] {
147+if (effectiveNodes === null) {
148+return paired;
149+}
150+const pairedById = new Map(paired.map((node) => [node.nodeId, node]));
151+const seen = new Set<string>();
152+const rows: PairedNodeListRow[] = [];
153+for (const effective of effectiveNodes) {
154+const pairedNode = pairedById.get(effective.nodeId);
155+if (!pairedNode && effective.paired !== true) {
156+continue;
157+}
158+seen.add(effective.nodeId);
159+rows.push(mergePairedNodeWithEffectiveNode(pairedNode, effective));
160+}
161+for (const node of paired) {
162+if (!seen.has(node.nodeId)) {
163+rows.push(node);
164+}
165+}
166+return rows;
167+}
168+169+async function tryReadNodeList(opts: NodesRpcOpts): Promise<NodeListNode[] | null> {
170+try {
171+return parseNodeList(await callGatewayCli("node.list", opts, {}));
172+} catch {
173+return null;
174+}
175+}
176+177+function sanitizePairedNodeForListJson(node: PairedNodeListRow): Omit<PairedNodeListRow, "token"> {
178+const copy: Record<string, unknown> = { ...node };
179+delete copy.token;
180+return copy as Omit<PairedNodeListRow, "token">;
181+}
182+114183export function registerNodesStatusCommands(nodes: Command) {
115184nodesCallOpts(
116185nodes
@@ -176,7 +245,6 @@ export function registerNodesStatusCommands(nodes: Command) {
176245}
177246178247const rows = filtered.map((n) => {
179-const name = n.displayName?.trim() ? n.displayName.trim() : n.nodeId;
180248const perms = formatPermissions(n.permissions);
181249const versions = formatNodeVersions(n);
182250const pathEnv = formatPathEnv(n.pathEnv);
@@ -188,9 +256,11 @@ export function registerNodesStatusCommands(nodes: Command) {
188256perms ? `perms: ${perms}` : null,
189257versions,
190258pathEnv ? `path: ${pathEnv}` : null,
191-].filter(Boolean) as string[];
259+]
260+.filter(Boolean)
261+.map((part) => sanitizeTerminalText(String(part)));
192262const caps = Array.isArray(n.caps)
193- ? n.caps.map(String).filter(Boolean).toSorted().join(", ")
263+ ? sanitizeTerminalText(n.caps.map(String).filter(Boolean).toSorted().join(", "))
194264 : "?";
195265const paired = n.paired ? ok("paired") : warn("unpaired");
196266const connected = n.connected ? ok("connected") : muted("disconnected");
@@ -200,9 +270,9 @@ export function registerNodesStatusCommands(nodes: Command) {
200270 : "";
201271202272return {
203-Node: name,
204-ID: n.nodeId,
205-IP: n.remoteIp ?? "",
273+Node: formatNodeTerminalLabel(n),
274+ID: sanitizeTerminalText(n.nodeId),
275+IP: sanitizeTerminalText(n.remoteIp ?? ""),
206276Detail: detailParts.join(" · "),
207277Status: `${paired} · ${connected}${since}`,
208278Caps: caps,
@@ -275,17 +345,17 @@ export function registerNodesStatusCommands(nodes: Command) {
275345 }`;
276346const tableWidth = getTerminalTableWidth();
277347const rows = [
278-{ Field: "ID", Value: nodeId },
279-displayName ? { Field: "Name", Value: displayName } : null,
280-client ? { Field: "Client", Value: client } : null,
281-ip ? { Field: "IP", Value: ip } : null,
282-family ? { Field: "Device", Value: family } : null,
283-model ? { Field: "Model", Value: model } : null,
284-perms ? { Field: "Perms", Value: perms } : null,
285-versions ? { Field: "Version", Value: versions } : null,
286-pathEnv ? { Field: "PATH", Value: pathEnv } : null,
348+{ Field: "ID", Value: sanitizeTerminalText(nodeId) },
349+displayName ? { Field: "Name", Value: sanitizeTerminalText(displayName) } : null,
350+client ? { Field: "Client", Value: sanitizeTerminalText(client) } : null,
351+ip ? { Field: "IP", Value: sanitizeTerminalText(ip) } : null,
352+family ? { Field: "Device", Value: sanitizeTerminalText(family) } : null,
353+model ? { Field: "Model", Value: sanitizeTerminalText(model) } : null,
354+perms ? { Field: "Perms", Value: sanitizeTerminalText(perms) } : null,
355+versions ? { Field: "Version", Value: sanitizeTerminalText(versions) } : null,
356+pathEnv ? { Field: "PATH", Value: sanitizeTerminalText(pathEnv) } : null,
287357{ Field: "Status", Value: status },
288-{ Field: "Caps", Value: caps ? caps.join(", ") : "?" },
358+{ Field: "Caps", Value: caps ? sanitizeTerminalText(caps.join(", ")) : "?" },
289359].filter(Boolean) as Array<{ Field: string; Value: string }>;
290360291361defaultRuntime.log(heading("Node"));
@@ -329,28 +399,22 @@ export function registerNodesStatusCommands(nodes: Command) {
329399const now = Date.now();
330400const hasFilters = connectedOnly || sinceMs !== undefined;
331401const pendingRows = hasFilters ? [] : pending;
332-const connectedById = hasFilters
333- ? new Map(
334-parseNodeList(await callGatewayCli("node.list", opts, {})).map((node) => [
335-node.nodeId,
336-node,
337-]),
338-)
339- : null;
340-const filteredPaired = paired.filter((node) => {
402+const effectiveNodes = hasFilters
403+ ? parseNodeList(await callGatewayCli("node.list", opts, {}))
404+ : await tryReadNodeList(opts);
405+const effectivePairedRows = mergePairedNodesWithEffectiveNodes(paired, effectiveNodes);
406+const filteredPaired = effectivePairedRows.filter((node) => {
341407if (connectedOnly) {
342-const live = connectedById?.get(node.nodeId);
343-if (!live?.connected) {
408+if (!node.connected) {
344409return false;
345410}
346411}
347412if (sinceMs !== undefined) {
348-const live = connectedById?.get(node.nodeId);
349413const lastConnectedAtMs =
350414typeof node.lastConnectedAtMs === "number"
351415 ? node.lastConnectedAtMs
352- : typeof live?.connectedAtMs === "number"
353- ? live.connectedAtMs
416+ : typeof node.connectedAtMs === "number"
417+ ? node.connectedAtMs
354418 : undefined;
355419if (typeof lastConnectedAtMs !== "number") {
356420return false;
@@ -368,7 +432,10 @@ export function registerNodesStatusCommands(nodes: Command) {
368432);
369433370434if (opts.json) {
371-defaultRuntime.writeJson({ pending: pendingRows, paired: filteredPaired });
435+defaultRuntime.writeJson({
436+pending: pendingRows,
437+paired: filteredPaired.map(sanitizePairedNodeForListJson),
438+});
372439return;
373440}
374441@@ -385,18 +452,17 @@ export function registerNodesStatusCommands(nodes: Command) {
385452}
386453387454if (filteredPaired.length > 0) {
388-const pairedRows = filteredPaired.map((n) => {
389-const live = connectedById?.get(n.nodeId);
455+const pairedTableRows = filteredPaired.map((n) => {
390456const lastConnectedAtMs =
391457typeof n.lastConnectedAtMs === "number"
392458 ? n.lastConnectedAtMs
393- : typeof live?.connectedAtMs === "number"
394- ? live.connectedAtMs
459+ : typeof n.connectedAtMs === "number"
460+ ? n.connectedAtMs
395461 : undefined;
396462return {
397-Node: n.displayName?.trim() ? n.displayName.trim() : n.nodeId,
398-Id: n.nodeId,
399-IP: n.remoteIp ?? "",
463+Node: formatNodeTerminalLabel(n),
464+Id: sanitizeTerminalText(n.nodeId),
465+IP: sanitizeTerminalText(n.remoteIp ?? ""),
400466LastConnect:
401467typeof lastConnectedAtMs === "number"
402468 ? formatTimeAgo(Math.max(0, now - lastConnectedAtMs))
@@ -414,7 +480,7 @@ export function registerNodesStatusCommands(nodes: Command) {
414480{ key: "IP", header: "IP", minWidth: 10 },
415481{ key: "LastConnect", header: "Last Connect", minWidth: 14 },
416482],
417-rows: pairedRows,
483+rows: pairedTableRows,
418484}).trimEnd(),
419485);
420486}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。