UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

193 lines (192 loc) 6.73 kB
import { createServer } from "node:http"; import { createHmac } from "node:crypto"; import { log } from "../util/logging.js"; export class GitHubWebhookServer { server; port; secret; handlers = new Map(); isRunning = false; constructor(port = 3000, secret) { this.port = port; this.secret = secret || process.env.GITHUB_WEBHOOK_SECRET || ""; this.server = createServer(this.handleRequest.bind(this)); } // Start the webhook server async start() { return new Promise((resolve, reject) => { this.server.listen(this.port, (error) => { if (error) { reject(error); } else { this.isRunning = true; log.info(`🎣 Webhook server listening on port ${this.port}`); resolve(); } }); }); } // Stop the webhook server async stop() { return new Promise((resolve) => { this.server.close(() => { this.isRunning = false; log.info("🛑 Webhook server stopped"); resolve(); }); }); } // Register event handler on(event, handler) { if (!this.handlers.has(event)) { this.handlers.set(event, []); } this.handlers.get(event).push(handler); } // Remove event handler off(event, handler) { const handlers = this.handlers.get(event); if (handlers) { const index = handlers.indexOf(handler); if (index >= 0) { handlers.splice(index, 1); } } } // Handle incoming webhook request async handleRequest(req, res) { try { // Only handle POST requests to /webhook if (req.method !== 'POST' || req.url !== '/webhook') { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); return; } // Get headers const githubEvent = req.headers['x-github-event']; const signature = req.headers['x-hub-signature-256']; const delivery = req.headers['x-github-delivery']; if (!githubEvent) { res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Missing X-GitHub-Event header'); return; } // Read request body const body = await this.readRequestBody(req); const payload = JSON.parse(body); // Verify signature if secret is configured if (this.secret && signature) { if (!this.verifySignature(body, signature)) { res.writeHead(401, { 'Content-Type': 'text/plain' }); res.end('Invalid signature'); return; } } // Create webhook event const event = { event: githubEvent, payload, signature, delivery }; // Handle the event await this.processEvent(event); // Send success response res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ message: 'Webhook processed successfully' })); } catch (error) { log.error('Webhook processing error:', error); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); } } // Read request body readRequestBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', (chunk) => { body += chunk.toString(); }); req.on('end', () => { resolve(body); }); req.on('error', reject); }); } // Verify GitHub webhook signature verifySignature(payload, signature) { const expectedSignature = 'sha256=' + createHmac('sha256', this.secret) .update(payload, 'utf8') .digest('hex'); return signature === expectedSignature; } // Process webhook event async processEvent(event) { log.step('Webhook received', `${event.event} from ${event.payload.repository?.full_name || 'unknown'}`); // Get handlers for this event type const handlers = this.handlers.get(event.event) || []; const allHandlers = this.handlers.get('*') || []; // Run all handlers const promises = [...handlers, ...allHandlers].map(handler => handler(event).catch(error => log.error(`Handler error for ${event.event}:`, error))); await Promise.all(promises); } // Check if server is running isListening() { return this.isRunning; } } // Parse @termcoder commands from GitHub comments export function parseTermCoderCommands(commentBody, context) { const commands = []; // Look for @termcoder mentions const mentions = commentBody.match(/@termcoder\s+([^\n\r]+)/gi); if (!mentions) return commands; for (const mention of mentions) { // Extract command after @termcoder const commandMatch = mention.match(/@termcoder\s+(.+)/i); if (!commandMatch) continue; const commandLine = commandMatch[1].trim(); const parts = commandLine.split(/\s+/); const command = parts[0]; const args = parts.slice(1); commands.push({ command, args, repoOwner: context.repository.owner.login, repoName: context.repository.name, issueNumber: context.issue?.number, prNumber: context.pull_request?.number, commentId: context.comment.id, author: context.comment.user.login, authorAssociation: context.comment.author_association }); } return commands; } // Default webhook server instance let defaultServer = null; // Get or create default server export function getWebhookServer(port, secret) { if (!defaultServer) { defaultServer = new GitHubWebhookServer(port, secret); } return defaultServer; } // Start default webhook server export async function startWebhookServer(port, secret) { const server = getWebhookServer(port, secret); if (!server.isListening()) { await server.start(); } return server; } // Stop default webhook server export async function stopWebhookServer() { if (defaultServer && defaultServer.isListening()) { await defaultServer.stop(); } }