@hecom/codearts 0.2.2 → 0.3.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/README.md CHANGED
@@ -35,7 +35,7 @@ npx @hecom/codearts bug-rate "迭代1,迭代2"
35
35
  npx @hecom/codearts config
36
36
 
37
37
  # 单独更新角色配置
38
- npx @hecom/codearts config role-id
38
+ npx @hecom/codearts config role
39
39
 
40
40
  # 查看当前配置
41
41
  npx @hecom/codearts config show
package/dist/bin/cli.js CHANGED
@@ -42,6 +42,7 @@ const config_command_1 = require("../commands/config.command");
42
42
  const daily_command_1 = require("../commands/daily.command");
43
43
  const work_hour_command_1 = require("../commands/work-hour.command");
44
44
  const config_loader_1 = require("../utils/config-loader");
45
+ const console_1 = require("../utils/console");
45
46
  const logger_1 = require("../utils/logger");
46
47
  // 读取 package.json 中的版本号
47
48
  const packageJsonPath = path.join(__dirname, '../../package.json');
@@ -51,13 +52,14 @@ const program = new commander_1.Command();
51
52
  program.name('codearts').description('华为云 CodeArts 统计分析工具').version(version);
52
53
  // 全局选项(环境变量覆盖)
53
54
  program
54
- .option('--role-id <ids>', '角色 ID(支持逗号分隔,如: 1,2)')
55
+ .option('--role <ids>', '角色 ID(支持逗号分隔,如: 1,2)')
55
56
  .option('--output <format>', '输出格式:console、csv、json', 'console');
56
57
  // config 命令 - 交互式配置向导
57
58
  const configCmd = program
58
59
  .command('config')
59
60
  .description('交互式配置向导,引导用户创建或更新全局配置文件')
60
61
  .action(async () => {
62
+ (0, console_1.showLogo)();
61
63
  await (0, config_command_1.configCommand)();
62
64
  });
63
65
  // config show 子命令 - 显示当前配置
@@ -65,6 +67,7 @@ configCmd
65
67
  .command('show')
66
68
  .description('显示当前配置信息')
67
69
  .action(async () => {
70
+ (0, console_1.showLogo)();
68
71
  await (0, config_command_1.showConfigCommand)();
69
72
  });
70
73
  // 为每个项目配置项添加子命令
@@ -75,6 +78,7 @@ availableConfigs.forEach((configItem) => {
75
78
  .command(subCommandName)
76
79
  .description(`更新${configItem.label}`)
77
80
  .action(async () => {
81
+ (0, console_1.showLogo)();
78
82
  await (0, config_command_1.updateProjectConfigCommand)(configItem.key);
79
83
  });
80
84
  });
@@ -99,18 +103,19 @@ program
99
103
  });
100
104
  // bug-rate 命令
101
105
  program
102
- .command('bug-rate <iterations>')
103
- .description('产品缺陷率统计,支持多个迭代')
104
- .action(async (iterations, options, command) => {
106
+ .command('bug-rate')
107
+ .description('产品缺陷率统计')
108
+ .action(async (options, command) => {
105
109
  const cliOptions = command.parent.opts();
106
110
  logger_1.logger.setOutputFormat(cliOptions.output);
107
- await (0, bug_command_1.bugCommand)(iterations, cliOptions);
111
+ await (0, bug_command_1.bugCommand)(cliOptions);
108
112
  });
109
113
  // 检查配置并自动执行 config 命令
110
114
  async function checkConfigAndRun() {
111
115
  const args = process.argv.slice(2);
112
116
  // 如果没有参数(直接执行 codearts),检测配置
113
117
  if (args.length === 0) {
118
+ (0, console_1.showLogo)();
114
119
  // 检查是否有全局配置
115
120
  const hasConfig = (0, config_loader_1.configExists)();
116
121
  if (!hasConfig) {
@@ -125,6 +130,16 @@ async function checkConfigAndRun() {
125
130
  // 有参数,正常解析命令
126
131
  program.parse();
127
132
  }
133
+ process.on('uncaughtException', (error) => {
134
+ if (error instanceof Error && error.name === 'ExitPromptError') {
135
+ console.log('👋 操作取消!');
136
+ process.exit(0);
137
+ }
138
+ else {
139
+ // 重新抛出未知错误
140
+ throw error;
141
+ }
142
+ });
128
143
  checkConfigAndRun().catch((error) => {
129
144
  logger_1.logger.error('执行失败: ', error);
130
145
  process.exit(1);
@@ -2,4 +2,4 @@ import { CliOptions } from '../utils/config-loader';
2
2
  /**
3
3
  * bug 命令入口
4
4
  */
5
- export declare function bugCommand(iterationTitlesStr: string, cliOptions?: CliOptions): Promise<void>;
5
+ export declare function bugCommand(cliOptions?: CliOptions): Promise<void>;
@@ -1,10 +1,17 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.bugCommand = bugCommand;
7
+ const prompts_1 = require("@inquirer/prompts");
8
+ const ora_1 = __importDefault(require("ora"));
4
9
  const business_service_1 = require("../services/business.service");
5
10
  const types_1 = require("../types");
6
11
  const config_loader_1 = require("../utils/config-loader");
12
+ const console_1 = require("../utils/console");
7
13
  const csv_writer_1 = require("../utils/csv-writer");
14
+ const inquirer_theme_1 = require("../utils/inquirer-theme");
8
15
  const logger_1 = require("../utils/logger");
9
16
  /**
10
17
  * 判断 Bug 是否属于需求变更或产品设计问题
@@ -20,55 +27,17 @@ function isRequirementOrDesignBug(bug) {
20
27
  /**
21
28
  * 查询 Bug 统计数据
22
29
  */
23
- async function queryBugReportData(businessService, projectId, roleIds, iterationTitles) {
30
+ async function queryBugReportData(businessService, projectId, roleIds, iterations) {
24
31
  const roleNames = new Set();
25
32
  const targetMemberIds = new Set();
26
- if (roleIds.length > 0) {
27
- for (const roleId of roleIds) {
28
- const members = await businessService.getMembersByRoleId(projectId, roleId);
29
- members.forEach((member) => {
30
- targetMemberIds.add(member.user_num_id);
31
- roleNames.add(member.role_name);
32
- });
33
- }
34
- }
35
- const stories = await businessService.getStoriesByIterationTitles(projectId, iterationTitles);
36
- if (stories.length === 0) {
37
- return {
38
- iterations: iterationTitles,
39
- roleIds,
40
- roleNames: Array.from(roleNames),
41
- userStats: [],
42
- summary: {
43
- totalBugs: 0,
44
- totalProductBugs: 0,
45
- overallProductDefectRate: 0,
46
- userCount: 0,
47
- },
48
- };
49
- }
50
- const filteredStories = roleIds.length > 0
51
- ? stories.filter((story) => {
52
- const assignedUserId = story.assigned_user?.id;
53
- return assignedUserId && targetMemberIds.has(assignedUserId);
54
- })
55
- : stories;
56
- if (filteredStories.length === 0) {
57
- return {
58
- iterations: iterationTitles,
59
- roleIds,
60
- roleNames: Array.from(roleNames),
61
- userStats: [],
62
- summary: {
63
- totalBugs: 0,
64
- totalProductBugs: 0,
65
- overallProductDefectRate: 0,
66
- userCount: 0,
67
- },
68
- };
69
- }
33
+ const members = await businessService.getMembersByRoleIds(projectId, roleIds);
34
+ members.forEach((member) => {
35
+ targetMemberIds.add(member.user_num_id);
36
+ roleNames.add(member.role_name);
37
+ });
38
+ const stories = await businessService.getStoriesByIterations(projectId, iterations, members.map((m) => m.user_id));
70
39
  const bugStatsMap = new Map();
71
- for (const story of filteredStories) {
40
+ for (const story of stories) {
72
41
  const childIssues = await businessService.getChildIssues(projectId, String(story.id));
73
42
  const allBugs = childIssues.filter((issue) => issue.tracker.id === 3);
74
43
  if (allBugs.length === 0) {
@@ -82,7 +51,7 @@ async function queryBugReportData(businessService, projectId, roleIds, iteration
82
51
  }
83
52
  if (!bugStatsMap.has(assignedUserId)) {
84
53
  bugStatsMap.set(assignedUserId, {
85
- assignedUser: assignedUserName,
54
+ userName: assignedUserName,
86
55
  userId: assignedUserId,
87
56
  totalBugCount: 0,
88
57
  productBugCount: 0,
@@ -96,6 +65,7 @@ async function queryBugReportData(businessService, projectId, roleIds, iteration
96
65
  userStats.stories.push({
97
66
  storyName: story.name,
98
67
  storyId: story.id,
68
+ iterationName: story.iteration?.name,
99
69
  totalBugCount: allBugs.length,
100
70
  productBugCount: productBugs.length,
101
71
  });
@@ -115,34 +85,27 @@ async function queryBugReportData(businessService, projectId, roleIds, iteration
115
85
  });
116
86
  const overallProductDefectRate = totalBugs > 0 ? Math.round((totalProductBugs / totalBugs) * 100 * 100) / 100 : 0;
117
87
  return {
118
- iterations: iterationTitles,
119
- roleIds,
88
+ title: '产品缺陷率统计',
120
89
  roleNames: Array.from(roleNames),
121
- userStats: sortedStats,
122
- summary: {
123
- totalBugs,
124
- totalProductBugs,
125
- overallProductDefectRate,
126
- userCount: sortedStats.length,
127
- },
90
+ list: sortedStats,
91
+ totalMap: [
92
+ ['产品缺陷率', `${overallProductDefectRate.toFixed(2)}%`],
93
+ ['总 Bug 数', `${totalBugs}个`],
94
+ ['产品问题数', `${totalProductBugs}个`],
95
+ ['统计人数', `${sortedStats.length}人`],
96
+ ['统计迭代', `${iterations.map((i) => i.name).join(', ')}`],
97
+ ],
128
98
  };
129
99
  }
130
100
  /**
131
101
  * 控制台输出产品缺陷率统计
132
102
  */
133
103
  function outputConsole(data) {
134
- logger_1.logger.info(`产品缺陷率统计 [${data.roleNames.join(', ')}]`);
135
- logger_1.logger.info('='.repeat(80));
136
- logger_1.logger.info(`产品缺陷率: ${data.summary.overallProductDefectRate.toFixed(2)}% `);
137
- logger_1.logger.info(`总 Bug 数: ${data.summary.totalBugs}个`);
138
- logger_1.logger.info(`产品问题: ${data.summary.totalProductBugs}个`);
139
- logger_1.logger.info(`统计人数: ${data.summary.userCount}人`);
140
- logger_1.logger.info(`统计迭代: ${data.iterations.join(', ')}`);
141
- logger_1.logger.info('='.repeat(80));
142
- data.userStats.forEach((stats) => {
143
- logger_1.logger.info(`\x1b[31m${stats.assignedUser}: ${stats.productDefectRate.toFixed(2)}% (${stats.productBugCount}/${stats.totalBugCount})\x1b[0m`);
104
+ (0, console_1.consoleTotal)(data);
105
+ data.list.forEach((stats) => {
106
+ logger_1.logger.info(`\x1b[31m${stats.userName}: ${stats.productDefectRate.toFixed(2)}% (${stats.productBugCount}/${stats.totalBugCount})\x1b[0m`);
144
107
  stats.stories.forEach((story) => {
145
- logger_1.logger.info(` ${story.storyName} (${story.productBugCount}/${story.totalBugCount})`);
108
+ logger_1.logger.info(` [${story.iterationName}] ${story.storyName} (${story.productBugCount}/${story.totalBugCount})`);
146
109
  });
147
110
  logger_1.logger.info('');
148
111
  });
@@ -150,18 +113,27 @@ function outputConsole(data) {
150
113
  /**
151
114
  * CSV 文件输出
152
115
  */
153
- function outputCsv(data, iterationTitles) {
116
+ function outputCsv(list, projectId) {
154
117
  const csvLines = [];
155
- csvLines.push('迭代,处理人,Story名称,StoryID,总Bug数,产品问题Bug数,产品缺陷率');
156
- data.userStats.forEach((userStat) => {
118
+ csvLines.push((0, csv_writer_1.buildCsvRow)(['迭代', '处理人', 'Story', '总Bug数', '产品问题Bug', '产品缺陷率']));
119
+ list.forEach((userStat) => {
157
120
  userStat.stories.forEach((story) => {
158
121
  const defectRate = story.totalBugCount > 0
159
122
  ? ((story.productBugCount / story.totalBugCount) * 100).toFixed(2)
160
123
  : '0.00';
161
- csvLines.push(`"${(0, csv_writer_1.escapeCsv)(data.iterations.join(', '))}",${userStat.assignedUser ?? ''},"${(0, csv_writer_1.escapeCsv)(story.storyName ?? '')}",${story.storyId ?? ''},${story.totalBugCount ?? ''},${story.productBugCount ?? ''},${defectRate}%`);
124
+ const storyUrl = (0, console_1.issueLink)(projectId, story.storyId);
125
+ const storyLink = (0, csv_writer_1.createHyperlinkFormula)(storyUrl, story.storyName ?? '');
126
+ csvLines.push((0, csv_writer_1.buildCsvRow)([
127
+ story.iterationName,
128
+ userStat.userName ?? '',
129
+ storyLink,
130
+ story.totalBugCount ?? '',
131
+ story.productBugCount ?? '',
132
+ `${defectRate}%`,
133
+ ]));
162
134
  });
163
135
  });
164
- const filename = `bug-rate-${iterationTitles.join('-')}.csv`;
136
+ const filename = `bug-rate.csv`;
165
137
  (0, csv_writer_1.writeCsvFile)(filename, csvLines, logger_1.logger);
166
138
  }
167
139
  /**
@@ -173,30 +145,40 @@ function outputJson(data) {
173
145
  /**
174
146
  * bug 命令入口
175
147
  */
176
- async function bugCommand(iterationTitlesStr, cliOptions = {}) {
177
- try {
178
- if (!iterationTitlesStr || iterationTitlesStr.trim() === '') {
179
- throw new Error('请指定至少一个迭代标题');
180
- }
181
- const iterationTitles = iterationTitlesStr
182
- .split(/[,,;;|\s、]+/)
183
- .map((title) => title.trim())
184
- .filter((title) => title.length > 0);
185
- const { projectId, roleIds, config, outputFormat } = (0, config_loader_1.loadConfig)(cliOptions);
186
- const businessService = new business_service_1.BusinessService(config);
187
- const reportData = await queryBugReportData(businessService, projectId, roleIds, iterationTitles);
188
- if (outputFormat === 'console') {
189
- outputConsole(reportData);
190
- }
191
- else if (outputFormat === 'csv') {
192
- outputCsv(reportData, iterationTitles);
193
- }
194
- else if (outputFormat === 'json') {
195
- outputJson(reportData);
196
- }
148
+ async function bugCommand(cliOptions = {}) {
149
+ const { projectId, roleIds, config, outputFormat } = (0, config_loader_1.loadConfig)(cliOptions);
150
+ const businessService = new business_service_1.BusinessService(config);
151
+ const iterations = await businessService.getIterations(projectId);
152
+ if (iterations.length === 0) {
153
+ throw new Error('未获取到任何迭代信息');
154
+ }
155
+ const iterationChoices = iterations.map((iteration) => ({
156
+ name: `${iteration.name} (${iteration.begin_time} ~ ${iteration.end_time})`,
157
+ value: iteration,
158
+ checked: false,
159
+ }));
160
+ const selectedIterations = await (0, prompts_1.checkbox)({
161
+ message: '请选择要统计的迭代:',
162
+ choices: iterationChoices,
163
+ validate: (answer) => {
164
+ if (answer.length === 0) {
165
+ return '至少需要选择一个迭代';
166
+ }
167
+ return true;
168
+ },
169
+ theme: inquirer_theme_1.globalTheme,
170
+ });
171
+ const spinner = (0, ora_1.default)('正在查询数据...').start();
172
+ const reportData = await queryBugReportData(businessService, projectId, roleIds, selectedIterations);
173
+ spinner.stop();
174
+ logger_1.logger.info();
175
+ if (outputFormat === 'console') {
176
+ outputConsole(reportData);
177
+ }
178
+ else if (outputFormat === 'csv') {
179
+ outputCsv(reportData.list, projectId);
197
180
  }
198
- catch (error) {
199
- logger_1.logger.error(`执行过程中发生错误: `, error);
200
- process.exit(1);
181
+ else if (outputFormat === 'json') {
182
+ outputJson(reportData.list);
201
183
  }
202
184
  }