UNPKG

@pimzino/spec-workflow-mcp

Version:

MCP server for spec-driven development workflow with real-time web dashboard

104 lines 4.06 kB
import { EventEmitter } from 'events'; import chokidar from 'chokidar'; import { PathUtils } from '../core/path-utils.js'; export class SpecWatcher extends EventEmitter { projectPath; parser; watcher; constructor(projectPath, parser) { super(); this.projectPath = projectPath; this.parser = parser; } async start() { const workflowRoot = PathUtils.getWorkflowRoot(this.projectPath); const specsPath = PathUtils.getSpecPath(this.projectPath, ''); const steeringPath = PathUtils.getSteeringPath(this.projectPath); // Watch for changes in specs and steering directories this.watcher = chokidar.watch([ `${specsPath}/**/*.md`, `${steeringPath}/*.md` ], { ignoreInitial: true, persistent: true, ignorePermissionErrors: true }); this.watcher.on('add', (filePath) => this.handleFileChange('created', filePath)); this.watcher.on('change', (filePath) => this.handleFileChange('updated', filePath)); this.watcher.on('unlink', (filePath) => this.handleFileChange('deleted', filePath)); // File watcher started for workflow directories } async stop() { if (this.watcher) { // Remove all listeners before closing to prevent memory leaks this.watcher.removeAllListeners(); await this.watcher.close(); this.watcher = undefined; // File watcher stopped } // Clean up EventEmitter listeners this.removeAllListeners(); } async handleFileChange(action, filePath) { try { const normalizedPath = filePath.replace(/\\/g, '/'); // Add small delay for file creation/updates to ensure file is fully written if (action === 'created' || action === 'updated') { await new Promise(resolve => setTimeout(resolve, 100)); } // Determine if this is a spec or steering change if (normalizedPath.includes('/specs/')) { await this.handleSpecChange(action, normalizedPath); } else if (normalizedPath.includes('/steering/')) { await this.handleSteeringChange(action, normalizedPath); } } catch (error) { // Error handling file change } } async handleSpecChange(action, filePath) { // Extract spec name from path like: /path/to/.spec-workflow/specs/user-auth/requirements.md const pathParts = filePath.split('/'); const specsIndex = pathParts.findIndex(part => part === 'specs'); if (specsIndex === -1 || specsIndex + 1 >= pathParts.length) return; const specName = pathParts[specsIndex + 1]; const document = pathParts[specsIndex + 2]?.replace('.md', ''); let specData = null; if (action !== 'deleted') { specData = await this.parser.getSpec(specName); } const event = { type: 'spec', action, name: specName, data: specData }; // Spec change detected this.emit('change', event); // Emit specific task update event if this was a tasks.md file if (document === 'tasks') { this.emit('task-update', { specName, action }); } } async handleSteeringChange(action, filePath) { // Extract document name from path like: /path/to/.spec-workflow/steering/tech.md const pathParts = filePath.split('/'); const document = pathParts[pathParts.length - 1]?.replace('.md', ''); const steeringStatus = await this.parser.getProjectSteeringStatus(); const event = { type: 'steering', action, name: document, steeringStatus }; // Steering change detected this.emit('steering-change', event); } } //# sourceMappingURL=watcher.js.map