@codehance/rapid-stack
Version:
A modern full-stack development toolkit for rapid application development
375 lines (329 loc) ⢠12.8 kB
JavaScript
'use strict';
const path = require('path');
const fs = require('fs');
const os = require('os');
const { spawn } = require('child_process');
const BaseGenerator = require('../base');
const { findProjectRoot, validateRequiredFields, getConfigField } = require('../../lib/utils');
const { execSync } = require('child_process');
const fetch = require('node-fetch');
module.exports = class extends BaseGenerator {
constructor(args, opts) {
super(args, opts);
this.originalDir = process.cwd();
}
async initializing() {
// Validate required fields first
validateRequiredFields();
// Check if we're in a valid Rapid Stack project
const frontendDir = path.join(this.originalDir, 'frontend');
const backendDir = path.join(this.originalDir, 'backend');
const devopsDir = path.join(this.originalDir, 'devops');
if (!fs.existsSync(frontendDir) && !fs.existsSync(backendDir) && !fs.existsSync(devopsDir)) {
this.log('\n' + '='.repeat(80));
this.log('â No Rapid Stack project found!');
this.log('='.repeat(80));
this.log('\nThis directory does not appear to be a Rapid Stack project.');
this.log('Please run this command from the root of your Rapid Stack project.');
this.log('\n' + '='.repeat(80) + '\n');
process.exit(1);
}
// Get project name from .rapidrc using getConfigField
this.appName = getConfigField('config.app_name');
if (!this.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);
}
// Check if .rapid_stack directory exists
const rapidStackDir = path.join(os.homedir(), '.rapid_stack');
if (!fs.existsSync(rapidStackDir)) {
this.log('\n' + '='.repeat(80));
this.log('â Configuration Directory Not Found!');
this.log('='.repeat(80));
this.log('\nđ Expected directory:');
this.log(` ${rapidStackDir}`);
this.log('\nPlease run "rapid init" first to create a project configuration.');
this.log('\n' + '='.repeat(80) + '\n');
process.exit(1);
}
}
async prompting() {
this.log('\n' + '='.repeat(80));
this.log('â ď¸ DESTRUCTION WARNING');
this.log('='.repeat(80));
this.log('\nThe following resources will be DESTROYED:');
this.log('\nCloud Resources:');
this.log(' ⢠Cloudflare DNS records and configurations');
this.log(' ⢠Digital Ocean droplet and associated resources');
this.log('\nGitHub Resources:');
this.log(' ⢠All GitHub repositories associated with this project');
this.log('\nLocal Resources:');
this.log(' ⢠Frontend directory and all its contents');
this.log(' ⢠Backend directory and all its contents');
this.log(' ⢠DevOps directory and all its contents');
this.log('\n' + '='.repeat(80));
const answers = await this.prompt([
{
type: 'confirm',
name: 'confirmDestroy',
message: 'Are you absolutely sure you want to proceed with destruction?',
default: false
}
]);
if (!answers.confirmDestroy) {
this.log('\nDestruction cancelled. No resources were destroyed.\n');
process.exit(0);
}
}
async _confirmStep(message, defaultAnswer = false) {
const answers = await this.prompt([
{
type: 'confirm',
name: 'confirm',
message: message,
default: defaultAnswer
}
]);
return answers.confirm;
}
async _destroyInfrastructure() {
this.log('\n' + '='.repeat(80));
this.log('đ Infrastructure Destruction');
this.log('='.repeat(80));
this.log('\nThis will destroy all infrastructure resources in DigitalOcean:');
this.log(' ⢠Droplets (Manager and Worker nodes)');
this.log(' ⢠Floating IPs (Load balancer and node IPs)');
this.log(' ⢠SSH Keys (For server access)');
this.log(' ⢠Domain Records (DNS configurations)');
this.log(' ⢠SSL Certificates (Let\'s Encrypt certificates)');
this.log(' ⢠Load Balancer (HAProxy configuration)');
this.log(' ⢠Spaces Buckets (Object storage)');
this.log(' ⢠Ansible Configuration (Server setup files)');
this.log(' ⢠GitHub Secrets (Repository configurations)');
this.log(' ⢠Vault Secrets (Application secrets)');
const proceed = await this._confirmStep('Do you want to destroy all infrastructure resources? [y/N]');
if (!proceed) {
this.log('\nâ ď¸ Skipping infrastructure destruction');
return;
}
this.log('\nđ Running terraform destroy...');
const devopsDir = path.join(this.originalDir, 'devops');
const terraformProcess = spawn('terraform', ['destroy', '-auto-approve'], {
cwd: path.join(devopsDir, 'terraform'),
stdio: 'inherit',
shell: true
});
return new Promise((resolve) => {
terraformProcess.on('exit', (code) => {
if (code === 0) {
this.log('\nâ
Infrastructure destroyed successfully!');
// Clear terraform.tfvars
const tfvarsPath = path.join(devopsDir, 'terraform', 'terraform.tfvars');
fs.writeFileSync(tfvarsPath, '# Terraform variables\n');
this.log('đď¸ terraform.tfvars has been cleared');
} else {
this.log('\nâ Failed to destroy infrastructure!');
}
resolve();
});
});
}
async _deleteGitHubRepos() {
this.log('\n' + '='.repeat(80));
this.log('đ GitHub Repository Cleanup');
this.log('='.repeat(80));
const proceed = await this._confirmStep('Do you want to delete all GitHub repositories? [y/N]');
if (!proceed) {
this.log('\nâ ď¸ Skipping GitHub repository deletion');
return;
}
const folders = ['devops', 'frontend', 'backend'];
const projectPath = process.cwd();
for (const folder of folders) {
const folderPath = path.join(projectPath, folder);
this.log(`\nChecking ${folder} directory...`);
if (!fs.existsSync(folderPath)) {
this.log(`â ď¸ Warning: ${folder} directory does not exist in ${projectPath}`);
continue;
}
try {
process.chdir(folderPath);
// Check if it's a git repository
try {
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
} catch (error) {
this.log(`â ď¸ Warning: ${folder} is not a git repository`);
process.chdir(projectPath);
continue;
}
// Get remote URL
let remoteUrl;
try {
remoteUrl = execSync('git config --get remote.origin.url', { encoding: 'utf8' }).trim();
} catch (error) {
this.log(`â ď¸ Warning: ${folder} has no remote repository configured`);
process.chdir(projectPath);
continue;
}
if (remoteUrl) {
// Extract repository information
const repoInfo = this._parseGitUrl(remoteUrl);
if (!repoInfo) {
this.log(`â ď¸ Warning: Could not parse remote URL for ${folder}`);
process.chdir(projectPath);
continue;
}
if (repoInfo.service === 'github') {
await this._deleteGitHubRepo(repoInfo.owner, repoInfo.repo, token);
this.log(`â Deleted remote repository for ${folder}`);
} else if (repoInfo.service === 'gitlab') {
await this._deleteGitLabRepo(repoInfo.owner, repoInfo.repo, token);
this.log(`â Deleted remote repository for ${folder}`);
} else {
this.log(`â ď¸ Warning: Unsupported git hosting service for ${folder}`);
}
}
process.chdir(projectPath);
} catch (error) {
this.log(`â ď¸ Warning: Could not delete remote repository for ${folder}: ${error.message}`);
process.chdir(projectPath);
}
}
}
_parseGitUrl(url) {
if (!url) return null;
// Handle both SSH and HTTPS URLs
const sshMatch = url.match(/git@(.*?):(.*?)\/(.*?)\.git/);
const httpsMatch = url.match(/https:\/\/(.*?)\/(.*?)\/(.*?)\.git/);
if (sshMatch) {
const [_, domain, owner, repo] = sshMatch;
return {
service: domain.includes('github') ? 'github' : 'gitlab',
owner,
repo
};
} else if (httpsMatch) {
const [_, domain, owner, repo] = httpsMatch;
return {
service: domain.includes('github') ? 'github' : 'gitlab',
owner,
repo
};
}
return null;
}
async _deleteGitHubRepo(owner, repo, token) {
try {
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
method: 'DELETE',
headers: {
'Authorization': `token ${token}`,
'Accept': 'application/vnd.github.v3+json'
}
});
if (!response.ok) {
throw new Error(`Failed to delete repository: ${response.statusText}`);
}
} catch (error) {
throw new Error(`Failed to delete GitHub repository: ${error.message}`);
}
}
async _deleteGitLabRepo(owner, repo, token) {
try {
const response = await fetch(`https://gitlab.com/api/v4/projects/${owner}%2F${repo}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Failed to delete repository: ${response.statusText}`);
}
} catch (error) {
throw new Error(`Failed to delete GitLab repository: ${error.message}`);
}
}
async _deleteLocalDirectories() {
this.log('\n' + '='.repeat(80));
this.log('đť Local Directory Cleanup');
this.log('='.repeat(80));
const proceed = await this._confirmStep('Do you want to delete all local directories? [y/N]');
if (!proceed) {
this.log('\nâ ď¸ Skipping local directory deletion');
return;
}
const folders = ['devops', 'frontend', 'backend'];
const projectPath = process.cwd();
for (const folder of folders) {
const folderPath = path.join(projectPath, folder);
this.log(`\nChecking ${folder} directory...`);
if (!fs.existsSync(folderPath)) {
this.log(`â ď¸ Warning: ${folder} directory does not exist in ${projectPath}`);
continue;
}
try {
// First, try to remove the directory directly
try {
fs.rmSync(folderPath, { recursive: true, force: true });
this.log(`â Deleted ${folder} directory`);
continue;
} catch (error) {
this.log(`â ď¸ First attempt to delete ${folder} failed: ${error.message}`);
}
// If direct removal fails, try to remove contents first
try {
const files = fs.readdirSync(folderPath);
for (const file of files) {
const filePath = path.join(folderPath, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true });
} else {
fs.unlinkSync(filePath);
}
}
// Now try to remove the directory itself
fs.rmdirSync(folderPath);
this.log(`â Deleted ${folder} directory`);
} catch (error) {
this.log(`â ď¸ Could not delete ${folder} directory: ${error.message}`);
this.log(` Directory path: ${folderPath}`);
}
} catch (error) {
this.log(`â ď¸ Error processing ${folder} directory: ${error.message}`);
}
}
// Verify deletion
const remainingDirs = folders.filter(folder =>
fs.existsSync(path.join(projectPath, folder))
);
if (remainingDirs.length > 0) {
this.log('\nâ ď¸ The following directories could not be deleted:');
remainingDirs.forEach(dir => this.log(` ⢠${dir}`));
} else {
this.log('\nâ All project directories have been deleted');
}
}
async install() {
try {
// Step 1: Destroy Infrastructure
await this._destroyInfrastructure();
// Step 2: Delete GitHub Repositories
await this._deleteGitHubRepos();
// Step 3: Delete Local Directories
await this._deleteLocalDirectories();
this.log('\n' + '='.repeat(80));
this.log('â
Destruction process completed');
this.log('='.repeat(80) + '\n');
} catch (error) {
this.log('\nâ Error during destruction process:');
this.log(error.message);
process.exit(1);
}
}
};