@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
JavaScript
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