@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
text/typescript
/**
* 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;
}
}
}