coderev-cli 1.0.26 → 1.3.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/README.md +776 -0
- package/package.json +1 -1
- package/src/cli.js +162 -1
- package/src/issue-validator.js +499 -0
- package/src/issue-validator.test.js +404 -0
- package/src/models.js +59 -0
- package/src/models.test.js +139 -2
- package/src/rag-indexer.js +700 -0
- package/src/rag-indexer.test.js +385 -0
- package/src/reviewer.js +36 -6
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -43,6 +43,9 @@ program
|
|
|
43
43
|
.option('--min-confidence <number>', 'Minimum confidence threshold 0-100 (default: 60)', '60')
|
|
44
44
|
.option('--agents <list>', 'Comma-separated agent list: security,bugs,quality')
|
|
45
45
|
.option('--blame', 'Enable git blame context analysis to distinguish new vs pre-existing issues')
|
|
46
|
+
.option('--rag', 'Enable RAG codebase context retrieval (requires coderev index first)')
|
|
47
|
+
.option('--issue <url>', 'Validate that PR addresses a linked GitHub/GitLab issue')
|
|
48
|
+
.option('--verify-issue', 'Auto-verify PR addresses linked issues from commit messages')
|
|
46
49
|
.action(async (options) => {
|
|
47
50
|
try {
|
|
48
51
|
const config = loadConfig(options.config);
|
|
@@ -183,8 +186,35 @@ program
|
|
|
183
186
|
single: options.single || undefined,
|
|
184
187
|
minConfidence: parseInt(options.minConfidence) || undefined,
|
|
185
188
|
blame: options.blame || undefined,
|
|
189
|
+
rag: options.rag || undefined,
|
|
190
|
+
repoRoot: options.repo || options.config ? path.dirname(options.config) : process.cwd(),
|
|
186
191
|
});
|
|
187
192
|
|
|
193
|
+
// ── Issue Validation ──
|
|
194
|
+
if (options.issue) {
|
|
195
|
+
const { validateIssue } = require('./issue-validator');
|
|
196
|
+
try {
|
|
197
|
+
const issueResult = await validateIssue(options.issue, diff, {
|
|
198
|
+
token: options.githubToken || options.gitlabToken,
|
|
199
|
+
repoPath: options.repo || process.cwd(),
|
|
200
|
+
reviewResult: result,
|
|
201
|
+
});
|
|
202
|
+
// Append issue validation to result
|
|
203
|
+
result._issueValidation = issueResult.report;
|
|
204
|
+
result._issueValidationFormatted = issueResult.formatted;
|
|
205
|
+
} catch (err) {
|
|
206
|
+
result._issueValidation = { error: err.message };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (options.verifyIssue) {
|
|
211
|
+
const { findRelatedIssues, parseCommitLog } = require('./issue-validator');
|
|
212
|
+
const repoPath = options.repo || process.cwd();
|
|
213
|
+
const commitLog = parseCommitLog(repoPath);
|
|
214
|
+
const relatedIssues = findRelatedIssues(diff, commitLog);
|
|
215
|
+
result._relatedIssuesFound = relatedIssues;
|
|
216
|
+
}
|
|
217
|
+
|
|
188
218
|
let output;
|
|
189
219
|
if (options.output === 'json') {
|
|
190
220
|
output = JSON.stringify(result, null, 2);
|
|
@@ -325,6 +355,12 @@ program
|
|
|
325
355
|
return;
|
|
326
356
|
}
|
|
327
357
|
|
|
358
|
+
// ── Print issue validation report if available ──
|
|
359
|
+
if (result._issueValidationFormatted) {
|
|
360
|
+
// Print after main output (sent to console.error so it's visible even with piped output)
|
|
361
|
+
console.error(result._issueValidationFormatted);
|
|
362
|
+
}
|
|
363
|
+
|
|
328
364
|
// ── CI Mode ──
|
|
329
365
|
if (options.ci && (result.issues || []).length > 0) {
|
|
330
366
|
const errorIssues = result.issues.filter(i => i.type === 'error');
|
|
@@ -888,9 +924,11 @@ program
|
|
|
888
924
|
.option('--agent-security <name>', 'Model for security agent')
|
|
889
925
|
.option('--agent-bugs <name>', 'Model for bug detection agent')
|
|
890
926
|
.option('--agent-quality <name>', 'Model for quality agent')
|
|
927
|
+
.option('--auto', 'Auto-detect available providers from environment variables')
|
|
928
|
+
.option('--quick', 'Alias for --auto')
|
|
891
929
|
.action((options) => {
|
|
892
930
|
const fs = require('fs');
|
|
893
|
-
const { resolveTemplate } = require('./models');
|
|
931
|
+
const { resolveTemplate, autoDetectProvider, listTemplates } = require('./models');
|
|
894
932
|
const configPath = path.join(process.cwd(), '.coderevrc.json');
|
|
895
933
|
|
|
896
934
|
let config = {};
|
|
@@ -900,6 +938,86 @@ program
|
|
|
900
938
|
|
|
901
939
|
if (!config.ai) config.ai = {};
|
|
902
940
|
|
|
941
|
+
// ── Auto-detect mode ──
|
|
942
|
+
if (options.auto || options.quick) {
|
|
943
|
+
console.log(chalk.bold('\n🔍 Auto-detecting AI providers...\n'));
|
|
944
|
+
|
|
945
|
+
const result = autoDetectProvider();
|
|
946
|
+
|
|
947
|
+
if (!result) {
|
|
948
|
+
console.log(chalk.yellow('⚠ No API key environment variables detected.'));
|
|
949
|
+
console.log(chalk.gray('\n Supported providers and their env vars:'));
|
|
950
|
+
const allTemplates = listTemplates();
|
|
951
|
+
const seen = new Set();
|
|
952
|
+
for (const t of allTemplates) {
|
|
953
|
+
if (!seen.has(t.apiKeyEnv)) {
|
|
954
|
+
seen.add(t.apiKeyEnv);
|
|
955
|
+
console.log(chalk.gray(` ${t.apiKeyEnv} → ${t.name} (${t.desc})`));
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
console.log(chalk.blue('\n 💡 Set one of these env vars and run `coderev setup --auto` again.'));
|
|
959
|
+
console.log(chalk.blue(' Or use `coderev setup --model <name>` to pick manually.'));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Show detection summary
|
|
964
|
+
console.log(chalk.bold('📋 Detection Results / 检测结果:'));
|
|
965
|
+
console.log('━'.repeat(50));
|
|
966
|
+
|
|
967
|
+
// List all detected keys
|
|
968
|
+
for (const name of result.allDetected) {
|
|
969
|
+
const t = result.allDetected.includes(name) ? require('./models').getTemplate(name) : null;
|
|
970
|
+
if (t) {
|
|
971
|
+
const masked = process.env[t.apiKeyEnv].slice(0, 6) + '...' + process.env[t.apiKeyEnv].slice(-4);
|
|
972
|
+
console.log(chalk.green(` ✔ ${t.apiKeyEnv}`) + chalk.gray(` → ${name} (${masked})`));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Show providers that were NOT detected
|
|
977
|
+
const allNames = Object.keys(require('./models').BUILTIN_TEMPLATES);
|
|
978
|
+
const uniqueNotDetected = [...new Set(
|
|
979
|
+
allNames.filter(n => !result.allDetected.includes(n))
|
|
980
|
+
)];
|
|
981
|
+
|
|
982
|
+
console.log('');
|
|
983
|
+
console.log(chalk.bold(`✨ Selected: ${chalk.green(result.chosen)}`));
|
|
984
|
+
console.log(chalk.gray(` Provider: ${result.template.provider}`));
|
|
985
|
+
console.log(chalk.gray(` Model: ${result.template.model}`));
|
|
986
|
+
console.log(chalk.gray(` API Key: ${result.template.apiKeyEnv}`));
|
|
987
|
+
console.log('');
|
|
988
|
+
|
|
989
|
+
// Write config
|
|
990
|
+
try {
|
|
991
|
+
const resolved = resolveTemplate(result.chosen);
|
|
992
|
+
Object.assign(config.ai, resolved);
|
|
993
|
+
|
|
994
|
+
// Also add fallback if second provider is available
|
|
995
|
+
const remaining = result.allDetected.filter(n => n !== result.chosen);
|
|
996
|
+
if (remaining.length > 0) {
|
|
997
|
+
const fallbackName = remaining[0];
|
|
998
|
+
const fallbackResolved = resolveTemplate(fallbackName);
|
|
999
|
+
config.ai.fallback = {
|
|
1000
|
+
enabled: true,
|
|
1001
|
+
provider: fallbackResolved.provider,
|
|
1002
|
+
baseURL: fallbackResolved.baseURL,
|
|
1003
|
+
model: fallbackResolved.model,
|
|
1004
|
+
apiKeyEnv: fallbackResolved.apiKeyEnv,
|
|
1005
|
+
temperature: fallbackResolved.temperature,
|
|
1006
|
+
maxTokens: fallbackResolved.maxTokens,
|
|
1007
|
+
};
|
|
1008
|
+
console.log(chalk.blue(` Fallback: ${fallbackName} (${fallbackResolved.model})`));
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1012
|
+
console.log(chalk.green(`\n✔ Config saved to ${configPath}`));
|
|
1013
|
+
console.log(chalk.blue(' Run `coderev review` to start reviewing code!'));
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
903
1021
|
// Set primary model
|
|
904
1022
|
if (options.model) {
|
|
905
1023
|
try {
|
|
@@ -1105,6 +1223,45 @@ program
|
|
|
1105
1223
|
}
|
|
1106
1224
|
});
|
|
1107
1225
|
|
|
1226
|
+
program
|
|
1227
|
+
.command('index')
|
|
1228
|
+
.description('Build codebase index for RAG-powered code reviews')
|
|
1229
|
+
.option('-r, --repo <path>', 'Path to git repository', '.')
|
|
1230
|
+
.option('--max-files <number>', 'Maximum files to index (default: 500)', '500')
|
|
1231
|
+
.option('--json', 'Output as JSON')
|
|
1232
|
+
.action(async (options) => {
|
|
1233
|
+
try {
|
|
1234
|
+
const { buildIndex, loadIndex, INDEX_DIR } = require('./rag-indexer');
|
|
1235
|
+
const repoRoot = path.resolve(options.repo);
|
|
1236
|
+
|
|
1237
|
+
console.error(chalk.blue(`📚 Building codebase index for ${repoRoot}...`));
|
|
1238
|
+
console.error(chalk.gray(` Max files: ${options.maxFiles}`));
|
|
1239
|
+
|
|
1240
|
+
const index = buildIndex(repoRoot, { maxFiles: parseInt(options.maxFiles) });
|
|
1241
|
+
|
|
1242
|
+
if (options.json) {
|
|
1243
|
+
console.log(JSON.stringify(index.stats, null, 2));
|
|
1244
|
+
} else {
|
|
1245
|
+
console.log(chalk.green(`\n✅ Index built successfully!`));
|
|
1246
|
+
console.log(chalk.bold(`\n📊 Statistics:`));
|
|
1247
|
+
console.log(` Files scanned: ${index.stats.filesScanned}`);
|
|
1248
|
+
console.log(` Symbols extracted: ${index.stats.symbolsExtracted}`);
|
|
1249
|
+
console.log(` Time: ${index.stats.timeMs}ms`);
|
|
1250
|
+
console.log(` Stored at: ${path.join(repoRoot, INDEX_DIR)}`);
|
|
1251
|
+
if (Object.keys(index.stats.languageBreakdown).length > 0) {
|
|
1252
|
+
console.log(chalk.bold(`\n🔤 Language breakdown:`));
|
|
1253
|
+
for (const [lang, count] of Object.entries(index.stats.languageBreakdown).sort((a, b) => b[1] - a[1])) {
|
|
1254
|
+
console.log(` ${lang}: ${count} symbols`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
console.log(chalk.blue(`\n💡 Tip: Run \`coderev review --rag\` to use codebase context in reviews.`));
|
|
1258
|
+
}
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
1261
|
+
process.exit(1);
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1108
1265
|
program.parse(process.argv);
|
|
1109
1266
|
|
|
1110
1267
|
// ── Helpers ───────────────────────────────────────────────────
|
|
@@ -1204,6 +1361,10 @@ function formatTerminal(result) {
|
|
|
1204
1361
|
enLines.push(' ' + chalk.cyan(' Files analyzed: ') + bc.filesAnalyzed);
|
|
1205
1362
|
}
|
|
1206
1363
|
}
|
|
1364
|
+
if (result._rag) {
|
|
1365
|
+
enLines.push('\n' + chalk.bold('RAG Context:'));
|
|
1366
|
+
enLines.push(' ' + chalk.magenta(`Indexed ${result._rag.indexedSymbols} symbols from codebase`));
|
|
1367
|
+
}
|
|
1207
1368
|
enLines.push('\n' + '━'.repeat(50));
|
|
1208
1369
|
|
|
1209
1370
|
return cnLines.join('\n') + '\n' + enLines.join('\n');
|