From 9de762faa24e8cb1c3d012e4bbdc4cb7b40fbb1a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 23:29:30 +0000 Subject: [PATCH] refactor: unify gateway daemon install plan --- src/cli/daemon-cli/install.ts | 41 ++--------- src/commands/configure.daemon.ts | 45 ++---------- src/commands/daemon-install-helpers.ts | 70 +++++++++++++++++++ src/commands/doctor-gateway-daemon-flow.ts | 59 +++++----------- src/commands/doctor-gateway-services.ts | 65 ++++++----------- .../local/daemon-install.ts | 49 +++---------- src/wizard/onboarding.finalize.ts | 51 +++----------- 7 files changed, 140 insertions(+), 240 deletions(-) create mode 100644 src/commands/daemon-install-helpers.ts diff --git a/src/cli/daemon-cli/install.ts b/src/cli/daemon-cli/install.ts index a489af526..169451b92 100644 --- a/src/cli/daemon-cli/install.ts +++ b/src/cli/daemon-cli/install.ts @@ -1,19 +1,11 @@ -import path from "node:path"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, isGatewayDaemonRuntime, } from "../../commands/daemon-runtime.js"; +import { buildGatewayInstallPlan } from "../../commands/daemon-install-helpers.js"; import { loadConfig, resolveGatewayPort } from "../../config/config.js"; import { resolveIsNixMode } from "../../config/paths.js"; -import { resolveGatewayLaunchAgentLabel } from "../../daemon/constants.js"; -import { resolveGatewayProgramArguments } from "../../daemon/program-args.js"; -import { - renderSystemNodeWarning, - resolvePreferredNodePath, - resolveSystemNodeInfo, -} from "../../daemon/runtime-paths.js"; import { resolveGatewayService } from "../../daemon/service.js"; -import { buildServiceEnvironment } from "../../daemon/service-env.js"; import { defaultRuntime } from "../../runtime.js"; import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js"; import { parsePort } from "./shared.js"; @@ -96,34 +88,15 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) { } } - const devMode = - process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts"); - const nodePath = await resolvePreferredNodePath({ - env: process.env, - runtime: runtimeRaw, - }); - const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ - port, - dev: devMode, - runtime: runtimeRaw, - nodePath, - }); - if (runtimeRaw === "node") { - const systemNode = await resolveSystemNodeInfo({ env: process.env }); - const warning = renderSystemNodeWarning(systemNode, programArguments[0]); - if (warning) { - if (json) warnings.push(warning); - else defaultRuntime.log(warning); - } - } - const environment = buildServiceEnvironment({ + const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port, token: opts.token || cfg.gateway?.auth?.token || process.env.CLAWDBOT_GATEWAY_TOKEN, - launchdLabel: - process.platform === "darwin" - ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) - : undefined, + runtime: runtimeRaw, + warn: (message) => { + if (json) warnings.push(message); + else defaultRuntime.log(message); + }, }); try { diff --git a/src/commands/configure.daemon.ts b/src/commands/configure.daemon.ts index 02ef3a360..44f6ed457 100644 --- a/src/commands/configure.daemon.ts +++ b/src/commands/configure.daemon.ts @@ -1,13 +1,5 @@ -import path from "node:path"; -import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; -import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; -import { - renderSystemNodeWarning, - resolvePreferredNodePath, - resolveSystemNodeInfo, -} from "../daemon/runtime-paths.js"; +import { buildGatewayInstallPlan, gatewayInstallErrorHint } from "./daemon-install-helpers.js"; import { resolveGatewayService } from "../daemon/service.js"; -import { buildServiceEnvironment } from "../daemon/service-env.js"; import { withProgress } from "../cli/progress.js"; import type { RuntimeEnv } from "../runtime.js"; import { note } from "../terminal/note.js"; @@ -89,32 +81,12 @@ export async function maybeInstallDaemon(params: { progress.setLabel("Preparing Gateway daemon…"); - const devMode = - process.argv[1]?.includes(`${path.sep}src${path.sep}`) && - process.argv[1]?.endsWith(".ts"); - const nodePath = await resolvePreferredNodePath({ - env: process.env, - runtime: daemonRuntime, - }); - const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ - port: params.port, - dev: devMode, - runtime: daemonRuntime, - nodePath, - }); - if (daemonRuntime === "node") { - const systemNode = await resolveSystemNodeInfo({ env: process.env }); - const warning = renderSystemNodeWarning(systemNode, programArguments[0]); - if (warning) note(warning, "Gateway runtime"); - } - const environment = buildServiceEnvironment({ + const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port: params.port, token: params.gatewayToken, - launchdLabel: - process.platform === "darwin" - ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) - : undefined, + runtime: daemonRuntime, + warn: (message, title) => note(message, title), }); progress.setLabel("Installing Gateway daemon…"); @@ -135,14 +107,7 @@ export async function maybeInstallDaemon(params: { ); if (installError) { note("Gateway daemon install failed: " + installError, "Gateway"); - if (process.platform === "win32") { - note( - "Tip: rerun from an elevated PowerShell (Start → type PowerShell → right-click → Run as administrator) or skip daemon install.", - "Gateway", - ); - } else { - note("Tip: rerun `clawdbot daemon install` after fixing the error.", "Gateway"); - } + note(gatewayInstallErrorHint(), "Gateway"); return; } shouldCheckLinger = true; diff --git a/src/commands/daemon-install-helpers.ts b/src/commands/daemon-install-helpers.ts new file mode 100644 index 000000000..89a2c5783 --- /dev/null +++ b/src/commands/daemon-install-helpers.ts @@ -0,0 +1,70 @@ +import path from "node:path"; + +import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; +import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; +import { + renderSystemNodeWarning, + resolvePreferredNodePath, + resolveSystemNodeInfo, +} from "../daemon/runtime-paths.js"; +import { buildServiceEnvironment } from "../daemon/service-env.js"; +import type { GatewayDaemonRuntime } from "./daemon-runtime.js"; + +type WarnFn = (message: string, title?: string) => void; + +export type GatewayInstallPlan = { + programArguments: string[]; + workingDirectory?: string; + environment: Record; +}; + +export function resolveGatewayDevMode(argv: string[] = process.argv): boolean { + const entry = argv[1]; + return Boolean(entry?.includes(`${path.sep}src${path.sep}`) && entry.endsWith(".ts")); +} + +export async function buildGatewayInstallPlan(params: { + env: Record; + port: number; + runtime: GatewayDaemonRuntime; + token?: string; + devMode?: boolean; + nodePath?: string; + warn?: WarnFn; +}): Promise { + const devMode = params.devMode ?? resolveGatewayDevMode(); + const nodePath = + params.nodePath ?? + (await resolvePreferredNodePath({ + env: params.env, + runtime: params.runtime, + })); + const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ + port: params.port, + dev: devMode, + runtime: params.runtime, + nodePath, + }); + if (params.runtime === "node") { + const systemNode = await resolveSystemNodeInfo({ env: params.env }); + const warning = renderSystemNodeWarning(systemNode, programArguments[0]); + if (warning) params.warn?.(warning, "Gateway runtime"); + } + const environment = buildServiceEnvironment({ + env: params.env, + port: params.port, + token: params.token, + launchdLabel: + process.platform === "darwin" + ? resolveGatewayLaunchAgentLabel(params.env.CLAWDBOT_PROFILE) + : undefined, + }); + + return { programArguments, workingDirectory, environment }; +} + +export function gatewayInstallErrorHint(platform = process.platform): string { + return platform === "win32" + ? "Tip: rerun from an elevated PowerShell (Start → type PowerShell → right-click → Run as administrator) or skip daemon install." + : "Tip: rerun `clawdbot daemon install` after fixing the error."; +} diff --git a/src/commands/doctor-gateway-daemon-flow.ts b/src/commands/doctor-gateway-daemon-flow.ts index 1f4cf4dc7..cb34f0438 100644 --- a/src/commands/doctor-gateway-daemon-flow.ts +++ b/src/commands/doctor-gateway-daemon-flow.ts @@ -1,17 +1,7 @@ -import path from "node:path"; - import type { ClawdbotConfig } from "../config/config.js"; import { resolveGatewayPort } from "../config/config.js"; -import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; import { readLastGatewayErrorLine } from "../daemon/diagnostics.js"; -import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; -import { - renderSystemNodeWarning, - resolvePreferredNodePath, - resolveSystemNodeInfo, -} 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 { renderSystemdUnavailableHints } from "../daemon/systemd-hints.js"; import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js"; @@ -24,6 +14,10 @@ import { GATEWAY_DAEMON_RUNTIME_OPTIONS, type GatewayDaemonRuntime, } from "./daemon-runtime.js"; +import { + buildGatewayInstallPlan, + gatewayInstallErrorHint, +} from "./daemon-install-helpers.js"; import { buildGatewayRuntimeHints, formatGatewayRuntimeSummary } from "./doctor-format.js"; import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js"; import { healthCommand } from "./health.js"; @@ -81,41 +75,26 @@ export async function maybeRepairGatewayDaemon(params: { }, DEFAULT_GATEWAY_DAEMON_RUNTIME, ); - const devMode = - process.argv[1]?.includes(`${path.sep}src${path.sep}`) && - process.argv[1]?.endsWith(".ts"); const port = resolveGatewayPort(params.cfg, process.env); - const nodePath = await resolvePreferredNodePath({ - env: process.env, - runtime: daemonRuntime, - }); - const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ - port, - dev: devMode, - runtime: daemonRuntime, - nodePath, - }); - if (daemonRuntime === "node") { - const systemNode = await resolveSystemNodeInfo({ env: process.env }); - const warning = renderSystemNodeWarning(systemNode, programArguments[0]); - if (warning) note(warning, "Gateway runtime"); - } - const environment = buildServiceEnvironment({ + const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port, token: params.cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN, - launchdLabel: - process.platform === "darwin" - ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) - : undefined, - }); - await service.install({ - env: process.env, - stdout: process.stdout, - programArguments, - workingDirectory, - environment, + runtime: daemonRuntime, + warn: (message, title) => note(message, title), }); + try { + await service.install({ + env: process.env, + stdout: process.stdout, + programArguments, + workingDirectory, + environment, + }); + } catch (err) { + note(`Gateway daemon install failed: ${String(err)}`, "Gateway"); + note(gatewayInstallErrorHint(), "Gateway"); + } } } return; diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index db9610055..179833991 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -2,13 +2,10 @@ import path from "node:path"; import type { ClawdbotConfig } from "../config/config.js"; import { resolveGatewayPort, resolveIsNixMode } from "../config/paths.js"; -import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; import { findExtraGatewayServices, renderGatewayServiceCleanupHints } from "../daemon/inspect.js"; import { findLegacyGatewayServices, uninstallLegacyGatewayServices } from "../daemon/legacy.js"; -import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; import { renderSystemNodeWarning, - resolvePreferredNodePath, resolveSystemNodeInfo, } from "../daemon/runtime-paths.js"; import { resolveGatewayService } from "../daemon/service.js"; @@ -17,9 +14,12 @@ import { needsNodeRuntimeMigration, SERVICE_AUDIT_CODES, } from "../daemon/service-audit.js"; -import { buildServiceEnvironment } from "../daemon/service-env.js"; import type { RuntimeEnv } from "../runtime.js"; import { note } from "../terminal/note.js"; +import { + buildGatewayInstallPlan, + gatewayInstallErrorHint, +} from "./daemon-install-helpers.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, GATEWAY_DAEMON_RUNTIME_OPTIONS, @@ -109,35 +109,26 @@ export async function maybeMigrateLegacyGatewayService( }, DEFAULT_GATEWAY_DAEMON_RUNTIME, ); - const devMode = - process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts"); const port = resolveGatewayPort(cfg, process.env); - const nodePath = await resolvePreferredNodePath({ - env: process.env, - runtime: daemonRuntime, - }); - const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ - port, - dev: devMode, - runtime: daemonRuntime, - nodePath, - }); - const environment = buildServiceEnvironment({ + const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port, token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN, - launchdLabel: - process.platform === "darwin" - ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) - : undefined, - }); - await service.install({ - env: process.env, - stdout: process.stdout, - programArguments, - workingDirectory, - environment, + runtime: daemonRuntime, + warn: (message, title) => note(message, title), }); + try { + await service.install({ + env: process.env, + stdout: process.stdout, + programArguments, + workingDirectory, + environment, + }); + } catch (err) { + runtime.error(`Gateway daemon install failed: ${String(err)}`); + note(gatewayInstallErrorHint(), "Gateway"); + } } export async function maybeRepairGatewayServiceConfig( @@ -183,15 +174,15 @@ export async function maybeRepairGatewayServiceConfig( ); } - const devMode = - process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts"); const port = resolveGatewayPort(cfg, process.env); const runtimeChoice = detectGatewayRuntime(command.programArguments); - const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ + const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ + env: process.env, port, - dev: devMode, + token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN, runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice, nodePath: systemNodePath ?? undefined, + warn: (message, title) => note(message, title), }); const expectedEntrypoint = findGatewayEntrypoint(programArguments); const currentEntrypoint = findGatewayEntrypoint(command.programArguments); @@ -239,16 +230,6 @@ export async function maybeRepairGatewayServiceConfig( initialValue: true, }); if (!repair) return; - const environment = buildServiceEnvironment({ - env: process.env, - port, - token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN, - launchdLabel: - process.platform === "darwin" - ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) - : undefined, - }); - try { await service.install({ env: process.env, diff --git a/src/commands/onboard-non-interactive/local/daemon-install.ts b/src/commands/onboard-non-interactive/local/daemon-install.ts index b0f1497e3..0692efbce 100644 --- a/src/commands/onboard-non-interactive/local/daemon-install.ts +++ b/src/commands/onboard-non-interactive/local/daemon-install.ts @@ -1,18 +1,12 @@ -import path from "node:path"; - import type { ClawdbotConfig } from "../../../config/config.js"; -import { resolveGatewayLaunchAgentLabel } from "../../../daemon/constants.js"; -import { resolveGatewayProgramArguments } from "../../../daemon/program-args.js"; -import { - renderSystemNodeWarning, - resolvePreferredNodePath, - resolveSystemNodeInfo, -} 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 type { RuntimeEnv } from "../../../runtime.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, isGatewayDaemonRuntime } from "../../daemon-runtime.js"; +import { + buildGatewayInstallPlan, + gatewayInstallErrorHint, +} from "../../daemon-install-helpers.js"; import type { OnboardOptions } from "../../onboard-types.js"; import { ensureSystemdUserLingerNonInteractive } from "../../systemd-linger.js"; @@ -41,33 +35,12 @@ export async function installGatewayDaemonNonInteractive(params: { } 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, - runtime: daemonRuntimeRaw, - nodePath, - }); - - if (daemonRuntimeRaw === "node") { - const systemNode = await resolveSystemNodeInfo({ env: process.env }); - const warning = renderSystemNodeWarning(systemNode, programArguments[0]); - if (warning) runtime.log(warning); - } - - const environment = buildServiceEnvironment({ + const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port, token: gatewayToken, - launchdLabel: - process.platform === "darwin" - ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) - : undefined, + runtime: daemonRuntimeRaw, + warn: (message) => runtime.log(message), }); try { await service.install({ @@ -79,13 +52,7 @@ export async function installGatewayDaemonNonInteractive(params: { }); } catch (err) { runtime.error(`Gateway daemon install failed: ${String(err)}`); - if (process.platform === "win32") { - runtime.log( - "Tip: rerun from an elevated PowerShell (Start → type PowerShell → right-click → Run as administrator) or skip daemon install.", - ); - } else { - runtime.log("Tip: rerun `clawdbot daemon install` after fixing the error."); - } + runtime.log(gatewayInstallErrorHint()); return; } await ensureSystemdUserLingerNonInteractive({ runtime }); diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index c0d5cbff9..13a77fe21 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -1,6 +1,4 @@ import fs from "node:fs/promises"; -import path from "node:path"; - import { DEFAULT_BOOTSTRAP_FILENAME } from "../agents/workspace.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, @@ -19,20 +17,16 @@ import { } from "../commands/onboard-helpers.js"; import type { OnboardOptions } from "../commands/onboard-types.js"; import type { ClawdbotConfig } from "../config/config.js"; -import { resolveGatewayLaunchAgentLabel } from "../daemon/constants.js"; -import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; -import { - renderSystemNodeWarning, - resolvePreferredNodePath, - resolveSystemNodeInfo, -} 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 { runTui } from "../tui/tui.js"; import { resolveUserPath } from "../utils.js"; +import { + buildGatewayInstallPlan, + gatewayInstallErrorHint, +} from "../commands/daemon-install-helpers.js"; import type { GatewayWizardSettings, WizardFlow } from "./onboarding.types.js"; import type { WizardPrompter } from "./prompts.js"; @@ -161,35 +155,16 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption } if (!loaded || (loaded && (await service.isLoaded({ env: process.env })) === false)) { - const devMode = - process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts"); const progress = prompter.progress("Gateway daemon"); let installError: string | null = null; try { progress.update("Preparing Gateway daemon…"); - const nodePath = await resolvePreferredNodePath({ - env: process.env, - runtime: daemonRuntime, - }); - const { programArguments, workingDirectory } = await resolveGatewayProgramArguments({ - port: settings.port, - dev: devMode, - runtime: daemonRuntime, - nodePath, - }); - if (daemonRuntime === "node") { - const systemNode = await resolveSystemNodeInfo({ env: process.env }); - const warning = renderSystemNodeWarning(systemNode, programArguments[0]); - if (warning) await prompter.note(warning, "Gateway runtime"); - } - const environment = buildServiceEnvironment({ + const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port: settings.port, token: settings.gatewayToken, - launchdLabel: - process.platform === "darwin" - ? resolveGatewayLaunchAgentLabel(process.env.CLAWDBOT_PROFILE) - : undefined, + runtime: daemonRuntime, + warn: (message, title) => prompter.note(message, title), }); progress.update("Installing Gateway daemon…"); @@ -207,17 +182,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption } if (installError) { await prompter.note(`Gateway daemon install failed: ${installError}`, "Gateway"); - if (process.platform === "win32") { - await prompter.note( - "Tip: rerun from an elevated PowerShell (Start → type PowerShell → right-click → Run as administrator) or skip daemon install.", - "Gateway", - ); - } else { - await prompter.note( - "Tip: rerun `clawdbot daemon install` after fixing the error.", - "Gateway", - ); - } + await prompter.note(gatewayInstallErrorHint(), "Gateway"); } } }