UNPKG

claude-code-automation

Version:

šŸš€ Generic project automation system with anti-compaction protection and recovery capabilities. Automatically detects project type (React, Node.js, Python, Rust, Go, Java) and provides intelligent analysis. Claude Code optimized - run 'welcome' after inst

559 lines (479 loc) • 18.8 kB
/** * Live Preservation Engine - Real-Time Compaction Protection * * Mission: Provide bulletproof protection against compaction truncation during active development * Strategy: Real-time file monitoring with sub-second preservation response times * * Features: * - File system watchers for all source files * - Smart throttling to prevent excessive saves * - Activity classification and pattern recognition * - Multi-layered backup redundancy * - Performance optimization (<5% overhead) */ const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); class LivePreservationEngine { constructor(options = {}) { this.projectRoot = options.projectRoot || path.resolve(__dirname, '../..'); this.isActive = false; this.lastPreservation = {}; this.pendingPreservations = new Map(); // Configuration this.config = { throttleDelay: options.throttleDelay || 10000, // 10 seconds between preservations per file watchPatterns: options.watchPatterns || [ '**/*.js', '**/*.json', '**/*.md', '**/*.html', '**/*.css', '**/*.ts', '**/*.jsx', '**/*.tsx' ], ignoredPaths: options.ignoredPaths || [ '**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/.backup/**', '**/coverage/**' ], maxPreservationsPerMinute: options.maxPreservations || 6, emergencyPreservationThreshold: options.emergencyThreshold || 50000 // 50KB of changes }; // State tracking this.stats = { totalPreservations: 0, filesWatched: 0, lastActivity: null, averagePreservationTime: 0, totalChangesDetected: 0, throttledPreservations: 0 }; // Backup locations for redundancy this.backupLocations = [ path.join(this.projectRoot, '.backup'), path.join(this.projectRoot, 'docs/state'), path.join(this.projectRoot, 'docs/live-protection/session-states'), '/tmp/claude-live-backup', path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.claude-emergency-backup') ]; this.watcher = null; this.preservationQueue = []; this.isProcessingQueue = false; } /** * Start the live preservation system */ async start() { if (this.isActive) { console.log('šŸ”„ Live preservation engine already active'); return; } console.log('šŸš€ Starting Live Preservation Engine...'); try { await this.initializeBackupLocations(); await this.initializeFileWatcher(); await this.startPreservationQueue(); this.isActive = true; this.stats.lastActivity = Date.now(); console.log(`āœ… Live preservation engine started`); console.log(`šŸ“ Watching ${this.stats.filesWatched} files for changes`); console.log(`šŸ’¾ ${this.backupLocations.length} backup locations configured`); } catch (error) { console.error('āŒ Failed to start live preservation engine:', error); throw error; } } /** * Stop the live preservation system */ async stop() { if (!this.isActive) return; console.log('šŸ”„ Stopping live preservation engine...'); if (this.watcher) { await this.watcher.close(); this.watcher = null; } // Process any pending preservations await this.processPreservationQueue(); this.isActive = false; console.log('āœ… Live preservation engine stopped'); } /** * Initialize backup locations with proper permissions */ async initializeBackupLocations() { for (const location of this.backupLocations) { try { await fs.mkdir(location, { recursive: true }); // Test write permissions const testFile = path.join(location, '.write-test'); await fs.writeFile(testFile, 'test'); await fs.unlink(testFile); } catch (error) { console.warn(`āš ļø Backup location not available: ${location} - ${error.message}`); // Remove from available locations const index = this.backupLocations.indexOf(location); if (index > -1) { this.backupLocations.splice(index, 1); } } } if (this.backupLocations.length === 0) { throw new Error('No backup locations available - cannot ensure compaction protection'); } } /** * Initialize file system watcher with chokidar-like functionality */ async initializeFileWatcher() { // For now, we'll use a basic fs.watch implementation // In production, this should use chokidar for better reliability const watchedFiles = new Set(); const setupWatcher = async (dirPath) => { try { const items = await fs.readdir(dirPath, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dirPath, item.name); // Skip ignored paths if (this.shouldIgnorePath(fullPath)) continue; if (item.isDirectory()) { await setupWatcher(fullPath); } else if (this.shouldWatchFile(fullPath)) { watchedFiles.add(fullPath); try { const watcher = fs.watch(fullPath, (eventType) => { if (eventType === 'change') { this.onFileChange(fullPath); } }); // Store watcher reference for cleanup if (!this.fileWatchers) this.fileWatchers = []; this.fileWatchers.push(watcher); } catch (watchError) { // File might not exist or be accessible console.warn(`āš ļø Cannot watch ${fullPath}: ${watchError.message}`); } } } } catch (error) { console.warn(`āš ļø Cannot read directory ${dirPath}: ${error.message}`); } }; await setupWatcher(this.projectRoot); this.stats.filesWatched = watchedFiles.size; } /** * Check if path should be ignored */ shouldIgnorePath(filePath) { const relativePath = path.relative(this.projectRoot, filePath); return this.config.ignoredPaths.some(pattern => { return relativePath.includes(pattern.replace(/\*\*/g, '').replace(/\*/g, '')); }); } /** * Check if file should be watched */ shouldWatchFile(filePath) { const ext = path.extname(filePath); const watchExtensions = ['.js', '.json', '.md', '.html', '.css', '.ts', '.jsx', '.tsx']; return watchExtensions.includes(ext); } /** * Handle file change events */ onFileChange(filePath) { const now = Date.now(); this.stats.totalChangesDetected++; this.stats.lastActivity = now; // Throttling check const lastPreservation = this.lastPreservation[filePath] || 0; if (now - lastPreservation < this.config.throttleDelay) { this.stats.throttledPreservations++; return; } // Add to preservation queue this.queuePreservation({ type: 'file_change', filePath, timestamp: now, priority: this.calculatePriority(filePath) }); } /** * Calculate preservation priority based on file type and importance */ calculatePriority(filePath) { const fileName = path.basename(filePath); const ext = path.extname(filePath); // High priority files if (fileName === 'package.json' || fileName === 'CLAUDE.md') return 10; if (ext === '.js' || ext === '.ts') return 8; if (ext === '.json') return 7; if (ext === '.md') return 6; return 5; // Default priority } /** * Queue preservation request */ queuePreservation(preservationRequest) { this.preservationQueue.push(preservationRequest); // Sort by priority (higher first) this.preservationQueue.sort((a, b) => b.priority - a.priority); // Trigger queue processing if not already running if (!this.isProcessingQueue) { setImmediate(() => this.processPreservationQueue()); } } /** * Process preservation queue */ async processPreservationQueue() { if (this.isProcessingQueue || this.preservationQueue.length === 0) return; this.isProcessingQueue = true; try { while (this.preservationQueue.length > 0) { const request = this.preservationQueue.shift(); await this.executePreservation(request); // Rate limiting - don't exceed max preservations per minute const preservationsInLastMinute = this.getRecentPreservationCount(); if (preservationsInLastMinute >= this.config.maxPreservationsPerMinute) { const delay = 60000 / this.config.maxPreservationsPerMinute; await this.sleep(delay); } } } finally { this.isProcessingQueue = false; } } /** * Execute actual preservation */ async executePreservation(request) { const startTime = Date.now(); try { const projectState = await this.captureProjectState(request); await this.storeStateRedundantly(projectState, request.timestamp); // Update tracking this.lastPreservation[request.filePath] = request.timestamp; this.stats.totalPreservations++; const duration = Date.now() - startTime; this.updateAveragePreservationTime(duration); console.log(`šŸ’¾ Preserved state (${request.type}) - ${duration}ms`); } catch (error) { console.error(`āŒ Preservation failed for ${request.filePath}:`, error); } } /** * Capture current project state */ async captureProjectState(request) { const timestamp = new Date(request.timestamp).toISOString(); return { timestamp, trigger: request.type, changedFile: request.filePath, metadata: await this.captureBasicMetadata(), files: await this.captureRecentChanges(), session: { totalPreservations: this.stats.totalPreservations, totalChanges: this.stats.totalChangesDetected, lastActivity: this.stats.lastActivity, uptime: Date.now() - (this.stats.lastActivity || Date.now()) } }; } /** * Capture basic project metadata */ async captureBasicMetadata() { try { const packageJsonPath = path.join(this.projectRoot, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); return { name: packageJson.name, version: packageJson.version, scripts: packageJson.scripts, dependencies: Object.keys(packageJson.dependencies || {}), devDependencies: Object.keys(packageJson.devDependencies || {}) }; } catch (error) { return { error: 'Could not read package.json' }; } } /** * Capture recent file changes */ async captureRecentChanges() { const recentChanges = []; const cutoffTime = Date.now() - (5 * 60 * 1000); // Last 5 minutes for (const [filePath, timestamp] of Object.entries(this.lastPreservation)) { if (timestamp > cutoffTime) { try { const content = await fs.readFile(filePath, 'utf8'); recentChanges.push({ path: path.relative(this.projectRoot, filePath), timestamp, size: content.length, hash: crypto.createHash('md5').update(content).digest('hex').substring(0, 8) }); } catch (error) { // File might have been deleted recentChanges.push({ path: path.relative(this.projectRoot, filePath), timestamp, error: 'File not accessible' }); } } } return recentChanges; } /** * Store state in multiple locations for redundancy */ async storeStateRedundantly(projectState, timestamp) { const filename = `live-state-${timestamp}.json`; const content = JSON.stringify(projectState, null, 2); const storePromises = this.backupLocations.map(async (location) => { try { const filePath = path.join(location, filename); await fs.writeFile(filePath, content); return { location, success: true }; } catch (error) { return { location, success: false, error: error.message }; } }); const results = await Promise.all(storePromises); const successCount = results.filter(r => r.success).length; if (successCount === 0) { throw new Error('Failed to store state in any backup location'); } // Clean up old backups (keep last 10) await this.cleanupOldBackups(); } /** * Clean up old backup files */ async cleanupOldBackups() { for (const location of this.backupLocations) { try { const files = await fs.readdir(location); const liveStateFiles = files .filter(f => f.startsWith('live-state-')) .sort() .reverse(); // Keep only the 10 most recent const filesToDelete = liveStateFiles.slice(10); for (const file of filesToDelete) { await fs.unlink(path.join(location, file)); } } catch (error) { // Ignore cleanup errors } } } /** * Get count of recent preservations */ getRecentPreservationCount() { const oneMinuteAgo = Date.now() - 60000; return Object.values(this.lastPreservation) .filter(timestamp => timestamp > oneMinuteAgo) .length; } /** * Update average preservation time */ updateAveragePreservationTime(duration) { if (this.stats.averagePreservationTime === 0) { this.stats.averagePreservationTime = duration; } else { this.stats.averagePreservationTime = (this.stats.averagePreservationTime * 0.8) + (duration * 0.2); } } /** * Sleep utility */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Get current statistics */ getStats() { return { ...this.stats, isActive: this.isActive, filesWatched: this.stats.filesWatched, backupLocations: this.backupLocations.length, queueLength: this.preservationQueue.length, recentPreservations: this.getRecentPreservationCount() }; } /** * Start preservation queue processor */ async startPreservationQueue() { // Start periodic queue processing this.queueProcessor = setInterval(() => { if (!this.isProcessingQueue && this.preservationQueue.length > 0) { this.processPreservationQueue(); } }, 1000); // Check every second } /** * Emergency preservation - for immediate protection needs */ async emergencyPreservation(reason = 'Emergency trigger') { console.log(`🚨 Emergency preservation triggered: ${reason}`); const request = { type: 'emergency', filePath: 'N/A', timestamp: Date.now(), priority: 100, reason }; // Execute immediately, bypassing queue await this.executePreservation(request); } /** * Cleanup resources */ async cleanup() { await this.stop(); if (this.queueProcessor) { clearInterval(this.queueProcessor); this.queueProcessor = null; } if (this.fileWatchers) { for (const watcher of this.fileWatchers) { try { watcher.close(); } catch (error) { // Ignore cleanup errors } } this.fileWatchers = []; } } } module.exports = LivePreservationEngine; // Auto-execute if run directly if (require.main === module) { const engine = new LivePreservationEngine(); process.on('SIGINT', async () => { console.log('\nšŸ”„ Shutting down live preservation engine...'); await engine.cleanup(); process.exit(0); }); engine.start() .then(() => { console.log('šŸš€ Live preservation engine running. Press Ctrl+C to stop.'); console.log('šŸ“Š Status:', engine.getStats()); }) .catch(error => { console.error('āŒ Failed to start live preservation engine:', error); process.exit(1); }); }