forge-deploy-cli
Version:
Professional CLI for local deployments with automatic subdomain routing, SSL certificates, and infrastructure management
581 lines • 28.6 kB
JavaScript
;
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