@every-env/cli
Version:
Multi-agent orchestrator for AI-powered development workflows
675 lines ⢠26.5 kB
JavaScript
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