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