@coderpp/yunxiao-cli 0.1.7 → 0.1.9

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,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,173 @@ 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 "pipeline":
46
+ case "pl":
47
+ return runPipelineCommand(command, parsed, client);
48
+ case "workitem":
49
+ case "wi":
50
+ return runWorkitemCommand(command, parsed, client, env);
51
+ default:
52
+ throw new Error(`Unknown command scope: ${scope}`);
53
+ }
54
+ }
55
+ async function runMergeRequestCommand(command, parsed, client) {
56
+ const [, , repositoryId, localIdRaw] = parsed.positionals;
57
+ if (command === "list") {
58
+ return client.listChangeRequests(listOptions(parsed.flags));
59
+ }
60
+ if (!repositoryId || !localIdRaw) {
61
+ throw new Error(`Command mr ${command} requires <repositoryId> and <localId>.`);
62
+ }
63
+ if (command === "release") {
64
+ requireConfirmation(parsed.flags);
65
+ return client.releaseByRepositoryName({
66
+ repositoryName: repositoryId,
67
+ sourceBranch: localIdRaw,
68
+ targetBranch: requiredPositional(parsed.positionals[4], "targetBranch"),
69
+ createOnly: hasFlag(parsed.flags, "create-only"),
70
+ title: optionalString(parsed.flags.title),
71
+ description: optionalString(parsed.flags.description),
72
+ mergeType: mergeOptions(parsed.flags).mergeType
73
+ });
74
+ }
75
+ const localId = parseLocalId(localIdRaw);
76
+ switch (command) {
77
+ case "review":
78
+ return client.reviewChangeRequest(repositoryId, localId, {
79
+ reviewComment: optionalString(parsed.flags.comment)
80
+ });
81
+ case "merge":
82
+ requireConfirmation(parsed.flags);
83
+ return client.mergeChangeRequest(repositoryId, localId, mergeOptions(parsed.flags));
84
+ case "approve-and-merge":
85
+ requireConfirmation(parsed.flags);
86
+ return client.approveAndMerge(repositoryId, localId, {
87
+ reviewComment: optionalString(parsed.flags.comment),
88
+ ...mergeOptions(parsed.flags)
89
+ });
90
+ case "tree":
91
+ return client.getChangeRequestTree(repositoryId, localId, {
92
+ fromPatchSetId: requiredFlag(parsed.flags, "from-patch-set-id"),
93
+ toPatchSetId: requiredFlag(parsed.flags, "to-patch-set-id")
94
+ });
95
+ case "patches":
96
+ return client.listChangeRequestPatchSets(repositoryId, localId);
97
+ default:
98
+ throw new Error(`Unknown mr command: ${command}`);
99
+ }
100
+ }
101
+ async function runProjectCommand(command, parsed, client) {
102
+ const projectId = parsed.positionals[2];
103
+ switch (command) {
104
+ case "list":
105
+ case "search":
106
+ return client.searchProjects(projectSearchOptions(parsed.flags));
107
+ case "get":
108
+ return client.getProject(requiredPositional(projectId, "projectId"));
109
+ case "members":
110
+ return client.listProjectMembers(requiredPositional(projectId, "projectId"), {
111
+ name: optionalString(parsed.flags.name),
112
+ roleId: optionalString(parsed.flags["role-id"])
113
+ });
114
+ case "member-add":
115
+ case "add-member":
116
+ requireConfirmation(parsed.flags);
117
+ return client.createProjectMember(requiredPositional(projectId, "projectId"), {
118
+ operatorId: optionalString(parsed.flags["operator-id"]),
119
+ roleId: optionalString(parsed.flags["role-id"]) ?? "project.participant",
120
+ userIds: requiredCsvFlag(parsed.flags, "user-ids")
121
+ });
122
+ case "member-remove":
123
+ case "remove-member":
124
+ requireConfirmation(parsed.flags);
125
+ return client.deleteProjectMember(requiredPositional(projectId, "projectId"), {
126
+ operatorId: optionalString(parsed.flags["operator-id"]),
127
+ roleId: optionalString(parsed.flags["role-id"]) ?? "project.participant",
128
+ userIds: requiredCsvFlag(parsed.flags, "user-ids")
129
+ });
130
+ default:
131
+ throw new Error(`Unknown project command: ${command}`);
132
+ }
133
+ }
134
+ async function runMemberCommand(command, parsed, client) {
135
+ switch (command) {
136
+ case "list":
137
+ return client.listMembers({
138
+ page: optionalNumber(parsed.flags.page),
139
+ perPage: optionalNumber(parsed.flags["per-page"])
140
+ });
141
+ case "get":
142
+ return client.getMember(requiredPositional(parsed.positionals[2], "memberId"));
143
+ default:
144
+ throw new Error(`Unknown member command: ${command}`);
145
+ }
146
+ }
147
+ async function runPipelineCommand(command, parsed, client) {
148
+ if (command === "failed-runs" || command === "failed" || command === "latest-failed-runs") {
149
+ return client.listLatestFailedPipelineRuns(latestFailedPipelineRunsOptions(parsed.flags));
150
+ }
151
+ const pipelineId = requiredPositional(parsed.positionals[2], "pipelineId");
152
+ const pipelineRunId = parsed.positionals[3];
153
+ const jobId = parsed.positionals[4];
154
+ switch (command) {
155
+ case "runs":
156
+ case "list-runs":
157
+ return client.listPipelineRuns(pipelineId, pipelineRunsOptions(parsed.flags));
158
+ case "run":
159
+ case "get-run":
160
+ case "get":
161
+ return client.getPipelineRun(pipelineId, requiredPositional(pipelineRunId, "pipelineRunId"));
162
+ case "retry-job":
163
+ case "retry":
164
+ requireConfirmation(parsed.flags);
165
+ return client.retryPipelineJobRun(pipelineId, requiredPositional(pipelineRunId, "pipelineRunId"), requiredPositional(jobId, "jobId"));
166
+ default:
167
+ throw new Error(`Unknown pipeline command: ${command}`);
168
+ }
169
+ }
170
+ async function runWorkitemCommand(command, parsed, client, env) {
171
+ const workitemId = parsed.positionals[2];
172
+ switch (command) {
173
+ case "list":
174
+ case "search":
175
+ return searchWorkitems(client, parsed.flags, env);
176
+ case "mine": {
177
+ const currentUser = await client.getCurrentUser();
178
+ return searchWorkitems(client, { ...parsed.flags, "assigned-to": currentUser.id }, env);
179
+ }
180
+ case "get":
181
+ return client.getWorkitem(requiredPositional(workitemId, "workitemId"));
182
+ case "update":
183
+ requireConfirmation(parsed.flags);
184
+ return client.updateWorkitem(requiredPositional(workitemId, "workitemId"), workitemUpdateBody(parsed.flags));
185
+ case "activities":
186
+ return client.listWorkitemActivities(requiredPositional(workitemId, "workitemId"));
187
+ case "comments":
188
+ return client.listWorkitemComments(requiredPositional(workitemId, "workitemId"));
189
+ case "comment":
190
+ case "comment-create":
191
+ requireConfirmation(parsed.flags);
192
+ return client.createWorkitemComment(requiredPositional(workitemId, "workitemId"), {
193
+ content: requiredFlag(parsed.flags, "content"),
194
+ operatorId: optionalString(parsed.flags["operator-id"]),
195
+ parentId: optionalString(parsed.flags["parent-id"])
196
+ });
197
+ case "timeline":
198
+ case "history":
199
+ return client.getWorkitemTimeline(requiredPositional(workitemId, "workitemId"));
200
+ default:
201
+ throw new Error(`Unknown workitem command: ${command}`);
202
+ }
203
+ }
79
204
  function createClient(flags, env, fetcher) {
80
205
  return new YunxiaoClient({
81
206
  token: stringFlag(flags.token) ?? env.YUNXIAO_TOKEN ?? "",
@@ -96,20 +221,28 @@ function parseArgs(args) {
96
221
  }
97
222
  const [rawName, inlineValue] = arg.slice(2).split("=", 2);
98
223
  if (inlineValue !== undefined) {
99
- flags[rawName] = inlineValue;
224
+ setFlag(flags, rawName, inlineValue);
100
225
  continue;
101
226
  }
102
227
  const next = args[index + 1];
103
- if (next && !next.startsWith("--") && !["json", "yes", "remove-source-branch", "help"].includes(rawName)) {
104
- flags[rawName] = next;
228
+ if (next && !next.startsWith("--") && !booleanFlags.has(rawName)) {
229
+ setFlag(flags, rawName, next);
105
230
  index += 1;
106
231
  }
107
232
  else {
108
- flags[rawName] = true;
233
+ setFlag(flags, rawName, true);
109
234
  }
110
235
  }
111
236
  return { positionals, flags };
112
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
+ }
113
246
  function listOptions(flags) {
114
247
  return {
115
248
  page: optionalNumber(flags.page),
@@ -125,6 +258,153 @@ function listOptions(flags) {
125
258
  createdAfter: optionalString(flags["created-after"])
126
259
  };
127
260
  }
261
+ function projectSearchOptions(flags) {
262
+ return {
263
+ conditions: optionalString(flags.conditions) ?? projectNameConditions(optionalString(flags.name)),
264
+ extraConditions: optionalString(flags["extra-conditions"]),
265
+ orderBy: optionalString(flags["order-by"]) ?? "gmtCreate",
266
+ page: optionalNumber(flags.page) ?? 1,
267
+ perPage: optionalNumber(flags["per-page"]) ?? 20,
268
+ sort: optionalString(flags.sort) ?? "desc"
269
+ };
270
+ }
271
+ function pipelineRunsOptions(flags) {
272
+ return {
273
+ page: optionalNumber(flags.page),
274
+ perPage: optionalNumber(flags["per-page"]),
275
+ startTime: optionalNumber(flags["start-time"]),
276
+ endTime: optionalNumber(flags["end-time"]) ?? optionalNumber(flags["end-tme"]),
277
+ status: optionalString(flags.status),
278
+ triggerMode: optionalNumber(flags["trigger-mode"])
279
+ };
280
+ }
281
+ function latestFailedPipelineRunsOptions(flags) {
282
+ return {
283
+ hours: optionalNumber(flags.hours) ?? 24,
284
+ startTime: optionalNumber(flags["start-time"]),
285
+ endTime: optionalNumber(flags["end-time"]) ?? optionalNumber(flags["end-tme"]),
286
+ perPage: optionalNumber(flags["per-page"]) ?? 30
287
+ };
288
+ }
289
+ async function searchWorkitems(client, flags, env) {
290
+ const projectIds = workitemProjectIds(flags, env);
291
+ const perPage = optionalNumber(flags["per-page"]) ?? 20;
292
+ const firstPage = optionalNumber(flags.page) ?? 1;
293
+ const results = [];
294
+ 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
+ }
305
+ }
306
+ return filterWorkitems(results, flags);
307
+ }
308
+ function workitemSearchOptions(flags, projectId, page, perPage, env) {
309
+ return {
310
+ category: optionalString(flags.category) ?? env.YUNXIAO_WORKITEM_CATEGORIES ?? "Req,Task,Bug",
311
+ conditions: workitemConditions(flags),
312
+ orderBy: optionalString(flags["order-by"]) ?? "gmtCreate",
313
+ page,
314
+ perPage,
315
+ sort: optionalString(flags.sort) ?? "desc",
316
+ spaceId: projectId,
317
+ spaceType: optionalString(flags["space-type"]) ?? "Project"
318
+ };
319
+ }
320
+ function workitemConditions(flags) {
321
+ const rawConditions = optionalString(flags.conditions);
322
+ const filters = [];
323
+ const assignedTo = optionalString(flags["assigned-to"]) ?? optionalString(flags.assignee);
324
+ const creator = optionalString(flags.creator);
325
+ const statusIds = optionalString(flags["status-ids"]);
326
+ const subject = optionalString(flags.subject);
327
+ const from = optionalString(flags.from) ?? optionalString(flags["created-after"]);
328
+ 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.");
331
+ }
332
+ if (rawConditions) {
333
+ return rawConditions;
334
+ }
335
+ if (assignedTo) {
336
+ filters.push(userCondition("assignedTo", csvValues(assignedTo)));
337
+ }
338
+ if (creator) {
339
+ filters.push(userCondition("creator", csvValues(creator)));
340
+ }
341
+ if (statusIds) {
342
+ filters.push({
343
+ fieldIdentifier: "status",
344
+ operator: "CONTAINS",
345
+ value: csvValues(statusIds),
346
+ toValue: null,
347
+ className: "status",
348
+ format: "list"
349
+ });
350
+ }
351
+ if (subject) {
352
+ filters.push({
353
+ fieldIdentifier: "subject",
354
+ operator: "CONTAINS",
355
+ value: [subject],
356
+ toValue: null,
357
+ className: "string",
358
+ format: "input"
359
+ });
360
+ }
361
+ if (from || to) {
362
+ filters.push({
363
+ fieldIdentifier: optionalString(flags["date-field"]) ?? "gmtCreate",
364
+ operator: "BETWEEN",
365
+ value: from ? [from] : [],
366
+ toValue: to ?? null,
367
+ className: "dateTime",
368
+ format: "input"
369
+ });
370
+ }
371
+ return filters.length > 0 ? JSON.stringify({ conditionGroups: [filters] }) : undefined;
372
+ }
373
+ function userCondition(fieldIdentifier, value) {
374
+ return {
375
+ fieldIdentifier,
376
+ operator: "CONTAINS",
377
+ value,
378
+ toValue: null,
379
+ className: "user",
380
+ format: "list"
381
+ };
382
+ }
383
+ function workitemUpdateBody(flags) {
384
+ const body = {
385
+ ...jsonObjectFlag(flags.fields)
386
+ };
387
+ for (const field of flagStrings(flags.field)) {
388
+ const [key, value] = parseKeyValue(field, "--field");
389
+ body[key] = parseValue(value);
390
+ }
391
+ const assignedTo = optionalString(flags["assigned-to"]) ?? optionalString(flags.assignee);
392
+ if (assignedTo) {
393
+ body.assignedTo = assignedTo;
394
+ }
395
+ const participants = optionalString(flags.participants);
396
+ if (participants) {
397
+ body.participants = csvValues(participants);
398
+ }
399
+ const dueDate = optionalString(flags["due-date"]) ?? optionalString(flags["plan-finish-at"]);
400
+ if (dueDate) {
401
+ body[optionalString(flags["due-field"]) ?? "dueDate"] = dueDate;
402
+ }
403
+ if (Object.keys(body).length === 0) {
404
+ throw new Error("No workitem fields to update. Use --assigned-to, --participants, --due-date, --fields, or --field.");
405
+ }
406
+ return body;
407
+ }
128
408
  function mergeOptions(flags) {
129
409
  const mergeType = optionalString(flags["merge-type"]) ?? "no-fast-forward";
130
410
  if (!mergeTypes.has(mergeType)) {
@@ -133,20 +413,20 @@ function mergeOptions(flags) {
133
413
  return {
134
414
  mergeMessage: optionalString(flags.message),
135
415
  mergeType: mergeType,
136
- removeSourceBranch: Boolean(flags["remove-source-branch"])
416
+ removeSourceBranch: hasFlag(flags, "remove-source-branch")
137
417
  };
138
418
  }
139
419
  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.");
420
+ if (!hasFlag(flags, "yes")) {
421
+ throw new Error("This command changes state. Re-run with --yes after you have reviewed the operation.");
142
422
  }
143
423
  }
144
424
  function outputFormatFromFlags(flags) {
145
425
  const requestedOutput = optionalString(flags.output);
146
- if (flags.json && requestedOutput && requestedOutput !== "json") {
426
+ if (hasFlag(flags, "json") && requestedOutput && requestedOutput !== "json") {
147
427
  throw new Error("--json cannot be combined with --output table.");
148
428
  }
149
- const output = flags.json ? "json" : requestedOutput ?? "table";
429
+ const output = hasFlag(flags, "json") ? "json" : requestedOutput ?? "table";
150
430
  if (!outputFormats.has(output)) {
151
431
  throw new Error(`Invalid --output: ${output}. Supported formats: table, json.`);
152
432
  }
@@ -176,7 +456,36 @@ function formatTable(rows) {
176
456
  return "No records found.\n";
177
457
  }
178
458
  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));
459
+ const preferredKeys = [
460
+ "pipelineName",
461
+ "pipelineRunId",
462
+ "pipelineId",
463
+ "localId",
464
+ "projectId",
465
+ "queryProjectId",
466
+ "id",
467
+ "serialNumber",
468
+ "name",
469
+ "subject",
470
+ "title",
471
+ "state",
472
+ "status",
473
+ "triggerMode",
474
+ "triggerModeName",
475
+ "startTime",
476
+ "startTimeText",
477
+ "endTime",
478
+ "endTimeText",
479
+ "createTime",
480
+ "assignedTo",
481
+ "userId",
482
+ "userName",
483
+ "roleName",
484
+ "email",
485
+ "sourceBranch",
486
+ "targetBranch",
487
+ "detailUrl"
488
+ ].filter((key) => normalizedRows.some((row) => row[key] !== undefined));
180
489
  const keys = preferredKeys.length > 0 ? preferredKeys : uniqueKeys(normalizedRows);
181
490
  const widths = keys.map((key) => Math.max(key.length, ...normalizedRows.map((row) => formatCellValue(row[key]).length)));
182
491
  const line = (values) => values.map((value, index) => value.padEnd(widths[index])).join(" ");
@@ -200,7 +509,14 @@ function formatCellValue(value) {
200
509
  if (value === null || value === undefined) {
201
510
  return "";
202
511
  }
203
- if (typeof value === "object") {
512
+ if (Array.isArray(value)) {
513
+ return value.map(formatCellValue).join(",");
514
+ }
515
+ if (isRecord(value)) {
516
+ const displayValue = value.displayName ?? value.name ?? value.userName ?? value.title ?? value.id;
517
+ if (displayValue !== undefined) {
518
+ return String(displayValue);
519
+ }
204
520
  return JSON.stringify(value);
205
521
  }
206
522
  return String(value);
@@ -225,19 +541,143 @@ function requiredFlag(flags, name) {
225
541
  }
226
542
  return value;
227
543
  }
544
+ function requiredCsvFlag(flags, name) {
545
+ return csvValues(requiredFlag(flags, name));
546
+ }
547
+ function projectNameConditions(name) {
548
+ if (!name) {
549
+ return undefined;
550
+ }
551
+ return JSON.stringify({
552
+ conditionGroups: [
553
+ [
554
+ {
555
+ className: "string",
556
+ fieldIdentifier: "name",
557
+ format: "input",
558
+ operator: "CONTAINS",
559
+ toValue: null,
560
+ value: [name]
561
+ }
562
+ ]
563
+ ]
564
+ });
565
+ }
566
+ function workitemProjectIds(flags, env) {
567
+ const value = optionalString(flags["project-ids"]) ?? optionalString(flags["project-id"]) ?? env.YUNXIAO_PROJECT_IDS;
568
+ if (!value) {
569
+ throw new Error("Missing workitem project scope. Set --project-id, --project-ids, or YUNXIAO_PROJECT_IDS.");
570
+ }
571
+ return csvValues(value);
572
+ }
573
+ function addQueryProjectId(row, projectId) {
574
+ return isRecord(row) ? { queryProjectId: projectId, ...row } : row;
575
+ }
576
+ function filterWorkitems(rows, flags) {
577
+ const state = optionalString(flags.state);
578
+ const statusNameEn = optionalString(flags["status-name-en"]);
579
+ const statusName = optionalString(flags["status-name"]);
580
+ return rows.filter((row) => {
581
+ if (!isRecord(row)) {
582
+ return true;
583
+ }
584
+ if (statusNameEn && statusText(row.status, "nameEn") !== statusNameEn) {
585
+ return false;
586
+ }
587
+ if (statusName && statusText(row.status, "name") !== statusName && statusText(row.status, "displayName") !== statusName) {
588
+ return false;
589
+ }
590
+ if (state && state !== "all") {
591
+ const expected = workitemStateNames.get(state);
592
+ if (!expected) {
593
+ throw new Error(`Invalid --state: ${state}. Supported values: todo, doing, in-progress, done, completed, all.`);
594
+ }
595
+ const actualValues = [
596
+ statusText(row.status, "nameEn"),
597
+ statusText(row.status, "name"),
598
+ statusText(row.status, "displayName")
599
+ ].filter((value) => Boolean(value));
600
+ return actualValues.some((actual) => expected.includes(actual));
601
+ }
602
+ return true;
603
+ });
604
+ }
605
+ function statusText(value, key) {
606
+ if (!isRecord(value)) {
607
+ return undefined;
608
+ }
609
+ const text = value[key];
610
+ return typeof text === "string" ? text : undefined;
611
+ }
612
+ function jsonObjectFlag(value) {
613
+ const raw = optionalString(value);
614
+ if (!raw) {
615
+ return {};
616
+ }
617
+ const parsed = JSON.parse(raw);
618
+ if (!isRecord(parsed) || Array.isArray(parsed)) {
619
+ throw new Error("--fields must be a JSON object.");
620
+ }
621
+ return parsed;
622
+ }
623
+ function parseKeyValue(value, flagName) {
624
+ const separatorIndex = value.indexOf("=");
625
+ if (separatorIndex <= 0) {
626
+ throw new Error(`${flagName} expects key=value, got: ${value}`);
627
+ }
628
+ return [value.slice(0, separatorIndex), value.slice(separatorIndex + 1)];
629
+ }
630
+ function parseValue(value) {
631
+ if (value === "true") {
632
+ return true;
633
+ }
634
+ if (value === "false") {
635
+ return false;
636
+ }
637
+ if (value === "null") {
638
+ return null;
639
+ }
640
+ if (/^[\[{]/.test(value)) {
641
+ return JSON.parse(value);
642
+ }
643
+ return value;
644
+ }
645
+ function csvValues(value) {
646
+ return value
647
+ .split(",")
648
+ .map((item) => item.trim())
649
+ .filter(Boolean);
650
+ }
651
+ function flagValues(value) {
652
+ return Array.isArray(value) ? value : [value];
653
+ }
654
+ function flagStrings(value) {
655
+ if (value === undefined) {
656
+ return [];
657
+ }
658
+ return flagValues(value).filter((item) => typeof item === "string");
659
+ }
660
+ function hasFlag(flags, name) {
661
+ const value = flags[name];
662
+ return value === true || (Array.isArray(value) && value.includes(true));
663
+ }
228
664
  function optionalString(value) {
665
+ if (Array.isArray(value)) {
666
+ return [...value].reverse().find((item) => typeof item === "string");
667
+ }
229
668
  return typeof value === "string" ? value : undefined;
230
669
  }
231
670
  function stringFlag(value) {
232
671
  return optionalString(value);
233
672
  }
234
673
  function optionalNumber(value) {
235
- if (typeof value !== "string") {
674
+ const stringValue = optionalString(value);
675
+ if (!stringValue) {
236
676
  return undefined;
237
677
  }
238
- const number = Number(value);
678
+ const number = Number(stringValue);
239
679
  if (!Number.isInteger(number) || number <= 0) {
240
- throw new Error(`Expected a positive integer, got: ${value}`);
680
+ throw new Error(`Expected a positive integer, got: ${stringValue}`);
241
681
  }
242
682
  return number;
243
683
  }
@@ -245,16 +685,35 @@ function isRecord(value) {
245
685
  return typeof value === "object" && value !== null;
246
686
  }
247
687
  function helpText() {
248
- return `yunxiao - Yunxiao Codeup merge request helper
688
+ return `yunxiao - Yunxiao DevOps command helper
249
689
 
250
690
  Usage:
251
691
  yunxiao mr list [--state opened] [--project-ids 2813489] [--output table|json]
252
692
  yunxiao mr review <repositoryId> <localId> [--comment "LGTM"] [--output table|json]
253
693
  yunxiao mr merge <repositoryId> <localId> --yes [--merge-type no-fast-forward] [--output table|json]
254
694
  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]
695
+ yunxiao mr release <repositoryName> <sourceBranch> <targetBranch> --yes [--create-only] [--title "Release"] [--output table|json]
256
696
  yunxiao mr tree <repositoryId> <localId> --from-patch-set-id <id> --to-patch-set-id <id> [--output table|json]
257
697
  yunxiao mr patches <repositoryId> <localId> [--output table|json]
698
+ yunxiao project list [--name keyword] [--output table|json]
699
+ yunxiao project get <projectId> [--output table|json]
700
+ yunxiao project members <projectId> [--name keyword] [--role-id project.admin] [--output table|json]
701
+ yunxiao project member-add <projectId> --user-ids user1,user2 [--role-id project.participant] --yes
702
+ yunxiao project member-remove <projectId> --user-ids user1,user2 [--role-id project.participant] --yes
703
+ yunxiao member list [--page 1] [--per-page 100] [--output table|json]
704
+ yunxiao member get <memberId> [--output table|json]
705
+ yunxiao pipeline failed-runs [--hours 24] [--output table|json]
706
+ yunxiao pipeline runs <pipelineId> [--status FAIL] [--start-time 1729178040000] [--end-time 1729181640000] [--output table|json]
707
+ yunxiao pipeline run <pipelineId> <pipelineRunId> [--output table|json]
708
+ yunxiao pipeline retry-job <pipelineId> <pipelineRunId> <jobId> --yes [--output table|json]
709
+ 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"]
710
+ yunxiao workitem mine --project-ids p1,p2 [--state todo|doing|done] [--output table|json]
711
+ yunxiao workitem get <workitemId> [--output table|json]
712
+ yunxiao workitem update <workitemId> --yes [--assigned-to userId] [--participants user1,user2] [--due-date "2026-05-01 18:00:00"] [--field key=value]
713
+ yunxiao workitem activities <workitemId> [--output table|json]
714
+ yunxiao workitem comments <workitemId> [--output table|json]
715
+ yunxiao workitem comment <workitemId> --content "comment" --yes [--output table|json]
716
+ yunxiao workitem timeline <workitemId> [--output table|json]
258
717
 
259
718
  Output:
260
719
  --output table Plain text table output, default
@@ -266,5 +725,7 @@ Config:
266
725
  YUNXIAO_DOMAIN API domain, default openapi-rdc.aliyuncs.com
267
726
  YUNXIAO_ORGANIZATION_ID Required for center edition
268
727
  YUNXIAO_EDITION center or region
728
+ YUNXIAO_PROJECT_IDS Default comma-separated project IDs for workitem commands
729
+ YUNXIAO_WORKITEM_CATEGORIES Default workitem categories, default Req,Task,Bug
269
730
  `;
270
731
  }