smart-review 1.0.2 → 1.0.3

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/bin/install.js CHANGED
@@ -1,417 +1,419 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs';
4
- import path from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { execSync } from 'child_process';
7
- import { logger } from '../lib/utils/logger.js';
8
- import { FILE_PERMISSIONS, BATCH_CONSTANTS } from '../lib/utils/constants.js';
9
- import { t } from '../lib/utils/i18n.js';
10
-
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = path.dirname(__filename);
13
-
14
- class Installer {
15
- constructor() {
16
- this.projectRoot = this.findGitRoot();
17
- this.reviewDir = path.join(this.projectRoot, '.smart-review');
18
- this.templatesDir = path.join(__dirname, '../templates');
19
- }
20
-
21
- findGitRoot() {
22
- let currentDir = process.cwd();
23
- logger.debug(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_search_git_root_dbg', { dir: currentDir }));
24
-
25
- for (let i = 0; i < BATCH_CONSTANTS.MAX_DIRECTORY_SEARCH_DEPTH; i++) {
26
- const gitDir = path.join(currentDir, '.git');
27
- if (fs.existsSync(gitDir)) {
28
- logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_found_git_root_success', { dir: currentDir }));
29
- return currentDir;
30
- }
31
-
32
- const parentDir = path.dirname(currentDir);
33
- if (parentDir === currentDir) {
34
- break; // 到达根目录
35
- }
36
- currentDir = parentDir;
37
- }
38
-
39
- logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_no_git_use_current'));
40
- return process.cwd();
41
- }
42
-
43
- async install() {
44
- logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_start'));
45
-
46
- try {
47
- this.createReviewDirectory();
48
- await this.copyTemplateFiles();
49
- this.installGitHooks();
50
- this.showNextSteps();
51
-
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'));
55
-
56
- } catch (error) {
57
- logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_failed', { error: error.message }));
58
- process.exit(1);
59
- }
60
- }
61
-
62
- createReviewDirectory() {
63
- if (!fs.existsSync(this.reviewDir)) {
64
- fs.mkdirSync(this.reviewDir, { recursive: true });
65
- logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_review_dir'));
66
- }
67
-
68
- // 创建AI提示词子目录(用于AI自定义提示)
69
- const aiPromptsDir = path.join(this.reviewDir, 'ai-rules');
70
- if (!fs.existsSync(aiPromptsDir)) {
71
- fs.mkdirSync(aiPromptsDir, { recursive: true });
72
- logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_ai_rules_dir'));
73
- }
74
-
75
- // 创建本地静态规则目录
76
- const localRulesDir = path.join(this.reviewDir, 'local-rules');
77
- if (!fs.existsSync(localRulesDir)) {
78
- fs.mkdirSync(localRulesDir, { recursive: true });
79
- logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_local_rules_dir'));
80
- }
81
- }
82
-
83
- async copyTemplateFiles() {
84
- // 将模板源路径(templates 下)映射到目标路径(.smart-review 下)
85
- const templatesMap = [
86
- { src: 'smart-review.json', dest: 'smart-review.json', description: '主配置文件' },
87
- { src: 'rules/security.js', dest: 'local-rules/security.js', description: '安全规则' },
88
- { src: 'rules/performance.js', dest: 'local-rules/performance.js', description: '性能规则' },
89
- { src: 'rules/best-practices.js', dest: 'local-rules/best-practices.js', description: '最佳实践规则' }
90
- ];
91
-
92
- // 根据 locale 选择模板目录(优先 rules/<locale>/,否则回退到 rules/zh-CN/)
93
- const loc = await this.resolveLocale();
94
-
95
- for (const { src, dest, description } of templatesMap) {
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);
110
- const targetPath = path.join(this.reviewDir, dest);
111
-
112
- if (fs.existsSync(templatePath) && !fs.existsSync(targetPath)) {
113
- // 确保目标目录存在
114
- const targetDir = path.dirname(targetPath);
115
- if (!fs.existsSync(targetDir)) {
116
- fs.mkdirSync(targetDir, { recursive: true });
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
- }
167
-
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
- // 忽略读取失败,继续尝试模板配置
182
- }
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`;
220
- }
221
-
222
- installGitHooks() {
223
- // 1) 检测是否存在 git 命令
224
- let gitAvailable = false;
225
- try {
226
- execSync('git --version', { stdio: 'ignore' });
227
- gitAvailable = true;
228
- } catch (e) {
229
- gitAvailable = false;
230
- }
231
-
232
- if (!gitAvailable) {
233
- logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_git_missing', { url: 'https://git-scm.com/downloads' }));
234
- process.exit(1);
235
- }
236
-
237
- // 2) 若项目未初始化为 Git 仓库,执行 git init
238
- const gitDir = path.join(this.projectRoot, '.git');
239
- // review-disable-start
240
- if (!fs.existsSync(gitDir)) {
241
- logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_init_git_warn'));
242
- try {
243
- execSync('git init', { cwd: this.projectRoot, stdio: 'ignore' });
244
- } catch (e) {
245
- logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_init_git_failed'));
246
- process.exit(1);
247
- }
248
- }
249
- // review-disable-end
250
-
251
- // 3) 确保 hooks 目录存在
252
- const gitHooksDir = path.join(gitDir, 'hooks');
253
- // review-disable-start
254
- if (!fs.existsSync(gitHooksDir)) {
255
- fs.mkdirSync(gitHooksDir, { recursive: true });
256
- }
257
- // review-disable-end
258
-
259
- const preCommitHook = path.join(gitHooksDir, 'pre-commit');
260
-
261
- const loc = process.env.SMART_REVIEW_LOCALE || 'zh-CN';
262
- const hookContent = `#!/usr/bin/env bash
263
- # ${t(loc, 'hook_header_comment')}
264
-
265
- echo "${t(loc, 'hook_start_review')}"
266
-
267
- # 获取暂存区文件
268
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
269
-
270
- if [ -z "$STAGED_FILES" ]; then
271
- echo "${t(loc, 'hook_no_staged')}"
272
- exit 0
273
- fi
274
-
275
- echo "${t(loc, 'hook_found_staged_header')}"
276
- echo "$STAGED_FILES"
277
-
278
- # 运行代码审查(定位到仓库根目录)
279
- REPO_ROOT=$(git rev-parse --show-toplevel)
280
- cd "$REPO_ROOT" || { echo "${t(loc, 'hook_cd_repo_fail')}"; exit 1; }
281
-
282
- ROOT_BIN="$REPO_ROOT/node_modules/.bin/smart-review"
283
-
284
- FOUND_CMD=""
285
- FOUND_IS_ENTRY=0
286
-
287
- if [ -f "$ROOT_BIN" ]; then
288
- FOUND_CMD="$ROOT_BIN"
289
- else
290
- MAX_ASCEND=6
291
- while IFS= read -r file; do
292
- [ -z "$file" ] && continue
293
- dir=$(dirname "$file")
294
- depth=0
295
- while [ "$dir" != "." ] && [ $depth -lt $MAX_ASCEND ]; do
296
- candidate_bin="$REPO_ROOT/$dir/node_modules/.bin/smart-review"
297
- candidate_entry="$REPO_ROOT/$dir/node_modules/smart-review/bin/review.js"
298
- if [ -f "$candidate_bin" ]; then
299
- FOUND_CMD="$candidate_bin"; FOUND_IS_ENTRY=0; break 2
300
- elif [ -f "$candidate_entry" ]; then
301
- FOUND_CMD="$candidate_entry"; FOUND_IS_ENTRY=1; break 2
302
- fi
303
- dir=$(dirname "$dir")
304
- depth=$((depth + 1))
305
- done
306
- done <<< "$STAGED_FILES"
307
- fi
308
-
309
- if [ -z "$FOUND_CMD" ] && command -v smart-review >/dev/null 2>&1; then
310
- FOUND_CMD="smart-review"; FOUND_IS_ENTRY=0
311
- fi
312
-
313
- if [ -z "$FOUND_CMD" ]; then
314
- echo "${t(loc, 'hook_cmd_not_found1')}"
315
- echo "${t(loc, 'hook_cmd_not_found2')}"
316
- exit 1
317
- fi
318
-
319
- echo "${t(loc, 'hook_use_command_prefix')} $FOUND_CMD --staged"
320
- if [ $FOUND_IS_ENTRY -eq 1 ]; then
321
- node "$FOUND_CMD" --staged
322
- else
323
- "$FOUND_CMD" --staged
324
- fi
325
-
326
- EXIT_CODE=$?
327
- if [ $EXIT_CODE -ne 0 ]; then
328
- echo "${t(loc, 'hook_review_fail')}"
329
- exit 1
330
- else
331
- echo "${t(loc, 'hook_review_pass')}"
332
- exit 0
333
- fi
334
- `;
335
-
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
- }
355
-
356
- // 设置执行权限
357
- try {
358
- fs.chmodSync(preCommitHook, FILE_PERMISSIONS.EXECUTABLE);
359
- logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_precommit_installed_success'));
360
- } catch (error) {
361
- logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_precommit_perm_warn'));
362
- }
363
-
364
- // 测试钩子是否能正常执行
365
- this.testHook(preCommitHook);
366
- }
367
-
368
- testHook(hookPath) {
369
- logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_hook'));
370
-
371
- // 检查文件是否存在且可执行
372
- if (!fs.existsSync(hookPath)) {
373
- logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_missing'));
374
- return;
375
- }
376
-
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
- }
383
- const stats = fs.statSync(hookPath);
384
- const isExecutable = !!(stats.mode & 0o111);
385
- logger.debug(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_dbg', { mode: stats.mode.toString(8), exec: isExecutable }));
386
- if (!isExecutable) {
387
- logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_fix_warn'));
388
- fs.chmodSync(hookPath, FILE_PERMISSIONS.EXECUTABLE);
389
- }
390
- // POSIX 环境下权限检查完成,提示成功
391
- logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_hook_success'));
392
- } catch (error) {
393
- logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_check_failed', { error: error.message }));
394
- }
395
- }
396
-
397
- showNextSteps() {
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'));
405
- logger.info(` ${path.join(this.reviewDir, 'smart-review.json')}`);
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/') }));
408
-
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'));
412
- }
413
- }
414
-
415
- // 运行安装
416
- const installer = new Installer();
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { execSync } from 'child_process';
7
+ import { logger } from '../lib/utils/logger.js';
8
+ import { FILE_PERMISSIONS, BATCH_CONSTANTS } from '../lib/utils/constants.js';
9
+ import { t } from '../lib/utils/i18n.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ class Installer {
15
+ constructor() {
16
+ this.projectRoot = this.findGitRoot();
17
+ this.reviewDir = path.join(this.projectRoot, '.smart-review');
18
+ this.templatesDir = path.join(__dirname, '../templates');
19
+ }
20
+
21
+ findGitRoot() {
22
+ let currentDir = process.cwd();
23
+ logger.debug(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_search_git_root_dbg', { dir: currentDir }));
24
+
25
+ for (let i = 0; i < BATCH_CONSTANTS.MAX_DIRECTORY_SEARCH_DEPTH; i++) {
26
+ const gitDir = path.join(currentDir, '.git');
27
+ if (fs.existsSync(gitDir)) {
28
+ logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_found_git_root_success', { dir: currentDir }));
29
+ return currentDir;
30
+ }
31
+
32
+ const parentDir = path.dirname(currentDir);
33
+ if (parentDir === currentDir) {
34
+ break; // 到达根目录
35
+ }
36
+ currentDir = parentDir;
37
+ }
38
+
39
+ logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_no_git_use_current'));
40
+ return process.cwd();
41
+ }
42
+
43
+ async install() {
44
+ logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_start'));
45
+
46
+ try {
47
+ this.createReviewDirectory();
48
+ await this.copyTemplateFiles();
49
+ this.installGitHooks();
50
+ this.showNextSteps();
51
+
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'));
55
+
56
+ } catch (error) {
57
+ logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_failed', { error: error.message }));
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ createReviewDirectory() {
63
+ if (!fs.existsSync(this.reviewDir)) {
64
+ fs.mkdirSync(this.reviewDir, { recursive: true });
65
+ logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_review_dir'));
66
+ }
67
+
68
+ // 创建AI提示词子目录(用于AI自定义提示)
69
+ const aiPromptsDir = path.join(this.reviewDir, 'ai-rules');
70
+ if (!fs.existsSync(aiPromptsDir)) {
71
+ fs.mkdirSync(aiPromptsDir, { recursive: true });
72
+ logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_ai_rules_dir'));
73
+ }
74
+
75
+ // 创建本地静态规则目录
76
+ const localRulesDir = path.join(this.reviewDir, 'local-rules');
77
+ if (!fs.existsSync(localRulesDir)) {
78
+ fs.mkdirSync(localRulesDir, { recursive: true });
79
+ logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_create_local_rules_dir'));
80
+ }
81
+ }
82
+
83
+ async copyTemplateFiles() {
84
+ // 将模板源路径(templates 下)映射到目标路径(.smart-review 下)
85
+ const templatesMap = [
86
+ { src: 'smart-review.json', dest: 'smart-review.json', description: '主配置文件' },
87
+ { src: 'rules/security.js', dest: 'local-rules/security.js', description: '安全规则' },
88
+ { src: 'rules/performance.js', dest: 'local-rules/performance.js', description: '性能规则' },
89
+ { src: 'rules/best-practices.js', dest: 'local-rules/best-practices.js', description: '最佳实践规则' }
90
+ ];
91
+
92
+ // 根据 locale 选择模板目录(优先 rules/<locale>/,否则回退到 rules/zh-CN/)
93
+ const loc = await this.resolveLocale();
94
+
95
+ for (const { src, dest, description } of templatesMap) {
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);
110
+ const targetPath = path.join(this.reviewDir, dest);
111
+
112
+ if (fs.existsSync(templatePath) && !fs.existsSync(targetPath)) {
113
+ // 确保目标目录存在
114
+ const targetDir = path.dirname(targetPath);
115
+ if (!fs.existsSync(targetDir)) {
116
+ fs.mkdirSync(targetDir, { recursive: true });
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
+ }
167
+
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
+ // 忽略读取失败,继续尝试模板配置
182
+ }
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`;
220
+ }
221
+
222
+ installGitHooks() {
223
+ // 1) 检测是否存在 git 命令
224
+ let gitAvailable = false;
225
+ try {
226
+ execSync('git --version', { stdio: 'ignore' });
227
+ gitAvailable = true;
228
+ } catch (e) {
229
+ gitAvailable = false;
230
+ }
231
+
232
+ if (!gitAvailable) {
233
+ logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_git_missing', { url: 'https://git-scm.com/downloads' }));
234
+ process.exit(1);
235
+ }
236
+
237
+ // 2) 若项目未初始化为 Git 仓库,执行 git init
238
+ const gitDir = path.join(this.projectRoot, '.git');
239
+ // review-disable-start
240
+ if (!fs.existsSync(gitDir)) {
241
+ logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_init_git_warn'));
242
+ try {
243
+ execSync('git init', { cwd: this.projectRoot, stdio: 'ignore' });
244
+ } catch (e) {
245
+ logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_init_git_failed'));
246
+ process.exit(1);
247
+ }
248
+ }
249
+ // review-disable-end
250
+
251
+ // 3) 确保 hooks 目录存在
252
+ const gitHooksDir = path.join(gitDir, 'hooks');
253
+ // review-disable-start
254
+ if (!fs.existsSync(gitHooksDir)) {
255
+ fs.mkdirSync(gitHooksDir, { recursive: true });
256
+ }
257
+ // review-disable-end
258
+
259
+ const preCommitHook = path.join(gitHooksDir, 'pre-commit');
260
+
261
+ const loc = process.env.SMART_REVIEW_LOCALE || 'zh-CN';
262
+ const hookContent = `#!/usr/bin/env bash
263
+ # ${t(loc, 'hook_header_comment')}
264
+
265
+ echo "${t(loc, 'hook_start_review')}"
266
+
267
+ # 获取暂存区文件
268
+ STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
269
+
270
+ if [ -z "$STAGED_FILES" ]; then
271
+ echo "${t(loc, 'hook_no_staged')}"
272
+ exit 0
273
+ fi
274
+
275
+ echo "${t(loc, 'hook_found_staged_header')}"
276
+ echo "$STAGED_FILES"
277
+
278
+ # 运行代码审查(定位到仓库根目录)
279
+ REPO_ROOT=$(git rev-parse --show-toplevel)
280
+ cd "$REPO_ROOT" || { echo "${t(loc, 'hook_cd_repo_fail')}"; exit 1; }
281
+
282
+ ROOT_BIN="$REPO_ROOT/node_modules/.bin/smart-review"
283
+
284
+ FOUND_CMD=""
285
+ FOUND_IS_ENTRY=0
286
+
287
+ if [ -f "$ROOT_BIN" ]; then
288
+ FOUND_CMD="$ROOT_BIN"
289
+ else
290
+ MAX_ASCEND=6
291
+ while IFS= read -r file; do
292
+ [ -z "$file" ] && continue
293
+ dir=$(dirname "$file")
294
+ depth=0
295
+ while [ "$dir" != "." ] && [ $depth -lt $MAX_ASCEND ]; do
296
+ candidate_bin="$REPO_ROOT/$dir/node_modules/.bin/smart-review"
297
+ candidate_entry="$REPO_ROOT/$dir/node_modules/smart-review/bin/review.js"
298
+ if [ -f "$candidate_bin" ]; then
299
+ FOUND_CMD="$candidate_bin"; FOUND_IS_ENTRY=0; break 2
300
+ elif [ -f "$candidate_entry" ]; then
301
+ FOUND_CMD="$candidate_entry"; FOUND_IS_ENTRY=1; break 2
302
+ fi
303
+ dir=$(dirname "$dir")
304
+ depth=$((depth + 1))
305
+ done
306
+ done <<< "$STAGED_FILES"
307
+ fi
308
+
309
+ if [ -z "$FOUND_CMD" ] && command -v smart-review >/dev/null 2>&1; then
310
+ FOUND_CMD="smart-review"; FOUND_IS_ENTRY=0
311
+ fi
312
+
313
+ if [ -z "$FOUND_CMD" ]; then
314
+ echo "${t(loc, 'hook_cmd_not_found1')}"
315
+ echo "${t(loc, 'hook_cmd_not_found2')}"
316
+ echo "${t(loc, 'hook_cmd_missing_continue')}"
317
+ # 未安装 smart-review,跳过自动审查但不阻断提交
318
+ exit 0
319
+ fi
320
+
321
+ echo "${t(loc, 'hook_use_command_prefix')} $FOUND_CMD --staged"
322
+ if [ $FOUND_IS_ENTRY -eq 1 ]; then
323
+ node "$FOUND_CMD" --staged
324
+ else
325
+ "$FOUND_CMD" --staged
326
+ fi
327
+
328
+ EXIT_CODE=$?
329
+ if [ $EXIT_CODE -ne 0 ]; then
330
+ echo "${t(loc, 'hook_review_fail')}"
331
+ exit 1
332
+ else
333
+ echo "${t(loc, 'hook_review_pass')}"
334
+ exit 0
335
+ fi
336
+ `;
337
+
338
+ fs.writeFileSync(preCommitHook, hookContent);
339
+ // Windows 兼容:提供 CMD 包装器,调用 bash 执行同名脚本
340
+ try {
341
+ const preCommitCmd = path.join(gitHooksDir, 'pre-commit.cmd');
342
+ const cmdContent = [
343
+ '@echo off',
344
+ 'SETLOCAL',
345
+ 'set HOOK=%~dp0pre-commit',
346
+ 'if not exist "%HOOK%" (',
347
+ ' echo [smart-review] pre-commit hook missing.',
348
+ ' exit /b 1',
349
+ ')',
350
+ 'bash "%HOOK%"',
351
+ 'exit /b %ERRORLEVEL%\r\n'
352
+ ].join('\r\n');
353
+ fs.writeFileSync(preCommitCmd, cmdContent);
354
+ } catch (e) {
355
+ // 忽略 CMD 包装器写入失败
356
+ }
357
+
358
+ // 设置执行权限
359
+ try {
360
+ fs.chmodSync(preCommitHook, FILE_PERMISSIONS.EXECUTABLE);
361
+ logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_precommit_installed_success'));
362
+ } catch (error) {
363
+ logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_precommit_perm_warn'));
364
+ }
365
+
366
+ // 测试钩子是否能正常执行
367
+ this.testHook(preCommitHook);
368
+ }
369
+
370
+ testHook(hookPath) {
371
+ logger.info(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_hook'));
372
+
373
+ // 检查文件是否存在且可执行
374
+ if (!fs.existsSync(hookPath)) {
375
+ logger.error(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_missing'));
376
+ return;
377
+ }
378
+
379
+ try {
380
+ // Windows 上无需检查可执行位,直接提示成功
381
+ if (process.platform === 'win32') {
382
+ logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_hook_success'));
383
+ return;
384
+ }
385
+ const stats = fs.statSync(hookPath);
386
+ const isExecutable = !!(stats.mode & 0o111);
387
+ logger.debug(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_dbg', { mode: stats.mode.toString(8), exec: isExecutable }));
388
+ if (!isExecutable) {
389
+ logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_fix_warn'));
390
+ fs.chmodSync(hookPath, FILE_PERMISSIONS.EXECUTABLE);
391
+ }
392
+ // POSIX 环境下权限检查完成,提示成功
393
+ logger.success(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_hook_success'));
394
+ } catch (error) {
395
+ logger.warn(t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_hook_perm_check_failed', { error: error.message }));
396
+ }
397
+ }
398
+
399
+ showNextSteps() {
400
+ logger.info('\n' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_header'));
401
+ logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_item1'));
402
+ logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_item2'));
403
+ logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_item3'));
404
+ logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_optional_item4'));
405
+
406
+ logger.info('\n' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_paths_header'));
407
+ logger.info(` ${path.join(this.reviewDir, 'smart-review.json')}`);
408
+ logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_local_rules_path', { path: path.join(this.reviewDir, 'local-rules/') }));
409
+ logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_ai_rules_path', { path: path.join(this.reviewDir, 'ai-rules/') }));
410
+
411
+ logger.info('\n' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_header'));
412
+ logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_git_commit'));
413
+ logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', 'install_test_cli'));
414
+ }
415
+ }
416
+
417
+ // 运行安装
418
+ const installer = new Installer();
417
419
  (async () => { await installer.install(); })();