UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

631 lines (630 loc) 25.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.OperationManager = exports.StepType = exports.OperationStatus = exports.OperationType = void 0; exports.createOperationManager = createOperationManager; exports.getGlobalOperationManager = getGlobalOperationManager; exports.setGlobalOperationManager = setGlobalOperationManager; const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); var OperationType; (function (OperationType) { OperationType["CREATE_PROJECT"] = "create_project"; OperationType["ADD_MICROFRONTEND"] = "add_microfrontend"; OperationType["INSTALL_DEPENDENCIES"] = "install_dependencies"; OperationType["UPDATE_CONFIGURATION"] = "update_configuration"; OperationType["FILE_MODIFICATION"] = "file_modification"; OperationType["GENERATE_CODE"] = "generate_code"; OperationType["BUILD_OPERATION"] = "build_operation"; OperationType["DEPLOY_OPERATION"] = "deploy_operation"; OperationType["PLUGIN_INSTALL"] = "plugin_install"; OperationType["WORKSPACE_INIT"] = "workspace_init"; OperationType["TEMPLATE_APPLICATION"] = "template_application"; OperationType["CUSTOM"] = "custom"; })(OperationType || (exports.OperationType = OperationType = {})); var OperationStatus; (function (OperationStatus) { OperationStatus["PENDING"] = "pending"; OperationStatus["RUNNING"] = "running"; OperationStatus["COMPLETED"] = "completed"; OperationStatus["FAILED"] = "failed"; OperationStatus["ROLLED_BACK"] = "rolled_back"; OperationStatus["ROLLBACK_FAILED"] = "rollback_failed"; })(OperationStatus || (exports.OperationStatus = OperationStatus = {})); var StepType; (function (StepType) { StepType["FILE_CREATE"] = "file_create"; StepType["FILE_MODIFY"] = "file_modify"; StepType["FILE_DELETE"] = "file_delete"; StepType["DIRECTORY_CREATE"] = "directory_create"; StepType["DIRECTORY_DELETE"] = "directory_delete"; StepType["PACKAGE_INSTALL"] = "package_install"; StepType["PACKAGE_UNINSTALL"] = "package_uninstall"; StepType["CONFIG_UPDATE"] = "config_update"; StepType["COMMAND_EXECUTE"] = "command_execute"; StepType["TEMPLATE_COPY"] = "template_copy"; StepType["DEPENDENCY_RESOLUTION"] = "dependency_resolution"; StepType["VALIDATION"] = "validation"; StepType["CLEANUP"] = "cleanup"; })(StepType || (exports.StepType = StepType = {})); class OperationManager extends events_1.EventEmitter { constructor(workspaceRoot, options = {}) { super(); this.workspaceRoot = workspaceRoot; this.options = options; this.operations = new Map(); this.defaultOptions = { autoBackup: true, backupBeforeRollback: true, continueOnRollbackFailure: false, maxRollbackAttempts: 3, rollbackTimeout: 300000, // 5 minutes preserveUserFiles: true, confirmRollback: true }; this.backupDirectory = path.join(workspaceRoot, '.re-shell', 'backups'); this.options = { ...this.defaultOptions, ...options }; this.ensureBackupDirectory(); } ensureBackupDirectory() { try { fs.ensureDirSync(this.backupDirectory); } catch (error) { console.warn('Failed to create backup directory:', error); } } async startOperation(type, name, description, command, args) { if (this.activeOperation) { throw new Error('Another operation is already in progress'); } const operationId = this.generateOperationId(); const timestamp = new Date(); const operation = { id: operationId, type, name, description, timestamp, status: OperationStatus.PENDING, context: { workingDirectory: process.cwd(), backupDirectory: this.createBackupDirectory(operationId), environmentVariables: { ...process.env }, affectedFiles: [], createdFiles: [], modifiedFiles: [], deletedFiles: [], installedPackages: [], configurationChanges: [] }, steps: [], rollbackSteps: [], metadata: { command, args, version: this.getCliVersion(), workspace: this.workspaceRoot } }; this.operations.set(operationId, operation); this.activeOperation = operation; // Create backup if enabled if (this.options.autoBackup) { await this.createWorkspaceBackup(operation); } this.emit('operation:started', operation); return operationId; } addStep(step) { if (!this.activeOperation) { throw new Error('No active operation'); } const stepId = this.generateStepId(); const operationStep = { ...step, id: stepId, executed: false }; this.activeOperation.steps.push(operationStep); this.emit('step:added', { operation: this.activeOperation, step: operationStep }); return stepId; } addRollbackStep(step) { if (!this.activeOperation) { throw new Error('No active operation'); } const stepId = this.generateStepId(); const rollbackStep = { ...step, id: stepId }; this.activeOperation.rollbackSteps.unshift(rollbackStep); // Add to beginning for reverse order return stepId; } async executeStep(stepId) { if (!this.activeOperation) { throw new Error('No active operation'); } const step = this.activeOperation.steps.find(s => s.id === stepId); if (!step) { throw new Error(`Step ${stepId} not found`); } if (step.executed) { throw new Error(`Step ${stepId} already executed`); } // Check dependencies if (step.dependencies) { for (const depId of step.dependencies) { const depStep = this.activeOperation.steps.find(s => s.id === depId); if (!depStep || !depStep.executed || !depStep.result?.success) { throw new Error(`Dependency step ${depId} not completed successfully`); } } } this.activeOperation.status = OperationStatus.RUNNING; step.timestamp = new Date(); this.emit('step:start', { operation: this.activeOperation, step }); try { const startTime = Date.now(); const result = await step.action(); step.duration = Date.now() - startTime; step.executed = true; step.result = result; // Track context changes this.updateOperationContext(step, result); // Auto-generate rollback step if possible if (result.success && result.rollbackInfo) { this.generateAutoRollbackStep(step, result.rollbackInfo); } this.emit('step:complete', { operation: this.activeOperation, step, result }); return result; } catch (error) { step.duration = Date.now() - (step.timestamp?.getTime() || Date.now()); step.executed = true; step.result = { success: false, message: `Step failed: ${error.message}`, errors: [error.message] }; this.emit('step:error', { operation: this.activeOperation, step, error }); throw error; } } updateOperationContext(step, result) { if (!this.activeOperation || !result.data) return; const context = this.activeOperation.context; switch (step.type) { case StepType.FILE_CREATE: if (result.data.filePath) { context.createdFiles.push(result.data.filePath); } break; case StepType.FILE_MODIFY: if (result.data.filePath) { context.modifiedFiles.push(result.data.filePath); } break; case StepType.FILE_DELETE: if (result.data.filePath) { context.deletedFiles.push(result.data.filePath); } break; case StepType.PACKAGE_INSTALL: if (result.data.packages) { context.installedPackages.push(...result.data.packages); } break; case StepType.CONFIG_UPDATE: if (result.data.changes) { context.configurationChanges.push(...result.data.changes); } break; } } generateAutoRollbackStep(step, rollbackInfo) { if (!this.activeOperation) return; let rollbackStep; switch (rollbackInfo.type) { case 'file_backup': rollbackStep = { id: this.generateStepId(), name: `Rollback ${step.name}`, description: `Restore file from backup: ${rollbackInfo.data.originalPath}`, critical: false, timeout: 30000, action: async () => { await fs.copy(rollbackInfo.data.backupPath, rollbackInfo.data.originalPath); } }; break; case 'config_backup': rollbackStep = { id: this.generateStepId(), name: `Rollback ${step.name}`, description: `Restore configuration: ${rollbackInfo.data.configPath}`, critical: false, timeout: 30000, action: async () => { await fs.writeJson(rollbackInfo.data.configPath, rollbackInfo.data.originalValue, { spaces: 2 }); } }; break; default: return; // Skip unknown rollback types } this.activeOperation.rollbackSteps.unshift(rollbackStep); } async completeOperation() { if (!this.activeOperation) { throw new Error('No active operation'); } // Check if all steps were executed successfully const failedSteps = this.activeOperation.steps.filter(s => s.executed && !s.result?.success); if (failedSteps.length > 0) { this.activeOperation.status = OperationStatus.FAILED; this.emit('operation:failed', { operation: this.activeOperation, failedSteps }); } else { this.activeOperation.status = OperationStatus.COMPLETED; this.activeOperation.duration = Date.now() - this.activeOperation.timestamp.getTime(); this.emit('operation:completed', this.activeOperation); } this.activeOperation = undefined; } async rollbackOperation(operationId, options = {}) { const operation = this.operations.get(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } if (operation.status === OperationStatus.ROLLED_BACK) { throw new Error(`Operation ${operationId} already rolled back`); } const opts = { ...this.defaultOptions, ...this.options, ...options }; this.emit('rollback:start', operation); // Create backup before rollback if enabled if (opts.backupBeforeRollback) { await this.createRollbackBackup(operation); } const result = { success: true, completedSteps: 0, failedSteps: 0, errors: [], warnings: [], duration: 0 }; const startTime = Date.now(); try { // Execute rollback steps in reverse order for (const rollbackStep of operation.rollbackSteps) { // Check condition if present if (rollbackStep.condition && !rollbackStep.condition()) { result.warnings.push(`Skipped rollback step: ${rollbackStep.name} (condition not met)`); continue; } this.emit('rollback:step:start', { operation, step: rollbackStep }); try { await this.withTimeout(rollbackStep.action(), rollbackStep.timeout); result.completedSteps++; this.emit('rollback:step:success', { operation, step: rollbackStep }); } catch (error) { result.failedSteps++; result.errors.push({ step: rollbackStep.name, error: error.message }); this.emit('rollback:step:error', { operation, step: rollbackStep, error }); if (rollbackStep.critical && !opts.continueOnRollbackFailure) { result.success = false; break; } } } // Additional automatic rollback for tracked changes await this.performAutomaticRollback(operation, result, opts); operation.status = result.success ? OperationStatus.ROLLED_BACK : OperationStatus.ROLLBACK_FAILED; result.duration = Date.now() - startTime; this.emit('rollback:complete', { operation, result }); return result; } catch (error) { result.success = false; result.errors.push({ step: 'rollback_process', error: error.message }); result.duration = Date.now() - startTime; operation.status = OperationStatus.ROLLBACK_FAILED; this.emit('rollback:error', { operation, error }); return result; } } async performAutomaticRollback(operation, result, options) { const context = operation.context; // Remove created files for (const filePath of context.createdFiles || []) { try { if (await fs.pathExists(filePath)) { if (options.preserveUserFiles && await this.isUserModified(filePath, operation)) { result.warnings.push(`Preserved user-modified file: ${filePath}`); continue; } await fs.remove(filePath); result.completedSteps++; } } catch (error) { result.errors.push({ step: `remove_file_${filePath}`, error: error.message }); result.failedSteps++; } } // Restore from backup if available if (context.backupDirectory && await fs.pathExists(context.backupDirectory)) { try { for (const filePath of context.modifiedFiles || []) { const backupPath = path.join(context.backupDirectory, path.relative(context.workingDirectory, filePath)); if (await fs.pathExists(backupPath)) { await fs.copy(backupPath, filePath); result.completedSteps++; } } } catch (error) { result.errors.push({ step: 'restore_from_backup', error: error.message }); result.failedSteps++; } } // Uninstall packages if (context.installedPackages && context.installedPackages.length > 0) { try { await this.uninstallPackages(context.installedPackages); result.completedSteps++; } catch (error) { result.errors.push({ step: 'uninstall_packages', error: error.message }); result.failedSteps++; } } } async isUserModified(filePath, operation) { try { const stats = await fs.stat(filePath); return stats.mtime > operation.timestamp; } catch { return false; } } async uninstallPackages(packages) { const { execSync } = require('child_process'); const packageManager = this.detectPackageManager(); let command; switch (packageManager) { case 'yarn': command = `yarn remove ${packages.join(' ')}`; break; case 'pnpm': command = `pnpm remove ${packages.join(' ')}`; break; case 'bun': command = `bun remove ${packages.join(' ')}`; break; default: command = `npm uninstall ${packages.join(' ')}`; } execSync(command, { stdio: 'inherit' }); } detectPackageManager() { if (fs.existsSync('yarn.lock')) return 'yarn'; if (fs.existsSync('pnpm-lock.yaml')) return 'pnpm'; if (fs.existsSync('bun.lockb')) return 'bun'; return 'npm'; } async createWorkspaceBackup(operation) { const backupDir = operation.context.backupDirectory; await fs.ensureDir(backupDir); // Backup critical files const criticalFiles = [ 'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', '.re-shell/config.yaml', 're-shell.workspaces.yaml' ]; for (const file of criticalFiles) { const filePath = path.join(operation.context.workingDirectory, file); if (await fs.pathExists(filePath)) { const backupPath = path.join(backupDir, file); await fs.ensureDir(path.dirname(backupPath)); await fs.copy(filePath, backupPath); } } } async createRollbackBackup(operation) { const rollbackBackupDir = path.join(this.backupDirectory, `rollback_${operation.id}_${Date.now()}`); await fs.ensureDir(rollbackBackupDir); // Backup current state before rollback const workspaceFiles = await this.getWorkspaceFiles(operation.context.workingDirectory); for (const file of workspaceFiles.slice(0, 100)) { // Limit to prevent huge backups const relativePath = path.relative(operation.context.workingDirectory, file); const backupPath = path.join(rollbackBackupDir, relativePath); await fs.ensureDir(path.dirname(backupPath)); await fs.copy(file, backupPath); } } async getWorkspaceFiles(directory) { const files = []; const entries = await fs.readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.') && entry.name !== '.re-shell') continue; if (entry.name === 'node_modules') continue; const fullPath = path.join(directory, entry.name); if (entry.isDirectory()) { files.push(...await this.getWorkspaceFiles(fullPath)); } else { files.push(fullPath); } } return files; } createBackupDirectory(operationId) { return path.join(this.backupDirectory, `operation_${operationId}_${Date.now()}`); } async withTimeout(promise, timeoutMs) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Operation timed out after ${timeoutMs}ms`)); }, timeoutMs); promise .then(result => { clearTimeout(timer); resolve(result); }) .catch(error => { clearTimeout(timer); reject(error); }); }); } generateOperationId() { return `op_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`; } generateStepId() { return `step_${Date.now()}_${Math.random().toString(36).substr(2, 8)}`; } getCliVersion() { try { const packagePath = path.join(__dirname, '..', '..', 'package.json'); const pkg = fs.readJsonSync(packagePath); return pkg.version || 'unknown'; } catch { return 'unknown'; } } // Query methods getOperation(id) { return this.operations.get(id); } getActiveOperation() { return this.activeOperation; } getAllOperations() { return Array.from(this.operations.values()) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } getOperationsByStatus(status) { return Array.from(this.operations.values()) .filter(op => op.status === status) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } getFailedOperations() { return this.getOperationsByStatus(OperationStatus.FAILED); } getOperationStats() { const operations = Array.from(this.operations.values()); return { total: operations.length, completed: operations.filter(op => op.status === OperationStatus.COMPLETED).length, failed: operations.filter(op => op.status === OperationStatus.FAILED).length, rolledBack: operations.filter(op => op.status === OperationStatus.ROLLED_BACK).length, pending: operations.filter(op => op.status === OperationStatus.PENDING).length, running: operations.filter(op => op.status === OperationStatus.RUNNING).length }; } // Cleanup methods cleanupOldOperations(maxAge = 30 * 24 * 60 * 60 * 1000) { const cutoff = new Date(Date.now() - maxAge); let cleaned = 0; for (const [id, operation] of this.operations) { if (operation.timestamp < cutoff && operation.status !== OperationStatus.RUNNING) { this.operations.delete(id); cleaned++; // Cleanup backup directory if (operation.context.backupDirectory) { fs.remove(operation.context.backupDirectory).catch(() => { }); } } } return cleaned; } async cleanupBackups(maxAge = 7 * 24 * 60 * 60 * 1000) { const cutoff = new Date(Date.now() - maxAge); let cleaned = 0; try { const backupDirs = await fs.readdir(this.backupDirectory, { withFileTypes: true }); for (const dir of backupDirs) { if (dir.isDirectory()) { const dirPath = path.join(this.backupDirectory, dir.name); const stats = await fs.stat(dirPath); if (stats.birthtime < cutoff) { await fs.remove(dirPath); cleaned++; } } } } catch (error) { console.warn('Failed to cleanup backups:', error); } return cleaned; } } exports.OperationManager = OperationManager; // Global operation manager let globalOperationManager = null; function createOperationManager(workspaceRoot, options) { return new OperationManager(workspaceRoot, options); } function getGlobalOperationManager() { if (!globalOperationManager) { globalOperationManager = new OperationManager(process.cwd()); } return globalOperationManager; } function setGlobalOperationManager(manager) { globalOperationManager = manager; }