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
JavaScript
"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