@blockdeepanshu/ai-pr-review-cli 1.2.0
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/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +470 -0
- package/batch-review.js +409 -0
- package/bin/cli.js +113 -0
- package/package.json +72 -0
- package/src/ai.js +38 -0
- package/src/config.js +52 -0
- package/src/git.js +190 -0
- package/src/prompt.js +22 -0
- package/src/reviewer.js +207 -0
package/batch-review.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Batch Review Script
|
|
5
|
+
* Automatically reviews large repositories in chunks to avoid rate limits
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
async function batchReview() {
|
|
12
|
+
const isFrontend = process.argv.includes('--frontend');
|
|
13
|
+
const batchSizeArg = process.argv.indexOf('--batch-size');
|
|
14
|
+
const defaultBatchSize = batchSizeArg > -1 ? parseInt(process.argv[batchSizeArg + 1]) : 8;
|
|
15
|
+
|
|
16
|
+
console.log('š¤ Starting Batch AI Review...\n');
|
|
17
|
+
if (isFrontend) {
|
|
18
|
+
console.log('šØ Frontend optimization enabled');
|
|
19
|
+
}
|
|
20
|
+
console.log('DEBUG: Current directory:', process.cwd());
|
|
21
|
+
console.log('DEBUG: Default batch size:', defaultBatchSize);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Get all changed files (both staged and unstaged)
|
|
25
|
+
let changedFiles = [];
|
|
26
|
+
try {
|
|
27
|
+
// Try staged files first
|
|
28
|
+
const staged = execSync('git diff --name-only --cached', { encoding: 'utf-8' })
|
|
29
|
+
.split('\n')
|
|
30
|
+
.filter(f => f.trim().length > 0);
|
|
31
|
+
|
|
32
|
+
// Try unstaged files
|
|
33
|
+
const unstaged = execSync('git diff --name-only', { encoding: 'utf-8' })
|
|
34
|
+
.split('\n')
|
|
35
|
+
.filter(f => f.trim().length > 0);
|
|
36
|
+
|
|
37
|
+
// Combine and deduplicate
|
|
38
|
+
changedFiles = [...new Set([...staged, ...unstaged])];
|
|
39
|
+
|
|
40
|
+
console.log('DEBUG: Staged files:', staged);
|
|
41
|
+
console.log('DEBUG: Unstaged files:', unstaged);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.log('DEBUG: Git diff failed:', error.message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (changedFiles.length === 0) {
|
|
47
|
+
console.log('ā No changes detected. Nothing to review.');
|
|
48
|
+
console.log('\nš” To include files for review:');
|
|
49
|
+
console.log(' git add your-file.js # Stage specific file');
|
|
50
|
+
console.log(' git add . # Stage all changes');
|
|
51
|
+
console.log(' git status # See what can be staged');
|
|
52
|
+
console.log('\nš The tool reviews:');
|
|
53
|
+
console.log(' ⢠Staged files (git add)');
|
|
54
|
+
console.log(' ⢠Uncommitted changes');
|
|
55
|
+
console.log(' ⢠Commits since base branch');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`š Found ${changedFiles.length} changed files`);
|
|
60
|
+
console.log('Files:', changedFiles.join(', '));
|
|
61
|
+
|
|
62
|
+
// Group files by type/directory for logical batching
|
|
63
|
+
const batches = createBatches(changedFiles, defaultBatchSize, isFrontend);
|
|
64
|
+
|
|
65
|
+
console.log(`\nš Creating ${batches.length} review batches...\n`);
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < batches.length; i++) {
|
|
68
|
+
const batch = batches[i];
|
|
69
|
+
console.log(`--- Batch ${i + 1}/${batches.length}: ${batch.name} ---`);
|
|
70
|
+
console.log(`Files: ${batch.files.join(', ')}`);
|
|
71
|
+
|
|
72
|
+
// Stage only these files
|
|
73
|
+
execSync('git reset'); // Clear staging area
|
|
74
|
+
batch.files.forEach(file => {
|
|
75
|
+
try {
|
|
76
|
+
execSync(`git add "${file}"`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.log(`ā ļø Could not add ${file}: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Review this batch
|
|
83
|
+
try {
|
|
84
|
+
console.log('š Reviewing...');
|
|
85
|
+
|
|
86
|
+
// Check if we're in dry-run mode
|
|
87
|
+
const isDryRun = process.argv.includes('--dry-run');
|
|
88
|
+
|
|
89
|
+
if (isDryRun) {
|
|
90
|
+
console.log('DRY RUN: Would run: node bin/cli.js review');
|
|
91
|
+
console.log('DRY RUN: Simulating AI review...\n');
|
|
92
|
+
} else {
|
|
93
|
+
// Use the local development version instead of global npm package
|
|
94
|
+
const path = require('path');
|
|
95
|
+
const cliPath = path.join(__dirname, 'bin', 'cli.js');
|
|
96
|
+
execSync(`node "${cliPath}" review`, { stdio: 'inherit' });
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.log(`ā Review failed for batch ${i + 1}: ${error.message}`);
|
|
100
|
+
console.log('ā³ Waiting 30 seconds before continuing...\n');
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, 30000));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log('\n');
|
|
105
|
+
|
|
106
|
+
// Wait between batches to respect rate limits
|
|
107
|
+
if (i < batches.length - 1) {
|
|
108
|
+
console.log('ā³ Waiting 10 seconds before next batch...\n');
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Reset staging area
|
|
114
|
+
execSync('git reset');
|
|
115
|
+
console.log('ā
Batch review completed!');
|
|
116
|
+
console.log('š” Remember to stage and commit your changes after addressing feedback.');
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('ā Batch review failed:', error.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createBatches(files, batchSize = 8, isFrontend = false) {
|
|
124
|
+
const batches = [];
|
|
125
|
+
|
|
126
|
+
// Filter out files we should skip entirely
|
|
127
|
+
const filteredFiles = files.filter(f => {
|
|
128
|
+
const skipPatterns = [
|
|
129
|
+
/node_modules/,
|
|
130
|
+
/\.git/,
|
|
131
|
+
/dist\//,
|
|
132
|
+
/build\//,
|
|
133
|
+
/coverage\//,
|
|
134
|
+
/\.min\.(js|css)$/,
|
|
135
|
+
/bundle.*\.js$/,
|
|
136
|
+
/chunk.*\.js$/,
|
|
137
|
+
/\.map$/,
|
|
138
|
+
/\.log$/
|
|
139
|
+
];
|
|
140
|
+
return !skipPatterns.some(pattern => pattern.test(f));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Universal grouping (works for all project types)
|
|
144
|
+
let groups;
|
|
145
|
+
|
|
146
|
+
if (isFrontend) {
|
|
147
|
+
// Frontend-specific grouping
|
|
148
|
+
groups = createFrontendGroups(filteredFiles);
|
|
149
|
+
} else {
|
|
150
|
+
// Universal grouping for all project types
|
|
151
|
+
groups = createUniversalGroups(filteredFiles);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create batches from groups with dynamic batch sizes
|
|
155
|
+
Object.entries(groups).forEach(([groupName, groupFiles]) => {
|
|
156
|
+
if (groupFiles.length === 0) return;
|
|
157
|
+
|
|
158
|
+
// Adjust batch size based on file type and frontend optimization
|
|
159
|
+
let dynamicBatchSize = batchSize;
|
|
160
|
+
|
|
161
|
+
if (isFrontend) {
|
|
162
|
+
// Frontend-optimized batch sizes
|
|
163
|
+
if (groupName === 'packageLock') {
|
|
164
|
+
dynamicBatchSize = 1; // Package-lock is huge, review alone
|
|
165
|
+
} else if (groupName === 'styles') {
|
|
166
|
+
dynamicBatchSize = 15; // CSS files are usually smaller in frontend
|
|
167
|
+
} else if (groupName === 'components') {
|
|
168
|
+
dynamicBatchSize = 5; // React/Vue components can be complex
|
|
169
|
+
} else if (groupName === 'pages') {
|
|
170
|
+
dynamicBatchSize = 4; // Pages are usually larger/complex
|
|
171
|
+
} else if (groupName === 'hooks') {
|
|
172
|
+
dynamicBatchSize = 8; // Hooks are typically focused
|
|
173
|
+
} else if (groupName === 'utils') {
|
|
174
|
+
dynamicBatchSize = 10; // Utility functions are usually small
|
|
175
|
+
} else if (groupName === 'config') {
|
|
176
|
+
dynamicBatchSize = 12; // Config files are small
|
|
177
|
+
} else if (groupName === 'tests') {
|
|
178
|
+
dynamicBatchSize = 6; // Test files can be substantial
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Universal batch sizes for all project types
|
|
182
|
+
if (groupName === 'lockFiles') {
|
|
183
|
+
dynamicBatchSize = 1; // Lock files are huge, review alone
|
|
184
|
+
} else if (groupName === 'api') {
|
|
185
|
+
dynamicBatchSize = 6; // API endpoints can be complex
|
|
186
|
+
} else if (groupName === 'services') {
|
|
187
|
+
dynamicBatchSize = 5; // Services contain business logic
|
|
188
|
+
} else if (groupName === 'models') {
|
|
189
|
+
dynamicBatchSize = 8; // Models are usually focused
|
|
190
|
+
} else if (groupName === 'frontend') {
|
|
191
|
+
dynamicBatchSize = 6; // Frontend components in full-stack
|
|
192
|
+
} else if (groupName === 'utils') {
|
|
193
|
+
dynamicBatchSize = 10; // Utility functions are usually small
|
|
194
|
+
} else if (groupName === 'tests') {
|
|
195
|
+
dynamicBatchSize = 8; // Test files vary in size
|
|
196
|
+
} else if (groupName === 'config') {
|
|
197
|
+
dynamicBatchSize = 12; // Config files are typically small
|
|
198
|
+
} else if (groupName === 'infrastructure') {
|
|
199
|
+
dynamicBatchSize = 7; // Infrastructure files can be complex
|
|
200
|
+
} else if (groupName === 'scripts') {
|
|
201
|
+
dynamicBatchSize = 10; // Scripts are usually focused
|
|
202
|
+
} else if (groupName === 'docs') {
|
|
203
|
+
dynamicBatchSize = 15; // Documentation files are usually text
|
|
204
|
+
} else if (groupName === 'styles') {
|
|
205
|
+
dynamicBatchSize = 12; // CSS files
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Split large groups into smaller batches
|
|
210
|
+
for (let i = 0; i < groupFiles.length; i += dynamicBatchSize) {
|
|
211
|
+
const batchFiles = groupFiles.slice(i, i + dynamicBatchSize);
|
|
212
|
+
const batchNumber = Math.floor(i / dynamicBatchSize) + 1;
|
|
213
|
+
const name = groupFiles.length > dynamicBatchSize
|
|
214
|
+
? `${groupName} (batch ${batchNumber})`
|
|
215
|
+
: groupName;
|
|
216
|
+
|
|
217
|
+
batches.push({
|
|
218
|
+
name,
|
|
219
|
+
files: batchFiles
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Sort batches by priority (most important first)
|
|
225
|
+
const priorityOrder = isFrontend ? [
|
|
226
|
+
'components', 'pages', 'hooks', 'utils', 'config',
|
|
227
|
+
'styles', 'tests', 'assets', 'packageLock', 'docs', 'other'
|
|
228
|
+
] : [
|
|
229
|
+
'api', 'services', 'models', 'frontend', 'utils', 'config',
|
|
230
|
+
'tests', 'infrastructure', 'scripts', 'styles', 'lockFiles', 'docs', 'other'
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
batches.sort((a, b) => {
|
|
234
|
+
const aPriority = priorityOrder.findIndex(p => a.name.includes(p));
|
|
235
|
+
const bPriority = priorityOrder.findIndex(p => b.name.includes(p));
|
|
236
|
+
return aPriority - bPriority;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return batches;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function createFrontendGroups(filteredFiles) {
|
|
243
|
+
return {
|
|
244
|
+
components: filteredFiles.filter(f =>
|
|
245
|
+
/\.(js|jsx|ts|tsx|vue)$/.test(f) &&
|
|
246
|
+
/\/(components|ui|widgets)\//.test(f)
|
|
247
|
+
),
|
|
248
|
+
pages: filteredFiles.filter(f =>
|
|
249
|
+
/\.(js|jsx|ts|tsx|vue)$/.test(f) &&
|
|
250
|
+
/\/(pages|views|screens|routes)\//.test(f)
|
|
251
|
+
),
|
|
252
|
+
hooks: filteredFiles.filter(f =>
|
|
253
|
+
/\.(js|jsx|ts|tsx)$/.test(f) &&
|
|
254
|
+
/\/(hooks|composables|custom)\//.test(f)
|
|
255
|
+
),
|
|
256
|
+
utils: filteredFiles.filter(f =>
|
|
257
|
+
/\.(js|jsx|ts|tsx)$/.test(f) &&
|
|
258
|
+
/\/(utils|helpers|lib|shared)\//.test(f)
|
|
259
|
+
),
|
|
260
|
+
styles: filteredFiles.filter(f =>
|
|
261
|
+
/\.(css|scss|sass|less|stylus|module\.css)$/.test(f)
|
|
262
|
+
),
|
|
263
|
+
config: filteredFiles.filter(f => {
|
|
264
|
+
// Split package.json separately due to size
|
|
265
|
+
if (f.includes('package-lock.json')) return false;
|
|
266
|
+
return /\.(json|yml|yaml|toml|js|ts)$/.test(f) &&
|
|
267
|
+
/(config|settings|env|webpack|vite|rollup|babel)/.test(f);
|
|
268
|
+
}),
|
|
269
|
+
packageLock: filteredFiles.filter(f => f.includes('package-lock.json')),
|
|
270
|
+
tests: filteredFiles.filter(f =>
|
|
271
|
+
/\.(test|spec)\.(js|ts|jsx|tsx)$/.test(f) ||
|
|
272
|
+
/\/__tests__\//.test(f)
|
|
273
|
+
),
|
|
274
|
+
docs: filteredFiles.filter(f => /\.(md|txt|rst)$/.test(f)),
|
|
275
|
+
assets: filteredFiles.filter(f =>
|
|
276
|
+
/\/(assets|static|public)\/.*\.(js|ts|jsx|tsx|css|scss)$/.test(f)
|
|
277
|
+
),
|
|
278
|
+
other: filteredFiles.filter(f => {
|
|
279
|
+
const assigned = [
|
|
280
|
+
/(components|ui|widgets|pages|views|screens|routes|hooks|composables|custom|utils|helpers|lib|shared)\//,
|
|
281
|
+
/\.(css|scss|sass|less|stylus|module\.css)$/,
|
|
282
|
+
/(config|settings|env|webpack|vite|rollup|babel)/,
|
|
283
|
+
/package-lock\.json/,
|
|
284
|
+
/\.(test|spec)\.(js|ts|jsx|tsx)$/,
|
|
285
|
+
/\/__tests__\//,
|
|
286
|
+
/\.(md|txt|rst)$/,
|
|
287
|
+
/\/(assets|static|public)\/.*\.(js|ts|jsx|tsx|css|scss)$/
|
|
288
|
+
];
|
|
289
|
+
return !assigned.some(pattern => pattern.test(f));
|
|
290
|
+
})
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
return groups;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function createUniversalGroups(filteredFiles) {
|
|
297
|
+
return {
|
|
298
|
+
// Backend/API files
|
|
299
|
+
api: filteredFiles.filter(f =>
|
|
300
|
+
/\.(js|ts|py|java|go|rb|php|cs)$/.test(f) &&
|
|
301
|
+
/\/(api|routes|controllers|endpoints|handlers)\//.test(f)
|
|
302
|
+
),
|
|
303
|
+
|
|
304
|
+
// Services and business logic
|
|
305
|
+
services: filteredFiles.filter(f =>
|
|
306
|
+
/\.(js|ts|py|java|go|rb|php|cs)$/.test(f) &&
|
|
307
|
+
/\/(services|business|logic|core|domain)\//.test(f)
|
|
308
|
+
),
|
|
309
|
+
|
|
310
|
+
// Models and database
|
|
311
|
+
models: filteredFiles.filter(f =>
|
|
312
|
+
/\.(js|ts|py|java|go|rb|php|cs|sql)$/.test(f) &&
|
|
313
|
+
/\/(models|entities|schemas|database|db|migrations)\//.test(f)
|
|
314
|
+
),
|
|
315
|
+
|
|
316
|
+
// Frontend components (for full-stack projects)
|
|
317
|
+
frontend: filteredFiles.filter(f =>
|
|
318
|
+
/\.(js|jsx|ts|tsx|vue)$/.test(f) &&
|
|
319
|
+
/\/(components|pages|views|screens|ui)\//.test(f)
|
|
320
|
+
),
|
|
321
|
+
|
|
322
|
+
// Utilities and helpers
|
|
323
|
+
utils: filteredFiles.filter(f =>
|
|
324
|
+
/\.(js|ts|py|java|go|rb|php|cs)$/.test(f) &&
|
|
325
|
+
/\/(utils|helpers|lib|shared|common)\//.test(f)
|
|
326
|
+
),
|
|
327
|
+
|
|
328
|
+
// Tests
|
|
329
|
+
tests: filteredFiles.filter(f =>
|
|
330
|
+
/\.(test|spec)\.(js|ts|py|java|go|rb|php|cs)$/.test(f) ||
|
|
331
|
+
/\/(tests?|__tests__|spec)\//.test(f) ||
|
|
332
|
+
/test_.*\.py$/.test(f)
|
|
333
|
+
),
|
|
334
|
+
|
|
335
|
+
// Configuration files
|
|
336
|
+
config: filteredFiles.filter(f => {
|
|
337
|
+
if (f.includes('package-lock.json') || f.includes('yarn.lock') || f.includes('poetry.lock')) return false;
|
|
338
|
+
return /\.(json|yml|yaml|toml|ini|cfg|conf|properties|xml)$/.test(f) ||
|
|
339
|
+
/(config|settings|env)/.test(f);
|
|
340
|
+
}),
|
|
341
|
+
|
|
342
|
+
// Lock files (handled separately due to size)
|
|
343
|
+
lockFiles: filteredFiles.filter(f =>
|
|
344
|
+
/\.(lock|lock\.json)$/.test(f) ||
|
|
345
|
+
f.includes('package-lock.json') ||
|
|
346
|
+
f.includes('yarn.lock') ||
|
|
347
|
+
f.includes('poetry.lock') ||
|
|
348
|
+
f.includes('Gemfile.lock')
|
|
349
|
+
),
|
|
350
|
+
|
|
351
|
+
// Infrastructure and DevOps
|
|
352
|
+
infrastructure: filteredFiles.filter(f =>
|
|
353
|
+
/\.(tf|yml|yaml)$/.test(f) && /(terraform|ansible|kubernetes|docker|k8s)/.test(f) ||
|
|
354
|
+
/Dockerfile|docker-compose|\.tf$/.test(f)
|
|
355
|
+
),
|
|
356
|
+
|
|
357
|
+
// Scripts and automation
|
|
358
|
+
scripts: filteredFiles.filter(f =>
|
|
359
|
+
/\.(sh|bash|ps1|py|rb|js)$/.test(f) &&
|
|
360
|
+
/\/(scripts|bin|tools|automation)\//.test(f) ||
|
|
361
|
+
/Makefile|Rakefile/.test(f)
|
|
362
|
+
),
|
|
363
|
+
|
|
364
|
+
// Documentation
|
|
365
|
+
docs: filteredFiles.filter(f =>
|
|
366
|
+
/\.(md|txt|rst|adoc|tex)$/.test(f) ||
|
|
367
|
+
/\/(docs|documentation)\//.test(f) ||
|
|
368
|
+
/README|CHANGELOG|LICENSE/.test(f)
|
|
369
|
+
),
|
|
370
|
+
|
|
371
|
+
// Styles (for any project with styling)
|
|
372
|
+
styles: filteredFiles.filter(f =>
|
|
373
|
+
/\.(css|scss|sass|less|stylus)$/.test(f)
|
|
374
|
+
),
|
|
375
|
+
|
|
376
|
+
// Other files
|
|
377
|
+
other: filteredFiles.filter(f => {
|
|
378
|
+
const assigned = [
|
|
379
|
+
/\/(api|routes|controllers|endpoints|handlers)\//,
|
|
380
|
+
/\/(services|business|logic|core|domain)\//,
|
|
381
|
+
/\/(models|entities|schemas|database|db|migrations)\//,
|
|
382
|
+
/\/(components|pages|views|screens|ui)\//,
|
|
383
|
+
/\/(utils|helpers|lib|shared|common)\//,
|
|
384
|
+
/\.(test|spec)\.(js|ts|py|java|go|rb|php|cs)$/,
|
|
385
|
+
/\/(tests?|__tests__|spec)\//,
|
|
386
|
+
/test_.*\.py$/,
|
|
387
|
+
/\.(json|yml|yaml|toml|ini|cfg|conf|properties|xml)$/,
|
|
388
|
+
/\.(lock|lock\.json)$/,
|
|
389
|
+
/package-lock\.json|yarn\.lock|poetry\.lock|Gemfile\.lock/,
|
|
390
|
+
/\.(tf|yml|yaml).*terraform|ansible|kubernetes|docker|k8s/,
|
|
391
|
+
/Dockerfile|docker-compose|\.tf$/,
|
|
392
|
+
/\.(sh|bash|ps1|py|rb|js).*\/(scripts|bin|tools|automation)\//,
|
|
393
|
+
/Makefile|Rakefile/,
|
|
394
|
+
/\.(md|txt|rst|adoc|tex)$/,
|
|
395
|
+
/\/(docs|documentation)\//,
|
|
396
|
+
/README|CHANGELOG|LICENSE/,
|
|
397
|
+
/\.(css|scss|sass|less|stylus)$/
|
|
398
|
+
];
|
|
399
|
+
return !assigned.some(pattern => pattern.test(f));
|
|
400
|
+
})
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return groups;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Run the batch review
|
|
407
|
+
batchReview().catch(console.error);
|
|
408
|
+
|
|
409
|
+
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require("commander");
|
|
4
|
+
const review = require("../src/reviewer");
|
|
5
|
+
const { createGlobalConfig } = require("../src/config");
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name("ai-pr-review")
|
|
11
|
+
.description("š¤ AI-powered Pull Request reviewer")
|
|
12
|
+
.version("1.0.0");
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command("review")
|
|
16
|
+
.description("š Review current branch changes with AI")
|
|
17
|
+
.option("-b, --base <branch>", "Base branch to compare against (e.g., main, origin/main, develop)")
|
|
18
|
+
.option("-m, --model <model>", "AI model to use (default: gpt-4o-mini)")
|
|
19
|
+
.option("--provider <provider>", "AI provider (default: openai)")
|
|
20
|
+
.option("-v, --verbose", "Show detailed output")
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
// Show a nice header
|
|
23
|
+
const { default: chalk } = await import("chalk");
|
|
24
|
+
console.log(chalk.blue.bold("\nš¤ AI PR Review"));
|
|
25
|
+
console.log(chalk.gray("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n"));
|
|
26
|
+
|
|
27
|
+
await review(options);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("config")
|
|
32
|
+
.description("āļø Set up global configuration")
|
|
33
|
+
.option("-m, --model <model>", "Default AI model to use")
|
|
34
|
+
.option("-b, --base <branch>", "Default base branch")
|
|
35
|
+
.option("--provider <provider>", "Default AI provider")
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
const { default: chalk } = await import("chalk");
|
|
38
|
+
|
|
39
|
+
if (Object.keys(options).length === 0) {
|
|
40
|
+
console.log(chalk.yellow("Please specify configuration options:"));
|
|
41
|
+
console.log(chalk.gray(" ai-pr-review config --model gpt-4o-mini --base main"));
|
|
42
|
+
console.log(chalk.gray(" ai-pr-review config --provider openai"));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const configPath = createGlobalConfig(options);
|
|
48
|
+
console.log(chalk.green("ā
Global configuration saved!"));
|
|
49
|
+
console.log(chalk.gray(` Config file: ${configPath}`));
|
|
50
|
+
|
|
51
|
+
if (options.model) console.log(chalk.blue(` Default model: ${options.model}`));
|
|
52
|
+
if (options.provider) console.log(chalk.blue(` Default provider: ${options.provider}`));
|
|
53
|
+
if (options.base) console.log(chalk.blue(` Default base branch: ${options.base}`));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(chalk.red(`ā Failed to save configuration: ${error.message}`));
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command("batch")
|
|
61
|
+
.description("š Review large repositories in batches to avoid rate limits")
|
|
62
|
+
.option("--dry-run", "Show what would be reviewed without actually running AI review")
|
|
63
|
+
.option("--frontend", "Optimize batching for frontend projects (React, Vue, Angular)")
|
|
64
|
+
.option("--batch-size <number>", "Number of files per batch (default: 8)", "8")
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
const { default: chalk } = await import("chalk");
|
|
67
|
+
console.log(chalk.blue.bold("\nš Batch AI Review"));
|
|
68
|
+
console.log(chalk.gray("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n"));
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const { spawn } = require('child_process');
|
|
72
|
+
const path = require('path');
|
|
73
|
+
const batchScriptPath = path.join(__dirname, '..', 'batch-review.js');
|
|
74
|
+
|
|
75
|
+
const args = [batchScriptPath];
|
|
76
|
+
if (options.dryRun) {
|
|
77
|
+
args.push('--dry-run');
|
|
78
|
+
}
|
|
79
|
+
if (options.frontend) {
|
|
80
|
+
args.push('--frontend');
|
|
81
|
+
}
|
|
82
|
+
if (options.batchSize) {
|
|
83
|
+
args.push('--batch-size', options.batchSize);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const child = spawn('node', args, { stdio: 'inherit' });
|
|
87
|
+
child.on('close', (code) => {
|
|
88
|
+
if (code !== 0) {
|
|
89
|
+
console.error(chalk.red(`\nā Batch review exited with code ${code}`));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(chalk.red(`ā Failed to run batch review: ${error.message}`));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Also support direct usage without subcommand
|
|
98
|
+
program
|
|
99
|
+
.option("-b, --base <branch>", "Base branch to compare against")
|
|
100
|
+
.option("-m, --model <model>", "AI model to use")
|
|
101
|
+
.option("--provider <provider>", "AI provider")
|
|
102
|
+
.option("-v, --verbose", "Show detailed output")
|
|
103
|
+
.action(async (options) => {
|
|
104
|
+
// If no specific command was run, default to review
|
|
105
|
+
if (process.argv.length > 2 && !process.argv.includes('review')) {
|
|
106
|
+
const { default: chalk } = await import("chalk");
|
|
107
|
+
console.log(chalk.blue.bold("\nš¤ AI PR Review"));
|
|
108
|
+
console.log(chalk.gray("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n"));
|
|
109
|
+
await review(options);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blockdeepanshu/ai-pr-review-cli",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "š¤ AI-powered Pull Request reviewer - Get instant feedback on your code changes using GPT",
|
|
5
|
+
"main": "src/reviewer.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ai-pr-review": "./bin/cli.js",
|
|
8
|
+
"aipr": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"type": "commonjs",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=16.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node bin/cli.js",
|
|
19
|
+
"batch-review": "node batch-review.js",
|
|
20
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
21
|
+
"prepublishOnly": "echo 'Ready to publish!'"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ai",
|
|
25
|
+
"artificial-intelligence",
|
|
26
|
+
"pr-review",
|
|
27
|
+
"pull-request",
|
|
28
|
+
"code-review",
|
|
29
|
+
"cli",
|
|
30
|
+
"git",
|
|
31
|
+
"github",
|
|
32
|
+
"openai",
|
|
33
|
+
"gpt",
|
|
34
|
+
"developer-tools",
|
|
35
|
+
"automation"
|
|
36
|
+
],
|
|
37
|
+
"author": {
|
|
38
|
+
"name": "Deepanshu Chauhan",
|
|
39
|
+
"email": "chauhandeepanshu336@gmail.com"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/blockDeepanshu/ai-pr-review.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/blockDeepanshu/ai-pr-review/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/blockDeepanshu/ai-pr-review#readme",
|
|
50
|
+
"files": [
|
|
51
|
+
"bin/",
|
|
52
|
+
"src/",
|
|
53
|
+
"batch-review.js",
|
|
54
|
+
"README.md",
|
|
55
|
+
"CHANGELOG.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
],
|
|
58
|
+
"preferGlobal": true,
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"axios": "^1.6.0",
|
|
61
|
+
"chalk": "^5.3.0",
|
|
62
|
+
"commander": "^11.0.0",
|
|
63
|
+
"dotenv": "^17.3.1",
|
|
64
|
+
"ora": "^6.3.1",
|
|
65
|
+
"simple-git": "^3.19.1"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {},
|
|
68
|
+
"funding": {
|
|
69
|
+
"type": "individual",
|
|
70
|
+
"url": "https://github.com/blockDeepanshu"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/ai.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
require("dotenv").config();
|
|
3
|
+
async function openai(prompt, model) {
|
|
4
|
+
try {
|
|
5
|
+
const res = await axios.post(
|
|
6
|
+
"https://api.openai.com/v1/chat/completions",
|
|
7
|
+
{
|
|
8
|
+
model,
|
|
9
|
+
messages: [{ role: "user", content: prompt }],
|
|
10
|
+
temperature: 0.2,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
headers: {
|
|
14
|
+
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return res.data.choices[0].message.content;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.response?.status === 429) {
|
|
22
|
+
const resetTime = error.response.headers['x-ratelimit-reset-requests'] || 'unknown';
|
|
23
|
+
throw new Error(`Rate limit exceeded. Please wait and try again. Reset time: ${resetTime}. Consider upgrading your OpenAI plan for higher limits.`);
|
|
24
|
+
} else if (error.response?.status === 401) {
|
|
25
|
+
throw new Error('Invalid OpenAI API key. Please check your OPENAI_API_KEY environment variable.');
|
|
26
|
+
} else if (error.response?.status === 403) {
|
|
27
|
+
throw new Error('OpenAI API access denied. Check your API key permissions.');
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error(`OpenAI API error: ${error.response?.data?.error?.message || error.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function runAI(prompt, config) {
|
|
35
|
+
return openai(prompt, config.model);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { runAI };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
function getConfig() {
|
|
6
|
+
// Default configuration
|
|
7
|
+
const defaultConfig = {
|
|
8
|
+
provider: "openai",
|
|
9
|
+
model: "gpt-4o-mini",
|
|
10
|
+
baseBranch: null, // Will be auto-detected
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Try to load global config from user's home directory
|
|
14
|
+
const globalConfigPath = path.join(os.homedir(), ".aiprconfig.json");
|
|
15
|
+
let config = { ...defaultConfig };
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(globalConfigPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const globalConfig = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
|
|
20
|
+
config = { ...config, ...globalConfig };
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.warn("Warning: Could not parse global config file:", globalConfigPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Try to load local project config (overrides global)
|
|
27
|
+
const localConfigPath = path.join(process.cwd(), ".aiprconfig.json");
|
|
28
|
+
if (fs.existsSync(localConfigPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, "utf-8"));
|
|
31
|
+
config = { ...config, ...localConfig };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn("Warning: Could not parse local config file:", localConfigPath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createGlobalConfig(options) {
|
|
41
|
+
const globalConfigPath = path.join(os.homedir(), ".aiprconfig.json");
|
|
42
|
+
const config = {
|
|
43
|
+
provider: options.provider || "openai",
|
|
44
|
+
model: options.model || "gpt-4o-mini",
|
|
45
|
+
...(options.baseBranch && { baseBranch: options.baseBranch })
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
fs.writeFileSync(globalConfigPath, JSON.stringify(config, null, 2));
|
|
49
|
+
return globalConfigPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { getConfig, createGlobalConfig };
|