fix: colorize daemon status output

This commit is contained in:
Peter Steinberger
2026-01-09 03:22:02 +00:00
parent 3e19f82af2
commit 16cfd24967

View File

@@ -48,6 +48,7 @@ import {
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js"; import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
import { getResolvedLoggerSettings } from "../logging.js"; import { getResolvedLoggerSettings } from "../logging.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { colorize, isRich, theme } from "../terminal/theme.js";
import { createDefaultDeps } from "./deps.js"; import { createDefaultDeps } from "./deps.js";
import { withProgress } from "./progress.js"; import { withProgress } from "./progress.js";
@@ -523,100 +524,142 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
return; 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 { service, rpc, legacyServices, extraServices } = status;
const serviceStatus = service.loaded
? okText(service.loadedText)
: warnText(service.notLoadedText);
defaultRuntime.log( defaultRuntime.log(
`Service: ${service.label} (${service.loaded ? service.loadedText : service.notLoadedText})`, `${label("Service:")} ${accent(service.label)} (${serviceStatus})`,
); );
try { try {
const logFile = getResolvedLoggerSettings().file; const logFile = getResolvedLoggerSettings().file;
defaultRuntime.log(`File logs: ${logFile}`); defaultRuntime.log(`${label("File logs:")} ${infoText(logFile)}`);
} catch { } catch {
// ignore missing config/log resolution // ignore missing config/log resolution
} }
if (service.command?.programArguments?.length) { if (service.command?.programArguments?.length) {
defaultRuntime.log( defaultRuntime.log(
`Command: ${service.command.programArguments.join(" ")}`, `${label("Command:")} ${infoText(service.command.programArguments.join(" "))}`,
); );
} }
if (service.command?.sourcePath) { if (service.command?.sourcePath) {
defaultRuntime.log(`Service file: ${service.command.sourcePath}`); defaultRuntime.log(
`${label("Service file:")} ${infoText(service.command.sourcePath)}`,
);
} }
if (service.command?.workingDirectory) { 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); const daemonEnvLines = safeDaemonEnv(service.command?.environment);
if (daemonEnvLines.length > 0) { if (daemonEnvLines.length > 0) {
defaultRuntime.log(`Daemon env: ${daemonEnvLines.join(" ")}`); defaultRuntime.log(`${label("Daemon env:")} ${daemonEnvLines.join(" ")}`);
} }
if (service.configAudit?.issues.length) { 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) { for (const issue of service.configAudit.issues) {
const detail = issue.detail ? ` (${issue.detail})` : ""; 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( defaultRuntime.error(
'Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").', warnText(
'Recommendation: run "clawdbot doctor" (or "clawdbot doctor --repair").',
),
); );
} }
if (status.config) { if (status.config) {
const cliCfg = `${status.config.cli.path}${status.config.cli.exists ? "" : " (missing)"}${status.config.cli.valid ? "" : " (invalid)"}`; 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) { if (!status.config.cli.valid && status.config.cli.issues?.length) {
for (const issue of status.config.cli.issues.slice(0, 5)) { for (const issue of status.config.cli.issues.slice(0, 5)) {
defaultRuntime.error( defaultRuntime.error(
`Config issue: ${issue.path || "<root>"}: ${issue.message}`, `${errorText("Config issue:")} ${issue.path || "<root>"}: ${issue.message}`,
); );
} }
} }
if (status.config.daemon) { if (status.config.daemon) {
const daemonCfg = `${status.config.daemon.path}${status.config.daemon.exists ? "" : " (missing)"}${status.config.daemon.valid ? "" : " (invalid)"}`; 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) { if (!status.config.daemon.valid && status.config.daemon.issues?.length) {
for (const issue of status.config.daemon.issues.slice(0, 5)) { for (const issue of status.config.daemon.issues.slice(0, 5)) {
defaultRuntime.error( defaultRuntime.error(
`Daemon config issue: ${issue.path || "<root>"}: ${issue.message}`, `${errorText("Daemon config issue:")} ${issue.path || "<root>"}: ${issue.message}`,
); );
} }
} }
} }
if (status.config.mismatch) { if (status.config.mismatch) {
defaultRuntime.error( 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( 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) { if (status.gateway) {
const bindHost = status.gateway.bindHost ?? "n/a"; const bindHost = status.gateway.bindHost ?? "n/a";
defaultRuntime.log( 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; const controlUiEnabled = status.config?.daemon?.controlUi?.enabled ?? true;
if (!controlUiEnabled) { if (!controlUiEnabled) {
defaultRuntime.log("Dashboard: disabled"); defaultRuntime.log(`${label("Dashboard:")} ${warnText("disabled")}`);
} else { } else {
const links = resolveControlUiLinks({ const links = resolveControlUiLinks({
port: status.gateway.port, port: status.gateway.port,
bind: status.gateway.bindMode, bind: status.gateway.bindMode,
basePath: status.config?.daemon?.controlUi?.basePath, basePath: status.config?.daemon?.controlUi?.basePath,
}); });
defaultRuntime.log(`Dashboard: ${links.httpUrl}`); defaultRuntime.log(`${label("Dashboard:")} ${infoText(links.httpUrl)}`);
} }
if (status.gateway.probeNote) { 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) { if (status.gateway.bindMode === "tailnet" && !status.gateway.bindHost) {
defaultRuntime.error( 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); const runtimeLine = formatRuntimeStatus(service.runtime);
if (runtimeLine) { 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 ( if (
rpc && rpc &&
@@ -625,44 +668,47 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
service.runtime?.status === "running" service.runtime?.status === "running"
) { ) {
defaultRuntime.log( 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) {
if (rpc.ok) { if (rpc.ok) {
defaultRuntime.log("RPC probe: ok"); defaultRuntime.log(`${label("RPC probe:")} ${okText("ok")}`);
} else { } else {
defaultRuntime.error("RPC probe: failed"); defaultRuntime.error(`${label("RPC probe:")} ${errorText("failed")}`);
if (rpc.url) defaultRuntime.error(`RPC target: ${rpc.url}`); if (rpc.url)
defaultRuntime.error(`${label("RPC target:")} ${rpc.url}`);
const lines = String(rpc.error ?? "unknown") const lines = String(rpc.error ?? "unknown")
.split(/\r?\n/) .split(/\r?\n/)
.filter(Boolean); .filter(Boolean);
for (const line of lines.slice(0, 12)) { for (const line of lines.slice(0, 12)) {
defaultRuntime.error(` ${line}`); defaultRuntime.error(` ${errorText(line)}`);
} }
} }
} }
if (service.runtime?.missingUnit) { if (service.runtime?.missingUnit) {
defaultRuntime.error("Service unit not found."); defaultRuntime.error(errorText("Service unit not found."));
for (const hint of renderRuntimeHints(service.runtime)) { for (const hint of renderRuntimeHints(service.runtime)) {
defaultRuntime.error(hint); defaultRuntime.error(errorText(hint));
} }
} else if (service.loaded && service.runtime?.status === "stopped") { } else if (service.loaded && service.runtime?.status === "stopped") {
defaultRuntime.error( 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( for (const hint of renderRuntimeHints(
service.runtime, service.runtime,
(service.command?.environment ?? process.env) as NodeJS.ProcessEnv, (service.command?.environment ?? process.env) as NodeJS.ProcessEnv,
)) { )) {
defaultRuntime.error(hint); defaultRuntime.error(errorText(hint));
} }
} }
if (service.runtime?.cachedLabel) { if (service.runtime?.cachedLabel) {
defaultRuntime.error( 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)) { if (status.port && shouldReportPortUsage(status.port.status, rpc?.ok)) {
for (const line of formatPortDiagnostics({ for (const line of formatPortDiagnostics({
@@ -671,7 +717,7 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
listeners: status.port.listeners, listeners: status.port.listeners,
hints: status.port.hints, hints: status.port.hints,
})) { })) {
defaultRuntime.error(line); defaultRuntime.error(errorText(line));
} }
} }
if (status.port) { if (status.port) {
@@ -683,12 +729,12 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
), ),
); );
if (addrs.length > 0) { 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) { if (status.portCli && status.portCli.port !== status.port?.port) {
defaultRuntime.log( 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 ( if (
@@ -698,52 +744,68 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
status.port.status !== "busy" status.port.status !== "busy"
) { ) {
defaultRuntime.error( 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) { if (status.lastError) {
defaultRuntime.error(`Last gateway error: ${status.lastError}`); defaultRuntime.error(
`${errorText("Last gateway error:")} ${status.lastError}`,
);
} }
if (process.platform === "linux") { if (process.platform === "linux") {
defaultRuntime.error( 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") { } else if (process.platform === "darwin") {
const logs = resolveGatewayLogPaths( const logs = resolveGatewayLogPaths(
(service.command?.environment ?? process.env) as NodeJS.ProcessEnv, (service.command?.environment ?? process.env) as NodeJS.ProcessEnv,
); );
defaultRuntime.error(`Logs: ${logs.stdoutPath}`); defaultRuntime.error(`${errorText("Logs:")} ${logs.stdoutPath}`);
defaultRuntime.error(`Errors: ${logs.stderrPath}`); defaultRuntime.error(`${errorText("Errors:")} ${logs.stderrPath}`);
} }
} }
if (legacyServices.length > 0) { if (legacyServices.length > 0) {
defaultRuntime.error("Legacy Clawdis services detected:"); defaultRuntime.error(errorText("Legacy Clawdis services detected:"));
for (const svc of legacyServices) { 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) { 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) { 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()) { for (const hint of renderGatewayServiceCleanupHints()) {
defaultRuntime.error(`Cleanup hint: ${hint}`); defaultRuntime.error(`${errorText("Cleanup hint:")} ${hint}`);
} }
} }
if (legacyServices.length > 0 || extraServices.length > 0) { if (legacyServices.length > 0 || extraServices.length > 0) {
defaultRuntime.error( 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( 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(`${label("Troubles:")} run clawdbot status`);
defaultRuntime.log("Troubleshooting: https://docs.clawd.bot/troubleshooting"); defaultRuntime.log(
`${label("Troubleshooting:")} https://docs.clawd.bot/troubleshooting`,
);
} }
export async function runDaemonStatus(opts: DaemonStatusOptions) { export async function runDaemonStatus(opts: DaemonStatusOptions) {
@@ -755,7 +817,10 @@ export async function runDaemonStatus(opts: DaemonStatusOptions) {
}); });
printDaemonStatus(status, { json: Boolean(opts.json) }); printDaemonStatus(status, { json: Boolean(opts.json) });
} catch (err) { } 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); defaultRuntime.exit(1);
} }
} }