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