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.

218 lines (173 loc) 7.33 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.smartfile.fs.fileExists(absolutePath); if (!exists) { // File doesn't exist yet (will be created), so we skip backup return; } // Read file content and metadata const content = await plugins.smartfile.fs.toStringSync(absolutePath); const stats = await plugins.smartfile.fs.stat(absolutePath); const checksum = this.calculateChecksum(content); // Create backup const backupPath = this.getBackupPath(operationId, filepath); await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath)); await plugins.smartfile.memory.toFs(content, backupPath); // 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) { throw new Error(`Operation ${operationId} not found`); } 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.smartfile.fs.toStringSync(backupPath); const backupChecksum = this.calculateChecksum(backupContent); if (backupChecksum !== file.checksum) { throw new Error(`Backup integrity check failed for ${file.path}`); } // Restore file await plugins.smartfile.memory.toFs(file.originalContent, absolutePath); // 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.smartfile.fs.remove(operationDir); // 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.smartfile.fs.fileExists(backupPath); if (!exists) { return false; } const content = await plugins.smartfile.fs.toStringSync(backupPath); 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.smartfile.fs.ensureDir(this.backupDir); await plugins.smartfile.fs.ensureDir(plugins.path.join(this.backupDir, 'operations')); } 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 exists = await plugins.smartfile.fs.fileExists(this.manifestPath); if (!exists) { return { operations: [] }; } const content = await plugins.smartfile.fs.toStringSync(this.manifestPath); return JSON.parse(content); } private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> { await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath); } 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); } }