smart-review 1.0.1 → 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,280 +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
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = path.dirname(__filename);
12
-
13
- class Installer {
14
- constructor() {
15
- this.projectRoot = this.findGitRoot();
16
- this.reviewDir = path.join(this.projectRoot, '.smart-review');
17
- this.templatesDir = path.join(__dirname, '../templates');
18
- }
19
-
20
- findGitRoot() {
21
- let currentDir = process.cwd();
22
- logger.debug(`查找Git根目录,从 ${currentDir} 开始...`);
23
-
24
- for (let i = 0; i < BATCH_CONSTANTS.MAX_DIRECTORY_SEARCH_DEPTH; i++) {
25
- const gitDir = path.join(currentDir, '.git');
26
- if (fs.existsSync(gitDir)) {
27
- logger.success(`找到Git根目录: ${currentDir}`);
28
- return currentDir;
29
- }
30
-
31
- const parentDir = path.dirname(currentDir);
32
- if (parentDir === currentDir) {
33
- break; // 到达根目录
34
- }
35
- currentDir = parentDir;
36
- }
37
-
38
- logger.info('ℹ️ 未找到.git目录,使用当前目录作为项目根目录');
39
- return process.cwd();
40
- }
41
-
42
- install() {
43
- logger.info('🚀 开始安装智能代码审查系统...');
44
-
45
- try {
46
- this.createReviewDirectory();
47
- this.copyTemplateFiles();
48
- this.installGitHooks();
49
- this.showNextSteps();
50
-
51
- logger.success('\n🎉 智能代码审查系统安装完成!');
52
- logger.info('💡 系统已内置默认配置和规则,无需额外配置即可使用');
53
- logger.info('📝 如需自定义,请编辑 .smart-review/ 目录下的配置文件');
54
-
55
- } catch (error) {
56
- logger.error('安装失败:', error);
57
- process.exit(1);
58
- }
59
- }
60
-
61
- createReviewDirectory() {
62
- if (!fs.existsSync(this.reviewDir)) {
63
- fs.mkdirSync(this.reviewDir, { recursive: true });
64
- logger.success('创建 .smart-review 目录');
65
- }
66
-
67
- // 创建AI提示词子目录(用于AI自定义提示)
68
- const aiPromptsDir = path.join(this.reviewDir, 'ai-rules');
69
- if (!fs.existsSync(aiPromptsDir)) {
70
- fs.mkdirSync(aiPromptsDir, { recursive: true });
71
- logger.success('创建 ai-rules 子目录(AI提示词)');
72
- }
73
-
74
- // 创建本地静态规则目录
75
- const localRulesDir = path.join(this.reviewDir, 'local-rules');
76
- if (!fs.existsSync(localRulesDir)) {
77
- fs.mkdirSync(localRulesDir, { recursive: true });
78
- logger.success('创建 local-rules 子目录(静态规则)');
79
- }
80
- }
81
-
82
- copyTemplateFiles() {
83
- // 将模板源路径(templates 下)映射到目标路径(.smart-review 下)
84
- const templatesMap = [
85
- { src: 'smart-review.json', dest: 'smart-review.json', description: '主配置文件' },
86
- { src: 'rules/security.js', dest: 'local-rules/security.js', description: '安全规则' },
87
- { src: 'rules/performance.js', dest: 'local-rules/performance.js', description: '性能规则' },
88
- { src: 'rules/best-practices.js', dest: 'local-rules/best-practices.js', description: '最佳实践规则' }
89
- ];
90
-
91
- for (const { src, dest, description } of templatesMap) {
92
- const templatePath = path.join(this.templatesDir, src);
93
- const targetPath = path.join(this.reviewDir, dest);
94
-
95
- if (fs.existsSync(templatePath) && !fs.existsSync(targetPath)) {
96
- // 确保目标目录存在
97
- const targetDir = path.dirname(targetPath);
98
- if (!fs.existsSync(targetDir)) {
99
- fs.mkdirSync(targetDir, { recursive: true });
100
- }
101
-
102
- fs.copyFileSync(templatePath, targetPath);
103
- logger.success(`创建 ${description}`);
104
- }
105
- }
106
- }
107
-
108
- installGitHooks() {
109
- // 1) 检测是否存在 git 命令
110
- let gitAvailable = false;
111
- try {
112
- execSync('git --version', { stdio: 'ignore' });
113
- gitAvailable = true;
114
- } catch (e) {
115
- gitAvailable = false;
116
- }
117
-
118
- if (!gitAvailable) {
119
- logger.error('未检测到 Git,请先安装后重试: https://git-scm.com/downloads');
120
- process.exit(1);
121
- }
122
-
123
- // 2) 若项目未初始化为 Git 仓库,执行 git init
124
- const gitDir = path.join(this.projectRoot, '.git');
125
- // review-disable-start
126
- if (!fs.existsSync(gitDir)) {
127
- logger.warn('未检测到 .git 目录,正在初始化 Git 仓库...');
128
- try {
129
- execSync('git init', { cwd: this.projectRoot, stdio: 'ignore' });
130
- } catch (e) {
131
- logger.error('Git 仓库初始化失败,请手动执行 `git init` 后重试');
132
- process.exit(1);
133
- }
134
- }
135
- // review-disable-end
136
-
137
- // 3) 确保 hooks 目录存在
138
- const gitHooksDir = path.join(gitDir, 'hooks');
139
- // review-disable-start
140
- if (!fs.existsSync(gitHooksDir)) {
141
- fs.mkdirSync(gitHooksDir, { recursive: true });
142
- }
143
- // review-disable-end
144
-
145
- const preCommitHook = path.join(gitHooksDir, 'pre-commit');
146
-
147
- const hookContent = `#!/bin/bash
148
- # 智能代码审查 - pre-commit钩子(子项目兼容,基于暂存文件逐层定位)
149
-
150
- echo "🔍 启动代码审查..."
151
-
152
- # 获取暂存区文件
153
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
154
-
155
- if [ -z "$STAGED_FILES" ]; then
156
- echo "📭 没有暂存的文件需要审查"
157
- exit 0
158
- fi
159
-
160
- echo "📁 发现暂存文件:"
161
- echo "$STAGED_FILES"
162
-
163
- # 运行代码审查(定位到仓库根目录)
164
- REPO_ROOT=$(git rev-parse --show-toplevel)
165
- cd "$REPO_ROOT" || { echo "❌ 无法进入仓库根目录"; exit 1; }
166
-
167
- # 优先使用仓库根目录安装的 CLI
168
- ROOT_BIN="$REPO_ROOT/node_modules/.bin/smart-review"
169
-
170
- FOUND_CMD=""
171
- FOUND_IS_ENTRY=0
172
-
173
- if [ -f "$ROOT_BIN" ]; then
174
- FOUND_CMD="$ROOT_BIN"
175
- else
176
- # 基于暂存文件的路径,逐层向上查找其子项目的 node_modules
177
- # 限制最大向上层级,避免卡住
178
- MAX_ASCEND=6
179
- while IFS= read -r file; do
180
- [ -z "$file" ] && continue
181
- dir=$(dirname "$file")
182
- depth=0
183
- while [ "$dir" != "." ] && [ $depth -lt $MAX_ASCEND ]; do
184
- candidate_bin="$REPO_ROOT/$dir/node_modules/.bin/smart-review"
185
- candidate_entry="$REPO_ROOT/$dir/node_modules/smart-review/bin/review.js"
186
- if [ -f "$candidate_bin" ]; then
187
- FOUND_CMD="$candidate_bin"; FOUND_IS_ENTRY=0; break 2
188
- elif [ -f "$candidate_entry" ]; then
189
- FOUND_CMD="$candidate_entry"; FOUND_IS_ENTRY=1; break 2
190
- fi
191
- dir=$(dirname "$dir")
192
- depth=$((depth + 1))
193
- done
194
- done <<< "$STAGED_FILES"
195
- fi
196
-
197
- # 额外兜底:PATH 中的全局 smart-review
198
- if [ -z "$FOUND_CMD" ] && command -v smart-review >/dev/null 2>&1; then
199
- FOUND_CMD="smart-review"; FOUND_IS_ENTRY=0
200
- fi
201
-
202
- if [ -z "$FOUND_CMD" ]; then
203
- echo "❌ 未找到 smart-review。请在对应子项目安装:npm i -D smart-review"
204
- echo " 或在仓库根安装供统一使用:npm i -D smart-review"
205
- exit 1
206
- fi
207
-
208
- echo "⚙️ 使用命令: $FOUND_CMD --staged"
209
- if [ $FOUND_IS_ENTRY -eq 1 ]; then
210
- node "$FOUND_CMD" --staged
211
- else
212
- "$FOUND_CMD" --staged
213
- fi
214
-
215
- EXIT_CODE=$?
216
- if [ $EXIT_CODE -ne 0 ]; then
217
- echo "❌ 代码审查未通过,请修复问题后重新提交"
218
- exit 1
219
- else
220
- echo "✅ 代码审查通过,继续提交"
221
- exit 0
222
- fi
223
- `;
224
- fs.writeFileSync(preCommitHook, hookContent);
225
-
226
- // 设置执行权限
227
- try {
228
- fs.chmodSync(preCommitHook, FILE_PERMISSIONS.EXECUTABLE);
229
- logger.success('安装 pre-commit Git钩子');
230
- } catch (error) {
231
- logger.warn('无法设置执行权限,但钩子文件已创建');
232
- }
233
-
234
- // 测试钩子是否能正常执行
235
- this.testHook(preCommitHook);
236
- }
237
-
238
- testHook(hookPath) {
239
- logger.info('🧪 测试Git钩子...');
240
-
241
- // 检查文件是否存在且可执行
242
- if (!fs.existsSync(hookPath)) {
243
- logger.error('钩子文件不存在');
244
- return;
245
- }
246
-
247
- try {
248
- const stats = fs.statSync(hookPath);
249
- const isExecutable = !!(stats.mode & 0o111);
250
- logger.debug(`钩子文件权限: ${stats.mode.toString(8)}, 可执行: ${isExecutable}`);
251
-
252
- if (!isExecutable) {
253
- logger.warn('钩子文件不可执行,尝试重新设置权限...');
254
- fs.chmodSync(hookPath, FILE_PERMISSIONS.EXECUTABLE);
255
- }
256
- } catch (error) {
257
- logger.warn('无法检查钩子文件权限:', error.message);
258
- }
259
- }
260
-
261
- showNextSteps() {
262
- logger.info('\n📝 可选配置:');
263
- logger.info(' 1. 编辑 .smart-review/smart-review.json 配置AI参数和风险等级');
264
- logger.info(' 2. 在 .smart-review/local-rules/ 目录添加静态规则文件');
265
- logger.info(' 3. 在 .smart-review/ai-rules/ 目录添加AI提示词文件');
266
- logger.info(' 4. 设置 OPENAI_API_KEY 环境变量启用AI审查');
267
- logger.info('\n⚙️ 配置文件位置:');
268
- logger.info(` ${path.join(this.reviewDir, 'smart-review.json')}`);
269
- logger.info(` 静态规则: ${path.join(this.reviewDir, 'local-rules/')}`);
270
- logger.info(` AI提示词: ${path.join(this.reviewDir, 'ai-rules/')}`);
271
-
272
- logger.info('\n🔧 测试命令:');
273
- logger.info(' git add . && git commit -m "test" # 测试提交触发审查');
274
- logger.info(' npx smart-review --files test/src/test-file.js # 手动测试审查(使用项目内CLI)');
275
- }
276
- }
277
-
278
- // 运行安装
279
- const installer = new Installer();
280
- installer.install();
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();
419
+ (async () => { await installer.install(); })();