import type { WizardPrompter } from './prompter/interface.js'; import type { ConfigService } from './config/config-service.js'; import type { WizardState } from './types.js'; import { welcomeStage } from './stages/welcome.js'; import { detectInstallStage } from './stages/detect-install.js'; import { modeSelectStage } from './stages/mode-select.js'; import { soulSetupStage } from './stages/soul-setup.js'; import { userSetupStage } from './stages/user-setup.js'; import { toolsSetupStage } from './stages/tools-setup.js'; import { runtimeSetupStage } from './stages/runtime-setup.js'; import { hooksPreviewStage } from './stages/hooks-preview.js'; import { skillsSelectStage } from './stages/skills-select.js'; import { finalizeStage } from './stages/finalize.js'; import { gatewayConfigStage } from './stages/gateway-config.js'; import { gatewayBootstrapStage } from './stages/gateway-bootstrap.js'; export interface WizardOptions { mosaicHome: string; sourceDir: string; prompter: WizardPrompter; configService: ConfigService; cliOverrides?: Partial; /** * Skip the terminal gateway stages. Used by callers that only want to * configure the framework (SOUL.md/USER.md/skills/hooks) without touching * the gateway daemon. Defaults to `false` — the unified first-run flow * runs everything end-to-end. */ skipGateway?: boolean; /** Host passed through to the gateway config stage. Defaults to localhost. */ gatewayHost?: string; /** Default gateway port (14242) — overridable by CLI flag. */ gatewayPort?: number; /** * Explicit port override from the caller. Honored even when resuming * from an existing `.env` (useful when the saved port conflicts with * another service). */ gatewayPortOverride?: number; /** Skip `npm install -g @mosaicstack/gateway` during the config stage. */ skipGatewayNpmInstall?: boolean; } export async function runWizard(options: WizardOptions): Promise { const { prompter, configService, mosaicHome, sourceDir } = options; const state: WizardState = { mosaicHome, sourceDir, mode: 'quick', installAction: 'fresh', soul: {}, user: {}, tools: {}, runtimes: { detected: [], mcpConfigured: false }, selectedSkills: [], }; // Apply CLI overrides (strip undefined values) if (options.cliOverrides) { if (options.cliOverrides.soul) { for (const [k, v] of Object.entries(options.cliOverrides.soul)) { if (v !== undefined) { (state.soul as Record)[k] = v; } } } if (options.cliOverrides.user) { for (const [k, v] of Object.entries(options.cliOverrides.user)) { if (v !== undefined) { (state.user as Record)[k] = v; } } } if (options.cliOverrides.tools) { for (const [k, v] of Object.entries(options.cliOverrides.tools)) { if (v !== undefined) { (state.tools as Record)[k] = v; } } } if (options.cliOverrides.mode) { state.mode = options.cliOverrides.mode; } } // Stage 1: Welcome await welcomeStage(prompter, state); // Stage 2: Existing Install Detection await detectInstallStage(prompter, state, configService); // Stage 3: Quick Start vs Advanced (skip if keeping existing) if (state.installAction === 'fresh' || state.installAction === 'reset') { await modeSelectStage(prompter, state); } else if (state.installAction === 'reconfigure') { state.mode = 'advanced'; } // Stage 4: SOUL.md await soulSetupStage(prompter, state); // Stage 5: USER.md await userSetupStage(prompter, state); // Stage 6: TOOLS.md await toolsSetupStage(prompter, state); // Stage 7: Runtime Detection & Installation await runtimeSetupStage(prompter, state); // Stage 8: Hooks preview (Claude only — skipped if Claude not detected) await hooksPreviewStage(prompter, state); // Stage 9: Skills Selection await skillsSelectStage(prompter, state); // Stage 10: Finalize (writes configs, links runtime assets, runs doctor) await finalizeStage(prompter, state, configService); // Stages 11 & 12: Gateway config + admin bootstrap. // The unified first-run flow runs these as terminal stages so the user // goes from "welcome" through "admin user created" in a single cohesive // experience. Callers that only want the framework portion pass // `skipGateway: true`. if (!options.skipGateway) { const headlessRun = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY; try { const configResult = await gatewayConfigStage(prompter, state, { host: options.gatewayHost ?? 'localhost', defaultPort: options.gatewayPort ?? 14242, portOverride: options.gatewayPortOverride, skipInstall: options.skipGatewayNpmInstall, }); if (!configResult.ready || !configResult.host || !configResult.port) { if (headlessRun) { prompter.warn('Gateway configuration failed in headless mode — aborting wizard.'); process.exit(1); } } else { const bootstrapResult = await gatewayBootstrapStage(prompter, state, { host: configResult.host, port: configResult.port, }); if (!bootstrapResult.completed && headlessRun) { prompter.warn('Admin bootstrap failed in headless mode — aborting wizard.'); process.exit(1); } } } catch (err) { // Stages normally return structured `ready: false` results for // expected failures. Anything that reaches here is an unexpected // runtime error — render a concise warning for UX AND re-throw so // the CLI (and `tools/install.sh` auto-launch) sees a non-zero exit. // Swallowing here would let headless installs report success even // when the gateway stage crashed. prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`); throw err; } } }