@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.
@@ -1,3 +1,4 @@
1
+ export { YunxiaoApiError } from "./transport.js";
1
2
  export type YunxiaoEdition = "center" | "region";
2
3
  export type Fetcher = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
3
4
  export interface YunxiaoClientOptions {
@@ -6,6 +7,8 @@ export interface YunxiaoClientOptions {
6
7
  organizationId?: string;
7
8
  edition?: YunxiaoEdition;
8
9
  fetcher?: Fetcher;
10
+ timeoutMs?: number;
11
+ retries?: number;
9
12
  }
10
13
  export interface ListChangeRequestsOptions {
11
14
  page?: number;
@@ -151,11 +154,13 @@ export interface ReleaseByRepositoryNameOptions {
151
154
  sourceBranch: string;
152
155
  targetBranch: string;
153
156
  createOnly?: boolean;
157
+ resume?: number;
154
158
  title?: string;
155
159
  description?: string;
156
160
  mergeType?: MergeChangeRequestOptions["mergeType"];
157
161
  }
158
162
  export interface ReleaseResult {
163
+ status?: string;
159
164
  repository: RepositorySummary;
160
165
  changeRequest: unknown;
161
166
  review: unknown | null;
@@ -163,14 +168,9 @@ export interface ReleaseResult {
163
168
  }
164
169
  export interface ApproveAndMergeOptions extends ReviewChangeRequestOptions, MergeChangeRequestOptions {
165
170
  }
166
- export declare class YunxiaoApiError extends Error {
167
- readonly status: number;
168
- readonly responseBody: string;
169
- readonly method: string;
170
- readonly url: string;
171
- constructor(message: string, status: number, responseBody: string, method: string, url: string);
172
- }
173
171
  export declare class YunxiaoClient {
172
+ private readonly timeoutMs;
173
+ private readonly retries;
174
174
  private readonly token;
175
175
  private readonly domain;
176
176
  private readonly organizationId?;
@@ -189,6 +189,8 @@ export declare class YunxiaoClient {
189
189
  getMember(memberId: string): Promise<unknown>;
190
190
  searchWorkitems(options: SearchWorkitemsOptions): Promise<unknown>;
191
191
  getWorkitem(workitemId: string): Promise<unknown>;
192
+ getWorkitemFile(workitemId: string, fileId: string): Promise<unknown>;
193
+ listWorkitemAttachments(workitemId: string): Promise<unknown>;
192
194
  updateWorkitem(workitemId: string, options: UpdateWorkitemOptions): Promise<unknown>;
193
195
  listWorkitemActivities(workitemId: string): Promise<unknown>;
194
196
  listWorkitemComments(workitemId: string): Promise<unknown>;
@@ -199,6 +201,16 @@ export declare class YunxiaoClient {
199
201
  listLatestFailedPipelineRuns(options?: ListLatestFailedPipelineRunsOptions): Promise<LatestFailedPipelineRun[]>;
200
202
  getPipelineRun(pipelineId: string, pipelineRunId: string): Promise<unknown>;
201
203
  retryPipelineJobRun(pipelineId: string, pipelineRunId: string, jobId: string): Promise<unknown>;
204
+ getChangeRequest(repositoryId: string, localId: number): Promise<unknown>;
205
+ updateChangeRequest(repositoryId: string, localId: number, body: Record<string, unknown>): Promise<unknown>;
206
+ closeChangeRequest(repositoryId: string, localId: number): Promise<unknown>;
207
+ listChangeRequestComments(repositoryId: string, localId: number, body?: Record<string, unknown>): Promise<unknown>;
208
+ createChangeRequestComment(repositoryId: string, localId: number, body: Record<string, unknown>): Promise<unknown>;
209
+ compare(repositoryId: string, from: string, to: string, straight: boolean): Promise<unknown>;
210
+ getRepository(repositoryId: string): Promise<unknown>;
211
+ getPipelineJobLog(pipelineId: string, runId: string, jobId: string): Promise<unknown>;
212
+ startPipeline(pipelineId: string, params?: Record<string, unknown>): Promise<unknown>;
213
+ cancelPipeline(pipelineId: string, runId: string): Promise<unknown>;
202
214
  createChangeRequest(repositoryId: string, options: CreateChangeRequestOptions): Promise<unknown>;
203
215
  reviewChangeRequest(repositoryId: string, localId: number, options?: ReviewChangeRequestOptions): Promise<unknown>;
204
216
  mergeChangeRequest(repositoryId: string, localId: number, options?: MergeChangeRequestOptions): Promise<unknown>;
@@ -1,18 +1,11 @@
1
- export class YunxiaoApiError extends Error {
2
- status;
3
- responseBody;
4
- method;
5
- url;
6
- constructor(message, status, responseBody, method, url) {
7
- super(message);
8
- this.status = status;
9
- this.responseBody = responseBody;
10
- this.method = method;
11
- this.url = url;
12
- this.name = "YunxiaoApiError";
13
- }
14
- }
1
+ import { release } from "./release.js";
2
+ import { requestJson } from "./transport.js";
3
+ export { YunxiaoApiError } from "./transport.js";
4
+ import { paginate } from "./pagination.js";
5
+ import { CliError } from "./errors.js";
15
6
  export class YunxiaoClient {
7
+ timeoutMs;
8
+ retries;
16
9
  token;
17
10
  domain;
18
11
  organizationId;
@@ -20,15 +13,28 @@ export class YunxiaoClient {
20
13
  fetcher;
21
14
  constructor(options) {
22
15
  if (!options.token) {
23
- throw new Error("Yunxiao token is required. Set YUNXIAO_TOKEN or pass --token.");
16
+ throw new CliError("INVALID_CONFIG", "Yunxiao token is required. Set YUNXIAO_TOKEN or pass --token.");
24
17
  }
18
+ this.timeoutMs = options.timeoutMs ?? 30000;
19
+ this.retries = options.retries ?? 2;
20
+ if (!Number.isSafeInteger(this.timeoutMs) ||
21
+ this.timeoutMs < 1 ||
22
+ !Number.isSafeInteger(this.retries) ||
23
+ this.retries < 0 ||
24
+ this.retries > 10)
25
+ throw new CliError("INVALID_CONFIG", "Invalid timeout or retry count.");
25
26
  this.token = options.token;
26
27
  this.domain = stripProtocol(options.domain ?? "openapi-rdc.aliyuncs.com");
27
28
  this.organizationId = options.organizationId;
28
- this.edition = options.edition ?? (options.organizationId ? "center" : "region");
29
+ this.edition =
30
+ options.edition ?? (options.organizationId ? "center" : "region");
29
31
  this.fetcher = options.fetcher ?? fetch;
32
+ if (!["center", "region"].includes(this.edition))
33
+ throw new CliError("INVALID_CONFIG", "Invalid edition.");
34
+ if (!/^[a-zA-Z0-9.-]+(?::[0-9]+)?$/.test(this.domain))
35
+ throw new CliError("INVALID_CONFIG", "Domain must be an HTTPS hostname, optionally with port.");
30
36
  if (this.edition === "center" && !this.organizationId) {
31
- throw new Error("Center edition requires organizationId. Set YUNXIAO_ORGANIZATION_ID or pass --organization-id.");
37
+ throw new CliError("INVALID_CONFIG", "Center edition requires organizationId. Set YUNXIAO_ORGANIZATION_ID or pass --organization-id.");
32
38
  }
33
39
  }
34
40
  listChangeRequests(options = {}) {
@@ -53,7 +59,7 @@ export class YunxiaoClient {
53
59
  return this.request("POST", `${this.projectPath(projectId)}/members`, undefined, {
54
60
  operatorId: options.operatorId,
55
61
  roleId: options.roleId,
56
- userIds: options.userIds
62
+ userIds: options.userIds,
57
63
  });
58
64
  }
59
65
  deleteProjectMember(projectId, options) {
@@ -61,7 +67,7 @@ export class YunxiaoClient {
61
67
  operatorId: options.operatorId,
62
68
  roleIds: [options.roleId],
63
69
  userId: options.userIds.length === 1 ? options.userIds[0] : undefined,
64
- userIds: options.userIds.length > 1 ? options.userIds : undefined
70
+ userIds: options.userIds.length > 1 ? options.userIds : undefined,
65
71
  });
66
72
  }
67
73
  listMembers(options = {}) {
@@ -79,12 +85,18 @@ export class YunxiaoClient {
79
85
  perPage: options.perPage,
80
86
  sort: options.sort,
81
87
  spaceId: options.spaceId,
82
- spaceType: options.spaceType ?? "Project"
88
+ spaceType: options.spaceType ?? "Project",
83
89
  });
84
90
  }
85
91
  getWorkitem(workitemId) {
86
92
  return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}`);
87
93
  }
94
+ getWorkitemFile(workitemId, fileId) {
95
+ return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/files/${encodePathSegment(fileId)}`);
96
+ }
97
+ listWorkitemAttachments(workitemId) {
98
+ return this.request("GET", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/attachments`);
99
+ }
88
100
  updateWorkitem(workitemId, options) {
89
101
  return this.request("PUT", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}`, undefined, options);
90
102
  }
@@ -98,14 +110,14 @@ export class YunxiaoClient {
98
110
  return this.request("POST", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/comments`, undefined, {
99
111
  content: options.content,
100
112
  operatorId: options.operatorId,
101
- parentId: options.parentId
113
+ parentId: options.parentId,
102
114
  });
103
115
  }
104
116
  async getWorkitemTimeline(workitemId) {
105
117
  const [workitem, activities, comments] = await Promise.all([
106
118
  this.getWorkitem(workitemId),
107
119
  this.listWorkitemActivities(workitemId),
108
- this.listWorkitemComments(workitemId)
120
+ this.listWorkitemComments(workitemId),
109
121
  ]);
110
122
  return { workitem, activities, comments };
111
123
  }
@@ -116,7 +128,7 @@ export class YunxiaoClient {
116
128
  startTime: options.startTime,
117
129
  endTme: options.endTime,
118
130
  status: options.status,
119
- triggerMode: options.triggerMode
131
+ triggerMode: options.triggerMode,
120
132
  });
121
133
  }
122
134
  listPipelines(options = {}) {
@@ -128,7 +140,7 @@ export class YunxiaoClient {
128
140
  statusList: options.statusList,
129
141
  pipelineName: options.pipelineName,
130
142
  createStartTime: options.createStartTime,
131
- createEndTime: options.createEndTime
143
+ createEndTime: options.createEndTime,
132
144
  });
133
145
  }
134
146
  async listLatestFailedPipelineRuns(options = {}) {
@@ -140,13 +152,15 @@ export class YunxiaoClient {
140
152
  perPage,
141
153
  executeStartTime: startTime,
142
154
  executeEndTime: endTime,
143
- statusList: "FAIL"
155
+ statusList: "FAIL",
144
156
  });
145
157
  const uniquePipelines = uniqueBy(pipelines, (pipeline) => String(recordValue(pipeline, "pipelineId") ?? recordValue(pipeline, "id")));
146
158
  const results = [];
147
159
  for (const pipeline of uniquePipelines) {
148
160
  const pipelineId = recordValue(pipeline, "pipelineId") ?? recordValue(pipeline, "id");
149
- if (pipelineId === undefined || pipelineId === null || pipelineId === "") {
161
+ if (pipelineId === undefined ||
162
+ pipelineId === null ||
163
+ pipelineId === "") {
150
164
  continue;
151
165
  }
152
166
  const runs = await this.listAllPipelineRuns(String(pipelineId), {
@@ -154,17 +168,20 @@ export class YunxiaoClient {
154
168
  perPage,
155
169
  startTime,
156
170
  endTime,
157
- status: "FAIL"
171
+ status: "FAIL",
158
172
  });
159
173
  const latestRun = runs
160
174
  .filter((run) => timestampInRange(recordValue(run, "startTime"), startTime, endTime))
161
- .sort((left, right) => Number(recordValue(right, "startTime") ?? 0) - Number(recordValue(left, "startTime") ?? 0))[0];
175
+ .sort((left, right) => Number(recordValue(right, "startTime") ?? 0) -
176
+ Number(recordValue(left, "startTime") ?? 0))[0];
162
177
  if (!latestRun) {
163
178
  continue;
164
179
  }
165
180
  results.push({
166
181
  pipelineId: normalizeNumericId(pipelineId),
167
- pipelineName: stringRecordValue(pipeline, "pipelineName") ?? stringRecordValue(pipeline, "name") ?? "",
182
+ pipelineName: stringRecordValue(pipeline, "pipelineName") ??
183
+ stringRecordValue(pipeline, "name") ??
184
+ "",
168
185
  pipelineRunId: normalizeNumericId(recordValue(latestRun, "pipelineRunId") ?? ""),
169
186
  status: stringRecordValue(latestRun, "status"),
170
187
  triggerMode: numericOrStringValue(recordValue(latestRun, "triggerMode")),
@@ -173,7 +190,7 @@ export class YunxiaoClient {
173
190
  startTimeText: formatTimestamp(recordValue(latestRun, "startTime")),
174
191
  endTime: numericOrStringValue(recordValue(latestRun, "endTime")),
175
192
  endTimeText: formatTimestamp(recordValue(latestRun, "endTime")),
176
- creatorAccountId: stringRecordValue(latestRun, "creatorAccountId")
193
+ creatorAccountId: stringRecordValue(latestRun, "creatorAccountId"),
177
194
  });
178
195
  }
179
196
  return results.sort((left, right) => Number(right.startTime ?? 0) - Number(left.startTime ?? 0));
@@ -184,6 +201,36 @@ export class YunxiaoClient {
184
201
  retryPipelineJobRun(pipelineId, pipelineRunId, jobId) {
185
202
  return this.request("PUT", `${this.pipelinePath(pipelineId)}/pipelineRuns/${encodePathSegment(pipelineRunId)}/jobs/${encodePathSegment(jobId)}/retry`);
186
203
  }
204
+ getChangeRequest(repositoryId, localId) {
205
+ return this.request("GET", this.repositoryChangeRequestPath(repositoryId, localId));
206
+ }
207
+ updateChangeRequest(repositoryId, localId, body) {
208
+ return this.request("PUT", this.repositoryChangeRequestPath(repositoryId, localId), undefined, body);
209
+ }
210
+ closeChangeRequest(repositoryId, localId) {
211
+ return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/close`);
212
+ }
213
+ listChangeRequestComments(repositoryId, localId, body = {}) {
214
+ return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/comments/list`, undefined, body);
215
+ }
216
+ createChangeRequestComment(repositoryId, localId, body) {
217
+ return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/comments`, undefined, body);
218
+ }
219
+ compare(repositoryId, from, to, straight) {
220
+ return this.request("GET", `${this.repositoryPath(repositoryId)}/compares`, { from, to, straight });
221
+ }
222
+ getRepository(repositoryId) {
223
+ return this.request("GET", this.repositoryPath(repositoryId));
224
+ }
225
+ getPipelineJobLog(pipelineId, runId, jobId) {
226
+ return this.request("GET", `${this.pipelinePath(pipelineId)}/runs/${encodePathSegment(runId)}/job/${encodePathSegment(jobId)}/log`);
227
+ }
228
+ startPipeline(pipelineId, params) {
229
+ return this.request("POST", `${this.pipelinePath(pipelineId)}/runs`, undefined, { params: params === undefined ? undefined : JSON.stringify(params) });
230
+ }
231
+ cancelPipeline(pipelineId, runId) {
232
+ return this.request("PUT", `${this.pipelinePath(pipelineId)}/runs/${encodePathSegment(runId)}`);
233
+ }
187
234
  createChangeRequest(repositoryId, options) {
188
235
  return this.request("POST", `${this.repositoryPath(repositoryId)}/changeRequests`, undefined, {
189
236
  createFrom: options.createFrom ?? "WEB",
@@ -195,37 +242,37 @@ export class YunxiaoClient {
195
242
  targetProjectId: normalizeProjectId(options.targetProjectId),
196
243
  title: options.title,
197
244
  triggerAIReviewRun: options.triggerAIReviewRun ?? false,
198
- workItemIds: options.workItemIds
245
+ workItemIds: options.workItemIds,
199
246
  });
200
247
  }
201
248
  reviewChangeRequest(repositoryId, localId, options = {}) {
202
249
  return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/review`, undefined, {
203
250
  ...compactBody({
204
251
  reviewComment: options.reviewComment,
205
- submitDraftCommentIds: options.submitDraftCommentIds
252
+ submitDraftCommentIds: options.submitDraftCommentIds,
206
253
  }),
207
- reviewOpinion: options.reviewOpinion ?? "PASS"
254
+ reviewOpinion: options.reviewOpinion ?? "PASS",
208
255
  });
209
256
  }
210
257
  mergeChangeRequest(repositoryId, localId, options = {}) {
211
258
  return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/merge`, undefined, {
212
259
  ...compactBody({
213
260
  mergeMessage: options.mergeMessage,
214
- removeSourceBranch: options.removeSourceBranch
261
+ removeSourceBranch: options.removeSourceBranch,
215
262
  }),
216
- mergeType: options.mergeType ?? "no-fast-forward"
263
+ mergeType: options.mergeType ?? "no-fast-forward",
217
264
  });
218
265
  }
219
266
  async approveAndMerge(repositoryId, localId, options = {}) {
220
267
  await this.reviewChangeRequest(repositoryId, localId, {
221
268
  reviewComment: options.reviewComment,
222
269
  reviewOpinion: options.reviewOpinion ?? "PASS",
223
- submitDraftCommentIds: options.submitDraftCommentIds
270
+ submitDraftCommentIds: options.submitDraftCommentIds,
224
271
  });
225
272
  return this.mergeChangeRequest(repositoryId, localId, {
226
273
  mergeMessage: options.mergeMessage,
227
274
  mergeType: options.mergeType,
228
- removeSourceBranch: options.removeSourceBranch
275
+ removeSourceBranch: options.removeSourceBranch,
229
276
  });
230
277
  }
231
278
  getChangeRequestTree(repositoryId, localId, options) {
@@ -235,52 +282,7 @@ export class YunxiaoClient {
235
282
  return this.request("GET", `${this.repositoryChangeRequestPath(repositoryId, localId)}/diffs/patches`);
236
283
  }
237
284
  async releaseByRepositoryName(options) {
238
- const [currentUser, repositories] = await Promise.all([
239
- this.getCurrentUser(),
240
- this.listRepositories({
241
- page: 1,
242
- perPage: 20,
243
- search: options.repositoryName,
244
- archived: false
245
- })
246
- ]);
247
- const repository = findRepositoryByName(repositories, options.repositoryName);
248
- if (!repository) {
249
- throw new Error(`Repository not found by name: ${options.repositoryName}`);
250
- }
251
- const repositoryId = String(repository.id);
252
- const title = options.title ?? `Release ${options.sourceBranch} into ${options.targetBranch}`;
253
- const changeRequest = await this.createChangeRequest(repositoryId, {
254
- sourceBranch: options.sourceBranch,
255
- targetBranch: options.targetBranch,
256
- sourceProjectId: repository.id,
257
- targetProjectId: repository.id,
258
- reviewerUserIds: [currentUser.id],
259
- title,
260
- description: options.description
261
- });
262
- const localId = localIdFromChangeRequest(changeRequest);
263
- if (options.createOnly) {
264
- return {
265
- repository,
266
- changeRequest,
267
- review: null,
268
- merge: null
269
- };
270
- }
271
- const review = await this.reviewChangeRequest(repositoryId, localId, {
272
- reviewComment: "Approved by yunxiao-cli release workflow"
273
- });
274
- const merge = await this.mergeChangeRequest(repositoryId, localId, {
275
- mergeType: options.mergeType,
276
- removeSourceBranch: false
277
- });
278
- return {
279
- repository,
280
- changeRequest,
281
- review,
282
- merge
283
- };
285
+ return release(this, options);
284
286
  }
285
287
  changeRequestsPath() {
286
288
  if (this.edition === "center") {
@@ -338,35 +340,15 @@ export class YunxiaoClient {
338
340
  return `${this.pipelinesPath()}/${encodedPipelineId}`;
339
341
  }
340
342
  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;
343
+ return (await paginate((page, perPage) => this.listPipelines({ ...options, page, perPage }), { ...options, allPages: true }));
352
344
  }
353
345
  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;
346
+ return (await paginate((page, perPage) => this.listPipelineRuns(pipelineId, { ...options, page, perPage }), { ...options, allPages: true }));
365
347
  }
366
348
  async request(method, path, query, body) {
367
349
  const url = new URL(`https://${this.domain}${path}`);
368
350
  for (const [key, value] of Object.entries(query ?? {})) {
369
- if (value !== undefined && value !== null && value !== "") {
351
+ if (value !== undefined && value !== null) {
370
352
  url.searchParams.set(key, String(value));
371
353
  }
372
354
  }
@@ -374,21 +356,20 @@ export class YunxiaoClient {
374
356
  method,
375
357
  headers: {
376
358
  "content-type": "application/json",
377
- "x-yunxiao-token": this.token
378
- }
359
+ "x-yunxiao-token": this.token,
360
+ },
379
361
  };
380
362
  if (body) {
381
363
  init.body = JSON.stringify(compactBody(body));
382
364
  }
383
- const response = await this.fetcher(url.toString(), init);
384
- const text = await response.text();
385
- if (!response.ok) {
386
- throw new YunxiaoApiError(`Yunxiao API request failed with HTTP ${response.status}: ${method} ${url.toString()}${text ? `\n${text}` : ""}`, response.status, text, method, url.toString());
387
- }
388
- if (!text) {
389
- return null;
390
- }
391
- return JSON.parse(text);
365
+ return requestJson(this.fetcher, url, init, {
366
+ timeoutMs: this.timeoutMs,
367
+ retries: this.retries,
368
+ token: this.token,
369
+ readOnly: method === "GET" ||
370
+ path.endsWith(":search") ||
371
+ path.endsWith("/comments/list"),
372
+ });
392
373
  }
393
374
  }
394
375
  function stripProtocol(domain) {
@@ -425,7 +406,7 @@ function uniqueBy(values, keyFn) {
425
406
  }
426
407
  function timestampInRange(value, startTime, endTime) {
427
408
  const timestamp = Number(value);
428
- return Number.isFinite(timestamp) && timestamp >= startTime && timestamp <= endTime;
409
+ return (Number.isFinite(timestamp) && timestamp >= startTime && timestamp <= endTime);
429
410
  }
430
411
  function normalizeNumericId(value) {
431
412
  const numberValue = Number(value);
@@ -467,31 +448,9 @@ function triggerModeName(value) {
467
448
  }
468
449
  }
469
450
  function compactBody(body) {
470
- return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null && value !== ""));
451
+ return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== undefined && value !== null));
471
452
  }
472
453
  function normalizeProjectId(value) {
473
454
  const numberValue = typeof value === "string" ? Number(value) : value;
474
455
  return Number.isInteger(numberValue) ? numberValue : value;
475
456
  }
476
- function findRepositoryByName(repositories, repositoryName) {
477
- const normalizedName = normalizeRepositoryName(repositoryName);
478
- const matches = repositories.filter((repository) => [repository.name, repository.path, repository.nameWithNamespace, repository.pathWithNamespace]
479
- .filter((value) => typeof value === "string")
480
- .some((value) => normalizeRepositoryName(value) === normalizedName));
481
- if (matches.length > 1) {
482
- throw new Error(`Multiple repositories matched name: ${repositoryName}. Use a more specific path.`);
483
- }
484
- return matches[0];
485
- }
486
- function normalizeRepositoryName(value) {
487
- return value.replace(/\s+\/\s+/g, "/").trim().toLowerCase();
488
- }
489
- function localIdFromChangeRequest(changeRequest) {
490
- if (typeof changeRequest === "object" && changeRequest !== null && "localId" in changeRequest) {
491
- const localId = Number(changeRequest.localId);
492
- if (Number.isInteger(localId) && localId > 0) {
493
- return localId;
494
- }
495
- }
496
- throw new Error("Create change request response did not include a valid localId.");
497
- }
@@ -0,0 +1,4 @@
1
+ import type { Flags } from "./arguments.js";
2
+ export declare function configPath(f: Flags, env: NodeJS.ProcessEnv): string;
3
+ export declare function resolveConfig(f: Flags, env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv>;
4
+ export declare function configCommand(command: string, name: string | undefined, f: Flags, env: NodeJS.ProcessEnv): Promise<unknown>;
@@ -0,0 +1,106 @@
1
+ import { readFile, mkdir, writeFile, rename } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { randomUUID } from "node:crypto";
5
+ import { CliError } from "./errors.js";
6
+ const keys = {
7
+ domain: "YUNXIAO_DOMAIN",
8
+ "organization-id": "YUNXIAO_ORGANIZATION_ID",
9
+ edition: "YUNXIAO_EDITION",
10
+ "project-ids": "YUNXIAO_PROJECT_IDS",
11
+ category: "YUNXIAO_WORKITEM_CATEGORIES",
12
+ };
13
+ export function configPath(f, env) {
14
+ return String(f.config ??
15
+ env.YUNXIAO_CONFIG ??
16
+ join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "yunxiao", "config.json"));
17
+ }
18
+ async function readConfig(file) {
19
+ try {
20
+ const c = JSON.parse(await readFile(file, "utf8"));
21
+ if (!c ||
22
+ !c.profiles ||
23
+ typeof c.profiles !== "object" ||
24
+ Array.isArray(c.profiles))
25
+ throw Error();
26
+ for (const [name, p] of Object.entries(c.profiles)) {
27
+ if (!name || !p || typeof p !== "object" || Array.isArray(p))
28
+ throw Error();
29
+ for (const [k, v] of Object.entries(p))
30
+ if (![...Object.keys(keys), "token-env"].includes(k) ||
31
+ typeof v !== "string")
32
+ throw Error();
33
+ }
34
+ if (c.current !== undefined &&
35
+ (typeof c.current !== "string" || !Object.hasOwn(c.profiles, c.current)))
36
+ throw Error();
37
+ return c;
38
+ }
39
+ catch (e) {
40
+ if (e.code === "ENOENT")
41
+ return { profiles: {} };
42
+ throw new CliError("INVALID_CONFIG", "Invalid configuration file; expected profiles with supported string fields.");
43
+ }
44
+ }
45
+ export async function resolveConfig(f, env) {
46
+ const c = await readConfig(configPath(f, env));
47
+ const name = String(f.profile ?? env.YUNXIAO_PROFILE ?? c.current ?? "");
48
+ if (!name)
49
+ return { ...env };
50
+ if (!Object.hasOwn(c.profiles, name))
51
+ throw new CliError("INVALID_CONFIG", `Profile not found: ${name}`);
52
+ const p = c.profiles[name];
53
+ const result = {};
54
+ for (const [k, v] of Object.entries(keys))
55
+ if (p[k])
56
+ result[v] = p[k];
57
+ if (p["token-env"])
58
+ result.YUNXIAO_TOKEN = env[p["token-env"]];
59
+ for (const [k, v] of Object.entries(env))
60
+ if (v !== undefined)
61
+ result[k] = v;
62
+ return result;
63
+ }
64
+ export async function configCommand(command, name, f, env) {
65
+ const file = configPath(f, env);
66
+ const c = await readConfig(file);
67
+ if (command === "list")
68
+ return Object.keys(c.profiles).map((name) => ({
69
+ name,
70
+ current: name === c.current,
71
+ }));
72
+ if (command === "show") {
73
+ const selected = String(f.profile ?? env.YUNXIAO_PROFILE ?? c.current ?? "");
74
+ if (!Object.hasOwn(c.profiles, selected))
75
+ throw new CliError("INVALID_CONFIG", "Select an existing profile.");
76
+ return { name: selected, ...c.profiles[selected] };
77
+ }
78
+ if (!name ||
79
+ !/^[-\w.]+$/.test(name) ||
80
+ ["__proto__", "constructor", "prototype"].includes(name))
81
+ throw new CliError("INVALID_CONFIG", "Invalid profile name.");
82
+ if (command === "use") {
83
+ if (!Object.hasOwn(c.profiles, name))
84
+ throw new CliError("INVALID_CONFIG", `Profile not found: ${name}`);
85
+ c.current = name;
86
+ }
87
+ else if (command === "set") {
88
+ if (f.token !== undefined)
89
+ throw new CliError("INVALID_CONFIG", "Use --token-env to reference a credential environment variable.");
90
+ const p = { ...(Object.hasOwn(c.profiles, name) ? c.profiles[name] : {}) };
91
+ for (const k of [...Object.keys(keys), "token-env"])
92
+ if (f[k] !== undefined)
93
+ p[k] = String(f[k]);
94
+ if (p["token-env"] && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(p["token-env"]))
95
+ throw new CliError("INVALID_CONFIG", "Invalid --token-env.");
96
+ c.profiles[name] = p;
97
+ }
98
+ await mkdir(dirname(file), { recursive: true, mode: 0o700 });
99
+ const temp = `${file}.${randomUUID()}.tmp`;
100
+ await writeFile(temp, JSON.stringify(c, null, 2) + "\n", {
101
+ mode: 0o600,
102
+ flag: "wx",
103
+ });
104
+ await rename(temp, file);
105
+ return { file, profile: name, current: c.current };
106
+ }
@@ -0,0 +1,14 @@
1
+ export declare class CliError extends Error {
2
+ readonly code: string;
3
+ readonly exitCode: number;
4
+ readonly details?: unknown | undefined;
5
+ constructor(code: string, message: string, exitCode?: number, details?: unknown | undefined);
6
+ }
7
+ export declare function redact(text: string, secrets?: string[]): string;
8
+ export declare function errorInfo(error: unknown): {
9
+ code: string;
10
+ message: string;
11
+ exitCode: number;
12
+ details?: unknown;
13
+ requestId?: string;
14
+ };
@@ -0,0 +1,42 @@
1
+ export class CliError extends Error {
2
+ code;
3
+ exitCode;
4
+ details;
5
+ constructor(code, message, exitCode = 2, details) {
6
+ super(message);
7
+ this.code = code;
8
+ this.exitCode = exitCode;
9
+ this.details = details;
10
+ }
11
+ }
12
+ export function redact(text, secrets = []) {
13
+ for (const secret of secrets)
14
+ if (secret)
15
+ text = text.split(secret).join("[REDACTED]");
16
+ return text
17
+ .replace(/pt-[\w.-]+/g, "[REDACTED]")
18
+ .replace(/https?:\/\/[^\s"<>]+\?[^\s"<>]+/g, "[URL query redacted]");
19
+ }
20
+ export function errorInfo(error) {
21
+ if (error instanceof CliError)
22
+ return error;
23
+ const e = error;
24
+ const status = e?.status;
25
+ const exitCode = status === 401 || status === 403
26
+ ? 3
27
+ : status === 404
28
+ ? 4
29
+ : status === 409 || status === 412
30
+ ? 5
31
+ : status === 429 || (status ?? 0) >= 500
32
+ ? 6
33
+ : status && status >= 400
34
+ ? 2
35
+ : 1;
36
+ return {
37
+ code: e?.apiCode ?? (status ? `HTTP_${status}` : "ERROR"),
38
+ message: e?.message ?? String(error),
39
+ exitCode,
40
+ requestId: e?.requestId,
41
+ };
42
+ }