fix: harden onboarding for non-systemd environments
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user