/** * Gitea Integration for AgentDash * Provides real-time data from Gitea API */ const https = require('https'); const http = require('http'); class GiteaIntegration { constructor(config = {}) { this.baseUrl = config.baseUrl || 'https://gitea.tophermayor.com'; this.token = config.token || process.env.GITEA_TOKEN; this.owner = config.owner || 'TopherMayor'; this.cache = new Map(); this.cacheTimeout = config.cacheTimeout || 30000; // 30 seconds } /** * Make authenticated request to Gitea API */ async request(endpoint) { return new Promise((resolve, reject) => { const url = new URL(endpoint, this.baseUrl); const client = url.protocol === 'https:' ? https : http; const options = { hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method: 'GET', headers: { 'Authorization': `token ${this.token}`, 'Accept': 'application/json' } }; const req = client.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(new Error(`Failed to parse response: ${e.message}`)); } }); }); req.on('error', reject); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Request timeout')); }); req.end(); }); } /** * Get cached data or fetch fresh */ async getCached(key, fetcher) { const cached = this.cache.get(key); if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { return cached.data; } const data = await fetcher(); this.cache.set(key, { data, timestamp: Date.now() }); return data; } /** * Get all repositories for the owner */ async getRepos() { return this.getCached('repos', () => this.request(`/api/v1/user/repos?limit=50`) ); } /** * Get repository details */ async getRepo(repoName) { return this.getCached(`repo:${repoName}`, () => this.request(`/api/v1/repos/${this.owner}/${repoName}`) ); } /** * Get open pull requests for a repository */ async getPullRequests(repoName, state = 'open') { return this.getCached(`prs:${repoName}:${state}`, () => this.request(`/api/v1/repos/${this.owner}/${repoName}/pulls?state=${state}&limit=20`) ); } /** * Get issues for a repository */ async getIssues(repoName, state = 'open') { return this.getCached(`issues:${repoName}:${state}`, () => this.request(`/api/v1/repos/${this.owner}/${repoName}/issues?state=${state}&limit=20`) ); } /** * Get recent commits for a repository */ async getCommits(repoName, branch = 'main', limit = 10) { return this.getCached(`commits:${repoName}:${branch}`, () => this.request(`/api/v1/repos/${this.owner}/${repoName}/commits?sha=${branch}&limit=${limit}`) ); } /** * Get branches for a repository */ async getBranches(repoName) { return this.getCached(`branches:${repoName}`, () => this.request(`/api/v1/repos/${this.owner}/${repoName}/branches`) ); } /** * Get repository activity feed */ async getActivity(repoName) { return this.getCached(`activity:${repoName}`, () => this.request(`/api/v1/repos/${this.owner}/${repoName}/activities/feeds?limit=20`) ); } /** * Get user info */ async getUser() { return this.getCached('user', () => this.request('/api/v1/user') ); } /** * Get organization info (if applicable) */ async getOrgs() { return this.getCached('orgs', () => this.request('/api/v1/user/orgs') ); } /** * Clear cache */ clearCache() { this.cache.clear(); } /** * Get swarm summary - all repos with PRs and issues */ async getSwarmSummary() { const repos = await this.getRepos(); const summary = []; for (const repo of repos.slice(0, 10)) { // Limit to 10 repos try { const [prs, issues, branches] = await Promise.all([ this.getPullRequests(repo.name, 'open').catch(() => []), this.getIssues(repo.name, 'open').catch(() => []), this.getBranches(repo.name).catch(() => []) ]); summary.push({ name: repo.name, full_name: repo.full_name, stars: repo.stars_count || 0, forks: repo.forks_count || 0, open_prs: prs.length, open_issues: issues.length, branches: branches.length, updated_at: repo.updated_at, html_url: repo.html_url }); } catch (e) { console.error(`Error fetching data for ${repo.name}:`, e.message); } } return summary; } /** * Get pending reviews (PRs needing attention) */ async getPendingReviews() { const repos = await this.getRepos(); const pending = []; for (const repo of repos.slice(0, 10)) { try { const prs = await this.getPullRequests(repo.name, 'open'); for (const pr of prs) { pending.push({ repo: repo.name, repo_url: repo.html_url, pr_number: pr.number, pr_title: pr.title, pr_url: pr.html_url, author: pr.user?.login, created_at: pr.created_at, mergeable: pr.mergeable, draft: pr.draft, labels: pr.labels || [] }); } } catch (e) { console.error(`Error fetching PRs for ${repo.name}:`, e.message); } } return pending; } /** * Get recent activity across all repos */ async getRecentActivity() { const repos = await this.getRepos(); const activities = []; for (const repo of repos.slice(0, 5)) { try { const activity = await this.getActivity(repo.name); for (const act of (activity || []).slice(0, 5)) { activities.push({ repo: repo.name, repo_url: repo.html_url, ...act }); } } catch (e) { // Skip repos without activity access } } return activities .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) .slice(0, 20); } } module.exports = GiteaIntegration;