From 3129e59c63fad11d3d782456c96898371ecc92de Mon Sep 17 00:00:00 2001 From: Ayush Ojha Date: Thu, 29 Jan 2026 23:51:37 -0800 Subject: [PATCH] fix(doctor): show cleanup hints for detected services, not default openclaw service renderGatewayServiceCleanupHints() always generated cleanup commands for the current openclaw service (ai.openclaw.gateway, openclaw-gateway, etc.) regardless of which extra/legacy services were actually detected. Now accepts the detected ExtraGatewayService[] array and generates platform-specific cleanup hints using each service's actual label and detail paths (plist/unit paths). Fixes #4454 --- src/cli/daemon-cli.coverage.test.ts | 2 +- src/cli/daemon-cli/status.gather.ts | 4 +- src/cli/daemon-cli/status.print.ts | 2 +- src/commands/doctor-gateway-services.ts | 2 +- src/daemon/inspect.ts | 59 ++++++++++++++++--------- 5 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/cli/daemon-cli.coverage.test.ts b/src/cli/daemon-cli.coverage.test.ts index a458069e7..a6da28a7d 100644 --- a/src/cli/daemon-cli.coverage.test.ts +++ b/src/cli/daemon-cli.coverage.test.ts @@ -59,7 +59,7 @@ vi.mock("../daemon/legacy.js", () => ({ vi.mock("../daemon/inspect.js", () => ({ findExtraGatewayServices: (env: unknown, opts?: unknown) => findExtraGatewayServices(env, opts), - renderGatewayServiceCleanupHints: () => [], + renderGatewayServiceCleanupHints: (_services: unknown[]) => [], })); vi.mock("../infra/ports.js", () => ({ diff --git a/src/cli/daemon-cli/status.gather.ts b/src/cli/daemon-cli/status.gather.ts index 7b04c0b30..ba534034e 100644 --- a/src/cli/daemon-cli/status.gather.ts +++ b/src/cli/daemon-cli/status.gather.ts @@ -6,7 +6,7 @@ import { } from "../../config/config.js"; import type { GatewayBindMode, GatewayControlUiConfig } from "../../config/types.js"; import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js"; -import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js"; +import type { ExtraGatewayService, FindExtraGatewayServicesOptions } from "../../daemon/inspect.js"; import { findExtraGatewayServices } from "../../daemon/inspect.js"; import { resolveGatewayService } from "../../daemon/service.js"; import type { ServiceConfigAudit } from "../../daemon/service-audit.js"; @@ -92,7 +92,7 @@ export type DaemonStatus = { error?: string; url?: string; }; - extraServices: Array<{ label: string; detail: string; scope: string }>; + extraServices: ExtraGatewayService[]; }; function shouldReportPortUsage(status: PortUsageStatus | undefined, rpcOk?: boolean) { diff --git a/src/cli/daemon-cli/status.print.ts b/src/cli/daemon-cli/status.print.ts index ca6d1d440..d24f23cc2 100644 --- a/src/cli/daemon-cli/status.print.ts +++ b/src/cli/daemon-cli/status.print.ts @@ -292,7 +292,7 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) for (const svc of extraServices) { defaultRuntime.error(`- ${errorText(svc.label)} (${svc.scope}, ${svc.detail})`); } - for (const hint of renderGatewayServiceCleanupHints()) { + for (const hint of renderGatewayServiceCleanupHints(extraServices)) { defaultRuntime.error(`${errorText("Cleanup hint:")} ${hint}`); } spacer(); diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index 7ca23a130..93521f848 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -248,7 +248,7 @@ export async function maybeScanExtraGatewayServices( } } - const cleanupHints = renderGatewayServiceCleanupHints(); + const cleanupHints = renderGatewayServiceCleanupHints(extraServices); if (cleanupHints.length > 0) { note(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints"); } diff --git a/src/daemon/inspect.ts b/src/daemon/inspect.ts index c1e83305c..8f4d59b88 100644 --- a/src/daemon/inspect.ts +++ b/src/daemon/inspect.ts @@ -27,29 +27,46 @@ export type FindExtraGatewayServicesOptions = { const EXTRA_MARKERS = ["openclaw", "clawdbot", "moltbot"] as const; const execFileAsync = promisify(execFile); -export function renderGatewayServiceCleanupHints( - env: Record = process.env as Record, -): string[] { - const profile = env.OPENCLAW_PROFILE; - switch (process.platform) { - case "darwin": { - const label = resolveGatewayLaunchAgentLabel(profile); - return [`launchctl bootout gui/$UID/${label}`, `rm ~/Library/LaunchAgents/${label}.plist`]; +export function renderGatewayServiceCleanupHints(services: ExtraGatewayService[]): string[] { + const hints: string[] = []; + for (const svc of services) { + switch (svc.platform) { + case "darwin": { + hints.push(`launchctl bootout gui/$UID/${svc.label}`); + const plistPath = extractPlistPath(svc.detail); + if (plistPath) { + hints.push(`rm ${plistPath}`); + } + break; + } + case "linux": { + const unitName = svc.label.endsWith(".service") ? svc.label : `${svc.label}.service`; + hints.push(`systemctl --user disable --now ${unitName}`); + const unitPath = extractUnitPath(svc.detail); + if (unitPath) { + hints.push(`rm ${unitPath}`); + } + break; + } + case "win32": { + hints.push(`schtasks /Delete /TN "${svc.label}" /F`); + break; + } } - case "linux": { - const unit = resolveGatewaySystemdServiceName(profile); - return [ - `systemctl --user disable --now ${unit}.service`, - `rm ~/.config/systemd/user/${unit}.service`, - ]; - } - case "win32": { - const task = resolveGatewayWindowsTaskName(profile); - return [`schtasks /Delete /TN "${task}" /F`]; - } - default: - return []; } + return hints; +} + +function extractPlistPath(detail: string): string | null { + if (!detail.startsWith("plist:")) return null; + const value = detail.slice("plist:".length).trim(); + return value.length > 0 ? value : null; +} + +function extractUnitPath(detail: string): string | null { + if (!detail.startsWith("unit:")) return null; + const value = detail.slice("unit:".length).trim(); + return value.length > 0 ? value : null; } function resolveHomeDir(env: Record): string {