@ttjl/ai-code-review 1.0.0
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/.env.example +3 -0
- package/Readme.md +262 -0
- package/bin/ai-review +2 -0
- package/config/default.config.js +67 -0
- package/lefthook.yml +13 -0
- package/package.json +57 -0
- package/src/cli/index.js +193 -0
- package/src/core/ai-client.js +257 -0
- package/src/core/config-loader.js +102 -0
- package/src/core/file-collector.js +115 -0
- package/src/core/reviewer.js +163 -0
- package/src/formatters/console-formatter.js +146 -0
- package/src/formatters/json-formatter.js +189 -0
- package/src/utils/error-handler.js +183 -0
- package/src/utils/git.js +164 -0
- package/templates/.ai-reviewrc.json +66 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
class ConsoleFormatter {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
if (this.config.output.console.color) {
|
|
7
|
+
this.colors = {
|
|
8
|
+
error: chalk.red,
|
|
9
|
+
warning: chalk.yellow,
|
|
10
|
+
info: chalk.blue,
|
|
11
|
+
success: chalk.green,
|
|
12
|
+
dim: chalk.gray,
|
|
13
|
+
cyan: chalk.cyan
|
|
14
|
+
};
|
|
15
|
+
} else {
|
|
16
|
+
this.colors = {
|
|
17
|
+
error: (text) => text,
|
|
18
|
+
warning: (text) => text,
|
|
19
|
+
info: (text) => text,
|
|
20
|
+
success: (text) => text,
|
|
21
|
+
dim: (text) => text,
|
|
22
|
+
cyan: (text) => text
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 格式化单个文件的审查结果
|
|
29
|
+
*/
|
|
30
|
+
formatFile(result) {
|
|
31
|
+
if (result.error) {
|
|
32
|
+
console.log(` ❌ ${this.colors.error(result.error)}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!result.issues || result.issues.length === 0) {
|
|
37
|
+
console.log(` ✅ ${this.colors.success('通过')}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
result.issues.forEach(issue => {
|
|
42
|
+
this.formatIssue(issue);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 格式化单个问题
|
|
48
|
+
*/
|
|
49
|
+
formatIssue(issue) {
|
|
50
|
+
const color = this.colors[issue.severity] || this.colors.dim;
|
|
51
|
+
const symbol = this.getSymbol(issue.severity);
|
|
52
|
+
|
|
53
|
+
console.log(
|
|
54
|
+
` ${symbol} ${color(`[${issue.severity.toUpperCase()}]`)} ` +
|
|
55
|
+
`${this.colors.dim(`Line ${issue.line}:`)} ${issue.message}`
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (issue.suggestion) {
|
|
59
|
+
console.log(
|
|
60
|
+
` ${this.colors.dim('💡 建议:')} ${this.colors.cyan(
|
|
61
|
+
issue.suggestion
|
|
62
|
+
)}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (issue.code) {
|
|
67
|
+
console.log(` ${this.colors.dim('代码:')}`);
|
|
68
|
+
const codeLines = issue.code.split('\n');
|
|
69
|
+
codeLines.forEach(line => {
|
|
70
|
+
console.log(` ${this.colors.dim(line)}`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 格式化总结
|
|
77
|
+
*/
|
|
78
|
+
formatSummary(results, issues) {
|
|
79
|
+
console.log('\n' + chalk.bold('='.repeat(60)));
|
|
80
|
+
console.log(chalk.bold(' 代码审查总结'));
|
|
81
|
+
console.log(chalk.bold('='.repeat(60)));
|
|
82
|
+
|
|
83
|
+
console.log(`\n 📁 审查文件: ${chalk.cyan(results.length)}`);
|
|
84
|
+
|
|
85
|
+
const errors = issues.filter(i => i.severity === 'error').length;
|
|
86
|
+
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
87
|
+
const infos = issues.filter(i => i.severity === 'info').length;
|
|
88
|
+
|
|
89
|
+
console.log(` ❌ 错误: ${chalk.red(errors)}`);
|
|
90
|
+
console.log(` ⚠️ 警告: ${chalk.yellow(warnings)}`);
|
|
91
|
+
console.log(` ℹ️ 提示: ${chalk.blue(infos)}`);
|
|
92
|
+
|
|
93
|
+
if (errors > 0 && this.config.review.onFail === 'block') {
|
|
94
|
+
console.log(
|
|
95
|
+
`\n ${chalk.bgRed.white.bold(' 提交被阻止 ')} ` +
|
|
96
|
+
`${chalk.red('请修复错误后再提交')}`
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(`\n ✅ ${chalk.green.bold('审查完成')}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(chalk.bold('='.repeat(60)) + '\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 获取严重级别对应的符号
|
|
107
|
+
*/
|
|
108
|
+
getSymbol(severity) {
|
|
109
|
+
const symbols = {
|
|
110
|
+
error: '❌',
|
|
111
|
+
warning: '⚠️ ',
|
|
112
|
+
info: 'ℹ️ '
|
|
113
|
+
};
|
|
114
|
+
return symbols[severity] || '•';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 格式化进度信息
|
|
119
|
+
*/
|
|
120
|
+
formatProgress(message) {
|
|
121
|
+
console.log(chalk.cyan(`\n${message}`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 格式化错误信息
|
|
126
|
+
*/
|
|
127
|
+
formatError(message) {
|
|
128
|
+
console.error(chalk.red(`\n❌ ${message}`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 格式化成功信息
|
|
133
|
+
*/
|
|
134
|
+
formatSuccess(message) {
|
|
135
|
+
console.log(chalk.green(`\n✅ ${message}`));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 格式化警告信息
|
|
140
|
+
*/
|
|
141
|
+
formatWarning(message) {
|
|
142
|
+
console.warn(chalk.yellow(`\n⚠️ ${message}`));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = ConsoleFormatter;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
class JSONFormatter {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 生成 JSON 报告
|
|
12
|
+
*/
|
|
13
|
+
async generate(results, issues) {
|
|
14
|
+
const report = {
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
summary: {
|
|
17
|
+
totalFiles: results.length,
|
|
18
|
+
totalIssues: issues.length,
|
|
19
|
+
bySeverity: this.groupBySeverity(issues),
|
|
20
|
+
byCategory: this.groupByCategory(issues)
|
|
21
|
+
},
|
|
22
|
+
files: results.map(r => ({
|
|
23
|
+
path: r.file,
|
|
24
|
+
summary: r.summary,
|
|
25
|
+
issues: r.issues || [],
|
|
26
|
+
metrics: r.metrics || {},
|
|
27
|
+
error: r.error || null,
|
|
28
|
+
duration: r.duration || 0
|
|
29
|
+
})),
|
|
30
|
+
issues: issues
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const filename = this.getFilename();
|
|
34
|
+
const filepath = path.join(this.config.output.file.path, filename);
|
|
35
|
+
|
|
36
|
+
await this.ensureDirectory();
|
|
37
|
+
await fs.writeFile(filepath, JSON.stringify(report, null, 2));
|
|
38
|
+
|
|
39
|
+
console.log(`\n📄 报告已生成: ${chalk.cyan(filepath)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 按严重级别分组
|
|
44
|
+
*/
|
|
45
|
+
groupBySeverity(issues) {
|
|
46
|
+
return {
|
|
47
|
+
error: issues.filter(i => i.severity === 'error').length,
|
|
48
|
+
warning: issues.filter(i => i.severity === 'warning').length,
|
|
49
|
+
info: issues.filter(i => i.severity === 'info').length
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 按类别分组
|
|
55
|
+
*/
|
|
56
|
+
groupByCategory(issues) {
|
|
57
|
+
const groups = {};
|
|
58
|
+
issues.forEach(issue => {
|
|
59
|
+
groups[issue.category] = (groups[issue.category] || 0) + 1;
|
|
60
|
+
});
|
|
61
|
+
return groups;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 生成文件名
|
|
66
|
+
*/
|
|
67
|
+
getFilename() {
|
|
68
|
+
const now = new Date();
|
|
69
|
+
let filename = this.config.output.file.filename;
|
|
70
|
+
|
|
71
|
+
// 替换时间戳占位符
|
|
72
|
+
filename = filename.replace('{timestamp}', Date.now());
|
|
73
|
+
filename = filename.replace('{date}', now.toISOString().split('T')[0]);
|
|
74
|
+
filename = filename.replace('{time}', now.toTimeString().split(' ')[0]);
|
|
75
|
+
filename = filename.replace(
|
|
76
|
+
'{datetime}',
|
|
77
|
+
now.toISOString().replace(/[:.]/g, '-')
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 确保文件名以 .json 结尾
|
|
81
|
+
if (!filename.endsWith('.json')) {
|
|
82
|
+
filename += '.json';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return filename;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 确保输出目录存在
|
|
90
|
+
*/
|
|
91
|
+
async ensureDirectory() {
|
|
92
|
+
const dir = this.config.output.file.path;
|
|
93
|
+
try {
|
|
94
|
+
await fs.access(dir);
|
|
95
|
+
} catch {
|
|
96
|
+
await fs.mkdir(dir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 生成 Markdown 报告 (可选功能)
|
|
102
|
+
*/
|
|
103
|
+
async generateMarkdown(results, issues) {
|
|
104
|
+
const report = this.buildMarkdownReport(results, issues);
|
|
105
|
+
const filename = this.getFilename().replace('.json', '.md');
|
|
106
|
+
const filepath = path.join(this.config.output.file.path, filename);
|
|
107
|
+
|
|
108
|
+
await this.ensureDirectory();
|
|
109
|
+
await fs.writeFile(filepath, report);
|
|
110
|
+
|
|
111
|
+
console.log(`\n📄 Markdown 报告已生成: ${chalk.cyan(filepath)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 构建 Markdown 报告内容
|
|
116
|
+
*/
|
|
117
|
+
buildMarkdownReport(results, issues) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
|
|
120
|
+
lines.push('# 代码审查报告\n');
|
|
121
|
+
lines.push(`**生成时间**: ${new Date().toLocaleString('zh-CN')}\n`);
|
|
122
|
+
lines.push('---\n');
|
|
123
|
+
|
|
124
|
+
// 总结部分
|
|
125
|
+
lines.push('## 📊 总结\n');
|
|
126
|
+
lines.push(`- **审查文件数**: ${results.length}`);
|
|
127
|
+
lines.push(`- **发现问题数**: ${issues.length}`);
|
|
128
|
+
lines.push(` - ❌ 错误: ${issues.filter(i => i.severity === 'error').length}`);
|
|
129
|
+
lines.push(` - ⚠️ 警告: ${issues.filter(i => i.severity === 'warning').length}`);
|
|
130
|
+
lines.push(` - ℹ️ 提示: ${issues.filter(i => i.severity === 'info').length}`);
|
|
131
|
+
lines.push('\n---\n');
|
|
132
|
+
|
|
133
|
+
// 问题分类
|
|
134
|
+
lines.push('## 🔍 问题分类\n');
|
|
135
|
+
const byCategory = this.groupByCategory(issues);
|
|
136
|
+
Object.entries(byCategory).forEach(([category, count]) => {
|
|
137
|
+
const labels = {
|
|
138
|
+
quality: '代码质量',
|
|
139
|
+
security: '安全问题',
|
|
140
|
+
performance: '性能问题',
|
|
141
|
+
naming: '命名规范',
|
|
142
|
+
'best-practices': '最佳实践'
|
|
143
|
+
};
|
|
144
|
+
lines.push(`- **${labels[category] || category}**: ${count}`);
|
|
145
|
+
});
|
|
146
|
+
lines.push('\n---\n');
|
|
147
|
+
|
|
148
|
+
// 文件详情
|
|
149
|
+
lines.push('## 📁 文件详情\n');
|
|
150
|
+
|
|
151
|
+
results.forEach(result => {
|
|
152
|
+
lines.push(`### ${result.file}\n`);
|
|
153
|
+
|
|
154
|
+
if (result.error) {
|
|
155
|
+
lines.push(`**错误**: ${result.error}\n`);
|
|
156
|
+
} else if (result.issues && result.issues.length > 0) {
|
|
157
|
+
result.issues.forEach(issue => {
|
|
158
|
+
const icon =
|
|
159
|
+
issue.severity === 'error'
|
|
160
|
+
? '❌'
|
|
161
|
+
: issue.severity === 'warning'
|
|
162
|
+
? '⚠️'
|
|
163
|
+
: 'ℹ️';
|
|
164
|
+
lines.push(
|
|
165
|
+
`${icon} **Line ${issue.line}** [${issue.severity.toUpperCase()}]: ${issue.message}`
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (issue.suggestion) {
|
|
169
|
+
lines.push(` - 💡 建议: ${issue.suggestion}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (issue.code) {
|
|
173
|
+
lines.push(` - 代码:\n \`\`\`\n ${issue.code}\n \`\`\``);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
lines.push('');
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
lines.push('✅ 通过\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
lines.push('---\n');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = JSONFormatter;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
class ErrorHandler {
|
|
4
|
+
/**
|
|
5
|
+
* 处理错误
|
|
6
|
+
*/
|
|
7
|
+
static handle(error, context = {}) {
|
|
8
|
+
const errorType = this.classifyError(error);
|
|
9
|
+
const errorInfo = {
|
|
10
|
+
type: errorType,
|
|
11
|
+
message: error.message,
|
|
12
|
+
context,
|
|
13
|
+
timestamp: new Date().toISOString()
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
switch (errorType) {
|
|
17
|
+
case 'API_ERROR':
|
|
18
|
+
return this.handleAPIError(errorInfo);
|
|
19
|
+
case 'NETWORK_ERROR':
|
|
20
|
+
return this.handleNetworkError(errorInfo);
|
|
21
|
+
case 'CONFIG_ERROR':
|
|
22
|
+
return this.handleConfigError(errorInfo);
|
|
23
|
+
case 'GIT_ERROR':
|
|
24
|
+
return this.handleGitError(errorInfo);
|
|
25
|
+
case 'FILE_ERROR':
|
|
26
|
+
return this.handleFileError(errorInfo);
|
|
27
|
+
default:
|
|
28
|
+
return this.handleUnknownError(errorInfo);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 分类错误
|
|
34
|
+
*/
|
|
35
|
+
static classifyError(error) {
|
|
36
|
+
if (
|
|
37
|
+
error.code === 'ENOTFOUND' ||
|
|
38
|
+
error.code === 'ECONNREFUSED' ||
|
|
39
|
+
error.code === 'ETIMEDOUT'
|
|
40
|
+
) {
|
|
41
|
+
return 'NETWORK_ERROR';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
error.message?.includes('API') ||
|
|
46
|
+
error.message?.includes('认证') ||
|
|
47
|
+
error.status === 401 ||
|
|
48
|
+
error.status === 403
|
|
49
|
+
) {
|
|
50
|
+
return 'API_ERROR';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
error.message?.includes('Git') ||
|
|
55
|
+
error.message?.includes('仓库') ||
|
|
56
|
+
error.message?.includes('暂存')
|
|
57
|
+
) {
|
|
58
|
+
return 'GIT_ERROR';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (error.code === 'ENOENT' || error.code === 'EACCES') {
|
|
62
|
+
return 'FILE_ERROR';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
error.message?.includes('配置') ||
|
|
67
|
+
error.message?.includes('Config')
|
|
68
|
+
) {
|
|
69
|
+
return 'CONFIG_ERROR';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return 'UNKNOWN_ERROR';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 处理 API 错误
|
|
77
|
+
*/
|
|
78
|
+
static handleAPIError(error) {
|
|
79
|
+
console.error(chalk.red('\n❌ API 调用失败'));
|
|
80
|
+
console.error(chalk.red(` 错误: ${error.message}`));
|
|
81
|
+
|
|
82
|
+
if (error.message.includes('认证') || error.context?.status === 401) {
|
|
83
|
+
console.error(chalk.yellow('\n💡 建议:'));
|
|
84
|
+
console.error(chalk.yellow(' 1. 检查 GEMINI_API_KEY 是否正确'));
|
|
85
|
+
console.error(chalk.yellow(' 2. 确认 API Key 已启用 Gemini API'));
|
|
86
|
+
console.error(chalk.yellow(' 3. 检查 API Key 是否有足够的配额'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { shouldExit: true, exitCode: 1 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 处理网络错误
|
|
94
|
+
*/
|
|
95
|
+
static handleNetworkError(error) {
|
|
96
|
+
console.error(chalk.red('\n❌ 网络连接失败'));
|
|
97
|
+
console.error(chalk.red(` 错误: ${error.message}`));
|
|
98
|
+
|
|
99
|
+
console.error(chalk.yellow('\n💡 建议:'));
|
|
100
|
+
console.error(chalk.yellow(' 1. 检查网络连接'));
|
|
101
|
+
console.error(chalk.yellow(' 2. 检查是否需要配置代理'));
|
|
102
|
+
console.error(chalk.yellow(' 3. 稍后重试'));
|
|
103
|
+
|
|
104
|
+
return { shouldExit: true, exitCode: 2 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 处理配置错误
|
|
109
|
+
*/
|
|
110
|
+
static handleConfigError(error) {
|
|
111
|
+
console.error(chalk.red('\n❌ 配置文件错误'));
|
|
112
|
+
console.error(chalk.red(` 错误: ${error.message}`));
|
|
113
|
+
|
|
114
|
+
console.error(chalk.yellow('\n💡 建议:'));
|
|
115
|
+
console.error(chalk.yellow(' 1. 检查配置文件格式是否正确'));
|
|
116
|
+
console.error(chalk.yellow(' 2. 运行 `ai-review init` 重新生成配置文件'));
|
|
117
|
+
console.error(chalk.yellow(' 3. 参考文档检查配置项'));
|
|
118
|
+
|
|
119
|
+
return { shouldExit: true, exitCode: 3 };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 处理 Git 错误
|
|
124
|
+
*/
|
|
125
|
+
static handleGitError(error) {
|
|
126
|
+
console.error(chalk.red('\n❌ Git 操作失败'));
|
|
127
|
+
console.error(chalk.red(` 错误: ${error.message}`));
|
|
128
|
+
|
|
129
|
+
console.error(chalk.yellow('\n💡 建议:'));
|
|
130
|
+
console.error(chalk.yellow(' 1. 确保在 Git 仓库中运行'));
|
|
131
|
+
console.error(chalk.yellow(' 2. 检查是否有文件已暂存'));
|
|
132
|
+
console.error(chalk.yellow(' 3. 运行 `git status` 查看状态'));
|
|
133
|
+
|
|
134
|
+
return { shouldExit: true, exitCode: 4 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 处理文件错误
|
|
139
|
+
*/
|
|
140
|
+
static handleFileError(error) {
|
|
141
|
+
console.error(chalk.red('\n❌ 文件操作失败'));
|
|
142
|
+
console.error(chalk.red(` 错误: ${error.message}`));
|
|
143
|
+
|
|
144
|
+
console.error(chalk.yellow('\n💡 建议:'));
|
|
145
|
+
console.error(chalk.yellow(' 1. 检查文件路径是否正确'));
|
|
146
|
+
console.error(chalk.yellow(' 2. 检查文件权限'));
|
|
147
|
+
console.error(chalk.yellow(' 3. 确保文件存在'));
|
|
148
|
+
|
|
149
|
+
return { shouldExit: false, exitCode: 0 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 处理未知错误
|
|
154
|
+
*/
|
|
155
|
+
static handleUnknownError(error) {
|
|
156
|
+
console.error(chalk.red('\n❌ 未知错误'));
|
|
157
|
+
console.error(chalk.red(` 错误: ${error.message}`));
|
|
158
|
+
|
|
159
|
+
if (process.env.DEBUG) {
|
|
160
|
+
console.error(chalk.gray('\n📋 详细信息:'));
|
|
161
|
+
console.error(chalk.gray(JSON.stringify(error, null, 2)));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.error(chalk.yellow('\n💡 建议:'));
|
|
165
|
+
console.error(chalk.yellow(' 1. 查看错误日志'));
|
|
166
|
+
console.error(chalk.yellow(' 2. 设置 DEBUG=1 环境变量获取详细信息'));
|
|
167
|
+
console.error(chalk.yellow(' 3. 提交 Issue 到 GitHub'));
|
|
168
|
+
|
|
169
|
+
return { shouldExit: true, exitCode: 1 };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 显示错误堆栈 (调试模式)
|
|
174
|
+
*/
|
|
175
|
+
static showStack(error) {
|
|
176
|
+
if (process.env.DEBUG) {
|
|
177
|
+
console.error(chalk.gray('\n📋 堆栈信息:'));
|
|
178
|
+
console.error(chalk.gray(error.stack));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = ErrorHandler;
|
package/src/utils/git.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
class GitUtils {
|
|
4
|
+
/**
|
|
5
|
+
* 检查是否在 Git 仓库中
|
|
6
|
+
*/
|
|
7
|
+
static isGitRepo() {
|
|
8
|
+
try {
|
|
9
|
+
execSync('git rev-parse --is-inside-work-tree', {
|
|
10
|
+
encoding: 'utf-8',
|
|
11
|
+
stdio: 'pipe'
|
|
12
|
+
});
|
|
13
|
+
return true;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 获取暂存区的文件列表
|
|
21
|
+
*/
|
|
22
|
+
static getStagedFiles() {
|
|
23
|
+
try {
|
|
24
|
+
const output = execSync(
|
|
25
|
+
'git diff --cached --name-only --diff-filter=ACM',
|
|
26
|
+
{
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
stdio: 'pipe'
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
return output.trim().split('\n').filter(Boolean);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`获取暂存文件失败: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 获取变更的文件列表
|
|
39
|
+
*/
|
|
40
|
+
static getChangedFiles(base = 'HEAD') {
|
|
41
|
+
try {
|
|
42
|
+
const output = execSync(`git diff --name-only ${base}`, {
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
stdio: 'pipe'
|
|
45
|
+
});
|
|
46
|
+
return output.trim().split('\n').filter(Boolean);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new Error(`获取变更文件失败: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 获取文件差异
|
|
54
|
+
*/
|
|
55
|
+
static getFileDiff(filePath, base = 'HEAD') {
|
|
56
|
+
try {
|
|
57
|
+
const output = execSync(`git diff ${base} -- ${filePath}`, {
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
stdio: 'pipe'
|
|
60
|
+
});
|
|
61
|
+
return output;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// 文件可能是新文件,没有差异
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 获取暂存区文件差异
|
|
70
|
+
*/
|
|
71
|
+
static getStagedFileDiff(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
const output = execSync(`git diff --cached -- ${filePath}`, {
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
stdio: 'pipe'
|
|
76
|
+
});
|
|
77
|
+
return output;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// 文件可能是新文件,没有差异
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取文件内容
|
|
86
|
+
*/
|
|
87
|
+
static getFileContent(filePath, base = 'HEAD') {
|
|
88
|
+
try {
|
|
89
|
+
const output = execSync(`git show ${base}:${filePath}`, {
|
|
90
|
+
encoding: 'utf-8',
|
|
91
|
+
stdio: 'pipe'
|
|
92
|
+
});
|
|
93
|
+
return output;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new Error(`获取文件内容失败: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 检查文件是否存在
|
|
101
|
+
*/
|
|
102
|
+
static fileExists(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
execSync(`git cat-file -e HEAD:${filePath}`, {
|
|
105
|
+
encoding: 'utf-8',
|
|
106
|
+
stdio: 'pipe'
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 获取当前分支名
|
|
116
|
+
*/
|
|
117
|
+
static getCurrentBranch() {
|
|
118
|
+
try {
|
|
119
|
+
const output = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
120
|
+
encoding: 'utf-8',
|
|
121
|
+
stdio: 'pipe'
|
|
122
|
+
});
|
|
123
|
+
return output.trim();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return 'unknown';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 获取最新的提交信息
|
|
131
|
+
*/
|
|
132
|
+
static getLastCommit() {
|
|
133
|
+
try {
|
|
134
|
+
const output = execSync('git log -1 --pretty=format:"%h %s"', {
|
|
135
|
+
encoding: 'utf-8',
|
|
136
|
+
stdio: 'pipe'
|
|
137
|
+
});
|
|
138
|
+
return output.trim();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return 'unknown';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 获取文件统计信息
|
|
146
|
+
*/
|
|
147
|
+
static getFileStats(filePath) {
|
|
148
|
+
try {
|
|
149
|
+
const output = execSync(`git diff --numstat ${base} -- ${filePath}`, {
|
|
150
|
+
encoding: 'utf-8',
|
|
151
|
+
stdio: 'pipe'
|
|
152
|
+
});
|
|
153
|
+
const [added, deleted] = output.trim().split('\t');
|
|
154
|
+
return {
|
|
155
|
+
added: parseInt(added) || 0,
|
|
156
|
+
deleted: parseInt(deleted) || 0
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { added: 0, deleted: 0 };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = GitUtils;
|