smart-review 1.0.1

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 ADDED
@@ -0,0 +1,280 @@
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();
package/bin/review.js ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { CodeReviewer } from '../lib/reviewer.js';
6
+ import { ConfigLoader } from '../lib/config-loader.js';
7
+ import { logger } from '../lib/utils/logger.js';
8
+ import { BATCH_CONSTANTS } from '../lib/utils/constants.js';
9
+
10
+ class ReviewCLI {
11
+ constructor() {
12
+ this.projectRoot = this.findProjectRoot();
13
+ }
14
+
15
+ findProjectRoot() {
16
+ let currentDir = process.cwd();
17
+ // 静默查找项目根目录,优先 .smart-review 其次 .git
18
+ for (let i = 0; i < BATCH_CONSTANTS.MAX_DIRECTORY_SEARCH_DEPTH; i++) {
19
+ if (fs.existsSync(path.join(currentDir, '.smart-review'))) {
20
+ return currentDir;
21
+ }
22
+ if (fs.existsSync(path.join(currentDir, '.git'))) {
23
+ return currentDir;
24
+ }
25
+ const parentDir = path.dirname(currentDir);
26
+ if (parentDir === currentDir) break;
27
+ currentDir = parentDir;
28
+ }
29
+ return process.cwd();
30
+ }
31
+
32
+ async run() {
33
+ const args = process.argv.slice(2);
34
+
35
+ try {
36
+ logger.progress('代码审查启动中,请等待...');
37
+ // 加载配置
38
+ const configLoader = new ConfigLoader(this.projectRoot);
39
+ const config = await configLoader.loadConfig();
40
+
41
+ // 调试日志开关(命令行)
42
+ if (args.includes('--debug')) {
43
+ logger.debugMode = true;
44
+ logger.info('通过命令行参数启用调试日志模式');
45
+ }
46
+
47
+ // 处理AI相关的命令行参数
48
+ if (args.includes('--no-ai')) {
49
+ config.ai = { ...config.ai, enabled: false };
50
+ logger.info('通过命令行参数禁用AI分析');
51
+ } else if (args.includes('--ai')) {
52
+ config.ai = { ...config.ai, enabled: true };
53
+ logger.info('通过命令行参数启用AI分析');
54
+ }
55
+
56
+ // 处理Git Diff审查相关参数
57
+ if (args.includes('--diff-only')) {
58
+ config.ai = { ...config.ai, reviewOnlyChanges: true };
59
+ logger.info('通过命令行参数启用Git Diff增量审查模式');
60
+ }
61
+
62
+ const rules = await configLoader.loadRules(config);
63
+ // 创建审查器
64
+ const reviewer = new CodeReviewer(config, rules);
65
+
66
+ let result;
67
+ if (args.includes('--staged')) {
68
+ result = await reviewer.reviewStagedFiles();
69
+ } else if (args.includes('--files')) {
70
+ const filesIndex = args.indexOf('--files');
71
+ const fileList = args[filesIndex + 1]?.split(',').map(f => f.trim()) || [];
72
+ result = await reviewer.reviewSpecificFiles(fileList);
73
+ } else {
74
+ logger.info('使用方法:');
75
+ logger.info(' npx smart-code-reviewer --staged # 审查暂存区文件');
76
+ logger.info(' npx smart-code-reviewer --staged --diff-only # 仅审查暂存区变动内容(git diff)');
77
+ logger.info(' npx smart-code-reviewer --files file1,file2 # 审查指定文件');
78
+ process.exit(1);
79
+ }
80
+
81
+ this.printResults(result);
82
+ process.exit(result.blockSubmission ? 1 : 0);
83
+
84
+ } catch (error) {
85
+ logger.error('审查执行失败:', error);
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ printResults(result) {
91
+ const staticIssues = result.issues.filter(i => i.source === 'static');
92
+ const aiIssues = result.issues.filter(i => i.source === 'ai');
93
+ // 本地规则审查结果
94
+ logger.info('\n本地规则审查结果');
95
+ const staticByFile = this.groupIssuesByFile(staticIssues);
96
+ if (staticIssues.length === 0) {
97
+ logger.info('无');
98
+ } else {
99
+ Object.entries(staticByFile).forEach(([file, issues]) => {
100
+ logger.info(`\n文件: ${file}`);
101
+ // 根据行号排序,保证位置从上到下
102
+ const getLineKey = (i) => {
103
+ const s = Number(i.lineStart);
104
+ const single = Number(i.line);
105
+ const e = Number(i.lineEnd);
106
+ if (Number.isFinite(s) && s > 0) return s;
107
+ if (Number.isFinite(single) && single > 0) return single;
108
+ if (Number.isFinite(e) && e > 0) return e;
109
+ return Number.POSITIVE_INFINITY;
110
+ };
111
+ const sorted = [...issues].sort((a, b) => getLineKey(a) - getLineKey(b));
112
+ sorted.forEach((issue, index) => {
113
+ logger.info(`\n问题${index + 1}:`);
114
+ const locationLabel = this.formatLocationLabel(issue);
115
+ if (locationLabel) logger.info(locationLabel);
116
+ // 美化代码片段输出:去除行号前缀并统一缩进
117
+ if (issue.snippet && issue.snippet.trim().length > 0) {
118
+ logger.info('代码片段:');
119
+ logger.info(this.formatSnippet(issue.snippet));
120
+ } else {
121
+ logger.info('代码片段:(全局性问题)');
122
+ }
123
+ logger.info(`风险等级:${this.getRiskLevelText(issue.risk)}`);
124
+ logger.info(`风险原因:${issue.message}`);
125
+ if (issue.suggestion) logger.info(`修改建议:${issue.suggestion}`);
126
+ });
127
+ });
128
+ }
129
+
130
+ // AI代码分析结果(若有)
131
+ if (result.aiRan) {
132
+ logger.info('\nAI代码分析结果');
133
+ // 说明:行号可能不连续是预处理所致(剥离注释/无需审查片段),请忽略行号跳跃
134
+ logger.info('提示:片段行号为源文件绝对行号,因清洗注释/无需审查片段可能出现跳跃,请忽略行号不连续。');
135
+ // 去重:按 file+line+message 进行去重,避免重复输出
136
+ // 打印时不再对 AI 结果做粗略去重(聚合逻辑已在 reviewer.generateResult 中完成),仅分文件展示
137
+ const aiByFile = this.groupIssuesByFile(aiIssues);
138
+ if (aiIssues.length === 0) {
139
+ logger.info('无');
140
+ } else {
141
+ Object.entries(aiByFile).forEach(([file, issues]) => {
142
+ logger.info(`\n文件: ${file}`);
143
+ // 根据行号排序:起始行号优先,其次单行号;无行号的排后
144
+ const getLineKey = (i) => {
145
+ const s = Number(i.lineStart);
146
+ const single = Number(i.line);
147
+ const e = Number(i.lineEnd);
148
+ if (Number.isFinite(s) && s > 0) return s;
149
+ if (Number.isFinite(single) && single > 0) return single;
150
+ if (Number.isFinite(e) && e > 0) return e;
151
+ return Number.POSITIVE_INFINITY;
152
+ };
153
+ const sorted = [...issues].sort((a, b) => getLineKey(a) - getLineKey(b));
154
+ sorted.forEach((issue, index) => {
155
+ logger.info(`\n问题${index + 1}:`);
156
+ const locationLabel = this.formatLocationLabel(issue);
157
+ if (locationLabel) logger.info(locationLabel);
158
+ // 美化代码片段输出:去除行号前缀并统一缩进
159
+ if (issue.snippet && issue.snippet.trim().length > 0) {
160
+ logger.info('代码片段:');
161
+ logger.info(this.formatSnippet(issue.snippet));
162
+ } else {
163
+ logger.info('代码片段:(全局性问题)');
164
+ }
165
+ logger.info(`风险等级:${this.getRiskLevelText(issue.risk)}`);
166
+ logger.info(`风险原因:${issue.message}`);
167
+ if (issue.suggestion) logger.info(`修改建议:${issue.suggestion}`);
168
+ });
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ groupIssuesByFile(issues) {
175
+ return issues.reduce((groups, issue) => {
176
+ // 确保 issue.file 存在且为字符串
177
+ if (!issue.file || typeof issue.file !== 'string') {
178
+ logger.warn('发现无效的问题对象,缺少有效的文件路径:', issue);
179
+ return groups;
180
+ }
181
+
182
+ const relativePath = path.relative(this.projectRoot, issue.file);
183
+ if (!groups[relativePath]) {
184
+ groups[relativePath] = [];
185
+ }
186
+ groups[relativePath].push(issue);
187
+ return groups;
188
+ }, {});
189
+ }
190
+
191
+ getRiskLevelText(risk) {
192
+ const levels = {
193
+ 'critical': '致命',
194
+ 'high': '高危',
195
+ 'medium': '中危',
196
+ 'low': '低危',
197
+ 'suggestion': '建议'
198
+ };
199
+ return levels[risk] || risk;
200
+ }
201
+
202
+ // 位置标签:范围为“行号范围:start-end”,单行为“行号:n”
203
+ formatLocationLabel(issue) {
204
+ const start = Number(issue.lineStart);
205
+ const end = Number(issue.lineEnd);
206
+ const single = Number(issue.line);
207
+ if (Number.isFinite(start) && Number.isFinite(end) && start > 0 && end >= start) {
208
+ if (start === end) return `行号:${start}`;
209
+ return `行号范围:${start}-${end}`;
210
+ }
211
+ if (Number.isFinite(single) && single > 0) {
212
+ return `行号:${single}`;
213
+ }
214
+ return '';
215
+ }
216
+
217
+ // 美化代码片段:
218
+ // - 去除开头的 [n] 行号标记
219
+ // - 去除片段前后的空行并合并连续空行
220
+ // - 按第一行的缩进等比例裁剪,保持代码梯度
221
+ // - 统一前置两空格缩进以便阅读
222
+ formatSnippet(snippet) {
223
+ if (!snippet || typeof snippet !== 'string') return '';
224
+ const lines = snippet.split('\n').map(line => line.replace(/^\s*[+ ]?\[(\d+)\]\s?/, ''));
225
+ // 去除片段前后的空行
226
+ while (lines.length > 0 && lines[0].trim() === '') lines.shift();
227
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop();
228
+ // 合并连续空行
229
+ const compact = [];
230
+ let prevBlank = false;
231
+ for (const line of lines) {
232
+ const isBlank = line.trim() === '';
233
+ if (isBlank) {
234
+ if (!prevBlank) compact.push('');
235
+ prevBlank = true;
236
+ } else {
237
+ compact.push(line);
238
+ prevBlank = false;
239
+ }
240
+ }
241
+ // 以第一行非空的缩进作为基准,等比例裁剪
242
+ const firstNonEmpty = compact.find(l => l.trim() !== '') || '';
243
+ const baseIndent = (firstNonEmpty.match(/^[\t ]*/)?.[0]?.length) || 0;
244
+ const normalized = compact.map(l => {
245
+ const thisIndent = (l.match(/^[\t ]*/)?.[0]?.length) || 0;
246
+ const removeLen = Math.min(baseIndent, thisIndent);
247
+ return l.slice(removeLen);
248
+ });
249
+ // 统一两空格缩进
250
+ return normalized.map(l => ` ${l}`).join('\n');
251
+ }
252
+ }
253
+
254
+ // 运行审查
255
+ const cli = new ReviewCLI();
256
+ cli.run().catch(error => logger.error('CLI运行失败:', error));
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { CodeReviewer } from './lib/reviewer.js';
2
+ export { AIClient } from './lib/ai-client.js';
3
+ export { ConfigLoader } from './lib/config-loader.js';
4
+ export { defaultConfig, defaultRules } from './lib/default-config.js';
5
+ // https://ci.das-security.cn/repository/ah_npm