@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
388 lines • 14.3 kB
JavaScript
;
/**
* File Watcher Service
* Handles real-time file system monitoring for code changes and TTL file synchronization
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileWatcher = void 0;
const chokidar_1 = require("chokidar");
const events_1 = require("events");
const path_1 = require("path");
const promises_1 = require("fs/promises");
const glob_1 = require("glob");
const minimatch_1 = require("minimatch");
const logger_1 = __importDefault(require("../../../utils/logger"));
class FileWatcher extends events_1.EventEmitter {
watchers = new Map();
repositories = new Map();
config;
constructor(config = {}) {
super();
this.config = {
ignored: [
'**/node_modules/**',
'**/.git/**',
'**/dist/**',
'**/build/**',
'**/.next/**',
'**/coverage/**',
'**/*.log',
'**/.DS_Store',
'**/Thumbs.db'
],
persistent: true,
ignoreInitial: true,
followSymlinks: false,
depth: 10,
awaitWriteFinish: {
stabilityThreshold: 100,
pollInterval: 10
},
usePolling: false,
interval: 100,
binaryInterval: 300,
...config
};
}
/**
* Initialize file watcher service
*/
async initialize() {
logger_1.default.info('Initializing File Watcher service');
try {
// Set up global error handling
this.setupErrorHandling();
logger_1.default.info('File Watcher service initialized');
}
catch (error) {
logger_1.default.error('Failed to initialize File Watcher service:', error);
throw error;
}
}
/**
* Start watching a repository
*/
async startWatching(repository) {
const watcherId = repository.id;
logger_1.default.info(`Starting file watcher for repository: ${repository.name} at ${repository.path}`);
try {
// Stop existing watcher if any
await this.stopWatching(watcherId);
// Create patterns for watching
const watchPatterns = this.createWatchPatterns(repository);
// Initialize chokidar watcher
const watcher = (0, chokidar_1.watch)(watchPatterns, {
...this.config,
cwd: repository.path,
ignored: [
...this.config.ignored,
...repository.config.excludePatterns
]
});
// Set up event handlers
this.setupWatcherEvents(watcher, repository);
// Store watcher and repository
this.watchers.set(watcherId, watcher);
this.repositories.set(watcherId, repository);
// Wait for watcher to be ready
await new Promise((resolve, reject) => {
watcher.on('ready', () => {
logger_1.default.info(`File watcher ready for repository: ${repository.name}`);
resolve();
});
watcher.on('error', (error) => {
logger_1.default.error(`File watcher error for repository ${repository.name}:`, error);
reject(error);
});
// Timeout after 30 seconds
setTimeout(() => {
reject(new Error(`File watcher initialization timeout for repository: ${repository.name}`));
}, 30000);
});
}
catch (error) {
logger_1.default.error(`Failed to start file watcher for repository ${repository.name}:`, error);
throw error;
}
}
/**
* Stop watching a repository
*/
async stopWatching(watcherId) {
const watcher = this.watchers.get(watcherId);
const repository = this.repositories.get(watcherId);
if (watcher) {
logger_1.default.info(`Stopping file watcher for repository: ${repository?.name || watcherId}`);
try {
await watcher.close();
this.watchers.delete(watcherId);
this.repositories.delete(watcherId);
logger_1.default.info(`File watcher stopped for repository: ${repository?.name || watcherId}`);
}
catch (error) {
logger_1.default.error(`Error stopping file watcher for repository ${repository?.name || watcherId}:`, error);
throw error;
}
}
}
/**
* Get watched files for a repository
*/
async getWatchedFiles(repositoryId) {
const repository = this.repositories.get(repositoryId);
if (!repository) {
throw new Error(`Repository not found: ${repositoryId}`);
}
try {
const patterns = repository.config.includePatterns;
const files = [];
for (const pattern of patterns) {
const matches = await (0, glob_1.glob)(pattern, {
cwd: repository.path,
ignore: [
...this.config.ignored,
...repository.config.excludePatterns
],
absolute: false,
dot: false
});
files.push(...matches);
}
// Remove duplicates and sort
return [...new Set(files)].sort();
}
catch (error) {
logger_1.default.error(`Error getting watched files for repository ${repository.name}:`, error);
throw error;
}
}
/**
* Check if a file should be watched based on repository configuration
*/
shouldWatchFile(repositoryId, filePath) {
const repository = this.repositories.get(repositoryId);
if (!repository) {
return false;
}
// Check include patterns
const included = repository.config.includePatterns.some(pattern => (0, minimatch_1.minimatch)(filePath, pattern));
if (!included) {
return false;
}
// Check exclude patterns
const excluded = repository.config.excludePatterns.some(pattern => (0, minimatch_1.minimatch)(filePath, pattern));
return !excluded;
}
/**
* Get file statistics
*/
async getFileStats(filePath) {
try {
const stats = await (0, promises_1.stat)(filePath);
return {
size: stats.size,
mtime: stats.mtime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile()
};
}
catch (error) {
logger_1.default.error(`Error getting file stats for ${filePath}:`, error);
throw error;
}
}
/**
* Cleanup all watchers
*/
async cleanup() {
logger_1.default.info('Cleaning up File Watcher service');
const watcherIds = Array.from(this.watchers.keys());
for (const watcherId of watcherIds) {
try {
await this.stopWatching(watcherId);
}
catch (error) {
logger_1.default.error(`Error stopping watcher ${watcherId} during cleanup:`, error);
}
}
this.removeAllListeners();
logger_1.default.info('File Watcher service cleanup completed');
}
// Private helper methods
createWatchPatterns(repository) {
const patterns = [];
// Add include patterns
patterns.push(...repository.config.includePatterns);
// Add TTL file pattern for knowledge graph synchronization
patterns.push('**/.module-knowledge.ttl');
return patterns;
}
setupWatcherEvents(watcher, repository) {
// File added
watcher.on('add', async (path, stats) => {
try {
const event = await this.createFileWatchEvent('add', path, repository, stats);
this.handleFileEvent(event, repository);
}
catch (error) {
logger_1.default.error(`Error handling file add event for ${path}:`, error);
}
});
// File changed
watcher.on('change', async (path, stats) => {
try {
const event = await this.createFileWatchEvent('change', path, repository, stats);
this.handleFileEvent(event, repository);
}
catch (error) {
logger_1.default.error(`Error handling file change event for ${path}:`, error);
}
});
// File removed
watcher.on('unlink', async (path) => {
try {
const event = await this.createFileWatchEvent('unlink', path, repository);
this.handleFileEvent(event, repository);
}
catch (error) {
logger_1.default.error(`Error handling file unlink event for ${path}:`, error);
}
});
// Directory added
watcher.on('addDir', async (path, stats) => {
try {
const event = await this.createFileWatchEvent('addDir', path, repository, stats);
this.emit('directoryAdded', { repository, event });
}
catch (error) {
logger_1.default.error(`Error handling directory add event for ${path}:`, error);
}
});
// Directory removed
watcher.on('unlinkDir', async (path) => {
try {
const event = await this.createFileWatchEvent('unlinkDir', path, repository);
this.emit('directoryRemoved', { repository, event });
}
catch (error) {
logger_1.default.error(`Error handling directory unlink event for ${path}:`, error);
}
});
// Watcher errors
watcher.on('error', (error) => {
logger_1.default.error(`File watcher error for repository ${repository.name}:`, error);
this.emit('watcherError', { repository, error });
});
}
async createFileWatchEvent(type, path, repository, stats) {
const absolutePath = (0, path_1.join)(repository.path, path);
let fileStats;
if (stats) {
fileStats = {
size: stats.size,
mtime: stats.mtime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile()
};
}
else if (type !== 'unlink' && type !== 'unlinkDir') {
try {
fileStats = await this.getFileStats(absolutePath);
}
catch (error) {
// File might have been deleted between event and stat call
logger_1.default.debug(`Could not get stats for ${absolutePath}:`, error);
}
}
const event = {
type,
path,
timestamp: new Date()
};
if (fileStats !== undefined) {
event.stats = fileStats;
}
return event;
}
async handleFileEvent(event, repository) {
const filePath = event.path;
// Check if it's a TTL file
if ((0, path_1.basename)(filePath) === '.module-knowledge.ttl') {
await this.handleTTLFileChange(event, repository);
return;
}
// Check if file should be watched
if (!this.shouldWatchFile(repository.id, filePath)) {
return;
}
// Determine file language
const language = this.detectFileLanguage(filePath);
if (!language || !repository.config.languages.includes(language)) {
return;
}
logger_1.default.debug(`File ${event.type} detected: ${filePath} (${language})`);
// Emit file change event
this.emit('fileChanged', {
repository,
event,
language
});
}
async handleTTLFileChange(event, repository) {
logger_1.default.info(`TTL file ${event.type} detected: ${event.path}`);
const ttlChange = {
repositoryId: repository.id,
filePath: event.path,
changeType: event.type === 'unlink' ? 'deleted' :
event.type === 'add' ? 'created' : 'modified',
timestamp: event.timestamp
};
if (event.stats !== undefined) {
ttlChange.fileStats = event.stats;
}
this.emit('ttlFileChanged', ttlChange);
}
detectFileLanguage(filePath) {
const ext = (0, path_1.extname)(filePath).toLowerCase();
const languageMap = {
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.mjs': 'javascript',
'.py': 'python',
'.java': 'java',
'.go': 'go',
'.rs': 'rust',
'.cpp': 'cpp',
'.cc': 'cpp',
'.cxx': 'cpp',
'.c': 'cpp',
'.h': 'cpp',
'.hpp': 'cpp'
};
return languageMap[ext] || null;
}
setupErrorHandling() {
// Handle uncaught errors from watchers
this.on('watcherError', ({ repository, error }) => {
logger_1.default.error(`Watcher error for repository ${repository.name}:`, error);
// Attempt to restart the watcher
setTimeout(async () => {
try {
logger_1.default.info(`Attempting to restart watcher for repository: ${repository.name}`);
await this.startWatching(repository);
}
catch (restartError) {
logger_1.default.error(`Failed to restart watcher for repository ${repository.name}:`, restartError);
}
}, 5000);
});
}
}
exports.FileWatcher = FileWatcher;
exports.default = FileWatcher;
//# sourceMappingURL=FileWatcher.js.map