/* eslint-disable no-console */ import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import axios from 'axios'; import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js'; // ───────────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────────── interface GenesisStep { id: string; title: string; step_number: number; icon: string; verification_path?: string; } interface GenesisStatusResponse { success: boolean; data: { genesis_complete: boolean; genesis_steps: GenesisStep[]; genesis_count: number; total_roadmap_steps: number; detected_template: string; detected_stack_key: string; project_name: string; }; error?: string; } interface GenesisTriggerResponse { success: boolean; simulation?: boolean; data: { project_name: string; template: string; stack_key: string; steps_created: number; steps: GenesisStep[]; existing_steps_shifted: number; message: string; }; error?: string; } // ───────────────────────────────────────────────────────────────────────────── // Command Definition // ───────────────────────────────────────────────────────────────────────────── export function createGenesisCommand(): Command { return new Command('genesis') .description('Initialize project foundation (Phase 0). Detects stack and injects foundation steps.') .option('--force', 'Re-run genesis even if already initialized (use with caution)') .option('--simulate', 'Dry-run: Calculate plan without modifying database') .option('--status', 'Check genesis status without triggering') .option('--project-id ', 'Override project ID (defaults to linked project)') .action(async (options) => { const apiKey = getApiKey(); const apiUrl = getApiUrl(); const projectId = options.projectId || getProjectId(); if (!projectId) { console.error(chalk.red('❌ No project linked. Run: rigstate link')); process.exit(1); } if (!apiKey) { console.error(chalk.red('❌ Not authenticated. Run: rigstate login')); process.exit(1); } if (options.status) { await checkGenesisStatus(projectId, apiKey, apiUrl); } else { await triggerGenesis(projectId, apiKey, apiUrl, options.force ?? false, options.simulate ?? false); } }); } // ───────────────────────────────────────────────────────────────────────────── // Status Check // ───────────────────────────────────────────────────────────────────────────── export async function checkGenesisStatus( projectId: string, apiKey: string, apiUrl: string ): Promise<{ complete: boolean; stepCount: number }> { // ── Offline-first: check local manifest cache ───────────────────────────── try { const { loadManifest } = await import('../utils/manifest.js'); const manifest = await loadManifest(); if (manifest?.genesis_complete) { // Local cache hit — skip API call return { complete: true, stepCount: 0 }; // stepCount not cached, but complete is enough } } catch { // Ignore manifest read errors } const spinner = ora('Checking Genesis status...').start(); try { const response = await axios.get( `${apiUrl}/api/v1/genesis`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 } ); spinner.stop(); if (!response.data.success) { console.log(chalk.yellow(`⚠️ Could not check genesis status: ${response.data.error}`)); return { complete: false, stepCount: 0 }; } const { data } = response.data; console.log(''); console.log(chalk.bold('🏗️ GENESIS STATUS')); console.log(chalk.dim('────────────────────────────────────────')); console.log(`${chalk.bold('Project:')} ${chalk.cyan(data.project_name)}`); console.log(`${chalk.bold('Stack:')} ${chalk.magenta(data.detected_template)}`); console.log(`${chalk.bold('Genesis:')} ${data.genesis_complete ? chalk.green('✅ Complete') : chalk.yellow('⚠️ Pending')}`); console.log(`${chalk.bold('Foundation:')} ${chalk.white(data.genesis_count)} steps`); console.log(`${chalk.bold('Total Roadmap:')} ${chalk.white(data.total_roadmap_steps)} steps`); if (data.genesis_complete && data.genesis_steps.length > 0) { console.log(''); console.log(chalk.dim('Foundation Steps:')); data.genesis_steps.forEach(step => { console.log(` ${step.icon} ${chalk.bold(`T-${step.step_number}`)}: ${step.title}`); }); } else if (!data.genesis_complete) { console.log(''); console.log(chalk.yellow('⚡ Genesis not yet initialized.')); console.log(chalk.dim(' Run: ') + chalk.white('rigstate genesis')); } console.log(chalk.dim('────────────────────────────────────────')); console.log(''); return { complete: data.genesis_complete, stepCount: data.genesis_count }; } catch (err: any) { spinner.stop(); if (err.response?.status === 422) { console.log(chalk.yellow('⚠️ Genesis pending: Complete onboarding with Frank first.')); console.log(chalk.dim(' Then run: rigstate genesis')); } return { complete: false, stepCount: 0 }; } } // ───────────────────────────────────────────────────────────────────────────── // Trigger Genesis // ───────────────────────────────────────────────────────────────────────────── export async function triggerGenesis( projectId: string, apiKey: string, apiUrl: string, force = false, simulate = false ): Promise { console.log(''); if (simulate) { console.log(chalk.bold.magenta('🔮 GENESIS SIMULATION')); console.log(chalk.dim('Dry-run: Calculating plan without executing changes...')); } else { console.log(chalk.bold.blue('🏗️ GENESIS PROTOCOL')); console.log(chalk.dim('Initializing project foundation...')); } console.log(''); const spinner = ora('Detecting tech stack...').start(); try { // Step 1: Check status first (unless force) if (!force) { const statusRes = await axios.get( `${apiUrl}/api/v1/genesis`, { params: { project_id: projectId }, headers: { Authorization: `Bearer ${apiKey}` }, timeout: 10000 } ); if (statusRes.data.success && statusRes.data.data.genesis_complete) { spinner.stop(); console.log(chalk.green('✅ Genesis already complete.')); console.log(chalk.dim(` ${statusRes.data.data.genesis_count} foundation steps already in roadmap.`)); console.log(chalk.dim(' Use --force to re-initialize.')); console.log(''); return true; } if (statusRes.data.success) { spinner.text = `Stack detected: ${statusRes.data.data.detected_template}. Generating foundation...`; } } else { spinner.text = 'Force mode: Re-generating foundation...'; } // Step 2: Trigger Genesis const response = await axios.post( `${apiUrl}/api/v1/genesis`, { project_id: projectId, force, simulate }, { headers: { Authorization: `Bearer ${apiKey}` }, timeout: 60000 // AI enrichment can take time } ); spinner.stop(); if (!response.data.success) { // Handle specific error cases if ((response as any).status === 409) { console.log(chalk.yellow('⚠️ Genesis already initialized. Use --force to re-run.')); return true; } if ((response as any).status === 422) { console.log(''); console.log(chalk.yellow('⚠️ Cannot initialize Genesis yet.')); console.log(chalk.dim(' Complete onboarding with Frank first to define your tech stack.')); console.log(chalk.dim(' Then run: ') + chalk.white('rigstate genesis')); console.log(''); return false; } console.error(chalk.red(`❌ Genesis failed: ${response.data.error}`)); return false; } const { data } = response.data; // ── Simulation Output ───────────────────────────────────────────────── if (response.data.simulation) { console.log(chalk.bold.magenta('🔮 SIMULATION RESULTS')); console.log(chalk.dim('────────────────────────────────────────')); console.log(`${chalk.bold('Project:')} ${chalk.cyan(data.project_name)}`); console.log(`${chalk.bold('Stack:')} ${chalk.magenta(data.template)}`); console.log(`${chalk.bold('Will Create:')} ${chalk.white(data.steps_created)} foundation steps`); if (data.existing_steps_shifted > 0) { console.log(`${chalk.bold('Will Shift:')} ${chalk.yellow(`${data.existing_steps_shifted} existing steps down`)}`); } console.log(''); console.log(chalk.bold('📋 Planner Preview:')); data.steps.forEach(step => { const stepNum = step.step_number; // In simulation, step numbers returned might be 1-based index relative to insert, // but usually the AI/Builder assigns them relative to start. console.log(` ${step.icon || '🔹'} ${chalk.bold(`T-${stepNum}`)}: ${step.title}`); if (step.verification_path) { console.log(` ${chalk.dim(`Verify: ${step.verification_path}`)}`); } }); console.log(''); console.log(chalk.dim('To execute this plan, run without --simulate.')); return true; } // ── Success Output ──────────────────────────────────────────────────── console.log(chalk.bold.green('✅ GENESIS COMPLETE')); console.log(chalk.dim('────────────────────────────────────────')); console.log(`${chalk.bold('Project:')} ${chalk.cyan(data.project_name)}`); console.log(`${chalk.bold('Stack:')} ${chalk.magenta(data.template)}`); console.log(`${chalk.bold('Created:')} ${chalk.white(data.steps_created)} foundation steps`); if (data.existing_steps_shifted > 0) { console.log(`${chalk.bold('Shifted:')} ${chalk.dim(`${data.existing_steps_shifted} existing steps moved down`)}`); } console.log(''); console.log(chalk.bold('📋 Foundation Steps:')); data.steps.forEach(step => { console.log(` ${step.icon} ${chalk.bold(`T-${step.step_number}`)}: ${step.title}`); if (step.verification_path) { console.log(` ${chalk.dim(`Verify: ${step.verification_path}`)}`); } }); console.log(''); console.log(chalk.bold.yellow('⚡ NEXT MOVE:')); if (data.steps.length > 0) { const firstStep = data.steps[0]; console.log(` ${chalk.white(`> rigstate work start ${firstStep.id}`)} ${chalk.dim(`(Start: ${firstStep.title})`)}`); } console.log(chalk.dim('────────────────────────────────────────')); console.log(''); // ── Persist genesis status to local manifest (enables offline checks) ── try { const { saveManifest } = await import('../utils/manifest.js'); await saveManifest({ genesis_complete: true, genesis_template: data.template, genesis_stack_key: data.stack_key, genesis_initialized_at: new Date().toISOString() }); } catch { // Non-critical: local cache write failure doesn't break the flow } return true; } catch (err: any) { spinner.stop(); // Handle axios error responses if (err.response?.status === 409) { console.log(chalk.green('✅ Genesis already complete.')); console.log(chalk.dim(' Use --force to re-initialize.')); return true; } if (err.response?.status === 422) { console.log(''); console.log(chalk.yellow('⚠️ Genesis pending: Onboarding not complete.')); console.log(chalk.dim(' Finish the Frank conversation to define your tech stack.')); console.log(chalk.dim(' Then run: ') + chalk.white('rigstate genesis')); console.log(''); return false; } if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') { console.log(chalk.dim(' (Genesis skipped: API unreachable)')); return false; } console.error(chalk.red(`❌ Genesis error: ${err.response?.data?.error || err.message}`)); return false; } }