@cnbcool/cnb-api-generate 1.2.8 → 2.0.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/built/codegen/printer/printer-skills-client-core.js +10 -0
- package/built/utils/clean-array-desc.js +9 -0
- package/built/utils/collect-used-keys.js +16 -0
- package/built/utils/flat-option.js +2 -0
- package/built/utils/flatten-array-object-options.js +35 -0
- package/built/utils/flatten-nested-object-options.js +40 -0
- package/built/utils/flatten-tool-options.js +108 -0
- package/built/utils/generate-flat-options.js +50 -0
- package/built/utils/is-array-of-objects.js +10 -0
- package/built/utils/is-nested-object.js +9 -0
- package/built/utils/option-value-flag.js +14 -0
- package/built/utils/to-display-key.js +10 -0
- package/built/utils/trim-summary.js +12 -0
- package/client/index.ts +29 -504
- package/client/lib/execute-action.ts +126 -0
- package/client/lib/extra-help.ts +15 -0
- package/client/lib/flat-options-data.ts +13 -0
- package/client/lib/format-output.ts +78 -0
- package/client/lib/format-params.ts +220 -0
- package/client/lib/help-data.ts +13 -0
- package/client/lib/key-mapping-data.ts +13 -0
- package/client/lib/parsers.ts +130 -0
- package/client/lib/register-fallback.ts +14 -0
- package/client/lib/register-modules.ts +121 -0
- package/client/lib/summary-extractors.ts +189 -0
- package/client/lib/trim-summary.ts +11 -0
- package/client/shortcuts.ts +10 -198
- package/client/utils/build-nested-field-map.ts +38 -0
- package/client/utils/flat-key-from-segments.ts +8 -0
- package/client/utils/match-array-object-field.ts +19 -0
- package/client/utils/restore-original-keys.ts +23 -0
- package/package.json +3 -2
- package/client/modules.help.ts +0 -49
- package/client/schemaToJson.ts +0 -26
- package/client/tools.help.ts +0 -124
package/client/index.ts
CHANGED
|
@@ -1,515 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { showShort, resolveShortcut, summarizeResponse } from './shortcuts';
|
|
8
|
-
import { handleUpload } from './utils/upload';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { getExtraHelpText } from './lib/extra-help';
|
|
5
|
+
import { registerModuleCommands } from './lib/register-modules';
|
|
6
|
+
import { registerFallbackAction } from './lib/register-fallback';
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
);
|
|
14
|
-
if (!helpFileContent) {
|
|
15
|
-
console.error('help.json not found');
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
8
|
+
// ============================================================
|
|
9
|
+
// Commander 程序定义
|
|
10
|
+
// ============================================================
|
|
18
11
|
|
|
19
|
-
const
|
|
12
|
+
const program = new Command();
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
program
|
|
15
|
+
.name(process.env.CNB_CLI_CMD || 'cnb')
|
|
16
|
+
.description('CNB OpenAPI 命令行工具')
|
|
17
|
+
.allowUnknownOption()
|
|
18
|
+
.allowExcessArguments()
|
|
19
|
+
.helpOption('-h, --help', '显示帮助文档')
|
|
20
|
+
.option('-s, --short', '显示当前仓库的快捷命令')
|
|
21
|
+
.addHelpText('after', getExtraHelpText());
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
// ============================================================
|
|
24
|
+
// 注册命令
|
|
25
|
+
// ============================================================
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
registerModuleCommands(program);
|
|
28
|
+
registerFallbackAction(program);
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// 支持 key=value 格式 (e.g., --config=file.json)
|
|
41
|
-
if (fullKey.includes('=')) {
|
|
42
|
-
const [key, ...valueParts] = fullKey.split('=');
|
|
43
|
-
result[key] = valueParts.join('=');
|
|
44
|
-
i++;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
30
|
+
// ============================================================
|
|
31
|
+
// 导出(保持兼容性)
|
|
32
|
+
// ============================================================
|
|
47
33
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// 检查下一个参数是否是值(不以 - 开头)
|
|
51
|
-
const nextArg = args[i + 1];
|
|
52
|
-
const isNextArgValue =
|
|
53
|
-
i + 1 < args.length &&
|
|
54
|
-
!nextArg.startsWith('--') &&
|
|
55
|
-
!nextArg.startsWith('-');
|
|
34
|
+
export { parseUnknownOptions as parseArguments } from './lib/parsers';
|
|
56
35
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} else {
|
|
61
|
-
// 没有值,视为布尔标志
|
|
62
|
-
result[key] = true;
|
|
63
|
-
i++;
|
|
64
|
-
}
|
|
65
|
-
} else if (arg.startsWith('-') && arg.length > 1 && !/^-?\d+$/.test(arg)) {
|
|
66
|
-
// --- 处理短参数 (e.g., -h, -v),排除负数 ---
|
|
67
|
-
const key = arg.slice(1);
|
|
68
|
-
|
|
69
|
-
const nextArg = args[i + 1];
|
|
70
|
-
const isNextArgValue =
|
|
71
|
-
i + 1 < args.length &&
|
|
72
|
-
!nextArg.startsWith('--') &&
|
|
73
|
-
!nextArg.startsWith('-');
|
|
36
|
+
// ============================================================
|
|
37
|
+
// 启动
|
|
38
|
+
// ============================================================
|
|
74
39
|
|
|
75
|
-
|
|
76
|
-
if (isNextArgValue && key.length === 1) {
|
|
77
|
-
result[key] = nextArg;
|
|
78
|
-
i += 2;
|
|
79
|
-
} else {
|
|
80
|
-
result[key] = true;
|
|
81
|
-
i++;
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
// --- 处理位置参数 (Positional Args) ---
|
|
85
|
-
if (positionalCount === 0) {
|
|
86
|
-
result.module = arg;
|
|
87
|
-
} else if (positionalCount === 1) {
|
|
88
|
-
result.tool = arg;
|
|
89
|
-
}
|
|
90
|
-
positionalCount++;
|
|
91
|
-
i++;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 验证必须参数是否存在
|
|
100
|
-
* @param params 解析后的参数对象
|
|
101
|
-
* @returns 验证结果
|
|
102
|
-
*/
|
|
103
|
-
function validateRequiredParams(
|
|
104
|
-
params: Record<string, string | boolean | undefined>,
|
|
105
|
-
): boolean {
|
|
106
|
-
if (!params.tool || params.help) {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 尝试解析JSON字符串
|
|
114
|
-
* 先将真实控制字符转义为 JSON 合法形式,处理 shell/AI 传入的原始换行等情况
|
|
115
|
-
* @param str 要解析的字符串
|
|
116
|
-
* @returns 解析后的对象或原始字符串
|
|
117
|
-
*/
|
|
118
|
-
function tryParseJSON(str: string | boolean | undefined): any {
|
|
119
|
-
if (typeof str !== 'string') return str;
|
|
120
|
-
|
|
121
|
-
const escaped = str.replace(/[\x00-\x1F\x7F]/g, (ch) => {
|
|
122
|
-
const map = { '\n': '\\n', '\r': '\\r', '\t': '\\t', '\b': '\\b', '\f': '\\f' };
|
|
123
|
-
return map[ch] || '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0');
|
|
124
|
-
});
|
|
125
|
-
try {
|
|
126
|
-
return JSON.parse(escaped);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
return str;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 尝试从文件引用或 stdin 读取内容(类似 curl 的 @file / @- 语法)
|
|
134
|
-
*/
|
|
135
|
-
function tryReadFileRef(str: string | boolean | undefined) {
|
|
136
|
-
if (typeof str !== 'string' || !str.startsWith('@')) return str;
|
|
137
|
-
|
|
138
|
-
const ref = str.slice(1);
|
|
139
|
-
|
|
140
|
-
// @- 表示从 stdin 读取
|
|
141
|
-
if (ref === '-') {
|
|
142
|
-
try {
|
|
143
|
-
return fs.readFileSync(0, 'utf8').trim();
|
|
144
|
-
} catch (e) {
|
|
145
|
-
console.error('从 stdin 读取失败:', e.message);
|
|
146
|
-
return str;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// @/path/to/file 表示从文件读取
|
|
151
|
-
if (fs.existsSync(ref)) {
|
|
152
|
-
return fs.readFileSync(ref, 'utf8').trim();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return str;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* 获取 tool 的参数定义(用于自动分发 --key value 到 path/query)
|
|
160
|
-
*/
|
|
161
|
-
function getToolParamDefs(moduleName: string, toolName: string) {
|
|
162
|
-
const toolHelp = helpData.modulesHelp?.[moduleName]?.[toolName];
|
|
163
|
-
if (!toolHelp) return null;
|
|
164
|
-
return toolHelp.help?.parameter || {};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* 格式化参数
|
|
169
|
-
* 支持新的 --key value 扁平格式,同时向后兼容旧的 --path/--query JSON 格式。
|
|
170
|
-
* 根据 help.json 中的参数定义,自动将扁平参数分发到 path 或 query。
|
|
171
|
-
* @param params 原始参数对象
|
|
172
|
-
* @returns 格式化后的参数对象,包含 module, tool, path, query, data 等
|
|
173
|
-
*/
|
|
174
|
-
function formatParams(
|
|
175
|
-
params: Record<string, string | boolean | undefined>,
|
|
176
|
-
): Record<string, any> {
|
|
177
|
-
const formatted: Record<string, any> = {
|
|
178
|
-
module: params.module,
|
|
179
|
-
tool: params.tool,
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// 保留控制标志
|
|
183
|
-
if (params.help) formatted.help = true;
|
|
184
|
-
if (params.short) formatted.short = true;
|
|
185
|
-
if (params.verbose) formatted.verbose = true;
|
|
186
|
-
|
|
187
|
-
// 旧格式兼容:--path / --query / --data 是 JSON 字符串
|
|
188
|
-
if (typeof params.path === 'string') {
|
|
189
|
-
formatted.path = tryParseJSON(tryReadFileRef(params.path));
|
|
190
|
-
}
|
|
191
|
-
if (typeof params.query === 'string') {
|
|
192
|
-
formatted.query = tryParseJSON(tryReadFileRef(params.query));
|
|
193
|
-
}
|
|
194
|
-
if (typeof params.data === 'string') {
|
|
195
|
-
formatted.data = tryParseJSON(tryReadFileRef(params.data));
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 新格式:将其他 --key value 根据 help.json 自动分发到 path/query
|
|
199
|
-
const paramDefs = getToolParamDefs(
|
|
200
|
-
params.module as string,
|
|
201
|
-
params.tool as string,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
if (paramDefs) {
|
|
205
|
-
const pathDef = paramDefs.path || {};
|
|
206
|
-
const queryDef = paramDefs.query || {};
|
|
207
|
-
const reservedKeys = new Set([
|
|
208
|
-
'module', 'tool', 'help', 'short', 'verbose',
|
|
209
|
-
'path', 'query', 'data', 'h', 'v',
|
|
210
|
-
]);
|
|
211
|
-
|
|
212
|
-
for (const [key, value] of Object.entries(params)) {
|
|
213
|
-
if (reservedKeys.has(key)) continue;
|
|
214
|
-
if (typeof value === 'boolean') continue;
|
|
215
|
-
|
|
216
|
-
if (pathDef[key]) {
|
|
217
|
-
// 归入 path
|
|
218
|
-
if (!formatted.path) formatted.path = {};
|
|
219
|
-
formatted.path[key] = value;
|
|
220
|
-
} else if (queryDef[key]) {
|
|
221
|
-
// 归入 query
|
|
222
|
-
if (!formatted.query) formatted.query = {};
|
|
223
|
-
// 自动转数字
|
|
224
|
-
const paramType = queryDef[key].type;
|
|
225
|
-
if (paramType === 'number' && typeof value === 'string' && !isNaN(Number(value))) {
|
|
226
|
-
formatted.query[key] = Number(value);
|
|
227
|
-
} else {
|
|
228
|
-
formatted.query[key] = value;
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
// 未知参数,尝试放入 query(兼容)
|
|
232
|
-
if (!formatted.query) formatted.query = {};
|
|
233
|
-
formatted.query[key] = typeof value === 'string' && !isNaN(Number(value)) ? Number(value) : value;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 当没有传递 query 时,要判断当前 tool 是否支持 query
|
|
239
|
-
if (formatted.query === undefined) {
|
|
240
|
-
const paramDefs2 = getToolParamDefs(
|
|
241
|
-
formatted.module,
|
|
242
|
-
formatted.tool,
|
|
243
|
-
);
|
|
244
|
-
if (paramDefs2?.query) {
|
|
245
|
-
formatted.query = {};
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return formatted;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* 精简响应对象(非 verbose 模式使用)
|
|
254
|
-
* - 去掉 trace(仅错误时保留)
|
|
255
|
-
* - 去掉 header(原始 x-cnb-* 头)
|
|
256
|
-
* - 仅列表 API 时保留分页信息
|
|
257
|
-
*/
|
|
258
|
-
function compactResponse(response: any): any {
|
|
259
|
-
if (!response || typeof response !== 'object') return response;
|
|
260
|
-
|
|
261
|
-
const compact: any = { status: response.status };
|
|
262
|
-
|
|
263
|
-
// 仅错误时保留 trace
|
|
264
|
-
if (response.status >= 300 && response.trace) {
|
|
265
|
-
compact.trace = response.trace;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// 仅列表 API 时保留分页信息
|
|
269
|
-
if (response.total != null) {
|
|
270
|
-
compact.page = response.page;
|
|
271
|
-
compact.pageSize = response.pageSize;
|
|
272
|
-
compact.total = response.total;
|
|
273
|
-
compact.totalPages = response.totalPages;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
compact.data = response.data;
|
|
277
|
-
return compact;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* 判断是否是标准响应结构(有 status + data 字段)
|
|
282
|
-
* cag.config.js 中的 responseConverter 可能返回裸 data(不含 status),需要区分处理
|
|
283
|
-
*/
|
|
284
|
-
function isStandardResponse(response: any): boolean {
|
|
285
|
-
return response && typeof response === 'object' && typeof response.status === 'number' && 'data' in response;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* 格式化 CLI 输出
|
|
290
|
-
* - verbose 模式:完整 JSON(含 trace、header 等全部字段)
|
|
291
|
-
* - 快捷命令默认:只输出核心摘要字段
|
|
292
|
-
* - 非快捷命令默认:精简 JSON(去掉 trace、header,保留完整 data)
|
|
293
|
-
* @param response 原始响应
|
|
294
|
-
* @param verbose 是否 verbose 模式
|
|
295
|
-
* @param summary 是否 summary 模式
|
|
296
|
-
* @param toolKey 当前 tool 标识,格式为 "module/tool",用于匹配摘要配置
|
|
297
|
-
*/
|
|
298
|
-
function formatOutput(response: any, verbose: boolean, summary: boolean, toolKey: string): string {
|
|
299
|
-
// --verbose 模式:输出完整原始信息
|
|
300
|
-
if (verbose) {
|
|
301
|
-
return JSON.stringify(response, null, 2);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// converter 可能返回裸 data(没有标准 {status, data} 结构),直接输出
|
|
305
|
-
if (!isStandardResponse(response)) {
|
|
306
|
-
if (summary) {
|
|
307
|
-
const summarized = summarizeResponse(response, toolKey);
|
|
308
|
-
if (summarized !== null) {
|
|
309
|
-
return JSON.stringify(summarized, null, 2);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return JSON.stringify(response, null, 2);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// 精简响应
|
|
316
|
-
const compact = compactResponse(response);
|
|
317
|
-
|
|
318
|
-
// summary 模式:对特定 tool 应用摘要提取(仅成功响应)
|
|
319
|
-
if (summary && compact.status >= 200 && compact.status < 300) {
|
|
320
|
-
const summarized = summarizeResponse(compact.data, toolKey);
|
|
321
|
-
if (summarized !== null) {
|
|
322
|
-
compact.data = summarized;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return JSON.stringify(compact, null, 2);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* 显示帮助文档
|
|
331
|
-
* - 无参数:显示顶层帮助(模块列表 + 参数说明 + 用法示例)
|
|
332
|
-
* - 指定 module:显示模块帮助(工具列表)
|
|
333
|
-
* - 指定 module + tool:显示工具帮助(参数详情 + 示例)
|
|
334
|
-
*/
|
|
335
|
-
function showHelp(moduleName?: string, tool?: string): void {
|
|
336
|
-
if (moduleName && tool) {
|
|
337
|
-
showToolHelp(helpData, moduleName, tool);
|
|
338
|
-
} else if (moduleName) {
|
|
339
|
-
showModuleHelp(helpData, moduleName);
|
|
340
|
-
} else {
|
|
341
|
-
const cliCmd = process.env.CNB_CLI_CMD || 'cnb';
|
|
342
|
-
|
|
343
|
-
// 紧凑的模块列表
|
|
344
|
-
const moduleParts: string[] = [];
|
|
345
|
-
for (const [mod, count] of Object.entries(helpData.mainHelp)) {
|
|
346
|
-
moduleParts.push(`${mod}(${count})`);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// 每行放几个模块
|
|
350
|
-
const lines: string[] = [];
|
|
351
|
-
let currentLine = ' ';
|
|
352
|
-
for (const part of moduleParts) {
|
|
353
|
-
if (currentLine.length + part.length > 78) {
|
|
354
|
-
lines.push(currentLine);
|
|
355
|
-
currentLine = ' ' + part + ' ';
|
|
356
|
-
} else {
|
|
357
|
-
currentLine += part + ' ';
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (currentLine.trim()) lines.push(currentLine);
|
|
361
|
-
|
|
362
|
-
const helpMsg = `
|
|
363
|
-
CNB OpenAPI CLI
|
|
364
|
-
|
|
365
|
-
模块(tool数量):
|
|
366
|
-
${lines.join('\n')}
|
|
367
|
-
|
|
368
|
-
参数:
|
|
369
|
-
<module> 模块名称 (如: issues, pulls, git)
|
|
370
|
-
<tool> 工具名称 (如: list-issues, get-issue)
|
|
371
|
-
--key value 路径或查询参数,CLI 自动识别归类
|
|
372
|
-
--data 'JSON' 请求体参数,JSON 字符串
|
|
373
|
-
--verbose 输出完整原始响应(含 trace、header 等全部字段)
|
|
374
|
-
--help 显示帮助文档
|
|
375
|
-
--short 显示当前仓库的快捷命令
|
|
376
|
-
|
|
377
|
-
用法: ${cliCmd} issues list-issues --repo my-org/my-repo --page 1 --pageSize 10
|
|
378
|
-
${cliCmd} issues create-issue --repo my-org/my-repo --data '{"title":"Bug"}'
|
|
379
|
-
帮助: ${cliCmd} <module> --help
|
|
380
|
-
${cliCmd} <module> <tool> --help
|
|
381
|
-
快捷: ${cliCmd} --short
|
|
382
|
-
`;
|
|
383
|
-
console.log(helpMsg);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* 主函数
|
|
389
|
-
*/
|
|
390
|
-
async function main() {
|
|
391
|
-
const params = parseArguments();
|
|
392
|
-
|
|
393
|
-
// 处理 --short
|
|
394
|
-
if (params.short) {
|
|
395
|
-
showShort();
|
|
396
|
-
process.exit(0);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// 尝试解析快捷命令
|
|
400
|
-
const shortcut = resolveShortcut(
|
|
401
|
-
params.module as string | undefined,
|
|
402
|
-
params.tool as string | undefined,
|
|
403
|
-
);
|
|
404
|
-
if (shortcut) {
|
|
405
|
-
params.module = shortcut.module;
|
|
406
|
-
params.tool = shortcut.tool;
|
|
407
|
-
|
|
408
|
-
// 自动注入 path 参数
|
|
409
|
-
for (const [key, value] of Object.entries(shortcut.autoPath)) {
|
|
410
|
-
if (!params[key]) {
|
|
411
|
-
params[key] = value;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// 自动注入 data 参数
|
|
416
|
-
if (shortcut.autoData && !params.data) {
|
|
417
|
-
params.data = JSON.stringify(shortcut.autoData);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// 验证必须参数
|
|
422
|
-
if (!validateRequiredParams(params)) {
|
|
423
|
-
showHelp(
|
|
424
|
-
params.module as string | undefined,
|
|
425
|
-
params.tool as string | undefined,
|
|
426
|
-
);
|
|
427
|
-
process.exit(0);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// 格式化参数
|
|
431
|
-
const formattedParams = formatParams(params);
|
|
432
|
-
|
|
433
|
-
// 参数预检:缺少必填 path 参数时自动输出 tool help
|
|
434
|
-
const toolHelpData = helpData.modulesHelp?.[formattedParams.module]?.[formattedParams.tool];
|
|
435
|
-
if (toolHelpData) {
|
|
436
|
-
const pathDef = toolHelpData.help?.parameter?.path;
|
|
437
|
-
if (pathDef) {
|
|
438
|
-
const missingRequired = Object.entries(pathDef)
|
|
439
|
-
.filter(([, p]: [string, any]) => p.required && !formattedParams.path?.[p.name])
|
|
440
|
-
.map(([k]) => `--${k}`);
|
|
441
|
-
if (missingRequired.length > 0) {
|
|
442
|
-
console.error(`缺少必填参数: ${missingRequired.join(', ')}\n`);
|
|
443
|
-
showToolHelp(helpData, formattedParams.module, formattedParams.tool);
|
|
444
|
-
process.exit(1);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// 动态引入模块
|
|
450
|
-
const toolPath = path.join(
|
|
451
|
-
__dirname,
|
|
452
|
-
'../modules',
|
|
453
|
-
`${formattedParams.module}/${formattedParams.tool}.js`,
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
if (!fs.existsSync(toolPath)) {
|
|
457
|
-
console.error(`工具文件不存在: ${toolPath}`);
|
|
458
|
-
process.exit(1);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const toolModule = require(toolPath);
|
|
462
|
-
const toolFunction = toolModule.default;
|
|
463
|
-
|
|
464
|
-
if (!toolFunction) {
|
|
465
|
-
console.error(`工具函数不存在`);
|
|
466
|
-
process.exit(1);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const toolsParam: any[] = [];
|
|
470
|
-
let pathAndQueryParams: Record<string, any> | string | null = null;
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
formattedParams.path &&
|
|
474
|
-
Object.keys(formattedParams.path).length === 1 &&
|
|
475
|
-
!formattedParams.query
|
|
476
|
-
) {
|
|
477
|
-
pathAndQueryParams =
|
|
478
|
-
formattedParams.path[Object.keys(formattedParams.path)[0]];
|
|
479
|
-
} else {
|
|
480
|
-
if (formattedParams.path) {
|
|
481
|
-
pathAndQueryParams = { ...formattedParams.path };
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (formattedParams.query) {
|
|
485
|
-
pathAndQueryParams = {
|
|
486
|
-
...(pathAndQueryParams && typeof pathAndQueryParams === 'object'
|
|
487
|
-
? pathAndQueryParams
|
|
488
|
-
: {}),
|
|
489
|
-
...formattedParams.query,
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (pathAndQueryParams) {
|
|
495
|
-
toolsParam.push(pathAndQueryParams);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// 上传快捷命令:走完整上传流程(获取 URL → PUT 文件 → 返回结果)
|
|
499
|
-
let data: any;
|
|
500
|
-
if (shortcut?.upload) {
|
|
501
|
-
data = await handleUpload(shortcut, formattedParams.data?.file, toolFunction, pathAndQueryParams as { repo: string; number?: string });
|
|
502
|
-
} else {
|
|
503
|
-
if (formattedParams.data) {
|
|
504
|
-
toolsParam.push(formattedParams.data);
|
|
505
|
-
}
|
|
506
|
-
data = await toolFunction(...toolsParam);
|
|
507
|
-
}
|
|
508
|
-
const toolKey = `${formattedParams.module}/${formattedParams.tool}`;
|
|
509
|
-
// 快捷命令默认输出摘要,--verbose 时输出全部信息
|
|
510
|
-
const isVerbose = !!formattedParams.verbose;
|
|
511
|
-
const isSummary = shortcut ? !isVerbose : false;
|
|
512
|
-
console.log(formatOutput(data, isVerbose, isSummary, toolKey));
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
main();
|
|
40
|
+
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { showShort, resolveShortcut } from '../shortcuts';
|
|
5
|
+
import { handleUpload } from '../utils/upload';
|
|
6
|
+
import { formatParams } from './format-params';
|
|
7
|
+
import { formatOutput } from './format-output';
|
|
8
|
+
|
|
9
|
+
export async function executeAction(
|
|
10
|
+
moduleArg: string | undefined,
|
|
11
|
+
toolArg: string | undefined,
|
|
12
|
+
opts: Record<string, any>,
|
|
13
|
+
parentCmd: Command,
|
|
14
|
+
) {
|
|
15
|
+
// Commander 已解析好所有 option,直接构建 params
|
|
16
|
+
const params: Record<string, string | boolean | undefined> = {};
|
|
17
|
+
|
|
18
|
+
if (moduleArg) params.module = moduleArg;
|
|
19
|
+
if (toolArg) params.tool = toolArg;
|
|
20
|
+
|
|
21
|
+
// 从 Commander 解析好的 opts 中取出所有参数
|
|
22
|
+
const reservedKeys = new Set(['help', 'h']);
|
|
23
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
24
|
+
if (reservedKeys.has(key)) continue;
|
|
25
|
+
if (value !== undefined) {
|
|
26
|
+
params[key] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (params.short) {
|
|
31
|
+
showShort();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const shortcut = resolveShortcut(
|
|
36
|
+
params.module as string | undefined,
|
|
37
|
+
params.tool as string | undefined,
|
|
38
|
+
);
|
|
39
|
+
if (shortcut) {
|
|
40
|
+
params.module = shortcut.module;
|
|
41
|
+
params.tool = shortcut.tool;
|
|
42
|
+
|
|
43
|
+
for (const [key, value] of Object.entries(shortcut.autoPath)) {
|
|
44
|
+
if (!params[key]) {
|
|
45
|
+
params[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (shortcut.autoData) {
|
|
50
|
+
for (const [key, value] of Object.entries(shortcut.autoData)) {
|
|
51
|
+
if (params[key] === undefined) {
|
|
52
|
+
params[key] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!params.tool) {
|
|
59
|
+
parentCmd.help();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const formattedParams = formatParams(params);
|
|
63
|
+
|
|
64
|
+
const toolPath = path.join(
|
|
65
|
+
__dirname,
|
|
66
|
+
'../../modules',
|
|
67
|
+
`${formattedParams.module}/${formattedParams.tool}.js`,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(toolPath)) {
|
|
71
|
+
console.error(`工具文件不存在: ${toolPath}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const toolModule = require(toolPath);
|
|
76
|
+
const toolFunction = toolModule.default;
|
|
77
|
+
|
|
78
|
+
if (!toolFunction) {
|
|
79
|
+
console.error(`工具函数不存在`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const toolsParam: any[] = [];
|
|
84
|
+
let pathAndQueryParams: Record<string, any> | string | null = null;
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
formattedParams.path &&
|
|
88
|
+
Object.keys(formattedParams.path).length === 1 &&
|
|
89
|
+
!formattedParams.query
|
|
90
|
+
) {
|
|
91
|
+
pathAndQueryParams =
|
|
92
|
+
formattedParams.path[Object.keys(formattedParams.path)[0]];
|
|
93
|
+
} else {
|
|
94
|
+
if (formattedParams.path) {
|
|
95
|
+
pathAndQueryParams = { ...formattedParams.path };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (formattedParams.query) {
|
|
99
|
+
pathAndQueryParams = {
|
|
100
|
+
...(pathAndQueryParams && typeof pathAndQueryParams === 'object'
|
|
101
|
+
? pathAndQueryParams
|
|
102
|
+
: {}),
|
|
103
|
+
...formattedParams.query,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (pathAndQueryParams) {
|
|
109
|
+
toolsParam.push(pathAndQueryParams);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let data: any;
|
|
113
|
+
if (shortcut?.upload) {
|
|
114
|
+
data = await handleUpload(shortcut, formattedParams.data?.file, toolFunction, pathAndQueryParams as { repo: string; number?: string });
|
|
115
|
+
} else {
|
|
116
|
+
if (formattedParams.data) {
|
|
117
|
+
toolsParam.push(formattedParams.data);
|
|
118
|
+
}
|
|
119
|
+
data = await toolFunction(...toolsParam);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const toolKey = `${formattedParams.module}/${formattedParams.tool}`;
|
|
123
|
+
const isVerbose = !!formattedParams.verbose;
|
|
124
|
+
const isSummary = shortcut ? !isVerbose : false;
|
|
125
|
+
console.log(formatOutput(data, isVerbose, isSummary, toolKey));
|
|
126
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成额外的帮助提示字符串
|
|
3
|
+
* 包含模块列表和用法示例,供 commander .addHelpText() 使用
|
|
4
|
+
*/
|
|
5
|
+
export function getExtraHelpText(): string {
|
|
6
|
+
const cliCmd = process.env.CNB_CLI_CMD || 'cnb';
|
|
7
|
+
|
|
8
|
+
return `
|
|
9
|
+
|
|
10
|
+
用法: ${cliCmd} issues list-issues --repo my-org/my-repo --page 1 --pageSize 10
|
|
11
|
+
${cliCmd} issues create-issue --repo my-org/my-repo --title Bug
|
|
12
|
+
帮助: ${cliCmd} <module> --help
|
|
13
|
+
${cliCmd} <module> <tool> --help
|
|
14
|
+
快捷: ${cliCmd} --short`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const flatOptionsFilePath = path.join(__dirname, '../flat-options.json');
|
|
5
|
+
|
|
6
|
+
let data: Record<string, Record<string, Record<string, any>>> = {};
|
|
7
|
+
|
|
8
|
+
if (fs.existsSync(flatOptionsFilePath)) {
|
|
9
|
+
const content = fs.readFileSync(flatOptionsFilePath, 'utf8');
|
|
10
|
+
data = JSON.parse(content);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const flatOptionsData = data;
|