fix: migrate legacy gateway services

This commit is contained in:
Peter Steinberger
2026-01-30 04:01:31 +01:00
parent d47b4e6f81
commit 02576615cb
11 changed files with 165 additions and 27 deletions

View File

@@ -1,4 +1,8 @@
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import type { OpenClawConfig } from "../config/config.js";
import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js";
@@ -16,6 +20,8 @@ import { buildGatewayInstallPlan } from "./daemon-install-helpers.js";
import { DEFAULT_GATEWAY_DAEMON_RUNTIME, type GatewayDaemonRuntime } from "./daemon-runtime.js";
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
const execFileAsync = promisify(execFile);
function detectGatewayRuntime(programArguments: string[] | undefined): GatewayDaemonRuntime {
const first = programArguments?.[0];
if (first) {
@@ -37,6 +43,42 @@ function normalizeExecutablePath(value: string): string {
return path.resolve(value);
}
function extractDetailPath(detail: string, prefix: string): string | null {
if (!detail.startsWith(prefix)) return null;
const value = detail.slice(prefix.length).trim();
return value.length > 0 ? value : null;
}
async function cleanupLegacyLaunchdService(params: {
label: string;
plistPath: string;
}): Promise<string | null> {
const domain = typeof process.getuid === "function" ? `gui/${process.getuid()}` : "gui/501";
await execFileAsync("launchctl", ["bootout", domain, params.plistPath]).catch(() => undefined);
await execFileAsync("launchctl", ["unload", params.plistPath]).catch(() => undefined);
const trashDir = path.join(os.homedir(), ".Trash");
try {
await fs.mkdir(trashDir, { recursive: true });
} catch {
// ignore
}
try {
await fs.access(params.plistPath);
} catch {
return null;
}
const dest = path.join(trashDir, `${params.label}-${Date.now()}.plist`);
try {
await fs.rename(params.plistPath, dest);
return dest;
} catch {
return null;
}
}
export async function maybeRepairGatewayServiceConfig(
cfg: OpenClawConfig,
mode: "local" | "remote",
@@ -150,7 +192,11 @@ export async function maybeRepairGatewayServiceConfig(
}
}
export async function maybeScanExtraGatewayServices(options: DoctorOptions) {
export async function maybeScanExtraGatewayServices(
options: DoctorOptions,
runtime: RuntimeEnv,
prompter: DoctorPrompter,
) {
const extraServices = await findExtraGatewayServices(process.env, {
deep: options.deep,
});
@@ -161,6 +207,47 @@ export async function maybeScanExtraGatewayServices(options: DoctorOptions) {
"Other gateway-like services detected",
);
const legacyServices = extraServices.filter((svc) => svc.legacy === true);
if (legacyServices.length > 0) {
const shouldRemove = await prompter.confirmSkipInNonInteractive({
message: "Remove legacy gateway services (clawdbot/moltbot) now?",
initialValue: true,
});
if (shouldRemove) {
const removed: string[] = [];
const failed: string[] = [];
for (const svc of legacyServices) {
if (svc.platform !== "darwin") {
failed.push(`${svc.label} (${svc.platform})`);
continue;
}
if (svc.scope !== "user") {
failed.push(`${svc.label} (${svc.scope})`);
continue;
}
const plistPath = extractDetailPath(svc.detail, "plist:");
if (!plistPath) {
failed.push(`${svc.label} (missing plist path)`);
continue;
}
const dest = await cleanupLegacyLaunchdService({
label: svc.label,
plistPath,
});
removed.push(dest ? `${svc.label} -> ${dest}` : svc.label);
}
if (removed.length > 0) {
note(removed.map((line) => `- ${line}`).join("\n"), "Legacy gateway removed");
}
if (failed.length > 0) {
note(failed.map((line) => `- ${line}`).join("\n"), "Legacy gateway cleanup skipped");
}
if (removed.length > 0) {
runtime.log("Legacy gateway services removed. Installing OpenClaw gateway next.");
}
}
}
const cleanupHints = renderGatewayServiceCleanupHints();
if (cleanupHints.length > 0) {
note(cleanupHints.map((hint) => `- ${hint}`).join("\n"), "Cleanup hints");

View File

@@ -185,7 +185,7 @@ export async function doctorCommand(
cfg = await maybeRepairSandboxImages(cfg, runtime, prompter);
noteSandboxScopeWarnings(cfg);
await maybeScanExtraGatewayServices(options);
await maybeScanExtraGatewayServices(options, runtime, prompter);
await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter);
await noteMacLaunchAgentOverrides();
await noteMacLaunchctlGatewayEnvOverrides(cfg);

View File

@@ -65,9 +65,11 @@ export function randomToken(): string {
export function printWizardHeader(runtime: RuntimeEnv) {
const header = [
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
"█████░█████░█████░█░░░█░█████░█░░░░░███░█░░░█",
"█░░░█░█░░░█░███░░░██░░█░█░░░░░█░░░░░█░░░█░█░░█",
"█████░████░░█████░█░░██░█████░█████░█████░█░██",
"█████░█████░█████░█░░░█░█████░█░░░░░█████░█░░░█",
"█░░░█░█░░░█░█░░░░░██░░█░█░░░░░█░░░░░█░░░█░█░░█",
"█░░░█░█████░████░░█░█░█░█░░░░░█░░░░░█████░█░██",
"█░░░█░█░░░░░█░░░░░█░░██░█░░░░░█░░░░░█░░░█░██░██",
"█████░█░░░░░█████░█░░░█░█████░█████░█░░░█░█░░░█",
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
" 🦞 FRESH DAILY 🦞 ",
" ",