aide-cli
Version:
AIDE - The companion control system for Claude Code with intelligent task management
507 lines (407 loc) ⢠16.4 kB
JavaScript
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)
};