From 16cfd24967584465facb2a20a46544338fa4227a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 03:22:02 +0000 Subject: [PATCH] fix: colorize daemon status output --- src/cli/daemon-cli.ts | 169 +++++++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 52 deletions(-) diff --git a/src/cli/daemon-cli.ts b/src/cli/daemon-cli.ts index 908c4fb7a..f26679f11 100644 --- a/src/cli/daemon-cli.ts +++ b/src/cli/daemon-cli.ts @@ -48,6 +48,7 @@ import { import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js"; import { getResolvedLoggerSettings } from "../logging.js"; import { defaultRuntime } from "../runtime.js"; +import { colorize, isRich, theme } from "../terminal/theme.js"; import { createDefaultDeps } from "./deps.js"; import { withProgress } from "./progress.js"; @@ -523,100 +524,142 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { return; } + const rich = isRich(); + const label = (value: string) => colorize(rich, theme.muted, value); + const accent = (value: string) => colorize(rich, theme.accent, value); + const infoText = (value: string) => colorize(rich, theme.info, value); + const okText = (value: string) => colorize(rich, theme.success, value); + const warnText = (value: string) => colorize(rich, theme.warn, value); + const errorText = (value: string) => colorize(rich, theme.error, value); + const { service, rpc, legacyServices, extraServices } = status; + const serviceStatus = service.loaded + ? okText(service.loadedText) + : warnText(service.notLoadedText); defaultRuntime.log( - `Service: ${service.label} (${service.loaded ? service.loadedText : service.notLoadedText})`, + `${label("Service:")} ${accent(service.label)} (${serviceStatus})`, ); try { const logFile = getResolvedLoggerSettings().file; - defaultRuntime.log(`File logs: ${logFile}`); + defaultRuntime.log(`${label("File logs:")} ${infoText(logFile)}`); } catch { // ignore missing config/log resolution } if (service.command?.programArguments?.length) { defaultRuntime.log( - `Command: ${service.command.programArguments.join(" ")}`, + `${label("Command:")} ${infoText(service.command.programArguments.join(" "))}`, ); } if (service.command?.sourcePath) { - defaultRuntime.log(`Service file: ${service.command.sourcePath}`); + defaultRuntime.log( + `${label("Service file:")} ${infoText(service.command.sourcePath)}`, + ); } if (service.command?.workingDirectory) { - defaultRuntime.log(`Working dir: ${service.command.workingDirectory}`); + defaultRuntime.log( + `${label("Working dir:")} ${infoText(service.command.workingDirectory)}`, + ); } const daemonEnvLines = safeDaemonEnv(service.command?.environment); if (daemonEnvLines.length > 0) { - defaultRuntime.log(`Daemon env: ${daemonEnvLines.join(" ")}`); + defaultRuntime.log(`${label("Daemon env:")} ${daemonEnvLines.join(" ")}`); } if (service.configAudit?.issues.length) { - defaultRuntime.error("Service config looks out of date or non-standard."); + defaultRuntime.error( + warnText("Service config looks out of date or non-standard."), + ); for (const issue of service.configAudit.issues) { const detail = issue.detail ? ` (${issue.detail})` : ""; - defaultRuntime.error(`Service config issue: ${issue.message}${detail}`); + defaultRuntime.error( + `${warnText("Service config issue:")} ${issue.message}${detail}`, + ); } defaultRuntime.error( - 'Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").', + warnText( + 'Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").', + ), ); } if (status.config) { const cliCfg = `${status.config.cli.path}${status.config.cli.exists ? "" : " (missing)"}${status.config.cli.valid ? "" : " (invalid)"}`; - defaultRuntime.log(`Config (cli): ${cliCfg}`); + defaultRuntime.log(`${label("Config (cli):")} ${infoText(cliCfg)}`); if (!status.config.cli.valid && status.config.cli.issues?.length) { for (const issue of status.config.cli.issues.slice(0, 5)) { defaultRuntime.error( - `Config issue: ${issue.path || ""}: ${issue.message}`, + `${errorText("Config issue:")} ${issue.path || ""}: ${issue.message}`, ); } } if (status.config.daemon) { const daemonCfg = `${status.config.daemon.path}${status.config.daemon.exists ? "" : " (missing)"}${status.config.daemon.valid ? "" : " (invalid)"}`; - defaultRuntime.log(`Config (daemon): ${daemonCfg}`); + defaultRuntime.log(`${label("Config (daemon):")} ${infoText(daemonCfg)}`); if (!status.config.daemon.valid && status.config.daemon.issues?.length) { for (const issue of status.config.daemon.issues.slice(0, 5)) { defaultRuntime.error( - `Daemon config issue: ${issue.path || ""}: ${issue.message}`, + `${errorText("Daemon config issue:")} ${issue.path || ""}: ${issue.message}`, ); } } } if (status.config.mismatch) { defaultRuntime.error( - "Root cause: CLI and daemon are using different config paths (likely a profile/state-dir mismatch).", + errorText( + "Root cause: CLI and daemon are using different config paths (likely a profile/state-dir mismatch).", + ), ); defaultRuntime.error( - "Fix: rerun `clawdbot daemon install --force` from the same --profile / CLAWDBOT_STATE_DIR you expect.", + errorText( + "Fix: rerun `clawdbot daemon install --force` from the same --profile / CLAWDBOT_STATE_DIR you expect.", + ), ); } } if (status.gateway) { const bindHost = status.gateway.bindHost ?? "n/a"; defaultRuntime.log( - `Gateway: bind=${status.gateway.bindMode} (${bindHost}), port=${status.gateway.port} (${status.gateway.portSource})`, + `${label("Gateway:")} bind=${infoText(status.gateway.bindMode)} (${infoText(bindHost)}), port=${infoText(String(status.gateway.port))} (${infoText(status.gateway.portSource)})`, + ); + defaultRuntime.log( + `${label("Probe target:")} ${infoText(status.gateway.probeUrl)}`, ); - defaultRuntime.log(`Probe target: ${status.gateway.probeUrl}`); const controlUiEnabled = status.config?.daemon?.controlUi?.enabled ?? true; if (!controlUiEnabled) { - defaultRuntime.log("Dashboard: disabled"); + defaultRuntime.log(`${label("Dashboard:")} ${warnText("disabled")}`); } else { const links = resolveControlUiLinks({ port: status.gateway.port, bind: status.gateway.bindMode, basePath: status.config?.daemon?.controlUi?.basePath, }); - defaultRuntime.log(`Dashboard: ${links.httpUrl}`); + defaultRuntime.log(`${label("Dashboard:")} ${infoText(links.httpUrl)}`); } if (status.gateway.probeNote) { - defaultRuntime.log(`Probe note: ${status.gateway.probeNote}`); + defaultRuntime.log( + `${label("Probe note:")} ${infoText(status.gateway.probeNote)}`, + ); } if (status.gateway.bindMode === "tailnet" && !status.gateway.bindHost) { defaultRuntime.error( - "Root cause: gateway bind=tailnet but no tailnet interface was found.", + errorText( + "Root cause: gateway bind=tailnet but no tailnet interface was found.", + ), ); } } const runtimeLine = formatRuntimeStatus(service.runtime); if (runtimeLine) { - defaultRuntime.log(`Runtime: ${runtimeLine}`); + const runtimeStatus = service.runtime?.status ?? "unknown"; + const runtimeColor = + runtimeStatus === "running" + ? theme.success + : runtimeStatus === "stopped" + ? theme.error + : runtimeStatus === "unknown" + ? theme.muted + : theme.warn; + defaultRuntime.log( + `${label("Runtime:")} ${colorize(rich, runtimeColor, runtimeLine)}`, + ); } if ( rpc && @@ -625,44 +668,47 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { service.runtime?.status === "running" ) { defaultRuntime.log( - "Warm-up: launch agents can take a few seconds. Try again shortly.", + warnText("Warm-up: launch agents can take a few seconds. Try again shortly."), ); } if (rpc) { if (rpc.ok) { - defaultRuntime.log("RPC probe: ok"); + defaultRuntime.log(`${label("RPC probe:")} ${okText("ok")}`); } else { - defaultRuntime.error("RPC probe: failed"); - if (rpc.url) defaultRuntime.error(`RPC target: ${rpc.url}`); + defaultRuntime.error(`${label("RPC probe:")} ${errorText("failed")}`); + if (rpc.url) + defaultRuntime.error(`${label("RPC target:")} ${rpc.url}`); const lines = String(rpc.error ?? "unknown") .split(/\r?\n/) .filter(Boolean); for (const line of lines.slice(0, 12)) { - defaultRuntime.error(` ${line}`); + defaultRuntime.error(` ${errorText(line)}`); } } } if (service.runtime?.missingUnit) { - defaultRuntime.error("Service unit not found."); + defaultRuntime.error(errorText("Service unit not found.")); for (const hint of renderRuntimeHints(service.runtime)) { - defaultRuntime.error(hint); + defaultRuntime.error(errorText(hint)); } } else if (service.loaded && service.runtime?.status === "stopped") { defaultRuntime.error( - "Service is loaded but not running (likely exited immediately).", + errorText("Service is loaded but not running (likely exited immediately)."), ); for (const hint of renderRuntimeHints( service.runtime, (service.command?.environment ?? process.env) as NodeJS.ProcessEnv, )) { - defaultRuntime.error(hint); + defaultRuntime.error(errorText(hint)); } } if (service.runtime?.cachedLabel) { defaultRuntime.error( - `LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`, + errorText( + `LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`, + ), ); - defaultRuntime.error("Then reinstall: clawdbot daemon install"); + defaultRuntime.error(errorText("Then reinstall: clawdbot daemon install")); } if (status.port && shouldReportPortUsage(status.port.status, rpc?.ok)) { for (const line of formatPortDiagnostics({ @@ -671,7 +717,7 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { listeners: status.port.listeners, hints: status.port.hints, })) { - defaultRuntime.error(line); + defaultRuntime.error(errorText(line)); } } if (status.port) { @@ -683,12 +729,12 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { ), ); if (addrs.length > 0) { - defaultRuntime.log(`Listening: ${addrs.join(", ")}`); + defaultRuntime.log(`${label("Listening:")} ${infoText(addrs.join(", "))}`); } } if (status.portCli && status.portCli.port !== status.port?.port) { defaultRuntime.log( - `Note: CLI config resolves gateway port=${status.portCli.port} (${status.portCli.status}).`, + `${label("Note:")} CLI config resolves gateway port=${status.portCli.port} (${status.portCli.status}).`, ); } if ( @@ -698,52 +744,68 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { status.port.status !== "busy" ) { defaultRuntime.error( - `Gateway port ${status.port.port} is not listening (service appears running).`, + errorText( + `Gateway port ${status.port.port} is not listening (service appears running).`, + ), ); if (status.lastError) { - defaultRuntime.error(`Last gateway error: ${status.lastError}`); + defaultRuntime.error( + `${errorText("Last gateway error:")} ${status.lastError}`, + ); } if (process.platform === "linux") { defaultRuntime.error( - `Logs: journalctl --user -u ${GATEWAY_SYSTEMD_SERVICE_NAME}.service -n 200 --no-pager`, + errorText( + `Logs: journalctl --user -u ${GATEWAY_SYSTEMD_SERVICE_NAME}.service -n 200 --no-pager`, + ), ); } else if (process.platform === "darwin") { const logs = resolveGatewayLogPaths( (service.command?.environment ?? process.env) as NodeJS.ProcessEnv, ); - defaultRuntime.error(`Logs: ${logs.stdoutPath}`); - defaultRuntime.error(`Errors: ${logs.stderrPath}`); + defaultRuntime.error(`${errorText("Logs:")} ${logs.stdoutPath}`); + defaultRuntime.error(`${errorText("Errors:")} ${logs.stderrPath}`); } } if (legacyServices.length > 0) { - defaultRuntime.error("Legacy Clawdis services detected:"); + defaultRuntime.error(errorText("Legacy Clawdis services detected:")); for (const svc of legacyServices) { - defaultRuntime.error(`- ${svc.label} (${svc.detail})`); + defaultRuntime.error(`- ${errorText(svc.label)} (${svc.detail})`); } - defaultRuntime.error("Cleanup: clawdbot doctor"); + defaultRuntime.error(errorText("Cleanup: clawdbot doctor")); } if (extraServices.length > 0) { - defaultRuntime.error("Other gateway-like services detected (best effort):"); + defaultRuntime.error( + errorText("Other gateway-like services detected (best effort):"), + ); for (const svc of extraServices) { - defaultRuntime.error(`- ${svc.label} (${svc.scope}, ${svc.detail})`); + defaultRuntime.error( + `- ${errorText(svc.label)} (${svc.scope}, ${svc.detail})`, + ); } for (const hint of renderGatewayServiceCleanupHints()) { - defaultRuntime.error(`Cleanup hint: ${hint}`); + defaultRuntime.error(`${errorText("Cleanup hint:")} ${hint}`); } } if (legacyServices.length > 0 || extraServices.length > 0) { defaultRuntime.error( - "Recommendation: run a single gateway per machine. One gateway supports multiple agents.", + errorText( + "Recommendation: run a single gateway per machine. One gateway supports multiple agents.", + ), ); defaultRuntime.error( - "If you need multiple gateways, isolate ports + config/state (see docs: /gateway#multiple-gateways-same-host).", + errorText( + "If you need multiple gateways, isolate ports + config/state (see docs: /gateway#multiple-gateways-same-host).", + ), ); } - defaultRuntime.log("Troubles: run clawdbot status"); - defaultRuntime.log("Troubleshooting: https://docs.clawd.bot/troubleshooting"); + defaultRuntime.log(`${label("Troubles:")} run clawdbot status`); + defaultRuntime.log( + `${label("Troubleshooting:")} https://docs.clawd.bot/troubleshooting`, + ); } export async function runDaemonStatus(opts: DaemonStatusOptions) { @@ -755,7 +817,10 @@ export async function runDaemonStatus(opts: DaemonStatusOptions) { }); printDaemonStatus(status, { json: Boolean(opts.json) }); } catch (err) { - defaultRuntime.error(`Daemon status failed: ${String(err)}`); + const rich = isRich(); + defaultRuntime.error( + colorize(rich, theme.error, `Daemon status failed: ${String(err)}`), + ); defaultRuntime.exit(1); } }