UNPKG

dna-template-cli

Version:

DNA Template CLI v0.3.4 - Enhanced Commands Added (enhanced-create, enhanced-list, enhanced-validate)

415 lines 18.2 kB
"use strict"; /** * @fileoverview Robust rollback functionality with atomic operations * Provides transaction-like behavior with file system backup and restore mechanisms */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RollbackManager = void 0; const tslib_1 = require("tslib"); const fs_extra_1 = tslib_1.__importDefault(require("fs-extra")); const path_1 = tslib_1.__importDefault(require("path")); const logger_1 = require("../../utils/logger"); const error_types_1 = require("../errors/error-types"); class RollbackManager { constructor() { this.operations = new Map(); this.snapshots = new Map(); this.tempDir = path_1.default.join(process.cwd(), '.dna-temp'); } static getInstance() { if (!RollbackManager.instance) { RollbackManager.instance = new RollbackManager(); } return RollbackManager.instance; } /** * Start a new transaction for atomic operations */ async startTransaction(description, projectPath) { const transactionId = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; this.currentTransactionId = transactionId; this.operations.set(transactionId, []); // Ensure temp directory exists await fs_extra_1.default.ensureDir(this.tempDir); logger_1.logger.debug(`Started rollback transaction: ${transactionId} - ${description}`); return transactionId; } /** * Create a snapshot of the current state */ async createSnapshot(transactionId, description, projectPath) { const operations = this.operations.get(transactionId) || []; const snapshot = { id: `snapshot_${transactionId}`, operations: [...operations], timestamp: new Date(), description, projectPath, }; this.snapshots.set(snapshot.id, snapshot); logger_1.logger.debug(`Created snapshot: ${snapshot.id}`); } /** * Record a file creation operation */ async recordFileCreation(transactionId, filePath, content) { const operation = { id: `op_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: 'create_file', target: filePath, data: content, timestamp: new Date(), completed: false, }; this.addOperation(transactionId, operation); try { if (content !== undefined) { await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath)); await fs_extra_1.default.writeFile(filePath, content); } operation.completed = true; logger_1.logger.debug(`Recorded file creation: ${filePath}`); } catch (error) { throw new error_types_1.RollbackError(`Failed to create file: ${filePath}`, 'FILE_CREATION_FAILED', 'Check file path and permissions', { filePath, error: error instanceof Error ? error.message : 'Unknown error' }); } } /** * Record a directory creation operation */ async recordDirectoryCreation(transactionId, dirPath) { const operation = { id: `op_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: 'create_directory', target: dirPath, timestamp: new Date(), completed: false, }; this.addOperation(transactionId, operation); try { await fs_extra_1.default.ensureDir(dirPath); operation.completed = true; logger_1.logger.debug(`Recorded directory creation: ${dirPath}`); } catch (error) { throw new error_types_1.RollbackError(`Failed to create directory: ${dirPath}`, 'DIRECTORY_CREATION_FAILED', 'Check directory path and permissions', { dirPath, error: error instanceof Error ? error.message : 'Unknown error' }); } } /** * Record a file modification operation with backup */ async recordFileModification(transactionId, filePath, newContent) { let backupPath; // Create backup if file exists if (await fs_extra_1.default.pathExists(filePath)) { backupPath = path_1.default.join(this.tempDir, `backup_${transactionId}_${path_1.default.basename(filePath)}_${Date.now()}`); await fs_extra_1.default.copy(filePath, backupPath); } const operation = { id: `op_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: 'modify_file', target: filePath, backup: backupPath, data: newContent, timestamp: new Date(), completed: false, }; this.addOperation(transactionId, operation); try { await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath)); await fs_extra_1.default.writeFile(filePath, newContent); operation.completed = true; logger_1.logger.debug(`Recorded file modification: ${filePath}`); } catch (error) { throw new error_types_1.RollbackError(`Failed to modify file: ${filePath}`, 'FILE_MODIFICATION_FAILED', 'Check file path and permissions', { filePath, error: error instanceof Error ? error.message : 'Unknown error' }); } } /** * Record a file copy operation */ async recordFileCopy(transactionId, sourcePath, targetPath) { const operation = { id: `op_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: 'copy_file', target: targetPath, data: sourcePath, timestamp: new Date(), completed: false, }; this.addOperation(transactionId, operation); try { await fs_extra_1.default.ensureDir(path_1.default.dirname(targetPath)); await fs_extra_1.default.copy(sourcePath, targetPath); operation.completed = true; logger_1.logger.debug(`Recorded file copy: ${sourcePath} -> ${targetPath}`); } catch (error) { throw new error_types_1.RollbackError(`Failed to copy file: ${sourcePath} -> ${targetPath}`, 'FILE_COPY_FAILED', 'Check source file exists and target path is writable', { sourcePath, targetPath, error: error instanceof Error ? error.message : 'Unknown error' }); } } /** * Record a file move operation */ async recordFileMove(transactionId, sourcePath, targetPath) { let backupPath; // Create backup of source file if (await fs_extra_1.default.pathExists(sourcePath)) { backupPath = path_1.default.join(this.tempDir, `backup_move_${transactionId}_${path_1.default.basename(sourcePath)}_${Date.now()}`); await fs_extra_1.default.copy(sourcePath, backupPath); } const operation = { id: `op_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: 'move_file', target: targetPath, backup: backupPath, data: sourcePath, timestamp: new Date(), completed: false, }; this.addOperation(transactionId, operation); try { await fs_extra_1.default.ensureDir(path_1.default.dirname(targetPath)); await fs_extra_1.default.move(sourcePath, targetPath); operation.completed = true; logger_1.logger.debug(`Recorded file move: ${sourcePath} -> ${targetPath}`); } catch (error) { throw new error_types_1.RollbackError(`Failed to move file: ${sourcePath} -> ${targetPath}`, 'FILE_MOVE_FAILED', 'Check source file exists and target path is writable', { sourcePath, targetPath, error: error instanceof Error ? error.message : 'Unknown error' }); } } /** * Commit the transaction (cleanup temp files) */ async commitTransaction(transactionId) { const operations = this.operations.get(transactionId); if (!operations) { throw new error_types_1.RollbackError(`Transaction not found: ${transactionId}`, 'TRANSACTION_NOT_FOUND', 'Ensure the transaction was started correctly'); } // Clean up backup files for this transaction for (const operation of operations) { if (operation.backup && await fs_extra_1.default.pathExists(operation.backup)) { try { await fs_extra_1.default.remove(operation.backup); } catch (error) { logger_1.logger.warn(`Failed to cleanup backup file: ${operation.backup}`); } } } // Remove transaction from tracking this.operations.delete(transactionId); // Clean up snapshots const snapshotId = `snapshot_${transactionId}`; this.snapshots.delete(snapshotId); if (this.currentTransactionId === transactionId) { this.currentTransactionId = undefined; } logger_1.logger.debug(`Committed transaction: ${transactionId}`); } /** * Rollback a transaction */ async rollbackTransaction(transactionId) { const operations = this.operations.get(transactionId); if (!operations) { throw new error_types_1.RollbackError(`Transaction not found: ${transactionId}`, 'TRANSACTION_NOT_FOUND', 'Ensure the transaction was started correctly'); } const completedOperations = []; const failedRollbacks = []; // Process operations in reverse order const reversedOperations = [...operations].reverse(); for (const operation of reversedOperations) { if (!operation.completed) { continue; // Skip operations that weren't completed } try { await this.rollbackOperation(operation); completedOperations.push(operation.id); } catch (error) { logger_1.logger.error(`Failed to rollback operation ${operation.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); failedRollbacks.push(operation.id); } } // Clean up transaction this.operations.delete(transactionId); if (this.currentTransactionId === transactionId) { this.currentTransactionId = undefined; } if (failedRollbacks.length > 0) { throw new error_types_1.RollbackFailedError(`Failed to rollback ${failedRollbacks.length} operations`, completedOperations); } logger_1.logger.info(`Successfully rolled back transaction: ${transactionId} (${completedOperations.length} operations)`); } /** * Rollback to a specific snapshot */ async rollbackToSnapshot(snapshotId) { const snapshot = this.snapshots.get(snapshotId); if (!snapshot) { throw new error_types_1.RollbackError(`Snapshot not found: ${snapshotId}`, 'SNAPSHOT_NOT_FOUND', 'Ensure the snapshot was created correctly'); } const completedOperations = []; const failedRollbacks = []; // Process operations from the snapshot in reverse order const reversedOperations = [...snapshot.operations].reverse(); for (const operation of reversedOperations) { if (!operation.completed) { continue; } try { await this.rollbackOperation(operation); completedOperations.push(operation.id); } catch (error) { logger_1.logger.error(`Failed to rollback operation ${operation.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); failedRollbacks.push(operation.id); } } if (failedRollbacks.length > 0) { throw new error_types_1.RollbackFailedError(`Failed to rollback ${failedRollbacks.length} operations from snapshot`, completedOperations); } logger_1.logger.info(`Successfully rolled back to snapshot: ${snapshotId} (${completedOperations.length} operations)`); } /** * Emergency cleanup - removes all files created in the current transaction */ async emergencyCleanup(projectPath) { try { if (await fs_extra_1.default.pathExists(projectPath)) { const stats = await fs_extra_1.default.stat(projectPath); if (stats.isDirectory()) { await fs_extra_1.default.remove(projectPath); logger_1.logger.info(`Emergency cleanup: removed project directory ${projectPath}`); } } } catch (error) { logger_1.logger.error(`Emergency cleanup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw new error_types_1.RollbackError('Emergency cleanup failed', 'EMERGENCY_CLEANUP_FAILED', 'Manual cleanup may be required', { projectPath, error: error instanceof Error ? error.message : 'Unknown error' }); } } /** * Get transaction status */ getTransactionStatus(transactionId) { const operations = this.operations.get(transactionId); if (!operations) { return { exists: false, operationCount: 0, completedCount: 0 }; } const completedCount = operations.filter(op => op.completed).length; return { exists: true, operationCount: operations.length, completedCount }; } /** * List all active transactions */ getActiveTransactions() { return Array.from(this.operations.keys()); } /** * List all snapshots */ getSnapshots() { return Array.from(this.snapshots.values()); } /** * Clean up temp directory */ async cleanupTempDirectory() { try { if (await fs_extra_1.default.pathExists(this.tempDir)) { await fs_extra_1.default.remove(this.tempDir); logger_1.logger.debug('Cleaned up temp directory'); } } catch (error) { logger_1.logger.warn(`Failed to cleanup temp directory: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Private helper methods addOperation(transactionId, operation) { const operations = this.operations.get(transactionId) || []; operations.push(operation); this.operations.set(transactionId, operations); } async rollbackOperation(operation) { switch (operation.type) { case 'create_file': await this.rollbackFileCreation(operation); break; case 'create_directory': await this.rollbackDirectoryCreation(operation); break; case 'modify_file': await this.rollbackFileModification(operation); break; case 'copy_file': await this.rollbackFileCopy(operation); break; case 'move_file': await this.rollbackFileMove(operation); break; default: throw new Error(`Unknown operation type: ${operation.type}`); } } async rollbackFileCreation(operation) { if (await fs_extra_1.default.pathExists(operation.target)) { await fs_extra_1.default.remove(operation.target); logger_1.logger.debug(`Rolled back file creation: ${operation.target}`); } } async rollbackDirectoryCreation(operation) { if (await fs_extra_1.default.pathExists(operation.target)) { const stats = await fs_extra_1.default.stat(operation.target); if (stats.isDirectory()) { // Only remove if directory is empty const contents = await fs_extra_1.default.readdir(operation.target); if (contents.length === 0) { await fs_extra_1.default.remove(operation.target); logger_1.logger.debug(`Rolled back directory creation: ${operation.target}`); } else { logger_1.logger.warn(`Cannot rollback directory ${operation.target}: not empty`); } } } } async rollbackFileModification(operation) { if (operation.backup && await fs_extra_1.default.pathExists(operation.backup)) { // Restore from backup await fs_extra_1.default.copy(operation.backup, operation.target); await fs_extra_1.default.remove(operation.backup); logger_1.logger.debug(`Rolled back file modification: ${operation.target}`); } else { // No backup, remove the file if (await fs_extra_1.default.pathExists(operation.target)) { await fs_extra_1.default.remove(operation.target); logger_1.logger.debug(`Rolled back file modification (no backup): ${operation.target}`); } } } async rollbackFileCopy(operation) { if (await fs_extra_1.default.pathExists(operation.target)) { await fs_extra_1.default.remove(operation.target); logger_1.logger.debug(`Rolled back file copy: ${operation.target}`); } } async rollbackFileMove(operation) { if (operation.backup && await fs_extra_1.default.pathExists(operation.backup)) { // Restore original file from backup const sourcePath = operation.data; await fs_extra_1.default.copy(operation.backup, sourcePath); await fs_extra_1.default.remove(operation.backup); // Remove the moved file if (await fs_extra_1.default.pathExists(operation.target)) { await fs_extra_1.default.remove(operation.target); } logger_1.logger.debug(`Rolled back file move: ${operation.target} -> ${sourcePath}`); } } } exports.RollbackManager = RollbackManager; //# sourceMappingURL=rollback-manager.js.map