Download src/utils/hooks/fileChangedWatcher.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 5.31 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/hooks/fileChangedWatcher.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/hooks/fileChangedWatcher.ts
-
curl -L -o fileChangedWatcher.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/hooks/fileChangedWatcher.ts
5.31 kB
| import chokidar, { type FSWatcher } from 'chokidar' | |
| import { isAbsolute, join } from 'path' | |
| import { registerCleanup } from '../cleanupRegistry.js' | |
| import { logForDebugging } from '../debug.js' | |
| import { errorMessage } from '../errors.js' | |
| import { | |
| executeCwdChangedHooks, | |
| executeFileChangedHooks, | |
| type HookOutsideReplResult, | |
| } from '../hooks.js' | |
| import { clearCwdEnvFiles } from '../sessionEnvironment.js' | |
| import { getHooksConfigFromSnapshot } from './hooksConfigSnapshot.js' | |
| let watcher: FSWatcher | null = null | |
| let currentCwd: string | |
| let dynamicWatchPaths: string[] = [] | |
| let dynamicWatchPathsSorted: string[] = [] | |
| let initialized = false | |
| let hasEnvHooks = false | |
| let notifyCallback: ((text: string, isError: boolean) => void) | null = null | |
| export function setEnvHookNotifier( | |
| cb: ((text: string, isError: boolean) => void) | null, | |
| ): void { | |
| notifyCallback = cb | |
| } | |
| export function initializeFileChangedWatcher(cwd: string): void { | |
| if (initialized) return | |
| initialized = true | |
| currentCwd = cwd | |
| const config = getHooksConfigFromSnapshot() | |
| hasEnvHooks = | |
| (config?.CwdChanged?.length ?? 0) > 0 || | |
| (config?.FileChanged?.length ?? 0) > 0 | |
| if (hasEnvHooks) { | |
| registerCleanup(async () => dispose()) | |
| } | |
| const paths = resolveWatchPaths(config) | |
| if (paths.length === 0) return | |
| startWatching(paths) | |
| } | |
| function resolveWatchPaths( | |
| config?: ReturnType<typeof getHooksConfigFromSnapshot>, | |
| ): string[] { | |
| const matchers = (config ?? getHooksConfigFromSnapshot())?.FileChanged ?? [] | |
| // Matcher field: filenames to watch in cwd, pipe-separated (e.g. ".envrc|.env") | |
| const staticPaths: string[] = [] | |
| for (const m of matchers) { | |
| if (!m.matcher) continue | |
| for (const name of m.matcher.split('|').map(s => s.trim())) { | |
| if (!name) continue | |
| staticPaths.push(isAbsolute(name) ? name : join(currentCwd, name)) | |
| } | |
| } | |
| // Combine static matcher paths with dynamic paths from hook output | |
| return [...new Set([...staticPaths, ...dynamicWatchPaths])] | |
| } | |
| function startWatching(paths: string[]): void { | |
| logForDebugging(`FileChanged: watching ${paths.length} paths`) | |
| watcher = chokidar.watch(paths, { | |
| persistent: true, | |
| ignoreInitial: true, | |
| awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 200 }, | |
| ignorePermissionErrors: true, | |
| }) | |
| watcher.on('change', p => handleFileEvent(p, 'change')) | |
| watcher.on('add', p => handleFileEvent(p, 'add')) | |
| watcher.on('unlink', p => handleFileEvent(p, 'unlink')) | |
| } | |
| function handleFileEvent( | |
| path: string, | |
| event: 'change' | 'add' | 'unlink', | |
| ): void { | |
| logForDebugging(`FileChanged: ${event} ${path}`) | |
| void executeFileChangedHooks(path, event) | |
| .then(({ results, watchPaths, systemMessages }) => { | |
| if (watchPaths.length > 0) { | |
| updateWatchPaths(watchPaths) | |
| } | |
| for (const msg of systemMessages) { | |
| notifyCallback?.(msg, false) | |
| } | |
| for (const r of results) { | |
| if (!r.succeeded && r.output) { | |
| notifyCallback?.(r.output, true) | |
| } | |
| } | |
| }) | |
| .catch(e => { | |
| const msg = errorMessage(e) | |
| logForDebugging(`FileChanged hook failed: ${msg}`, { | |
| level: 'error', | |
| }) | |
| notifyCallback?.(msg, true) | |
| }) | |
| } | |
| export function updateWatchPaths(paths: string[]): void { | |
| if (!initialized) return | |
| const sorted = paths.slice().sort() | |
| if ( | |
| sorted.length === dynamicWatchPathsSorted.length && | |
| sorted.every((p, i) => p === dynamicWatchPathsSorted[i]) | |
| ) { | |
| return | |
| } | |
| dynamicWatchPaths = paths | |
| dynamicWatchPathsSorted = sorted | |
| restartWatching() | |
| } | |
| function restartWatching(): void { | |
| if (watcher) { | |
| void watcher.close() | |
| watcher = null | |
| } | |
| const paths = resolveWatchPaths() | |
| if (paths.length > 0) { | |
| startWatching(paths) | |
| } | |
| } | |
| export async function onCwdChangedForHooks( | |
| oldCwd: string, | |
| newCwd: string, | |
| ): Promise<void> { | |
| if (oldCwd === newCwd) return | |
| // Re-evaluate from the current snapshot so mid-session hook changes are picked up | |
| const config = getHooksConfigFromSnapshot() | |
| const currentHasEnvHooks = | |
| (config?.CwdChanged?.length ?? 0) > 0 || | |
| (config?.FileChanged?.length ?? 0) > 0 | |
| if (!currentHasEnvHooks) return | |
| currentCwd = newCwd | |
| await clearCwdEnvFiles() | |
| const hookResult = await executeCwdChangedHooks(oldCwd, newCwd).catch(e => { | |
| const msg = errorMessage(e) | |
| logForDebugging(`CwdChanged hook failed: ${msg}`, { | |
| level: 'error', | |
| }) | |
| notifyCallback?.(msg, true) | |
| return { | |
| results: [] as HookOutsideReplResult[], | |
| watchPaths: [] as string[], | |
| systemMessages: [] as string[], | |
| } | |
| }) | |
| dynamicWatchPaths = hookResult.watchPaths | |
| dynamicWatchPathsSorted = hookResult.watchPaths.slice().sort() | |
| for (const msg of hookResult.systemMessages) { | |
| notifyCallback?.(msg, false) | |
| } | |
| for (const r of hookResult.results) { | |
| if (!r.succeeded && r.output) { | |
| notifyCallback?.(r.output, true) | |
| } | |
| } | |
| // Re-resolve matcher paths against the new cwd | |
| if (initialized) { | |
| restartWatching() | |
| } | |
| } | |
| function dispose(): void { | |
| if (watcher) { | |
| void watcher.close() | |
| watcher = null | |
| } | |
| dynamicWatchPaths = [] | |
| dynamicWatchPathsSorted = [] | |
| initialized = false | |
| hasEnvHooks = false | |
| notifyCallback = null | |
| } | |
| export function resetFileChangedWatcherForTesting(): void { | |
| dispose() | |
| } | |