UNPKG

@aerocorp/cli

Version:

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

286 lines (236 loc) 9.29 kB
import axios from 'axios'; import chalk from 'chalk'; import ora from 'ora'; import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { AuthService } from './auth'; import { ConfigService } from './config'; export class DeployService { private authService = new AuthService(); private configService = new ConfigService(); async deploy(options: any) { console.log(chalk.cyan('╔═══════════════════════╗')); console.log(chalk.cyan('║ AeroCorp Deployment ║')); console.log(chalk.cyan('╚═══════════════════════╝')); // Check authentication if (!this.authService.isAuthenticated()) { throw new Error('Not authenticated. Run "aerocorp login" first.'); } const spinner = ora('Preparing deployment...').start(); try { // Read project configuration const projectConfig = await this.readProjectConfig(); const deploymentConfig = this.buildDeploymentConfig(options, projectConfig); spinner.text = 'Creating deployment...'; // Get Coolify API details const coolifyUrl = this.authService.getCoolifyUrl(); const headers = this.authService.getAuthHeaders(); // Create or update application const appResponse = await this.createOrUpdateApplication(coolifyUrl, headers, deploymentConfig); spinner.text = 'Starting deployment...'; // Trigger deployment const deployResponse = await this.triggerDeployment(coolifyUrl, headers, appResponse.data, deploymentConfig); spinner.succeed('Deployment initiated successfully'); console.log(chalk.green('✓ Deployment started')); console.log(chalk.blue(`📦 Application: ${deploymentConfig.name}`)); console.log(chalk.blue(`🌍 Environment: ${deploymentConfig.environment}`)); console.log(chalk.blue(`🔗 URL: ${deployResponse.url || 'Pending...'}`)); // Monitor deployment if requested if (options.follow || options.watch) { await this.monitorDeployment(coolifyUrl, headers, deployResponse.id); } return deployResponse; } catch (error) { spinner.fail('Deployment failed'); throw error; } } private async readProjectConfig(): Promise<any> { const configFiles = [ 'aerocorp.json', 'aerocorp.yml', 'aerocorp.yaml', 'package.json', 'docker-compose.yml', 'Dockerfile' ]; let config: any = {}; for (const file of configFiles) { if (existsSync(file)) { try { if (file.endsWith('.json')) { const content = readFileSync(file, 'utf8'); const parsed = JSON.parse(content); if (file === 'package.json') { config = { name: parsed.name, version: parsed.version, description: parsed.description, scripts: parsed.scripts, dependencies: parsed.dependencies, ...config }; } else { config = { ...config, ...parsed }; } } } catch (error) { console.warn(chalk.yellow(`⚠ Could not parse ${file}: ${error.message}`)); } } } return config; } private buildDeploymentConfig(options: any, projectConfig: any): any { const environment = options.prod ? 'production' : options.staging ? 'staging' : options.preview ? 'preview' : 'development'; return { name: options.name || projectConfig.name || 'aerocorp-app', environment, force: options.force || false, buildEnv: this.parseEnvString(options.buildEnv), runtimeEnv: this.parseEnvString(options.env), regions: options.region ? options.region.split(',') : ['default'], replicas: parseInt(options.scale) || 1, projectConfig, source: { type: 'git', repository: process.cwd(), branch: 'main' } }; } private parseEnvString(envString?: string): Record<string, string> { if (!envString) return {}; const env: Record<string, string> = {}; const pairs = envString.split(','); for (const pair of pairs) { const [key, ...valueParts] = pair.split('='); if (key && valueParts.length > 0) { env[key.trim()] = valueParts.join('=').trim(); } } return env; } private async createOrUpdateApplication(coolifyUrl: string, headers: any, config: any): Promise<any> { try { // First, try to get existing applications const appsResponse = await axios.get(`${coolifyUrl}/api/v1/applications`, { headers }); const existingApp = appsResponse.data.find((app: any) => app.name === config.name); if (existingApp) { // Update existing application const updateResponse = await axios.put( `${coolifyUrl}/api/v1/applications/${existingApp.id}`, { name: config.name, description: config.projectConfig.description, environment: config.environment, build_env: config.buildEnv, runtime_env: config.runtimeEnv }, { headers } ); return updateResponse; } else { // Create new application const createResponse = await axios.post( `${coolifyUrl}/api/v1/applications`, { name: config.name, description: config.projectConfig.description || 'AeroCorp Application', environment: config.environment, source: config.source, build_env: config.buildEnv, runtime_env: config.runtimeEnv, replicas: config.replicas }, { headers } ); return createResponse; } } catch (error) { if (error.response?.status === 404) { // Fallback: create application in default project const projectsResponse = await axios.get(`${coolifyUrl}/api/v1/projects`, { headers }); const defaultProject = projectsResponse.data[0]; if (!defaultProject) { throw new Error('No projects found. Please create a project in Coolify first.'); } const createResponse = await axios.post( `${coolifyUrl}/api/v1/projects/${defaultProject.id}/applications`, { name: config.name, description: config.projectConfig.description || 'AeroCorp Application', environment: config.environment, source: config.source, build_env: config.buildEnv, runtime_env: config.runtimeEnv, replicas: config.replicas }, { headers } ); return createResponse; } throw error; } } private async triggerDeployment(coolifyUrl: string, headers: any, application: any, config: any): Promise<any> { const deployResponse = await axios.post( `${coolifyUrl}/api/v1/applications/${application.id}/deploy`, { force: config.force, environment: config.environment }, { headers } ); return { id: deployResponse.data.id || application.id, url: deployResponse.data.url || application.url, status: deployResponse.data.status || 'pending', application: application }; } private async monitorDeployment(coolifyUrl: string, headers: any, deploymentId: string): Promise<void> { console.log(chalk.blue('\n📊 Monitoring deployment...')); const spinner = ora('Deployment in progress...').start(); let attempts = 0; const maxAttempts = 60; // 5 minutes with 5-second intervals while (attempts < maxAttempts) { try { const statusResponse = await axios.get( `${coolifyUrl}/api/v1/deployments/${deploymentId}`, { headers } ); const status = statusResponse.data.status; switch (status) { case 'success': case 'completed': spinner.succeed('Deployment completed successfully'); console.log(chalk.green('🎉 Your application is now live!')); return; case 'failed': case 'error': spinner.fail('Deployment failed'); console.log(chalk.red('❌ Deployment failed. Check logs for details.')); return; case 'building': spinner.text = 'Building application...'; break; case 'deploying': spinner.text = 'Deploying application...'; break; default: spinner.text = `Status: ${status}`; } await new Promise(resolve => setTimeout(resolve, 5000)); attempts++; } catch (error) { console.warn(chalk.yellow(`⚠ Could not fetch deployment status: ${error.message}`)); break; } } spinner.warn('Deployment monitoring timed out'); console.log(chalk.yellow('⏰ Deployment is still in progress. Check Coolify dashboard for updates.')); } }