UNPKG

scai

Version:

> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.

155 lines (154 loc) 6.05 kB
import { ensureGitHubAuth } from './auth.js'; import { getRepoDetails } from './repo.js'; export async function fetchOpenPullRequests(token, owner, repo) { const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=open&per_page=100`; const res = await fetch(url, { headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json', }, }); if (!res.ok) { throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); } const prs = await res.json(); const augmentedPRs = await Promise.all(prs.map(async (pr) => { const reviewsRes = await fetch(`${pr.url}/reviews`, { headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json', } }); let latestState = '—'; let reviewers = []; if (reviewsRes.ok) { const reviews = await reviewsRes.json(); const latestPerUser = new Map(); for (const review of reviews) { latestPerUser.set(review.user.login, review.state); } reviewers = [...latestPerUser.keys()]; // ✅ actual reviewers if ([...latestPerUser.values()].includes('CHANGES_REQUESTED')) { latestState = 'Changes Requested'; } else if ([...latestPerUser.values()].includes('APPROVED')) { latestState = 'Approved'; } } return { number: pr.number, title: pr.title, url: pr.url, diff_url: pr.diff_url, draft: pr.draft, merged_at: pr.merged_at, base: pr.base, user: pr.user?.login || 'unknown', created_at: pr.created_at, requested_reviewers: pr.requested_reviewers?.map((r) => r.login) || [], reviewers, review_status: latestState, }; })); return augmentedPRs; } export async function getGitHubUsername(token) { const res = await fetch('https://api.github.com/user', { headers: { Authorization: `token ${token}`, Accept: "application/vnd.github.v3+json", }, }); if (!res.ok) { throw new Error(`Error fetching user info: ${res.status} ${res.statusText}`); } const user = await res.json(); return user.login; // GitHub username } export async function fetchPullRequestDiff(pr, token) { const res = await fetch(pr.diff_url, { headers: { Authorization: `token ${token}`, Accept: "application/vnd.github.v3.diff", }, }); if (!res.ok) { throw new Error(`Error fetching PR diff: ${res.status} ${res.statusText}`); } return await res.text(); } export async function submitReview(prNumber, body, event, comments) { const token = await ensureGitHubAuth(); const { owner, repo } = await getRepoDetails(); const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`; // Prepare payload const payload = { body, event }; if (comments && comments.length > 0) { payload.comments = comments; } const res = await fetch(url, { method: 'POST', headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json', }, body: JSON.stringify(payload), }); if (!res.ok) { const errorText = await res.text(); let parsed = {}; try { parsed = JSON.parse(errorText); } catch (_) { // fallback to raw text } const knownErrors = Array.isArray(parsed.errors) ? parsed.errors.map((e) => e.message || e).join('; ') : ''; if (res.status === 422) { if (knownErrors.includes('Can not approve your own pull request')) { console.warn(`⚠️ Skipping approval: You cannot approve your own pull request.`); return; } if (knownErrors.includes('Comments may only be specified on pull requests with a diff')) { console.warn(`⚠️ Cannot post comments: PR has no diff.`); return; } if (knownErrors.includes('path is missing') || knownErrors.includes('line is missing') || knownErrors.includes('position is missing')) { console.warn(`⚠️ Some inline comments are missing required fields. Skipping review.`); return; } if (knownErrors.includes('Position is invalid') || knownErrors.includes('line must be part of the diff')) { console.warn(`⚠️ One or more comment positions are invalid — probably outside the diff. Skipping review.`); return; } } throw new Error(`Failed to submit review: ${res.status} ${res.statusText} - ${errorText}`); } console.log(`✅ Submitted ${event} review for PR #${prNumber}`); } export async function postInlineComment(prNumber, commitId, path, body, line, side = 'RIGHT', reviewId = null // Associate with a review if available ) { const token = await ensureGitHubAuth(); const { owner, repo } = await getRepoDetails(); const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/comments`; const res = await fetch(url, { method: 'POST', headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json', }, body: JSON.stringify({ body, commit_id: commitId, path, line, side, review_id: reviewId, // Include review_id if available }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to post inline comment: ${res.status} ${res.statusText} - ${errorText}`); } console.log(`💬 Posted inline comment on ${path}:${line}`); }