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 +92 -2
- package/package.json +4 -1
- package/src/commands/analyze.js +383 -97
- package/src/commands/developer.js +116 -0
- package/src/commands/history.js +120 -0
- package/src/commands/init.js +76 -86
- package/src/commands/report.js +138 -0
- package/src/commands/stats.js +139 -0
- package/src/index.js +9 -35
- package/src/prompts/report-prompt.js +187 -0
- package/src/prompts/review-prompt.js +26 -46
- package/src/services/database.js +524 -0
- package/src/services/git.js +200 -101
- package/src/services/report-generator.js +298 -0
- package/src/services/reporter.js +96 -97
- package/src/utils/config-store.js +6 -12
- package/src/utils/logger.js +33 -33
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getDatabase } from '../services/database.js';
|
|
3
|
+
import logger from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
export async function developerCommand(action, args) {
|
|
6
|
+
const db = getDatabase();
|
|
7
|
+
|
|
8
|
+
switch (action) {
|
|
9
|
+
case 'list':
|
|
10
|
+
listDevelopers(db);
|
|
11
|
+
break;
|
|
12
|
+
|
|
13
|
+
case 'alias':
|
|
14
|
+
if (args.length < 2) {
|
|
15
|
+
logger.error('用法: goodiffer developer alias <email_pattern> <target_email>');
|
|
16
|
+
console.log();
|
|
17
|
+
console.log('示例:');
|
|
18
|
+
console.log(' goodiffer developer alias "john@old-email.com" "john@new-email.com"');
|
|
19
|
+
console.log(' goodiffer developer alias "*@company.com" "team@company.com"');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setAlias(db, args[0], args[1]);
|
|
23
|
+
break;
|
|
24
|
+
|
|
25
|
+
case 'rename':
|
|
26
|
+
if (args.length < 2) {
|
|
27
|
+
logger.error('用法: goodiffer developer rename <email> <new_name>');
|
|
28
|
+
console.log();
|
|
29
|
+
console.log('示例:');
|
|
30
|
+
console.log(' goodiffer developer rename "john@example.com" "John Doe"');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
renameDeveloper(db, args[0], args[1]);
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
case 'team':
|
|
37
|
+
if (args.length < 2) {
|
|
38
|
+
logger.error('用法: goodiffer developer team <email> <team_name>');
|
|
39
|
+
console.log();
|
|
40
|
+
console.log('示例:');
|
|
41
|
+
console.log(' goodiffer developer team "john@example.com" "Frontend Team"');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setTeam(db, args[0], args[1]);
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
default:
|
|
48
|
+
logger.error(`未知操作: ${action}`);
|
|
49
|
+
console.log();
|
|
50
|
+
console.log('可用操作:');
|
|
51
|
+
console.log(' list - 列出所有开发者');
|
|
52
|
+
console.log(' alias - 设置邮箱别名映射');
|
|
53
|
+
console.log(' rename - 修改显示名称');
|
|
54
|
+
console.log(' team - 设置团队');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function listDevelopers(db) {
|
|
59
|
+
const developers = db.listDevelopers();
|
|
60
|
+
|
|
61
|
+
if (developers.length === 0) {
|
|
62
|
+
logger.info('暂无开发者数据');
|
|
63
|
+
console.log();
|
|
64
|
+
console.log('提示: 运行 goodiffer 分析代码后会自动记录开发者信息');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
logger.title('开发者列表');
|
|
69
|
+
|
|
70
|
+
for (const dev of developers) {
|
|
71
|
+
console.log(chalk.cyan(dev.display_name));
|
|
72
|
+
console.log(chalk.gray(` 邮箱: ${dev.git_email}`));
|
|
73
|
+
if (dev.team) {
|
|
74
|
+
console.log(chalk.gray(` 团队: ${dev.team}`));
|
|
75
|
+
}
|
|
76
|
+
if (dev.display_name !== dev.git_name) {
|
|
77
|
+
console.log(chalk.gray(` Git 名: ${dev.git_name}`));
|
|
78
|
+
}
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(chalk.gray(`共 ${developers.length} 位开发者`));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setAlias(db, emailPattern, targetEmail) {
|
|
86
|
+
try {
|
|
87
|
+
db.setDeveloperAlias(emailPattern, targetEmail);
|
|
88
|
+
logger.success(`已设置别名: ${emailPattern} → ${targetEmail}`);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error(error.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renameDeveloper(db, email, newName) {
|
|
95
|
+
const dev = db.getDeveloper(email);
|
|
96
|
+
if (!dev) {
|
|
97
|
+
logger.error(`开发者 "${email}" 不存在`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
db.updateDeveloper(email, { displayName: newName });
|
|
102
|
+
logger.success(`已将 "${email}" 的显示名称修改为 "${newName}"`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setTeam(db, email, teamName) {
|
|
106
|
+
const dev = db.getDeveloper(email);
|
|
107
|
+
if (!dev) {
|
|
108
|
+
logger.error(`开发者 "${email}" 不存在`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
db.updateDeveloper(email, { team: teamName });
|
|
113
|
+
logger.success(`已将 "${dev.display_name}" 设置为团队 "${teamName}"`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default developerCommand;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import { getDatabase } from '../services/database.js';
|
|
4
|
+
import { GitService } from '../services/git.js';
|
|
5
|
+
import logger from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
export async function historyCommand(options) {
|
|
8
|
+
const db = getDatabase();
|
|
9
|
+
const git = new GitService();
|
|
10
|
+
|
|
11
|
+
// 构建筛选条件
|
|
12
|
+
const filters = {
|
|
13
|
+
limit: parseInt(options.limit) || 20,
|
|
14
|
+
offset: 0
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// 项目筛选
|
|
18
|
+
if (options.project !== undefined) {
|
|
19
|
+
let projectName;
|
|
20
|
+
if (options.project === true) {
|
|
21
|
+
// -p 无参数,使用当前项目
|
|
22
|
+
projectName = await git.getProjectName();
|
|
23
|
+
} else {
|
|
24
|
+
projectName = options.project;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const project = db.getProject(projectName);
|
|
28
|
+
if (!project) {
|
|
29
|
+
logger.error(`项目 "${projectName}" 不存在`);
|
|
30
|
+
console.log();
|
|
31
|
+
console.log('可用项目:');
|
|
32
|
+
const projects = db.listProjects();
|
|
33
|
+
projects.forEach(p => {
|
|
34
|
+
console.log(` - ${p.name}`);
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
filters.projectId = project.id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 开发者筛选
|
|
42
|
+
if (options.developer) {
|
|
43
|
+
const developers = db.listDevelopers();
|
|
44
|
+
const dev = developers.find(d =>
|
|
45
|
+
d.display_name.toLowerCase().includes(options.developer.toLowerCase()) ||
|
|
46
|
+
d.git_email.toLowerCase().includes(options.developer.toLowerCase())
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (!dev) {
|
|
50
|
+
logger.error(`开发者 "${options.developer}" 不存在`);
|
|
51
|
+
console.log();
|
|
52
|
+
console.log('可用开发者:');
|
|
53
|
+
developers.forEach(d => {
|
|
54
|
+
console.log(` - ${d.display_name} (${d.git_email})`);
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
filters.developerId = dev.id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 日期筛选
|
|
62
|
+
if (options.since) {
|
|
63
|
+
filters.since = dayjs(options.since).startOf('day').toISOString();
|
|
64
|
+
}
|
|
65
|
+
if (options.until) {
|
|
66
|
+
filters.until = dayjs(options.until).endOf('day').toISOString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 查询记录
|
|
70
|
+
const reviews = db.queryReviews(filters);
|
|
71
|
+
|
|
72
|
+
if (reviews.length === 0) {
|
|
73
|
+
logger.info('没有找到匹配的 Review 记录');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// JSON 输出
|
|
78
|
+
if (options.json) {
|
|
79
|
+
console.log(JSON.stringify(reviews, null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 表格输出
|
|
84
|
+
logger.title('Code Review 历史记录');
|
|
85
|
+
|
|
86
|
+
console.log(chalk.gray(`共 ${reviews.length} 条记录`));
|
|
87
|
+
console.log();
|
|
88
|
+
|
|
89
|
+
for (const review of reviews) {
|
|
90
|
+
const date = dayjs(review.commit_date).format('YYYY-MM-DD HH:mm');
|
|
91
|
+
const sha = review.commit_sha.substring(0, 7);
|
|
92
|
+
|
|
93
|
+
// 状态颜色
|
|
94
|
+
let statusIcon = chalk.green('✓');
|
|
95
|
+
if (review.error_count > 0) {
|
|
96
|
+
statusIcon = chalk.red('✗');
|
|
97
|
+
} else if (review.warning_count > 0) {
|
|
98
|
+
statusIcon = chalk.yellow('⚠');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(
|
|
102
|
+
statusIcon,
|
|
103
|
+
chalk.gray(date),
|
|
104
|
+
chalk.cyan(`[${sha}]`),
|
|
105
|
+
chalk.white(review.commit_message.substring(0, 50))
|
|
106
|
+
);
|
|
107
|
+
console.log(
|
|
108
|
+
' ',
|
|
109
|
+
chalk.gray('项目:'), review.project_name,
|
|
110
|
+
chalk.gray('| 开发者:'), review.developer_name,
|
|
111
|
+
chalk.gray('| 问题:'),
|
|
112
|
+
chalk.red(`${review.error_count}E`),
|
|
113
|
+
chalk.yellow(`${review.warning_count}W`),
|
|
114
|
+
chalk.blue(`${review.info_count}I`)
|
|
115
|
+
);
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default historyCommand;
|
package/src/commands/init.js
CHANGED
|
@@ -1,137 +1,127 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
|
-
import {
|
|
2
|
+
import { setConfig, getConfig } from '../utils/config-store.js';
|
|
3
3
|
import logger from '../utils/logger.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
{ name: 'gpt-4o (OpenAI)', value: 'gpt-4o' },
|
|
11
|
-
{ name: 'gpt-4-turbo (OpenAI)', value: 'gpt-4-turbo' },
|
|
12
|
-
{ name: 'deepseek-chat (DeepSeek)', value: 'deepseek-chat' },
|
|
13
|
-
{ name: '自定义模型...', value: '__custom__' }
|
|
14
|
-
];
|
|
5
|
+
const API_HOSTS = {
|
|
6
|
+
anthropic: 'https://api.anthropic.com',
|
|
7
|
+
openai: 'https://api.openai.com',
|
|
8
|
+
packyapi: 'https://www.packyapi.com'
|
|
9
|
+
};
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{ name: '自定义 API Host...', value: '__custom__' }
|
|
22
|
-
];
|
|
11
|
+
const MODELS = {
|
|
12
|
+
anthropic: ['claude-sonnet-4-5-20250929', 'claude-3-opus-20240229', 'claude-3-5-sonnet-20241022'],
|
|
13
|
+
openai: ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
14
|
+
packyapi: ['claude-sonnet-4-5-20250929', 'gpt-4o']
|
|
15
|
+
};
|
|
23
16
|
|
|
24
17
|
export async function initCommand() {
|
|
25
|
-
logger.title('Goodiffer
|
|
18
|
+
logger.title('Goodiffer 配置向导');
|
|
26
19
|
|
|
27
20
|
const currentConfig = getConfig();
|
|
28
21
|
|
|
29
22
|
const answers = await inquirer.prompt([
|
|
30
23
|
{
|
|
31
24
|
type: 'list',
|
|
32
|
-
name: '
|
|
25
|
+
name: 'hostChoice',
|
|
33
26
|
message: '选择 API Host:',
|
|
34
|
-
choices:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
27
|
+
choices: [
|
|
28
|
+
{ name: 'Anthropic (官方 Claude API)', value: 'anthropic' },
|
|
29
|
+
{ name: 'OpenAI (官方 GPT API)', value: 'openai' },
|
|
30
|
+
{ name: 'PackyAPI (第三方代理)', value: 'packyapi' },
|
|
31
|
+
{ name: '自定义 URL', value: 'custom' }
|
|
32
|
+
],
|
|
33
|
+
default: currentConfig.apiHost ? 'custom' : 'anthropic'
|
|
42
34
|
},
|
|
43
35
|
{
|
|
44
36
|
type: 'input',
|
|
45
|
-
name: '
|
|
46
|
-
message: '输入自定义 API Host
|
|
47
|
-
when: (
|
|
37
|
+
name: 'customHost',
|
|
38
|
+
message: '输入自定义 API Host URL:',
|
|
39
|
+
when: (ans) => ans.hostChoice === 'custom',
|
|
48
40
|
default: currentConfig.apiHost || '',
|
|
49
41
|
validate: (input) => {
|
|
50
|
-
if (!input
|
|
51
|
-
|
|
42
|
+
if (!input) return '请输入 API Host URL';
|
|
43
|
+
try {
|
|
44
|
+
new URL(input);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return '请输入有效的 URL';
|
|
52
48
|
}
|
|
53
|
-
return true;
|
|
54
49
|
}
|
|
55
50
|
},
|
|
56
51
|
{
|
|
57
52
|
type: 'password',
|
|
58
53
|
name: 'apiKey',
|
|
59
|
-
message: 'API Key:',
|
|
54
|
+
message: '输入 API Key:',
|
|
60
55
|
mask: '*',
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return '请输入有效的 API Key';
|
|
64
|
-
}
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
56
|
+
default: currentConfig.apiKey || '',
|
|
57
|
+
validate: (input) => input ? true : '请输入 API Key'
|
|
67
58
|
},
|
|
68
59
|
{
|
|
69
60
|
type: 'list',
|
|
70
61
|
name: 'modelChoice',
|
|
71
62
|
message: '选择模型:',
|
|
72
|
-
choices:
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
choices: (ans) => {
|
|
64
|
+
const host = ans.hostChoice;
|
|
65
|
+
if (host === 'custom') {
|
|
66
|
+
return [
|
|
67
|
+
{ name: 'claude-sonnet-4-5-20250929', value: 'claude-sonnet-4-5-20250929' },
|
|
68
|
+
{ name: 'gpt-4o', value: 'gpt-4o' },
|
|
69
|
+
{ name: '自定义模型', value: 'custom' }
|
|
70
|
+
];
|
|
77
71
|
}
|
|
78
|
-
|
|
72
|
+
const models = MODELS[host] || MODELS.anthropic;
|
|
73
|
+
return [
|
|
74
|
+
...models.map(m => ({ name: m, value: m })),
|
|
75
|
+
{ name: '自定义模型', value: 'custom' }
|
|
76
|
+
];
|
|
79
77
|
}
|
|
80
78
|
},
|
|
81
79
|
{
|
|
82
80
|
type: 'input',
|
|
83
81
|
name: 'customModel',
|
|
84
82
|
message: '输入自定义模型名称:',
|
|
85
|
-
when: (
|
|
86
|
-
|
|
87
|
-
validate: (input) => {
|
|
88
|
-
if (!input || input.length < 1) {
|
|
89
|
-
return '请输入模型名称';
|
|
90
|
-
}
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
83
|
+
when: (ans) => ans.modelChoice === 'custom',
|
|
84
|
+
validate: (input) => input ? true : '请输入模型名称'
|
|
93
85
|
}
|
|
94
86
|
]);
|
|
95
87
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
: answers.modelChoice;
|
|
88
|
+
// 确定 API Host
|
|
89
|
+
let apiHost;
|
|
90
|
+
if (answers.hostChoice === 'custom') {
|
|
91
|
+
apiHost = answers.customHost;
|
|
92
|
+
} else {
|
|
93
|
+
apiHost = API_HOSTS[answers.hostChoice];
|
|
94
|
+
}
|
|
104
95
|
|
|
105
|
-
//
|
|
106
|
-
let provider
|
|
107
|
-
if (
|
|
96
|
+
// 确定 provider
|
|
97
|
+
let provider;
|
|
98
|
+
if (answers.hostChoice === 'anthropic' || answers.hostChoice === 'packyapi') {
|
|
108
99
|
provider = 'claude';
|
|
109
|
-
} else if (
|
|
100
|
+
} else if (answers.hostChoice === 'openai') {
|
|
110
101
|
provider = 'openai';
|
|
102
|
+
} else {
|
|
103
|
+
// 自定义时根据模型名判断
|
|
104
|
+
const model = answers.customModel || answers.modelChoice;
|
|
105
|
+
provider = model.toLowerCase().startsWith('claude') ? 'claude' : 'openai';
|
|
111
106
|
}
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
apiHost: apiHost.replace(/\/+$/, ''), // 移除末尾斜杠
|
|
116
|
-
apiKey: answers.apiKey,
|
|
117
|
-
model
|
|
118
|
-
};
|
|
108
|
+
// 确定模型
|
|
109
|
+
const model = answers.customModel || answers.modelChoice;
|
|
119
110
|
|
|
120
|
-
|
|
111
|
+
// 保存配置
|
|
112
|
+
setConfig('apiHost', apiHost);
|
|
113
|
+
setConfig('apiKey', answers.apiKey);
|
|
114
|
+
setConfig('model', model);
|
|
115
|
+
setConfig('provider', provider);
|
|
121
116
|
|
|
117
|
+
console.log();
|
|
122
118
|
logger.success('配置已保存!');
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
console.log('');
|
|
130
|
-
if (provider === 'claude' && apiHost.includes('anthropic.com')) {
|
|
131
|
-
logger.info(`API 端点: ${configToSave.apiHost}/v1/messages`);
|
|
132
|
-
} else {
|
|
133
|
-
logger.info(`API 端点: ${configToSave.apiHost}/v1/chat/completions`);
|
|
134
|
-
}
|
|
119
|
+
console.log();
|
|
120
|
+
console.log(` API Host: ${apiHost}`);
|
|
121
|
+
console.log(` Model: ${model}`);
|
|
122
|
+
console.log(` Provider: ${provider}`);
|
|
123
|
+
console.log();
|
|
124
|
+
logger.info('运行 goodiffer 开始分析代码');
|
|
135
125
|
}
|
|
136
126
|
|
|
137
127
|
export default initCommand;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import { getDatabase } from '../services/database.js';
|
|
4
|
+
import { ReportGenerator } from '../services/report-generator.js';
|
|
5
|
+
import { GitService } from '../services/git.js';
|
|
6
|
+
import { isConfigured } from '../utils/config-store.js';
|
|
7
|
+
import logger from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
export async function reportCommand(options) {
|
|
10
|
+
// 检查配置
|
|
11
|
+
if (!isConfigured()) {
|
|
12
|
+
logger.error('请先运行 goodiffer init 进行配置');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const db = getDatabase();
|
|
17
|
+
const git = new GitService();
|
|
18
|
+
const generator = new ReportGenerator();
|
|
19
|
+
|
|
20
|
+
let spinner;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
if (options.developer) {
|
|
24
|
+
// 生成开发者报告
|
|
25
|
+
const developer = db.getDeveloper(options.developer);
|
|
26
|
+
if (!developer) {
|
|
27
|
+
// 尝试模糊匹配
|
|
28
|
+
const developers = db.listDevelopers();
|
|
29
|
+
const match = developers.find(d =>
|
|
30
|
+
d.display_name.toLowerCase().includes(options.developer.toLowerCase()) ||
|
|
31
|
+
d.git_email.toLowerCase().includes(options.developer.toLowerCase())
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!match) {
|
|
35
|
+
logger.error(`开发者 "${options.developer}" 不存在`);
|
|
36
|
+
console.log();
|
|
37
|
+
console.log('可用开发者:');
|
|
38
|
+
developers.forEach(d => {
|
|
39
|
+
console.log(` - ${d.display_name} (${d.git_email})`);
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
options.developer = match.git_email;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
spinner = ora('正在生成开发者报告...').start();
|
|
47
|
+
|
|
48
|
+
const outputPath = await generator.generateDeveloperReport(
|
|
49
|
+
options.developer,
|
|
50
|
+
options,
|
|
51
|
+
(msg) => { spinner.text = msg; }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
spinner.succeed('报告生成完成');
|
|
55
|
+
console.log();
|
|
56
|
+
logger.success(`报告已保存到: ${outputPath}`);
|
|
57
|
+
|
|
58
|
+
if (options.open) {
|
|
59
|
+
await open(outputPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
} else {
|
|
63
|
+
// 生成项目报告
|
|
64
|
+
let projectName;
|
|
65
|
+
|
|
66
|
+
if (options.project === true || options.project === undefined) {
|
|
67
|
+
// 使用当前项目
|
|
68
|
+
projectName = await git.getProjectName();
|
|
69
|
+
} else if (options.project) {
|
|
70
|
+
projectName = options.project;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 检查项目是否存在
|
|
74
|
+
const project = db.getProject(projectName);
|
|
75
|
+
if (!project) {
|
|
76
|
+
logger.error(`项目 "${projectName}" 暂无 Review 记录`);
|
|
77
|
+
console.log();
|
|
78
|
+
|
|
79
|
+
const projects = db.listProjects();
|
|
80
|
+
if (projects.length > 0) {
|
|
81
|
+
console.log('可用项目:');
|
|
82
|
+
projects.forEach(p => {
|
|
83
|
+
console.log(` - ${p.name}`);
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
console.log('提示: 运行 goodiffer 分析代码后会自动保存记录');
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 检查是否有足够的数据
|
|
92
|
+
const stats = db.getProjectStats(project.id, {});
|
|
93
|
+
if (!stats || stats.total_reviews === 0) {
|
|
94
|
+
logger.error('项目暂无足够的 Review 数据生成报告');
|
|
95
|
+
console.log();
|
|
96
|
+
console.log('提示: 运行 goodiffer 分析更多 commit 后再生成报告');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
spinner = ora('正在生成项目报告...').start();
|
|
101
|
+
|
|
102
|
+
const outputPath = await generator.generateProjectReport(
|
|
103
|
+
projectName,
|
|
104
|
+
options,
|
|
105
|
+
(msg) => { spinner.text = msg; }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
spinner.succeed('报告生成完成');
|
|
109
|
+
console.log();
|
|
110
|
+
logger.success(`报告已保存到: ${outputPath}`);
|
|
111
|
+
console.log();
|
|
112
|
+
|
|
113
|
+
// 显示报告统计
|
|
114
|
+
console.log(` 项目: ${projectName}`);
|
|
115
|
+
console.log(` Reviews: ${stats.total_reviews}`);
|
|
116
|
+
console.log(` 问题: ${stats.total_errors || 0}E / ${stats.total_warnings || 0}W / ${stats.total_infos || 0}I`);
|
|
117
|
+
|
|
118
|
+
if (options.open) {
|
|
119
|
+
console.log();
|
|
120
|
+
logger.info('正在打开报告...');
|
|
121
|
+
await open(outputPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (spinner) {
|
|
127
|
+
spinner.fail('报告生成失败');
|
|
128
|
+
}
|
|
129
|
+
logger.error(error.message);
|
|
130
|
+
|
|
131
|
+
if (error.message.includes('401') || error.message.includes('403')) {
|
|
132
|
+
console.log();
|
|
133
|
+
logger.info('提示: 请检查 API 配置是否正确');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default reportCommand;
|