goodiffer 1.0.0 → 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/goodiffer.js CHANGED
@@ -1,5 +1,95 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { program } from '../src/index.js';
3
+ import { program } from 'commander';
4
+ import { initCommand } from '../src/commands/init.js';
5
+ import { analyzeCommand } from '../src/commands/analyze.js';
6
+ import { configCommand } from '../src/commands/config.js';
7
+ import { historyCommand } from '../src/commands/history.js';
8
+ import { statsCommand } from '../src/commands/stats.js';
9
+ import { developerCommand } from '../src/commands/developer.js';
10
+ import { reportCommand } from '../src/commands/report.js';
4
11
 
5
- program.parse(process.argv);
12
+ program
13
+ .name('goodiffer')
14
+ .description('AI-powered git diff analyzer for code review')
15
+ .version('1.0.1');
16
+
17
+ // 默认命令 - 分析
18
+ program
19
+ .option('-s, --staged', '分析暂存区的更改')
20
+ .option('-c, --commit <sha>', '分析指定的 commit')
21
+ .option('--from <sha>', '起始 commit (与 --to 配合使用)')
22
+ .option('--to <sha>', '结束 commit (与 --from 配合使用)')
23
+ .option('-n <number>', '分析最近 n 条 commit (n <= 10), 或与 -m 配合表示起始位置')
24
+ .option('-m <number>', '与 -n 配合使用,表示结束位置 (m-n <= 10)')
25
+ .option('--no-save', '不保存到数据库')
26
+ .action(async (options) => {
27
+ await analyzeCommand(options);
28
+ });
29
+
30
+ // init 命令
31
+ program
32
+ .command('init')
33
+ .description('初始化配置')
34
+ .action(async () => {
35
+ await initCommand();
36
+ });
37
+
38
+ // config 命令
39
+ program
40
+ .command('config <action> [key] [value]')
41
+ .description('配置管理 (list, get, set, clear)')
42
+ .action((action, key, value) => {
43
+ configCommand(action, key, value);
44
+ });
45
+
46
+ // history 命令
47
+ program
48
+ .command('history')
49
+ .description('查看 Code Review 历史记录')
50
+ .option('-p, --project [name]', '按项目筛选 (默认当前项目)')
51
+ .option('-d, --developer <name>', '按开发者筛选')
52
+ .option('--since <date>', '开始日期 (YYYY-MM-DD)')
53
+ .option('--until <date>', '结束日期 (YYYY-MM-DD)')
54
+ .option('-n, --limit <number>', '显示数量', '20')
55
+ .option('--json', '输出 JSON 格式')
56
+ .action(async (options) => {
57
+ await historyCommand(options);
58
+ });
59
+
60
+ // stats 命令
61
+ program
62
+ .command('stats')
63
+ .description('显示统计信息')
64
+ .option('-p, --project', '项目统计 (默认)')
65
+ .option('-d, --developer', '开发者统计')
66
+ .option('--since <date>', '开始日期 (YYYY-MM-DD)')
67
+ .option('--until <date>', '结束日期 (YYYY-MM-DD)')
68
+ .action(async (options) => {
69
+ await statsCommand(options);
70
+ });
71
+
72
+ // developer 命令
73
+ program
74
+ .command('developer <action> [args...]')
75
+ .description('开发者管理 (list, alias, rename, team)')
76
+ .action(async (action, args) => {
77
+ await developerCommand(action, args);
78
+ });
79
+
80
+ // report 命令
81
+ program
82
+ .command('report')
83
+ .description('生成 H5 分析报告')
84
+ .option('-p, --project [name]', '项目名称 (默认当前项目)')
85
+ .option('-d, --developer <email>', '生成个人报告')
86
+ .option('--all', '包含所有项目')
87
+ .option('--since <date>', '开始日期 (YYYY-MM-DD)')
88
+ .option('--until <date>', '结束日期 (YYYY-MM-DD)')
89
+ .option('-o, --output <path>', '输出文件路径')
90
+ .option('--open', '生成后自动打开')
91
+ .action(async (options) => {
92
+ await reportCommand(options);
93
+ });
94
+
95
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goodiffer",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI-powered git diff analyzer for code review - 智能代码审查工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,13 @@
41
41
  "node": ">=18.0.0"
42
42
  },
43
43
  "dependencies": {
44
+ "better-sqlite3": "^12.5.0",
44
45
  "chalk": "^5.3.0",
45
46
  "commander": "^12.1.0",
46
47
  "conf": "^12.0.0",
48
+ "dayjs": "^1.11.19",
47
49
  "inquirer": "^9.2.15",
50
+ "open": "^11.0.0",
48
51
  "openai": "^4.70.0",
49
52
  "ora": "^8.0.1",
50
53
  "simple-git": "^3.22.0"
@@ -1,137 +1,423 @@
1
1
  import ora from 'ora';
2
- import chalk from 'chalk';
3
2
  import { getConfig, isConfigured } from '../utils/config-store.js';
4
- import logger from '../utils/logger.js';
5
- import { isGitRepo, getLastCommitInfo, getCommitInfo, getDiff, getChangedFiles } from '../services/git.js';
3
+ import { GitService } from '../services/git.js';
6
4
  import { AIClient } from '../services/ai-client.js';
7
5
  import { buildReviewPrompt } from '../prompts/review-prompt.js';
8
- import { parseAIResponse, printReport } from '../services/reporter.js';
6
+ import { generateReport } from '../services/reporter.js';
7
+ import { getDatabase } from '../services/database.js';
8
+ import logger from '../utils/logger.js';
9
9
 
10
- export async function analyzeCommand(options = {}) {
11
- // 检查是否在 git 仓库中
12
- if (!await isGitRepo()) {
13
- logger.error('当前目录不是 git 仓库');
14
- process.exit(1);
10
+ // 解析 AI 响应
11
+ function parseAIResponse(response) {
12
+ try {
13
+ let jsonStr = response;
14
+ const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
15
+ if (jsonMatch) {
16
+ jsonStr = jsonMatch[1].trim();
17
+ }
18
+ return JSON.parse(jsonStr);
19
+ } catch {
20
+ return null;
15
21
  }
22
+ }
16
23
 
24
+ export async function analyzeCommand(options) {
17
25
  // 检查配置
18
26
  if (!isConfigured()) {
19
- logger.error('尚未配置 API。请先运行: goodiffer init');
27
+ logger.error('请先运行 goodiffer init 进行配置');
20
28
  process.exit(1);
21
29
  }
22
30
 
23
31
  const config = getConfig();
32
+ const git = new GitService();
24
33
 
25
- // 获取 commit 信息
26
- let commitInfo;
27
- if (options.commit) {
28
- commitInfo = await getCommitInfo(options.commit);
29
- } else if (options.staged) {
30
- commitInfo = {
31
- sha: 'staged',
32
- message: '(暂存区变更)',
33
- author: '',
34
- date: new Date().toISOString()
35
- };
36
- } else {
37
- commitInfo = await getLastCommitInfo();
34
+ // 检查是否在 git 仓库中
35
+ const isRepo = await git.isGitRepo();
36
+ if (!isRepo) {
37
+ logger.error('当前目录不是 git 仓库');
38
+ process.exit(1);
38
39
  }
39
40
 
40
- if (!commitInfo && !options.staged) {
41
- logger.error('无法获取 commit 信息');
42
- process.exit(1);
41
+ // 处理 -n -m 参数的多 commit 模式
42
+ if (options.n || options.m) {
43
+ await analyzeMultipleCommits(options, config, git);
44
+ return;
43
45
  }
44
46
 
45
- // 获取 diff
46
- const spinner = ora('获取代码变更...').start();
47
+ let spinner = ora('获取 Git 信息...').start();
47
48
 
48
- const diff = await getDiff({
49
- staged: options.staged,
50
- commit: options.commit,
51
- from: options.from,
52
- to: options.to
53
- });
49
+ try {
50
+ // 获取 commit 信息和 diff
51
+ let commitInfo;
52
+ let diff;
53
+ let reviewType = 'commit';
54
+ let author;
55
+ let diffStats;
54
56
 
55
- if (!diff || diff.trim().length === 0) {
56
- spinner.fail('没有找到代码变更');
57
- process.exit(0);
58
- }
57
+ if (options.staged) {
58
+ // 分析暂存区
59
+ commitInfo = { message: '(暂存区更改)', sha: 'staged' };
60
+ diff = await git.getStagedDiff();
61
+ reviewType = 'staged';
62
+ author = await git.getCommitAuthor('staged');
63
+ diffStats = await git.getStagedDiffStats();
64
+ } else if (options.commit) {
65
+ // 分析指定 commit
66
+ commitInfo = await git.getCommitInfo(options.commit);
67
+ diff = await git.getCommitDiff(options.commit);
68
+ author = await git.getCommitAuthor(options.commit);
69
+ diffStats = await git.getDiffStats(`${options.commit}~1`, options.commit);
70
+ } else if (options.from && options.to) {
71
+ // 分析 commit 范围
72
+ commitInfo = { message: `${options.from}..${options.to}`, sha: 'range' };
73
+ diff = await git.getRangeDiff(options.from, options.to);
74
+ reviewType = 'range';
75
+ author = await git.getLastCommitAuthor();
76
+ diffStats = await git.getDiffStats(options.from, options.to);
77
+ } else {
78
+ // 默认: 分析最近一次 commit
79
+ commitInfo = await git.getLastCommitInfo();
80
+ diff = await git.getLastCommitDiff();
81
+ author = await git.getLastCommitAuthor();
82
+ diffStats = await git.getDiffStats('HEAD~1', 'HEAD');
83
+ }
59
84
 
60
- const changedFiles = await getChangedFiles({
61
- staged: options.staged,
62
- commit: options.commit,
63
- from: options.from,
64
- to: options.to
65
- });
85
+ if (!diff || diff.trim() === '') {
86
+ spinner.fail('没有找到代码变更');
87
+ process.exit(1);
88
+ }
66
89
 
67
- spinner.succeed(`找到 ${changedFiles.length} 个文件变更`);
90
+ // 获取项目信息和分支
91
+ const projectName = await git.getProjectName();
92
+ const branch = await git.getCurrentBranch();
68
93
 
69
- // 检查 diff 大小
70
- const diffLines = diff.split('\n').length;
71
- if (diffLines > 2000) {
72
- logger.warn(`diff 较大 (${diffLines} 行),分析可能需要较长时间`);
73
- }
94
+ spinner.succeed('获取 Git 信息完成');
74
95
 
75
- // 构建提示词
76
- const prompt = buildReviewPrompt(commitInfo.message, diff, changedFiles);
96
+ // 构建提示词
97
+ const prompt = buildReviewPrompt(commitInfo.message, diff);
77
98
 
78
- // 创建 AI 客户端
79
- const aiClient = new AIClient(config);
99
+ // 调用 AI 分析
100
+ spinner = ora('AI 正在分析代码...').start();
80
101
 
81
- // 流式分析
82
- console.log(chalk.cyan('\n🤖 AI 分析中...\n'));
83
- console.log(chalk.gray('─'.repeat(60)));
102
+ const aiClient = new AIClient(config);
103
+ let response = '';
84
104
 
85
- let fullResponse = '';
86
- let isShowingStream = true;
105
+ try {
106
+ response = await aiClient.analyzeStream(prompt, (chunk) => {
107
+ // 流式输出时更新 spinner
108
+ spinner.text = `AI 正在分析... (${response.length} 字符)`;
109
+ });
110
+ } catch (error) {
111
+ spinner.fail('AI 分析失败');
112
+ if (error.message.includes('401')) {
113
+ logger.error('API Key 无效或已过期');
114
+ } else if (error.message.includes('403')) {
115
+ logger.error('API 请求被拒绝 (403)');
116
+ logger.info('请检查:');
117
+ console.log(' 1. API Key 是否正确');
118
+ console.log(' 2. API Host 是否正确');
119
+ console.log(' 3. 模型名称是否正确');
120
+ console.log('');
121
+ console.log(` 当前配置:`);
122
+ console.log(` apiHost: ${config.apiHost}`);
123
+ console.log(` model: ${config.model}`);
124
+ } else if (error.message.includes('429')) {
125
+ logger.error('请求过于频繁,请稍后重试');
126
+ } else {
127
+ logger.error(`API 错误: ${error.message}`);
128
+ }
129
+ process.exit(1);
130
+ }
87
131
 
88
- try {
89
- fullResponse = await aiClient.analyzeStream(prompt, (chunk) => {
90
- if (isShowingStream) {
91
- process.stdout.write(chalk.gray(chunk));
132
+ spinner.succeed('AI 分析完成');
133
+
134
+ // 生成报告
135
+ generateReport(response, commitInfo);
136
+
137
+ // 保存到数据库 (除非指定 --no-save)
138
+ if (options.save !== false) {
139
+ try {
140
+ const db = getDatabase();
141
+
142
+ // 获取或创建项目
143
+ const project = db.getOrCreateProject(projectName, process.cwd());
144
+
145
+ // 获取或创建开发者
146
+ const developer = db.getOrCreateDeveloper(author.email, author.name);
147
+
148
+ // 解析 AI 响应
149
+ const parsed = parseAIResponse(response);
150
+
151
+ // 提取统计数据
152
+ let summary = '';
153
+ let commitMatch = false;
154
+ let commitMatchReason = '';
155
+ let errorCount = 0;
156
+ let warningCount = 0;
157
+ let infoCount = 0;
158
+ let riskCount = 0;
159
+ let issues = [];
160
+ let associationRisks = [];
161
+
162
+ if (parsed) {
163
+ summary = parsed.summary || '';
164
+ commitMatch = parsed.commitMatch || false;
165
+ commitMatchReason = parsed.commitMatchReason || '';
166
+
167
+ if (parsed.issues && Array.isArray(parsed.issues)) {
168
+ issues = parsed.issues;
169
+ errorCount = issues.filter(i => i.level === 'error').length;
170
+ warningCount = issues.filter(i => i.level === 'warning').length;
171
+ infoCount = issues.filter(i => i.level === 'info').length;
172
+ }
173
+
174
+ if (parsed.associationRisks && Array.isArray(parsed.associationRisks)) {
175
+ associationRisks = parsed.associationRisks;
176
+ riskCount = associationRisks.length;
177
+ }
178
+ }
179
+
180
+ // 保存 review 记录
181
+ const reviewId = db.saveReview({
182
+ projectId: project.id,
183
+ developerId: developer.id,
184
+ commitSha: commitInfo.sha,
185
+ commitMessage: commitInfo.message,
186
+ commitDate: author.date,
187
+ branch,
188
+ reviewType,
189
+ fromSha: options.from || null,
190
+ toSha: options.to || null,
191
+ filesChanged: diffStats.filesChanged,
192
+ insertions: diffStats.insertions,
193
+ deletions: diffStats.deletions,
194
+ diffContent: null, // 不存储 diff 内容以节省空间
195
+ aiResponse: response,
196
+ summary,
197
+ commitMatch,
198
+ commitMatchReason,
199
+ errorCount,
200
+ warningCount,
201
+ infoCount,
202
+ riskCount,
203
+ modelUsed: config.model,
204
+ issues,
205
+ associationRisks
206
+ });
207
+
208
+ logger.success(`Review #${reviewId} 已保存到数据库`);
209
+ } catch (dbError) {
210
+ logger.warning(`保存到数据库失败: ${dbError.message}`);
92
211
  }
93
- });
212
+ }
213
+
214
+ } catch (error) {
215
+ spinner.fail('分析失败');
216
+ logger.error(error.message);
217
+ process.exit(1);
218
+ }
219
+ }
220
+
221
+ export default analyzeCommand;
222
+
223
+ // 分析多个 commits
224
+ async function analyzeMultipleCommits(options, config, git) {
225
+ const n = options.n ? parseInt(options.n, 10) : null;
226
+ const m = options.m ? parseInt(options.m, 10) : null;
227
+
228
+ // 验证参数
229
+ if (m !== null && n === null) {
230
+ logger.error('-m 参数必须与 -n 配合使用');
231
+ process.exit(1);
232
+ }
94
233
 
95
- console.log(chalk.gray('\n' + '─'.repeat(60)));
234
+ if (n !== null && m === null) {
235
+ // 只有 -n,表示分析最近 n 条
236
+ if (n <= 0 || n > 10) {
237
+ logger.error('n 必须在 1-10 之间');
238
+ process.exit(1);
239
+ }
240
+ }
96
241
 
97
- // 解析响应
98
- const report = parseAIResponse(fullResponse);
242
+ if (n !== null && m !== null) {
243
+ // 同时有 -n 和 -m,表示第 n 条到第 m 条
244
+ if (n <= 0 || m <= 0) {
245
+ logger.error('n 和 m 必须大于 0');
246
+ process.exit(1);
247
+ }
248
+ if (m < n) {
249
+ logger.error('m 必须大于等于 n');
250
+ process.exit(1);
251
+ }
252
+ if (m - n > 10) {
253
+ logger.error('m - n 不能超过 10');
254
+ process.exit(1);
255
+ }
256
+ }
257
+
258
+ let spinner = ora('获取 Git 信息...').start();
99
259
 
100
- if (report) {
101
- // 打印格式化报告
102
- printReport(commitInfo, report);
260
+ try {
261
+ // 获取 commits
262
+ let commits;
263
+ if (m !== null) {
264
+ // 第 n 条到第 m 条
265
+ commits = await git.getCommitRange(n, m);
103
266
  } else {
104
- logger.error('无法解析 AI 响应,请查看上方原始输出');
267
+ // 最近 n 条
268
+ commits = await git.getRecentCommits(n);
105
269
  }
106
270
 
107
- } catch (error) {
271
+ if (commits.length === 0) {
272
+ spinner.fail('没有找到 commits');
273
+ process.exit(1);
274
+ }
275
+
276
+ const projectName = await git.getProjectName();
277
+ const branch = await git.getCurrentBranch();
278
+
279
+ spinner.succeed(`找到 ${commits.length} 个 commit`);
280
+
281
+ // 显示要分析的 commits
282
+ logger.info('\n要分析的 commits:');
283
+ commits.forEach((commit, index) => {
284
+ const shortSha = commit.sha.substring(0, 7);
285
+ const shortMsg = commit.message.split('\n')[0].substring(0, 50);
286
+ console.log(` ${index + 1}. ${shortSha} - ${shortMsg}`);
287
+ });
108
288
  console.log('');
109
- const errMsg = error.message || String(error);
110
-
111
- if (errMsg.includes('401') || errMsg.includes('API key') || errMsg.includes('Unauthorized')) {
112
- logger.error('API Key 无效或已过期。请运行 goodiffer init 重新配置');
113
- } else if (errMsg.includes('403') || errMsg.includes('blocked') || errMsg.includes('Forbidden')) {
114
- logger.error('API 请求被拒绝 (403)');
115
- logger.info('可能原因:');
116
- logger.info(' 1. API Key 无效或无权限');
117
- logger.info(' 2. 模型名称不正确 (第三方 API 可能使用不同名称)');
118
- logger.info(' 3. 账户余额不足或已过期');
119
- logger.info(`当前配置: apiHost=${config.apiHost}, model=${config.model}`);
120
- logger.info('请运行 goodiffer init 检查配置');
121
- } else if (errMsg.includes('404')) {
122
- logger.error('API 端点不存在 (404)');
123
- logger.info(`当前端点: ${config.apiHost}/v1/chat/completions`);
124
- logger.info('请检查 API Host 是否正确');
125
- } else if (errMsg.includes('rate limit') || errMsg.includes('429')) {
126
- logger.error('API 请求频率超限,请稍后重试');
127
- } else if (errMsg.includes('ENOTFOUND') || errMsg.includes('ECONNREFUSED')) {
128
- logger.error('无法连接到 API 服务器');
129
- logger.info(`当前 API Host: ${config.apiHost}`);
130
- } else {
131
- logger.error(`分析失败: ${errMsg}`);
289
+
290
+ // 逐个分析
291
+ const aiClient = new AIClient(config);
292
+ const results = [];
293
+
294
+ for (let i = 0; i < commits.length; i++) {
295
+ const commit = commits[i];
296
+ const shortSha = commit.sha.substring(0, 7);
297
+
298
+ spinner = ora(`[${i + 1}/${commits.length}] 分析 commit ${shortSha}...`).start();
299
+
300
+ try {
301
+ // 获取 diff
302
+ const diff = await git.getCommitDiff(commit.sha);
303
+ if (!diff || diff.trim() === '') {
304
+ spinner.warn(`[${i + 1}/${commits.length}] commit ${shortSha} 没有代码变更,跳过`);
305
+ continue;
306
+ }
307
+
308
+ const diffStats = await git.getDiffStats(`${commit.sha}~1`, commit.sha);
309
+
310
+ // 构建提示词并分析
311
+ const prompt = buildReviewPrompt(commit.message, diff);
312
+ let response = '';
313
+
314
+ try {
315
+ response = await aiClient.analyzeStream(prompt, (chunk) => {
316
+ spinner.text = `[${i + 1}/${commits.length}] 分析 commit ${shortSha}... (${response.length} 字符)`;
317
+ });
318
+ } catch (error) {
319
+ spinner.fail(`[${i + 1}/${commits.length}] commit ${shortSha} 分析失败: ${error.message}`);
320
+ continue;
321
+ }
322
+
323
+ spinner.succeed(`[${i + 1}/${commits.length}] commit ${shortSha} 分析完成`);
324
+
325
+ // 生成报告
326
+ generateReport(response, { sha: commit.sha, message: commit.message });
327
+
328
+ // 保存到数据库
329
+ if (options.save !== false) {
330
+ try {
331
+ const db = getDatabase();
332
+ const project = db.getOrCreateProject(projectName, process.cwd());
333
+ const developer = db.getOrCreateDeveloper(commit.author.email, commit.author.name);
334
+ const parsed = parseAIResponse(response);
335
+
336
+ let summary = '';
337
+ let commitMatch = false;
338
+ let commitMatchReason = '';
339
+ let errorCount = 0;
340
+ let warningCount = 0;
341
+ let infoCount = 0;
342
+ let riskCount = 0;
343
+ let issues = [];
344
+ let associationRisks = [];
345
+
346
+ if (parsed) {
347
+ summary = parsed.summary || '';
348
+ commitMatch = parsed.commitMatch || false;
349
+ commitMatchReason = parsed.commitMatchReason || '';
350
+
351
+ if (parsed.issues && Array.isArray(parsed.issues)) {
352
+ issues = parsed.issues;
353
+ errorCount = issues.filter(i => i.level === 'error').length;
354
+ warningCount = issues.filter(i => i.level === 'warning').length;
355
+ infoCount = issues.filter(i => i.level === 'info').length;
356
+ }
357
+
358
+ if (parsed.associationRisks && Array.isArray(parsed.associationRisks)) {
359
+ associationRisks = parsed.associationRisks;
360
+ riskCount = associationRisks.length;
361
+ }
362
+ }
363
+
364
+ const reviewId = db.saveReview({
365
+ projectId: project.id,
366
+ developerId: developer.id,
367
+ commitSha: commit.sha,
368
+ commitMessage: commit.message,
369
+ commitDate: commit.author.date,
370
+ branch,
371
+ reviewType: 'commit',
372
+ fromSha: null,
373
+ toSha: null,
374
+ filesChanged: diffStats.filesChanged,
375
+ insertions: diffStats.insertions,
376
+ deletions: diffStats.deletions,
377
+ diffContent: null,
378
+ aiResponse: response,
379
+ summary,
380
+ commitMatch,
381
+ commitMatchReason,
382
+ errorCount,
383
+ warningCount,
384
+ infoCount,
385
+ riskCount,
386
+ modelUsed: config.model,
387
+ issues,
388
+ associationRisks
389
+ });
390
+
391
+ results.push({ commit, reviewId, success: true });
392
+ } catch (dbError) {
393
+ logger.warning(`保存 commit ${shortSha} 到数据库失败: ${dbError.message}`);
394
+ results.push({ commit, success: false, error: dbError.message });
395
+ }
396
+ }
397
+
398
+ // 在 commits 之间添加分隔线
399
+ if (i < commits.length - 1) {
400
+ console.log('\n' + '─'.repeat(60) + '\n');
401
+ }
402
+
403
+ } catch (error) {
404
+ spinner.fail(`[${i + 1}/${commits.length}] commit ${shortSha} 处理失败: ${error.message}`);
405
+ results.push({ commit, success: false, error: error.message });
406
+ }
407
+ }
408
+
409
+ // 显示汇总
410
+ console.log('\n' + '═'.repeat(60));
411
+ logger.success(`\n分析完成!共处理 ${commits.length} 个 commit`);
412
+
413
+ const successCount = results.filter(r => r.success).length;
414
+ if (successCount > 0) {
415
+ logger.info(`成功保存 ${successCount} 条记录到数据库`);
132
416
  }
417
+
418
+ } catch (error) {
419
+ spinner.fail('分析失败');
420
+ logger.error(error.message);
133
421
  process.exit(1);
134
422
  }
135
423
  }
136
-
137
- export default analyzeCommand;