cfn-forge
Version:
CloudFormation deployment automation tool with git-based workflows
486 lines (430 loc) • 18.5 kB
JavaScript
/**
* 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();
}