From f1ac18933c732c41b3ffaf42ec9de7027a6a7d1b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 08:28:33 +0000 Subject: [PATCH] fix(cli): daemon output + health colors --- src/cli/gateway-cli/register.ts | 25 +++++++++++++++++++- src/commands/health.command.coverage.test.ts | 5 +++- src/commands/health.ts | 25 +++++++++++++++++++- src/daemon/launchd.ts | 2 ++ src/daemon/schtasks.ts | 2 ++ src/daemon/systemd.ts | 2 ++ 6 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/cli/gateway-cli/register.ts b/src/cli/gateway-cli/register.ts index 194238b79..0a4361140 100644 --- a/src/cli/gateway-cli/register.ts +++ b/src/cli/gateway-cli/register.ts @@ -18,6 +18,29 @@ import { } from "./discover.js"; import { addGatewayRunCommand } from "./run.js"; +function styleHealthChannelLine(line: string, rich: boolean): string { + if (!rich) return line; + const colon = line.indexOf(":"); + if (colon === -1) return line; + + const label = line.slice(0, colon + 1); + const detail = line.slice(colon + 1).trimStart(); + const normalized = detail.toLowerCase(); + + const applyPrefix = (prefix: string, color: (value: string) => string) => + `${label} ${color(detail.slice(0, prefix.length))}${detail.slice(prefix.length)}`; + + if (normalized.startsWith("failed")) return applyPrefix("failed", theme.error); + if (normalized.startsWith("ok")) return applyPrefix("ok", theme.success); + if (normalized.startsWith("linked")) return applyPrefix("linked", theme.success); + if (normalized.startsWith("configured")) return applyPrefix("configured", theme.success); + if (normalized.startsWith("not linked")) return applyPrefix("not linked", theme.warn); + if (normalized.startsWith("not configured")) return applyPrefix("not configured", theme.muted); + if (normalized.startsWith("unknown")) return applyPrefix("unknown", theme.warn); + + return line; +} + export function registerGatewayCli(program: Command) { const gateway = addGatewayRunCommand( program @@ -84,7 +107,7 @@ export function registerGatewayCli(program: Command) { ); if (obj.channels && typeof obj.channels === "object") { for (const line of formatHealthChannelLines(obj as HealthSummary)) { - defaultRuntime.log(line); + defaultRuntime.log(styleHealthChannelLine(line, rich)); } } } catch (err) { diff --git a/src/commands/health.command.coverage.test.ts b/src/commands/health.command.coverage.test.ts index 568de95a7..00c691ad4 100644 --- a/src/commands/health.command.coverage.test.ts +++ b/src/commands/health.command.coverage.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { HealthSummary } from "./health.js"; import { healthCommand } from "./health.js"; +import { stripAnsi } from "../terminal/ansi.js"; const callGatewayMock = vi.fn(); const logWebSelfIdMock = vi.fn(); @@ -70,7 +71,9 @@ describe("healthCommand (coverage)", () => { await healthCommand({ json: false, timeoutMs: 1000 }, runtime as never); expect(runtime.exit).not.toHaveBeenCalled(); - expect(runtime.log.mock.calls.map((c) => String(c[0])).join("\n")).toMatch(/WhatsApp: linked/i); + expect(stripAnsi(runtime.log.mock.calls.map((c) => String(c[0])).join("\n"))).toMatch( + /WhatsApp: linked/i, + ); expect(logWebSelfIdMock).toHaveBeenCalled(); }); }); diff --git a/src/commands/health.ts b/src/commands/health.ts index ff7dba4d5..dcc788ab1 100644 --- a/src/commands/health.ts +++ b/src/commands/health.ts @@ -8,6 +8,7 @@ import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js"; import { info } from "../globals.js"; import { formatErrorMessage } from "../infra/errors.js"; import type { RuntimeEnv } from "../runtime.js"; +import { theme } from "../terminal/theme.js"; import { resolveHeartbeatSeconds } from "../web/reconnect.js"; export type ChannelHealthSummary = { @@ -79,6 +80,28 @@ const formatProbeLine = (probe: unknown): string | null => { return label; }; +function styleHealthChannelLine(line: string): string { + const colon = line.indexOf(":"); + if (colon === -1) return line; + + const label = line.slice(0, colon + 1); + const detail = line.slice(colon + 1).trimStart(); + const normalized = detail.toLowerCase(); + + const applyPrefix = (prefix: string, color: (value: string) => string) => + `${label} ${color(detail.slice(0, prefix.length))}${detail.slice(prefix.length)}`; + + if (normalized.startsWith("failed")) return applyPrefix("failed", theme.error); + if (normalized.startsWith("ok")) return applyPrefix("ok", theme.success); + if (normalized.startsWith("linked")) return applyPrefix("linked", theme.success); + if (normalized.startsWith("configured")) return applyPrefix("configured", theme.success); + if (normalized.startsWith("not linked")) return applyPrefix("not linked", theme.warn); + if (normalized.startsWith("not configured")) return applyPrefix("not configured", theme.muted); + if (normalized.startsWith("unknown")) return applyPrefix("unknown", theme.warn); + + return line; +} + export const formatHealthChannelLines = (summary: HealthSummary): string[] => { const channels = summary.channels ?? {}; const channelOrder = @@ -263,7 +286,7 @@ export async function healthCommand( } } for (const line of formatHealthChannelLines(summary)) { - runtime.log(line); + runtime.log(styleHealthChannelLine(line)); } const cfg = loadConfig(); for (const plugin of listChannelPlugins()) { diff --git a/src/daemon/launchd.ts b/src/daemon/launchd.ts index 8a89d4216..492344a3a 100644 --- a/src/daemon/launchd.ts +++ b/src/daemon/launchd.ts @@ -415,6 +415,8 @@ export async function installLaunchAgent({ } await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]); + // Ensure we don't end up writing to a clack spinner line (wizards show progress without a newline). + stdout.write("\n"); stdout.write(`${formatLine("Installed LaunchAgent", plistPath)}\n`); stdout.write(`${formatLine("Logs", stdoutPath)}\n`); return { plistPath }; diff --git a/src/daemon/schtasks.ts b/src/daemon/schtasks.ts index 7a4466972..ea008b14b 100644 --- a/src/daemon/schtasks.ts +++ b/src/daemon/schtasks.ts @@ -240,6 +240,8 @@ export async function installScheduledTask({ } await execSchtasks(["/Run", "/TN", taskName]); + // Ensure we don't end up writing to a clack spinner line (wizards show progress without a newline). + stdout.write("\n"); stdout.write(`${formatLine("Installed Scheduled Task", taskName)}\n`); stdout.write(`${formatLine("Task script", scriptPath)}\n`); return { scriptPath }; diff --git a/src/daemon/systemd.ts b/src/daemon/systemd.ts index be7214f93..f5f2fbbd2 100644 --- a/src/daemon/systemd.ts +++ b/src/daemon/systemd.ts @@ -238,6 +238,8 @@ export async function installSystemdService({ throw new Error(`systemctl restart failed: ${restart.stderr || restart.stdout}`.trim()); } + // Ensure we don't end up writing to a clack spinner line (wizards show progress without a newline). + stdout.write("\n"); stdout.write(`${formatLine("Installed systemd service", unitPath)}\n`); return { unitPath }; }