UNPKG

forge-deploy-cli

Version:

Professional CLI for local deployments with automatic subdomain routing, SSL certificates, and infrastructure management

1,105 lines (1,083 loc) 53.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LocalDeploymentManager = void 0; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const child_process_1 = require("child_process"); const chalk_1 = __importDefault(require("chalk")); const system_1 = require("../utils/system"); const firewall_1 = require("../utils/firewall"); const types_1 = require("../types"); const os_1 = __importDefault(require("os")); class LocalDeploymentManager { /** * Deploy a project locally */ static async deployLocally(deploymentData) { try { console.log(chalk_1.default.cyan('Setting up local deployment...')); // Find available port const port = await this.findAvailablePort(); console.log(chalk_1.default.gray(`Assigned port: ${port}`)); // Create deployment record const localIP = (0, system_1.getSystemIP)(); const storageLimit = 15 * 1024 * 1024 * 1024; // 15GB in bytes const deployment = { id: deploymentData.id, projectName: deploymentData.projectName, subdomain: deploymentData.subdomain, framework: deploymentData.framework, projectPath: deploymentData.projectPath, port, status: 'stopped', url: `http://${deploymentData.subdomain}.${this.BASE_DOMAIN}`, storageLimit }; // Start the application await this.startApplication(deployment, deploymentData.buildOutputDir); // Setup initial nginx configuration (HTTP-only) await this.setupNginxConfig(deployment, false); // Setup SSL certificate if public IP is available let sslConfigured = false; if (deploymentData.publicIP) { try { sslConfigured = await this.setupSSLForDeployment(deploymentData.id, deploymentData.subdomain, deploymentData.publicIP); if (sslConfigured) { deployment.url = `https://${deploymentData.subdomain}.${this.BASE_DOMAIN}`; // Update nginx configuration to enable SSL await this.setupNginxConfig(deployment, true); } } catch (sslError) { console.log(chalk_1.default.yellow(`SSL setup failed: ${sslError}`)); sslConfigured = false; } } // Save deployment record await this.saveDeployment(deployment); console.log(chalk_1.default.green('Local deployment configured successfully!')); console.log(chalk_1.default.blue('Access Information:')); console.log(` ${chalk_1.default.cyan('Local URL:')} http://localhost:${port}`); console.log(` ${chalk_1.default.cyan('Network URL:')} http://${localIP}:${port}`); console.log(` ${chalk_1.default.cyan('Public URL:')} ${deployment.url}`); if (deploymentData.publicIP) { console.log(); if (sslConfigured && deployment.url.startsWith('https://')) { console.log(chalk_1.default.green('SSL Certificate: Configured')); } else { console.log(chalk_1.default.yellow('SSL Certificate: Not configured (using HTTP)')); console.log(chalk_1.default.gray(' To enable SSL:')); console.log(chalk_1.default.gray(' 1. Configure firewall (see instructions above)')); console.log(chalk_1.default.gray(' 2. Run: forge infra --ssl')); console.log(chalk_1.default.gray(' 3. Redeploy: forge deploy <repo-url>')); } console.log(); console.log(chalk_1.default.blue('Security Notes:')); console.log(chalk_1.default.gray(` • Firewall: Open ports ${port}, 80, and 443`)); console.log(chalk_1.default.gray(` • DNS Management: Handled automatically via API`)); console.log(chalk_1.default.gray(` • SSL Certificates: Managed by Let's Encrypt`)); } else { console.log(); console.log(chalk_1.default.yellow('For Public Access:')); console.log(chalk_1.default.gray(` • Open port ${port} on your firewall`)); console.log(chalk_1.default.gray(` • Domain routing is handled automatically via API`)); } return deployment; } catch (error) { throw new Error(`Local deployment failed: ${error}`); } } /** * Start an application using PM2 for better process management */ static async startApplication(deployment, buildOutputDir) { const { framework, projectPath, port, id } = deployment; // Ensure PM2 is installed await this.ensurePM2Installed(); let startScript; let cwd = projectPath; let interpreter = 'node'; switch (framework) { case types_1.Framework.NEXTJS: startScript = 'npm start'; break; case types_1.Framework.REACT: case types_1.Framework.VUE: case types_1.Framework.ANGULAR: // Serve static build output if (buildOutputDir) { cwd = path_1.default.join(projectPath, buildOutputDir); startScript = 'npx serve -s . -p ' + port; } else { startScript = 'npm start'; } break; case types_1.Framework.VITE: if (buildOutputDir) { cwd = path_1.default.join(projectPath, buildOutputDir); startScript = 'npx serve -s . -p ' + port; } else { const distPath = path_1.default.join(projectPath, 'dist'); if (await this.hasFile(projectPath, 'dist')) { cwd = distPath; startScript = 'npx serve -s . -p ' + port; } else { startScript = 'npm run preview || npm run dev -- --port ' + port + ' --host 0.0.0.0'; } } break; case types_1.Framework.EXPRESS: case types_1.Framework.FASTIFY: case types_1.Framework.NEST: startScript = 'npm start'; break; case types_1.Framework.DJANGO: // Django development server startScript = 'python manage.py runserver 0.0.0.0:' + port; interpreter = 'python'; break; case types_1.Framework.FLASK: // Flask development server startScript = 'python app.py'; interpreter = 'python'; // Set Flask environment variables break; case types_1.Framework.FASTAPI: // FastAPI with uvicorn startScript = 'uvicorn main:app --host 0.0.0.0 --port ' + port; interpreter = 'none'; // uvicorn is the interpreter break; case types_1.Framework.STATIC: // Serve static files startScript = 'npx serve -s . -p ' + port; break; case types_1.Framework.NUXT: startScript = 'npm run start'; break; default: // Try to detect based on files in project if (await this.hasFile(projectPath, 'manage.py')) { // Django project startScript = 'python manage.py runserver 0.0.0.0:' + port; interpreter = 'python'; } else if (await this.hasFile(projectPath, 'app.py')) { // Flask project startScript = 'python app.py'; interpreter = 'python'; } else if (await this.hasFile(projectPath, 'main.py')) { // FastAPI or generic Python startScript = 'uvicorn main:app --host 0.0.0.0 --port ' + port; interpreter = 'none'; } else if (await this.hasFile(projectPath, 'vite.config.js') || await this.hasFile(projectPath, 'vite.config.ts')) { // Vite project detected const distPath = path_1.default.join(projectPath, 'dist'); if (await this.hasFile(projectPath, 'dist')) { cwd = distPath; startScript = 'npx serve -s . -p ' + port; } else { startScript = 'npm run preview || npm run dev -- --port ' + port + ' --host 0.0.0.0'; } } else { // Generic static file serving startScript = 'npx serve -s . -p ' + port; } break; } try { console.log(chalk_1.default.gray(`Starting application with PM2: ${startScript}`)); // Create PM2 ecosystem file for this deployment const ecosystemConfig = { apps: [{ name: `forge-${id}`, script: startScript.includes('npx') ? 'npx' : startScript.split(' ')[0], args: startScript.includes('npx') ? startScript.split(' ').slice(1).join(' ') : startScript.split(' ').slice(1).join(' '), cwd: cwd, interpreter: this.getInterpreter(startScript, interpreter), env: this.getEnvironmentVariables(framework, port), instances: 1, autorestart: true, watch: false, max_memory_restart: '1G', min_uptime: '10s', max_restarts: 5, restart_delay: 1000, error_file: path_1.default.join(process.cwd(), `logs/${id}-error.log`), out_file: path_1.default.join(process.cwd(), `logs/${id}-out.log`), log_file: path_1.default.join(process.cwd(), `logs/${id}-combined.log`), time: true, merge_logs: true, kill_timeout: 5000 }] }; // Ensure logs directory exists await fs_extra_1.default.ensureDir(path_1.default.join(process.cwd(), 'logs')); // Write ecosystem file const ecosystemPath = path_1.default.join(process.cwd(), `forge-${id}.config.js`); await fs_extra_1.default.writeFile(ecosystemPath, `module.exports = ${JSON.stringify(ecosystemConfig, null, 2)}`); // Start with PM2 (0, child_process_1.execSync)(`pm2 start ${ecosystemPath}`, { stdio: 'inherit' }); // Save PM2 configuration (0, child_process_1.execSync)('pm2 save', { stdio: 'pipe' }); deployment.status = 'running'; deployment.startedAt = new Date(); // Wait a moment for PM2 to start monitoring, then calculate initial resources setTimeout(async () => { await this.updateDeploymentResources(deployment.id); }, 2000); console.log(chalk_1.default.green(`Application started with PM2 on port ${port}`)); } catch (error) { deployment.status = 'failed'; throw new Error(`Failed to start application with PM2: ${error}`); } } static async stopDeployment(deploymentId) { const deployment = await this.getDeployment(deploymentId); if (!deployment) { throw new Error('Deployment not found'); } try { // Stop PM2 process - try multiple approaches const appName = `forge-${deploymentId}`; try { // First try to stop by name (0, child_process_1.execSync)(`pm2 stop ${appName}`, { stdio: 'pipe' }); (0, child_process_1.execSync)(`pm2 delete ${appName}`, { stdio: 'pipe' }); console.log(chalk_1.default.gray(`Stopped PM2 process: ${appName}`)); } catch (pm2Error) { // If that fails, try to find and stop by ID or other name variations try { const pm2List = (0, child_process_1.execSync)('pm2 jlist', { encoding: 'utf8' }); const processes = JSON.parse(pm2List); for (const proc of processes) { if (proc.name.includes(deploymentId) || proc.name === appName) { (0, child_process_1.execSync)(`pm2 stop ${proc.pm_id}`, { stdio: 'pipe' }); (0, child_process_1.execSync)(`pm2 delete ${proc.pm_id}`, { stdio: 'pipe' }); console.log(chalk_1.default.gray(`Stopped PM2 process: ${proc.name} (ID: ${proc.pm_id})`)); break; } } } catch (listError) { console.log(chalk_1.default.yellow(`Warning: Could not stop PM2 process cleanly: ${pm2Error}`)); } } // Remove ecosystem file const ecosystemPath = path_1.default.join(process.cwd(), `forge-${deploymentId}.config.js`); if (await fs_extra_1.default.pathExists(ecosystemPath)) { await fs_extra_1.default.remove(ecosystemPath); console.log(chalk_1.default.gray(`Removed ecosystem file: ${ecosystemPath}`)); } // Remove nginx config const configPath = path_1.default.join(this.NGINX_CONFIG_DIR, `${deployment.subdomain}.conf`); if (await fs_extra_1.default.pathExists(configPath)) { await fs_extra_1.default.remove(configPath); console.log(chalk_1.default.gray(`Removed nginx config: ${configPath}`)); // Reload nginx try { if (os_1.default.platform() === 'win32') { (0, child_process_1.execSync)('nginx -s reload', { stdio: 'pipe' }); } else { (0, child_process_1.execSync)('sudo nginx -s reload', { stdio: 'pipe' }); } console.log(chalk_1.default.gray('Nginx configuration reloaded')); } catch { console.log(chalk_1.default.yellow('Warning: Could not reload nginx automatically')); } } deployment.status = 'stopped'; deployment.pid = undefined; await this.saveDeployment(deployment); console.log(chalk_1.default.green(`Deployment ${deploymentId} stopped successfully`)); } catch (error) { throw new Error(`Failed to stop deployment: ${error}`); } } static async listDeployments() { try { if (!await fs_extra_1.default.pathExists(this.DEPLOYMENTS_FILE)) { return []; } const data = await fs_extra_1.default.readJSON(this.DEPLOYMENTS_FILE); return Array.isArray(data) ? data : []; } catch { return []; } } static async getDeployment(deploymentId) { const deployments = await this.listDeployments(); return deployments.find(d => d.id === deploymentId) || null; } static async saveDeployment(deployment) { const deployments = await this.listDeployments(); const index = deployments.findIndex(d => d.id === deployment.id); if (index >= 0) { deployments[index] = deployment; } else { deployments.push(deployment); } await fs_extra_1.default.writeJSON(this.DEPLOYMENTS_FILE, deployments, { spaces: 2 }); } static async findAvailablePort() { const net = await Promise.resolve().then(() => __importStar(require('net'))); for (let port = this.MIN_PORT; port <= this.MAX_PORT; port++) { if (await this.isPortAvailable(port)) { return port; } } throw new Error('No available ports found'); } static async isPortAvailable(port) { return new Promise((resolve) => { const net = require('net'); const server = net.createServer(); server.listen(port, () => { server.once('close', () => resolve(true)); server.close(); }); server.on('error', () => resolve(false)); }); } static async ensureServeInstalled() { try { (0, child_process_1.execSync)('npx serve --version', { stdio: 'pipe' }); } catch { console.log(chalk_1.default.cyan('Installing serve package...')); (0, child_process_1.execSync)('npm install -g serve', { stdio: 'inherit' }); } } static async cleanup() { const deployments = await this.listDeployments(); const activeDeployments = deployments.filter(deployment => { if (deployment.pid) { try { // Check if process is still running process.kill(deployment.pid, 0); return true; } catch { // Process not running deployment.status = 'stopped'; deployment.pid = undefined; return true; } } return true; }); await fs_extra_1.default.writeJSON(this.DEPLOYMENTS_FILE, activeDeployments, { spaces: 2 }); } static async getDeploymentStatus(deploymentId) { const deployment = await this.getDeployment(deploymentId); if (!deployment) return null; // Health check if (deployment.pid && deployment.status === 'running') { try { const response = await fetch(`http://localhost:${deployment.port}`, { signal: AbortSignal.timeout(5000) }); deployment.lastAccessed = new Date(); if (response.ok) { deployment.status = 'running'; } else { deployment.status = 'failed'; } } catch { deployment.status = 'failed'; } await this.saveDeployment(deployment); } return deployment; } static async ensurePM2Installed() { try { (0, child_process_1.execSync)('pm2 --version', { stdio: 'pipe' }); } catch { console.log(chalk_1.default.cyan('Installing PM2 for process management...')); (0, child_process_1.execSync)('npm install -g pm2', { stdio: 'inherit' }); // Setup PM2 startup on Windows/Linux try { if (os_1.default.platform() === 'win32') { (0, child_process_1.execSync)('pm2-windows-service install', { stdio: 'inherit' }); } else { (0, child_process_1.execSync)('pm2 startup', { stdio: 'inherit' }); } } catch (error) { console.log(chalk_1.default.yellow('Warning: Could not setup PM2 auto-startup')); } } } static async setupNginxConfig(deployment, sslConfigured = false) { const { subdomain, port } = deployment; try { await fs_extra_1.default.ensureDir(this.NGINX_CONFIG_DIR); await this.ensureMainNginxConfig(); const nginxConfig = this.generateNginxConfig(subdomain, port, sslConfigured); const configPath = path_1.default.join(this.NGINX_CONFIG_DIR, `${subdomain}.conf`); await fs_extra_1.default.writeFile(configPath, nginxConfig); console.log(chalk_1.default.gray(`Nginx config created: ${configPath}`)); if (await this.testNginxConfig()) { await this.reloadNginx(); console.log(chalk_1.default.green('Nginx configuration applied successfully')); } else { throw new Error('Nginx configuration test failed'); } } catch (error) { console.log(chalk_1.default.yellow(`Warning: Could not setup nginx config: ${error}`)); throw error; } } static generateNginxConfig(subdomain, port, sslConfigured = false) { const domain = `${subdomain}.forgecli.tech`; if (!sslConfigured) { return `# Forge deployment configuration for ${subdomain} # Generated on ${new Date().toISOString()} # HTTP-only configuration # HTTP server server { listen 80; listen [::]:80; server_name ${domain}; # Basic security headers add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; # Health check endpoint location /health { access_log off; return 200 "healthy\\n"; add_header Content-Type text/plain; } # Main application proxy location / { proxy_pass http://127.0.0.1:${port}; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_cache_bypass $http_upgrade; # Timeouts proxy_connect_timeout 30s; proxy_send_timeout 60s; proxy_read_timeout 60s; # Buffer settings proxy_buffering on; proxy_buffer_size 8k; proxy_buffers 16 8k; proxy_busy_buffers_size 16k; } # Static file caching location ~* \\.(jpg|jpeg|png|gif|ico|svg|webp|avif|css|js|woff|woff2|ttf|eot)$ { proxy_pass http://127.0.0.1:${port}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; expires 1y; add_header Cache-Control "public, immutable"; } } # Logging access_log /var/log/nginx/${subdomain}_access.log combined; error_log /var/log/nginx/${subdomain}_error.log warn; `; } // HTTPS configuration with SSL certificate for this specific subdomain return `# Forge deployment configuration for ${subdomain} # Generated on ${new Date().toISOString()} # HTTPS configuration with per-subdomain SSL certificate # HTTP server - redirects to HTTPS server { listen 80; listen [::]:80; server_name ${domain}; # Let's Encrypt ACME challenge location location /.well-known/acme-challenge/ { root /var/www/html; try_files $uri =404; } # Health check endpoint (accessible via HTTP) location /health { access_log off; return 200 "healthy\\n"; add_header Content-Type text/plain; } # Redirect all other HTTP traffic to HTTPS location / { return 301 https://$server_name$request_uri; } } # HTTPS server with SSL certificate for this subdomain server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name ${domain}; # SSL certificate configuration for this specific subdomain ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem; # Modern SSL settings ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # Security headers for HTTPS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Health check endpoint location /health { access_log off; return 200 "healthy\\n"; add_header Content-Type text/plain; } # Main application proxy location / { proxy_pass http://127.0.0.1:${port}; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_cache_bypass $http_upgrade; # Timeouts proxy_connect_timeout 30s; proxy_send_timeout 60s; proxy_read_timeout 60s; # Buffer settings proxy_buffering on; proxy_buffer_size 8k; proxy_buffers 16 8k; proxy_busy_buffers_size 16k; } # Static file caching location ~* \\.(jpg|jpeg|png|gif|ico|svg|webp|avif|css|js|woff|woff2|ttf|eot)$ { proxy_pass http://127.0.0.1:${port}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; expires 1y; add_header Cache-Control "public, immutable"; } } # Logging access_log /var/log/nginx/${subdomain}_access.log combined; error_log /var/log/nginx/${subdomain}_error.log warn; `; } /** * Test nginx configuration */ static async testNginxConfig() { const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process'))); try { execSync('nginx -t', { stdio: 'pipe' }); return true; } catch (error) { console.log(chalk_1.default.red('Nginx configuration test failed:')); try { execSync('nginx -t', { stdio: 'inherit' }); } catch { // Error already shown above } return false; } } /** * Reload nginx configuration */ static async reloadNginx() { const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process'))); try { if (os_1.default.platform() === 'win32') { execSync('nginx -s reload', { stdio: 'pipe' }); } else { // Try systemctl first, then fallback to nginx -s reload try { execSync('systemctl reload nginx', { stdio: 'pipe' }); } catch { execSync('nginx -s reload', { stdio: 'pipe' }); } } } catch (error) { throw new Error(`Failed to reload nginx: ${error}`); } } /** * Install and configure nginx */ static async setupNginx() { console.log(chalk_1.default.cyan('Setting up nginx...')); try { if (os_1.default.platform() === 'win32') { // Windows nginx setup console.log(chalk_1.default.gray('For Windows, please manually install nginx:')); console.log(chalk_1.default.gray('1. Download nginx from http://nginx.org/en/download.html')); console.log(chalk_1.default.gray('2. Extract to C:\\nginx')); console.log(chalk_1.default.gray('3. Run "C:\\nginx\\nginx.exe" to start')); } else { // Linux nginx setup try { (0, child_process_1.execSync)('which nginx', { stdio: 'pipe' }); console.log(chalk_1.default.green('Nginx is already installed')); } catch { console.log(chalk_1.default.cyan('Installing nginx...')); // Detect package manager and install try { (0, child_process_1.execSync)('apt-get update && apt-get install -y nginx', { stdio: 'inherit' }); } catch { try { (0, child_process_1.execSync)('yum install -y nginx', { stdio: 'inherit' }); } catch { try { (0, child_process_1.execSync)('dnf install -y nginx', { stdio: 'inherit' }); } catch { throw new Error('Could not install nginx automatically'); } } } } // Enable and start nginx try { (0, child_process_1.execSync)('systemctl enable nginx', { stdio: 'pipe' }); (0, child_process_1.execSync)('systemctl start nginx', { stdio: 'pipe' }); console.log(chalk_1.default.green('Nginx enabled and started')); } catch { console.log(chalk_1.default.yellow('Warning: Could not enable/start nginx automatically')); } } // Create nginx configuration directory await fs_extra_1.default.ensureDir(this.NGINX_CONFIG_DIR); // Create main nginx config if needed await this.ensureMainNginxConfig(); } catch (error) { console.log(chalk_1.default.red(`Failed to setup nginx: ${error}`)); throw error; } } /** * Ensure main nginx configuration includes forge sites */ static async ensureMainNginxConfig() { const mainConfigPath = os_1.default.platform() === 'win32' ? 'C:\\nginx\\conf\\nginx.conf' : '/etc/nginx/nginx.conf'; try { if (await fs_extra_1.default.pathExists(mainConfigPath)) { const config = await fs_extra_1.default.readFile(mainConfigPath, 'utf8'); const includePattern = os_1.default.platform() === 'win32' ? 'include forge-sites/*.conf;' : 'include /etc/nginx/forge-sites/*.conf;'; if (!config.includes(includePattern)) { console.log(chalk_1.default.gray('Adding forge sites include to nginx.conf')); // Add include directive right after the http { line with proper formatting const updatedConfig = config.replace(/http\s*{/, `http {\n # Include forge sites configuration\n ${includePattern}`); await fs_extra_1.default.writeFile(mainConfigPath, updatedConfig); // Test nginx configuration const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process'))); try { execSync('nginx -t', { stdio: 'pipe' }); console.log(chalk_1.default.green('Nginx configuration updated successfully')); } catch (testError) { console.log(chalk_1.default.red('Nginx configuration test failed after update')); // Restore original configuration await fs_extra_1.default.writeFile(mainConfigPath, config); throw new Error('Failed to update nginx configuration - restored original'); } } else { console.log(chalk_1.default.gray('Forge sites include already present in nginx.conf')); } } } catch (error) { console.log(chalk_1.default.yellow(`Warning: Could not update main nginx config: ${error}`)); } } /** * Setup SSL certificate for a deployment using per-subdomain certificate * Returns true if SSL was successfully configured, false if skipped */ static async setupSSLForDeployment(deploymentId, subdomain, publicIP) { if (os_1.default.platform() === 'win32') { console.log(chalk_1.default.yellow('SSL certificate setup skipped on Windows')); console.log(chalk_1.default.gray('Consider using Cloudflare or another reverse proxy for SSL')); return false; } const domain = `${subdomain}.forgecli.tech`; try { console.log(chalk_1.default.cyan(`Setting up SSL certificate for ${domain}...`)); // Quick firewall check before attempting SSL setup console.log(chalk_1.default.gray('Performing quick SSL readiness check...')); const firewallOk = await (0, firewall_1.performFirewallPreflightCheck)(); if (!firewallOk) { console.log(chalk_1.default.yellow('SSL setup skipped due to firewall issues.')); console.log(chalk_1.default.gray('Your site will work over HTTP, but SSL certificates cannot be issued.')); console.log(chalk_1.default.gray('Configure firewall as shown above, then redeploy for SSL.')); return false; } // Check if certificate for this specific subdomain exists const certPath = `/etc/letsencrypt/live/${domain}`; const fs = await Promise.resolve().then(() => __importStar(require('fs-extra'))); if (!await fs.pathExists(certPath)) { console.log(chalk_1.default.gray(`Certificate not found for ${domain}. Requesting new certificate...`)); // Update Cloudflare DNS record for the subdomain first await this.updateCloudflareRecord(deploymentId, publicIP); // Wait a moment for DNS propagation console.log(chalk_1.default.gray('Waiting for DNS propagation...')); await new Promise(resolve => setTimeout(resolve, 5000)); // Request certificate for this specific subdomain await this.requestSubdomainCertificate(domain); // Check again if (!await fs.pathExists(certPath)) { throw new Error(`Certificate request failed for ${domain}`); } } else { console.log(chalk_1.default.green(`Existing SSL certificate found for ${domain}`)); // Still update DNS record to ensure it points to correct IP await this.updateCloudflareRecord(deploymentId, publicIP); } // Test nginx configuration if (!await this.testNginxConfig()) { throw new Error('Nginx configuration test failed'); } // Reload nginx to apply SSL configuration await this.reloadNginx(); console.log(chalk_1.default.green(`SSL setup completed for ${domain}`)); return true; } catch (error) { console.log(chalk_1.default.yellow(`Warning: SSL setup failed: ${error}`)); console.log(chalk_1.default.gray('Your site will still work over HTTP')); // Provide helpful guidance based on error type const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('Timeout during connect') || errorMessage.includes('firewall')) { console.log(chalk_1.default.blue('💡 This looks like a firewall issue:')); console.log(chalk_1.default.gray(' • Make sure ports 80 and 443 are open')); console.log(chalk_1.default.gray(' • Check cloud provider firewall settings')); console.log(chalk_1.default.gray(' • Run: forge infra --ssl (to recheck)')); } else if (errorMessage.includes('DNS') || errorMessage.includes('domain')) { console.log(chalk_1.default.blue('💡 This looks like a DNS issue:')); console.log(chalk_1.default.gray(' • DNS may not have propagated yet')); console.log(chalk_1.default.gray(' • Check if the subdomain resolves correctly')); console.log(chalk_1.default.gray(' • Try again in a few minutes')); } return false; } } /** * Request SSL certificate for a specific subdomain using Let's Encrypt */ static async requestSubdomainCertificate(domain) { console.log(chalk_1.default.gray(`Requesting SSL certificate for ${domain}...`)); try { const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process'))); // Use nginx plugin since nginx is already running const certbotCommand = [ 'certbot', '--nginx', '-d', domain, '--non-interactive', '--agree-tos', '--email', 'admin@forgecli.tech', '--cert-name', domain, '--redirect' ].join(' '); console.log(chalk_1.default.gray('Running certbot with nginx plugin...')); execSync(certbotCommand, { stdio: 'pipe' }); console.log(chalk_1.default.green(`SSL certificate successfully obtained for ${domain}`)); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.log(chalk_1.default.red(`Certificate request failed: ${errorMessage}`)); // Try with webroot method as fallback if nginx plugin fails console.log(chalk_1.default.gray('Trying webroot method as fallback...')); try { const fs = await Promise.resolve().then(() => __importStar(require('fs-extra'))); const webrootPath = '/var/www/html'; await fs.ensureDir(webrootPath); const webrootCommand = [ 'certbot', 'certonly', '--webroot', '-w', webrootPath, '--non-interactive', '--agree-tos', '--email', 'admin@forgecli.tech', '-d', domain, '--cert-name', domain ].join(' '); (0, child_process_1.execSync)(webrootCommand, { stdio: 'pipe' }); console.log(chalk_1.default.green(`SSL certificate successfully obtained for ${domain} (webroot method)`)); } catch (fallbackError) { // Try one more fallback with manual challenge if both fail console.log(chalk_1.default.gray('Trying manual HTTP challenge...')); try { // Stop nginx temporarily for standalone mode (0, child_process_1.execSync)('systemctl stop nginx', { stdio: 'pipe' }); const standaloneCommand = [ 'certbot', 'certonly', '--standalone', '--non-interactive', '--agree-tos', '--email', 'admin@forgecli.tech', '-d', domain, '--cert-name', domain ].join(' '); (0, child_process_1.execSync)(standaloneCommand, { stdio: 'pipe' }); // Restart nginx (0, child_process_1.execSync)('systemctl start nginx', { stdio: 'pipe' }); console.log(chalk_1.default.green(`SSL certificate successfully obtained for ${domain} (standalone method)`)); } catch (standaloneError) { try { (0, child_process_1.execSync)('systemctl start nginx', { stdio: 'pipe' }); } catch { } throw new Error(`All certificate methods failed. Latest error: ${standaloneError}`); } } } } static async updateCloudflareRecord(deploymentId, publicIP) { try { console.log(chalk_1.default.gray(`Updating DNS record for deployment ${deploymentId} -> ${publicIP} via API...`)); // Get API service const { ConfigService } = await Promise.resolve().then(() => __importStar(require('./config'))); const { ForgeApiService } = await Promise.resolve().then(() => __importStar(require('./api'))); const configService = new ConfigService(); const globalConfig = await configService.loadGlobalConfig(); if (!globalConfig?.apiKey) { throw new Error('API key not found. Run "forge login" first.'); } const apiService = new ForgeApiService(); apiService.setApiKey(globalConfig.apiKey); // Create or update subdomain via API const response = await apiService.updateSubdomain(deploymentId, publicIP); if (!response.success) { throw new Error(response.error?.message || 'Failed to update DNS record'); } console.log(chalk_1.default.green(`DNS record updated for deployment ${deploymentId}`)); } catch (error) { console.log(chalk_1.default.yellow(`Warning: Could not update DNS record: ${error}`)); console.log(chalk_1.default.gray('DNS updates are handled via the Forge API for security')); } } static async calculateResourceUsage(deployment) { let resources = { cpu: 0, memory: 0, diskUsed: 0, diskUsagePercent: 0 }; try { // Calculate disk usage for the project directory const diskUsed = await this.getDirectorySize(deployment.projectPath); const diskUsagePercent = (diskUsed / deployment.storageLimit) * 100; resources.diskUsed = diskUsed; resources.diskUsagePercent = Math.min(diskUsagePercent, 100); // Get PM2 process information for better accuracy const pm2Data = await this.getPM2ProcessData(deployment.id); if (pm2Data) { resources.cpu = pm2Data.cpu; resources.memory = pm2Data.memory; // Update deployment PID and status based on PM2 data deployment.pid = pm2Data.pid; deployment.status = pm2Data.status === 'online' ? 'running' : pm2Data.status === 'stopped' ? 'stopped' : 'failed'; } else if (deployment.status === 'running') { // Fallback to process monitoring if PM2 data unavailable await this.getFallbackProcessData(deployment, resources); } } catch (error) { console.log(chalk_1.default.yellow(`Warning: Could not calculate resource usage: ${error}`)); } return resources; } /** * Get directory size in bytes */ static async getDirectorySize(dirPath) { let totalSize = 0; try { const stat = await fs_extra_1.default.stat(dirPath); if (stat.isFile()) { return stat.size; } if (stat.isDirectory()) { const items = await fs_extra_1.default.readdir(dirPath); for (const item of items) { if (item === 'node_modules' || item === '.git' || item === 'dist' || item === 'build' || item === '__pycache__') { continue; } const itemPath = path_1.default.join(dirPath, item); try { totalSize += await this.getDirectorySize(itemPath); } catch (error) { continue; } } } } catch (error) { return 0; } return totalSize; } /** * Update deployment with real-time resource usage */ static async updateDeploymentResources(deploymentId) { const deployment = await this.getDeployment(deploymentId); if (!deployment) return; const resources = await this.calculateResourceUsage(deployment); deployment.resources = resources; await this.saveDeployment(deployment); } /** * Get process data from PM2 for accurate resource monitoring */ static async getPM2ProcessData(deploymentId) { try { const appName = `forge-${deploymentId}`; const pm2List = (0, child_process_1.execSync)('pm2 jlist', { encoding: 'utf8', stdio: 'pipe' }); const processes = JSON.parse(pm2List); const process = processes.find((proc) => proc.name === appName); if (process) { // Get system memory for percentage calculation const totalMemoryGB = os_1.default.totalmem() / (1024 * 1024 * 1024); const usedMemoryMB = parseFloat(process.monit?.memory?.toString() || '0') / (1024 * 1024); const memoryPercent = (usedMemoryMB / (totalMemoryGB * 1024)) * 100; return { cpu: parseFloat(process.monit?.cpu?.toString() || '0'), memory: Math.min(memoryPercent, 100), pid: process.pid || 0, status: process.pm2_env?.status || 'unknown' }; } } catch (error) { // PM2 might not be available or process doesn't exist } return null; } /** * Fallback process monitoring for non-PM2 processes */ static async getFallbackProcessData(deployment, resources) { if (!deployment.pid) return; try { if (os_1.default.platform() === 'win32') { // Windows process monitoring const output = (0, child_process_1.execSync)(`wmic process where processid=${deployment.pid} get PageFileUsage,WorkingSetSize /format:csv`, { encoding: 'utf8', stdio: 'pipe' }); const lines = output.split('\n').filter(line => line.trim() && !line.startsWith('Node')); if (lines.length > 0) { const parts = lines[0].split(','); if (parts.length >= 3) { // Memory usage in KB, convert to percentage based on total system memory const memoryKB = parseInt(parts[1]) || 0; const totalMemoryKB = os_1.default.totalmem() / 1024; resources.memory = Math.min((memoryKB / totalMemoryKB) * 100, 100); } } } else { // Linux/macOS process monitoring using ps const o