scai 0.1.74 → 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.
|
@@ -1,318 +1,375 @@
|
|
|
1
1
|
import readline from 'readline';
|
|
2
|
-
import { fetchOpenPullRequests,
|
|
2
|
+
import { fetchOpenPullRequests, getGitHubUsername, submitReview } from '../github/github.js';
|
|
3
3
|
import { getRepoDetails } from '../github/repo.js';
|
|
4
4
|
import { ensureGitHubAuth } from '../github/auth.js';
|
|
5
|
-
import { postReviewComment } from '../github/postComments.js';
|
|
6
5
|
import { reviewModule } from '../pipeline/modules/reviewModule.js';
|
|
7
6
|
import chalk from 'chalk';
|
|
8
|
-
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { spawnSync } from 'child_process';
|
|
11
|
+
import columnify from 'columnify';
|
|
12
|
+
function truncate(str, length) {
|
|
13
|
+
return str.length > length ? str.slice(0, length - 3) + '...' : str;
|
|
14
|
+
}
|
|
15
|
+
// Fetch open PRs with review requested
|
|
16
|
+
import { Spinner } from '../lib/spinner.js'; // adjust path as needed
|
|
9
17
|
export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
|
|
10
|
-
const
|
|
18
|
+
const spinner = new Spinner('Fetching pull requests and diffs...');
|
|
19
|
+
spinner.start();
|
|
11
20
|
const filtered = [];
|
|
12
21
|
const failedPRs = [];
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
21
30
|
try {
|
|
22
|
-
|
|
31
|
+
const prNumber = pr.number;
|
|
23
32
|
const prRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
|
|
24
33
|
headers: {
|
|
25
34
|
Authorization: `token ${token}`,
|
|
26
35
|
Accept: 'application/vnd.github.v3+json',
|
|
27
36
|
},
|
|
28
37
|
});
|
|
29
|
-
if (!prRes.ok)
|
|
30
|
-
throw new Error(`Failed to fetch full PR
|
|
31
|
-
}
|
|
38
|
+
if (!prRes.ok)
|
|
39
|
+
throw new Error(`Failed to fetch full PR #${prNumber}`);
|
|
32
40
|
const fullPR = await prRes.json();
|
|
33
|
-
pr.body = fullPR.body ?? '';
|
|
34
|
-
|
|
35
|
-
const diffUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}.diff`;
|
|
36
|
-
const diffRes = await fetch(diffUrl, {
|
|
41
|
+
pr.body = fullPR.body ?? '';
|
|
42
|
+
const diffRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}.diff`, {
|
|
37
43
|
headers: {
|
|
38
44
|
Authorization: `token ${token}`,
|
|
39
45
|
Accept: 'application/vnd.github.v3.diff',
|
|
40
46
|
},
|
|
41
47
|
});
|
|
42
|
-
if (!diffRes.ok)
|
|
43
|
-
throw new Error(
|
|
44
|
-
}
|
|
48
|
+
if (!diffRes.ok)
|
|
49
|
+
throw new Error(`Failed to fetch diff for PR #${prNumber}`);
|
|
45
50
|
const diff = await diffRes.text();
|
|
46
51
|
filtered.push({ pr, diff });
|
|
47
52
|
}
|
|
48
53
|
catch (err) {
|
|
49
|
-
console.warn(`⚠️ Skipping PR #${pr.number}
|
|
54
|
+
console.warn(`⚠️ Skipping PR #${pr.number}: ${err.message}`);
|
|
50
55
|
failedPRs.push(pr);
|
|
51
56
|
}
|
|
52
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);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
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}`);
|
|
70
|
+
}
|
|
71
|
+
return filtered;
|
|
53
72
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
catch (err) {
|
|
74
|
+
spinner.fail(`Error fetching pull requests: ${err.message}`);
|
|
75
|
+
return [];
|
|
57
76
|
}
|
|
58
|
-
return filtered;
|
|
59
77
|
}
|
|
60
|
-
//
|
|
78
|
+
// Prompt user to select PR
|
|
61
79
|
function askUserToPickPR(prs) {
|
|
62
80
|
return new Promise((resolve) => {
|
|
63
81
|
if (prs.length === 0) {
|
|
64
82
|
console.log("⚠️ No pull requests with review requested.");
|
|
65
83
|
return resolve(null);
|
|
66
84
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
const rows = prs.map((pr, i) => ({
|
|
86
|
+
'#': i + 1,
|
|
87
|
+
ID: `#${pr.number}`,
|
|
88
|
+
Title: truncate(pr.title, 50),
|
|
89
|
+
Author: pr.user || '—',
|
|
90
|
+
Status: pr.draft ? 'Draft' : 'Open',
|
|
91
|
+
Created: pr.created_at?.split('T')[0] || '',
|
|
92
|
+
Reviewers: pr.requested_reviewers?.map(r => r.login).join(', ') || '—',
|
|
93
|
+
}));
|
|
94
|
+
console.log(chalk.blue("\n📦 Open Pull Requests:"));
|
|
95
|
+
console.log(columnify(rows, {
|
|
96
|
+
columnSplitter: ' ',
|
|
97
|
+
headingTransform: (h) => chalk.cyan(h.toUpperCase()),
|
|
98
|
+
config: {
|
|
99
|
+
Title: { maxWidth: 50 },
|
|
100
|
+
Reviewers: { maxWidth: 30 }
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
104
|
+
rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
|
|
105
|
+
rl.close();
|
|
106
|
+
const index = parseInt(answer, 10);
|
|
107
|
+
if (!isNaN(index) && index >= 1 && index <= prs.length) {
|
|
108
|
+
resolve(index - 1);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log('⚠️ Invalid selection.');
|
|
112
|
+
resolve(null);
|
|
113
|
+
}
|
|
74
114
|
});
|
|
75
|
-
const askQuestion = () => {
|
|
76
|
-
rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
|
|
77
|
-
const index = parseInt(answer, 10);
|
|
78
|
-
if (!isNaN(index) && index >= 1 && index <= prs.length) {
|
|
79
|
-
resolve(index - 1); // Return array index, not PR number
|
|
80
|
-
rl.close();
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
console.log('⚠️ Invalid selection. Please enter a number between 1 and ' + prs.length);
|
|
84
|
-
askQuestion(); // Retry asking for input
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
};
|
|
88
|
-
askQuestion(); // Initial call to ask the user
|
|
89
115
|
});
|
|
90
116
|
}
|
|
91
|
-
//
|
|
117
|
+
// Prompt for review method
|
|
92
118
|
function askReviewMethod() {
|
|
93
119
|
return new Promise((resolve) => {
|
|
94
|
-
const rl = readline.createInterface({
|
|
95
|
-
input: process.stdin,
|
|
96
|
-
output: process.stdout,
|
|
97
|
-
});
|
|
120
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
98
121
|
console.log("\n🔍 Choose review method:");
|
|
99
122
|
console.log('1) Review whole PR at once');
|
|
100
123
|
console.log('2) Review chunk by chunk');
|
|
101
124
|
rl.question(`👉 Choose an option [1-2]: `, (answer) => {
|
|
102
125
|
rl.close();
|
|
103
|
-
|
|
104
|
-
resolve('whole');
|
|
105
|
-
}
|
|
106
|
-
else if (answer === '2') {
|
|
107
|
-
resolve('chunk');
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
console.log('⚠️ Invalid selection. Defaulting to "whole".');
|
|
111
|
-
resolve('whole');
|
|
112
|
-
}
|
|
126
|
+
resolve(answer === '2' ? 'chunk' : 'whole');
|
|
113
127
|
});
|
|
114
128
|
});
|
|
115
129
|
}
|
|
116
|
-
//
|
|
117
|
-
function askReviewApproval(
|
|
130
|
+
// Prompt for review approval
|
|
131
|
+
function askReviewApproval() {
|
|
118
132
|
return new Promise((resolve) => {
|
|
119
|
-
console.log('\n💡 AI-suggested review:\n');
|
|
120
|
-
console.log(suggestion);
|
|
121
133
|
console.log('\n---');
|
|
122
134
|
console.log('1) ✅ Approve');
|
|
123
135
|
console.log('2) ❌ Reject');
|
|
124
136
|
console.log('3) ✍️ Edit');
|
|
125
137
|
console.log('4) ⌨️ Write your own review');
|
|
126
138
|
console.log('5) 🚫 Cancel');
|
|
127
|
-
const rl = readline.createInterface({
|
|
128
|
-
input: process.stdin,
|
|
129
|
-
output: process.stdout,
|
|
130
|
-
});
|
|
139
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
131
140
|
rl.question(`\n👉 Choose an option [1-5]: `, (answer) => {
|
|
132
141
|
rl.close();
|
|
133
|
-
if (answer === '1')
|
|
142
|
+
if (answer === '1')
|
|
134
143
|
resolve('approve');
|
|
135
|
-
|
|
136
|
-
else if (answer === '2') {
|
|
144
|
+
else if (answer === '2')
|
|
137
145
|
resolve('reject');
|
|
138
|
-
|
|
139
|
-
else if (answer === '3') {
|
|
146
|
+
else if (answer === '3')
|
|
140
147
|
resolve('edit');
|
|
141
|
-
|
|
142
|
-
else if (answer === '4') {
|
|
148
|
+
else if (answer === '4')
|
|
143
149
|
resolve('custom');
|
|
144
|
-
|
|
145
|
-
else if (answer === '5') {
|
|
150
|
+
else
|
|
146
151
|
resolve('cancel');
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
console.log('⚠️ Invalid selection. Defaulting to "approve".');
|
|
150
|
-
resolve('approve');
|
|
151
|
-
}
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
-
// Prompt for custom review
|
|
155
|
+
// Prompt for custom review
|
|
156
156
|
function promptCustomReview() {
|
|
157
157
|
return new Promise((resolve) => {
|
|
158
|
-
const rl = readline.createInterface({
|
|
159
|
-
input: process.stdin,
|
|
160
|
-
output: process.stdout,
|
|
161
|
-
});
|
|
158
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
162
159
|
rl.question('\n📝 Enter your custom review:\n> ', (input) => {
|
|
163
160
|
rl.close();
|
|
164
161
|
resolve(input.trim());
|
|
165
162
|
});
|
|
166
163
|
});
|
|
167
164
|
}
|
|
165
|
+
// Prompt for editing review
|
|
166
|
+
export async function promptEditReview(suggestedReview) {
|
|
167
|
+
const tmpFilePath = path.join(os.tmpdir(), 'scai-review.txt');
|
|
168
|
+
fs.writeFileSync(tmpFilePath, `# Edit your review below.\n# Lines starting with '#' will be ignored.\n\n${suggestedReview}`);
|
|
169
|
+
const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'vi');
|
|
170
|
+
spawnSync(editor, [tmpFilePath], { stdio: 'inherit' });
|
|
171
|
+
const editedContent = fs.readFileSync(tmpFilePath, 'utf-8');
|
|
172
|
+
return editedContent
|
|
173
|
+
.split('\n')
|
|
174
|
+
.filter(line => !line.trim().startsWith('#'))
|
|
175
|
+
.join('\n')
|
|
176
|
+
.trim() || suggestedReview;
|
|
177
|
+
}
|
|
178
|
+
// Split diff into file-based chunks
|
|
168
179
|
function chunkDiff(diff) {
|
|
169
180
|
const rawChunks = diff.split(/^diff --git /m).filter(Boolean);
|
|
170
181
|
return rawChunks.map(chunk => {
|
|
171
182
|
const fullChunk = 'diff --git ' + chunk;
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
const filePathMatch = fullChunk.match(/^diff --git a\/(.+?) b\//);
|
|
184
|
+
const filePath = filePathMatch ? filePathMatch[1] : 'unknown';
|
|
185
|
+
return {
|
|
186
|
+
filePath,
|
|
187
|
+
content: fullChunk,
|
|
188
|
+
};
|
|
176
189
|
});
|
|
177
190
|
}
|
|
178
|
-
//
|
|
191
|
+
// Color lines in diff
|
|
179
192
|
function colorDiffLine(line) {
|
|
180
|
-
if (line.startsWith('+'))
|
|
181
|
-
return chalk.green(line);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return chalk.yellow(line); // Modified lines (with context)
|
|
188
|
-
}
|
|
189
|
-
return line; // Default case (unchanged lines)
|
|
193
|
+
if (line.startsWith('+'))
|
|
194
|
+
return chalk.green(line);
|
|
195
|
+
if (line.startsWith('-'))
|
|
196
|
+
return chalk.red(line);
|
|
197
|
+
if (line.startsWith('@@'))
|
|
198
|
+
return chalk.yellow(line);
|
|
199
|
+
return line;
|
|
190
200
|
}
|
|
191
|
-
//
|
|
201
|
+
// Review a single chunk
|
|
192
202
|
export async function reviewChunk(chunk, chunkIndex, totalChunks) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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`);
|
|
199
215
|
console.log(coloredDiff);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
console.log(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
227
|
+
}
|
|
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
|
+
});
|
|
207
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
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// Main command to review PR
|
|
208
298
|
export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
209
299
|
try {
|
|
210
|
-
|
|
211
|
-
const token = await ensureGitHubAuth(); // Get GitHub token
|
|
300
|
+
const token = await ensureGitHubAuth();
|
|
212
301
|
const username = await getGitHubUsername(token);
|
|
213
|
-
const { owner, repo } = getRepoDetails();
|
|
214
|
-
console.log(`👤 Authenticated user: ${username}`);
|
|
215
|
-
console.log(`📦 GitHub repo: ${owner}/${repo}`);
|
|
216
|
-
console.log(`🔍 Filtering ${showAll ? "all" : "user-specific"} PRs for branch: ${branch}`);
|
|
302
|
+
const { owner, repo } = getRepoDetails();
|
|
217
303
|
const prsWithReviewRequested = await getPullRequestsForReview(token, owner, repo, username, branch, !showAll);
|
|
218
|
-
|
|
219
|
-
if (prsWithReviewRequested.length === 0) {
|
|
220
|
-
console.log("⚠️ No PRs found with review requested.");
|
|
304
|
+
if (prsWithReviewRequested.length === 0)
|
|
221
305
|
return;
|
|
222
|
-
}
|
|
223
306
|
const selectedIndex = await askUserToPickPR(prsWithReviewRequested.map(p => p.pr));
|
|
224
307
|
if (selectedIndex === null)
|
|
225
308
|
return;
|
|
226
309
|
const { pr, diff } = prsWithReviewRequested[selectedIndex];
|
|
227
310
|
if (pr.body) {
|
|
228
|
-
console.log(chalk.magentaBright('\n📝
|
|
229
|
-
console.log(chalk.gray(pr.body));
|
|
230
|
-
console.log(chalk.magentaBright('\n---\n'));
|
|
311
|
+
console.log(chalk.magentaBright('\n📝 PR Description:\n') + chalk.gray(pr.body));
|
|
231
312
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
const chunkComments = {}; // Declare here for global access
|
|
238
|
-
const reviewMethod = await askReviewMethod(); // Ask user for review method (whole or chunk)
|
|
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
|
|
239
317
|
if (reviewMethod === 'whole') {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Store the entire review choice in chunkComments
|
|
245
|
-
const finalReviewChoice = await askReviewApproval('Do you approve, reject, or leave a final review for this PR?');
|
|
318
|
+
const suggestion = await reviewModule.run({ content: diff, filepath: 'Whole PR Diff' });
|
|
319
|
+
console.log(chalk.yellowBright("Suggestion: ", suggestion));
|
|
320
|
+
const finalReviewChoice = await askReviewApproval();
|
|
321
|
+
let reviewText = '';
|
|
246
322
|
if (finalReviewChoice === 'approve') {
|
|
247
|
-
|
|
248
|
-
await
|
|
249
|
-
await submitReview(pr.number, 'PR approved', 'APPROVE');
|
|
323
|
+
reviewText = 'PR approved';
|
|
324
|
+
await submitReview(pr.number, suggestion.content, 'APPROVE'); // Final review approval
|
|
250
325
|
}
|
|
251
326
|
else if (finalReviewChoice === 'reject') {
|
|
252
|
-
|
|
253
|
-
await
|
|
254
|
-
await submitReview(pr.number, 'Changes requested', 'REQUEST_CHANGES');
|
|
327
|
+
reviewText = 'Changes requested';
|
|
328
|
+
await submitReview(pr.number, suggestion.content, 'REQUEST_CHANGES'); // Final review rejection
|
|
255
329
|
}
|
|
256
|
-
else if (finalReviewChoice === '
|
|
257
|
-
|
|
258
|
-
|
|
330
|
+
else if (finalReviewChoice === 'custom') {
|
|
331
|
+
reviewText = await promptCustomReview();
|
|
332
|
+
await submitReview(pr.number, reviewText, 'COMMENT'); // Custom comment
|
|
259
333
|
}
|
|
260
|
-
else {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
await postReviewComments(pr, chunkComments, token);
|
|
264
|
-
await submitReview(pr.number, customReview, 'COMMENT');
|
|
334
|
+
else if (finalReviewChoice === 'edit') {
|
|
335
|
+
reviewText = await promptEditReview(suggestion.content);
|
|
336
|
+
await submitReview(pr.number, reviewText, 'COMMENT'); // Edited comment
|
|
265
337
|
}
|
|
266
338
|
}
|
|
267
339
|
else {
|
|
268
|
-
const chunks = chunkDiff(prDiff); // Split the diff into chunks
|
|
269
|
-
// Log the total number of chunks
|
|
270
340
|
console.log(chalk.cyan(`🔍 Total Chunks: ${chunks.length}`));
|
|
271
|
-
// Iterate over each chunk, passing the index and total chunk count
|
|
272
341
|
for (let i = 0; i < chunks.length; i++) {
|
|
273
342
|
const chunk = chunks[i];
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
chunkComments[chunk.filePath] = ['Approved'];
|
|
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');
|
|
279
347
|
}
|
|
280
|
-
else if (
|
|
281
|
-
|
|
282
|
-
|
|
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');
|
|
283
352
|
}
|
|
284
|
-
else if (
|
|
285
|
-
console.log('🚪 Review process cancelled.');
|
|
286
|
-
return; // Exit if user cancels
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
console.log('✍️ Custom review added.');
|
|
353
|
+
else if (choice === 'custom') {
|
|
290
354
|
const customReview = await promptCustomReview();
|
|
291
|
-
|
|
292
|
-
|
|
355
|
+
await submitReview(pr.number, customReview, 'COMMENT');
|
|
356
|
+
}
|
|
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');
|
|
293
363
|
}
|
|
294
364
|
}
|
|
295
|
-
// After
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
await postReviewComments(pr, chunkComments, token);
|
|
300
|
-
await submitReview(pr.number, 'PR approved', 'APPROVE');
|
|
301
|
-
}
|
|
302
|
-
else if (finalReviewChoice === 'reject') {
|
|
303
|
-
console.log(`❌ Review for PR #${pr.number} rejected.`);
|
|
304
|
-
await postReviewComments(pr, chunkComments, token);
|
|
305
|
-
await submitReview(pr.number, 'Changes requested', 'REQUEST_CHANGES');
|
|
306
|
-
}
|
|
307
|
-
else if (finalReviewChoice === 'cancel') {
|
|
308
|
-
console.log(`🚪 Review process cancelled.`);
|
|
309
|
-
return;
|
|
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
|
|
310
369
|
}
|
|
311
370
|
else {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
await postReviewComments(pr, chunkComments, token);
|
|
315
|
-
await submitReview(pr.number, customReview, 'COMMENT');
|
|
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
|
|
316
373
|
}
|
|
317
374
|
}
|
|
318
375
|
}
|
|
@@ -320,15 +377,3 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
|
320
377
|
console.error("❌ Error reviewing PR:", err.message);
|
|
321
378
|
}
|
|
322
379
|
}
|
|
323
|
-
// Function to post all comments to GitHub after the review
|
|
324
|
-
async function postReviewComments(pr, chunkComments, token) {
|
|
325
|
-
const { owner, repo } = getRepoDetails(); // Get the repo details
|
|
326
|
-
for (const chunk in chunkComments) {
|
|
327
|
-
const comments = chunkComments[chunk];
|
|
328
|
-
const fileName = chunk; // Use chunk's file path
|
|
329
|
-
const lineNumber = 10; // Extract the actual line number from the chunk content
|
|
330
|
-
for (const comment of comments) {
|
|
331
|
-
await postReviewComment(token, owner, repo, pr.number, fileName, lineNumber, comment);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
@@ -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}
|
package/dist/utils/summarizer.js
CHANGED