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
This commit is contained in:
Ayush Ojha 2026-01-29 23:51:37 -08:00
parent 9025da2296
commit 3129e59c63
5 changed files with 43 additions and 26 deletions

View File

@ -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", () => ({

View File

@ -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) {

View File

@ -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();

View File

@ -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");
}

View File

@ -27,29 +27,46 @@ export type FindExtraGatewayServicesOptions = {
const EXTRA_MARKERS = ["openclaw", "clawdbot", "moltbot"] as const;
const execFileAsync = promisify(execFile);
export function renderGatewayServiceCleanupHints(
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
): 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, string | undefined>): string {