UNPKG

@aerocorp/cli

Version:

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

337 lines (289 loc) • 9.85 kB
/** * AeroCorp CLI 5.0.0 - Native SSH Service * Pure Node.js SSH implementation that works on Windows without WSL * Uses ssh2 library for cross-platform SSH functionality */ import { Client, ConnectConfig } from 'ssh2'; import chalk from 'chalk'; import ora from 'ora'; import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { ConfigService } from './config'; export interface SSHConnectionConfig { host: string; port: number; username: string; privateKey?: Buffer; password?: string; timeout?: number; } export interface SSHCommandOptions { timeout?: number; follow?: boolean; encoding?: string; } export class NativeSSHService { private configService: ConfigService; private defaultConfig: SSHConnectionConfig; private client?: Client; constructor() { this.configService = new ConfigService(); this.defaultConfig = { host: this.configService.get('server_ip') || '128.140.35.238', port: 22, username: 'root', timeout: 10000 }; } /** * Connect to SSH server using Node.js ssh2 library (Windows native) */ async connect(config?: Partial<SSHConnectionConfig>): Promise<boolean> { const spinner = ora('Connecting to server via SSH...').start(); try { const connectionConfig: SSHConnectionConfig = { ...this.defaultConfig, ...config }; // Try to load SSH private key const privateKey = await this.loadSSHKey(); if (privateKey) { connectionConfig.privateKey = privateKey; } this.client = new Client(); return new Promise((resolve, reject) => { this.client!.on('ready', () => { spinner.succeed('SSH connection established'); console.log(chalk.green(`āœ… Connected to ${connectionConfig.host}`)); resolve(true); }); this.client!.on('error', (error) => { spinner.fail('SSH connection failed'); console.error(chalk.red('āŒ SSH Error:'), error.message); if (error.message.includes('ECONNREFUSED')) { console.log(chalk.yellow('šŸ’” Check if SSH service is running on the server')); } else if (error.message.includes('Authentication')) { console.log(chalk.yellow('šŸ’” Check SSH key or try: aerocorp windows setup-ssh')); } resolve(false); }); this.client!.connect(connectionConfig as ConnectConfig); }); } catch (error) { spinner.fail('SSH connection setup failed'); console.error(chalk.red('āŒ Error:'), error.message); return false; } } /** * Execute command via SSH */ async executeCommand(command: string, options: SSHCommandOptions = {}): Promise<{ success: boolean; output: string; error?: string; }> { if (!this.client) { const connected = await this.connect(); if (!connected) { throw new Error('SSH connection failed'); } } return new Promise((resolve, reject) => { this.client!.exec(command, (err, stream) => { if (err) { reject(new Error(`SSH exec error: ${err.message}`)); return; } let output = ''; let error = ''; stream.on('close', (code: number) => { resolve({ success: code === 0, output: output.trim(), error: error.trim() }); }); stream.on('data', (data: Buffer) => { const text = data.toString(); output += text; if (options.follow) { process.stdout.write(text); } }); stream.stderr.on('data', (data: Buffer) => { const text = data.toString(); error += text; if (options.follow) { process.stderr.write(chalk.red(text)); } }); // Handle timeout if (options.timeout) { setTimeout(() => { stream.destroy(); reject(new Error('SSH command timed out')); }, options.timeout); } }); }); } /** * Get Docker logs using pure Node.js SSH */ async getDockerLogs(applicationId: string, options: { lines?: number; follow?: boolean; timeout?: number; } = {}): Promise<void> { const spinner = ora(`Fetching Docker logs for ${applicationId}...`).start(); try { const dockerCommand = `docker logs ${options.follow ? '-f' : ''} --tail ${options.lines || 100} $(docker ps --filter "label=coolify.applicationId=${applicationId}" --format "{{.ID}}" | head -1)`; spinner.stop(); console.log(chalk.cyan(`\nšŸ“‹ Docker Logs (${options.follow ? 'following' : 'last ' + (options.lines || 100) + ' lines'}):`)); console.log(chalk.gray('─'.repeat(80))); if (options.follow) { console.log(chalk.gray('Press Ctrl+C to stop following logs\n')); } const result = await this.executeCommand(dockerCommand, { follow: options.follow, timeout: options.timeout }); if (!options.follow) { if (result.success && result.output) { console.log(result.output); } else if (result.error) { console.error(chalk.red('āŒ Error:'), result.error); } else { console.log(chalk.yellow('āš ļø No logs available')); } } } catch (error) { spinner.fail('Failed to get Docker logs'); throw error; } } /** * Load SSH private key from standard locations */ private async loadSSHKey(): Promise<Buffer | null> { const possibleKeyPaths = [ join(homedir(), '.ssh', 'id_ed25519'), join(homedir(), '.ssh', 'id_rsa'), join(homedir(), '.ssh', 'aerocorp_key'), // Windows-specific paths join(process.env.USERPROFILE || homedir(), '.ssh', 'id_ed25519'), join(process.env.USERPROFILE || homedir(), '.ssh', 'id_rsa') ]; for (const keyPath of possibleKeyPaths) { try { if (existsSync(keyPath)) { const privateKey = readFileSync(keyPath); console.log(chalk.blue(`šŸ”‘ Using SSH key: ${keyPath}`)); return privateKey; } } catch (error) { // Continue to next key path } } console.log(chalk.yellow('āš ļø No SSH private key found')); console.log(chalk.blue('šŸ’” Generate one with: aerocorp windows setup-ssh')); return null; } /** * Generate SSH key pair using Node.js crypto (Windows compatible) */ async generateSSHKeyPair(): Promise<{ publicKey: string; privateKey: string }> { const { generateKeyPairSync } = await import('crypto'); const { publicKey, privateKey } = generateKeyPairSync('ed25519', { publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); return { publicKey, privateKey }; } /** * Test SSH connection */ async testConnection(): Promise<boolean> { const spinner = ora('Testing SSH connection...').start(); try { const connected = await this.connect(); if (connected) { // Test a simple command const result = await this.executeCommand('echo "SSH test successful"'); if (result.success) { spinner.succeed('SSH connection test passed'); console.log(chalk.green('āœ… SSH functionality is working')); return true; } else { spinner.fail('SSH command execution failed'); return false; } } else { spinner.fail('SSH connection failed'); return false; } } catch (error) { spinner.fail('SSH test failed'); console.error(chalk.red('āŒ Error:'), error.message); return false; } finally { await this.disconnect(); } } /** * Disconnect SSH client */ async disconnect(): Promise<void> { if (this.client) { this.client.end(); this.client = undefined; } } /** * Check if SSH is available (either native Windows SSH or Node.js ssh2) */ async checkSSHAvailability(): Promise<{ nativeSSH: boolean; nodeSSH: boolean; recommended: 'native' | 'node' | 'none'; }> { const spinner = ora('Checking SSH availability...').start(); try { // Check for Windows native SSH let nativeSSH = false; try { const { spawn } = require('child_process'); const sshProcess = spawn('ssh', ['-V'], { stdio: 'pipe' }); await new Promise((resolve) => { sshProcess.on('close', (code) => { nativeSSH = code === 0; resolve(void 0); }); sshProcess.on('error', () => { nativeSSH = false; resolve(void 0); }); }); } catch (error) { nativeSSH = false; } // Node.js ssh2 is always available if installed const nodeSSH = true; // We just installed it spinner.succeed('SSH availability checked'); const recommended = nativeSSH ? 'native' : (nodeSSH ? 'node' : 'none'); console.log(chalk.cyan('\nšŸ”— SSH Availability:')); console.log(nativeSSH ? chalk.green(' āœ… Windows native SSH (ssh.exe)') : chalk.yellow(' āš ļø Windows native SSH not available')); console.log(nodeSSH ? chalk.green(' āœ… Node.js SSH (ssh2 library)') : chalk.red(' āŒ Node.js SSH not available')); console.log(chalk.blue(` šŸŽÆ Recommended: ${recommended === 'native' ? 'Windows native SSH' : recommended === 'node' ? 'Node.js SSH' : 'Install SSH client'}`)); return { nativeSSH, nodeSSH, recommended }; } catch (error) { spinner.fail('SSH availability check failed'); throw error; } } }