@ace-sdk/cli
Version:
ACE CLI - Command-line tool for intelligent pattern learning and playbook management
350 lines • 16.2 kB
JavaScript
/**
* 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