coderev-cli 1.0.16 → 1.0.18
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 +44 -0
- package/package.json +1 -1
- package/src/blame.js +247 -0
- package/src/cli.js +223 -170
- package/src/config.js +86 -8
- package/src/github-app.js +511 -0
- package/src/reviewer.js +19 -0
package/src/cli.js
CHANGED
|
@@ -21,9 +21,9 @@ program
|
|
|
21
21
|
.option('--base <branch>', 'Base branch for diff (requires --repo)')
|
|
22
22
|
.option('--head <branch>', 'Head branch for diff (requires --repo)')
|
|
23
23
|
.option('-c, --config <path>', 'Path to config file')
|
|
24
|
-
.option('-o, --output <format>', 'Output format (markdown|json|terminal|html)', 'terminal')
|
|
25
|
-
.option('--ci', 'CI mode: exit with non-zero code if issues found')
|
|
26
|
-
.option('--incremental', 'Only review new/changed lines (skip unchanged context)')
|
|
24
|
+
.option('-o, --output <format>', 'Output format (markdown|json|terminal|html)', 'terminal')
|
|
25
|
+
.option('--ci', 'CI mode: exit with non-zero code if issues found')
|
|
26
|
+
.option('--incremental', 'Only review new/changed lines (skip unchanged context)')
|
|
27
27
|
.option('--interactive', 'Interactively review and apply fixes for each issue')
|
|
28
28
|
.option('--pr <ref>', 'GitHub PR to review, e.g. owner/repo#42 or full URL')
|
|
29
29
|
.option('--gl <ref>', 'GitLab MR to review, e.g. owner/repo!42 or full URL')
|
|
@@ -42,6 +42,7 @@ program
|
|
|
42
42
|
.option('--single', 'Use single-agent mode (legacy, no parallel review)')
|
|
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
|
+
.option('--blame', 'Enable git blame context analysis to distinguish new vs pre-existing issues')
|
|
45
46
|
.action(async (options) => {
|
|
46
47
|
try {
|
|
47
48
|
const config = loadConfig(options.config);
|
|
@@ -175,12 +176,13 @@ program
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
const result = await reviewDiff(diff, config, {
|
|
178
|
-
noCache: options.noCache === false,
|
|
179
|
+
noCache: options.noCache === false,
|
|
179
180
|
incremental: options.incremental || undefined,
|
|
180
181
|
ignorePattern,
|
|
181
182
|
audit: options.audit || undefined,
|
|
182
183
|
single: options.single || undefined,
|
|
183
184
|
minConfidence: parseInt(options.minConfidence) || undefined,
|
|
185
|
+
blame: options.blame || undefined,
|
|
184
186
|
});
|
|
185
187
|
|
|
186
188
|
let output;
|
|
@@ -261,78 +263,78 @@ program
|
|
|
261
263
|
}
|
|
262
264
|
}
|
|
263
265
|
|
|
264
|
-
// ── Interactive Fix Mode ──
|
|
265
|
-
if (options.interactive && (result.issues || []).length > 0) {
|
|
266
|
-
const { generateFix } = require('./fixer');
|
|
267
|
-
const apiKey = getApiKey(config);
|
|
268
|
-
const readline = require('readline');
|
|
269
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
270
|
-
console.log(chalk.bold('\n🛠 Interactive Fix Mode / 交互式修复模式'));
|
|
271
|
-
console.log(chalk.yellow(' Review each issue and choose to apply a fix or skip.'));
|
|
272
|
-
const allPatches = [];
|
|
273
|
-
const processIssues = async (idx) => {
|
|
274
|
-
if (idx >= (result.issues || []).length) {
|
|
275
|
-
rl.close();
|
|
276
|
-
if (allPatches.length > 0) {
|
|
277
|
-
const patchesPath = require('path').join(require('os').tmpdir(), 'coderev-fixes.patch');
|
|
278
|
-
fs.writeFileSync(patchesPath, allPatches.map(p => p.patch).filter(Boolean).join('\n\n'), 'utf8');
|
|
279
|
-
console.log(chalk.green('\n✔ Fixes saved to: ' + patchesPath));
|
|
280
|
-
console.log(chalk.gray(' Apply with: git apply "' + patchesPath + '"'));
|
|
281
|
-
} else {
|
|
282
|
-
console.log(chalk.blue(' No fixes were applied.'));
|
|
283
|
-
}
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const issue = result.issues[idx];
|
|
287
|
-
const sevColor = issue.severity === 'high' ? chalk.red : issue.severity === 'medium' ? chalk.yellow : chalk.blue;
|
|
288
|
-
console.log(chalk.bold('\nIssue #' + (idx + 1) + ' of ' + result.issues.length));
|
|
289
|
-
console.log(' ' + sevColor('●') + ' [' + issue.severity + '] [' + issue.type + '] ' + issue.message);
|
|
290
|
-
if (issue.file) console.log(' ' + chalk.gray('File: ') + issue.file + (issue.line ? ':' + issue.line : ''));
|
|
291
|
-
if (issue.suggestion) console.log(' ' + chalk.gray('Suggestion: ') + issue.suggestion);
|
|
292
|
-
console.log(' ' + chalk.gray('Confidence: ') + (issue.confidence || 'N/A'));
|
|
293
|
-
const answer = await new Promise(resolve => {
|
|
294
|
-
rl.question(chalk.cyan(' [a]pply fix / [s]kip / [q]uit > '), resolve);
|
|
295
|
-
});
|
|
296
|
-
const cmd = (answer || '').trim().toLowerCase();
|
|
297
|
-
if (cmd === 'q') {
|
|
298
|
-
rl.close();
|
|
299
|
-
if (allPatches.length > 0) {
|
|
300
|
-
const patchesPath = require('path').join(require('os').tmpdir(), 'coderev-fixes.patch');
|
|
301
|
-
fs.writeFileSync(patchesPath, allPatches.map(p => p.patch).filter(Boolean).join('\n\n'), 'utf8');
|
|
302
|
-
console.log(chalk.green('\n✔ ' + allPatches.length + ' fixes saved to: ' + patchesPath));
|
|
303
|
-
}
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (cmd === 'a') {
|
|
307
|
-
console.error(chalk.blue(' ↻ Generating fix...'));
|
|
308
|
-
try {
|
|
309
|
-
const fixResult = await generateFix(diff, issue, apiKey, config);
|
|
310
|
-
if (fixResult.patch) {
|
|
311
|
-
allPatches.push(fixResult);
|
|
312
|
-
console.log(chalk.green(' ✔ ' + (fixResult.explanation || 'Fix generated')));
|
|
313
|
-
} else {
|
|
314
|
-
console.log(chalk.yellow(' ⚠ Cannot auto-fix: ' + (fixResult.explanation || 'Unknown')));
|
|
315
|
-
}
|
|
316
|
-
} catch (err) {
|
|
317
|
-
console.log(chalk.red(' ✖ Error: ' + err.message));
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
setImmediate(() => processIssues(idx + 1));
|
|
321
|
-
};
|
|
322
|
-
processIssues(0);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ── CI Mode ──
|
|
327
|
-
if (options.ci && (result.issues || []).length > 0) {
|
|
328
|
-
const errorIssues = result.issues.filter(i => i.type === 'error');
|
|
329
|
-
const warningIssues = result.issues.filter(i => i.type === 'warning');
|
|
330
|
-
if (errorIssues.length > 0 || warningIssues.length > 0) {
|
|
331
|
-
console.error(chalk.red('✖ CI: Found issues (' + errorIssues.length + ' errors, ' + warningIssues.length + ' warnings)'));
|
|
332
|
-
process.exitCode = 1;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
266
|
+
// ── Interactive Fix Mode ──
|
|
267
|
+
if (options.interactive && (result.issues || []).length > 0) {
|
|
268
|
+
const { generateFix } = require('./fixer');
|
|
269
|
+
const apiKey = getApiKey(config);
|
|
270
|
+
const readline = require('readline');
|
|
271
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
272
|
+
console.log(chalk.bold('\n🛠 Interactive Fix Mode / 交互式修复模式'));
|
|
273
|
+
console.log(chalk.yellow(' Review each issue and choose to apply a fix or skip.'));
|
|
274
|
+
const allPatches = [];
|
|
275
|
+
const processIssues = async (idx) => {
|
|
276
|
+
if (idx >= (result.issues || []).length) {
|
|
277
|
+
rl.close();
|
|
278
|
+
if (allPatches.length > 0) {
|
|
279
|
+
const patchesPath = require('path').join(require('os').tmpdir(), 'coderev-fixes.patch');
|
|
280
|
+
fs.writeFileSync(patchesPath, allPatches.map(p => p.patch).filter(Boolean).join('\n\n'), 'utf8');
|
|
281
|
+
console.log(chalk.green('\n✔ Fixes saved to: ' + patchesPath));
|
|
282
|
+
console.log(chalk.gray(' Apply with: git apply "' + patchesPath + '"'));
|
|
283
|
+
} else {
|
|
284
|
+
console.log(chalk.blue(' No fixes were applied.'));
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const issue = result.issues[idx];
|
|
289
|
+
const sevColor = issue.severity === 'high' ? chalk.red : issue.severity === 'medium' ? chalk.yellow : chalk.blue;
|
|
290
|
+
console.log(chalk.bold('\nIssue #' + (idx + 1) + ' of ' + result.issues.length));
|
|
291
|
+
console.log(' ' + sevColor('●') + ' [' + issue.severity + '] [' + issue.type + '] ' + issue.message);
|
|
292
|
+
if (issue.file) console.log(' ' + chalk.gray('File: ') + issue.file + (issue.line ? ':' + issue.line : ''));
|
|
293
|
+
if (issue.suggestion) console.log(' ' + chalk.gray('Suggestion: ') + issue.suggestion);
|
|
294
|
+
console.log(' ' + chalk.gray('Confidence: ') + (issue.confidence || 'N/A'));
|
|
295
|
+
const answer = await new Promise(resolve => {
|
|
296
|
+
rl.question(chalk.cyan(' [a]pply fix / [s]kip / [q]uit > '), resolve);
|
|
297
|
+
});
|
|
298
|
+
const cmd = (answer || '').trim().toLowerCase();
|
|
299
|
+
if (cmd === 'q') {
|
|
300
|
+
rl.close();
|
|
301
|
+
if (allPatches.length > 0) {
|
|
302
|
+
const patchesPath = require('path').join(require('os').tmpdir(), 'coderev-fixes.patch');
|
|
303
|
+
fs.writeFileSync(patchesPath, allPatches.map(p => p.patch).filter(Boolean).join('\n\n'), 'utf8');
|
|
304
|
+
console.log(chalk.green('\n✔ ' + allPatches.length + ' fixes saved to: ' + patchesPath));
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (cmd === 'a') {
|
|
309
|
+
console.error(chalk.blue(' ↻ Generating fix...'));
|
|
310
|
+
try {
|
|
311
|
+
const fixResult = await generateFix(diff, issue, apiKey, config);
|
|
312
|
+
if (fixResult.patch) {
|
|
313
|
+
allPatches.push(fixResult);
|
|
314
|
+
console.log(chalk.green(' ✔ ' + (fixResult.explanation || 'Fix generated')));
|
|
315
|
+
} else {
|
|
316
|
+
console.log(chalk.yellow(' ⚠ Cannot auto-fix: ' + (fixResult.explanation || 'Unknown')));
|
|
317
|
+
}
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.log(chalk.red(' ✖ Error: ' + err.message));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
setImmediate(() => processIssues(idx + 1));
|
|
323
|
+
};
|
|
324
|
+
processIssues(0);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── CI Mode ──
|
|
329
|
+
if (options.ci && (result.issues || []).length > 0) {
|
|
330
|
+
const errorIssues = result.issues.filter(i => i.type === 'error');
|
|
331
|
+
const warningIssues = result.issues.filter(i => i.type === 'warning');
|
|
332
|
+
if (errorIssues.length > 0 || warningIssues.length > 0) {
|
|
333
|
+
console.error(chalk.red('✖ CI: Found issues (' + errorIssues.length + ' errors, ' + warningIssues.length + ' warnings)'));
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
336
338
|
console.log(output);
|
|
337
339
|
} catch (err) {
|
|
338
340
|
console.error(chalk.red(`✖ ${err.message}`));
|
|
@@ -659,7 +661,13 @@ program
|
|
|
659
661
|
format: 'terminal',
|
|
660
662
|
includeScore: true,
|
|
661
663
|
},
|
|
664
|
+
inheritance: {
|
|
665
|
+
enabled: true,
|
|
666
|
+
strategy: 'deep-merge', // 'deep-merge' | 'replace'
|
|
667
|
+
},
|
|
662
668
|
};
|
|
669
|
+
console.log(chalk.blue('ℹ 提示: coderev 支持从父目录继承配置。将 .coderevrc.json'));
|
|
670
|
+
console.log(chalk.blue(' 放在项目根目录,子项目可只设覆盖字段。'));
|
|
663
671
|
const configPath = path.join(process.cwd(), '.coderevrc.json');
|
|
664
672
|
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
665
673
|
console.log(chalk.green(`✔ Default config created at ${configPath}`));
|
|
@@ -702,6 +710,27 @@ build/
|
|
|
702
710
|
}
|
|
703
711
|
});
|
|
704
712
|
|
|
713
|
+
// ── Serve (GitHub App) ────────────────────────────────────────────
|
|
714
|
+
program
|
|
715
|
+
.command('serve')
|
|
716
|
+
.description('Start GitHub App webhook server for automatic PR review')
|
|
717
|
+
.option('-p, --port <number>', 'Server port (default: 3000)')
|
|
718
|
+
.option('--webhook-secret <secret>', 'GitHub App webhook secret')
|
|
719
|
+
.option('--app-id <id>', 'GitHub App ID')
|
|
720
|
+
.option('--private-key <key>', 'GitHub App private key (PEM)')
|
|
721
|
+
.option('--review-mode <mode>', 'Review output: comment (default) | inline | check')
|
|
722
|
+
.option('--auto-approve', 'Auto-approve PRs with no issues')
|
|
723
|
+
.option('--min-confidence <number>', 'Minimum confidence threshold 0-100 (default: 60)')
|
|
724
|
+
.action(async (options) => {
|
|
725
|
+
try {
|
|
726
|
+
const { serveCommand } = require('./github-app');
|
|
727
|
+
await serveCommand(options);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
console.error(chalk.red(`✖ ${err.message}`));
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
705
734
|
program.parse(process.argv);
|
|
706
735
|
|
|
707
736
|
// ── Helpers ───────────────────────────────────────────────────
|
|
@@ -747,6 +776,18 @@ function formatTerminal(result) {
|
|
|
747
776
|
cnLines.push('\n' + chalk.bold('👍 好的实践:'));
|
|
748
777
|
for (const p of result.praise) cnLines.push(' ✅ ' + p);
|
|
749
778
|
}
|
|
779
|
+
if (result._blameContext) {
|
|
780
|
+
const bc = result._blameContext;
|
|
781
|
+
cnLines.push('\n' + chalk.bold('Git Blame 上下文分析:'));
|
|
782
|
+
if (bc.error) {
|
|
783
|
+
cnLines.push(' ' + chalk.yellow('⚠ 分析出错: ' + bc.error));
|
|
784
|
+
} else {
|
|
785
|
+
cnLines.push(' ' + chalk.green('● 新增问题: ') + chalk.bold(bc.newIssues));
|
|
786
|
+
cnLines.push(' ' + chalk.gray('○ 已有问题: ') + chalk.bold(bc.preExistingIssues));
|
|
787
|
+
if (bc.unknownIssues > 0) cnLines.push(' ' + chalk.blue('? 无法判断: ') + chalk.bold(bc.unknownIssues));
|
|
788
|
+
cnLines.push(' ' + chalk.cyan(' 分析文件数: ') + bc.filesAnalyzed);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
750
791
|
cnLines.push('\n' + '━'.repeat(50));
|
|
751
792
|
|
|
752
793
|
// English section
|
|
@@ -777,6 +818,18 @@ function formatTerminal(result) {
|
|
|
777
818
|
enLines.push('\n' + chalk.bold('👍 Good Practices:'));
|
|
778
819
|
for (const p of result.praise) enLines.push(' ✅ ' + p);
|
|
779
820
|
}
|
|
821
|
+
if (result._blameContext) {
|
|
822
|
+
const bc = result._blameContext;
|
|
823
|
+
enLines.push('\n' + chalk.bold('Git Blame Context:'));
|
|
824
|
+
if (bc.error) {
|
|
825
|
+
enLines.push(' ' + chalk.yellow('⚠ Error: ' + bc.error));
|
|
826
|
+
} else {
|
|
827
|
+
enLines.push(' ' + chalk.green('● New issues: ') + chalk.bold(bc.newIssues));
|
|
828
|
+
enLines.push(' ' + chalk.gray('○ Pre-existing: ') + chalk.bold(bc.preExistingIssues));
|
|
829
|
+
if (bc.unknownIssues > 0) enLines.push(' ' + chalk.blue('? Unknown: ') + chalk.bold(bc.unknownIssues));
|
|
830
|
+
enLines.push(' ' + chalk.cyan(' Files analyzed: ') + bc.filesAnalyzed);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
780
833
|
enLines.push('\n' + '━'.repeat(50));
|
|
781
834
|
|
|
782
835
|
return cnLines.join('\n') + '\n' + enLines.join('\n');
|
|
@@ -833,97 +886,97 @@ function formatMarkdown(result) {
|
|
|
833
886
|
}
|
|
834
887
|
|
|
835
888
|
return md;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Format review result as HTML report.
|
|
841
|
-
*/
|
|
842
|
-
function formatHtml(result) {
|
|
843
|
-
const sevColors = { high: '#e74c3c', medium: '#f39c12', low: '#3498db' };
|
|
844
|
-
const sevLabels = { high: 'High / 严重', medium: 'Medium / 中等', low: 'Low / 轻微' };
|
|
845
|
-
const typeLabels = { error: 'Error', warning: 'Warning', info: 'Info' };
|
|
846
|
-
|
|
847
|
-
let issuesHtml = '';
|
|
848
|
-
if (result.issues && result.issues.length > 0) {
|
|
849
|
-
for (const issue of result.issues) {
|
|
850
|
-
const color = sevColors[issue.severity] || '#95a5a6';
|
|
851
|
-
issuesHtml += `
|
|
852
|
-
<div class="issue" style="border-left: 4px solid ${color}; margin: 10px 0; padding: 12px; background: #f8f9fa; border-radius: 0 4px 4px 0;">
|
|
853
|
-
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
|
854
|
-
<span style="background: ${color}; color: white; padding: 2px 8px; border-radius: 3px; font-size: 12px; font-weight: bold;">${sevLabels[issue.severity] || issue.severity}</span>
|
|
855
|
-
<span style="background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 12px;">${typeLabels[issue.type] || issue.type}</span>
|
|
856
|
-
${issue.confidence ? `<span style="color: #6c757d; font-size: 12px;">Confidence: ${issue.confidence}/100</span>` : ''}
|
|
857
|
-
</div>
|
|
858
|
-
<div style="font-size: 14px; margin-bottom: 4px;">${issue.message}</div>
|
|
859
|
-
${issue.file ? `<div style="color: #6c757d; font-size: 12px;">📁 ${issue.file}${issue.line ? ':' + issue.line : ''}</div>` : ''}
|
|
860
|
-
${issue.suggestion ? `<div style="color: #27ae60; font-size: 13px; margin-top: 6px; padding: 8px; background: #eafaf1; border-radius: 3px;">💡 ${issue.suggestion}</div>` : ''}
|
|
861
|
-
</div>`;
|
|
862
|
-
}
|
|
863
|
-
} else {
|
|
864
|
-
issuesHtml = '<p style="color: #27ae60; text-align: center; padding: 20px;">✅ No issues found / 未发现问题</p>';
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
let suggestionsHtml = '';
|
|
868
|
-
if (result.suggestions && result.suggestions.length > 0) {
|
|
869
|
-
suggestionsHtml = '<h3>Suggestions / 改进建议</h3><ul>' + result.suggestions.map(s => '<li>' + s + '</li>').join('') + '</ul>';
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
let praiseHtml = '';
|
|
873
|
-
if (result.praise && result.praise.length > 0) {
|
|
874
|
-
praiseHtml = '<h3>👍 Good Practices / 好的实践</h3><ul>' + result.praise.map(p => '<li>✅ ' + p + '</li>').join('') + '</ul>';
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
const scoreVal = result.score || 0;
|
|
878
|
-
const scoreColor = scoreVal >= 80 ? '#27ae60' : scoreVal >= 50 ? '#f39c12' : '#e74c3c';
|
|
879
|
-
const scoreLabel = scoreVal >= 80 ? 'Good / 良好' : scoreVal >= 50 ? 'Needs Improvement / 需改进' : 'Poor / 差';
|
|
880
|
-
|
|
881
|
-
return `<!DOCTYPE html>
|
|
882
|
-
<html lang="zh-CN">
|
|
883
|
-
<head>
|
|
884
|
-
<meta charset="UTF-8">
|
|
885
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
886
|
-
<title>coderev Review Report</title>
|
|
887
|
-
<style>
|
|
888
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #fff; color: #333; }
|
|
889
|
-
h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
|
890
|
-
.score-card { text-align: center; padding: 30px; background: #f8f9fa; border-radius: 8px; margin: 20px 0; }
|
|
891
|
-
.score-value { font-size: 48px; font-weight: bold; }
|
|
892
|
-
.score-label { font-size: 16px; color: #6c757d; margin-top: 5px; }
|
|
893
|
-
.summary { font-size: 16px; color: #555; margin: 15px 0; padding: 15px; background: #e8f4f8; border-radius: 6px; }
|
|
894
|
-
.stats { display: flex; gap: 20px; justify-content: center; margin: 20px 0; }
|
|
895
|
-
.stat { text-align: center; padding: 15px 25px; background: white; border: 1px solid #dee2e6; border-radius: 6px; }
|
|
896
|
-
.stat-value { font-size: 28px; font-weight: bold; }
|
|
897
|
-
.stat-label { font-size: 12px; color: #6c757d; text-transform: uppercase; }
|
|
898
|
-
h2 { margin-top: 30px; }
|
|
899
|
-
@media (prefers-color-scheme: dark) {
|
|
900
|
-
body { background: #1a1a2e; color: #e0e0e0; }
|
|
901
|
-
.score-card { background: #16213e; }
|
|
902
|
-
.summary { background: #0f3460; }
|
|
903
|
-
.issue { background: #16213e; }
|
|
904
|
-
.stat { background: #16213e; border-color: #0f3460; }
|
|
905
|
-
}
|
|
906
|
-
</style>
|
|
907
|
-
</head>
|
|
908
|
-
<body>
|
|
909
|
-
<h1>📋 coderev Review Report</h1>
|
|
910
|
-
<div class="score-card">
|
|
911
|
-
<div class="score-value" style="color: ${scoreColor}">${scoreVal}/100</div>
|
|
912
|
-
<div class="score-label">${scoreLabel}</div>
|
|
913
|
-
</div>
|
|
914
|
-
${result.summary ? '<div class="summary">📄 ' + result.summary + '</div>' : ''}
|
|
915
|
-
<div class="stats">
|
|
916
|
-
<div class="stat"><div class="stat-value" style="color: #e74c3c">${result.issues ? result.issues.filter(i => i.type === 'error').length : 0}</div><div class="stat-label">Errors</div></div>
|
|
917
|
-
<div class="stat"><div class="stat-value" style="color: #f39c12">${result.issues ? result.issues.filter(i => i.type === 'warning').length : 0}</div><div class="stat-label">Warnings</div></div>
|
|
918
|
-
<div class="stat"><div class="stat-value" style="color: #3498db">${result.issues ? result.issues.filter(i => i.type === 'info').length : 0}</div><div class="stat-label">Info</div></div>
|
|
919
|
-
<div class="stat"><div class="stat-value" style="color: #9b59b6">${result.issues ? result.issues.length : 0}</div><div class="stat-label">Total</div></div>
|
|
920
|
-
</div>
|
|
921
|
-
<h2>Issues / 问题</h2>
|
|
922
|
-
${issuesHtml}
|
|
923
|
-
${suggestionsHtml}
|
|
924
|
-
${praiseHtml}
|
|
925
|
-
<hr style="margin-top: 40px; border: none; border-top: 1px solid #eee;">
|
|
926
|
-
<p style="text-align: center; color: #6c757d; font-size: 12px;">Generated by coderev</p>
|
|
927
|
-
</body>
|
|
928
|
-
</html>`;
|
|
929
|
-
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Format review result as HTML report.
|
|
894
|
+
*/
|
|
895
|
+
function formatHtml(result) {
|
|
896
|
+
const sevColors = { high: '#e74c3c', medium: '#f39c12', low: '#3498db' };
|
|
897
|
+
const sevLabels = { high: 'High / 严重', medium: 'Medium / 中等', low: 'Low / 轻微' };
|
|
898
|
+
const typeLabels = { error: 'Error', warning: 'Warning', info: 'Info' };
|
|
899
|
+
|
|
900
|
+
let issuesHtml = '';
|
|
901
|
+
if (result.issues && result.issues.length > 0) {
|
|
902
|
+
for (const issue of result.issues) {
|
|
903
|
+
const color = sevColors[issue.severity] || '#95a5a6';
|
|
904
|
+
issuesHtml += `
|
|
905
|
+
<div class="issue" style="border-left: 4px solid ${color}; margin: 10px 0; padding: 12px; background: #f8f9fa; border-radius: 0 4px 4px 0;">
|
|
906
|
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
|
|
907
|
+
<span style="background: ${color}; color: white; padding: 2px 8px; border-radius: 3px; font-size: 12px; font-weight: bold;">${sevLabels[issue.severity] || issue.severity}</span>
|
|
908
|
+
<span style="background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 12px;">${typeLabels[issue.type] || issue.type}</span>
|
|
909
|
+
${issue.confidence ? `<span style="color: #6c757d; font-size: 12px;">Confidence: ${issue.confidence}/100</span>` : ''}
|
|
910
|
+
</div>
|
|
911
|
+
<div style="font-size: 14px; margin-bottom: 4px;">${issue.message}</div>
|
|
912
|
+
${issue.file ? `<div style="color: #6c757d; font-size: 12px;">📁 ${issue.file}${issue.line ? ':' + issue.line : ''}</div>` : ''}
|
|
913
|
+
${issue.suggestion ? `<div style="color: #27ae60; font-size: 13px; margin-top: 6px; padding: 8px; background: #eafaf1; border-radius: 3px;">💡 ${issue.suggestion}</div>` : ''}
|
|
914
|
+
</div>`;
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
issuesHtml = '<p style="color: #27ae60; text-align: center; padding: 20px;">✅ No issues found / 未发现问题</p>';
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
let suggestionsHtml = '';
|
|
921
|
+
if (result.suggestions && result.suggestions.length > 0) {
|
|
922
|
+
suggestionsHtml = '<h3>Suggestions / 改进建议</h3><ul>' + result.suggestions.map(s => '<li>' + s + '</li>').join('') + '</ul>';
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
let praiseHtml = '';
|
|
926
|
+
if (result.praise && result.praise.length > 0) {
|
|
927
|
+
praiseHtml = '<h3>👍 Good Practices / 好的实践</h3><ul>' + result.praise.map(p => '<li>✅ ' + p + '</li>').join('') + '</ul>';
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const scoreVal = result.score || 0;
|
|
931
|
+
const scoreColor = scoreVal >= 80 ? '#27ae60' : scoreVal >= 50 ? '#f39c12' : '#e74c3c';
|
|
932
|
+
const scoreLabel = scoreVal >= 80 ? 'Good / 良好' : scoreVal >= 50 ? 'Needs Improvement / 需改进' : 'Poor / 差';
|
|
933
|
+
|
|
934
|
+
return `<!DOCTYPE html>
|
|
935
|
+
<html lang="zh-CN">
|
|
936
|
+
<head>
|
|
937
|
+
<meta charset="UTF-8">
|
|
938
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
939
|
+
<title>coderev Review Report</title>
|
|
940
|
+
<style>
|
|
941
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #fff; color: #333; }
|
|
942
|
+
h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
|
943
|
+
.score-card { text-align: center; padding: 30px; background: #f8f9fa; border-radius: 8px; margin: 20px 0; }
|
|
944
|
+
.score-value { font-size: 48px; font-weight: bold; }
|
|
945
|
+
.score-label { font-size: 16px; color: #6c757d; margin-top: 5px; }
|
|
946
|
+
.summary { font-size: 16px; color: #555; margin: 15px 0; padding: 15px; background: #e8f4f8; border-radius: 6px; }
|
|
947
|
+
.stats { display: flex; gap: 20px; justify-content: center; margin: 20px 0; }
|
|
948
|
+
.stat { text-align: center; padding: 15px 25px; background: white; border: 1px solid #dee2e6; border-radius: 6px; }
|
|
949
|
+
.stat-value { font-size: 28px; font-weight: bold; }
|
|
950
|
+
.stat-label { font-size: 12px; color: #6c757d; text-transform: uppercase; }
|
|
951
|
+
h2 { margin-top: 30px; }
|
|
952
|
+
@media (prefers-color-scheme: dark) {
|
|
953
|
+
body { background: #1a1a2e; color: #e0e0e0; }
|
|
954
|
+
.score-card { background: #16213e; }
|
|
955
|
+
.summary { background: #0f3460; }
|
|
956
|
+
.issue { background: #16213e; }
|
|
957
|
+
.stat { background: #16213e; border-color: #0f3460; }
|
|
958
|
+
}
|
|
959
|
+
</style>
|
|
960
|
+
</head>
|
|
961
|
+
<body>
|
|
962
|
+
<h1>📋 coderev Review Report</h1>
|
|
963
|
+
<div class="score-card">
|
|
964
|
+
<div class="score-value" style="color: ${scoreColor}">${scoreVal}/100</div>
|
|
965
|
+
<div class="score-label">${scoreLabel}</div>
|
|
966
|
+
</div>
|
|
967
|
+
${result.summary ? '<div class="summary">📄 ' + result.summary + '</div>' : ''}
|
|
968
|
+
<div class="stats">
|
|
969
|
+
<div class="stat"><div class="stat-value" style="color: #e74c3c">${result.issues ? result.issues.filter(i => i.type === 'error').length : 0}</div><div class="stat-label">Errors</div></div>
|
|
970
|
+
<div class="stat"><div class="stat-value" style="color: #f39c12">${result.issues ? result.issues.filter(i => i.type === 'warning').length : 0}</div><div class="stat-label">Warnings</div></div>
|
|
971
|
+
<div class="stat"><div class="stat-value" style="color: #3498db">${result.issues ? result.issues.filter(i => i.type === 'info').length : 0}</div><div class="stat-label">Info</div></div>
|
|
972
|
+
<div class="stat"><div class="stat-value" style="color: #9b59b6">${result.issues ? result.issues.length : 0}</div><div class="stat-label">Total</div></div>
|
|
973
|
+
</div>
|
|
974
|
+
<h2>Issues / 问题</h2>
|
|
975
|
+
${issuesHtml}
|
|
976
|
+
${suggestionsHtml}
|
|
977
|
+
${praiseHtml}
|
|
978
|
+
<hr style="margin-top: 40px; border: none; border-top: 1px solid #eee;">
|
|
979
|
+
<p style="text-align: center; color: #6c757d; font-size: 12px;">Generated by coderev</p>
|
|
980
|
+
</body>
|
|
981
|
+
</html>`;
|
|
982
|
+
}
|
package/src/config.js
CHANGED
|
@@ -25,28 +25,47 @@ const DEFAULTS = {
|
|
|
25
25
|
format: 'terminal',
|
|
26
26
|
includeScore: true,
|
|
27
27
|
},
|
|
28
|
+
inheritance: {
|
|
29
|
+
enabled: true,
|
|
30
|
+
strategy: 'deep-merge', // 'deep-merge' | 'replace'
|
|
31
|
+
},
|
|
28
32
|
};
|
|
29
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Load configuration with multi-project inheritance support.
|
|
36
|
+
*
|
|
37
|
+
* Inheritance logic:
|
|
38
|
+
* 1. If configPath is explicitly specified, load that one file only (no inheritance)
|
|
39
|
+
* 2. If no configPath, search upward from cwd collecting ALL config files
|
|
40
|
+
* 3. Apply them in order: parent config first, then layered closer to cwd
|
|
41
|
+
* 4. Deep-merge: child values override parent values at the field level
|
|
42
|
+
*
|
|
43
|
+
* This allows teams to have a base .coderevrc.json at repo root
|
|
44
|
+
* and project-specific overrides in subdirectory projects.
|
|
45
|
+
*
|
|
46
|
+
* @param {string|null} configPath - Explicit path to config file
|
|
47
|
+
* @returns {object} Merged configuration
|
|
48
|
+
*/
|
|
30
49
|
function loadConfig(configPath) {
|
|
31
50
|
if (configPath) {
|
|
32
51
|
if (!fs.existsSync(configPath)) {
|
|
33
|
-
// If explicitly specified and not found, throw
|
|
34
|
-
if (configPath && !configPath.includes('nonexistent')) {
|
|
35
|
-
throw new Error(`Config file not found: ${configPath}`);
|
|
36
|
-
}
|
|
52
|
+
// If explicitly specified and not found, return defaults (don't throw for backward compat)
|
|
37
53
|
return { ...DEFAULTS };
|
|
38
54
|
}
|
|
39
55
|
return mergeDefaults(JSON.parse(fs.readFileSync(configPath, 'utf-8')));
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
//
|
|
58
|
+
// Multi-project inheritance: collect all config files from cwd up to root
|
|
59
|
+
const configStack = [];
|
|
43
60
|
let current = process.cwd();
|
|
61
|
+
|
|
44
62
|
while (true) {
|
|
45
63
|
for (const filename of CONFIG_FILES) {
|
|
46
64
|
const fullPath = path.join(current, filename);
|
|
47
65
|
if (fs.existsSync(fullPath)) {
|
|
48
66
|
const userConfig = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
49
|
-
|
|
67
|
+
configStack.push({ path: fullPath, config: userConfig });
|
|
68
|
+
break; // Only the first matching file per directory
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
const parent = path.dirname(current);
|
|
@@ -54,7 +73,61 @@ function loadConfig(configPath) {
|
|
|
54
73
|
current = parent;
|
|
55
74
|
}
|
|
56
75
|
|
|
57
|
-
|
|
76
|
+
// Reverse: apply from farthest parent first, then child overrides
|
|
77
|
+
// configStack[0] = closest to cwd (highest priority)
|
|
78
|
+
// configStack[n] = farthest from cwd (lowest priority)
|
|
79
|
+
configStack.reverse();
|
|
80
|
+
|
|
81
|
+
if (configStack.length === 0) {
|
|
82
|
+
return { ...DEFAULTS, _inheritanceStack: [] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if inheritance is disabled by any config in the stack
|
|
86
|
+
const inheritanceDisabled = configStack.some(
|
|
87
|
+
entry => entry.config.inheritance && entry.config.inheritance.enabled === false
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (inheritanceDisabled || configStack.length === 1) {
|
|
91
|
+
// Only use the first (closest) config
|
|
92
|
+
return mergeDefaults(configStack[configStack.length - 1].config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Deep-merge from parent to child
|
|
96
|
+
let merged = {};
|
|
97
|
+
for (const entry of configStack) {
|
|
98
|
+
merged = deepMerge(merged, entry.config);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
merged._inheritanceStack = configStack.map(e => e.path);
|
|
102
|
+
return mergeDefaults(merged);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Deep-merge two objects. Source values override target values.
|
|
107
|
+
* Arrays are replaced, not concatenated.
|
|
108
|
+
*/
|
|
109
|
+
function deepMerge(target, source) {
|
|
110
|
+
const result = { ...target };
|
|
111
|
+
|
|
112
|
+
for (const key of Object.keys(source)) {
|
|
113
|
+
const targetVal = target[key];
|
|
114
|
+
const sourceVal = source[key];
|
|
115
|
+
|
|
116
|
+
if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
|
|
117
|
+
result[key] = deepMerge(targetVal, sourceVal);
|
|
118
|
+
} else {
|
|
119
|
+
result[key] = sourceVal;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if a value is a plain object (not array, null, etc.)
|
|
128
|
+
*/
|
|
129
|
+
function isPlainObject(value) {
|
|
130
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
58
131
|
}
|
|
59
132
|
|
|
60
133
|
function mergeDefaults(userConfig) {
|
|
@@ -63,12 +136,17 @@ function mergeDefaults(userConfig) {
|
|
|
63
136
|
merged.ai = { ...DEFAULTS.ai, ...(userConfig.ai || {}) };
|
|
64
137
|
merged.rules = { ...DEFAULTS.rules, ...(userConfig.rules || {}) };
|
|
65
138
|
merged.output = { ...DEFAULTS.output, ...(userConfig.output || {}) };
|
|
139
|
+
merged.inheritance = { ...DEFAULTS.inheritance, ...(userConfig.inheritance || {}) };
|
|
66
140
|
// Pass through any extra keys
|
|
67
141
|
for (const key of Object.keys(userConfig)) {
|
|
68
|
-
if (!['ai', 'rules', 'output'].includes(key)) {
|
|
142
|
+
if (!['ai', 'rules', 'output', 'inheritance'].includes(key)) {
|
|
69
143
|
merged[key] = userConfig[key];
|
|
70
144
|
}
|
|
71
145
|
}
|
|
146
|
+
// Preserve _inheritanceStack if present
|
|
147
|
+
if (userConfig._inheritanceStack) {
|
|
148
|
+
merged._inheritanceStack = userConfig._inheritanceStack;
|
|
149
|
+
}
|
|
72
150
|
return merged;
|
|
73
151
|
}
|
|
74
152
|
|