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

@@ -51,6 +51,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 { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
@@ -120,14 +121,26 @@ export async function runOnboardingWizard(
const quickstartHint = "Configure details later via clawdbot configure.";
const advancedHint = "Configure port, network, Tailscale, and auth options.";
let flow = (await prompter.select({
message: "Onboarding mode",
options: [
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
{ value: "advanced", label: "Advanced", hint: advancedHint },
],
initialValue: "quickstart",
})) as "quickstart" | "advanced";
const explicitFlow = opts.flow?.trim();
if (
explicitFlow &&
explicitFlow !== "quickstart" &&
explicitFlow !== "advanced"
) {
runtime.error("Invalid --flow (use quickstart or advanced).");
runtime.exit(1);
return;
}
let flow =
explicitFlow ??
((await prompter.select({
message: "Onboarding mode",
options: [
{ value: "quickstart", label: "QuickStart", hint: quickstartHint },
{ value: "advanced", label: "Advanced", hint: advancedHint },
],
initialValue: "quickstart",
})) as "quickstart" | "advanced");
if (opts.mode === "remote" && flow === "quickstart") {
await prompter.note(
@@ -309,14 +322,16 @@ export async function runOnboardingWizard(
const authStore = ensureAuthProfileStore(undefined, {
allowKeychainPrompt: false,
});
const authChoice = (await prompter.select({
message: "Model/auth choice",
options: buildAuthChoiceOptions({
store: authStore,
includeSkip: true,
includeClaudeCliIfMissing: true,
}),
})) as AuthChoice;
const authChoice =
opts.authChoice ??
((await prompter.select({
message: "Model/auth choice",
options: buildAuthChoiceOptions({
store: authStore,
includeSkip: true,
includeClaudeCliIfMissing: true,
}),
})) as AuthChoice);
const authResult = await applyAuthChoice({
authChoice,
@@ -501,14 +516,18 @@ export async function runOnboardingWizard(
},
};
nextConfig = await setupProviders(nextConfig, runtime, prompter, {
allowSignalInstall: true,
forceAllowFromProviders:
flow === "quickstart" ? ["telegram", "whatsapp"] : [],
skipDmPolicyPrompt: flow === "quickstart",
skipConfirm: flow === "quickstart",
quickstartDefaults: flow === "quickstart",
});
if (opts.skipProviders) {
await prompter.note("Skipping provider setup.", "Providers");
} else {
nextConfig = await setupProviders(nextConfig, runtime, prompter, {
allowSignalInstall: true,
forceAllowFromProviders:
flow === "quickstart" ? ["telegram", "whatsapp"] : [],
skipDmPolicyPrompt: flow === "quickstart",
skipConfirm: flow === "quickstart",
quickstartDefaults: flow === "quickstart",
});
}
await writeConfigFile(nextConfig);
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
@@ -516,28 +535,59 @@ export async function runOnboardingWizard(
skipBootstrap: Boolean(nextConfig.agents?.defaults?.skipBootstrap),
});
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
if (opts.skipSkills) {
await prompter.note("Skipping skills setup.", "Skills");
} else {
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
}
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
await writeConfigFile(nextConfig);
await ensureSystemdUserLingerInteractive({
runtime,
prompter: {
confirm: prompter.confirm,
note: prompter.note,
},
reason:
"Linux installs use a systemd user service by default. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.",
requireConfirm: false,
});
const systemdAvailable =
process.platform === "linux" ? await isSystemdUserServiceAvailable() : true;
if (process.platform === "linux" && !systemdAvailable) {
await prompter.note(
"Systemd user services are unavailable. Skipping lingering checks and daemon install.",
"Systemd",
);
}
const installDaemon =
flow === "quickstart"
? true
: await prompter.confirm({
message: "Install Gateway daemon (recommended)",
initialValue: true,
});
if (process.platform === "linux" && systemdAvailable) {
await ensureSystemdUserLingerInteractive({
runtime,
prompter: {
confirm: prompter.confirm,
note: prompter.note,
},
reason:
"Linux installs use a systemd user service by default. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.",
requireConfirm: false,
});
}
const explicitInstallDaemon =
typeof opts.installDaemon === "boolean" ? opts.installDaemon : undefined;
let installDaemon: boolean;
if (explicitInstallDaemon !== undefined) {
installDaemon = explicitInstallDaemon;
} else if (process.platform === "linux" && !systemdAvailable) {
installDaemon = false;
} else if (flow === "quickstart") {
installDaemon = true;
} else {
installDaemon = await prompter.confirm({
message: "Install Gateway daemon (recommended)",
initialValue: true,
});
}
if (process.platform === "linux" && !systemdAvailable && installDaemon) {
await prompter.note(
"Systemd user services are unavailable; skipping daemon install. Use your container supervisor or `docker compose up -d`.",
"Gateway daemon",
);
installDaemon = false;
}
if (installDaemon) {
const daemonRuntime =
@@ -609,19 +659,21 @@ export async function runOnboardingWizard(
}
}
await sleep(1500);
try {
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
} catch (err) {
runtime.error(`Health check failed: ${String(err)}`);
await prompter.note(
[
"Docs:",
"https://docs.clawd.bot/gateway/health",
"https://docs.clawd.bot/gateway/troubleshooting",
].join("\n"),
"Health check help",
);
if (!opts.skipHealth) {
await sleep(1500);
try {
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
} catch (err) {
runtime.error(`Health check failed: ${String(err)}`);
await prompter.note(
[
"Docs:",
"https://docs.clawd.bot/gateway/health",
"https://docs.clawd.bot/gateway/troubleshooting",
].join("\n"),
"Health check help",
);
}
}
const controlUiAssets = await ensureControlUiAssetsBuilt(runtime);
@@ -676,7 +728,7 @@ export async function runOnboardingWizard(
"Control UI",
);
if (gatewayProbe.ok) {
if (!opts.skipUi && gatewayProbe.ok) {
if (hasBootstrap) {
await prompter.note(
[
@@ -731,6 +783,8 @@ export async function runOnboardingWizard(
}
}
}
} else if (opts.skipUi) {
await prompter.note("Skipping Control UI/TUI prompts.", "Control UI");
}
await prompter.note(