coderev-cli 1.0.14 → 1.0.15

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 CHANGED
@@ -3,6 +3,13 @@
3
3
  > 多智能体 AI 代码审查工具 — Security / Bug / Quality 三个 Agent 并行审查,带置信度评分。
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/coderev-cli)](https://www.npmjs.com/package/coderev-cli)
6
+ [![GitHub Sponsors](https://img.shields.io/github/sponsors/jishuanjimingtian)](https://github.com/sponsors/jishuanjimingtian)
7
+
8
+ ---
9
+
10
+ > 🌟 **Support coderev!** If you find this tool useful, consider [sponsoring on GitHub](https://github.com/sponsors/jishuanjimingtian) or buying me a coffee.
11
+ >
12
+ > 如果这个工具对你有帮助,可以考虑[在 GitHub 上赞助](https://github.com/sponsors/jishuanjimingtian)支持持续开发!
6
13
 
7
14
  ---
8
15
 
@@ -90,6 +97,50 @@ coderev review --single # 单 Agent 模式(v0.2 兼容,更
90
97
  coderev review --audit # 安全审计模式(OWASP 级)
91
98
  coderev review --no-cache # 跳过缓存
92
99
  coderev review --format json # JSON 输出
100
+ coderev review --format html # HTML 报告输出
101
+ coderev review --incremental # 只审查 diff 新增/变更行
102
+ coderev review --interactive # 交互式逐条修复问题
103
+ coderev review --ci # CI 模式(发现问题则 exit code 1)
104
+ ```
105
+
106
+ ### interactive 交互式修复
107
+
108
+ `--interactive` 模式让你逐一审查每个 issue,选择修复(AI 生成补丁)或跳过:
109
+
110
+ ```bash
111
+ coderev review --interactive
112
+ # 输出示例:
113
+ # Issue #1 of 3
114
+ # ● [high] [error] SQL injection risk in query construction
115
+ # File: src/db.js:42
116
+ # Suggestion: Use parameterized queries
117
+ # [a]pply fix / [s]kip / [q]uit > a
118
+ ```
119
+
120
+ ### CI 模式
121
+
122
+ `--ci` 模式用于 CI/CD 管道,发现问题时以非零退出码终止:
123
+
124
+ ```bash
125
+ coderev review --ci --min-confidence 70
126
+ coderev review --ci --output json # 结合 JSON 输出用于上游处理
127
+ ```
128
+
129
+ ### HTML 报告
130
+
131
+ 生成漂亮的可视化 HTML 报告:
132
+
133
+ ```bash
134
+ coderev review --output html > report.html
135
+ # 支持深色/浅色模式自动适配
136
+ ```
137
+
138
+ ### 增量审查
139
+
140
+ 只关注 diff 中新增和修改的行,忽略移除的上下文:
141
+
142
+ ```bash
143
+ coderev review --incremental
93
144
  ```
94
145
 
95
146
  ### fix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderev-cli",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "Multi-agent AI code review for git -- parallel agents with confidence scoring",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -5,7 +5,7 @@ const chalk = require('chalk');
5
5
  const path = require('path');
6
6
  const pkg = require('../package.json');
7
7
  const { reviewDiff } = require('./reviewer');
8
- const { loadConfig } = require('./config');
8
+ const { loadConfig, getApiKey } = require('./config');
9
9
  const { resolvePrRef, fetchPrDiff, postPrComment, resolveToken, fetchPrFiles, postInlineComments } = require('./github');
10
10
 
11
11
  program
@@ -21,7 +21,10 @@ 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)', 'terminal')
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
+ .option('--interactive', 'Interactively review and apply fixes for each issue')
25
28
  .option('--pr <ref>', 'GitHub PR to review, e.g. owner/repo#42 or full URL')
26
29
  .option('--gl <ref>', 'GitLab MR to review, e.g. owner/repo!42 or full URL')
27
30
  .option('--gee <ref>', 'Gitee PR to review, e.g. owner/repo!42 or full URL')
@@ -172,7 +175,8 @@ program
172
175
  }
173
176
 
174
177
  const result = await reviewDiff(diff, config, {
175
- noCache: options.noCache === false,
178
+ noCache: options.noCache === false,
179
+ incremental: options.incremental || undefined,
176
180
  ignorePattern,
177
181
  audit: options.audit || undefined,
178
182
  single: options.single || undefined,
@@ -184,6 +188,8 @@ program
184
188
  output = JSON.stringify(result, null, 2);
185
189
  } else if (options.output === 'markdown') {
186
190
  output = formatMarkdown(result);
191
+ } else if (options.output === 'html') {
192
+ output = formatHtml(result);
187
193
  } else {
188
194
  output = formatTerminal(result);
189
195
  }
@@ -255,6 +261,78 @@ program
255
261
  }
256
262
  }
257
263
 
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
+
258
336
  console.log(output);
259
337
  } catch (err) {
260
338
  console.error(chalk.red(`✖ ${err.message}`));
@@ -639,120 +717,213 @@ async function getGitDiff(repoPath, base = 'main', head) {
639
717
  }
640
718
  }
641
719
 
642
- function formatTerminal(result) {
643
- // Chinese section
644
- const cnLines = [];
645
- cnLines.push(chalk.bold('\n📋 代码审查报告'));
646
- cnLines.push('━'.repeat(50));
647
- if (result.summary) cnLines.push('\n' + chalk.bold('摘要:') + ' ' + result.summary);
648
- if (result.score !== undefined && result.score !== null) {
649
- const color = result.score >= 80 ? chalk.green : result.score >= 50 ? chalk.yellow : chalk.red;
650
- cnLines.push('\n' + chalk.bold('评分:') + ' ' + color(result.score + '/100'));
651
- }
652
- if (result.issues && result.issues.length > 0) {
653
- cnLines.push('\n' + chalk.bold('问题 (' + result.issues.length + '):'));
654
- for (const issue of result.issues) {
655
- const typeLabel = issue.type === 'error' ? chalk.red('✖') : issue.type === 'warning' ? chalk.yellow('⚠') : chalk.blue('ℹ');
656
- const sevMap = { high: '严重', medium: '中等', low: '轻微' };
657
- const sevLabel = issue.severity && sevMap[issue.severity] ? ' [' + sevMap[issue.severity] + ']' : '';
658
- cnLines.push(' ' + typeLabel + sevLabel + ' ' + issue.message);
659
- if (issue.file) cnLines.push(' ' + chalk.gray('文件:') + ' ' + issue.file);
660
- if (issue.line) cnLines.push(' ' + chalk.gray('行号:') + ' ' + issue.line);
661
- if (issue.suggestion) cnLines.push(' ' + chalk.gray('建议:') + ' ' + issue.suggestion);
662
- }
663
- }
664
- if (result.suggestions && result.suggestions.length > 0) {
665
- cnLines.push('\n' + chalk.bold('改进建议:'));
666
- for (const s of result.suggestions) cnLines.push(' 💡 ' + s);
667
- }
668
- if (result.praise && result.praise.length > 0) {
669
- cnLines.push('\n' + chalk.bold('👍 好的实践:'));
670
- for (const p of result.praise) cnLines.push(' ✅ ' + p);
671
- }
672
- cnLines.push('\n' + '━'.repeat(50));
720
+ function formatTerminal(result) {
721
+ // Chinese section
722
+ const cnLines = [];
723
+ cnLines.push(chalk.bold('\n📋 代码审查报告'));
724
+ cnLines.push('━'.repeat(50));
725
+ if (result.summary) cnLines.push('\n' + chalk.bold('摘要:') + ' ' + result.summary);
726
+ if (result.score !== undefined && result.score !== null) {
727
+ const color = result.score >= 80 ? chalk.green : result.score >= 50 ? chalk.yellow : chalk.red;
728
+ cnLines.push('\n' + chalk.bold('评分:') + ' ' + color(result.score + '/100'));
729
+ }
730
+ if (result.issues && result.issues.length > 0) {
731
+ cnLines.push('\n' + chalk.bold('问题 (' + result.issues.length + '):'));
732
+ for (const issue of result.issues) {
733
+ const typeLabel = issue.type === 'error' ? chalk.red('✖') : issue.type === 'warning' ? chalk.yellow('⚠') : chalk.blue('ℹ');
734
+ const sevMap = { high: '严重', medium: '中等', low: '轻微' };
735
+ const sevLabel = issue.severity && sevMap[issue.severity] ? ' [' + sevMap[issue.severity] + ']' : '';
736
+ cnLines.push(' ' + typeLabel + sevLabel + ' ' + issue.message);
737
+ if (issue.file) cnLines.push(' ' + chalk.gray('文件:') + ' ' + issue.file);
738
+ if (issue.line) cnLines.push(' ' + chalk.gray('行号:') + ' ' + issue.line);
739
+ if (issue.suggestion) cnLines.push(' ' + chalk.gray('建议:') + ' ' + issue.suggestion);
740
+ }
741
+ }
742
+ if (result.suggestions && result.suggestions.length > 0) {
743
+ cnLines.push('\n' + chalk.bold('改进建议:'));
744
+ for (const s of result.suggestions) cnLines.push(' 💡 ' + s);
745
+ }
746
+ if (result.praise && result.praise.length > 0) {
747
+ cnLines.push('\n' + chalk.bold('👍 好的实践:'));
748
+ for (const p of result.praise) cnLines.push(' ✅ ' + p);
749
+ }
750
+ cnLines.push('\n' + '━'.repeat(50));
751
+
752
+ // English section
753
+ const enLines = [];
754
+ enLines.push(chalk.bold('\n📋 Code Review Report'));
755
+ enLines.push('━'.repeat(50));
756
+ if (result.summary) enLines.push('\n' + chalk.bold('Summary:') + ' ' + result.summary);
757
+ if (result.score !== undefined && result.score !== null) {
758
+ const color = result.score >= 80 ? chalk.green : result.score >= 50 ? chalk.yellow : chalk.red;
759
+ enLines.push('\n' + chalk.bold('Score:') + ' ' + color(result.score + '/100'));
760
+ }
761
+ if (result.issues && result.issues.length > 0) {
762
+ enLines.push('\n' + chalk.bold('Issues (' + result.issues.length + '):'));
763
+ for (const issue of result.issues) {
764
+ const typeLabel = issue.type === 'error' ? chalk.red('✖') : issue.type === 'warning' ? chalk.yellow('⚠') : chalk.blue('ℹ');
765
+ const sev = issue.severity ? ' [' + issue.severity + ']' : '';
766
+ enLines.push(' ' + typeLabel + sev + ' ' + issue.message);
767
+ if (issue.file) enLines.push(' ' + chalk.gray('File:') + ' ' + issue.file);
768
+ if (issue.line) enLines.push(' ' + chalk.gray('Line:') + ' ' + issue.line);
769
+ if (issue.suggestion) enLines.push(' ' + chalk.gray('Suggestion:') + ' ' + issue.suggestion);
770
+ }
771
+ }
772
+ if (result.suggestions && result.suggestions.length > 0) {
773
+ enLines.push('\n' + chalk.bold('Suggestions:'));
774
+ for (const s of result.suggestions) enLines.push(' 💡 ' + s);
775
+ }
776
+ if (result.praise && result.praise.length > 0) {
777
+ enLines.push('\n' + chalk.bold('👍 Good Practices:'));
778
+ for (const p of result.praise) enLines.push(' ✅ ' + p);
779
+ }
780
+ enLines.push('\n' + '━'.repeat(50));
781
+
782
+ return cnLines.join('\n') + '\n' + enLines.join('\n');
783
+ }
784
+
785
+ function formatMarkdown(result) {
786
+ // Chinese section
787
+ let md = '# 📋 代码审查报告\n\n';
788
+ if (result.summary) md += '**摘要:** ' + result.summary + '\n\n';
789
+ if (result.score !== undefined) md += '**评分:** ' + result.score + '/100\n\n';
790
+ if (result.issues?.length) {
791
+ md += '## 问题 (' + result.issues.length + ')\n\n';
792
+ for (const issue of result.issues) {
793
+ const sevMap = { high: '严重', medium: '中等', low: '轻微' };
794
+ const sevLabel = issue.severity && sevMap[issue.severity] ? ' [' + sevMap[issue.severity] + ']' : '';
795
+ md += '- **' + issue.type.toUpperCase() + '**' + sevLabel + ': ' + issue.message + '\n';
796
+ if (issue.file) md += ' - 文件: \`' + issue.file + '\`\n';
797
+ if (issue.line) md += ' - 行号: ' + issue.line + '\n';
798
+ if (issue.suggestion) md += ' - 建议: ' + issue.suggestion + '\n';
799
+ }
800
+ }
801
+ if (result.suggestions?.length) {
802
+ md += '\n## 改进建议\n\n';
803
+ for (const s of result.suggestions) md += '- 💡 ' + s + '\n';
804
+ }
805
+ if (result.praise?.length) {
806
+ md += '\n## 👍 好的实践\n\n';
807
+ for (const p of result.praise) md += '- ✅ ' + p + '\n';
808
+ }
809
+
810
+ // English section
811
+ md += '\n---\n\n';
812
+ md += '# 📋 Code Review Report\n\n';
813
+ if (result.summary) md += '**Summary:** ' + result.summary + '\n\n';
814
+ if (result.score !== undefined) md += '**Score:** ' + result.score + '/100\n\n';
815
+ if (result.issues?.length) {
816
+ md += '## Issues (' + result.issues.length + ')\n\n';
817
+ for (const issue of result.issues) {
818
+ md += '- **' + issue.type.toUpperCase() + '**';
819
+ if (issue.severity) md += ' [' + issue.severity + ']';
820
+ md += ': ' + issue.message + '\n';
821
+ if (issue.file) md += ' - File: \`' + issue.file + '\`\n';
822
+ if (issue.line) md += ' - Line: ' + issue.line + '\n';
823
+ if (issue.suggestion) md += ' - Suggestion: ' + issue.suggestion + '\n';
824
+ }
825
+ }
826
+ if (result.suggestions?.length) {
827
+ md += '\n## Suggestions\n\n';
828
+ for (const s of result.suggestions) md += '- 💡 ' + s + '\n';
829
+ }
830
+ if (result.praise?.length) {
831
+ md += '\n## 👍 Good Practices\n\n';
832
+ for (const p of result.praise) md += '- ✅ ' + p + '\n';
833
+ }
834
+
835
+ return md;
836
+ }
673
837
 
674
- // English section
675
- const enLines = [];
676
- enLines.push(chalk.bold('\n📋 Code Review Report'));
677
- enLines.push('━'.repeat(50));
678
- if (result.summary) enLines.push('\n' + chalk.bold('Summary:') + ' ' + result.summary);
679
- if (result.score !== undefined && result.score !== null) {
680
- const color = result.score >= 80 ? chalk.green : result.score >= 50 ? chalk.yellow : chalk.red;
681
- enLines.push('\n' + chalk.bold('Score:') + ' ' + color(result.score + '/100'));
682
- }
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 = '';
683
848
  if (result.issues && result.issues.length > 0) {
684
- enLines.push('\n' + chalk.bold('Issues (' + result.issues.length + '):'));
685
849
  for (const issue of result.issues) {
686
- const typeLabel = issue.type === 'error' ? chalk.red('✖') : issue.type === 'warning' ? chalk.yellow('⚠') : chalk.blue('ℹ');
687
- const sev = issue.severity ? ' [' + issue.severity + ']' : '';
688
- enLines.push(' ' + typeLabel + sev + ' ' + issue.message);
689
- if (issue.file) enLines.push(' ' + chalk.gray('File:') + ' ' + issue.file);
690
- if (issue.line) enLines.push(' ' + chalk.gray('Line:') + ' ' + issue.line);
691
- if (issue.suggestion) enLines.push(' ' + chalk.gray('Suggestion:') + ' ' + issue.suggestion);
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>`;
692
862
  }
863
+ } else {
864
+ issuesHtml = '<p style="color: #27ae60; text-align: center; padding: 20px;">✅ No issues found / 未发现问题</p>';
693
865
  }
866
+
867
+ let suggestionsHtml = '';
694
868
  if (result.suggestions && result.suggestions.length > 0) {
695
- enLines.push('\n' + chalk.bold('Suggestions:'));
696
- for (const s of result.suggestions) enLines.push(' 💡 ' + s);
869
+ suggestionsHtml = '<h3>Suggestions / 改进建议</h3><ul>' + result.suggestions.map(s => '<li>' + s + '</li>').join('') + '</ul>';
697
870
  }
871
+
872
+ let praiseHtml = '';
698
873
  if (result.praise && result.praise.length > 0) {
699
- enLines.push('\n' + chalk.bold('👍 Good Practices:'));
700
- for (const p of result.praise) enLines.push(' ✅ ' + p);
874
+ praiseHtml = '<h3>👍 Good Practices / 好的实践</h3><ul>' + result.praise.map(p => '<li>✅ ' + p + '</li>').join('') + '</ul>';
701
875
  }
702
- enLines.push('\n' + '━'.repeat(50));
703
876
 
704
- return cnLines.join('\n') + '\n' + enLines.join('\n');
705
- }
706
-
707
- function formatMarkdown(result) {
708
- // Chinese section
709
- let md = '# 📋 代码审查报告\n\n';
710
- if (result.summary) md += '**摘要:** ' + result.summary + '\n\n';
711
- if (result.score !== undefined) md += '**评分:** ' + result.score + '/100\n\n';
712
- if (result.issues?.length) {
713
- md += '## 问题 (' + result.issues.length + ')\n\n';
714
- for (const issue of result.issues) {
715
- const sevMap = { high: '严重', medium: '中等', low: '轻微' };
716
- const sevLabel = issue.severity && sevMap[issue.severity] ? ' [' + sevMap[issue.severity] + ']' : '';
717
- md += '- **' + issue.type.toUpperCase() + '**' + sevLabel + ': ' + issue.message + '\n';
718
- if (issue.file) md += ' - 文件: \`' + issue.file + '\`\n';
719
- if (issue.line) md += ' - 行号: ' + issue.line + '\n';
720
- if (issue.suggestion) md += ' - 建议: ' + issue.suggestion + '\n';
721
- }
722
- }
723
- if (result.suggestions?.length) {
724
- md += '\n## 改进建议\n\n';
725
- for (const s of result.suggestions) md += '- 💡 ' + s + '\n';
726
- }
727
- if (result.praise?.length) {
728
- md += '\n## 👍 好的实践\n\n';
729
- for (const p of result.praise) md += '- ✅ ' + p + '\n';
730
- }
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 / 差';
731
880
 
732
- // English section
733
- md += '\n---\n\n';
734
- md += '# 📋 Code Review Report\n\n';
735
- if (result.summary) md += '**Summary:** ' + result.summary + '\n\n';
736
- if (result.score !== undefined) md += '**Score:** ' + result.score + '/100\n\n';
737
- if (result.issues?.length) {
738
- md += '## Issues (' + result.issues.length + ')\n\n';
739
- for (const issue of result.issues) {
740
- md += '- **' + issue.type.toUpperCase() + '**';
741
- if (issue.severity) md += ' [' + issue.severity + ']';
742
- md += ': ' + issue.message + '\n';
743
- if (issue.file) md += ' - File: \`' + issue.file + '\`\n';
744
- if (issue.line) md += ' - Line: ' + issue.line + '\n';
745
- if (issue.suggestion) md += ' - Suggestion: ' + issue.suggestion + '\n';
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; }
746
905
  }
747
- }
748
- if (result.suggestions?.length) {
749
- md += '\n## Suggestions\n\n';
750
- for (const s of result.suggestions) md += '- 💡 ' + s + '\n';
751
- }
752
- if (result.praise?.length) {
753
- md += '\n## 👍 Good Practices\n\n';
754
- for (const p of result.praise) md += '- ✅ ' + p + '\n';
755
- }
756
-
757
- return md;
758
- }
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
+ }
package/src/fixer.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Interactive fix and incremental diff utilities for coderev.
3
+ */
4
+
5
+ /**
6
+ * Generate a fix patch for a specific issue in the diff.
7
+ * @param {string} diff - Original diff text
8
+ * @param {object} issue - The issue to fix
9
+ * @param {string} apiKey - API key
10
+ * @param {object} config - Config object
11
+ * @returns {Promise<{patch: string|null, explanation: string}>}
12
+ */
13
+ async function generateFix(diff, issue, apiKey, config) {
14
+ const { callAI } = require('./reviewer');
15
+
16
+ const systemMsg = 'You are an expert programmer. Given a diff and an issue found during code review, generate a unified patch that fixes the issue.\n\nReturn ONLY a valid JSON object:\n```json\n{\n "patch": "the unified diff patch content",\n "explanation": "one-line explanation of what was changed"\n}\n```\n\nRules:\n- Generate a proper unified diff format patch (git diff format)\n- Fix ONLY the specific issue described\n- Do NOT introduce any other changes\n- If you cannot generate a fix (e.g., the issue requires human judgment), return { "patch": null, "explanation": "Cannot auto-fix: reason" }';
17
+
18
+ const userContent = 'Diff that needs fixing:\n```diff\n' + diff.slice(0, 4000) + '\n```\n\nIssue to fix:\n- Type: ' + issue.type + '\n- Severity: ' + issue.severity + '\n- File: ' + (issue.file || 'N/A') + '\n- Line: ' + (issue.line || 'N/A') + '\n- Message: ' + issue.message + '\n- Suggestion: ' + (issue.suggestion || 'N/A') + '\n\nGenerate the fix patch:';
19
+
20
+ const prompt = [
21
+ { role: 'system', content: systemMsg },
22
+ { role: 'user', content: userContent },
23
+ ];
24
+
25
+ try {
26
+ const text = await callAI(apiKey, prompt, config);
27
+ const parsed = JSON.parse(text);
28
+ return parsed;
29
+ } catch {
30
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
31
+ if (jsonMatch) {
32
+ try {
33
+ return JSON.parse(jsonMatch[0]);
34
+ } catch {}
35
+ }
36
+ return { patch: null, explanation: 'Failed to parse AI fix response' };
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Parse a diff to extract only the added/changed lines (incremental review).
42
+ * @param {string} diff - Full git diff
43
+ * @returns {string} Filtered diff with only new/changed content
44
+ */
45
+ function parseIncrementalDiff(diff) {
46
+ if (!diff) return diff;
47
+ const lines = diff.split('\n');
48
+ const result = [];
49
+ let inHunk = false;
50
+ let addedLines = [];
51
+
52
+ for (const line of lines) {
53
+ if (line.startsWith('diff --git')) {
54
+ if (addedLines.length > 0) {
55
+ result.push(...addedLines);
56
+ addedLines = [];
57
+ }
58
+ result.push(line);
59
+ inHunk = false;
60
+ } else if (line.startsWith('--- ') || line.startsWith('+++ ')) {
61
+ result.push(line);
62
+ } else if (line.startsWith('@@ ')) {
63
+ if (addedLines.length > 0) {
64
+ result.push(...addedLines);
65
+ addedLines = [];
66
+ }
67
+ result.push(line);
68
+ inHunk = true;
69
+ addedLines = [];
70
+ } else if (inHunk) {
71
+ if (line.startsWith('+')) {
72
+ addedLines.push(line);
73
+ } else if (line.startsWith('-')) {
74
+ // Skip removed lines
75
+ } else {
76
+ addedLines.push(line);
77
+ }
78
+ }
79
+ }
80
+ if (addedLines.length > 0) {
81
+ result.push(...addedLines);
82
+ }
83
+ return result.join('\n');
84
+ }
85
+
86
+ module.exports = { generateFix, parseIncrementalDiff };
package/src/reviewer.js CHANGED
@@ -558,4 +558,4 @@ function loadProjectHint() {
558
558
  return '';
559
559
  }
560
560
 
561
- module.exports = { reviewDiff, parseReviewResponse };
561
+ module.exports = { reviewDiff, parseReviewResponse, callAI };