UNPKG

coderrr-cli

Version:

AI-powered coding agent that understands natural language requests and autonomously creates, modifies, and manages code across your projects

229 lines (200 loc) 7.21 kB
/** * Command executor with user permission prompts (like GitHub Copilot) * Refactored to work with new Agent architecture */ const { spawn } = require('child_process'); const fs = require('fs').promises; const path = require('path'); const ui = require('./ui'); class CommandExecutor { constructor() { this.history = []; } // Add this utility function at the top of the class getCommandSeparator() { // Returns the appropriate command separator based on OS return process.platform === 'win32' ? ';' : '&&'; } // Add this method to normalize commands based on OS normalizeCommand(command) { const isWindows = process.platform === 'win32'; if (isWindows) { // Replace && with ; for Windows PowerShell return command.replace(/&&/g, ';'); } // Keep && for Unix-like systems return command; } /** * Execute a shell command with user permission */ async execute(command, options = {}) { const { requirePermission = true, cwd = process.cwd(), shell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash' } = options; // Normalize command based on OS const normalizedCommand = this.normalizeCommand(command); ui.displayCommand(normalizedCommand); // Ask for permission if required if (requirePermission) { const confirmed = await ui.confirm('Execute this command?', false); if (!confirmed) { ui.warning('Command execution cancelled by user'); return { success: false, cancelled: true }; } } // Execute command return new Promise((resolve) => { ui.info('Executing...'); const startTime = Date.now(); let stdout = ''; let stderr = ''; const child = spawn(normalizedCommand, { cwd, shell, stdio: ['inherit', 'pipe', 'pipe'] }); child.stdout.on('data', (data) => { const text = data.toString(); stdout += text; process.stdout.write(text); }); child.stderr.on('data', (data) => { const text = data.toString(); stderr += text; process.stderr.write(text); }); child.on('close', (code) => { const duration = Date.now() - startTime; const result = { success: code === 0, code, stdout, stderr, duration, command: normalizedCommand }; this.history.push(result); if (code === 0) { ui.success(`Command completed successfully (${duration}ms)`); } else { ui.error(`Command failed with exit code ${code}`); } resolve(result); }); child.on('error', (error) => { ui.error(`Failed to execute command: ${error.message}`); resolve({ success: false, error: error.message, command: normalizedCommand }); }); }); } /** * Execute multiple commands in sequence */ async executeBatch(commands, options = {}) { const results = []; for (const command of commands) { const result = await this.execute(command, options); results.push(result); // Stop on first failure unless continueOnError is true if (!result.success && !options.continueOnError) { break; } } return results; } /** * Get command execution history */ getHistory() { return this.history; } /** * Clear history */ clearHistory() { this.history = []; } } // Legacy function for backward compatibility with old blessed TUI code async function safeWriteFile(filePath, content) { const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, content, 'utf8'); } async function safeReadFile(filePath) { try { const txt = await fs.readFile(filePath, 'utf8'); return txt; } catch (e) { return null; } } async function executePlan(plan, ctx) { // Legacy function - kept for backward compatibility const { appendMessage, askYesNo, status } = ctx; for (let i = 0; i < plan.length; ++i) { const step = plan[i]; const idx = i + 1; appendMessage('assistant', `Step ${idx}/${plan.length}: ${step.action} ${step.path || step.command || ''}`); status.setContent(`Executing step ${idx}/${plan.length}${step.action}`); let confirmNeeded = ['create_file','update_file','patch_file','delete_file','run_command'].includes(step.action); let ok = true; if (confirmNeeded) { ok = await askYesNo(`Proceed with step ${idx}: ${step.action} ${step.path || step.command || ''}?`); } if (!ok) { appendMessage('assistant', `Skipped step ${idx} by user.`); continue; } try { if (step.action === 'create_file') { const exists = await safeReadFile(step.path); if (exists !== null) { appendMessage('assistant', `File ${step.path} already exists. Asking before overwrite.`); const ov = await askYesNo(`File ${step.path} exists. Overwrite?`); if (!ov) { appendMessage('assistant','Skipped creation.'); continue; } } await safeWriteFile(step.path, step.content || ''); appendMessage('assistant', `✅ Created/overwritten ${step.path}`); } else if (step.action === 'update_file' || step.action === 'patch_file') { const old = await safeReadFile(step.path); if (old === null) { appendMessage('assistant', `File ${step.path} doesn't exist — will create.`); } await safeWriteFile(step.path, step.content || ''); appendMessage('assistant', `✅ Updated ${step.path}`); } else if (step.action === 'delete_file') { const exists = await safeReadFile(step.path); if (exists === null) { appendMessage('assistant', `File ${step.path} does not exist — nothing to delete.`); } else { await fs.unlink(step.path); appendMessage('assistant', `✅ Deleted ${step.path}`); } } else if (step.action === 'read_file') { const txt = await safeReadFile(step.path); appendMessage('assistant', `Contents of ${step.path}:\n${txt === null ? '[NOT FOUND]' : txt}`); } else if (step.action === 'run_command') { const executor = new CommandExecutor(); const result = await executor.execute(step.command, { requirePermission: true, cwd: process.cwd() }); if (!result.success) { appendMessage('assistant', `Command failed. Stopping further steps.`); break; } } else { appendMessage('assistant', `Unknown action: ${JSON.stringify(step)}`); } } catch (e) { appendMessage('assistant', `Error during step: ${String(e)}`); } } status.setContent('{green-fg}Idle{/}'); } module.exports = { CommandExecutor, executePlan, safeWriteFile, safeReadFile };