feat(doctor): repair launch agent bootstrap

Co-authored-by: Dr Alexander Mikhalev <alex@metacortex.engineer>
This commit is contained in:
Peter Steinberger
2026-01-18 16:09:55 +00:00
parent d024dceef7
commit 1db0384090
4 changed files with 270 additions and 31 deletions

View File

@@ -1,7 +1,13 @@
import type { ClawdbotConfig } from "../config/config.js";
import { resolveGatewayPort } from "../config/config.js";
import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js";
import { resolveGatewayLaunchAgentLabel, resolveNodeLaunchAgentLabel } from "../daemon/constants.js";
import { readLastGatewayErrorLine } from "../daemon/diagnostics.js";
import {
isLaunchAgentListed,
isLaunchAgentLoaded,
launchAgentPlistExists,
repairLaunchAgentBootstrap,
} from "../daemon/launchd.js";
import { resolveGatewayService } from "../daemon/service.js";
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
import { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js";
@@ -21,6 +27,53 @@ import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
import { healthCommand } from "./health.js";
import { formatHealthCheckFailure } from "./health-format.js";
async function maybeRepairLaunchAgentBootstrap(params: {
env: Record<string, string | undefined>;
title: string;
runtime: RuntimeEnv;
prompter: DoctorPrompter;
}): Promise<boolean> {
if (process.platform !== "darwin") return false;
const listed = await isLaunchAgentListed({ env: params.env });
if (!listed) return false;
const loaded = await isLaunchAgentLoaded({ env: params.env });
if (loaded) return false;
const plistExists = await launchAgentPlistExists(params.env);
if (!plistExists) return false;
note(
"LaunchAgent is listed but not loaded in launchd.",
`${params.title} LaunchAgent`,
);
const shouldFix = await params.prompter.confirmSkipInNonInteractive({
message: `Repair ${params.title} LaunchAgent bootstrap now?`,
initialValue: true,
});
if (!shouldFix) return false;
params.runtime.log(`Bootstrapping ${params.title} LaunchAgent...`);
const repair = await repairLaunchAgentBootstrap({ env: params.env });
if (!repair.ok) {
params.runtime.error(
`${params.title} LaunchAgent bootstrap failed: ${repair.detail ?? "unknown error"}`,
);
return false;
}
const verified = await isLaunchAgentLoaded({ env: params.env });
if (!verified) {
params.runtime.error(`${params.title} LaunchAgent still not loaded after repair.`);
return false;
}
note(`${params.title} LaunchAgent repaired.`, `${params.title} LaunchAgent`);
return true;
}
export async function maybeRepairGatewayDaemon(params: {
cfg: ClawdbotConfig;
runtime: RuntimeEnv;
@@ -32,12 +85,33 @@ export async function maybeRepairGatewayDaemon(params: {
if (params.healthOk) return;
const service = resolveGatewayService();
const loaded = await service.isLoaded({ env: process.env });
let loaded = await service.isLoaded({ env: process.env });
let serviceRuntime: Awaited<ReturnType<typeof service.readRuntime>> | undefined;
if (loaded) {
serviceRuntime = await service.readRuntime(process.env).catch(() => undefined);
}
if (process.platform === "darwin" && params.cfg.gateway?.mode !== "remote") {
const gatewayRepaired = await maybeRepairLaunchAgentBootstrap({
env: process.env,
title: "Gateway",
runtime: params.runtime,
prompter: params.prompter,
});
await maybeRepairLaunchAgentBootstrap({
env: { ...process.env, CLAWDBOT_LAUNCHD_LABEL: resolveNodeLaunchAgentLabel() },
title: "Node",
runtime: params.runtime,
prompter: params.prompter,
});
if (gatewayRepaired) {
loaded = await service.isLoaded({ env: process.env });
if (loaded) {
serviceRuntime = await service.readRuntime(process.env).catch(() => undefined);
}
}
}
if (params.cfg.gateway?.mode !== "remote") {
const port = resolveGatewayPort(params.cfg, process.env);
const diagnostics = await inspectPortUsage(port);