aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
203 lines • 5.48 kB
JavaScript
/**
* Watch Service
*
* Monitors file system for changes and triggers workflow processing.
* Uses chokidar for efficient file watching with debouncing.
*/
import chokidar from 'chokidar';
import * as path from 'path';
/**
* File watching service with debouncing
*/
export class WatchService {
watcher = null;
callbacks = [];
debounceTimers = new Map();
debounceMs = 500;
stats;
isRunning = false;
constructor() {
this.stats = {
filesWatched: 0,
eventsProcessed: 0,
errors: 0,
startTime: new Date()
};
}
/**
* Start watching files
*/
async start(patterns, config) {
if (this.isRunning) {
throw new Error('Watch service is already running');
}
this.debounceMs = config.debounce;
// Configure chokidar
this.watcher = chokidar.watch(patterns, {
ignored: config.ignorePatterns || [],
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 200,
pollInterval: 100
}
});
// Set up event handlers
this.watcher.on('add', (path) => this.handleEvent('add', path));
this.watcher.on('change', (path) => this.handleEvent('change', path));
this.watcher.on('unlink', (path) => this.handleEvent('unlink', path));
this.watcher.on('ready', () => {
const watched = this.watcher?.getWatched() || {};
this.stats.filesWatched = Object.values(watched).reduce((sum, files) => sum + files.length, 0);
});
this.watcher.on('error', (error) => {
this.stats.errors++;
console.error('Watch error:', error);
});
this.isRunning = true;
// Wait for ready
await new Promise((resolve) => {
if (this.watcher) {
this.watcher.on('ready', () => resolve());
}
else {
resolve();
}
});
}
/**
* Stop watching
*/
async stop() {
if (!this.isRunning) {
return;
}
// Clear pending debounce timers
for (const timer of this.debounceTimers.values()) {
clearTimeout(timer);
}
this.debounceTimers.clear();
// Close watcher
if (this.watcher) {
await this.watcher.close();
this.watcher = null;
}
this.isRunning = false;
}
/**
* Register callback for file changes
*/
onFileChange(callback) {
this.callbacks.push(callback);
}
/**
* Remove callback
*/
removeCallback(callback) {
const index = this.callbacks.indexOf(callback);
if (index > -1) {
this.callbacks.splice(index, 1);
}
}
/**
* Set debounce time
*/
debounce(ms) {
if (ms < 0) {
throw new Error('Debounce time must be >= 0');
}
this.debounceMs = ms;
}
/**
* Get watch statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Check if service is running
*/
running() {
return this.isRunning;
}
/**
* Get list of watched files
*/
getWatchedFiles() {
if (!this.watcher) {
return [];
}
const watched = this.watcher.getWatched();
const files = [];
for (const [dir, fileList] of Object.entries(watched)) {
for (const file of fileList) {
files.push(path.join(dir, file));
}
}
return files;
}
// Private methods
handleEvent(type, path) {
// Clear existing timer for this path
const existingTimer = this.debounceTimers.get(path);
if (existingTimer) {
clearTimeout(existingTimer);
}
// Set new timer
const timer = setTimeout(() => {
this.processEvent(type, path);
this.debounceTimers.delete(path);
}, this.debounceMs);
this.debounceTimers.set(path, timer);
}
async processEvent(type, path) {
const event = {
type,
path,
timestamp: new Date()
};
this.stats.eventsProcessed++;
this.stats.lastEvent = event.timestamp;
// Call all registered callbacks
for (const callback of this.callbacks) {
try {
await callback(event);
}
catch (error) {
this.stats.errors++;
console.error(`Error processing ${path}:`, error);
}
}
}
/**
* Add pattern to watch
*/
addPattern(pattern) {
if (!this.watcher) {
throw new Error('Watch service is not running');
}
this.watcher.add(pattern);
}
/**
* Remove pattern from watch
*/
removePattern(pattern) {
if (!this.watcher) {
throw new Error('Watch service is not running');
}
this.watcher.unwatch(pattern);
}
/**
* Reset statistics
*/
resetStats() {
this.stats = {
filesWatched: this.stats.filesWatched,
eventsProcessed: 0,
errors: 0,
startTime: new Date(),
lastEvent: undefined
};
}
}
//# sourceMappingURL=watch-service.js.map