@coderpp/yunxiao-cli 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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?;
@@ -201,6 +201,16 @@ export declare class YunxiaoClient {
201
201
  listLatestFailedPipelineRuns(options?: ListLatestFailedPipelineRunsOptions): Promise<LatestFailedPipelineRun[]>;
202
202
  getPipelineRun(pipelineId: string, pipelineRunId: string): Promise<unknown>;
203
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>;
204
214
  createChangeRequest(repositoryId: string, options: CreateChangeRequestOptions): Promise<unknown>;
205
215
  reviewChangeRequest(repositoryId: string, localId: number, options?: ReviewChangeRequestOptions): Promise<unknown>;
206
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,7 +85,7 @@ 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) {
@@ -104,14 +110,14 @@ export class YunxiaoClient {
104
110
  return this.request("POST", `${this.projexPath("/workitems")}/${encodePathSegment(workitemId)}/comments`, undefined, {
105
111
  content: options.content,
106
112
  operatorId: options.operatorId,
107
- parentId: options.parentId
113
+ parentId: options.parentId,
108
114
  });
109
115
  }
110
116
  async getWorkitemTimeline(workitemId) {
111
117
  const [workitem, activities, comments] = await Promise.all([
112
118
  this.getWorkitem(workitemId),
113
119
  this.listWorkitemActivities(workitemId),
114
- this.listWorkitemComments(workitemId)
120
+ this.listWorkitemComments(workitemId),
115
121
  ]);
116
122
  return { workitem, activities, comments };
117
123
  }
@@ -122,7 +128,7 @@ export class YunxiaoClient {
122
128
  startTime: options.startTime,
123
129
  endTme: options.endTime,
124
130
  status: options.status,
125
- triggerMode: options.triggerMode
131
+ triggerMode: options.triggerMode,
126
132
  });
127
133
  }
128
134
  listPipelines(options = {}) {
@@ -134,7 +140,7 @@ export class YunxiaoClient {
134
140
  statusList: options.statusList,
135
141
  pipelineName: options.pipelineName,
136
142
  createStartTime: options.createStartTime,
137
- createEndTime: options.createEndTime
143
+ createEndTime: options.createEndTime,
138
144
  });
139
145
  }
140
146
  async listLatestFailedPipelineRuns(options = {}) {
@@ -146,13 +152,15 @@ export class YunxiaoClient {
146
152
  perPage,
147
153
  executeStartTime: startTime,
148
154
  executeEndTime: endTime,
149
- statusList: "FAIL"
155
+ statusList: "FAIL",
150
156
  });
151
157
  const uniquePipelines = uniqueBy(pipelines, (pipeline) => String(recordValue(pipeline, "pipelineId") ?? recordValue(pipeline, "id")));
152
158
  const results = [];
153
159
  for (const pipeline of uniquePipelines) {
154
160
  const pipelineId = recordValue(pipeline, "pipelineId") ?? recordValue(pipeline, "id");
155
- if (pipelineId === undefined || pipelineId === null || pipelineId === "") {
161
+ if (pipelineId === undefined ||
162
+ pipelineId === null ||
163
+ pipelineId === "") {
156
164
  continue;
157
165
  }
158
166
  const runs = await this.listAllPipelineRuns(String(pipelineId), {
@@ -160,17 +168,20 @@ export class YunxiaoClient {
160
168
  perPage,
161
169
  startTime,
162
170
  endTime,
163
- status: "FAIL"
171
+ status: "FAIL",
164
172
  });
165
173
  const latestRun = runs
166
174
  .filter((run) => timestampInRange(recordValue(run, "startTime"), startTime, endTime))
167
- .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];
168
177
  if (!latestRun) {
169
178
  continue;
170
179
  }
171
180
  results.push({
172
181
  pipelineId: normalizeNumericId(pipelineId),
173
- pipelineName: stringRecordValue(pipeline, "pipelineName") ?? stringRecordValue(pipeline, "name") ?? "",
182
+ pipelineName: stringRecordValue(pipeline, "pipelineName") ??
183
+ stringRecordValue(pipeline, "name") ??
184
+ "",
174
185
  pipelineRunId: normalizeNumericId(recordValue(latestRun, "pipelineRunId") ?? ""),
175
186
  status: stringRecordValue(latestRun, "status"),
176
187
  triggerMode: numericOrStringValue(recordValue(latestRun, "triggerMode")),
@@ -179,7 +190,7 @@ export class YunxiaoClient {
179
190
  startTimeText: formatTimestamp(recordValue(latestRun, "startTime")),
180
191
  endTime: numericOrStringValue(recordValue(latestRun, "endTime")),
181
192
  endTimeText: formatTimestamp(recordValue(latestRun, "endTime")),
182
- creatorAccountId: stringRecordValue(latestRun, "creatorAccountId")
193
+ creatorAccountId: stringRecordValue(latestRun, "creatorAccountId"),
183
194
  });
184
195
  }
185
196
  return results.sort((left, right) => Number(right.startTime ?? 0) - Number(left.startTime ?? 0));
@@ -190,6 +201,36 @@ export class YunxiaoClient {
190
201
  retryPipelineJobRun(pipelineId, pipelineRunId, jobId) {
191
202
  return this.request("PUT", `${this.pipelinePath(pipelineId)}/pipelineRuns/${encodePathSegment(pipelineRunId)}/jobs/${encodePathSegment(jobId)}/retry`);
192
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
+ }
193
234
  createChangeRequest(repositoryId, options) {
194
235
  return this.request("POST", `${this.repositoryPath(repositoryId)}/changeRequests`, undefined, {
195
236
  createFrom: options.createFrom ?? "WEB",
@@ -201,37 +242,37 @@ export class YunxiaoClient {
201
242
  targetProjectId: normalizeProjectId(options.targetProjectId),
202
243
  title: options.title,
203
244
  triggerAIReviewRun: options.triggerAIReviewRun ?? false,
204
- workItemIds: options.workItemIds
245
+ workItemIds: options.workItemIds,
205
246
  });
206
247
  }
207
248
  reviewChangeRequest(repositoryId, localId, options = {}) {
208
249
  return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/review`, undefined, {
209
250
  ...compactBody({
210
251
  reviewComment: options.reviewComment,
211
- submitDraftCommentIds: options.submitDraftCommentIds
252
+ submitDraftCommentIds: options.submitDraftCommentIds,
212
253
  }),
213
- reviewOpinion: options.reviewOpinion ?? "PASS"
254
+ reviewOpinion: options.reviewOpinion ?? "PASS",
214
255
  });
215
256
  }
216
257
  mergeChangeRequest(repositoryId, localId, options = {}) {
217
258
  return this.request("POST", `${this.repositoryChangeRequestPath(repositoryId, localId)}/merge`, undefined, {
218
259
  ...compactBody({
219
260
  mergeMessage: options.mergeMessage,
220
- removeSourceBranch: options.removeSourceBranch
261
+ removeSourceBranch: options.removeSourceBranch,
221
262
  }),
222
- mergeType: options.mergeType ?? "no-fast-forward"
263
+ mergeType: options.mergeType ?? "no-fast-forward",
223
264
  });
224
265
  }
225
266
  async approveAndMerge(repositoryId, localId, options = {}) {
226
267
  await this.reviewChangeRequest(repositoryId, localId, {
227
268
  reviewComment: options.reviewComment,
228
269
  reviewOpinion: options.reviewOpinion ?? "PASS",
229
- submitDraftCommentIds: options.submitDraftCommentIds
270
+ submitDraftCommentIds: options.submitDraftCommentIds,
230
271
  });
231
272
  return this.mergeChangeRequest(repositoryId, localId, {
232
273
  mergeMessage: options.mergeMessage,
233
274
  mergeType: options.mergeType,
234
- removeSourceBranch: options.removeSourceBranch
275
+ removeSourceBranch: options.removeSourceBranch,
235
276
  });
236
277
  }
237
278
  getChangeRequestTree(repositoryId, localId, options) {
@@ -241,52 +282,7 @@ export class YunxiaoClient {
241
282
  return this.request("GET", `${this.repositoryChangeRequestPath(repositoryId, localId)}/diffs/patches`);
242
283
  }
243
284
  async releaseByRepositoryName(options) {
244
- const [currentUser, repositories] = await Promise.all([
245
- this.getCurrentUser(),
246
- this.listRepositories({
247
- page: 1,
248
- perPage: 20,
249
- search: options.repositoryName,
250
- archived: false
251
- })
252
- ]);
253
- const repository = findRepositoryByName(repositories, options.repositoryName);
254
- if (!repository) {
255
- throw new Error(`Repository not found by name: ${options.repositoryName}`);
256
- }
257
- const repositoryId = String(repository.id);
258
- const title = options.title ?? `Release ${options.sourceBranch} into ${options.targetBranch}`;
259
- const changeRequest = await this.createChangeRequest(repositoryId, {
260
- sourceBranch: options.sourceBranch,
261
- targetBranch: options.targetBranch,
262
- sourceProjectId: repository.id,
263
- targetProjectId: repository.id,
264
- reviewerUserIds: [currentUser.id],
265
- title,
266
- description: options.description
267
- });
268
- const localId = localIdFromChangeRequest(changeRequest);
269
- if (options.createOnly) {
270
- return {
271
- repository,
272
- changeRequest,
273
- review: null,
274
- merge: null
275
- };
276
- }
277
- const review = await this.reviewChangeRequest(repositoryId, localId, {
278
- reviewComment: "Approved by yunxiao-cli release workflow"
279
- });
280
- const merge = await this.mergeChangeRequest(repositoryId, localId, {
281
- mergeType: options.mergeType,
282
- removeSourceBranch: false
283
- });
284
- return {
285
- repository,
286
- changeRequest,
287
- review,
288
- merge
289
- };
285
+ return release(this, options);
290
286
  }
291
287
  changeRequestsPath() {
292
288
  if (this.edition === "center") {
@@ -344,35 +340,15 @@ export class YunxiaoClient {
344
340
  return `${this.pipelinesPath()}/${encodedPipelineId}`;
345
341
  }
346
342
  async listAllPipelines(options) {
347
- const rows = [];
348
- const perPage = options.perPage ?? 30;
349
- for (let page = options.page ?? 1;; page += 1) {
350
- const response = await this.listPipelines({ ...options, page, perPage });
351
- const pageRows = arrayRecords(response);
352
- rows.push(...pageRows);
353
- if (pageRows.length < perPage) {
354
- break;
355
- }
356
- }
357
- return rows;
343
+ return (await paginate((page, perPage) => this.listPipelines({ ...options, page, perPage }), { ...options, allPages: true }));
358
344
  }
359
345
  async listAllPipelineRuns(pipelineId, options) {
360
- const rows = [];
361
- const perPage = options.perPage ?? 30;
362
- for (let page = options.page ?? 1;; page += 1) {
363
- const response = await this.listPipelineRuns(pipelineId, { ...options, page, perPage });
364
- const pageRows = arrayRecords(response);
365
- rows.push(...pageRows);
366
- if (pageRows.length < perPage) {
367
- break;
368
- }
369
- }
370
- return rows;
346
+ return (await paginate((page, perPage) => this.listPipelineRuns(pipelineId, { ...options, page, perPage }), { ...options, allPages: true }));
371
347
  }
372
348
  async request(method, path, query, body) {
373
349
  const url = new URL(`https://${this.domain}${path}`);
374
350
  for (const [key, value] of Object.entries(query ?? {})) {
375
- if (value !== undefined && value !== null && value !== "") {
351
+ if (value !== undefined && value !== null) {
376
352
  url.searchParams.set(key, String(value));
377
353
  }
378
354
  }
@@ -380,21 +356,20 @@ export class YunxiaoClient {
380
356
  method,
381
357
  headers: {
382
358
  "content-type": "application/json",
383
- "x-yunxiao-token": this.token
384
- }
359
+ "x-yunxiao-token": this.token,
360
+ },
385
361
  };
386
362
  if (body) {
387
363
  init.body = JSON.stringify(compactBody(body));
388
364
  }
389
- const response = await this.fetcher(url.toString(), init);
390
- const text = await response.text();
391
- if (!response.ok) {
392
- throw new YunxiaoApiError(`Yunxiao API request failed with HTTP ${response.status}: ${method} ${url.toString()}${text ? `\n${text}` : ""}`, response.status, text, method, url.toString());
393
- }
394
- if (!text) {
395
- return null;
396
- }
397
- 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
+ });
398
373
  }
399
374
  }
400
375
  function stripProtocol(domain) {
@@ -431,7 +406,7 @@ function uniqueBy(values, keyFn) {
431
406
  }
432
407
  function timestampInRange(value, startTime, endTime) {
433
408
  const timestamp = Number(value);
434
- return Number.isFinite(timestamp) && timestamp >= startTime && timestamp <= endTime;
409
+ return (Number.isFinite(timestamp) && timestamp >= startTime && timestamp <= endTime);
435
410
  }
436
411
  function normalizeNumericId(value) {
437
412
  const numberValue = Number(value);
@@ -473,31 +448,9 @@ function triggerModeName(value) {
473
448
  }
474
449
  }
475
450
  function compactBody(body) {
476
- 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));
477
452
  }
478
453
  function normalizeProjectId(value) {
479
454
  const numberValue = typeof value === "string" ? Number(value) : value;
480
455
  return Number.isInteger(numberValue) ? numberValue : value;
481
456
  }
482
- function findRepositoryByName(repositories, repositoryName) {
483
- const normalizedName = normalizeRepositoryName(repositoryName);
484
- const matches = repositories.filter((repository) => [repository.name, repository.path, repository.nameWithNamespace, repository.pathWithNamespace]
485
- .filter((value) => typeof value === "string")
486
- .some((value) => normalizeRepositoryName(value) === normalizedName));
487
- if (matches.length > 1) {
488
- throw new Error(`Multiple repositories matched name: ${repositoryName}. Use a more specific path.`);
489
- }
490
- return matches[0];
491
- }
492
- function normalizeRepositoryName(value) {
493
- return value.replace(/\s+\/\s+/g, "/").trim().toLowerCase();
494
- }
495
- function localIdFromChangeRequest(changeRequest) {
496
- if (typeof changeRequest === "object" && changeRequest !== null && "localId" in changeRequest) {
497
- const localId = Number(changeRequest.localId);
498
- if (Number.isInteger(localId) && localId > 0) {
499
- return localId;
500
- }
501
- }
502
- throw new Error("Create change request response did not include a valid localId.");
503
- }
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ import { YunxiaoClient } from "./client.js";
2
+ import type { Flags } from "./arguments.js";
3
+ export declare function record(v: unknown): Record<string, unknown>;
4
+ export declare function records(v: unknown): Record<string, unknown>[];
5
+ export declare function required(f: Flags, key: string): string;
6
+ export declare function confirmed(f: Flags): void;
7
+ export declare function latestPatch(patches: Record<string, unknown>[], type: string): Record<string, unknown>;
8
+ export declare function patchPair(client: YunxiaoClient, repo: string, id: number, f: Flags): Promise<{
9
+ from: Record<string, unknown>;
10
+ to: Record<string, unknown>;
11
+ }>;
12
+ export declare function mrDiff(client: YunxiaoClient, repo: string, id: number, f: Flags): Promise<unknown>;
13
+ export declare function runExtendedMr(command: string, p: string[], f: Flags, c: YunxiaoClient): Promise<unknown>;