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