UNPKG

@codehance/rapid-stack

Version:

A modern full-stack development toolkit for rapid application development

432 lines (366 loc) 14.5 kB
const BaseGenerator = require('../base'); const fs = require('fs'); const path = require('path'); const { handlePrompt, getConfigField, createGitHubRepos, validateRequiredFields, initializeAndPushGitRepo, updateGitHubSecrets, deleteGitHubRepos } = require('../../lib/utils'); module.exports = class extends BaseGenerator { constructor(args, opts) { super(args, opts); } async initializing() { validateRequiredFields( [ 'config.cloudflare_api_key', 'config.cloudflare_account_id', 'config.github_username', 'config.repo_access_token', 'config.dockerhub_username', 'config.dockerhub_password', 'config.email' ] ); if (this.options.rm) { await this._handleDeletion(); process.exit(0); } } // Cleanup local Git repository async _cleanupLocalGit(deployment, originalDir) { const deploymentDir = path.join(originalDir, deployment); if (!fs.existsSync(deploymentDir)) { this.log(`⚠️ ${deployment} directory not found at ${deploymentDir}`); return; } try { process.chdir(deploymentDir); this.log(`\nCleaning up Git configuration in ${deployment} directory...`); // Check if it's a Git repository const { execSync } = require('child_process'); try { execSync('git rev-parse --git-dir', { stdio: 'ignore' }); // Remove remote try { execSync('git remote remove origin', { stdio: 'ignore' }); this.log(`✓ Removed origin remote from ${deployment}`); } catch (error) { this.log(`⚠️ No origin remote found in ${deployment}`); } // Remove .git directory const gitDir = path.join(deploymentDir, '.git'); if (fs.existsSync(gitDir)) { fs.rmSync(gitDir, { recursive: true, force: true }); this.log(`✓ Removed .git directory from ${deployment}`); } } catch (error) { this.log(`⚠️ ${deployment} is not a Git repository`); } } catch (error) { this.log(`❌ Error cleaning up ${deployment}:`, error.message); } finally { // Always return to original directory process.chdir(originalDir); } } async _handleDeletion() { const pendingDeployments = this._scanForDeployments(); if (pendingDeployments.length === 0) { this.log('No deployments found to delete.'); return; } this.log('\nFound the following deployments:'); pendingDeployments.forEach(deployment => { this.log(`- ${deployment}`); }); const { confirmDelete } = await handlePrompt(this, [{ type: 'confirm', name: 'confirmDelete', message: 'Do you want to delete these repositories?', default: false }]); if (!confirmDelete) { this.log('Repository deletion cancelled.'); return; } try { const githubUsername = getConfigField('config.github_username'); const repoAccessToken = getConfigField('config.repo_access_token'); const appName = getConfigField('config.app_name'); // Create repository names const repoNames = pendingDeployments.map(deployment => `${appName}-${deployment}`).join(','); // Delete repositories await deleteGitHubRepos(githubUsername, repoAccessToken, repoNames); this.log('✓ Remote repositories deleted successfully'); // Store original directory const originalDir = process.cwd(); // Clean up local Git configuration for each deployment for (const deployment of pendingDeployments) { await this._cleanupLocalGit(deployment, originalDir); } this.log('\n✓ Cleanup completed'); } catch (error) { this.log('❌ Error during deletion process:', error.message); process.exit(1); } } _scanForDeployments() { const currentDir = process.cwd(); const pendingDeployments = []; // Check for backend directory if (fs.existsSync(path.join(currentDir, 'backend'))) { pendingDeployments.push('backend'); } // Check for frontend directory if (fs.existsSync(path.join(currentDir, 'frontend'))) { pendingDeployments.push('frontend'); } // Check for devops directory if (fs.existsSync(path.join(currentDir, 'devops'))) { pendingDeployments.push('devops'); } return pendingDeployments; } _displayDeployments(pendingDeployments) { if (pendingDeployments.length > 0) { this.log('\nPending deployments found:'); pendingDeployments.forEach(deployment => { this.log(`- ${deployment}`); }); } else { this.log('\nNo pending deployments found.'); } } _generateRandomPassword(length = 32) { const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'; let password = ''; for (let i = 0; i < length; i++) { password += chars.charAt(Math.floor(Math.random() * chars.length)); } return password; } _generateSecureKey(length = 64) { try { return require('child_process').execSync(`openssl rand -hex ${length}`).toString().trim(); } catch (error) { this.log('❌ Error generating secure key:', error.message); process.exit(1); } } _generateSSHKey() { try { const { execSync } = require('child_process'); const sshDir = path.join(process.env.HOME, '.ssh'); const keyName = `rapid_stack_${Date.now()}`; const privateKeyPath = path.join(sshDir, keyName); const publicKeyPath = `${privateKeyPath}.pub`; // Create .ssh directory if it doesn't exist if (!fs.existsSync(sshDir)) { fs.mkdirSync(sshDir, { mode: 0o700 }); } // Generate SSH key pair execSync(`ssh-keygen -t ed25519 -f ${privateKeyPath} -N ""`, { stdio: 'inherit' }); // Set proper permissions fs.chmodSync(privateKeyPath, 0o600); fs.chmodSync(publicKeyPath, 0o644); this.log(`✓ Generated new SSH key pair: ${keyName}`); return keyName; } catch (error) { this.log('❌ Error generating SSH key:', error.message); process.exit(1); } } async _createGitHubSecrets(githubUsername, repoName, repoAccessToken, appName, deploymentType) { try { // Get additional config values const dockerhubUsername = getConfigField('config.dockerhub_username'); const dockerhubPassword = getConfigField('config.dockerhub_password'); const remoteMachine = getConfigField('config.remote_machine'); const appSupportEmail = getConfigField('config.app_support_email'); const mailerFromName = getConfigField('config.mailer_from_name'); const mailerFromAddress = getConfigField('config.mailer_from_address'); const postmarkApiKey = getConfigField('config.postmark_api_key'); const cloudflareApiKey = getConfigField('config.cloudflare_api_key'); const cloudflareAccountId = getConfigField('config.cloudflare_account_id'); const appName = getConfigField('config.app_name'); // Generate MongoDB secrets for backend const mongodbDatabase = `${appName}_prod`; const mongodbUser = `${appName}_user`; const mongodbPassword = this._generateRandomPassword(); // Generate secure keys const jwtSecretKey = this._generateSecureKey(64); const secretKeyBase = this._generateSecureKey(64); const railsMasterKey = this._generateSecureKey(16); // Get or generate SSH key const sshDir = path.join(process.env.HOME, '.ssh'); let matchingKey = null; if (fs.existsSync(sshDir)) { const sshFiles = fs.readdirSync(sshDir); matchingKey = sshFiles.find(file => file.startsWith('rapid_stack_')); } if (!matchingKey) { this.log('No existing SSH key found, generating a new one...'); matchingKey = this._generateSSHKey(); } const sshKeyContent = fs.readFileSync(path.join(sshDir, matchingKey), 'utf8'); // Set up GitHub secrets const secrets = []; // Add MongoDB secrets only for backend if (deploymentType === 'backend') { secrets.push( { key: 'MONGODB_DATABASE', value: mongodbDatabase }, { key: 'MONGODB_USER', value: mongodbUser }, { key: 'MONGODB_PASSWORD', value: mongodbPassword }, { key: 'MY_GITHUB_USERNAME', value: githubUsername }, { key: 'APP_NAME', value: appName }, { key: 'REPO_ACCESS_TOKEN', value: repoAccessToken }, { key: 'DOCKERHUB_USERNAME', value: dockerhubUsername }, { key: 'DOCKERHUB_PASSWORD', value: dockerhubPassword }, { key: 'REMOTE_MACHINE', value: remoteMachine }, { key: 'SSH_PRIVATE_KEY', value: sshKeyContent }, { key: 'JWT_SECRET_KEY', value: jwtSecretKey }, { key: 'SECRET_KEY_BASE', value: secretKeyBase }, { key: 'RAILS_MASTER_KEY', value: railsMasterKey }, { key: 'APP_SUPPORT_EMAIL', value: appSupportEmail }, { key: 'MAILER_FROM_NAME', value: mailerFromName }, { key: 'MAILER_FROM_ADDRESS', value: mailerFromAddress }, { key: 'POSTMARK_API_KEY', value: postmarkApiKey } ); } if (deploymentType === 'frontend') { secrets.push( { key: 'CLOUDFLARE_API_TOKEN', value: cloudflareApiKey }, { key: 'CLOUDFLARE_ACCOUNT_ID', value: cloudflareAccountId }, { key: 'APP_NAME', value: appName } ); } if (deploymentType === 'devops') { secrets.push( { key: 'APP_NAME', value: appName } ); } // Use the improved updateGitHubSecrets function await updateGitHubSecrets(githubUsername, repoName, repoAccessToken, secrets); } catch (error) { this.log('❌ Error creating GitHub secrets:', error.message); process.exit(1); } } async _setupGitHubRepository(deploymentType) { try { // Get GitHub credentials from config const githubUsername = getConfigField('config.github_username'); const appName = getConfigField('config.app_name'); const repoAccessToken = getConfigField('config.repo_access_token'); // Ask user if they want to create the repository const { createRepo } = await handlePrompt(this, [{ type: 'confirm', name: 'createRepo', message: `Would you like to create the ${appName}-${deploymentType} repository?`, default: true }]); if (createRepo) { // Ask if repository should be private const { isPrivate } = await handlePrompt(this, [{ type: 'confirm', name: 'isPrivate', message: 'Should the repository be private?', default: true }]); try { const repoName = `${appName}-${deploymentType}`; // Create GitHub repository await createGitHubRepos( githubUsername, repoAccessToken, repoName, !isPrivate // invert isPrivate since createGitHubRepos expects isPublic ); // Create GitHub secrets before pushing await this._createGitHubSecrets(githubUsername, repoName, repoAccessToken, appName, deploymentType); // Set up GitHub Actions workflow before initializing Git await this._setupGitHubActions(deploymentType); // Initialize Git repository and push initial commit this.log('Initializing Git repository and pushing initial commit...'); await initializeAndPushGitRepo(githubUsername, repoName); this.log('✓ Initial commit pushed to GitHub'); } catch (error) { this.log('❌ Error creating GitHub repository:', error.message); this.log('Please check your GitHub credentials and permissions.'); process.exit(1); } } else { this.log('⚠️ Skipping GitHub repository creation'); } } catch (error) { this.log('❌ Error accessing config file:', error.message); this.log('Please ensure your .rapidrc file is properly configured.'); process.exit(1); } } async _setupGitHubActions(deploymentType) { // Skip GitHub Actions setup for devops if (deploymentType === 'devops') { return; } const workflowDir = path.join(process.cwd(), '.github', 'workflows'); const workflowPath = path.join(workflowDir, 'ci.yml'); // Create .github/workflows directory if it doesn't exist if (!fs.existsSync(workflowDir)) { fs.mkdirSync(workflowDir, { recursive: true }); } // Check if workflow file exists if (fs.existsSync(workflowPath)) { const { confirmOverwrite } = await handlePrompt(this, [{ type: 'confirm', name: 'confirmOverwrite', message: 'GitHub Actions workflow file already exists. Do you want to overwrite it?', default: false }]); if (!confirmOverwrite) { this.log('Skipping GitHub Actions workflow setup.'); return; } } // Copy the appropriate workflow file based on deployment type const templatePath = `github/workflows/${deploymentType}/ci.yml.erb`; // Copy the template file this.fs.copy( this.templatePath(templatePath), workflowPath ); // Force write to disk immediately await new Promise((resolve, reject) => { this.fs.commit((err) => { if (err) reject(err); else resolve(); }); }); this.log(`✓ Added GitHub Actions workflow for ${deploymentType}`); } async install() { const pendingDeployments = this._scanForDeployments(); this._displayDeployments(pendingDeployments); // Store the original working directory const originalDir = process.cwd(); // Create GitHub repositories for each deployment for (const deployment of pendingDeployments) { // Change to the deployment directory const deploymentDir = path.join(originalDir, deployment); if (!fs.existsSync(deploymentDir)) { this.log(`❌ ${deployment} directory not found at ${deploymentDir}`); process.exit(1); } process.chdir(deploymentDir); this.log(`Changed to ${deployment} directory: ${deploymentDir}`); await this._setupGitHubRepository(deployment); // Change back to the original directory process.chdir(originalDir); } } };