fix: honor gateway service override labels

This commit is contained in:
Peter Steinberger
2026-01-13 05:58:46 +00:00
parent 42ff634a9d
commit 5918def440
16 changed files with 128 additions and 35 deletions

View File

@@ -367,7 +367,10 @@ async function gatherDaemonStatus(opts: {
const service = resolveGatewayService();
const [loaded, command, runtime] = await Promise.all([
service
.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })
.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
})
.catch(() => false),
service.readCommand(process.env).catch(() => null),
service.readRuntime(process.env).catch(() => undefined),
@@ -899,7 +902,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false;
try {
loaded = await service.isLoaded({ profile });
loaded = await service.isLoaded({ env: process.env, profile });
} catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1);
@@ -980,7 +983,7 @@ export async function runDaemonStart() {
const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false;
try {
loaded = await service.isLoaded({ profile });
loaded = await service.isLoaded({ env: process.env, profile });
} catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1);
@@ -994,7 +997,11 @@ export async function runDaemonStart() {
return;
}
try {
await service.restart({ profile, stdout: process.stdout });
await service.restart({
env: process.env,
profile,
stdout: process.stdout,
});
} catch (err) {
defaultRuntime.error(`Gateway start failed: ${String(err)}`);
for (const hint of renderGatewayServiceStartHints()) {
@@ -1009,7 +1016,7 @@ export async function runDaemonStop() {
const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false;
try {
loaded = await service.isLoaded({ profile });
loaded = await service.isLoaded({ env: process.env, profile });
} catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1);
@@ -1020,7 +1027,7 @@ export async function runDaemonStop() {
return;
}
try {
await service.stop({ profile, stdout: process.stdout });
await service.stop({ env: process.env, profile, stdout: process.stdout });
} catch (err) {
defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
defaultRuntime.exit(1);
@@ -1037,7 +1044,7 @@ export async function runDaemonRestart(): Promise<boolean> {
const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false;
try {
loaded = await service.isLoaded({ profile });
loaded = await service.isLoaded({ env: process.env, profile });
} catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1);
@@ -1051,7 +1058,11 @@ export async function runDaemonRestart(): Promise<boolean> {
return false;
}
try {
await service.restart({ profile, stdout: process.stdout });
await service.restart({
env: process.env,
profile,
stdout: process.stdout,
});
return true;
} catch (err) {
defaultRuntime.error(`Gateway restart failed: ${String(err)}`);

View File

@@ -399,7 +399,10 @@ async function maybeExplainGatewayServiceStop() {
const service = resolveGatewayService();
let loaded: boolean | null = null;
try {
loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
loaded = await service.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
} catch {
loaded = null;
}

View File

@@ -473,6 +473,7 @@ async function maybeInstallDaemon(params: {
}) {
const service = resolveGatewayService();
const loaded = await service.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
let shouldCheckLinger = false;
@@ -492,6 +493,7 @@ async function maybeInstallDaemon(params: {
);
if (action === "restart") {
await service.restart({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout,
});

View File

@@ -100,6 +100,7 @@ export async function maybeMigrateLegacyGatewayService(
const service = resolveGatewayService();
const loaded = await service.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
if (loaded) {

View File

@@ -450,6 +450,7 @@ export async function doctorCommand(
let loaded = false;
try {
loaded = await service.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
} catch {
@@ -575,6 +576,7 @@ export async function doctorCommand(
if (!healthOk) {
const service = resolveGatewayService();
const loaded = await service.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
let serviceRuntime:
@@ -676,6 +678,7 @@ export async function doctorCommand(
});
if (start) {
await service.restart({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout,
});
@@ -698,6 +701,7 @@ export async function doctorCommand(
});
if (restart) {
await service.restart({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout,
});

View File

@@ -47,14 +47,14 @@ async function stopGatewayIfRunning(runtime: RuntimeEnv) {
const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false;
try {
loaded = await service.isLoaded({ profile });
loaded = await service.isLoaded({ env: process.env, profile });
} catch (err) {
runtime.error(`Gateway service check failed: ${String(err)}`);
return;
}
if (!loaded) return;
try {
await service.stop({ profile, stdout: process.stdout });
await service.stop({ env: process.env, profile, stdout: process.stdout });
} catch (err) {
runtime.error(`Gateway stop failed: ${String(err)}`);
}

View File

@@ -167,7 +167,10 @@ export async function statusAllCommand(
const service = resolveGatewayService();
const [loaded, runtimeInfo, command] = await Promise.all([
service
.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })
.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
})
.catch(() => false),
service.readRuntime(process.env).catch(() => undefined),
service.readCommand(process.env).catch(() => null),

View File

@@ -336,7 +336,10 @@ async function getDaemonStatusSummary(): Promise<{
const service = resolveGatewayService();
const [loaded, runtime, command] = await Promise.all([
service
.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })
.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
})
.catch(() => false),
service.readRuntime(process.env).catch(() => undefined),
service.readCommand(process.env).catch(() => null),

View File

@@ -70,7 +70,7 @@ async function stopAndUninstallService(runtime: RuntimeEnv): Promise<boolean> {
const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false;
try {
loaded = await service.isLoaded({ profile });
loaded = await service.isLoaded({ env: process.env, profile });
} catch (err) {
runtime.error(`Gateway service check failed: ${String(err)}`);
return false;
@@ -80,7 +80,7 @@ async function stopAndUninstallService(runtime: RuntimeEnv): Promise<boolean> {
return true;
}
try {
await service.stop({ profile, stdout: process.stdout });
await service.stop({ env: process.env, profile, stdout: process.stdout });
} catch (err) {
runtime.error(`Gateway stop failed: ${String(err)}`);
}

View File

@@ -19,6 +19,15 @@ const formatLine = (label: string, value: string) => {
const rich = isRich();
return `${colorize(rich, theme.muted, `${label}:`)} ${colorize(rich, theme.command, value)}`;
};
function resolveLaunchAgentLabel(params?: {
env?: Record<string, string | undefined>;
profile?: string;
}): string {
const envLabel = params?.env?.CLAWDBOT_LAUNCHD_LABEL?.trim();
if (envLabel) return envLabel;
return resolveGatewayLaunchAgentLabel(params?.profile);
}
function resolveHomeDir(env: Record<string, string | undefined>): string {
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
if (!home) throw new Error("Missing HOME");
@@ -284,9 +293,12 @@ export function parseLaunchctlPrint(output: string): LaunchctlPrintInfo {
return info;
}
export async function isLaunchAgentLoaded(profile?: string): Promise<boolean> {
export async function isLaunchAgentLoaded(params?: {
env?: Record<string, string | undefined>;
profile?: string;
}): Promise<boolean> {
const domain = resolveGuiDomain();
const label = resolveGatewayLaunchAgentLabel(profile);
const label = resolveLaunchAgentLabel(params);
const res = await execLaunchctl(["print", `${domain}/${label}`]);
return res.code === 0;
}
@@ -461,13 +473,15 @@ function isLaunchctlNotLoaded(res: {
export async function stopLaunchAgent({
stdout,
env,
profile,
}: {
stdout: NodeJS.WritableStream;
env?: Record<string, string | undefined>;
profile?: string;
}): Promise<void> {
const domain = resolveGuiDomain();
const label = resolveGatewayLaunchAgentLabel(profile);
const label = resolveLaunchAgentLabel({ env, profile });
const res = await execLaunchctl(["bootout", `${domain}/${label}`]);
if (res.code !== 0 && !isLaunchctlNotLoaded(res)) {
throw new Error(
@@ -548,13 +562,15 @@ export async function installLaunchAgent({
export async function restartLaunchAgent({
stdout,
env,
profile,
}: {
stdout: NodeJS.WritableStream;
env?: Record<string, string | undefined>;
profile?: string;
}): Promise<void> {
const domain = resolveGuiDomain();
const label = resolveGatewayLaunchAgentLabel(profile);
const label = resolveLaunchAgentLabel({ env, profile });
const res = await execLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
if (res.code !== 0) {
throw new Error(

View File

@@ -45,14 +45,19 @@ export type GatewayService = {
stdout: NodeJS.WritableStream;
}) => Promise<void>;
stop: (args: {
env?: Record<string, string | undefined>;
profile?: string;
stdout: NodeJS.WritableStream;
}) => Promise<void>;
restart: (args: {
env?: Record<string, string | undefined>;
profile?: string;
stdout: NodeJS.WritableStream;
}) => Promise<void>;
isLoaded: (args: { profile?: string }) => Promise<boolean>;
isLoaded: (args: {
env?: Record<string, string | undefined>;
profile?: string;
}) => Promise<boolean>;
readCommand: (env: Record<string, string | undefined>) => Promise<{
programArguments: string[];
workingDirectory?: string;
@@ -77,15 +82,21 @@ export function resolveGatewayService(): GatewayService {
await uninstallLaunchAgent(args);
},
stop: async (args) => {
await stopLaunchAgent({ stdout: args.stdout, profile: args.profile });
await stopLaunchAgent({
stdout: args.stdout,
profile: args.profile,
env: args.env,
});
},
restart: async (args) => {
await restartLaunchAgent({
stdout: args.stdout,
profile: args.profile,
env: args.env,
});
},
isLoaded: async (args) => isLaunchAgentLoaded(args.profile),
isLoaded: async (args) =>
isLaunchAgentLoaded({ profile: args.profile, env: args.env }),
readCommand: readLaunchAgentProgramArguments,
readRuntime: readLaunchAgentRuntime,
};
@@ -106,15 +117,18 @@ export function resolveGatewayService(): GatewayService {
await stopSystemdService({
stdout: args.stdout,
profile: args.profile,
env: args.env,
});
},
restart: async (args) => {
await restartSystemdService({
stdout: args.stdout,
profile: args.profile,
env: args.env,
});
},
isLoaded: async (args) => isSystemdServiceEnabled(args.profile),
isLoaded: async (args) =>
isSystemdServiceEnabled({ profile: args.profile, env: args.env }),
readCommand: readSystemdServiceExecStart,
readRuntime: async (env) => await readSystemdServiceRuntime(env),
};
@@ -132,7 +146,10 @@ export function resolveGatewayService(): GatewayService {
await uninstallScheduledTask(args);
},
stop: async (args) => {
await stopScheduledTask({ stdout: args.stdout, profile: args.profile });
await stopScheduledTask({
stdout: args.stdout,
profile: args.profile,
});
},
restart: async (args) => {
await restartScheduledTask({

View File

@@ -46,6 +46,14 @@ function resolveSystemdServiceName(
return resolveGatewaySystemdServiceName(env.CLAWDBOT_PROFILE);
}
function resolveSystemdServiceNameFromParams(params?: {
env?: Record<string, string | undefined>;
profile?: string;
}): string {
if (params?.env) return resolveSystemdServiceName(params.env);
return resolveGatewaySystemdServiceName(params?.profile);
}
function resolveSystemdUnitPath(
env: Record<string, string | undefined>,
): string {
@@ -466,13 +474,15 @@ export async function uninstallSystemdService({
export async function stopSystemdService({
stdout,
env,
profile,
}: {
stdout: NodeJS.WritableStream;
env?: Record<string, string | undefined>;
profile?: string;
}): Promise<void> {
await assertSystemdAvailable();
const serviceName = resolveGatewaySystemdServiceName(profile);
const serviceName = resolveSystemdServiceNameFromParams({ env, profile });
const unitName = `${serviceName}.service`;
const res = await execSystemctl(["--user", "stop", unitName]);
if (res.code !== 0) {
@@ -485,13 +495,15 @@ export async function stopSystemdService({
export async function restartSystemdService({
stdout,
env,
profile,
}: {
stdout: NodeJS.WritableStream;
env?: Record<string, string | undefined>;
profile?: string;
}): Promise<void> {
await assertSystemdAvailable();
const serviceName = resolveGatewaySystemdServiceName(profile);
const serviceName = resolveSystemdServiceNameFromParams({ env, profile });
const unitName = `${serviceName}.service`;
const res = await execSystemctl(["--user", "restart", unitName]);
if (res.code !== 0) {
@@ -502,11 +514,12 @@ export async function restartSystemdService({
stdout.write(`${formatLine("Restarted systemd service", unitName)}\n`);
}
export async function isSystemdServiceEnabled(
profile?: string,
): Promise<boolean> {
export async function isSystemdServiceEnabled(params?: {
env?: Record<string, string | undefined>;
profile?: string;
}): Promise<boolean> {
await assertSystemdAvailable();
const serviceName = resolveGatewaySystemdServiceName(profile);
const serviceName = resolveSystemdServiceNameFromParams(params);
const unitName = `${serviceName}.service`;
const res = await execSystemctl(["--user", "is-enabled", unitName]);
return res.code === 0;

View File

@@ -697,6 +697,7 @@ export async function runOnboardingWizard(
}
const service = resolveGatewayService();
const loaded = await service.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
if (loaded) {
@@ -710,6 +711,7 @@ export async function runOnboardingWizard(
})) as "restart" | "reinstall" | "skip";
if (action === "restart") {
await service.restart({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout,
});
@@ -721,8 +723,10 @@ export async function runOnboardingWizard(
if (
!loaded ||
(loaded &&
(await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })) ===
false)
(await service.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
})) === false)
) {
const devMode =
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&