import { resolveControlUiLinks } from "../../commands/onboard-helpers.js"; import { resolveGatewayLaunchAgentLabel, resolveGatewaySystemdServiceName, } from "../../daemon/constants.js"; import { renderGatewayServiceCleanupHints } from "../../daemon/inspect.js"; import { resolveGatewayLogPaths } from "../../daemon/launchd.js"; import { isSystemdUnavailableDetail, renderSystemdUnavailableHints, } from "../../daemon/systemd-hints.js"; import { isWSLEnv } from "../../infra/wsl.js"; import { getResolvedLoggerSettings } from "../../logging.js"; import { defaultRuntime } from "../../runtime.js"; import { colorize, isRich, theme } from "../../terminal/theme.js"; import { formatCliCommand } from "../command-format.js"; import { formatRuntimeStatus, renderRuntimeHints, safeDaemonEnv } from "./shared.js"; import { type DaemonStatus, renderPortDiagnosticsForCli, resolvePortListeningAddresses, } from "./status.gather.js"; export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) { if (opts.json) { defaultRuntime.log(JSON.stringify(status, null, 2)); 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 spacer = () => defaultRuntime.log(""); const { service, rpc, legacyServices, extraServices } = status; const serviceStatus = service.loaded ? okText(service.loadedText) : warnText(service.notLoadedText); defaultRuntime.log(`${label("Service:")} ${accent(service.label)} (${serviceStatus})`); try { const logFile = getResolvedLoggerSettings().file; defaultRuntime.log(`${label("File logs:")} ${infoText(logFile)}`); } catch { // ignore missing config/log resolution } if (service.command?.programArguments?.length) { defaultRuntime.log( `${label("Command:")} ${infoText(service.command.programArguments.join(" "))}`, ); } if (service.command?.sourcePath) { defaultRuntime.log(`${label("Service file:")} ${infoText(service.command.sourcePath)}`); } if (service.command?.workingDirectory) { defaultRuntime.log(`${label("Working dir:")} ${infoText(service.command.workingDirectory)}`); } const daemonEnvLines = safeDaemonEnv(service.command?.environment); if (daemonEnvLines.length > 0) { defaultRuntime.log(`${label("Service env:")} ${daemonEnvLines.join(" ")}`); } spacer(); if (service.configAudit?.issues.length) { 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(`${warnText("Service config issue:")} ${issue.message}${detail}`); } defaultRuntime.error( warnText( `Recommendation: run "${formatCliCommand("clawdbot doctor")}" (or "${formatCliCommand("clawdbot doctor --repair")}").`, ), ); } if (status.config) { const cliCfg = `${status.config.cli.path}${status.config.cli.exists ? "" : " (missing)"}${status.config.cli.valid ? "" : " (invalid)"}`; 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( `${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(`${label("Config (service):")} ${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( `${errorText("Service config issue:")} ${issue.path || ""}: ${issue.message}`, ); } } } if (status.config.mismatch) { defaultRuntime.error( errorText( "Root cause: CLI and service are using different config paths (likely a profile/state-dir mismatch).", ), ); defaultRuntime.error( errorText( `Fix: rerun \`${formatCliCommand("clawdbot gateway install --force")}\` from the same --profile / CLAWDBOT_STATE_DIR you expect.`, ), ); } spacer(); } if (status.gateway) { const bindHost = status.gateway.bindHost ?? "n/a"; defaultRuntime.log( `${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)}`); const controlUiEnabled = status.config?.daemon?.controlUi?.enabled ?? true; if (!controlUiEnabled) { defaultRuntime.log(`${label("Dashboard:")} ${warnText("disabled")}`); } else { const links = resolveControlUiLinks({ port: status.gateway.port, bind: status.gateway.bindMode, customBindHost: status.gateway.customBindHost, basePath: status.config?.daemon?.controlUi?.basePath, }); defaultRuntime.log(`${label("Dashboard:")} ${infoText(links.httpUrl)}`); } if (status.gateway.probeNote) { defaultRuntime.log(`${label("Probe note:")} ${infoText(status.gateway.probeNote)}`); } spacer(); } const runtimeLine = formatRuntimeStatus(service.runtime); if (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 && !rpc.ok && service.loaded && service.runtime?.status === "running") { defaultRuntime.log( warnText("Warm-up: launch agents can take a few seconds. Try again shortly."), ); } if (rpc) { if (rpc.ok) { defaultRuntime.log(`${label("RPC probe:")} ${okText("ok")}`); } else { 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(` ${errorText(line)}`); } } spacer(); } const systemdUnavailable = process.platform === "linux" && isSystemdUnavailableDetail(service.runtime?.detail); if (systemdUnavailable) { defaultRuntime.error(errorText("systemd user services unavailable.")); for (const hint of renderSystemdUnavailableHints({ wsl: isWSLEnv() })) { defaultRuntime.error(errorText(hint)); } spacer(); } if (service.runtime?.missingUnit) { defaultRuntime.error(errorText("Service unit not found.")); for (const hint of renderRuntimeHints(service.runtime)) { defaultRuntime.error(errorText(hint)); } } else if (service.loaded && service.runtime?.status === "stopped") { defaultRuntime.error( 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(errorText(hint)); } spacer(); } if (service.runtime?.cachedLabel) { const env = (service.command?.environment ?? process.env) as NodeJS.ProcessEnv; const labelValue = resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE); defaultRuntime.error( errorText( `LaunchAgent label cached but plist missing. Clear with: launchctl bootout gui/$UID/${labelValue}`, ), ); defaultRuntime.error( errorText(`Then reinstall: ${formatCliCommand("clawdbot gateway install")}`), ); spacer(); } for (const line of renderPortDiagnosticsForCli(status, rpc?.ok)) { defaultRuntime.error(errorText(line)); } if (status.port) { const addrs = resolvePortListeningAddresses(status); if (addrs.length > 0) { defaultRuntime.log(`${label("Listening:")} ${infoText(addrs.join(", "))}`); } } if (status.portCli && status.portCli.port !== status.port?.port) { defaultRuntime.log( `${label("Note:")} CLI config resolves gateway port=${status.portCli.port} (${status.portCli.status}).`, ); } if ( service.loaded && service.runtime?.status === "running" && status.port && status.port.status !== "busy" ) { defaultRuntime.error( errorText(`Gateway port ${status.port.port} is not listening (service appears running).`), ); if (status.lastError) { defaultRuntime.error(`${errorText("Last gateway error:")} ${status.lastError}`); } if (process.platform === "linux") { const env = (service.command?.environment ?? process.env) as NodeJS.ProcessEnv; const unit = resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE); defaultRuntime.error( errorText(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`), ); } else if (process.platform === "darwin") { const logs = resolveGatewayLogPaths( (service.command?.environment ?? process.env) as NodeJS.ProcessEnv, ); defaultRuntime.error(`${errorText("Logs:")} ${logs.stdoutPath}`); defaultRuntime.error(`${errorText("Errors:")} ${logs.stderrPath}`); } spacer(); } if (legacyServices.length > 0) { defaultRuntime.error(errorText("Legacy gateway services detected:")); for (const svc of legacyServices) { defaultRuntime.error(`- ${errorText(svc.label)} (${svc.detail})`); } defaultRuntime.error(errorText(`Cleanup: ${formatCliCommand("clawdbot doctor")}`)); spacer(); } if (extraServices.length > 0) { defaultRuntime.error(errorText("Other gateway-like services detected (best effort):")); for (const svc of extraServices) { defaultRuntime.error(`- ${errorText(svc.label)} (${svc.scope}, ${svc.detail})`); } for (const hint of renderGatewayServiceCleanupHints()) { defaultRuntime.error(`${errorText("Cleanup hint:")} ${hint}`); } spacer(); } if (legacyServices.length > 0 || extraServices.length > 0) { defaultRuntime.error( errorText( "Recommendation: run a single gateway per machine for most setups. One gateway supports multiple agents (see docs: /gateway#multiple-gateways-same-host).", ), ); defaultRuntime.error( errorText( "If you need multiple gateways (e.g., a rescue bot on the same host), isolate ports + config/state (see docs: /gateway#multiple-gateways-same-host).", ), ); spacer(); } defaultRuntime.log(`${label("Troubles:")} run ${formatCliCommand("clawdbot status")}`); defaultRuntime.log(`${label("Troubleshooting:")} https://docs.clawd.bot/troubleshooting`); }