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 +418 -416
- package/lib/default-config.js +16 -15
- package/lib/reviewer.js +22 -0
- package/lib/utils/i18n.js +2 -8
- package/package.json +1 -1
- package/templates/rules/en-US/best-practices.js +24 -12
- package/templates/rules/en-US/performance.js +24 -11
- package/templates/rules/en-US/security.js +67 -33
- package/templates/rules/zh-CN/best-practices.js +24 -12
- package/templates/rules/zh-CN/performance.js +24 -11
- package/templates/rules/zh-CN/security.js +67 -33
- package/templates/smart-review.json +3 -1
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
"$FOUND_CMD" --staged
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
'
|
|
344
|
-
'
|
|
345
|
-
'
|
|
346
|
-
'
|
|
347
|
-
'
|
|
348
|
-
'
|
|
349
|
-
'
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
logger.
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
logger.
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
logger.info('
|
|
401
|
-
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', '
|
|
402
|
-
logger.info(' ' + t(process.env.SMART_REVIEW_LOCALE || 'zh-CN', '
|
|
403
|
-
|
|
404
|
-
logger.info('
|
|
405
|
-
|
|
406
|
-
logger.info('
|
|
407
|
-
logger.info(
|
|
408
|
-
|
|
409
|
-
logger.info('
|
|
410
|
-
|
|
411
|
-
logger.info('
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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(); })();
|