UNPKG

@every-env/cli

Version:

Multi-agent orchestrator for AI-powered development workflows

675 lines • 26.5 kB
#!/usr/bin/env node import { Command } from 'commander'; import { ConfigLoader } from './core/config-loader.js'; import { PatternExecutor } from './patterns/pattern-executor.js'; import { AgentManager } from './core/agent-manager.js'; import { logger, setLogLevel } from './utils/logger.js'; import { ProgressDisplay } from './utils/progress-display.js'; import chalk from 'chalk'; import ora from 'ora'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { defaultConfig, defaultPrompts } from './templates/config.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import { executeCopyCommands } from './commands/copy-commands.js'; const execAsync = promisify(exec); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')); const program = new Command(); program .name('every') .description('Multi-agent orchestrator for AI-powered development workflows') .version(packageJson.version) .option('--interactive', 'Launch interactive mode') .option('--classic', 'Use classic CLI mode (default)'); // Create docs subcommand const docsCommand = program.command('docs'); docsCommand.description('Documentation generation commands'); // Global options for docs command docsCommand .option('-c, --config <path>', 'Configuration file path') .option('-p, --parallel <number>', 'Max parallel agents', parseInt) .option('--dry-run', 'Show what would be executed without running') .option('-v, --verbose', 'Verbose output') .option('--debug', 'Debug output'); // Update command docsCommand .command('update') .description('Update existing documentation') .option('--pattern <names...>', 'Specific patterns to update') .option('--force', 'Force regeneration') .action(async (options) => { await runCommand('update', { ...docsCommand.opts(), ...options }); }); // Run command docsCommand .command('run <pattern>') .description('Run a specific documentation pattern') .option('--only <items...>', 'Only process specific items') .option('--force', 'Force regeneration') .action(async (pattern, options) => { await runCommand('run', { ...docsCommand.opts(), ...options, pattern }); }); // List command docsCommand .command('list') .description('List available documentation patterns') .action(async () => { await runCommand('list', docsCommand.opts()); }); // Status command docsCommand .command('status') .description('Show status of last documentation run') .action(async () => { await runCommand('status', docsCommand.opts()); }); // Main command runner async function runCommand(command, options) { try { // Set log level if (options.debug) { setLogLevel('debug'); } else if (options.verbose) { setLogLevel('info'); } // Show version // eslint-disable-next-line no-console console.log(chalk.cyan(`\nšŸš€ @every/cli v${packageJson.version}\n`)); if (command === 'init' && !options.config) { await initializeProject(options); return; } // Load configuration const spinner = ora('Loading configuration...').start(); const configLoader = new ConfigLoader(); const config = await configLoader.load(options.config); spinner.succeed('Configuration loaded'); // Override parallelism if specified if (options.parallel) { config.parallelism.maxAgents = options.parallel; } // Execute command switch (command) { case 'init': await executeInit(config, options); break; case 'update': await executeUpdate(config, options); break; case 'run': await executeRun(config, options); break; case 'list': await executeList(config); break; case 'status': await executeStatus(config); break; } } catch (error) { logger.error('Command failed:', error); // eslint-disable-next-line no-console console.error(chalk.red(`\nāœ– Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); process.exit(1); } } export async function initializeProject(options) { const everyEnvDir = '.every-env'; const configPath = join(everyEnvDir, 'config.json'); // Create .every-env directory if it doesn't exist if (!existsSync(everyEnvDir)) { mkdirSync(everyEnvDir, { recursive: true }); // eslint-disable-next-line no-console console.log(chalk.green(`āœ“ Created ${everyEnvDir} directory`)); } // Check if config already exists if (existsSync(configPath) && !options.force) { // eslint-disable-next-line no-console console.log(chalk.yellow('Configuration file already exists. Use --force to overwrite.')); return; } // Check which Claude command is available const claudeCommand = await detectClaudeCommand(); if (claudeCommand) { // eslint-disable-next-line no-console console.log(chalk.green(`āœ“ Detected Claude command: ${claudeCommand}`)); } else { // eslint-disable-next-line no-console console.log(chalk.yellow('⚠ Claude Code not detected. Please install it with: npm install -g @anthropic-ai/claude-code')); } // eslint-disable-next-line no-console console.log(chalk.green('Creating sample configuration...')); // Update the sample config with the detected command const configWithCommand = { ...defaultConfig, docs: { ...defaultConfig.docs, defaultCommand: claudeCommand || 'claude' } }; // Write the config file writeFileSync(configPath, JSON.stringify(configWithCommand, null, 2)); // eslint-disable-next-line no-console console.log(chalk.green(`āœ“ Created ${configPath}`)); // Create prompts directory const promptsDir = join(everyEnvDir, 'prompts'); if (!existsSync(promptsDir)) { mkdirSync(promptsDir, { recursive: true }); // eslint-disable-next-line no-console console.log(chalk.green(`āœ“ Created ${promptsDir} directory`)); } // Write sample prompts for (const [filename, content] of Object.entries(defaultPrompts)) { // Update the path to be inside .every-env const updatedFilename = filename.replace('prompts/', '.every-env/prompts/'); const filepath = join(process.cwd(), updatedFilename); const dir = dirname(filepath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(filepath, content); // eslint-disable-next-line no-console console.log(chalk.green(`āœ“ Created ${updatedFilename}`)); } // eslint-disable-next-line no-console console.log(chalk.cyan('\n✨ Project initialized successfully!')); // eslint-disable-next-line no-console console.log('\nNext steps:'); // eslint-disable-next-line no-console console.log('1. Review and customize the configuration in .every-env/config.json'); // eslint-disable-next-line no-console console.log('2. Customize the prompt templates in the .every-env/prompts/ directory'); // eslint-disable-next-line no-console console.log('3. Run "every docs update" to generate documentation'); } // Command implementations async function executeInit(config, options) { const executor = new PatternExecutor(config); // Get all pattern names const patternNames = config.patterns.map(p => p.name); // First get the tasks to know how many agents will run const tasks = await executor.prepareTasks(patternNames); if (options.dryRun) { executor.printDryRun(tasks); return; } const manager = new AgentManager(config.parallelism.maxAgents); const progressDisplay = new ProgressDisplay(manager, options.debug); // Start progress display progressDisplay.start(tasks.length); try { const results = await manager.runBatch(tasks); progressDisplay.stop(); showSummary(results); } catch (error) { progressDisplay.stop(); throw error; } } async function executeUpdate(config, options) { const executor = new PatternExecutor(config); const patterns = Array.isArray(options.pattern) ? options.pattern : options.pattern ? [options.pattern] : config.patterns.map(p => p.name); // First get the tasks to know how many agents will run const tasks = await executor.prepareTasks(patterns); if (options.dryRun) { executor.printDryRun(tasks); return; } const manager = new AgentManager(config.parallelism.maxAgents); const progressDisplay = new ProgressDisplay(manager, options.debug); // Start progress display progressDisplay.start(tasks.length); try { const results = await manager.runBatch(tasks); progressDisplay.stop(); showSummary(results); } catch (error) { progressDisplay.stop(); throw error; } } async function executeRun(config, options) { const executor = new PatternExecutor(config); const tasks = await executor.prepareTasks([options.pattern]); if (options.dryRun) { executor.printDryRun(tasks); return; } const manager = new AgentManager(config.parallelism.maxAgents); const progressDisplay = new ProgressDisplay(manager, options.debug); // Start progress display progressDisplay.start(tasks.length); try { const results = await manager.runBatch(tasks); progressDisplay.stop(); showSummary(results); } catch (error) { progressDisplay.stop(); throw error; } } async function executeList(config) { console.log(chalk.cyan('\nšŸ“‹ Available patterns:\n')); for (const pattern of config.patterns) { console.log(chalk.bold(` ${pattern.name}`)); if (pattern.description) { console.log(chalk.gray(` ${pattern.description}`)); } console.log(chalk.gray(` Agents: ${pattern.agents.length}`)); console.log(); } } async function executeStatus(_config) { // TODO: Implement status tracking console.log(chalk.cyan('\nšŸ“Š Last run status:\n')); console.log(' Status tracking not yet implemented'); } // Helper functions function showSummary(results) { console.log(chalk.cyan('\nšŸ“Š Summary:\n')); const successful = results.filter(r => r.status === 'success').length; const failed = results.filter(r => r.status === 'failed').length; const totalTime = results.reduce((sum, r) => sum + r.duration, 0) / 1000; console.log(` Total agents: ${results.length}`); console.log(` ${chalk.green(`Successful:`)} ${successful}`); if (failed > 0) { console.log(` ${chalk.red(`Failed:`)} ${failed}`); } console.log(` Total time: ${totalTime.toFixed(1)}s`); console.log(); if (failed > 0) { console.log(chalk.red('\nāŒ Failed agents:')); results .filter(r => r.status === 'failed') .forEach(r => { console.log(` - ${r.agentId}: ${r.error}`); }); } if (successful === results.length) { console.log(chalk.green('\n✨ All documentation generated successfully!')); } } async function detectClaudeCommand() { // Default path for Claude local installation const defaultPath = `${process.env.HOME}/.claude/local/claude`; try { await execAsync(`test -f "${defaultPath}"`); return defaultPath; } catch { // Not at default location } // Check if claude command works (for npm global installs) try { await execAsync('claude --version'); return 'claude'; } catch { return null; } } async function runPlanCommand(args, options) { try { // Set log level if (options.debug) { setLogLevel('debug'); } else if (options.verbose) { setLogLevel('info'); } logger.debug('Starting plan command', { args, options }); // Show version console.log(chalk.cyan(`\nšŸš€ @every/cli v${packageJson.version}\n`)); // Load configuration const spinner = ora('Loading configuration...').start(); const configLoader = new ConfigLoader(); let config; try { config = await configLoader.load(options.config); } catch (error) { // If no config found, create a minimal one config = { defaultCommand: await detectClaudeCommand() || 'claude', defaultFlags: [], patterns: [], parallelism: { maxAgents: 1, batchSize: 100 }, variables: {} }; } spinner.succeed('Configuration loaded'); // Detect Claude command if not configured const claudeCommand = config.defaultCommand || await detectClaudeCommand() || 'claude'; // Join all arguments as the plan description const planDescription = args.join(' '); logger.debug('Plan description:', { planDescription }); if (!planDescription) { throw new Error('Please provide a plan description. Usage: every plan "your feature description"'); } // Create output directory const outputDir = options.output || 'plans'; logger.debug('Creating output directory:', { outputDir }); if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); logger.debug('Output directory created'); } // Generate filename based on current date and sanitized title const date = new Date().toISOString().split('T')[0]; const sanitizedTitle = planDescription .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .substring(0, 20); // Limit to 20 characters for shorter filenames const outputFile = join(outputDir, `${date}-${sanitizedTitle}.md`); logger.debug('Generated output file path:', { outputFile, date, sanitizedTitle }); // Create the task to run Claude with the plan workflow const promptPath = 'plan.md'; logger.debug('Creating plan task with prompt:', { promptPath, claudeCommand }); const task = { pattern: { name: 'plan', description: 'Create implementation plan', agents: [] }, agent: { id: 'plan-agent', command: claudeCommand, promptFile: promptPath, promptMode: 'stdin', workingDir: process.cwd(), flags: [], timeout: options.timeout, allowedTools: [ 'Read', 'Write', 'Edit', 'MultiEdit', 'Glob', 'Grep', 'LS' ] }, variables: { ARGUMENTS: planDescription, outputFile, outputDir, date, timestamp: new Date().toISOString(), includeTimeline: options.timeline || false, includeResources: options.resources || false, includeTasks: options.tasks || false, }, outputPath: outputFile }; if (options.dryRun) { console.log(chalk.yellow('\nDry run mode - would execute:\n')); console.log(chalk.gray(` Command: ${claudeCommand}`)); console.log(chalk.gray(` Prompt: plan.md`)); console.log(chalk.gray(` Arguments: ${planDescription}`)); console.log(chalk.gray(` Output: ${outputFile}`)); return; } // Run the agent const manager = new AgentManager(1); const progressDisplay = new ProgressDisplay(manager, options.debug); console.log(chalk.cyan(`\nšŸ“‹ Creating plan for: "${planDescription}"\n`)); logger.debug('Starting agent execution'); progressDisplay.start(1); try { logger.debug('Running agent batch with task:', { taskId: task.agent.id }); const results = await manager.runBatch([task]); progressDisplay.stop(); logger.debug('Agent execution completed:', { status: results[0].status, duration: results[0].duration, error: results[0].error }); if (results[0].status === 'success') { // Check if the agent created the file logger.debug('Checking if output file exists:', { outputFile }); if (existsSync(outputFile)) { logger.debug('Output file found, plan created successfully'); console.log(chalk.green(`\n✨ Plan created successfully!`)); console.log(chalk.gray(` Output: ${outputFile}`)); } else { logger.error('Output file not found after successful execution'); throw new Error('Plan completed but output file was not created'); } } else { logger.error('Plan generation failed:', { error: results[0].error }); throw new Error(results[0].error || 'Plan generation failed'); } } catch (error) { progressDisplay.stop(); throw error; } } catch (error) { logger.error('Plan command failed:', error); console.error(chalk.red(`\nāœ– Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); process.exit(1); } } async function runResearchCommand(args, options) { try { // Set log level if (options.debug) { setLogLevel('debug'); } else if (options.verbose) { setLogLevel('info'); } // Show version console.log(chalk.cyan(`\nšŸš€ @every-env/cli v${packageJson.version}\n`)); // Load configuration const spinner = ora('Loading configuration...').start(); const configLoader = new ConfigLoader(); let config; try { config = await configLoader.load(options.config); } catch (error) { // If no config found, create a minimal one config = { defaultCommand: await detectClaudeCommand() || 'claude', defaultFlags: [], patterns: [], parallelism: { maxAgents: 1, batchSize: 100 }, variables: {} }; } spinner.succeed('Configuration loaded'); // Detect Claude command if not configured const claudeCommand = config.defaultCommand || await detectClaudeCommand() || 'claude'; // Join all arguments as the research task description const researchTask = args.join(' '); if (!researchTask) { throw new Error('Please provide a research task. Usage: every-env research "your task description"'); } // Create output directory const outputDir = options.output || 'research'; if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } // Generate filename based on current date and sanitized task const date = new Date().toISOString().split('T')[0]; const sanitizedTask = researchTask .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .substring(0, 50); // Limit to 50 characters for reasonable filenames const outputFile = join(outputDir, `research-task-${date}-${sanitizedTask}.md`); // Always use research-task.md for GitHub repository analysis const researchType = options.type || 'task'; const promptPath = 'research-task.md'; // Create the task to run Claude with the research workflow const task = { pattern: { name: 'research', description: 'Conduct research', agents: [] }, agent: { id: 'research-agent', command: claudeCommand, promptFile: promptPath, promptMode: 'stdin', workingDir: process.cwd(), flags: [], timeout: options.timeout, allowedTools: [ 'Read', 'Write', 'Edit', 'MultiEdit', 'Glob', 'Grep', 'LS', 'WebSearch', 'WebFetch' ] }, variables: { TASK: researchTask, outputFile, outputDir, date, timestamp: new Date().toISOString(), researchType, depth: options.depth || 'standard', }, outputPath: outputFile }; if (options.dryRun) { console.log(chalk.yellow('\nDry run mode - would execute:\n')); console.log(chalk.gray(` Command: ${claudeCommand}`)); console.log(chalk.gray(` Prompt: ${promptPath}`)); console.log(chalk.gray(` Task: ${researchTask}`)); console.log(chalk.gray(` Type: ${researchType}`)); console.log(chalk.gray(` Output: ${outputFile}`)); return; } // Run the agent const manager = new AgentManager(1); const progressDisplay = new ProgressDisplay(manager); console.log(chalk.cyan(`\nšŸ” Researching: "${researchTask}"\n`)); console.log(chalk.gray(`Research type: ${researchType}`)); progressDisplay.start(1); try { const results = await manager.runBatch([task]); progressDisplay.stop(); if (results[0].status === 'success') { // Check if the agent created the file if (existsSync(outputFile)) { console.log(chalk.green(`\n✨ Research completed successfully!`)); console.log(chalk.gray(` Output: ${outputFile}`)); } else { throw new Error('Research completed but output file was not created'); } } else { throw new Error(results[0].error || 'Research generation failed'); } } catch (error) { progressDisplay.stop(); throw error; } } catch (error) { logger.error('Research command failed:', error); console.error(chalk.red(`\nāœ– Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); process.exit(1); } } // Add init command as top-level program .command('init') .description('Initialize every in your project') .option('--force', 'Force regeneration even if files exist') .option('--template <name>', 'Use a template (e.g., rails)') .action(async (options) => { await runCommand('init', options); }); // Add copy-commands command program .command('copy-commands') .description('Copy Claude Code workflow templates from the package to your project') .option('--force', 'Overwrite existing files') .action(async (options) => { try { await executeCopyCommands(options); } catch (error) { console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Add plan command directly program .command('plan [description...]') .description('Create implementation plans and work breakdowns') .option('-o, --output <path>', 'Output directory (default: plans)') .option('-t, --template <name>', 'Use a specific plan template') .option('--timeline', 'Include timeline in plan') .option('--resources', 'Include resource requirements') .option('--tasks', 'Generate work breakdown structure') .option('-c, --config <path>', 'Configuration file path') .option('--dry-run', 'Show what would be executed without running') .option('-v, --verbose', 'Verbose output') .option('--debug', 'Debug output') .option('--timeout <ms>', 'Process timeout in milliseconds (default: 7200000 / 2 hours)', parseInt) .action(async (description, options) => { await runPlanCommand(description, options); }); // Add research command program .command('research [task...]') .description('Conduct in-depth research on a specific task or topic') .option('-o, --output <path>', 'Output directory (default: research)') .option('-t, --type <type>', 'Research type: task, feature, or codebase (default: task)') .option('--depth <level>', 'Research depth: quick, standard, or deep (default: standard)') .option('-c, --config <path>', 'Configuration file path') .option('--dry-run', 'Show what would be executed without running') .option('-v, --verbose', 'Verbose output') .option('--debug', 'Debug output') .option('--timeout <ms>', 'Process timeout in milliseconds (default: 7200000 / 2 hours)', parseInt) .action(async (task, options) => { await runResearchCommand(task, options); }); // Parse and run program.parse(); // Handle interactive mode const options = program.opts(); if (options.interactive) { // Launch interactive mode import('./cli-interactive.js').then(() => { // Interactive mode handles its own lifecycle }).catch(error => { console.error('Failed to launch interactive mode:', error); process.exit(1); }); } //# sourceMappingURL=cli-old.js.map