@pimzino/claude-code-spec-workflow
Version:
Automated workflows for Claude Code. Includes spec-driven development (Requirements → Design → Tasks → Implementation) with intelligent orchestration, optional steering documents and streamlined bug fix workflow (Report → Analyze → Fix → Verify). We have
217 lines • 8.69 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DashboardServer = void 0;
const fastify_1 = __importDefault(require("fastify"));
const static_1 = __importDefault(require("@fastify/static"));
const websocket_1 = __importDefault(require("@fastify/websocket"));
const path_1 = require("path");
const promises_1 = require("fs/promises");
const watcher_1 = require("./watcher");
const parser_1 = require("./parser");
const open_1 = __importDefault(require("open"));
const utils_1 = require("../utils");
const git_1 = require("../git");
const logger_1 = require("./logger");
class DashboardServer {
constructor(options) {
this.clients = new Set();
this.options = options;
this.parser = new parser_1.SpecParser(options.projectPath);
this.watcher = new watcher_1.SpecWatcher(options.projectPath, this.parser);
this.app = (0, fastify_1.default)({ logger: false });
}
async start() {
// Register plugins
await this.app.register(static_1.default, {
root: (0, path_1.join)(__dirname, 'public'),
prefix: '/',
});
await this.app.register(websocket_1.default);
// WebSocket endpoint for real-time updates
const self = this;
this.app.register(async function (fastify) {
fastify.get('/ws', { websocket: true }, (connection) => {
const socket = connection.socket;
(0, logger_1.debug)('WebSocket client connected');
// Add client to set
self.clients.add(socket);
// Send initial state
Promise.all([
self.parser.getAllSpecs(),
self.parser.getAllBugs()
])
.then(([specs, bugs]) => {
socket.send(JSON.stringify({
type: 'initial',
data: { specs, bugs },
}));
})
.catch((error) => {
console.error('Error getting initial data:', error);
});
// Handle client disconnect
socket.on('close', () => {
self.clients.delete(socket);
});
socket.on('error', (error) => {
console.error('WebSocket error:', error);
self.clients.delete(socket);
});
});
});
// Serve Claude icon as favicon
this.app.get('/favicon.ico', async (request, reply) => {
return reply.sendFile('claude-icon.svg');
});
// API endpoints
this.app.get('/api/test', async () => {
return { message: 'Test endpoint works!' };
});
this.app.get('/api/specs', async () => {
const specs = await this.parser.getAllSpecs();
return specs;
});
this.app.get('/api/bugs', async () => {
const bugs = await this.parser.getAllBugs();
return bugs;
});
this.app.get('/api/info', async () => {
const projectName = this.options.projectPath.split('/').pop() || 'Project';
const gitInfo = await git_1.GitUtils.getGitInfo(this.options.projectPath);
const steeringStatus = await this.parser.getProjectSteeringStatus();
return {
projectName,
steering: steeringStatus,
...gitInfo
};
});
this.app.get('/api/specs/:name', async (request, reply) => {
const { name } = request.params;
const spec = await this.parser.getSpec(name);
if (!spec) {
reply.code(404).send({ error: 'Spec not found' });
}
return spec;
});
this.app.get('/api/bugs/:name', async (request, reply) => {
const { name } = request.params;
const bug = await this.parser.getBug(name);
if (!bug) {
reply.code(404).send({ error: 'Bug not found' });
}
return bug;
});
// Get raw markdown content
this.app.get('/api/specs/:name/:document', async (request, reply) => {
const { name, document } = request.params;
const allowedDocs = ['requirements', 'design', 'tasks'];
if (!allowedDocs.includes(document)) {
reply.code(400).send({ error: 'Invalid document type' });
return;
}
const docPath = (0, path_1.join)(this.options.projectPath, '.claude', 'specs', name, `${document}.md`);
try {
const content = await (0, promises_1.readFile)(docPath, 'utf-8');
return { content };
}
catch {
reply.code(404).send({ error: 'Document not found' });
}
});
// Get raw bug markdown content
this.app.get('/api/bugs/:name/:document', async (request, reply) => {
const { name, document } = request.params;
const allowedDocs = ['report', 'analysis', 'verification'];
if (!allowedDocs.includes(document)) {
reply.code(400).send({ error: 'Invalid document type' });
return;
}
const docPath = (0, path_1.join)(this.options.projectPath, '.claude', 'bugs', name, `${document}.md`);
try {
const content = await (0, promises_1.readFile)(docPath, 'utf-8');
return { content };
}
catch {
reply.code(404).send({ error: 'Document not found' });
}
});
// Set up file watcher
this.watcher.on('change', (event) => {
// Broadcast to all connected clients
const message = JSON.stringify({
type: 'update',
data: event,
});
this.clients.forEach((client) => {
if (client.readyState === 1) {
// WebSocket.OPEN
client.send(message);
}
});
});
// Set up bug change watcher
this.watcher.on('bug-change', (event) => {
// Broadcast to all connected clients
const message = JSON.stringify({
type: 'bug-update',
data: event,
});
this.clients.forEach((client) => {
if (client.readyState === 1) {
// WebSocket.OPEN
client.send(message);
}
});
});
// Set up steering change watcher
this.watcher.on('steering-change', (event) => {
// Broadcast steering updates to all connected clients
const message = JSON.stringify({
type: 'steering-update',
data: event.steeringStatus,
});
this.clients.forEach((client) => {
if (client.readyState === 1) {
// WebSocket.OPEN
client.send(message);
}
});
});
// Start watcher
await this.watcher.start();
// Find available port if the requested port is busy
let actualPort = this.options.port;
if (!(await (0, utils_1.isPortAvailable)(this.options.port))) {
console.log(`Port ${this.options.port} is in use, finding alternative...`);
actualPort = await (0, utils_1.findAvailablePort)(this.options.port);
console.log(`Using port ${actualPort} instead`);
}
// Start server
await this.app.listen({ port: actualPort, host: '0.0.0.0' });
// Update the port in options for URL generation
this.options.port = actualPort;
// Open browser if requested
if (this.options.autoOpen) {
await (0, open_1.default)(`http://localhost:${this.options.port}`);
}
}
async stop() {
// Close all WebSocket connections
this.clients.forEach((client) => {
if (client.readyState === 1) {
// WebSocket.OPEN
client.close();
}
});
this.clients.clear();
// Stop the watcher
await this.watcher.stop();
// Close the Fastify server
await this.app.close();
}
}
exports.DashboardServer = DashboardServer;
//# sourceMappingURL=server.js.map