feat: add node core/ui versions in bridge

This commit is contained in:
Peter Steinberger
2026-01-18 15:59:39 +00:00
parent 633e0d9382
commit 1721d04405
11 changed files with 123 additions and 2 deletions

View File

@@ -4,6 +4,9 @@ export type NodeListNode = {
nodeId: string;
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
remoteIp?: string;
deviceFamily?: string;
modelIdentifier?: string;
@@ -20,6 +23,8 @@ type PendingRequest = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
remoteIp?: string;
isRepair?: boolean;
ts: number;
@@ -31,6 +36,8 @@ type PairedNode = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
remoteIp?: string;
permissions?: Record<string, boolean>;
createdAtMs?: number;

View File

@@ -4,6 +4,43 @@ import { formatAge, formatPermissions, parseNodeList, parsePairingList } from ".
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
import type { NodesRpcOpts } from "./types.js";
function formatVersionLabel(raw: string) {
const trimmed = raw.trim();
if (!trimmed) return raw;
if (trimmed.toLowerCase().startsWith("v")) return trimmed;
return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed;
}
function resolveNodeVersions(node: {
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
}) {
const core = node.coreVersion?.trim() || undefined;
const ui = node.uiVersion?.trim() || undefined;
if (core || ui) return { core, ui };
const legacy = node.version?.trim();
if (!legacy) return { core: undefined, ui: undefined };
const platform = node.platform?.trim().toLowerCase() ?? "";
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;
}
export function registerNodesStatusCommands(nodes: Command) {
nodesCallOpts(
nodes
@@ -29,6 +66,8 @@ export function registerNodesStatusCommands(nodes: Command) {
const hw = n.modelIdentifier ? ` · hw: ${n.modelIdentifier}` : "";
const perms = formatPermissions(n.permissions);
const permsText = perms ? ` · perms: ${perms}` : "";
const versions = formatNodeVersions(n);
const versionText = versions ? ` · ${versions}` : "";
const caps =
Array.isArray(n.caps) && n.caps.length > 0
? `[${n.caps.map(String).filter(Boolean).sort().join(",")}]`
@@ -37,7 +76,7 @@ export function registerNodesStatusCommands(nodes: Command) {
: "?";
const pairing = n.paired ? "paired" : "unpaired";
defaultRuntime.log(
`- ${name} · ${n.nodeId}${ip}${device}${hw}${permsText} · ${pairing} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`,
`- ${name} · ${n.nodeId}${ip}${device}${hw}${permsText}${versionText} · ${pairing} · ${n.connected ? "connected" : "disconnected"} · caps: ${caps}`,
);
}
} catch (err) {
@@ -77,12 +116,19 @@ export function registerNodesStatusCommands(nodes: Command) {
const family = typeof obj.deviceFamily === "string" ? obj.deviceFamily : null;
const model = typeof obj.modelIdentifier === "string" ? obj.modelIdentifier : null;
const ip = typeof obj.remoteIp === "string" ? obj.remoteIp : null;
const versions = formatNodeVersions(obj as {
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
});
const parts: string[] = ["Node:", displayName, nodeId];
if (ip) parts.push(ip);
if (family) parts.push(`device: ${family}`);
if (model) parts.push(`hw: ${model}`);
if (perms) parts.push(`perms: ${perms}`);
if (versions) parts.push(versions);
parts.push(connected ? "connected" : "disconnected");
parts.push(`caps: ${caps ? `[${caps.join(",")}]` : "?"}`);
defaultRuntime.log(parts.join(" · "));

View File

@@ -46,6 +46,8 @@ export type NodeListNode = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
remoteIp?: string;
deviceFamily?: string;
modelIdentifier?: string;
@@ -62,6 +64,8 @@ export type PendingRequest = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
remoteIp?: string;
isRepair?: boolean;
ts: number;
@@ -73,6 +77,8 @@ export type PairedNode = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
remoteIp?: string;
permissions?: Record<string, boolean>;
createdAtMs?: number;

View File

@@ -8,6 +8,8 @@ export const NodePairRequestParamsSchema = Type.Object(
displayName: Type.Optional(NonEmptyString),
platform: Type.Optional(NonEmptyString),
version: Type.Optional(NonEmptyString),
coreVersion: Type.Optional(NonEmptyString),
uiVersion: Type.Optional(NonEmptyString),
deviceFamily: Type.Optional(NonEmptyString),
modelIdentifier: Type.Optional(NonEmptyString),
caps: Type.Optional(Type.Array(NonEmptyString)),

View File

@@ -42,6 +42,8 @@ export const nodeHandlers: GatewayRequestHandlers = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
caps?: string[];
@@ -55,6 +57,8 @@ export const nodeHandlers: GatewayRequestHandlers = {
displayName: p.displayName,
platform: p.platform,
version: p.version,
coreVersion: p.coreVersion,
uiVersion: p.uiVersion,
deviceFamily: p.deviceFamily,
modelIdentifier: p.modelIdentifier,
caps: p.caps,
@@ -215,6 +219,8 @@ export const nodeHandlers: GatewayRequestHandlers = {
displayName: live?.displayName ?? paired?.displayName,
platform: live?.platform ?? paired?.platform,
version: live?.version ?? paired?.version,
coreVersion: live?.coreVersion ?? paired?.coreVersion,
uiVersion: live?.uiVersion ?? paired?.uiVersion,
deviceFamily: live?.deviceFamily ?? paired?.deviceFamily,
modelIdentifier: live?.modelIdentifier ?? paired?.modelIdentifier,
remoteIp: live?.remoteIp ?? paired?.remoteIp,
@@ -275,6 +281,8 @@ export const nodeHandlers: GatewayRequestHandlers = {
displayName: live?.displayName ?? paired?.displayName,
platform: live?.platform ?? paired?.platform,
version: live?.version ?? paired?.version,
coreVersion: live?.coreVersion ?? paired?.coreVersion,
uiVersion: live?.uiVersion ?? paired?.uiVersion,
deviceFamily: live?.deviceFamily ?? paired?.deviceFamily,
modelIdentifier: live?.modelIdentifier ?? paired?.modelIdentifier,
remoteIp: live?.remoteIp ?? paired?.remoteIp,

View File

@@ -43,6 +43,25 @@ export async function startGatewayNodeBridge(params: {
}): Promise<GatewayNodeBridgeRuntime> {
const nodePresenceTimers = new Map<string, ReturnType<typeof setInterval>>();
const formatVersionLabel = (raw: string): string => {
const trimmed = raw.trim();
if (!trimmed) return raw;
if (trimmed.toLowerCase().startsWith("v")) return trimmed;
return /^\d/.test(trimmed) ? `v${trimmed}` : trimmed;
};
const resolveNodeVersionLabel = (node: {
coreVersion?: string;
uiVersion?: string;
}): string | null => {
const core = node.coreVersion?.trim();
const ui = node.uiVersion?.trim();
const parts: string[] = [];
if (core) parts.push(`core ${formatVersionLabel(core)}`);
if (ui) parts.push(`ui ${formatVersionLabel(ui)}`);
return parts.length > 0 ? parts.join(" · ") : null;
};
const stopNodePresenceTimer = (nodeId: string) => {
const timer = nodePresenceTimers.get(nodeId);
if (timer) {
@@ -57,6 +76,8 @@ export async function startGatewayNodeBridge(params: {
displayName?: string;
remoteIp?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
platform?: string;
deviceFamily?: string;
modelIdentifier?: string;
@@ -66,7 +87,7 @@ export async function startGatewayNodeBridge(params: {
const host = node.displayName?.trim() || node.nodeId;
const rawIp = node.remoteIp?.trim();
const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined;
const version = node.version?.trim() || "unknown";
const version = resolveNodeVersionLabel(node) ?? node.version?.trim() ?? "unknown";
const platform = node.platform?.trim() || undefined;
const deviceFamily = node.deviceFamily?.trim() || undefined;
const modelIdentifier = node.modelIdentifier?.trim() || undefined;

View File

@@ -168,6 +168,8 @@ export function createNodeBridgeConnectionHandler(params: {
displayName: verified.node.displayName ?? hello.displayName,
platform: verified.node.platform ?? hello.platform,
version: verified.node.version ?? hello.version,
coreVersion: verified.node.coreVersion ?? hello.coreVersion,
uiVersion: verified.node.uiVersion ?? hello.uiVersion,
deviceFamily: verified.node.deviceFamily ?? hello.deviceFamily,
modelIdentifier: verified.node.modelIdentifier ?? hello.modelIdentifier,
caps,
@@ -181,6 +183,8 @@ export function createNodeBridgeConnectionHandler(params: {
displayName: nodeInfo.displayName,
platform: nodeInfo.platform,
version: nodeInfo.version,
coreVersion: nodeInfo.coreVersion,
uiVersion: nodeInfo.uiVersion,
deviceFamily: nodeInfo.deviceFamily,
modelIdentifier: nodeInfo.modelIdentifier,
remoteIp: nodeInfo.remoteIp,
@@ -243,6 +247,8 @@ export function createNodeBridgeConnectionHandler(params: {
displayName: req.displayName,
platform: req.platform,
version: req.version,
coreVersion: req.coreVersion,
uiVersion: req.uiVersion,
deviceFamily: req.deviceFamily,
modelIdentifier: req.modelIdentifier,
caps: Array.isArray(req.caps)
@@ -286,6 +292,8 @@ export function createNodeBridgeConnectionHandler(params: {
displayName: req.displayName,
platform: req.platform,
version: req.version,
coreVersion: req.coreVersion,
uiVersion: req.uiVersion,
deviceFamily: req.deviceFamily,
modelIdentifier: req.modelIdentifier,
caps: Array.isArray(req.caps) ? req.caps.map((c) => String(c)).filter(Boolean) : undefined,

View File

@@ -9,6 +9,8 @@ export type BridgeHelloFrame = {
token?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
caps?: string[];
@@ -22,6 +24,8 @@ export type BridgePairRequestFrame = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
caps?: string[];
@@ -113,6 +117,8 @@ export type NodeBridgeClientInfo = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
remoteIp?: string;

View File

@@ -9,6 +9,8 @@ export type NodePairingPendingRequest = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
caps?: string[];
@@ -26,6 +28,8 @@ export type NodePairingPairedNode = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
caps?: string[];
@@ -186,6 +190,8 @@ export async function requestNodePairing(
displayName: req.displayName,
platform: req.platform,
version: req.version,
coreVersion: req.coreVersion,
uiVersion: req.uiVersion,
deviceFamily: req.deviceFamily,
modelIdentifier: req.modelIdentifier,
caps: req.caps,
@@ -219,6 +225,8 @@ export async function approveNodePairing(
displayName: pending.displayName,
platform: pending.platform,
version: pending.version,
coreVersion: pending.coreVersion,
uiVersion: pending.uiVersion,
deviceFamily: pending.deviceFamily,
modelIdentifier: pending.modelIdentifier,
caps: pending.caps,
@@ -278,6 +286,8 @@ export async function updatePairedNodeMetadata(
displayName: patch.displayName ?? existing.displayName,
platform: patch.platform ?? existing.platform,
version: patch.version ?? existing.version,
coreVersion: patch.coreVersion ?? existing.coreVersion,
uiVersion: patch.uiVersion ?? existing.uiVersion,
deviceFamily: patch.deviceFamily ?? existing.deviceFamily,
modelIdentifier: patch.modelIdentifier ?? existing.modelIdentifier,
remoteIp: patch.remoteIp ?? existing.remoteIp,

View File

@@ -27,6 +27,8 @@ export type BridgeClientOptions = {
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
caps?: string[];
@@ -172,6 +174,8 @@ export class BridgeClient {
displayName: this.opts.displayName,
platform: this.opts.platform,
version: this.opts.version,
coreVersion: this.opts.coreVersion,
uiVersion: this.opts.uiVersion,
deviceFamily: this.opts.deviceFamily,
modelIdentifier: this.opts.modelIdentifier,
caps: this.opts.caps,
@@ -188,6 +192,8 @@ export class BridgeClient {
displayName: this.opts.displayName,
platform: this.opts.platform,
version: this.opts.version,
coreVersion: this.opts.coreVersion,
uiVersion: this.opts.uiVersion,
deviceFamily: this.opts.deviceFamily,
modelIdentifier: this.opts.modelIdentifier,
caps: this.opts.caps,

View File

@@ -346,6 +346,7 @@ export async function runNodeHost(opts: NodeHostRunOptions): Promise<void> {
displayName,
platform: process.platform,
version: VERSION,
coreVersion: VERSION,
deviceFamily: os.platform(),
modelIdentifier: os.hostname(),
caps: ["system"],