@coderpp/yunxiao-cli 0.1.11 → 0.2.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
@@ -15,11 +15,11 @@ npm run build
15
15
  node dist/src/index.js --help
16
16
  ```
17
17
 
18
- 如果后续发布为 npm 包,安装后可直接使用 `yunxiao` 命令。
18
+ 安装 npm 包后可直接使用 `yunxiao` 命令。
19
19
 
20
20
  ## 通过 npx 使用
21
21
 
22
- 发布到 npm 后,可以直接用 `npx` 执行:
22
+ 可以直接用 `npx` 执行:
23
23
 
24
24
  ```bash
25
25
  npx @coderpp/yunxiao-cli mr list --state opened
@@ -453,3 +453,163 @@ npx --yes --package @coderpp/yunxiao-cli yunxiao workitem attachments <workitemI
453
453
  已有文件不覆盖,重复读取请选择新目录;失败返回非零退出码,清理未完成文件并保留已完成文件。
454
454
  仅支持带 `fileIdentifier` 的云效图片(PNG/JPEG/GIF/WebP/BMP/ICO);外部图片和 data URL 会报错。
455
455
  需要工作项、评论、附件只读权限。完整内容不递归包含关联工作项或历史动态。
456
+
457
+ ## 0.2.0 新增能力
458
+
459
+ 以下命令与错误契约要求 `@coderpp/yunxiao-cli >= 0.2.0`。
460
+
461
+ ## 完整查询与可靠请求
462
+
463
+ 分页接口统一支持 `--page`、`--per-page`(1–100)、`--all-pages`:
464
+
465
+ ```bash
466
+ yunxiao mr list --state opened --all-pages --output json
467
+ yunxiao project list --all-pages
468
+ yunxiao member list --all-pages
469
+ yunxiao pipeline list --name 发布 --all-pages
470
+ yunxiao pipeline runs 123 --all-pages
471
+ yunxiao workitem mine --all-pages
472
+ ```
473
+
474
+ 默认每页 20 条;`pipeline failed-runs` 保持内部全量扫描和默认每页 30 条。
475
+ 工作项按项目分别分页后再过滤状态。没有使用 `--all-pages` 的状态查询会提示结果范围。
476
+ 分页优先采用响应头的下一页和实际页大小;无分页元信息时根据返回条数判断。
477
+ 重复页、分页中断或保护上限触发时返回非零退出码,不输出不完整的聚合结果。
478
+ `project members`、工作项评论等无分页接口直接返回完整 API 响应,不接受分页选项。
479
+
480
+ API 请求默认 `--timeout-ms 30000 --retries 2`。超时覆盖响应体读取;文件下载也使用该超时,失败清理未完成文件。
481
+ 仅只读操作会在网络临时错误、429、502/503/504 时退避重试,包括工作项/项目搜索和 MR 评论查询这些只读 POST。
482
+ 遵循 `Retry-After`;要求等待超过 60 秒时返回限流错误,由调用方稍后重试。
483
+ 写操作不自动重放,网络错误、超时或服务端 5xx 可能返回“写入结果未知”,应先查服务端状态。
484
+ 文件下载保留单次读取行为,不自动重试。
485
+
486
+ 未知参数、缺值、重复的非集合参数、额外位置参数、非法枚举和数值会报错。
487
+ `--field` 可重复;`--key=a=b` 会完整保留 `a=b`。布尔参数支持 `--yes=false` 等写法。
488
+ `--version` 和 `yunxiao mr diff --help` 等子命令帮助不需要 Token。
489
+ 原有 `pl`、`wi` 和命令别名继续支持,输出与主命令一致。
490
+
491
+ ## MR 详情、差异与评论
492
+
493
+ ```bash
494
+ yunxiao mr get 2813489 12 --output json
495
+ yunxiao mr diff 2813489 12 --output json
496
+ yunxiao mr diff 2813489 12 --file src/example.ts --output text
497
+ yunxiao mr diff 2813489 12 --from-patch-set-id target-id --to-patch-set-id source-id --output json
498
+ yunxiao mr comments 2813489 12 --resolved=false --output json
499
+ yunxiao mr comments 2813489 12 --comment-type INLINE_COMMENT --file src/example.ts
500
+
501
+ yunxiao mr comment 2813489 12 --content-file ./review.md --yes
502
+ yunxiao mr comment 2813489 12 --parent-id comment-id --content "已处理,请复核" --yes
503
+ yunxiao mr comment 2813489 12 --file src/example.ts --line 42 --side new \
504
+ --from-patch-set-id target-id --to-patch-set-id source-id --content "这里需要处理空值" --yes
505
+ ```
506
+
507
+ `mr diff` 从 MR 版本列表解析并固定版本 ID、提交 SHA;默认取最新目标和源版本。
508
+ 使用固定 SHA 调用代码比较 API(目标到源使用 merge-base;源版本之间使用直接比较),并与相同版本对的 MR 文件树核对文件对象 ID、数量及增删行数。
509
+ 响应包含版本 ID、SHA、`complete` 和逐文件原因;二进制、缺失、截断或比较不一致时标记不完整。
510
+ 这是完整性核对而非绕过服务端 diff 限额;跨库提交不可访问时会保留服务端错误。
511
+ `--output text` 可直接读取 diff,但机器验收应检查 JSON 的 `complete`;成功读取不等同于内容完整。
512
+
513
+ 行内评论必须指定文件、正整数行号、`--side old|new` 和 `--to-patch-set-id`;未指定起始版本时解析最新目标版本。
514
+ 两端版本必须属于该 MR,文件必须存在于对应侧的变更树;服务端继续校验实际评论行位置。
515
+ 普通评论默认绑定最新源版本,也可用 `--to-patch-set-id` 固定版本。
516
+ 评论支持 `--parent-id` 回复,默认直接发表、未解决。评论编辑、删除、解决状态操作不在本轮范围。
517
+
518
+ ## 通用 MR 管理与发布恢复
519
+
520
+ ```bash
521
+ yunxiao mr create 2813489 --source feature/demo --target master --title "功能说明" \
522
+ --description-file ./mr.md --reviewers user-1,user-2 --yes
523
+ yunxiao mr update 2813489 12 --title "更新标题" --description-file ./mr.md --yes
524
+ yunxiao mr update 2813489 12 --description= --yes
525
+ yunxiao mr close 2813489 12 --yes
526
+ yunxiao mr review 2813489 12 --opinion not-pass --comment-file ./review.md
527
+ yunxiao mr release example-web master release --resume 12 --yes --output json
528
+ ```
529
+
530
+ `mr create` 只创建;默认同仓库,可用 `--source-project-id` 指定源库。
531
+ `mr review` 默认 `--opinion pass`,保留已有调用方式。
532
+ 新建、修改、关闭、评论、流水线启动和取消等新写命令都需要 `--yes`。
533
+
534
+ 发布按以下规则执行:
535
+
536
+ - 仓库查询完整分页并精确匹配;同名歧义需要完整命名空间路径。
537
+ - 创建前查找相同源/目标库及分支的未关闭 MR;发现已有 MR 返回错误、链接和显式恢复参数,避免自动接管。
538
+ - `--resume` 校验 MR 的库及分支;已合并返回 `already-merged`,已关闭拒绝执行。
539
+ - 仅在服务端能证明当前用户已于当前版本创建后评审通过时跳过评审,否则重新评审当前版本;评审前后核对源版本,变化时停止合并。
540
+ - 创建请求结果未知时只读查询相同 MR 并返回恢复信息;不会自动再次创建或直接评审。
541
+ - 合并响应丢失时查询 MR,已合并按成功返回,否则保留结果未知。
542
+ - 明确的“源分支相对目标分支没有改动,不能新建代码评审”返回 `status: skipped`;其他创建错误仍失败。
543
+ - 部分完成返回已创建 MR、已完成阶段、失败阶段及 `resumeArgs`。这些参数为数组,调用方应逐项传参;显式传入的 profile、配置文件、组织、域名和版本会被保留,Token 不会写入恢复参数。
544
+
545
+ 服务端最终负责合并冲突及门禁检查。版本检查和合并不是原子操作,无法保证检查后分支不会被其他人更新。
546
+ MR 合并成功不表示流水线或部署成功;用流水线命令另行核对。
547
+
548
+ ## 流水线列表、日志、启动和取消
549
+
550
+ ```bash
551
+ yunxiao pipeline list --name 发布 --all-pages
552
+ yunxiao pipeline jobs 123 456 --output json
553
+ yunxiao pipeline logs 123 456 789 --output text
554
+ yunxiao pipeline start 123 --params-file ./run.json --yes --output json
555
+ yunxiao pipeline cancel 123 456 --yes --output json
556
+ ```
557
+
558
+ `jobs` 从运行详情整理阶段、任务信息;`logs` 返回单次日志快照,JSON 保留 `content`、`last`、`more`,文本模式在存在更多日志时提示。
559
+ `start` 返回 `pipelineRunId`;`params-file` 内容直接写运行参数对象,CLI 按 API 要求包装成 `params` JSON 字符串。例如:
560
+
561
+ ```json
562
+ {"envs":{"ENV":"test"},"runningBranchs":{"https://codeup.aliyun.com/org/app.git":"master"},"comment":"手动验证"}
563
+ ```
564
+
565
+ `cancel` 返回接受结果和随后的运行状态;返回接受成功不代表运行已终止。读取状态失败返回部分完成错误。
566
+
567
+ ## 配置与诊断
568
+
569
+ ```bash
570
+ yunxiao config set dev --organization-id org-id --edition center --token-env DEV_YUNXIAO_TOKEN --project-ids project-1
571
+ yunxiao config use dev
572
+ yunxiao config list
573
+ yunxiao config show --profile dev
574
+ yunxiao auth whoami --profile dev
575
+ yunxiao doctor
576
+ yunxiao doctor --remote
577
+ ```
578
+
579
+ 默认配置文件为 `$XDG_CONFIG_HOME/yunxiao/config.json`,未设置时为 `~/.config/yunxiao/config.json`。
580
+ 可用 `--config` 或 `YUNXIAO_CONFIG` 覆盖路径;配置文件权限为 0600。
581
+ 通过 `--profile`、`YUNXIAO_PROFILE` 或保存的当前 profile 选择配置。
582
+ 取值优先级:命令参数 > 环境变量 > profile > 默认值。
583
+ profile 保存域名、组织、版本、项目、分类及 `token-env` 环境变量名,不保存明文 Token。
584
+ `config show` 显示 profile 保存值;环境变量覆盖的最终效果可通过诊断核对。
585
+
586
+ `doctor` 检查配置、Token 是否存在和域名/版本格式,不发请求。
587
+ `doctor --remote` 额外读取当前用户验证连接和身份;不会验证每个项目的访问权限或写权限。
588
+
589
+ ## 文件输入与错误契约
590
+
591
+ 描述、内容、评审意见及运行参数分别支持 `--description-file`、`--content-file`、`--comment-file`、`--params-file`。
592
+ 文件名 `-` 读取 stdin;一次命令只能有一个 stdin 输入。相同字段的直接文本与文件输入互斥。
593
+ UTF-8 内容、换行和等号原样保留;文件无法读取时在 API 调用前失败。
594
+
595
+ ```bash
596
+ yunxiao workitem comment workitem-id --content-file ./comment.md --yes
597
+ yunxiao workitem comment workitem-id --parent-id comment-id --content "回复" --yes
598
+ yunxiao mr get 2813489 12 --output json --error-format json
599
+ ```
600
+
601
+ 成功结果写 stdout,分页提示与错误写 stderr。`--error-format json` 的错误结构:
602
+
603
+ ```json
604
+ {"error":{"code":"PARTIAL_OPERATION","message":"...","details":{"localId":12,"completed":["create"],"failedStage":"review","resumeArgs":["mr","release","app","master","release","--resume","12","--yes"]}}}
605
+ ```
606
+
607
+ 服务端提供请求 ID 时错误中保留 `requestId`;Token 和带查询参数的 URL 会被脱敏。
608
+ 退出码从原先统一的 1 细分为:0 成功/明确跳过,1 未分类/响应格式错误,2 参数或配置错误,3 认证或权限错误,4 不存在,5 冲突或前置条件不满足,6 网络/超时/限流/服务不可用,7 部分完成或写入结果未知。
609
+ 已有脚本应以非零判断失败,需要细分时再按上述约定处理。
610
+
611
+ ### API 契约参考
612
+
613
+ - [MR 版本列表](https://help.aliyun.com/zh/yunxiao/developer-reference/listchangerequestpatchsets-query-the-list-of-merge-request-versions)、[变更文件树](https://help.aliyun.com/zh/yunxiao/developer-reference/getchangerequesttree-queries-the-change-file-tree-for-merge-requests)、[固定提交比较](https://help.aliyun.com/zh/yunxiao/developer-reference/getcompare)
614
+ - [MR 评论列表](https://help.aliyun.com/zh/yunxiao/developer-reference/listmergerequestcomments)、[创建 MR 评论](https://help.aliyun.com/zh/yunxiao/developer-reference/createchangerequestcomment)
615
+ - [启动流水线](https://help.aliyun.com/zh/yunxiao/developer-reference/createpipelinerun)、[终止运行](https://help.aliyun.com/zh/yunxiao/developer-reference/updatepipelinerun)、[任务日志](https://help.aliyun.com/zh/yunxiao/developer-reference/getpipelinejobrunlog)
@@ -0,0 +1,11 @@
1
+ export type FlagValue = string | boolean | Array<string | boolean>;
2
+ export type Flags = Record<string, FlagValue>;
3
+ export type ParsedArgs = {
4
+ positionals: string[];
5
+ flags: Flags;
6
+ };
7
+ export declare const specs: Record<string, [number, string]>;
8
+ export declare function parseArgs(args: string[]): ParsedArgs;
9
+ export declare function validateArgs(parsed: ParsedArgs): void;
10
+ export declare function loadFileFlags(flags: Flags, stdin?: () => Promise<string>): Promise<void>;
11
+ export declare function commandHelp(p: string[]): string;
@@ -0,0 +1,271 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { CliError } from "./errors.js";
3
+ const globals = "token domain organization-id edition profile config timeout-ms retries output json error-format help version";
4
+ const pages = "page per-page all-pages";
5
+ const merge = "yes merge-type message remove-source-branch";
6
+ const workSearch = `${pages} project-id project-ids category assigned-to assignee created-by creator state status-ids status-name status-name-en subject created-after created-before from to date-field conditions order-by sort space-type`;
7
+ export const specs = {
8
+ "mr list": [
9
+ 0,
10
+ `${pages} state project-ids author-ids reviewer-ids search order-by sort created-before created-after`,
11
+ ],
12
+ "mr get": [2, ""],
13
+ "mr patches": [2, ""],
14
+ "mr tree": [2, "from-patch-set-id to-patch-set-id"],
15
+ "mr diff": [2, "file from-patch-set-id to-patch-set-id"],
16
+ "mr comments": [2, "file comment-type resolved patch-set-ids"],
17
+ "mr comment": [
18
+ 2,
19
+ "yes content content-file file line side from-patch-set-id to-patch-set-id parent-id",
20
+ ],
21
+ "mr create": [
22
+ 1,
23
+ "yes source target title description description-file reviewers source-project-id",
24
+ ],
25
+ "mr update": [2, "yes title description description-file"],
26
+ "mr close": [2, "yes"],
27
+ "mr review": [2, "comment comment-file opinion"],
28
+ "mr merge": [2, merge],
29
+ "mr approve-and-merge": [2, `${merge} comment comment-file`],
30
+ "mr release": [
31
+ 3,
32
+ "yes create-only title description description-file merge-type resume",
33
+ ],
34
+ "project list": [
35
+ 0,
36
+ `${pages} name conditions extra-conditions order-by sort`,
37
+ ],
38
+ "project get": [1, ""],
39
+ "project members": [1, "name role-id"],
40
+ "project member-add": [1, "yes user-ids role-id operator-id"],
41
+ "project member-remove": [1, "yes user-ids role-id operator-id"],
42
+ "member list": [0, pages],
43
+ "member get": [1, ""],
44
+ "pipeline list": [
45
+ 0,
46
+ `${pages} name status create-start-time create-end-time start-time end-time`,
47
+ ],
48
+ "pipeline runs": [
49
+ 1,
50
+ `${pages} status start-time end-time end-tme trigger-mode`,
51
+ ],
52
+ "pipeline failed-runs": [0, "hours start-time end-time end-tme per-page"],
53
+ "pipeline run": [2, ""],
54
+ "pipeline jobs": [2, ""],
55
+ "pipeline logs": [3, ""],
56
+ "pipeline start": [1, "yes params params-file"],
57
+ "pipeline cancel": [2, "yes"],
58
+ "pipeline retry-job": [3, "yes"],
59
+ "workitem list": [0, workSearch],
60
+ "workitem mine": [0, workSearch],
61
+ "workitem get": [1, ""],
62
+ "workitem activities": [1, ""],
63
+ "workitem comments": [1, ""],
64
+ "workitem timeline": [1, ""],
65
+ "workitem content": [1, "dir project-id project-ids download-attachments"],
66
+ "workitem images": [1, "dir"],
67
+ "workitem attachments": [1, "dir"],
68
+ "workitem comment": [1, "yes content content-file parent-id operator-id"],
69
+ "workitem update": [
70
+ 1,
71
+ "yes fields field assigned-to assignee participants due-date plan-finish-at due-field",
72
+ ],
73
+ "auth whoami": [0, ""],
74
+ doctor: [0, "remote"],
75
+ "config list": [0, ""],
76
+ "config show": [0, ""],
77
+ "config use": [1, ""],
78
+ "config set": [1, "token-env project-ids category"],
79
+ };
80
+ const booleans = new Set("yes json help version all-pages create-only remove-source-branch download-attachments remote resolved".split(" "));
81
+ const aliases = {
82
+ "project search": "project list",
83
+ "project add-member": "project member-add",
84
+ "project remove-member": "project member-remove",
85
+ "pipeline list-runs": "pipeline runs",
86
+ "pipeline get-run": "pipeline run",
87
+ "pipeline get": "pipeline run",
88
+ "pipeline retry": "pipeline retry-job",
89
+ "pipeline failed": "pipeline failed-runs",
90
+ "pipeline latest-failed-runs": "pipeline failed-runs",
91
+ "workitem search": "workitem list",
92
+ "workitem comment-create": "workitem comment",
93
+ "workitem history": "workitem timeline",
94
+ };
95
+ export function parseArgs(args) {
96
+ const positionals = [];
97
+ const flags = {};
98
+ for (let i = 0; i < args.length; i++) {
99
+ const arg = args[i];
100
+ if (arg === "--") {
101
+ positionals.push(...args.slice(i + 1));
102
+ break;
103
+ }
104
+ if (!arg.startsWith("--")) {
105
+ positionals.push(arg);
106
+ continue;
107
+ }
108
+ const equal = arg.indexOf("=");
109
+ const name = arg.slice(2, equal < 0 ? undefined : equal);
110
+ let value;
111
+ if (booleans.has(name)) {
112
+ const raw = equal < 0 ? undefined : arg.slice(equal + 1);
113
+ if (raw !== undefined && raw !== "true" && raw !== "false")
114
+ throw new CliError("INVALID_ARGUMENT", `--${name} expects true or false.`);
115
+ value = raw !== "false";
116
+ }
117
+ else {
118
+ if (equal >= 0)
119
+ value = arg.slice(equal + 1);
120
+ else {
121
+ if (!args[i + 1] || args[i + 1].startsWith("--"))
122
+ throw new CliError("INVALID_ARGUMENT", `--${name} requires a value.`);
123
+ value = args[++i];
124
+ }
125
+ }
126
+ if (flags[name] !== undefined && name !== "field")
127
+ throw new CliError("INVALID_ARGUMENT", `Duplicate --${name}.`);
128
+ flags[name] =
129
+ name === "field" && flags[name] !== undefined
130
+ ? [...(Array.isArray(flags[name]) ? flags[name] : [flags[name]]), value]
131
+ : value;
132
+ }
133
+ if (positionals[0] === "pl")
134
+ positionals[0] = "pipeline";
135
+ if (positionals[0] === "wi")
136
+ positionals[0] = "workitem";
137
+ const alias = aliases[positionals.slice(0, 2).join(" ")];
138
+ if (alias)
139
+ positionals.splice(0, 2, ...alias.split(" "));
140
+ return { positionals, flags };
141
+ }
142
+ export function validateArgs(parsed) {
143
+ const { positionals: p, flags: f } = parsed;
144
+ const key = p[0] === "doctor" ? "doctor" : p.slice(0, 2).join(" ");
145
+ const spec = specs[key];
146
+ if (!spec)
147
+ throw new CliError("INVALID_ARGUMENT", `Unknown command: ${key}`);
148
+ const allowed = new Set(`${globals} ${spec[1]}`.split(" "));
149
+ for (const name of Object.keys(f))
150
+ if (!allowed.has(name))
151
+ throw new CliError("INVALID_ARGUMENT", `Unknown option --${name} for ${key}.`);
152
+ const expected = (key === "doctor" ? 1 : 2) + spec[0];
153
+ if (p.length !== expected)
154
+ throw new CliError("INVALID_ARGUMENT", `${key} requires ${spec[0]} positional argument(s).`);
155
+ for (const name of [
156
+ "page",
157
+ "per-page",
158
+ "timeout-ms",
159
+ "hours",
160
+ "start-time",
161
+ "end-time",
162
+ "end-tme",
163
+ "create-start-time",
164
+ "create-end-time",
165
+ "trigger-mode",
166
+ "line",
167
+ "resume",
168
+ "retries",
169
+ ])
170
+ if (f[name] !== undefined) {
171
+ const n = Number(f[name]);
172
+ if (!Number.isSafeInteger(n) ||
173
+ n < (name === "retries" ? 0 : 1) ||
174
+ (name === "per-page" && n > 100) ||
175
+ (name === "retries" && n > 10))
176
+ throw new CliError("INVALID_ARGUMENT", `Invalid --${name}: ${f[name]}`);
177
+ }
178
+ for (const [name, values] of Object.entries({
179
+ edition: ["center", "region"],
180
+ sort: ["asc", "desc"],
181
+ opinion: ["pass", "not-pass"],
182
+ side: ["old", "new"],
183
+ output: ["table", "json", "text"],
184
+ "error-format": ["text", "json"],
185
+ "merge-type": ["ff-only", "no-fast-forward", "squash", "rebase"],
186
+ "comment-type": ["GLOBAL_COMMENT", "INLINE_COMMENT"],
187
+ }))
188
+ if (f[name] !== undefined && !values.includes(String(f[name])))
189
+ throw new CliError("INVALID_ARGUMENT", `Invalid --${name}: ${f[name]}`);
190
+ if (key === "mr list" &&
191
+ f.state !== undefined &&
192
+ !["opened", "merged", "closed"].includes(String(f.state)))
193
+ throw new CliError("INVALID_ARGUMENT", "Invalid --state.");
194
+ if (key.startsWith("workitem ") &&
195
+ f.state !== undefined &&
196
+ !["todo", "opened", "doing", "in-progress", "done", "completed"].includes(String(f.state)))
197
+ throw new CliError("INVALID_ARGUMENT", "Invalid --state.");
198
+ if (key === "mr comment" &&
199
+ f.file !== undefined &&
200
+ (!f.line || !f.side || !f["to-patch-set-id"]))
201
+ throw new CliError("INVALID_ARGUMENT", "Inline comments require --file, --line, --side and --to-patch-set-id.");
202
+ for (const [start, end] of [
203
+ ["start-time", "end-time"],
204
+ ["create-start-time", "create-end-time"],
205
+ ])
206
+ if (f[start] !== undefined &&
207
+ f[end] !== undefined &&
208
+ Number(f[start]) > Number(f[end]))
209
+ throw new CliError("INVALID_ARGUMENT", `--${start} must not exceed --${end}.`);
210
+ if (f.output === "text" && !["mr diff", "pipeline logs"].includes(key))
211
+ throw new CliError("INVALID_ARGUMENT", "--output text is only supported for mr diff and pipeline logs.");
212
+ }
213
+ export async function loadFileFlags(flags, stdin) {
214
+ let usedStdin = false;
215
+ for (const name of ["description", "content", "comment", "params"])
216
+ if (flags[`${name}-file`] !== undefined) {
217
+ if (flags[name] !== undefined)
218
+ throw new CliError("INVALID_ARGUMENT", `--${name} and --${name}-file are mutually exclusive.`);
219
+ const file = String(flags[`${name}-file`]);
220
+ if (file === "-" && usedStdin)
221
+ throw new CliError("INVALID_ARGUMENT", "stdin may only be read once.");
222
+ try {
223
+ flags[name] =
224
+ file === "-"
225
+ ? await (stdin
226
+ ? stdin()
227
+ : new Promise((resolve, reject) => {
228
+ let text = "";
229
+ process.stdin.setEncoding("utf8");
230
+ process.stdin.on("data", (chunk) => (text += chunk));
231
+ process.stdin.on("end", () => resolve(text));
232
+ process.stdin.on("error", reject);
233
+ }))
234
+ : await readFile(file, "utf8");
235
+ }
236
+ catch {
237
+ throw new CliError("INVALID_ARGUMENT", `Unable to read --${name}-file.`);
238
+ }
239
+ if (file === "-")
240
+ usedStdin = true;
241
+ }
242
+ }
243
+ export function commandHelp(p) {
244
+ const prefix = p.join(" ");
245
+ function positionalNames(key, count) {
246
+ const [scope, command] = key.split(" ");
247
+ if (!count)
248
+ return [];
249
+ if (scope === "mr")
250
+ return command === "release"
251
+ ? ["repositoryName", "sourceBranch", "targetBranch"]
252
+ : ["repositoryId", "localId"].slice(0, count);
253
+ if (scope === "pipeline")
254
+ return ["pipelineId", "pipelineRunId", "jobId"].slice(0, count);
255
+ if (scope === "project")
256
+ return ["projectId"];
257
+ if (scope === "member")
258
+ return ["memberId"];
259
+ if (scope === "config")
260
+ return ["profileName"];
261
+ return [command === "content" ? "workitemUrl" : "workitemId"];
262
+ }
263
+ const formatFlag = (flag) => `--${flag}${booleans.has(flag) ? "" : " <value>"}`;
264
+ return (Object.entries(specs)
265
+ .filter(([key]) => !prefix || key === prefix || key.startsWith(prefix + " "))
266
+ .map(([key, [count, flags]]) => `yunxiao ${key}${positionalNames(key, count)
267
+ .map((name) => ` <${name}>`)
268
+ .join("")}\n ${flags.split(" ").filter(Boolean).map(formatFlag).join(" ")}\n`)
269
+ .join("\n") +
270
+ `\nGlobal: ${globals.split(" ").map(formatFlag).join(" ")}\n`);
271
+ }
package/dist/src/cli.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type Fetcher } from "./client.js";
2
2
  export interface CliIo {
3
3
  fetcher?: Fetcher;
4
+ stdin?: () => Promise<string>;
4
5
  stdout?: (text: string) => void;
5
6
  stderr?: (text: string) => void;
6
7
  }