Files
stack-mos-dt-0/packages/mosaic/src/wizard.ts
T

126 lines
4.0 KiB
TypeScript

import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
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';
// ─── Transient install session state (CU-07-02) ───────────────────────────────
const INSTALL_STATE_FILE = join(
process.env['XDG_RUNTIME_DIR'] ?? process.env['TMPDIR'] ?? tmpdir(),
'mosaic-install-state.json',
);
function writeInstallState(mosaicHome: string): void {
try {
const state = {
wizardCompletedAt: new Date().toISOString(),
mosaicHome,
};
writeFileSync(INSTALL_STATE_FILE, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
} catch {
// Non-fatal — gateway install will just ask for home again
}
}
export interface WizardOptions {
mosaicHome: string;
sourceDir: string;
prompter: WizardPrompter;
configService: ConfigService;
cliOverrides?: Partial<WizardState>;
}
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
await finalizeStage(prompter, state, configService);
// CU-07-02: Write transient session state so `mosaic gateway install` can
// pick up mosaicHome without re-prompting.
writeInstallState(state.mosaicHome);
}