@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/dist/src/cli.js CHANGED
@@ -1,41 +1,195 @@
1
- import { readWorkitemContent, downloadWorkitemImages, downloadWorkitemAttachments, parseWorkitemUrl } from "./workitem-content.js";
2
- import { YunxiaoClient } from "./client.js";
1
+ import { readFileSync } from "node:fs";
2
+ import { parseArgs, validateArgs, loadFileFlags, commandHelp, } from "./arguments.js";
3
+ import { resolveConfig, configCommand } from "./config.js";
4
+ import { CliError, errorInfo, redact } from "./errors.js";
5
+ import { paginate } from "./pagination.js";
6
+ import { runExtendedMr } from "./mr.js";
7
+ import { runExtendedPipeline } from "./pipeline.js";
8
+ import { readWorkitemContent, downloadWorkitemImages, downloadWorkitemAttachments, parseWorkitemUrl, } from "./workitem-content.js";
9
+ import { YunxiaoClient, } from "./client.js";
3
10
  const mergeTypes = new Set(["ff-only", "no-fast-forward", "squash", "rebase"]);
4
- const outputFormats = new Set(["table", "json"]);
5
- const booleanFlags = new Set(["download-attachments", "all-pages", "create-only", "help", "json", "remove-source-branch", "yes"]);
11
+ const outputFormats = new Set(["table", "json", "text"]);
6
12
  const workitemStateNames = new Map([
7
13
  ["todo", ["TODO", "待处理", "待办"]],
8
14
  ["opened", ["TODO", "待处理", "待办"]],
9
15
  ["doing", ["DOING", "IN_PROGRESS", "PROCESSING", "进行中", "处理中"]],
10
16
  ["in-progress", ["DOING", "IN_PROGRESS", "PROCESSING", "进行中", "处理中"]],
11
17
  ["done", ["DONE", "COMPLETED", "CLOSED", "已完成", "完成", "已关闭"]],
12
- ["completed", ["DONE", "COMPLETED", "CLOSED", "已完成", "完成", "已关闭"]]
18
+ ["completed", ["DONE", "COMPLETED", "CLOSED", "已完成", "完成", "已关闭"]],
13
19
  ]);
14
20
  export async function runCli(args = process.argv.slice(2), env = process.env, io = {}) {
15
21
  const stdout = io.stdout ?? ((text) => process.stdout.write(text));
16
22
  const stderr = io.stderr ?? ((text) => process.stderr.write(text));
23
+ let parsed;
24
+ let effectiveEnv = env;
17
25
  try {
18
- const parsed = parseArgs(args);
19
- if (parsed.positionals.length === 0 || parsed.flags.help) {
20
- stdout(helpText());
26
+ parsed = parseArgs(args);
27
+ if (parsed.flags.version === true) {
28
+ stdout(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version + "\n");
21
29
  return 0;
22
30
  }
23
- const [scope, command] = parsed.positionals;
24
- if (!command) {
25
- throw new Error(`Unknown command: ${parsed.positionals.join(" ")}`);
31
+ if (parsed.positionals.length === 0 || parsed.flags.help === true) {
32
+ stdout(parsed.positionals.length
33
+ ? commandHelp(parsed.positionals)
34
+ : "yunxiao - Yunxiao DevOps command helper\n\n" + commandHelp([]));
35
+ return 0;
26
36
  }
37
+ validateArgs(parsed);
38
+ await loadFileFlags(parsed.flags, io.stdin);
39
+ const [scope, command] = parsed.positionals;
27
40
  const outputFormat = outputFormatFromFlags(parsed.flags);
28
- const client = createClient(parsed.flags, env, io.fetcher);
29
- const result = await runCommand(scope, command, parsed, client, env);
30
- printResult(result, scope === "workitem" && ["content", "images", "attachments"].includes(command) ? "json" : outputFormat, stdout);
41
+ if (scope === "config") {
42
+ printResult(await configCommand(command, parsed.positionals[2], parsed.flags, env), outputFormat, stdout);
43
+ return 0;
44
+ }
45
+ effectiveEnv = await resolveConfig(parsed.flags, env);
46
+ if (scope === "doctor" && parsed.flags.remote !== true) {
47
+ const checks = {
48
+ tokenConfigured: !!(parsed.flags.token ?? effectiveEnv.YUNXIAO_TOKEN),
49
+ domain: parsed.flags.domain ??
50
+ effectiveEnv.YUNXIAO_DOMAIN ??
51
+ "openapi-rdc.aliyuncs.com",
52
+ edition: parsed.flags.edition ??
53
+ effectiveEnv.YUNXIAO_EDITION ??
54
+ ((parsed.flags["organization-id"] ??
55
+ effectiveEnv.YUNXIAO_ORGANIZATION_ID)
56
+ ? "center"
57
+ : "region"),
58
+ };
59
+ createClient(parsed.flags, effectiveEnv, io.fetcher);
60
+ printResult({ ...checks, valid: true, remoteChecked: false }, outputFormat, stdout);
61
+ return 0;
62
+ }
63
+ const client = createClient(parsed.flags, effectiveEnv, io.fetcher);
64
+ let result;
65
+ if (scope === "auth")
66
+ result = await client.getCurrentUser();
67
+ else if (scope === "doctor")
68
+ result = {
69
+ valid: true,
70
+ remoteChecked: true,
71
+ user: await client.getCurrentUser(),
72
+ writePermissions: "not verified",
73
+ };
74
+ else
75
+ result = await runCommand(scope, command, parsed, client, effectiveEnv, stderr);
76
+ if (outputFormat === "text") {
77
+ const r = result;
78
+ if (scope === "pipeline") {
79
+ stdout(String(r.content ?? ""));
80
+ if (r.more)
81
+ stderr("Server reports more log content; this is a single snapshot.\n");
82
+ }
83
+ else {
84
+ stdout(r.files
85
+ .map((f) => `${f.complete ? "" : `# INCOMPLETE: ${f.reason}\n`}${f.diff}`)
86
+ .join("\n"));
87
+ if (!r.complete)
88
+ stderr("Diff is incomplete; inspect JSON metadata.\n");
89
+ }
90
+ }
91
+ else
92
+ printResult(result, scope === "workitem" &&
93
+ ["content", "images", "attachments"].includes(command)
94
+ ? "json"
95
+ : outputFormat, stdout);
31
96
  return 0;
32
97
  }
33
98
  catch (error) {
34
- stderr(`${error instanceof Error ? error.message : String(error)}\n`);
35
- return 1;
99
+ const info = errorInfo(error);
100
+ if (parsed?.positionals[0] === "mr" &&
101
+ parsed.positionals[1] === "release" &&
102
+ info.details &&
103
+ typeof info.details === "object") {
104
+ const details = info.details;
105
+ if (Array.isArray(details.resumeArgs)) {
106
+ const contextArgs = [
107
+ "config",
108
+ "profile",
109
+ "domain",
110
+ "organization-id",
111
+ "edition",
112
+ ].flatMap((key) => typeof parsed.flags[key] === "string"
113
+ ? [`--${key}`, String(parsed.flags[key])]
114
+ : []);
115
+ info.details = {
116
+ ...details,
117
+ resumeArgs: [...details.resumeArgs, ...contextArgs],
118
+ };
119
+ }
120
+ }
121
+ const secrets = [
122
+ String(parsed?.flags.token ?? ""),
123
+ effectiveEnv.YUNXIAO_TOKEN ?? "",
124
+ ];
125
+ const json = parsed?.flags["error-format"] === "json" ||
126
+ args.includes("--error-format=json") ||
127
+ args.some((v, i) => v === "--error-format" && args[i + 1] === "json");
128
+ stderr(redact(json
129
+ ? JSON.stringify({
130
+ error: {
131
+ code: info.code,
132
+ message: info.message,
133
+ requestId: info.requestId,
134
+ details: info.details,
135
+ },
136
+ })
137
+ : info.message +
138
+ (info.details ? "\n" + JSON.stringify(info.details) : ""), secrets) + "\n");
139
+ return info.exitCode;
140
+ }
141
+ }
142
+ async function runCommand(scope, command, parsed, client, env, warn) {
143
+ const f = parsed.flags;
144
+ const pageOptions = {
145
+ page: optionalNumber(f.page),
146
+ perPage: optionalNumber(f["per-page"]) ?? 20,
147
+ allPages: hasFlag(f, "all-pages"),
148
+ warn,
149
+ };
150
+ if (scope === "mr" &&
151
+ [
152
+ "get",
153
+ "diff",
154
+ "comments",
155
+ "comment",
156
+ "create",
157
+ "update",
158
+ "close",
159
+ ].includes(command))
160
+ return runExtendedMr(command, parsed.positionals, f, client);
161
+ if (scope === "pipeline" &&
162
+ ["jobs", "logs", "start", "cancel"].includes(command))
163
+ return runExtendedPipeline(command, parsed.positionals, f, client);
164
+ if (scope === "mr" && command === "list")
165
+ return paginate((page, perPage) => client.listChangeRequests({ ...listOptions(f), page, perPage }), pageOptions);
166
+ if (scope === "project" && command === "list")
167
+ return paginate((page, perPage) => client.searchProjects({ ...projectSearchOptions(f), page, perPage }), pageOptions);
168
+ if (scope === "member" && command === "list")
169
+ return paginate((page, perPage) => client.listMembers({ page, perPage }), pageOptions);
170
+ if (scope === "pipeline" && command === "runs")
171
+ return paginate((page, perPage) => client.listPipelineRuns(parsed.positionals[2], {
172
+ ...pipelineRunsOptions(f),
173
+ page,
174
+ perPage,
175
+ }), pageOptions);
176
+ if (scope === "pipeline" && command === "list")
177
+ return paginate((page, perPage) => client.listPipelines({
178
+ page,
179
+ perPage,
180
+ pipelineName: optionalString(f.name),
181
+ statusList: optionalString(f.status),
182
+ createStartTime: optionalNumber(f["create-start-time"]),
183
+ createEndTime: optionalNumber(f["create-end-time"]),
184
+ executeStartTime: optionalNumber(f["start-time"]),
185
+ executeEndTime: optionalNumber(f["end-time"]),
186
+ }), pageOptions);
187
+ if (scope === "workitem" && ["list", "mine"].includes(command)) {
188
+ const flags = command === "mine"
189
+ ? { ...f, "assigned-to": (await client.getCurrentUser()).id }
190
+ : f;
191
+ return searchWorkitems(client, flags, env, warn);
36
192
  }
37
- }
38
- async function runCommand(scope, command, parsed, client, env) {
39
193
  switch (scope) {
40
194
  case "mr":
41
195
  return runMergeRequestCommand(command, parsed, client);
@@ -50,7 +204,7 @@ async function runCommand(scope, command, parsed, client, env) {
50
204
  case "wi":
51
205
  return runWorkitemCommand(command, parsed, client, env);
52
206
  default:
53
- throw new Error(`Unknown command scope: ${scope}`);
207
+ throw new CliError("INVALID_ARGUMENT", `Unknown command scope: ${scope}`);
54
208
  }
55
209
  }
56
210
  async function runMergeRequestCommand(command, parsed, client) {
@@ -59,7 +213,7 @@ async function runMergeRequestCommand(command, parsed, client) {
59
213
  return client.listChangeRequests(listOptions(parsed.flags));
60
214
  }
61
215
  if (!repositoryId || !localIdRaw) {
62
- throw new Error(`Command mr ${command} requires <repositoryId> and <localId>.`);
216
+ throw new CliError("INVALID_ARGUMENT", `Command mr ${command} requires <repositoryId> and <localId>.`);
63
217
  }
64
218
  if (command === "release") {
65
219
  requireConfirmation(parsed.flags);
@@ -68,16 +222,18 @@ async function runMergeRequestCommand(command, parsed, client) {
68
222
  sourceBranch: localIdRaw,
69
223
  targetBranch: requiredPositional(parsed.positionals[4], "targetBranch"),
70
224
  createOnly: hasFlag(parsed.flags, "create-only"),
225
+ resume: optionalNumber(parsed.flags.resume),
71
226
  title: optionalString(parsed.flags.title),
72
227
  description: optionalString(parsed.flags.description),
73
- mergeType: mergeOptions(parsed.flags).mergeType
228
+ mergeType: mergeOptions(parsed.flags).mergeType,
74
229
  });
75
230
  }
76
231
  const localId = parseLocalId(localIdRaw);
77
232
  switch (command) {
78
233
  case "review":
79
234
  return client.reviewChangeRequest(repositoryId, localId, {
80
- reviewComment: optionalString(parsed.flags.comment)
235
+ reviewComment: optionalString(parsed.flags.comment),
236
+ reviewOpinion: parsed.flags.opinion === "not-pass" ? "NOT_PASS" : "PASS",
81
237
  });
82
238
  case "merge":
83
239
  requireConfirmation(parsed.flags);
@@ -86,17 +242,17 @@ async function runMergeRequestCommand(command, parsed, client) {
86
242
  requireConfirmation(parsed.flags);
87
243
  return client.approveAndMerge(repositoryId, localId, {
88
244
  reviewComment: optionalString(parsed.flags.comment),
89
- ...mergeOptions(parsed.flags)
245
+ ...mergeOptions(parsed.flags),
90
246
  });
91
247
  case "tree":
92
248
  return client.getChangeRequestTree(repositoryId, localId, {
93
249
  fromPatchSetId: requiredFlag(parsed.flags, "from-patch-set-id"),
94
- toPatchSetId: requiredFlag(parsed.flags, "to-patch-set-id")
250
+ toPatchSetId: requiredFlag(parsed.flags, "to-patch-set-id"),
95
251
  });
96
252
  case "patches":
97
253
  return client.listChangeRequestPatchSets(repositoryId, localId);
98
254
  default:
99
- throw new Error(`Unknown mr command: ${command}`);
255
+ throw new CliError("INVALID_ARGUMENT", `Unknown mr command: ${command}`);
100
256
  }
101
257
  }
102
258
  async function runProjectCommand(command, parsed, client) {
@@ -110,7 +266,7 @@ async function runProjectCommand(command, parsed, client) {
110
266
  case "members":
111
267
  return client.listProjectMembers(requiredPositional(projectId, "projectId"), {
112
268
  name: optionalString(parsed.flags.name),
113
- roleId: optionalString(parsed.flags["role-id"])
269
+ roleId: optionalString(parsed.flags["role-id"]),
114
270
  });
115
271
  case "member-add":
116
272
  case "add-member":
@@ -118,7 +274,7 @@ async function runProjectCommand(command, parsed, client) {
118
274
  return client.createProjectMember(requiredPositional(projectId, "projectId"), {
119
275
  operatorId: optionalString(parsed.flags["operator-id"]),
120
276
  roleId: optionalString(parsed.flags["role-id"]) ?? "project.participant",
121
- userIds: requiredCsvFlag(parsed.flags, "user-ids")
277
+ userIds: requiredCsvFlag(parsed.flags, "user-ids"),
122
278
  });
123
279
  case "member-remove":
124
280
  case "remove-member":
@@ -126,10 +282,10 @@ async function runProjectCommand(command, parsed, client) {
126
282
  return client.deleteProjectMember(requiredPositional(projectId, "projectId"), {
127
283
  operatorId: optionalString(parsed.flags["operator-id"]),
128
284
  roleId: optionalString(parsed.flags["role-id"]) ?? "project.participant",
129
- userIds: requiredCsvFlag(parsed.flags, "user-ids")
285
+ userIds: requiredCsvFlag(parsed.flags, "user-ids"),
130
286
  });
131
287
  default:
132
- throw new Error(`Unknown project command: ${command}`);
288
+ throw new CliError("INVALID_ARGUMENT", `Unknown project command: ${command}`);
133
289
  }
134
290
  }
135
291
  async function runMemberCommand(command, parsed, client) {
@@ -137,16 +293,18 @@ async function runMemberCommand(command, parsed, client) {
137
293
  case "list":
138
294
  return client.listMembers({
139
295
  page: optionalNumber(parsed.flags.page),
140
- perPage: optionalNumber(parsed.flags["per-page"])
296
+ perPage: optionalNumber(parsed.flags["per-page"]),
141
297
  });
142
298
  case "get":
143
299
  return client.getMember(requiredPositional(parsed.positionals[2], "memberId"));
144
300
  default:
145
- throw new Error(`Unknown member command: ${command}`);
301
+ throw new CliError("INVALID_ARGUMENT", `Unknown member command: ${command}`);
146
302
  }
147
303
  }
148
304
  async function runPipelineCommand(command, parsed, client) {
149
- if (command === "failed-runs" || command === "failed" || command === "latest-failed-runs") {
305
+ if (command === "failed-runs" ||
306
+ command === "failed" ||
307
+ command === "latest-failed-runs") {
150
308
  return client.listLatestFailedPipelineRuns(latestFailedPipelineRunsOptions(parsed.flags));
151
309
  }
152
310
  const pipelineId = requiredPositional(parsed.positionals[2], "pipelineId");
@@ -165,7 +323,7 @@ async function runPipelineCommand(command, parsed, client) {
165
323
  requireConfirmation(parsed.flags);
166
324
  return client.retryPipelineJobRun(pipelineId, requiredPositional(pipelineRunId, "pipelineRunId"), requiredPositional(jobId, "jobId"));
167
325
  default:
168
- throw new Error(`Unknown pipeline command: ${command}`);
326
+ throw new CliError("INVALID_ARGUMENT", `Unknown pipeline command: ${command}`);
169
327
  }
170
328
  }
171
329
  async function runWorkitemCommand(command, parsed, client, env) {
@@ -182,19 +340,23 @@ async function runWorkitemCommand(command, parsed, client, env) {
182
340
  const link = requiredPositional(workitemId, "workitemUrl");
183
341
  const { projectId, workitemId: id } = parseWorkitemUrl(link);
184
342
  if (!workitemProjectIds(parsed.flags, env).includes(projectId))
185
- throw new Error("Link project is outside the configured project scope.");
343
+ throw new CliError("INVALID_ARGUMENT", "Link project is outside the configured project scope.");
186
344
  return readWorkitemContent(client, link, {
187
345
  dir: optionalString(parsed.flags.dir) ?? `workitem-${id}-content`,
188
- downloadAttachments: hasFlag(parsed.flags, "download-attachments")
346
+ timeoutMs: optionalNumber(parsed.flags["timeout-ms"]),
347
+ downloadAttachments: hasFlag(parsed.flags, "download-attachments"),
189
348
  });
190
349
  }
191
350
  case "images":
192
351
  return downloadWorkitemImages(client, requiredPositional(workitemId, "workitemId"), {
193
- dir: optionalString(parsed.flags.dir) ?? `workitem-${workitemId}-images`
352
+ timeoutMs: optionalNumber(parsed.flags["timeout-ms"]),
353
+ dir: optionalString(parsed.flags.dir) ?? `workitem-${workitemId}-images`,
194
354
  });
195
355
  case "attachments":
196
356
  return downloadWorkitemAttachments(client, requiredPositional(workitemId, "workitemId"), {
197
- dir: optionalString(parsed.flags.dir) ?? `workitem-${workitemId}-attachments`
357
+ timeoutMs: optionalNumber(parsed.flags["timeout-ms"]),
358
+ dir: optionalString(parsed.flags.dir) ??
359
+ `workitem-${workitemId}-attachments`,
198
360
  });
199
361
  case "get":
200
362
  return client.getWorkitem(requiredPositional(workitemId, "workitemId"));
@@ -211,57 +373,28 @@ async function runWorkitemCommand(command, parsed, client, env) {
211
373
  return client.createWorkitemComment(requiredPositional(workitemId, "workitemId"), {
212
374
  content: requiredFlag(parsed.flags, "content"),
213
375
  operatorId: optionalString(parsed.flags["operator-id"]),
214
- parentId: optionalString(parsed.flags["parent-id"])
376
+ parentId: optionalString(parsed.flags["parent-id"]),
215
377
  });
216
378
  case "timeline":
217
379
  case "history":
218
380
  return client.getWorkitemTimeline(requiredPositional(workitemId, "workitemId"));
219
381
  default:
220
- throw new Error(`Unknown workitem command: ${command}`);
382
+ throw new CliError("INVALID_ARGUMENT", `Unknown workitem command: ${command}`);
221
383
  }
222
384
  }
223
385
  function createClient(flags, env, fetcher) {
224
386
  return new YunxiaoClient({
225
387
  token: stringFlag(flags.token) ?? env.YUNXIAO_TOKEN ?? "",
226
- domain: stringFlag(flags.domain) ?? env.YUNXIAO_DOMAIN ?? "openapi-rdc.aliyuncs.com",
388
+ domain: stringFlag(flags.domain) ??
389
+ env.YUNXIAO_DOMAIN ??
390
+ "openapi-rdc.aliyuncs.com",
227
391
  organizationId: stringFlag(flags["organization-id"]) ?? env.YUNXIAO_ORGANIZATION_ID,
228
392
  edition: (stringFlag(flags.edition) ?? env.YUNXIAO_EDITION),
229
- fetcher
393
+ fetcher,
394
+ timeoutMs: optionalNumber(flags["timeout-ms"]),
395
+ retries: flags.retries === undefined ? undefined : Number(flags.retries),
230
396
  });
231
397
  }
232
- function parseArgs(args) {
233
- const positionals = [];
234
- const flags = {};
235
- for (let index = 0; index < args.length; index += 1) {
236
- const arg = args[index];
237
- if (!arg.startsWith("--")) {
238
- positionals.push(arg);
239
- continue;
240
- }
241
- const [rawName, inlineValue] = arg.slice(2).split("=", 2);
242
- if (inlineValue !== undefined) {
243
- setFlag(flags, rawName, inlineValue);
244
- continue;
245
- }
246
- const next = args[index + 1];
247
- if (next && !next.startsWith("--") && !booleanFlags.has(rawName)) {
248
- setFlag(flags, rawName, next);
249
- index += 1;
250
- }
251
- else {
252
- setFlag(flags, rawName, true);
253
- }
254
- }
255
- return { positionals, flags };
256
- }
257
- function setFlag(flags, name, value) {
258
- const existing = flags[name];
259
- if (existing === undefined) {
260
- flags[name] = value;
261
- return;
262
- }
263
- flags[name] = [...flagValues(existing), ...flagValues(value)];
264
- }
265
398
  function listOptions(flags) {
266
399
  return {
267
400
  page: optionalNumber(flags.page),
@@ -274,17 +407,18 @@ function listOptions(flags) {
274
407
  orderBy: optionalString(flags["order-by"]),
275
408
  sort: optionalString(flags.sort),
276
409
  createdBefore: optionalString(flags["created-before"]),
277
- createdAfter: optionalString(flags["created-after"])
410
+ createdAfter: optionalString(flags["created-after"]),
278
411
  };
279
412
  }
280
413
  function projectSearchOptions(flags) {
281
414
  return {
282
- conditions: optionalString(flags.conditions) ?? projectNameConditions(optionalString(flags.name)),
415
+ conditions: optionalString(flags.conditions) ??
416
+ projectNameConditions(optionalString(flags.name)),
283
417
  extraConditions: optionalString(flags["extra-conditions"]),
284
418
  orderBy: optionalString(flags["order-by"]) ?? "gmtCreate",
285
419
  page: optionalNumber(flags.page) ?? 1,
286
420
  perPage: optionalNumber(flags["per-page"]) ?? 20,
287
- sort: optionalString(flags.sort) ?? "desc"
421
+ sort: optionalString(flags.sort) ?? "desc",
288
422
  };
289
423
  }
290
424
  function pipelineRunsOptions(flags) {
@@ -294,7 +428,7 @@ function pipelineRunsOptions(flags) {
294
428
  startTime: optionalNumber(flags["start-time"]),
295
429
  endTime: optionalNumber(flags["end-time"]) ?? optionalNumber(flags["end-tme"]),
296
430
  status: optionalString(flags.status),
297
- triggerMode: optionalNumber(flags["trigger-mode"])
431
+ triggerMode: optionalNumber(flags["trigger-mode"]),
298
432
  };
299
433
  }
300
434
  function latestFailedPipelineRunsOptions(flags) {
@@ -302,38 +436,34 @@ function latestFailedPipelineRunsOptions(flags) {
302
436
  hours: optionalNumber(flags.hours) ?? 24,
303
437
  startTime: optionalNumber(flags["start-time"]),
304
438
  endTime: optionalNumber(flags["end-time"]) ?? optionalNumber(flags["end-tme"]),
305
- perPage: optionalNumber(flags["per-page"]) ?? 30
439
+ perPage: optionalNumber(flags["per-page"]) ?? 30,
306
440
  };
307
441
  }
308
- async function searchWorkitems(client, flags, env) {
442
+ async function searchWorkitems(client, flags, env, warn) {
309
443
  const projectIds = workitemProjectIds(flags, env);
310
444
  const perPage = optionalNumber(flags["per-page"]) ?? 20;
311
445
  const firstPage = optionalNumber(flags.page) ?? 1;
312
446
  const results = [];
313
447
  for (const projectId of projectIds) {
314
- let page = firstPage;
315
- while (true) {
316
- const response = await client.searchWorkitems(workitemSearchOptions(flags, projectId, page, perPage, env));
317
- const rows = Array.isArray(response) ? response : [response];
318
- results.push(...rows.map((row) => addQueryProjectId(row, projectId)));
319
- if (!hasFlag(flags, "all-pages") || !Array.isArray(response) || response.length < perPage) {
320
- break;
321
- }
322
- page += 1;
323
- }
448
+ const rows = await paginate((page, size) => client.searchWorkitems(workitemSearchOptions(flags, projectId, page, size, env)), { page: firstPage, perPage, allPages: hasFlag(flags, "all-pages"), warn });
449
+ results.push(...rows.map((row) => addQueryProjectId(row, projectId)));
324
450
  }
451
+ if (flags.state && !hasFlag(flags, "all-pages"))
452
+ warn?.("State filtering applies only to fetched pages; use --all-pages for complete results.\n");
325
453
  return filterWorkitems(results, flags);
326
454
  }
327
455
  function workitemSearchOptions(flags, projectId, page, perPage, env) {
328
456
  return {
329
- category: optionalString(flags.category) ?? env.YUNXIAO_WORKITEM_CATEGORIES ?? "Req,Task,Bug",
457
+ category: optionalString(flags.category) ??
458
+ env.YUNXIAO_WORKITEM_CATEGORIES ??
459
+ "Req,Task,Bug",
330
460
  conditions: workitemConditions(flags),
331
461
  orderBy: optionalString(flags["order-by"]) ?? "gmtCreate",
332
462
  page,
333
463
  perPage,
334
464
  sort: optionalString(flags.sort) ?? "desc",
335
465
  spaceId: projectId,
336
- spaceType: optionalString(flags["space-type"]) ?? "Project"
466
+ spaceType: optionalString(flags["space-type"]) ?? "Project",
337
467
  };
338
468
  }
339
469
  function workitemConditions(flags) {
@@ -345,8 +475,9 @@ function workitemConditions(flags) {
345
475
  const subject = optionalString(flags.subject);
346
476
  const from = optionalString(flags.from) ?? optionalString(flags["created-after"]);
347
477
  const to = optionalString(flags.to) ?? optionalString(flags["created-before"]);
348
- if (rawConditions && (assignedTo || creator || statusIds || subject || from || to)) {
349
- throw new Error("--conditions cannot be combined with shortcut workitem filters.");
478
+ if (rawConditions &&
479
+ (assignedTo || creator || statusIds || subject || from || to)) {
480
+ throw new CliError("INVALID_ARGUMENT", "--conditions cannot be combined with shortcut workitem filters.");
350
481
  }
351
482
  if (rawConditions) {
352
483
  return rawConditions;
@@ -364,7 +495,7 @@ function workitemConditions(flags) {
364
495
  value: csvValues(statusIds),
365
496
  toValue: null,
366
497
  className: "status",
367
- format: "list"
498
+ format: "list",
368
499
  });
369
500
  }
370
501
  if (subject) {
@@ -374,7 +505,7 @@ function workitemConditions(flags) {
374
505
  value: [subject],
375
506
  toValue: null,
376
507
  className: "string",
377
- format: "input"
508
+ format: "input",
378
509
  });
379
510
  }
380
511
  if (from || to) {
@@ -384,10 +515,12 @@ function workitemConditions(flags) {
384
515
  value: from ? [from] : [],
385
516
  toValue: to ?? null,
386
517
  className: "dateTime",
387
- format: "input"
518
+ format: "input",
388
519
  });
389
520
  }
390
- return filters.length > 0 ? JSON.stringify({ conditionGroups: [filters] }) : undefined;
521
+ return filters.length > 0
522
+ ? JSON.stringify({ conditionGroups: [filters] })
523
+ : undefined;
391
524
  }
392
525
  function userCondition(fieldIdentifier, value) {
393
526
  return {
@@ -396,12 +529,12 @@ function userCondition(fieldIdentifier, value) {
396
529
  value,
397
530
  toValue: null,
398
531
  className: "user",
399
- format: "list"
532
+ format: "list",
400
533
  };
401
534
  }
402
535
  function workitemUpdateBody(flags) {
403
536
  const body = {
404
- ...jsonObjectFlag(flags.fields)
537
+ ...jsonObjectFlag(flags.fields),
405
538
  };
406
539
  for (const field of flagStrings(flags.field)) {
407
540
  const [key, value] = parseKeyValue(field, "--field");
@@ -415,39 +548,40 @@ function workitemUpdateBody(flags) {
415
548
  if (participants) {
416
549
  body.participants = csvValues(participants);
417
550
  }
418
- const dueDate = optionalString(flags["due-date"]) ?? optionalString(flags["plan-finish-at"]);
551
+ const dueDate = optionalString(flags["due-date"]) ??
552
+ optionalString(flags["plan-finish-at"]);
419
553
  if (dueDate) {
420
554
  body[optionalString(flags["due-field"]) ?? "dueDate"] = dueDate;
421
555
  }
422
556
  if (Object.keys(body).length === 0) {
423
- throw new Error("No workitem fields to update. Use --assigned-to, --participants, --due-date, --fields, or --field.");
557
+ throw new CliError("INVALID_ARGUMENT", "No workitem fields to update. Use --assigned-to, --participants, --due-date, --fields, or --field.");
424
558
  }
425
559
  return body;
426
560
  }
427
561
  function mergeOptions(flags) {
428
562
  const mergeType = optionalString(flags["merge-type"]) ?? "no-fast-forward";
429
563
  if (!mergeTypes.has(mergeType)) {
430
- throw new Error(`Invalid --merge-type: ${mergeType}`);
564
+ throw new CliError("INVALID_ARGUMENT", `Invalid --merge-type: ${mergeType}`);
431
565
  }
432
566
  return {
433
567
  mergeMessage: optionalString(flags.message),
434
568
  mergeType: mergeType,
435
- removeSourceBranch: hasFlag(flags, "remove-source-branch")
569
+ removeSourceBranch: hasFlag(flags, "remove-source-branch"),
436
570
  };
437
571
  }
438
572
  function requireConfirmation(flags) {
439
573
  if (!hasFlag(flags, "yes")) {
440
- throw new Error("This command changes state. Re-run with --yes after you have reviewed the operation.");
574
+ throw new CliError("INVALID_ARGUMENT", "This command changes state. Re-run with --yes after you have reviewed the operation.");
441
575
  }
442
576
  }
443
577
  function outputFormatFromFlags(flags) {
444
578
  const requestedOutput = optionalString(flags.output);
445
579
  if (hasFlag(flags, "json") && requestedOutput && requestedOutput !== "json") {
446
- throw new Error("--json cannot be combined with --output table.");
580
+ throw new CliError("INVALID_ARGUMENT", "--json cannot be combined with a non-JSON --output.");
447
581
  }
448
- const output = hasFlag(flags, "json") ? "json" : requestedOutput ?? "table";
582
+ const output = hasFlag(flags, "json") ? "json" : (requestedOutput ?? "table");
449
583
  if (!outputFormats.has(output)) {
450
- throw new Error(`Invalid --output: ${output}. Supported formats: table, json.`);
584
+ throw new CliError("INVALID_ARGUMENT", `Invalid --output: ${output}. Supported formats: table, json.`);
451
585
  }
452
586
  return output;
453
587
  }
@@ -474,7 +608,7 @@ function formatTable(rows) {
474
608
  if (rows.length === 0) {
475
609
  return "No records found.\n";
476
610
  }
477
- const normalizedRows = rows.map((row) => (isRecord(row) ? row : { value: row }));
611
+ const normalizedRows = rows.map((row) => isRecord(row) ? row : { value: row });
478
612
  const preferredKeys = [
479
613
  "pipelineName",
480
614
  "pipelineRunId",
@@ -506,16 +640,16 @@ function formatTable(rows) {
506
640
  "email",
507
641
  "sourceBranch",
508
642
  "targetBranch",
509
- "detailUrl"
643
+ "detailUrl",
510
644
  ].filter((key) => normalizedRows.some((row) => row[key] !== undefined));
511
645
  const keys = preferredKeys.length > 0 ? preferredKeys : uniqueKeys(normalizedRows);
512
646
  const widths = keys.map((key) => Math.max(key.length, ...normalizedRows.map((row) => formatCellValue(row[key]).length)));
513
647
  const line = (values) => values.map((value, index) => value.padEnd(widths[index])).join(" ");
514
- return [
648
+ return ([
515
649
  line(keys),
516
650
  line(keys.map((_, index) => "-".repeat(widths[index]))),
517
- ...normalizedRows.map((row) => line(keys.map((key) => formatCellValue(row[key]))))
518
- ].join("\n") + "\n";
651
+ ...normalizedRows.map((row) => line(keys.map((key) => formatCellValue(row[key])))),
652
+ ].join("\n") + "\n");
519
653
  }
520
654
  function uniqueKeys(rows) {
521
655
  return Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
@@ -523,7 +657,7 @@ function uniqueKeys(rows) {
523
657
  function formatKeyValueTable(record) {
524
658
  const rows = Object.entries(record).map(([key, value]) => ({
525
659
  key,
526
- value: formatCellValue(value)
660
+ value: formatCellValue(value),
527
661
  }));
528
662
  return formatTable(rows);
529
663
  }
@@ -535,7 +669,11 @@ function formatCellValue(value) {
535
669
  return value.map(formatCellValue).join(",");
536
670
  }
537
671
  if (isRecord(value)) {
538
- const displayValue = value.displayName ?? value.name ?? value.userName ?? value.title ?? value.id;
672
+ const displayValue = value.displayName ??
673
+ value.name ??
674
+ value.userName ??
675
+ value.title ??
676
+ value.id;
539
677
  if (displayValue !== undefined) {
540
678
  return String(displayValue);
541
679
  }
@@ -545,21 +683,21 @@ function formatCellValue(value) {
545
683
  }
546
684
  function parseLocalId(value) {
547
685
  const localId = Number(value);
548
- if (!Number.isInteger(localId) || localId <= 0) {
549
- throw new Error(`<localId> must be a positive integer: ${value}`);
686
+ if (!Number.isSafeInteger(localId) || localId <= 0) {
687
+ throw new CliError("INVALID_ARGUMENT", `<localId> must be a positive integer: ${value}`);
550
688
  }
551
689
  return localId;
552
690
  }
553
691
  function requiredPositional(value, name) {
554
692
  if (!value) {
555
- throw new Error(`Missing required positional argument <${name}>.`);
693
+ throw new CliError("INVALID_ARGUMENT", `Missing required positional argument <${name}>.`);
556
694
  }
557
695
  return value;
558
696
  }
559
697
  function requiredFlag(flags, name) {
560
698
  const value = optionalString(flags[name]);
561
699
  if (!value) {
562
- throw new Error(`Missing required flag --${name}.`);
700
+ throw new CliError("INVALID_ARGUMENT", `Missing required flag --${name}.`);
563
701
  }
564
702
  return value;
565
703
  }
@@ -579,16 +717,18 @@ function projectNameConditions(name) {
579
717
  format: "input",
580
718
  operator: "CONTAINS",
581
719
  toValue: null,
582
- value: [name]
583
- }
584
- ]
585
- ]
720
+ value: [name],
721
+ },
722
+ ],
723
+ ],
586
724
  });
587
725
  }
588
726
  function workitemProjectIds(flags, env) {
589
- const value = optionalString(flags["project-ids"]) ?? optionalString(flags["project-id"]) ?? env.YUNXIAO_PROJECT_IDS;
727
+ const value = optionalString(flags["project-ids"]) ??
728
+ optionalString(flags["project-id"]) ??
729
+ env.YUNXIAO_PROJECT_IDS;
590
730
  if (!value) {
591
- throw new Error("Missing workitem project scope. Set --project-id, --project-ids, or YUNXIAO_PROJECT_IDS.");
731
+ throw new CliError("INVALID_ARGUMENT", "Missing workitem project scope. Set --project-id, --project-ids, or YUNXIAO_PROJECT_IDS.");
592
732
  }
593
733
  return csvValues(value);
594
734
  }
@@ -606,18 +746,20 @@ function filterWorkitems(rows, flags) {
606
746
  if (statusNameEn && statusText(row.status, "nameEn") !== statusNameEn) {
607
747
  return false;
608
748
  }
609
- if (statusName && statusText(row.status, "name") !== statusName && statusText(row.status, "displayName") !== statusName) {
749
+ if (statusName &&
750
+ statusText(row.status, "name") !== statusName &&
751
+ statusText(row.status, "displayName") !== statusName) {
610
752
  return false;
611
753
  }
612
754
  if (state && state !== "all") {
613
755
  const expected = workitemStateNames.get(state);
614
756
  if (!expected) {
615
- throw new Error(`Invalid --state: ${state}. Supported values: todo, doing, in-progress, done, completed, all.`);
757
+ throw new CliError("INVALID_ARGUMENT", `Invalid --state: ${state}. Supported values: todo, doing, in-progress, done, completed, all.`);
616
758
  }
617
759
  const actualValues = [
618
760
  statusText(row.status, "nameEn"),
619
761
  statusText(row.status, "name"),
620
- statusText(row.status, "displayName")
762
+ statusText(row.status, "displayName"),
621
763
  ].filter((value) => Boolean(value));
622
764
  return actualValues.some((actual) => expected.includes(actual));
623
765
  }
@@ -638,14 +780,14 @@ function jsonObjectFlag(value) {
638
780
  }
639
781
  const parsed = JSON.parse(raw);
640
782
  if (!isRecord(parsed) || Array.isArray(parsed)) {
641
- throw new Error("--fields must be a JSON object.");
783
+ throw new CliError("INVALID_ARGUMENT", "--fields must be a JSON object.");
642
784
  }
643
785
  return parsed;
644
786
  }
645
787
  function parseKeyValue(value, flagName) {
646
788
  const separatorIndex = value.indexOf("=");
647
789
  if (separatorIndex <= 0) {
648
- throw new Error(`${flagName} expects key=value, got: ${value}`);
790
+ throw new CliError("INVALID_ARGUMENT", `${flagName} expects key=value, got: ${value}`);
649
791
  }
650
792
  return [value.slice(0, separatorIndex), value.slice(separatorIndex + 1)];
651
793
  }
@@ -685,7 +827,9 @@ function hasFlag(flags, name) {
685
827
  }
686
828
  function optionalString(value) {
687
829
  if (Array.isArray(value)) {
688
- return [...value].reverse().find((item) => typeof item === "string");
830
+ return [...value]
831
+ .reverse()
832
+ .find((item) => typeof item === "string");
689
833
  }
690
834
  return typeof value === "string" ? value : undefined;
691
835
  }
@@ -699,58 +843,10 @@ function optionalNumber(value) {
699
843
  }
700
844
  const number = Number(stringValue);
701
845
  if (!Number.isInteger(number) || number <= 0) {
702
- throw new Error(`Expected a positive integer, got: ${stringValue}`);
846
+ throw new CliError("INVALID_ARGUMENT", `Expected a positive integer, got: ${stringValue}`);
703
847
  }
704
848
  return number;
705
849
  }
706
850
  function isRecord(value) {
707
851
  return typeof value === "object" && value !== null;
708
852
  }
709
- function helpText() {
710
- return `yunxiao - Yunxiao DevOps command helper
711
-
712
- Usage:
713
- yunxiao mr list [--state opened] [--project-ids 2813489] [--output table|json]
714
- yunxiao mr review <repositoryId> <localId> [--comment "LGTM"] [--output table|json]
715
- yunxiao mr merge <repositoryId> <localId> --yes [--merge-type no-fast-forward] [--output table|json]
716
- yunxiao mr approve-and-merge <repositoryId> <localId> --yes [--merge-type no-fast-forward] [--output table|json]
717
- yunxiao mr release <repositoryName> <sourceBranch> <targetBranch> --yes [--create-only] [--title "Release"] [--output table|json]
718
- yunxiao mr tree <repositoryId> <localId> --from-patch-set-id <id> --to-patch-set-id <id> [--output table|json]
719
- yunxiao mr patches <repositoryId> <localId> [--output table|json]
720
- yunxiao project list [--name keyword] [--output table|json]
721
- yunxiao project get <projectId> [--output table|json]
722
- yunxiao project members <projectId> [--name keyword] [--role-id project.admin] [--output table|json]
723
- yunxiao project member-add <projectId> --user-ids user1,user2 [--role-id project.participant] --yes
724
- yunxiao project member-remove <projectId> --user-ids user1,user2 [--role-id project.participant] --yes
725
- yunxiao member list [--page 1] [--per-page 100] [--output table|json]
726
- yunxiao member get <memberId> [--output table|json]
727
- yunxiao pipeline failed-runs [--hours 24] [--output table|json]
728
- yunxiao pipeline runs <pipelineId> [--status FAIL] [--start-time 1729178040000] [--end-time 1729181640000] [--output table|json]
729
- yunxiao pipeline run <pipelineId> <pipelineRunId> [--output table|json]
730
- yunxiao pipeline retry-job <pipelineId> <pipelineRunId> <jobId> --yes [--output table|json]
731
- yunxiao workitem list --project-ids p1,p2 [--assigned-to userId] [--created-by userId] [--state todo|doing|done] [--from "2026-04-01 00:00:00"] [--to "2026-04-30 23:59:59"]
732
- yunxiao workitem mine --project-ids p1,p2 [--state todo|doing|done] [--output table|json]
733
- yunxiao workitem get <workitemId> [--output table|json]
734
- yunxiao workitem content <workitemUrl> [--dir directory] [--download-attachments]
735
- yunxiao workitem images <workitemId> [--dir directory]
736
- yunxiao workitem attachments <workitemId> [--dir directory]
737
- yunxiao workitem update <workitemId> --yes [--assigned-to userId] [--participants user1,user2] [--due-date "2026-05-01 18:00:00"] [--field key=value]
738
- yunxiao workitem activities <workitemId> [--output table|json]
739
- yunxiao workitem comments <workitemId> [--output table|json]
740
- yunxiao workitem comment <workitemId> --content "comment" --yes [--output table|json]
741
- yunxiao workitem timeline <workitemId> [--output table|json]
742
-
743
- Output:
744
- --output table Plain text table output, default
745
- --output json Pretty JSON output
746
- --json Alias for --output json
747
-
748
- Config:
749
- YUNXIAO_TOKEN Personal access token
750
- YUNXIAO_DOMAIN API domain, default openapi-rdc.aliyuncs.com
751
- YUNXIAO_ORGANIZATION_ID Required for center edition
752
- YUNXIAO_EDITION center or region
753
- YUNXIAO_PROJECT_IDS Default comma-separated project IDs for workitem commands
754
- YUNXIAO_WORKITEM_CATEGORIES Default workitem categories, default Req,Task,Bug
755
- `;
756
- }