/**
* FileWatcher - File system monitoring for real-time code analysis
*
* Handles:
* - Watching source files for changes
* - Debouncing file change events
* - Filtering relevant files
* - Triggering code analysis on changes
*/
import { EventEmitter } from "events";
import chokidar, { FSWatcher } from "chokidar";
import { ConfigManager } from "../config/ConfigManager.js";
import { Logger } from "../utils/Logger.js";
export class FileWatcher extends EventEmitter {
private logger: Logger;
private configManager: ConfigManager;
private watcher: FSWatcher | null = null;
private watchedFiles = new Set<string>();
private debounceTimers = new Map<string, NodeJS.Timeout>();
constructor(configManager: ConfigManager) {
super();
this.logger = new Logger("FileWatcher");
this.configManager = configManager;
}
/**
* Initialize file watcher
*/
async initialize(): Promise<void> {
try {
this.logger.info("Initializing FileWatcher...");
const config = this.configManager.getConfig();
if (!config.watching.enabled) {
this.logger.info("File watching disabled in configuration");
return;
}
await this.startWatching();
this.logger.info("FileWatcher initialized successfully");
} catch (error) {
this.logger.error("Failed to initialize FileWatcher:", error);
throw error;
}
}
/**
* Start watching files
*/
private async startWatching(): Promise<void> {
const config = this.configManager.getConfig();
this.watcher = chokidar.watch(config.watching.paths, {
ignored: (path: string, stats?: any) => {
// Check if file should be ignored
for (const pattern of config.watching.ignored) {
if (path.includes(pattern)) {
return true;
}
}
// Only watch specific file types
if (stats?.isFile()) {
const ext = path.split('.').pop()?.toLowerCase();
return !['js', 'jsx', 'ts', 'tsx', 'vue', 'svelte'].includes(ext || '');
}
return false;
},
persistent: true,
ignoreInitial: false,
followSymlinks: true,
cwd: process.cwd(),
depth: 99,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 100
}
});
// Setup event listeners
this.watcher
.on('add', (path) => {
this.watchedFiles.add(path);
this.logger.debug(`File added: ${path}`);
this.handleFileChange(path, 'add');
})
.on('change', (path) => {
this.logger.debug(`File changed: ${path}`);
this.handleFileChange(path, 'change');
})
.on('unlink', (path) => {
this.watchedFiles.delete(path);
this.logger.debug(`File removed: ${path}`);
this.handleFileChange(path, 'unlink');
})
.on('addDir', (path) => {
this.logger.debug(`Directory added: ${path}`);
})
.on('unlinkDir', (path) => {
this.logger.debug(`Directory removed: ${path}`);
})
.on('error', (error) => {
this.logger.error('Watcher error:', error);
this.emit('error', error);
})
.on('ready', () => {
this.logger.info(`Watching ${this.watchedFiles.size} files`);
this.emit('ready');
});
}
/**
* Handle file change with debouncing
*/
private handleFileChange(filePath: string, eventType: string): void {
const config = this.configManager.getConfig();
// Clear existing timer for this file
const existingTimer = this.debounceTimers.get(filePath);
if (existingTimer) {
clearTimeout(existingTimer);
}
// Set new debounced timer
const timer = setTimeout(() => {
this.debounceTimers.delete(filePath);
// Emit file change event
this.emit('fileChanged', filePath, eventType);
// Emit specific event types
switch (eventType) {
case 'add':
this.emit('fileAdded', filePath);
break;
case 'change':
this.emit('fileModified', filePath);
break;
case 'unlink':
this.emit('fileRemoved', filePath);
break;
}
}, config.watching.debounceMs);
this.debounceTimers.set(filePath, timer);
}
/**
* Get count of watched files
*/
getWatchedCount(): number {
return this.watchedFiles.size;
}
/**
* Get list of watched files
*/
getWatchedFiles(): string[] {
return Array.from(this.watchedFiles);
}
/**
* Add path to watch
*/
async addPath(path: string): Promise<void> {
if (this.watcher) {
this.watcher.add(path);
this.logger.info(`Added path to watch: ${path}`);
}
}
/**
* Remove path from watch
*/
async removePath(path: string): Promise<void> {
if (this.watcher) {
await this.watcher.unwatch(path);
this.watchedFiles.delete(path);
this.logger.info(`Removed path from watch: ${path}`);
}
}
/**
* Shutdown file watcher
*/
async shutdown(): Promise<void> {
try {
this.logger.info("Shutting down FileWatcher...");
// Clear all debounce timers
for (const timer of this.debounceTimers.values()) {
clearTimeout(timer);
}
this.debounceTimers.clear();
// Close watcher
if (this.watcher) {
await this.watcher.close();
this.watcher = null;
}
this.watchedFiles.clear();
this.logger.info("FileWatcher shutdown complete");
} catch (error) {
this.logger.error("Error during FileWatcher shutdown:", error);
throw error;
}
}
}