mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
refactor: consolidate status runtime and overview helpers
This commit is contained in:
@@ -6,6 +6,9 @@ import type { HealthSummary } from "./health.js";
|
||||
import { healthCommand } from "./health.js";
|
||||
|
||||
const callGatewayMock = vi.fn();
|
||||
const buildGatewayConnectionDetailsMock = vi.fn(() => ({
|
||||
message: "Gateway mode: local\nGateway target: ws://127.0.0.1:18789",
|
||||
}));
|
||||
const logWebSelfIdMock = vi.fn();
|
||||
|
||||
function createRecentSessionRows(now = Date.now()) {
|
||||
@@ -17,6 +20,7 @@ function createRecentSessionRows(now = Date.now()) {
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: (...args: unknown[]) => callGatewayMock(...args),
|
||||
buildGatewayConnectionDetails: (...args: unknown[]) => buildGatewayConnectionDetailsMock(...args),
|
||||
}));
|
||||
|
||||
describe("healthCommand (coverage)", () => {
|
||||
@@ -28,6 +32,9 @@ describe("healthCommand (coverage)", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
buildGatewayConnectionDetailsMock.mockReturnValue({
|
||||
message: "Gateway mode: local\nGateway target: ws://127.0.0.1:18789",
|
||||
});
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
@@ -125,4 +132,32 @@ describe("healthCommand (coverage)", () => {
|
||||
);
|
||||
expect(logWebSelfIdMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prints gateway connection details in verbose mode", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
ts: Date.now(),
|
||||
durationMs: 5,
|
||||
channels: {},
|
||||
channelOrder: [],
|
||||
channelLabels: {},
|
||||
heartbeatSeconds: 60,
|
||||
defaultAgentId: "main",
|
||||
agents: [],
|
||||
sessions: {
|
||||
path: "/tmp/sessions.json",
|
||||
count: 0,
|
||||
recent: [],
|
||||
},
|
||||
} satisfies HealthSummary);
|
||||
|
||||
await healthCommand({ json: false, verbose: true, timeoutMs: 1000 }, runtime as never);
|
||||
|
||||
expect(runtime.log.mock.calls.slice(0, 3)).toEqual([
|
||||
["Gateway connection:"],
|
||||
[" Gateway mode: local"],
|
||||
[" Gateway target: ws://127.0.0.1:18789"],
|
||||
]);
|
||||
expect(buildGatewayConnectionDetailsMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import { normalizeAgentId } from "../routing/session-key.js";
|
||||
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
|
||||
import { styleHealthChannelLine } from "../terminal/health-style.js";
|
||||
import { isRich } from "../terminal/theme.js";
|
||||
import { logGatewayConnectionDetails } from "./status.gateway-connection.js";
|
||||
|
||||
export type ChannelAccountHealthSummary = {
|
||||
accountId: string;
|
||||
@@ -624,10 +625,11 @@ export async function healthCommand(
|
||||
const rich = isRich();
|
||||
if (opts.verbose) {
|
||||
const details = buildGatewayConnectionDetails({ config: cfg });
|
||||
runtime.log(info("Gateway connection:"));
|
||||
for (const line of details.message.split("\n")) {
|
||||
runtime.log(` ${line}`);
|
||||
}
|
||||
logGatewayConnectionDetails({
|
||||
runtime,
|
||||
info,
|
||||
message: details.message,
|
||||
});
|
||||
}
|
||||
const localAgents = resolveAgentOrder(cfg);
|
||||
const defaultAgentId = summary.defaultAgentId ?? localAgents.defaultAgentId;
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
import { canExecRequestNode } from "../agents/exec-defaults.js";
|
||||
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { withProgress } from "../cli/progress.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 type { RuntimeEnv } from "../runtime.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import {
|
||||
buildStatusGatewaySurfaceValues,
|
||||
buildStatusOverviewRows,
|
||||
buildStatusUpdateSurface,
|
||||
formatStatusDashboardValue,
|
||||
formatStatusTailscaleValue,
|
||||
} from "./status-all/format.js";
|
||||
import { buildStatusAllReportData } from "./status-all/report-data.js";
|
||||
import { buildStatusAllReportLines } from "./status-all/report-lines.js";
|
||||
import {
|
||||
resolveStatusGatewayHealthSafe,
|
||||
resolveStatusServiceSummaries,
|
||||
} from "./status-runtime-shared.ts";
|
||||
import { resolveStatusServiceSummaries } from "./status-runtime-shared.ts";
|
||||
import { resolveNodeOnlyGatewayInfo } from "./status.node-mode.js";
|
||||
import { collectStatusScanOverview } from "./status.scan-overview.ts";
|
||||
|
||||
@@ -49,37 +30,6 @@ export async function statusAllCommand(
|
||||
summarizingChannels: "Summarizing channels…",
|
||||
},
|
||||
});
|
||||
const cfg = overview.cfg;
|
||||
const secretDiagnostics = overview.secretDiagnostics;
|
||||
const osSummary = overview.osSummary;
|
||||
const snap = await readConfigFileSnapshot().catch(() => null);
|
||||
const tailscaleMode = overview.tailscaleMode;
|
||||
const tailscaleHttpsUrl = overview.tailscaleHttpsUrl;
|
||||
const update = overview.update;
|
||||
const updateSurface = buildStatusUpdateSurface({
|
||||
updateConfigChannel: cfg.update?.channel,
|
||||
update,
|
||||
});
|
||||
const channelLabel = updateSurface.channelLabel;
|
||||
const gitLabel = updateSurface.gitLabel;
|
||||
const tailscale = {
|
||||
backendState: null,
|
||||
dnsName: overview.tailscaleDns,
|
||||
ips: [] as string[],
|
||||
error: null,
|
||||
};
|
||||
const {
|
||||
gatewayConnection: connection,
|
||||
gatewayMode,
|
||||
remoteUrlMissing,
|
||||
gatewayProbeAuth: probeAuth,
|
||||
gatewayProbeAuthWarning,
|
||||
gatewayProbe,
|
||||
gatewayReachable,
|
||||
gatewaySelf,
|
||||
gatewayCallOverrides,
|
||||
} = overview.gatewaySnapshot;
|
||||
|
||||
progress.setLabel("Checking services…");
|
||||
const [daemon, nodeService] = await resolveStatusServiceSummaries();
|
||||
const nodeOnlyGateway = await resolveNodeOnlyGatewayInfo({
|
||||
@@ -87,165 +37,16 @@ export async function statusAllCommand(
|
||||
node: nodeService,
|
||||
});
|
||||
progress.tick();
|
||||
const agentStatus = overview.agentStatus;
|
||||
const channels = overview.channels;
|
||||
|
||||
const connectionDetailsForReport = (() => {
|
||||
if (nodeOnlyGateway) {
|
||||
return nodeOnlyGateway.connectionDetails;
|
||||
}
|
||||
if (!remoteUrlMissing) {
|
||||
return connection.message;
|
||||
}
|
||||
const bindMode = cfg.gateway?.bind ?? "loopback";
|
||||
const configPath = snap?.path?.trim() ? snap.path.trim() : "(unknown config path)";
|
||||
return [
|
||||
"Gateway mode: remote",
|
||||
"Gateway target: (missing gateway.remote.url)",
|
||||
`Config: ${configPath}`,
|
||||
`Bind: ${bindMode}`,
|
||||
`Local fallback (used for probes): ${connection.url}`,
|
||||
"Fix: set gateway.remote.url, or set gateway.mode=local.",
|
||||
].join("\n");
|
||||
})();
|
||||
|
||||
const health = nodeOnlyGateway
|
||||
? undefined
|
||||
: await resolveStatusGatewayHealthSafe({
|
||||
config: cfg,
|
||||
timeoutMs: Math.min(8000, opts?.timeoutMs ?? 10_000),
|
||||
gatewayReachable,
|
||||
gatewayProbeError: gatewayProbe?.error ?? null,
|
||||
callOverrides: gatewayCallOverrides,
|
||||
});
|
||||
const channelsStatus = overview.channelsStatus;
|
||||
const channelIssues = overview.channelIssues;
|
||||
|
||||
progress.setLabel("Checking local state…");
|
||||
const sentinel = await readRestartSentinel().catch(() => null);
|
||||
const lastErr = await readLastGatewayErrorLine(process.env).catch(() => null);
|
||||
const port = resolveGatewayPort(cfg);
|
||||
const portUsage = await inspectPortUsage(port).catch(() => null);
|
||||
progress.tick();
|
||||
|
||||
const defaultWorkspace =
|
||||
agentStatus.agents.find((a) => a.id === agentStatus.defaultId)?.workspaceDir ??
|
||||
agentStatus.agents[0]?.workspaceDir ??
|
||||
null;
|
||||
const skillStatus =
|
||||
defaultWorkspace != null
|
||||
? (() => {
|
||||
try {
|
||||
return buildWorkspaceSkillStatus(defaultWorkspace, {
|
||||
config: cfg,
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg,
|
||||
agentId: agentStatus.defaultId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
const pluginCompatibility = buildPluginCompatibilityNotices({ config: cfg });
|
||||
|
||||
const { dashboardUrl, gatewayValue, gatewaySelfValue, gatewayServiceValue, nodeServiceValue } =
|
||||
buildStatusGatewaySurfaceValues({
|
||||
cfg,
|
||||
gatewayMode,
|
||||
remoteUrlMissing,
|
||||
gatewayConnection: connection,
|
||||
gatewayReachable,
|
||||
gatewayProbe,
|
||||
gatewayProbeAuth: probeAuth,
|
||||
gatewaySelf,
|
||||
gatewayService: daemon,
|
||||
nodeService,
|
||||
nodeOnlyGateway,
|
||||
});
|
||||
|
||||
const aliveThresholdMs = 10 * 60_000;
|
||||
const aliveAgents = agentStatus.agents.filter(
|
||||
(a) => a.lastActiveAgeMs != null && a.lastActiveAgeMs <= aliveThresholdMs,
|
||||
).length;
|
||||
|
||||
const overviewRows = buildStatusOverviewRows({
|
||||
prefixRows: [
|
||||
{ Item: "Version", Value: VERSION },
|
||||
{ Item: "OS", Value: osSummary.label },
|
||||
{ Item: "Node", Value: process.versions.node },
|
||||
{
|
||||
Item: "Config",
|
||||
Value: snap?.path?.trim() ? snap.path.trim() : "(unknown config path)",
|
||||
},
|
||||
],
|
||||
dashboardValue: formatStatusDashboardValue(dashboardUrl),
|
||||
tailscaleValue: formatStatusTailscaleValue({
|
||||
tailscaleMode,
|
||||
dnsName: tailscale.dnsName,
|
||||
httpsUrl: tailscaleHttpsUrl,
|
||||
backendState: tailscale.backendState,
|
||||
includeBackendStateWhenOff: true,
|
||||
includeBackendStateWhenOn: true,
|
||||
includeDnsNameWhenOff: true,
|
||||
}),
|
||||
channelLabel,
|
||||
gitLabel,
|
||||
updateValue: updateSurface.updateLine,
|
||||
gatewayValue,
|
||||
gatewayAuthWarning: gatewayProbeAuthWarning,
|
||||
middleRows: [
|
||||
{ Item: "Security", Value: `Run: ${formatCliCommand("openclaw security audit --deep")}` },
|
||||
],
|
||||
gatewaySelfValue: gatewaySelfValue ?? "unknown",
|
||||
gatewayServiceValue,
|
||||
nodeServiceValue,
|
||||
agentsValue: `${agentStatus.agents.length} total · ${agentStatus.bootstrapPendingCount} bootstrapping · ${aliveAgents} active · ${agentStatus.totalSessions} sessions`,
|
||||
suffixRows: [
|
||||
{
|
||||
Item: "Secrets",
|
||||
Value:
|
||||
secretDiagnostics.length > 0
|
||||
? `${secretDiagnostics.length} diagnostic${secretDiagnostics.length === 1 ? "" : "s"}`
|
||||
: "none",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const lines = await buildStatusAllReportLines({
|
||||
progress,
|
||||
overviewRows,
|
||||
channels,
|
||||
channelIssues: channelIssues.map((issue) => ({
|
||||
channel: issue.channel,
|
||||
message: issue.message,
|
||||
})),
|
||||
agentStatus,
|
||||
connectionDetailsForReport,
|
||||
diagnosis: {
|
||||
snap,
|
||||
remoteUrlMissing,
|
||||
secretDiagnostics,
|
||||
sentinel,
|
||||
lastErr,
|
||||
port,
|
||||
portUsage,
|
||||
tailscaleMode,
|
||||
tailscale,
|
||||
tailscaleHttpsUrl,
|
||||
skillStatus,
|
||||
pluginCompatibility,
|
||||
channelsStatus,
|
||||
channelIssues,
|
||||
gatewayReachable,
|
||||
health,
|
||||
...(await buildStatusAllReportData({
|
||||
overview,
|
||||
daemon,
|
||||
nodeService,
|
||||
nodeOnlyGateway,
|
||||
},
|
||||
progress,
|
||||
timeoutMs: opts?.timeoutMs,
|
||||
})),
|
||||
});
|
||||
|
||||
progress.setLabel("Rendering…");
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildStatusGatewaySurfaceValues,
|
||||
buildStatusOverviewRows,
|
||||
buildStatusOverviewSurfaceRows,
|
||||
buildStatusUpdateSurface,
|
||||
buildGatewayStatusJsonPayload,
|
||||
buildGatewayStatusSummaryParts,
|
||||
@@ -302,4 +303,80 @@ describe("status-all format", () => {
|
||||
{ Item: "Secrets", Value: "none" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds overview surface rows from shared gateway and update inputs", () => {
|
||||
expect(
|
||||
buildStatusOverviewSurfaceRows({
|
||||
cfg: {
|
||||
update: { channel: "stable" },
|
||||
gateway: { bind: "loopback" },
|
||||
},
|
||||
update: {
|
||||
installKind: "git",
|
||||
git: {
|
||||
branch: "main",
|
||||
tag: "v1.2.3",
|
||||
upstream: "origin/main",
|
||||
dirty: false,
|
||||
behind: 2,
|
||||
ahead: 0,
|
||||
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",
|
||||
},
|
||||
nodeService: {
|
||||
label: "node",
|
||||
installed: true,
|
||||
loadedText: "loaded",
|
||||
runtime: { status: "running", pid: 42 },
|
||||
},
|
||||
prefixRows: [{ Item: "Version", Value: "1.0.0" }],
|
||||
middleRows: [{ Item: "Security", Value: "Run audit" }],
|
||||
suffixRows: [{ Item: "Secrets", Value: "none" }],
|
||||
agentsValue: "2 total",
|
||||
updateValue: "available · custom update",
|
||||
gatewayAuthWarningValue: "warn(warn-text)",
|
||||
}),
|
||||
).toEqual([
|
||||
{ Item: "Version", Value: "1.0.0" },
|
||||
{ Item: "Dashboard", Value: "http://127.0.0.1:18789/" },
|
||||
{ Item: "Tailscale", Value: "serve · box.tail.ts.net · https://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) · reachable 123ms · auth token · gateway app 1.2.3",
|
||||
},
|
||||
{ Item: "Gateway auth warning", Value: "warn(warn-text)" },
|
||||
{ Item: "Security", Value: "Run audit" },
|
||||
{ Item: "Gateway self", Value: "gateway app 1.2.3" },
|
||||
{ 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" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,23 +17,29 @@ export type StatusOverviewRow = {
|
||||
Value: string;
|
||||
};
|
||||
|
||||
type UpdateConfigChannel = NonNullable<OpenClawConfig["update"]>["channel"];
|
||||
type StatusUpdateLike = UpdateCheckResult;
|
||||
|
||||
export function resolveStatusUpdateChannelInfo(params: {
|
||||
updateConfigChannel?: UpdateConfigChannel;
|
||||
update: UpdateCheckResult;
|
||||
updateConfigChannel?: string | null;
|
||||
update: {
|
||||
installKind?: UpdateCheckResult["installKind"];
|
||||
git?: {
|
||||
tag?: string | null;
|
||||
branch?: string | null;
|
||||
} | null;
|
||||
};
|
||||
}) {
|
||||
return resolveUpdateChannelDisplay({
|
||||
configChannel: normalizeUpdateChannel(params.updateConfigChannel),
|
||||
installKind: params.update.installKind ?? null,
|
||||
installKind: params.update.installKind ?? "unknown",
|
||||
gitTag: params.update.git?.tag ?? null,
|
||||
gitBranch: params.update.git?.branch ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildStatusUpdateSurface(params: {
|
||||
updateConfigChannel?: UpdateConfigChannel;
|
||||
update: UpdateCheckResult;
|
||||
updateConfigChannel?: string | null;
|
||||
update: StatusUpdateLike;
|
||||
}) {
|
||||
const channelInfo = resolveStatusUpdateChannelInfo({
|
||||
updateConfigChannel: params.updateConfigChannel,
|
||||
@@ -176,6 +182,129 @@ export function buildStatusOverviewRows(params: {
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function buildStatusOverviewSurfaceRows(params: {
|
||||
cfg: Pick<OpenClawConfig, "update" | "gateway">;
|
||||
update: StatusUpdateLike;
|
||||
tailscaleMode: string;
|
||||
tailscaleDns?: string | null;
|
||||
tailscaleHttpsUrl?: string | null;
|
||||
tailscaleBackendState?: string | null;
|
||||
includeBackendStateWhenOff?: boolean;
|
||||
includeBackendStateWhenOn?: boolean;
|
||||
includeDnsNameWhenOff?: boolean;
|
||||
decorateTailscaleOff?: (value: string) => string;
|
||||
decorateTailscaleWarn?: (value: string) => string;
|
||||
gatewayMode: "local" | "remote";
|
||||
remoteUrlMissing: boolean;
|
||||
gatewayConnection: {
|
||||
url: string;
|
||||
urlSource?: string;
|
||||
};
|
||||
gatewayReachable: boolean;
|
||||
gatewayProbe: {
|
||||
connectLatencyMs?: number | null;
|
||||
error?: string | 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?: {
|
||||
gatewayValue: string;
|
||||
} | null;
|
||||
decorateOk?: (value: string) => string;
|
||||
decorateWarn?: (value: string) => string;
|
||||
prefixRows?: StatusOverviewRow[];
|
||||
middleRows?: StatusOverviewRow[];
|
||||
suffixRows?: StatusOverviewRow[];
|
||||
agentsValue: string;
|
||||
updateValue?: string;
|
||||
gatewayAuthWarningValue?: string | null;
|
||||
gatewaySelfFallbackValue?: string | null;
|
||||
}) {
|
||||
const updateSurface = buildStatusUpdateSurface({
|
||||
updateConfigChannel: params.cfg.update?.channel,
|
||||
update: params.update,
|
||||
});
|
||||
const { dashboardUrl, gatewayValue, gatewaySelfValue, gatewayServiceValue, nodeServiceValue } =
|
||||
buildStatusGatewaySurfaceValues({
|
||||
cfg: params.cfg,
|
||||
gatewayMode: params.gatewayMode,
|
||||
remoteUrlMissing: params.remoteUrlMissing,
|
||||
gatewayConnection: params.gatewayConnection,
|
||||
gatewayReachable: params.gatewayReachable,
|
||||
gatewayProbe: params.gatewayProbe,
|
||||
gatewayProbeAuth: params.gatewayProbeAuth,
|
||||
gatewaySelf: params.gatewaySelf,
|
||||
gatewayService: params.gatewayService,
|
||||
nodeService: params.nodeService,
|
||||
nodeOnlyGateway: params.nodeOnlyGateway,
|
||||
decorateOk: params.decorateOk,
|
||||
decorateWarn: params.decorateWarn,
|
||||
});
|
||||
return buildStatusOverviewRows({
|
||||
prefixRows: params.prefixRows,
|
||||
dashboardValue: formatStatusDashboardValue(dashboardUrl),
|
||||
tailscaleValue: formatStatusTailscaleValue({
|
||||
tailscaleMode: params.tailscaleMode,
|
||||
dnsName: params.tailscaleDns,
|
||||
httpsUrl: params.tailscaleHttpsUrl,
|
||||
backendState: params.tailscaleBackendState,
|
||||
includeBackendStateWhenOff: params.includeBackendStateWhenOff,
|
||||
includeBackendStateWhenOn: params.includeBackendStateWhenOn,
|
||||
includeDnsNameWhenOff: params.includeDnsNameWhenOff,
|
||||
decorateOff: params.decorateTailscaleOff,
|
||||
decorateWarn: params.decorateTailscaleWarn,
|
||||
}),
|
||||
channelLabel: updateSurface.channelLabel,
|
||||
gitLabel: updateSurface.gitLabel,
|
||||
updateValue: params.updateValue ?? updateSurface.updateLine,
|
||||
gatewayValue,
|
||||
gatewayAuthWarning:
|
||||
params.gatewayAuthWarningValue !== undefined
|
||||
? params.gatewayAuthWarningValue
|
||||
: params.gatewayProbeAuthWarning,
|
||||
middleRows: params.middleRows,
|
||||
gatewaySelfValue: params.gatewaySelfFallbackValue ?? gatewaySelfValue,
|
||||
gatewayServiceValue,
|
||||
nodeServiceValue,
|
||||
agentsValue: params.agentsValue,
|
||||
suffixRows: params.suffixRows,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatGatewayAuthUsed(
|
||||
auth: {
|
||||
token?: string;
|
||||
|
||||
231
src/commands/status-all/report-data.ts
Normal file
231
src/commands/status-all/report-data.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
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 {
|
||||
resolveStatusGatewayHealthSafe,
|
||||
type resolveStatusServiceSummaries,
|
||||
} from "../status-runtime-shared.ts";
|
||||
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];
|
||||
type StatusNodeServiceSummary = StatusServiceSummaries[1];
|
||||
type StatusGatewayHealthSafe = Awaited<ReturnType<typeof resolveStatusGatewayHealthSafe>>;
|
||||
|
||||
type StatusAllProgress = {
|
||||
setLabel(label: string): void;
|
||||
tick(): void;
|
||||
};
|
||||
|
||||
function resolveStatusAllConfigPath(path: string | null | undefined): string {
|
||||
const trimmed = path?.trim();
|
||||
return trimmed && trimmed.length > 0 ? trimmed : "(unknown config path)";
|
||||
}
|
||||
|
||||
async function resolveStatusAllLocalDiagnosis(params: {
|
||||
overview: StatusScanOverviewResult;
|
||||
progress: StatusAllProgress;
|
||||
gatewayReachable: boolean;
|
||||
gatewayProbe: StatusScanOverviewResult["gatewaySnapshot"]["gatewayProbe"];
|
||||
gatewayCallOverrides: StatusScanOverviewResult["gatewaySnapshot"]["gatewayCallOverrides"];
|
||||
nodeOnlyGateway: NodeOnlyGatewayInfo | null;
|
||||
timeoutMs?: number;
|
||||
}): Promise<{
|
||||
configPath: string;
|
||||
health: StatusGatewayHealthSafe | undefined;
|
||||
diagnosis: {
|
||||
snap: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
remoteUrlMissing: boolean;
|
||||
secretDiagnostics: StatusScanOverviewResult["secretDiagnostics"];
|
||||
sentinel: Awaited<ReturnType<typeof readRestartSentinel>> | null;
|
||||
lastErr: string | null;
|
||||
port: number;
|
||||
portUsage: Awaited<ReturnType<typeof inspectPortUsage>> | null;
|
||||
tailscaleMode: string;
|
||||
tailscale: {
|
||||
backendState: null;
|
||||
dnsName: string | null;
|
||||
ips: string[];
|
||||
error: null;
|
||||
};
|
||||
tailscaleHttpsUrl: string | null;
|
||||
skillStatus: ReturnType<typeof buildWorkspaceSkillStatus> | null;
|
||||
pluginCompatibility: ReturnType<typeof buildPluginCompatibilityNotices>;
|
||||
channelsStatus: StatusScanOverviewResult["channelsStatus"];
|
||||
channelIssues: StatusScanOverviewResult["channelIssues"];
|
||||
gatewayReachable: boolean;
|
||||
health: StatusGatewayHealthSafe | undefined;
|
||||
nodeOnlyGateway: NodeOnlyGatewayInfo | null;
|
||||
};
|
||||
}> {
|
||||
const { overview } = params;
|
||||
const snap = await readConfigFileSnapshot().catch(() => null);
|
||||
const configPath = resolveStatusAllConfigPath(snap?.path);
|
||||
|
||||
const health = params.nodeOnlyGateway
|
||||
? undefined
|
||||
: await resolveStatusGatewayHealthSafe({
|
||||
config: overview.cfg,
|
||||
timeoutMs: Math.min(8000, params.timeoutMs ?? 10_000),
|
||||
gatewayReachable: params.gatewayReachable,
|
||||
gatewayProbeError: params.gatewayProbe?.error ?? null,
|
||||
callOverrides: params.gatewayCallOverrides ?? {},
|
||||
});
|
||||
|
||||
params.progress.setLabel("Checking local state…");
|
||||
const sentinel = await readRestartSentinel().catch(() => null);
|
||||
const lastErr = await readLastGatewayErrorLine(process.env).catch(() => null);
|
||||
const port = resolveGatewayPort(overview.cfg);
|
||||
const portUsage = await inspectPortUsage(port).catch(() => null);
|
||||
params.progress.tick();
|
||||
|
||||
const defaultWorkspace =
|
||||
overview.agentStatus.agents.find((a) => a.id === overview.agentStatus.defaultId)
|
||||
?.workspaceDir ??
|
||||
overview.agentStatus.agents[0]?.workspaceDir ??
|
||||
null;
|
||||
const skillStatus =
|
||||
defaultWorkspace != null
|
||||
? (() => {
|
||||
try {
|
||||
return buildWorkspaceSkillStatus(defaultWorkspace, {
|
||||
config: overview.cfg,
|
||||
eligibility: {
|
||||
remote: getRemoteSkillEligibility({
|
||||
advertiseExecNode: canExecRequestNode({
|
||||
cfg: overview.cfg,
|
||||
agentId: overview.agentStatus.defaultId,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
const pluginCompatibility = buildPluginCompatibilityNotices({ config: overview.cfg });
|
||||
|
||||
return {
|
||||
configPath,
|
||||
health,
|
||||
diagnosis: {
|
||||
snap,
|
||||
remoteUrlMissing: overview.gatewaySnapshot.remoteUrlMissing,
|
||||
secretDiagnostics: overview.secretDiagnostics,
|
||||
sentinel,
|
||||
lastErr,
|
||||
port,
|
||||
portUsage,
|
||||
tailscaleMode: overview.tailscaleMode,
|
||||
tailscale: {
|
||||
backendState: null,
|
||||
dnsName: overview.tailscaleDns,
|
||||
ips: [],
|
||||
error: null,
|
||||
},
|
||||
tailscaleHttpsUrl: overview.tailscaleHttpsUrl,
|
||||
skillStatus,
|
||||
pluginCompatibility,
|
||||
channelsStatus: overview.channelsStatus,
|
||||
channelIssues: overview.channelIssues,
|
||||
gatewayReachable: params.gatewayReachable,
|
||||
health,
|
||||
nodeOnlyGateway: params.nodeOnlyGateway,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildStatusAllReportData(params: {
|
||||
overview: StatusScanOverviewResult;
|
||||
daemon: StatusGatewayServiceSummary;
|
||||
nodeService: StatusNodeServiceSummary;
|
||||
nodeOnlyGateway: NodeOnlyGatewayInfo | null;
|
||||
progress: StatusAllProgress;
|
||||
timeoutMs?: number;
|
||||
}) {
|
||||
const gatewaySnapshot = params.overview.gatewaySnapshot;
|
||||
const { configPath, health, diagnosis } = await resolveStatusAllLocalDiagnosis({
|
||||
overview: params.overview,
|
||||
progress: params.progress,
|
||||
gatewayReachable: gatewaySnapshot.gatewayReachable,
|
||||
gatewayProbe: gatewaySnapshot.gatewayProbe,
|
||||
gatewayCallOverrides: gatewaySnapshot.gatewayCallOverrides,
|
||||
nodeOnlyGateway: params.nodeOnlyGateway,
|
||||
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,
|
||||
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",
|
||||
});
|
||||
|
||||
return {
|
||||
overviewRows,
|
||||
channels: params.overview.channels,
|
||||
channelIssues: params.overview.channelIssues.map((issue) => ({
|
||||
channel: issue.channel,
|
||||
message: issue.message,
|
||||
})),
|
||||
agentStatus: params.overview.agentStatus,
|
||||
connectionDetailsForReport: resolveStatusAllConnectionDetails({
|
||||
nodeOnlyGateway: params.nodeOnlyGateway,
|
||||
remoteUrlMissing: gatewaySnapshot.remoteUrlMissing,
|
||||
gatewayConnection: gatewaySnapshot.gatewayConnection,
|
||||
bindMode: params.overview.cfg.gateway?.bind ?? "loopback",
|
||||
configPath,
|
||||
}),
|
||||
diagnosis: {
|
||||
...diagnosis,
|
||||
health,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import { resolveStatusJsonOutput } from "./status-json-runtime.ts";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
buildStatusJsonPayload: vi.fn((input) => ({ built: true, input })),
|
||||
resolveStatusSecurityAudit: vi.fn(),
|
||||
resolveStatusRuntimeDetails: vi.fn(),
|
||||
resolveStatusRuntimeSnapshot: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./status-json-payload.ts", () => ({
|
||||
@@ -12,8 +11,7 @@ vi.mock("./status-json-payload.ts", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./status-runtime-shared.ts", () => ({
|
||||
resolveStatusSecurityAudit: mocks.resolveStatusSecurityAudit,
|
||||
resolveStatusRuntimeDetails: mocks.resolveStatusRuntimeDetails,
|
||||
resolveStatusRuntimeSnapshot: mocks.resolveStatusRuntimeSnapshot,
|
||||
}));
|
||||
|
||||
function createScan() {
|
||||
@@ -52,8 +50,8 @@ function createScan() {
|
||||
describe("status-json-runtime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveStatusSecurityAudit.mockResolvedValue({ summary: { critical: 1 } });
|
||||
mocks.resolveStatusRuntimeDetails.mockResolvedValue({
|
||||
mocks.resolveStatusRuntimeSnapshot.mockResolvedValue({
|
||||
securityAudit: { summary: { critical: 1 } },
|
||||
usage: { providers: [] },
|
||||
health: { ok: true },
|
||||
lastHeartbeat: { status: "ok" },
|
||||
@@ -70,13 +68,14 @@ describe("status-json-runtime", () => {
|
||||
includePluginCompatibility: true,
|
||||
});
|
||||
|
||||
expect(mocks.resolveStatusSecurityAudit).toHaveBeenCalled();
|
||||
expect(mocks.resolveStatusRuntimeDetails).toHaveBeenCalledWith({
|
||||
expect(mocks.resolveStatusRuntimeSnapshot).toHaveBeenCalledWith({
|
||||
config: { update: { channel: "stable" }, gateway: {} },
|
||||
sourceConfig: { gateway: {} },
|
||||
timeoutMs: 1234,
|
||||
usage: true,
|
||||
deep: true,
|
||||
gatewayReachable: true,
|
||||
includeSecurityAudit: true,
|
||||
suppressHealthErrors: undefined,
|
||||
});
|
||||
expect(mocks.buildStatusJsonPayload).toHaveBeenCalledWith(
|
||||
@@ -99,7 +98,8 @@ describe("status-json-runtime", () => {
|
||||
});
|
||||
|
||||
it("skips optional sections when flags are off", async () => {
|
||||
mocks.resolveStatusRuntimeDetails.mockResolvedValueOnce({
|
||||
mocks.resolveStatusRuntimeSnapshot.mockResolvedValueOnce({
|
||||
securityAudit: undefined,
|
||||
usage: undefined,
|
||||
health: undefined,
|
||||
lastHeartbeat: null,
|
||||
@@ -114,13 +114,14 @@ describe("status-json-runtime", () => {
|
||||
includePluginCompatibility: false,
|
||||
});
|
||||
|
||||
expect(mocks.resolveStatusSecurityAudit).not.toHaveBeenCalled();
|
||||
expect(mocks.resolveStatusRuntimeDetails).toHaveBeenCalledWith({
|
||||
expect(mocks.resolveStatusRuntimeSnapshot).toHaveBeenCalledWith({
|
||||
config: { update: { channel: "stable" }, gateway: {} },
|
||||
sourceConfig: { gateway: {} },
|
||||
timeoutMs: 500,
|
||||
usage: false,
|
||||
deep: false,
|
||||
gatewayReachable: true,
|
||||
includeSecurityAudit: false,
|
||||
suppressHealthErrors: undefined,
|
||||
});
|
||||
expect(mocks.buildStatusJsonPayload).toHaveBeenCalledWith(
|
||||
@@ -135,7 +136,8 @@ describe("status-json-runtime", () => {
|
||||
});
|
||||
|
||||
it("suppresses health errors when requested", async () => {
|
||||
mocks.resolveStatusRuntimeDetails.mockResolvedValueOnce({
|
||||
mocks.resolveStatusRuntimeSnapshot.mockResolvedValueOnce({
|
||||
securityAudit: undefined,
|
||||
usage: undefined,
|
||||
health: undefined,
|
||||
lastHeartbeat: { status: "ok" },
|
||||
@@ -155,12 +157,14 @@ describe("status-json-runtime", () => {
|
||||
health: undefined,
|
||||
}),
|
||||
);
|
||||
expect(mocks.resolveStatusRuntimeDetails).toHaveBeenCalledWith({
|
||||
expect(mocks.resolveStatusRuntimeSnapshot).toHaveBeenCalledWith({
|
||||
config: { update: { channel: "stable" }, gateway: {} },
|
||||
sourceConfig: { gateway: {} },
|
||||
timeoutMs: 500,
|
||||
usage: undefined,
|
||||
deep: true,
|
||||
gatewayReachable: true,
|
||||
includeSecurityAudit: false,
|
||||
suppressHealthErrors: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +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 {
|
||||
resolveStatusRuntimeDetails,
|
||||
resolveStatusSecurityAudit,
|
||||
} from "./status-runtime-shared.ts";
|
||||
import { resolveStatusRuntimeSnapshot } from "./status-runtime-shared.ts";
|
||||
|
||||
type StatusJsonScanLike = {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -55,19 +52,15 @@ export async function resolveStatusJsonOutput(params: {
|
||||
suppressHealthErrors?: boolean;
|
||||
}) {
|
||||
const { scan, opts } = params;
|
||||
const securityAudit = params.includeSecurityAudit
|
||||
? await resolveStatusSecurityAudit({
|
||||
config: scan.cfg,
|
||||
sourceConfig: scan.sourceConfig,
|
||||
})
|
||||
: undefined;
|
||||
const { usage, health, lastHeartbeat, gatewayService, nodeService } =
|
||||
await resolveStatusRuntimeDetails({
|
||||
const { securityAudit, usage, health, lastHeartbeat, gatewayService, nodeService } =
|
||||
await resolveStatusRuntimeSnapshot({
|
||||
config: scan.cfg,
|
||||
sourceConfig: scan.sourceConfig,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
usage: opts.usage,
|
||||
deep: opts.deep,
|
||||
gatewayReachable: scan.gatewayReachable,
|
||||
includeSecurityAudit: params.includeSecurityAudit,
|
||||
suppressHealthErrors: params.suppressHealthErrors,
|
||||
});
|
||||
|
||||
|
||||
63
src/commands/status-overview-values.test.ts
Normal file
63
src/commands/status-overview-values.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildStatusAllAgentsValue,
|
||||
buildStatusEventsValue,
|
||||
buildStatusPluginCompatibilityValue,
|
||||
buildStatusProbesValue,
|
||||
buildStatusSecretsValue,
|
||||
buildStatusSessionsOverviewValue,
|
||||
countActiveStatusAgents,
|
||||
} from "./status-overview-values.ts";
|
||||
|
||||
describe("status-overview-values", () => {
|
||||
it("counts active agents and formats status-all agent value", () => {
|
||||
const agentStatus = {
|
||||
bootstrapPendingCount: 2,
|
||||
totalSessions: 3,
|
||||
agents: [
|
||||
{ id: "main", lastActiveAgeMs: 5_000 },
|
||||
{ id: "ops", lastActiveAgeMs: 11 * 60_000 },
|
||||
{ id: "idle", lastActiveAgeMs: null },
|
||||
],
|
||||
};
|
||||
|
||||
expect(countActiveStatusAgents({ agentStatus })).toBe(1);
|
||||
expect(buildStatusAllAgentsValue({ agentStatus })).toBe(
|
||||
"3 total · 2 bootstrapping · 1 active · 3 sessions",
|
||||
);
|
||||
});
|
||||
|
||||
it("formats secrets events probes and plugin compatibility values", () => {
|
||||
expect(buildStatusSecretsValue(0)).toBe("none");
|
||||
expect(buildStatusSecretsValue(1)).toBe("1 diagnostic");
|
||||
expect(buildStatusEventsValue({ queuedSystemEvents: [] })).toBe("none");
|
||||
expect(buildStatusEventsValue({ queuedSystemEvents: ["a", "b"] })).toBe("2 queued");
|
||||
expect(
|
||||
buildStatusProbesValue({
|
||||
health: undefined,
|
||||
ok: (value) => `ok(${value})`,
|
||||
muted: (value) => `muted(${value})`,
|
||||
}),
|
||||
).toBe("muted(skipped (use --deep))");
|
||||
expect(
|
||||
buildStatusPluginCompatibilityValue({
|
||||
notices: [{ pluginId: "a" }, { pluginId: "a" }, { pluginId: "b" }],
|
||||
ok: (value) => `ok(${value})`,
|
||||
warn: (value) => `warn(${value})`,
|
||||
}),
|
||||
).toBe("warn(3 notices · 2 plugins)");
|
||||
});
|
||||
|
||||
it("formats sessions overview values", () => {
|
||||
expect(
|
||||
buildStatusSessionsOverviewValue({
|
||||
sessions: {
|
||||
count: 2,
|
||||
paths: ["store.json", "other.json"],
|
||||
defaults: { model: "gpt-5.4", contextTokens: 12_000 },
|
||||
},
|
||||
formatKTokens: (value) => `${Math.round(value / 1000)}k`,
|
||||
}),
|
||||
).toBe("2 active · default gpt-5.4 (12k ctx) · 2 stores");
|
||||
});
|
||||
});
|
||||
88
src/commands/status-overview-values.ts
Normal file
88
src/commands/status-overview-values.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
type AgentStatusLike = {
|
||||
bootstrapPendingCount: number;
|
||||
totalSessions: number;
|
||||
agents: Array<{
|
||||
id: string;
|
||||
lastActiveAgeMs?: number | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
type PluginCompatibilityNoticeLike = {
|
||||
pluginId?: string | null;
|
||||
plugin?: string | null;
|
||||
};
|
||||
|
||||
type SummarySessionsLike = {
|
||||
count: number;
|
||||
paths: string[];
|
||||
defaults: {
|
||||
model?: string | null;
|
||||
contextTokens?: number | null;
|
||||
};
|
||||
};
|
||||
|
||||
export function countActiveStatusAgents(params: {
|
||||
agentStatus: AgentStatusLike;
|
||||
activeThresholdMs?: number;
|
||||
}) {
|
||||
const activeThresholdMs = params.activeThresholdMs ?? 10 * 60_000;
|
||||
return params.agentStatus.agents.filter(
|
||||
(agent) => agent.lastActiveAgeMs != null && agent.lastActiveAgeMs <= activeThresholdMs,
|
||||
).length;
|
||||
}
|
||||
|
||||
export function buildStatusAllAgentsValue(params: {
|
||||
agentStatus: AgentStatusLike;
|
||||
activeThresholdMs?: number;
|
||||
}) {
|
||||
const activeAgents = countActiveStatusAgents(params);
|
||||
return `${params.agentStatus.agents.length} total · ${params.agentStatus.bootstrapPendingCount} bootstrapping · ${activeAgents} active · ${params.agentStatus.totalSessions} sessions`;
|
||||
}
|
||||
|
||||
export function buildStatusSecretsValue(count: number) {
|
||||
return count > 0 ? `${count} diagnostic${count === 1 ? "" : "s"}` : "none";
|
||||
}
|
||||
|
||||
export function buildStatusEventsValue(params: { queuedSystemEvents: string[] }) {
|
||||
return params.queuedSystemEvents.length > 0
|
||||
? `${params.queuedSystemEvents.length} queued`
|
||||
: "none";
|
||||
}
|
||||
|
||||
export function buildStatusProbesValue(params: {
|
||||
health?: unknown;
|
||||
ok: (value: string) => string;
|
||||
muted: (value: string) => string;
|
||||
}) {
|
||||
return params.health ? params.ok("enabled") : params.muted("skipped (use --deep)");
|
||||
}
|
||||
|
||||
export function buildStatusPluginCompatibilityValue(params: {
|
||||
notices: PluginCompatibilityNoticeLike[];
|
||||
ok: (value: string) => string;
|
||||
warn: (value: string) => string;
|
||||
}) {
|
||||
if (params.notices.length === 0) {
|
||||
return params.ok("none");
|
||||
}
|
||||
const pluginCount = new Set(
|
||||
params.notices.map((notice) => String(notice.pluginId ?? notice.plugin ?? "")),
|
||||
).size;
|
||||
return params.warn(
|
||||
`${params.notices.length} notice${params.notices.length === 1 ? "" : "s"} · ${pluginCount} plugin${pluginCount === 1 ? "" : "s"}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildStatusSessionsOverviewValue(params: {
|
||||
sessions: SummarySessionsLike;
|
||||
formatKTokens: (value: number) => string;
|
||||
}) {
|
||||
const defaultCtx = params.sessions.defaults.contextTokens
|
||||
? ` (${params.formatKTokens(params.sessions.defaults.contextTokens)} ctx)`
|
||||
: "";
|
||||
const storeLabel =
|
||||
params.sessions.paths.length > 1
|
||||
? `${params.sessions.paths.length} stores`
|
||||
: (params.sessions.paths[0] ?? "unknown");
|
||||
return `${params.sessions.count} active · default ${params.sessions.defaults.model ?? "unknown"}${defaultCtx} · ${storeLabel}`;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
resolveStatusGatewayHealthSafe,
|
||||
resolveStatusLastHeartbeat,
|
||||
resolveStatusRuntimeDetails,
|
||||
resolveStatusRuntimeSnapshot,
|
||||
resolveStatusSecurityAudit,
|
||||
resolveStatusServiceSummaries,
|
||||
resolveStatusUsageSummary,
|
||||
@@ -217,4 +218,32 @@ describe("status-runtime-shared", () => {
|
||||
nodeService: { label: "node" },
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves the shared runtime snapshot with security audit and runtime details", async () => {
|
||||
await expect(
|
||||
resolveStatusRuntimeSnapshot({
|
||||
config: { gateway: {} },
|
||||
sourceConfig: { gateway: { mode: "local" } },
|
||||
timeoutMs: 1234,
|
||||
usage: true,
|
||||
deep: true,
|
||||
gatewayReachable: true,
|
||||
includeSecurityAudit: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
securityAudit: { summary: { critical: 0 }, findings: [] },
|
||||
usage: { providers: [] },
|
||||
health: { ok: true },
|
||||
lastHeartbeat: { ok: true },
|
||||
gatewayService: { label: "LaunchAgent" },
|
||||
nodeService: { label: "node" },
|
||||
});
|
||||
expect(mocks.runSecurityAudit).toHaveBeenCalledWith({
|
||||
config: { gateway: {} },
|
||||
sourceConfig: { gateway: { mode: "local" } },
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,6 +108,7 @@ type StatusGatewayHealth = Awaited<ReturnType<typeof resolveStatusGatewayHealth>
|
||||
type StatusLastHeartbeat = Awaited<ReturnType<typeof resolveStatusLastHeartbeat>>;
|
||||
type StatusGatewayServiceSummary = Awaited<ReturnType<typeof getDaemonStatusSummary>>;
|
||||
type StatusNodeServiceSummary = Awaited<ReturnType<typeof getNodeDaemonStatusSummary>>;
|
||||
type StatusSecurityAudit = Awaited<ReturnType<typeof resolveStatusSecurityAudit>>;
|
||||
|
||||
export async function resolveStatusRuntimeDetails(params: {
|
||||
config: OpenClawConfig;
|
||||
@@ -159,3 +160,51 @@ export async function resolveStatusRuntimeDetails(params: {
|
||||
nodeService: StatusNodeServiceSummary;
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveStatusRuntimeSnapshot(params: {
|
||||
config: OpenClawConfig;
|
||||
sourceConfig: OpenClawConfig;
|
||||
timeoutMs?: number;
|
||||
usage?: boolean;
|
||||
deep?: boolean;
|
||||
gatewayReachable: boolean;
|
||||
includeSecurityAudit?: boolean;
|
||||
suppressHealthErrors?: boolean;
|
||||
resolveSecurityAudit?: (input: {
|
||||
config: OpenClawConfig;
|
||||
sourceConfig: OpenClawConfig;
|
||||
}) => Promise<StatusSecurityAudit>;
|
||||
resolveUsage?: (timeoutMs?: number) => Promise<StatusUsageSummary>;
|
||||
resolveHealth?: (input: {
|
||||
config: OpenClawConfig;
|
||||
timeoutMs?: number;
|
||||
}) => Promise<StatusGatewayHealth>;
|
||||
}) {
|
||||
const securityAudit = params.includeSecurityAudit
|
||||
? await (params.resolveSecurityAudit ?? resolveStatusSecurityAudit)({
|
||||
config: params.config,
|
||||
sourceConfig: params.sourceConfig,
|
||||
})
|
||||
: undefined;
|
||||
const runtimeDetails = await resolveStatusRuntimeDetails({
|
||||
config: params.config,
|
||||
timeoutMs: params.timeoutMs,
|
||||
usage: params.usage,
|
||||
deep: params.deep,
|
||||
gatewayReachable: params.gatewayReachable,
|
||||
suppressHealthErrors: params.suppressHealthErrors,
|
||||
resolveUsage: params.resolveUsage,
|
||||
resolveHealth: params.resolveHealth,
|
||||
});
|
||||
return {
|
||||
securityAudit,
|
||||
...runtimeDetails,
|
||||
} satisfies {
|
||||
securityAudit?: StatusSecurityAudit;
|
||||
usage?: StatusUsageSummary;
|
||||
health?: StatusGatewayHealth;
|
||||
lastHeartbeat: StatusLastHeartbeat;
|
||||
gatewayService: StatusGatewayServiceSummary;
|
||||
nodeService: StatusNodeServiceSummary;
|
||||
};
|
||||
}
|
||||
|
||||
133
src/commands/status.command-report-data.test.ts
Normal file
133
src/commands/status.command-report-data.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildStatusCommandReportData } from "./status.command-report-data.ts";
|
||||
|
||||
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,
|
||||
},
|
||||
registry: { latestVersion: "2026.4.9" },
|
||||
},
|
||||
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 },
|
||||
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 },
|
||||
recent: [
|
||||
{ key: "session-key", kind: "chat", updatedAt: 1, age: 5_000, model: "gpt-5.4" },
|
||||
],
|
||||
},
|
||||
},
|
||||
securityAudit: {
|
||||
summary: { critical: 0, warn: 1, info: 0 },
|
||||
findings: [{ severity: "warn", title: "Warn first", detail: "warn detail" }],
|
||||
},
|
||||
health: { durationMs: 42 },
|
||||
usageLines: ["usage line"],
|
||||
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 }],
|
||||
},
|
||||
channels: {
|
||||
rows: [{ id: "discord", label: "Discord", enabled: true, state: "ok", detail: "ready" }],
|
||||
},
|
||||
channelIssues: [{ channel: "discord", message: "warn msg" }],
|
||||
memory: { files: 1, chunks: 2, vector: {}, fts: {}, cache: {} },
|
||||
memoryPlugin: { enabled: true, slot: "memory" },
|
||||
pluginCompatibility: [{ pluginId: "a", severity: "warn", message: "legacy" }],
|
||||
pairingRecovery: { requestId: "req-1" },
|
||||
tableWidth: 120,
|
||||
ok: (value) => `ok(${value})`,
|
||||
warn: (value) => `warn(${value})`,
|
||||
muted: (value) => `muted(${value})`,
|
||||
shortenText: (value) => value,
|
||||
formatCliCommand: (value) => `cmd:${value}`,
|
||||
formatTimeAgo: (value) => `${value}ms`,
|
||||
formatKTokens: (value) => `${Math.round(value / 1000)}k`,
|
||||
formatTokensCompact: () => "12k",
|
||||
formatPromptCacheCompact: () => "cache ok",
|
||||
formatHealthChannelLines: () => ["Discord: OK · ready"],
|
||||
formatPluginCompatibilityNotice: (notice) => String(notice.message),
|
||||
formatUpdateAvailableHint: () => "update available",
|
||||
resolveMemoryVectorState: () => ({ state: "ready", tone: "ok" }),
|
||||
resolveMemoryFtsState: () => ({ state: "ready", tone: "warn" }),
|
||||
resolveMemoryCacheSummary: () => ({ text: "cache warm", tone: "muted" }),
|
||||
accentDim: (value) => `accent(${value})`,
|
||||
theme: {
|
||||
heading: (value) => `# ${value}`,
|
||||
muted: (value) => `muted(${value})`,
|
||||
warn: (value) => `warn(${value})`,
|
||||
error: (value) => `error(${value})`,
|
||||
},
|
||||
renderTable: ({ rows }) => `table:${rows.length}`,
|
||||
updateValue: "available · custom update",
|
||||
});
|
||||
|
||||
expect(result.overviewRows[0]).toEqual({
|
||||
Item: "OS",
|
||||
Value: "macOS · node " + process.versions.node,
|
||||
});
|
||||
expect(result.taskMaintenanceHint).toBe(
|
||||
"Task maintenance: cmd:openclaw tasks maintenance --apply",
|
||||
);
|
||||
expect(result.pluginCompatibilityLines).toEqual([" warn(WARN) legacy"]);
|
||||
expect(result.pairingRecoveryLines[0]).toBe("warn(Gateway pairing approval required.)");
|
||||
expect(result.channelsRows[0]?.Channel).toBe("Discord");
|
||||
expect(result.sessionsRows[0]?.Cache).toBe("cache ok");
|
||||
expect(result.healthRows?.[0]).toEqual({
|
||||
Item: "Gateway",
|
||||
Status: "ok(reachable)",
|
||||
Detail: "42ms",
|
||||
});
|
||||
expect(result.footerLines.at(-1)).toBe(" Need to test channels? cmd:openclaw status --deep");
|
||||
});
|
||||
});
|
||||
399
src/commands/status.command-report-data.ts
Normal file
399
src/commands/status.command-report-data.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
import {
|
||||
buildStatusChannelsTableRows,
|
||||
statusChannelsTableColumns,
|
||||
} from "./status-all/channels-table.js";
|
||||
import { buildStatusOverviewSurfaceRows } from "./status-all/format.js";
|
||||
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";
|
||||
|
||||
export async function buildStatusCommandReportData(params: {
|
||||
opts: {
|
||||
deep?: boolean;
|
||||
verbose?: boolean;
|
||||
};
|
||||
cfg: {
|
||||
update?: {
|
||||
channel?: string | null;
|
||||
};
|
||||
gateway?: {
|
||||
bind?: string;
|
||||
customBindHost?: string;
|
||||
controlUi?: {
|
||||
enabled?: boolean;
|
||||
basePath?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
update: Record<string, unknown>;
|
||||
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;
|
||||
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;
|
||||
};
|
||||
recent: Array<{
|
||||
key: string;
|
||||
kind: string;
|
||||
updatedAt?: number | null;
|
||||
age: number;
|
||||
model?: string | null;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
securityAudit: {
|
||||
summary: { critical: number; warn: number; info: number };
|
||||
findings: Array<{
|
||||
severity: "critical" | "warn" | "info";
|
||||
title: string;
|
||||
detail: string;
|
||||
remediation?: string | null;
|
||||
}>;
|
||||
};
|
||||
health?: unknown;
|
||||
usageLines?: string[];
|
||||
lastHeartbeat: unknown;
|
||||
agentStatus: {
|
||||
defaultId?: string | null;
|
||||
bootstrapPendingCount: number;
|
||||
totalSessions: number;
|
||||
agents: Array<{
|
||||
id: string;
|
||||
lastActiveAgeMs?: number | null;
|
||||
}>;
|
||||
};
|
||||
channels: {
|
||||
rows: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
state: "ok" | "warn" | "off" | "setup";
|
||||
detail: string;
|
||||
}>;
|
||||
};
|
||||
channelIssues: Array<{
|
||||
channel: string;
|
||||
message: string;
|
||||
}>;
|
||||
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>>;
|
||||
pairingRecovery: { requestId: string | null } | null;
|
||||
tableWidth: number;
|
||||
ok: (value: string) => string;
|
||||
warn: (value: string) => string;
|
||||
muted: (value: string) => string;
|
||||
shortenText: (value: string, maxLen: number) => string;
|
||||
formatCliCommand: (value: string) => string;
|
||||
formatTimeAgo: (ageMs: number) => string;
|
||||
formatKTokens: (value: number) => string;
|
||||
formatTokensCompact: (value: {
|
||||
key: string;
|
||||
kind: string;
|
||||
updatedAt?: number | null;
|
||||
age: number;
|
||||
model?: string | null;
|
||||
}) => string;
|
||||
formatPromptCacheCompact: (value: {
|
||||
key: string;
|
||||
kind: string;
|
||||
updatedAt?: number | null;
|
||||
age: number;
|
||||
model?: string | null;
|
||||
}) => string | null;
|
||||
formatHealthChannelLines: (summary: unknown, opts: { accountMode: "all" }) => string[];
|
||||
formatPluginCompatibilityNotice: (notice: Record<string, unknown>) => string;
|
||||
formatUpdateAvailableHint: (update: Record<string, unknown>) => string | null;
|
||||
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" };
|
||||
accentDim: (value: string) => string;
|
||||
theme: {
|
||||
heading: (value: string) => string;
|
||||
muted: (value: string) => string;
|
||||
warn: (value: string) => string;
|
||||
error: (value: string) => string;
|
||||
};
|
||||
renderTable: (input: {
|
||||
width: number;
|
||||
columns: Array<Record<string, unknown>>;
|
||||
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({
|
||||
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({
|
||||
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,
|
||||
});
|
||||
|
||||
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 = [
|
||||
{ key: "Key", header: "Key", minWidth: 20, flex: true },
|
||||
{ key: "Kind", header: "Kind", minWidth: 6 },
|
||||
{ key: "Age", header: "Age", minWidth: 9 },
|
||||
{ key: "Model", header: "Model", minWidth: 14 },
|
||||
{ key: "Tokens", header: "Tokens", minWidth: 16 },
|
||||
...(params.opts.verbose ? [{ key: "Cache", header: "Cache", minWidth: 16, flex: true }] : []),
|
||||
];
|
||||
return {
|
||||
heading: params.theme.heading,
|
||||
muted: params.theme.muted,
|
||||
renderTable: params.renderTable,
|
||||
width: params.tableWidth,
|
||||
overviewRows,
|
||||
showTaskMaintenanceHint: params.summary.taskAudit.errors > 0,
|
||||
taskMaintenanceHint: `Task maintenance: ${params.formatCliCommand("openclaw tasks maintenance --apply")}`,
|
||||
pluginCompatibilityLines: buildStatusPluginCompatibilityLines({
|
||||
notices: params.pluginCompatibility,
|
||||
formatNotice: params.formatPluginCompatibilityNotice,
|
||||
warn: params.theme.warn,
|
||||
muted: params.theme.muted,
|
||||
}),
|
||||
pairingRecoveryLines: buildStatusPairingRecoveryLines({
|
||||
pairingRecovery: params.pairingRecovery,
|
||||
warn: params.theme.warn,
|
||||
muted: params.theme.muted,
|
||||
formatCliCommand: params.formatCliCommand,
|
||||
}),
|
||||
securityAuditLines: buildStatusSecurityAuditLines({
|
||||
securityAudit: params.securityAudit,
|
||||
theme: params.theme,
|
||||
shortenText: params.shortenText,
|
||||
formatCliCommand: params.formatCliCommand,
|
||||
}),
|
||||
channelsColumns: statusChannelsTableColumns,
|
||||
channelsRows: buildStatusChannelsTableRows({
|
||||
rows: params.channels.rows,
|
||||
channelIssues: params.channelIssues,
|
||||
ok: params.ok,
|
||||
warn: params.warn,
|
||||
muted: params.muted,
|
||||
accentDim: params.accentDim,
|
||||
formatIssueMessage: (message) => params.shortenText(message, 84),
|
||||
}),
|
||||
sessionsColumns,
|
||||
sessionsRows: buildStatusSessionsRows({
|
||||
recent: params.summary.sessions.recent,
|
||||
verbose: params.opts.verbose,
|
||||
shortenText: params.shortenText,
|
||||
formatTimeAgo: params.formatTimeAgo,
|
||||
formatTokensCompact: params.formatTokensCompact,
|
||||
formatPromptCacheCompact: params.formatPromptCacheCompact,
|
||||
muted: params.muted,
|
||||
}),
|
||||
systemEventsRows: buildStatusSystemEventsRows({
|
||||
queuedSystemEvents: params.summary.queuedSystemEvents,
|
||||
}),
|
||||
systemEventsTrailer: buildStatusSystemEventsTrailer({
|
||||
queuedSystemEvents: params.summary.queuedSystemEvents,
|
||||
muted: params.muted,
|
||||
}),
|
||||
healthColumns: params.health ? statusHealthColumns : undefined,
|
||||
healthRows: params.health
|
||||
? buildStatusHealthRows({
|
||||
health: params.health as never,
|
||||
formatHealthChannelLines: params.formatHealthChannelLines as never,
|
||||
ok: params.ok,
|
||||
warn: params.warn,
|
||||
muted: params.muted,
|
||||
})
|
||||
: undefined,
|
||||
usageLines: params.usageLines,
|
||||
footerLines: buildStatusFooterLines({
|
||||
updateHint: params.formatUpdateAvailableHint(params.update),
|
||||
warn: params.theme.warn,
|
||||
formatCliCommand: params.formatCliCommand,
|
||||
nodeOnlyGateway: params.nodeOnlyGateway as never,
|
||||
gatewayReachable: params.gatewayReachable,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
} from "./status-all/channels-table.js";
|
||||
export {
|
||||
buildStatusGatewaySurfaceValues,
|
||||
buildStatusOverviewSurfaceRows,
|
||||
buildStatusOverviewRows,
|
||||
buildStatusUpdateSurface,
|
||||
buildGatewayStatusSummaryParts,
|
||||
|
||||
@@ -4,27 +4,13 @@ import { resolveStatusJsonOutput } from "./status-json-runtime.ts";
|
||||
import {
|
||||
loadStatusProviderUsageModule,
|
||||
resolveStatusGatewayHealth,
|
||||
resolveStatusRuntimeDetails,
|
||||
resolveStatusSecurityAudit,
|
||||
resolveStatusRuntimeSnapshot,
|
||||
resolveStatusUsageSummary,
|
||||
} from "./status-runtime-shared.ts";
|
||||
import { buildStatusCommandReportData } from "./status.command-report-data.ts";
|
||||
import { buildStatusCommandReportLines } from "./status.command-report.ts";
|
||||
import {
|
||||
buildStatusAgentsValue,
|
||||
buildStatusFooterLines,
|
||||
buildStatusHealthRows,
|
||||
buildStatusHeartbeatValue,
|
||||
buildStatusLastHeartbeatValue,
|
||||
buildStatusMemoryValue,
|
||||
buildStatusPairingRecoveryLines,
|
||||
buildStatusPluginCompatibilityLines,
|
||||
buildStatusSecurityAuditLines,
|
||||
buildStatusSessionsRows,
|
||||
buildStatusSystemEventsRows,
|
||||
buildStatusSystemEventsTrailer,
|
||||
buildStatusTasksValue,
|
||||
statusHealthColumns,
|
||||
} from "./status.command-sections.ts";
|
||||
import { logGatewayConnectionDetails } from "./status.gateway-connection.ts";
|
||||
|
||||
let statusScanModulePromise: Promise<typeof import("./status.scan.js")> | undefined;
|
||||
let statusScanFastJsonModulePromise:
|
||||
@@ -34,6 +20,9 @@ let statusAllModulePromise: Promise<typeof import("./status-all.js")> | undefine
|
||||
let statusCommandTextRuntimePromise:
|
||||
| Promise<typeof import("./status.command.text-runtime.js")>
|
||||
| undefined;
|
||||
let statusGatewayConnectionRuntimePromise:
|
||||
| Promise<typeof import("./status.gateway-connection.runtime.js")>
|
||||
| undefined;
|
||||
let statusNodeModeModulePromise: Promise<typeof import("./status.node-mode.js")> | undefined;
|
||||
|
||||
function loadStatusScanModule() {
|
||||
@@ -56,6 +45,11 @@ function loadStatusCommandTextRuntime() {
|
||||
return statusCommandTextRuntimePromise;
|
||||
}
|
||||
|
||||
function loadStatusGatewayConnectionRuntime() {
|
||||
statusGatewayConnectionRuntimePromise ??= import("./status.gateway-connection.runtime.js");
|
||||
return statusGatewayConnectionRuntimePromise;
|
||||
}
|
||||
|
||||
function loadStatusNodeModeModule() {
|
||||
statusNodeModeModulePromise ??= import("./status.node-mode.js");
|
||||
return statusNodeModeModulePromise;
|
||||
@@ -126,21 +120,6 @@ export async function statusCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
const runSecurityAudit = async () =>
|
||||
await resolveStatusSecurityAudit({
|
||||
config: scan.cfg,
|
||||
sourceConfig: scan.sourceConfig,
|
||||
});
|
||||
const securityAudit = opts.json
|
||||
? await runSecurityAudit()
|
||||
: await withProgress(
|
||||
{
|
||||
label: "Running security audit…",
|
||||
indeterminate: true,
|
||||
enabled: true,
|
||||
},
|
||||
async () => await runSecurityAudit(),
|
||||
);
|
||||
const {
|
||||
cfg,
|
||||
osSummary,
|
||||
@@ -167,17 +146,29 @@ export async function statusCommand(
|
||||
} = scan;
|
||||
|
||||
const {
|
||||
securityAudit,
|
||||
usage,
|
||||
health,
|
||||
lastHeartbeat,
|
||||
gatewayService: daemon,
|
||||
nodeService: nodeDaemon,
|
||||
} = await resolveStatusRuntimeDetails({
|
||||
} = await resolveStatusRuntimeSnapshot({
|
||||
config: scan.cfg,
|
||||
sourceConfig: scan.sourceConfig,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
usage: opts.usage,
|
||||
deep: opts.deep,
|
||||
gatewayReachable,
|
||||
includeSecurityAudit: true,
|
||||
resolveSecurityAudit: async (input) =>
|
||||
await withProgress(
|
||||
{
|
||||
label: "Running security audit…",
|
||||
indeterminate: true,
|
||||
enabled: true,
|
||||
},
|
||||
async () => await resolveStatusSecurityAudit(input),
|
||||
),
|
||||
resolveUsage: async (timeoutMs) =>
|
||||
await withProgress(
|
||||
{
|
||||
@@ -200,17 +191,11 @@ export async function statusCommand(
|
||||
|
||||
const rich = true;
|
||||
const {
|
||||
buildStatusGatewaySurfaceValues,
|
||||
buildStatusChannelsTableRows,
|
||||
buildStatusOverviewRows,
|
||||
buildStatusUpdateSurface,
|
||||
formatCliCommand,
|
||||
formatStatusDashboardValue,
|
||||
formatHealthChannelLines,
|
||||
formatKTokens,
|
||||
formatPromptCacheCompact,
|
||||
formatPluginCompatibilityNotice,
|
||||
formatStatusTailscaleValue,
|
||||
formatTimeAgo,
|
||||
formatTokensCompact,
|
||||
formatUpdateAvailableHint,
|
||||
@@ -221,8 +206,6 @@ export async function statusCommand(
|
||||
resolveMemoryFtsState,
|
||||
resolveMemoryVectorState,
|
||||
shortenText,
|
||||
statusChannelsTableColumns,
|
||||
summarizePluginCompatibility,
|
||||
theme,
|
||||
} = await loadStatusCommandTextRuntime();
|
||||
const muted = (value: string) => (rich ? theme.muted(value) : value);
|
||||
@@ -234,13 +217,14 @@ export async function statusCommand(
|
||||
});
|
||||
|
||||
if (opts.verbose) {
|
||||
const { buildGatewayConnectionDetails } = await import("../gateway/call.js");
|
||||
const { buildGatewayConnectionDetails } = await loadStatusGatewayConnectionRuntime();
|
||||
const details = buildGatewayConnectionDetails({ config: scan.cfg });
|
||||
runtime.log(info("Gateway connection:"));
|
||||
for (const line of details.message.split("\n")) {
|
||||
runtime.log(` ${line}`);
|
||||
}
|
||||
runtime.log("");
|
||||
logGatewayConnectionDetails({
|
||||
runtime,
|
||||
info,
|
||||
message: details.message,
|
||||
trailingBlankLine: true,
|
||||
});
|
||||
}
|
||||
|
||||
const tableWidth = getTerminalTableWidth();
|
||||
@@ -259,199 +243,72 @@ export async function statusCommand(
|
||||
node: nodeDaemon,
|
||||
}),
|
||||
);
|
||||
const { dashboardUrl, gatewayValue, gatewayServiceValue, nodeServiceValue } =
|
||||
buildStatusGatewaySurfaceValues({
|
||||
const pairingRecovery = resolvePairingRecoveryContext({
|
||||
error: gatewayProbe?.error ?? null,
|
||||
closeReason: gatewayProbe?.close?.reason ?? null,
|
||||
});
|
||||
|
||||
const usageLines = usage
|
||||
? await loadStatusProviderUsageModule().then(({ formatUsageReportLines }) =>
|
||||
formatUsageReportLines(usage),
|
||||
)
|
||||
: undefined;
|
||||
const lines = await buildStatusCommandReportLines(
|
||||
await buildStatusCommandReportData({
|
||||
opts,
|
||||
cfg,
|
||||
update,
|
||||
osSummary,
|
||||
tailscaleMode,
|
||||
tailscaleDns,
|
||||
tailscaleHttpsUrl,
|
||||
gatewayMode,
|
||||
remoteUrlMissing,
|
||||
gatewayConnection,
|
||||
gatewayReachable,
|
||||
gatewayProbe,
|
||||
gatewayProbeAuth,
|
||||
gatewayProbeAuthWarning,
|
||||
gatewaySelf,
|
||||
gatewayService: daemon,
|
||||
nodeService: nodeDaemon,
|
||||
nodeOnlyGateway,
|
||||
decorateOk: ok,
|
||||
decorateWarn: warn,
|
||||
});
|
||||
const pairingRecovery = resolvePairingRecoveryContext({
|
||||
error: gatewayProbe?.error ?? null,
|
||||
closeReason: gatewayProbe?.close?.reason ?? null,
|
||||
});
|
||||
|
||||
const agentsValue = buildStatusAgentsValue({ agentStatus, formatTimeAgo });
|
||||
|
||||
const defaults = summary.sessions.defaults;
|
||||
const defaultCtx = defaults.contextTokens
|
||||
? ` (${formatKTokens(defaults.contextTokens)} ctx)`
|
||||
: "";
|
||||
const eventsValue =
|
||||
summary.queuedSystemEvents.length > 0 ? `${summary.queuedSystemEvents.length} queued` : "none";
|
||||
const tasksValue = buildStatusTasksValue({ summary, warn, muted });
|
||||
|
||||
const probesValue = health ? ok("enabled") : muted("skipped (use --deep)");
|
||||
|
||||
const heartbeatValue = buildStatusHeartbeatValue({ summary });
|
||||
const lastHeartbeatValue = buildStatusLastHeartbeatValue({
|
||||
deep: opts.deep,
|
||||
gatewayReachable,
|
||||
lastHeartbeat,
|
||||
warn,
|
||||
muted,
|
||||
formatTimeAgo,
|
||||
});
|
||||
|
||||
const storeLabel =
|
||||
summary.sessions.paths.length > 1
|
||||
? `${summary.sessions.paths.length} stores`
|
||||
: (summary.sessions.paths[0] ?? "unknown");
|
||||
|
||||
const memoryValue = buildStatusMemoryValue({
|
||||
memory,
|
||||
memoryPlugin,
|
||||
ok,
|
||||
warn,
|
||||
muted,
|
||||
resolveMemoryVectorState,
|
||||
resolveMemoryFtsState,
|
||||
resolveMemoryCacheSummary,
|
||||
});
|
||||
|
||||
const channelLabel = updateSurface.channelLabel;
|
||||
const gitLabel = updateSurface.gitLabel;
|
||||
const pluginCompatibilitySummary = summarizePluginCompatibility(pluginCompatibility);
|
||||
const pluginCompatibilityValue =
|
||||
pluginCompatibilitySummary.noticeCount === 0
|
||||
? ok("none")
|
||||
: warn(
|
||||
`${pluginCompatibilitySummary.noticeCount} notice${pluginCompatibilitySummary.noticeCount === 1 ? "" : "s"} · ${pluginCompatibilitySummary.pluginCount} plugin${pluginCompatibilitySummary.pluginCount === 1 ? "" : "s"}`,
|
||||
);
|
||||
|
||||
const overviewRows = buildStatusOverviewRows({
|
||||
prefixRows: [{ Item: "OS", Value: `${osSummary.label} · node ${process.versions.node}` }],
|
||||
dashboardValue: formatStatusDashboardValue(dashboardUrl),
|
||||
tailscaleValue: formatStatusTailscaleValue({
|
||||
tailscaleMode,
|
||||
dnsName: tailscaleDns,
|
||||
httpsUrl: tailscaleHttpsUrl,
|
||||
decorateOff: muted,
|
||||
decorateWarn: warn,
|
||||
}),
|
||||
channelLabel,
|
||||
gitLabel,
|
||||
updateValue: updateSurface.updateAvailable
|
||||
? warn(`available · ${updateSurface.updateLine}`)
|
||||
: updateSurface.updateLine,
|
||||
gatewayValue,
|
||||
gatewayAuthWarning: gatewayProbeAuthWarning ? warn(gatewayProbeAuthWarning) : null,
|
||||
gatewayServiceValue,
|
||||
nodeServiceValue,
|
||||
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: `${summary.sessions.count} active · default ${defaults.model ?? "unknown"}${defaultCtx} · ${storeLabel}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
const securityAuditLines = buildStatusSecurityAuditLines({
|
||||
securityAudit,
|
||||
theme,
|
||||
shortenText,
|
||||
formatCliCommand,
|
||||
});
|
||||
|
||||
const sessionsColumns = [
|
||||
{ key: "Key", header: "Key", minWidth: 20, flex: true },
|
||||
{ key: "Kind", header: "Kind", minWidth: 6 },
|
||||
{ key: "Age", header: "Age", minWidth: 9 },
|
||||
{ key: "Model", header: "Model", minWidth: 14 },
|
||||
{ key: "Tokens", header: "Tokens", minWidth: 16 },
|
||||
...(opts.verbose ? [{ key: "Cache", header: "Cache", minWidth: 16, flex: true }] : []),
|
||||
];
|
||||
const sessionsRows = buildStatusSessionsRows({
|
||||
recent: summary.sessions.recent,
|
||||
verbose: opts.verbose,
|
||||
shortenText,
|
||||
formatTimeAgo,
|
||||
formatTokensCompact,
|
||||
formatPromptCacheCompact,
|
||||
muted,
|
||||
});
|
||||
const healthRows = health
|
||||
? buildStatusHealthRows({
|
||||
health,
|
||||
formatHealthChannelLines,
|
||||
ok,
|
||||
warn,
|
||||
muted,
|
||||
})
|
||||
: undefined;
|
||||
const usageLines = usage
|
||||
? await loadStatusProviderUsageModule().then(({ formatUsageReportLines }) =>
|
||||
formatUsageReportLines(usage),
|
||||
)
|
||||
: undefined;
|
||||
const updateHint = formatUpdateAvailableHint(update);
|
||||
const lines = await buildStatusCommandReportLines({
|
||||
heading: theme.heading,
|
||||
muted: theme.muted,
|
||||
renderTable,
|
||||
width: tableWidth,
|
||||
overviewRows,
|
||||
showTaskMaintenanceHint: summary.taskAudit.errors > 0,
|
||||
taskMaintenanceHint: `Task maintenance: ${formatCliCommand("openclaw tasks maintenance --apply")}`,
|
||||
pluginCompatibilityLines: buildStatusPluginCompatibilityLines({
|
||||
notices: pluginCompatibility,
|
||||
formatNotice: formatPluginCompatibilityNotice,
|
||||
warn: theme.warn,
|
||||
muted: theme.muted,
|
||||
}),
|
||||
pairingRecoveryLines: buildStatusPairingRecoveryLines({
|
||||
pairingRecovery,
|
||||
warn: theme.warn,
|
||||
muted: theme.muted,
|
||||
formatCliCommand,
|
||||
}),
|
||||
securityAuditLines,
|
||||
channelsColumns: statusChannelsTableColumns,
|
||||
channelsRows: buildStatusChannelsTableRows({
|
||||
rows: channels.rows,
|
||||
summary,
|
||||
securityAudit,
|
||||
health,
|
||||
usageLines,
|
||||
lastHeartbeat,
|
||||
agentStatus,
|
||||
channels,
|
||||
channelIssues,
|
||||
memory,
|
||||
memoryPlugin,
|
||||
pluginCompatibility,
|
||||
pairingRecovery,
|
||||
tableWidth,
|
||||
ok,
|
||||
warn,
|
||||
muted,
|
||||
accentDim: theme.accentDim,
|
||||
formatIssueMessage: (message) => shortenText(message, 84),
|
||||
}),
|
||||
sessionsColumns,
|
||||
sessionsRows,
|
||||
systemEventsRows: buildStatusSystemEventsRows({
|
||||
queuedSystemEvents: summary.queuedSystemEvents,
|
||||
}),
|
||||
systemEventsTrailer: buildStatusSystemEventsTrailer({
|
||||
queuedSystemEvents: summary.queuedSystemEvents,
|
||||
muted,
|
||||
}),
|
||||
healthColumns: health ? statusHealthColumns : undefined,
|
||||
healthRows,
|
||||
usageLines,
|
||||
footerLines: buildStatusFooterLines({
|
||||
updateHint,
|
||||
warn: theme.warn,
|
||||
shortenText,
|
||||
formatCliCommand,
|
||||
nodeOnlyGateway,
|
||||
gatewayReachable,
|
||||
formatTimeAgo,
|
||||
formatKTokens,
|
||||
formatTokensCompact,
|
||||
formatPromptCacheCompact,
|
||||
formatHealthChannelLines,
|
||||
formatPluginCompatibilityNotice,
|
||||
formatUpdateAvailableHint,
|
||||
resolveMemoryVectorState,
|
||||
resolveMemoryFtsState,
|
||||
resolveMemoryCacheSummary,
|
||||
accentDim: theme.accentDim,
|
||||
theme,
|
||||
renderTable,
|
||||
updateValue: updateSurface.updateAvailable
|
||||
? warn(`available · ${updateSurface.updateLine}`)
|
||||
: updateSurface.updateLine,
|
||||
}),
|
||||
});
|
||||
);
|
||||
for (const line of lines) {
|
||||
runtime.log(line);
|
||||
}
|
||||
|
||||
1
src/commands/status.gateway-connection.runtime.ts
Normal file
1
src/commands/status.gateway-connection.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||
59
src/commands/status.gateway-connection.test.ts
Normal file
59
src/commands/status.gateway-connection.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
logGatewayConnectionDetails,
|
||||
resolveStatusAllConnectionDetails,
|
||||
} from "./status.gateway-connection.js";
|
||||
|
||||
describe("status.gateway-connection", () => {
|
||||
it("logs gateway connection details with indentation", () => {
|
||||
const runtime = { log: vi.fn() };
|
||||
|
||||
logGatewayConnectionDetails({
|
||||
runtime,
|
||||
info: (value) => `info:${value}`,
|
||||
message: "Gateway mode: local\nGateway target: ws://127.0.0.1:18789",
|
||||
trailingBlankLine: true,
|
||||
});
|
||||
|
||||
expect(runtime.log.mock.calls).toEqual([
|
||||
["info:Gateway connection:"],
|
||||
[" Gateway mode: local"],
|
||||
[" Gateway target: ws://127.0.0.1:18789"],
|
||||
[""],
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds remote fallback connection details", () => {
|
||||
expect(
|
||||
resolveStatusAllConnectionDetails({
|
||||
nodeOnlyGateway: null,
|
||||
remoteUrlMissing: true,
|
||||
gatewayConnection: {
|
||||
url: "ws://127.0.0.1:18789",
|
||||
message: "ignored",
|
||||
},
|
||||
bindMode: "loopback",
|
||||
configPath: "/tmp/openclaw.json",
|
||||
}),
|
||||
).toContain("Local fallback (used for probes): ws://127.0.0.1:18789");
|
||||
});
|
||||
|
||||
it("prefers node-only connection details when present", () => {
|
||||
expect(
|
||||
resolveStatusAllConnectionDetails({
|
||||
nodeOnlyGateway: {
|
||||
gatewayTarget: "remote.example:18789",
|
||||
gatewayValue: "node → remote.example:18789 · no local gateway",
|
||||
connectionDetails: "Node-only mode detected",
|
||||
},
|
||||
remoteUrlMissing: false,
|
||||
gatewayConnection: {
|
||||
url: "ws://127.0.0.1:18789",
|
||||
message: "Gateway mode: local",
|
||||
},
|
||||
bindMode: "loopback",
|
||||
configPath: "/tmp/openclaw.json",
|
||||
}),
|
||||
).toBe("Node-only mode detected");
|
||||
});
|
||||
});
|
||||
41
src/commands/status.gateway-connection.ts
Normal file
41
src/commands/status.gateway-connection.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { NodeOnlyGatewayInfo } from "./status.node-mode.js";
|
||||
import type { StatusScanOverviewResult } from "./status.scan-overview.ts";
|
||||
|
||||
export function logGatewayConnectionDetails(params: {
|
||||
runtime: Pick<RuntimeEnv, "log">;
|
||||
info: (value: string) => string;
|
||||
message: string;
|
||||
trailingBlankLine?: boolean;
|
||||
}) {
|
||||
params.runtime.log(params.info("Gateway connection:"));
|
||||
for (const line of params.message.split("\n")) {
|
||||
params.runtime.log(` ${line}`);
|
||||
}
|
||||
if (params.trailingBlankLine) {
|
||||
params.runtime.log("");
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveStatusAllConnectionDetails(params: {
|
||||
nodeOnlyGateway: NodeOnlyGatewayInfo | null;
|
||||
remoteUrlMissing: boolean;
|
||||
gatewayConnection: StatusScanOverviewResult["gatewaySnapshot"]["gatewayConnection"];
|
||||
bindMode?: string | null;
|
||||
configPath: string;
|
||||
}): string {
|
||||
if (params.nodeOnlyGateway) {
|
||||
return params.nodeOnlyGateway.connectionDetails;
|
||||
}
|
||||
if (!params.remoteUrlMissing) {
|
||||
return params.gatewayConnection.message;
|
||||
}
|
||||
return [
|
||||
"Gateway mode: remote",
|
||||
"Gateway target: (missing gateway.remote.url)",
|
||||
`Config: ${params.configPath}`,
|
||||
`Bind: ${params.bindMode ?? "loopback"}`,
|
||||
`Local fallback (used for probes): ${params.gatewayConnection.url}`,
|
||||
"Fix: set gateway.remote.url, or set gateway.mode=local.",
|
||||
].join("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user