UNPKG

cfn-forge

Version:

CloudFormation deployment automation tool with git-based workflows

486 lines (430 loc) 18.5 kB
/** * CFN-Forge CLI Implementation * * Main command-line interface using Commander.js */ const { Command } = require('commander'); const chalk = require('chalk'); const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); const pkg = require('../../package.json'); const { setupGlobalErrorHandlers, withCommandErrorHandling, displaySystemInfo } = require('./utils/global-error-handler'); // Setup global error handling setupGlobalErrorHandlers(); // Display system info in debug mode displaySystemInfo(); // Commands const initCommand = require('./commands/init'); const deployCommand = require('./commands/deploy'); const watchCommand = require('./commands/watch'); const validateCommand = require('./commands/validate'); const templatesCommand = require('./commands/templates'); const costCommand = require('./commands/cost'); const driftCommand = require('./commands/drift'); const parametersCommand = require('./commands/parameters'); const { interactiveMode } = require('./commands/interactive'); // Utils const { logger } = require('./utils/logger'); const { setupAWSEnvironment, getProjectAWSProfile } = require('./utils/config'); const { loadConfig } = require('./utils/config'); const { checkDependencies } = require('./utils/dependencies'); // Create the main program const program = new Command(); // Configure the CLI program .name('cfn-forge') .description('CloudFormation deployment automation tool') .version(pkg.version, '-v, --version') .option('-d, --debug', 'enable debug mode') .option('-p, --profile <profile>', 'AWS profile to use') .option('-r, --region <region>', 'AWS region') .option('-e, --env <environment>', 'target environment', 'dev') .option('-y, --yes', 'skip confirmation prompts') .option('--no-color', 'disable colored output') .option('--no-interactive', 'disable interactive mode') .option('--secure-terminal', 'open in a new secure terminal window') .option('--timestamps', 'show timestamps in console output') .hook('preAction', async (thisCommand) => { // Set up debug mode if (thisCommand.opts().debug) { process.env.DEBUG = 'cfn-forge:*'; logger.debug('Debug mode enabled'); } // Enable timestamps if requested if (thisCommand.opts().timestamps) { logger.enableTimestamps(); } // Disable colors if requested if (thisCommand.opts().noColor) { chalk.level = 0; } // Check dependencies for commands that need them const commandName = thisCommand.name(); const needsAws = ['deploy', 'watch', 'validate', 'cost', 'drift'].includes(commandName); const needsGit = ['init', 'deploy', 'watch'].includes(commandName); if (needsAws || needsGit) { await checkDependencies({ aws: needsAws, git: needsGit }); } // Load project config if in a project directory const configPath = path.join(process.cwd(), 'cfn-forge.yaml'); if (fs.existsSync(configPath)) { try { const config = await loadConfig(configPath); thisCommand.cfnForgeConfig = config; logger.debug('Loaded project configuration'); } catch (error) { logger.warn(`Failed to load config: ${error.message}`); } } }); // Init command program .command('init [template]') .description('Initialize a new CloudFormation project') .option('-l, --list', 'list available templates') .option('-f, --force', 'overwrite existing files') .option('--no-git', 'skip git initialization') .option('--no-install', 'skip dependency installation') .action(initCommand); // Deploy command program .command('deploy') .description('Deploy CloudFormation stack') .option('-o, --once', 'deploy once instead of watching') .option('-w, --wait', 'wait for deployment to complete') .option('--environment <env>', 'target environment (dev, staging, prod)') .option('-t, --tag <tag>', 'deploy specific git tag') .option('--dry-run', 'show what would be deployed') .option('--no-rollback', 'disable automatic rollback on failure') .option('--aws-profile <profile>', 'AWS profile to use for this deployment') .option('-f, --force', 'force deployment despite uncommitted changes') .option('-p, --parameters <params>', 'CloudFormation parameters as key=value pairs (comma-separated)') .option('--parameter-file <file>', 'JSON file containing CloudFormation parameters') .action(withCommandErrorHandling('deploy', (options) => { if (options.awsProfile) { setupAWSEnvironment(options.awsProfile); } return deployCommand(options); })); // Watch command program .command('watch') .description('Watch git repository and auto-deploy changes') .option('-i, --interval <seconds>', 'check interval', '30') .option('--ignore <patterns>', 'comma-separated ignore patterns') .option('--aws-profile <profile>', 'AWS profile to use for deployments') .action((options) => { if (options.awsProfile) { setupAWSEnvironment(options.awsProfile); } watchCommand(options); }); // Validate command program .command('validate') .description('Validate CloudFormation templates and parameter compatibility') .option('-t, --template <path>', 'specific template to validate') .option('-p, --parameters <path>', 'specific parameter file to validate') .option('--strict', 'enable strict validation') .option('--fix', 'attempt to fix issues') .option('--aws-profile <profile>', 'AWS profile to use for validation') .action(async (options) => { if (options.awsProfile) { setupAWSEnvironment(options.awsProfile); } const isValid = await validateCommand(options); if (isValid === false) { process.exit(1); } }); // Templates command program .command('templates') .alias('list-templates') .description('List available project templates') .option('--json', 'output as JSON') .option('--installed', 'show only installed templates') .option('--detailed', 'show detailed template information') .action(withCommandErrorHandling('templates', templatesCommand)); // Cost command program .command('cost') .alias('estimate') .description('Estimate deployment costs') .option('-t, --template <path>', 'specific template to analyze') .option('--monthly', 'show monthly costs (default)') .option('--yearly', 'show yearly costs') .option('--json', 'output as JSON') .option('-v, --verbose', 'show detailed cost breakdown per resource') .action(withCommandErrorHandling('cost', costCommand)); // Parameters command program .command('parameters') .alias('params') .description('Manage CloudFormation template parameters') .option('-t, --template <path>', 'CloudFormation template to analyze') .option('-e, --environment <env>', 'show parameters for specific environment') .option('--json', 'output as JSON') .option('--detailed', 'show detailed parameter information') .option('--generate', 'generate parameter file template') .option('--output <file>', 'output file path for generated parameter file') .option('--validate <file>', 'validate parameter file format') .action(withCommandErrorHandling('parameters', parametersCommand)); // Drift command program .command('drift') .description('Detect stack drift and compare templates') .option('-e, --env <environment>', 'target environment', 'dev') .option('--skip-drift', 'skip CloudFormation drift detection') .option('--skip-template', 'skip template comparison') .option('--show-diff', 'show template differences') .option('--save-templates', 'save templates to files for comparison') .option('--get-template', 'retrieve and display deployed template') .option('-o, --output <file>', 'output file for deployed template') .option('--ignore-exit-code', 'do not exit with error code if drift found') .action(withCommandErrorHandling('drift', driftCommand)); // Status command program .command('status') .description('Show project and git repository status') .option('--json', 'output as JSON') .action(async (options) => { const { statusCommand } = require('./commands/status'); await statusCommand(options); }); // Config command program .command('config [key] [value]') .description('Get or set configuration values') .option('-g, --global', 'use global config') .option('-l, --list', 'list all config values') .option('--unset', 'remove a config value') .option('--aws', 'configure AWS settings') .option('--encrypt', 'encrypt sensitive values') .option('--profile <name>', 'AWS profile name for --aws option') .option('--list-profiles', 'list all AWS profiles') .option('--switch-profile <name>', 'switch default AWS profile') .option('--delete-profile <name>', 'delete an AWS profile') .action(async (key, value, options) => { const { configCommand } = require('./commands/config'); await configCommand(key, value, options); }); // Logs command program .command('logs [stack]') .description('View CloudFormation stack logs') .option('-f, --follow', 'follow log output') .option('-n, --lines <number>', 'number of lines to show', '50') .option('--since <time>', 'show logs since time (e.g., 2h, 30m)') .action(async (stack, options) => { const { viewLogs } = require('./commands/logs'); await viewLogs(stack, options); }); // Update command program .command('update') .description('Update cfn-forge to the latest version') .option('--check', 'check for updates without installing') .option('--branch <branch>', 'update from specific branch', 'main') .option('--commit <commit>', 'update to specific commit') .option('--force', 'force reinstall even if up to date') .option('--token [token]', 'set GitHub access token (prompts if no token provided)') .action(async (options) => { const { updateCfnForge } = require('./commands/update'); await updateCfnForge(options); }); // Plugin command program .command('plugin <action> [name]') .description('Manage cfn-forge plugins') .option('-g, --global', 'install globally') .action(async (action, name, options) => { const { pluginManager } = require('./commands/plugin'); await pluginManager(action, name, options); }); // Hidden debug command program .command('debug', { hidden: true }) .description('Debug information') .action(() => { console.log('CFN-Forge Debug Information'); console.log('=========================='); console.log(`Version: ${pkg.version}`); console.log(`Node: ${process.version}`); console.log(`Platform: ${process.platform} ${process.arch}`); console.log(`CWD: ${process.cwd()}`); console.log(`Home: ${process.env.CFN_FORGE_HOME}`); console.log(`Root: ${process.env.CFN_FORGE_ROOT}`); console.log(`Core: ${process.env.CFN_FORGE_CORE}`); console.log(`Templates: ${process.env.CFN_FORGE_TEMPLATES}`); console.log(); console.log('Environment Variables:'); Object.keys(process.env) .filter((key) => key.startsWith('CFN_') || key.startsWith('AWS_')) .sort() .forEach((key) => { const value = key.includes('SECRET') ? '***' : process.env[key]; console.log(` ${key}=${value}`); }); }); // Custom help program.addHelpText('after', ` Examples: $ cfn-forge ${chalk.white('# Interactive mode')} $ cfn-forge --secure-terminal ${chalk.white('# Launch in secure terminal')} $ cfn-forge init ${chalk.white('# Interactive project setup')} $ cfn-forge init serverless-api ${chalk.white('# Create from template')} $ cfn-forge watch ${chalk.white('# Start deployment watcher')} $ cfn-forge deploy --once ${chalk.white('# Deploy once')} $ cfn-forge validate ${chalk.white('# Validate templates')} $ cfn-forge status ${chalk.white('# Show project and git status')} Security: --secure-terminal ${chalk.white('# Isolates sensitive data in new terminal')} For more information, visit: ${chalk.white.underline('https://github.com/dmleblanc/cfn-forge')} `); // Error handling program.exitOverride(); /** * Launch CFN-Forge in a new secure terminal window */ async function launchSecureTerminal() { const os = require('os'); const path = require('path'); console.log(chalk.blue('\nLaunching CFN-Forge in secure terminal...\n')); // Get the current working directory and cfn-forge executable path const currentDir = process.cwd(); const cfnForgePath = process.argv[0]; // Node executable const cfnForgeScript = path.join(__dirname, '../../bin/cfn-forge'); // Build the command to run cfn-forge in interactive mode const command = `cd "${currentDir}" && node "${cfnForgeScript}"`; try { const platform = os.platform(); if (platform === 'darwin') { // macOS - Use Terminal.app const appleScript = ` tell application "Terminal" do script "echo 'CFN-Forge Secure Terminal' && echo 'Working Directory: ${currentDir}' && echo '' && ${command}" activate end tell `; execSync(`osascript -e '${appleScript}'`); } else if (platform === 'linux') { // Linux - Try common terminal emulators const terminals = ['gnome-terminal', 'xterm', 'konsole', 'xfce4-terminal']; let launched = false; for (const terminal of terminals) { try { if (terminal === 'gnome-terminal') { execSync(`${terminal} --title="CFN-Forge Secure Terminal" -- bash -c "echo 'CFN-Forge Secure Terminal'; echo 'Working Directory: ${currentDir}'; echo ''; ${command}; exec bash"`, { stdio: 'ignore' }); } else if (terminal === 'xterm') { execSync(`${terminal} -title "CFN-Forge Secure Terminal" -e bash -c "echo 'CFN-Forge Secure Terminal'; echo 'Working Directory: ${currentDir}'; echo ''; ${command}; exec bash"`, { stdio: 'ignore' }); } else { execSync(`${terminal} -e bash -c "echo 'CFN-Forge Secure Terminal'; echo 'Working Directory: ${currentDir}'; echo ''; ${command}; exec bash"`, { stdio: 'ignore' }); } launched = true; break; } catch (error) { // Try next terminal continue; } } if (!launched) { throw new Error('No supported terminal emulator found'); } } else if (platform === 'win32') { // Windows - Use Command Prompt or PowerShell try { // Try Windows Terminal first (modern) execSync(`wt.exe -d "${currentDir}" cmd /c "echo CFN-Forge Secure Terminal && echo Working Directory: ${currentDir} && echo. && ${command}"`, { stdio: 'ignore' }); } catch (error) { // Fallback to regular Command Prompt execSync(`start "CFN-Forge Secure Terminal" cmd /k "cd /d "${currentDir}" && echo CFN-Forge Secure Terminal && echo Working Directory: ${currentDir} && echo. && ${command}"`, { stdio: 'ignore' }); } } else { throw new Error(`Unsupported platform: ${platform}`); } console.log(chalk.green('Secure terminal launched successfully!')); console.log(chalk.gray('The new terminal window will:')); console.log(chalk.gray(' • Run in isolated environment')); console.log(chalk.gray(' • Clear sensitive data on exit')); console.log(chalk.gray(' • Not persist command history')); console.log(chalk.gray('\nYou can now close this terminal safely.\n')); // Give the new terminal time to open await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { console.error(chalk.red('Failed to launch secure terminal:'), error.message); console.log(chalk.yellow('\nFallback: Running in current terminal with enhanced security...')); // Clear screen and run in current terminal with security measures console.clear(); console.log(chalk.blue('CFN-Forge Secure Mode\n')); console.log(chalk.gray('Security measures active:')); console.log(chalk.gray(' • Screen cleared')); console.log(chalk.gray(' • Enhanced exit handling')); console.log(chalk.gray(' • Minimal command history\n')); // Set up enhanced security for current session process.on('exit', () => { console.clear(); console.log(chalk.gray('Secure session terminated - screen cleared')); }); // Run interactive mode await interactiveMode(); } } // Parse arguments async function run() { try { // Check if running without commands (just "cfn-forge") const args = process.argv.slice(2); const hasCommand = args.some((arg) => !arg.startsWith('-')); const wantsHelp = args.includes('--help') || args.includes('-h'); const wantsVersion = args.includes('--version') || args.includes('-v'); const noInteractive = args.includes('--no-interactive'); const secureTerminal = args.includes('--secure-terminal'); // Handle secure terminal mode if (secureTerminal && !hasCommand && !wantsHelp && !wantsVersion) { await launchSecureTerminal(); return; } if (!hasCommand && !wantsHelp && !wantsVersion && !noInteractive) { // Use interactive menu mode with arrow key navigation await interactiveMode(); return; } // Setup AWS environment from stored configuration // Check for project-level AWS profile override const projectProfile = getProjectAWSProfile(); setupAWSEnvironment(projectProfile); await program.parseAsync(process.argv); } catch (error) { if (error.code === 'commander.helpDisplayed') { process.exit(0); } if (error.code === 'commander.version') { process.exit(0); } logger.error(error.message); if (process.env.DEBUG) { console.error(error.stack); } // Provide helpful error messages if (error.message.includes('AWS credentials')) { logger.info('\nTo configure AWS credentials:'); logger.info(' $ aws configure'); logger.info(' or set AWS_PROFILE environment variable'); } else if (error.message.includes('git')) { logger.info('\nTo initialize a git repository:'); logger.info(' $ git init'); } else if (error.message.includes('not found')) { logger.info('\nTo see available commands:'); logger.info(' $ cfn-forge --help'); } process.exit(1); } } // Export for testing module.exports = { program, run }; // Run if called directly if (require.main === module) { run(); }