smart-review 1.0.1 → 1.0.2
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.en-US.md +580 -0
- package/README.md +93 -54
- package/bin/install.js +192 -55
- package/bin/review.js +42 -47
- package/index.js +0 -1
- package/lib/ai-client-pool.js +63 -25
- package/lib/ai-client.js +262 -415
- package/lib/config-loader.js +35 -7
- package/lib/default-config.js +33 -24
- package/lib/reviewer.js +267 -97
- package/lib/segmented-analyzer.js +102 -126
- package/lib/utils/git-diff-parser.js +9 -8
- package/lib/utils/i18n.js +986 -0
- package/package.json +2 -10
- package/templates/rules/en-US/best-practices.js +111 -0
- package/templates/rules/en-US/performance.js +123 -0
- package/templates/rules/en-US/security.js +311 -0
- package/templates/rules/zh-CN/best-practices.js +111 -0
- package/templates/rules/zh-CN/performance.js +123 -0
- package/templates/rules/zh-CN/security.js +311 -0
- package/templates/smart-review.json +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Smart Review
|
|
2
2
|
|
|
3
|
+
> 语言 / Language: [中文](README.md) | [English](README.en-US.md)
|
|
4
|
+
|
|
3
5
|
🚀 **AI智能代码审查工具** - 结合静态规则检测和AI智能分析,为你的代码提供全方位的安全和质量审查。
|
|
4
6
|
|
|
5
7
|
## ✨ 特性
|
|
@@ -441,36 +443,45 @@ Smart Reviewer 采用智能分批处理技术,能够高效处理超大文件
|
|
|
441
443
|
```json
|
|
442
444
|
{
|
|
443
445
|
"ai": {
|
|
444
|
-
"maxRequestTokens": 8000, // 单次请求最大token数
|
|
445
|
-
"
|
|
446
|
-
"
|
|
446
|
+
"maxRequestTokens": 8000, // 单次请求最大 token 数
|
|
447
|
+
"chunkOverlapLines": 5, // 分段重叠行数
|
|
448
|
+
"minFilesPerBatch": 1, // 每批次最少文件数
|
|
449
|
+
"maxFilesPerBatch": 10 // 每批次最多文件数
|
|
447
450
|
}
|
|
448
451
|
}
|
|
449
452
|
```
|
|
450
453
|
|
|
451
454
|
#### 性能优化建议
|
|
452
455
|
|
|
453
|
-
1.
|
|
456
|
+
1. **合理设置批处理参数**
|
|
454
457
|
```json
|
|
455
458
|
{
|
|
456
|
-
"
|
|
457
|
-
|
|
459
|
+
"ai": {
|
|
460
|
+
"maxRequestTokens": 6000,
|
|
461
|
+
"chunkOverlapLines": 10,
|
|
462
|
+
"minFilesPerBatch": 1,
|
|
463
|
+
"maxFilesPerBatch": 5
|
|
464
|
+
}
|
|
458
465
|
}
|
|
459
466
|
```
|
|
460
467
|
|
|
461
468
|
2. **启用并发处理**
|
|
462
469
|
```json
|
|
463
470
|
{
|
|
464
|
-
"
|
|
465
|
-
|
|
471
|
+
"ai": {
|
|
472
|
+
"concurrency": 3, // 并发 AI 请求数量
|
|
473
|
+
"maxFilesPerBatch": 5 // 控制批次文件数
|
|
474
|
+
}
|
|
466
475
|
}
|
|
467
476
|
```
|
|
468
477
|
|
|
469
|
-
3.
|
|
478
|
+
3. **调整智能分批参数**
|
|
470
479
|
```json
|
|
471
480
|
{
|
|
472
|
-
"
|
|
473
|
-
|
|
481
|
+
"ai": {
|
|
482
|
+
"minFilesPerBatch": 1,
|
|
483
|
+
"maxFilesPerBatch": 5
|
|
484
|
+
}
|
|
474
485
|
}
|
|
475
486
|
```
|
|
476
487
|
|
|
@@ -551,45 +562,71 @@ const reviewer = new CodeReviewer(customConfig, defaultRules);
|
|
|
551
562
|
```bash
|
|
552
563
|
# OpenAI API配置
|
|
553
564
|
export OPENAI_API_KEY="your-api-key"
|
|
554
|
-
export OPENAI_BASE_URL="https://api.openai.com/v1"
|
|
555
565
|
|
|
556
566
|
# 调试模式
|
|
557
567
|
export DEBUG_SMART_REVIEW=true
|
|
568
|
+
|
|
569
|
+
# 国际化(i18n)
|
|
570
|
+
# Windows PowerShell(当前会话)
|
|
571
|
+
$env:SMART_REVIEW_LOCALE='zh-CN' # 或 'en-US'
|
|
572
|
+
|
|
573
|
+
# macOS/Linux bash
|
|
574
|
+
export SMART_REVIEW_LOCALE=zh-CN # 或 en-US
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
如果需要使用自定义的 OpenAI 兼容服务,请在项目的配置文件中设置 `ai.baseURL`:
|
|
578
|
+
|
|
579
|
+
```json
|
|
580
|
+
{
|
|
581
|
+
"ai": { "baseURL": "https://api.openai.com/v1" }
|
|
582
|
+
}
|
|
558
583
|
```
|
|
559
584
|
|
|
585
|
+
## 🌍 国际化 (i18n)
|
|
586
|
+
|
|
587
|
+
- 配置项 `locale`:在 `.smart-review/smart-review.json` 顶层设置输出与模板语言,支持 `zh-CN`、`en-US`。示例:`{"locale": "en-US"}`。
|
|
588
|
+
- 环境变量 `SMART_REVIEW_LOCALE`:优先级最高,安装和复制模板时将按该值选择语言目录。
|
|
589
|
+
- 选择优先级:环境变量 > 项目配置 `.smart-review/smart-review.json` > 模板默认配置 `templates/smart-review.json` > `zh-CN`。
|
|
590
|
+
- 模板目录结构:`templates/rules/<locale>/security.js|performance.js|best-practices.js`;当指定语言缺失某文件时自动回退到 `zh-CN`。
|
|
591
|
+
- 控制台与 Git 钩子提示会随 `locale` 切换语言,无需额外配置。
|
|
592
|
+
- 切换示例:
|
|
593
|
+
- PowerShell:`$env:SMART_REVIEW_LOCALE='en-US'; node bin/install.js`
|
|
594
|
+
- Bash:`export SMART_REVIEW_LOCALE=en-US && node bin/install.js`
|
|
595
|
+
|
|
596
|
+
如需新增语言(例如 `ja-JP`),在 `templates/rules/ja-JP/` 下添加三类规则模板文件,并在配置中设置 `"locale": "ja-JP"` 或通过环境变量切换即可。
|
|
597
|
+
|
|
560
598
|
## 🔧 命令行参数
|
|
561
599
|
|
|
562
600
|
```bash
|
|
563
601
|
smart-review [options]
|
|
564
602
|
|
|
565
603
|
选项:
|
|
566
|
-
--staged 审查Git暂存区文件
|
|
567
|
-
--files <files>
|
|
568
|
-
--ai 强制启用AI分析
|
|
569
|
-
--no-ai 禁用AI分析
|
|
570
|
-
--diff-only
|
|
571
|
-
--
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
**增量审查相关参数说明:**
|
|
580
|
-
- `--diff-only`: 强制启用Git Diff增量审查模式,覆盖配置文件中的`reviewOnlyChanges`设置
|
|
581
|
-
- `--full-file`: 禁用增量审查,审查完整文件内容,适用于需要全面审查的场景
|
|
604
|
+
--staged 审查 Git 暂存区文件
|
|
605
|
+
--files <files> 审查指定文件(逗号分隔)
|
|
606
|
+
--ai 强制启用 AI 分析
|
|
607
|
+
--no-ai 禁用 AI 分析
|
|
608
|
+
--diff-only 仅审查变更行(Git Diff 模式)
|
|
609
|
+
--debug 输出调试日志
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
**增量审查相关说明:**
|
|
613
|
+
- `--diff-only`:仅审查变更行(Git Diff 模式),覆盖配置项 `ai.reviewOnlyChanges`
|
|
614
|
+
- 禁用增量审查:在 `.smart-review/smart-review.json` 将 `ai.reviewOnlyChanges` 设为 `false`,适用于需要全面审查的场景
|
|
582
615
|
|
|
583
616
|
**使用示例:**
|
|
584
617
|
```bash
|
|
585
618
|
# 强制使用增量审查模式
|
|
586
619
|
smart-review --staged --diff-only
|
|
587
620
|
|
|
588
|
-
#
|
|
589
|
-
smart-review
|
|
621
|
+
# 审查完整文件内容(在配置中关闭增量)
|
|
622
|
+
# .smart-review/smart-review.json
|
|
623
|
+
{
|
|
624
|
+
"ai": { "reviewOnlyChanges": false }
|
|
625
|
+
}
|
|
626
|
+
smart-review --files src/important.js
|
|
590
627
|
|
|
591
628
|
# 结合其他参数使用
|
|
592
|
-
smart-review --staged --diff-only --
|
|
629
|
+
smart-review --staged --diff-only --debug
|
|
593
630
|
```
|
|
594
631
|
|
|
595
632
|
## 🚀 CI/CD 集成
|
|
@@ -649,13 +686,13 @@ code_review:
|
|
|
649
686
|
**解决方案**: 检查 `OPENAI_API_KEY` 环境变量或配置文件中的密钥
|
|
650
687
|
|
|
651
688
|
4. **大文件处理超时**
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
689
|
+
```
|
|
690
|
+
❌ 文件分析超时
|
|
691
|
+
```
|
|
692
|
+
**解决方案**:
|
|
693
|
+
- 降低 `ai.maxRequestTokens` 或减少批次文件数(`maxFilesPerBatch`),并适当降低 `chunkOverlapLines`
|
|
694
|
+
- 增加 `chunkOverlapLines` 以减少分段数量
|
|
695
|
+
- 检查网络连接稳定性
|
|
659
696
|
|
|
660
697
|
5. **分段分析结果不完整**
|
|
661
698
|
```
|
|
@@ -667,28 +704,30 @@ code_review:
|
|
|
667
704
|
- 调整 `maxRequestTokens` 配置
|
|
668
705
|
|
|
669
706
|
6. **内存占用过高**
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
707
|
+
```
|
|
708
|
+
❌ 内存不足错误
|
|
709
|
+
```
|
|
710
|
+
**解决方案**:
|
|
711
|
+
- 减少 `maxFilesPerBatch` 配置值
|
|
712
|
+
- 调整 `minFilesPerBatch`/`maxFilesPerBatch` 控制每批文件数量
|
|
713
|
+
- 添加更多文件到 `ignoreFiles` 列表
|
|
677
714
|
|
|
678
715
|
7. **Token 限制错误**
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
716
|
+
```
|
|
717
|
+
❌ Request too large
|
|
718
|
+
```
|
|
719
|
+
**解决方案**:
|
|
720
|
+
- 降低 `maxRequestTokens` 配置值
|
|
721
|
+
- 减少每批文件数量或启用增量审查 `--diff-only`
|
|
722
|
+
- 检查是否有超大的单行代码
|
|
686
723
|
|
|
687
724
|
### 调试模式
|
|
688
725
|
|
|
689
|
-
|
|
726
|
+
启用调试日志:
|
|
690
727
|
```bash
|
|
691
|
-
DEBUG_SMART_REVIEW=true smart-review --staged
|
|
728
|
+
DEBUG_SMART_REVIEW=true smart-review --staged
|
|
729
|
+
# 或
|
|
730
|
+
smart-review --staged --debug
|
|
692
731
|
```
|
|
693
732
|
|
|
694
733
|
## 🤝 贡献
|
package/bin/install.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { execSync } from 'child_process';
|
|
7
7
|
import { logger } from '../lib/utils/logger.js';
|
|
8
8
|
import { FILE_PERMISSIONS, BATCH_CONSTANTS } from '../lib/utils/constants.js';
|
|
9
|
+
import { t } from '../lib/utils/i18n.js';
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -19,12 +20,12 @@ class Installer {
|
|
|
19
20
|
|
|
20
21
|
findGitRoot() {
|
|
21
22
|
let currentDir = process.cwd();
|
|
22
|
-
logger.debug(
|
|
23
|
+
logger.debug(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_search_git_root_dbg', { dir: currentDir }));
|
|
23
24
|
|
|
24
25
|
for (let i = 0; i < BATCH_CONSTANTS.MAX_DIRECTORY_SEARCH_DEPTH; i++) {
|
|
25
26
|
const gitDir = path.join(currentDir, '.git');
|
|
26
27
|
if (fs.existsSync(gitDir)) {
|
|
27
|
-
logger.success(
|
|
28
|
+
logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_found_git_root_success', { dir: currentDir }));
|
|
28
29
|
return currentDir;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -35,25 +36,25 @@ class Installer {
|
|
|
35
36
|
currentDir = parentDir;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
logger.info('
|
|
39
|
+
logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_no_git_use_current'));
|
|
39
40
|
return process.cwd();
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
install() {
|
|
43
|
-
logger.info('
|
|
43
|
+
async install() {
|
|
44
|
+
logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_start'));
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
47
|
this.createReviewDirectory();
|
|
47
|
-
this.copyTemplateFiles();
|
|
48
|
+
await this.copyTemplateFiles();
|
|
48
49
|
this.installGitHooks();
|
|
49
50
|
this.showNextSteps();
|
|
50
51
|
|
|
51
|
-
logger.success('\n
|
|
52
|
-
logger.info('
|
|
53
|
-
logger.info(
|
|
52
|
+
logger.success('\n' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_done_success'));
|
|
53
|
+
logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_bundled_info'));
|
|
54
|
+
logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_customize_tip'));
|
|
54
55
|
|
|
55
56
|
} catch (error) {
|
|
56
|
-
logger.error('
|
|
57
|
+
logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_failed', { error: error.message }));
|
|
57
58
|
process.exit(1);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -61,25 +62,25 @@ class Installer {
|
|
|
61
62
|
createReviewDirectory() {
|
|
62
63
|
if (!fs.existsSync(this.reviewDir)) {
|
|
63
64
|
fs.mkdirSync(this.reviewDir, { recursive: true });
|
|
64
|
-
logger.success('
|
|
65
|
+
logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_review_dir'));
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// 创建AI提示词子目录(用于AI自定义提示)
|
|
68
69
|
const aiPromptsDir = path.join(this.reviewDir, 'ai-rules');
|
|
69
70
|
if (!fs.existsSync(aiPromptsDir)) {
|
|
70
71
|
fs.mkdirSync(aiPromptsDir, { recursive: true });
|
|
71
|
-
logger.success('
|
|
72
|
+
logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_ai_rules_dir'));
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
// 创建本地静态规则目录
|
|
75
76
|
const localRulesDir = path.join(this.reviewDir, 'local-rules');
|
|
76
77
|
if (!fs.existsSync(localRulesDir)) {
|
|
77
78
|
fs.mkdirSync(localRulesDir, { recursive: true });
|
|
78
|
-
logger.success('
|
|
79
|
+
logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_local_rules_dir'));
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
copyTemplateFiles() {
|
|
83
|
+
async copyTemplateFiles() {
|
|
83
84
|
// 将模板源路径(templates 下)映射到目标路径(.smart-review 下)
|
|
84
85
|
const templatesMap = [
|
|
85
86
|
{ src: 'smart-review.json', dest: 'smart-review.json', description: '主配置文件' },
|
|
@@ -87,9 +88,25 @@ class Installer {
|
|
|
87
88
|
{ src: 'rules/performance.js', dest: 'local-rules/performance.js', description: '性能规则' },
|
|
88
89
|
{ src: 'rules/best-practices.js', dest: 'local-rules/best-practices.js', description: '最佳实践规则' }
|
|
89
90
|
];
|
|
91
|
+
|
|
92
|
+
// 根据 locale 选择模板目录(优先 rules/<locale>/,否则回退到 rules/zh-CN/)
|
|
93
|
+
const loc = await this.resolveLocale();
|
|
90
94
|
|
|
91
95
|
for (const { src, dest, description } of templatesMap) {
|
|
92
|
-
|
|
96
|
+
let effectiveSrc = src;
|
|
97
|
+
if (src.startsWith('rules/')) {
|
|
98
|
+
const fileName = path.basename(src);
|
|
99
|
+
const candidateRel = path.join('rules', loc, fileName);
|
|
100
|
+
const candidateAbs = path.join(this.templatesDir, candidateRel);
|
|
101
|
+
const fallbackRel = path.join('rules', 'zh-CN', fileName);
|
|
102
|
+
const fallbackAbs = path.join(this.templatesDir, fallbackRel);
|
|
103
|
+
if (fs.existsSync(candidateAbs)) {
|
|
104
|
+
effectiveSrc = candidateRel;
|
|
105
|
+
} else if (fs.existsSync(fallbackAbs)) {
|
|
106
|
+
effectiveSrc = fallbackRel;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const templatePath = path.join(this.templatesDir, effectiveSrc);
|
|
93
110
|
const targetPath = path.join(this.reviewDir, dest);
|
|
94
111
|
|
|
95
112
|
if (fs.existsSync(templatePath) && !fs.existsSync(targetPath)) {
|
|
@@ -98,11 +115,108 @@ class Installer {
|
|
|
98
115
|
if (!fs.existsSync(targetDir)) {
|
|
99
116
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
100
117
|
}
|
|
118
|
+
// smart-review.json 原样复制;规则文件按 locale 生成本地化版本
|
|
119
|
+
if (src === 'smart-review.json') {
|
|
120
|
+
fs.copyFileSync(templatePath, targetPath);
|
|
121
|
+
} else {
|
|
122
|
+
try {
|
|
123
|
+
const content = await this.buildLocalizedRuleModule(templatePath);
|
|
124
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// 失败时退回直接复制原模板
|
|
127
|
+
fs.copyFileSync(templatePath, targetPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_template_success', { desc: description }));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async resolveLocale() {
|
|
136
|
+
// 解析语言优先级:环境变量 > 已复制的项目配置 > 模板默认配置 > zh-CN
|
|
137
|
+
let loc = process.env.SMART_REVIEW_LOCALE || '';
|
|
138
|
+
if (!loc) {
|
|
139
|
+
try {
|
|
140
|
+
const projectCfg = path.join(this.reviewDir, 'smart-review.json');
|
|
141
|
+
if (fs.existsSync(projectCfg)) {
|
|
142
|
+
const cfg = JSON.parse(fs.readFileSync(projectCfg, 'utf8'));
|
|
143
|
+
if (cfg && typeof cfg.locale === 'string' && cfg.locale.trim()) {
|
|
144
|
+
loc = cfg.locale.trim();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
// 忽略读取失败,继续尝试模板配置
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!loc) {
|
|
152
|
+
try {
|
|
153
|
+
const templateCfg = path.join(this.templatesDir, 'smart-review.json');
|
|
154
|
+
if (fs.existsSync(templateCfg)) {
|
|
155
|
+
const cfg = JSON.parse(fs.readFileSync(templateCfg, 'utf8'));
|
|
156
|
+
if (cfg && typeof cfg.locale === 'string' && cfg.locale.trim()) {
|
|
157
|
+
loc = cfg.locale.trim();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// 忽略读取失败
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!loc) loc = 'zh-CN';
|
|
165
|
+
return loc;
|
|
166
|
+
}
|
|
101
167
|
|
|
102
|
-
|
|
103
|
-
|
|
168
|
+
async buildLocalizedRuleModule(templatePath) {
|
|
169
|
+
// 解析语言优先级:环境变量 > 已复制的项目配置 > 模板默认配置 > zh-CN
|
|
170
|
+
let loc = process.env.SMART_REVIEW_LOCALE || '';
|
|
171
|
+
if (!loc) {
|
|
172
|
+
try {
|
|
173
|
+
const projectCfg = path.join(this.reviewDir, 'smart-review.json');
|
|
174
|
+
if (fs.existsSync(projectCfg)) {
|
|
175
|
+
const cfg = JSON.parse(fs.readFileSync(projectCfg, 'utf8'));
|
|
176
|
+
if (cfg && typeof cfg.locale === 'string' && cfg.locale.trim()) {
|
|
177
|
+
loc = cfg.locale.trim();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// 忽略读取失败,继续尝试模板配置
|
|
104
182
|
}
|
|
105
183
|
}
|
|
184
|
+
if (!loc) {
|
|
185
|
+
try {
|
|
186
|
+
const templateCfg = path.join(this.templatesDir, 'smart-review.json');
|
|
187
|
+
if (fs.existsSync(templateCfg)) {
|
|
188
|
+
const cfg = JSON.parse(fs.readFileSync(templateCfg, 'utf8'));
|
|
189
|
+
if (cfg && typeof cfg.locale === 'string' && cfg.locale.trim()) {
|
|
190
|
+
loc = cfg.locale.trim();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
// 忽略读取失败
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!loc) loc = 'zh-CN';
|
|
198
|
+
const fileUrl = `file://${templatePath.replace(/\\/g, '/')}`;
|
|
199
|
+
const mod = await import(fileUrl);
|
|
200
|
+
const rules = Array.isArray(mod?.default) ? mod.default : (Array.isArray(mod?.rules) ? mod.rules : []);
|
|
201
|
+
const localized = rules.map((r) => {
|
|
202
|
+
const id = r?.id;
|
|
203
|
+
if (!id) return r;
|
|
204
|
+
const nameKey = `rule_${id}_name`;
|
|
205
|
+
const msgKey = `rule_${id}_message`;
|
|
206
|
+
const sugKey = `rule_${id}_suggestion`;
|
|
207
|
+
const name = t(loc, nameKey);
|
|
208
|
+
const message = t(loc, msgKey);
|
|
209
|
+
const suggestion = t(loc, sugKey);
|
|
210
|
+
return {
|
|
211
|
+
...r,
|
|
212
|
+
name: (typeof name === 'string' && name !== nameKey) ? name : r.name,
|
|
213
|
+
message: (typeof message === 'string' && message !== msgKey) ? message : r.message,
|
|
214
|
+
suggestion: (typeof suggestion === 'string' && suggestion !== sugKey) ? suggestion : r.suggestion,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
// 生成ESM模块内容
|
|
218
|
+
const json = JSON.stringify(localized, null, 2);
|
|
219
|
+
return `// Generated by smart-review install (locale: ${loc})\nexport default ${json};\n`;
|
|
106
220
|
}
|
|
107
221
|
|
|
108
222
|
installGitHooks() {
|
|
@@ -116,7 +230,7 @@ class Installer {
|
|
|
116
230
|
}
|
|
117
231
|
|
|
118
232
|
if (!gitAvailable) {
|
|
119
|
-
logger.error('
|
|
233
|
+
logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_git_missing', { url: 'https://git-scm.com/downloads' }));
|
|
120
234
|
process.exit(1);
|
|
121
235
|
}
|
|
122
236
|
|
|
@@ -124,11 +238,11 @@ class Installer {
|
|
|
124
238
|
const gitDir = path.join(this.projectRoot, '.git');
|
|
125
239
|
// review-disable-start
|
|
126
240
|
if (!fs.existsSync(gitDir)) {
|
|
127
|
-
logger.warn(
|
|
241
|
+
logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_init_git_warn'));
|
|
128
242
|
try {
|
|
129
243
|
execSync('git init', { cwd: this.projectRoot, stdio: 'ignore' });
|
|
130
244
|
} catch (e) {
|
|
131
|
-
logger.error(
|
|
245
|
+
logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_init_git_failed'));
|
|
132
246
|
process.exit(1);
|
|
133
247
|
}
|
|
134
248
|
}
|
|
@@ -144,27 +258,27 @@ class Installer {
|
|
|
144
258
|
|
|
145
259
|
const preCommitHook = path.join(gitHooksDir, 'pre-commit');
|
|
146
260
|
|
|
147
|
-
const
|
|
148
|
-
|
|
261
|
+
const loc = process.env.SMART_REVIEW_LOCALE || 'zh-CN';
|
|
262
|
+
const hookContent = `#!/usr/bin/env bash
|
|
263
|
+
# ${t(loc, 'hook_header_comment')}
|
|
149
264
|
|
|
150
|
-
echo "
|
|
265
|
+
echo "${t(loc, 'hook_start_review')}"
|
|
151
266
|
|
|
152
267
|
# 获取暂存区文件
|
|
153
268
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
154
269
|
|
|
155
270
|
if [ -z "$STAGED_FILES" ]; then
|
|
156
|
-
echo "
|
|
271
|
+
echo "${t(loc, 'hook_no_staged')}"
|
|
157
272
|
exit 0
|
|
158
273
|
fi
|
|
159
274
|
|
|
160
|
-
echo "
|
|
275
|
+
echo "${t(loc, 'hook_found_staged_header')}"
|
|
161
276
|
echo "$STAGED_FILES"
|
|
162
277
|
|
|
163
278
|
# 运行代码审查(定位到仓库根目录)
|
|
164
279
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
165
|
-
cd "$REPO_ROOT" || { echo "
|
|
280
|
+
cd "$REPO_ROOT" || { echo "${t(loc, 'hook_cd_repo_fail')}"; exit 1; }
|
|
166
281
|
|
|
167
|
-
# 优先使用仓库根目录安装的 CLI
|
|
168
282
|
ROOT_BIN="$REPO_ROOT/node_modules/.bin/smart-review"
|
|
169
283
|
|
|
170
284
|
FOUND_CMD=""
|
|
@@ -173,8 +287,6 @@ FOUND_IS_ENTRY=0
|
|
|
173
287
|
if [ -f "$ROOT_BIN" ]; then
|
|
174
288
|
FOUND_CMD="$ROOT_BIN"
|
|
175
289
|
else
|
|
176
|
-
# 基于暂存文件的路径,逐层向上查找其子项目的 node_modules
|
|
177
|
-
# 限制最大向上层级,避免卡住
|
|
178
290
|
MAX_ASCEND=6
|
|
179
291
|
while IFS= read -r file; do
|
|
180
292
|
[ -z "$file" ] && continue
|
|
@@ -194,18 +306,17 @@ else
|
|
|
194
306
|
done <<< "$STAGED_FILES"
|
|
195
307
|
fi
|
|
196
308
|
|
|
197
|
-
# 额外兜底:PATH 中的全局 smart-review
|
|
198
309
|
if [ -z "$FOUND_CMD" ] && command -v smart-review >/dev/null 2>&1; then
|
|
199
310
|
FOUND_CMD="smart-review"; FOUND_IS_ENTRY=0
|
|
200
311
|
fi
|
|
201
312
|
|
|
202
313
|
if [ -z "$FOUND_CMD" ]; then
|
|
203
|
-
echo "
|
|
204
|
-
echo "
|
|
314
|
+
echo "${t(loc, 'hook_cmd_not_found1')}"
|
|
315
|
+
echo "${t(loc, 'hook_cmd_not_found2')}"
|
|
205
316
|
exit 1
|
|
206
317
|
fi
|
|
207
318
|
|
|
208
|
-
echo "
|
|
319
|
+
echo "${t(loc, 'hook_use_command_prefix')} $FOUND_CMD --staged"
|
|
209
320
|
if [ $FOUND_IS_ENTRY -eq 1 ]; then
|
|
210
321
|
node "$FOUND_CMD" --staged
|
|
211
322
|
else
|
|
@@ -214,21 +325,40 @@ fi
|
|
|
214
325
|
|
|
215
326
|
EXIT_CODE=$?
|
|
216
327
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
217
|
-
echo "
|
|
328
|
+
echo "${t(loc, 'hook_review_fail')}"
|
|
218
329
|
exit 1
|
|
219
330
|
else
|
|
220
|
-
echo "
|
|
331
|
+
echo "${t(loc, 'hook_review_pass')}"
|
|
221
332
|
exit 0
|
|
222
333
|
fi
|
|
223
334
|
`;
|
|
335
|
+
|
|
224
336
|
fs.writeFileSync(preCommitHook, hookContent);
|
|
337
|
+
// Windows 兼容:提供 CMD 包装器,调用 bash 执行同名脚本
|
|
338
|
+
try {
|
|
339
|
+
const preCommitCmd = path.join(gitHooksDir, 'pre-commit.cmd');
|
|
340
|
+
const cmdContent = [
|
|
341
|
+
'@echo off',
|
|
342
|
+
'SETLOCAL',
|
|
343
|
+
'set HOOK=%~dp0pre-commit',
|
|
344
|
+
'if not exist "%HOOK%" (',
|
|
345
|
+
' echo [smart-review] pre-commit hook missing.',
|
|
346
|
+
' exit /b 1',
|
|
347
|
+
')',
|
|
348
|
+
'bash "%HOOK%"',
|
|
349
|
+
'exit /b %ERRORLEVEL%\r\n'
|
|
350
|
+
].join('\r\n');
|
|
351
|
+
fs.writeFileSync(preCommitCmd, cmdContent);
|
|
352
|
+
} catch (e) {
|
|
353
|
+
// 忽略 CMD 包装器写入失败
|
|
354
|
+
}
|
|
225
355
|
|
|
226
356
|
// 设置执行权限
|
|
227
357
|
try {
|
|
228
358
|
fs.chmodSync(preCommitHook, FILE_PERMISSIONS.EXECUTABLE);
|
|
229
|
-
logger.success('
|
|
359
|
+
logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_precommit_installed_success'));
|
|
230
360
|
} catch (error) {
|
|
231
|
-
logger.warn('
|
|
361
|
+
logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_precommit_perm_warn'));
|
|
232
362
|
}
|
|
233
363
|
|
|
234
364
|
// 测试钩子是否能正常执行
|
|
@@ -236,45 +366,52 @@ fi
|
|
|
236
366
|
}
|
|
237
367
|
|
|
238
368
|
testHook(hookPath) {
|
|
239
|
-
logger.info('
|
|
369
|
+
logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_hook'));
|
|
240
370
|
|
|
241
371
|
// 检查文件是否存在且可执行
|
|
242
372
|
if (!fs.existsSync(hookPath)) {
|
|
243
|
-
logger.error('
|
|
373
|
+
logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_missing'));
|
|
244
374
|
return;
|
|
245
375
|
}
|
|
246
376
|
|
|
247
377
|
try {
|
|
378
|
+
// 在 Windows 上无需检查可执行位,直接提示成功
|
|
379
|
+
if (process.platform === 'win32') {
|
|
380
|
+
logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_hook_success'));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
248
383
|
const stats = fs.statSync(hookPath);
|
|
249
384
|
const isExecutable = !!(stats.mode & 0o111);
|
|
250
|
-
logger.debug(
|
|
251
|
-
|
|
385
|
+
logger.debug(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_dbg', { mode: stats.mode.toString(8), exec: isExecutable }));
|
|
252
386
|
if (!isExecutable) {
|
|
253
|
-
logger.warn('
|
|
387
|
+
logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_fix_warn'));
|
|
254
388
|
fs.chmodSync(hookPath, FILE_PERMISSIONS.EXECUTABLE);
|
|
255
389
|
}
|
|
390
|
+
// POSIX 环境下权限检查完成,提示成功
|
|
391
|
+
logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_hook_success'));
|
|
256
392
|
} catch (error) {
|
|
257
|
-
logger.warn('
|
|
393
|
+
logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_check_failed', { error: error.message }));
|
|
258
394
|
}
|
|
259
395
|
}
|
|
260
396
|
|
|
261
397
|
showNextSteps() {
|
|
262
|
-
logger.info('\n
|
|
263
|
-
logger.info('
|
|
264
|
-
logger.info('
|
|
265
|
-
logger.info('
|
|
266
|
-
logger.info('
|
|
267
|
-
|
|
398
|
+
logger.info('\n' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_header'));
|
|
399
|
+
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_item1'));
|
|
400
|
+
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_item2'));
|
|
401
|
+
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_item3'));
|
|
402
|
+
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_item4'));
|
|
403
|
+
|
|
404
|
+
logger.info('\n' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_paths_header'));
|
|
268
405
|
logger.info(` ${path.join(this.reviewDir, 'smart-review.json')}`);
|
|
269
|
-
logger.info(
|
|
270
|
-
logger.info(
|
|
406
|
+
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_local_rules_path', { path: path.join(this.reviewDir, 'local-rules/') }));
|
|
407
|
+
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_ai_rules_path', { path: path.join(this.reviewDir, 'ai-rules/') }));
|
|
271
408
|
|
|
272
|
-
logger.info('\n
|
|
273
|
-
logger.info('
|
|
274
|
-
logger.info('
|
|
409
|
+
logger.info('\n' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_header'));
|
|
410
|
+
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_git_commit'));
|
|
411
|
+
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_cli'));
|
|
275
412
|
}
|
|
276
413
|
}
|
|
277
414
|
|
|
278
415
|
// 运行安装
|
|
279
416
|
const installer = new Installer();
|
|
280
|
-
installer.install();
|
|
417
|
+
(async () => { await installer.install(); })();
|