@cnbcool/cnb-api-generate 2.0.0 → 2.1.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.
@@ -1,3 +1,4 @@
1
+ import { printJSON } from './print-json';
1
2
  import { summarizeResponse } from './summary-extractors';
2
3
 
3
4
  /**
@@ -49,7 +50,7 @@ export function isStandardResponse(response: any): boolean {
49
50
  export function formatOutput(response: any, verbose: boolean, summary: boolean, toolKey: string): string {
50
51
  // --verbose 模式:输出完整原始信息
51
52
  if (verbose) {
52
- return JSON.stringify(response, null, 2);
53
+ return printJSON(response, verbose);
53
54
  }
54
55
 
55
56
  // converter 可能返回裸 data(没有标准 {status, data} 结构),直接输出
@@ -57,10 +58,10 @@ export function formatOutput(response: any, verbose: boolean, summary: boolean,
57
58
  if (summary) {
58
59
  const summarized = summarizeResponse(response, toolKey);
59
60
  if (summarized !== null) {
60
- return JSON.stringify(summarized, null, 2);
61
+ return printJSON(summarized);
61
62
  }
62
63
  }
63
- return JSON.stringify(response, null, 2);
64
+ return printJSON(response);
64
65
  }
65
66
 
66
67
  // 精简响应
@@ -74,5 +75,5 @@ export function formatOutput(response: any, verbose: boolean, summary: boolean,
74
75
  }
75
76
  }
76
77
 
77
- return JSON.stringify(compact, null, 2);
78
+ return printJSON(compact);
78
79
  }
@@ -111,7 +111,15 @@ export function formatParams(
111
111
  } else if (bodyProps[key]) {
112
112
  // body 字段(无冲突,直接用原 key)
113
113
  if (!formatted.data) formatted.data = {};
114
- formatted.data[key] = Array.isArray(value) ? value : tryParseJSON(value as string);
114
+ if (Array.isArray(value)) {
115
+ formatted.data[key] = value;
116
+ } else if (bodyProps[key].type === 'string') {
117
+ // schema 定义为 string 类型时,保持原始字符串,不做 JSON 解析
118
+ // 避免纯数字字符串(如 "123")被转为 number
119
+ formatted.data[key] = value;
120
+ } else {
121
+ formatted.data[key] = tryParseJSON(value as string);
122
+ }
115
123
  } else {
116
124
  // 处理 d- 前缀(冲突时 CLI 以 d- 前缀传入)
117
125
  const stripped = (key.startsWith('d-') || key.startsWith('d_'))
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 将数据转换为格式化的 JSON 字符串
3
+ * @param data 要序列化的数据(可以是任意类型)缩进)
4
+ * @param verbose 是否 verbose 模式
5
+ * @returns 格式化的 JSON 字符串,如果序列化失败则返回错误信息的 JSON
6
+ */
7
+ export function printJSON(
8
+ data: Record<string, any> | Array<any>,
9
+ verbose: boolean = false
10
+ ): string {
11
+ return verbose ? JSON.stringify(data, null, 2) : JSON.stringify(data);
12
+ }
@@ -3,7 +3,7 @@ import { trimSummary } from './trim-summary';
3
3
  import { helpData } from './help-data';
4
4
  import { flatOptionsData } from './flat-options-data';
5
5
  import { executeAction } from './execute-action';
6
- import { resolveShortcut } from '../shortcuts';
6
+ import { ISSUE_SHORTCUTS, PR_SHORTCUTS, type ShortcutDefinition } from '../shortcuts';
7
7
 
8
8
  interface FlatOption {
9
9
  optKey: string;
@@ -22,19 +22,22 @@ interface FlatOption {
22
22
  * 为单个 tool 子命令注册 Commander options
23
23
  * 直接读取预生成的 flat-options.json 中已扁平化的参数定义,无需运行时计算
24
24
  */
25
- function registerToolOptions(toolCmd: Command, moduleName: string, toolName: string): void {
25
+ function registerToolOptions(toolCmd: Command, moduleName: string, toolName: string, skipPathMandatory?: boolean): void {
26
26
  const moduleOptions = flatOptionsData[moduleName];
27
27
  if (!moduleOptions) return;
28
28
  const toolOptions: Record<string, FlatOption> = moduleOptions[toolName];
29
29
  if (!toolOptions) return;
30
30
 
31
31
  for (const [, opt] of Object.entries(toolOptions) as [string, FlatOption][]) {
32
+ // 快捷命令跳过 path 参数(由环境变量自动注入)
33
+ if (skipPathMandatory && opt.source === 'path') continue;
34
+
32
35
  const option = new Option(
33
36
  `--${opt.optKey} ${opt.valuePlaceholder}`.trim(),
34
37
  opt.description,
35
38
  );
36
39
 
37
- if (opt.required && opt.source === 'path') {
40
+ if (opt.required && opt.source === 'path' && !skipPathMandatory) {
38
41
  option.makeOptionMandatory();
39
42
  }
40
43
 
@@ -61,6 +64,15 @@ function registerToolOptions(toolCmd: Command, moduleName: string, toolName: str
61
64
  }
62
65
  }
63
66
 
67
+ /**
68
+ * 获取指定模块的快捷命令列表
69
+ */
70
+ function getShortcutsForModule(moduleName: string): ShortcutDefinition[] {
71
+ if (moduleName === 'issues') return ISSUE_SHORTCUTS;
72
+ if (moduleName === 'pulls') return PR_SHORTCUTS;
73
+ return [];
74
+ }
75
+
64
76
  /**
65
77
  * 为每个模块注册子命令,使 cnb <module> -h 显示模块帮助
66
78
  * 每个 tool 的参数通过 Commander 的 option 系统注册,--help 自动展示参数说明
@@ -75,6 +87,9 @@ export function registerModuleCommands(program: Command): void {
75
87
  .description(`${moduleName} 模块 (${toolEntries.length} tools)`)
76
88
  .helpOption('-h, --help', '显示帮助文档');
77
89
 
90
+ // 收集已注册的子命令名,避免快捷命令与真实 tool 名冲突
91
+ const registeredNames = new Set<string>();
92
+
78
93
  // 为每个 tool 注册子命令
79
94
  for (const [toolName, toolInfo] of Object.entries(tools) as [string, any][]) {
80
95
  const summary = trimSummary(toolInfo.summary || '');
@@ -95,6 +110,30 @@ export function registerModuleCommands(program: Command): void {
95
110
 
96
111
  // 根据 flat-options.json 注册参数选项
97
112
  registerToolOptions(toolCmd, moduleName, toolName);
113
+ registeredNames.add(toolName);
114
+ }
115
+
116
+ // 为快捷命令注册 Commander 子命令(别名)
117
+ // 复用对应真实 tool 的选项定义,但跳过 path 参数(由环境变量自动注入)
118
+ const shortcuts = getShortcutsForModule(moduleName);
119
+ for (const shortcut of shortcuts) {
120
+ // 如果快捷命令名与真实 tool 名重复,跳过(真实 tool 优先)
121
+ if (registeredNames.has(shortcut.shortName)) continue;
122
+
123
+ const shortcutCmd = sub
124
+ .command(shortcut.shortName)
125
+ .description(`[快捷] ${shortcut.description}`)
126
+ .option('-v, --verbose', '输出完整原始响应')
127
+ .helpOption('-h, --help', '显示帮助文档')
128
+ .showHelpAfterError(true)
129
+ .action(async (opts: Record<string, any>) => {
130
+ // 传入快捷命令名,executeAction 内部会通过 resolveShortcut 转换
131
+ await executeAction(moduleName, shortcut.shortName, opts, sub);
132
+ });
133
+
134
+ // 复用真实 tool 的选项,跳过 path 参数
135
+ registerToolOptions(shortcutCmd, moduleName, shortcut.realTool, true);
136
+ registeredNames.add(shortcut.shortName);
98
137
  }
99
138
 
100
139
  // 允许未匹配到子命令时也能正常执行(不报错)
@@ -102,18 +141,13 @@ export function registerModuleCommands(program: Command): void {
102
141
  sub.allowExcessArguments();
103
142
 
104
143
  // 模块级 action:处理未匹配到子命令的情况
105
- // 如果输入了快捷命令名(如 cnb issues get),Commander 匹配不到真实 tool 子命令,
106
- // 会走到这里,此时尝试 resolveShortcut 将其转换为真实 tool
107
144
  sub
108
145
  .argument('[tool]', '工具名称(支持快捷命令)')
109
- .action(async (toolArg: string | undefined, opts: Record<string, any>) => {
146
+ .action(async (toolArg: string | undefined, _opts: Record<string, any>) => {
110
147
  if (toolArg) {
111
- const shortcut = resolveShortcut(moduleName, toolArg);
112
- if (shortcut) {
113
- // 快捷命令匹配成功,走 executeAction 流程
114
- await executeAction(moduleName, toolArg, opts, sub);
115
- return;
116
- }
148
+ // 此处已不会再命中快捷命令(因为已注册为子命令),仅作兜底
149
+ sub.help();
150
+ return;
117
151
  }
118
152
  sub.help();
119
153
  });
@@ -67,7 +67,7 @@ function getPRNumber(): string {
67
67
  // 快捷命令定义
68
68
  // ============================================================
69
69
 
70
- const ISSUE_SHORTCUTS: ShortcutDefinition[] = [
70
+ export const ISSUE_SHORTCUTS: ShortcutDefinition[] = [
71
71
  { shortName: 'get', realTool: 'get-issue', description: '获取详情' },
72
72
  { shortName: 'list-comments', realTool: 'list-issue-comments', description: '获取评论列表' },
73
73
  { shortName: 'comment', realTool: 'post-issue-comment', description: '评论', dataTip: "--body 内容" },
@@ -81,7 +81,7 @@ const ISSUE_SHORTCUTS: ShortcutDefinition[] = [
81
81
  { shortName: 'upload-image', realTool: 'post-issue-image-asset-upload-url', description: '上传图片', upload: true, dataTip: "--file 图片路径" },
82
82
  ];
83
83
 
84
- const PR_SHORTCUTS: ShortcutDefinition[] = [
84
+ export const PR_SHORTCUTS: ShortcutDefinition[] = [
85
85
  { shortName: 'get', realTool: 'get-pull', description: '获取详情' },
86
86
  { shortName: 'list-files', realTool: 'list-pull-files', description: '获取文件变更' },
87
87
  { shortName: 'list-commits', realTool: 'list-pull-commits', description: '获取提交记录' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cnbcool/cnb-api-generate",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "main": "./built/index.js",
5
5
  "module": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -12,30 +12,29 @@ description: CNB OpenAPI 交互能力,支持仓库、Issue、PR、流水线、
12
12
  issues 快捷命令:
13
13
  - `<$CNB_CLI_CMD$> issues get` — 获取 Issue 详情
14
14
  - `<$CNB_CLI_CMD$> issues list-comments` — 列出 Issue 评论
15
- - `<$CNB_CLI_CMD$> issues comment --data '{"body":"内容"}'` — 发表 Issue 评论
15
+ - `<$CNB_CLI_CMD$> issues comment --body 内容` — 发表 Issue 评论
16
16
  - `<$CNB_CLI_CMD$> issues close` — 关闭 Issue
17
17
  - `<$CNB_CLI_CMD$> issues open` — 打开 Issue
18
18
  - `<$CNB_CLI_CMD$> issues list-labels` — 列出 Issue 标签
19
- - `<$CNB_CLI_CMD$> issues add-labels --data '{"labels":["标签"]}'` — 添加 Issue 标签
19
+ - `<$CNB_CLI_CMD$> issues add-labels --labels bug --labels feature` — 添加 Issue 标签
20
20
  - `<$CNB_CLI_CMD$> issues list-assignees` — 查看 Issue 处理人
21
- - `<$CNB_CLI_CMD$> issues add-assignees --data '{"assignees":["用户名"]}'` — 添加 Issue 处理人
22
- - `<$CNB_CLI_CMD$> issues upload-file --data '{"file":"本地文件路径"}'` — 上传文件到 Issue
23
- - `<$CNB_CLI_CMD$> issues upload-image --data '{"file":"本地图片路径"}'` — 上传图片到 Issue
21
+ - `<$CNB_CLI_CMD$> issues add-assignees --assignees username` — 添加 Issue 处理人
22
+ - `<$CNB_CLI_CMD$> issues upload-file --file 文件路径` — 上传文件到 Issue
23
+ - `<$CNB_CLI_CMD$> issues upload-image --file 图片路径` — 上传图片到 Issue
24
24
 
25
25
  pulls 快捷命令:
26
26
  - `<$CNB_CLI_CMD$> pulls get` — 获取 PR 详情
27
27
  - `<$CNB_CLI_CMD$> pulls list-files` — 列出 PR 变更文件
28
28
  - `<$CNB_CLI_CMD$> pulls list-commits` — 列出 PR 提交记录
29
29
  - `<$CNB_CLI_CMD$> pulls list-comments` — 列出 PR 评论
30
- - `<$CNB_CLI_CMD$> pulls comment --data '{"body":"内容"}'` — 发表 PR 评论
30
+ - `<$CNB_CLI_CMD$> pulls comment --body 内容` — 发表 PR 评论
31
31
  - `<$CNB_CLI_CMD$> pulls list-labels` — 列出 PR 标签
32
- - `<$CNB_CLI_CMD$> pulls add-labels --data '{"labels":["标签"]}'` — 添加 PR 标签
32
+ - `<$CNB_CLI_CMD$> pulls add-labels --labels ready --labels approved` — 添加 PR 标签
33
33
  - `<$CNB_CLI_CMD$> pulls check-status` — 查看 CI 状态
34
34
  - `<$CNB_CLI_CMD$> pulls list-reviews` — 查看评审列表
35
35
  - `<$CNB_CLI_CMD$> pulls list-assignees` — 查看 PR 处理人
36
- - `<$CNB_CLI_CMD$> pulls add-assignees --data '{"assignees":["用户名"]}'`添加 PR 处理人
37
- - `<$CNB_CLI_CMD$> pulls upload-file --data '{"file":"本地文件路径"}'`上传文件到 PR
38
- - `<$CNB_CLI_CMD$> pulls upload-image --data '{"file":"本地图片路径"}'` — 上传图片到 PR
36
+ - `<$CNB_CLI_CMD$> pulls upload-file --file 文件路径`上传文件到 PR
37
+ - `<$CNB_CLI_CMD$> pulls upload-image --file 图片路径` 上传图片到 PR
39
38
 
40
39
  注意:
41
40
  - **路径参数自动注入**:仓库 slug、Issue/PR 编号优先从环境变量自动获取,无需额外传入任何其他参数