UNPKG

woaru

Version:

Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language

255 lines 8.78 kB
import { EventEmitter } from 'events'; import fs from 'fs-extra'; import * as path from 'path'; class StateOperationLock { queue = []; processing = false; async execute(operation) { return new Promise((resolve, reject) => { this.queue.push(async () => { try { const result = await operation(); resolve(result); } catch (error) { reject(error); } }); this.processQueue(); }); } async processQueue() { if (this.processing || this.queue.length === 0) { return; } this.processing = true; while (this.queue.length > 0) { const operation = this.queue.shift(); if (operation) await operation(); } this.processing = false; } } export class StateManager extends EventEmitter { state; stateFile; saveInProgress = false; autoSaveInterval; stateLock = new StateOperationLock(); constructor(projectPath) { super(); this.stateFile = path.join(projectPath, '.woaru', 'state.json'); this.state = this.initializeState(projectPath); } initializeState(projectPath) { return { projectPath, language: 'unknown', frameworks: [], detectedTools: new Set(), missingTools: new Set(), codeIssues: new Map(), lastAnalysis: new Date(), healthScore: 0, fileCount: 0, watchedFiles: new Set(), }; } async load() { try { if (await fs.pathExists(this.stateFile)) { const savedState = await fs.readJson(this.stateFile); // Reconstruct Sets and Maps this.state = { ...savedState, detectedTools: new Set(savedState.detectedTools), missingTools: new Set(savedState.missingTools), codeIssues: new Map(savedState.codeIssues), watchedFiles: new Set(savedState.watchedFiles), lastAnalysis: new Date(savedState.lastAnalysis), }; } } catch { console.warn('Could not load previous state, starting fresh'); } } async save() { return this.stateLock.execute(async () => { if (this.saveInProgress) { return; // Skip if save is already in progress } this.saveInProgress = true; try { await fs.ensureDir(path.dirname(this.stateFile)); // Convert Sets and Maps for JSON serialization const serializableState = { ...this.state, detectedTools: Array.from(this.state.detectedTools), missingTools: Array.from(this.state.missingTools), codeIssues: Array.from(this.state.codeIssues.entries()), watchedFiles: Array.from(this.state.watchedFiles), }; // Atomic write using temporary file const tempFile = `${this.stateFile}.tmp`; await fs.writeJson(tempFile, serializableState, { spaces: 2 }); await fs.move(tempFile, this.stateFile, { overwrite: true }); this.emit('state_saved'); } catch (error) { console.error('Failed to save state:', error); throw error; } finally { this.saveInProgress = false; } }); } getState() { return { ...this.state }; } updateLanguage(language) { this.stateLock.execute(async () => { if (this.state.language !== language) { this.state.language = language; this.markDirty(); this.emit('language_changed', language); } }); } updateFrameworks(frameworks) { this.stateLock.execute(async () => { const frameworksChanged = JSON.stringify(this.state.frameworks) !== JSON.stringify(frameworks); if (frameworksChanged) { this.state.frameworks = frameworks; this.markDirty(); this.emit('frameworks_changed', frameworks); } }); } addDetectedTool(tool) { this.stateLock.execute(async () => { if (!this.state.detectedTools.has(tool)) { this.state.detectedTools.add(tool); this.state.missingTools.delete(tool); this.markDirty(); this.emit('tool_detected', tool); } }); } addMissingTool(tool) { this.stateLock.execute(async () => { if (!this.state.missingTools.has(tool) && !this.state.detectedTools.has(tool)) { this.state.missingTools.add(tool); this.markDirty(); this.emit('tool_missing', tool); } }); } updateCodeIssues(file, issues) { this.stateLock.execute(async () => { const previousIssues = this.state.codeIssues.get(file) || []; if (issues.length === 0) { this.state.codeIssues.delete(file); } else { this.state.codeIssues.set(file, issues); } // Check for new critical issues const newCritical = issues.filter(i => i.severity === 'critical' && !previousIssues.some(p => p.type === i.type && p.line === i.line)); if (newCritical.length > 0) { this.emit('critical_issues', newCritical); } this.markDirty(); this.updateHealthScore(); }); } addWatchedFile(filePath) { if (!this.state.watchedFiles.has(filePath)) { this.state.watchedFiles.add(filePath); this.state.fileCount = this.state.watchedFiles.size; this.markDirty(); } } removeWatchedFile(filePath) { if (this.state.watchedFiles.has(filePath)) { this.state.watchedFiles.delete(filePath); this.state.codeIssues.delete(filePath); this.state.fileCount = this.state.watchedFiles.size; this.markDirty(); } } applyFileChange(change) { switch (change.type) { case 'add': this.addWatchedFile(change.path); break; case 'unlink': this.removeWatchedFile(change.path); break; case 'change': // Will trigger re-analysis break; } } updateHealthScore() { const toolCoverage = this.calculateToolCoverage(); const issueScore = this.calculateIssueScore(); this.state.healthScore = Math.round((toolCoverage + issueScore) / 2); this.emit('health_score_updated', this.state.healthScore); } calculateToolCoverage() { const totalTools = this.state.detectedTools.size + this.state.missingTools.size; if (totalTools === 0) return 100; return Math.round((this.state.detectedTools.size / totalTools) * 100); } calculateIssueScore() { let totalScore = 100; const allIssues = Array.from(this.state.codeIssues.values()).flat(); // Deduct points based on severity const severityPenalty = { critical: 10, high: 5, medium: 2, low: 1, }; allIssues.forEach(issue => { totalScore -= severityPenalty[issue.severity]; }); return Math.max(0, totalScore); } // Auto-save every 30 seconds startAutoSave() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); } this.autoSaveInterval = setInterval(async () => { try { await this.save(); } catch (error) { console.error('Auto-save failed:', error); } }, 30000); } stopAutoSave() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); this.autoSaveInterval = undefined; } } markDirty() { // State is always considered dirty after any mutation // Removed dirty flag as we now save on every change } async destroy() { this.stopAutoSave(); await this.save(); this.removeAllListeners(); } } //# sourceMappingURL=StateManager.js.map