@cnbcool/cnb-api-generate 2.17.1 → 2.17.2
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/client/lib/execute-action.ts +12 -19
- package/client/lib/format-params.ts +10 -8
- package/client/lib/parsers.ts +23 -0
- package/client/lib/register-modules.ts +74 -18
- package/client/shortcuts.ts +31 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { showShort, resolveShortcut } from '../shortcuts';
|
|
2
|
+
import { showShort, resolveShortcut, mergeShortcutParams } from '../shortcuts';
|
|
3
3
|
import { handleUpload, handleIssueCreateUpload, type UploadPathParams } from '../utils/upload';
|
|
4
4
|
import { diagnoseBuild } from './build-diagnostics';
|
|
5
5
|
import { analyzeBuildTiming } from './build-timing';
|
|
@@ -76,31 +76,24 @@ export async function executeAction(
|
|
|
76
76
|
if (shortcut) {
|
|
77
77
|
params.module = shortcut.module;
|
|
78
78
|
params.tool = shortcut.tool;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!params[key]) {
|
|
82
|
-
params[key] = value;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (shortcut.autoData) {
|
|
87
|
-
for (const [key, value] of Object.entries(shortcut.autoData)) {
|
|
88
|
-
if (params[key] === undefined) {
|
|
89
|
-
params[key] = value;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
79
|
+
// 用户显式传入的 --repo / --number 优先,未传时用环境变量自动注入的 autoPath 兜底
|
|
80
|
+
mergeShortcutParams(params, shortcut);
|
|
93
81
|
}
|
|
94
82
|
|
|
95
83
|
if (!params.tool) {
|
|
96
84
|
parentCmd.help();
|
|
97
85
|
}
|
|
98
86
|
|
|
99
|
-
// 上传命令走独立分支:只需 --file +
|
|
87
|
+
// 上传命令走独立分支:只需 --file + path 参数,跳过 formatParams 常规流程。
|
|
88
|
+
// params 中已合并好 autoPath:用户显式传入的 --repo / --number 优先,
|
|
89
|
+
// 未传时由环境变量兜底,因此这里直接取 params 即可。
|
|
100
90
|
if (shortcut?.upload) {
|
|
101
91
|
const isVerbose = !!opts.verbose;
|
|
102
92
|
const toolFunction = loadToolFunction(shortcut.module, shortcut.tool, parentCmd);
|
|
103
|
-
const pathParams =
|
|
93
|
+
const pathParams: UploadPathParams = {
|
|
94
|
+
repo: (params.repo as string) || shortcut.autoPath.repo || '',
|
|
95
|
+
number: (params.number as string) || shortcut.autoPath.number,
|
|
96
|
+
};
|
|
104
97
|
let data: any;
|
|
105
98
|
|
|
106
99
|
if (shortcut.tool === 'post-asset-group') {
|
|
@@ -116,7 +109,7 @@ export async function executeAction(
|
|
|
116
109
|
|
|
117
110
|
if (shortcut?.tool === '__get-ci-logs__') {
|
|
118
111
|
const result = await diagnoseBuild({
|
|
119
|
-
repo:
|
|
112
|
+
repo: (params.repo as string) || '',
|
|
120
113
|
sn: (opts.sn as string) || undefined,
|
|
121
114
|
prNumber: process.env.CNB_PULL_REQUEST_IID || undefined,
|
|
122
115
|
});
|
|
@@ -126,7 +119,7 @@ export async function executeAction(
|
|
|
126
119
|
|
|
127
120
|
if (shortcut?.tool === '__get-ci-timing__') {
|
|
128
121
|
const result = await analyzeBuildTiming({
|
|
129
|
-
repo:
|
|
122
|
+
repo: (params.repo as string) || '',
|
|
130
123
|
sn: (opts.sn as string) || undefined,
|
|
131
124
|
prNumber: process.env.CNB_PULL_REQUEST_IID || undefined,
|
|
132
125
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { removeSlashes } from 'slashes';
|
|
2
1
|
import { helpData } from './help-data';
|
|
3
|
-
import { tryParseJSON, readFileContent } from './parsers';
|
|
2
|
+
import { tryParseJSON, readFileContent, unescapeString } from './parsers';
|
|
4
3
|
import { buildNestedFieldMap } from '../utils/build-nested-field-map';
|
|
5
4
|
import type { NestedFieldMapping } from '../utils/build-nested-field-map';
|
|
6
5
|
import { restoreOriginalKeys } from '../utils/restore-original-keys';
|
|
@@ -25,8 +24,11 @@ export function getToolParamDefs(moduleName: string, toolName: string) {
|
|
|
25
24
|
* - 其他字段:走 tryParseJSON 以兼容嵌套 JSON
|
|
26
25
|
*
|
|
27
26
|
* 说明:shell 在单/双引号下不解释 `\n`,外部程序(包括本 CLI 经 commander)
|
|
28
|
-
* 收到的是字面的反斜杠 + n。这里用
|
|
27
|
+
* 收到的是字面的反斜杠 + n。这里用 unescapeString 做一次反解析,
|
|
29
28
|
* 让 `--body "a\n b"` 与 `echo -e` 体验一致,正确还原为真实换行/制表符等。
|
|
29
|
+
*
|
|
30
|
+
* 不能用 slashes.removeSlashes:其「无损伤规则」会吞掉不认识的反斜杠,
|
|
31
|
+
* 把 `C:\path` 变成 `C:path`、`\s` 变成 `s`,静默损坏路径与正则片段。
|
|
30
32
|
*/
|
|
31
33
|
/**
|
|
32
34
|
* 过滤非法字符(如 \u0000 NUL),避免破坏环境变量和 HTTP 传输。
|
|
@@ -61,7 +63,7 @@ function assignBodyValue(
|
|
|
61
63
|
if (Array.isArray(value)) {
|
|
62
64
|
target[key] = value;
|
|
63
65
|
} else if (propType === 'string') {
|
|
64
|
-
target[key] = typeof value === 'string' ? filterIllegalChars(
|
|
66
|
+
target[key] = typeof value === 'string' ? filterIllegalChars(unescapeString(value)) : value;
|
|
65
67
|
} else {
|
|
66
68
|
target[key] = tryParseJSON(value as string);
|
|
67
69
|
}
|
|
@@ -121,10 +123,10 @@ function assignMapField(
|
|
|
121
123
|
if (rawV === 'true') v = true;
|
|
122
124
|
else if (rawV === 'false') v = false;
|
|
123
125
|
} else {
|
|
124
|
-
// 与 assignBodyValue
|
|
125
|
-
// 原因:环境变量常见值(URL / Windows 路径 / JWT
|
|
126
|
-
//
|
|
127
|
-
//
|
|
126
|
+
// 与 assignBodyValue 的 string 字段不同:map value 不做转义反解。
|
|
127
|
+
// 原因:环境变量常见值(URL / Windows 路径 / JWT 等)中的反斜杠一旦被
|
|
128
|
+
// 误吞(如 "C:\path" → "C:path")就会损坏数据;且 map 值通常是原始串,
|
|
129
|
+
// 用户没有「像 --body 那样写 \n」的预期。这里仅过滤非法 NUL 字符。
|
|
128
130
|
v = filterIllegalChars(rawV);
|
|
129
131
|
}
|
|
130
132
|
map[k] = v;
|
package/client/lib/parsers.ts
CHANGED
|
@@ -50,6 +50,29 @@ export function readFileContent(filePath: string): string {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 解析字符串中的转义序列
|
|
55
|
+
* 支持:\n \t \r \\ \a \f \v \e
|
|
56
|
+
* 不支持(避免破坏代码片段):\xHH \uHHHH \UHHHHHHHH \0NNN \c
|
|
57
|
+
* 不认识的转义序列原样保留(如 \s \d \w 等正则元字符)
|
|
58
|
+
* @param str 要解析的字符串
|
|
59
|
+
* @returns 解析后的字符串
|
|
60
|
+
*/
|
|
61
|
+
export function unescapeString(str: unknown): any {
|
|
62
|
+
if (typeof str !== 'string') return str;
|
|
63
|
+
const map: Record<string, string> = {
|
|
64
|
+
n: '\n',
|
|
65
|
+
t: '\t',
|
|
66
|
+
r: '\r',
|
|
67
|
+
'\\': '\\',
|
|
68
|
+
a: '\x07',
|
|
69
|
+
f: '\f',
|
|
70
|
+
v: '\v',
|
|
71
|
+
e: '\x1B',
|
|
72
|
+
};
|
|
73
|
+
return str.replace(/\\(n|t|r|\\|a|f|v|e)/g, (_: string, seq: string) => map[seq]);
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
/**
|
|
54
77
|
* 解析 commander 未识别的未知选项
|
|
55
78
|
* commander 开启 allowUnknownOption 后,未知选项会残留在 process.argv 中,
|
|
@@ -30,6 +30,19 @@ interface FlatOption {
|
|
|
30
30
|
const AUTO_INJECTED_PATH_KEYS = new Set(['repo', 'number', 'review_id']);
|
|
31
31
|
const AUTO_INJECTED_DISPLAY_KEYS = new Set(['repo', 'number', 'review-id']);
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 为不走 registerToolOptions 的快捷命令(upload / CI 自定义命令)注册
|
|
35
|
+
* 由环境变量自动注入的 path 参数,使其可被显式覆盖。
|
|
36
|
+
* - repo:始终注册,未传时由 autoPath 从 CNB_REPO_SLUG 回填
|
|
37
|
+
* - number:仅在命令所属接口真的需要 number 时注册,避免帮助里出现「有选项但静默无效」
|
|
38
|
+
*/
|
|
39
|
+
function registerAutoInjectedPathOptions(cmd: Command, needNumber?: boolean): void {
|
|
40
|
+
cmd.option('--repo <string>', '仓库名称(可选,默认取 CNB_REPO_SLUG)');
|
|
41
|
+
if (needNumber) {
|
|
42
|
+
cmd.option('--number <string>', '编号(可选,默认取 CNB_ISSUE_IID / CNB_PULL_REQUEST_IID)');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
/**
|
|
34
47
|
* 为单个 tool 子命令注册 Commander options
|
|
35
48
|
* 直接读取预生成的 flat-options.json 中已扁平化的参数定义,无需运行时计算
|
|
@@ -41,23 +54,28 @@ function registerToolOptions(toolCmd: Command, moduleName: string, toolName: str
|
|
|
41
54
|
if (!toolOptions) return;
|
|
42
55
|
|
|
43
56
|
for (const [, opt] of Object.entries(toolOptions) as [string, FlatOption][]) {
|
|
44
|
-
//
|
|
45
|
-
// (repo / number / review_id
|
|
46
|
-
//
|
|
47
|
-
if (
|
|
48
|
-
|
|
57
|
+
// 快捷命令下,"由 shortcut.autoPath 从环境变量自动注入"的 path 参数
|
|
58
|
+
// (repo / number / review_id)改为注册为【可选】参数,而非直接跳过。
|
|
59
|
+
// 这样既保留环境变量自动注入的省流默认行为(executeAction 中
|
|
60
|
+
// `if (!params[key])` 仅在用户未显式传值时回填 autoPath),
|
|
61
|
+
// 又允许用户显式覆盖,例如 `cnb pulls get --repo x/y --number 97`
|
|
62
|
+
// 或跨 Issue/PR 场景下查询其它编号。
|
|
63
|
+
// 其余 path 参数(如 img_path、file_path)仍需显式传入,保留 required 标记。
|
|
64
|
+
const isAutoInjectedPath = skipPathMandatory
|
|
49
65
|
&& opt.source === 'path'
|
|
50
|
-
&& (AUTO_INJECTED_PATH_KEYS.has(opt.optKey) || AUTO_INJECTED_DISPLAY_KEYS.has(opt.optKey))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
&& (AUTO_INJECTED_PATH_KEYS.has(opt.optKey) || AUTO_INJECTED_DISPLAY_KEYS.has(opt.optKey));
|
|
67
|
+
|
|
68
|
+
// 自动注入参数在快捷命令下是「可选、可覆盖」的,去掉源描述里与语义冲突的 [必填] 前缀
|
|
69
|
+
const autoInjectedDesc = isAutoInjectedPath
|
|
70
|
+
? `${opt.description.replace(/^\[必填\]\s*/, '')}(可选,默认取当前 Issue/PR 环境变量)`
|
|
71
|
+
: opt.description;
|
|
54
72
|
|
|
55
73
|
const option = new Option(
|
|
56
74
|
`--${opt.optKey} ${opt.valuePlaceholder}`.trim(),
|
|
57
|
-
|
|
75
|
+
autoInjectedDesc,
|
|
58
76
|
);
|
|
59
77
|
|
|
60
|
-
if (opt.required && opt.source === 'path') {
|
|
78
|
+
if (opt.required && opt.source === 'path' && !isAutoInjectedPath) {
|
|
61
79
|
option.makeOptionMandatory();
|
|
62
80
|
}
|
|
63
81
|
|
|
@@ -98,6 +116,40 @@ function getShortcutsForModule(moduleName: string): ShortcutDefinition[] {
|
|
|
98
116
|
return [];
|
|
99
117
|
}
|
|
100
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 模块下未匹配到任何子命令时的兜底处理。
|
|
121
|
+
*
|
|
122
|
+
* Commander 允许未知位置参数落到兜底 action,静默打印帮助并 return 会让调用方
|
|
123
|
+
* 拿到 exit 0 —— 对自动化(含 AI Agent)是「失败伪装成成功」:拼错一个字母
|
|
124
|
+
* (如 submit-reviewx)不会发出任何请求,却与成功调用一样返回 0,
|
|
125
|
+
* Agent 据此认为命令已生效。
|
|
126
|
+
*
|
|
127
|
+
* 因此这里向 stderr 输出双语报错并置非零退出码,让调用方能靠退出码判失败。
|
|
128
|
+
* 不传位置参数(仅 `cnb pulls`)时仍打印帮助并返回 0;
|
|
129
|
+
* 显式 `--help` / `-h` 由 Commander 内建路径处理,不会走到这里。
|
|
130
|
+
*
|
|
131
|
+
* 文案沿用本仓库既有惯例(见 execute-action.ts 的 buildToolNotFoundMessage):
|
|
132
|
+
* 中文行 `错误:` 开头、英文行 `Error:` 开头,两条平行,且都给出模块级与全局帮助入口。
|
|
133
|
+
*
|
|
134
|
+
* 区分「选项」与「命令」是因为 sub 上开了 allowUnknownOption():
|
|
135
|
+
* 所有未识别的 token 都会塞进 [tool],模块级选项(如 `cnb issues --repo foo/bar`、
|
|
136
|
+
* `cnb pulls --json`)同样落到这里。这类输入的真实错因是漏写子命令或选错了选项,
|
|
137
|
+
* 报成「未知命令」会把选项名当命令名,排障方向也带偏。
|
|
138
|
+
*/
|
|
139
|
+
function guardUnknownTool(moduleName: string, sub: Command, toolArg: string | undefined): void {
|
|
140
|
+
if (!toolArg) {
|
|
141
|
+
sub.help();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const kind = toolArg.startsWith('-') ? '选项' : '命令';
|
|
145
|
+
const kindEn = toolArg.startsWith('-') ? 'option' : 'command';
|
|
146
|
+
console.error(`错误:未知${kind}「${toolArg}」。`);
|
|
147
|
+
console.error(`可通过 \`cnb ${moduleName} --help\` 查看该模块支持的工具列表,或用 \`cnb --help\` 查看全部模块。`);
|
|
148
|
+
console.error(`Error: unknown ${kindEn} "${toolArg}". Please check the name.`);
|
|
149
|
+
console.error(`Run \`cnb ${moduleName} --help\` to list tools of this module, or \`cnb --help\` for all modules.`);
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
101
153
|
/**
|
|
102
154
|
* 为每个模块注册子命令,使 cnb <module> -h 显示模块帮助
|
|
103
155
|
* 每个 tool 的参数通过 Commander 的 option 系统注册,--help 自动展示参数说明
|
|
@@ -159,11 +211,19 @@ export function registerModuleCommands(program: Command): void {
|
|
|
159
211
|
// 按命令类别注册选项:upload / ci 自定义 / 普通 swagger 命令
|
|
160
212
|
if (shortcut.upload) {
|
|
161
213
|
shortcutCmd.requiredOption('--file <string>', '本地文件路径。必填。');
|
|
214
|
+
// 是否需要 number 由接口本身决定,而非 shortcut.repoOnly:
|
|
215
|
+
// - post-issue-comment-*-asset-upload-url(issue 评论上传):path 为 repo + number
|
|
216
|
+
// - post-asset-group(创建 issue 时上传):path 只有 repo,number 传了也会被忽略
|
|
217
|
+
// - pull 侧 upload-files / upload-imgs:按仓库维度上传,同样不需要 number
|
|
218
|
+
const needNumber = shortcut.realTool !== 'post-asset-group' && !shortcut.repoOnly;
|
|
219
|
+
registerAutoInjectedPathOptions(shortcutCmd, needNumber);
|
|
162
220
|
} else if (
|
|
163
221
|
shortcut.realTool === '__get-ci-logs__' ||
|
|
164
222
|
shortcut.realTool === '__get-ci-timing__'
|
|
165
223
|
) {
|
|
166
224
|
shortcutCmd.option('--sn <string>', '构建号(可选,默认自动从环境变量或 PR 状态获取)');
|
|
225
|
+
// CI 命令按仓库维度查询,允许显式覆盖环境变量中的仓库
|
|
226
|
+
shortcutCmd.option('--repo <string>', '仓库名称(可选,默认取 CNB_REPO_SLUG)');
|
|
167
227
|
} else {
|
|
168
228
|
registerToolOptions(shortcutCmd, moduleName, shortcut.realTool, true);
|
|
169
229
|
}
|
|
@@ -179,12 +239,8 @@ export function registerModuleCommands(program: Command): void {
|
|
|
179
239
|
sub
|
|
180
240
|
.argument('[tool]', '工具名称(支持快捷命令)')
|
|
181
241
|
.action(async (toolArg: string | undefined, _opts: Record<string, any>) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
sub.help();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
sub.help();
|
|
242
|
+
// 此处已不会再命中快捷命令(因为已注册为子命令),仅作兜底
|
|
243
|
+
guardUnknownTool(moduleName, sub, toolArg);
|
|
188
244
|
});
|
|
189
245
|
}
|
|
190
246
|
|
|
@@ -225,7 +281,7 @@ export function registerModuleCommands(program: Command): void {
|
|
|
225
281
|
skillsSub
|
|
226
282
|
.argument('[tool]', '工具名称(支持快捷命令)')
|
|
227
283
|
.action(async (toolArg: string | undefined, _opts: Record<string, any>) => {
|
|
228
|
-
skillsSub
|
|
284
|
+
guardUnknownTool('skills', skillsSub, toolArg);
|
|
229
285
|
});
|
|
230
286
|
}
|
|
231
287
|
}
|
package/client/shortcuts.ts
CHANGED
|
@@ -285,3 +285,34 @@ export function resolveShortcut(
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* 将快捷命令的 autoPath / autoData 合并进用户传入的参数。
|
|
290
|
+
*
|
|
291
|
+
* 优先级:用户显式传入 > 环境变量自动注入(autoPath)> 常量默认值(autoData)。
|
|
292
|
+
* - autoPath(repo / number / review_id):用户未传或传空值时用环境变量兜底
|
|
293
|
+
* - autoData(如 close 的 state='closed'):仅在用户完全未传时兜底
|
|
294
|
+
*
|
|
295
|
+
* 这样 `--repo` / `--number` 可作为可选参数显式覆盖环境变量,
|
|
296
|
+
* 未传时保持原有「从环境变量自动注入」的省流行为。
|
|
297
|
+
*
|
|
298
|
+
* @param params 用户传入参数(会被就地修改)
|
|
299
|
+
* @param shortcut 解析后的快捷命令定义
|
|
300
|
+
* @returns 合并后的 params
|
|
301
|
+
*/
|
|
302
|
+
export function mergeShortcutParams(
|
|
303
|
+
params: Record<string, any>,
|
|
304
|
+
shortcut: Pick<ResolvedShortcut, 'autoPath' | 'autoData'> | null | undefined,
|
|
305
|
+
): Record<string, any> {
|
|
306
|
+
if (!shortcut) return params;
|
|
307
|
+
for (const [key, value] of Object.entries(shortcut.autoPath || {})) {
|
|
308
|
+
if (!params[key]) {
|
|
309
|
+
params[key] = value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const [key, value] of Object.entries(shortcut.autoData || {})) {
|
|
313
|
+
if (params[key] === undefined) {
|
|
314
|
+
params[key] = value;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return params;
|
|
318
|
+
}
|