Project Files
src / watcher / fileWatcher.ts
/**
* FileWatcher — monitors the playbook directory for MD/TXT changes.
* Uses hash comparison to avoid re-indexing unchanged files.
*/
import chokidar, { type FSWatcher } from "chokidar";
import path from "node:path";
export interface FileWatcherCallbacks {
onFileAdded: (filePath: string) => Promise<void>;
onFileChanged: (filePath: string) => Promise<void>;
onFileDeleted: (filePath: string) => Promise<void>;
onReady?: () => void;
}
const SUPPORTED_EXTENSIONS = new Set([".md", ".txt"]);
function isSupported(filePath: string): boolean {
return SUPPORTED_EXTENSIONS.has(path.extname(filePath).toLowerCase());
}
export class FileWatcher {
private watcher: FSWatcher | null = null;
private directory: string;
private callbacks: FileWatcherCallbacks;
constructor(directory: string, callbacks: FileWatcherCallbacks) {
this.directory = directory;
this.callbacks = callbacks;
}
start(): Promise<void> {
return new Promise((resolve) => {
this.watcher = chokidar.watch(this.directory, {
persistent: true,
ignoreInitial: true, // initial scan is handled by IndexManager at startup
awaitWriteFinish: {
stabilityThreshold: 1500,
pollInterval: 100,
},
ignored: (filePath: string) => {
const basename = path.basename(filePath);
if (basename.startsWith(".")) return true;
if (filePath.includes("node_modules")) return true;
// Accept directories (no extension), reject unsupported file types
const ext = path.extname(filePath);
if (ext && !SUPPORTED_EXTENSIONS.has(ext.toLowerCase())) return true;
return false;
},
});
this.watcher
.on("add", async (filePath) => {
if (!isSupported(filePath)) return;
try {
await this.callbacks.onFileAdded(filePath);
} catch (err) {
console.error(`[FileWatcher] onFileAdded error for ${filePath}:`, err);
}
})
.on("change", async (filePath) => {
if (!isSupported(filePath)) return;
try {
await this.callbacks.onFileChanged(filePath);
} catch (err) {
console.error(`[FileWatcher] onFileChanged error for ${filePath}:`, err);
}
})
.on("unlink", async (filePath) => {
if (!isSupported(filePath)) return;
try {
await this.callbacks.onFileDeleted(filePath);
} catch (err) {
console.error(`[FileWatcher] onFileDeleted error for ${filePath}:`, err);
}
})
.on("error", (err) => {
console.error("[FileWatcher] error:", err);
})
.on("ready", () => {
console.log("[FileWatcher] ready, watching:", this.directory);
this.callbacks.onReady?.();
resolve();
});
});
}
async stop(): Promise<void> {
if (this.watcher) {
await this.watcher.close();
this.watcher = null;
}
}
}