scai 0.1.83 → 0.1.84
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.
|
@@ -2,15 +2,13 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { runModulePipeline } from '../pipeline/runModulePipeline.js';
|
|
4
4
|
import { addCommentsModule } from '../pipeline/modules/commentModule.js';
|
|
5
|
+
import { normalizePath } from '../utils/normalizePath.js';
|
|
5
6
|
export async function handleRefactor(filepath, options = {}) {
|
|
6
7
|
try {
|
|
7
|
-
// Normalize
|
|
8
|
-
|
|
9
|
-
filepath = `./${filepath}`;
|
|
10
|
-
}
|
|
8
|
+
// Normalize and resolve filepath (includes expanding ~ and consistent slashes)
|
|
9
|
+
filepath = normalizePath(filepath);
|
|
11
10
|
const { dir, name, ext } = path.parse(filepath);
|
|
12
11
|
const refactoredPath = path.join(dir, `${name}.refactored${ext}`);
|
|
13
|
-
// --apply flag: use existing refactored file and overwrite original
|
|
14
12
|
if (options.apply) {
|
|
15
13
|
try {
|
|
16
14
|
const refactoredCode = await fs.readFile(refactoredPath, 'utf-8');
|
|
@@ -23,13 +21,10 @@ export async function handleRefactor(filepath, options = {}) {
|
|
|
23
21
|
}
|
|
24
22
|
return;
|
|
25
23
|
}
|
|
26
|
-
// Read source code
|
|
27
24
|
const content = await fs.readFile(filepath, 'utf-8');
|
|
28
|
-
// Run through pipeline modules
|
|
29
25
|
const response = await runModulePipeline([addCommentsModule], { content });
|
|
30
26
|
if (!response.content.trim())
|
|
31
27
|
throw new Error('⚠️ Model returned empty result');
|
|
32
|
-
// Save refactored output
|
|
33
28
|
await fs.writeFile(refactoredPath, response.content, 'utf-8');
|
|
34
29
|
console.log(`✅ Refactored code saved to: ${refactoredPath}`);
|
|
35
30
|
console.log(`ℹ️ Run again with '--apply' to overwrite the original.`);
|
|
@@ -164,24 +164,6 @@ function askReviewApproval() {
|
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
|
-
function askFinalReviewApproval() {
|
|
168
|
-
return new Promise((resolve) => {
|
|
169
|
-
console.log('\n---');
|
|
170
|
-
console.log('1) ✅ Approve');
|
|
171
|
-
console.log('2) ❌ Request Changes');
|
|
172
|
-
console.log('3) 🚫 Cancel');
|
|
173
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
174
|
-
rl.question(`\n👉 Choose an option [1-3]: `, (answer) => {
|
|
175
|
-
rl.close();
|
|
176
|
-
if (answer === '1')
|
|
177
|
-
resolve('approve');
|
|
178
|
-
else if (answer === '2')
|
|
179
|
-
resolve('request-changes');
|
|
180
|
-
else
|
|
181
|
-
resolve('cancel');
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
167
|
// Prompt for custom review
|
|
186
168
|
function promptCustomReview() {
|
|
187
169
|
return new Promise((resolve) => {
|
|
@@ -212,23 +194,20 @@ function chunkDiff(diff, review_id) {
|
|
|
212
194
|
const fullChunk = 'diff --git ' + chunk;
|
|
213
195
|
const filePathMatch = fullChunk.match(/^diff --git a\/(.+?) b\//);
|
|
214
196
|
const filePath = filePathMatch ? filePathMatch[1] : 'unknown';
|
|
215
|
-
// Now we extract hunks and lines as per the DiffHunk type
|
|
216
197
|
const hunks = [];
|
|
217
198
|
let currentHunk = null;
|
|
218
|
-
//
|
|
199
|
+
// This counts diff lines for *this file only* (context/+/- lines after first @@)
|
|
200
|
+
let positionCounter = 0;
|
|
219
201
|
const lines = fullChunk.split('\n');
|
|
220
202
|
lines.forEach(line => {
|
|
221
|
-
const hunkHeaderMatch = line.match(/^@@ -(\d+)
|
|
203
|
+
const hunkHeaderMatch = line.match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
|
222
204
|
if (hunkHeaderMatch) {
|
|
223
|
-
|
|
224
|
-
if (currentHunk) {
|
|
205
|
+
if (currentHunk)
|
|
225
206
|
hunks.push(currentHunk);
|
|
226
|
-
}
|
|
227
|
-
// Parse the hunk header
|
|
228
207
|
const oldStart = parseInt(hunkHeaderMatch[1], 10);
|
|
229
208
|
const newStart = parseInt(hunkHeaderMatch[3], 10);
|
|
230
|
-
const oldLines = parseInt(hunkHeaderMatch[2], 10);
|
|
231
|
-
const newLines = parseInt(hunkHeaderMatch[4], 10);
|
|
209
|
+
const oldLines = parseInt(hunkHeaderMatch[2] || '0', 10);
|
|
210
|
+
const newLines = parseInt(hunkHeaderMatch[4] || '0', 10);
|
|
232
211
|
currentHunk = {
|
|
233
212
|
oldStart,
|
|
234
213
|
newStart,
|
|
@@ -236,33 +215,33 @@ function chunkDiff(diff, review_id) {
|
|
|
236
215
|
newLines,
|
|
237
216
|
lines: [],
|
|
238
217
|
};
|
|
218
|
+
return; // don’t count @@ header line in positionCounter
|
|
239
219
|
}
|
|
240
|
-
|
|
241
|
-
//
|
|
220
|
+
if (currentHunk) {
|
|
221
|
+
// Each line after @@ counts towards the diff position
|
|
222
|
+
positionCounter++;
|
|
242
223
|
let lineType = 'context';
|
|
243
224
|
if (line.startsWith('+'))
|
|
244
225
|
lineType = 'add';
|
|
245
226
|
if (line.startsWith('-'))
|
|
246
227
|
lineType = 'del';
|
|
247
|
-
// Create the DiffLine object
|
|
248
228
|
currentHunk.lines.push({
|
|
249
229
|
line,
|
|
250
230
|
type: lineType,
|
|
251
231
|
lineNumberOld: lineType === 'del' ? currentHunk.oldStart++ : undefined,
|
|
252
232
|
lineNumberNew: lineType === 'add' ? currentHunk.newStart++ : undefined,
|
|
253
|
-
|
|
233
|
+
position: positionCounter, // <-- key for GitHub inline API
|
|
234
|
+
review_id,
|
|
254
235
|
});
|
|
255
236
|
}
|
|
256
237
|
});
|
|
257
|
-
|
|
258
|
-
if (currentHunk) {
|
|
238
|
+
if (currentHunk)
|
|
259
239
|
hunks.push(currentHunk);
|
|
260
|
-
}
|
|
261
240
|
return {
|
|
262
241
|
filePath,
|
|
263
242
|
content: fullChunk,
|
|
264
|
-
hunks,
|
|
265
|
-
review_id,
|
|
243
|
+
hunks,
|
|
244
|
+
review_id,
|
|
266
245
|
};
|
|
267
246
|
});
|
|
268
247
|
}
|
|
@@ -419,48 +398,36 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
|
419
398
|
for (let i = 0; i < chunks.length; i++) {
|
|
420
399
|
const chunk = chunks[i];
|
|
421
400
|
const { choice, summary } = await reviewChunk(chunk, i, chunks.length);
|
|
422
|
-
if (choice === '
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
body: summary,
|
|
426
|
-
line: chunk.hunks[0]?.newStart || 1,
|
|
427
|
-
side: 'RIGHT',
|
|
428
|
-
});
|
|
429
|
-
console.log(`💬 Posted AI review for chunk ${i + 1}`);
|
|
430
|
-
}
|
|
431
|
-
else if (choice === 'reject') {
|
|
432
|
-
allApproved = false;
|
|
433
|
-
reviewComments.push({
|
|
434
|
-
path: chunk.filePath,
|
|
435
|
-
body: summary,
|
|
436
|
-
line: chunk.hunks[0]?.newStart || 1,
|
|
437
|
-
side: 'RIGHT',
|
|
438
|
-
});
|
|
401
|
+
if (choice === 'cancel' || choice === 'skip') {
|
|
402
|
+
console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
|
|
403
|
+
continue;
|
|
439
404
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
});
|
|
405
|
+
// Find the first line in the chunk with a valid position (usually the first 'add' or 'context' line)
|
|
406
|
+
const firstLineWithPosition = chunk.hunks
|
|
407
|
+
.flatMap(hunk => hunk.lines)
|
|
408
|
+
.find(line => line.position !== undefined);
|
|
409
|
+
if (!firstLineWithPosition) {
|
|
410
|
+
console.warn(`⚠️ Could not find valid position for inline comment in chunk ${i + 1}. Skipping comment.`);
|
|
411
|
+
continue;
|
|
448
412
|
}
|
|
449
|
-
|
|
450
|
-
|
|
413
|
+
let commentBody = summary;
|
|
414
|
+
if (choice === 'custom') {
|
|
415
|
+
commentBody = await promptCustomReview();
|
|
451
416
|
}
|
|
452
|
-
else if (typeof choice === 'string') {
|
|
453
|
-
|
|
454
|
-
path: chunk.filePath,
|
|
455
|
-
body: choice,
|
|
456
|
-
line: chunk.hunks[0]?.newStart || 1,
|
|
457
|
-
side: 'RIGHT',
|
|
458
|
-
});
|
|
417
|
+
else if (typeof choice === 'string' && !['approve', 'reject', 'custom'].includes(choice)) {
|
|
418
|
+
commentBody = choice;
|
|
459
419
|
}
|
|
420
|
+
reviewComments.push({
|
|
421
|
+
path: chunk.filePath,
|
|
422
|
+
body: commentBody,
|
|
423
|
+
position: firstLineWithPosition.position,
|
|
424
|
+
});
|
|
425
|
+
if (choice === 'reject')
|
|
426
|
+
allApproved = false;
|
|
460
427
|
}
|
|
461
428
|
console.log(chalk.blueBright('\n📝 Review Comments Preview:'));
|
|
462
429
|
reviewComments.forEach((comment, idx) => {
|
|
463
|
-
console.log(`${idx + 1}. ${comment.path}:${comment.
|
|
430
|
+
console.log(`${idx + 1}. ${comment.path}:${comment.position} — ${comment.body}`);
|
|
464
431
|
});
|
|
465
432
|
const shouldApprove = allApproved;
|
|
466
433
|
const hasInlineComments = reviewComments.length > 0;
|
package/dist/github/github.js
CHANGED
|
@@ -81,30 +81,29 @@ export async function submitReview(prNumber, body, event, comments) {
|
|
|
81
81
|
const token = await ensureGitHubAuth();
|
|
82
82
|
const { owner, repo } = await getRepoDetails();
|
|
83
83
|
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`;
|
|
84
|
+
// Prepare payload
|
|
85
|
+
const payload = { body, event };
|
|
86
|
+
if (comments && comments.length > 0) {
|
|
87
|
+
payload.comments = comments;
|
|
88
|
+
}
|
|
84
89
|
const res = await fetch(url, {
|
|
85
90
|
method: 'POST',
|
|
86
91
|
headers: {
|
|
87
92
|
Authorization: `token ${token}`,
|
|
88
93
|
Accept: 'application/vnd.github.v3+json',
|
|
89
94
|
},
|
|
90
|
-
body: JSON.stringify(
|
|
91
|
-
body,
|
|
92
|
-
event,
|
|
93
|
-
comments,
|
|
94
|
-
}),
|
|
95
|
+
body: JSON.stringify(payload),
|
|
95
96
|
});
|
|
96
97
|
if (!res.ok) {
|
|
97
98
|
const errorText = await res.text();
|
|
98
|
-
// Attempt to parse error body
|
|
99
99
|
let parsed = {};
|
|
100
100
|
try {
|
|
101
101
|
parsed = JSON.parse(errorText);
|
|
102
102
|
}
|
|
103
103
|
catch (_) {
|
|
104
|
-
//
|
|
104
|
+
// fallback to raw text
|
|
105
105
|
}
|
|
106
|
-
const knownErrors = Array.isArray(parsed.errors) ? parsed.errors.join('; ') : '';
|
|
107
|
-
// Handle known error cases
|
|
106
|
+
const knownErrors = Array.isArray(parsed.errors) ? parsed.errors.map((e) => e.message || e).join('; ') : '';
|
|
108
107
|
if (res.status === 422) {
|
|
109
108
|
if (knownErrors.includes('Can not approve your own pull request')) {
|
|
110
109
|
console.warn(`⚠️ Skipping approval: You cannot approve your own pull request.`);
|
|
@@ -114,8 +113,8 @@ export async function submitReview(prNumber, body, event, comments) {
|
|
|
114
113
|
console.warn(`⚠️ Cannot post comments: PR has no diff.`);
|
|
115
114
|
return;
|
|
116
115
|
}
|
|
117
|
-
if (knownErrors.includes('path is missing') || knownErrors.includes('line is missing')) {
|
|
118
|
-
console.warn(`⚠️ Some inline comments are missing
|
|
116
|
+
if (knownErrors.includes('path is missing') || knownErrors.includes('line is missing') || knownErrors.includes('position is missing')) {
|
|
117
|
+
console.warn(`⚠️ Some inline comments are missing required fields. Skipping review.`);
|
|
119
118
|
return;
|
|
120
119
|
}
|
|
121
120
|
if (knownErrors.includes('Position is invalid') || knownErrors.includes('line must be part of the diff')) {
|
|
@@ -123,7 +122,6 @@ export async function submitReview(prNumber, body, event, comments) {
|
|
|
123
122
|
return;
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
|
-
// Unknown error
|
|
127
125
|
throw new Error(`Failed to submit review: ${res.status} ${res.statusText} - ${errorText}`);
|
|
128
126
|
}
|
|
129
127
|
console.log(`✅ Submitted ${event} review for PR #${prNumber}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/utils/normalizePath.ts
|
|
2
|
+
import os from 'os';
|
|
2
3
|
import path from "path";
|
|
3
4
|
/**
|
|
4
5
|
* Normalizes a path string for loose, fuzzy matching:
|
|
@@ -11,6 +12,9 @@ export function normalizePathForLooseMatch(p) {
|
|
|
11
12
|
}
|
|
12
13
|
// Helper to normalize and resolve paths to a consistent format (forward slashes)
|
|
13
14
|
export function normalizePath(p) {
|
|
15
|
+
if (p.startsWith('~')) {
|
|
16
|
+
p = path.join(os.homedir(), p.slice(1));
|
|
17
|
+
}
|
|
14
18
|
return path.resolve(p).replace(/\\/g, '/');
|
|
15
19
|
}
|
|
16
20
|
export function getRepoKeyForPath(pathToMatch, config) {
|