UNPKG

@ace-sdk/cli

Version:

ACE CLI - Command-line tool for intelligent pattern learning and playbook management

350 lines 16.2 kB
/** * Bootstrap playbook from codebase * * Uses SSE streaming for real-time progress feedback. * Integrates import graph analysis for smart file selection. */ import { globalOptions } from '../cli.js'; import { createContext } from '../types/config.js'; import { ACEServerClient } from '../services/server-client.js'; import { InitializationService } from '../services/initialization.js'; import { THOROUGHNESS_PRESETS, // Bootstrap streaming from core bootstrapWithStreaming, BOOTSTRAP_STAGE_LABELS, isDoneEvent, isErrorEvent, // Import graph for smart file selection buildImportGraph, graphToMetrics, calculateHealthMetrics, createEmptyProjectDNA, // Language detection LanguageDetector } from '@ace-sdk/core'; import { Logger } from '../services/logger.js'; import chalk from 'chalk'; /** * Initialize playbook from codebase */ export async function bootstrapCommand(options) { const logger = new Logger(globalOptions); const useStreaming = options.stream !== false; const useSmartSelect = options.smartSelect !== false; // Fetch server config to get defaults let serverDefaults = { mode: 'hybrid', thoroughness: 'medium' }; try { const context = await createContext({ org: globalOptions.org, project: globalOptions.project }); const tempClient = new ACEServerClient(context, logger); const serverConfig = await tempClient.getConfig(); if (serverConfig?.runtime_settings) { const configMode = serverConfig.runtime_settings.bootstrapDefaultMode; if (configMode === 'hybrid' || configMode === 'local-files' || configMode === 'git-history' || configMode === 'docs-only') { serverDefaults.mode = configMode; } const configThoroughness = serverConfig.runtime_settings.bootstrapDefaultThoroughness; if (configThoroughness === 'light' || configThoroughness === 'medium' || configThoroughness === 'deep') { serverDefaults.thoroughness = configThoroughness; } logger.debug(`Using server config: mode=${serverDefaults.mode}, thoroughness=${serverDefaults.thoroughness}`); } } catch (error) { logger.debug('Failed to fetch server config, using hardcoded defaults'); } // Apply thoroughness preset (CLI option > server default > hardcoded) const thoroughnessLevel = options.thoroughness || serverDefaults.thoroughness; const preset = THOROUGHNESS_PRESETS[thoroughnessLevel]; const effectiveMaxFiles = options.maxFiles ?? preset.max_files; const effectiveCommitLimit = options.commitLimit ?? preset.commit_limit; const effectiveDaysBack = options.daysBack ?? preset.days_back; const analysisMode = options.mode || serverDefaults.mode; const projectPath = options.repoPath || process.cwd(); if (!logger.isJson()) { logger.info(chalk.bold(`\n🚀 Bootstrapping Playbook\n`)); logger.info(` ${chalk.cyan('Mode:')} ${analysisMode}`); logger.info(` ${chalk.cyan('Thoroughness:')} ${thoroughnessLevel}`); logger.info(` ${chalk.cyan('Max Files:')} ${effectiveMaxFiles === -1 ? 'unlimited' : effectiveMaxFiles}`); logger.info(` ${chalk.cyan('Commits:')} ${effectiveCommitLimit}`); logger.info(` ${chalk.cyan('Days Back:')} ${effectiveDaysBack}`); logger.info(` ${chalk.cyan('Repository:')} ${projectPath}`); logger.info(` ${chalk.cyan('Streaming:')} ${useStreaming ? 'enabled' : 'disabled'}`); logger.info(` ${chalk.cyan('Smart Select:')} ${useSmartSelect ? 'enabled' : 'disabled'}\n`); } let spinner = logger.spinner('Analyzing codebase...'); try { const context = await createContext({ org: globalOptions.org, project: globalOptions.project }); const client = new ACEServerClient(context, logger); const initService = new InitializationService(); // Build Project DNA for profiling let projectDNA; if (useSmartSelect) { spinner = logger.spinner('Building project profile (DNA)...'); projectDNA = await buildProjectDNA(projectPath, context.orgId || 'default', context.projectId); logger.debug(`Project DNA: ${projectDNA.primaryLanguage}, ${projectDNA.projectType}`); } // Run local codebase analysis spinner = logger.spinner('Running local codebase analysis...'); const playbook = await initService.initializeFromCodebase(projectPath, { mode: analysisMode, commitLimit: effectiveCommitLimit, daysBack: effectiveDaysBack, maxFiles: effectiveMaxFiles }); // Count total patterns discovered const allBullets = [ ...playbook.strategies_and_hard_rules, ...playbook.useful_code_snippets, ...playbook.troubleshooting_and_pitfalls, ...playbook.apis_to_use ]; spinner = logger.spinner(`Discovered ${allBullets.length} patterns locally`); if (allBullets.length === 0) { spinner?.warn('No patterns found in codebase'); if (logger.isJson()) { logger.output({ success: true, patternsExtracted: 0, message: 'No patterns found in codebase' }); } else { logger.info(chalk.yellow('\n⚠️ No patterns found in codebase')); logger.info(chalk.dim('Try working on real tasks to build the playbook through online learning\n')); } return; } // Build code blocks array for bootstrap endpoint const codeBlocks = []; for (const snippet of playbook.useful_code_snippets) { if (snippet.evidence && snippet.evidence.length > 0) { codeBlocks.push(`File: ${snippet.evidence[0]}\n\n${snippet.content}`); } else { codeBlocks.push(snippet.content); } } // Also include strategies, troubleshooting, and API patterns for (const strategy of playbook.strategies_and_hard_rules) { codeBlocks.push(`[STRATEGY] ${strategy.content}`); } for (const trouble of playbook.troubleshooting_and_pitfalls) { codeBlocks.push(`[TROUBLESHOOTING] ${trouble.content}`); } for (const api of playbook.apis_to_use) { codeBlocks.push(`[API] ${api.content}`); } // Choose streaming or non-streaming bootstrap if (useStreaming) { spinner = logger.spinner('Sending patterns to ACE server (streaming)...'); // SSE streaming bootstrap with progress updates const result = await bootstrapWithStreaming({ serverUrl: context.serverUrl, orgId: context.orgId || 'default', projectId: context.projectId, apiToken: context.apiToken, mode: analysisMode, codeBlocks, metadata: { files_scanned: projectDNA?.graph.totalFiles, blocks_extracted: codeBlocks.length, thoroughness: thoroughnessLevel }, projectDNA, onEvent: (event) => { // Update spinner with progress const label = BOOTSTRAP_STAGE_LABELS[event.stage] || event.stage; spinner = logger.spinner(`${label}: ${event.message}`); if (isDoneEvent(event)) { const stats = event.data; logger.debug(`Done: ${stats.patterns_extracted} patterns, ${stats.compression_percentage}% compression`); } else if (isErrorEvent(event)) { logger.debug(`Error: ${event.data.error_code} - ${event.data.message}`); } }, onError: (error) => { logger.debug(`Stream error: ${error.message}`); } }); // Process streaming result handleBootstrapResult(result, allBullets.length, spinner, logger, client); } else { // Non-streaming fallback spinner = logger.spinner('Sending patterns to ACE server...'); const result = await client.bootstrap({ mode: analysisMode, code_blocks: codeBlocks, metadata: { blocks_extracted: codeBlocks.length, thoroughness: thoroughnessLevel } }); // Invalidate cache client.invalidateCache(); spinner?.succeed('Bootstrap complete'); if (logger.isJson()) { logger.output({ success: true, patternsExtracted: allBullets.length, patternsAfterDedup: result.patterns_after_dedup || allBullets.length, compressionRatio: result.compression_ratio, message: 'Playbook initialized successfully' }); } else { logger.info(chalk.green('\n✅ Playbook initialized successfully\n')); logger.info(` ${chalk.cyan('Patterns Extracted:')} ${allBullets.length}`); logger.info(` ${chalk.cyan('After Deduplication:')} ${result.patterns_after_dedup || allBullets.length}`); if (result.compression_ratio) { logger.info(` ${chalk.cyan('Compression Ratio:')} ${result.compression_ratio}`); } logger.info(''); } } } catch (error) { spinner?.fail('Bootstrap failed'); if (logger.isJson()) { logger.error(JSON.stringify({ error: error instanceof Error ? error.message : String(error) })); } else { logger.error(chalk.red(`\n❌ Error: ${error instanceof Error ? error.message : String(error)}\n`)); } process.exit(1); } } /** * Handle bootstrap streaming result */ function handleBootstrapResult(result, localPatterns, spinner, logger, client) { // Invalidate cache client.invalidateCache(); if (result.success) { spinner?.succeed('Bootstrap complete'); if (logger.isJson()) { logger.output({ success: true, patternsExtracted: localPatterns, patternsAfterDedup: result.playbook?.totalPatterns || localPatterns, statistics: result.statistics, processingTime: result.processingTime, message: 'Playbook initialized successfully' }); } else { logger.info(chalk.green('\n✅ Playbook initialized successfully\n')); logger.info(` ${chalk.cyan('Patterns Extracted:')} ${localPatterns}`); logger.info(` ${chalk.cyan('After Deduplication:')} ${result.playbook?.totalPatterns || localPatterns}`); if (result.statistics) { logger.info(` ${chalk.cyan('Compression:')} ${result.statistics.compression_percentage}%`); logger.info(` ${chalk.cyan('Avg Confidence:')} ${(result.statistics.average_confidence * 100).toFixed(0)}%`); logger.info(` ${chalk.cyan('Processing Time:')} ${result.processingTime?.toFixed(1)}s`); } if (result.playbook?.bySection) { logger.info(chalk.dim('\n By Section:')); logger.info(` ${chalk.cyan('Strategies:')} ${result.playbook.bySection.strategies_and_hard_rules || 0}`); logger.info(` ${chalk.cyan('Snippets:')} ${result.playbook.bySection.useful_code_snippets || 0}`); logger.info(` ${chalk.cyan('Troubleshooting:')} ${result.playbook.bySection.troubleshooting_and_pitfalls || 0}`); logger.info(` ${chalk.cyan('APIs:')} ${result.playbook.bySection.apis_to_use || 0}`); } logger.info(''); } } else { spinner?.fail('Bootstrap failed'); if (logger.isJson()) { logger.output({ success: false, error: result.error, message: result.error?.message || 'Bootstrap failed' }); } else { logger.error(chalk.red(`\n❌ Bootstrap failed: ${result.error?.message || 'Unknown error'}`)); if (result.error?.retryable) { logger.info(chalk.dim('This error is retryable - try again\n')); } } process.exit(1); } } /** * Build Project DNA for intelligent pattern matching */ async function buildProjectDNA(projectPath, orgId, projectId) { const dna = createEmptyProjectDNA(orgId, projectId); try { // Detect languages using linguist const detector = new LanguageDetector(); const langStats = await detector.getLanguageBreakdown(projectPath); // Convert to ProjectDNA format const languages = Object.entries(langStats) .map(([name, percentage]) => ({ name, percentage: percentage, files: 0, // Not tracked by simple breakdown bytes: 0 })) .sort((a, b) => b.percentage - a.percentage); dna.languages = languages; dna.primaryLanguage = languages[0]?.name || 'Unknown'; // Build import graph for structural analysis try { const graph = await buildImportGraph({ repoPath: projectPath }); dna.graph = graphToMetrics(graph); // Calculate health metrics with estimated file stats const totalFiles = graph.nodes.size; const avgFileSize = 100; // Estimated average const maxFileSize = 500; // Estimated max dna.health = calculateHealthMetrics(graph, totalFiles, avgFileSize, maxFileSize); } catch (graphError) { // Import graph may fail for non-JS/TS projects console.error(` Note: Import graph analysis skipped (${graphError.message})`); } // Detect project type from structure dna.projectType = await detectProjectType(projectPath); // Get git commit try { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); const { stdout } = await execAsync(`git -C "${projectPath}" rev-parse HEAD`, { timeout: 5000 }); dna.gitCommit = stdout.trim(); } catch { // Not a git repo or git not available } dna.analyzedAt = new Date().toISOString(); } catch (error) { console.error(` Warning: Project DNA analysis incomplete (${error.message})`); } return dna; } /** * Detect project type from folder structure */ async function detectProjectType(projectPath) { const fs = await import('fs/promises'); const path = await import('path'); const checks = [ { type: 'monorepo', files: ['packages', 'lerna.json', 'pnpm-workspace.yaml'] }, { type: 'cli', files: ['bin', 'src/commands', 'src/cli.ts', 'src/cli.js'] }, { type: 'api-server', files: ['src/routes', 'src/controllers', 'src/middleware'] }, { type: 'webapp', files: ['src/pages', 'src/components', 'src/routes', 'app'] }, { type: 'library', files: ['src/index.ts', 'src/index.js', 'lib'] }, { type: 'mobile', files: ['src/screens', 'src/navigation', 'App.tsx'] } ]; for (const check of checks) { for (const file of check.files) { try { await fs.access(path.join(projectPath, file)); return check.type; } catch { // File doesn't exist } } } return 'unknown'; } //# sourceMappingURL=bootstrap.js.map