scai 0.1.76 → 0.1.77
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.
- package/dist/commands/ReviewCmd.js +124 -26
- package/dist/github/github.js +51 -1
- package/package.json +1 -1
|
@@ -152,6 +152,24 @@ function askReviewApproval() {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
+
function askFinalReviewApproval() {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
console.log('\n---');
|
|
158
|
+
console.log('1) ✅ Approve');
|
|
159
|
+
console.log('2) ❌ Request Changes');
|
|
160
|
+
console.log('3) 🚫 Cancel');
|
|
161
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
162
|
+
rl.question(`\n👉 Choose an option [1-3]: `, (answer) => {
|
|
163
|
+
rl.close();
|
|
164
|
+
if (answer === '1')
|
|
165
|
+
resolve('approve');
|
|
166
|
+
else if (answer === '2')
|
|
167
|
+
resolve('request-changes');
|
|
168
|
+
else
|
|
169
|
+
resolve('cancel');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
155
173
|
// Prompt for custom review
|
|
156
174
|
function promptCustomReview() {
|
|
157
175
|
return new Promise((resolve) => {
|
|
@@ -176,15 +194,63 @@ export async function promptEditReview(suggestedReview) {
|
|
|
176
194
|
.trim() || suggestedReview;
|
|
177
195
|
}
|
|
178
196
|
// Split diff into file-based chunks
|
|
179
|
-
function chunkDiff(diff) {
|
|
197
|
+
function chunkDiff(diff, review_id) {
|
|
180
198
|
const rawChunks = diff.split(/^diff --git /m).filter(Boolean);
|
|
181
199
|
return rawChunks.map(chunk => {
|
|
182
200
|
const fullChunk = 'diff --git ' + chunk;
|
|
183
201
|
const filePathMatch = fullChunk.match(/^diff --git a\/(.+?) b\//);
|
|
184
202
|
const filePath = filePathMatch ? filePathMatch[1] : 'unknown';
|
|
203
|
+
// Now we extract hunks and lines as per the DiffHunk type
|
|
204
|
+
const hunks = [];
|
|
205
|
+
let currentHunk = null;
|
|
206
|
+
// Split chunk into lines
|
|
207
|
+
const lines = fullChunk.split('\n');
|
|
208
|
+
lines.forEach(line => {
|
|
209
|
+
const hunkHeaderMatch = line.match(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@/);
|
|
210
|
+
if (hunkHeaderMatch) {
|
|
211
|
+
// When we encounter a new hunk, process the previous one (if it exists) and start a new one
|
|
212
|
+
if (currentHunk) {
|
|
213
|
+
hunks.push(currentHunk);
|
|
214
|
+
}
|
|
215
|
+
// Parse the hunk header
|
|
216
|
+
const oldStart = parseInt(hunkHeaderMatch[1], 10);
|
|
217
|
+
const newStart = parseInt(hunkHeaderMatch[3], 10);
|
|
218
|
+
const oldLines = parseInt(hunkHeaderMatch[2], 10);
|
|
219
|
+
const newLines = parseInt(hunkHeaderMatch[4], 10);
|
|
220
|
+
currentHunk = {
|
|
221
|
+
oldStart,
|
|
222
|
+
newStart,
|
|
223
|
+
oldLines,
|
|
224
|
+
newLines,
|
|
225
|
+
lines: [],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
else if (currentHunk) {
|
|
229
|
+
// Process the lines inside the hunk
|
|
230
|
+
let lineType = 'context';
|
|
231
|
+
if (line.startsWith('+'))
|
|
232
|
+
lineType = 'add';
|
|
233
|
+
if (line.startsWith('-'))
|
|
234
|
+
lineType = 'del';
|
|
235
|
+
// Create the DiffLine object
|
|
236
|
+
currentHunk.lines.push({
|
|
237
|
+
line,
|
|
238
|
+
type: lineType,
|
|
239
|
+
lineNumberOld: lineType === 'del' ? currentHunk.oldStart++ : undefined,
|
|
240
|
+
lineNumberNew: lineType === 'add' ? currentHunk.newStart++ : undefined,
|
|
241
|
+
review_id, // Assign the review_id here
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// Push the last hunk (if any)
|
|
246
|
+
if (currentHunk) {
|
|
247
|
+
hunks.push(currentHunk);
|
|
248
|
+
}
|
|
185
249
|
return {
|
|
186
250
|
filePath,
|
|
187
251
|
content: fullChunk,
|
|
252
|
+
hunks, // Return hunks, which now contain DiffLine objects with line numbers and review_id
|
|
253
|
+
review_id, // Assign the review_id here for each chunk
|
|
188
254
|
};
|
|
189
255
|
});
|
|
190
256
|
}
|
|
@@ -247,10 +313,10 @@ function waitForSpaceOrQ() {
|
|
|
247
313
|
export async function promptChunkReviewMenu() {
|
|
248
314
|
return new Promise((resolve) => {
|
|
249
315
|
console.log('\nReview options for this chunk:');
|
|
250
|
-
console.log(' 1) 💬
|
|
316
|
+
console.log(' 1) 💬 Approve and post AI review as comment');
|
|
251
317
|
console.log(' 2) ✍️ Edit the review before posting');
|
|
252
318
|
console.log(' 3) ⌨️ Write a custom comment');
|
|
253
|
-
console.log(' 4) ❌
|
|
319
|
+
console.log(' 4) ❌ Mark this chunk as needing changes');
|
|
254
320
|
console.log(' 5) ⏭️ Skip this chunk without commenting');
|
|
255
321
|
console.log(chalk.gray(' (Press [space] to skip chunk, [q] to quit review, or any other key to show menu)\n'));
|
|
256
322
|
// Fallback to menu input if key was not space/q
|
|
@@ -310,10 +376,9 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
|
310
376
|
if (pr.body) {
|
|
311
377
|
console.log(chalk.magentaBright('\n📝 PR Description:\n') + chalk.gray(pr.body));
|
|
312
378
|
}
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
let allApproved = true; // Track if everything is approved
|
|
379
|
+
const chunks = chunkDiff(diff, pr.number.toString());
|
|
380
|
+
const reviewMethod = chunks.length > 1 ? await askReviewMethod() : 'chunk';
|
|
381
|
+
let reviewComments = [];
|
|
317
382
|
if (reviewMethod === 'whole') {
|
|
318
383
|
const suggestion = await reviewModule.run({ content: diff, filepath: 'Whole PR Diff' });
|
|
319
384
|
console.log(chalk.yellowBright("Suggestion: ", suggestion));
|
|
@@ -321,55 +386,88 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
|
321
386
|
let reviewText = '';
|
|
322
387
|
if (finalReviewChoice === 'approve') {
|
|
323
388
|
reviewText = 'PR approved';
|
|
324
|
-
await submitReview(pr.number, suggestion.content, 'APPROVE');
|
|
389
|
+
await submitReview(pr.number, suggestion.content, 'APPROVE');
|
|
325
390
|
}
|
|
326
391
|
else if (finalReviewChoice === 'reject') {
|
|
327
392
|
reviewText = 'Changes requested';
|
|
328
|
-
await submitReview(pr.number, suggestion.content, 'REQUEST_CHANGES');
|
|
393
|
+
await submitReview(pr.number, suggestion.content, 'REQUEST_CHANGES');
|
|
329
394
|
}
|
|
330
395
|
else if (finalReviewChoice === 'custom') {
|
|
331
396
|
reviewText = await promptCustomReview();
|
|
332
|
-
await submitReview(pr.number, reviewText, 'COMMENT');
|
|
397
|
+
await submitReview(pr.number, reviewText, 'COMMENT');
|
|
333
398
|
}
|
|
334
399
|
else if (finalReviewChoice === 'edit') {
|
|
335
400
|
reviewText = await promptEditReview(suggestion.content);
|
|
336
|
-
await submitReview(pr.number, reviewText, 'COMMENT');
|
|
401
|
+
await submitReview(pr.number, reviewText, 'COMMENT');
|
|
337
402
|
}
|
|
338
403
|
}
|
|
339
404
|
else {
|
|
340
405
|
console.log(chalk.cyan(`🔍 Total Chunks: ${chunks.length}`));
|
|
406
|
+
let allApproved = true;
|
|
341
407
|
for (let i = 0; i < chunks.length; i++) {
|
|
342
408
|
const chunk = chunks[i];
|
|
343
409
|
const { choice, summary } = await reviewChunk(chunk, i, chunks.length);
|
|
344
410
|
if (choice === 'approve') {
|
|
345
|
-
|
|
346
|
-
|
|
411
|
+
reviewComments.push({
|
|
412
|
+
path: chunk.filePath,
|
|
413
|
+
body: summary,
|
|
414
|
+
line: chunk.hunks[0]?.newStart || 1,
|
|
415
|
+
side: 'RIGHT',
|
|
416
|
+
});
|
|
417
|
+
console.log(`💬 Posted AI review for chunk ${i + 1}`);
|
|
347
418
|
}
|
|
348
419
|
else if (choice === 'reject') {
|
|
349
420
|
allApproved = false;
|
|
350
|
-
|
|
351
|
-
|
|
421
|
+
reviewComments.push({
|
|
422
|
+
path: chunk.filePath,
|
|
423
|
+
body: summary,
|
|
424
|
+
line: chunk.hunks[0]?.newStart || 1,
|
|
425
|
+
side: 'RIGHT',
|
|
426
|
+
});
|
|
352
427
|
}
|
|
353
428
|
else if (choice === 'custom') {
|
|
354
429
|
const customReview = await promptCustomReview();
|
|
355
|
-
|
|
430
|
+
reviewComments.push({
|
|
431
|
+
path: chunk.filePath,
|
|
432
|
+
body: customReview,
|
|
433
|
+
line: chunk.hunks[0]?.newStart || 1,
|
|
434
|
+
side: 'RIGHT',
|
|
435
|
+
});
|
|
356
436
|
}
|
|
357
437
|
else if (choice === 'cancel' || choice === 'skip') {
|
|
358
438
|
console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
|
|
359
439
|
}
|
|
360
440
|
else if (typeof choice === 'string') {
|
|
361
|
-
|
|
362
|
-
|
|
441
|
+
reviewComments.push({
|
|
442
|
+
path: chunk.filePath,
|
|
443
|
+
body: choice,
|
|
444
|
+
line: chunk.hunks[0]?.newStart || 1,
|
|
445
|
+
side: 'RIGHT',
|
|
446
|
+
});
|
|
363
447
|
}
|
|
364
448
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
console.log(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
449
|
+
console.log(chalk.blueBright('\n📝 Review Comments Preview:'));
|
|
450
|
+
reviewComments.forEach((comment, idx) => {
|
|
451
|
+
console.log(`${idx + 1}. ${comment.path}:${comment.line} [${comment.side}] — ${comment.body}`);
|
|
452
|
+
});
|
|
453
|
+
const shouldApprove = allApproved;
|
|
454
|
+
const hasInlineComments = reviewComments.length > 0;
|
|
455
|
+
// We always submit comments first if any
|
|
456
|
+
const initialReviewState = shouldApprove ? 'APPROVE' : 'REQUEST_CHANGES';
|
|
457
|
+
const initialReviewBody = shouldApprove
|
|
458
|
+
? 'Reviewed all chunks.'
|
|
459
|
+
: 'Requested changes based on chunk reviews.';
|
|
460
|
+
console.log(shouldApprove && !hasInlineComments
|
|
461
|
+
? chalk.green('✔️ All chunks approved. Submitting final PR approval.')
|
|
462
|
+
: !shouldApprove
|
|
463
|
+
? chalk.red('❌ Not all chunks were approved. Changes requested.')
|
|
464
|
+
: chalk.green('📝 Submitting inline comments before approval.') // ✅ NEW
|
|
465
|
+
);
|
|
466
|
+
// ✅ Submit review with inline comments or direct approval/request
|
|
467
|
+
await submitReview(pr.number, initialReviewBody, initialReviewState, reviewComments);
|
|
468
|
+
// ✅ Then submit separate approval if needed
|
|
469
|
+
if (shouldApprove && hasInlineComments) {
|
|
470
|
+
await submitReview(pr.number, 'PR approved after inline comments.', 'APPROVE');
|
|
373
471
|
}
|
|
374
472
|
}
|
|
375
473
|
}
|
package/dist/github/github.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function fetchPullRequestDiff(pr, token) {
|
|
|
48
48
|
}
|
|
49
49
|
return await res.text();
|
|
50
50
|
}
|
|
51
|
-
export async function submitReview(prNumber, body, event
|
|
51
|
+
export async function submitReview(prNumber, body, event, comments) {
|
|
52
52
|
const token = await ensureGitHubAuth();
|
|
53
53
|
const { owner, repo } = getRepoDetails();
|
|
54
54
|
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`;
|
|
@@ -61,6 +61,7 @@ export async function submitReview(prNumber, body, event = 'COMMENT') {
|
|
|
61
61
|
body: JSON.stringify({
|
|
62
62
|
body,
|
|
63
63
|
event,
|
|
64
|
+
comments
|
|
64
65
|
}),
|
|
65
66
|
});
|
|
66
67
|
if (!res.ok) {
|
|
@@ -69,3 +70,52 @@ export async function submitReview(prNumber, body, event = 'COMMENT') {
|
|
|
69
70
|
}
|
|
70
71
|
console.log(`✅ Submitted ${event} review for PR #${prNumber}`);
|
|
71
72
|
}
|
|
73
|
+
export async function postInlineComment(prNumber, commitId, path, body, line, side = 'RIGHT', reviewId = null // Associate with a review if available
|
|
74
|
+
) {
|
|
75
|
+
const token = await ensureGitHubAuth();
|
|
76
|
+
const { owner, repo } = getRepoDetails();
|
|
77
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/comments`;
|
|
78
|
+
const res = await fetch(url, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `token ${token}`,
|
|
82
|
+
Accept: 'application/vnd.github.v3+json',
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
body,
|
|
86
|
+
commit_id: commitId,
|
|
87
|
+
path,
|
|
88
|
+
line,
|
|
89
|
+
side,
|
|
90
|
+
review_id: reviewId, // Include review_id if available
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const errorText = await res.text();
|
|
95
|
+
throw new Error(`Failed to post inline comment: ${res.status} ${res.statusText} - ${errorText}`);
|
|
96
|
+
}
|
|
97
|
+
console.log(`💬 Posted inline comment on ${path}:${line}`);
|
|
98
|
+
}
|
|
99
|
+
export async function createReviewForPR(prNumber, body, event = 'COMMENT') {
|
|
100
|
+
const token = await ensureGitHubAuth();
|
|
101
|
+
const { owner, repo } = getRepoDetails();
|
|
102
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`;
|
|
103
|
+
const res = await fetch(url, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `token ${token}`,
|
|
107
|
+
Accept: 'application/vnd.github.v3+json',
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
body,
|
|
111
|
+
event,
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const errorText = await res.text();
|
|
116
|
+
throw new Error(`Failed to create review: ${res.status} ${res.statusText} - ${errorText}`);
|
|
117
|
+
}
|
|
118
|
+
const review = await res.json();
|
|
119
|
+
console.log(`✅ Created review for PR #${prNumber}`);
|
|
120
|
+
return review.id; // Return the review ID to be used for inline comments
|
|
121
|
+
}
|