/** * Thin wrapper over the unified first-run stages. * * `mosaic gateway install` is kept as a standalone entry point for users who * already went through `mosaic wizard` and only need to (re)configure the * gateway daemon. It builds a minimal `WizardState`, invokes * `gatewayConfigStage` and `gatewayBootstrapStage` directly, and returns. * * The heavy lifting — prompts, env writes, daemon lifecycle, bootstrap POST — * lives in `packages/mosaic/src/stages/gateway-config.ts` and * `packages/mosaic/src/stages/gateway-bootstrap.ts` so that the same code * path runs under both the unified wizard and this standalone command. */ import { homedir } from 'node:os'; import { join } from 'node:path'; import { ClackPrompter } from '../../prompter/clack-prompter.js'; import type { WizardState } from '../../types.js'; interface InstallOpts { host: string; port: number; skipInstall?: boolean; } function isHeadlessRun(): boolean { return process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY; } export async function runInstall(opts: InstallOpts): Promise { const mosaicHome = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic'); const prompter = new ClackPrompter(); const state: WizardState = { mosaicHome, sourceDir: mosaicHome, mode: 'quick', installAction: 'fresh', soul: {}, user: {}, tools: {}, runtimes: { detected: [], mcpConfigured: false }, selectedSkills: [], }; const { gatewayConfigStage } = await import('../../stages/gateway-config.js'); const { gatewayBootstrapStage } = await import('../../stages/gateway-bootstrap.js'); // Preserve the legacy "explicit --port wins over saved config" semantic: // commander defaults the port to 14242, so any other value is treated as // an explicit user override that the config stage should honor even on // resume. const portOverride = opts.port !== 14242 ? opts.port : undefined; const headless = isHeadlessRun(); try { const configResult = await gatewayConfigStage(prompter, state, { host: opts.host, defaultPort: opts.port, portOverride, skipInstall: opts.skipInstall, }); if (!configResult.ready || !configResult.host || configResult.port === undefined) { // In headless/scripted installs, a non-ready config stage is a fatal // error — we must not report "complete" when the gateway was never // configured. Exit non-zero so CI notices. if (headless) { prompter.warn('Gateway configuration failed in headless mode — aborting.'); process.exit(1); } return; } const bootstrapResult = await gatewayBootstrapStage(prompter, state, { host: configResult.host, port: configResult.port, }); if (!bootstrapResult.completed && headless) { prompter.warn('Admin bootstrap failed in headless mode — aborting.'); process.exit(1); } prompter.log('─── Installation Complete ───'); prompter.log(` Endpoint: http://${configResult.host}:${configResult.port.toString()}`); prompter.log(` Logs: mosaic gateway logs`); prompter.log(` Status: mosaic gateway status`); // Post-install verification (CU-07-03) — non-fatal. try { const { runPostInstallVerification } = await import('./verify.js'); await runPostInstallVerification(configResult.host, configResult.port); } catch { // Non-fatal — verification is a courtesy } } catch (err) { // Stages normally return structured results for expected failures. // Anything that reaches here is an unexpected runtime error — render a // concise warning AND re-throw so the command exits non-zero. Silent // swallowing would let scripted installs report success on failure. prompter.warn(`Gateway install failed: ${err instanceof Error ? err.message : String(err)}`); throw err; } }