import { loadConfig } from './config.js'; import { initLogger, getLogger } from './logger.js'; import { initGitHub, fetchPR, hasExistingComment, postComment, updateComment, formatComment, } from './github.js'; import { createApp } from './server.js'; import { createEngine } from './engine/index.js'; import { startPolling } from './polling.js'; import type { WebhookEvent } from './types.js'; async function analyzeOne(target: string) { const match = target.match(/^([^/]+)\/([^#]+)#(\d+)$/); if (!match) { console.error('Usage: troutbot analyze '); process.exit(1); } const [, owner, repo, numStr] = match; const prNumber = parseInt(numStr, 10); const config = loadConfig(); initLogger(config.logging); const logger = getLogger(); if (!process.env.GITHUB_TOKEN) { logger.error('GITHUB_TOKEN is required for analyze mode'); process.exit(1); } initGitHub(process.env.GITHUB_TOKEN); const prData = await fetchPR(owner, repo, prNumber); if (!prData) { logger.error(`Could not fetch PR ${owner}/${repo}#${prNumber}`); process.exit(1); } const event: WebhookEvent = { action: 'analyze', type: 'pull_request', number: prNumber, title: prData.title, body: prData.body, owner, repo, author: prData.author, labels: prData.labels, branch: prData.branch, sha: prData.sha, }; const engine = createEngine(config.engine); const analysis = await engine.analyze(event); logger.info( `Analyzed ${owner}/${repo}#${prNumber}: impact=${analysis.impact}, confidence=${analysis.confidence.toFixed(2)}` ); logger.info(`Reasoning: ${analysis.reasoning}`); const { commentMarker, allowUpdates } = config.response; const existing = await hasExistingComment(owner, repo, prNumber, commentMarker); if (existing.exists && !allowUpdates) { logger.info(`Already commented on ${owner}/${repo}#${prNumber}, skipping`); return; } const body = formatComment( config.response, event.type, analysis.impact, analysis.confidence, analysis.reasoning ); if (existing.exists && allowUpdates && existing.commentId) { await updateComment(owner, repo, existing.commentId, body); } else { await postComment(owner, repo, prNumber, body); } } function serve() { const config = loadConfig(); initLogger(config.logging); const logger = getLogger(); initGitHub(process.env.GITHUB_TOKEN); if (!process.env.GITHUB_TOKEN) { logger.warn( 'No GITHUB_TOKEN - running in dry-run mode (checks and diff backends will be inactive)' ); } if (!process.env.WEBHOOK_SECRET) { logger.warn( 'No WEBHOOK_SECRET - webhook signature verification is disabled (not needed for polling-only mode)' ); } const app = createApp(config); const port = config.server.port; const enabledBackends = Object.entries(config.engine.backends) .filter(([, v]) => v.enabled) .map(([k]) => k); const host = config.server.host || '127.0.0.1'; const server = app.listen(port, host, async () => { logger.info(`Troutbot listening on ${host}:${port}`); logger.info(`Enabled backends: ${enabledBackends.join(', ')}`); // Watched repos if (config.repositories.length > 0) { const repos = config.repositories.map((r) => `${r.owner}/${r.repo}`).join(', '); logger.info(`Watched repos: ${repos}`); } else { logger.info('Watched repos: all (no repository filter)'); } // Active filters (only log non-empty ones) const { filters } = config; if (filters.labels.include.length > 0) logger.info(`Label include filter: ${filters.labels.include.join(', ')}`); if (filters.labels.exclude.length > 0) logger.info(`Label exclude filter: ${filters.labels.exclude.join(', ')}`); if (filters.authors.exclude.length > 0) logger.info(`Excluded authors: ${filters.authors.exclude.join(', ')}`); if (filters.branches.include.length > 0) logger.info(`Branch filter: ${filters.branches.include.join(', ')}`); // Engine weights and confidence threshold const { weights, confidenceThreshold } = config.engine; logger.info( `Engine weights: checks=${weights.checks}, diff=${weights.diff}, quality=${weights.quality} | threshold=${confidenceThreshold}` ); // Comment update mode logger.info(`Comment updates: ${config.response.allowUpdates ? 'enabled' : 'disabled'}`); const displayHost = host === '0.0.0.0' ? 'localhost' : host; logger.info(`Dashboard available at http://${displayHost}:${port}/dashboard`); // Start polling if enabled await startPolling(config); }); function shutdown(signal: string) { logger.info(`Received ${signal}, shutting down gracefully...`); server.close(() => { logger.info('Server closed'); process.exit(0); }); setTimeout(() => { logger.warn('Graceful shutdown timed out, forcing exit'); process.exit(1); }, 10_000).unref(); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); } const args = process.argv.slice(2); if (args[0] === 'analyze' && args[1]) { analyzeOne(args[1]).catch((err) => { console.error(err); process.exit(1); }); } else { serve(); }