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:
@@ -0,0 +1,70 @@
|
||||
import type { Command } from 'commander';
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import { createInterface } from 'readline/promises';
|
||||
import { parse } from 'yaml';
|
||||
import { getConfigPath } from './shared.js';
|
||||
import { createPrompter } from './setup/prompts.js';
|
||||
import { ConfigBuilder } from './setup/config.js';
|
||||
import { runFirstRunWizard, runMenu } from './setup/orchestrator.js';
|
||||
|
||||
export async function runSetup(configPath: string): Promise<void> {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
const p = createPrompter(rl);
|
||||
|
||||
try {
|
||||
if (existsSync(configPath)) {
|
||||
// Existing config → menu mode
|
||||
const raw = readFileSync(configPath, 'utf-8');
|
||||
const parsed = parse(raw) ?? {};
|
||||
const builder = ConfigBuilder.fromObject(parsed);
|
||||
await runMenu(p, builder);
|
||||
saveConfig(configPath, builder, p);
|
||||
} else {
|
||||
// No config → first-run wizard
|
||||
const builder = await runFirstRunWizard(p);
|
||||
saveConfig(configPath, builder, p);
|
||||
|
||||
const shouldStart = await p.confirm('Start Flynn now?', true);
|
||||
if (shouldStart) {
|
||||
rl.close();
|
||||
const { startDaemon } = await import('../daemon/index.js');
|
||||
const { loadConfig } = await import('../config/index.js');
|
||||
const config = loadConfig(configPath);
|
||||
const daemon = await startDaemon(config);
|
||||
await new Promise<void>(resolve => daemon.lifecycle.onShutdown(async () => resolve()));
|
||||
return;
|
||||
}
|
||||
|
||||
const wantMore = await p.confirm('Configure more features?', false);
|
||||
if (wantMore) {
|
||||
const raw = readFileSync(configPath, 'utf-8');
|
||||
const parsed = parse(raw) ?? {};
|
||||
const menuBuilder = ConfigBuilder.fromObject(parsed);
|
||||
await runMenu(p, menuBuilder);
|
||||
saveConfig(configPath, menuBuilder, p);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig(configPath: string, builder: ConfigBuilder, p: { println(msg?: string): void }): void {
|
||||
const dir = dirname(configPath);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(configPath, builder.toYaml(), 'utf-8');
|
||||
p.println();
|
||||
p.println(`✓ Config saved to ${configPath}`);
|
||||
}
|
||||
|
||||
export function registerSetupCommand(program: Command): void {
|
||||
program
|
||||
.command('setup')
|
||||
.description('Interactive setup wizard')
|
||||
.option('-c, --config <path>', 'Config file path')
|
||||
.action(async (opts: { config?: string }) => {
|
||||
const configPath = opts.config ?? getConfigPath();
|
||||
await runSetup(configPath);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user