UNPKG

cfn-forge

Version:

CloudFormation deployment automation tool with git-based workflows

462 lines (395 loc) 10.9 kB
/** * CFN-Forge Init Command * * Initializes a new CloudFormation project with templates and configuration */ const fs = require('fs').promises; const path = require('path'); const chalk = require('chalk'); const inquirer = require('inquirer'); const ora = require('ora'); const { execSync } = require('child_process'); const { logger } = require('../utils/logger'); const { copyTemplate, listTemplates } = require('../../lib/templates/engine'); const { validateProjectName } = require('../utils/validators'); async function initCommand(template, options) { const command = this; // If --list flag, show templates and exit if (options.list) { await showTemplates(); return; } logger.info(chalk.white('CloudFormation Forge - Project Initialization')); console.log(); // Check if already in a cfn-forge project try { await fs.access('.cfn-forge.yaml'); if (!options.force) { logger.error('Already in a cfn-forge project!'); logger.info('Use --force to reinitialize'); process.exit(1); } } catch { // Good, not in a project } // Interactive or template-based initialization let projectConfig; if (template) { projectConfig = await initFromTemplate(template, options); } else { projectConfig = await initInteractive(options); } // Create project structure const spinner = ora('Creating project structure...').start(); try { await createProjectStructure(projectConfig, options); spinner.succeed('Project structure created'); // Initialize git if requested if (!options.noGit && !isGitRepo()) { spinner.start('Initializing git repository...'); initGitRepo(); spinner.succeed('Git repository initialized'); } // Install dependencies if needed if (!options.noInstall && projectConfig.hasDependencies) { spinner.start('Installing dependencies...'); await installDependencies(projectConfig); spinner.succeed('Dependencies installed'); } // Show success message console.log(); logger.success(chalk.green('Project initialized successfully!')); console.log(); showNextSteps(projectConfig); } catch (error) { spinner.fail('Failed to create project'); logger.error(error.message); process.exit(1); } } async function initInteractive(options) { console.log('Setting up CloudFormation project...'); console.log(); const answers = await inquirer.prompt([ { type: 'input', name: 'projectName', message: 'Project name:', default: path.basename(process.cwd()), validate: validateProjectName, }, { type: 'input', name: 'description', message: 'Project description:', default: 'CloudFormation project managed by cfn-forge', }, { type: 'list', name: 'template', message: 'Select a project template:', choices: [ { name: 'Serverless API (Lambda + API Gateway)', value: 'serverless-api' }, { name: 'Static Website (S3 + CloudFront)', value: 'static-website' }, { name: 'Container Service (ECS/Fargate)', value: 'container-service' }, { name: 'Full Stack Application', value: 'full-stack' }, { name: 'Blank Project', value: 'blank' }, ], }, { type: 'input', name: 'awsRegion', message: 'AWS Region:', default: process.env.AWS_DEFAULT_REGION || 'us-east-1', }, { type: 'confirm', name: 'useEnvironments', message: 'Set up multiple environments (dev/staging/prod)?', default: true, }, { type: 'confirm', name: 'deployOnTags', message: 'Deploy to production only on git tags?', default: true, when: (answers) => answers.useEnvironments, }, ]); return { ...answers, stackName: generateStackName(answers.projectName), hasDependencies: answers.template !== 'blank', }; } async function initFromTemplate(templateName, options) { // Verify template exists const templates = await listTemplates(); const template = templates.find((t) => t.name === templateName); if (!template) { logger.error(`Template '${templateName}' not found`); logger.info('Available templates:'); templates.forEach((t) => { console.log(` • ${t.name} - ${t.description}`); }); process.exit(1); } logger.info(`Using template: ${chalk.cyan(template.name)}`); console.log(chalk.white(template.description)); console.log(); // Get minimal config for template const answers = await inquirer.prompt([ { type: 'input', name: 'projectName', message: 'Project name:', default: path.basename(process.cwd()), validate: validateProjectName, }, { type: 'input', name: 'awsRegion', message: 'AWS Region:', default: process.env.AWS_DEFAULT_REGION || 'us-east-1', }, ]); return { ...answers, template: templateName, templateConfig: template, stackName: generateStackName(answers.projectName), description: `${template.description} - Managed by cfn-forge`, useEnvironments: true, deployOnTags: true, hasDependencies: template.hasDependencies, }; } async function createProjectStructure(config, options) { // Create minimal directory structure if (config.useEnvironments) { await fs.mkdir('parameters', { recursive: true }); } // Copy template files if using a template if (config.template && config.template !== 'blank') { await copyTemplate(config.template, process.cwd(), config); } // Create .cfn-forge.yaml await createConfigFile(config); // Create CloudFormation template await createCloudFormationTemplate(config); // Create parameter files for environments if enabled if (config.useEnvironments) { await createParameterFiles(config); } // Create minimal .gitignore await createMinimalGitignore(); } async function createConfigFile(config) { const configContent = { version: 1, project: { name: config.projectName, description: config.description, template: config.template, }, defaults: { region: config.awsRegion, profile: process.env.AWS_PROFILE, }, templates: { main: 'template.yaml', }, }; if (config.useEnvironments) { configContent.environments = { dev: { stackName: `${config.stackName}-dev`, deployOnTags: false, region: config.awsRegion, }, staging: { stackName: `${config.stackName}-staging`, deployOnTags: true, region: config.awsRegion, }, prod: { stackName: `${config.stackName}-prod`, deployOnTags: config.deployOnTags, region: config.awsRegion, requireApproval: true, }, }; } else { configContent.environments = { default: { stackName: config.stackName, deployOnTags: false, region: config.awsRegion, }, }; } if (config.template === 'serverless-api' || config.template === 'full-stack') { configContent.lambda = { sourceDir: 'lambda', buildCommand: 'npm run build', testCommand: 'npm test', }; } const yaml = require('js-yaml'); const yamlStr = yaml.dump(configContent, { indent: 2 }); await fs.writeFile('.cfn-forge.yaml', yamlStr); } async function createGitignore() { const content = `# AWS .aws-sam/ samconfig.toml # Environment .env .env.* !.env.example # Dependencies node_modules/ venv/ __pycache__/ *.pyc # Build artifacts *.zip build/ dist/ .cache/ # IDE .vscode/ .idea/ *.swp *.swo # OS .DS_Store Thumbs.db # CFN-Forge .cfn-forge/ deploy/.env deploy/.env.hash deploy/.env.bak # Logs *.log logs/ `; await fs.writeFile('.gitignore', content); } async function createCloudFormationTemplate(config) { const template = `AWSTemplateFormatVersion: '2010-09-09' Description: '${config.projectName} - ${config.description}' Parameters: Environment: Type: String Default: dev AllowedValues: [dev, staging, prod] Description: Deployment environment Resources: # Add your AWS resources here ExampleBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub '\${AWS::StackName}-example-bucket' Tags: - Key: Project Value: ${config.projectName} - Key: Environment Value: !Ref Environment - Key: ManagedBy Value: cfn-forge Outputs: BucketName: Description: Name of the example S3 bucket Value: !Ref ExampleBucket Export: Name: !Sub '\${AWS::StackName}-bucket-name' `; await fs.writeFile('template.yaml', template); } async function createParameterFiles(config) { const environments = ['dev', 'staging', 'prod']; for (const env of environments) { const parameters = [ { ParameterKey: 'Environment', ParameterValue: env, }, ]; await fs.writeFile( `parameters/${env}.json`, JSON.stringify(parameters, null, 2), ); } } async function createMinimalGitignore() { const content = `# AWS .aws-sam/ samconfig.toml # Environment .env .env.* # Build artifacts *.zip build/ dist/ # OS .DS_Store Thumbs.db # Logs *.log `; await fs.writeFile('.gitignore', content); } function isGitRepo() { try { execSync('git rev-parse --git-dir', { stdio: 'ignore' }); return true; } catch { return false; } } function initGitRepo() { execSync('git init', { stdio: 'ignore' }); execSync('git add .', { stdio: 'ignore' }); execSync('git commit -m "Initial commit - CFN-Forge project"', { stdio: 'ignore' }); } async function installDependencies(config) { if (config.template === 'serverless-api' || config.template === 'full-stack') { process.chdir('lambda'); execSync('npm install', { stdio: 'inherit' }); process.chdir('..'); } if (config.template === 'static-website' || config.template === 'full-stack') { if (await fs.access('website/package.json').then(() => true).catch(() => false)) { process.chdir('website'); execSync('npm install', { stdio: 'inherit' }); process.chdir('..'); } } } function generateStackName(projectName) { return projectName .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/--+/g, '-') .replace(/^-|-$/g, ''); } async function showTemplates() { const templates = await listTemplates(); console.log(chalk.white('📋 Available Templates')); console.log(); templates.forEach((template) => { console.log(` ${chalk.cyan(template.name.padEnd(20))} ${template.description}`); }); console.log(); console.log('Usage: cfn-forge init <template-name>'); } function showNextSteps(config) { console.log(chalk.white('Next steps:')); console.log(); console.log(chalk.white(' cfn-forge deploy')); console.log(chalk.white(' cfn-forge watch')); console.log(); } module.exports = initCommand;