/**
* File Watcher for Auto-Reload
*
* Task 1.2: File Watcher for Auto-Reload
* Constraint: Manual reload signals → Automatic file change detection
*
* Witness Outcome: Tool file saved → auto-reload triggered → new code active within 100ms
*
* Acceptance Criteria:
* - [ ] File watcher monitors tool class files
* - [ ] File change triggers reload_tool()
* - [ ] Debouncing prevents reload spam (500ms)
* - [ ] Reload success/failure logged
*/
import chokidar from 'chokidar';
import type { ToolRegistry } from './tool-registry.js';
import type { InfrastructureRegistry } from './infrastructure-registry.js';
import { DevelopmentMetrics } from './development-metrics.js';
/**
* Reload handler interface
*/
interface ReloadHandler {
reload(name: string): Promise<void>;
}
export class FileWatcher {
private watcher: chokidar.FSWatcher | null = null;
private debounceTimers: Map<string, NodeJS.Timeout> = new Map();
private readonly debounceDelay = 500; // 500ms debounce
private metrics = new DevelopmentMetrics();
constructor(
private toolRegistry?: ToolRegistry,
private infraRegistry?: InfrastructureRegistry
) {}
/**
* Start watching tool files for changes
*/
start(watchPath: string = './src/tools/**/*.ts'): void {
console.error(`[FileWatcher] Starting file watcher: ${watchPath}`);
this.watcher = chokidar.watch(watchPath, {
ignored: /(^|[\/\\])\../, // Ignore dotfiles
persistent: true,
ignoreInitial: true, // Don't trigger on initial scan
});
this.watcher
.on('change', (path) => this.onFileChanged(path))
.on('error', (error) => console.error(`[FileWatcher] Error:`, error));
console.error('[FileWatcher] Watching for tool file changes');
}
/**
* Stop watching files
*/
async stop(): Promise<void> {
if (this.watcher) {
await this.watcher.close();
this.watcher = null;
console.error('[FileWatcher] Stopped watching files');
}
}
/**
* Handle file change event
*/
private onFileChanged(filePath: string): void {
console.error(`[FileWatcher] File changed: ${filePath}`);
// Determine if this is a tool or infrastructure file
const toolName = this.extractToolName(filePath);
const infraName = this.extractInfraName(filePath);
if (toolName) {
console.error(`[FileWatcher] Tool file changed: ${toolName}`);
this.metrics.onCodeChange(toolName);
this.debouncedReload(toolName, 'tool');
} else if (infraName) {
console.error(`[FileWatcher] Infrastructure file changed: ${infraName}`);
this.metrics.onCodeChange(infraName);
this.debouncedReload(infraName, 'infra');
} else {
console.error(`[FileWatcher] Could not classify file: ${filePath}`);
}
}
/**
* Debounced reload (500ms delay)
*
* Prevents reload spam when multiple file saves occur rapidly
*/
private debouncedReload(name: string, type: 'tool' | 'infra'): void {
const key = `${type}:${name}`;
// Cancel existing timer
const existingTimer = this.debounceTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
// Create new timer
const timer = setTimeout(() => {
this.executeReload(name, type);
this.debounceTimers.delete(key);
}, this.debounceDelay);
this.debounceTimers.set(key, timer);
console.error(`[FileWatcher] Debouncing reload for: ${type}:${name} (${this.debounceDelay}ms)`);
}
/**
* Execute the actual reload
*/
private async executeReload(name: string, type: 'tool' | 'infra'): Promise<void> {
try {
if (type === 'tool' && this.toolRegistry) {
await this.toolRegistry.reloadTool(name);
this.metrics.onReloadComplete(name);
console.error(`[FileWatcher] ✓ Tool reload successful: ${name}`);
} else if (type === 'infra' && this.infraRegistry) {
await this.infraRegistry.reload(name);
this.metrics.onReloadComplete(name);
console.error(`[FileWatcher] ✓ Infrastructure reload successful: ${name}`);
} else {
console.error(`[FileWatcher] No registry available for ${type}:${name}`);
}
} catch (error) {
console.error(`[FileWatcher] ✗ Reload failed: ${type}:${name}`, error);
}
}
/**
* Extract tool name from file path
*
* Example: ./dist/tools/example-tool.js → example-tool
*/
private extractToolName(filePath: string): string | null {
const match = filePath.match(/\/tools\/(.+)\.js$/);
return match ? match[1] : null;
}
/**
* Extract infrastructure name from file path
*
* Example: ./dist/core/conversation-manager.js → conversation-manager
*/
private extractInfraName(filePath: string): string | null {
const match = filePath.match(/\/core\/(conversation-manager|alignment-detector)\.js$/);
return match ? match[1] : null;
}
}