UNPKG

aide-cli

Version:

AIDE - The companion control system for Claude Code with intelligent task management

507 lines (407 loc) • 16.4 kB
const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const chalk = require('chalk'); const inquirer = require('inquirer'); const PythonDetector = require('./python-detector'); class AIDEInstaller { constructor() { this.pythonDetector = new PythonDetector(); this.homeDir = os.homedir(); this.claudeDir = path.join(this.homeDir, '.claude'); this.scriptsDir = this.claudeDir; this.packageDir = path.dirname(__dirname); } async install(options = {}) { console.log(chalk.blue('šŸŽÆ Installing AIDE integration for Claude Code')); // 1. Detect system and Python const systemInfo = await this.pythonDetector.getPlatformInfo(); const pythonInfo = await this.pythonDetector.findBestPython(options.python); // 2. Setup directories const claudeDir = options.claudeDir || this.claudeDir; await this.setupDirectories(claudeDir); // 3. Backup existing configuration await this.backupExistingConfig(claudeDir, options.force); // 4. Install Python scripts await this.installPythonScripts(claudeDir); // 5. Create wrapper scripts await this.createWrapperScripts(claudeDir, pythonInfo, systemInfo); // 6. Configure AIDE daemon (if enabled) if (options.daemon !== false) { await this.configureDaemon(claudeDir); await this.setupAutoStartOnBoot(claudeDir, systemInfo); await this.startDaemonAfterInstall(claudeDir); } // 7. Update or create CLAUDE.md await this.updateClaude(claudeDir, pythonInfo, systemInfo, options.daemon !== false); // 8. Verify installation await this.verifyInstallation(claudeDir, pythonInfo); console.log(chalk.green('\nāœ… AIDE installed successfully!')); return { claudeDir, pythonCommand: pythonInfo.command, scriptsInstalled: true }; } async setupDirectories(claudeDir) { console.log(chalk.gray('šŸ“ Setting up directories...')); // Create .claude directory if it doesn't exist await fs.ensureDir(claudeDir); // Create subdirectories await fs.ensureDir(path.join(claudeDir, 'backups')); await fs.ensureDir(path.join(claudeDir, 'scripts')); console.log(chalk.green(` āœ… Directories ready at ${claudeDir}`)); } async backupExistingConfig(claudeDir, force = false) { const claudeMdPath = path.join(claudeDir, 'CLAUDE.md'); if (await fs.pathExists(claudeMdPath)) { if (!force) { const { shouldBackup } = await inquirer.prompt([{ type: 'confirm', name: 'shouldBackup', message: 'Existing CLAUDE.md found. Create backup before proceeding?', default: true }]); if (!shouldBackup) { console.log(chalk.yellow('āš ļø Proceeding without backup...')); return; } } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = path.join(claudeDir, 'backups', `CLAUDE-${timestamp}.md`); await fs.copy(claudeMdPath, backupPath); console.log(chalk.green(` āœ… Backup created: ${path.basename(backupPath)}`)); } } async installPythonScripts(claudeDir) { console.log(chalk.gray('šŸ Installing Python scripts...')); const scriptsToInstall = [ 'aide_init.py', 'aide_track.py', 'aide_status.py', 'aide_adapt.py', 'aide_task_controller.py', 'aide_pre_task.py' ]; for (const script of scriptsToInstall) { const sourcePath = path.join(this.packageDir, 'scripts', script); const targetPath = path.join(claudeDir, script); // If source doesn't exist in package, use the ones we created earlier if (await fs.pathExists(sourcePath)) { await fs.copy(sourcePath, targetPath); } else { // Copy from the original location we created const originalPath = path.join('/Users/eduardeveloper/.claude', script); if (await fs.pathExists(originalPath)) { await fs.copy(originalPath, targetPath); } } // Make executable on Unix systems if (process.platform !== 'win32') { await fs.chmod(targetPath, '755'); } } console.log(chalk.green(` āœ… ${scriptsToInstall.length} Python scripts installed`)); } async createWrapperScripts(claudeDir, pythonInfo, systemInfo) { console.log(chalk.gray('šŸ”§ Creating wrapper scripts...')); const templateData = { PYTHON_COMMAND: pythonInfo.command, SCRIPTS_DIR: claudeDir, ENABLE_FALLBACK: 'true' }; if (systemInfo.isWindows) { await this.createWrapperFromTemplate( 'aide-wrapper.bat', path.join(claudeDir, 'aide-wrapper.bat'), templateData ); } else { await this.createWrapperFromTemplate( 'aide-wrapper.sh', path.join(claudeDir, 'aide-wrapper.sh'), templateData ); // Make executable await fs.chmod(path.join(claudeDir, 'aide-wrapper.sh'), '755'); } console.log(chalk.green(' āœ… Wrapper scripts created')); } async configureDaemon(claudeDir) { console.log(chalk.gray('šŸ”§ Configuring AIDE daemon...')); try { // Create daemon directory for logs and tokens const daemonDir = path.join(claudeDir, '.aide-daemon'); await fs.ensureDir(daemonDir); // Create daemon command wrapper const daemonWrapperContent = `#!/usr/bin/env node const path = require('path'); const { spawn } = require('child_process'); const daemonScript = path.join('${this.packageDir}', 'lib', 'aide-daemon.js'); const args = process.argv.slice(2); const child = spawn('node', [daemonScript, ...args], { stdio: 'inherit' }); child.on('close', (code) => { process.exit(code); }); `; const daemonWrapperPath = path.join(claudeDir, 'aide-daemon'); await fs.writeFile(daemonWrapperPath, daemonWrapperContent); // Make executable on Unix-like systems if (process.platform !== 'win32') { const { exec } = require('child_process'); await new Promise((resolve) => { exec(`chmod +x "${daemonWrapperPath}"`, () => resolve()); }); } console.log(chalk.green(' āœ… Daemon configured for automatic startup')); } catch (error) { console.log(chalk.yellow(' āš ļø Daemon configuration failed (non-critical)')); } } async setupAutoStartOnBoot(claudeDir, systemInfo) { console.log(chalk.gray('āš™ļø Setting up auto-start on boot...')); try { if (systemInfo.platform === 'darwin') { // macOS - usar LaunchAgent await this.setupMacOSLaunchAgent(claudeDir); } else if (systemInfo.platform === 'linux') { // Linux - usar systemd user service await this.setupLinuxSystemdService(claudeDir); } else if (systemInfo.platform === 'win32') { // Windows - agregar a startup await this.setupWindowsStartup(claudeDir); } else { console.log(chalk.yellow(' āš ļø Auto-start not supported on this platform')); } } catch (error) { console.log(chalk.yellow(' āš ļø Auto-start setup failed (daemon will start when first used)')); } } async setupMacOSLaunchAgent(claudeDir) { const os = require('os'); const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents'); const plistFile = path.join(launchAgentsDir, 'com.aide.daemon.plist'); // Asegurar que el directorio existe await fs.ensureDir(launchAgentsDir); const daemonScript = path.join(this.packageDir, 'lib', 'aide-daemon.js'); const plistContent = `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.aide.daemon</string> <key>ProgramArguments</key> <array> <string>node</string> <string>${daemonScript}</string> <string>run</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <dict> <key>SuccessfulExit</key> <false/> </dict> <key>StandardOutPath</key> <string>${claudeDir}/.aide-daemon/stdout.log</string> <key>StandardErrorPath</key> <string>${claudeDir}/.aide-daemon/stderr.log</string> </dict> </plist>`; await fs.writeFile(plistFile, plistContent); // Cargar el LaunchAgent const { exec } = require('child_process'); await new Promise((resolve) => { exec(`launchctl load "${plistFile}"`, () => resolve()); }); console.log(chalk.green(' āœ… macOS auto-start configured')); } async setupLinuxSystemdService(claudeDir) { const os = require('os'); const systemdDir = path.join(os.homedir(), '.config', 'systemd', 'user'); const serviceFile = path.join(systemdDir, 'aide-daemon.service'); await fs.ensureDir(systemdDir); const daemonScript = path.join(this.packageDir, 'lib', 'aide-daemon.js'); const serviceContent = `[Unit] Description=AIDE Daemon After=graphical-session.target [Service] Type=simple ExecStart=node "${daemonScript}" run Restart=on-failure RestartSec=5 StandardOutput=append:${claudeDir}/.aide-daemon/stdout.log StandardError=append:${claudeDir}/.aide-daemon/stderr.log [Install] WantedBy=default.target`; await fs.writeFile(serviceFile, serviceContent); // Enable and start the service const { exec } = require('child_process'); await new Promise((resolve) => { exec('systemctl --user daemon-reload && systemctl --user enable aide-daemon.service', () => resolve()); }); console.log(chalk.green(' āœ… Linux systemd auto-start configured')); } async setupWindowsStartup(claudeDir) { const os = require('os'); const startupDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup'); const batFile = path.join(startupDir, 'aide-daemon.bat'); const daemonScript = path.join(this.packageDir, 'lib', 'aide-daemon.js'); const batContent = `@echo off cd /d "${this.packageDir}" node "${daemonScript}" run > "${claudeDir}\\.aide-daemon\\stdout.log" 2>&1`; await fs.writeFile(batFile, batContent); console.log(chalk.green(' āœ… Windows startup auto-start configured')); } async startDaemonAfterInstall(claudeDir) { console.log(chalk.gray('šŸš€ Starting AIDE daemon...')); try { const { spawn } = require('child_process'); const daemonScript = path.join(this.packageDir, 'lib', 'aide-daemon.js'); // Start daemon in background and wait for confirmation const child = spawn('node', [daemonScript, 'start'], { stdio: 'pipe', detached: false }); let output = ''; child.stdout.on('data', (data) => { output += data.toString(); }); child.stderr.on('data', (data) => { output += data.toString(); }); await new Promise((resolve) => { child.on('close', (code) => { resolve(); }); // Wait for daemon startup confirmation or timeout setTimeout(() => { // Don't kill the child, just resolve - daemon will continue in background resolve(); }, 2000); // Reduced timeout to 2 seconds }); // Wait a moment for daemon to start, then check status await new Promise(resolve => setTimeout(resolve, 1000)); // Check if daemon is actually running by trying to get status try { const statusChild = spawn('node', [daemonScript, 'status'], { stdio: 'pipe' }); let statusOutput = ''; statusChild.stdout.on('data', (data) => { statusOutput += data.toString(); }); await new Promise((resolve) => { statusChild.on('close', (code) => { if (statusOutput.includes('running') || statusOutput.includes('active')) { console.log(chalk.green(' āœ… AIDE daemon started and ready')); } else { console.log(chalk.yellow(' āš ļø Daemon initialization in progress (will be ready shortly)')); } resolve(); }); setTimeout(() => { statusChild.kill(); console.log(chalk.yellow(' āš ļø Daemon starting in background (will be ready when needed)')); resolve(); }, 1000); }); } catch (statusError) { console.log(chalk.yellow(' āš ļø Daemon status check failed (daemon will start when first used)')); } } catch (error) { console.log(chalk.yellow(' āš ļø Daemon auto-start failed (will start when first used)')); } } async createWrapperFromTemplate(templateName, outputPath, data) { const templatePath = path.join(this.packageDir, 'templates', templateName); let content = await fs.readFile(templatePath, 'utf8'); // Replace template variables for (const [key, value] of Object.entries(data)) { const placeholder = `{{${key}}}`; content = content.replace(new RegExp(placeholder, 'g'), value); } await fs.writeFile(outputPath, content); } async updateClaude(claudeDir, pythonInfo, systemInfo, useDaemon = true) { console.log(chalk.gray('šŸ“ Updating CLAUDE.md configuration...')); const claudeMdPath = path.join(claudeDir, 'CLAUDE.md'); const claudeGenerator = require('./claude-generator'); let existingContent = ''; if (await fs.pathExists(claudeMdPath)) { existingContent = await fs.readFile(claudeMdPath, 'utf8'); } const newContent = await claudeGenerator.generateClaude( existingContent, pythonInfo, systemInfo, claudeDir, useDaemon ); await fs.writeFile(claudeMdPath, newContent); const modeText = useDaemon ? 'with daemon support' : 'with direct execution'; console.log(chalk.green(` āœ… CLAUDE.md updated with AIDE integration (${modeText})`)); } async verifyInstallation(claudeDir, pythonInfo) { console.log(chalk.gray('šŸ” Verifying installation...')); const verifier = require('./verifier'); const result = await verifier.verify(claudeDir); if (!result.success) { throw new Error(`Installation verification failed: ${result.issues.join(', ')}`); } console.log(chalk.green(' āœ… Installation verified successfully')); } async uninstall(options = {}) { console.log(chalk.yellow('šŸ—‘ļø Removing AIDE integration...')); const claudeDir = this.claudeDir; // Remove Python scripts const scriptsToRemove = [ 'aide_init.py', 'aide_track.py', 'aide_status.py', 'aide_adapt.py', 'aide-wrapper.sh', 'aide-wrapper.bat' ]; for (const script of scriptsToRemove) { const scriptPath = path.join(claudeDir, script); if (await fs.pathExists(scriptPath)) { await fs.remove(scriptPath); } } // Restore backup if available if (!options.keepBackups) { const backupsDir = path.join(claudeDir, 'backups'); if (await fs.pathExists(backupsDir)) { const backups = await fs.readdir(backupsDir); const latestBackup = backups .filter(f => f.startsWith('CLAUDE-')) .sort() .pop(); if (latestBackup) { const { shouldRestore } = await inquirer.prompt([{ type: 'confirm', name: 'shouldRestore', message: `Restore backup ${latestBackup}?`, default: true }]); if (shouldRestore) { await fs.copy( path.join(backupsDir, latestBackup), path.join(claudeDir, 'CLAUDE.md') ); console.log(chalk.green(` āœ… Restored backup: ${latestBackup}`)); } } } } console.log(chalk.green('āœ… AIDE uninstalled')); } } module.exports = { install: (options) => new AIDEInstaller().install(options), uninstall: (options) => new AIDEInstaller().uninstall(options) };