- Bug 1 [CRITICAL]: drop `import type` on BootstrapSetupDto in bootstrap.controller.ts so NestJS preserves the class reference for design:paramtypes; ValidationPipe now correctly validates the DTO instead of 400ing on every field. Add e2e integration test (bootstrap.e2e.spec.ts) via @nestjs/testing + supertest that exercises the real DI/Fastify binding path to guard against regressions. Configure vitest to use unplugin-swc with decoratorMetadata:true. - Bug 2: remove `&& headlessRun` guard in wizard.ts so bootstrap failure (completed:false) aborts with process.exit(1) in both interactive and headless modes — no more silent '✔ Wizard complete' after a 400. - Bug 3: add `initialValue` to WizardPrompter.text() interface, ClackPrompter, and HeadlessPrompter; use it in gateway-config.ts promptPort() so 14242 prefills the input buffer and users can press Enter to accept. Apply same pattern to other gateway config prompts (databaseUrl, valkeyUrl, corsOrigin). - Bug 4: add Pi SDK to the 'What is Mosaic?' intro copy in welcome.ts. - Bump @mosaicstack/mosaic 0.0.25 → 0.0.26. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
5.9 KiB
TypeScript
164 lines
5.9 KiB
TypeScript
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<WizardState>;
|
|
/**
|
|
* 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<void> {
|
|
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<string, unknown>)[k] = v;
|
|
}
|
|
}
|
|
}
|
|
if (options.cliOverrides.user) {
|
|
for (const [k, v] of Object.entries(options.cliOverrides.user)) {
|
|
if (v !== undefined) {
|
|
(state.user as Record<string, unknown>)[k] = v;
|
|
}
|
|
}
|
|
}
|
|
if (options.cliOverrides.tools) {
|
|
for (const [k, v] of Object.entries(options.cliOverrides.tools)) {
|
|
if (v !== undefined) {
|
|
(state.tools as Record<string, unknown>)[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) {
|
|
prompter.warn('Admin bootstrap failed — 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;
|
|
}
|
|
}
|
|
}
|