fix: harden onboarding for non-systemd environments

This commit is contained in:
Peter Steinberger
2026-01-09 22:16:17 +01:00
parent 402c35b91c
commit 55e830b009
11 changed files with 409 additions and 170 deletions

View File

@@ -19,6 +19,7 @@ import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
import { resolvePreferredNodePath } from "../daemon/runtime-paths.js";
import { resolveGatewayService } from "../daemon/service.js";
import { buildServiceEnvironment } from "../daemon/service-env.js";
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
import { upsertSharedEnvVar } from "../infra/env-file.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
@@ -429,41 +430,53 @@ export async function runNonInteractiveOnboarding(
const daemonRuntimeRaw = opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
if (opts.installDaemon) {
if (!isGatewayDaemonRuntime(daemonRuntimeRaw)) {
runtime.error("Invalid --daemon-runtime (use node or bun)");
runtime.exit(1);
return;
}
const service = resolveGatewayService();
const devMode =
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
process.argv[1]?.endsWith(".ts");
const nodePath = await resolvePreferredNodePath({
env: process.env,
runtime: daemonRuntimeRaw,
});
const { programArguments, workingDirectory } =
await resolveGatewayProgramArguments({
port,
dev: devMode,
const systemdAvailable =
process.platform === "linux"
? await isSystemdUserServiceAvailable()
: true;
if (process.platform === "linux" && !systemdAvailable) {
runtime.log(
"Systemd user services are unavailable; skipping daemon install.",
);
} else {
if (!isGatewayDaemonRuntime(daemonRuntimeRaw)) {
runtime.error("Invalid --daemon-runtime (use node or bun)");
runtime.exit(1);
return;
}
const service = resolveGatewayService();
const devMode =
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
process.argv[1]?.endsWith(".ts");
const nodePath = await resolvePreferredNodePath({
env: process.env,
runtime: daemonRuntimeRaw,
nodePath,
});
const environment = buildServiceEnvironment({
env: process.env,
port,
token: gatewayToken,
launchdLabel:
process.platform === "darwin" ? GATEWAY_LAUNCH_AGENT_LABEL : undefined,
});
await service.install({
env: process.env,
stdout: process.stdout,
programArguments,
workingDirectory,
environment,
});
await ensureSystemdUserLingerNonInteractive({ runtime });
const { programArguments, workingDirectory } =
await resolveGatewayProgramArguments({
port,
dev: devMode,
runtime: daemonRuntimeRaw,
nodePath,
});
const environment = buildServiceEnvironment({
env: process.env,
port,
token: gatewayToken,
launchdLabel:
process.platform === "darwin"
? GATEWAY_LAUNCH_AGENT_LABEL
: undefined,
});
await service.install({
env: process.env,
stdout: process.stdout,
programArguments,
workingDirectory,
environment,
});
await ensureSystemdUserLingerNonInteractive({ runtime });
}
}
if (!opts.skipHealth) {

View File

@@ -27,6 +27,7 @@ export type ProviderChoice = ChatProviderId;
export type OnboardOptions = {
mode?: OnboardMode;
flow?: "quickstart" | "advanced";
workspace?: string;
nonInteractive?: boolean;
authChoice?: AuthChoice;
@@ -51,8 +52,10 @@ export type OnboardOptions = {
tailscaleResetOnExit?: boolean;
installDaemon?: boolean;
daemonRuntime?: GatewayDaemonRuntime;
skipProviders?: boolean;
skipSkills?: boolean;
skipHealth?: boolean;
skipUi?: boolean;
nodeManager?: NodeManagerChoice;
remoteUrl?: string;
remoteToken?: string;

View File

@@ -2,6 +2,7 @@ import { note as clackNote } from "@clack/prompts";
import {
enableSystemdUserLinger,
isSystemdUserServiceAvailable,
readSystemdUserLingerStatus,
} from "../daemon/systemd.js";
import type { RuntimeEnv } from "../runtime.js";
@@ -32,6 +33,13 @@ export async function ensureSystemdUserLingerInteractive(params: {
const env = params.env ?? process.env;
const prompter = params.prompter ?? { note };
const title = params.title ?? "Systemd";
if (!(await isSystemdUserServiceAvailable())) {
await prompter.note(
"Systemd user services are unavailable. Skipping lingering checks.",
title,
);
return;
}
const status = await readSystemdUserLingerStatus(env);
if (!status) {
await prompter.note(
@@ -98,6 +106,7 @@ export async function ensureSystemdUserLingerNonInteractive(params: {
}): Promise<void> {
if (process.platform !== "linux") return;
const env = params.env ?? process.env;
if (!(await isSystemdUserServiceAvailable())) return;
const status = await readSystemdUserLingerStatus(env);
if (!status || status.linger === "yes") return;