UNPKG

forge-deploy-cli

Version:

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

581 lines 28.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.deployCommand = void 0; const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); const child_process_1 = require("child_process"); const commander_1 = require("commander"); const config_1 = require("../services/config"); const api_1 = require("../services/api"); const git_1 = require("../services/git"); const localDeployment_1 = require("../services/localDeployment"); const workspaceManager_1 = require("../services/workspaceManager"); const apiServer_1 = require("../services/apiServer"); const system_1 = require("../utils/system"); const types_1 = require("../types"); exports.deployCommand = new commander_1.Command('deploy') .description('Deploy your application to Forge') .argument('[source]', 'GitHub repository URL or local project directory') .option('-b, --branch <branch>', 'Git branch to deploy', 'main') .option('-e, --environment <env>', 'Target environment', 'production') .option('--skip-build', 'Skip the build step') .option('--skip-workspace-setup', 'Skip interactive workspace configuration') .option('--use-workspace-config', 'Use existing workspace configuration from forge.config.json') .option('-f, --force', 'Force deployment even if checks fail') .option('--subdomain <subdomain>', 'Custom subdomain (for web projects)') .action(async (source, options) => { try { console.log(chalk_1.default.blue('Forge Deployment')); console.log(chalk_1.default.gray('Preparing your application for deployment...')); console.log(); // Check system privileges for infrastructure setup (0, system_1.checkSystemPrivileges)(); const configService = new config_1.ConfigService(); let projectPath = process.cwd(); let isGitRepo = false; let gitRepository; let projectName; let framework; // Determine source type and project details if (source) { if (isGitHubUrl(source)) { console.log(chalk_1.default.cyan('Detected GitHub repository URL')); gitRepository = source; isGitRepo = true; projectName = extractRepoName(source); // Clone the repository for local deployment console.log(chalk_1.default.cyan('Cloning repository for deployment...')); const cloneResult = await git_1.GitService.cloneRepository(source, { branch: options.branch, depth: 1 }); if (!cloneResult.success) { console.log(chalk_1.default.red(`Failed to clone repository: ${cloneResult.error}`)); process.exit(1); } projectPath = cloneResult.localPath; framework = await detectFrameworkFromDirectory(projectPath); } else { console.log(chalk_1.default.cyan('Detected local directory')); projectPath = path_1.default.resolve(source); if (!await fs_extra_1.default.pathExists(projectPath)) { console.log(chalk_1.default.red(`Error: Directory "${source}" does not exist`)); process.exit(1); } // Check if it's a git repository try { const gitRemote = (0, child_process_1.execSync)('git remote get-url origin', { cwd: projectPath, encoding: 'utf8' }).trim(); gitRepository = gitRemote; isGitRepo = true; } catch { // Not a git repository } projectName = path_1.default.basename(projectPath); framework = await detectFrameworkFromDirectory(projectPath); } } else { // Use current directory console.log(chalk_1.default.cyan('Using current directory')); // Check if we have a project config const existingConfig = await configService.loadProjectConfig(); if (existingConfig) { projectName = existingConfig.projectName || path_1.default.basename(projectPath); framework = existingConfig.framework || await detectFrameworkFromDirectory(projectPath); } else { projectName = path_1.default.basename(projectPath); framework = await detectFrameworkFromDirectory(projectPath); } // Check if it's a git repository try { gitRepository = (0, child_process_1.execSync)('git remote get-url origin', { encoding: 'utf8' }).trim(); isGitRepo = true; } catch { // Not a git repository } } console.log(chalk_1.default.gray(`Project: ${projectName}`)); console.log(chalk_1.default.gray(`Framework: ${framework}`)); if (gitRepository) { console.log(chalk_1.default.gray(`Repository: ${gitRepository}`)); } // Get public IP for local deployment routing const publicIP = await (0, system_1.getPublicIP)(); const localIP = (0, system_1.getSystemIP)(); console.log(chalk_1.default.gray(`Public IP: ${publicIP}`)); console.log(chalk_1.default.gray(`Local IP: ${localIP}`)); console.log(); // Check authentication const globalConfig = await configService.loadGlobalConfig(); if (!globalConfig?.apiKey) { console.log(chalk_1.default.red('Error: Not authenticated')); console.log('Run "forge login" to authenticate'); process.exit(1); } const apiService = new api_1.ForgeApiService(); apiService.setApiKey(globalConfig.apiKey); // Verify API key console.log(chalk_1.default.gray('Verifying authentication...')); const authResponse = await apiService.verifyApiKey(); if (!authResponse.success) { console.log(chalk_1.default.red('Error: Authentication failed')); console.log('Run "forge login" to re-authenticate'); process.exit(1); } // Custom subdomain (if provided) const customSubdomain = options.subdomain; if (customSubdomain) { console.log(chalk_1.default.gray(`Using custom subdomain: ${customSubdomain}`)); } // Build configuration const buildConfig = await getBuildConfiguration(framework, projectPath); // Initialize workspace setup variable let workspaceSetup; // Enhanced workspace setup and build process if (!options.skipBuild) { const packageJsonPath = path_1.default.join(projectPath, 'package.json'); if (await fs_extra_1.default.pathExists(packageJsonPath)) { // Initialize workspace manager const workspaceManager = new workspaceManager_1.WorkspaceManager(projectPath); // Check for existing workspace configuration const existingConfig = await configService.loadProjectConfig(); if (options.useWorkspaceConfig && existingConfig?.workspaceSetup) { console.log(chalk_1.default.green('Using existing workspace configuration')); workspaceSetup = existingConfig.workspaceSetup; } else if (!options.skipWorkspaceSetup) { // Interactive workspace setup console.log(); console.log(chalk_1.default.blue('Analyzing workspace...')); try { workspaceSetup = await workspaceManager.interactiveSetup(); // Save workspace setup to config const currentConfig = await configService.loadProjectConfig() || {}; currentConfig.workspaceSetup = workspaceSetup; await configService.saveProjectConfig(currentConfig); console.log(chalk_1.default.green('Workspace configuration saved')); } catch (error) { console.log(chalk_1.default.yellow('Workspace analysis failed, falling back to simple build')); workspaceSetup = await workspaceManager.analyzeWorkspace(); } } else { // Auto-detect workspace setup without interaction console.log(chalk_1.default.cyan('Auto-detecting workspace setup...')); workspaceSetup = await workspaceManager.analyzeWorkspace(); } // Install dependencies console.log(chalk_1.default.cyan('Installing dependencies...')); try { (0, child_process_1.execSync)(workspaceSetup.installCommand, { stdio: 'inherit', cwd: projectPath }); console.log(chalk_1.default.green('Dependencies installed successfully')); } catch (error) { console.log(chalk_1.default.red('Failed to install dependencies')); if (!options.force) { process.exit(1); } console.log(chalk_1.default.yellow('Continuing deployment due to --force flag')); } // Execute pre-deploy steps if (workspaceSetup.preDeploySteps && workspaceSetup.preDeploySteps.length > 0) { console.log(); console.log(chalk_1.default.blue('Executing pre-deploy steps...')); try { await workspaceManager.executeWorkflow(workspaceSetup.preDeploySteps); } catch (error) { console.log(chalk_1.default.red('Pre-deploy steps failed')); if (!options.force) { process.exit(1); } console.log(chalk_1.default.yellow('Continuing deployment due to --force flag')); } } // Build project if build command exists if (buildConfig.buildCommand) { console.log(); console.log(chalk_1.default.cyan('Building project...')); try { (0, child_process_1.execSync)(buildConfig.buildCommand, { stdio: 'inherit', cwd: projectPath }); console.log(chalk_1.default.green('Build completed successfully')); } catch (error) { console.log(chalk_1.default.red('Build failed')); if (!options.force) { process.exit(1); } console.log(chalk_1.default.yellow('Continuing deployment due to --force flag')); } } // Execute build steps if defined (alternative to single build command) if (workspaceSetup.buildSteps && workspaceSetup.buildSteps.length > 0) { console.log(); console.log(chalk_1.default.blue('Executing custom build steps...')); try { await workspaceManager.executeWorkflow(workspaceSetup.buildSteps); } catch (error) { console.log(chalk_1.default.red('Custom build steps failed')); if (!options.force) { process.exit(1); } console.log(chalk_1.default.yellow('Continuing deployment due to --force flag')); } } } } // Create deployment console.log(chalk_1.default.cyan('Creating deployment...')); const deploymentData = { projectName, gitRepository, gitBranch: options.branch, framework, buildCommand: buildConfig.buildCommand, outputDirectory: buildConfig.outputDirectory, environmentVariables: buildConfig.environmentVariables || {}, publicIP, localIP, projectPath: isGitRepo ? projectPath : undefined, ...(customSubdomain && { customSubdomain }) }; const deployResponse = await apiService.createDeployment(deploymentData); if (deployResponse.success) { const deployment = deployResponse.data.deployment; console.log(); console.log(chalk_1.default.green('Deployment created successfully!')); console.log(); console.log(chalk_1.default.blue('Deployment Details:')); console.log(` ${chalk_1.default.cyan('ID:')} ${deployment.id}`); console.log(` ${chalk_1.default.cyan('Subdomain:')} ${deployment.subdomain}`); console.log(` ${chalk_1.default.cyan('URL:')} ${deployment.url}`); console.log(` ${chalk_1.default.cyan('Status:')} ${deployment.status}`); console.log(` ${chalk_1.default.cyan('Framework:')} ${framework}`); console.log(); // Save deployment configuration including workspace setup const projectConfig = { projectName, framework, buildCommand: buildConfig.buildCommand, outputDirectory: buildConfig.outputDirectory, environmentVariables: buildConfig.environmentVariables, deploymentId: deployment.id, subdomain: deployment.subdomain, ...(workspaceSetup && { workspaceSetup }) }; await configService.saveProjectConfig(projectConfig); // Start local deployment console.log(); console.log(chalk_1.default.cyan('Starting local deployment...')); try { const localDeployment = await localDeployment_1.LocalDeploymentManager.deployLocally({ id: deployment.id, projectName, subdomain: deployment.subdomain, framework, projectPath, buildOutputDir: buildConfig.outputDirectory, publicIP }); console.log(chalk_1.default.green('Local deployment started successfully!')); // Start or ensure API server is running for remote monitoring console.log(chalk_1.default.cyan('Ensuring monitoring API server is running...')); try { // Check if API server is already running const healthCheck = await fetch('http://localhost:8080/health', { signal: AbortSignal.timeout(2000) }).catch(() => null); if (healthCheck?.ok) { console.log(chalk_1.default.green('API server is already running')); } else { // Try to start with PM2 first try { (0, child_process_1.execSync)('pm2 describe forge-api-server', { stdio: 'ignore' }); // Server exists but might be stopped (0, child_process_1.execSync)('pm2 restart forge-api-server', { stdio: 'ignore' }); console.log(chalk_1.default.green('API server restarted with PM2')); } catch { // Server doesn't exist, start it const currentDir = process.cwd(); const cliDir = path_1.default.dirname(path_1.default.dirname(__filename)); try { // Change to CLI directory and start server process.chdir(cliDir); (0, child_process_1.execSync)('npm run build', { stdio: 'ignore' }); (0, child_process_1.execSync)('npm run server:start', { stdio: 'ignore' }); console.log(chalk_1.default.green('API server started with PM2')); } catch (pm2Error) { // Fallback to direct start await (0, apiServer_1.startAPIServer)(); console.log(chalk_1.default.green('API server started (direct mode)')); } finally { // Restore original directory process.chdir(currentDir); } } } } catch (error) { console.log(chalk_1.default.yellow('Warning: Could not start API server:', error)); } console.log(); console.log(chalk_1.default.blue('Project Information:')); console.log(` ${chalk_1.default.cyan('Project Path:')} ${projectPath}`); console.log(` ${chalk_1.default.cyan('Framework:')} ${framework}`); console.log(` ${chalk_1.default.cyan('Deployment ID:')} ${deployment.id}`); console.log(); console.log(chalk_1.default.blue('Access Your Application:')); console.log(` ${chalk_1.default.cyan('Local:')} http://localhost:${localDeployment.port}`); console.log(` ${chalk_1.default.cyan('Network:')} http://${localIP}:${localDeployment.port}`); console.log(` ${chalk_1.default.cyan('Public:')} ${localDeployment.url}`); console.log(); console.log(chalk_1.default.yellow('For Public Access:')); console.log(` ${chalk_1.default.gray('1. Open port')} ${localDeployment.port} ${chalk_1.default.gray('in your firewall')}`); console.log(` ${chalk_1.default.gray('2. Domain routing is handled automatically')}`); console.log(); console.log(chalk_1.default.blue('Pro Tips:')); console.log(` ${chalk_1.default.cyan('forge infra --all')} - Setup nginx & PM2 for better management`); console.log(` ${chalk_1.default.cyan('forge status')} - Check all deployment status`); console.log(` ${chalk_1.default.cyan('forge pause')} - Pause this deployment`); console.log(` ${chalk_1.default.cyan('forge stop')} - Stop this deployment`); console.log(` ${chalk_1.default.cyan('forge logs')} - View deployment logs`); } catch (localError) { console.log(chalk_1.default.yellow('WARNING: Local deployment failed, but remote deployment created')); console.log(chalk_1.default.gray(`Local error: ${localError}`)); console.log(chalk_1.default.gray('Use "forge status" to check deployment progress')); console.log(chalk_1.default.gray('Use "forge logs" to view deployment logs')); } } else { throw new Error(deployResponse.error?.message || 'Deployment failed'); } } catch (error) { console.log(chalk_1.default.red(`Deployment failed: ${error}`)); process.exit(1); } }); // Helper functions function isGitHubUrl(url) { return /^https?:\/\/(www\.)?(github|gitlab|bitbucket)\.com\/[^\/]+\/[^\/]+/.test(url); } function extractRepoName(url) { const match = url.match(/\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/)?$/); return match ? match[2] : 'project'; } async function detectFrameworkFromDirectory(projectPath) { try { // Check for package.json const packageJsonPath = path_1.default.join(projectPath, 'package.json'); if (await fs_extra_1.default.pathExists(packageJsonPath)) { const packageJson = await fs_extra_1.default.readJSON(packageJsonPath); // Check dependencies and devDependencies for framework indicators const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Next.js detection if (allDeps.next || packageJson.scripts?.dev?.includes('next')) { return types_1.Framework.NEXTJS; } // Nuxt detection if (allDeps.nuxt || allDeps['@nuxt/kit'] || packageJson.scripts?.dev?.includes('nuxt')) { return types_1.Framework.NUXT; } // Vue detection if (allDeps.vue || allDeps['@vue/cli-service']) { return types_1.Framework.VUE; } // React detection (must come after Next.js check) if (allDeps.react) { return types_1.Framework.REACT; } // Angular detection if (allDeps['@angular/core'] || allDeps['@angular/cli']) { return types_1.Framework.ANGULAR; } // Svelte detection if (allDeps.svelte || allDeps['@sveltejs/kit']) { return types_1.Framework.SVELTE; } // Express detection if (allDeps.express && !allDeps.react && !allDeps.vue) { return types_1.Framework.EXPRESS; } // Fastify detection if (allDeps.fastify) { return types_1.Framework.FASTIFY; } // NestJS detection if (allDeps['@nestjs/core']) { return types_1.Framework.NEST; } // Default to static if Node.js project but no specific framework return types_1.Framework.STATIC; } // Check for Python files const pythonFiles = await fs_extra_1.default.readdir(projectPath); const hasPythonFiles = pythonFiles.some(file => file.endsWith('.py')); if (hasPythonFiles) { // Check for requirements.txt or common Python frameworks const requirementsPath = path_1.default.join(projectPath, 'requirements.txt'); if (await fs_extra_1.default.pathExists(requirementsPath)) { const requirements = await fs_extra_1.default.readFile(requirementsPath, 'utf8'); if (requirements.includes('django') || requirements.includes('Django')) { return types_1.Framework.DJANGO; } if (requirements.includes('flask') || requirements.includes('Flask')) { return types_1.Framework.FLASK; } if (requirements.includes('fastapi') || requirements.includes('FastAPI')) { return types_1.Framework.FASTAPI; } } // Check for manage.py (Django indicator) if (await fs_extra_1.default.pathExists(path_1.default.join(projectPath, 'manage.py'))) { return types_1.Framework.DJANGO; } // Default to Flask for Python projects return types_1.Framework.FLASK; } // Check for PHP files const phpFiles = pythonFiles.some(file => file.endsWith('.php')); if (phpFiles) { // Check for composer.json const composerPath = path_1.default.join(projectPath, 'composer.json'); if (await fs_extra_1.default.pathExists(composerPath)) { const composer = await fs_extra_1.default.readJSON(composerPath); if (composer.require?.['laravel/framework']) { return types_1.Framework.LARAVEL; } if (composer.require?.['symfony/framework-bundle']) { return types_1.Framework.SYMFONY; } } // Check for wp-config.php (WordPress indicator) if (await fs_extra_1.default.pathExists(path_1.default.join(projectPath, 'wp-config.php'))) { return types_1.Framework.WORDPRESS; } // Default to Laravel for PHP projects return types_1.Framework.LARAVEL; } // Default to static return types_1.Framework.STATIC; } catch (error) { console.log(chalk_1.default.yellow('Could not detect framework, defaulting to static')); return types_1.Framework.STATIC; } } function isWebFramework(framework) { return [ types_1.Framework.NEXTJS, types_1.Framework.NUXT, types_1.Framework.REACT, types_1.Framework.VUE, types_1.Framework.ANGULAR, types_1.Framework.SVELTE, types_1.Framework.STATIC, types_1.Framework.DJANGO, types_1.Framework.FLASK, types_1.Framework.FASTAPI, types_1.Framework.LARAVEL, types_1.Framework.SYMFONY, types_1.Framework.WORDPRESS ].includes(framework); } async function getBuildConfiguration(framework, projectPath) { const config = {}; switch (framework) { case types_1.Framework.NEXTJS: config.buildCommand = 'npm run build'; config.outputDirectory = '.next'; break; case types_1.Framework.NUXT: config.buildCommand = 'npm run build'; config.outputDirectory = '.output'; break; case types_1.Framework.REACT: config.buildCommand = 'npm run build'; config.outputDirectory = 'build'; break; case types_1.Framework.VUE: config.buildCommand = 'npm run build'; config.outputDirectory = 'dist'; break; case types_1.Framework.ANGULAR: config.buildCommand = 'npm run build'; config.outputDirectory = 'dist'; break; case types_1.Framework.SVELTE: config.buildCommand = 'npm run build'; config.outputDirectory = 'build'; break; case types_1.Framework.EXPRESS: case types_1.Framework.FASTIFY: case types_1.Framework.NEST: config.buildCommand = 'npm run build'; config.outputDirectory = 'dist'; break; case types_1.Framework.DJANGO: config.buildCommand = 'pip install -r requirements.txt && python manage.py collectstatic --noinput'; break; case types_1.Framework.FLASK: case types_1.Framework.FASTAPI: config.buildCommand = 'pip install -r requirements.txt'; break; case types_1.Framework.LARAVEL: config.buildCommand = 'composer install --no-dev && npm run build'; config.outputDirectory = 'public'; break; case types_1.Framework.SYMFONY: config.buildCommand = 'composer install --no-dev'; config.outputDirectory = 'public'; break; case types_1.Framework.STATIC: default: // No build command for static sites break; } // Check if package.json exists and has custom scripts try { const packageJsonPath = path_1.default.join(projectPath, 'package.json'); if (await fs_extra_1.default.pathExists(packageJsonPath)) { const packageJson = await fs_extra_1.default.readJSON(packageJsonPath); // Override build command if package.json has a build script if (packageJson.scripts?.build && !config.buildCommand) { // Use npm as default since we're using it for installation config.buildCommand = 'npm run build'; } } } catch { // Ignore errors reading package.json } return config; } //# sourceMappingURL=deploy.js.map