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/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
- // Search up from cwd
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
- return mergeDefaults(userConfig);
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
- return { ...DEFAULTS };
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