/** * ai command * AI prompt building and suggestion commands * Generated from SpecVerse specification */ import { Command } from 'commander'; import { readFileSync, writeFileSync, existsSync, mkdirSync, cpSync, rmSync, statSync, readdirSync } from 'fs'; import { resolve, basename } from 'path'; import { EngineRegistry } from '@specverse/entities'; import type { ParserEngine } from '@specverse/types'; /** * Register the ai command on the program. */ export function registerAiCommand(program: Command): void { const cmd = program .command('ai') .description('AI prompt building and suggestion commands'); cmd .command('docs ') .description('Generate AI documentation prompts from specification') .option('-o, --output ', 'Output file path') .option('--config ', 'Configuration file with requirements, scale, etc.') .action(async (file: string, options: any) => { try { const registry = new EngineRegistry(); await registry.discover(); const parser = registry.getEngineForCapability('parse') as ParserEngine; if (!parser) { console.error('No parser engine found.'); process.exit(1); } await parser.initialize(); const content = readFileSync(file, 'utf8'); const parseResult = parser.parseContent(content, file); if (parseResult.errors.length > 0) { console.error('Invalid spec:'); parseResult.errors.forEach((e: string) => console.error(' ', e)); process.exit(1); } const aiEngine = registry.getEngineForCapability('ai-prompts') as any; if (!aiEngine) { console.error('AI engine not available. Install @specverse/engines.'); process.exit(1); } await aiEngine.initialize({ provider: options.provider }); const prompt = await aiEngine.generatePrompt(parseResult.ast!, { type: 'docs' }); const outputFile = options.output || basename(file, '.specly') + '-ai-docs.md'; writeFileSync(outputFile, prompt); console.log('AI documentation prompt generated: ' + outputFile); } catch (error: any) { console.error('Error:', error.message); process.exit(1); } }); cmd .command('suggest ') .description('Get AI suggestions for improving a specification') .action(async (file: string, _options: any) => { try { const registry = new EngineRegistry(); await registry.discover(); const parser = registry.getEngineForCapability('parse') as ParserEngine; if (!parser) { console.error('No parser engine found.'); process.exit(1); } await parser.initialize(); const content = readFileSync(file, 'utf8'); const parseResult = parser.parseContent(content, file); if (parseResult.errors.length > 0) { console.error('Invalid spec:'); parseResult.errors.forEach((e: string) => console.error(' ', e)); process.exit(1); } const aiEngine = registry.getEngineForCapability('ai-suggestions') as any; if (!aiEngine) { console.error('AI engine not available. Install @specverse/engines.'); process.exit(1); } await aiEngine.initialize(); const suggestions = await aiEngine.suggest(parseResult.ast!); if (suggestions.length === 0) { console.log('No suggestions — spec looks good!'); } else { const warnings = suggestions.filter((s: any) => s.severity === 'warning'); const improvements = suggestions.filter((s: any) => s.severity === 'improvement'); const info = suggestions.filter((s: any) => s.severity === 'info'); if (warnings.length > 0) { console.log('\nWarnings:'); warnings.forEach((s: any) => console.log(' [' + s.target + '] ' + s.description)); } if (improvements.length > 0) { console.log('\nSuggested improvements:'); improvements.forEach((s: any) => console.log(' [' + s.target + '] ' + s.description)); } if (info.length > 0) { console.log('\nInfo:'); info.forEach((s: any) => console.log(' [' + s.target + '] ' + s.description)); } console.log('\n' + suggestions.length + ' suggestion(s): ' + warnings.length + ' warning, ' + improvements.length + ' improvement, ' + info.length + ' info'); } } catch (error: any) { console.error('Error:', error.message); process.exit(1); } }); cmd .command('template ') .description('Generate AI implementation templates') .option('-o, --output ', 'Output file path') .option('--config ', 'Configuration file') .action(async (operation: string, options: any) => { try { const registry = new EngineRegistry(); await registry.discover(); const aiEngine = registry.getEngineForCapability('ai-templates') as any; if (!aiEngine) { console.error('AI engine not available. Install @specverse/engines.'); process.exit(1); } await aiEngine.initialize(); const template = await aiEngine.template(operation, { config: options.config }); if (options.output) { writeFileSync(options.output, template); console.log('Template written to: ' + options.output); } else { console.log(template); } } catch (error: any) { console.error('Error:', error.message); process.exit(1); } }); cmd .command('regenerate ') .description('Regenerate an AI-generated behavior function for a controller or service') .option('--spec ', 'Spec file (default: specs/main-inferred.specly, fallback specs/main.specly)') .option('-o, --output ', 'Output root (default: generated/code)') .option('--all', 'Regenerate every AI-generated function for the owner (not just one)', false) .action(async (fn: string, options: any) => { try { // Parse Owner.functionName — or just Owner with --all const target = fn; const dotIdx = target.indexOf('.'); const ownerName = dotIdx >= 0 ? target.slice(0, dotIdx) : target; const functionName = dotIdx >= 0 ? target.slice(dotIdx + 1) : null; if (!functionName && !options.all) { console.error('Function must be specified as Owner.functionName, or use --all to regenerate every AI function in the owner.'); process.exit(1); } // Resolve spec file — default to specs/main-inferred.specly next to cwd const userCwd = process.env.SPECVERSE_USER_CWD || process.cwd(); const specCandidates = options.spec ? [resolve(userCwd, options.spec)] : [resolve(userCwd, 'specs/main-inferred.specly'), resolve(userCwd, 'specs/main.specly')]; const specPath = specCandidates.find(p => existsSync(p)); if (!specPath) { console.error('Spec not found. Tried: ' + specCandidates.join(', ')); console.error('Pass --spec or run from a project root with specs/main.specly.'); process.exit(1); } console.log('Using spec: ' + specPath); const registry = new EngineRegistry(); await registry.discover(); const parser = registry.getEngineForCapability('parse') as ParserEngine; if (!parser) { console.error('No parser engine found.'); process.exit(1); } await parser.initialize(); const specContent = readFileSync(specPath, 'utf8'); const parseResult = parser.parseContent(specContent, specPath); if (parseResult.errors.length > 0) { console.error('Invalid spec:'); parseResult.errors.forEach((e: string) => console.error(' ' + e)); process.exit(1); } // Flatten components down to a realize-shaped spec // (controllers, services, models at top level) — matches what // regenerateBehavior expects. Collections may be arrays (parser // output) or objects (inferred spec); preserve the incoming shape. const ast: any = parseResult.ast; const flat: any = { ...ast }; const components = ast?.components || {}; for (const comp of Object.values(components) as any[]) { for (const key of ['models', 'controllers', 'services']) { const collection = comp?.[key]; if (!collection) continue; if (Array.isArray(collection)) { flat[key] = [...(Array.isArray(flat[key]) ? flat[key] : []), ...collection]; } else { flat[key] = { ...(flat[key] && !Array.isArray(flat[key]) ? flat[key] : {}), ...collection }; } } } const { regenerateBehavior } = await import('@specverse/engines/ai'); const outputDir = resolve(userCwd, options.output || 'generated/code'); try { const result = await regenerateBehavior({ spec: flat, ownerName, targetFunction: options.all ? null : functionName, outputDir, allFunctions: !!options.all, }); console.log(`✅ Regenerated ${result.filePath}`); console.log(` Owner: ${ownerName}`); console.log(` Unmatched functions: ${result.unmatchedCount}`); console.log(` Target: ${options.all ? 'all' : functionName}`); console.log(` Cache entries cleared: ${result.cacheCleared}`); } catch (err: any) { console.error('Regeneration failed: ' + (err?.message || err)); process.exit(1); } } catch (error: any) { console.error('Error:', error.message); process.exit(1); } }); cmd .command('analyse ') .description('Analyse a source codebase and extract a SpecVerse spec via the LLM. Writes a structured run dir (input/, facts/, prompts/, llm-output/, specs/, project/).') .option('-o, --output ', 'Output dir for the structured run (default: ./runs/analyse//_