UNPKG

@git.zone/cli

Version:

A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.

341 lines (286 loc) 9.86 kB
import * as plugins from './mod.plugins.js'; import * as paths from '../paths.js'; import type { IFormatOperation } from './interfaces.format.js'; export class RollbackManager { private backupDir: string; private manifestPath: string; constructor() { this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups'); this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json'); } async createOperation(): Promise<IFormatOperation> { await this.ensureBackupDir(); const operation: IFormatOperation = { id: this.generateOperationId(), timestamp: Date.now(), files: [], status: 'pending', }; await this.updateManifest(operation); return operation; } async backupFile(filepath: string, operationId: string): Promise<void> { const operation = await this.getOperation(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } const absolutePath = plugins.path.isAbsolute(filepath) ? filepath : plugins.path.join(paths.cwd, filepath); // Check if file exists const exists = await plugins.smartfs.file(absolutePath).exists(); if (!exists) { // File doesn't exist yet (will be created), so we skip backup return; } // Read file content and metadata const content = (await plugins.smartfs .file(absolutePath) .encoding('utf8') .read()) as string; const stats = await plugins.smartfs.file(absolutePath).stat(); const checksum = this.calculateChecksum(content); // Create backup const backupPath = this.getBackupPath(operationId, filepath); await plugins.smartfs .directory(plugins.path.dirname(backupPath)) .recursive() .create(); await plugins.smartfs.file(backupPath).encoding('utf8').write(content); // Update operation operation.files.push({ path: filepath, originalContent: content, checksum, permissions: stats.mode.toString(8), }); await this.updateManifest(operation); } async rollback(operationId: string): Promise<void> { const operation = await this.getOperation(operationId); if (!operation) { // Operation doesn't exist, might have already been rolled back or never created console.warn(`Operation ${operationId} not found for rollback, skipping`); return; } if (operation.status === 'rolled-back') { throw new Error(`Operation ${operationId} has already been rolled back`); } // Restore files in reverse order for (let i = operation.files.length - 1; i >= 0; i--) { const file = operation.files[i]; const absolutePath = plugins.path.isAbsolute(file.path) ? file.path : plugins.path.join(paths.cwd, file.path); // Verify backup integrity const backupPath = this.getBackupPath(operationId, file.path); const backupContent = await plugins.smartfs .file(backupPath) .encoding('utf8') .read(); const backupChecksum = this.calculateChecksum(backupContent); if (backupChecksum !== file.checksum) { throw new Error(`Backup integrity check failed for ${file.path}`); } // Restore file await plugins.smartfs .file(absolutePath) .encoding('utf8') .write(file.originalContent); // Restore permissions const mode = parseInt(file.permissions, 8); // Note: Permissions restoration may not work on all platforms } // Update operation status operation.status = 'rolled-back'; await this.updateManifest(operation); } async markComplete(operationId: string): Promise<void> { const operation = await this.getOperation(operationId); if (!operation) { throw new Error(`Operation ${operationId} not found`); } operation.status = 'completed'; await this.updateManifest(operation); } async cleanOldBackups(retentionDays: number): Promise<void> { const manifest = await this.getManifest(); const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000; const operationsToDelete = manifest.operations.filter( (op) => op.timestamp < cutoffTime && op.status === 'completed', ); for (const operation of operationsToDelete) { // Remove backup files const operationDir = plugins.path.join( this.backupDir, 'operations', operation.id, ); await plugins.smartfs.directory(operationDir).recursive().delete(); // Remove from manifest manifest.operations = manifest.operations.filter( (op) => op.id !== operation.id, ); } await this.saveManifest(manifest); } async verifyBackup(operationId: string): Promise<boolean> { const operation = await this.getOperation(operationId); if (!operation) { return false; } for (const file of operation.files) { const backupPath = this.getBackupPath(operationId, file.path); const exists = await plugins.smartfs.file(backupPath).exists(); if (!exists) { return false; } const content = await plugins.smartfs .file(backupPath) .encoding('utf8') .read(); const checksum = this.calculateChecksum(content); if (checksum !== file.checksum) { return false; } } return true; } async listBackups(): Promise<IFormatOperation[]> { const manifest = await this.getManifest(); return manifest.operations; } private async ensureBackupDir(): Promise<void> { await plugins.smartfs.directory(this.backupDir).recursive().create(); await plugins.smartfs .directory(plugins.path.join(this.backupDir, 'operations')) .recursive() .create(); } private generateOperationId(): string { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const random = Math.random().toString(36).substring(2, 8); return `${timestamp}-${random}`; } private getBackupPath(operationId: string, filepath: string): string { const filename = plugins.path.basename(filepath); const dir = plugins.path.dirname(filepath); const safeDir = dir.replace(/[/\\]/g, '__'); return plugins.path.join( this.backupDir, 'operations', operationId, 'files', safeDir, `${filename}.backup`, ); } private calculateChecksum(content: string | Buffer): string { return plugins.crypto.createHash('sha256').update(content).digest('hex'); } private async getManifest(): Promise<{ operations: IFormatOperation[] }> { const defaultManifest = { operations: [] }; const exists = await plugins.smartfs.file(this.manifestPath).exists(); if (!exists) { return defaultManifest; } try { const content = (await plugins.smartfs .file(this.manifestPath) .encoding('utf8') .read()) as string; const manifest = JSON.parse(content); // Validate the manifest structure if (this.isValidManifest(manifest)) { return manifest; } else { console.warn( 'Invalid rollback manifest structure, returning default manifest', ); return defaultManifest; } } catch (error) { console.warn( `Failed to read rollback manifest: ${error.message}, returning default manifest`, ); // Try to delete the corrupted file try { await plugins.smartfs.file(this.manifestPath).delete(); } catch (removeError) { // Ignore removal errors } return defaultManifest; } } private async saveManifest(manifest: { operations: IFormatOperation[]; }): Promise<void> { // Validate before saving if (!this.isValidManifest(manifest)) { throw new Error('Invalid rollback manifest structure, cannot save'); } // Ensure directory exists await this.ensureBackupDir(); // Write directly with proper JSON stringification const jsonContent = JSON.stringify(manifest, null, 2); await plugins.smartfs .file(this.manifestPath) .encoding('utf8') .write(jsonContent); } private async getOperation( operationId: string, ): Promise<IFormatOperation | null> { const manifest = await this.getManifest(); return manifest.operations.find((op) => op.id === operationId) || null; } private async updateManifest(operation: IFormatOperation): Promise<void> { const manifest = await this.getManifest(); const existingIndex = manifest.operations.findIndex( (op) => op.id === operation.id, ); if (existingIndex !== -1) { manifest.operations[existingIndex] = operation; } else { manifest.operations.push(operation); } await this.saveManifest(manifest); } private isValidManifest( manifest: any, ): manifest is { operations: IFormatOperation[] } { // Check if manifest has the required structure if (!manifest || typeof manifest !== 'object') { return false; } // Check required fields if (!Array.isArray(manifest.operations)) { return false; } // Check each operation entry for (const operation of manifest.operations) { if ( !operation || typeof operation !== 'object' || typeof operation.id !== 'string' || typeof operation.timestamp !== 'number' || typeof operation.status !== 'string' || !Array.isArray(operation.files) ) { return false; } // Check each file in the operation for (const file of operation.files) { if ( !file || typeof file !== 'object' || typeof file.path !== 'string' || typeof file.checksum !== 'string' ) { return false; } } } return true; } }