fix(cli): keep nodes list aligned with nodes status (#72619)

* fix(cli): keep nodes list aligned with nodes status

* fix(clownfish): address review for ghcrawl-156588-autonomous-smoke (1)

* fix(cli): keep nodes list aligned with nodes status
This commit is contained in:
Vincent Koc
2026-04-27 02:39:33 -07:00
committed by GitHub
parent af03f9248d
commit a50edbdc60
4 changed files with 295 additions and 47 deletions

View File

@@ -1,4 +1,5 @@
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { sanitizeTerminalText } from "../../terminal/safe-text.js";
import { renderTable } from "../../terminal/table.js";
import type { PendingRequest } from "./types.js";
@@ -13,13 +14,16 @@ export function renderPendingPairingRequestsTable(params: {
};
}) {
const { pending, now, tableWidth, theme } = params;
const rows = pending.map((r) => ({
Request: r.requestId,
Node: r.displayName?.trim() ? r.displayName.trim() : r.nodeId,
IP: r.remoteIp ?? "",
Requested:
typeof r.ts === "number" ? formatTimeAgo(Math.max(0, now - r.ts)) : theme.muted("unknown"),
}));
const rows = pending.map((r) => {
const nodeLabel = r.displayName?.trim() ? r.displayName.trim() : r.nodeId;
return {
Request: sanitizeTerminalText(r.requestId),
Node: sanitizeTerminalText(nodeLabel),
IP: sanitizeTerminalText(r.remoteIp ?? ""),
Requested:
typeof r.ts === "number" ? formatTimeAgo(Math.max(0, now - r.ts)) : theme.muted("unknown"),
};
});
return {
heading: theme.heading("Pending"),
table: renderTable({

View File

@@ -7,6 +7,7 @@ import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js";
import { sanitizeTerminalText } from "../../terminal/safe-text.js";
import { getTerminalTableWidth, renderTable } from "../../terminal/table.js";
import { shortenHomeInString } from "../../utils.js";
import { parseDurationMs } from "../parse-duration.js";
@@ -14,7 +15,9 @@ import { getNodesTheme, runNodesCommand } from "./cli-utils.js";
import { formatPermissions, parseNodeList, parsePairingList } from "./format.js";
import { renderPendingPairingRequestsTable } from "./pairing-render.js";
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js";
import type { NodeListNode, NodesRpcOpts, PairedNode } from "./types.js";
type PairedNodeListRow = PairedNode & Partial<NodeListNode>;
function formatVersionLabel(raw: string) {
const trimmed = raw.trim();
@@ -88,6 +91,11 @@ function formatClientLabel(node: { clientId?: string; clientMode?: string }): st
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 parseSinceMs(raw: unknown, label: string): number | undefined {
if (raw === undefined || raw === null) {
return undefined;
@@ -111,6 +119,67 @@ function parseSinceMs(raw: unknown, label: string): number | undefined {
}
}
function mergePairedNodeWithEffectiveNode(
paired: PairedNode | undefined,
effective: NodeListNode,
): PairedNodeListRow {
return {
...paired,
...effective,
token: paired?.token,
createdAtMs: paired?.createdAtMs,
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 callGatewayCli("node.list", opts, {}));
} catch {
return null;
}
}
function sanitizePairedNodeForListJson(node: PairedNodeListRow): Omit<PairedNodeListRow, "token"> {
const copy: Record<string, unknown> = { ...node };
delete copy.token;
return copy as Omit<PairedNodeListRow, "token">;
}
export function registerNodesStatusCommands(nodes: Command) {
nodesCallOpts(
nodes
@@ -176,7 +245,6 @@ export function registerNodesStatusCommands(nodes: Command) {
}
const rows = filtered.map((n) => {
const name = n.displayName?.trim() ? n.displayName.trim() : n.nodeId;
const perms = formatPermissions(n.permissions);
const versions = formatNodeVersions(n);
const pathEnv = formatPathEnv(n.pathEnv);
@@ -188,9 +256,11 @@ export function registerNodesStatusCommands(nodes: Command) {
perms ? `perms: ${perms}` : null,
versions,
pathEnv ? `path: ${pathEnv}` : null,
].filter(Boolean) as string[];
]
.filter(Boolean)
.map((part) => sanitizeTerminalText(String(part)));
const caps = Array.isArray(n.caps)
? n.caps.map(String).filter(Boolean).toSorted().join(", ")
? 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");
@@ -200,9 +270,9 @@ export function registerNodesStatusCommands(nodes: Command) {
: "";
return {
Node: name,
ID: n.nodeId,
IP: n.remoteIp ?? "",
Node: formatNodeTerminalLabel(n),
ID: sanitizeTerminalText(n.nodeId),
IP: sanitizeTerminalText(n.remoteIp ?? ""),
Detail: detailParts.join(" · "),
Status: `${paired} · ${connected}${since}`,
Caps: caps,
@@ -275,17 +345,17 @@ export function registerNodesStatusCommands(nodes: Command) {
}`;
const tableWidth = getTerminalTableWidth();
const rows = [
{ Field: "ID", Value: nodeId },
displayName ? { Field: "Name", Value: displayName } : null,
client ? { Field: "Client", Value: client } : null,
ip ? { Field: "IP", Value: ip } : null,
family ? { Field: "Device", Value: family } : null,
model ? { Field: "Model", Value: model } : null,
perms ? { Field: "Perms", Value: perms } : null,
versions ? { Field: "Version", Value: versions } : null,
pathEnv ? { Field: "PATH", Value: pathEnv } : null,
{ 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,
pathEnv ? { Field: "PATH", Value: sanitizeTerminalText(pathEnv) } : null,
{ Field: "Status", Value: status },
{ Field: "Caps", Value: caps ? caps.join(", ") : "?" },
{ Field: "Caps", Value: caps ? sanitizeTerminalText(caps.join(", ")) : "?" },
].filter(Boolean) as Array<{ Field: string; Value: string }>;
defaultRuntime.log(heading("Node"));
@@ -329,28 +399,22 @@ export function registerNodesStatusCommands(nodes: Command) {
const now = Date.now();
const hasFilters = connectedOnly || sinceMs !== undefined;
const pendingRows = hasFilters ? [] : pending;
const connectedById = hasFilters
? new Map(
parseNodeList(await callGatewayCli("node.list", opts, {})).map((node) => [
node.nodeId,
node,
]),
)
: null;
const filteredPaired = paired.filter((node) => {
const effectiveNodes = hasFilters
? parseNodeList(await callGatewayCli("node.list", opts, {}))
: await tryReadNodeList(opts);
const effectivePairedRows = mergePairedNodesWithEffectiveNodes(paired, effectiveNodes);
const filteredPaired = effectivePairedRows.filter((node) => {
if (connectedOnly) {
const live = connectedById?.get(node.nodeId);
if (!live?.connected) {
if (!node.connected) {
return false;
}
}
if (sinceMs !== undefined) {
const live = connectedById?.get(node.nodeId);
const lastConnectedAtMs =
typeof node.lastConnectedAtMs === "number"
? node.lastConnectedAtMs
: typeof live?.connectedAtMs === "number"
? live.connectedAtMs
: typeof node.connectedAtMs === "number"
? node.connectedAtMs
: undefined;
if (typeof lastConnectedAtMs !== "number") {
return false;
@@ -368,7 +432,10 @@ export function registerNodesStatusCommands(nodes: Command) {
);
if (opts.json) {
defaultRuntime.writeJson({ pending: pendingRows, paired: filteredPaired });
defaultRuntime.writeJson({
pending: pendingRows,
paired: filteredPaired.map(sanitizePairedNodeForListJson),
});
return;
}
@@ -385,18 +452,17 @@ export function registerNodesStatusCommands(nodes: Command) {
}
if (filteredPaired.length > 0) {
const pairedRows = filteredPaired.map((n) => {
const live = connectedById?.get(n.nodeId);
const pairedTableRows = filteredPaired.map((n) => {
const lastConnectedAtMs =
typeof n.lastConnectedAtMs === "number"
? n.lastConnectedAtMs
: typeof live?.connectedAtMs === "number"
? live.connectedAtMs
: typeof n.connectedAtMs === "number"
? n.connectedAtMs
: undefined;
return {
Node: n.displayName?.trim() ? n.displayName.trim() : n.nodeId,
Id: n.nodeId,
IP: n.remoteIp ?? "",
Node: formatNodeTerminalLabel(n),
Id: sanitizeTerminalText(n.nodeId),
IP: sanitizeTerminalText(n.remoteIp ?? ""),
LastConnect:
typeof lastConnectedAtMs === "number"
? formatTimeAgo(Math.max(0, now - lastConnectedAtMs))
@@ -414,7 +480,7 @@ export function registerNodesStatusCommands(nodes: Command) {
{ key: "IP", header: "IP", minWidth: 10 },
{ key: "LastConnect", header: "Last Connect", minWidth: 14 },
],
rows: pairedRows,
rows: pairedTableRows,
}).trimEnd(),
);
}