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