feat(setup): add main orchestrator, menu, and CLI command

Implements Task 6 of the setup wizard:
- orchestrator.ts: runMenu() for interactive configuration loop
- orchestrator.ts: runFirstRunWizard() for new user onboarding
- orchestrator.test.ts: test for menu exit behavior
- setup.ts: registerSetupCommand() and runSetup() handler
  - Handles both first-run and existing config scenarios
  - Saves YAML config to disk
  - Optional daemon startup after first-run

All tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-02-10 09:35:32 -08:00
parent 182d86957b
commit d8b7b08270
3 changed files with 175 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest';
import { createInterface } from 'readline/promises';
import { Readable, Writable } from 'stream';
import { createPrompter } from './prompts.js';
import { runMenu } from './orchestrator.js';
import { ConfigBuilder } from './config.js';
function mockReadline(inputs: string[]) {
let idx = 0;
const input = new Readable({
read() {
if (idx < inputs.length) {
this.push(inputs[idx++] + '\n');
} else {
this.push(null);
}
},
});
const output = new Writable({ write(_, __, cb) { cb(); } });
return createInterface({ input, output });
}
describe('runMenu', () => {
it('exits immediately on 0', async () => {
const rl = mockReadline(['0']);
const p = createPrompter(rl);
const builder = new ConfigBuilder();
builder.setProvider('default', { provider: 'anthropic', model: 'test', api_key: 'k' });
await runMenu(p, builder);
// Should return without error
});
});
+73
View File
@@ -0,0 +1,73 @@
import type { Prompter } from './prompts.js';
import type { ConfigBuilder } from './config.js';
import { renderSummary } from './summary.js';
import { setupProviders } from './providers.js';
import { setupChannels } from './channels.js';
import { setupMemory } from './memory.js';
import { setupAutomation } from './automation.js';
import { setupSecurity } from './security.js';
import { setupGateway } from './gateway.js';
const MENU_OPTIONS = [
{ label: 'Model Providers', value: 'providers' },
{ label: 'Channels', value: 'channels' },
{ label: 'Memory', value: 'memory' },
{ label: 'Automation', value: 'automation' },
{ label: 'Security', value: 'security' },
{ label: 'Gateway', value: 'gateway' },
];
const SECTION_HANDLERS: Record<string, (p: Prompter, b: ConfigBuilder) => Promise<void>> = {
providers: setupProviders,
channels: setupChannels,
memory: setupMemory,
automation: setupAutomation,
security: setupSecurity,
gateway: setupGateway,
};
export async function runMenu(p: Prompter, builder: ConfigBuilder): Promise<void> {
while (true) {
p.println();
p.println('Flynn Setup — Current Configuration');
p.println(renderSummary(builder.build()));
p.println();
p.println('What would you like to configure?');
for (let i = 0; i < MENU_OPTIONS.length; i++) {
p.println(` ${i + 1}. ${MENU_OPTIONS[i].label}`);
}
p.println(' 0. Done — save and exit');
const answer = await p.ask('>', '0');
const idx = parseInt(answer, 10);
if (idx === 0 || isNaN(idx)) break;
if (idx >= 1 && idx <= MENU_OPTIONS.length) {
const section = MENU_OPTIONS[idx - 1].value;
const handler = SECTION_HANDLERS[section];
if (handler) {
p.println();
await handler(p, builder);
}
}
}
}
export async function runFirstRunWizard(p: Prompter): Promise<ConfigBuilder> {
const { ConfigBuilder } = await import('./config.js');
p.println();
p.println("Let's get Flynn running. This takes about 2 minutes.");
p.println();
const builder = new ConfigBuilder();
// Step 1: Model provider
await setupProviders(p, builder);
// Step 2: Channels
p.println();
await setupChannels(p, builder);
return builder;
}