refactor: share status overview and json helpers

This commit is contained in:
Peter Steinberger
2026-04-06 13:22:01 +01:00
parent ad2df63547
commit 143f501fe5
16 changed files with 1145 additions and 361 deletions

View File

@@ -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<ReturnType<typeof resolveStatusServiceSummaries>>;
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 {

View File

@@ -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,
},
});
});
});

View File

@@ -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<Parameters<typeof resolveStatusJsonOutput>[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,
}),
);
}

View File

@@ -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: [],
}),

View File

@@ -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<OpenClawConfig["update"]>["channel"];
export function buildStatusJsonPayload(params: {
summary: Record<string, unknown>;
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<Record<string, unknown>> | 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 } : {}),

View File

@@ -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,
}),
);

View File

@@ -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,

View File

@@ -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,
});
}

View File

@@ -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" },
]),
);
});
});

View File

@@ -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<string, unknown>>;
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",
});
}

View File

@@ -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",
});
});
});

View File

@@ -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<OpenClawConfig, "update" | "gateway">;
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,
});
}

View File

@@ -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 },

View File

@@ -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<string, unknown>;
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<Record<string, string>>;
}) => 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,
}),
};
}

View File

@@ -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,

View File

@@ -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<string, unknown>).length,
),
listPotentialConfiguredChannelIds: (cfg: { channels?: Record<string, unknown> }) =>
Object.keys(cfg.channels ?? {}).filter((key) => key !== "defaults" && key !== "modelByChannel"),
}));