feat: unified first-run flow — merge wizard + gateway install (IUH-M03) (#433)
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed

This commit was merged in pull request #433.
This commit is contained in:
2026-04-05 19:13:02 +00:00
parent be917e2496
commit 732f8a49cf
14 changed files with 1730 additions and 640 deletions

View File

@@ -1,6 +1,3 @@
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';
@@ -14,25 +11,8 @@ 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
}
}
import { gatewayConfigStage } from './stages/gateway-config.js';
import { gatewayBootstrapStage } from './stages/gateway-bootstrap.js';
export interface WizardOptions {
mosaicHome: string;
@@ -40,6 +20,25 @@ export interface WizardOptions {
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> {
@@ -116,10 +115,49 @@ export async function runWizard(options: WizardOptions): Promise<void> {
// Stage 9: Skills Selection
await skillsSelectStage(prompter, state);
// Stage 10: Finalize
// Stage 10: Finalize (writes configs, links runtime assets, runs doctor)
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);
// 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;
}
}
}