@coderpp/yunxiao-cli 0.1.8 → 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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @coderpp/yunxiao-cli
2
2
 
3
- 用于评审和合并云效 Codeup 合并请求、查询项目协作工作项并维护项目成员的命令行工具。
3
+ 用于评审和合并云效 Codeup 合并请求、重试云效流水线任务、查询项目协作工作项并维护项目成员的命令行工具。
4
4
 
5
5
  ## 安装
6
6
 
@@ -69,6 +69,38 @@ npm version 0.1.1
69
69
  git push origin main v0.1.1
70
70
  ```
71
71
 
72
+ ## 技能同步
73
+
74
+ 当前仓库的技能位于 `.agents/skills`,可以同步到常用技能目录:
75
+
76
+ ```bash
77
+ npm run skills:sync
78
+ ```
79
+
80
+ 同步目标:
81
+
82
+ - `~/pp-note/.agents/skills`
83
+ - `~/pp-note/.claude/skills`
84
+ - `~/.hermes/skills`
85
+
86
+ 如果希望技能文件修改后自动同步,启动 watcher:
87
+
88
+ ```bash
89
+ npm run skills:watch
90
+ ```
91
+
92
+ Watcher 会先同步一次,然后持续监听 `.agents/skills` 文件变化;同名技能会覆盖,目标目录里的其他技能不会被删除。
93
+
94
+ 如果希望后台常驻自动同步:
95
+
96
+ ```bash
97
+ npm run skills:watch:start
98
+ npm run skills:watch:status
99
+ npm run skills:watch:stop
100
+ ```
101
+
102
+ 后台 watcher 的 PID 和日志保存在 `.git/skill-sync-watch.*`,不会提交到仓库。
103
+
72
104
  ## 配置
73
105
 
74
106
  不要把个人访问令牌写入代码或提交到仓库。建议通过环境变量提供:
@@ -186,6 +218,9 @@ node dist/src/index.js mr list --output json
186
218
  node dist/src/index.js mr review 2813489 12 --output table
187
219
  node dist/src/index.js mr release yunxiao-cli release/1.2.3 master --yes --create-only --output json
188
220
  node dist/src/index.js mr patches 2813489 12 --output json
221
+ node dist/src/index.js pipeline failed-runs --hours 24 --output json
222
+ node dist/src/index.js pipeline runs 123 --status FAIL --output json
223
+ node dist/src/index.js pipeline retry-job 123 456 789 --yes --output table
189
224
  ```
190
225
 
191
226
  支持的格式:
@@ -254,6 +289,62 @@ node dist/src/index.js member list --per-page 100
254
289
  node dist/src/index.js member get <memberId> --output json
255
290
  ```
256
291
 
292
+ ## 流水线命令
293
+
294
+ 查询最近失败流水线运行实例,并且同一个流水线只保留最新一次失败运行:
295
+
296
+ ```bash
297
+ node dist/src/index.js pipeline failed-runs --hours 24 --output table
298
+ ```
299
+
300
+ 默认查询最近 24 小时,等价于:
301
+
302
+ ```bash
303
+ node dist/src/index.js pipeline failed-runs --hours 24
304
+ ```
305
+
306
+ 也可以用毫秒时间戳指定范围:
307
+
308
+ ```bash
309
+ node dist/src/index.js pipeline failed-runs \
310
+ --start-time 1777358348118 \
311
+ --end-time 1777444748118 \
312
+ --output json
313
+ ```
314
+
315
+ 查询流水线运行实例列表:
316
+
317
+ ```bash
318
+ node dist/src/index.js pipeline runs <pipelineId> --status FAIL --per-page 30
319
+ ```
320
+
321
+ 可用过滤参数:
322
+
323
+ - `--page`、`--per-page`
324
+ - `--start-time`、`--end-time`:毫秒时间戳,例如 `1729178040000`
325
+ - `--status`:`FAIL`、`SUCCESS`、`RUNNING`
326
+ - `--trigger-mode`:触发方式,常见值为 `1` 人工触发、`2` 定时触发、`3` 代码提交触发、`5` 流水线触发、`6` Webhook 触发
327
+
328
+ 获取单次流水线运行详情:
329
+
330
+ ```bash
331
+ node dist/src/index.js pipeline run <pipelineId> <pipelineRunId> --output json
332
+ ```
333
+
334
+ 运行详情里包含阶段和任务信息,重试任务时需要从返回结果里的 `stages[].stageInfo.jobs[].id` 获取 `jobId`。
335
+
336
+ 重试指定流水线任务:
337
+
338
+ ```bash
339
+ node dist/src/index.js pipeline retry-job <pipelineId> <pipelineRunId> <jobId> --yes
340
+ ```
341
+
342
+ `pipeline` 也可以简写为 `pl`,`retry-job` 也可以简写为 `retry`:
343
+
344
+ ```bash
345
+ node dist/src/index.js pl retry <pipelineId> <pipelineRunId> <jobId> --yes
346
+ ```
347
+
257
348
  ## 工作项命令
258
349
 
259
350
  查询当前用户待办工作项:
@@ -335,5 +426,6 @@ node dist/src/index.js workitem comment <workitemId> --content "处理完成,
335
426
 
336
427
  - `project member-add`
337
428
  - `project member-remove`
429
+ - `pipeline retry-job`
338
430
  - `workitem update`
339
431
  - `workitem comment`
package/dist/src/cli.js CHANGED
@@ -42,6 +42,9 @@ async function runCommand(scope, command, parsed, client, env) {
42
42
  return runProjectCommand(command, parsed, client);
43
43
  case "member":
44
44
  return runMemberCommand(command, parsed, client);
45
+ case "pipeline":
46
+ case "pl":
47
+ return runPipelineCommand(command, parsed, client);
45
48
  case "workitem":
46
49
  case "wi":
47
50
  return runWorkitemCommand(command, parsed, client, env);
@@ -141,6 +144,29 @@ async function runMemberCommand(command, parsed, client) {
141
144
  throw new Error(`Unknown member command: ${command}`);
142
145
  }
143
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
+ }
144
170
  async function runWorkitemCommand(command, parsed, client, env) {
145
171
  const workitemId = parsed.positionals[2];
146
172
  switch (command) {
@@ -242,6 +268,24 @@ function projectSearchOptions(flags) {
242
268
  sort: optionalString(flags.sort) ?? "desc"
243
269
  };
244
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
+ }
245
289
  async function searchWorkitems(client, flags, env) {
246
290
  const projectIds = workitemProjectIds(flags, env);
247
291
  const perPage = optionalNumber(flags["per-page"]) ?? 20;
@@ -413,6 +457,9 @@ function formatTable(rows) {
413
457
  }
414
458
  const normalizedRows = rows.map((row) => (isRecord(row) ? row : { value: row }));
415
459
  const preferredKeys = [
460
+ "pipelineName",
461
+ "pipelineRunId",
462
+ "pipelineId",
416
463
  "localId",
417
464
  "projectId",
418
465
  "queryProjectId",
@@ -423,6 +470,13 @@ function formatTable(rows) {
423
470
  "title",
424
471
  "state",
425
472
  "status",
473
+ "triggerMode",
474
+ "triggerModeName",
475
+ "startTime",
476
+ "startTimeText",
477
+ "endTime",
478
+ "endTimeText",
479
+ "createTime",
426
480
  "assignedTo",
427
481
  "userId",
428
482
  "userName",
@@ -648,6 +702,10 @@ Usage:
648
702
  yunxiao project member-remove <projectId> --user-ids user1,user2 [--role-id project.participant] --yes
649
703
  yunxiao member list [--page 1] [--per-page 100] [--output table|json]
650
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]
651
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"]
652
710
  yunxiao workitem mine --project-ids p1,p2 [--state todo|doing|done] [--output table|json]
653
711
  yunxiao workitem get <workitemId> [--output table|json]
@@ -84,6 +84,43 @@ export interface CreateWorkitemCommentOptions {
84
84
  parentId?: string;
85
85
  }
86
86
  export type UpdateWorkitemOptions = Record<string, unknown>;
87
+ export interface ListPipelineRunsOptions {
88
+ page?: number;
89
+ perPage?: number;
90
+ startTime?: number;
91
+ endTime?: number;
92
+ status?: "FAIL" | "SUCCESS" | "RUNNING" | string;
93
+ triggerMode?: number;
94
+ }
95
+ export interface ListPipelinesOptions {
96
+ createStartTime?: number;
97
+ createEndTime?: number;
98
+ executeStartTime?: number;
99
+ executeEndTime?: number;
100
+ pipelineName?: string;
101
+ statusList?: string;
102
+ page?: number;
103
+ perPage?: number;
104
+ }
105
+ export interface ListLatestFailedPipelineRunsOptions {
106
+ hours?: number;
107
+ startTime?: number;
108
+ endTime?: number;
109
+ perPage?: number;
110
+ }
111
+ export interface LatestFailedPipelineRun {
112
+ pipelineId: number | string;
113
+ pipelineName: string;
114
+ pipelineRunId: number | string;
115
+ status?: string;
116
+ triggerMode?: number | string;
117
+ triggerModeName: string;
118
+ startTime?: number | string;
119
+ startTimeText: string;
120
+ endTime?: number | string;
121
+ endTimeText: string;
122
+ creatorAccountId?: string;
123
+ }
87
124
  export interface RepositorySummary {
88
125
  id: number | string;
89
126
  name?: string;
@@ -157,6 +194,11 @@ export declare class YunxiaoClient {
157
194
  listWorkitemComments(workitemId: string): Promise<unknown>;
158
195
  createWorkitemComment(workitemId: string, options: CreateWorkitemCommentOptions): Promise<unknown>;
159
196
  getWorkitemTimeline(workitemId: string): Promise<unknown>;
197
+ listPipelineRuns(pipelineId: string, options?: ListPipelineRunsOptions): Promise<unknown>;
198
+ listPipelines(options?: ListPipelinesOptions): Promise<unknown>;
199
+ listLatestFailedPipelineRuns(options?: ListLatestFailedPipelineRunsOptions): Promise<LatestFailedPipelineRun[]>;
200
+ getPipelineRun(pipelineId: string, pipelineRunId: string): Promise<unknown>;
201
+ retryPipelineJobRun(pipelineId: string, pipelineRunId: string, jobId: string): Promise<unknown>;
160
202
  createChangeRequest(repositoryId: string, options: CreateChangeRequestOptions): Promise<unknown>;
161
203
  reviewChangeRequest(repositoryId: string, localId: number, options?: ReviewChangeRequestOptions): Promise<unknown>;
162
204
  mergeChangeRequest(repositoryId: string, localId: number, options?: MergeChangeRequestOptions): Promise<unknown>;
@@ -172,5 +214,9 @@ export declare class YunxiaoClient {
172
214
  private workitemsSearchPath;
173
215
  private projectPath;
174
216
  private platformMembersPath;
217
+ private pipelinesPath;
218
+ private pipelinePath;
219
+ private listAllPipelines;
220
+ private listAllPipelineRuns;
175
221
  private request;
176
222
  }
@@ -109,6 +109,81 @@ export class YunxiaoClient {
109
109
  ]);
110
110
  return { workitem, activities, comments };
111
111
  }
112
+ listPipelineRuns(pipelineId, options = {}) {
113
+ return this.request("GET", `${this.pipelinePath(pipelineId)}/runs`, {
114
+ page: options.page,
115
+ perPage: options.perPage,
116
+ startTime: options.startTime,
117
+ endTme: options.endTime,
118
+ status: options.status,
119
+ triggerMode: options.triggerMode
120
+ });
121
+ }
122
+ listPipelines(options = {}) {
123
+ return this.request("GET", this.pipelinesPath(), {
124
+ page: options.page,
125
+ perPage: options.perPage,
126
+ executeStartTime: options.executeStartTime,
127
+ executeEndTime: options.executeEndTime,
128
+ statusList: options.statusList,
129
+ pipelineName: options.pipelineName,
130
+ createStartTime: options.createStartTime,
131
+ createEndTime: options.createEndTime
132
+ });
133
+ }
134
+ async listLatestFailedPipelineRuns(options = {}) {
135
+ const endTime = options.endTime ?? Date.now();
136
+ const startTime = options.startTime ?? endTime - (options.hours ?? 24) * 60 * 60 * 1000;
137
+ const perPage = options.perPage ?? 30;
138
+ const pipelines = await this.listAllPipelines({
139
+ page: 1,
140
+ perPage,
141
+ executeStartTime: startTime,
142
+ executeEndTime: endTime,
143
+ statusList: "FAIL"
144
+ });
145
+ const uniquePipelines = uniqueBy(pipelines, (pipeline) => String(recordValue(pipeline, "pipelineId") ?? recordValue(pipeline, "id")));
146
+ const results = [];
147
+ for (const pipeline of uniquePipelines) {
148
+ const pipelineId = recordValue(pipeline, "pipelineId") ?? recordValue(pipeline, "id");
149
+ if (pipelineId === undefined || pipelineId === null || pipelineId === "") {
150
+ continue;
151
+ }
152
+ const runs = await this.listAllPipelineRuns(String(pipelineId), {
153
+ page: 1,
154
+ perPage,
155
+ startTime,
156
+ endTime,
157
+ status: "FAIL"
158
+ });
159
+ const latestRun = runs
160
+ .filter((run) => timestampInRange(recordValue(run, "startTime"), startTime, endTime))
161
+ .sort((left, right) => Number(recordValue(right, "startTime") ?? 0) - Number(recordValue(left, "startTime") ?? 0))[0];
162
+ if (!latestRun) {
163
+ continue;
164
+ }
165
+ results.push({
166
+ pipelineId: normalizeNumericId(pipelineId),
167
+ pipelineName: stringRecordValue(pipeline, "pipelineName") ?? stringRecordValue(pipeline, "name") ?? "",
168
+ pipelineRunId: normalizeNumericId(recordValue(latestRun, "pipelineRunId") ?? ""),
169
+ status: stringRecordValue(latestRun, "status"),
170
+ triggerMode: numericOrStringValue(recordValue(latestRun, "triggerMode")),
171
+ triggerModeName: triggerModeName(recordValue(latestRun, "triggerMode")),
172
+ startTime: numericOrStringValue(recordValue(latestRun, "startTime")),
173
+ startTimeText: formatTimestamp(recordValue(latestRun, "startTime")),
174
+ endTime: numericOrStringValue(recordValue(latestRun, "endTime")),
175
+ endTimeText: formatTimestamp(recordValue(latestRun, "endTime")),
176
+ creatorAccountId: stringRecordValue(latestRun, "creatorAccountId")
177
+ });
178
+ }
179
+ return results.sort((left, right) => Number(right.startTime ?? 0) - Number(left.startTime ?? 0));
180
+ }
181
+ getPipelineRun(pipelineId, pipelineRunId) {
182
+ return this.request("GET", `${this.pipelinePath(pipelineId)}/runs/${encodePathSegment(pipelineRunId)}`);
183
+ }
184
+ retryPipelineJobRun(pipelineId, pipelineRunId, jobId) {
185
+ return this.request("PUT", `${this.pipelinePath(pipelineId)}/pipelineRuns/${encodePathSegment(pipelineRunId)}/jobs/${encodePathSegment(jobId)}/retry`);
186
+ }
112
187
  createChangeRequest(repositoryId, options) {
113
188
  return this.request("POST", `${this.repositoryPath(repositoryId)}/changeRequests`, undefined, {
114
189
  createFrom: options.createFrom ?? "WEB",
@@ -252,6 +327,42 @@ export class YunxiaoClient {
252
327
  }
253
328
  return "/oapi/v1/platform/members";
254
329
  }
330
+ pipelinesPath() {
331
+ if (this.edition === "center") {
332
+ return `/oapi/v1/flow/organizations/${encodePathSegment(this.organizationId ?? "")}/pipelines`;
333
+ }
334
+ return "/oapi/v1/flow/pipelines";
335
+ }
336
+ pipelinePath(pipelineId) {
337
+ const encodedPipelineId = encodePathSegment(pipelineId);
338
+ return `${this.pipelinesPath()}/${encodedPipelineId}`;
339
+ }
340
+ async listAllPipelines(options) {
341
+ const rows = [];
342
+ const perPage = options.perPage ?? 30;
343
+ for (let page = options.page ?? 1;; page += 1) {
344
+ const response = await this.listPipelines({ ...options, page, perPage });
345
+ const pageRows = arrayRecords(response);
346
+ rows.push(...pageRows);
347
+ if (pageRows.length < perPage) {
348
+ break;
349
+ }
350
+ }
351
+ return rows;
352
+ }
353
+ async listAllPipelineRuns(pipelineId, options) {
354
+ const rows = [];
355
+ const perPage = options.perPage ?? 30;
356
+ for (let page = options.page ?? 1;; page += 1) {
357
+ const response = await this.listPipelineRuns(pipelineId, { ...options, page, perPage });
358
+ const pageRows = arrayRecords(response);
359
+ rows.push(...pageRows);
360
+ if (pageRows.length < perPage) {
361
+ break;
362
+ }
363
+ }
364
+ return rows;
365
+ }
255
366
  async request(method, path, query, body) {
256
367
  const url = new URL(`https://${this.domain}${path}`);
257
368
  for (const [key, value] of Object.entries(query ?? {})) {
@@ -286,6 +397,75 @@ function stripProtocol(domain) {
286
397
  function encodePathSegment(value) {
287
398
  return encodeURIComponent(value);
288
399
  }
400
+ function arrayRecords(value) {
401
+ return Array.isArray(value) ? value.filter(isRecord) : [];
402
+ }
403
+ function isRecord(value) {
404
+ return typeof value === "object" && value !== null && !Array.isArray(value);
405
+ }
406
+ function recordValue(record, key) {
407
+ return record[key];
408
+ }
409
+ function stringRecordValue(record, key) {
410
+ const value = record[key];
411
+ return typeof value === "string" ? value : undefined;
412
+ }
413
+ function uniqueBy(values, keyFn) {
414
+ const seen = new Set();
415
+ const result = [];
416
+ for (const value of values) {
417
+ const key = keyFn(value);
418
+ if (!key || seen.has(key)) {
419
+ continue;
420
+ }
421
+ seen.add(key);
422
+ result.push(value);
423
+ }
424
+ return result;
425
+ }
426
+ function timestampInRange(value, startTime, endTime) {
427
+ const timestamp = Number(value);
428
+ return Number.isFinite(timestamp) && timestamp >= startTime && timestamp <= endTime;
429
+ }
430
+ function normalizeNumericId(value) {
431
+ const numberValue = Number(value);
432
+ return Number.isInteger(numberValue) ? numberValue : String(value);
433
+ }
434
+ function numericOrStringValue(value) {
435
+ if (value === undefined || value === null || value === "") {
436
+ return undefined;
437
+ }
438
+ const numberValue = Number(value);
439
+ return Number.isFinite(numberValue) ? numberValue : String(value);
440
+ }
441
+ function formatTimestamp(value) {
442
+ const timestamp = Number(value);
443
+ if (!Number.isFinite(timestamp)) {
444
+ return "";
445
+ }
446
+ const date = new Date(timestamp);
447
+ if (Number.isNaN(date.getTime())) {
448
+ return "";
449
+ }
450
+ const pad = (part) => String(part).padStart(2, "0");
451
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
452
+ }
453
+ function triggerModeName(value) {
454
+ switch (Number(value)) {
455
+ case 1:
456
+ return "人工触发";
457
+ case 2:
458
+ return "定时触发";
459
+ case 3:
460
+ return "代码提交触发";
461
+ case 5:
462
+ return "流水线触发";
463
+ case 6:
464
+ return "Webhook触发";
465
+ default:
466
+ return value === undefined || value === null ? "" : String(value);
467
+ }
468
+ }
289
469
  function compactBody(body) {
290
470
  return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null && value !== ""));
291
471
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coderpp/yunxiao-cli",
3
- "version": "0.1.8",
4
- "description": "A CLI for Yunxiao merge request, project member, and workitem workflows.",
3
+ "version": "0.1.9",
4
+ "description": "A CLI for Yunxiao merge request, pipeline retry, project member, and workitem workflows.",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -18,6 +18,11 @@
18
18
  "build": "tsc -p tsconfig.json",
19
19
  "pack:dry-run": "npm pack --dry-run",
20
20
  "prepack": "npm run build",
21
+ "skills:sync": "scripts/sync-skills.sh",
22
+ "skills:watch": "scripts/watch-skills.sh",
23
+ "skills:watch:start": "scripts/start-skill-sync-watch.sh",
24
+ "skills:watch:status": "scripts/status-skill-sync-watch.sh",
25
+ "skills:watch:stop": "scripts/stop-skill-sync-watch.sh",
21
26
  "test": "tsc -p tsconfig.json && node --test dist/test/*.test.js"
22
27
  },
23
28
  "publishConfig": {