fix(wizard): avoid rerunning completed setup steps
Some checks failed
ci/woodpecker/push/ci Pipeline was canceled
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Jarvis
2026-06-25 13:22:59 -05:00
parent b96cc7982a
commit 0113d89ab8
6 changed files with 182 additions and 6 deletions

View File

@@ -167,6 +167,45 @@ describe('gatewayConfigStage', () => {
expect(state.gateway?.regeneratedConfig).toBe(true);
});
it('does not ask for a gateway API key when provider setup was completed with no key', async () => {
delete process.env['MOSAIC_ASSUME_YES'];
const originalIsTTY = process.stdin.isTTY;
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
try {
const textFn = vi.fn(async (opts: { message: string; initialValue?: string }) => {
if (opts.message === 'Gateway port') return opts.initialValue ?? '14242';
if (opts.message === 'Web UI hostname (for browser access)') return 'localhost';
if (opts.message.includes('API_KEY')) {
throw new Error('gateway API key prompt should be skipped');
}
return '';
});
const p = buildPrompter({ text: textFn, select: vi.fn().mockResolvedValue('local') });
const state = makeState('/home/user/.config/mosaic');
const result = await gatewayConfigStage(p, state, {
host: 'localhost',
defaultPort: 14242,
skipInstall: true,
providerType: 'none',
});
expect(result.ready).toBe(true);
expect(textFn).not.toHaveBeenCalledWith(
expect.objectContaining({ message: expect.stringContaining('API_KEY') }),
);
const envContents = readFileSync(daemonState.envFile, 'utf-8');
expect(envContents).not.toContain('ANTHROPIC_API_KEY=');
expect(envContents).not.toContain('OPENAI_API_KEY=');
} finally {
Object.defineProperty(process.stdin, 'isTTY', {
value: originalIsTTY,
configurable: true,
});
}
});
it('short-circuits when gateway is already fully installed and user declines rerun', async () => {
// Pre-populate both files + running daemon + meta with token
const fs = require('node:fs');

View File

@@ -506,6 +506,9 @@ async function collectAndWriteConfig(
if (opts.providerKey) {
anthropicKey = opts.providerKey;
p.log(`Using API key from provider setup (${opts.providerType ?? 'unknown'}).`);
} else if (opts.providerType === 'none') {
anthropicKey = '';
p.log('No API key provided during provider setup; skipping gateway API key prompt.');
} else {
anthropicKey = await p.text({
message: 'ANTHROPIC_API_KEY (optional, press Enter to skip)',

View File

@@ -37,6 +37,7 @@ export async function quickStartPath(
// 1. Provider setup (first question)
await providerSetupStage(prompter, state);
state.completedSections?.add('providers');
// Apply sensible defaults for everything else
state.soul.agentName ??= 'Mosaic';
@@ -57,6 +58,7 @@ export async function quickStartPath(
// Skills (recommended set, no user input in quick mode)
await skillsSelectStage(prompter, state);
state.completedSections?.add('skills');
// Finalize writes configs/assets/skills, but defer the success summary until
// after the gateway health/bootstrap gates complete.
@@ -75,7 +77,7 @@ export async function quickStartPath(
portOverride: options.gatewayPortOverride,
skipInstall: options.skipGatewayNpmInstall,
providerKey: state.providerKey,
providerType: state.providerType ?? 'none',
providerType: state.providerType,
});
if (!configResult.ready || !configResult.host || !configResult.port) {

View File

@@ -126,6 +126,11 @@ type MenuChoice =
| 'advanced'
| 'finish';
function menuSectionKey(section: MenuChoice): MenuSection | null {
if (section === 'quick-start' || section === 'finish') return null;
return section === 'gateway-config' ? 'gateway' : section;
}
function menuLabel(section: MenuChoice, completed: Set<MenuSection>): string {
const labels: Record<MenuChoice, string> = {
'quick-start': 'Quick Start',
@@ -137,14 +142,24 @@ function menuLabel(section: MenuChoice, completed: Set<MenuSection>): string {
finish: 'Finish & Apply',
};
const base = labels[section];
const sectionKey: MenuSection =
section === 'gateway-config' ? 'gateway' : (section as MenuSection);
if (completed.has(sectionKey)) {
const sectionKey = menuSectionKey(section);
if (sectionKey && completed.has(sectionKey)) {
return `${base} [done]`;
}
return base;
}
function skipCompletedMenuChoice(
prompter: WizardPrompter,
completed: Set<MenuSection>,
choice: MenuChoice,
): boolean {
const sectionKey = menuSectionKey(choice);
if (!sectionKey || !completed.has(sectionKey)) return false;
prompter.log(`${menuLabel(choice, completed)} is already complete; skipping.`);
return true;
}
async function runMenuLoop(
prompter: WizardPrompter,
state: WizardState,
@@ -201,21 +216,25 @@ async function runMenuLoop(
return; // Quick start is a complete flow — exit menu
case 'providers':
if (skipCompletedMenuChoice(prompter, completed, choice)) break;
await providerSetupStage(prompter, state);
completed.add('providers');
break;
case 'identity':
if (skipCompletedMenuChoice(prompter, completed, choice)) break;
await agentIntentStage(prompter, state);
completed.add('identity');
break;
case 'skills':
if (skipCompletedMenuChoice(prompter, completed, choice)) break;
await skillsSelectStage(prompter, state);
completed.add('skills');
break;
case 'gateway-config':
if (skipCompletedMenuChoice(prompter, completed, choice)) break;
// Gateway config is handled during Finish — mark as "configured"
// after user reviews settings.
await runGatewaySubMenu(prompter, state, options);
@@ -223,6 +242,7 @@ async function runMenuLoop(
break;
case 'advanced':
if (skipCompletedMenuChoice(prompter, completed, choice)) break;
await runAdvancedSubMenu(prompter, state);
completed.add('advanced');
break;
@@ -325,7 +345,7 @@ async function runFinishPath(
portOverride: options.gatewayPortOverride,
skipInstall: options.skipGatewayNpmInstall,
providerKey: state.providerKey,
providerType: state.providerType ?? 'none',
providerType: state.providerType,
});
if (configResult.ready && configResult.host && configResult.port) {
@@ -396,7 +416,7 @@ async function runHeadlessPath(
portOverride: options.gatewayPortOverride,
skipInstall: options.skipGatewayNpmInstall,
providerKey: state.providerKey,
providerType: state.providerType ?? 'none',
providerType: state.providerType,
});
if (!configResult.ready || !configResult.host || !configResult.port) {