feat(cli): colorize gateway health + daemon output

This commit is contained in:
Peter Steinberger
2026-01-10 02:52:42 +01:00
parent f28a4a34ad
commit 63b0a16357
8 changed files with 148 additions and 22 deletions

View File

@@ -67,6 +67,7 @@ import {
GOOGLE_GEMINI_DEFAULT_MODEL,
} from "./google-gemini-model-default.js";
import { healthCommand } from "./health.js";
import { formatHealthCheckFailure } from "./health-format.js";
import {
applyAuthProfileConfig,
applyMinimaxConfig,
@@ -1286,7 +1287,7 @@ export async function runConfigureWizard(
try {
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
} catch (err) {
runtime.error(`Health check failed: ${String(err)}`);
runtime.error(formatHealthCheckFailure(err));
note(
[
"Docs:",

View File

@@ -80,6 +80,7 @@ import {
shouldSuggestMemorySystem,
} from "./doctor-workspace.js";
import { healthCommand } from "./health.js";
import { formatHealthCheckFailure } from "./health-format.js";
import { applyWizardMetadata, printWizardHeader } from "./onboard-helpers.js";
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
@@ -310,7 +311,7 @@ export async function doctorCommand(
note("Gateway not running.", "Gateway");
note(gatewayDetails.message, "Gateway connection");
} else {
runtime.error(`Health check failed: ${message}`);
runtime.error(formatHealthCheckFailure(err));
}
}
@@ -455,7 +456,7 @@ export async function doctorCommand(
note("Gateway not running.", "Gateway");
note(gatewayDetails.message, "Gateway connection");
} else {
runtime.error(`Health check failed: ${message}`);
runtime.error(formatHealthCheckFailure(err));
}
}
}

View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { formatHealthCheckFailure } from "./health-format.js";
const stripAnsi = (input: string) =>
input.replace(
// biome-ignore lint/suspicious/noControlCharactersInRegex: strip ANSI escape sequences
/\u001b\[[0-9;]*m/g,
"",
);
describe("formatHealthCheckFailure", () => {
it("keeps non-rich output stable", () => {
const err = new Error(
"gateway closed (1006 abnormal closure): no close reason",
);
expect(formatHealthCheckFailure(err, { rich: false })).toBe(
`Health check failed: ${String(err)}`,
);
});
it("formats gateway connection details as indented key/value lines", () => {
const err = new Error(
[
"gateway closed (1006 abnormal closure (no close frame)): no close reason",
"Gateway target: ws://127.0.0.1:19001",
"Source: local loopback",
"Config: /Users/steipete/.clawdbot-dev/clawdbot.json",
"Bind: loopback",
].join("\n"),
);
expect(stripAnsi(formatHealthCheckFailure(err, { rich: true }))).toBe(
[
"Health check failed: gateway closed (1006 abnormal closure (no close frame)): no close reason",
" Gateway target: ws://127.0.0.1:19001",
" Source: local loopback",
" Config: /Users/steipete/.clawdbot-dev/clawdbot.json",
" Bind: loopback",
].join("\n"),
);
});
});

View File

@@ -0,0 +1,48 @@
import { colorize, isRich, theme } from "../terminal/theme.js";
const formatKv = (line: string, rich: boolean) => {
const idx = line.indexOf(": ");
if (idx <= 0) return colorize(rich, theme.muted, line);
const key = line.slice(0, idx);
const value = line.slice(idx + 2);
const valueColor =
key === "Gateway target" || key === "Config"
? theme.command
: key === "Source"
? theme.muted
: theme.info;
return `${colorize(rich, theme.muted, `${key}:`)} ${colorize(rich, valueColor, value)}`;
};
export function formatHealthCheckFailure(
err: unknown,
opts: { rich?: boolean } = {},
): string {
const rich = opts.rich ?? isRich();
const raw = String(err);
const message = err instanceof Error ? err.message : raw;
if (!rich) return `Health check failed: ${raw}`;
const lines = message
.split("\n")
.map((l) => l.trimEnd())
.filter(Boolean);
const detailsIdx = lines.findIndex((l) => l.startsWith("Gateway target: "));
const summaryLines = (detailsIdx >= 0 ? lines.slice(0, detailsIdx) : lines)
.map((l) => l.trim())
.filter(Boolean);
const detailLines = detailsIdx >= 0 ? lines.slice(detailsIdx) : [];
const summary = summaryLines.length > 0 ? summaryLines.join(" ") : message;
const header = colorize(rich, theme.error.bold, "Health check failed");
const out: string[] = [`${header}: ${summary}`];
for (const line of detailLines) {
out.push(` ${formatKv(line, rich)}`);
}
return out.join("\n");
}