@coderpp/yunxiao-cli 0.1.7 → 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 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,10 +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
+
129
153
  创建合并请求时会使用云效 `createFrom=WEB`,以保证云效能正确解析源分支提交。
130
154
 
155
+ 发布版本时,如果云效返回“源分支相对目标分支没有改动,不能新建代码评审”,可视为该仓库无改动并跳过。
156
+
157
+ 发布所有版本时,推荐顺序为:
158
+
159
+ 1. 商混版:`master` -> `gx-concrete/dev`
160
+ 2. 蜀道版:`master` -> `sdjt/dev`
161
+ 3. 标准版:`master` -> `release`
162
+
163
+ 标准版应最后发布。
164
+
131
165
  查询变更文件树:
132
166
 
133
167
  ```bash
@@ -150,7 +184,7 @@ node dist/src/index.js mr patches 2813489 12
150
184
  node dist/src/index.js mr list --output table
151
185
  node dist/src/index.js mr list --output json
152
186
  node dist/src/index.js mr review 2813489 12 --output table
153
- 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
154
188
  node dist/src/index.js mr patches 2813489 12 --output json
155
189
  ```
156
190
 
@@ -174,3 +208,132 @@ node dist/src/index.js mr patches 2813489 12 --output json
174
208
  这是为了避免误合并。
175
209
 
176
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, repositoryId, localIdRaw] = parsed.positionals;
14
- if (scope !== "mr" || !command) {
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
- let result;
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[rawName] = inlineValue;
198
+ setFlag(flags, rawName, inlineValue);
100
199
  continue;
101
200
  }
102
201
  const next = args[index + 1];
103
- if (next && !next.startsWith("--") && !["json", "yes", "remove-source-branch", "help"].includes(rawName)) {
104
- flags[rawName] = next;
202
+ if (next && !next.startsWith("--") && !booleanFlags.has(rawName)) {
203
+ setFlag(flags, rawName, next);
105
204
  index += 1;
106
205
  }
107
206
  else {
108
- flags[rawName] = true;
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: Boolean(flags["remove-source-branch"])
372
+ removeSourceBranch: hasFlag(flags, "remove-source-branch")
137
373
  };
138
374
  }
139
375
  function requireConfirmation(flags) {
140
- if (!flags.yes) {
141
- throw new Error("This command changes repository state. Re-run with --yes after you have reviewed the merge request.");
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.json && requestedOutput && requestedOutput !== "json") {
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.json ? "json" : requestedOutput ?? "table";
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 = ["localId", "projectId", "title", "state", "sourceBranch", "targetBranch", "detailUrl"].filter((key) => normalizedRows.some((row) => row[key] !== undefined));
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 (typeof value === "object") {
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
- if (typeof value !== "string") {
620
+ const stringValue = optionalString(value);
621
+ if (!stringValue) {
236
622
  return undefined;
237
623
  }
238
- const number = Number(value);
624
+ const number = Number(stringValue);
239
625
  if (!Number.isInteger(number) || number <= 0) {
240
- throw new Error(`Expected a positive integer, got: ${value}`);
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 Codeup merge request helper
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
  }
@@ -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
  }
@@ -40,6 +40,75 @@ 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
114
  createFrom: options.createFrom ?? "WEB",
@@ -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.7",
4
- "description": "A CLI for Yunxiao Codeup merge request review and merge workflows.",
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",