@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
JavaScript
"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;
}