@hecom/codearts 0.2.1 → 0.2.3

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
@@ -24,14 +24,24 @@ npx @hecom/codearts daily
24
24
  # 生成当年工时统计
25
25
  npx @hecom/codearts work-hour
26
26
 
27
+ # 按迭代统计产品缺陷率
28
+ npx @hecom/codearts bug-rate "迭代1,迭代2"
29
+ ```
30
+
31
+ ### 3. 更新配置
32
+
33
+ ```bash
27
34
  # 更新全局配置
28
35
  npx @hecom/codearts config
29
36
 
30
37
  # 单独更新角色配置
31
38
  npx @hecom/codearts config role-id
39
+
40
+ # 查看当前配置
41
+ npx @hecom/codearts config show
32
42
  ```
33
43
 
34
- ### 3. 升级
44
+ ### 4. 升级
35
45
 
36
46
  ```bash
37
47
  # 更新最新版本
@@ -56,13 +66,15 @@ npm install
56
66
 
57
67
  ### 本地运行
58
68
 
69
+ > 本地运行命令时,注意使用 `--` 分隔 npm 参数和 CLI 参数,否则 CLI 参数可能无法正确传递。
70
+
59
71
  ```bash
60
72
  # 运行命令
61
73
  npm run dev
62
74
 
63
- npm run dev daily
75
+ npm run dev -- daily
64
76
 
65
- npm run dev work-hour
77
+ npm run dev -- work-hour
66
78
  ```
67
79
 
68
80
  ### 本地链接 CLI 工具
@@ -79,10 +91,10 @@ codearts --help
79
91
  | --------------------------- | ---------------------------------- | ---- |
80
92
  | `HUAWEI_CLOUD_IAM_ENDPOINT` | IAM 认证端点 | 是 |
81
93
  | `HUAWEI_CLOUD_REGION` | 华为云区域 | 是 |
94
+ | `CODEARTS_BASE_URL` | CodeArts API 地址 | 是 |
95
+ | `HUAWEI_CLOUD_DOMAIN` | 华为云账号名 | 是 |
82
96
  | `HUAWEI_CLOUD_USERNAME` | IAM 用户名 | 是 |
83
97
  | `HUAWEI_CLOUD_PASSWORD` | IAM 密码 | 是 |
84
- | `HUAWEI_CLOUD_DOMAIN` | 华为云账号名 | 是 |
85
- | `CODEARTS_BASE_URL` | CodeArts API 地址 | 是 |
86
98
  | `PROJECT_ID` | 项目 ID | 是 |
87
99
  | `ROLE_ID` | 角色 ID(支持逗号分隔,如: 1,2,3) | 是 |
88
100
 
package/dist/bin/cli.js CHANGED
@@ -41,7 +41,9 @@ const bug_command_1 = require("../commands/bug.command");
41
41
  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
+ const constant_1 = require("../constant");
44
45
  const config_loader_1 = require("../utils/config-loader");
46
+ const logger_1 = require("../utils/logger");
45
47
  // 读取 package.json 中的版本号
46
48
  const packageJsonPath = path.join(__dirname, '../../package.json');
47
49
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
@@ -49,12 +51,15 @@ const version = packageJson.version;
49
51
  const program = new commander_1.Command();
50
52
  program.name('codearts').description('华为云 CodeArts 统计分析工具').version(version);
51
53
  // 全局选项(环境变量覆盖)
52
- program.option('--role-id <ids>', '角色 ID(支持逗号分隔,如: 1,2,3),优先级高于环境变量 ROLE_ID');
54
+ program
55
+ .option('--role-id <ids>', '角色 ID(支持逗号分隔,如: 1,2)')
56
+ .option('--output <format>', '输出格式:console、csv、json', 'console');
53
57
  // config 命令 - 交互式配置向导
54
58
  const configCmd = program
55
59
  .command('config')
56
- .description('交互式配置向导,引导用户创建或更新全局配置文件\n\n')
60
+ .description('交互式配置向导,引导用户创建或更新全局配置文件')
57
61
  .action(async () => {
62
+ (0, constant_1.showLogo)();
58
63
  await (0, config_command_1.configCommand)();
59
64
  });
60
65
  // config show 子命令 - 显示当前配置
@@ -62,6 +67,7 @@ configCmd
62
67
  .command('show')
63
68
  .description('显示当前配置信息')
64
69
  .action(async () => {
70
+ (0, constant_1.showLogo)();
65
71
  await (0, config_command_1.showConfigCommand)();
66
72
  });
67
73
  // 为每个项目配置项添加子命令
@@ -72,43 +78,49 @@ availableConfigs.forEach((configItem) => {
72
78
  .command(subCommandName)
73
79
  .description(`更新${configItem.label}`)
74
80
  .action(async () => {
81
+ (0, constant_1.showLogo)();
75
82
  await (0, config_command_1.updateProjectConfigCommand)(configItem.key);
76
83
  });
77
84
  });
78
85
  // daily 命令
79
86
  program
80
87
  .command('daily [date]')
81
- .description('生成日报统计')
82
- .action(async (date) => {
83
- const opts = program.opts();
84
- await (0, daily_command_1.dailyCommand)(date, opts);
88
+ .description('每日工时统计(默认日期为当天)')
89
+ .option('-r, --report', '显示总结报告', false)
90
+ .action(async (date, options, command) => {
91
+ const cliOptions = { ...command.parent.opts(), report: options.report };
92
+ logger_1.logger.setOutputFormat(cliOptions.output);
93
+ await (0, daily_command_1.dailyCommand)(date, cliOptions);
85
94
  });
86
95
  // work-hour 命令
87
96
  program
88
97
  .command('work-hour [year]')
89
- .description('生成年度工时统计')
90
- .action(async (year) => {
91
- const opts = program.opts();
92
- await (0, work_hour_command_1.workHourCommand)(year, opts);
98
+ .description('年度工时统计(默认当前年份)')
99
+ .action(async (year, options, command) => {
100
+ const cliOptions = command.parent.opts();
101
+ logger_1.logger.setOutputFormat(cliOptions.output);
102
+ await (0, work_hour_command_1.workHourCommand)(year, cliOptions);
93
103
  });
94
104
  // bug-rate 命令
95
105
  program
96
106
  .command('bug-rate <iterations>')
97
- .description('按迭代统计产品缺陷率,支持多个迭代(逗号分隔)')
98
- .action(async (iterations) => {
99
- const opts = program.opts();
100
- await (0, bug_command_1.bugCommand)(iterations, opts);
107
+ .description('产品缺陷率统计,支持多个迭代')
108
+ .action(async (iterations, options, command) => {
109
+ const cliOptions = command.parent.opts();
110
+ logger_1.logger.setOutputFormat(cliOptions.output);
111
+ await (0, bug_command_1.bugCommand)(iterations, cliOptions);
101
112
  });
102
113
  // 检查配置并自动执行 config 命令
103
114
  async function checkConfigAndRun() {
104
115
  const args = process.argv.slice(2);
105
116
  // 如果没有参数(直接执行 codearts),检测配置
106
117
  if (args.length === 0) {
118
+ (0, constant_1.showLogo)();
107
119
  // 检查是否有全局配置
108
- const hasGlobalConfig = (0, config_loader_1.globalConfigExists)();
109
- if (!hasGlobalConfig) {
120
+ const hasConfig = (0, config_loader_1.configExists)();
121
+ if (!hasConfig) {
110
122
  // 没有配置,自动执行 config 命令
111
- console.log('未检测到配置文件,启动配置向导...\n');
123
+ logger_1.logger.info('未检测到配置文件,启动配置向导...\n');
112
124
  await (0, config_command_1.configCommand)();
113
125
  return;
114
126
  }
@@ -118,7 +130,17 @@ async function checkConfigAndRun() {
118
130
  // 有参数,正常解析命令
119
131
  program.parse();
120
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
+ });
121
143
  checkConfigAndRun().catch((error) => {
122
- console.error('执行失败:', error);
144
+ logger_1.logger.error('执行失败: ', error);
123
145
  process.exit(1);
124
146
  });
@@ -1,7 +1,5 @@
1
1
  import { CliOptions } from '../utils/config-loader';
2
2
  /**
3
3
  * bug 命令入口
4
- * @param iterationTitlesStr 迭代标题(支持逗号、分号、空格、竖线、顿号等分隔符)
5
- * @param cliOptions CLI 选项
6
4
  */
7
5
  export declare function bugCommand(iterationTitlesStr: string, cliOptions?: CliOptions): Promise<void>;
@@ -4,13 +4,12 @@ exports.bugCommand = bugCommand;
4
4
  const business_service_1 = require("../services/business.service");
5
5
  const types_1 = require("../types");
6
6
  const config_loader_1 = require("../utils/config-loader");
7
+ const csv_writer_1 = require("../utils/csv-writer");
8
+ const logger_1 = require("../utils/logger");
7
9
  /**
8
10
  * 判断 Bug 是否属于需求变更或产品设计问题
9
- * @param bug Bug 工作项
10
- * @returns 是否属于需求变更或产品设计问题
11
11
  */
12
12
  function isRequirementOrDesignBug(bug) {
13
- // 检查 customValueNew(v2 版本的自定义字段,是对象格式)
14
13
  const defectAnalysisValue = bug.customValueNew?.custom_field32;
15
14
  if (defectAnalysisValue) {
16
15
  return (defectAnalysisValue === types_1.DefectAnalysisType.REQUIREMENT_CHANGE ||
@@ -19,32 +18,35 @@ function isRequirementOrDesignBug(bug) {
19
18
  return false;
20
19
  }
21
20
  /**
22
- * 生成 Bug 统计报告
21
+ * 查询 Bug 统计数据
23
22
  */
24
- async function generateBugReport(businessService, projectId, roleIds, iterationTitles) {
25
- console.log(`\n正在统计迭代中的 Bug 数据...`);
26
- console.log(`目标迭代: ${iterationTitles.join(', ')}`);
27
- console.log(`角色过滤: ${roleIds.length > 0 ? roleIds.join(', ') : '无(保留所有人)'}`);
28
- console.log(`统计规则: 统计所有 Bug,同时标记"需求变更问题"和"产品设计问题"`);
29
- // Step 1: 获取指定角色的成员列表(如果启用了角色过滤)
23
+ async function queryBugReportData(businessService, projectId, roleIds, iterationTitles) {
24
+ const roleNames = new Set();
30
25
  const targetMemberIds = new Set();
31
26
  if (roleIds.length > 0) {
32
27
  for (const roleId of roleIds) {
33
28
  const members = await businessService.getMembersByRoleId(projectId, roleId);
34
- members.forEach((member) => targetMemberIds.add(member.user_num_id));
29
+ members.forEach((member) => {
30
+ targetMemberIds.add(member.user_num_id);
31
+ roleNames.add(member.role_name);
32
+ });
35
33
  }
36
- console.log(`\n目标角色成员数: ${targetMemberIds.size} 人`);
37
- }
38
- else {
39
- console.log(`\n角色过滤: 已禁用,将统计所有人`);
40
34
  }
41
- // Step 2: 获取所有 Story
42
35
  const stories = await businessService.getStoriesByIterationTitles(projectId, iterationTitles);
43
36
  if (stories.length === 0) {
44
- console.log('未找到任何 Story');
45
- return;
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
+ };
46
49
  }
47
- // Step 3: 过滤出目标角色成员处理的 Story(如果启用了角色过滤)
48
50
  const filteredStories = roleIds.length > 0
49
51
  ? stories.filter((story) => {
50
52
  const assignedUserId = story.assigned_user?.id;
@@ -52,27 +54,27 @@ async function generateBugReport(businessService, projectId, roleIds, iterationT
52
54
  })
53
55
  : stories;
54
56
  if (filteredStories.length === 0) {
55
- console.log(roleIds.length > 0 ? '未找到目标角色成员处理的 Story' : '未找到任何 Story');
56
- return;
57
- }
58
- if (roleIds.length > 0) {
59
- console.log(`\n找到 ${stories.length} 个 Story,其中 ${filteredStories.length} 个由目标角色成员处理,正在统计 Bug...`);
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
+ };
60
69
  }
61
- else {
62
- console.log(`\n找到 ${filteredStories.length} 个 Story,正在统计 Bug...`);
63
- }
64
- // Step 4: 获取每个 Story 的子工作项(Bug)
65
70
  const bugStatsMap = new Map();
66
71
  for (const story of filteredStories) {
67
72
  const childIssues = await businessService.getChildIssues(projectId, String(story.id));
68
- // 过滤出所有 Bug 类型的子工作项(tracker_id = 3)
69
73
  const allBugs = childIssues.filter((issue) => issue.tracker.id === 3);
70
74
  if (allBugs.length === 0) {
71
75
  continue;
72
76
  }
73
- // 统计产品问题 Bug(需求变更 + 产品设计)
74
77
  const productBugs = allBugs.filter((bug) => isRequirementOrDesignBug(bug));
75
- // Step 3: 按 Story 的处理人统计 Bug
76
78
  const assignedUserId = story.assigned_user?.id;
77
79
  const assignedUserName = story.assigned_user?.nick_name;
78
80
  if (!assignedUserId) {
@@ -98,61 +100,103 @@ async function generateBugReport(businessService, projectId, roleIds, iterationT
98
100
  productBugCount: productBugs.length,
99
101
  });
100
102
  }
101
- // 计算每个人的产品缺陷率
102
103
  bugStatsMap.forEach((stats) => {
103
104
  stats.productDefectRate =
104
- stats.totalBugCount > 0 ? (stats.productBugCount / stats.totalBugCount) * 100 : 0;
105
+ stats.totalBugCount > 0
106
+ ? Math.round((stats.productBugCount / stats.totalBugCount) * 100 * 100) / 100
107
+ : 0;
105
108
  });
106
- // Step 4: 输出统计结果
107
- console.log('\n');
108
- console.log('='.repeat(80));
109
- console.log(`Bug 统计报告 [${iterationTitles.join(', ')}]`);
110
- console.log('='.repeat(80));
111
- console.log('');
112
- if (bugStatsMap.size === 0) {
113
- console.log('未找到任何 Bug');
114
- return;
115
- }
116
- // 按总 Bug 数量降序排列
117
109
  const sortedStats = Array.from(bugStatsMap.values()).sort((a, b) => b.totalBugCount - a.totalBugCount);
118
110
  let totalBugs = 0;
119
111
  let totalProductBugs = 0;
120
112
  sortedStats.forEach((stats) => {
121
- console.log(`\x1b[31m${stats.assignedUser}: 总 Bug ${stats.totalBugCount} 个 | 产品问题 ${stats.productBugCount} 个 | 产品缺陷率 ${stats.productDefectRate.toFixed(1)}%\x1b[0m`);
122
- // 输出该用户的 Story 及其 Bug 数量
123
- stats.stories.forEach((story) => {
124
- console.log(` - ${story.storyName} (总 ${story.totalBugCount} 个 Bug,其中产品问题 ${story.productBugCount} 个)`);
125
- });
126
113
  totalBugs += stats.totalBugCount;
127
114
  totalProductBugs += stats.productBugCount;
128
- console.log('');
129
115
  });
130
- const overallProductDefectRate = totalBugs > 0 ? (totalProductBugs / totalBugs) * 100 : 0;
131
- console.log('='.repeat(80));
132
- console.log(`总计: ${totalBugs} 个 Bug(产品问题 ${totalProductBugs} 个,产品缺陷率 ${overallProductDefectRate.toFixed(1)}%),涉及 ${sortedStats.length} 位处理人`);
133
- console.log('='.repeat(80));
116
+ const overallProductDefectRate = totalBugs > 0 ? Math.round((totalProductBugs / totalBugs) * 100 * 100) / 100 : 0;
117
+ return {
118
+ iterations: iterationTitles,
119
+ roleIds,
120
+ roleNames: Array.from(roleNames),
121
+ userStats: sortedStats,
122
+ summary: {
123
+ totalBugs,
124
+ totalProductBugs,
125
+ overallProductDefectRate,
126
+ userCount: sortedStats.length,
127
+ },
128
+ };
129
+ }
130
+ /**
131
+ * 控制台输出产品缺陷率统计
132
+ */
133
+ 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`);
144
+ stats.stories.forEach((story) => {
145
+ logger_1.logger.info(` ${story.storyName} (${story.productBugCount}/${story.totalBugCount})`);
146
+ });
147
+ logger_1.logger.info('');
148
+ });
149
+ }
150
+ /**
151
+ * CSV 文件输出
152
+ */
153
+ function outputCsv(data, iterationTitles) {
154
+ const csvLines = [];
155
+ csvLines.push('迭代,处理人,Story名称,StoryID,总Bug数,产品问题Bug数,产品缺陷率');
156
+ data.userStats.forEach((userStat) => {
157
+ userStat.stories.forEach((story) => {
158
+ const defectRate = story.totalBugCount > 0
159
+ ? ((story.productBugCount / story.totalBugCount) * 100).toFixed(2)
160
+ : '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}%`);
162
+ });
163
+ });
164
+ const filename = `bug-rate-${iterationTitles.join('-')}.csv`;
165
+ (0, csv_writer_1.writeCsvFile)(filename, csvLines, logger_1.logger);
166
+ }
167
+ /**
168
+ * JSON 输出(直接打印到控制台,供编程调用)
169
+ */
170
+ function outputJson(data) {
171
+ logger_1.logger.json(data);
134
172
  }
135
173
  /**
136
174
  * bug 命令入口
137
- * @param iterationTitlesStr 迭代标题(支持逗号、分号、空格、竖线、顿号等分隔符)
138
- * @param cliOptions CLI 选项
139
175
  */
140
176
  async function bugCommand(iterationTitlesStr, cliOptions = {}) {
141
177
  try {
142
178
  if (!iterationTitlesStr || iterationTitlesStr.trim() === '') {
143
179
  throw new Error('请指定至少一个迭代标题');
144
180
  }
145
- // 解析迭代标题(支持多种分隔符:逗号、分号、空格、竖线、顿号等)
146
181
  const iterationTitles = iterationTitlesStr
147
182
  .split(/[,,;;|\s、]+/)
148
183
  .map((title) => title.trim())
149
184
  .filter((title) => title.length > 0);
150
- const { projectId, roleIds, config } = (0, config_loader_1.loadConfig)(cliOptions);
185
+ const { projectId, roleIds, config, outputFormat } = (0, config_loader_1.loadConfig)(cliOptions);
151
186
  const businessService = new business_service_1.BusinessService(config);
152
- await generateBugReport(businessService, projectId, roleIds, iterationTitles);
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
+ }
153
197
  }
154
198
  catch (error) {
155
- console.error('执行过程中发生错误:', error);
199
+ logger_1.logger.error(`执行过程中发生错误: `, error);
156
200
  process.exit(1);
157
201
  }
158
202
  }