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