diff --git a/src/commands/status-all/report-data.ts b/src/commands/status-all/report-data.ts index 8e9a14fd191..b2bb7215bca 100644 --- a/src/commands/status-all/report-data.ts +++ b/src/commands/status-all/report-data.ts @@ -1,14 +1,16 @@ import { canExecRequestNode } from "../../agents/exec-defaults.js"; import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js"; -import { formatCliCommand } from "../../cli/command-format.js"; import { readConfigFileSnapshot, resolveGatewayPort } from "../../config/config.js"; import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js"; import { inspectPortUsage } from "../../infra/ports.js"; import { readRestartSentinel } from "../../infra/restart-sentinel.js"; import { getRemoteSkillEligibility } from "../../infra/skills-remote.js"; import { buildPluginCompatibilityNotices } from "../../plugins/status.js"; -import { VERSION } from "../../version.js"; -import { buildStatusAllAgentsValue, buildStatusSecretsValue } from "../status-overview-values.ts"; +import { buildStatusAllOverviewRows } from "../status-overview-rows.ts"; +import { + buildStatusOverviewSurfaceFromOverview, + type StatusOverviewSurface, +} from "../status-overview-surface.ts"; import { resolveStatusGatewayHealthSafe, type resolveStatusServiceSummaries, @@ -16,7 +18,6 @@ import { import { resolveStatusAllConnectionDetails } from "../status.gateway-connection.ts"; import type { NodeOnlyGatewayInfo } from "../status.node-mode.js"; import type { StatusScanOverviewResult } from "../status.scan-overview.ts"; -import { buildStatusOverviewSurfaceRows } from "./format.js"; type StatusServiceSummaries = Awaited>; type StatusGatewayServiceSummary = StatusServiceSummaries[0]; @@ -166,46 +167,19 @@ export async function buildStatusAllReportData(params: { timeoutMs: params.timeoutMs, }); - const overviewRows = buildStatusOverviewSurfaceRows({ - cfg: params.overview.cfg, - update: params.overview.update, - tailscaleMode: params.overview.tailscaleMode, - tailscaleDns: params.overview.tailscaleDns, - tailscaleHttpsUrl: params.overview.tailscaleHttpsUrl, - tailscaleBackendState: diagnosis.tailscale.backendState, - includeBackendStateWhenOff: true, - includeBackendStateWhenOn: true, - includeDnsNameWhenOff: true, - gatewayMode: gatewaySnapshot.gatewayMode, - remoteUrlMissing: gatewaySnapshot.remoteUrlMissing, - gatewayConnection: gatewaySnapshot.gatewayConnection, - gatewayReachable: gatewaySnapshot.gatewayReachable, - gatewayProbe: gatewaySnapshot.gatewayProbe, - gatewayProbeAuth: gatewaySnapshot.gatewayProbeAuth, - gatewayProbeAuthWarning: gatewaySnapshot.gatewayProbeAuthWarning, - gatewaySelf: gatewaySnapshot.gatewaySelf, + const overviewSurface: StatusOverviewSurface = buildStatusOverviewSurfaceFromOverview({ + overview: params.overview, gatewayService: params.daemon, nodeService: params.nodeService, nodeOnlyGateway: params.nodeOnlyGateway, - prefixRows: [ - { Item: "Version", Value: VERSION }, - { Item: "OS", Value: params.overview.osSummary.label }, - { Item: "Node", Value: process.versions.node }, - { Item: "Config", Value: configPath }, - ], - middleRows: [ - { Item: "Security", Value: `Run: ${formatCliCommand("openclaw security audit --deep")}` }, - ], - agentsValue: buildStatusAllAgentsValue({ - agentStatus: params.overview.agentStatus, - }), - suffixRows: [ - { - Item: "Secrets", - Value: buildStatusSecretsValue(params.overview.secretDiagnostics.length), - }, - ], - gatewaySelfFallbackValue: "unknown", + }); + const overviewRows = buildStatusAllOverviewRows({ + surface: overviewSurface, + osLabel: params.overview.osSummary.label, + configPath, + secretDiagnosticsCount: params.overview.secretDiagnostics.length, + agentStatus: params.overview.agentStatus, + tailscaleBackendState: diagnosis.tailscale.backendState, }); return { diff --git a/src/commands/status-json-command.test.ts b/src/commands/status-json-command.test.ts new file mode 100644 index 00000000000..2dd98325e5c --- /dev/null +++ b/src/commands/status-json-command.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { runStatusJsonCommand } from "./status-json-command.ts"; + +const mocks = vi.hoisted(() => ({ + writeRuntimeJson: vi.fn(), + resolveStatusJsonOutput: vi.fn(async (input) => ({ built: true, input })), +})); + +vi.mock("../runtime.js", () => ({ + writeRuntimeJson: mocks.writeRuntimeJson, +})); + +vi.mock("./status-json-runtime.ts", () => ({ + resolveStatusJsonOutput: mocks.resolveStatusJsonOutput, +})); + +describe("runStatusJsonCommand", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("shares the fast-json scan and output flow", async () => { + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + } as never; + const scan = { + cfg: { gateway: {} }, + sourceConfig: { gateway: {} }, + summary: { ok: true }, + update: { installKind: "package", packageManager: "npm" }, + osSummary: { platform: "linux" }, + memory: null, + memoryPlugin: null, + gatewayMode: "local" as const, + gatewayConnection: { url: "ws://127.0.0.1:18789", urlSource: "config" }, + remoteUrlMissing: false, + gatewayReachable: true, + gatewayProbe: null, + gatewayProbeAuth: { token: "tok" }, + gatewaySelf: null, + gatewayProbeAuthWarning: null, + agentStatus: [], + secretDiagnostics: [], + }; + const scanStatusJsonFast = vi.fn(async () => scan); + + await runStatusJsonCommand({ + opts: { deep: true, usage: true, timeoutMs: 1234, all: true }, + runtime, + scanStatusJsonFast, + includeSecurityAudit: true, + includePluginCompatibility: true, + suppressHealthErrors: true, + }); + + expect(scanStatusJsonFast).toHaveBeenCalledWith({ timeoutMs: 1234, all: true }, runtime); + expect(mocks.resolveStatusJsonOutput).toHaveBeenCalledWith({ + scan, + opts: { deep: true, usage: true, timeoutMs: 1234, all: true }, + includeSecurityAudit: true, + includePluginCompatibility: true, + suppressHealthErrors: true, + }); + expect(mocks.writeRuntimeJson).toHaveBeenCalledWith(runtime, { + built: true, + input: { + scan, + opts: { deep: true, usage: true, timeoutMs: 1234, all: true }, + includeSecurityAudit: true, + includePluginCompatibility: true, + suppressHealthErrors: true, + }, + }); + }); +}); diff --git a/src/commands/status-json-command.ts b/src/commands/status-json-command.ts new file mode 100644 index 00000000000..a8057b28839 --- /dev/null +++ b/src/commands/status-json-command.ts @@ -0,0 +1,36 @@ +import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; +import { resolveStatusJsonOutput } from "./status-json-runtime.ts"; + +export type StatusJsonCommandOptions = { + deep?: boolean; + usage?: boolean; + timeoutMs?: number; + all?: boolean; +}; + +export async function runStatusJsonCommand(params: { + opts: StatusJsonCommandOptions; + runtime: RuntimeEnv; + includeSecurityAudit: boolean; + includePluginCompatibility?: boolean; + suppressHealthErrors?: boolean; + scanStatusJsonFast: ( + opts: { timeoutMs?: number; all?: boolean }, + runtime: RuntimeEnv, + ) => Promise[0]["scan"]>; +}) { + const scan = await params.scanStatusJsonFast( + { timeoutMs: params.opts.timeoutMs, all: params.opts.all }, + params.runtime, + ); + writeRuntimeJson( + params.runtime, + await resolveStatusJsonOutput({ + scan, + opts: params.opts, + includeSecurityAudit: params.includeSecurityAudit, + includePluginCompatibility: params.includePluginCompatibility, + suppressHealthErrors: params.suppressHealthErrors, + }), + ); +} diff --git a/src/commands/status-json-payload.test.ts b/src/commands/status-json-payload.test.ts index cfd25249ab4..9f4493c451c 100644 --- a/src/commands/status-json-payload.test.ts +++ b/src/commands/status-json-payload.test.ts @@ -59,25 +59,29 @@ describe("status-json-payload", () => { expect( buildStatusJsonPayload({ summary: { ok: true }, - updateConfigChannel: "stable", - update: { - root: "/tmp/openclaw", - installKind: "package", - packageManager: "npm", - registry: { latestVersion: "1.2.3" }, + surface: { + cfg: { update: { channel: "stable" }, gateway: {} }, + update: { + root: "/tmp/openclaw", + installKind: "package", + packageManager: "npm", + registry: { latestVersion: "1.2.3" }, + } as never, + tailscaleMode: "serve", + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewaySelf: { host: "gateway" }, + gatewayProbeAuthWarning: "warn", + gatewayService: { label: "LaunchAgent" }, + nodeService: { label: "node" }, }, osSummary: { platform: "linux" }, memory: null, memoryPlugin: { enabled: true }, - gatewayMode: "remote", - gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, - remoteUrlMissing: false, - gatewayReachable: true, - gatewayProbe: { connectLatencyMs: 42, error: null }, - gatewaySelf: { host: "gateway" }, - gatewayProbeAuthWarning: "warn", - gatewayService: { label: "LaunchAgent" }, - nodeService: { label: "node" }, agents: [{ id: "main" }], secretDiagnostics: ["diag"], securityAudit: { summary: { critical: 1 } }, @@ -143,24 +147,28 @@ describe("status-json-payload", () => { expect( buildStatusJsonPayload({ summary: { ok: true }, - updateConfigChannel: null, - update: { - root: "/tmp/openclaw", - installKind: "package", - packageManager: "npm", + surface: { + cfg: { gateway: {} }, + update: { + root: "/tmp/openclaw", + installKind: "package", + packageManager: "npm", + } as never, + tailscaleMode: "off", + gatewayMode: "local", + remoteUrlMissing: false, + gatewayConnection: { url: "ws://127.0.0.1:18789" }, + gatewayReachable: false, + gatewayProbe: null, + gatewayProbeAuth: null, + gatewaySelf: null, + gatewayProbeAuthWarning: null, + gatewayService: { label: "LaunchAgent", installed: false, loadedText: "not installed" }, + nodeService: { label: "node", installed: false, loadedText: "not installed" }, }, osSummary: { platform: "linux" }, memory: null, memoryPlugin: null, - gatewayMode: "local", - gatewayConnection: { url: "ws://127.0.0.1:18789" }, - remoteUrlMissing: false, - gatewayReachable: false, - gatewayProbe: null, - gatewaySelf: null, - gatewayProbeAuthWarning: null, - gatewayService: null, - nodeService: null, agents: [], secretDiagnostics: [], }), diff --git a/src/commands/status-json-payload.ts b/src/commands/status-json-payload.ts index f0411a159e3..f1c45b48ab7 100644 --- a/src/commands/status-json-payload.ts +++ b/src/commands/status-json-payload.ts @@ -1,47 +1,17 @@ -import type { OpenClawConfig } from "../config/types.js"; -import type { UpdateCheckResult } from "../infra/update-check.js"; +import { resolveStatusUpdateChannelInfo } from "./status-all/format.js"; import { - buildGatewayStatusJsonPayload, - resolveStatusUpdateChannelInfo, -} from "./status-all/format.js"; + buildStatusGatewayJsonPayloadFromSurface, + type StatusOverviewSurface, +} from "./status-overview-surface.ts"; export { resolveStatusUpdateChannelInfo } from "./status-all/format.js"; -type UpdateConfigChannel = NonNullable["channel"]; - export function buildStatusJsonPayload(params: { summary: Record; - updateConfigChannel?: UpdateConfigChannel | null; - update: UpdateCheckResult; + surface: StatusOverviewSurface; osSummary: unknown; memory: unknown; memoryPlugin: unknown; - gatewayMode: "local" | "remote"; - gatewayConnection: { - url: string; - urlSource?: string; - }; - remoteUrlMissing: boolean; - gatewayReachable: boolean; - gatewayProbe: - | { - connectLatencyMs?: number | null; - error?: string | null; - } - | null - | undefined; - gatewaySelf: - | { - host?: string | null; - ip?: string | null; - version?: string | null; - platform?: string | null; - } - | null - | undefined; - gatewayProbeAuthWarning?: string | null; - gatewayService: unknown; - nodeService: unknown; agents: unknown; secretDiagnostics: string[]; securityAudit?: unknown; @@ -51,28 +21,20 @@ export function buildStatusJsonPayload(params: { pluginCompatibility?: Array> | null | undefined; }) { const channelInfo = resolveStatusUpdateChannelInfo({ - updateConfigChannel: params.updateConfigChannel ?? undefined, - update: params.update, + updateConfigChannel: params.surface.cfg.update?.channel ?? undefined, + update: params.surface.update, }); return { ...params.summary, os: params.osSummary, - update: params.update, + update: params.surface.update, updateChannel: channelInfo.channel, updateChannelSource: channelInfo.source, memory: params.memory, memoryPlugin: params.memoryPlugin, - gateway: buildGatewayStatusJsonPayload({ - gatewayMode: params.gatewayMode, - gatewayConnection: params.gatewayConnection, - remoteUrlMissing: params.remoteUrlMissing, - gatewayReachable: params.gatewayReachable, - gatewayProbe: params.gatewayProbe, - gatewaySelf: params.gatewaySelf, - gatewayProbeAuthWarning: params.gatewayProbeAuthWarning, - }), - gatewayService: params.gatewayService, - nodeService: params.nodeService, + gateway: buildStatusGatewayJsonPayloadFromSurface({ surface: params.surface }), + gatewayService: params.surface.gatewayService, + nodeService: params.surface.nodeService, agents: params.agents, secretDiagnostics: params.secretDiagnostics, ...(params.securityAudit ? { securityAudit: params.securityAudit } : {}), diff --git a/src/commands/status-json-runtime.test.ts b/src/commands/status-json-runtime.test.ts index 3cb0cbc469d..037363a6f76 100644 --- a/src/commands/status-json-runtime.test.ts +++ b/src/commands/status-json-runtime.test.ts @@ -32,6 +32,7 @@ function createScan() { remoteUrlMissing: false, gatewayReachable: true, gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, gatewaySelf: { host: "gateway" }, gatewayProbeAuthWarning: null, agentStatus: { agents: [{ id: "main" }], defaultId: "main" }, @@ -80,6 +81,12 @@ describe("status-json-runtime", () => { }); expect(mocks.buildStatusJsonPayload).toHaveBeenCalledWith( expect.objectContaining({ + surface: expect.objectContaining({ + gatewayConnection: { url: "ws://127.0.0.1:18789", urlSource: "config" }, + gatewayProbeAuth: { token: "tok" }, + gatewayService: { label: "LaunchAgent" }, + nodeService: { label: "node" }, + }), securityAudit: { summary: { critical: 1 } }, usage: { providers: [] }, health: { ok: true }, @@ -126,6 +133,9 @@ describe("status-json-runtime", () => { }); expect(mocks.buildStatusJsonPayload).toHaveBeenCalledWith( expect.objectContaining({ + surface: expect.objectContaining({ + gatewayProbeAuth: { token: "tok" }, + }), securityAudit: undefined, usage: undefined, health: undefined, @@ -154,6 +164,9 @@ describe("status-json-runtime", () => { expect(mocks.buildStatusJsonPayload).toHaveBeenCalledWith( expect.objectContaining({ + surface: expect.objectContaining({ + gatewayProbeAuth: { token: "tok" }, + }), health: undefined, }), ); diff --git a/src/commands/status-json-runtime.ts b/src/commands/status-json-runtime.ts index 7240f852f46..e0552e823f7 100644 --- a/src/commands/status-json-runtime.ts +++ b/src/commands/status-json-runtime.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "../config/types.js"; import type { UpdateCheckResult } from "../infra/update-check.js"; import { buildStatusJsonPayload } from "./status-json-payload.ts"; +import { buildStatusOverviewSurfaceFromScan } from "./status-overview-surface.ts"; import { resolveStatusRuntimeSnapshot } from "./status-runtime-shared.ts"; type StatusJsonScanLike = { @@ -25,6 +26,13 @@ type StatusJsonScanLike = { } | null | undefined; + gatewayProbeAuth: + | { + token?: string; + password?: string; + } + | null + | undefined; gatewaySelf: | { host?: string | null; @@ -66,20 +74,14 @@ export async function resolveStatusJsonOutput(params: { return buildStatusJsonPayload({ summary: scan.summary, - updateConfigChannel: scan.cfg.update?.channel, - update: scan.update, + surface: buildStatusOverviewSurfaceFromScan({ + scan, + gatewayService, + nodeService, + }), osSummary: scan.osSummary, memory: scan.memory, memoryPlugin: scan.memoryPlugin, - gatewayMode: scan.gatewayMode, - gatewayConnection: scan.gatewayConnection, - remoteUrlMissing: scan.remoteUrlMissing, - gatewayReachable: scan.gatewayReachable, - gatewayProbe: scan.gatewayProbe, - gatewaySelf: scan.gatewaySelf, - gatewayProbeAuthWarning: scan.gatewayProbeAuthWarning, - gatewayService, - nodeService, agents: scan.agentStatus, secretDiagnostics: scan.secretDiagnostics, securityAudit, diff --git a/src/commands/status-json.ts b/src/commands/status-json.ts index 372eff476cb..29c23a906fb 100644 --- a/src/commands/status-json.ts +++ b/src/commands/status-json.ts @@ -1,5 +1,5 @@ -import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; -import { resolveStatusJsonOutput } from "./status-json-runtime.ts"; +import { type RuntimeEnv } from "../runtime.js"; +import { runStatusJsonCommand } from "./status-json-command.ts"; import { scanStatusJsonFast } from "./status.scan.fast-json.js"; export async function statusJsonCommand( @@ -11,14 +11,11 @@ export async function statusJsonCommand( }, runtime: RuntimeEnv, ) { - const scan = await scanStatusJsonFast({ timeoutMs: opts.timeoutMs, all: opts.all }, runtime); - writeRuntimeJson( + await runStatusJsonCommand({ + opts, runtime, - await resolveStatusJsonOutput({ - scan, - opts, - includeSecurityAudit: opts.all === true, - suppressHealthErrors: true, - }), - ); + scanStatusJsonFast, + includeSecurityAudit: opts.all === true, + suppressHealthErrors: true, + }); } diff --git a/src/commands/status-overview-rows.test.ts b/src/commands/status-overview-rows.test.ts new file mode 100644 index 00000000000..ccc6ae09c37 --- /dev/null +++ b/src/commands/status-overview-rows.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; +import { + buildStatusAllOverviewRows, + buildStatusCommandOverviewRows, +} from "./status-overview-rows.ts"; + +describe("status-overview-rows", () => { + it("builds command overview rows from the shared surface", () => { + expect( + buildStatusCommandOverviewRows({ + opts: { deep: true }, + surface: { + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { + installKind: "git", + git: { + branch: "main", + tag: "v1.2.3", + upstream: "origin/main", + behind: 2, + ahead: 0, + dirty: false, + fetchOk: true, + }, + registry: { latestVersion: "2026.4.9" }, + } as never, + tailscaleMode: "serve", + tailscaleDns: "box.tail.ts.net", + tailscaleHttpsUrl: "https://box.tail.ts.net", + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", + }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, + }, + osLabel: "macOS", + summary: { + tasks: { total: 3, active: 1, failures: 0, byStatus: { queued: 1, running: 1 } }, + taskAudit: { errors: 1, warnings: 0 }, + heartbeat: { + agents: [{ agentId: "main", enabled: true, everyMs: 60_000, every: "1m" }], + }, + queuedSystemEvents: ["one", "two"], + sessions: { + count: 2, + paths: ["store.json"], + defaults: { model: "gpt-5.4", contextTokens: 12_000 }, + }, + }, + health: { durationMs: 42 }, + lastHeartbeat: { + ts: Date.now() - 30_000, + status: "ok", + channel: "discord", + accountId: "acct", + }, + agentStatus: { + defaultId: "main", + bootstrapPendingCount: 1, + totalSessions: 2, + agents: [{ id: "main", lastActiveAgeMs: 60_000 }], + }, + memory: { files: 1, chunks: 2, vector: {}, fts: {}, cache: {} }, + memoryPlugin: { enabled: true, slot: "memory" }, + pluginCompatibility: [{ pluginId: "a", severity: "warn", message: "legacy" }], + ok: (value) => `ok(${value})`, + warn: (value) => `warn(${value})`, + muted: (value) => `muted(${value})`, + formatTimeAgo: (value) => `${value}ms`, + formatKTokens: (value) => `${Math.round(value / 1000)}k`, + resolveMemoryVectorState: () => ({ state: "ready", tone: "ok" }), + resolveMemoryFtsState: () => ({ state: "ready", tone: "warn" }), + resolveMemoryCacheSummary: () => ({ text: "cache warm", tone: "muted" }), + updateValue: "available · custom update", + }), + ).toEqual( + expect.arrayContaining([ + { Item: "OS", Value: `macOS · node ${process.versions.node}` }, + { + Item: "Memory", + Value: + "1 files · 2 chunks · plugin memory · ok(vector ready) · warn(fts ready) · muted(cache warm)", + }, + { Item: "Plugin compatibility", Value: "warn(1 notice · 1 plugin)" }, + { Item: "Sessions", Value: "2 active · default gpt-5.4 (12k ctx) · store.json" }, + ]), + ); + }); + + it("builds status-all overview rows from the shared surface", () => { + expect( + buildStatusAllOverviewRows({ + surface: { + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { + installKind: "git", + git: { branch: "main", tag: "v1.2.3", upstream: "origin/main" }, + } as never, + tailscaleMode: "off", + tailscaleDns: "box.tail.ts.net", + tailscaleHttpsUrl: null, + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", + }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, + }, + osLabel: "macOS", + configPath: "/tmp/openclaw.json", + secretDiagnosticsCount: 2, + agentStatus: { + bootstrapPendingCount: 1, + totalSessions: 2, + agents: [{ id: "main", lastActiveAgeMs: 60_000 }], + }, + tailscaleBackendState: "Running", + }), + ).toEqual( + expect.arrayContaining([ + { Item: "Version", Value: expect.any(String) }, + { Item: "OS", Value: "macOS" }, + { Item: "Config", Value: "/tmp/openclaw.json" }, + { Item: "Security", Value: "Run: openclaw security audit --deep" }, + { Item: "Secrets", Value: "2 diagnostics" }, + ]), + ); + }); +}); diff --git a/src/commands/status-overview-rows.ts b/src/commands/status-overview-rows.ts new file mode 100644 index 00000000000..3c74e63f076 --- /dev/null +++ b/src/commands/status-overview-rows.ts @@ -0,0 +1,208 @@ +import { formatCliCommand } from "../cli/command-format.js"; +import { VERSION } from "../version.js"; +import { + buildStatusOverviewRowsFromSurface, + type StatusOverviewSurface, +} from "./status-overview-surface.ts"; +import { + buildStatusAllAgentsValue, + buildStatusEventsValue, + buildStatusPluginCompatibilityValue, + buildStatusProbesValue, + buildStatusSecretsValue, + buildStatusSessionsOverviewValue, +} from "./status-overview-values.ts"; +import { + buildStatusAgentsValue, + buildStatusHeartbeatValue, + buildStatusLastHeartbeatValue, + buildStatusMemoryValue, + buildStatusTasksValue, +} from "./status.command-sections.js"; + +export function buildStatusCommandOverviewRows(params: { + opts: { + deep?: boolean; + }; + surface: StatusOverviewSurface; + osLabel: string; + summary: { + tasks: { + total: number; + active: number; + failures: number; + byStatus: { queued: number; running: number }; + }; + taskAudit: { + errors: number; + warnings: number; + }; + heartbeat: { + agents: Array<{ + agentId: string; + enabled?: boolean | null; + everyMs?: number | null; + every: string; + }>; + }; + queuedSystemEvents: string[]; + sessions: { + count: number; + paths: string[]; + defaults: { + model?: string | null; + contextTokens?: number | null; + }; + }; + }; + health?: unknown; + lastHeartbeat: unknown; + agentStatus: { + defaultId?: string | null; + bootstrapPendingCount: number; + totalSessions: number; + agents: Array<{ + id: string; + lastActiveAgeMs?: number | null; + }>; + }; + memory: { + files: number; + chunks: number; + dirty?: boolean; + sources?: string[]; + vector?: unknown; + fts?: unknown; + cache?: unknown; + } | null; + memoryPlugin: { + enabled: boolean; + reason?: string | null; + slot?: string | null; + }; + pluginCompatibility: Array<{ severity?: "warn" | "info" | null } & Record>; + ok: (value: string) => string; + warn: (value: string) => string; + muted: (value: string) => string; + formatTimeAgo: (ageMs: number) => string; + formatKTokens: (value: number) => string; + resolveMemoryVectorState: (value: unknown) => { state: string; tone: "ok" | "warn" | "muted" }; + resolveMemoryFtsState: (value: unknown) => { state: string; tone: "ok" | "warn" | "muted" }; + resolveMemoryCacheSummary: (value: unknown) => { text: string; tone: "ok" | "warn" | "muted" }; + updateValue?: string; +}) { + const agentsValue = buildStatusAgentsValue({ + agentStatus: params.agentStatus, + formatTimeAgo: params.formatTimeAgo, + }); + const eventsValue = buildStatusEventsValue({ + queuedSystemEvents: params.summary.queuedSystemEvents, + }); + const tasksValue = buildStatusTasksValue({ + summary: params.summary, + warn: params.warn, + muted: params.muted, + }); + const probesValue = buildStatusProbesValue({ + health: params.health, + ok: params.ok, + muted: params.muted, + }); + const heartbeatValue = buildStatusHeartbeatValue({ summary: params.summary }); + const lastHeartbeatValue = buildStatusLastHeartbeatValue({ + deep: params.opts.deep, + gatewayReachable: params.surface.gatewayReachable, + lastHeartbeat: params.lastHeartbeat as never, + warn: params.warn, + muted: params.muted, + formatTimeAgo: params.formatTimeAgo, + }); + const memoryValue = buildStatusMemoryValue({ + memory: params.memory, + memoryPlugin: params.memoryPlugin, + ok: params.ok, + warn: params.warn, + muted: params.muted, + resolveMemoryVectorState: params.resolveMemoryVectorState, + resolveMemoryFtsState: params.resolveMemoryFtsState, + resolveMemoryCacheSummary: params.resolveMemoryCacheSummary, + }); + const pluginCompatibilityValue = buildStatusPluginCompatibilityValue({ + notices: params.pluginCompatibility, + ok: params.ok, + warn: params.warn, + }); + + return buildStatusOverviewRowsFromSurface({ + surface: params.surface, + decorateOk: params.ok, + decorateWarn: params.warn, + decorateTailscaleOff: params.muted, + decorateTailscaleWarn: params.warn, + prefixRows: [{ Item: "OS", Value: `${params.osLabel} · node ${process.versions.node}` }], + updateValue: params.updateValue, + agentsValue, + suffixRows: [ + { Item: "Memory", Value: memoryValue }, + { Item: "Plugin compatibility", Value: pluginCompatibilityValue }, + { Item: "Probes", Value: probesValue }, + { Item: "Events", Value: eventsValue }, + { Item: "Tasks", Value: tasksValue }, + { Item: "Heartbeat", Value: heartbeatValue }, + ...(lastHeartbeatValue ? [{ Item: "Last heartbeat", Value: lastHeartbeatValue }] : []), + { + Item: "Sessions", + Value: buildStatusSessionsOverviewValue({ + sessions: params.summary.sessions, + formatKTokens: params.formatKTokens, + }), + }, + ], + gatewayAuthWarningValue: params.surface.gatewayProbeAuthWarning + ? params.warn(params.surface.gatewayProbeAuthWarning) + : null, + }); +} + +export function buildStatusAllOverviewRows(params: { + surface: StatusOverviewSurface; + osLabel: string; + configPath: string; + secretDiagnosticsCount: number; + agentStatus: { + bootstrapPendingCount: number; + totalSessions: number; + agents: Array<{ + id: string; + lastActiveAgeMs?: number | null; + }>; + }; + tailscaleBackendState?: string | null; +}) { + return buildStatusOverviewRowsFromSurface({ + surface: params.surface, + tailscaleBackendState: params.tailscaleBackendState, + includeBackendStateWhenOff: true, + includeBackendStateWhenOn: true, + includeDnsNameWhenOff: true, + prefixRows: [ + { Item: "Version", Value: VERSION }, + { Item: "OS", Value: params.osLabel }, + { Item: "Node", Value: process.versions.node }, + { Item: "Config", Value: params.configPath }, + ], + middleRows: [ + { Item: "Security", Value: `Run: ${formatCliCommand("openclaw security audit --deep")}` }, + ], + agentsValue: buildStatusAllAgentsValue({ + agentStatus: params.agentStatus, + }), + suffixRows: [ + { + Item: "Secrets", + Value: buildStatusSecretsValue(params.secretDiagnosticsCount), + }, + ], + gatewaySelfFallbackValue: "unknown", + }); +} diff --git a/src/commands/status-overview-surface.test.ts b/src/commands/status-overview-surface.test.ts new file mode 100644 index 00000000000..44075c2a263 --- /dev/null +++ b/src/commands/status-overview-surface.test.ts @@ -0,0 +1,266 @@ +import { describe, expect, it } from "vitest"; +import { + buildStatusGatewayJsonPayloadFromSurface, + buildStatusOverviewRowsFromSurface, + buildStatusOverviewSurfaceFromOverview, + buildStatusOverviewSurfaceFromScan, +} from "./status-overview-surface.ts"; + +describe("status-overview-surface", () => { + it("builds the shared overview surface from a status scan result", () => { + expect( + buildStatusOverviewSurfaceFromScan({ + scan: { + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { installKind: "git", git: { branch: "main", tag: "v1.2.3" } } as never, + tailscaleMode: "serve", + tailscaleDns: "box.tail.ts.net", + tailscaleHttpsUrl: "https://box.tail.ts.net", + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + }, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", + }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, + }), + ).toEqual({ + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { installKind: "git", git: { branch: "main", tag: "v1.2.3" } }, + tailscaleMode: "serve", + tailscaleDns: "box.tail.ts.net", + tailscaleHttpsUrl: "https://box.tail.ts.net", + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", + }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, + }); + }); + + it("builds the shared overview surface from scan overview data", () => { + expect( + buildStatusOverviewSurfaceFromOverview({ + overview: { + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { installKind: "git", git: { branch: "main", tag: "v1.2.3" } } as never, + tailscaleMode: "serve", + tailscaleDns: "box.tail.ts.net", + tailscaleHttpsUrl: "https://box.tail.ts.net", + gatewaySnapshot: { + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + }, + } as never, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", + }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, + }), + ).toEqual({ + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { installKind: "git", git: { branch: "main", tag: "v1.2.3" } }, + tailscaleMode: "serve", + tailscaleDns: "box.tail.ts.net", + tailscaleHttpsUrl: "https://box.tail.ts.net", + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", + }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, + }); + }); + + it("builds overview rows from the shared surface bundle", () => { + expect( + buildStatusOverviewRowsFromSurface({ + surface: { + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { + installKind: "git", + git: { + branch: "main", + tag: "v1.2.3", + upstream: "origin/main", + behind: 2, + ahead: 0, + dirty: false, + fetchOk: true, + }, + registry: { latestVersion: "2026.4.9" }, + } as never, + tailscaleMode: "off", + tailscaleDns: "box.tail.ts.net", + tailscaleHttpsUrl: null, + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { + url: "wss://gateway.example.com", + urlSource: "config", + }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", + }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, + }, + prefixRows: [{ Item: "OS", Value: "macOS · node 22" }], + suffixRows: [{ Item: "Secrets", Value: "none" }], + agentsValue: "2 total", + updateValue: "available · custom update", + gatewayAuthWarningValue: "warn(warn-text)", + gatewaySelfFallbackValue: "gateway-self", + includeBackendStateWhenOff: true, + includeDnsNameWhenOff: true, + decorateOk: (value) => `ok(${value})`, + decorateWarn: (value) => `warn(${value})`, + decorateTailscaleOff: (value) => `muted(${value})`, + }), + ).toEqual([ + { Item: "OS", Value: "macOS · node 22" }, + { Item: "Dashboard", Value: "http://127.0.0.1:18789/" }, + { Item: "Tailscale", Value: "muted(off · box.tail.ts.net)" }, + { Item: "Channel", Value: "stable (config)" }, + { Item: "Git", Value: "main · tag v1.2.3" }, + { Item: "Update", Value: "available · custom update" }, + { + Item: "Gateway", + Value: + "remote · wss://gateway.example.com (config) · ok(reachable 42ms) · auth token · gateway app 1.2.3", + }, + { Item: "Gateway auth warning", Value: "warn(warn-text)" }, + { Item: "Gateway self", Value: "gateway-self" }, + { Item: "Gateway service", Value: "LaunchAgent installed · loaded · running" }, + { Item: "Node service", Value: "node loaded · running (pid 42)" }, + { Item: "Agents", Value: "2 total" }, + { Item: "Secrets", Value: "none" }, + ]); + }); + + it("builds the shared gateway json payload from the overview surface", () => { + expect( + buildStatusGatewayJsonPayloadFromSurface({ + surface: { + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { installKind: "package", packageManager: "npm" } as never, + tailscaleMode: "serve", + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { + url: "wss://gateway.example.com", + urlSource: "config", + }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 42, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", + }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, + }, + }), + ).toEqual({ + mode: "remote", + url: "wss://gateway.example.com", + urlSource: "config", + misconfigured: false, + reachable: true, + connectLatencyMs: 42, + self: { host: "gateway", version: "1.2.3" }, + error: null, + authWarning: "warn-text", + }); + }); +}); diff --git a/src/commands/status-overview-surface.ts b/src/commands/status-overview-surface.ts new file mode 100644 index 00000000000..67c7aa2f327 --- /dev/null +++ b/src/commands/status-overview-surface.ts @@ -0,0 +1,212 @@ +import type { OpenClawConfig } from "../config/types.js"; +import type { UpdateCheckResult } from "../infra/update-check.js"; +import { + buildGatewayStatusJsonPayload, + buildStatusOverviewSurfaceRows, + type StatusOverviewRow, +} from "./status-all/format.js"; +import type { NodeOnlyGatewayInfo } from "./status.node-mode.js"; +import type { StatusScanOverviewResult } from "./status.scan-overview.ts"; +import type { StatusScanResult } from "./status.scan-result.ts"; + +type StatusGatewayConnection = { + url: string; + urlSource?: string; +}; + +type StatusGatewayProbe = { + connectLatencyMs?: number | null; + error?: string | null; +} | null; + +type StatusGatewayAuth = { + token?: string; + password?: string; +} | null; + +type StatusGatewaySelf = + | { + host?: string | null; + ip?: string | null; + version?: string | null; + platform?: string | null; + } + | null + | undefined; + +type StatusServiceSummary = { + label: string; + installed: boolean | null; + managedByOpenClaw?: boolean; + loadedText: string; + runtimeShort?: string | null; + runtime?: { + status?: string | null; + pid?: number | null; + } | null; +}; + +export type StatusOverviewSurface = { + cfg: Pick; + update: UpdateCheckResult; + tailscaleMode: string; + tailscaleDns?: string | null; + tailscaleHttpsUrl?: string | null; + gatewayMode: "local" | "remote"; + remoteUrlMissing: boolean; + gatewayConnection: StatusGatewayConnection; + gatewayReachable: boolean; + gatewayProbe: StatusGatewayProbe; + gatewayProbeAuth: StatusGatewayAuth; + gatewayProbeAuthWarning?: string | null; + gatewaySelf: StatusGatewaySelf; + gatewayService: StatusServiceSummary; + nodeService: StatusServiceSummary; + nodeOnlyGateway?: NodeOnlyGatewayInfo | null; +}; + +export function buildStatusOverviewSurfaceFromScan(params: { + scan: Pick< + StatusScanResult, + | "cfg" + | "update" + | "tailscaleMode" + | "tailscaleDns" + | "tailscaleHttpsUrl" + | "gatewayMode" + | "remoteUrlMissing" + | "gatewayConnection" + | "gatewayReachable" + | "gatewayProbe" + | "gatewayProbeAuth" + | "gatewayProbeAuthWarning" + | "gatewaySelf" + >; + gatewayService: StatusServiceSummary; + nodeService: StatusServiceSummary; + nodeOnlyGateway?: NodeOnlyGatewayInfo | null; +}): StatusOverviewSurface { + return { + cfg: params.scan.cfg, + update: params.scan.update, + tailscaleMode: params.scan.tailscaleMode, + tailscaleDns: params.scan.tailscaleDns, + tailscaleHttpsUrl: params.scan.tailscaleHttpsUrl, + gatewayMode: params.scan.gatewayMode, + remoteUrlMissing: params.scan.remoteUrlMissing, + gatewayConnection: params.scan.gatewayConnection, + gatewayReachable: params.scan.gatewayReachable, + gatewayProbe: params.scan.gatewayProbe, + gatewayProbeAuth: params.scan.gatewayProbeAuth, + gatewayProbeAuthWarning: params.scan.gatewayProbeAuthWarning, + gatewaySelf: params.scan.gatewaySelf, + gatewayService: params.gatewayService, + nodeService: params.nodeService, + nodeOnlyGateway: params.nodeOnlyGateway, + }; +} + +export function buildStatusOverviewSurfaceFromOverview(params: { + overview: Pick< + StatusScanOverviewResult, + "cfg" | "update" | "tailscaleMode" | "tailscaleDns" | "tailscaleHttpsUrl" | "gatewaySnapshot" + >; + gatewayService: StatusServiceSummary; + nodeService: StatusServiceSummary; + nodeOnlyGateway?: NodeOnlyGatewayInfo | null; +}): StatusOverviewSurface { + return { + cfg: params.overview.cfg, + update: params.overview.update, + tailscaleMode: params.overview.tailscaleMode, + tailscaleDns: params.overview.tailscaleDns, + tailscaleHttpsUrl: params.overview.tailscaleHttpsUrl, + gatewayMode: params.overview.gatewaySnapshot.gatewayMode, + remoteUrlMissing: params.overview.gatewaySnapshot.remoteUrlMissing, + gatewayConnection: params.overview.gatewaySnapshot.gatewayConnection, + gatewayReachable: params.overview.gatewaySnapshot.gatewayReachable, + gatewayProbe: params.overview.gatewaySnapshot.gatewayProbe, + gatewayProbeAuth: params.overview.gatewaySnapshot.gatewayProbeAuth, + gatewayProbeAuthWarning: params.overview.gatewaySnapshot.gatewayProbeAuthWarning, + gatewaySelf: params.overview.gatewaySnapshot.gatewaySelf, + gatewayService: params.gatewayService, + nodeService: params.nodeService, + nodeOnlyGateway: params.nodeOnlyGateway, + }; +} + +export function buildStatusOverviewRowsFromSurface(params: { + surface: StatusOverviewSurface; + prefixRows?: StatusOverviewRow[]; + middleRows?: StatusOverviewRow[]; + suffixRows?: StatusOverviewRow[]; + agentsValue: string; + updateValue?: string; + gatewayAuthWarningValue?: string | null; + gatewaySelfFallbackValue?: string | null; + tailscaleBackendState?: string | null; + includeBackendStateWhenOff?: boolean; + includeBackendStateWhenOn?: boolean; + includeDnsNameWhenOff?: boolean; + decorateOk?: (value: string) => string; + decorateWarn?: (value: string) => string; + decorateTailscaleOff?: (value: string) => string; + decorateTailscaleWarn?: (value: string) => string; +}) { + return buildStatusOverviewSurfaceRows({ + cfg: params.surface.cfg, + update: params.surface.update, + tailscaleMode: params.surface.tailscaleMode, + tailscaleDns: params.surface.tailscaleDns, + tailscaleHttpsUrl: params.surface.tailscaleHttpsUrl, + tailscaleBackendState: params.tailscaleBackendState, + includeBackendStateWhenOff: params.includeBackendStateWhenOff, + includeBackendStateWhenOn: params.includeBackendStateWhenOn, + includeDnsNameWhenOff: params.includeDnsNameWhenOff, + decorateTailscaleOff: params.decorateTailscaleOff, + decorateTailscaleWarn: params.decorateTailscaleWarn, + gatewayMode: params.surface.gatewayMode, + remoteUrlMissing: params.surface.remoteUrlMissing, + gatewayConnection: params.surface.gatewayConnection, + gatewayReachable: params.surface.gatewayReachable, + gatewayProbe: params.surface.gatewayProbe, + gatewayProbeAuth: params.surface.gatewayProbeAuth, + gatewayProbeAuthWarning: params.surface.gatewayProbeAuthWarning, + gatewaySelf: params.surface.gatewaySelf, + gatewayService: params.surface.gatewayService, + nodeService: params.surface.nodeService, + nodeOnlyGateway: params.surface.nodeOnlyGateway, + decorateOk: params.decorateOk, + decorateWarn: params.decorateWarn, + prefixRows: params.prefixRows, + middleRows: params.middleRows, + suffixRows: params.suffixRows, + agentsValue: params.agentsValue, + updateValue: params.updateValue, + gatewayAuthWarningValue: params.gatewayAuthWarningValue, + gatewaySelfFallbackValue: params.gatewaySelfFallbackValue, + }); +} + +export function buildStatusGatewayJsonPayloadFromSurface(params: { + surface: Pick< + StatusOverviewSurface, + | "gatewayMode" + | "gatewayConnection" + | "remoteUrlMissing" + | "gatewayReachable" + | "gatewayProbe" + | "gatewaySelf" + | "gatewayProbeAuthWarning" + >; +}) { + return buildGatewayStatusJsonPayload({ + gatewayMode: params.surface.gatewayMode, + gatewayConnection: params.surface.gatewayConnection, + remoteUrlMissing: params.surface.remoteUrlMissing, + gatewayReachable: params.surface.gatewayReachable, + gatewayProbe: params.surface.gatewayProbe, + gatewaySelf: params.surface.gatewaySelf, + gatewayProbeAuthWarning: params.surface.gatewayProbeAuthWarning, + }); +} diff --git a/src/commands/status.command-report-data.test.ts b/src/commands/status.command-report-data.test.ts index 987ae6cf47e..bd342e07445 100644 --- a/src/commands/status.command-report-data.test.ts +++ b/src/commands/status.command-report-data.test.ts @@ -5,46 +5,48 @@ describe("buildStatusCommandReportData", () => { it("builds report inputs from shared status surfaces", async () => { const result = await buildStatusCommandReportData({ opts: { deep: true, verbose: true }, - cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, - update: { - installKind: "git", - git: { - branch: "main", - tag: "v1.2.3", - upstream: "origin/main", - behind: 2, - ahead: 0, - dirty: false, - fetchOk: true, + surface: { + cfg: { update: { channel: "stable" }, gateway: { bind: "loopback" } }, + update: { + installKind: "git", + git: { + branch: "main", + tag: "v1.2.3", + upstream: "origin/main", + behind: 2, + ahead: 0, + dirty: false, + fetchOk: true, + }, + registry: { latestVersion: "2026.4.9" }, + } as never, + tailscaleMode: "serve", + tailscaleDns: "box.tail.ts.net", + tailscaleHttpsUrl: "https://box.tail.ts.net", + gatewayMode: "remote", + remoteUrlMissing: false, + gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, + gatewayReachable: true, + gatewayProbe: { connectLatencyMs: 123, error: null }, + gatewayProbeAuth: { token: "tok" }, + gatewayProbeAuthWarning: "warn-text", + gatewaySelf: { host: "gateway", version: "1.2.3" }, + gatewayService: { + label: "LaunchAgent", + installed: true, + managedByOpenClaw: true, + loadedText: "loaded", + runtimeShort: "running", }, - registry: { latestVersion: "2026.4.9" }, + nodeService: { + label: "node", + installed: true, + loadedText: "loaded", + runtime: { status: "running", pid: 42 }, + }, + nodeOnlyGateway: null, }, osSummary: { label: "macOS" }, - tailscaleMode: "serve", - tailscaleDns: "box.tail.ts.net", - tailscaleHttpsUrl: "https://box.tail.ts.net", - gatewayMode: "remote", - remoteUrlMissing: false, - gatewayConnection: { url: "wss://gateway.example.com", urlSource: "config" }, - gatewayReachable: true, - gatewayProbe: { connectLatencyMs: 123, error: null }, - gatewayProbeAuth: { token: "tok" }, - gatewayProbeAuthWarning: "warn-text", - gatewaySelf: { host: "gateway", version: "1.2.3" }, - gatewayService: { - label: "LaunchAgent", - installed: true, - managedByOpenClaw: true, - loadedText: "loaded", - runtimeShort: "running", - }, - nodeService: { - label: "node", - installed: true, - loadedText: "loaded", - runtime: { status: "running", pid: 42 }, - }, - nodeOnlyGateway: null, summary: { tasks: { total: 3, active: 1, failures: 0, byStatus: { queued: 1, running: 1 } }, taskAudit: { errors: 1, warnings: 0 }, diff --git a/src/commands/status.command-report-data.ts b/src/commands/status.command-report-data.ts index dd79bf99052..64002c2099d 100644 --- a/src/commands/status.command-report-data.ts +++ b/src/commands/status.command-report-data.ts @@ -2,27 +2,17 @@ import { buildStatusChannelsTableRows, statusChannelsTableColumns, } from "./status-all/channels-table.js"; -import { buildStatusOverviewSurfaceRows } from "./status-all/format.js"; +import { buildStatusCommandOverviewRows } from "./status-overview-rows.ts"; +import { type StatusOverviewSurface } from "./status-overview-surface.ts"; import { - buildStatusEventsValue, - buildStatusPluginCompatibilityValue, - buildStatusProbesValue, - buildStatusSessionsOverviewValue, -} from "./status-overview-values.ts"; -import { - buildStatusAgentsValue, buildStatusFooterLines, buildStatusHealthRows, - buildStatusHeartbeatValue, - buildStatusLastHeartbeatValue, - buildStatusMemoryValue, buildStatusPairingRecoveryLines, buildStatusPluginCompatibilityLines, buildStatusSecurityAuditLines, buildStatusSessionsRows, buildStatusSystemEventsRows, buildStatusSystemEventsTrailer, - buildStatusTasksValue, statusHealthColumns, } from "./status.command-sections.js"; @@ -31,75 +21,8 @@ export async function buildStatusCommandReportData(params: { deep?: boolean; verbose?: boolean; }; - cfg: { - update?: { - channel?: string | null; - }; - gateway?: { - bind?: string; - customBindHost?: string; - controlUi?: { - enabled?: boolean; - basePath?: string; - }; - }; - }; - update: Record; + surface: StatusOverviewSurface; osSummary: { label: string }; - tailscaleMode: string; - tailscaleDns?: string | null; - tailscaleHttpsUrl?: string | null; - gatewayMode: "local" | "remote"; - remoteUrlMissing: boolean; - gatewayConnection: { - url: string; - urlSource?: string; - }; - gatewayReachable: boolean; - gatewayProbe: { - connectLatencyMs?: number | null; - error?: string | null; - close?: { - reason?: string | null; - } | null; - } | null; - gatewayProbeAuth: { - token?: string; - password?: string; - } | null; - gatewayProbeAuthWarning?: string | null; - gatewaySelf: - | { - host?: string | null; - ip?: string | null; - version?: string | null; - platform?: string | null; - } - | null - | undefined; - gatewayService: { - label: string; - installed: boolean | null; - managedByOpenClaw?: boolean; - loadedText: string; - runtimeShort?: string | null; - runtime?: { - status?: string | null; - pid?: number | null; - } | null; - }; - nodeService: { - label: string; - installed: boolean | null; - managedByOpenClaw?: boolean; - loadedText: string; - runtimeShort?: string | null; - runtime?: { - status?: string | null; - pid?: number | null; - } | null; - }; - nodeOnlyGateway: unknown; summary: { tasks: { total: number; @@ -215,6 +138,7 @@ export async function buildStatusCommandReportData(params: { resolveMemoryFtsState: (value: unknown) => { state: string; tone: "ok" | "warn" | "muted" }; resolveMemoryCacheSummary: (value: unknown) => { text: string; tone: "ok" | "warn" | "muted" }; accentDim: (value: string) => string; + updateValue?: string; theme: { heading: (value: string) => string; muted: (value: string) => string; @@ -227,93 +151,26 @@ export async function buildStatusCommandReportData(params: { rows: Array>; }) => string; }) { - const agentsValue = buildStatusAgentsValue({ - agentStatus: params.agentStatus, - formatTimeAgo: params.formatTimeAgo, - }); - const eventsValue = buildStatusEventsValue({ - queuedSystemEvents: params.summary.queuedSystemEvents, - }); - const tasksValue = buildStatusTasksValue({ + const overviewRows = buildStatusCommandOverviewRows({ + opts: params.opts, + surface: params.surface, + osLabel: params.osSummary.label, summary: params.summary, - warn: params.warn, - muted: params.muted, - }); - const probesValue = buildStatusProbesValue({ health: params.health, - ok: params.ok, - muted: params.muted, - }); - const heartbeatValue = buildStatusHeartbeatValue({ summary: params.summary }); - const lastHeartbeatValue = buildStatusLastHeartbeatValue({ - deep: params.opts.deep, - gatewayReachable: params.gatewayReachable, - lastHeartbeat: params.lastHeartbeat as never, - warn: params.warn, - muted: params.muted, - formatTimeAgo: params.formatTimeAgo, - }); - const memoryValue = buildStatusMemoryValue({ + lastHeartbeat: params.lastHeartbeat, + agentStatus: params.agentStatus, memory: params.memory, memoryPlugin: params.memoryPlugin, + pluginCompatibility: params.pluginCompatibility, ok: params.ok, warn: params.warn, muted: params.muted, + formatTimeAgo: params.formatTimeAgo, + formatKTokens: params.formatKTokens, resolveMemoryVectorState: params.resolveMemoryVectorState, resolveMemoryFtsState: params.resolveMemoryFtsState, resolveMemoryCacheSummary: params.resolveMemoryCacheSummary, - }); - const pluginCompatibilityValue = buildStatusPluginCompatibilityValue({ - notices: params.pluginCompatibility, - ok: params.ok, - warn: params.warn, - }); - - const overviewRows = buildStatusOverviewSurfaceRows({ - cfg: params.cfg, - update: params.update as never, - tailscaleMode: params.tailscaleMode, - tailscaleDns: params.tailscaleDns, - tailscaleHttpsUrl: params.tailscaleHttpsUrl, - gatewayMode: params.gatewayMode, - remoteUrlMissing: params.remoteUrlMissing, - gatewayConnection: params.gatewayConnection, - gatewayReachable: params.gatewayReachable, - gatewayProbe: params.gatewayProbe, - gatewayProbeAuth: params.gatewayProbeAuth, - gatewayProbeAuthWarning: params.gatewayProbeAuthWarning, - gatewaySelf: params.gatewaySelf, - gatewayService: params.gatewayService, - nodeService: params.nodeService, - nodeOnlyGateway: params.nodeOnlyGateway as never, - decorateOk: params.ok, - decorateWarn: params.warn, - decorateTailscaleOff: params.muted, - decorateTailscaleWarn: params.warn, - prefixRows: [ - { Item: "OS", Value: `${params.osSummary.label} · node ${process.versions.node}` }, - ], updateValue: params.updateValue, - agentsValue, - suffixRows: [ - { Item: "Memory", Value: memoryValue }, - { Item: "Plugin compatibility", Value: pluginCompatibilityValue }, - { Item: "Probes", Value: probesValue }, - { Item: "Events", Value: eventsValue }, - { Item: "Tasks", Value: tasksValue }, - { Item: "Heartbeat", Value: heartbeatValue }, - ...(lastHeartbeatValue ? [{ Item: "Last heartbeat", Value: lastHeartbeatValue }] : []), - { - Item: "Sessions", - Value: buildStatusSessionsOverviewValue({ - sessions: params.summary.sessions, - formatKTokens: params.formatKTokens, - }), - }, - ], - gatewayAuthWarningValue: params.gatewayProbeAuthWarning - ? params.warn(params.gatewayProbeAuthWarning) - : null, }); const sessionsColumns = [ @@ -389,11 +246,11 @@ export async function buildStatusCommandReportData(params: { : undefined, usageLines: params.usageLines, footerLines: buildStatusFooterLines({ - updateHint: params.formatUpdateAvailableHint(params.update), + updateHint: params.formatUpdateAvailableHint(params.surface.update), warn: params.theme.warn, formatCliCommand: params.formatCliCommand, - nodeOnlyGateway: params.nodeOnlyGateway as never, - gatewayReachable: params.gatewayReachable, + nodeOnlyGateway: params.surface.nodeOnlyGateway, + gatewayReachable: params.surface.gatewayReachable, }), }; } diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index 87c2f545da4..ceeded57695 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -1,6 +1,7 @@ import { withProgress } from "../cli/progress.js"; -import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; -import { resolveStatusJsonOutput } from "./status-json-runtime.ts"; +import { type RuntimeEnv } from "../runtime.js"; +import { runStatusJsonCommand } from "./status-json-command.ts"; +import { buildStatusOverviewSurfaceFromScan } from "./status-overview-surface.ts"; import { loadStatusProviderUsageModule, resolveStatusGatewayHealth, @@ -100,26 +101,24 @@ export async function statusCommand( return; } - const scan = opts.json - ? await loadStatusScanFastJsonModule().then(({ scanStatusJsonFast }) => - scanStatusJsonFast({ timeoutMs: opts.timeoutMs, all: opts.all }, runtime), - ) - : await loadStatusScanModule().then(({ scanStatus }) => - scanStatus({ json: false, timeoutMs: opts.timeoutMs, all: opts.all }, runtime), - ); if (opts.json) { - writeRuntimeJson( + await runStatusJsonCommand({ + opts, runtime, - await resolveStatusJsonOutput({ - scan, - opts, - includeSecurityAudit: true, - includePluginCompatibility: true, - }), - ); + includeSecurityAudit: true, + includePluginCompatibility: true, + scanStatusJsonFast: async (scanOpts, runtimeForScan) => + await loadStatusScanFastJsonModule().then(({ scanStatusJsonFast }) => + scanStatusJsonFast(scanOpts, runtimeForScan), + ), + }); return; } + const scan = await loadStatusScanModule().then(({ scanStatus }) => + scanStatus({ json: false, timeoutMs: opts.timeoutMs, all: opts.all }, runtime), + ); + const { cfg, osSummary, @@ -253,12 +252,10 @@ export async function statusCommand( formatUsageReportLines(usage), ) : undefined; - const lines = await buildStatusCommandReportLines( - await buildStatusCommandReportData({ - opts, + const overviewSurface = buildStatusOverviewSurfaceFromScan({ + scan: { cfg, update, - osSummary, tailscaleMode, tailscaleDns, tailscaleHttpsUrl, @@ -270,9 +267,16 @@ export async function statusCommand( gatewayProbeAuth, gatewayProbeAuthWarning, gatewaySelf, - gatewayService: daemon, - nodeService: nodeDaemon, - nodeOnlyGateway, + }, + gatewayService: daemon, + nodeService: nodeDaemon, + nodeOnlyGateway, + }); + const lines = await buildStatusCommandReportLines( + await buildStatusCommandReportData({ + opts, + surface: overviewSurface, + osSummary, summary, securityAudit, health, diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index 6ddba2e3707..7f6f1b93087 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -284,6 +284,10 @@ const mocks = vi.hoisted(() => ({ vi.mock("../channels/config-presence.js", () => ({ hasPotentialConfiguredChannels: mocks.hasPotentialConfiguredChannels, + hasMeaningfulChannelConfig: (entry: unknown) => + Boolean( + entry && typeof entry === "object" && Object.keys(entry as Record).length, + ), listPotentialConfiguredChannelIds: (cfg: { channels?: Record }) => Object.keys(cfg.channels ?? {}).filter((key) => key !== "defaults" && key !== "modelByChannel"), }));