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