openclaw/src/cli/daemon-cli/install.ts
kumarabhirup e865265f0f
Gateway: pre-build web app + Control UI before daemon install (2026.2.6-3.7)
The Next.js web app was only built inside the gateway process on first
boot.  When the daemon was freshly installed (e.g. `onboard
--install-daemon`), the LaunchAgent would start and block on `next
build`, causing a noticeably slow first startup.

Add `ensureWebAppBuilt()` to `src/gateway/server-web-app.ts` — a
standalone pre-build function that checks for `.next/BUILD_ID` and runs
dep install + `next build` if missing.  Skips silently when the web app
is disabled, already built, in dev mode, or inapplicable (global npm
install without `apps/web`).

Call both `ensureWebAppBuilt()` and `ensureControlUiAssetsBuilt()` before
the daemon is installed in every relevant path:

- Interactive onboarding (`onboarding.finalize.ts`) — moved the existing
  Control UI build from after the daemon install to before it, and added
  the web app build alongside it.
- Non-interactive onboarding (`daemon-install.ts`) — added both pre-build
  calls before `service.install()`.
- Standalone `openclaw gateway install` CLI (`daemon-cli/install.ts`) —
  added both pre-build calls before `service.install()`.
- Configure wizard (`configure.wizard.ts`) — added the web app build
  alongside the existing Control UI build.

Updated test mocks for `ensureWebAppBuilt` in onboarding, configure
wizard, and daemon CLI coverage tests.

Bumped version to 2026.2.6-3.7 and published to npm.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 01:32:18 -08:00

160 lines
4.6 KiB
TypeScript

import type { DaemonInstallOptions } from "./types.js";
import { buildGatewayInstallPlan } from "../../commands/daemon-install-helpers.js";
import {
DEFAULT_GATEWAY_DAEMON_RUNTIME,
isGatewayDaemonRuntime,
} from "../../commands/daemon-runtime.js";
import { loadConfig, resolveGatewayPort } from "../../config/config.js";
import { resolveIsNixMode } from "../../config/paths.js";
import { resolveGatewayService } from "../../daemon/service.js";
import { ensureWebAppBuilt } from "../../gateway/server-web-app.js";
import { ensureControlUiAssetsBuilt } from "../../infra/control-ui-assets.js";
import { defaultRuntime } from "../../runtime.js";
import { formatCliCommand } from "../command-format.js";
import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
import { parsePort } from "./shared.js";
export async function runDaemonInstall(opts: DaemonInstallOptions) {
const json = Boolean(opts.json);
const warnings: string[] = [];
const stdout = json ? createNullWriter() : process.stdout;
const emit = (payload: {
ok: boolean;
result?: string;
message?: string;
error?: string;
service?: {
label: string;
loaded: boolean;
loadedText: string;
notLoadedText: string;
};
hints?: string[];
warnings?: string[];
}) => {
if (!json) {
return;
}
emitDaemonActionJson({ action: "install", ...payload });
};
const fail = (message: string) => {
if (json) {
emit({ ok: false, error: message, warnings: warnings.length ? warnings : undefined });
} else {
defaultRuntime.error(message);
}
defaultRuntime.exit(1);
};
if (resolveIsNixMode(process.env)) {
fail("Nix mode detected; service install is disabled.");
return;
}
const cfg = loadConfig();
const portOverride = parsePort(opts.port);
if (opts.port !== undefined && portOverride === null) {
fail("Invalid port");
return;
}
const port = portOverride ?? resolveGatewayPort(cfg);
if (!Number.isFinite(port) || port <= 0) {
fail("Invalid port");
return;
}
const runtimeRaw = opts.runtime ? String(opts.runtime) : DEFAULT_GATEWAY_DAEMON_RUNTIME;
if (!isGatewayDaemonRuntime(runtimeRaw)) {
fail('Invalid --runtime (use "node" or "bun")');
return;
}
const service = resolveGatewayService();
let loaded = false;
try {
loaded = await service.isLoaded({ env: process.env });
} catch (err) {
fail(`Gateway service check failed: ${String(err)}`);
return;
}
if (loaded) {
if (!opts.force) {
emit({
ok: true,
result: "already-installed",
message: `Gateway service already ${service.loadedText}.`,
service: buildDaemonServiceSnapshot(service, loaded),
warnings: warnings.length ? warnings : undefined,
});
if (!json) {
defaultRuntime.log(`Gateway service already ${service.loadedText}.`);
defaultRuntime.log(
`Reinstall with: ${formatCliCommand("openclaw gateway install --force")}`,
);
}
return;
}
}
// Pre-build web app + Control UI assets so the gateway doesn't block on
// builds when it boots for the first time after daemon install.
const webAppResult = await ensureWebAppBuilt(defaultRuntime, {
webAppConfig: cfg.gateway?.webApp,
});
if (!webAppResult.ok && webAppResult.message) {
if (json) {
warnings.push(webAppResult.message);
} else {
defaultRuntime.error(webAppResult.message);
}
}
const controlUiResult = await ensureControlUiAssetsBuilt(defaultRuntime);
if (!controlUiResult.ok && controlUiResult.message) {
if (json) {
warnings.push(controlUiResult.message);
} else {
defaultRuntime.error(controlUiResult.message);
}
}
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: process.env,
port,
token: opts.token || cfg.gateway?.auth?.token || process.env.OPENCLAW_GATEWAY_TOKEN,
runtime: runtimeRaw,
warn: (message) => {
if (json) {
warnings.push(message);
} else {
defaultRuntime.log(message);
}
},
config: cfg,
});
try {
await service.install({
env: process.env,
stdout,
programArguments,
workingDirectory,
environment,
});
} catch (err) {
fail(`Gateway install failed: ${String(err)}`);
return;
}
let installed = true;
try {
installed = await service.isLoaded({ env: process.env });
} catch {
installed = true;
}
emit({
ok: true,
result: "installed",
service: buildDaemonServiceSnapshot(service, installed),
warnings: warnings.length ? warnings : undefined,
});
}