@coderpp/yunxiao-cli 0.1.6 → 0.1.8
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 +168 -3
- package/dist/src/cli.js +473 -70
- package/dist/src/client.d.ts +63 -2
- package/dist/src/client.js +101 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @coderpp/yunxiao-cli
|
|
2
2
|
|
|
3
|
-
用于评审和合并云效 Codeup
|
|
3
|
+
用于评审和合并云效 Codeup 合并请求、查询项目协作工作项并维护项目成员的命令行工具。
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
@@ -78,6 +78,8 @@ export YUNXIAO_TOKEN="pt-..."
|
|
|
78
78
|
export YUNXIAO_DOMAIN="openapi-rdc.aliyuncs.com"
|
|
79
79
|
export YUNXIAO_ORGANIZATION_ID="your-organization-id"
|
|
80
80
|
export YUNXIAO_EDITION="center"
|
|
81
|
+
export YUNXIAO_PROJECT_IDS="project-id-1,project-id-2"
|
|
82
|
+
export YUNXIAO_WORKITEM_CATEGORIES="Req,Task,Bug"
|
|
81
83
|
```
|
|
82
84
|
|
|
83
85
|
说明:
|
|
@@ -85,8 +87,10 @@ export YUNXIAO_EDITION="center"
|
|
|
85
87
|
- 中心版默认使用 `openapi-rdc.aliyuncs.com`,并需要 `YUNXIAO_ORGANIZATION_ID`。
|
|
86
88
|
- Region 版设置 `YUNXIAO_EDITION=region`,通常不需要组织 ID。
|
|
87
89
|
- 也可以用命令参数覆盖配置:`--token`、`--domain`、`--organization-id`、`--edition`。
|
|
90
|
+
- 工作项查询不会默认扫描所有项目,需要通过 `--project-id`、`--project-ids` 或 `YUNXIAO_PROJECT_IDS` 指定项目范围。
|
|
91
|
+
- 工作项类型默认查询 `Req,Task,Bug`,可以通过 `--category` 或 `YUNXIAO_WORKITEM_CATEGORIES` 覆盖。
|
|
88
92
|
|
|
89
|
-
##
|
|
93
|
+
## 合并请求命令
|
|
90
94
|
|
|
91
95
|
查询合并请求列表:
|
|
92
96
|
|
|
@@ -124,8 +128,40 @@ node dist/src/index.js mr approve-and-merge 2813489 12 --yes --merge-type squash
|
|
|
124
128
|
node dist/src/index.js mr release yunxiao-cli release/1.2.3 master --yes
|
|
125
129
|
```
|
|
126
130
|
|
|
131
|
+
参数顺序是:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
mr release <repositoryName> <sourceBranch> <targetBranch>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
例如,把 `release` 合并到 `master`:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
node dist/src/index.js mr release syjc-web release master --yes
|
|
141
|
+
```
|
|
142
|
+
|
|
127
143
|
这个命令会先按仓库名称查询代码库 ID,再用当前 token 对应的用户作为评审人创建合并请求,随后评审通过并合并。合并时固定保留源分支,不会删除源分支。
|
|
128
144
|
|
|
145
|
+
如果只想创建合并请求,不自动评审和合并,使用 `--create-only`:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
node dist/src/index.js mr release syjc-web release master --yes --create-only
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`--create-only` 仍会在云效创建合并请求,所以仍然需要 `--yes`。
|
|
152
|
+
|
|
153
|
+
创建合并请求时会使用云效 `createFrom=WEB`,以保证云效能正确解析源分支提交。
|
|
154
|
+
|
|
155
|
+
发布版本时,如果云效返回“源分支相对目标分支没有改动,不能新建代码评审”,可视为该仓库无改动并跳过。
|
|
156
|
+
|
|
157
|
+
发布所有版本时,推荐顺序为:
|
|
158
|
+
|
|
159
|
+
1. 商混版:`master` -> `gx-concrete/dev`
|
|
160
|
+
2. 蜀道版:`master` -> `sdjt/dev`
|
|
161
|
+
3. 标准版:`master` -> `release`
|
|
162
|
+
|
|
163
|
+
标准版应最后发布。
|
|
164
|
+
|
|
129
165
|
查询变更文件树:
|
|
130
166
|
|
|
131
167
|
```bash
|
|
@@ -148,7 +184,7 @@ node dist/src/index.js mr patches 2813489 12
|
|
|
148
184
|
node dist/src/index.js mr list --output table
|
|
149
185
|
node dist/src/index.js mr list --output json
|
|
150
186
|
node dist/src/index.js mr review 2813489 12 --output table
|
|
151
|
-
node dist/src/index.js mr release yunxiao-cli release/1.2.3 master --yes --output json
|
|
187
|
+
node dist/src/index.js mr release yunxiao-cli release/1.2.3 master --yes --create-only --output json
|
|
152
188
|
node dist/src/index.js mr patches 2813489 12 --output json
|
|
153
189
|
```
|
|
154
190
|
|
|
@@ -172,3 +208,132 @@ node dist/src/index.js mr patches 2813489 12 --output json
|
|
|
172
208
|
这是为了避免误合并。
|
|
173
209
|
|
|
174
210
|
未指定 `--merge-type` 时,默认使用 `no-fast-forward`。
|
|
211
|
+
|
|
212
|
+
## 项目与成员命令
|
|
213
|
+
|
|
214
|
+
查询项目列表:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
node dist/src/index.js project list --name 研发 --output table
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
获取项目信息:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
node dist/src/index.js project get <projectId> --output json
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
获取项目成员:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
node dist/src/index.js project members <projectId> --role-id project.admin
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
添加项目成员:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
node dist/src/index.js project member-add <projectId> \
|
|
236
|
+
--user-ids user-id-1,user-id-2 \
|
|
237
|
+
--role-id project.participant \
|
|
238
|
+
--yes
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
移除项目成员:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
node dist/src/index.js project member-remove <projectId> \
|
|
245
|
+
--user-ids user-id-1 \
|
|
246
|
+
--role-id project.participant \
|
|
247
|
+
--yes
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
查询组织成员:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
node dist/src/index.js member list --per-page 100
|
|
254
|
+
node dist/src/index.js member get <memberId> --output json
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 工作项命令
|
|
258
|
+
|
|
259
|
+
查询当前用户待办工作项:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
node dist/src/index.js workitem mine --project-ids project-id-1,project-id-2 --state todo
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
查询指定成员待办、进行中或已完成工作项:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
node dist/src/index.js workitem list --project-ids project-id-1,project-id-2 --assigned-to user-id --state todo
|
|
269
|
+
node dist/src/index.js workitem list --project-ids project-id-1,project-id-2 --assigned-to user-id --state doing
|
|
270
|
+
node dist/src/index.js workitem list --project-ids project-id-1,project-id-2 --assigned-to user-id --state done
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
按时间范围查询工作项:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
node dist/src/index.js workitem list \
|
|
277
|
+
--project-ids project-id-1,project-id-2 \
|
|
278
|
+
--from "2026-04-01 00:00:00" \
|
|
279
|
+
--to "2026-04-30 23:59:59"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
默认时间字段为 `gmtCreate`。如需按更新时间等字段过滤,可加 `--date-field gmtModified` 或其他云效支持的字段标识。
|
|
283
|
+
|
|
284
|
+
如果已知云效状态 ID,可使用服务端状态过滤:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
node dist/src/index.js workitem list --project-ids project-id-1 --status-ids 100005,100010
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`--state todo|doing|done` 是便捷过滤,会根据返回结果中的状态英文名或中文名做本地过滤;不同项目模板状态差异较大时,优先使用 `--status-ids`。
|
|
291
|
+
|
|
292
|
+
默认只查询每个项目的一页结果。需要拉取所有分页时可以加 `--all-pages`:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
node dist/src/index.js workitem list --project-ids project-id-1,project-id-2 --all-pages
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
获取工作项详情、动态、评论:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
node dist/src/index.js workitem get <workitemId> --output json
|
|
302
|
+
node dist/src/index.js workitem activities <workitemId>
|
|
303
|
+
node dist/src/index.js workitem comments <workitemId>
|
|
304
|
+
node dist/src/index.js workitem timeline <workitemId> --output json
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
修改工作项负责人、参与人和计划完成时间:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
node dist/src/index.js workitem update <workitemId> \
|
|
311
|
+
--assigned-to user-id \
|
|
312
|
+
--participants user-id-1,user-id-2 \
|
|
313
|
+
--due-date "2026-05-01 18:00:00" \
|
|
314
|
+
--yes
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
计划完成时间默认写入字段 `dueDate`。如果项目模板使用的是自定义字段,可以用 `--due-field <fieldId>` 覆盖:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
node dist/src/index.js workitem update <workitemId> \
|
|
321
|
+
--due-field custom-plan-finish-field-id \
|
|
322
|
+
--due-date "2026-05-01 18:00:00" \
|
|
323
|
+
--yes
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
也可以用 `--field key=value` 或 `--fields '{"fieldId":"value"}'` 更新云效支持的其他字段。
|
|
327
|
+
|
|
328
|
+
创建工作项评论:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
node dist/src/index.js workitem comment <workitemId> --content "处理完成,请确认。" --yes
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
会改变云效数据的命令需要 `--yes`:
|
|
335
|
+
|
|
336
|
+
- `project member-add`
|
|
337
|
+
- `project member-remove`
|
|
338
|
+
- `workitem update`
|
|
339
|
+
- `workitem comment`
|
package/dist/src/cli.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { YunxiaoClient } from "./client.js";
|
|
2
2
|
const mergeTypes = new Set(["ff-only", "no-fast-forward", "squash", "rebase"]);
|
|
3
3
|
const outputFormats = new Set(["table", "json"]);
|
|
4
|
+
const booleanFlags = new Set(["all-pages", "create-only", "help", "json", "remove-source-branch", "yes"]);
|
|
5
|
+
const workitemStateNames = new Map([
|
|
6
|
+
["todo", ["TODO", "待处理", "待办"]],
|
|
7
|
+
["opened", ["TODO", "待处理", "待办"]],
|
|
8
|
+
["doing", ["DOING", "IN_PROGRESS", "PROCESSING", "进行中", "处理中"]],
|
|
9
|
+
["in-progress", ["DOING", "IN_PROGRESS", "PROCESSING", "进行中", "处理中"]],
|
|
10
|
+
["done", ["DONE", "COMPLETED", "CLOSED", "已完成", "完成", "已关闭"]],
|
|
11
|
+
["completed", ["DONE", "COMPLETED", "CLOSED", "已完成", "完成", "已关闭"]]
|
|
12
|
+
]);
|
|
4
13
|
export async function runCli(args = process.argv.slice(2), env = process.env, io = {}) {
|
|
5
14
|
const stdout = io.stdout ?? ((text) => process.stdout.write(text));
|
|
6
15
|
const stderr = io.stderr ?? ((text) => process.stderr.write(text));
|
|
@@ -10,64 +19,13 @@ export async function runCli(args = process.argv.slice(2), env = process.env, io
|
|
|
10
19
|
stdout(helpText());
|
|
11
20
|
return 0;
|
|
12
21
|
}
|
|
13
|
-
const [scope, command
|
|
14
|
-
if (
|
|
22
|
+
const [scope, command] = parsed.positionals;
|
|
23
|
+
if (!command) {
|
|
15
24
|
throw new Error(`Unknown command: ${parsed.positionals.join(" ")}`);
|
|
16
25
|
}
|
|
17
26
|
const outputFormat = outputFormatFromFlags(parsed.flags);
|
|
18
27
|
const client = createClient(parsed.flags, env, io.fetcher);
|
|
19
|
-
|
|
20
|
-
if (command === "list") {
|
|
21
|
-
result = await client.listChangeRequests(listOptions(parsed.flags));
|
|
22
|
-
printResult(result, outputFormat, stdout);
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
25
|
-
if (!repositoryId || !localIdRaw) {
|
|
26
|
-
throw new Error(`Command mr ${command} requires <repositoryId> and <localId>.`);
|
|
27
|
-
}
|
|
28
|
-
if (command === "release") {
|
|
29
|
-
requireConfirmation(parsed.flags);
|
|
30
|
-
result = await client.releaseByRepositoryName({
|
|
31
|
-
repositoryName: repositoryId,
|
|
32
|
-
sourceBranch: localIdRaw,
|
|
33
|
-
targetBranch: requiredPositional(parsed.positionals[4], "targetBranch"),
|
|
34
|
-
title: optionalString(parsed.flags.title),
|
|
35
|
-
description: optionalString(parsed.flags.description),
|
|
36
|
-
mergeType: mergeOptions(parsed.flags).mergeType
|
|
37
|
-
});
|
|
38
|
-
printResult(result, outputFormat, stdout);
|
|
39
|
-
return 0;
|
|
40
|
-
}
|
|
41
|
-
const localId = parseLocalId(localIdRaw);
|
|
42
|
-
switch (command) {
|
|
43
|
-
case "review":
|
|
44
|
-
result = await client.reviewChangeRequest(repositoryId, localId, {
|
|
45
|
-
reviewComment: optionalString(parsed.flags.comment)
|
|
46
|
-
});
|
|
47
|
-
break;
|
|
48
|
-
case "merge":
|
|
49
|
-
requireConfirmation(parsed.flags);
|
|
50
|
-
result = await client.mergeChangeRequest(repositoryId, localId, mergeOptions(parsed.flags));
|
|
51
|
-
break;
|
|
52
|
-
case "approve-and-merge":
|
|
53
|
-
requireConfirmation(parsed.flags);
|
|
54
|
-
result = await client.approveAndMerge(repositoryId, localId, {
|
|
55
|
-
reviewComment: optionalString(parsed.flags.comment),
|
|
56
|
-
...mergeOptions(parsed.flags)
|
|
57
|
-
});
|
|
58
|
-
break;
|
|
59
|
-
case "tree":
|
|
60
|
-
result = await client.getChangeRequestTree(repositoryId, localId, {
|
|
61
|
-
fromPatchSetId: requiredFlag(parsed.flags, "from-patch-set-id"),
|
|
62
|
-
toPatchSetId: requiredFlag(parsed.flags, "to-patch-set-id")
|
|
63
|
-
});
|
|
64
|
-
break;
|
|
65
|
-
case "patches":
|
|
66
|
-
result = await client.listChangeRequestPatchSets(repositoryId, localId);
|
|
67
|
-
break;
|
|
68
|
-
default:
|
|
69
|
-
throw new Error(`Unknown mr command: ${command}`);
|
|
70
|
-
}
|
|
28
|
+
const result = await runCommand(scope, command, parsed, client, env);
|
|
71
29
|
printResult(result, outputFormat, stdout);
|
|
72
30
|
return 0;
|
|
73
31
|
}
|
|
@@ -76,6 +34,147 @@ export async function runCli(args = process.argv.slice(2), env = process.env, io
|
|
|
76
34
|
return 1;
|
|
77
35
|
}
|
|
78
36
|
}
|
|
37
|
+
async function runCommand(scope, command, parsed, client, env) {
|
|
38
|
+
switch (scope) {
|
|
39
|
+
case "mr":
|
|
40
|
+
return runMergeRequestCommand(command, parsed, client);
|
|
41
|
+
case "project":
|
|
42
|
+
return runProjectCommand(command, parsed, client);
|
|
43
|
+
case "member":
|
|
44
|
+
return runMemberCommand(command, parsed, client);
|
|
45
|
+
case "workitem":
|
|
46
|
+
case "wi":
|
|
47
|
+
return runWorkitemCommand(command, parsed, client, env);
|
|
48
|
+
default:
|
|
49
|
+
throw new Error(`Unknown command scope: ${scope}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function runMergeRequestCommand(command, parsed, client) {
|
|
53
|
+
const [, , repositoryId, localIdRaw] = parsed.positionals;
|
|
54
|
+
if (command === "list") {
|
|
55
|
+
return client.listChangeRequests(listOptions(parsed.flags));
|
|
56
|
+
}
|
|
57
|
+
if (!repositoryId || !localIdRaw) {
|
|
58
|
+
throw new Error(`Command mr ${command} requires <repositoryId> and <localId>.`);
|
|
59
|
+
}
|
|
60
|
+
if (command === "release") {
|
|
61
|
+
requireConfirmation(parsed.flags);
|
|
62
|
+
return client.releaseByRepositoryName({
|
|
63
|
+
repositoryName: repositoryId,
|
|
64
|
+
sourceBranch: localIdRaw,
|
|
65
|
+
targetBranch: requiredPositional(parsed.positionals[4], "targetBranch"),
|
|
66
|
+
createOnly: hasFlag(parsed.flags, "create-only"),
|
|
67
|
+
title: optionalString(parsed.flags.title),
|
|
68
|
+
description: optionalString(parsed.flags.description),
|
|
69
|
+
mergeType: mergeOptions(parsed.flags).mergeType
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const localId = parseLocalId(localIdRaw);
|
|
73
|
+
switch (command) {
|
|
74
|
+
case "review":
|
|
75
|
+
return client.reviewChangeRequest(repositoryId, localId, {
|
|
76
|
+
reviewComment: optionalString(parsed.flags.comment)
|
|
77
|
+
});
|
|
78
|
+
case "merge":
|
|
79
|
+
requireConfirmation(parsed.flags);
|
|
80
|
+
return client.mergeChangeRequest(repositoryId, localId, mergeOptions(parsed.flags));
|
|
81
|
+
case "approve-and-merge":
|
|
82
|
+
requireConfirmation(parsed.flags);
|
|
83
|
+
return client.approveAndMerge(repositoryId, localId, {
|
|
84
|
+
reviewComment: optionalString(parsed.flags.comment),
|
|
85
|
+
...mergeOptions(parsed.flags)
|
|
86
|
+
});
|
|
87
|
+
case "tree":
|
|
88
|
+
return client.getChangeRequestTree(repositoryId, localId, {
|
|
89
|
+
fromPatchSetId: requiredFlag(parsed.flags, "from-patch-set-id"),
|
|
90
|
+
toPatchSetId: requiredFlag(parsed.flags, "to-patch-set-id")
|
|
91
|
+
});
|
|
92
|
+
case "patches":
|
|
93
|
+
return client.listChangeRequestPatchSets(repositoryId, localId);
|
|
94
|
+
default:
|
|
95
|
+
throw new Error(`Unknown mr command: ${command}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function runProjectCommand(command, parsed, client) {
|
|
99
|
+
const projectId = parsed.positionals[2];
|
|
100
|
+
switch (command) {
|
|
101
|
+
case "list":
|
|
102
|
+
case "search":
|
|
103
|
+
return client.searchProjects(projectSearchOptions(parsed.flags));
|
|
104
|
+
case "get":
|
|
105
|
+
return client.getProject(requiredPositional(projectId, "projectId"));
|
|
106
|
+
case "members":
|
|
107
|
+
return client.listProjectMembers(requiredPositional(projectId, "projectId"), {
|
|
108
|
+
name: optionalString(parsed.flags.name),
|
|
109
|
+
roleId: optionalString(parsed.flags["role-id"])
|
|
110
|
+
});
|
|
111
|
+
case "member-add":
|
|
112
|
+
case "add-member":
|
|
113
|
+
requireConfirmation(parsed.flags);
|
|
114
|
+
return client.createProjectMember(requiredPositional(projectId, "projectId"), {
|
|
115
|
+
operatorId: optionalString(parsed.flags["operator-id"]),
|
|
116
|
+
roleId: optionalString(parsed.flags["role-id"]) ?? "project.participant",
|
|
117
|
+
userIds: requiredCsvFlag(parsed.flags, "user-ids")
|
|
118
|
+
});
|
|
119
|
+
case "member-remove":
|
|
120
|
+
case "remove-member":
|
|
121
|
+
requireConfirmation(parsed.flags);
|
|
122
|
+
return client.deleteProjectMember(requiredPositional(projectId, "projectId"), {
|
|
123
|
+
operatorId: optionalString(parsed.flags["operator-id"]),
|
|
124
|
+
roleId: optionalString(parsed.flags["role-id"]) ?? "project.participant",
|
|
125
|
+
userIds: requiredCsvFlag(parsed.flags, "user-ids")
|
|
126
|
+
});
|
|
127
|
+
default:
|
|
128
|
+
throw new Error(`Unknown project command: ${command}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function runMemberCommand(command, parsed, client) {
|
|
132
|
+
switch (command) {
|
|
133
|
+
case "list":
|
|
134
|
+
return client.listMembers({
|
|
135
|
+
page: optionalNumber(parsed.flags.page),
|
|
136
|
+
perPage: optionalNumber(parsed.flags["per-page"])
|
|
137
|
+
});
|
|
138
|
+
case "get":
|
|
139
|
+
return client.getMember(requiredPositional(parsed.positionals[2], "memberId"));
|
|
140
|
+
default:
|
|
141
|
+
throw new Error(`Unknown member command: ${command}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function runWorkitemCommand(command, parsed, client, env) {
|
|
145
|
+
const workitemId = parsed.positionals[2];
|
|
146
|
+
switch (command) {
|
|
147
|
+
case "list":
|
|
148
|
+
case "search":
|
|
149
|
+
return searchWorkitems(client, parsed.flags, env);
|
|
150
|
+
case "mine": {
|
|
151
|
+
const currentUser = await client.getCurrentUser();
|
|
152
|
+
return searchWorkitems(client, { ...parsed.flags, "assigned-to": currentUser.id }, env);
|
|
153
|
+
}
|
|
154
|
+
case "get":
|
|
155
|
+
return client.getWorkitem(requiredPositional(workitemId, "workitemId"));
|
|
156
|
+
case "update":
|
|
157
|
+
requireConfirmation(parsed.flags);
|
|
158
|
+
return client.updateWorkitem(requiredPositional(workitemId, "workitemId"), workitemUpdateBody(parsed.flags));
|
|
159
|
+
case "activities":
|
|
160
|
+
return client.listWorkitemActivities(requiredPositional(workitemId, "workitemId"));
|
|
161
|
+
case "comments":
|
|
162
|
+
return client.listWorkitemComments(requiredPositional(workitemId, "workitemId"));
|
|
163
|
+
case "comment":
|
|
164
|
+
case "comment-create":
|
|
165
|
+
requireConfirmation(parsed.flags);
|
|
166
|
+
return client.createWorkitemComment(requiredPositional(workitemId, "workitemId"), {
|
|
167
|
+
content: requiredFlag(parsed.flags, "content"),
|
|
168
|
+
operatorId: optionalString(parsed.flags["operator-id"]),
|
|
169
|
+
parentId: optionalString(parsed.flags["parent-id"])
|
|
170
|
+
});
|
|
171
|
+
case "timeline":
|
|
172
|
+
case "history":
|
|
173
|
+
return client.getWorkitemTimeline(requiredPositional(workitemId, "workitemId"));
|
|
174
|
+
default:
|
|
175
|
+
throw new Error(`Unknown workitem command: ${command}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
79
178
|
function createClient(flags, env, fetcher) {
|
|
80
179
|
return new YunxiaoClient({
|
|
81
180
|
token: stringFlag(flags.token) ?? env.YUNXIAO_TOKEN ?? "",
|
|
@@ -96,20 +195,28 @@ function parseArgs(args) {
|
|
|
96
195
|
}
|
|
97
196
|
const [rawName, inlineValue] = arg.slice(2).split("=", 2);
|
|
98
197
|
if (inlineValue !== undefined) {
|
|
99
|
-
flags
|
|
198
|
+
setFlag(flags, rawName, inlineValue);
|
|
100
199
|
continue;
|
|
101
200
|
}
|
|
102
201
|
const next = args[index + 1];
|
|
103
|
-
if (next && !next.startsWith("--") && !
|
|
104
|
-
flags
|
|
202
|
+
if (next && !next.startsWith("--") && !booleanFlags.has(rawName)) {
|
|
203
|
+
setFlag(flags, rawName, next);
|
|
105
204
|
index += 1;
|
|
106
205
|
}
|
|
107
206
|
else {
|
|
108
|
-
flags
|
|
207
|
+
setFlag(flags, rawName, true);
|
|
109
208
|
}
|
|
110
209
|
}
|
|
111
210
|
return { positionals, flags };
|
|
112
211
|
}
|
|
212
|
+
function setFlag(flags, name, value) {
|
|
213
|
+
const existing = flags[name];
|
|
214
|
+
if (existing === undefined) {
|
|
215
|
+
flags[name] = value;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
flags[name] = [...flagValues(existing), ...flagValues(value)];
|
|
219
|
+
}
|
|
113
220
|
function listOptions(flags) {
|
|
114
221
|
return {
|
|
115
222
|
page: optionalNumber(flags.page),
|
|
@@ -125,6 +232,135 @@ function listOptions(flags) {
|
|
|
125
232
|
createdAfter: optionalString(flags["created-after"])
|
|
126
233
|
};
|
|
127
234
|
}
|
|
235
|
+
function projectSearchOptions(flags) {
|
|
236
|
+
return {
|
|
237
|
+
conditions: optionalString(flags.conditions) ?? projectNameConditions(optionalString(flags.name)),
|
|
238
|
+
extraConditions: optionalString(flags["extra-conditions"]),
|
|
239
|
+
orderBy: optionalString(flags["order-by"]) ?? "gmtCreate",
|
|
240
|
+
page: optionalNumber(flags.page) ?? 1,
|
|
241
|
+
perPage: optionalNumber(flags["per-page"]) ?? 20,
|
|
242
|
+
sort: optionalString(flags.sort) ?? "desc"
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function searchWorkitems(client, flags, env) {
|
|
246
|
+
const projectIds = workitemProjectIds(flags, env);
|
|
247
|
+
const perPage = optionalNumber(flags["per-page"]) ?? 20;
|
|
248
|
+
const firstPage = optionalNumber(flags.page) ?? 1;
|
|
249
|
+
const results = [];
|
|
250
|
+
for (const projectId of projectIds) {
|
|
251
|
+
let page = firstPage;
|
|
252
|
+
while (true) {
|
|
253
|
+
const response = await client.searchWorkitems(workitemSearchOptions(flags, projectId, page, perPage, env));
|
|
254
|
+
const rows = Array.isArray(response) ? response : [response];
|
|
255
|
+
results.push(...rows.map((row) => addQueryProjectId(row, projectId)));
|
|
256
|
+
if (!hasFlag(flags, "all-pages") || !Array.isArray(response) || response.length < perPage) {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
page += 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return filterWorkitems(results, flags);
|
|
263
|
+
}
|
|
264
|
+
function workitemSearchOptions(flags, projectId, page, perPage, env) {
|
|
265
|
+
return {
|
|
266
|
+
category: optionalString(flags.category) ?? env.YUNXIAO_WORKITEM_CATEGORIES ?? "Req,Task,Bug",
|
|
267
|
+
conditions: workitemConditions(flags),
|
|
268
|
+
orderBy: optionalString(flags["order-by"]) ?? "gmtCreate",
|
|
269
|
+
page,
|
|
270
|
+
perPage,
|
|
271
|
+
sort: optionalString(flags.sort) ?? "desc",
|
|
272
|
+
spaceId: projectId,
|
|
273
|
+
spaceType: optionalString(flags["space-type"]) ?? "Project"
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function workitemConditions(flags) {
|
|
277
|
+
const rawConditions = optionalString(flags.conditions);
|
|
278
|
+
const filters = [];
|
|
279
|
+
const assignedTo = optionalString(flags["assigned-to"]) ?? optionalString(flags.assignee);
|
|
280
|
+
const creator = optionalString(flags.creator);
|
|
281
|
+
const statusIds = optionalString(flags["status-ids"]);
|
|
282
|
+
const subject = optionalString(flags.subject);
|
|
283
|
+
const from = optionalString(flags.from) ?? optionalString(flags["created-after"]);
|
|
284
|
+
const to = optionalString(flags.to) ?? optionalString(flags["created-before"]);
|
|
285
|
+
if (rawConditions && (assignedTo || creator || statusIds || subject || from || to)) {
|
|
286
|
+
throw new Error("--conditions cannot be combined with shortcut workitem filters.");
|
|
287
|
+
}
|
|
288
|
+
if (rawConditions) {
|
|
289
|
+
return rawConditions;
|
|
290
|
+
}
|
|
291
|
+
if (assignedTo) {
|
|
292
|
+
filters.push(userCondition("assignedTo", csvValues(assignedTo)));
|
|
293
|
+
}
|
|
294
|
+
if (creator) {
|
|
295
|
+
filters.push(userCondition("creator", csvValues(creator)));
|
|
296
|
+
}
|
|
297
|
+
if (statusIds) {
|
|
298
|
+
filters.push({
|
|
299
|
+
fieldIdentifier: "status",
|
|
300
|
+
operator: "CONTAINS",
|
|
301
|
+
value: csvValues(statusIds),
|
|
302
|
+
toValue: null,
|
|
303
|
+
className: "status",
|
|
304
|
+
format: "list"
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (subject) {
|
|
308
|
+
filters.push({
|
|
309
|
+
fieldIdentifier: "subject",
|
|
310
|
+
operator: "CONTAINS",
|
|
311
|
+
value: [subject],
|
|
312
|
+
toValue: null,
|
|
313
|
+
className: "string",
|
|
314
|
+
format: "input"
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (from || to) {
|
|
318
|
+
filters.push({
|
|
319
|
+
fieldIdentifier: optionalString(flags["date-field"]) ?? "gmtCreate",
|
|
320
|
+
operator: "BETWEEN",
|
|
321
|
+
value: from ? [from] : [],
|
|
322
|
+
toValue: to ?? null,
|
|
323
|
+
className: "dateTime",
|
|
324
|
+
format: "input"
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return filters.length > 0 ? JSON.stringify({ conditionGroups: [filters] }) : undefined;
|
|
328
|
+
}
|
|
329
|
+
function userCondition(fieldIdentifier, value) {
|
|
330
|
+
return {
|
|
331
|
+
fieldIdentifier,
|
|
332
|
+
operator: "CONTAINS",
|
|
333
|
+
value,
|
|
334
|
+
toValue: null,
|
|
335
|
+
className: "user",
|
|
336
|
+
format: "list"
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function workitemUpdateBody(flags) {
|
|
340
|
+
const body = {
|
|
341
|
+
...jsonObjectFlag(flags.fields)
|
|
342
|
+
};
|
|
343
|
+
for (const field of flagStrings(flags.field)) {
|
|
344
|
+
const [key, value] = parseKeyValue(field, "--field");
|
|
345
|
+
body[key] = parseValue(value);
|
|
346
|
+
}
|
|
347
|
+
const assignedTo = optionalString(flags["assigned-to"]) ?? optionalString(flags.assignee);
|
|
348
|
+
if (assignedTo) {
|
|
349
|
+
body.assignedTo = assignedTo;
|
|
350
|
+
}
|
|
351
|
+
const participants = optionalString(flags.participants);
|
|
352
|
+
if (participants) {
|
|
353
|
+
body.participants = csvValues(participants);
|
|
354
|
+
}
|
|
355
|
+
const dueDate = optionalString(flags["due-date"]) ?? optionalString(flags["plan-finish-at"]);
|
|
356
|
+
if (dueDate) {
|
|
357
|
+
body[optionalString(flags["due-field"]) ?? "dueDate"] = dueDate;
|
|
358
|
+
}
|
|
359
|
+
if (Object.keys(body).length === 0) {
|
|
360
|
+
throw new Error("No workitem fields to update. Use --assigned-to, --participants, --due-date, --fields, or --field.");
|
|
361
|
+
}
|
|
362
|
+
return body;
|
|
363
|
+
}
|
|
128
364
|
function mergeOptions(flags) {
|
|
129
365
|
const mergeType = optionalString(flags["merge-type"]) ?? "no-fast-forward";
|
|
130
366
|
if (!mergeTypes.has(mergeType)) {
|
|
@@ -133,20 +369,20 @@ function mergeOptions(flags) {
|
|
|
133
369
|
return {
|
|
134
370
|
mergeMessage: optionalString(flags.message),
|
|
135
371
|
mergeType: mergeType,
|
|
136
|
-
removeSourceBranch:
|
|
372
|
+
removeSourceBranch: hasFlag(flags, "remove-source-branch")
|
|
137
373
|
};
|
|
138
374
|
}
|
|
139
375
|
function requireConfirmation(flags) {
|
|
140
|
-
if (!flags
|
|
141
|
-
throw new Error("This command changes
|
|
376
|
+
if (!hasFlag(flags, "yes")) {
|
|
377
|
+
throw new Error("This command changes state. Re-run with --yes after you have reviewed the operation.");
|
|
142
378
|
}
|
|
143
379
|
}
|
|
144
380
|
function outputFormatFromFlags(flags) {
|
|
145
381
|
const requestedOutput = optionalString(flags.output);
|
|
146
|
-
if (flags
|
|
382
|
+
if (hasFlag(flags, "json") && requestedOutput && requestedOutput !== "json") {
|
|
147
383
|
throw new Error("--json cannot be combined with --output table.");
|
|
148
384
|
}
|
|
149
|
-
const output = flags
|
|
385
|
+
const output = hasFlag(flags, "json") ? "json" : requestedOutput ?? "table";
|
|
150
386
|
if (!outputFormats.has(output)) {
|
|
151
387
|
throw new Error(`Invalid --output: ${output}. Supported formats: table, json.`);
|
|
152
388
|
}
|
|
@@ -176,7 +412,26 @@ function formatTable(rows) {
|
|
|
176
412
|
return "No records found.\n";
|
|
177
413
|
}
|
|
178
414
|
const normalizedRows = rows.map((row) => (isRecord(row) ? row : { value: row }));
|
|
179
|
-
const preferredKeys = [
|
|
415
|
+
const preferredKeys = [
|
|
416
|
+
"localId",
|
|
417
|
+
"projectId",
|
|
418
|
+
"queryProjectId",
|
|
419
|
+
"id",
|
|
420
|
+
"serialNumber",
|
|
421
|
+
"name",
|
|
422
|
+
"subject",
|
|
423
|
+
"title",
|
|
424
|
+
"state",
|
|
425
|
+
"status",
|
|
426
|
+
"assignedTo",
|
|
427
|
+
"userId",
|
|
428
|
+
"userName",
|
|
429
|
+
"roleName",
|
|
430
|
+
"email",
|
|
431
|
+
"sourceBranch",
|
|
432
|
+
"targetBranch",
|
|
433
|
+
"detailUrl"
|
|
434
|
+
].filter((key) => normalizedRows.some((row) => row[key] !== undefined));
|
|
180
435
|
const keys = preferredKeys.length > 0 ? preferredKeys : uniqueKeys(normalizedRows);
|
|
181
436
|
const widths = keys.map((key) => Math.max(key.length, ...normalizedRows.map((row) => formatCellValue(row[key]).length)));
|
|
182
437
|
const line = (values) => values.map((value, index) => value.padEnd(widths[index])).join(" ");
|
|
@@ -200,7 +455,14 @@ function formatCellValue(value) {
|
|
|
200
455
|
if (value === null || value === undefined) {
|
|
201
456
|
return "";
|
|
202
457
|
}
|
|
203
|
-
if (
|
|
458
|
+
if (Array.isArray(value)) {
|
|
459
|
+
return value.map(formatCellValue).join(",");
|
|
460
|
+
}
|
|
461
|
+
if (isRecord(value)) {
|
|
462
|
+
const displayValue = value.displayName ?? value.name ?? value.userName ?? value.title ?? value.id;
|
|
463
|
+
if (displayValue !== undefined) {
|
|
464
|
+
return String(displayValue);
|
|
465
|
+
}
|
|
204
466
|
return JSON.stringify(value);
|
|
205
467
|
}
|
|
206
468
|
return String(value);
|
|
@@ -225,19 +487,143 @@ function requiredFlag(flags, name) {
|
|
|
225
487
|
}
|
|
226
488
|
return value;
|
|
227
489
|
}
|
|
490
|
+
function requiredCsvFlag(flags, name) {
|
|
491
|
+
return csvValues(requiredFlag(flags, name));
|
|
492
|
+
}
|
|
493
|
+
function projectNameConditions(name) {
|
|
494
|
+
if (!name) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
return JSON.stringify({
|
|
498
|
+
conditionGroups: [
|
|
499
|
+
[
|
|
500
|
+
{
|
|
501
|
+
className: "string",
|
|
502
|
+
fieldIdentifier: "name",
|
|
503
|
+
format: "input",
|
|
504
|
+
operator: "CONTAINS",
|
|
505
|
+
toValue: null,
|
|
506
|
+
value: [name]
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
]
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function workitemProjectIds(flags, env) {
|
|
513
|
+
const value = optionalString(flags["project-ids"]) ?? optionalString(flags["project-id"]) ?? env.YUNXIAO_PROJECT_IDS;
|
|
514
|
+
if (!value) {
|
|
515
|
+
throw new Error("Missing workitem project scope. Set --project-id, --project-ids, or YUNXIAO_PROJECT_IDS.");
|
|
516
|
+
}
|
|
517
|
+
return csvValues(value);
|
|
518
|
+
}
|
|
519
|
+
function addQueryProjectId(row, projectId) {
|
|
520
|
+
return isRecord(row) ? { queryProjectId: projectId, ...row } : row;
|
|
521
|
+
}
|
|
522
|
+
function filterWorkitems(rows, flags) {
|
|
523
|
+
const state = optionalString(flags.state);
|
|
524
|
+
const statusNameEn = optionalString(flags["status-name-en"]);
|
|
525
|
+
const statusName = optionalString(flags["status-name"]);
|
|
526
|
+
return rows.filter((row) => {
|
|
527
|
+
if (!isRecord(row)) {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
if (statusNameEn && statusText(row.status, "nameEn") !== statusNameEn) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
if (statusName && statusText(row.status, "name") !== statusName && statusText(row.status, "displayName") !== statusName) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
if (state && state !== "all") {
|
|
537
|
+
const expected = workitemStateNames.get(state);
|
|
538
|
+
if (!expected) {
|
|
539
|
+
throw new Error(`Invalid --state: ${state}. Supported values: todo, doing, in-progress, done, completed, all.`);
|
|
540
|
+
}
|
|
541
|
+
const actualValues = [
|
|
542
|
+
statusText(row.status, "nameEn"),
|
|
543
|
+
statusText(row.status, "name"),
|
|
544
|
+
statusText(row.status, "displayName")
|
|
545
|
+
].filter((value) => Boolean(value));
|
|
546
|
+
return actualValues.some((actual) => expected.includes(actual));
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
function statusText(value, key) {
|
|
552
|
+
if (!isRecord(value)) {
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
const text = value[key];
|
|
556
|
+
return typeof text === "string" ? text : undefined;
|
|
557
|
+
}
|
|
558
|
+
function jsonObjectFlag(value) {
|
|
559
|
+
const raw = optionalString(value);
|
|
560
|
+
if (!raw) {
|
|
561
|
+
return {};
|
|
562
|
+
}
|
|
563
|
+
const parsed = JSON.parse(raw);
|
|
564
|
+
if (!isRecord(parsed) || Array.isArray(parsed)) {
|
|
565
|
+
throw new Error("--fields must be a JSON object.");
|
|
566
|
+
}
|
|
567
|
+
return parsed;
|
|
568
|
+
}
|
|
569
|
+
function parseKeyValue(value, flagName) {
|
|
570
|
+
const separatorIndex = value.indexOf("=");
|
|
571
|
+
if (separatorIndex <= 0) {
|
|
572
|
+
throw new Error(`${flagName} expects key=value, got: ${value}`);
|
|
573
|
+
}
|
|
574
|
+
return [value.slice(0, separatorIndex), value.slice(separatorIndex + 1)];
|
|
575
|
+
}
|
|
576
|
+
function parseValue(value) {
|
|
577
|
+
if (value === "true") {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
if (value === "false") {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
if (value === "null") {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
if (/^[\[{]/.test(value)) {
|
|
587
|
+
return JSON.parse(value);
|
|
588
|
+
}
|
|
589
|
+
return value;
|
|
590
|
+
}
|
|
591
|
+
function csvValues(value) {
|
|
592
|
+
return value
|
|
593
|
+
.split(",")
|
|
594
|
+
.map((item) => item.trim())
|
|
595
|
+
.filter(Boolean);
|
|
596
|
+
}
|
|
597
|
+
function flagValues(value) {
|
|
598
|
+
return Array.isArray(value) ? value : [value];
|
|
599
|
+
}
|
|
600
|
+
function flagStrings(value) {
|
|
601
|
+
if (value === undefined) {
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
return flagValues(value).filter((item) => typeof item === "string");
|
|
605
|
+
}
|
|
606
|
+
function hasFlag(flags, name) {
|
|
607
|
+
const value = flags[name];
|
|
608
|
+
return value === true || (Array.isArray(value) && value.includes(true));
|
|
609
|
+
}
|
|
228
610
|
function optionalString(value) {
|
|
611
|
+
if (Array.isArray(value)) {
|
|
612
|
+
return [...value].reverse().find((item) => typeof item === "string");
|
|
613
|
+
}
|
|
229
614
|
return typeof value === "string" ? value : undefined;
|
|
230
615
|
}
|
|
231
616
|
function stringFlag(value) {
|
|
232
617
|
return optionalString(value);
|
|
233
618
|
}
|
|
234
619
|
function optionalNumber(value) {
|
|
235
|
-
|
|
620
|
+
const stringValue = optionalString(value);
|
|
621
|
+
if (!stringValue) {
|
|
236
622
|
return undefined;
|
|
237
623
|
}
|
|
238
|
-
const number = Number(
|
|
624
|
+
const number = Number(stringValue);
|
|
239
625
|
if (!Number.isInteger(number) || number <= 0) {
|
|
240
|
-
throw new Error(`Expected a positive integer, got: ${
|
|
626
|
+
throw new Error(`Expected a positive integer, got: ${stringValue}`);
|
|
241
627
|
}
|
|
242
628
|
return number;
|
|
243
629
|
}
|
|
@@ -245,16 +631,31 @@ function isRecord(value) {
|
|
|
245
631
|
return typeof value === "object" && value !== null;
|
|
246
632
|
}
|
|
247
633
|
function helpText() {
|
|
248
|
-
return `yunxiao - Yunxiao
|
|
634
|
+
return `yunxiao - Yunxiao DevOps command helper
|
|
249
635
|
|
|
250
636
|
Usage:
|
|
251
637
|
yunxiao mr list [--state opened] [--project-ids 2813489] [--output table|json]
|
|
252
638
|
yunxiao mr review <repositoryId> <localId> [--comment "LGTM"] [--output table|json]
|
|
253
639
|
yunxiao mr merge <repositoryId> <localId> --yes [--merge-type no-fast-forward] [--output table|json]
|
|
254
640
|
yunxiao mr approve-and-merge <repositoryId> <localId> --yes [--merge-type no-fast-forward] [--output table|json]
|
|
255
|
-
yunxiao mr release <repositoryName> <sourceBranch> <targetBranch> --yes [--title "Release"] [--output table|json]
|
|
641
|
+
yunxiao mr release <repositoryName> <sourceBranch> <targetBranch> --yes [--create-only] [--title "Release"] [--output table|json]
|
|
256
642
|
yunxiao mr tree <repositoryId> <localId> --from-patch-set-id <id> --to-patch-set-id <id> [--output table|json]
|
|
257
643
|
yunxiao mr patches <repositoryId> <localId> [--output table|json]
|
|
644
|
+
yunxiao project list [--name keyword] [--output table|json]
|
|
645
|
+
yunxiao project get <projectId> [--output table|json]
|
|
646
|
+
yunxiao project members <projectId> [--name keyword] [--role-id project.admin] [--output table|json]
|
|
647
|
+
yunxiao project member-add <projectId> --user-ids user1,user2 [--role-id project.participant] --yes
|
|
648
|
+
yunxiao project member-remove <projectId> --user-ids user1,user2 [--role-id project.participant] --yes
|
|
649
|
+
yunxiao member list [--page 1] [--per-page 100] [--output table|json]
|
|
650
|
+
yunxiao member get <memberId> [--output table|json]
|
|
651
|
+
yunxiao workitem list --project-ids p1,p2 [--assigned-to userId] [--state todo|doing|done] [--from "2026-04-01 00:00:00"] [--to "2026-04-30 23:59:59"]
|
|
652
|
+
yunxiao workitem mine --project-ids p1,p2 [--state todo|doing|done] [--output table|json]
|
|
653
|
+
yunxiao workitem get <workitemId> [--output table|json]
|
|
654
|
+
yunxiao workitem update <workitemId> --yes [--assigned-to userId] [--participants user1,user2] [--due-date "2026-05-01 18:00:00"] [--field key=value]
|
|
655
|
+
yunxiao workitem activities <workitemId> [--output table|json]
|
|
656
|
+
yunxiao workitem comments <workitemId> [--output table|json]
|
|
657
|
+
yunxiao workitem comment <workitemId> --content "comment" --yes [--output table|json]
|
|
658
|
+
yunxiao workitem timeline <workitemId> [--output table|json]
|
|
258
659
|
|
|
259
660
|
Output:
|
|
260
661
|
--output table Plain text table output, default
|
|
@@ -266,5 +667,7 @@ Config:
|
|
|
266
667
|
YUNXIAO_DOMAIN API domain, default openapi-rdc.aliyuncs.com
|
|
267
668
|
YUNXIAO_ORGANIZATION_ID Required for center edition
|
|
268
669
|
YUNXIAO_EDITION center or region
|
|
670
|
+
YUNXIAO_PROJECT_IDS Default comma-separated project IDs for workitem commands
|
|
671
|
+
YUNXIAO_WORKITEM_CATEGORIES Default workitem categories, default Req,Task,Bug
|
|
269
672
|
`;
|
|
270
673
|
}
|
package/dist/src/client.d.ts
CHANGED
|
@@ -42,6 +42,48 @@ export interface ListRepositoriesOptions {
|
|
|
42
42
|
search?: string;
|
|
43
43
|
archived?: boolean;
|
|
44
44
|
}
|
|
45
|
+
export interface SearchProjectsOptions {
|
|
46
|
+
conditions?: string;
|
|
47
|
+
extraConditions?: string;
|
|
48
|
+
orderBy?: "gmtCreate" | "name" | string;
|
|
49
|
+
page?: number;
|
|
50
|
+
perPage?: number;
|
|
51
|
+
sort?: "asc" | "desc" | string;
|
|
52
|
+
}
|
|
53
|
+
export interface ListProjectMembersOptions {
|
|
54
|
+
name?: string;
|
|
55
|
+
roleId?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ChangeProjectMemberOptions {
|
|
58
|
+
operatorId?: string;
|
|
59
|
+
roleId: string;
|
|
60
|
+
userIds: string[];
|
|
61
|
+
}
|
|
62
|
+
export interface DeleteProjectMemberOptions {
|
|
63
|
+
operatorId?: string;
|
|
64
|
+
roleId: string;
|
|
65
|
+
userIds: string[];
|
|
66
|
+
}
|
|
67
|
+
export interface ListMembersOptions {
|
|
68
|
+
page?: number;
|
|
69
|
+
perPage?: number;
|
|
70
|
+
}
|
|
71
|
+
export interface SearchWorkitemsOptions {
|
|
72
|
+
category: string;
|
|
73
|
+
conditions?: string;
|
|
74
|
+
orderBy?: "gmtCreate" | "gmtModified" | "updateStatusAt" | "name" | string;
|
|
75
|
+
page?: number;
|
|
76
|
+
perPage?: number;
|
|
77
|
+
sort?: "asc" | "desc" | string;
|
|
78
|
+
spaceId: string;
|
|
79
|
+
spaceType?: "Project" | "Program" | string;
|
|
80
|
+
}
|
|
81
|
+
export interface CreateWorkitemCommentOptions {
|
|
82
|
+
content: string;
|
|
83
|
+
operatorId?: string;
|
|
84
|
+
parentId?: string;
|
|
85
|
+
}
|
|
86
|
+
export type UpdateWorkitemOptions = Record<string, unknown>;
|
|
45
87
|
export interface RepositorySummary {
|
|
46
88
|
id: number | string;
|
|
47
89
|
name?: string;
|
|
@@ -71,6 +113,7 @@ export interface ReleaseByRepositoryNameOptions {
|
|
|
71
113
|
repositoryName: string;
|
|
72
114
|
sourceBranch: string;
|
|
73
115
|
targetBranch: string;
|
|
116
|
+
createOnly?: boolean;
|
|
74
117
|
title?: string;
|
|
75
118
|
description?: string;
|
|
76
119
|
mergeType?: MergeChangeRequestOptions["mergeType"];
|
|
@@ -78,8 +121,8 @@ export interface ReleaseByRepositoryNameOptions {
|
|
|
78
121
|
export interface ReleaseResult {
|
|
79
122
|
repository: RepositorySummary;
|
|
80
123
|
changeRequest: unknown;
|
|
81
|
-
review: unknown;
|
|
82
|
-
merge: unknown;
|
|
124
|
+
review: unknown | null;
|
|
125
|
+
merge: unknown | null;
|
|
83
126
|
}
|
|
84
127
|
export interface ApproveAndMergeOptions extends ReviewChangeRequestOptions, MergeChangeRequestOptions {
|
|
85
128
|
}
|
|
@@ -100,6 +143,20 @@ export declare class YunxiaoClient {
|
|
|
100
143
|
listChangeRequests(options?: ListChangeRequestsOptions): Promise<unknown>;
|
|
101
144
|
getCurrentUser(): Promise<CurrentUser>;
|
|
102
145
|
listRepositories(options?: ListRepositoriesOptions): Promise<RepositorySummary[]>;
|
|
146
|
+
searchProjects(options?: SearchProjectsOptions): Promise<unknown>;
|
|
147
|
+
getProject(projectId: string): Promise<unknown>;
|
|
148
|
+
listProjectMembers(projectId: string, options?: ListProjectMembersOptions): Promise<unknown>;
|
|
149
|
+
createProjectMember(projectId: string, options: ChangeProjectMemberOptions): Promise<unknown>;
|
|
150
|
+
deleteProjectMember(projectId: string, options: DeleteProjectMemberOptions): Promise<unknown>;
|
|
151
|
+
listMembers(options?: ListMembersOptions): Promise<unknown>;
|
|
152
|
+
getMember(memberId: string): Promise<unknown>;
|
|
153
|
+
searchWorkitems(options: SearchWorkitemsOptions): Promise<unknown>;
|
|
154
|
+
getWorkitem(workitemId: string): Promise<unknown>;
|
|
155
|
+
updateWorkitem(workitemId: string, options: UpdateWorkitemOptions): Promise<unknown>;
|
|
156
|
+
listWorkitemActivities(workitemId: string): Promise<unknown>;
|
|
157
|
+
listWorkitemComments(workitemId: string): Promise<unknown>;
|
|
158
|
+
createWorkitemComment(workitemId: string, options: CreateWorkitemCommentOptions): Promise<unknown>;
|
|
159
|
+
getWorkitemTimeline(workitemId: string): Promise<unknown>;
|
|
103
160
|
createChangeRequest(repositoryId: string, options: CreateChangeRequestOptions): Promise<unknown>;
|
|
104
161
|
reviewChangeRequest(repositoryId: string, localId: number, options?: ReviewChangeRequestOptions): Promise<unknown>;
|
|
105
162
|
mergeChangeRequest(repositoryId: string, localId: number, options?: MergeChangeRequestOptions): Promise<unknown>;
|
|
@@ -111,5 +168,9 @@ export declare class YunxiaoClient {
|
|
|
111
168
|
private repositoriesPath;
|
|
112
169
|
private repositoryPath;
|
|
113
170
|
private repositoryChangeRequestPath;
|
|
171
|
+
private projexPath;
|
|
172
|
+
private workitemsSearchPath;
|
|
173
|
+
private projectPath;
|
|
174
|
+
private platformMembersPath;
|
|
114
175
|
private request;
|
|
115
176
|
}
|
package/dist/src/client.js
CHANGED
|
@@ -40,9 +40,78 @@ export class YunxiaoClient {
|
|
|
40
40
|
listRepositories(options = {}) {
|
|
41
41
|
return this.request("GET", this.repositoriesPath(), options);
|
|
42
42
|
}
|
|
43
|
+
searchProjects(options = {}) {
|
|
44
|
+
return this.request("POST", this.projexPath("/projects:search"), undefined, options);
|
|
45
|
+
}
|
|
46
|
+
getProject(projectId) {
|
|
47
|
+
return this.request("GET", this.projectPath(projectId));
|
|
48
|
+
}
|
|
49
|
+
listProjectMembers(projectId, options = {}) {
|
|
50
|
+
return this.request("GET", `${this.projectPath(projectId)}/members`, options);
|
|
51
|
+
}
|
|
52
|
+
createProjectMember(projectId, options) {
|
|
53
|
+
return this.request("POST", `${this.projectPath(projectId)}/members`, undefined, {
|
|
54
|
+
operatorId: options.operatorId,
|
|
55
|
+
roleId: options.roleId,
|
|
56
|
+
userIds: options.userIds
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
deleteProjectMember(projectId, options) {
|
|
60
|
+
return this.request("DELETE", `${this.projectPath(projectId)}/members`, undefined, {
|
|
61
|
+
operatorId: options.operatorId,
|
|
62
|
+
roleIds: [options.roleId],
|
|
63
|
+
userId: options.userIds.length === 1 ? options.userIds[0] : undefined,
|
|
64
|
+
userIds: options.userIds.length > 1 ? options.userIds : undefined
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
listMembers(options = {}) {
|
|
68
|
+
return this.request("GET", this.platformMembersPath(), options);
|
|
69
|
+
}
|
|
70
|
+
getMember(memberId) {
|
|
71
|
+
return this.request("GET", `${this.platformMembersPath()}/${encodePathSegment(memberId)}`);
|
|
72
|
+
}
|
|
73
|
+
searchWorkitems(options) {
|
|
74
|
+
return this.request("POST", this.workitemsSearchPath(), undefined, {
|
|
75
|
+
category: options.category,
|
|
76
|
+
conditions: options.conditions,
|
|
77
|
+
orderBy: options.orderBy,
|
|
78
|
+
page: options.page,
|
|
79
|
+
perPage: options.perPage,
|
|
80
|
+
sort: options.sort,
|
|
81
|
+
spaceId: options.spaceId,
|
|
82
|
+
spaceType: options.spaceType ?? "Project"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
getWorkitem(workitemId) {
|
|
86
|
+
return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}`);
|
|
87
|
+
}
|
|
88
|
+
updateWorkitem(workitemId, options) {
|
|
89
|
+
return this.request("PUT", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}`, undefined, options);
|
|
90
|
+
}
|
|
91
|
+
listWorkitemActivities(workitemId) {
|
|
92
|
+
return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/activities`);
|
|
93
|
+
}
|
|
94
|
+
listWorkitemComments(workitemId) {
|
|
95
|
+
return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/comments`);
|
|
96
|
+
}
|
|
97
|
+
createWorkitemComment(workitemId, options) {
|
|
98
|
+
return this.request("POST", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/comments`, undefined, {
|
|
99
|
+
content: options.content,
|
|
100
|
+
operatorId: options.operatorId,
|
|
101
|
+
parentId: options.parentId
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async getWorkitemTimeline(workitemId) {
|
|
105
|
+
const [workitem, activities, comments] = await Promise.all([
|
|
106
|
+
this.getWorkitem(workitemId),
|
|
107
|
+
this.listWorkitemActivities(workitemId),
|
|
108
|
+
this.listWorkitemComments(workitemId)
|
|
109
|
+
]);
|
|
110
|
+
return { workitem, activities, comments };
|
|
111
|
+
}
|
|
43
112
|
createChangeRequest(repositoryId, options) {
|
|
44
113
|
return this.request("POST", `${this.repositoryPath(repositoryId)}/changeRequests`, undefined, {
|
|
45
|
-
createFrom: options.createFrom ?? "
|
|
114
|
+
createFrom: options.createFrom ?? "WEB",
|
|
46
115
|
description: options.description,
|
|
47
116
|
reviewerUserIds: options.reviewerUserIds,
|
|
48
117
|
sourceBranch: options.sourceBranch,
|
|
@@ -116,6 +185,14 @@ export class YunxiaoClient {
|
|
|
116
185
|
description: options.description
|
|
117
186
|
});
|
|
118
187
|
const localId = localIdFromChangeRequest(changeRequest);
|
|
188
|
+
if (options.createOnly) {
|
|
189
|
+
return {
|
|
190
|
+
repository,
|
|
191
|
+
changeRequest,
|
|
192
|
+
review: null,
|
|
193
|
+
merge: null
|
|
194
|
+
};
|
|
195
|
+
}
|
|
119
196
|
const review = await this.reviewChangeRequest(repositoryId, localId, {
|
|
120
197
|
reviewComment: "Approved by yunxiao-cli release workflow"
|
|
121
198
|
});
|
|
@@ -152,6 +229,29 @@ export class YunxiaoClient {
|
|
|
152
229
|
repositoryChangeRequestPath(repositoryId, localId) {
|
|
153
230
|
return `${this.repositoryPath(repositoryId)}/changeRequests/${localId}`;
|
|
154
231
|
}
|
|
232
|
+
projexPath(path) {
|
|
233
|
+
if (this.edition === "center") {
|
|
234
|
+
return `/oapi/v1/projex/organizations/${encodePathSegment(this.organizationId ?? "")}${path}`;
|
|
235
|
+
}
|
|
236
|
+
return `/oapi/v1/projex${path}`;
|
|
237
|
+
}
|
|
238
|
+
workitemsSearchPath() {
|
|
239
|
+
if (this.edition === "center") {
|
|
240
|
+
return this.projexPath("/workitems:search");
|
|
241
|
+
}
|
|
242
|
+
return this.organizationId
|
|
243
|
+
? `/oapi/v1/projex/${encodePathSegment(this.organizationId)}/workitems:search`
|
|
244
|
+
: "/oapi/v1/projex/workitems:search";
|
|
245
|
+
}
|
|
246
|
+
projectPath(projectId) {
|
|
247
|
+
return `${this.projexPath("/projects")}/${encodePathSegment(projectId)}`;
|
|
248
|
+
}
|
|
249
|
+
platformMembersPath() {
|
|
250
|
+
if (this.edition === "center") {
|
|
251
|
+
return `/oapi/v1/platform/organizations/${encodePathSegment(this.organizationId ?? "")}/members`;
|
|
252
|
+
}
|
|
253
|
+
return "/oapi/v1/platform/members";
|
|
254
|
+
}
|
|
155
255
|
async request(method, path, query, body) {
|
|
156
256
|
const url = new URL(`https://${this.domain}${path}`);
|
|
157
257
|
for (const [key, value] of Object.entries(query ?? {})) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderpp/yunxiao-cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A CLI for Yunxiao
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "A CLI for Yunxiao merge request, project member, and workitem workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|