scai 0.1.75 → 0.1.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -13,15 +13,20 @@ function truncate(str, length) {
|
|
|
13
13
|
return str.length > length ? str.slice(0, length - 3) + '...' : str;
|
|
14
14
|
}
|
|
15
15
|
// Fetch open PRs with review requested
|
|
16
|
+
import { Spinner } from '../lib/spinner.js'; // adjust path as needed
|
|
16
17
|
export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
|
|
17
|
-
const
|
|
18
|
+
const spinner = new Spinner('Fetching pull requests and diffs...');
|
|
19
|
+
spinner.start();
|
|
18
20
|
const filtered = [];
|
|
19
21
|
const failedPRs = [];
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
try {
|
|
23
|
+
const prs = await fetchOpenPullRequests(token, owner, repo);
|
|
24
|
+
for (const pr of prs) {
|
|
25
|
+
const shouldInclude = !pr.draft &&
|
|
26
|
+
!pr.merged_at &&
|
|
27
|
+
(!filterForUser || pr.requested_reviewers?.some(r => r.login === username));
|
|
28
|
+
if (!shouldInclude)
|
|
29
|
+
continue;
|
|
25
30
|
try {
|
|
26
31
|
const prNumber = pr.number;
|
|
27
32
|
const prRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
|
|
@@ -50,21 +55,25 @@ export async function getPullRequestsForReview(token, owner, repo, username, bra
|
|
|
50
55
|
failedPRs.push(pr);
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
if (filtered.length === 0) {
|
|
59
|
+
const msg = filterForUser
|
|
60
|
+
? `No open pull requests found for review by '${username}'.`
|
|
61
|
+
: `No open pull requests found.`;
|
|
62
|
+
spinner.succeed(msg);
|
|
58
63
|
}
|
|
59
64
|
else {
|
|
60
|
-
|
|
65
|
+
spinner.succeed(`Fetched ${filtered.length} PR(s) with diffs.`);
|
|
66
|
+
}
|
|
67
|
+
if (failedPRs.length > 0) {
|
|
68
|
+
const failedList = failedPRs.map(pr => `#${pr.number}`).join(', ');
|
|
69
|
+
console.warn(`⚠️ Skipped ${failedPRs.length} PR(s): ${failedList}`);
|
|
61
70
|
}
|
|
71
|
+
return filtered;
|
|
62
72
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
catch (err) {
|
|
74
|
+
spinner.fail(`Error fetching pull requests: ${err.message}`);
|
|
75
|
+
return [];
|
|
66
76
|
}
|
|
67
|
-
return filtered;
|
|
68
77
|
}
|
|
69
78
|
// Prompt user to select PR
|
|
70
79
|
function askUserToPickPR(prs) {
|
|
@@ -119,7 +128,7 @@ function askReviewMethod() {
|
|
|
119
128
|
});
|
|
120
129
|
}
|
|
121
130
|
// Prompt for review approval
|
|
122
|
-
function askReviewApproval(
|
|
131
|
+
function askReviewApproval() {
|
|
123
132
|
return new Promise((resolve) => {
|
|
124
133
|
console.log('\n---');
|
|
125
134
|
console.log('1) ✅ Approve');
|
|
@@ -191,18 +200,99 @@ function colorDiffLine(line) {
|
|
|
191
200
|
}
|
|
192
201
|
// Review a single chunk
|
|
193
202
|
export async function reviewChunk(chunk, chunkIndex, totalChunks) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
const lines = chunk.content.split('\n');
|
|
204
|
+
const coloredDiff = lines.map(colorDiffLine).join('\n');
|
|
205
|
+
console.log(chalk.gray('\n' + '━'.repeat(60)));
|
|
206
|
+
console.log(`📄 ${chalk.bold('File')}: ${chalk.cyan(chunk.filePath)}`);
|
|
207
|
+
console.log(`🔢 ${chalk.bold('Chunk')}: ${chunkIndex + 1} of ${totalChunks}`);
|
|
208
|
+
const suggestion = await reviewModule.run({
|
|
209
|
+
content: chunk.content,
|
|
210
|
+
filepath: chunk.filePath
|
|
211
|
+
});
|
|
212
|
+
const summary = suggestion.content?.trim() || 'AI review summary not available.';
|
|
213
|
+
console.log(`🔍 ${chalk.bold('Summary')}: ${summary}`);
|
|
214
|
+
console.log(`\n${chalk.bold('--- Diff ---')}\n`);
|
|
197
215
|
console.log(coloredDiff);
|
|
198
|
-
|
|
199
|
-
console.log(
|
|
200
|
-
console.log(
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
|
|
216
|
+
console.log(`\n${chalk.bold('--- AI Review ---')}\n`);
|
|
217
|
+
console.log(chalk.blue(`💬 ${summary}`));
|
|
218
|
+
console.log(chalk.gray('━'.repeat(60)));
|
|
219
|
+
const choice = await promptChunkReviewMenu();
|
|
220
|
+
if (choice === 'edit') {
|
|
221
|
+
const edited = await promptEditReview(summary); // edit based on the suggestion
|
|
222
|
+
return { choice: edited, summary: edited };
|
|
223
|
+
}
|
|
224
|
+
else if (choice === 'skip') {
|
|
225
|
+
await waitForSpaceOrQ(); // pause between chunks
|
|
226
|
+
return { choice: 'cancel', summary }; // skip this one
|
|
204
227
|
}
|
|
205
|
-
return
|
|
228
|
+
return { choice, summary };
|
|
229
|
+
}
|
|
230
|
+
function waitForSpaceOrQ() {
|
|
231
|
+
return new Promise(resolve => {
|
|
232
|
+
process.stdin.setRawMode(true);
|
|
233
|
+
process.stdin.resume();
|
|
234
|
+
process.stdout.write('\n⏭️ (Press [space] to skip, [q] to quit, or any other key to show menu)\n');
|
|
235
|
+
function onKeyPress(chunk) {
|
|
236
|
+
const key = chunk.toString();
|
|
237
|
+
if (key === ' ' || key === 'q' || key === 'Q') {
|
|
238
|
+
process.stdin.setRawMode(false);
|
|
239
|
+
process.stdin.pause();
|
|
240
|
+
process.stdin.removeListener('data', onKeyPress);
|
|
241
|
+
resolve();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
process.stdin.on('data', onKeyPress);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export async function promptChunkReviewMenu() {
|
|
248
|
+
return new Promise((resolve) => {
|
|
249
|
+
console.log('\nReview options for this chunk:');
|
|
250
|
+
console.log(' 1) 💬 Post AI review as comment');
|
|
251
|
+
console.log(' 2) ✍️ Edit the review before posting');
|
|
252
|
+
console.log(' 3) ⌨️ Write a custom comment');
|
|
253
|
+
console.log(' 4) ❌ Mark this chunk as needing changes');
|
|
254
|
+
console.log(' 5) ⏭️ Skip this chunk without commenting');
|
|
255
|
+
console.log(chalk.gray(' (Press [space] to skip chunk, [q] to quit review, or any other key to show menu)\n'));
|
|
256
|
+
// Fallback to menu input if key was not space/q
|
|
257
|
+
function askWithReadline() {
|
|
258
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
259
|
+
rl.question('👉 Choose [1–5]: ', (answer) => {
|
|
260
|
+
rl.close();
|
|
261
|
+
switch (answer.trim()) {
|
|
262
|
+
case '1': return resolve('approve');
|
|
263
|
+
case '2': return resolve('edit');
|
|
264
|
+
case '3': return resolve('custom');
|
|
265
|
+
case '4': return resolve('reject');
|
|
266
|
+
case '5': return resolve('skip');
|
|
267
|
+
default:
|
|
268
|
+
console.log('⚠️ Invalid option. Skipping chunk.');
|
|
269
|
+
return resolve('skip');
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
// Raw key listener for quick actions
|
|
274
|
+
function onKeyPress(key) {
|
|
275
|
+
const keyStr = key.toString().toLowerCase();
|
|
276
|
+
process.stdin.setRawMode(false);
|
|
277
|
+
process.stdin.pause();
|
|
278
|
+
if (keyStr === ' ') {
|
|
279
|
+
return resolve('skip');
|
|
280
|
+
}
|
|
281
|
+
else if (keyStr === 'q') {
|
|
282
|
+
console.log('\n👋 Exiting review.');
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// flush any remaining input
|
|
287
|
+
process.stdin.removeAllListeners('data');
|
|
288
|
+
askWithReadline();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Prepare for keypress
|
|
292
|
+
process.stdin.setRawMode(true);
|
|
293
|
+
process.stdin.resume();
|
|
294
|
+
process.stdin.once('data', onKeyPress);
|
|
295
|
+
});
|
|
206
296
|
}
|
|
207
297
|
// Main command to review PR
|
|
208
298
|
export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
@@ -220,49 +310,67 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
|
220
310
|
if (pr.body) {
|
|
221
311
|
console.log(chalk.magentaBright('\n📝 PR Description:\n') + chalk.gray(pr.body));
|
|
222
312
|
}
|
|
223
|
-
|
|
313
|
+
// Skip review method if there's only one chunk
|
|
314
|
+
const chunks = chunkDiff(diff);
|
|
315
|
+
const reviewMethod = chunks.length > 1 ? await askReviewMethod() : 'chunk'; // Auto-select chunk review if only 1 chunk
|
|
316
|
+
let allApproved = true; // Track if everything is approved
|
|
224
317
|
if (reviewMethod === 'whole') {
|
|
225
318
|
const suggestion = await reviewModule.run({ content: diff, filepath: 'Whole PR Diff' });
|
|
226
|
-
console.log(suggestion
|
|
227
|
-
const finalReviewChoice = await askReviewApproval(
|
|
319
|
+
console.log(chalk.yellowBright("Suggestion: ", suggestion));
|
|
320
|
+
const finalReviewChoice = await askReviewApproval();
|
|
228
321
|
let reviewText = '';
|
|
229
322
|
if (finalReviewChoice === 'approve') {
|
|
230
323
|
reviewText = 'PR approved';
|
|
231
|
-
await submitReview(pr.number,
|
|
324
|
+
await submitReview(pr.number, suggestion.content, 'APPROVE'); // Final review approval
|
|
232
325
|
}
|
|
233
326
|
else if (finalReviewChoice === 'reject') {
|
|
234
327
|
reviewText = 'Changes requested';
|
|
235
|
-
await submitReview(pr.number,
|
|
328
|
+
await submitReview(pr.number, suggestion.content, 'REQUEST_CHANGES'); // Final review rejection
|
|
236
329
|
}
|
|
237
330
|
else if (finalReviewChoice === 'custom') {
|
|
238
331
|
reviewText = await promptCustomReview();
|
|
239
|
-
await submitReview(pr.number, reviewText, 'COMMENT');
|
|
332
|
+
await submitReview(pr.number, reviewText, 'COMMENT'); // Custom comment
|
|
240
333
|
}
|
|
241
334
|
else if (finalReviewChoice === 'edit') {
|
|
242
335
|
reviewText = await promptEditReview(suggestion.content);
|
|
243
|
-
await submitReview(pr.number, reviewText, 'COMMENT');
|
|
336
|
+
await submitReview(pr.number, reviewText, 'COMMENT'); // Edited comment
|
|
244
337
|
}
|
|
245
338
|
}
|
|
246
339
|
else {
|
|
247
|
-
const chunks = chunkDiff(diff);
|
|
248
340
|
console.log(chalk.cyan(`🔍 Total Chunks: ${chunks.length}`));
|
|
249
341
|
for (let i = 0; i < chunks.length; i++) {
|
|
250
342
|
const chunk = chunks[i];
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
253
|
-
|
|
343
|
+
const { choice, summary } = await reviewChunk(chunk, i, chunks.length);
|
|
344
|
+
if (choice === 'approve') {
|
|
345
|
+
console.log(`✔️ Approved chunk ${i + 1}`);
|
|
346
|
+
await submitReview(pr.number, summary, 'COMMENT');
|
|
254
347
|
}
|
|
255
|
-
else if (
|
|
256
|
-
|
|
348
|
+
else if (choice === 'reject') {
|
|
349
|
+
allApproved = false;
|
|
350
|
+
const reviewText = `Changes requested for chunk ${i + 1}: ${summary}`;
|
|
351
|
+
await submitReview(pr.number, reviewText, 'REQUEST_CHANGES');
|
|
257
352
|
}
|
|
258
|
-
else if (
|
|
353
|
+
else if (choice === 'custom') {
|
|
259
354
|
const customReview = await promptCustomReview();
|
|
260
355
|
await submitReview(pr.number, customReview, 'COMMENT');
|
|
261
356
|
}
|
|
262
|
-
else {
|
|
263
|
-
|
|
357
|
+
else if (choice === 'cancel' || choice === 'skip') {
|
|
358
|
+
console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
|
|
359
|
+
}
|
|
360
|
+
else if (typeof choice === 'string') {
|
|
361
|
+
// e.g., from 'edit' returning custom text
|
|
362
|
+
await submitReview(pr.number, choice, 'COMMENT');
|
|
264
363
|
}
|
|
265
364
|
}
|
|
365
|
+
// After reviewing all chunks, approve or reject the whole PR based on chunk reviews
|
|
366
|
+
if (allApproved) {
|
|
367
|
+
console.log(chalk.green('✔️ All chunks approved. Submitting final PR approval.'));
|
|
368
|
+
await submitReview(pr.number, 'PR approved after reviewing all chunks', 'APPROVE'); // Final PR approval
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.log(chalk.red('❌ Not all chunks were approved. Changes requested.'));
|
|
372
|
+
await submitReview(pr.number, 'PR changes requested based on chunk reviews', 'REQUEST_CHANGES'); // Final PR rejection
|
|
373
|
+
}
|
|
266
374
|
}
|
|
267
375
|
}
|
|
268
376
|
catch (err) {
|
|
@@ -7,8 +7,12 @@ export const reviewModule = {
|
|
|
7
7
|
const model = Config.getModel();
|
|
8
8
|
const prompt = `
|
|
9
9
|
You are a senior software engineer reviewing a pull request.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
ALWAYS make 3 concise suggestions for improvements based on the input code diff.
|
|
11
|
+
Use this format ONLY and output ONLY those suggestions:
|
|
12
|
+
|
|
13
|
+
1. ...
|
|
14
|
+
2. ...
|
|
15
|
+
3. ...
|
|
12
16
|
|
|
13
17
|
Changes:
|
|
14
18
|
${content}
|