/** * File Watcher - Monitors filesystem for changes using chokidar */ import * as chokidar from 'chokidar'; import path from 'path'; import { EventEmitter } from 'events'; export interface FileWatcherEvents { change: (filePath: string) => void; add: (filePath: string) => void; unlink: (filePath: string) => void; error: (error: Error) => void; ready: () => void; } export interface FileWatcher extends EventEmitter { start(): void; stop(): Promise; on(event: K, listener: FileWatcherEvents[K]): this; emit(event: K, ...args: Parameters): boolean; } const CODE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.md']; function isCodeFile(filePath: string): boolean { const ext = path.extname(filePath).toLowerCase(); return CODE_EXTENSIONS.includes(ext); } export function createFileWatcher(watchPath: string): FileWatcher { const emitter = new EventEmitter() as FileWatcher; let watcher: chokidar.FSWatcher | null = null; emitter.start = () => { const absolutePath = path.resolve(process.cwd(), watchPath); watcher = chokidar.watch(absolutePath, { ignored: (pathStr) => { // Ignore clearly non-code directories and heavy assets // Convert to relative for easier matching const relativePath = path.relative(process.cwd(), pathStr); const segments = relativePath.split(path.sep); const ignoreDirs = [ 'node_modules', '.git', '.next', '.turbo', 'dist', 'build', '.rigstate', 'coverage', 'tmp', 'temp', 'vendor', '.cache', 'public', 'artifacts', 'out', '.vercel', '.npm', '.agent', '.cursor', '.npm-cache', 'backups', 'docs', 'tests', 'tools', 'scripts', 'supabase' ]; if (segments.some(segment => ignoreDirs.includes(segment))) { return true; } // If it's a file, only watch if it's a code file // Directories don't have an extension in our convention for this check const isFile = !!path.extname(pathStr); if (isFile && !isCodeFile(pathStr)) { return true; } return false; }, persistent: true, ignoreInitial: true, ignorePermissionErrors: true, depth: 5, // Strongly reduced for major monorepos awaitWriteFinish: { stabilityThreshold: 500, // Increased for stability pollInterval: 200 }, usePolling: false, followSymlinks: false, // Prevent symlink loops and extra handles atomic: true }); watcher.on('change', (filePath: string) => { if (isCodeFile(filePath)) { emitter.emit('change', path.relative(process.cwd(), filePath)); } }); watcher.on('add', (filePath: string) => { if (isCodeFile(filePath)) { emitter.emit('add', path.relative(process.cwd(), filePath)); } }); watcher.on('unlink', (filePath: string) => { if (isCodeFile(filePath)) { emitter.emit('unlink', path.relative(process.cwd(), filePath)); } }); watcher.on('error', (error: any) => { emitter.emit('error', error); }); watcher.on('ready', () => { emitter.emit('ready'); }); }; emitter.stop = async () => { if (watcher) { await watcher.close(); watcher = null; } }; return emitter; }