import { assertNoSystemLaunchDaemonOwnership } from "../../daemon/launchd-system.js"; // macOS LaunchAgent recovery helper for daemon lifecycle commands. import { formatLaunchAgentGuiSessionError, launchAgentPlistExists, repairLaunchAgentBootstrap, resolveLaunchAgentLabel, } from "../../daemon/launchd.js"; const LAUNCH_AGENT_RECOVERY_MESSAGE = "Gateway LaunchAgent was installed but not loaded; re-bootstrapped launchd service."; type LaunchAgentRecoveryAction = "started" | "restarted"; type LaunchAgentRecoveryResult = { result: TResult; loaded: true; message: string; }; /** Re-bootstrap an installed but unloaded LaunchAgent after a daemon start/restart command. */ export async function recoverInstalledLaunchAgent< TResult extends LaunchAgentRecoveryAction, >(params: { result: TResult; env?: Record; }): Promise | null> { if (process.platform !== "darwin") { return null; } const env = params.env ?? (process.env as Record); // Check host-wide ownership even when no user plist exists. Otherwise start // and restart would report "not installed" for a gateway already supervised // by a same-label system LaunchDaemon. await assertNoSystemLaunchDaemonOwnership(resolveLaunchAgentLabel(env)); const plistExists = await launchAgentPlistExists(env).catch(() => false); if (!plistExists) { return null; } const repaired = await repairLaunchAgentBootstrap({ env }).catch(() => ({ ok: false as const, status: "bootstrap-failed" as const, })); if (!repaired.ok) { if ( repaired.status === "system-launchdaemon-conflict" || repaired.status === "system-launchdaemon-unverifiable" ) { throw new Error(repaired.detail); } if (repaired.status === "gui-session-unavailable") { const actionHint = params.result === "started" ? "openclaw gateway start" : "openclaw gateway restart"; throw new Error( formatLaunchAgentGuiSessionError({ detail: repaired.detail, domain: repaired.domain, actionHint, }), ); } return null; } return { result: params.result, loaded: true, message: LAUNCH_AGENT_RECOVERY_MESSAGE, }; }