UNPKG

@neurolint/cli

Version:

NeuroLint CLI - Deterministic code fixing for TypeScript, JavaScript, React, and Next.js with 8-layer architecture including Security Forensics, Next.js 16, React Compiler, and Turbopack support

416 lines (370 loc) 12.4 kB
/** * NeuroLint - Centralized Backup Management System * Provides clean, organized backup functionality for NeuroLint * * Copyright (c) 2025 NeuroLint * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); class BackupManager { constructor(options = {}) { this.backupDir = options.backupDir || '.neurolint-backups'; this.maxBackups = options.maxBackups || 10; this.verbose = options.verbose !== undefined ? options.verbose : false; this.excludePatterns = options.excludePatterns || [ '**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**', '**/.git/**', '**/coverage/**', '**/.neurolint-backups/**', '**/*.backup-*', '**/states-*.json' ]; this.includePatterns = options.includePatterns || [ '**/*.js', '**/*.ts', '**/*.jsx', '**/*.tsx', '**/*.json', '**/*.md', '**/*.css', '**/*.scss', '**/*.html' ]; } /** * Initialize backup directory */ async initialize() { try { await fs.mkdir(this.backupDir, { recursive: true }); if (this.verbose) { console.log(`Backup directory initialized: ${this.backupDir}`); } } catch (error) { if (this.verbose) { console.error(`Failed to initialize backup directory: ${error.message}`); } } } /** * Create a backup of a file with organized structure */ async createBackup(filePath, operation = 'auto') { try { // Read original file const content = await fs.readFile(filePath, 'utf8'); // Generate backup metadata const timestamp = Date.now(); const hash = crypto.createHash('md5').update(content).digest('hex').substring(0, 8); const relativePath = path.relative(process.cwd(), filePath); // Create organized backup path const backupFileName = `${path.basename(filePath)}.${timestamp}.${hash}`; const backupSubDir = path.dirname(relativePath); const backupPath = path.join(this.backupDir, backupSubDir, backupFileName); // Ensure backup directory exists await fs.mkdir(path.dirname(backupPath), { recursive: true }); // Create backup with metadata const backupData = { originalPath: relativePath, timestamp, hash, operation, content }; await fs.writeFile(backupPath, JSON.stringify(backupData, null, 2)); // Clean old backups for this file await this.cleanOldBackups(relativePath); return { success: true, backupPath, originalPath: filePath, timestamp, hash }; } catch (error) { if (this.verbose) { console.error(`Failed to create backup for ${filePath}: ${error.message}`); } return { success: false, error: error.message, originalPath: filePath }; } } /** * Restore a file from backup */ async restoreFromBackup(backupPath, targetPath = null) { try { const raw = await fs.readFile(backupPath, 'utf8'); const backupData = JSON.parse(raw); const restorePath = targetPath || path.join(process.cwd(), backupData.originalPath); // Ensure target directory exists await fs.mkdir(path.dirname(restorePath), { recursive: true }); // Write to a temp file first for atomic-like behavior const tempPath = `${restorePath}.neurolint-restore-tmp-${Date.now()}`; await fs.writeFile(tempPath, backupData.content); // Optional integrity check using md5 hash if present if (backupData.hash) { const computed = require('crypto').createHash('md5').update(backupData.content).digest('hex').substring(0, 8); if (computed !== backupData.hash) { // Cleanup temp and abort try { await fs.unlink(tempPath); } catch {} return { success: false, error: 'Backup integrity check failed (hash mismatch)', backupPath }; } } // Rename temp to final path // On Windows, if target exists, replace by unlink then rename try { await fs.rename(tempPath, restorePath); } catch (err) { if (err && (err.code === 'EEXIST' || err.code === 'EPERM')) { try { await fs.unlink(restorePath); } catch {} await fs.rename(tempPath, restorePath); } else { // Cleanup temp and rethrow try { await fs.unlink(tempPath); } catch {} throw err; } } return { success: true, restoredPath: restorePath, backupInfo: backupData }; } catch (error) { if (this.verbose) { console.error(`Failed to restore from backup ${backupPath}: ${error.message}`); } return { success: false, error: error.message, backupPath }; } } /** * Clean old backups for a specific file */ async cleanOldBackups(relativePath) { try { const backupSubDir = path.dirname(relativePath); const backupDirPath = path.join(this.backupDir, backupSubDir); // Get all backups for this file const files = await fs.readdir(backupDirPath); const fileBackups = files .filter(file => file.startsWith(path.basename(relativePath) + '.')) .map(file => ({ name: file, path: path.join(backupDirPath, file), timestamp: parseInt(file.split('.')[1]) })) .sort((a, b) => b.timestamp - a.timestamp); // Remove old backups beyond maxBackups if (fileBackups.length > this.maxBackups) { const toRemove = fileBackups.slice(this.maxBackups); for (const backup of toRemove) { await fs.unlink(backup.path); } } } catch (error) { // Ignore errors for non-existent directories if (error.code !== 'ENOENT' && this.verbose) { console.error(`Failed to clean old backups: ${error.message}`); } } } /** * List all backups */ async listBackups(filter = {}) { try { const backups = []; const scanDir = async (dirPath, relativePath = '') => { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); const entryRelativePath = path.join(relativePath, entry.name); if (entry.isDirectory()) { await scanDir(fullPath, entryRelativePath); } else if (entry.isFile() && entry.name.includes('.')) { try { const backupData = JSON.parse(await fs.readFile(fullPath, 'utf8')); // Apply filters if (filter.operation && backupData.operation !== filter.operation) continue; if (filter.since && backupData.timestamp < filter.since) continue; if (filter.until && backupData.timestamp > filter.until) continue; backups.push({ backupPath: fullPath, originalPath: backupData.originalPath, timestamp: backupData.timestamp, hash: backupData.hash, operation: backupData.operation, size: backupData.content.length }); } catch (error) { // Skip non-backup files } } } }; await scanDir(this.backupDir); return backups.sort((a, b) => b.timestamp - a.timestamp); } catch (error) { if (this.verbose) { console.error(`Failed to list backups: ${error.message}`); } return []; } } /** * Clean all backups */ async cleanAllBackups() { try { await fs.rm(this.backupDir, { recursive: true, force: true }); if (this.verbose) { console.log('All backups cleaned'); } } catch (error) { if (this.verbose) { console.error(`Failed to clean all backups: ${error.message}`); } } } /** * Get backup statistics */ async getStats() { try { const backups = await this.listBackups(); const totalSize = backups.reduce((sum, backup) => sum + backup.size, 0); return { totalBackups: backups.length, totalSize, totalSizeMB: (totalSize / 1024 / 1024).toFixed(2), backupDir: this.backupDir, maxBackups: this.maxBackups }; } catch (error) { if (this.verbose) { console.error(`Failed to get backup stats: ${error.message}`); } return null; } } /** * Check if file should be excluded from processing */ shouldExclude(filePath) { const relativePath = path.relative(process.cwd(), filePath); for (const pattern of this.excludePatterns) { if (this.matchesPattern(relativePath, pattern)) { return true; } } return false; } /** * Check if file should be included in processing */ shouldInclude(filePath) { const relativePath = path.relative(process.cwd(), filePath); for (const pattern of this.includePatterns) { if (this.matchesPattern(relativePath, pattern)) { return true; } } return false; } /** * Simple pattern matching for glob-like patterns */ matchesPattern(filePath, pattern) { // Convert glob pattern to regex const regexPattern = pattern .replace(/\*\*/g, '.*') .replace(/\*/g, '[^/]*') .replace(/\?/g, '.'); const regex = new RegExp(`^${regexPattern}$`); return regex.test(filePath); } /** * Safely write file with integrity verification */ async safeWriteFile(filePath, content, operation = 'auto') { try { // Create backup first if file exists let backupResult = null; try { await fs.access(filePath); backupResult = await this.createBackup(filePath, operation); if (!backupResult.success && this.verbose) { console.warn(`Warning: Could not create backup for ${filePath}: ${backupResult.error}`); } } catch { // File doesn't exist, no backup needed } // Write content atomically using temp file const tempPath = `${filePath}.neurolint-write-tmp-${Date.now()}`; await fs.writeFile(tempPath, content); // Verify write integrity by reading back const writtenContent = await fs.readFile(tempPath, 'utf8'); if (writtenContent !== content) { // Cleanup temp and fail try { await fs.unlink(tempPath); } catch {} return { success: false, error: 'Write integrity check failed - content mismatch', filePath, backupPath: backupResult?.backupPath }; } // Rename temp to final path try { await fs.rename(tempPath, filePath); } catch (err) { if (err && (err.code === 'EEXIST' || err.code === 'EPERM')) { try { await fs.unlink(filePath); } catch {} await fs.rename(tempPath, filePath); } else { // Cleanup temp and rethrow try { await fs.unlink(tempPath); } catch {} throw err; } } return { success: true, filePath, backupPath: backupResult?.backupPath, size: content.length }; } catch (error) { if (this.verbose) { console.error(`Failed to safely write ${filePath}: ${error.message}`); } return { success: false, error: error.message, filePath }; } } } module.exports = BackupManager;