import { watch } from "node:fs" import type { WorkflowDef } from "./types" export class FileWatcher { private watchers: Map> = new Map() private timers: Map> = new Map() watch(def: WorkflowDef, worktree: string, onTrigger: () => void): void { if (def.trigger !== "file_change" || !def.paths) return const key = def.name this.unwatch(key) const patterns = def.paths.map((p) => { let escaped = p .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex metachars .replace(/\*\*/g, "\x00") // globstar placeholder .replace(/\*/g, "[^/]*") // * → any non-slash .replace(/\x00/g, ".*") // globstar placeholder → .* .replace(/\?/g, "[^/]") // ? → single char return new RegExp("^" + escaped + "$") }) const debounceMs = (def.debounce ?? 60) * 1000 const watcher = watch(worktree, { recursive: true, persistent: false }, (_event, filename) => { if (!filename) return if (patterns.some((r) => r.test(filename))) { const existing = this.timers.get(key) if (existing) clearTimeout(existing) this.timers.set(key, setTimeout(onTrigger, debounceMs)) } }) this.watchers.set(key, watcher) } unwatch(key: string): void { const w = this.watchers.get(key) if (w) { w.close(); this.watchers.delete(key) } const t = this.timers.get(key) if (t) { clearTimeout(t); this.timers.delete(key) } } stopAll(): void { for (const [k] of this.watchers) this.unwatch(k) } }