UNPKG

@felixgeelhaar/cclint

Version:

Catch CLAUDE.md drift before Claude misbehaves. Lints CLAUDE.md, skills, subagents, and hooks for Claude Code projects.

129 lines 3.97 kB
import chokidar, {} from 'chokidar'; import { EventEmitter } from 'events'; /** * Cross-platform file watcher with debouncing support. * Uses chokidar for reliable file system watching. */ export class FileWatcher extends EventEmitter { watcher = null; options; debounceTimers = new Map(); isReady = false; constructor(options) { super(); this.options = { patterns: options.patterns, recursive: options.recursive ?? true, debounceMs: options.debounceMs ?? 300, ignored: options.ignored ?? [ '**/node_modules/**', '**/.git/**', '**/dist/**', '**/coverage/**', ], cwd: options.cwd ?? process.cwd(), }; } /** * Start watching for file changes */ async start() { if (this.watcher) { return; } return new Promise((resolve, reject) => { const watchOptions = { ignored: this.options.ignored, persistent: true, ignoreInitial: true, cwd: this.options.cwd, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50, }, }; // Only set depth if not recursive (0 means current dir only) if (!this.options.recursive) { watchOptions.depth = 0; } this.watcher = chokidar.watch(this.options.patterns, watchOptions); this.watcher.on('ready', () => { this.isReady = true; this.emit('ready'); resolve(); }); this.watcher.on('error', (err) => { const error = err instanceof Error ? err : new Error(String(err)); this.emit('error', error); if (!this.isReady) { reject(error); } }); this.watcher.on('add', (path) => { this.handleChange('add', path); }); this.watcher.on('change', (path) => { this.handleChange('change', path); }); this.watcher.on('unlink', (path) => { this.handleChange('unlink', path); }); }); } /** * Stop watching for file changes */ async stop() { // Clear all pending debounce timers for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear(); if (this.watcher) { await this.watcher.close(); this.watcher = null; this.isReady = false; } } /** * Get the list of watched paths */ getWatchedPaths() { if (!this.watcher) { return []; } const watched = this.watcher.getWatched(); const paths = []; for (const [dir, files] of Object.entries(watched)) { for (const file of files) { paths.push(dir === '.' ? file : `${dir}/${file}`); } } return paths; } /** * Check if the watcher is ready */ isWatcherReady() { return this.isReady; } handleChange(type, path) { // Cancel any pending debounce for this file const existingTimer = this.debounceTimers.get(path); if (existingTimer) { clearTimeout(existingTimer); } // Set up debounced emission const timer = setTimeout(() => { this.debounceTimers.delete(path); const event = { type, path, timestamp: new Date(), }; this.emit('change', event); }, this.options.debounceMs); this.debounceTimers.set(path, timer); } } //# sourceMappingURL=FileWatcher.js.map