@pimzino/spec-workflow-mcp
Version:
MCP server for spec-driven development workflow with real-time web dashboard
104 lines • 4.06 kB
JavaScript
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