UNPKG

@aaswe/codebase-ai

Version:

AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs

388 lines 14.3 kB
"use strict"; /** * File Watcher Service * Handles real-time file system monitoring for code changes and TTL file synchronization */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileWatcher = void 0; const chokidar_1 = require("chokidar"); const events_1 = require("events"); const path_1 = require("path"); const promises_1 = require("fs/promises"); const glob_1 = require("glob"); const minimatch_1 = require("minimatch"); const logger_1 = __importDefault(require("../../../utils/logger")); class FileWatcher extends events_1.EventEmitter { watchers = new Map(); repositories = new Map(); config; constructor(config = {}) { super(); this.config = { ignored: [ '**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/.next/**', '**/coverage/**', '**/*.log', '**/.DS_Store', '**/Thumbs.db' ], persistent: true, ignoreInitial: true, followSymlinks: false, depth: 10, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 10 }, usePolling: false, interval: 100, binaryInterval: 300, ...config }; } /** * Initialize file watcher service */ async initialize() { logger_1.default.info('Initializing File Watcher service'); try { // Set up global error handling this.setupErrorHandling(); logger_1.default.info('File Watcher service initialized'); } catch (error) { logger_1.default.error('Failed to initialize File Watcher service:', error); throw error; } } /** * Start watching a repository */ async startWatching(repository) { const watcherId = repository.id; logger_1.default.info(`Starting file watcher for repository: ${repository.name} at ${repository.path}`); try { // Stop existing watcher if any await this.stopWatching(watcherId); // Create patterns for watching const watchPatterns = this.createWatchPatterns(repository); // Initialize chokidar watcher const watcher = (0, chokidar_1.watch)(watchPatterns, { ...this.config, cwd: repository.path, ignored: [ ...this.config.ignored, ...repository.config.excludePatterns ] }); // Set up event handlers this.setupWatcherEvents(watcher, repository); // Store watcher and repository this.watchers.set(watcherId, watcher); this.repositories.set(watcherId, repository); // Wait for watcher to be ready await new Promise((resolve, reject) => { watcher.on('ready', () => { logger_1.default.info(`File watcher ready for repository: ${repository.name}`); resolve(); }); watcher.on('error', (error) => { logger_1.default.error(`File watcher error for repository ${repository.name}:`, error); reject(error); }); // Timeout after 30 seconds setTimeout(() => { reject(new Error(`File watcher initialization timeout for repository: ${repository.name}`)); }, 30000); }); } catch (error) { logger_1.default.error(`Failed to start file watcher for repository ${repository.name}:`, error); throw error; } } /** * Stop watching a repository */ async stopWatching(watcherId) { const watcher = this.watchers.get(watcherId); const repository = this.repositories.get(watcherId); if (watcher) { logger_1.default.info(`Stopping file watcher for repository: ${repository?.name || watcherId}`); try { await watcher.close(); this.watchers.delete(watcherId); this.repositories.delete(watcherId); logger_1.default.info(`File watcher stopped for repository: ${repository?.name || watcherId}`); } catch (error) { logger_1.default.error(`Error stopping file watcher for repository ${repository?.name || watcherId}:`, error); throw error; } } } /** * Get watched files for a repository */ async getWatchedFiles(repositoryId) { const repository = this.repositories.get(repositoryId); if (!repository) { throw new Error(`Repository not found: ${repositoryId}`); } try { const patterns = repository.config.includePatterns; const files = []; for (const pattern of patterns) { const matches = await (0, glob_1.glob)(pattern, { cwd: repository.path, ignore: [ ...this.config.ignored, ...repository.config.excludePatterns ], absolute: false, dot: false }); files.push(...matches); } // Remove duplicates and sort return [...new Set(files)].sort(); } catch (error) { logger_1.default.error(`Error getting watched files for repository ${repository.name}:`, error); throw error; } } /** * Check if a file should be watched based on repository configuration */ shouldWatchFile(repositoryId, filePath) { const repository = this.repositories.get(repositoryId); if (!repository) { return false; } // Check include patterns const included = repository.config.includePatterns.some(pattern => (0, minimatch_1.minimatch)(filePath, pattern)); if (!included) { return false; } // Check exclude patterns const excluded = repository.config.excludePatterns.some(pattern => (0, minimatch_1.minimatch)(filePath, pattern)); return !excluded; } /** * Get file statistics */ async getFileStats(filePath) { try { const stats = await (0, promises_1.stat)(filePath); return { size: stats.size, mtime: stats.mtime, isDirectory: stats.isDirectory(), isFile: stats.isFile() }; } catch (error) { logger_1.default.error(`Error getting file stats for ${filePath}:`, error); throw error; } } /** * Cleanup all watchers */ async cleanup() { logger_1.default.info('Cleaning up File Watcher service'); const watcherIds = Array.from(this.watchers.keys()); for (const watcherId of watcherIds) { try { await this.stopWatching(watcherId); } catch (error) { logger_1.default.error(`Error stopping watcher ${watcherId} during cleanup:`, error); } } this.removeAllListeners(); logger_1.default.info('File Watcher service cleanup completed'); } // Private helper methods createWatchPatterns(repository) { const patterns = []; // Add include patterns patterns.push(...repository.config.includePatterns); // Add TTL file pattern for knowledge graph synchronization patterns.push('**/.module-knowledge.ttl'); return patterns; } setupWatcherEvents(watcher, repository) { // File added watcher.on('add', async (path, stats) => { try { const event = await this.createFileWatchEvent('add', path, repository, stats); this.handleFileEvent(event, repository); } catch (error) { logger_1.default.error(`Error handling file add event for ${path}:`, error); } }); // File changed watcher.on('change', async (path, stats) => { try { const event = await this.createFileWatchEvent('change', path, repository, stats); this.handleFileEvent(event, repository); } catch (error) { logger_1.default.error(`Error handling file change event for ${path}:`, error); } }); // File removed watcher.on('unlink', async (path) => { try { const event = await this.createFileWatchEvent('unlink', path, repository); this.handleFileEvent(event, repository); } catch (error) { logger_1.default.error(`Error handling file unlink event for ${path}:`, error); } }); // Directory added watcher.on('addDir', async (path, stats) => { try { const event = await this.createFileWatchEvent('addDir', path, repository, stats); this.emit('directoryAdded', { repository, event }); } catch (error) { logger_1.default.error(`Error handling directory add event for ${path}:`, error); } }); // Directory removed watcher.on('unlinkDir', async (path) => { try { const event = await this.createFileWatchEvent('unlinkDir', path, repository); this.emit('directoryRemoved', { repository, event }); } catch (error) { logger_1.default.error(`Error handling directory unlink event for ${path}:`, error); } }); // Watcher errors watcher.on('error', (error) => { logger_1.default.error(`File watcher error for repository ${repository.name}:`, error); this.emit('watcherError', { repository, error }); }); } async createFileWatchEvent(type, path, repository, stats) { const absolutePath = (0, path_1.join)(repository.path, path); let fileStats; if (stats) { fileStats = { size: stats.size, mtime: stats.mtime, isDirectory: stats.isDirectory(), isFile: stats.isFile() }; } else if (type !== 'unlink' && type !== 'unlinkDir') { try { fileStats = await this.getFileStats(absolutePath); } catch (error) { // File might have been deleted between event and stat call logger_1.default.debug(`Could not get stats for ${absolutePath}:`, error); } } const event = { type, path, timestamp: new Date() }; if (fileStats !== undefined) { event.stats = fileStats; } return event; } async handleFileEvent(event, repository) { const filePath = event.path; // Check if it's a TTL file if ((0, path_1.basename)(filePath) === '.module-knowledge.ttl') { await this.handleTTLFileChange(event, repository); return; } // Check if file should be watched if (!this.shouldWatchFile(repository.id, filePath)) { return; } // Determine file language const language = this.detectFileLanguage(filePath); if (!language || !repository.config.languages.includes(language)) { return; } logger_1.default.debug(`File ${event.type} detected: ${filePath} (${language})`); // Emit file change event this.emit('fileChanged', { repository, event, language }); } async handleTTLFileChange(event, repository) { logger_1.default.info(`TTL file ${event.type} detected: ${event.path}`); const ttlChange = { repositoryId: repository.id, filePath: event.path, changeType: event.type === 'unlink' ? 'deleted' : event.type === 'add' ? 'created' : 'modified', timestamp: event.timestamp }; if (event.stats !== undefined) { ttlChange.fileStats = event.stats; } this.emit('ttlFileChanged', ttlChange); } detectFileLanguage(filePath) { const ext = (0, path_1.extname)(filePath).toLowerCase(); const languageMap = { '.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.py': 'python', '.java': 'java', '.go': 'go', '.rs': 'rust', '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp' }; return languageMap[ext] || null; } setupErrorHandling() { // Handle uncaught errors from watchers this.on('watcherError', ({ repository, error }) => { logger_1.default.error(`Watcher error for repository ${repository.name}:`, error); // Attempt to restart the watcher setTimeout(async () => { try { logger_1.default.info(`Attempting to restart watcher for repository: ${repository.name}`); await this.startWatching(repository); } catch (restartError) { logger_1.default.error(`Failed to restart watcher for repository ${repository.name}:`, restartError); } }, 5000); }); } } exports.FileWatcher = FileWatcher; exports.default = FileWatcher; //# sourceMappingURL=FileWatcher.js.map