fix(wizard): report gateway failures before success summary (#691)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #691.
This commit is contained in:
2026-06-25 18:14:40 +00:00
parent 0883fb91ec
commit b96cc7982a
6 changed files with 198 additions and 66 deletions

View File

@@ -37,7 +37,7 @@ const RUNTIME_DEFS: Record<
label: 'Pi',
command: 'pi',
versionFlag: '--version',
installHint: 'npm install -g @mariozechner/pi-coding-agent',
installHint: 'curl -fsSL https://pi.dev/install.sh | sh',
},
};

View File

@@ -162,11 +162,24 @@ function setupPath(mosaicHome: string, _p: WizardPrompter): PathAction {
}
}
export interface FinalizeStageOptions {
/**
* Defer the success summary/outro so callers can run downstream readiness
* gates (gateway health/bootstrap) before claiming Mosaic is ready.
*/
deferSummary?: boolean;
}
export interface FinalizeStageResult {
showSummary: () => void;
}
export async function finalizeStage(
p: WizardPrompter,
state: WizardState,
config: ConfigService,
): Promise<void> {
options: FinalizeStageOptions = {},
): Promise<FinalizeStageResult> {
p.separator();
const spin = p.spinner();
@@ -213,44 +226,56 @@ export async function finalizeStage(
// 6. PATH setup
const pathAction = setupPath(state.mosaicHome, p);
// 7. Summary
const skillsSummary = skillsResult.success
? skillsResult.installedCount > 0
? `${skillsResult.installedCount.toString()} installed`
: 'none selected'
: `install failed — ${skillsResult.failureReason ?? 'unknown error'}`;
let summaryShown = false;
const showSummary = () => {
if (summaryShown) return;
summaryShown = true;
const summary: string[] = [
`Agent: ${state.soul.agentName ?? 'Assistant'}`,
`Style: ${state.soul.communicationStyle ?? 'direct'}`,
`Runtimes: ${state.runtimes.detected.join(', ') || 'none detected'}`,
`Skills: ${skillsSummary}`,
`Config: ${state.mosaicHome}`,
];
// 7. Summary
const skillsSummary = skillsResult.success
? skillsResult.installedCount > 0
? `${skillsResult.installedCount.toString()} installed`
: 'none selected'
: `install failed — ${skillsResult.failureReason ?? 'unknown error'}`;
if (doctorResult.warnings > 0) {
summary.push(
`Health: ${doctorResult.warnings.toString()} warning(s) — run 'mosaic doctor' for details`,
);
} else {
summary.push('Health: all checks passed');
const summary: string[] = [
`Agent: ${state.soul.agentName ?? 'Assistant'}`,
`Style: ${state.soul.communicationStyle ?? 'direct'}`,
`Runtimes: ${state.runtimes.detected.join(', ') || 'none detected'}`,
`Skills: ${skillsSummary}`,
`Config: ${state.mosaicHome}`,
];
if (doctorResult.warnings > 0) {
summary.push(
`Health: ${doctorResult.warnings.toString()} warning(s) — run 'mosaic doctor' for details`,
);
} else {
summary.push('Health: all checks passed');
}
p.note(summary.join('\n'), 'Installation Summary');
// 8. Next steps
const nextSteps: string[] = [];
if (pathAction === 'added') {
const profilePath = getShellProfilePath();
nextSteps.push(`Reload shell: source ${profilePath ?? '~/.profile'}`);
}
if (state.runtimes.detected.length === 0) {
nextSteps.push('Install at least one runtime (claude, codex, or opencode)');
}
nextSteps.push("Launch with 'mosaic claude' (or codex/opencode)");
nextSteps.push('Edit identity files directly in ~/.config/mosaic/ for fine-tuning');
p.note(nextSteps.map((s, i) => `${(i + 1).toString()}. ${s}`).join('\n'), 'Next Steps');
p.outro('Mosaic is ready.');
};
if (!options.deferSummary) {
showSummary();
}
p.note(summary.join('\n'), 'Installation Summary');
// 8. Next steps
const nextSteps: string[] = [];
if (pathAction === 'added') {
const profilePath = getShellProfilePath();
nextSteps.push(`Reload shell: source ${profilePath ?? '~/.profile'}`);
}
if (state.runtimes.detected.length === 0) {
nextSteps.push('Install at least one runtime (claude, codex, or opencode)');
}
nextSteps.push("Launch with 'mosaic claude' (or codex/opencode)");
nextSteps.push('Edit identity files directly in ~/.config/mosaic/ for fine-tuning');
p.note(nextSteps.map((s, i) => `${(i + 1).toString()}. ${s}`).join('\n'), 'Next Steps');
p.outro('Mosaic is ready.');
return { showSummary };
}

View File

@@ -58,8 +58,11 @@ export async function quickStartPath(
// Skills (recommended set, no user input in quick mode)
await skillsSelectStage(prompter, state);
// Finalize (writes configs, links runtime assets, syncs skills)
await finalizeStage(prompter, state, configService);
// Finalize writes configs/assets/skills, but defer the success summary until
// after the gateway health/bootstrap gates complete.
const finalizeResult = await finalizeStage(prompter, state, configService, {
deferSummary: true,
});
// Gateway config + bootstrap
if (!options.skipGateway) {
@@ -80,19 +83,24 @@ export async function quickStartPath(
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);
}
return;
}
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);
return;
}
finalizeResult.showSummary();
} catch (err) {
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
} else {
finalizeResult.showSummary();
}
}

View File

@@ -310,8 +310,11 @@ async function runFinishPath(
await skillsSelectStage(prompter, state);
}
// Finalize (writes configs, links runtime assets, syncs skills)
await finalizeStage(prompter, state, configService);
// Finalize writes configs/assets/skills, but defer the success summary until
// after the gateway health/bootstrap gates complete.
const finalizeResult = await finalizeStage(prompter, state, configService, {
deferSummary: true,
});
// Gateway stages
if (!options.skipGateway) {
@@ -333,12 +336,16 @@ async function runFinishPath(
if (!bootstrapResult.completed) {
prompter.warn('Admin bootstrap failed — aborting wizard.');
process.exit(1);
return;
}
finalizeResult.showSummary();
}
} catch (err) {
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
} else {
finalizeResult.showSummary();
}
}
@@ -374,8 +381,11 @@ async function runHeadlessPath(
// Skills
await skillsSelectStage(prompter, state);
// Finalize
await finalizeStage(prompter, state, configService);
// Finalize writes configs/assets/skills, but defer the success summary until
// after the gateway health/bootstrap gates complete.
const finalizeResult = await finalizeStage(prompter, state, configService, {
deferSummary: true,
});
// Gateway stages
if (!options.skipGateway) {
@@ -392,20 +402,25 @@ async function runHeadlessPath(
if (!configResult.ready || !configResult.host || !configResult.port) {
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);
}
return;
}
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);
return;
}
finalizeResult.showSummary();
} catch (err) {
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
} else {
finalizeResult.showSummary();
}
}
@@ -426,8 +441,11 @@ async function runKeepPath(
// Skills
await skillsSelectStage(prompter, state);
// Finalize
await finalizeStage(prompter, state, configService);
// Finalize writes configs/assets/skills, but defer the success summary until
// after the gateway health/bootstrap gates complete.
const finalizeResult = await finalizeStage(prompter, state, configService, {
deferSummary: true,
});
// Gateway stages
if (!options.skipGateway) {
@@ -447,11 +465,15 @@ async function runKeepPath(
if (!bootstrapResult.completed) {
prompter.warn('Admin bootstrap failed — aborting wizard.');
process.exit(1);
return;
}
finalizeResult.showSummary();
}
} catch (err) {
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
} else {
finalizeResult.showSummary();
}
}