UNPKG

@codehance/rapid-stack

Version:

A modern full-stack development toolkit for rapid application development

372 lines (320 loc) 12.3 kB
const Generator = require('yeoman-generator'); const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); const { handlePrompt, getConfigField, validateRequiredFields } = require('../../lib/utils'); const BaseGenerator = require('../base'); module.exports = class extends BaseGenerator { constructor(args, opts) { super(args, opts); // Add debug option this.option('debug', { desc: 'Enable debug mode', type: Boolean, default: false }); // Track modified files this.modifiedFiles = []; // Initialize prompting flag this._isPromptingComplete = false; } async initializing() { // Check if --rm flag is present if (this.options.rm) { const appName = getConfigField('config.app_name'); if (!appName) { this.log('\n' + '='.repeat(80)); this.log('❌ Could not find project name in .rapidrc!'); this.log('='.repeat(80)); this.log('\nPlease ensure your .rapidrc file contains a valid config.app_name field.'); this.log('\n' + '='.repeat(80) + '\n'); process.exit(1); } try { this.log('\nDestroying Cloudflare infrastructure...'); execSync('rapid run:static-devops --rm', { stdio: 'inherit' }); this.log('✓ Cloudflare infrastructure destroyed successfully'); } catch (error) { this.log.error('\n❌ Error destroying Cloudflare infrastructure:', error.message); process.exit(1); } // Ask about local project const localAnswer = await this.prompt([ { type: 'confirm', name: 'confirm', message: `Do you want to remove the static-devops folder?`, default: false } ]); if (localAnswer.confirm) { // Remove the static-devops directory try { const projectDir = path.join(process.cwd(), 'static-devops'); if (fs.existsSync(projectDir)) { fs.rmSync(projectDir, { recursive: true, force: true }); this.log('✓ Removed static-devops directory'); } else { this.log('static-devops directory not found.'); } process.exit(0); } catch (error) { this.log.error('\n❌ Error removing static-devops directory:', error.message); process.exit(1); } } this.log('🚀 Clean up completed...'); process.exit(0); } // Validate required fields validateRequiredFields([ 'config.cloudflare_api_key', 'config.cloudflare_account_id', 'config.github_username', 'config.repo_access_token' ]); } destinationPath(...paths) { // First call the parent's destinationPath to get the base path const basePath = super.destinationPath(...paths); // Only prepend project name if: // 1. We have answers (prompting phase is complete) // 2. We have a project name // 3. The path doesn't already include the project name // 4. We're not in the initial setup phase if (this.answers?.projectName && !basePath.includes(this.answers.projectName) && this._isPromptingComplete) { // Prepend the project name to the path return path.join(process.cwd(), this.answers.projectName, ...paths); } return basePath; } async prompting() { // Set default project name to 'devops' this.answers = { projectName: 'static-devops' }; // Set flag indicating prompting is complete this._isPromptingComplete = true; } async configuring() { const { projectName } = this.answers; const projectPath = path.join(process.cwd(), projectName); // Check if project exists if (fs.existsSync(projectPath)) { const { action } = await handlePrompt(this, [{ type: 'list', name: 'action', message: 'Project directory already exists. What would you like to do?', choices: [ { name: 'Update existing project', value: 'update' }, { name: 'Cancel installation', value: 'cancel' } ] }]); if (action === 'cancel') { this.log('Installation cancelled.'); process.exit(0); } } } async _setupProjectDirectory() { const { projectName } = this.answers; const projectPath = path.join(process.cwd(), projectName); if (!fs.existsSync(projectPath)) { const { confirmCreate } = await handlePrompt(this, [{ type: 'confirm', name: 'confirmCreate', message: `This will create a new devops project in: ${projectPath}\nAre you sure you want to continue?`, default: false }]); if (!confirmCreate) { this.log('Installation cancelled.'); process.exit(0); } fs.mkdirSync(projectPath, { recursive: true }); this.log('✓ Created project directory'); } else { this.log('Using existing project directory'); } } async _copyTemplateFile(templatePath, destinationPath, templateData) { try { // Get the full template path. It may return an array if multiple files match. const templateFullPath = this.templatePath(templatePath); // If it's an array, pick the first file. const src = Array.isArray(templateFullPath) ? templateFullPath[0] : templateFullPath; // Get the destination path within the project directory // Remove .erb extension from destination path if it exists const finalDestinationPath = destinationPath.endsWith('.erb') ? destinationPath.slice(0, -4) : destinationPath; const destinationFullPath = this.destinationPath(finalDestinationPath); // Ensure destination directory exists const destDir = path.dirname(destinationFullPath); if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, { recursive: true }); this._debugLog(`Created directory: ${destDir}`); } // Copy the template file with provided context this.fs.copyTpl(src, destinationFullPath, templateData); // Force write to disk immediately await new Promise((resolve, reject) => { this.fs.commit((err) => { if (err) reject(err); else resolve(); }); }); // If the final destination file is a shell script, make it executable if (finalDestinationPath.endsWith('.sh')) { try { fs.chmodSync(destinationFullPath, '755'); this._debugLog(`Made ${finalDestinationPath} executable`); } catch (error) { this.log(`Warning: Could not make ${finalDestinationPath} executable: ${error.message}`); } } this.modifiedFiles.push(finalDestinationPath); this._debugLog(`Copied template ${templatePath} to ${destinationFullPath}`); } catch (error) { this.log(`Error copying template ${templatePath}: ${error.message}`); if (this.options.debug) { this.log('Stack trace:', error.stack); } throw error; } } async _readme() { this._copyTemplateFile('README.md.erb', 'README.md', {}); this.log('✓ README created'); } async _setupTerraform() { const { confirmTerraform } = await handlePrompt(this, [{ type: 'confirm', name: 'confirmTerraform', message: 'Would you like to set up Terraform configuration?', default: true }]); if (confirmTerraform) { try { // Create terraform directory const terraformPath = this.destinationPath('terraform'); if (!fs.existsSync(terraformPath)) { fs.mkdirSync(terraformPath, { recursive: true }); this.log('✓ Terraform directory created'); } // Get source and destination paths const sourcePath = this.templatePath('terraform'); const destPath = this.destinationPath('terraform'); // Function to recursively copy files const copyFilesRecursively = async (sourceDir, destDir) => { const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); for (const entry of entries) { const sourcePath = path.join(sourceDir, entry.name); const destPath = path.join(destDir, entry.name); if (entry.isDirectory()) { // Create directory and recurse if (!fs.existsSync(destPath)) { fs.mkdirSync(destPath, { recursive: true }); } await copyFilesRecursively(sourcePath, destPath); } else { // Copy file const relativePath = path.relative(this.templatePath('terraform'), sourcePath); this._copyTemplateFile( `terraform/${relativePath}`, `terraform/${relativePath}`, {} ); } } }; // Get total number of files and directories for progress tracking const countItems = (dir) => { let count = 0; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { count++; if (entry.isDirectory()) { count += countItems(path.join(dir, entry.name)); } } return count; }; const totalItems = countItems(sourcePath); let processedItems = 0; // Function to update progress const updateProgress = (currentPath) => { processedItems++; const progress = Math.round((processedItems / totalItems) * 100); const progressBar = '█'.repeat(progress / 2) + '░'.repeat(50 - progress / 2); this.log(`[${progressBar}] ${progress}% - Processing: ${path.relative(sourcePath, currentPath)}`); }; // Modified recursive copy function with progress const copyWithProgress = async (sourceDir, destDir) => { const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); for (const entry of entries) { const sourcePath = path.join(sourceDir, entry.name); const destPath = path.join(destDir, entry.name); if (entry.isDirectory()) { if (!fs.existsSync(destPath)) { fs.mkdirSync(destPath, { recursive: true }); } await copyWithProgress(sourcePath, destPath); } else { const relativePath = path.relative(this.templatePath('terraform'), sourcePath); this._copyTemplateFile( `terraform/${relativePath}`, `terraform/${relativePath}`, {} ); } updateProgress(sourcePath); } }; this.log('\nCopying Terraform configuration...'); await copyWithProgress(sourcePath, destPath); this.log('\n✓ All Terraform files copied successfully'); // Force write to disk await new Promise((resolve, reject) => { this.fs.commit((err) => { if (err) reject(err); else resolve(); }); }); } catch (error) { this.log('Error setting up Terraform:', error.message); if (this.options.debug) { this.log('Stack trace:', error.stack); } // Don't throw the error, just log it and continue } } } async install() { try { // Setup project directory await this._setupProjectDirectory(); // Setup README await this._readme(); // Setup Terraform await this._setupTerraform(); // Copy .gitignore from template this._copyTemplateFile('gitignore.tpl', '.gitignore', {}); // Print summary this._printSummary(process.cwd(), this.answers.projectName); } catch (error) { this.log.error('Error during installation:', error); process.exit(1); } } _printSummary(installPath, projectName) { this.log('\n=== Installation Summary ==='); this.log('\nProject Setup:'); this.log(`✓ Created new devops project: ${projectName}`); this.log(`✓ Location: ${installPath}`); } _debugLog(message) { if (this.options.debug) { this.log(`[DEBUG] ${message}`); } } };