@pimzino/spec-workflow-mcp
Version:
MCP server for spec-driven development workflow with real-time web dashboard
242 lines • 9.53 kB
JavaScript
import { readFile, readdir, access, stat } from 'fs/promises';
import { join } from 'path';
import { PathUtils } from '../core/path-utils.js';
import { parseTaskProgress } from '../core/task-parser.js';
export class SpecParser {
projectPath;
specsPath;
archiveSpecsPath;
steeringPath;
constructor(projectPath) {
this.projectPath = projectPath;
this.specsPath = PathUtils.getSpecPath(projectPath, '');
this.archiveSpecsPath = PathUtils.getArchiveSpecsPath(projectPath);
this.steeringPath = PathUtils.getSteeringPath(projectPath);
}
async getAllSpecs() {
try {
await access(this.specsPath);
const entries = await readdir(this.specsPath, { withFileTypes: true });
const specDirs = entries.filter(entry => entry.isDirectory());
const specs = [];
for (const dir of specDirs) {
const spec = await this.getSpec(dir.name);
if (spec) {
specs.push(spec);
}
}
return specs.sort((a, b) => a.name.localeCompare(b.name));
}
catch {
return [];
}
}
async getAllArchivedSpecs() {
try {
await access(this.archiveSpecsPath);
const entries = await readdir(this.archiveSpecsPath, { withFileTypes: true });
const specDirs = entries.filter(entry => entry.isDirectory());
const specs = [];
for (const dir of specDirs) {
const spec = await this.getArchivedSpec(dir.name);
if (spec) {
specs.push(spec);
}
}
return specs.sort((a, b) => a.name.localeCompare(b.name));
}
catch {
return [];
}
}
async getSpec(name) {
try {
const specDir = PathUtils.getSpecPath(this.projectPath, name);
await access(specDir);
const spec = {
name,
displayName: this.formatDisplayName(name),
createdAt: '',
lastModified: '',
phases: {
requirements: { exists: false },
design: { exists: false },
tasks: { exists: false },
implementation: { exists: false }
}
};
// Get directory stats
const dirStats = await stat(specDir);
spec.createdAt = dirStats.birthtime.toISOString();
spec.lastModified = dirStats.mtime.toISOString();
// Check each phase
const requirementsPath = join(specDir, 'requirements.md');
const designPath = join(specDir, 'design.md');
const tasksPath = join(specDir, 'tasks.md');
// Check requirements
try {
await access(requirementsPath);
spec.phases.requirements.exists = true;
const reqStats = await stat(requirementsPath);
spec.phases.requirements.lastModified = reqStats.mtime.toISOString();
// Update overall last modified if this is newer
if (reqStats.mtime > new Date(spec.lastModified)) {
spec.lastModified = reqStats.mtime.toISOString();
}
}
catch { }
// Check design
try {
await access(designPath);
spec.phases.design.exists = true;
const designStats = await stat(designPath);
spec.phases.design.lastModified = designStats.mtime.toISOString();
if (designStats.mtime > new Date(spec.lastModified)) {
spec.lastModified = designStats.mtime.toISOString();
}
}
catch { }
// Check tasks
try {
await access(tasksPath);
spec.phases.tasks.exists = true;
const tasksStats = await stat(tasksPath);
spec.phases.tasks.lastModified = tasksStats.mtime.toISOString();
if (tasksStats.mtime > new Date(spec.lastModified)) {
spec.lastModified = tasksStats.mtime.toISOString();
}
// Parse tasks to get progress
const tasksContent = await readFile(tasksPath, 'utf-8');
const taskProgress = parseTaskProgress(tasksContent);
spec.taskProgress = {
total: taskProgress.total,
completed: taskProgress.completed,
pending: taskProgress.pending
};
}
catch { }
// Implementation phase is always considered "exists" since it's ongoing manual work
spec.phases.implementation.exists = true;
return spec;
}
catch {
return null;
}
}
async getArchivedSpec(name) {
try {
const specDir = PathUtils.getArchiveSpecPath(this.projectPath, name);
await access(specDir);
const spec = {
name,
displayName: this.formatDisplayName(name),
createdAt: '',
lastModified: '',
phases: {
requirements: { exists: false },
design: { exists: false },
tasks: { exists: false },
implementation: { exists: false }
}
};
// Get directory stats
const dirStats = await stat(specDir);
spec.createdAt = dirStats.birthtime.toISOString();
spec.lastModified = dirStats.mtime.toISOString();
// Check each phase
const requirementsPath = join(specDir, 'requirements.md');
const designPath = join(specDir, 'design.md');
const tasksPath = join(specDir, 'tasks.md');
// Check requirements
try {
await access(requirementsPath);
spec.phases.requirements.exists = true;
const reqStats = await stat(requirementsPath);
spec.phases.requirements.lastModified = reqStats.mtime.toISOString();
// Update overall last modified if this is newer
if (reqStats.mtime > new Date(spec.lastModified)) {
spec.lastModified = reqStats.mtime.toISOString();
}
}
catch { }
// Check design
try {
await access(designPath);
spec.phases.design.exists = true;
const designStats = await stat(designPath);
spec.phases.design.lastModified = designStats.mtime.toISOString();
if (designStats.mtime > new Date(spec.lastModified)) {
spec.lastModified = designStats.mtime.toISOString();
}
}
catch { }
// Check tasks
try {
await access(tasksPath);
spec.phases.tasks.exists = true;
const tasksStats = await stat(tasksPath);
spec.phases.tasks.lastModified = tasksStats.mtime.toISOString();
if (tasksStats.mtime > new Date(spec.lastModified)) {
spec.lastModified = tasksStats.mtime.toISOString();
}
// Parse tasks to get progress
const tasksContent = await readFile(tasksPath, 'utf-8');
const taskProgress = parseTaskProgress(tasksContent);
spec.taskProgress = {
total: taskProgress.total,
completed: taskProgress.completed,
pending: taskProgress.pending
};
}
catch { }
// Implementation phase is always considered "exists" since it's ongoing manual work
spec.phases.implementation.exists = true;
return spec;
}
catch {
return null;
}
}
async getProjectSteeringStatus() {
const status = {
exists: false,
documents: {
product: false,
tech: false,
structure: false
}
};
try {
await access(this.steeringPath);
status.exists = true;
// Check each steering document
try {
await access(join(this.steeringPath, 'product.md'));
status.documents.product = true;
}
catch { }
try {
await access(join(this.steeringPath, 'tech.md'));
status.documents.tech = true;
}
catch { }
try {
await access(join(this.steeringPath, 'structure.md'));
status.documents.structure = true;
}
catch { }
// Get last modified time for steering directory
const steeringStats = await stat(this.steeringPath);
status.lastModified = steeringStats.mtime.toISOString();
}
catch { }
return status;
}
formatDisplayName(kebabCase) {
return kebabCase
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
}
//# sourceMappingURL=parser.js.map