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
JavaScript
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();
}
}