coderev-cli 1.0.14 → 1.0.16
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 +568 -194
- package/package.json +1 -1
- package/src/cli.js +279 -108
- package/src/fixer.js +86 -0
- package/src/reviewer.js +1 -1
package/package.json
CHANGED
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
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
|
|
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