UNPKG

@aerocorp/cli

Version:

AeroCorp CLI 5.1.0 - Future-Proofed Enterprise Infrastructure with Live Preview, Tunneling & Advanced DevOps

501 lines (425 loc) โ€ข 17.7 kB
/** * AeroCorp CLI 5.0.0 - Windows Native Service * Implements Windows-native deployment without WSL dependency * Similar to how Vercel CLI works seamlessly on Windows */ import { spawn, ChildProcess } from 'child_process'; import { promisify } from 'util'; import chalk from 'chalk'; import ora from 'ora'; import { ConfigService } from './config'; export interface WindowsSSHConfig { host: string; user: string; port?: number; keyPath?: string; useBuiltInSSH?: boolean; } export interface PowerShellOptions { timeout?: number; shell?: 'powershell' | 'pwsh' | 'cmd'; encoding?: string; } export class WindowsNativeService { private configService: ConfigService; private sshConfig: WindowsSSHConfig; constructor() { this.configService = new ConfigService(); this.sshConfig = { host: this.configService.get('server_ip') || '128.140.35.238', user: 'root', port: 22, keyPath: '%USERPROFILE%\\.ssh\\id_ed25519', useBuiltInSSH: true }; } /** * Check Windows environment and available tools */ async checkWindowsEnvironment(): Promise<{ isWindows: boolean; hasOpenSSH: boolean; hasPowerShell: boolean; hasWSL: boolean; nodeVersion: string; }> { const spinner = ora('Checking Windows environment...').start(); try { const isWindows = process.platform === 'win32'; if (!isWindows) { spinner.info('Not running on Windows - using standard Unix tools'); return { isWindows: false, hasOpenSSH: false, hasPowerShell: false, hasWSL: false, nodeVersion: process.version }; } // Check for built-in OpenSSH const sshCheck = await this.executePowerShell('Get-Command ssh -ErrorAction SilentlyContinue'); const hasOpenSSH = sshCheck.success; // Check for PowerShell const pwshCheck = await this.executePowerShell('$PSVersionTable.PSVersion.Major'); const hasPowerShell = pwshCheck.success; // Check for WSL (optional) const wslCheck = await this.executePowerShell('Get-Command wsl -ErrorAction SilentlyContinue'); const hasWSL = wslCheck.success; spinner.succeed('Windows environment checked'); console.log(chalk.cyan('\n๐ŸชŸ Windows Environment:')); console.log(chalk.white(` OS: Windows ${process.arch}`)); console.log(chalk.white(` Node.js: ${process.version}`)); console.log(hasOpenSSH ? chalk.green(' โœ… OpenSSH client available') : chalk.red(' โŒ OpenSSH client missing')); console.log(hasPowerShell ? chalk.green(' โœ… PowerShell available') : chalk.red(' โŒ PowerShell missing')); console.log(hasWSL ? chalk.green(' โœ… WSL available (optional)') : chalk.yellow(' โš ๏ธ WSL not available (optional)')); return { isWindows, hasOpenSSH, hasPowerShell, hasWSL, nodeVersion: process.version }; } catch (error) { spinner.fail('Environment check failed'); throw error; } } /** * Setup SSH keys using Windows native OpenSSH */ async setupWindowsSSH(): Promise<boolean> { const spinner = ora('Setting up Windows SSH...').start(); try { const env = await this.checkWindowsEnvironment(); if (!env.isWindows) { spinner.info('Not on Windows - skipping Windows SSH setup'); return true; } if (!env.hasOpenSSH) { spinner.fail('OpenSSH client not found'); console.log(chalk.red('โŒ OpenSSH client is required')); console.log(chalk.yellow('๐Ÿ’ก Install OpenSSH:')); console.log(chalk.blue(' 1. Open Settings โ†’ Apps โ†’ Optional Features')); console.log(chalk.blue(' 2. Add "OpenSSH Client"')); console.log(chalk.blue(' 3. Or run: Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0')); return false; } spinner.text = 'Checking SSH key...'; // Check if SSH key exists const keyExists = await this.executePowerShell(`Test-Path "${this.sshConfig.keyPath}"`); if (!keyExists.success || keyExists.output.trim() === 'False') { spinner.text = 'Generating SSH key...'; // Create .ssh directory await this.executePowerShell(`New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\\.ssh"`); // Generate SSH key using Windows OpenSSH const keyGenResult = await this.executePowerShell( `ssh-keygen -t ed25519 -C "aerocorp-cli@windows" -f "${this.sshConfig.keyPath}" -N '""'` ); if (!keyGenResult.success) { throw new Error('Failed to generate SSH key'); } console.log(chalk.green('โœ… SSH key generated')); } else { console.log(chalk.blue('๐Ÿ”‘ SSH key already exists')); } spinner.text = 'Testing SSH connection...'; // Test SSH connection const sshTest = await this.executePowerShell( `ssh -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=no ${this.sshConfig.user}@${this.sshConfig.host} "echo 'Connection successful'"` ); if (sshTest.success) { spinner.succeed('Windows SSH setup completed'); console.log(chalk.green('โœ… SSH connection to Coolify server works')); return true; } else { spinner.warn('SSH key generated but connection failed'); console.log(chalk.yellow('โš ๏ธ SSH key needs to be copied to server manually')); // Show public key for manual copying const pubKeyResult = await this.executePowerShell(`Get-Content "${this.sshConfig.keyPath}.pub"`); if (pubKeyResult.success) { console.log(chalk.cyan('\n๐Ÿ“‹ Copy this public key to your server:')); console.log(chalk.gray('โ”€'.repeat(80))); console.log(pubKeyResult.output); console.log(chalk.gray('โ”€'.repeat(80))); console.log(chalk.yellow('๐Ÿ’ก Add to server: echo "YOUR_KEY" >> ~/.ssh/authorized_keys')); } return false; } } catch (error) { spinner.fail('Windows SSH setup failed'); console.error(chalk.red('โŒ Error:'), error.message); return false; } } /** * Execute PowerShell command (Windows native) */ async executePowerShell(command: string, options: PowerShellOptions = {}): Promise<{ success: boolean; output: string; error?: string; }> { return new Promise((resolve) => { const shell = options.shell || 'powershell'; const timeout = options.timeout || 30000; const process = spawn(shell, ['-Command', command], { stdio: ['pipe', 'pipe', 'pipe'], shell: false }); let output = ''; let error = ''; process.stdout?.on('data', (data) => { output += data.toString(); }); process.stderr?.on('data', (data) => { error += data.toString(); }); const timer = setTimeout(() => { process.kill(); resolve({ success: false, output, error: 'Command timed out' }); }, timeout); process.on('close', (code) => { clearTimeout(timer); resolve({ success: code === 0, output: output.trim(), error: error.trim() }); }); process.on('error', (err) => { clearTimeout(timer); resolve({ success: false, output, error: err.message }); }); }); } /** * Execute SSH command using Windows native SSH client */ async executeSSH(command: string, options: { follow?: boolean; timeout?: number } = {}): Promise<void> { const spinner = ora('Executing SSH command...').start(); try { const sshCommand = `ssh -o ConnectTimeout=10 ${this.sshConfig.user}@${this.sshConfig.host} "${command}"`; if (options.follow) { spinner.stop(); console.log(chalk.cyan(`\n๐Ÿ”— SSH Command: ${command}`)); console.log(chalk.gray('โ”€'.repeat(80))); // For following commands (like logs), use live output const sshProcess = spawn('ssh', [ '-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=no', `${this.sshConfig.user}@${this.sshConfig.host}`, command ], { stdio: 'inherit' }); return new Promise((resolve, reject) => { sshProcess.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`SSH command failed with code ${code}`)); } }); sshProcess.on('error', (error) => { reject(new Error(`SSH process error: ${error.message}`)); }); }); } else { // For one-time commands, capture output const result = await this.executePowerShell(sshCommand, { timeout: options.timeout }); spinner.stop(); if (result.success) { console.log(chalk.green('โœ… SSH command executed successfully')); if (result.output) { console.log(result.output); } } else { console.log(chalk.red('โŒ SSH command failed')); if (result.error) { console.error(chalk.red('Error:'), result.error); } throw new Error('SSH command execution failed'); } } } catch (error) { spinner.fail('SSH execution failed'); throw error; } } /** * Get Docker logs using Windows native SSH */ async getDockerLogsNative(applicationId: string, options: { lines?: number; follow?: boolean } = {}): Promise<void> { const dockerCommand = `docker logs ${options.follow ? '-f' : ''} --tail ${options.lines || 100} $(docker ps --filter "label=coolify.applicationId=${applicationId}" --format "{{.ID}}" | head -1)`; console.log(chalk.cyan(`\n๐Ÿ“‹ Docker Logs for Application: ${applicationId}`)); if (options.follow) { console.log(chalk.gray('Press Ctrl+C to stop following logs')); } await this.executeSSH(dockerCommand, { follow: options.follow }); } /** * Install OpenSSH client on Windows (requires admin) */ async installOpenSSH(): Promise<boolean> { const spinner = ora('Installing OpenSSH client...').start(); try { console.log(chalk.yellow('โš ๏ธ This requires Administrator privileges')); const installResult = await this.executePowerShell( 'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0', { timeout: 60000 } ); if (installResult.success) { spinner.succeed('OpenSSH client installed successfully'); console.log(chalk.green('โœ… OpenSSH client is now available')); console.log(chalk.blue('๐Ÿ’ก Restart your terminal to use SSH commands')); return true; } else { spinner.fail('OpenSSH installation failed'); console.log(chalk.red('โŒ Installation failed. Try manual installation:')); console.log(chalk.blue(' Settings โ†’ Apps โ†’ Optional Features โ†’ Add OpenSSH Client')); return false; } } catch (error) { spinner.fail('OpenSSH installation error'); console.error(chalk.red('โŒ Error:'), error.message); return false; } } /** * Show Windows-specific setup instructions */ showWindowsSetupInstructions(): void { console.log(chalk.cyan.bold('\n๐ŸชŸ Windows Native Setup Guide')); console.log(chalk.gray('โ”€'.repeat(60))); console.log(chalk.white('\n๐Ÿ“‹ Prerequisites:')); console.log(chalk.blue(' 1. Windows 10 (1803+) or Windows 11')); console.log(chalk.blue(' 2. Node.js 18+ installed')); console.log(chalk.blue(' 3. PowerShell 5.1+ or PowerShell Core 7+')); console.log(chalk.white('\n๐Ÿ”ง Setup Steps:')); console.log(chalk.blue(' 1. aerocorp windows check # Check environment')); console.log(chalk.blue(' 2. aerocorp windows setup-ssh # Setup SSH keys')); console.log(chalk.blue(' 3. aerocorp coolify health # Test Coolify API')); console.log(chalk.blue(' 4. aerocorp windows test # Test all functionality')); console.log(chalk.white('\n๐Ÿš€ Usage Examples:')); console.log(chalk.blue(' aerocorp coolify deploy <uuid> # Deploy via API')); console.log(chalk.blue(' aerocorp preview up --pr 123 --app <uuid> # Create PR preview')); console.log(chalk.blue(' aerocorp logs <uuid> # Get logs via API')); console.log(chalk.blue(' aerocorp logs <uuid> --ssh # Get logs via SSH')); console.log(chalk.white('\n๐Ÿ’ก Pro Tips:')); console.log(chalk.gray(' โ€ข Use API methods for automation (faster, more reliable)')); console.log(chalk.gray(' โ€ข Use SSH methods for debugging and live log streaming')); console.log(chalk.gray(' โ€ข Set COOLIFY_TOKEN environment variable for CI/CD')); } /** * Test all Windows functionality */ async testWindowsFunctionality(): Promise<boolean> { console.log(chalk.cyan.bold('\n๐Ÿงช Testing Windows Native Functionality')); console.log(chalk.gray('โ”€'.repeat(60))); let allPassed = true; // Test 1: Environment check console.log(chalk.blue('\n๐Ÿ“‹ Test 1: Environment Check')); try { const env = await this.checkWindowsEnvironment(); console.log(chalk.green('โœ… Environment check passed')); } catch (error) { console.log(chalk.red('โŒ Environment check failed')); allPassed = false; } // Test 2: PowerShell execution console.log(chalk.blue('\n๐Ÿ“‹ Test 2: PowerShell Execution')); try { const result = await this.executePowerShell('echo "PowerShell test successful"'); if (result.success) { console.log(chalk.green('โœ… PowerShell execution works')); } else { console.log(chalk.red('โŒ PowerShell execution failed')); allPassed = false; } } catch (error) { console.log(chalk.red('โŒ PowerShell test failed')); allPassed = false; } // Test 3: SSH availability console.log(chalk.blue('\n๐Ÿ“‹ Test 3: SSH Client Availability')); try { const sshResult = await this.executePowerShell('ssh -V'); if (sshResult.success) { console.log(chalk.green('โœ… SSH client available')); console.log(chalk.gray(` Version: ${sshResult.output.split('\n')[0]}`)); } else { console.log(chalk.yellow('โš ๏ธ SSH client not available')); console.log(chalk.blue('๐Ÿ’ก Run: aerocorp windows install-ssh')); } } catch (error) { console.log(chalk.yellow('โš ๏ธ SSH test inconclusive')); } // Test 4: Coolify API connectivity console.log(chalk.blue('\n๐Ÿ“‹ Test 4: Coolify API Connectivity')); try { const { CoolifyService } = await import('./coolify'); const coolifyService = new CoolifyService(); const healthCheck = await coolifyService.healthCheck(); if (healthCheck) { console.log(chalk.green('โœ… Coolify API connection works')); } else { console.log(chalk.red('โŒ Coolify API connection failed')); console.log(chalk.yellow('๐Ÿ’ก Check your COOLIFY_TOKEN and network connectivity')); allPassed = false; } } catch (error) { console.log(chalk.red('โŒ Coolify API test failed')); allPassed = false; } // Summary console.log(chalk.cyan('\n๐Ÿ“Š Test Summary:')); console.log(chalk.gray('โ”€'.repeat(30))); if (allPassed) { console.log(chalk.green.bold('๐ŸŽ‰ All tests passed! Windows native functionality is ready.')); console.log(chalk.white('\nYou can now use:')); console.log(chalk.blue(' โ€ข aerocorp coolify deploy <uuid>')); console.log(chalk.blue(' โ€ข aerocorp preview up --pr <num> --app <uuid> --branch <branch>')); console.log(chalk.blue(' โ€ข aerocorp logs <uuid> [--ssh]')); } else { console.log(chalk.yellow.bold('โš ๏ธ Some tests failed. Check the issues above.')); console.log(chalk.white('\nTroubleshooting:')); console.log(chalk.blue(' โ€ข aerocorp windows install-ssh # Install OpenSSH')); console.log(chalk.blue(' โ€ข aerocorp coolify login # Setup API authentication')); } return allPassed; } /** * Get system information for troubleshooting */ async getSystemInfo(): Promise<void> { console.log(chalk.cyan.bold('\n๐Ÿ–ฅ๏ธ System Information')); console.log(chalk.gray('โ”€'.repeat(50))); try { // Windows version const winVersion = await this.executePowerShell('(Get-CimInstance Win32_OperatingSystem).Caption'); if (winVersion.success) { console.log(chalk.white(`OS: ${winVersion.output}`)); } // PowerShell version const psVersion = await this.executePowerShell('$PSVersionTable.PSVersion.ToString()'); if (psVersion.success) { console.log(chalk.white(`PowerShell: ${psVersion.output}`)); } // Node.js info console.log(chalk.white(`Node.js: ${process.version} (${process.arch})`)); // Network connectivity const pingResult = await this.executePowerShell(`Test-NetConnection -ComputerName ${this.sshConfig.host} -Port 22 -InformationLevel Quiet`); if (pingResult.success && pingResult.output.trim() === 'True') { console.log(chalk.green('โœ… Network connectivity to Coolify server')); } else { console.log(chalk.red('โŒ Cannot reach Coolify server')); } } catch (error) { console.error(chalk.red('โŒ Failed to get system info:'), error.message); } } }