UNPKG

@aerocorp/cli

Version:

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

371 lines (310 loc) • 11.8 kB
/** * AeroCorp CLI 5.0.0 - Vercel-like Deployment Service * Windows-native deployment that works like "npx vercel --prod" * Uses HTTP APIs primarily, SSH only as fallback */ import axios, { AxiosInstance } from 'axios'; import chalk from 'chalk'; import ora from 'ora'; import { readFileSync, existsSync, statSync } from 'fs'; import { join, basename } from 'path'; import { createHash } from 'crypto'; import FormData from 'form-data'; import { ConfigService } from './config'; import { CoolifyService } from './coolify'; import { NativeSSHService } from './ssh-native'; import { WindowsNativeService } from './windows-native'; export interface DeploymentResult { success: boolean; url?: string; deploymentId?: string; logs?: string[]; duration?: number; } export interface ProjectDetection { type: 'vite' | 'next' | 'react' | 'node' | 'static' | 'unknown'; buildCommand?: string; outputDir?: string; packageManager: 'npm' | 'yarn' | 'pnpm'; hasDockerfile: boolean; } export class VercelLikeDeployService { private configService: ConfigService; private coolifyService: CoolifyService; private sshService: NativeSSHService; private windowsService: WindowsNativeService; constructor() { this.configService = new ConfigService(); this.coolifyService = new CoolifyService(); this.sshService = new NativeSSHService(); this.windowsService = new WindowsNativeService(); } /** * Main deployment function - works like "npx vercel --prod" */ async deploy(options: { prod?: boolean; preview?: boolean; app?: string; branch?: string; tag?: string; build?: boolean; yes?: boolean; } = {}): Promise<DeploymentResult> { const startTime = Date.now(); console.log(chalk.cyan.bold('\nšŸš€ AeroCorp Deployment (Vercel-like)')); console.log(chalk.gray('─'.repeat(50))); try { // Step 1: Environment validation await this.validateEnvironment(); // Step 2: Project detection const project = await this.detectProject(); console.log(chalk.blue(`šŸ“¦ Detected: ${project.type} project`)); // Step 3: Build if needed if (options.build !== false && project.buildCommand) { await this.buildProject(project); } // Step 4: Deploy via Coolify API (primary method) const deployResult = await this.deployViaCoolify(project, options); const duration = Date.now() - startTime; console.log(chalk.green.bold('\nšŸŽ‰ Deployment completed successfully!')); console.log(chalk.white(`ā±ļø Duration: ${Math.round(duration / 1000)}s`)); if (deployResult.url) { console.log(chalk.blue(`šŸ”— URL: ${deployResult.url}`)); } return { success: true, url: deployResult.url, deploymentId: deployResult.deploymentId, duration }; } catch (error) { const duration = Date.now() - startTime; console.log(chalk.red.bold('\nāŒ Deployment failed')); console.log(chalk.white(`ā±ļø Duration: ${Math.round(duration / 1000)}s`)); console.error(chalk.red('Error:'), error.message); return { success: false, duration }; } } /** * Validate deployment environment */ private async validateEnvironment(): Promise<void> { const spinner = ora('Validating environment...').start(); try { // Check authentication if (!this.coolifyService.isAuthenticated()) { spinner.fail('Authentication required'); console.log(chalk.red('āŒ Not authenticated with Coolify')); console.log(chalk.yellow('šŸ’” Run: aerocorp coolify login')); throw new Error('Authentication required'); } // Check Coolify connectivity const healthCheck = await this.coolifyService.healthCheck(); if (!healthCheck) { throw new Error('Coolify health check failed'); } spinner.succeed('Environment validated'); } catch (error) { spinner.fail('Environment validation failed'); throw error; } } /** * Detect project type and configuration */ private async detectProject(): Promise<ProjectDetection> { const spinner = ora('Detecting project type...').start(); try { let type: ProjectDetection['type'] = 'unknown'; let buildCommand: string | undefined; let outputDir: string | undefined; let packageManager: ProjectDetection['packageManager'] = 'npm'; // Check for package manager if (existsSync('pnpm-lock.yaml')) { packageManager = 'pnpm'; } else if (existsSync('yarn.lock')) { packageManager = 'yarn'; } // Check for project type if (existsSync('vite.config.ts') || existsSync('vite.config.js')) { type = 'vite'; buildCommand = `${packageManager} run build`; outputDir = 'dist'; } else if (existsSync('next.config.js') || existsSync('next.config.ts')) { type = 'next'; buildCommand = `${packageManager} run build`; outputDir = '.next'; } else if (existsSync('package.json')) { const pkg = JSON.parse(readFileSync('package.json', 'utf8')); if (pkg.dependencies?.react || pkg.devDependencies?.react) { type = 'react'; buildCommand = `${packageManager} run build`; outputDir = 'build'; } else { type = 'node'; buildCommand = pkg.scripts?.build ? `${packageManager} run build` : undefined; } } else if (existsSync('index.html')) { type = 'static'; outputDir = '.'; } const hasDockerfile = existsSync('Dockerfile'); spinner.succeed(`Project detected: ${type}`); return { type, buildCommand, outputDir, packageManager, hasDockerfile }; } catch (error) { spinner.fail('Project detection failed'); throw error; } } /** * Build project using detected package manager */ private async buildProject(project: ProjectDetection): Promise<void> { if (!project.buildCommand) { console.log(chalk.blue('šŸ“¦ No build command detected, skipping build')); return; } const spinner = ora(`Building project with ${project.packageManager}...`).start(); try { const { spawn } = require('child_process'); const [command, ...args] = project.buildCommand.split(' '); const buildProcess = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], shell: true, cwd: process.cwd() }); let buildOutput = ''; let buildError = ''; buildProcess.stdout?.on('data', (data) => { buildOutput += data.toString(); }); buildProcess.stderr?.on('data', (data) => { buildError += data.toString(); }); await new Promise((resolve, reject) => { buildProcess.on('close', (code) => { if (code === 0) { resolve(void 0); } else { reject(new Error(`Build failed with code ${code}\n${buildError}`)); } }); buildProcess.on('error', (error) => { reject(new Error(`Build process error: ${error.message}`)); }); }); spinner.succeed('Project built successfully'); console.log(chalk.green('āœ… Build completed')); } catch (error) { spinner.fail('Build failed'); throw error; } } /** * Deploy via Coolify API (primary method - like Vercel) */ private async deployViaCoolify(project: ProjectDetection, options: any): Promise<{ url?: string; deploymentId?: string; }> { const spinner = ora('Deploying via Coolify API...').start(); try { // Determine environment const environment = options.prod ? 'production' : (options.preview ? 'preview' : 'staging'); spinner.text = `Deploying to ${environment}...`; // Use Coolify service for deployment const deployResult = await this.coolifyService.deploy({ app: options.app, branch: options.branch, tag: options.tag, environment }); spinner.succeed(`Deployed to ${environment}`); return { url: deployResult.url, deploymentId: deployResult.deployment_id }; } catch (error) { spinner.fail('Coolify API deployment failed'); // Fallback to SSH deployment if API fails console.log(chalk.yellow('šŸ”„ Falling back to SSH deployment...')); return await this.deployViaSSH(project, options); } } /** * Fallback SSH deployment method */ private async deployViaSSH(project: ProjectDetection, options: any): Promise<{ url?: string; deploymentId?: string; }> { const spinner = ora('Deploying via SSH fallback...').start(); try { // Connect to server const connected = await this.sshService.connect(); if (!connected) { throw new Error('SSH connection failed'); } // Execute deployment commands via SSH const deployCommands = [ 'cd /opt/coolify', `docker-compose exec coolify php artisan app:deploy ${options.app || 'default'}` ]; for (const command of deployCommands) { spinner.text = `Executing: ${command}`; const result = await this.sshService.executeCommand(command); if (!result.success) { throw new Error(`SSH command failed: ${command}\n${result.error}`); } } spinner.succeed('SSH deployment completed'); return { url: `https://${options.app || 'app'}.aerocorpindustries.org`, deploymentId: `ssh-${Date.now()}` }; } catch (error) { spinner.fail('SSH deployment failed'); throw error; } finally { await this.sshService.disconnect(); } } /** * Show deployment help (Vercel-like) */ showDeploymentHelp(): void { console.log(chalk.cyan.bold('\nšŸš€ AeroCorp Deployment Commands')); console.log(chalk.gray('─'.repeat(50))); console.log(chalk.white('\nšŸ“‹ Basic Usage:')); console.log(chalk.blue(' aerocorp deploy # Deploy to staging')); console.log(chalk.blue(' aerocorp deploy --prod # Deploy to production')); console.log(chalk.blue(' aerocorp deploy --preview # Deploy preview')); console.log(chalk.white('\nšŸŽÆ Advanced Options:')); console.log(chalk.blue(' aerocorp deploy --app <uuid> # Specific application')); console.log(chalk.blue(' aerocorp deploy --branch <name> # Specific branch')); console.log(chalk.blue(' aerocorp deploy --tag <version> # Specific tag')); console.log(chalk.blue(' aerocorp deploy --no-build # Skip build step')); console.log(chalk.white('\nšŸ”„ PR Previews:')); console.log(chalk.blue(' aerocorp preview up --pr 123 --app <uuid> --branch <name>')); console.log(chalk.blue(' aerocorp preview down --pr 123 --app <uuid>')); console.log(chalk.white('\n🪟 Windows-Specific:')); console.log(chalk.blue(' aerocorp windows check # Check Windows environment')); console.log(chalk.blue(' aerocorp windows setup-ssh # Setup SSH (fallback)')); console.log(chalk.blue(' aerocorp windows test # Test all functionality')); console.log(chalk.white('\nšŸ’” Pro Tips:')); console.log(chalk.gray(' • Set COOLIFY_TOKEN environment variable for automation')); console.log(chalk.gray(' • Use --prod flag for production deployments')); console.log(chalk.gray(' • SSH is only used as fallback - HTTP API is primary')); console.log(chalk.gray(' • Works natively on Windows, macOS, and Linux')); } }