@ycadence/cli 0.1.0 → 0.2.1

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.
Files changed (3) hide show
  1. package/README.md +39 -14
  2. package/dist/bin.js +3642 -327
  3. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin.ts
4
- import { readFile } from "node:fs/promises";
4
+ import { spawn as spawn2 } from "node:child_process";
5
+ import { readFile as readFile3 } from "node:fs/promises";
6
+ import { createInterface } from "node:readline";
7
+
8
+ // src/runner.ts
9
+ import { randomUUID } from "node:crypto";
5
10
 
6
11
  // ../core/src/env.ts
7
12
  var ENV_PREFIX = "YCADENCE_";
@@ -20,129 +25,370 @@ function readEnv(name, env = processEnv()) {
20
25
  return;
21
26
  }
22
27
 
23
- // ../sdk/src/client.ts
28
+ // ../sdk/src/errors.ts
24
29
  class YCadenceError extends Error {
25
30
  status;
26
31
  code;
27
32
  issues;
28
- constructor(status, code, message, issues) {
33
+ requestId;
34
+ details;
35
+ retryAfter;
36
+ method;
37
+ path;
38
+ constructor(status, code, message, issues, extras = {}) {
29
39
  super(message);
30
40
  this.status = status;
31
41
  this.code = code;
32
42
  this.issues = issues;
33
43
  this.name = "YCadenceError";
44
+ this.requestId = extras.requestId;
45
+ this.details = extras.details;
46
+ this.retryAfter = extras.retryAfter;
47
+ this.method = extras.method;
48
+ this.path = extras.path;
34
49
  }
35
50
  get retryable() {
36
- return this.status === 429 || this.status >= 500;
51
+ return this.status === 0 || this.status === 408 || this.status === 429 || this.status >= 500;
37
52
  }
38
53
  }
39
54
 
55
+ class YCadenceValidationError extends YCadenceError {
56
+ name = "YCadenceValidationError";
57
+ }
58
+
59
+ class YCadencePermissionError extends YCadenceError {
60
+ name = "YCadencePermissionError";
61
+ }
62
+
63
+ class YCadenceNotFoundError extends YCadenceError {
64
+ name = "YCadenceNotFoundError";
65
+ }
66
+
67
+ class YCadenceConflictError extends YCadenceError {
68
+ name = "YCadenceConflictError";
69
+ }
70
+
71
+ class YCadencePreconditionError extends YCadenceError {
72
+ name = "YCadencePreconditionError";
73
+ }
74
+
75
+ class YCadenceRateLimitError extends YCadenceError {
76
+ name = "YCadenceRateLimitError";
77
+ }
78
+
79
+ class YCadenceNetworkError extends YCadenceError {
80
+ name = "YCadenceNetworkError";
81
+ }
82
+ function errorClassFor(status) {
83
+ if (status === 0 || status === 408)
84
+ return YCadenceNetworkError;
85
+ if (status === 400 || status === 422)
86
+ return YCadenceValidationError;
87
+ if (status === 401 || status === 403)
88
+ return YCadencePermissionError;
89
+ if (status === 404)
90
+ return YCadenceNotFoundError;
91
+ if (status === 409)
92
+ return YCadenceConflictError;
93
+ if (status === 412)
94
+ return YCadencePreconditionError;
95
+ if (status === 429)
96
+ return YCadenceRateLimitError;
97
+ return YCadenceError;
98
+ }
99
+ function createError(status, code, message, issues, extras = {}) {
100
+ const ErrorClass = errorClassFor(status);
101
+ return new ErrorClass(status, code, message, issues, extras);
102
+ }
103
+
104
+ class RequestIntercepted extends Error {
105
+ request;
106
+ name = "RequestIntercepted";
107
+ constructor(request) {
108
+ super(`${request.method} ${request.url} was not sent.`);
109
+ this.request = request;
110
+ }
111
+ }
112
+
113
+ // ../sdk/src/operations.ts
114
+ var OPERATIONS = {
115
+ health: { method: "GET", path: "/api/health" },
116
+ me: { method: "GET", path: "/api/v1/me" },
117
+ listProjects: { method: "GET", path: "/api/v1/projects" },
118
+ listWorkItems: { method: "GET", path: "/api/v1/projects/{projectId}/work-items" },
119
+ createWorkItem: { method: "POST", path: "/api/v1/projects/{projectId}/work-items" },
120
+ getWorkItem: { method: "GET", path: "/api/v1/work-items/{workItemId}" },
121
+ updateWorkItem: { method: "PATCH", path: "/api/v1/work-items/{workItemId}" },
122
+ deleteWorkItem: { method: "DELETE", path: "/api/v1/work-items/{workItemId}" },
123
+ listStates: { method: "GET", path: "/api/v1/projects/{projectId}/states" },
124
+ listLabels: { method: "GET", path: "/api/v1/projects/{projectId}/labels" },
125
+ createLabel: { method: "POST", path: "/api/v1/projects/{projectId}/labels" },
126
+ listTypes: { method: "GET", path: "/api/v1/projects/{projectId}/types" },
127
+ listCycles: { method: "GET", path: "/api/v1/projects/{projectId}/cycles" },
128
+ createCycle: { method: "POST", path: "/api/v1/projects/{projectId}/cycles" },
129
+ getCycleProgress: { method: "GET", path: "/api/v1/cycles/{cycleId}/progress" },
130
+ addWorkItemsToCycle: { method: "POST", path: "/api/v1/cycles/{cycleId}/work-items" },
131
+ listViews: { method: "GET", path: "/api/v1/projects/{projectId}/views" },
132
+ createView: { method: "POST", path: "/api/v1/projects/{projectId}/views" },
133
+ listIntake: { method: "GET", path: "/api/v1/projects/{projectId}/intake" },
134
+ submitIntake: { method: "POST", path: "/api/v1/projects/{projectId}/intake" },
135
+ triageIntake: { method: "POST", path: "/api/v1/intake-items/{intakeItemId}/triage" },
136
+ previewImport: { method: "POST", path: "/api/v1/projects/{projectId}/imports/preview" },
137
+ startImport: { method: "POST", path: "/api/v1/projects/{projectId}/imports" },
138
+ listImports: { method: "GET", path: "/api/v1/projects/{projectId}/imports" },
139
+ getImport: { method: "GET", path: "/api/v1/imports/{runId}" }
140
+ };
141
+ function isIdempotentMethod(method) {
142
+ return method === "GET" || method === "HEAD";
143
+ }
144
+ function buildPath(template, params = {}) {
145
+ return template.replace(/\{(\w+)\}/g, (_match, name) => {
146
+ const value = params[name];
147
+ if (value === undefined || value === "") {
148
+ throw new TypeError(`Missing path parameter "${name}" for ${template}.`);
149
+ }
150
+ return encodeURIComponent(value);
151
+ });
152
+ }
153
+
154
+ // ../sdk/src/pagination.ts
155
+ async function* paginate(fetchPage, options = {}) {
156
+ let cursor = options.cursor;
157
+ let yielded = 0;
158
+ do {
159
+ const page = await fetchPage(cursor);
160
+ for (const item of page.items) {
161
+ if (options.maxItems !== undefined && yielded >= options.maxItems)
162
+ return;
163
+ yielded += 1;
164
+ yield item;
165
+ }
166
+ cursor = page.nextCursor ?? undefined;
167
+ } while (cursor !== undefined);
168
+ }
169
+
170
+ // ../sdk/src/version.ts
171
+ var SDK_VERSION = "0.2.0";
172
+
173
+ // ../sdk/src/client.ts
174
+ var DEFAULT_RETRY = { retries: 2, baseDelayMs: 250, maxDelayMs: 8000 };
175
+
40
176
  class YCadenceClient {
177
+ options;
41
178
  baseUrl;
42
179
  apiKey;
43
180
  identityToken;
44
181
  timeoutMs;
45
182
  doFetch;
183
+ retry;
184
+ userAgent;
185
+ onResponse;
186
+ sleep;
46
187
  constructor(options) {
188
+ this.options = options;
47
189
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
48
190
  this.apiKey = options.apiKey;
49
191
  this.identityToken = options.identityToken;
50
192
  this.timeoutMs = options.timeoutMs ?? 30000;
51
- this.doFetch = options.fetch ?? globalThis.fetch;
193
+ this.doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
194
+ this.retry = { ...DEFAULT_RETRY, ...options.retry };
195
+ this.userAgent = [options.userAgent, `ycadence-sdk/${SDK_VERSION}`].filter((part) => part !== undefined && part !== "").join(" ");
196
+ this.onResponse = options.onResponse;
197
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
198
+ }
199
+ withOptions(overrides) {
200
+ return new YCadenceClient({ ...this.options, ...overrides });
201
+ }
202
+ get fetchImplementation() {
203
+ return this.doFetch;
52
204
  }
53
205
  async health() {
54
- return this.request("GET", "/api/health");
55
- }
56
- async listWorkItems(projectId, options = {}) {
57
- const query = new URLSearchParams;
58
- if (options.filter !== undefined)
59
- query.set("filter", JSON.stringify(options.filter));
60
- if (options.limit !== undefined)
61
- query.set("limit", String(options.limit));
62
- if (options.cursor !== undefined)
63
- query.set("cursor", options.cursor);
64
- return this.request("GET", `/api/v1/projects/${projectId}/work-items?${query}`);
65
- }
66
- async* iterateWorkItems(projectId, options = {}) {
67
- let cursor;
68
- do {
69
- const page = await this.listWorkItems(projectId, {
70
- filter: options.filter,
71
- limit: options.pageSize ?? 100,
72
- cursor
73
- });
74
- for (const item of page.items)
75
- yield item;
76
- cursor = page.nextCursor ?? undefined;
77
- } while (cursor !== undefined);
206
+ return this.call("health");
207
+ }
208
+ async me(options) {
209
+ return this.call("me", {}, undefined, undefined, options);
210
+ }
211
+ async listProjects(query = {}, options) {
212
+ return this.call("listProjects", {}, query, undefined, options);
213
+ }
214
+ async listWorkItems(projectId, query = {}, options) {
215
+ return this.call("listWorkItems", { projectId }, {
216
+ filter: query.filter === undefined ? undefined : JSON.stringify(query.filter),
217
+ limit: query.limit,
218
+ cursor: query.cursor,
219
+ state: query.state,
220
+ priority: query.priority
221
+ }, undefined, options);
222
+ }
223
+ iterateWorkItems(projectId, options = {}) {
224
+ return paginate((cursor) => this.listWorkItems(projectId, {
225
+ filter: options.filter,
226
+ limit: options.pageSize ?? 100,
227
+ cursor
228
+ }));
229
+ }
230
+ async createWorkItem(projectId, body, options) {
231
+ return this.call("createWorkItem", { projectId }, undefined, body, options);
232
+ }
233
+ async getWorkItem(workItemId, options) {
234
+ return this.call("getWorkItem", { workItemId }, undefined, undefined, options);
235
+ }
236
+ async updateWorkItem(workItemId, body, options) {
237
+ return this.call("updateWorkItem", { workItemId }, undefined, body, options);
238
+ }
239
+ async deleteWorkItem(workItemId, options) {
240
+ await this.call("deleteWorkItem", { workItemId }, undefined, undefined, options);
241
+ }
242
+ async listStates(projectId, options) {
243
+ return this.call("listStates", { projectId }, undefined, undefined, options);
244
+ }
245
+ async listLabels(projectId, options) {
246
+ return this.call("listLabels", { projectId }, undefined, undefined, options);
247
+ }
248
+ async createLabel(projectId, body, options) {
249
+ return this.call("createLabel", { projectId }, undefined, body, options);
250
+ }
251
+ async listTypes(projectId, options) {
252
+ return this.call("listTypes", { projectId }, undefined, undefined, options);
253
+ }
254
+ async listCycles(projectId, options) {
255
+ return this.call("listCycles", { projectId }, undefined, undefined, options);
78
256
  }
79
- async createWorkItem(projectId, body) {
80
- return this.request("POST", `/api/v1/projects/${projectId}/work-items`, body);
257
+ async createCycle(projectId, body, options) {
258
+ return this.call("createCycle", { projectId }, undefined, body, options);
81
259
  }
82
- async getWorkItem(workItemId) {
83
- return this.request("GET", `/api/v1/work-items/${workItemId}`);
260
+ async getCycleProgress(cycleId, options) {
261
+ return this.call("getCycleProgress", { cycleId }, undefined, undefined, options);
84
262
  }
85
- async updateWorkItem(workItemId, body) {
86
- return this.request("PATCH", `/api/v1/work-items/${workItemId}`, body);
263
+ async addWorkItemsToCycle(cycleId, workItemIds, options) {
264
+ return this.call("addWorkItemsToCycle", { cycleId }, undefined, { workItemIds }, options);
87
265
  }
88
- async deleteWorkItem(workItemId) {
89
- await this.request("DELETE", `/api/v1/work-items/${workItemId}`);
266
+ async listViews(projectId, options) {
267
+ return this.call("listViews", { projectId }, undefined, undefined, options);
90
268
  }
91
- async listStates(projectId) {
92
- return this.request("GET", `/api/v1/projects/${projectId}/states`);
269
+ async createView(projectId, body, options) {
270
+ return this.call("createView", { projectId }, undefined, body, options);
93
271
  }
94
- async listLabels(projectId) {
95
- return this.request("GET", `/api/v1/projects/${projectId}/labels`);
272
+ async listIntake(projectId, query = {}, options) {
273
+ return this.call("listIntake", { projectId }, query, undefined, options);
96
274
  }
97
- async listCycles(projectId) {
98
- return this.request("GET", `/api/v1/projects/${projectId}/cycles`);
275
+ async submitIntake(projectId, body, options) {
276
+ return this.call("submitIntake", { projectId }, undefined, body, options);
99
277
  }
100
- async previewImport(projectId, body) {
101
- return this.request("POST", `/api/v1/projects/${projectId}/imports/preview`, body);
278
+ async triageIntake(intakeItemId, body, options) {
279
+ return this.call("triageIntake", { intakeItemId }, undefined, body, options);
102
280
  }
103
- async startImport(projectId, body) {
104
- return this.request("POST", `/api/v1/projects/${projectId}/imports`, body);
281
+ async previewImport(projectId, body, options) {
282
+ return this.call("previewImport", { projectId }, undefined, body, options);
105
283
  }
106
- async getImport(runId) {
107
- return this.request("GET", `/api/v1/imports/${runId}`);
284
+ async startImport(projectId, body, options) {
285
+ return this.call("startImport", { projectId }, undefined, body, options);
108
286
  }
109
- async listImports(projectId) {
110
- return this.request("GET", `/api/v1/projects/${projectId}/imports`);
287
+ async getImport(runId, options) {
288
+ return this.call("getImport", { runId }, undefined, undefined, options);
111
289
  }
112
- async listViews(projectId) {
113
- return this.request("GET", `/api/v1/projects/${projectId}/views`);
290
+ async listImports(projectId, options) {
291
+ return this.call("listImports", { projectId }, undefined, undefined, options);
114
292
  }
115
- async request(method, path, body) {
293
+ async call(operation, params = {}, query, body, options = {}) {
294
+ const { method, path } = OPERATIONS[operation];
295
+ let url = buildPath(path, params);
296
+ if (query !== undefined) {
297
+ const search = new URLSearchParams;
298
+ for (const [name, value] of Object.entries(query)) {
299
+ if (value !== undefined)
300
+ search.set(name, String(value));
301
+ }
302
+ const encoded = search.toString();
303
+ if (encoded.length > 0)
304
+ url += `?${encoded}`;
305
+ }
306
+ return this.request(method, url, body, options);
307
+ }
308
+ async request(method, path, body, options) {
309
+ const retryable = isIdempotentMethod(method) || options.idempotencyKey !== undefined;
310
+ const retries = retryable ? options.retries ?? this.retry.retries : 0;
311
+ for (let attempt = 0;; attempt += 1) {
312
+ try {
313
+ return await this.send(method, path, body, options, attempt);
314
+ } catch (error) {
315
+ const failure = error;
316
+ const again = attempt < retries && typeof failure.retryable === "boolean" && (failure.retryable || failure.code === "request_in_progress");
317
+ if (!again)
318
+ throw error;
319
+ await this.sleep(this.backoff(attempt, failure.retryAfter));
320
+ }
321
+ }
322
+ }
323
+ backoff(attempt, retryAfter) {
324
+ if (retryAfter !== undefined)
325
+ return Math.min(retryAfter * 1000, this.retry.maxDelayMs);
326
+ const exponential = this.retry.baseDelayMs * 2 ** attempt;
327
+ const jittered = exponential / 2 + Math.random() * (exponential / 2);
328
+ return Math.min(jittered, this.retry.maxDelayMs);
329
+ }
330
+ async send(method, path, body, options, attempt) {
116
331
  const controller = new AbortController;
117
332
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
333
+ const abort = () => controller.abort();
334
+ options.signal?.addEventListener("abort", abort);
335
+ const started = Date.now();
118
336
  let response;
119
337
  try {
120
338
  const identityToken = typeof this.identityToken === "function" ? await this.identityToken() : this.identityToken;
339
+ const headers = {
340
+ "x-api-key": this.apiKey,
341
+ ...identityToken === undefined || identityToken.length === 0 ? {} : { Authorization: `Bearer ${identityToken}` },
342
+ ...body === undefined ? {} : { "Content-Type": "application/json" },
343
+ Accept: "application/json",
344
+ "User-Agent": this.userAgent
345
+ };
346
+ if (options.idempotencyKey !== undefined && !isIdempotentMethod(method)) {
347
+ headers["Idempotency-Key"] = options.idempotencyKey;
348
+ }
349
+ if (options.expectedVersion !== undefined && !isIdempotentMethod(method)) {
350
+ headers["If-Match"] = `"${String(options.expectedVersion).replace(/"/g, "")}"`;
351
+ }
121
352
  response = await this.doFetch(`${this.baseUrl}${path}`, {
122
353
  method,
123
- headers: {
124
- "x-api-key": this.apiKey,
125
- ...identityToken === undefined || identityToken.length === 0 ? {} : { Authorization: `Bearer ${identityToken}` },
126
- ...body === undefined ? {} : { "Content-Type": "application/json" }
127
- },
354
+ headers,
128
355
  body: body === undefined ? undefined : JSON.stringify(body),
129
356
  signal: controller.signal
130
357
  });
131
358
  } catch (error) {
359
+ if (error instanceof RequestIntercepted)
360
+ throw error;
132
361
  if (error instanceof Error && error.name === "AbortError") {
133
- throw new YCadenceError(408, "timeout", `${method} ${path} timed out after ${this.timeoutMs}ms.`);
362
+ throw createError(408, "timeout", `${method} ${path} timed out after ${this.timeoutMs}ms.`, undefined, { method, path });
134
363
  }
135
- throw new YCadenceError(0, "network_error", `Could not reach ${this.baseUrl}: ${error instanceof Error ? error.message : "unknown error"}`);
364
+ throw createError(0, "network_error", `Could not reach ${this.baseUrl}: ${error instanceof Error ? error.message : "unknown error"}`, undefined, { method, path });
136
365
  } finally {
137
366
  clearTimeout(timer);
367
+ options.signal?.removeEventListener("abort", abort);
138
368
  }
369
+ const headerRequestId = response.headers?.get?.("x-request-id") ?? undefined;
370
+ this.onResponse?.({
371
+ method,
372
+ path,
373
+ status: response.status,
374
+ requestId: headerRequestId,
375
+ durationMs: Date.now() - started,
376
+ attempt,
377
+ replayed: response.headers?.get?.("idempotent-replayed") === "true"
378
+ });
139
379
  if (response.status === 204)
140
380
  return;
141
381
  const text = await response.text();
142
382
  const parsed = text.length === 0 ? undefined : safeJson(text);
143
383
  if (!response.ok) {
144
- const failure = parsed ?? {};
145
- throw new YCadenceError(response.status, failure.code ?? "unknown", failure.error ?? `${method} ${path} failed with ${response.status}.`, failure.issues);
384
+ const failure = isRecord(parsed) ? parsed : {};
385
+ throw createError(response.status, typeof failure.code === "string" ? failure.code : codeForStatus(response.status), typeof failure.error === "string" ? failure.error : `${method} ${path} failed with ${response.status}.`, failure.issues, {
386
+ requestId: failure.requestId ?? headerRequestId,
387
+ details: failure.details,
388
+ retryAfter: parseRetryAfter(response.headers?.get?.("retry-after") ?? null),
389
+ method,
390
+ path
391
+ });
146
392
  }
147
393
  return parsed;
148
394
  }
@@ -154,316 +400,3385 @@ function safeJson(text) {
154
400
  return;
155
401
  }
156
402
  }
157
- // src/commands.ts
158
- var ok = (output) => ({ output, exitCode: 0 });
159
- var fail = (output, exitCode = 1) => ({ output, exitCode });
160
- var USAGE = `ycadence — project and work management from the command line
161
-
162
- ycadence doctor check configuration and connectivity
163
- ycadence work-item list <projectId> list work items
164
- ycadence work-item create <projectId> --title "..." [--priority high]
165
- ycadence work-item show <workItemId>
166
- ycadence work-item close <workItemId>
167
- ycadence states <projectId>
168
- ycadence cycles <projectId>
169
- ycadence import preview <projectId> --source csv --file issues.csv
170
- ycadence import run <projectId> --source csv --file issues.csv [--map "Done=<stateId>"]
171
- ycadence import status <runId>
172
- ycadence import list <projectId>
173
-
174
- Configuration:
175
- YCADENCE_URL the instance, e.g. https://devpulse.yarlis.com
403
+ function isRecord(value) {
404
+ return typeof value === "object" && value !== null && !Array.isArray(value);
405
+ }
406
+ function codeForStatus(status) {
407
+ const codes = {
408
+ 400: "bad_request",
409
+ 401: "unauthorized",
410
+ 403: "forbidden",
411
+ 404: "not_found",
412
+ 409: "conflict",
413
+ 412: "precondition_failed",
414
+ 422: "validation_failed",
415
+ 429: "rate_limited"
416
+ };
417
+ return codes[status] ?? (status >= 500 ? "server_error" : "unknown");
418
+ }
419
+ function parseRetryAfter(value, now = Date.now()) {
420
+ if (value === null || value.trim() === "")
421
+ return;
422
+ const seconds = Number(value);
423
+ if (Number.isFinite(seconds))
424
+ return Math.max(0, seconds);
425
+ const date = Date.parse(value);
426
+ if (Number.isNaN(date))
427
+ return;
428
+ return Math.max(0, Math.ceil((date - now) / 1000));
429
+ }
430
+ // ../sdk/src/types.ts
431
+ var PRIORITIES = ["urgent", "high", "medium", "low", "none"];
432
+ var WORK_ITEM_FIELDS = {
433
+ id: true,
434
+ key: true,
435
+ sequenceId: true,
436
+ name: true,
437
+ priority: true,
438
+ stateId: true,
439
+ assigneeIds: true,
440
+ labelIds: true,
441
+ parentId: true,
442
+ sortOrder: true,
443
+ createdAt: true,
444
+ projectId: true,
445
+ version: true
446
+ };
447
+ var WORK_ITEM_DETAIL_FIELDS = {
448
+ ...WORK_ITEM_FIELDS,
449
+ description: true,
450
+ startDate: true,
451
+ targetDate: true,
452
+ archivedAt: true,
453
+ updatedAt: true
454
+ };
455
+ // src/config.ts
456
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
457
+ import { join } from "node:path";
458
+
459
+ // src/exit.ts
460
+ var EXIT = {
461
+ ok: 0,
462
+ failure: 1,
463
+ usage: 2,
464
+ notFound: 3,
465
+ permission: 4,
466
+ conflict: 5,
467
+ validation: 6,
468
+ pending: 7,
469
+ tempfail: 75,
470
+ config: 78
471
+ };
472
+ var EXIT_CODE_TABLE = [
473
+ [EXIT.ok, "Success, confirmed by the server"],
474
+ [EXIT.failure, "Generic failure"],
475
+ [EXIT.usage, "Usage error: unknown command, bad flag, missing argument"],
476
+ [EXIT.notFound, "Not found (404)"],
477
+ [EXIT.permission, "Permission denied (401, 403)"],
478
+ [EXIT.conflict, "Conflict or failed precondition (409, 412)"],
479
+ [EXIT.validation, "Validation failed (400, 422)"],
480
+ [EXIT.pending, "Pending: approval or confirmation required (202)"],
481
+ [EXIT.tempfail, "Temporary failure; a retry may succeed (429, 5xx, network, timeout)"],
482
+ [EXIT.config, "Configuration error: no URL or key, or sign-in needed"]
483
+ ];
484
+
485
+ class UsageError extends Error {
486
+ name = "UsageError";
487
+ code = "usage";
488
+ exitCode = EXIT.usage;
489
+ }
490
+
491
+ class ConfigError extends Error {
492
+ name = "ConfigError";
493
+ code = "config";
494
+ exitCode = EXIT.config;
495
+ }
496
+
497
+ class CliError extends Error {
498
+ code;
499
+ exitCode;
500
+ name = "CliError";
501
+ constructor(message, code, exitCode = EXIT.failure) {
502
+ super(message);
503
+ this.code = code;
504
+ this.exitCode = exitCode;
505
+ }
506
+ }
507
+ function exitCodeForStatus(status) {
508
+ if (status === 0 || status === 408 || status === 429 || status >= 500)
509
+ return EXIT.tempfail;
510
+ if (status === 404)
511
+ return EXIT.notFound;
512
+ if (status === 401 || status === 403)
513
+ return EXIT.permission;
514
+ if (status === 409 || status === 412)
515
+ return EXIT.conflict;
516
+ if (status === 400 || status === 422)
517
+ return EXIT.validation;
518
+ if (status === 202)
519
+ return EXIT.pending;
520
+ return EXIT.failure;
521
+ }
522
+ function exitCodeFor(error) {
523
+ if (error instanceof YCadenceError)
524
+ return exitCodeForStatus(error.status);
525
+ if (error instanceof UsageError || error instanceof ConfigError || error instanceof CliError) {
526
+ return error.exitCode;
527
+ }
528
+ return EXIT.failure;
529
+ }
530
+
531
+ // src/config.ts
532
+ var PROFILE_KEYS = [
533
+ "url",
534
+ "org",
535
+ "project",
536
+ "format",
537
+ "oauthClientId",
538
+ "iapAudience"
539
+ ];
540
+ var PROFILE_KEY_HELP = {
541
+ url: "The instance, e.g. https://ycadence.example.com",
542
+ org: "Organization slug",
543
+ project: "Default project id (or identifier, once the server lists projects)",
544
+ format: "Default output format: table, json or ndjson",
545
+ oauthClientId: "Desktop OAuth client id used by `ycadence login` for IAP",
546
+ iapAudience: "The IAP OAuth client id; the audience of the identity token"
547
+ };
548
+ function configDirectory(env) {
549
+ if (env.YCADENCE_CONFIG_DIR)
550
+ return env.YCADENCE_CONFIG_DIR;
551
+ if (env.XDG_CONFIG_HOME)
552
+ return join(env.XDG_CONFIG_HOME, "ycadence");
553
+ const home = env.HOME ?? env.USERPROFILE;
554
+ return home ? join(home, ".config", "ycadence") : undefined;
555
+ }
556
+ function fileConfigStore(directory) {
557
+ const path = join(directory, "config.json");
558
+ return {
559
+ path,
560
+ directory,
561
+ async load() {
562
+ let text;
563
+ try {
564
+ text = await readFile(path, "utf8");
565
+ } catch (error) {
566
+ if (error.code === "ENOENT")
567
+ return { profiles: {} };
568
+ throw error;
569
+ }
570
+ try {
571
+ const parsed = JSON.parse(text);
572
+ return { currentProfile: parsed.currentProfile, profiles: parsed.profiles ?? {} };
573
+ } catch {
574
+ throw new ConfigError(`${path} is not valid JSON. Fix or delete it.`);
575
+ }
576
+ },
577
+ async save(config) {
578
+ await mkdir(directory, { recursive: true, mode: 448 });
579
+ const temporary = `${path}.${process.pid}.tmp`;
580
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}
581
+ `, { mode: 384 });
582
+ await chmod(temporary, 384);
583
+ await rename(temporary, path);
584
+ }
585
+ };
586
+ }
587
+ function memoryConfigStore(initial = { profiles: {} }) {
588
+ let current = structuredClone(initial);
589
+ return {
590
+ path: undefined,
591
+ directory: undefined,
592
+ async load() {
593
+ return structuredClone(current);
594
+ },
595
+ async save(config) {
596
+ current = structuredClone(config);
597
+ }
598
+ };
599
+ }
600
+ function assertProfileKey(key) {
601
+ if (key === "apiKey" || key === "api-key" || key === "token") {
602
+ throw new UsageError("Keys are not stored in the config file. Use `ycadence login --api-key-stdin`, which keeps it in the OS keychain.");
603
+ }
604
+ if (!PROFILE_KEYS.includes(key)) {
605
+ throw new UsageError(`Unknown setting "${key}". One of: ${PROFILE_KEYS.join(", ")}.`);
606
+ }
607
+ }
608
+ function assertProfileName(name) {
609
+ if (!/^[A-Za-z0-9_.-]{1,64}$/.test(name)) {
610
+ throw new UsageError(`Profile names are letters, digits, ".", "_" and "-"; got "${name}".`);
611
+ }
612
+ }
613
+ function envSource(name, env) {
614
+ const current = env[`YCADENCE_${name}`];
615
+ return current !== undefined && current !== "" ? "env" : "legacy-env";
616
+ }
617
+ function resolveConfig(file, env, flags) {
618
+ const profileFromEnv = readEnv("PROFILE", env);
619
+ const profile = flags.profile ?? profileFromEnv ?? file.currentProfile ?? "default";
620
+ const stored = file.profiles[profile] ?? {};
621
+ const sources = {
622
+ profile: flags.profile !== undefined ? "flag" : profileFromEnv !== undefined ? envSource("PROFILE", env) : file.currentProfile !== undefined ? "profile" : "default"
623
+ };
624
+ const pick = (key, envName, flag) => {
625
+ if (flag !== undefined) {
626
+ sources[key] = "flag";
627
+ return flag;
628
+ }
629
+ const fromEnv = readEnv(envName, env);
630
+ if (fromEnv !== undefined) {
631
+ sources[key] = envSource(envName, env);
632
+ return fromEnv;
633
+ }
634
+ const fromProfile = stored[key];
635
+ sources[key] = fromProfile === undefined ? "none" : "profile";
636
+ return fromProfile;
637
+ };
638
+ return {
639
+ profile,
640
+ url: pick("url", "URL"),
641
+ org: pick("org", "ORG", flags.org),
642
+ project: pick("project", "PROJECT", flags.project),
643
+ format: pick("format", "FORMAT"),
644
+ oauthClientId: readEnv("OAUTH_CLIENT_ID", env) ?? stored.oauthClientId,
645
+ iapAudience: readEnv("IAP_AUDIENCE", env) ?? stored.iapAudience,
646
+ sources,
647
+ stored
648
+ };
649
+ }
650
+
651
+ // src/credentials.ts
652
+ import { execFile, spawn } from "node:child_process";
653
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
654
+ import { join as join2 } from "node:path";
655
+ var SERVICE = "ycadence";
656
+ var secretAccount = {
657
+ apiKey: (profile) => `${profile}:api_key`,
658
+ refreshToken: (profile) => `${profile}:refresh_token`,
659
+ identityToken: (profile) => `${profile}:identity_token`,
660
+ oauthClientSecret: (profile) => `${profile}:oauth_client_secret`
661
+ };
662
+ var nodeExec = (command, args, stdin) => new Promise((resolve) => {
663
+ if (stdin === undefined) {
664
+ execFile(command, args, { encoding: "utf8" }, (error, stdout2, stderr2) => {
665
+ const code = error === null ? 0 : typeof error.code === "number" ? error.code : error ? 127 : 0;
666
+ resolve({ code, stdout: stdout2, stderr: stderr2 });
667
+ });
668
+ return;
669
+ }
670
+ const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
671
+ let stdout = "";
672
+ let stderr = "";
673
+ child.stdout.on("data", (chunk) => {
674
+ stdout += chunk.toString("utf8");
675
+ });
676
+ child.stderr.on("data", (chunk) => {
677
+ stderr += chunk.toString("utf8");
678
+ });
679
+ child.on("error", () => resolve({ code: 127, stdout, stderr }));
680
+ child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
681
+ child.stdin.end(stdin);
682
+ });
683
+ function assertAccount(account) {
684
+ if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(account)) {
685
+ throw new Error(`Invalid credential account name "${account}".`);
686
+ }
687
+ }
688
+ function hex(value) {
689
+ return Buffer.from(value, "utf8").toString("hex");
690
+ }
691
+ function macKeychainStore(exec = nodeExec) {
692
+ return {
693
+ kind: "keychain",
694
+ description: "macOS Keychain",
695
+ async get(account) {
696
+ assertAccount(account);
697
+ const result = await exec("security", [
698
+ "find-generic-password",
699
+ "-s",
700
+ SERVICE,
701
+ "-a",
702
+ account,
703
+ "-w"
704
+ ]);
705
+ if (result.code !== 0)
706
+ return;
707
+ const value = result.stdout.replace(/\n$/, "");
708
+ return value === "" ? undefined : value;
709
+ },
710
+ async set(account, value) {
711
+ assertAccount(account);
712
+ const result = await exec("security", ["-i"], `add-generic-password -U -s ${SERVICE} -a ${account} -l ${SERVICE} -X ${hex(value)}
713
+ `);
714
+ if (result.code !== 0) {
715
+ throw new Error(`Could not save to the macOS Keychain (security exited ${result.code}).`);
716
+ }
717
+ },
718
+ async delete(account) {
719
+ assertAccount(account);
720
+ await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", account]);
721
+ }
722
+ };
723
+ }
724
+ function libsecretStore(exec = nodeExec) {
725
+ return {
726
+ kind: "libsecret",
727
+ description: "Secret Service (libsecret)",
728
+ async get(account) {
729
+ assertAccount(account);
730
+ const result = await exec("secret-tool", ["lookup", "service", SERVICE, "account", account]);
731
+ if (result.code !== 0)
732
+ return;
733
+ const value = result.stdout.replace(/\n$/, "");
734
+ return value === "" ? undefined : value;
735
+ },
736
+ async set(account, value) {
737
+ assertAccount(account);
738
+ const result = await exec("secret-tool", ["store", `--label=${SERVICE} ${account}`, "service", SERVICE, "account", account], value);
739
+ if (result.code !== 0) {
740
+ throw new Error(`Could not save to the Secret Service (secret-tool exited ${result.code}).`);
741
+ }
742
+ },
743
+ async delete(account) {
744
+ assertAccount(account);
745
+ await exec("secret-tool", ["clear", "service", SERVICE, "account", account]);
746
+ }
747
+ };
748
+ }
749
+ function fileSecretStore(directory) {
750
+ const path = join2(directory, "credentials.json");
751
+ const load = async () => {
752
+ try {
753
+ return JSON.parse(await readFile2(path, "utf8"));
754
+ } catch {
755
+ return {};
756
+ }
757
+ };
758
+ const save = async (values) => {
759
+ await mkdir2(directory, { recursive: true, mode: 448 });
760
+ const temporary = `${path}.${process.pid}.tmp`;
761
+ await writeFile2(temporary, `${JSON.stringify(values, null, 2)}
762
+ `, { mode: 384 });
763
+ await chmod2(temporary, 384);
764
+ await rename2(temporary, path);
765
+ };
766
+ return {
767
+ kind: "file",
768
+ description: `${path} (mode 0600; no OS keychain found)`,
769
+ async get(account) {
770
+ return (await load())[account];
771
+ },
772
+ async set(account, value) {
773
+ const values = await load();
774
+ values[account] = value;
775
+ await save(values);
776
+ },
777
+ async delete(account) {
778
+ const values = await load();
779
+ if (!(account in values))
780
+ return;
781
+ delete values[account];
782
+ await save(values);
783
+ }
784
+ };
785
+ }
786
+ function memorySecretStore(initial = {}) {
787
+ const values = new Map(Object.entries(initial));
788
+ return {
789
+ kind: "memory",
790
+ description: "in memory (nothing is saved)",
791
+ async get(account) {
792
+ return values.get(account);
793
+ },
794
+ async set(account, value) {
795
+ values.set(account, value);
796
+ },
797
+ async delete(account) {
798
+ values.delete(account);
799
+ }
800
+ };
801
+ }
802
+ async function detectSecretStore(options) {
803
+ const exec = options.exec ?? nodeExec;
804
+ const platform = options.platform ?? process.platform;
805
+ const fallback = () => options.directory === undefined ? memorySecretStore() : fileSecretStore(options.directory);
806
+ if (options.env.YCADENCE_CREDENTIAL_STORE === "file")
807
+ return fallback();
808
+ if (platform === "darwin") {
809
+ const probe = await exec("security", ["list-keychains"]);
810
+ if (probe.code === 0)
811
+ return macKeychainStore(exec);
812
+ }
813
+ if (platform === "linux" && options.env.DBUS_SESSION_BUS_ADDRESS) {
814
+ const probe = await exec("secret-tool", []);
815
+ if (probe.code !== 127)
816
+ return libsecretStore(exec);
817
+ }
818
+ return fallback();
819
+ }
820
+
821
+ // src/parser.ts
822
+ var GLOBAL_FLAGS = [
823
+ { name: "help", short: "h", type: "boolean", description: "Help for the command" },
824
+ {
825
+ name: "format",
826
+ type: "string",
827
+ values: ["table", "json", "ndjson"],
828
+ placeholder: "<format>",
829
+ description: "Output format: table (default), json or ndjson"
830
+ },
831
+ { name: "json", type: "boolean", description: "Shorthand for --format json" },
832
+ {
833
+ name: "fields",
834
+ type: "string",
835
+ placeholder: "<a,b,c>",
836
+ description: "Columns for table output; the projection for json"
837
+ },
838
+ {
839
+ name: "quiet",
840
+ short: "q",
841
+ type: "boolean",
842
+ description: "Print only ids or keys, one per line"
843
+ },
844
+ {
845
+ name: "verbose",
846
+ short: "v",
847
+ type: "boolean",
848
+ description: "Request ids and timings on stderr; never credentials"
849
+ },
850
+ { name: "profile", type: "string", placeholder: "<name>", description: "Use a stored profile" },
851
+ { name: "org", type: "string", placeholder: "<slug>", description: "Organization slug" },
852
+ {
853
+ name: "project",
854
+ short: "p",
855
+ type: "string",
856
+ placeholder: "<id|identifier>",
857
+ description: "Project for commands that need one"
858
+ },
859
+ { name: "limit", type: "number", placeholder: "<n>", description: "Page size for lists" },
860
+ { name: "cursor", type: "string", placeholder: "<cursor>", description: "Continue a list" },
861
+ { name: "all", type: "boolean", description: "Follow the cursor to the end of a list" },
862
+ {
863
+ name: "dry-run",
864
+ type: "boolean",
865
+ description: "Show the write a command would send, and send nothing"
866
+ },
867
+ {
868
+ name: "yes",
869
+ short: "y",
870
+ type: "boolean",
871
+ description: "Skip confirmation (required outside a terminal for destructive commands)"
872
+ },
873
+ {
874
+ name: "idempotency-key",
875
+ type: "string",
876
+ placeholder: "<key>",
877
+ description: "Sent as Idempotency-Key on a write, so retrying it is safe"
878
+ },
879
+ {
880
+ name: "retry",
881
+ type: "number",
882
+ placeholder: "<n>",
883
+ description: "Retry temporary failures n times (a write gets an idempotency key)"
884
+ }
885
+ ];
886
+ var GLOBAL_BY_NAME = new Map(GLOBAL_FLAGS.map((flag) => [flag.name, flag]));
887
+ var GLOBAL_BY_SHORT = new Map(GLOBAL_FLAGS.filter((flag) => flag.short !== undefined).map((flag) => [flag.short, flag]));
888
+ function findChild(node, word) {
889
+ return node.children?.find((child) => child.name === word || (child.aliases?.includes(word) ?? false));
890
+ }
891
+ function globalTakesValue(token) {
892
+ if (token.includes("="))
893
+ return false;
894
+ const flag = token.startsWith("--") ? GLOBAL_BY_NAME.get(token.slice(2)) : GLOBAL_BY_SHORT.get(token.slice(1));
895
+ return flag !== undefined && flag.type !== "boolean";
896
+ }
897
+ function isGlobalToken(token) {
898
+ if (token.startsWith("--")) {
899
+ const name = token.slice(2).split("=")[0];
900
+ return GLOBAL_BY_NAME.has(name) || name.startsWith("no-") && GLOBAL_BY_NAME.has(name.slice(3));
901
+ }
902
+ return token.length === 2 && GLOBAL_BY_SHORT.has(token.slice(1));
903
+ }
904
+ function resolveCommand(root, argv) {
905
+ let node = root;
906
+ const path = [];
907
+ const consumed = new Set;
908
+ for (let index = 0;index < argv.length; index += 1) {
909
+ const token = argv[index];
910
+ if (token === "--")
911
+ break;
912
+ if (token.startsWith("-") && token !== "-") {
913
+ if (!isGlobalToken(token))
914
+ break;
915
+ if (globalTakesValue(token))
916
+ index += 1;
917
+ continue;
918
+ }
919
+ const child = findChild(node, token);
920
+ if (child === undefined)
921
+ break;
922
+ node = child;
923
+ path.push(child.name);
924
+ consumed.add(index);
925
+ }
926
+ return { path, command: node, consumed };
927
+ }
928
+ function closest(word, candidates) {
929
+ let best;
930
+ for (const candidate of candidates) {
931
+ const distance = levenshtein(word, candidate);
932
+ if (distance <= 2 && (best === undefined || distance < best.distance)) {
933
+ best = { name: candidate, distance };
934
+ }
935
+ }
936
+ return best?.name;
937
+ }
938
+ function levenshtein(a, b) {
939
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
940
+ for (let i = 1;i <= a.length; i += 1) {
941
+ let previous = row[0];
942
+ row[0] = i;
943
+ for (let j = 1;j <= b.length; j += 1) {
944
+ const current = row[j];
945
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
946
+ previous = current;
947
+ }
948
+ }
949
+ return row[b.length];
950
+ }
951
+ function convert(flag, raw, display) {
952
+ if (flag.type === "number") {
953
+ const value = Number(raw);
954
+ if (raw.trim() === "" || !Number.isFinite(value)) {
955
+ throw new UsageError(`${display} takes a number, got "${raw}".`);
956
+ }
957
+ return value;
958
+ }
959
+ if (flag.type === "boolean") {
960
+ if (raw === "true" || raw === "1" || raw === "")
961
+ return true;
962
+ if (raw === "false" || raw === "0")
963
+ return false;
964
+ throw new UsageError(`${display} is true or false, got "${raw}".`);
965
+ }
966
+ if (flag.values !== undefined && !flag.values.includes(raw)) {
967
+ throw new UsageError(`${display} is one of ${flag.values.join(", ")}; got "${raw}".`);
968
+ }
969
+ return raw;
970
+ }
971
+ function commandName(path) {
972
+ return ["ycadence", ...path].join(" ");
973
+ }
974
+ function parse(root, argv) {
975
+ const { path, command, consumed } = resolveCommand(root, argv);
976
+ const flags = [...GLOBAL_FLAGS, ...command.flags ?? []];
977
+ const byName = new Map(flags.map((flag) => [flag.name, flag]));
978
+ const byShort = new Map(flags.filter((flag) => flag.short !== undefined).map((flag) => [flag.short, flag]));
979
+ const values = {};
980
+ const positionals = [];
981
+ const rest = [];
982
+ const assign = (flag, value) => {
983
+ if (flag.multiple === true) {
984
+ const list = values[flag.name] ?? [];
985
+ values[flag.name] = [...list, value];
986
+ } else {
987
+ values[flag.name] = value;
988
+ }
989
+ };
990
+ for (let index = 0;index < argv.length; index += 1) {
991
+ if (consumed.has(index))
992
+ continue;
993
+ const token = argv[index];
994
+ if (token === "--") {
995
+ rest.push(...argv.slice(index + 1));
996
+ break;
997
+ }
998
+ if (token.startsWith("--")) {
999
+ const equals = token.indexOf("=");
1000
+ const name = equals === -1 ? token.slice(2) : token.slice(2, equals);
1001
+ let flag = byName.get(name);
1002
+ let negated = false;
1003
+ if (flag === undefined && name.startsWith("no-")) {
1004
+ const positive = byName.get(name.slice(3));
1005
+ if (positive?.type === "boolean") {
1006
+ flag = positive;
1007
+ negated = true;
1008
+ }
1009
+ }
1010
+ if (flag === undefined) {
1011
+ const suggestion = closest(name, flags.filter((candidate) => candidate.hidden !== true).map((candidate) => candidate.name));
1012
+ throw new UsageError(`Unknown flag --${name} for "${commandName(path)}".${suggestion === undefined ? "" : ` Did you mean --${suggestion}?`}`);
1013
+ }
1014
+ const display = `--${flag.name}`;
1015
+ if (negated) {
1016
+ values[flag.name] = false;
1017
+ continue;
1018
+ }
1019
+ if (equals !== -1) {
1020
+ assign(flag, convert(flag, token.slice(equals + 1), display));
1021
+ continue;
1022
+ }
1023
+ if (flag.type === "boolean") {
1024
+ assign(flag, true);
1025
+ continue;
1026
+ }
1027
+ const next = argv[index + 1];
1028
+ if (next === undefined || next.startsWith("--") && next.length > 2) {
1029
+ throw new UsageError(`${display} needs a value.`);
1030
+ }
1031
+ assign(flag, convert(flag, next, display));
1032
+ index += 1;
1033
+ continue;
1034
+ }
1035
+ if (token.startsWith("-") && token.length > 1 && !/^-\d/.test(token)) {
1036
+ const short = token.slice(1);
1037
+ const flag = short.length === 1 ? byShort.get(short) : undefined;
1038
+ if (flag === undefined) {
1039
+ throw new UsageError(`Unknown flag ${token} for "${commandName(path)}".`);
1040
+ }
1041
+ if (flag.type === "boolean") {
1042
+ assign(flag, true);
1043
+ continue;
1044
+ }
1045
+ const next = argv[index + 1];
1046
+ if (next === undefined)
1047
+ throw new UsageError(`${token} needs a value.`);
1048
+ assign(flag, convert(flag, next, `-${short}`));
1049
+ index += 1;
1050
+ continue;
1051
+ }
1052
+ positionals.push(token);
1053
+ }
1054
+ const help = values.help === true;
1055
+ const args = {};
1056
+ const specs = command.args ?? [];
1057
+ if (!help && command.run !== undefined) {
1058
+ let position = 0;
1059
+ for (const arg of specs) {
1060
+ if (arg.variadic === true) {
1061
+ args[arg.name] = positionals.slice(position);
1062
+ position = positionals.length;
1063
+ if (arg.required === true && args[arg.name].length === 0) {
1064
+ throw new UsageError(`Missing <${arg.name}>. Usage: ${usageLine(path, command)}`);
1065
+ }
1066
+ continue;
1067
+ }
1068
+ args[arg.name] = positionals[position];
1069
+ if (positionals[position] !== undefined)
1070
+ position += 1;
1071
+ else if (arg.required === true) {
1072
+ throw new UsageError(`Missing <${arg.name}>. Usage: ${usageLine(path, command)}`);
1073
+ }
1074
+ }
1075
+ if (position < positionals.length) {
1076
+ const extra = positionals[position];
1077
+ const hint = command.children === undefined ? "" : (() => {
1078
+ const suggestion = closest(extra, command.children.filter((child) => child.hidden !== true).map((child) => child.name));
1079
+ return suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`;
1080
+ })();
1081
+ throw new UsageError(`Unexpected argument "${extra}" for "${commandName(path)}".${hint} Usage: ${usageLine(path, command)}`);
1082
+ }
1083
+ for (const flag of command.flags ?? []) {
1084
+ if (flag.required === true && values[flag.name] === undefined) {
1085
+ throw new UsageError(`--${flag.name} is required. Usage: ${usageLine(path, command)}`);
1086
+ }
1087
+ }
1088
+ } else if (!help && positionals.length > 0) {
1089
+ const word = positionals[0];
1090
+ const candidates = (command.children ?? []).filter((child) => child.hidden !== true).map((child) => child.name);
1091
+ const suggestion = closest(word, candidates);
1092
+ throw new UsageError(path.length === 0 ? `Unknown command: ${word}.${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}` : `Unknown command "${word}" for "${commandName(path)}".${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`);
1093
+ }
1094
+ return { path, command, args, flags: values, rest, help };
1095
+ }
1096
+ function usageLine(path, command) {
1097
+ const parts = [commandName(path)];
1098
+ if (command.children !== undefined && command.run === undefined)
1099
+ parts.push("<command>");
1100
+ for (const arg of command.args ?? []) {
1101
+ const name = arg.variadic === true ? `${arg.name}...` : arg.name;
1102
+ parts.push(arg.required === true ? `<${name}>` : `[${name}]`);
1103
+ }
1104
+ for (const flag of command.flags ?? []) {
1105
+ if (flag.required === true)
1106
+ parts.push(`--${flag.name} ${flag.placeholder ?? "<value>"}`);
1107
+ }
1108
+ if ((command.flags ?? []).some((flag) => flag.required !== true && flag.hidden !== true)) {
1109
+ parts.push("[flags]");
1110
+ }
1111
+ return parts.join(" ");
1112
+ }
1113
+ function globalOptions(flags, defaults = {}) {
1114
+ const requested = flags.json === true ? "json" : flags.format ?? defaults.format;
1115
+ const format = requested === "json" || requested === "ndjson" ? requested : "table";
1116
+ const fields = typeof flags.fields === "string" ? flags.fields.split(",").map((field) => field.trim()).filter((field) => field !== "") : undefined;
1117
+ const limit = flags.limit;
1118
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 200)) {
1119
+ throw new UsageError("--limit is a whole number from 1 to 200.");
1120
+ }
1121
+ const retry = flags.retry;
1122
+ if (retry !== undefined && (!Number.isInteger(retry) || retry < 0 || retry > 10)) {
1123
+ throw new UsageError("--retry is a whole number from 0 to 10.");
1124
+ }
1125
+ const key = flags["idempotency-key"];
1126
+ if (key !== undefined && !/^[\x21-\x7e]{1,255}$/.test(key)) {
1127
+ throw new UsageError("--idempotency-key is 1 to 255 printable ASCII characters.");
1128
+ }
1129
+ return {
1130
+ format,
1131
+ fields,
1132
+ quiet: flags.quiet === true,
1133
+ verbose: flags.verbose === true,
1134
+ dryRun: flags["dry-run"] === true,
1135
+ yes: flags.yes === true,
1136
+ limit,
1137
+ cursor: flags.cursor,
1138
+ all: flags.all === true,
1139
+ idempotencyKey: key,
1140
+ retry,
1141
+ profile: flags.profile,
1142
+ org: flags.org,
1143
+ project: flags.project
1144
+ };
1145
+ }
1146
+
1147
+ // src/help.ts
1148
+ function visibleChildren(command) {
1149
+ return (command.children ?? []).filter((child) => child.hidden !== true);
1150
+ }
1151
+ function flagLabel(flag) {
1152
+ const names = [flag.short === undefined ? undefined : `-${flag.short}`, `--${flag.name}`].filter((name) => name !== undefined).join(", ");
1153
+ return flag.type === "boolean" ? names : `${names} ${flag.placeholder ?? "<value>"}`;
1154
+ }
1155
+ function flagLines(flags) {
1156
+ const visible = flags.filter((flag) => flag.hidden !== true);
1157
+ const labels = visible.map(flagLabel);
1158
+ const width = Math.max(0, ...labels.map((label) => label.length));
1159
+ return visible.map((flag, index) => {
1160
+ const notes = [
1161
+ flag.required === true ? "required" : undefined,
1162
+ flag.multiple === true ? "repeatable" : undefined
1163
+ ].filter((note) => note !== undefined);
1164
+ return ` ${labels[index].padEnd(width)} ${flag.description}${notes.length === 0 ? "" : ` (${notes.join(", ")})`}`;
1165
+ });
1166
+ }
1167
+ function commandLines(path, children) {
1168
+ const names = children.map((child) => [child.name, ...child.aliases ?? []].join(", "));
1169
+ const width = Math.max(0, ...names.map((name) => name.length));
1170
+ return children.map((child, index) => ` ${names[index].padEnd(width)} ${child.summary}`);
1171
+ }
1172
+ var CONFIGURATION_HELP = `Configuration:
1173
+ Profiles live in ~/.config/ycadence/config.json ($XDG_CONFIG_HOME aware);
1174
+ keys in the OS keychain. Environment variables override both:
1175
+ YCADENCE_URL the instance, e.g. https://ycadence.example.com
176
1176
  YCADENCE_API_KEY a project, organization or personal key
177
1177
  YCADENCE_IDENTITY_TOKEN
178
1178
  optional: a Google identity token, for an instance behind
179
- IAP. Its audience must be the IAP OAuth client id, and it
180
- lasts about an hour
1179
+ IAP (or use \`ycadence login\`)
1180
+ YCADENCE_PROFILE, YCADENCE_PROJECT, YCADENCE_ORG, YCADENCE_FORMAT
181
1181
 
182
1182
  The DEVPULSE_* names are still read when the YCADENCE_* one is unset.
183
- \`devpulse\` is kept as an alias of this command.`;
184
- async function run(argv, context) {
185
- const [command, ...rest] = argv;
1183
+ \`devpulse\` is kept as an alias of this command. For a short name, add
1184
+ \`alias yc=ycadence\` to your shell.`;
1185
+ function renderHelp(root, path, command) {
1186
+ const isRoot = path.length === 0;
1187
+ const lines = [];
1188
+ lines.push(isRoot ? "ycadence — project and work management from the command line" : `${commandName(path)} — ${command.summary}`);
1189
+ if (command.deprecatedFor !== undefined) {
1190
+ lines.push("", `Deprecated: use \`${command.deprecatedFor}\`.`);
1191
+ }
1192
+ if (command.description !== undefined)
1193
+ lines.push("", command.description);
1194
+ lines.push("", "Usage:", ` ${isRoot ? "ycadence <command> [flags]" : usageLine(path, command)}`);
1195
+ const children = visibleChildren(command);
1196
+ if (children.length > 0) {
1197
+ lines.push("", "Commands:", ...commandLines(path, children));
1198
+ }
1199
+ if ((command.args ?? []).length > 0) {
1200
+ const width = Math.max(...(command.args ?? []).map((arg) => arg.name.length + 2));
1201
+ lines.push("", "Arguments:", ...(command.args ?? []).map((arg) => ` ${`<${arg.name}>`.padEnd(width)} ${arg.description}`));
1202
+ }
1203
+ if ((command.flags ?? []).some((flag) => flag.hidden !== true)) {
1204
+ lines.push("", "Flags:", ...flagLines(command.flags ?? []));
1205
+ }
1206
+ if ((command.examples ?? []).length > 0) {
1207
+ lines.push("", "Examples:", ...(command.examples ?? []).map((example) => ` ${example}`));
1208
+ }
1209
+ if (isRoot) {
1210
+ lines.push("", "Global flags:", ...flagLines(GLOBAL_FLAGS));
1211
+ lines.push("", CONFIGURATION_HELP);
1212
+ lines.push("", "Exit codes:", ...EXIT_CODE_TABLE.map(([code, meaning]) => ` ${String(code).padStart(2)} ${meaning}`));
1213
+ lines.push("", "Run `ycadence <command> --help` for a command, or `ycadence help <command>`.");
1214
+ } else {
1215
+ lines.push("", "Global flags: see `ycadence --help`.");
1216
+ }
1217
+ return lines.join(`
1218
+ `);
1219
+ }
1220
+ function walk(command, path = [], includeHidden = false) {
1221
+ const entries = [];
1222
+ for (const child of command.children ?? []) {
1223
+ if (child.hidden === true && !includeHidden)
1224
+ continue;
1225
+ const childPath = [...path, child.name];
1226
+ entries.push({ path: childPath, command: child });
1227
+ entries.push(...walk(child, childPath, includeHidden));
1228
+ }
1229
+ return entries;
1230
+ }
1231
+
1232
+ // src/oauth.ts
1233
+ import { createHash, randomBytes } from "node:crypto";
1234
+ import { createServer } from "node:http";
1235
+ var GOOGLE_ENDPOINTS = {
1236
+ authorization: "https://accounts.google.com/o/oauth2/v2/auth",
1237
+ token: "https://oauth2.googleapis.com/token",
1238
+ device: "https://oauth2.googleapis.com/device/code"
1239
+ };
1240
+ function endpointsFrom(env) {
1241
+ return {
1242
+ authorization: env.YCADENCE_OAUTH_AUTH_URL || GOOGLE_ENDPOINTS.authorization,
1243
+ token: env.YCADENCE_OAUTH_TOKEN_URL || GOOGLE_ENDPOINTS.token,
1244
+ device: env.YCADENCE_OAUTH_DEVICE_URL || GOOGLE_ENDPOINTS.device
1245
+ };
1246
+ }
1247
+ var SCOPE = "openid email";
1248
+ function base64url(buffer) {
1249
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1250
+ }
1251
+ function createPkce(random = randomBytes) {
1252
+ const verifier = base64url(random(32));
1253
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
1254
+ return { verifier, challenge };
1255
+ }
1256
+ function authorizationUrl(options) {
1257
+ const url = new URL(options.client.endpoints.authorization);
1258
+ url.searchParams.set("client_id", options.client.clientId);
1259
+ url.searchParams.set("redirect_uri", options.redirectUri);
1260
+ url.searchParams.set("response_type", "code");
1261
+ url.searchParams.set("scope", SCOPE);
1262
+ url.searchParams.set("code_challenge", options.challenge);
1263
+ url.searchParams.set("code_challenge_method", "S256");
1264
+ url.searchParams.set("state", options.state);
1265
+ url.searchParams.set("access_type", "offline");
1266
+ url.searchParams.set("prompt", "consent");
1267
+ return url.toString();
1268
+ }
1269
+ function tokenExpiry(token) {
1270
+ const payload = token.split(".")[1];
1271
+ if (payload === undefined)
1272
+ return;
186
1273
  try {
187
- switch (command) {
188
- case undefined:
189
- case "help":
190
- case "--help":
191
- case "-h":
192
- return ok(USAGE);
193
- case "doctor":
194
- return await doctor(context);
195
- case "states": {
196
- const projectId = rest[0];
197
- if (projectId === undefined)
198
- return fail("Usage: ycadence states <projectId>");
199
- const { states } = await context.client.listStates(projectId);
200
- return ok(states.map((state) => `${state.id} ${state.name} (${state.group})`).join(`
201
- `));
202
- }
203
- case "cycles": {
204
- const projectId = rest[0];
205
- if (projectId === undefined)
206
- return fail("Usage: ycadence cycles <projectId>");
207
- const { cycles } = await context.client.listCycles(projectId);
208
- return ok(cycles.length === 0 ? "No cycles." : cycles.map((cycle) => `${cycle.id} ${cycle.name} [${cycle.status}]`).join(`
209
- `));
1274
+ const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
1275
+ return typeof claims.exp === "number" ? claims.exp * 1000 : undefined;
1276
+ } catch {
1277
+ return;
1278
+ }
1279
+ }
1280
+ async function postForm(fetchImpl, url, form) {
1281
+ const body = new URLSearchParams;
1282
+ for (const [name, value] of Object.entries(form)) {
1283
+ if (value !== undefined && value !== "")
1284
+ body.set(name, value);
1285
+ }
1286
+ let response;
1287
+ try {
1288
+ response = await fetchImpl(url, {
1289
+ method: "POST",
1290
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
1291
+ body: body.toString()
1292
+ });
1293
+ } catch (error) {
1294
+ throw new CliError(`Could not reach ${new URL(url).host}: ${error instanceof Error ? error.message : "error"}`, "network_error", EXIT.tempfail);
1295
+ }
1296
+ const text = await response.text();
1297
+ let parsed = {};
1298
+ try {
1299
+ parsed = JSON.parse(text);
1300
+ } catch {}
1301
+ return { status: response.status, body: parsed };
1302
+ }
1303
+ function tokenError(body, status) {
1304
+ const reason = body.error_description ?? body.error ?? `HTTP ${status}`;
1305
+ return new CliError(`Sign-in failed: ${reason}`, body.error ?? "oauth_error", body.error === "invalid_grant" ? EXIT.config : EXIT.failure);
1306
+ }
1307
+ function toTokenSet(body, now) {
1308
+ const idToken = body.id_token;
1309
+ return {
1310
+ refreshToken: body.refresh_token,
1311
+ idToken,
1312
+ expiresAt: (idToken === undefined ? undefined : tokenExpiry(idToken)) ?? (body.expires_in === undefined ? undefined : now + body.expires_in * 1000)
1313
+ };
1314
+ }
1315
+ async function exchangeCode(options) {
1316
+ const { status, body } = await postForm(options.fetch, options.client.endpoints.token, {
1317
+ grant_type: "authorization_code",
1318
+ code: options.code,
1319
+ code_verifier: options.verifier,
1320
+ redirect_uri: options.redirectUri,
1321
+ client_id: options.client.clientId,
1322
+ client_secret: options.client.clientSecret
1323
+ });
1324
+ if (status !== 200 || body.error !== undefined)
1325
+ throw tokenError(body, status);
1326
+ return toTokenSet(body, (options.now ?? Date.now)());
1327
+ }
1328
+ async function refreshIdToken(options) {
1329
+ const { status, body } = await postForm(options.fetch, options.client.endpoints.token, {
1330
+ grant_type: "refresh_token",
1331
+ refresh_token: options.refreshToken,
1332
+ client_id: options.client.clientId,
1333
+ client_secret: options.client.clientSecret,
1334
+ audience: options.audience
1335
+ });
1336
+ if (status !== 200 || body.error !== undefined)
1337
+ throw tokenError(body, status);
1338
+ const tokens = toTokenSet(body, (options.now ?? Date.now)());
1339
+ if (tokens.idToken === undefined) {
1340
+ throw new CliError("The token endpoint returned no id_token.", "oauth_error");
1341
+ }
1342
+ return { idToken: tokens.idToken, expiresAt: tokens.expiresAt };
1343
+ }
1344
+ var nodeListen = (handler) => new Promise((resolve, reject) => {
1345
+ const server = createServer((request, response) => {
1346
+ const result = handler({ url: request.url ?? "/" });
1347
+ response.writeHead(result.status, { "Content-Type": "text/html; charset=utf-8" });
1348
+ response.end(result.body);
1349
+ });
1350
+ server.on("error", reject);
1351
+ server.listen(0, "127.0.0.1", () => {
1352
+ const { port } = server.address();
1353
+ resolve({
1354
+ port,
1355
+ close: () => new Promise((done) => server.close(() => done()))
1356
+ });
1357
+ });
1358
+ });
1359
+ var PAGE = (message) => `<!doctype html><meta charset="utf-8"><title>ycadence</title><p style="font:16px system-ui;margin:3rem">${message}</p>`;
1360
+ async function loopbackLogin(options) {
1361
+ const random = options.random ?? randomBytes;
1362
+ const { verifier, challenge } = createPkce(random);
1363
+ const state = base64url(random(16));
1364
+ const listen = options.listen ?? nodeListen;
1365
+ let settle = () => {};
1366
+ const redirected = new Promise((resolve) => {
1367
+ settle = resolve;
1368
+ });
1369
+ const server = await listen((request) => {
1370
+ const url2 = new URL(request.url, "http://127.0.0.1");
1371
+ if (url2.pathname !== "/")
1372
+ return { status: 404, body: PAGE("Not found.") };
1373
+ if (url2.searchParams.get("state") !== state) {
1374
+ return { status: 400, body: PAGE("This sign-in link is stale. Run the command again.") };
1375
+ }
1376
+ const error = url2.searchParams.get("error");
1377
+ const code = url2.searchParams.get("code");
1378
+ settle(error !== null ? { error } : { code: code ?? undefined });
1379
+ return {
1380
+ status: 200,
1381
+ body: PAGE(error === null ? "Signed in. You can close this tab and return to the terminal." : "Sign-in was cancelled.")
1382
+ };
1383
+ });
1384
+ const redirectUri = `http://127.0.0.1:${server.port}`;
1385
+ const url = authorizationUrl({ client: options.client, redirectUri, challenge, state });
1386
+ options.print(`Opening your browser to sign in. If it does not open, visit:
1387
+
1388
+ ${url}
1389
+ `);
1390
+ await options.openUrl(url);
1391
+ let timer;
1392
+ const timeout = new Promise((resolve) => {
1393
+ timer = setTimeout(() => resolve({ error: "timed out" }), options.timeoutMs ?? 300000);
1394
+ });
1395
+ try {
1396
+ const result = await Promise.race([redirected, timeout]);
1397
+ if (result.error !== undefined || result.code === undefined) {
1398
+ throw new CliError(`Sign-in did not complete: ${result.error ?? "no code"}.`, "oauth_error");
1399
+ }
1400
+ return await exchangeCode({
1401
+ client: options.client,
1402
+ code: result.code,
1403
+ verifier,
1404
+ redirectUri,
1405
+ fetch: options.fetch,
1406
+ now: options.now
1407
+ });
1408
+ } finally {
1409
+ clearTimeout(timer);
1410
+ await server.close();
1411
+ }
1412
+ }
1413
+ async function deviceLogin(options) {
1414
+ const now = options.now ?? Date.now;
1415
+ const start = await postForm(options.fetch, options.client.endpoints.device, {
1416
+ client_id: options.client.clientId,
1417
+ scope: SCOPE
1418
+ });
1419
+ if (start.status !== 200 || start.body.error !== undefined) {
1420
+ throw tokenError(start.body, start.status);
1421
+ }
1422
+ const deviceCode = start.body.device_code;
1423
+ const userCode = start.body.user_code;
1424
+ const verificationUrl = start.body.verification_url ?? start.body.verification_uri;
1425
+ if (deviceCode === undefined || userCode === undefined || verificationUrl === undefined) {
1426
+ throw new CliError("The device endpoint returned an incomplete answer.", "oauth_error");
1427
+ }
1428
+ let interval = (start.body.interval ?? 5) * 1000;
1429
+ const deadline = now() + (start.body.expires_in ?? 1800) * 1000;
1430
+ options.print(`To sign in, visit ${verificationUrl} and enter the code ${userCode}`);
1431
+ while (now() < deadline) {
1432
+ await options.sleep(interval);
1433
+ const { status, body } = await postForm(options.fetch, options.client.endpoints.token, {
1434
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1435
+ device_code: deviceCode,
1436
+ client_id: options.client.clientId,
1437
+ client_secret: options.client.clientSecret
1438
+ });
1439
+ if (body.error === "authorization_pending")
1440
+ continue;
1441
+ if (body.error === "slow_down") {
1442
+ interval += 5000;
1443
+ continue;
1444
+ }
1445
+ if (status !== 200 || body.error !== undefined)
1446
+ throw tokenError(body, status);
1447
+ return toTokenSet(body, now());
1448
+ }
1449
+ throw new CliError("The sign-in code expired before it was approved.", "oauth_error");
1450
+ }
1451
+ function oauthClientFrom(options) {
1452
+ if (options.clientId === undefined || options.clientId === "") {
1453
+ throw new ConfigError("No OAuth client is configured. Set it with `ycadence config set oauthClientId <id>` (or YCADENCE_OAUTH_CLIENT_ID).");
1454
+ }
1455
+ return {
1456
+ clientId: options.clientId,
1457
+ clientSecret: options.clientSecret,
1458
+ endpoints: endpointsFrom(options.env)
1459
+ };
1460
+ }
1461
+
1462
+ // src/identity.ts
1463
+ function storedIdentityToken(options) {
1464
+ const { config, secrets } = options;
1465
+ let memory;
1466
+ const fresh = (entry) => entry !== undefined && (entry.expiresAt === undefined || entry.expiresAt - 60000 > options.now());
1467
+ return async () => {
1468
+ if (memory !== undefined && fresh(memory))
1469
+ return memory.token;
1470
+ const cached = await secrets.get(secretAccount.identityToken(config.profile));
1471
+ if (cached !== undefined) {
1472
+ try {
1473
+ const parsed = JSON.parse(cached);
1474
+ const entry = { token: parsed.token, expiresAt: parsed.expiresAt };
1475
+ if (fresh(entry)) {
1476
+ memory = entry;
1477
+ return entry.token;
1478
+ }
1479
+ } catch {}
1480
+ }
1481
+ const refreshToken = await secrets.get(secretAccount.refreshToken(config.profile));
1482
+ if (refreshToken === undefined || config.oauthClientId === undefined)
1483
+ return;
1484
+ const clientSecret = options.env.YCADENCE_OAUTH_CLIENT_SECRET || await secrets.get(secretAccount.oauthClientSecret(config.profile));
1485
+ const minted = await refreshIdToken({
1486
+ client: oauthClientFrom({ clientId: config.oauthClientId, clientSecret, env: options.env }),
1487
+ refreshToken,
1488
+ audience: config.iapAudience,
1489
+ fetch: options.fetch,
1490
+ now: options.now
1491
+ });
1492
+ memory = { token: minted.idToken, expiresAt: minted.expiresAt };
1493
+ await secrets.set(secretAccount.identityToken(config.profile), JSON.stringify({ token: minted.idToken, expiresAt: minted.expiresAt }));
1494
+ return minted.idToken;
1495
+ };
1496
+ }
1497
+
1498
+ // src/output.ts
1499
+ function pick(record, fields) {
1500
+ if (typeof record !== "object" || record === null)
1501
+ return record;
1502
+ const picked = {};
1503
+ for (const field of fields)
1504
+ picked[field] = record[field];
1505
+ return picked;
1506
+ }
1507
+ function project(data, fields) {
1508
+ if (fields === undefined || fields.length === 0)
1509
+ return data;
1510
+ return Array.isArray(data) ? data.map((item) => pick(item, fields)) : pick(data, fields);
1511
+ }
1512
+ function cell(value) {
1513
+ if (value === null || value === undefined)
1514
+ return "—";
1515
+ if (Array.isArray(value))
1516
+ return value.length === 0 ? "—" : value.map(cell).join(",");
1517
+ if (typeof value === "object")
1518
+ return JSON.stringify(value);
1519
+ return String(value);
1520
+ }
1521
+ function renderTable(rows, columns) {
1522
+ const cells = rows.map((row) => columns.map((column) => cell(row[column])));
1523
+ const widths = columns.map((column, index) => Math.max(column.length, ...cells.map((line2) => line2[index].length)));
1524
+ const line = (values) => values.map((value, index) => index === values.length - 1 ? value : value.padEnd(widths[index])).join(" ").trimEnd();
1525
+ return [line(columns.map((column) => column.toUpperCase())), ...cells.map(line)].join(`
1526
+ `);
1527
+ }
1528
+ function renderRecord(record, fields) {
1529
+ const keys = fields ?? Object.keys(record);
1530
+ const width = Math.max(0, ...keys.map((key) => key.length));
1531
+ return keys.map((key) => `${key.padEnd(width)} ${cell(record[key])}`).join(`
1532
+ `);
1533
+ }
1534
+ function idOf(row) {
1535
+ if (typeof row !== "object" || row === null)
1536
+ return typeof row === "string" ? row : undefined;
1537
+ const record = row;
1538
+ const value = record.key ?? record.id ?? record.runId ?? record.intakeItemId ?? record.name;
1539
+ return value === undefined ? undefined : String(value);
1540
+ }
1541
+ function formatSuccess(output, globals, requestId) {
1542
+ const data = output.data;
1543
+ const nextCursor = output.nextCursor ?? null;
1544
+ if (globals.quiet) {
1545
+ const ids = output.ids ?? (Array.isArray(data) ? data.map(idOf) : [idOf(data)]);
1546
+ return ids.filter((id) => id !== undefined).join(`
1547
+ `);
1548
+ }
1549
+ if (globals.format === "json") {
1550
+ const envelope = {
1551
+ ok: output.exitCode === undefined || output.exitCode === 0,
1552
+ data: project(data, globals.fields),
1553
+ nextCursor,
1554
+ requestId: requestId ?? null
1555
+ };
1556
+ return JSON.stringify(envelope, null, 2);
1557
+ }
1558
+ if (globals.format === "ndjson") {
1559
+ const projected = project(data, globals.fields);
1560
+ const lines = Array.isArray(projected) ? projected.map((item) => JSON.stringify(item)) : [JSON.stringify(projected)];
1561
+ return lines.join(`
1562
+ `);
1563
+ }
1564
+ if (output.text !== undefined && globals.fields === undefined)
1565
+ return output.text;
1566
+ if (Array.isArray(data)) {
1567
+ if (data.length === 0)
1568
+ return output.empty ?? "Nothing found.";
1569
+ const columns = globals.fields ?? output.columns ?? Object.keys(data[0] ?? {});
1570
+ const table = renderTable(data, columns);
1571
+ return nextCursor === null ? table : `${table}
1572
+ … more (use --cursor ${nextCursor})`;
1573
+ }
1574
+ if (typeof data === "object" && data !== null) {
1575
+ return renderRecord(data, globals.fields);
1576
+ }
1577
+ return data === undefined ? "" : String(data);
1578
+ }
1579
+ function formatDryRun(request, globals) {
1580
+ const path = (() => {
1581
+ try {
1582
+ const url = new URL(request.url);
1583
+ return `${url.pathname}${url.search}`;
1584
+ } catch {
1585
+ return request.url;
1586
+ }
1587
+ })();
1588
+ const data = { method: request.method, path, headers: request.headers, body: request.body };
1589
+ if (globals.format === "json" || globals.format === "ndjson") {
1590
+ const envelope = {
1591
+ ok: true,
1592
+ dryRun: true,
1593
+ data,
1594
+ nextCursor: null,
1595
+ requestId: null
1596
+ };
1597
+ return JSON.stringify(envelope, null, globals.format === "json" ? 2 : undefined);
1598
+ }
1599
+ const lines = [`Dry run: would send ${request.method} ${path}`];
1600
+ for (const [name, value] of Object.entries(request.headers))
1601
+ lines.push(` ${name}: ${value}`);
1602
+ if (request.body !== undefined)
1603
+ lines.push("", JSON.stringify(request.body, null, 2));
1604
+ lines.push("", "Nothing was sent.");
1605
+ return lines.join(`
1606
+ `);
1607
+ }
1608
+ function errorEnvelope(error, code, requestId) {
1609
+ if (error instanceof YCadenceError) {
1610
+ return {
1611
+ ok: false,
1612
+ error: {
1613
+ code: error.code,
1614
+ message: error.message,
1615
+ status: error.status,
1616
+ ...error.issues === undefined ? {} : { issues: error.issues },
1617
+ ...error.details === undefined ? {} : { details: error.details },
1618
+ requestId: error.requestId ?? requestId ?? null
210
1619
  }
211
- case "import":
212
- return await importCommand(rest, context);
213
- case "work-item":
214
- return await workItem(rest, context);
215
- default:
216
- return fail(`Unknown command: ${command}
1620
+ };
1621
+ }
1622
+ return {
1623
+ ok: false,
1624
+ error: {
1625
+ code,
1626
+ message: error instanceof Error ? error.message : "Unknown error.",
1627
+ status: null,
1628
+ requestId: requestId ?? null
1629
+ }
1630
+ };
1631
+ }
1632
+ function formatError(error, code, globals, requestId) {
1633
+ const envelope = errorEnvelope(error, code, requestId);
1634
+ if (globals.format === "json")
1635
+ return JSON.stringify(envelope, null, 2);
1636
+ if (globals.format === "ndjson")
1637
+ return JSON.stringify(envelope);
1638
+ const lines = [envelope.error.message];
1639
+ for (const issue of envelope.error.issues ?? [])
1640
+ lines.push(` ${issue.path}: ${issue.message}`);
1641
+ if (envelope.error.requestId !== null)
1642
+ lines.push(`(request id ${envelope.error.requestId})`);
1643
+ return lines.join(`
1644
+ `);
1645
+ }
217
1646
 
218
- ${USAGE}`);
1647
+ // src/resolve.ts
1648
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1649
+ var WORK_ITEM_KEY = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
1650
+ var isUuid = (value) => UUID.test(value);
1651
+ async function resolveProjectId(client, value) {
1652
+ if (isUuid(value))
1653
+ return value;
1654
+ try {
1655
+ const page = await client.listProjects({ q: value, limit: 50 });
1656
+ const match = page.items.find((project2) => project2.identifier.toLowerCase() === value.toLowerCase());
1657
+ if (match === undefined) {
1658
+ throw new CliError(`No project with identifier "${value}".`, "not_found", EXIT.notFound);
219
1659
  }
1660
+ return match.id;
220
1661
  } catch (error) {
221
- if (error instanceof YCadenceError) {
222
- const detail = error.issues === undefined ? error.message : `${error.message}
223
- ${error.issues.map((issue) => ` ${issue.path}: ${issue.message}`).join(`
224
- `)}`;
225
- return fail(detail, error.retryable ? 75 : 1);
226
- }
227
- return fail(error instanceof Error ? error.message : "Unknown error.");
228
- }
229
- }
230
- async function workItem(argv, context) {
231
- const [action, ...rest] = argv;
232
- switch (action) {
233
- case "list": {
234
- const projectId = rest[0];
235
- if (projectId === undefined)
236
- return fail("Usage: ycadence work-item list <projectId>");
237
- const page = await context.client.listWorkItems(projectId, { limit: 50 });
238
- if (page.items.length === 0)
239
- return ok("No work items.");
240
- const lines = page.items.map((item) => `${item.key.padEnd(10)} ${item.priority.padEnd(7)} ${item.name}`);
241
- if (page.nextCursor !== null)
242
- lines.push(`… more (use --cursor ${page.nextCursor})`);
243
- return ok(lines.join(`
244
- `));
245
- }
246
- case "create": {
247
- const projectId = rest[0];
248
- const flags = parseFlags(rest.slice(1));
249
- const title = flags.title;
250
- if (projectId === undefined || title === undefined) {
251
- return fail('Usage: ycadence work-item create <projectId> --title "..."');
252
- }
253
- const created = await context.client.createWorkItem(projectId, {
254
- name: title,
255
- priority: flags.priority,
256
- description: flags.description
257
- });
258
- return ok(`${created.key} ${created.name}`);
259
- }
260
- case "show": {
261
- const workItemId = rest[0];
262
- if (workItemId === undefined)
263
- return fail("Usage: ycadence work-item show <workItemId>");
264
- const item = await context.client.getWorkItem(workItemId);
265
- return ok([
266
- `${item.key} ${item.name}`,
267
- `priority ${item.priority}`,
268
- `state ${item.stateId ?? "—"}`,
269
- "",
270
- item.description ?? "(no description)"
271
- ].join(`
272
- `));
273
- }
274
- case "close": {
275
- const workItemId = rest[0];
276
- if (workItemId === undefined)
277
- return fail("Usage: ycadence work-item close <workItemId>");
278
- const item = await context.client.getWorkItem(workItemId);
279
- const { states } = await context.client.listStates(item.projectId ?? "");
280
- const done = states.find((state) => state.group === "completed");
281
- if (done === undefined) {
282
- return fail("This project has no completed state, so nothing can be closed.");
283
- }
284
- await context.client.updateWorkItem(workItemId, { stateId: done.id });
285
- return ok(`Closed ${item.key}.`);
1662
+ if (error instanceof YCadenceNotFoundError || error instanceof YCadenceError && error.status === 405) {
1663
+ throw new UsageError(`"${value}" is not a project id, and this server cannot look projects up by identifier yet. Pass the project's id.`);
286
1664
  }
287
- default:
288
- return fail(`Unknown work-item action: ${action ?? "(none)"}
1665
+ throw error;
1666
+ }
1667
+ }
1668
+ async function listStates(client, projectId) {
1669
+ const { states } = await client.listStates(projectId);
1670
+ return [...states].sort((a, b) => a.sequence < b.sequence ? -1 : a.sequence > b.sequence ? 1 : 0);
1671
+ }
1672
+ function findState(states, value) {
1673
+ const match = states.find((state) => state.id === value) ?? states.find((state) => state.name.toLowerCase() === value.toLowerCase());
1674
+ if (match === undefined) {
1675
+ throw new CliError(`No state "${value}" in this project. States: ${states.map((state) => state.name).join(", ")}.`, "validation_failed", EXIT.validation);
1676
+ }
1677
+ return match;
1678
+ }
1679
+ function closingState(states) {
1680
+ const done = states.find((state) => state.group === "completed");
1681
+ if (done === undefined) {
1682
+ throw new CliError("This project has no completed state, so nothing can be closed.", "validation_failed", EXIT.validation);
1683
+ }
1684
+ return done;
1685
+ }
1686
+ var KEY_SCAN_LIMIT = 2000;
1687
+ async function resolveWorkItemId(client, value, projectId) {
1688
+ if (isUuid(value))
1689
+ return value;
1690
+ if (!WORK_ITEM_KEY.test(value)) {
1691
+ throw new UsageError(`"${value}" is neither a work item id nor a key like DP-42.`);
1692
+ }
1693
+ const project2 = await projectId();
1694
+ const wanted = value.toUpperCase();
1695
+ let seen = 0;
1696
+ for await (const item of client.iterateWorkItems(project2, { pageSize: 200 })) {
1697
+ if (item.key.toUpperCase() === wanted)
1698
+ return item.id;
1699
+ seen += 1;
1700
+ if (seen >= KEY_SCAN_LIMIT)
1701
+ break;
1702
+ }
1703
+ throw new CliError(seen >= KEY_SCAN_LIMIT ? `${value} was not among the first ${KEY_SCAN_LIMIT} items; pass its id instead.` : `No work item ${value} in this project.`, "not_found", EXIT.notFound);
1704
+ }
1705
+ function projectOf(item) {
1706
+ return typeof item.projectId === "string" && item.projectId !== "" ? item.projectId : undefined;
1707
+ }
289
1708
 
290
- ${USAGE}`);
1709
+ // src/completion.ts
1710
+ var SHELLS = ["bash", "zsh", "fish"];
1711
+ function entries(root) {
1712
+ const globalFlags = GLOBAL_FLAGS.map((flag) => `--${flag.name}`);
1713
+ const all = [
1714
+ {
1715
+ key: "",
1716
+ words: visibleChildren(root).flatMap((child) => [child.name, ...child.aliases ?? []]),
1717
+ flags: globalFlags
1718
+ }
1719
+ ];
1720
+ for (const { path, command } of walk(root)) {
1721
+ all.push({
1722
+ key: path.join(" "),
1723
+ words: visibleChildren(command).flatMap((child) => [child.name, ...child.aliases ?? []]),
1724
+ flags: [
1725
+ ...globalFlags,
1726
+ ...(command.flags ?? []).filter((flag) => flag.hidden !== true).map((flag) => `--${flag.name}`)
1727
+ ]
1728
+ });
1729
+ for (const alias of command.aliases ?? []) {
1730
+ const last = all[all.length - 1];
1731
+ all.push({ ...last, key: [...path.slice(0, -1), alias].join(" ") });
1732
+ }
1733
+ }
1734
+ return all;
1735
+ }
1736
+ function enumValues(root) {
1737
+ const values = new Map;
1738
+ for (const flag of GLOBAL_FLAGS)
1739
+ if (flag.values)
1740
+ values.set(`--${flag.name}`, flag.values);
1741
+ for (const { command } of walk(root)) {
1742
+ for (const flag of command.flags ?? [])
1743
+ if (flag.values)
1744
+ values.set(`--${flag.name}`, flag.values);
291
1745
  }
1746
+ return values;
1747
+ }
1748
+ function bash(root) {
1749
+ const table = entries(root).map((entry) => ` "${entry.key}") words="${[...entry.words, ...entry.flags].join(" ")}" ;;`).join(`
1750
+ `);
1751
+ const values = [...enumValues(root)].map(([flag, list]) => ` ${flag}) COMPREPLY=($(compgen -W "${list.join(" ")}" -- "$cur")); return ;;`).join(`
1752
+ `);
1753
+ return `# bash completion for ycadence (generated; do not edit)
1754
+ # Add to ~/.bashrc: source <(ycadence completion bash)
1755
+ _ycadence() {
1756
+ local cur prev cmdpath words i
1757
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1758
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
1759
+ case "$prev" in
1760
+ ${values}
1761
+ esac
1762
+ cmdpath=""
1763
+ for ((i = 1; i < COMP_CWORD; i++)); do
1764
+ case "\${COMP_WORDS[i]}" in
1765
+ -*) ;;
1766
+ *) if _ycadence_known "\${cmdpath:+$cmdpath }\${COMP_WORDS[i]}"; then cmdpath="\${cmdpath:+$cmdpath }\${COMP_WORDS[i]}"; fi ;;
1767
+ esac
1768
+ done
1769
+ _ycadence_words "$cmdpath"
1770
+ COMPREPLY=($(compgen -W "$words" -- "$cur"))
1771
+ }
1772
+ _ycadence_words() {
1773
+ case "$1" in
1774
+ ${table}
1775
+ *) words="" ;;
1776
+ esac
292
1777
  }
1778
+ _ycadence_known() {
1779
+ local words
1780
+ _ycadence_words "$1"
1781
+ [ -n "$words" ]
1782
+ }
1783
+ complete -F _ycadence ycadence devpulse
1784
+ `;
1785
+ }
1786
+ function zsh(root) {
1787
+ const table = entries(root).map((entry) => ` "${entry.key}") reply=(${[...entry.words, ...entry.flags].join(" ")}) ;;`).join(`
1788
+ `);
1789
+ const values = [...enumValues(root)].map(([flag, list]) => ` ${flag}) compadd -- ${list.join(" ")}; return ;;`).join(`
1790
+ `);
1791
+ return `#compdef ycadence devpulse
1792
+ # zsh completion for ycadence (generated; do not edit)
1793
+ # Add to ~/.zshrc (after compinit): source <(ycadence completion zsh)
1794
+ _ycadence_words() {
1795
+ case "$1" in
1796
+ ${table}
1797
+ *) reply=() ;;
1798
+ esac
1799
+ }
1800
+ _ycadence() {
1801
+ local -a reply
1802
+ local cmdpath="" word i
1803
+ case "\${words[CURRENT-1]}" in
1804
+ ${values}
1805
+ esac
1806
+ for (( i = 2; i < CURRENT; i++ )); do
1807
+ word="\${words[i]}"
1808
+ [[ "$word" == -* ]] && continue
1809
+ _ycadence_words "\${cmdpath:+$cmdpath }$word"
1810
+ (( \${#reply} )) && cmdpath="\${cmdpath:+$cmdpath }$word"
1811
+ done
1812
+ _ycadence_words "$cmdpath"
1813
+ compadd -- $reply
1814
+ }
1815
+ compdef _ycadence ycadence devpulse
1816
+ `;
1817
+ }
1818
+ function fish(root) {
1819
+ const lines = [
1820
+ "# fish completion for ycadence (generated; do not edit)",
1821
+ "# Save as ~/.config/fish/completions/ycadence.fish: ycadence completion fish > ~/.config/fish/completions/ycadence.fish",
1822
+ "function __ycadence_path",
1823
+ " set -l tokens (commandline -opc)",
1824
+ " set -e tokens[1]",
1825
+ " set -l words",
1826
+ " for token in $tokens",
1827
+ " string match -q -- '-*' $token; or set words $words $token",
1828
+ " end",
1829
+ ' string join " " $words',
1830
+ "end"
1831
+ ];
1832
+ for (const entry of entries(root)) {
1833
+ const condition = `test (__ycadence_path) = '${entry.key}'`;
1834
+ if (entry.words.length > 0) {
1835
+ lines.push(`complete -c ycadence -f -n "${condition}" -a '${entry.words.join(" ")}'`);
1836
+ }
1837
+ }
1838
+ for (const flag of GLOBAL_FLAGS) {
1839
+ const values = flag.values ? ` -a '${flag.values.join(" ")}'` : "";
1840
+ lines.push(`complete -c ycadence -l ${flag.name}${flag.short ? ` -s ${flag.short}` : ""}${flag.type === "boolean" ? "" : " -r"}${values} -d '${flag.description.replace(/'/g, "\\'")}'`);
1841
+ }
1842
+ for (const { path, command } of walk(root)) {
1843
+ for (const flag of command.flags ?? []) {
1844
+ if (flag.hidden === true)
1845
+ continue;
1846
+ const values = flag.values ? ` -a '${flag.values.join(" ")}'` : "";
1847
+ lines.push(`complete -c ycadence -n "test (__ycadence_path) = '${path.join(" ")}'" -l ${flag.name}${flag.type === "boolean" ? "" : " -r"}${values} -d '${flag.description.replace(/'/g, "\\'")}'`);
1848
+ }
1849
+ }
1850
+ lines.push("complete -c devpulse -w ycadence", "");
1851
+ return lines.join(`
1852
+ `);
1853
+ }
1854
+ function completionScript(root, shell) {
1855
+ if (shell === "bash")
1856
+ return bash(root);
1857
+ if (shell === "zsh")
1858
+ return zsh(root);
1859
+ return fish(root);
1860
+ }
1861
+
1862
+ // src/handlers/auth.ts
1863
+ function str(value) {
1864
+ return typeof value === "string" ? value : undefined;
1865
+ }
1866
+ function keyHint(key) {
1867
+ return `${key.slice(0, 12)}… (${key.length} chars)`;
1868
+ }
1869
+ async function storedKey(context) {
1870
+ return readEnv("API_KEY", context.env) ?? await context.secrets.get(secretAccount.apiKey(context.config.profile));
1871
+ }
1872
+ var loginHandler = async (context) => {
1873
+ const profile = context.config.profile;
1874
+ assertProfileName(profile);
1875
+ const file = await context.configStore.load();
1876
+ const stored = { ...file.profiles[profile] ?? {} };
1877
+ const url = str(context.flags.url);
1878
+ if (url !== undefined) {
1879
+ try {
1880
+ new URL(url);
1881
+ } catch {
1882
+ throw new UsageError(`--url takes a URL, got "${url}".`);
1883
+ }
1884
+ stored.url = url.replace(/\/+$/, "");
1885
+ }
1886
+ const clientIdFlag = str(context.flags["oauth-client-id"]);
1887
+ if (clientIdFlag !== undefined)
1888
+ stored.oauthClientId = clientIdFlag;
1889
+ const audienceFlag = str(context.flags["iap-audience"]);
1890
+ if (audienceFlag !== undefined)
1891
+ stored.iapAudience = audienceFlag;
1892
+ file.profiles[profile] = stored;
1893
+ if (file.currentProfile === undefined)
1894
+ file.currentProfile = profile;
1895
+ await context.configStore.save(file);
1896
+ let apiKey;
1897
+ if (context.flags["api-key-stdin"] === true) {
1898
+ apiKey = (await context.readStdin()).trim();
1899
+ if (apiKey === "")
1900
+ throw new UsageError("--api-key-stdin read nothing from stdin.");
1901
+ }
1902
+ const clientId = readEnv("OAUTH_CLIENT_ID", context.env) ?? stored.oauthClientId;
1903
+ const audience = readEnv("IAP_AUDIENCE", context.env) ?? stored.iapAudience;
1904
+ let signedIn = false;
1905
+ if (context.flags["skip-oauth"] === true) {
1906
+ context.debug("Skipping the IAP sign-in (--skip-oauth).");
1907
+ } else if (clientId === undefined) {
1908
+ context.notice("No OAuth client is configured, so the IAP sign-in is skipped. That is right for an instance that is not behind IAP. For one that is, a person must first create the Desktop OAuth client (see docs/cli/work-management.md), then run `ycadence login --oauth-client-id <id> --iap-audience <IAP client id>`.");
1909
+ } else {
1910
+ const clientSecret = context.env.YCADENCE_OAUTH_CLIENT_SECRET || await context.secrets.get(secretAccount.oauthClientSecret(profile));
1911
+ if (clientSecret === undefined || clientSecret === "") {
1912
+ throw new ConfigError("The OAuth client secret is missing. Set YCADENCE_OAUTH_CLIENT_SECRET for this login; it is then kept in the credential store.");
1913
+ }
1914
+ const client = oauthClientFrom({ clientId, clientSecret, env: context.env });
1915
+ const tokens = context.flags.device === true ? await deviceLogin({
1916
+ client,
1917
+ fetch: context.fetch,
1918
+ print: (message) => context.notice(message),
1919
+ sleep: (ms) => context.sleep(ms),
1920
+ now: () => context.now()
1921
+ }) : await loopbackLogin({
1922
+ client,
1923
+ fetch: context.fetch,
1924
+ openUrl: context.flags["no-browser"] === true ? async () => false : (target) => context.openUrl(target),
1925
+ print: (message) => context.notice(message),
1926
+ now: () => context.now()
1927
+ });
1928
+ if (tokens.refreshToken === undefined) {
1929
+ throw new CliError("Google returned no refresh token. Revoke the app at myaccount.google.com/permissions and sign in again.", "oauth_error");
1930
+ }
1931
+ await context.secrets.set(secretAccount.refreshToken(profile), tokens.refreshToken);
1932
+ await context.secrets.set(secretAccount.oauthClientSecret(profile), clientSecret);
1933
+ const identity = audience === undefined ? tokens.idToken === undefined ? undefined : { idToken: tokens.idToken, expiresAt: tokens.expiresAt } : await refreshIdToken({
1934
+ client,
1935
+ refreshToken: tokens.refreshToken,
1936
+ audience,
1937
+ fetch: context.fetch,
1938
+ now: () => context.now()
1939
+ });
1940
+ if (identity === undefined) {
1941
+ await context.secrets.delete(secretAccount.identityToken(profile));
1942
+ } else {
1943
+ await context.secrets.set(secretAccount.identityToken(profile), JSON.stringify({ token: identity.idToken, expiresAt: identity.expiresAt }));
1944
+ }
1945
+ signedIn = true;
1946
+ }
1947
+ if (apiKey !== undefined)
1948
+ await context.secrets.set(secretAccount.apiKey(profile), apiKey);
1949
+ await context.reloadConfig();
1950
+ const key = await storedKey(context);
1951
+ const result = {
1952
+ profile,
1953
+ url: context.config.url ?? null,
1954
+ identity: signedIn,
1955
+ apiKey: key === undefined ? null : keyHint(key),
1956
+ credentialStore: context.secrets.kind,
1957
+ organization: null
1958
+ };
1959
+ if (context.config.url !== undefined && key !== undefined) {
1960
+ const client = await context.client();
1961
+ try {
1962
+ const me = await client.me();
1963
+ result.organization = me.organization.slug;
1964
+ } catch (error) {
1965
+ if (!(error instanceof YCadenceNotFoundError))
1966
+ throw error;
1967
+ await client.health();
1968
+ }
1969
+ }
1970
+ const lines = [`Profile "${profile}" saved.`];
1971
+ if (signedIn)
1972
+ lines.push("Signed in for IAP; the identity token refreshes itself.");
1973
+ if (key !== undefined)
1974
+ lines.push(`API key ${keyHint(key)} in the ${context.secrets.description}.`);
1975
+ else
1976
+ lines.push("No API key yet: run `ycadence login --api-key-stdin < key.txt`.");
1977
+ if (result.organization !== null)
1978
+ lines.push(`Connected to organization ${result.organization}.`);
1979
+ return { data: result, text: lines.join(`
1980
+ `) };
1981
+ };
1982
+ var statusHandler = async (context) => {
1983
+ const key = await storedKey(context);
1984
+ if (context.config.url === undefined || key === undefined) {
1985
+ throw new ConfigError("Not signed in: no URL or API key for this profile. Run `ycadence login`.");
1986
+ }
1987
+ const refresh = await context.secrets.get(secretAccount.refreshToken(context.config.profile));
1988
+ const data = {
1989
+ profile: context.config.profile,
1990
+ url: context.config.url,
1991
+ apiKey: keyHint(key),
1992
+ identity: refresh !== undefined,
1993
+ me: null
1994
+ };
1995
+ try {
1996
+ data.me = await (await context.client()).me();
1997
+ } catch (error) {
1998
+ if (!(error instanceof YCadenceNotFoundError))
1999
+ throw error;
2000
+ data.note = "This server does not have GET /api/v1/me yet.";
2001
+ }
2002
+ const me = data.me;
2003
+ const lines = [
2004
+ `profile ${data.profile}`,
2005
+ `url ${data.url}`,
2006
+ `api key ${data.apiKey}`,
2007
+ `identity ${data.identity === true ? "signed in (IAP)" : "not signed in"}`
2008
+ ];
2009
+ if (me !== null) {
2010
+ lines.push(`org ${me.organization?.slug ?? "—"}`);
2011
+ lines.push(`as ${me.principal?.email ?? me.principal?.kind ?? "—"}`);
2012
+ lines.push(`scopes ${(me.scopes ?? []).join(", ") || "—"}`);
2013
+ if (context.globals.org !== undefined && me.organization?.slug !== context.globals.org) {
2014
+ context.notice(`This key belongs to ${me.organization?.slug}, not ${context.globals.org}.`);
2015
+ }
2016
+ } else {
2017
+ lines.push(` ${data.note}`);
2018
+ }
2019
+ return { data, text: lines.join(`
2020
+ `) };
2021
+ };
2022
+ var logoutHandler = async (context) => {
2023
+ const profile = context.config.profile;
2024
+ for (const account of [
2025
+ secretAccount.apiKey(profile),
2026
+ secretAccount.refreshToken(profile),
2027
+ secretAccount.identityToken(profile),
2028
+ secretAccount.oauthClientSecret(profile)
2029
+ ]) {
2030
+ await context.secrets.delete(account);
2031
+ }
2032
+ return {
2033
+ data: { profile, loggedOut: true },
2034
+ text: `Removed the stored credentials for profile "${profile}". Its settings are kept; \`ycadence config list\` shows them.`
2035
+ };
2036
+ };
2037
+ var tokenHandler = async (context) => {
2038
+ const fromEnv = readEnv("IDENTITY_TOKEN", context.env);
2039
+ const token = fromEnv ?? await storedIdentityToken({
2040
+ config: context.config,
2041
+ secrets: context.secrets,
2042
+ env: context.env,
2043
+ fetch: context.fetch,
2044
+ now: () => context.now()
2045
+ })();
2046
+ if (token === undefined) {
2047
+ throw new ConfigError("No identity token: run `ycadence login` against an IAP instance first.");
2048
+ }
2049
+ return { data: { token }, text: token, ids: [token] };
2050
+ };
2051
+
2052
+ // src/handlers/config.ts
2053
+ function str2(value) {
2054
+ return typeof value === "string" ? value : undefined;
2055
+ }
2056
+ function validate(key, value) {
2057
+ if (key === "url") {
2058
+ try {
2059
+ new URL(value);
2060
+ } catch {
2061
+ throw new UsageError(`url takes a URL, got "${value}".`);
2062
+ }
2063
+ return value.replace(/\/+$/, "");
2064
+ }
2065
+ if (key === "format" && !["table", "json", "ndjson"].includes(value)) {
2066
+ throw new UsageError("format is one of table, json, ndjson.");
2067
+ }
2068
+ return value;
2069
+ }
2070
+ var configGetHandler = async (context) => {
2071
+ const key = str2(context.args.key);
2072
+ assertProfileKey(key);
2073
+ const value = (await context.configStore.load()).profiles[context.config.profile]?.[key];
2074
+ return { data: { profile: context.config.profile, key, value: value ?? null }, text: value ?? "" };
2075
+ };
2076
+ var configSetHandler = async (context) => {
2077
+ const key = str2(context.args.key);
2078
+ assertProfileKey(key);
2079
+ const value = validate(key, str2(context.args.value));
2080
+ const file = await context.configStore.load();
2081
+ const profile = context.config.profile;
2082
+ file.profiles[profile] = { ...file.profiles[profile] ?? {}, [key]: value };
2083
+ if (file.currentProfile === undefined)
2084
+ file.currentProfile = profile;
2085
+ await context.configStore.save(file);
2086
+ return { data: { profile, key, value }, text: `Set ${key} for profile "${profile}".` };
2087
+ };
2088
+ var configUnsetHandler = async (context) => {
2089
+ const key = str2(context.args.key);
2090
+ assertProfileKey(key);
2091
+ const file = await context.configStore.load();
2092
+ const profile = context.config.profile;
2093
+ const stored = { ...file.profiles[profile] ?? {} };
2094
+ delete stored[key];
2095
+ file.profiles[profile] = stored;
2096
+ await context.configStore.save(file);
2097
+ return { data: { profile, key, value: null }, text: `Unset ${key} for profile "${profile}".` };
2098
+ };
2099
+ var configListHandler = async (context) => {
2100
+ const config = context.config;
2101
+ const rows = PROFILE_KEYS.map((key) => {
2102
+ const value = config[key];
2103
+ const source = config.sources[key] ?? (value === undefined ? "none" : "profile");
2104
+ return { key, value: value ?? null, source, description: PROFILE_KEY_HELP[key] };
2105
+ });
2106
+ return {
2107
+ data: { profile: config.profile, path: context.configStore.path ?? null, settings: rows },
2108
+ text: [
2109
+ `profile "${config.profile}" (${context.configStore.path ?? "not saved"})`,
2110
+ ...rows.map((row) => ` ${row.key.padEnd(14)} ${String(row.value ?? "—").padEnd(40)} ${row.source}`)
2111
+ ].join(`
2112
+ `)
2113
+ };
2114
+ };
2115
+ var configProfilesHandler = async (context) => {
2116
+ const file = await context.configStore.load();
2117
+ const current = context.config.profile;
2118
+ const names = Object.keys(file.profiles).sort();
2119
+ const rows = names.map((name) => ({
2120
+ name,
2121
+ current: name === current,
2122
+ url: file.profiles[name]?.url ?? null
2123
+ }));
2124
+ return {
2125
+ data: rows,
2126
+ columns: ["name", "current", "url"],
2127
+ empty: "No profiles yet. `ycadence login` creates one.",
2128
+ text: rows.length === 0 ? "No profiles yet. `ycadence login` creates one." : rows.map((row) => `${row.current ? "*" : " "} ${row.name.padEnd(16)} ${row.url ?? "—"}`).join(`
2129
+ `)
2130
+ };
2131
+ };
2132
+ var configUseHandler = async (context) => {
2133
+ const name = str2(context.args.profile);
2134
+ assertProfileName(name);
2135
+ const file = await context.configStore.load();
2136
+ file.currentProfile = name;
2137
+ file.profiles[name] = file.profiles[name] ?? {};
2138
+ await context.configStore.save(file);
2139
+ return { data: { currentProfile: name }, text: `Now using profile "${name}".` };
2140
+ };
2141
+ var configPathHandler = async (context) => {
2142
+ const path = context.configStore.path ?? null;
2143
+ return { data: { path }, text: path ?? "No config directory (no HOME or XDG_CONFIG_HOME)." };
2144
+ };
2145
+
2146
+ // src/version.ts
2147
+ var CLI_VERSION = "0.2.1";
2148
+
2149
+ // src/handlers/doctor.ts
293
2150
  function legacyNote(name, env) {
294
2151
  const current = env[`YCADENCE_${name}`];
295
2152
  if (current !== undefined && current !== "")
296
2153
  return "";
297
2154
  return ` (read from ${LEGACY_ENV_PREFIX}${name}; rename it to YCADENCE_${name})`;
298
2155
  }
299
- async function doctor(context) {
2156
+ var doctorHandler = async (context) => {
300
2157
  const lines = [];
2158
+ const checks = [];
301
2159
  let healthy = true;
302
- const url = readEnv("URL", context.env);
303
- const key = readEnv("API_KEY", context.env);
304
- lines.push(url === undefined ? "✗ YCADENCE_URL is not set" : `✓ YCADENCE_URL ${url}${legacyNote("URL", context.env)}`);
305
- lines.push(key === undefined ? "✗ YCADENCE_API_KEY is not set" : `✓ YCADENCE_API_KEY ${key.slice(0, 12)}… (${key.length} chars)${legacyNote("API_KEY", context.env)}`);
306
- if (url === undefined || key === undefined)
307
- healthy = false;
308
- const identityToken = readEnv("IDENTITY_TOKEN", context.env);
309
- lines.push(identityToken === undefined || identityToken.length === 0 ? "· YCADENCE_IDENTITY_TOKEN not set (only needed behind IAP)" : `✓ YCADENCE_IDENTITY_TOKEN set (${identityToken.length} chars)${legacyNote("IDENTITY_TOKEN", context.env)}`);
310
- if (healthy) {
2160
+ const add = (name, ok, line, detail = line) => {
2161
+ lines.push(line);
2162
+ checks.push({ name, ok, detail });
2163
+ if (ok === false)
2164
+ healthy = false;
2165
+ };
2166
+ const env = context.env;
2167
+ const profile = context.config.profile;
2168
+ lines.push(`ycadence ${CLI_VERSION}, profile "${profile}"`);
2169
+ const envUrl = readEnv("URL", env);
2170
+ const url = context.config.url;
2171
+ if (url === undefined)
2172
+ add("url", false, "✗ YCADENCE_URL is not set");
2173
+ else if (envUrl !== undefined)
2174
+ add("url", true, `✓ YCADENCE_URL ${url}${legacyNote("URL", env)}`);
2175
+ else
2176
+ add("url", true, `✓ url ${url} (profile "${profile}")`);
2177
+ const envKey = readEnv("API_KEY", env);
2178
+ const storedKey2 = envKey === undefined ? await context.secrets.get(secretAccount.apiKey(profile)) : undefined;
2179
+ const key = envKey ?? storedKey2;
2180
+ if (key === undefined)
2181
+ add("apiKey", false, "✗ YCADENCE_API_KEY is not set");
2182
+ else if (envKey !== undefined) {
2183
+ add("apiKey", true, `✓ YCADENCE_API_KEY ${keyHint(key)}${legacyNote("API_KEY", env)}`);
2184
+ } else
2185
+ add("apiKey", true, `✓ API key ${keyHint(key)} (${context.secrets.description})`);
2186
+ add("credentialStore", context.secrets.kind === "file" ? null : true, context.secrets.kind === "file" ? `! credentials in ${context.secrets.description}` : `✓ credentials in ${context.secrets.description}`);
2187
+ const identityToken = readEnv("IDENTITY_TOKEN", env);
2188
+ const refresh = await context.secrets.get(secretAccount.refreshToken(profile));
2189
+ if (identityToken !== undefined && identityToken.length > 0) {
2190
+ add("identity", true, `✓ YCADENCE_IDENTITY_TOKEN set (${identityToken.length} chars)${legacyNote("IDENTITY_TOKEN", env)}`);
2191
+ } else if (refresh !== undefined) {
2192
+ add("identity", true, "✓ signed in for IAP (`ycadence login`)");
2193
+ } else {
2194
+ add("identity", null, "· YCADENCE_IDENTITY_TOKEN not set (only needed behind IAP)");
2195
+ }
2196
+ if (url !== undefined && key !== undefined) {
2197
+ const client = await context.client();
311
2198
  try {
312
- const health = await context.client.health();
313
- lines.push(`✓ reachable — status ${health.status}`);
314
- for (const [name, state] of Object.entries(health.checks)) {
315
- lines.push(` ${state === "ok" ? "✓" : "✗"} ${name}: ${state}`);
316
- if (state !== "ok")
317
- healthy = false;
2199
+ const health = await client.health();
2200
+ add("reachable", true, `✓ reachable — status ${health.status}`);
2201
+ for (const [name, state] of Object.entries(health.checks ?? {})) {
2202
+ add(`health.${name}`, state === "ok", ` ${state === "ok" ? "✓" : "✗"} ${name}: ${state}`);
318
2203
  }
319
2204
  } catch (error) {
320
- healthy = false;
321
- lines.push(`✗ unreachable — ${error instanceof Error ? error.message : "unknown error"}`);
322
- }
323
- }
324
- return healthy ? ok(lines.join(`
325
- `)) : fail(lines.join(`
326
- `));
327
- }
328
- function parseFlags(argv) {
329
- const flags = {};
330
- for (let index = 0;index < argv.length; index += 1) {
331
- const token = argv[index];
332
- if (!token.startsWith("--"))
333
- continue;
334
- const equals = token.indexOf("=");
335
- if (equals > -1) {
336
- flags[token.slice(2, equals)] = token.slice(equals + 1);
337
- continue;
2205
+ const message = error instanceof Error ? error.message : "unknown error";
2206
+ add("reachable", false, `✗ unreachable — ${message}`);
2207
+ if (/audience/i.test(message)) {
2208
+ lines.push(" The identity token was minted for the wrong client. Set iapAudience to the IAP OAuth client id and run `ycadence login` again.");
2209
+ } else if (error instanceof YCadenceError && error.status === 401 && refresh === undefined && identityToken === undefined) {
2210
+ lines.push(" If this instance is behind IAP, sign in with `ycadence login` first.");
2211
+ }
338
2212
  }
339
- const next = argv[index + 1];
340
- if (next === undefined || next.startsWith("--")) {
341
- flags[token.slice(2)] = "";
342
- continue;
2213
+ if (healthy) {
2214
+ try {
2215
+ const me = await client.me();
2216
+ if (typeof me === "object" && me !== null && "organization" in me) {
2217
+ add("key", true, `✓ key accepted — organization ${me.organization.slug}`);
2218
+ }
2219
+ } catch (error) {
2220
+ if (error instanceof YCadenceNotFoundError) {
2221
+ add("key", null, "· key not checked (this server has no /api/v1/me yet)");
2222
+ } else {
2223
+ add("key", false, `✗ key refused — ${error instanceof Error ? error.message : "error"}`);
2224
+ }
2225
+ }
343
2226
  }
344
- flags[token.slice(2)] = next;
345
- index += 1;
346
2227
  }
347
- return flags;
348
- }
2228
+ return {
2229
+ data: { healthy, profile, checks },
2230
+ text: lines.join(`
2231
+ `),
2232
+ exitCode: healthy ? EXIT.ok : EXIT.failure
2233
+ };
2234
+ };
2235
+
2236
+ // src/handlers/imports.ts
349
2237
  var IMPORT_SOURCES = ["csv", "github", "jira", "linear"];
350
- async function importCommand(argv, context) {
351
- const [action, target] = argv;
352
- const flags = parseFlags(argv.slice(2));
353
- if (action === "status") {
354
- if (target === undefined)
355
- return fail("Usage: ycadence import status <runId>");
356
- const run2 = await context.client.getImport(target);
357
- return ok(formatRun(run2));
358
- }
359
- if (action === "list") {
360
- if (target === undefined)
361
- return fail("Usage: ycadence import list <projectId>");
362
- const { runs } = await context.client.listImports(target);
363
- return ok(runs.length === 0 ? "No imports." : runs.map((run2) => formatRun(run2)).join(`
364
-
365
- `));
366
- }
367
- if (action !== "preview" && action !== "run") {
368
- return fail(`Usage: ycadence import preview|run|status|list …`);
369
- }
370
- if (target === undefined)
371
- return fail(`Usage: ycadence import ${action} <projectId> --file …`);
372
- const source = flags.source ?? "csv";
2238
+ function str3(value) {
2239
+ return typeof value === "string" ? value : undefined;
2240
+ }
2241
+ async function importBody(context) {
2242
+ const source = str3(context.flags.source) ?? "csv";
373
2243
  if (!IMPORT_SOURCES.includes(source)) {
374
- return fail(`Unknown source "${source}". One of: ${IMPORT_SOURCES.join(", ")}.`);
2244
+ throw new UsageError(`Unknown source "${source}". One of: ${IMPORT_SOURCES.join(", ")}.`);
2245
+ }
2246
+ const file = str3(context.flags.file);
2247
+ if (file === undefined)
2248
+ throw new UsageError("Pass --file with the export to import.");
2249
+ const text = await context.readFile(file);
2250
+ let data = text;
2251
+ if (source !== "csv") {
2252
+ try {
2253
+ data = JSON.parse(text);
2254
+ } catch {
2255
+ throw new UsageError(`${file} is not valid JSON; a ${source} export should be.`);
2256
+ }
375
2257
  }
376
- if (flags.file === undefined)
377
- return fail("Pass --file with the export to import.");
378
- const text = await context.readFile(flags.file);
379
- const data = source === "csv" ? text : JSON.parse(text);
380
2258
  const body = { source, data };
381
- if (flags.repository !== undefined)
382
- body.repository = flags.repository;
383
- if (flags["base-url"] !== undefined)
384
- body.baseUrl = flags["base-url"];
385
- if (action === "preview") {
386
- const preview = await context.client.previewImport(target, body);
387
- const lines = [
388
- `${preview.total} items from ${preview.source}.`,
389
- preview.idempotent ? "" : "This source has no stable ids, so re-importing an edited row creates a second item.",
390
- "",
391
- "States:",
392
- ...preview.states.map((state) => ` ${state.name} → ${state.suggestedStateId ?? "no match; will use the default state"}`),
393
- "",
394
- "Labels:",
395
- ...preview.labels.map((label) => ` ${label.name}${label.exists ? "" : " (will create)"}`),
396
- "",
397
- "Assignees:",
398
- ...preview.assignees.map((assignee) => ` ${assignee.value}${assignee.matched ? "" : " (no matching member)"}`)
399
- ];
400
- if (preview.unmappedPriorities.length > 0) {
401
- lines.push("", `Unrecognised priorities: ${preview.unmappedPriorities.join(", ")}`);
402
- }
403
- if (preview.skipped.length > 0) {
404
- lines.push("", "Skipped:");
405
- for (const skip of preview.skipped)
406
- lines.push(` row ${skip.row}: ${skip.reason}`);
407
- }
408
- return ok(lines.filter((line, index) => line !== "" || index > 0).join(`
409
- `));
2259
+ const repository = str3(context.flags.repository);
2260
+ if (repository !== undefined)
2261
+ body.repository = repository;
2262
+ const baseUrl = str3(context.flags["base-url"]);
2263
+ if (baseUrl !== undefined)
2264
+ body.baseUrl = baseUrl;
2265
+ return body;
2266
+ }
2267
+ function formatRun(run) {
2268
+ const lines = [
2269
+ `${run.id} [${run.status}] ${run.source}`,
2270
+ ` ${run.cursor}/${run.totalItems} committed — ${run.createdCount} created, ${run.updatedCount} updated`
2271
+ ];
2272
+ if (run.error !== null)
2273
+ lines.push(` error: ${run.error}`);
2274
+ for (const miss of run.unmatched.slice(0, 20)) {
2275
+ lines.push(` unmatched ${miss.kind} "${miss.value}" on ${miss.item}`);
410
2276
  }
2277
+ if (run.unmatched.length > 20) {
2278
+ lines.push(` …and ${run.unmatched.length - 20} more`);
2279
+ }
2280
+ return lines.join(`
2281
+ `);
2282
+ }
2283
+ var previewImportHandler = async (context) => {
2284
+ const projectId = await context.projectId(str3(context.args.projectId));
2285
+ const body = await importBody(context);
2286
+ const preview = await (await context.client()).previewImport(projectId, body);
2287
+ const lines = [
2288
+ `${preview.total} items from ${preview.source}.`,
2289
+ preview.idempotent ? "" : "This source has no stable ids, so re-importing an edited row creates a second item.",
2290
+ "",
2291
+ "States:",
2292
+ ...preview.states.map((state) => ` ${state.name} → ${state.suggestedStateId ?? "no match; will use the default state"}`),
2293
+ "",
2294
+ "Labels:",
2295
+ ...preview.labels.map((label) => ` ${label.name}${label.exists ? "" : " (will create)"}`),
2296
+ "",
2297
+ "Assignees:",
2298
+ ...preview.assignees.map((assignee) => ` ${assignee.value}${assignee.matched ? "" : " (no matching member)"}`)
2299
+ ];
2300
+ if (preview.unmappedPriorities.length > 0) {
2301
+ lines.push("", `Unrecognised priorities: ${preview.unmappedPriorities.join(", ")}`);
2302
+ }
2303
+ if (preview.skipped.length > 0) {
2304
+ lines.push("", "Skipped:");
2305
+ for (const skip of preview.skipped)
2306
+ lines.push(` row ${skip.row}: ${skip.reason}`);
2307
+ }
2308
+ return { data: preview, text: lines.filter((line, index) => line !== "" || index > 0).join(`
2309
+ `) };
2310
+ };
2311
+ var runImportHandler = async (context) => {
2312
+ const projectId = await context.projectId(str3(context.args.projectId));
2313
+ const body = await importBody(context);
411
2314
  const stateMap = {};
412
- for (const pair of toList(flags.map)) {
413
- const at = pair.indexOf("=");
2315
+ const pairs = context.flags.map ?? [];
2316
+ for (const pair of pairs.flatMap((value) => value.split(","))) {
2317
+ const trimmed = pair.trim();
2318
+ if (trimmed === "")
2319
+ continue;
2320
+ const at = trimmed.indexOf("=");
414
2321
  if (at < 0)
415
- return fail(`--map takes "SourceState=<stateId>", got "${pair}".`);
416
- stateMap[pair.slice(0, at)] = pair.slice(at + 1);
2322
+ throw new UsageError(`--map takes "SourceState=<stateId>", got "${trimmed}".`);
2323
+ stateMap[trimmed.slice(0, at)] = trimmed.slice(at + 1);
417
2324
  }
418
2325
  if (Object.keys(stateMap).length > 0)
419
2326
  body.stateMap = stateMap;
420
- const { runId } = await context.client.startImport(target, body);
421
- return ok(`Queued as ${runId}. Follow it with: ycadence import status ${runId}`);
2327
+ const { runId } = await (await context.client()).startImport(projectId, body, context.mutationOptions());
2328
+ return {
2329
+ data: { runId },
2330
+ text: `Queued as ${runId}. Follow it with: ycadence import status ${runId}`,
2331
+ ids: [runId]
2332
+ };
2333
+ };
2334
+ var importStatusHandler = async (context) => {
2335
+ const run = await (await context.client()).getImport(str3(context.args.runId));
2336
+ return { data: run, text: formatRun(run) };
2337
+ };
2338
+ var listImportsHandler = async (context) => {
2339
+ const projectId = await context.projectId(str3(context.args.projectId));
2340
+ const { runs } = await (await context.client()).listImports(projectId);
2341
+ return {
2342
+ data: runs,
2343
+ text: runs.length === 0 ? "No imports." : runs.map((run) => formatRun(run)).join(`
2344
+
2345
+ `),
2346
+ empty: "No imports."
2347
+ };
2348
+ };
2349
+
2350
+ // src/handlers/intake.ts
2351
+ function str4(value) {
2352
+ return typeof value === "string" ? value : undefined;
422
2353
  }
423
- function formatRun(run2) {
424
- const lines = [
425
- `${run2.id} [${run2.status}] ${run2.source}`,
426
- ` ${run2.cursor}/${run2.totalItems} committed — ${run2.createdCount} created, ${run2.updatedCount} updated`
427
- ];
428
- if (run2.error !== null)
429
- lines.push(` error: ${run2.error}`);
430
- for (const miss of run2.unmatched.slice(0, 20)) {
431
- lines.push(` unmatched ${miss.kind} "${miss.value}" on ${miss.item}`);
2354
+ var TRIAGE_DECISIONS = ["accepted", "declined", "snoozed", "duplicate"];
2355
+ var listIntakeHandler = async (context) => {
2356
+ const projectId = await context.projectId();
2357
+ const { items } = await (await context.client()).listIntake(projectId, {
2358
+ ...context.flags["include-decided"] === true ? { includeDecided: true } : {}
2359
+ });
2360
+ return {
2361
+ data: items,
2362
+ columns: ["id", "status", "source", "submittedAt"],
2363
+ empty: "The triage queue is empty."
2364
+ };
2365
+ };
2366
+ var submitIntakeHandler = async (context) => {
2367
+ const projectId = await context.projectId();
2368
+ const title = str4(context.flags.title);
2369
+ const file = str4(context.flags["description-file"]);
2370
+ const description = file === undefined ? str4(context.flags.description) : file === "-" ? await context.readStdin() : await context.readFile(file);
2371
+ const email = str4(context.flags.email);
2372
+ const result = await (await context.client()).submitIntake(projectId, {
2373
+ title,
2374
+ source: "api",
2375
+ ...description === undefined ? {} : { description },
2376
+ ...email === undefined ? {} : { sourceEmail: email }
2377
+ }, context.mutationOptions());
2378
+ return {
2379
+ data: result,
2380
+ text: `Submitted for triage as ${result.intakeItemId}.`,
2381
+ ids: [result.intakeItemId]
2382
+ };
2383
+ };
2384
+ var triageIntakeHandler = async (context) => {
2385
+ const client = await context.client();
2386
+ const intakeItemId = str4(context.args.intakeItem);
2387
+ if (!isUuid(intakeItemId))
2388
+ throw new UsageError(`"${intakeItemId}" is not an intake item id.`);
2389
+ const decision = str4(context.flags.decision);
2390
+ const body = { status: decision };
2391
+ if (decision === "snoozed") {
2392
+ const until = str4(context.flags.until);
2393
+ if (until === undefined || Number.isNaN(Date.parse(until))) {
2394
+ throw new UsageError("Snoozing needs --until <date>.");
2395
+ }
2396
+ body.snoozedUntil = until;
432
2397
  }
433
- if (run2.unmatched.length > 20) {
434
- lines.push(` …and ${run2.unmatched.length - 20} more`);
2398
+ if (decision === "duplicate") {
2399
+ const of = str4(context.flags["duplicate-of"]);
2400
+ if (of === undefined)
2401
+ throw new UsageError("Marking a duplicate needs --duplicate-of <work item>.");
2402
+ body.duplicateOfId = await resolveWorkItemId(client, of, () => context.projectId());
435
2403
  }
436
- return lines.join(`
2404
+ const result = await client.triageIntake(intakeItemId, body, context.mutationOptions());
2405
+ return { data: { intakeItemId, ...result }, text: `Triage recorded: ${result.status}.` };
2406
+ };
2407
+
2408
+ // src/handlers/project.ts
2409
+ function str5(value) {
2410
+ return typeof value === "string" ? value : undefined;
2411
+ }
2412
+ function legacyProject(context, command) {
2413
+ const positional = str5(context.args.projectId);
2414
+ if (positional !== undefined) {
2415
+ context.notice(`"ycadence ${command} <projectId>" is deprecated; use "ycadence ${command} list --project <projectId>".`);
2416
+ }
2417
+ return positional;
2418
+ }
2419
+ var listStatesHandler = async (context) => {
2420
+ const projectId = await context.projectId(legacyProject(context, "states"));
2421
+ const states = await listStates(await context.client(), projectId);
2422
+ return {
2423
+ data: states,
2424
+ columns: ["id", "name", "group"],
2425
+ empty: "No states.",
2426
+ text: states.length === 0 ? "No states." : states.map((state) => `${state.id} ${state.name} (${state.group})`).join(`
2427
+ `)
2428
+ };
2429
+ };
2430
+ var listLabelsHandler = async (context) => {
2431
+ const projectId = await context.projectId();
2432
+ const { labels } = await (await context.client()).listLabels(projectId);
2433
+ return { data: labels, columns: ["id", "name", "color"], empty: "No labels." };
2434
+ };
2435
+ var createLabelHandler = async (context) => {
2436
+ const projectId = await context.projectId();
2437
+ const name = str5(context.flags.name);
2438
+ const label = await (await context.client()).createLabel(projectId, {
2439
+ name,
2440
+ color: str5(context.flags.color),
2441
+ ...str5(context.flags.description) === undefined ? {} : { description: str5(context.flags.description) }
2442
+ }, context.mutationOptions());
2443
+ return { data: label, text: `Created label ${label.name} (${label.id}).`, ids: [label.id] };
2444
+ };
2445
+ var listTypesHandler = async (context) => {
2446
+ const projectId = await context.projectId();
2447
+ const { types: types2 } = await (await context.client()).listTypes(projectId);
2448
+ return { data: types2, columns: ["id", "name", "level"], empty: "No work item types." };
2449
+ };
2450
+ var listCyclesHandler = async (context) => {
2451
+ const projectId = await context.projectId(legacyProject(context, "cycles"));
2452
+ const { cycles } = await (await context.client()).listCycles(projectId);
2453
+ return {
2454
+ data: cycles,
2455
+ columns: ["id", "name", "status", "startDate", "endDate"],
2456
+ empty: "No cycles.",
2457
+ text: cycles.length === 0 ? "No cycles." : cycles.map((cycle) => `${cycle.id} ${cycle.name} [${cycle.status}]`).join(`
2458
+ `)
2459
+ };
2460
+ };
2461
+ var showCycleHandler = async (context) => {
2462
+ const cycleId = str5(context.args.cycle);
2463
+ const progress = await (await context.client()).getCycleProgress(cycleId);
2464
+ return { data: { id: cycleId, ...progress } };
2465
+ };
2466
+ var createCycleHandler = async (context) => {
2467
+ const projectId = await context.projectId();
2468
+ const cycle = await (await context.client()).createCycle(projectId, {
2469
+ name: str5(context.flags.name),
2470
+ ...str5(context.flags.description) === undefined ? {} : { description: str5(context.flags.description) },
2471
+ ...str5(context.flags["start-date"]) === undefined ? {} : { startDate: str5(context.flags["start-date"]) },
2472
+ ...str5(context.flags["end-date"]) === undefined ? {} : { endDate: str5(context.flags["end-date"]) }
2473
+ }, context.mutationOptions());
2474
+ return { data: cycle, text: `Created cycle ${cycle.name} (${cycle.id}).`, ids: [cycle.id] };
2475
+ };
2476
+ var addToCycleHandler = async (context) => {
2477
+ const client = await context.client();
2478
+ const cycleId = str5(context.args.cycle);
2479
+ const values = context.args.workItems ?? [];
2480
+ if (values.length > 200)
2481
+ throw new UsageError("Add at most 200 work items at a time.");
2482
+ const ids = [];
2483
+ for (const value of values) {
2484
+ ids.push(await resolveWorkItemId(client, value, () => context.projectId()));
2485
+ }
2486
+ const result = await client.addWorkItemsToCycle(cycleId, ids, context.mutationOptions());
2487
+ return {
2488
+ data: { cycleId, ...result },
2489
+ text: `Added ${result.added} work item${result.added === 1 ? "" : "s"} to the cycle.`
2490
+ };
2491
+ };
2492
+ var listViewsHandler = async (context) => {
2493
+ const projectId = await context.projectId();
2494
+ const { views } = await (await context.client()).listViews(projectId);
2495
+ return { data: views, columns: ["id", "name", "access"], empty: "No views." };
2496
+ };
2497
+ var createViewHandler = async (context) => {
2498
+ const projectId = await context.projectId();
2499
+ const rawFilter = str5(context.flags.filter);
2500
+ let where;
2501
+ if (rawFilter !== undefined) {
2502
+ try {
2503
+ where = JSON.parse(rawFilter);
2504
+ } catch {
2505
+ throw new UsageError("--filter takes a JSON filter expression.");
2506
+ }
2507
+ }
2508
+ const access = str5(context.flags.access);
2509
+ const view = await (await context.client()).createView(projectId, {
2510
+ name: str5(context.flags.name),
2511
+ ...str5(context.flags.description) === undefined ? {} : { description: str5(context.flags.description) },
2512
+ ...where === undefined ? {} : { where },
2513
+ ...access === undefined ? {} : { access }
2514
+ }, context.mutationOptions());
2515
+ return { data: view, text: `Saved view ${view.name} (${view.id}).`, ids: [view.id] };
2516
+ };
2517
+
2518
+ // src/handlers/work.ts
2519
+ var WORK_COLUMNS = ["key", "priority", "name"];
2520
+ var priorityFlag = {
2521
+ name: "priority",
2522
+ type: "string",
2523
+ values: PRIORITIES,
2524
+ placeholder: "<priority>",
2525
+ description: `One of ${PRIORITIES.join(", ")}`
2526
+ };
2527
+ var WORK_FIELD_FLAGS = {
2528
+ title: { name: "title", type: "string", placeholder: "<text>", description: "Title" },
2529
+ description: {
2530
+ name: "description",
2531
+ type: "string",
2532
+ placeholder: "<markdown>",
2533
+ description: "Description"
2534
+ },
2535
+ descriptionFile: {
2536
+ name: "description-file",
2537
+ type: "string",
2538
+ placeholder: "<path|->",
2539
+ description: "Read the description from a file, or - for stdin"
2540
+ },
2541
+ priority: priorityFlag,
2542
+ state: {
2543
+ name: "state",
2544
+ type: "string",
2545
+ placeholder: "<name|id>",
2546
+ description: "State, by name or id"
2547
+ },
2548
+ parent: {
2549
+ name: "parent",
2550
+ type: "string",
2551
+ placeholder: "<id>",
2552
+ description: "Parent work item id"
2553
+ },
2554
+ startDate: {
2555
+ name: "start-date",
2556
+ type: "string",
2557
+ placeholder: "<YYYY-MM-DD>",
2558
+ description: "Start date"
2559
+ },
2560
+ targetDate: {
2561
+ name: "target-date",
2562
+ type: "string",
2563
+ placeholder: "<YYYY-MM-DD>",
2564
+ description: "Target date"
2565
+ }
2566
+ };
2567
+ function str6(value) {
2568
+ return typeof value === "string" ? value : undefined;
2569
+ }
2570
+ function list(value) {
2571
+ return Array.isArray(value) ? value.map(String) : typeof value === "string" ? [value] : [];
2572
+ }
2573
+ function date(value, flag) {
2574
+ const text = str6(value);
2575
+ if (text === undefined)
2576
+ return;
2577
+ if (Number.isNaN(Date.parse(text)))
2578
+ throw new UsageError(`--${flag} takes a date, got "${text}".`);
2579
+ return text;
2580
+ }
2581
+ async function description(context) {
2582
+ const inline = str6(context.flags.description);
2583
+ const file = str6(context.flags["description-file"]);
2584
+ if (inline !== undefined && file !== undefined) {
2585
+ throw new UsageError("Pass --description or --description-file, not both.");
2586
+ }
2587
+ if (file === undefined)
2588
+ return inline;
2589
+ return file === "-" ? context.readStdin() : context.readFile(file);
2590
+ }
2591
+ function detailText(item) {
2592
+ return [
2593
+ `${item.key} ${item.name}`,
2594
+ `priority ${item.priority}`,
2595
+ `state ${item.stateId ?? "—"}`,
2596
+ "",
2597
+ item.description ?? "(no description)"
2598
+ ].join(`
437
2599
  `);
438
2600
  }
439
- function toList(value) {
440
- if (value === undefined)
441
- return [];
442
- return value.split(",").map((part) => part.trim()).filter((part) => part !== "");
2601
+ async function workItemId(context, value) {
2602
+ return resolveWorkItemId(await context.client(), value, () => context.projectId());
2603
+ }
2604
+ var listWork = async (context) => {
2605
+ const client = await context.client();
2606
+ const projectId = await context.projectId(str6(context.args.projectId));
2607
+ const conditions = [];
2608
+ const assignees = list(context.flags.assignee);
2609
+ if (assignees.length > 0)
2610
+ conditions.push({ field: "assignee", operator: "in", value: assignees });
2611
+ const labels = list(context.flags.label);
2612
+ if (labels.length > 0) {
2613
+ const known = (await client.listLabels(projectId)).labels;
2614
+ const ids = labels.map((value) => {
2615
+ const label = known.find((candidate) => candidate.id === value) ?? known.find((candidate) => candidate.name.toLowerCase() === value.toLowerCase());
2616
+ if (label === undefined) {
2617
+ throw new CliError(`No label "${value}" in this project.`, "validation_failed", EXIT.validation);
2618
+ }
2619
+ return label.id;
2620
+ });
2621
+ conditions.push({ field: "label", operator: "in", value: ids });
2622
+ }
2623
+ const rawFilter = str6(context.flags.filter);
2624
+ if (rawFilter !== undefined) {
2625
+ try {
2626
+ conditions.push(JSON.parse(rawFilter));
2627
+ } catch {
2628
+ throw new UsageError("--filter takes a JSON filter expression.");
2629
+ }
2630
+ }
2631
+ const stateValue = str6(context.flags.state);
2632
+ const state = stateValue === undefined ? undefined : isUuid(stateValue) ? stateValue : findState(await listStates(client, projectId), stateValue).id;
2633
+ const query = {
2634
+ filter: conditions.length === 0 ? undefined : conditions.length === 1 ? conditions[0] : { all: conditions },
2635
+ state,
2636
+ priority: str6(context.flags.priority)
2637
+ };
2638
+ if (context.globals.all) {
2639
+ const items = [];
2640
+ for await (const item of paginate((cursor) => client.listWorkItems(projectId, { ...query, limit: context.globals.limit ?? 200, cursor }), { cursor: context.globals.cursor })) {
2641
+ items.push(item);
2642
+ }
2643
+ return { data: items, nextCursor: null, columns: WORK_COLUMNS, empty: "No work items." };
2644
+ }
2645
+ const page = await client.listWorkItems(projectId, {
2646
+ ...query,
2647
+ limit: context.globals.limit ?? 50,
2648
+ cursor: context.globals.cursor
2649
+ });
2650
+ return {
2651
+ data: page.items,
2652
+ nextCursor: page.nextCursor,
2653
+ columns: WORK_COLUMNS,
2654
+ empty: "No work items."
2655
+ };
2656
+ };
2657
+ var getWork = async (context) => {
2658
+ const id = await workItemId(context, str6(context.args.workItem));
2659
+ const item = await (await context.client()).getWorkItem(id);
2660
+ return { data: item, text: detailText(item) };
2661
+ };
2662
+ var createWork = async (context) => {
2663
+ const title = str6(context.flags.title);
2664
+ if (title === undefined || title.trim() === "") {
2665
+ throw new UsageError("Pass --title with the work item title.");
2666
+ }
2667
+ const projectId = await context.projectId(str6(context.args.projectId));
2668
+ const client = await context.client();
2669
+ const body = { name: title };
2670
+ const text = await description(context);
2671
+ if (text !== undefined)
2672
+ body.description = text;
2673
+ const priority = str6(context.flags.priority);
2674
+ if (priority !== undefined)
2675
+ body.priority = priority;
2676
+ const stateValue = str6(context.flags.state);
2677
+ if (stateValue !== undefined) {
2678
+ body.stateId = isUuid(stateValue) ? stateValue : findState(await listStates(client, projectId), stateValue).id;
2679
+ }
2680
+ const assignees = list(context.flags.assignee);
2681
+ for (const assignee of assignees) {
2682
+ if (!isUuid(assignee)) {
2683
+ throw new UsageError(`--assignee takes a user id; "${assignee}" is not one.`);
2684
+ }
2685
+ }
2686
+ if (assignees.length > 0)
2687
+ body.assigneeIds = assignees;
2688
+ const labels = list(context.flags.label);
2689
+ if (labels.length > 0) {
2690
+ const known = (await client.listLabels(projectId)).labels;
2691
+ body.labelIds = labels.map((value) => {
2692
+ const label = known.find((candidate) => candidate.id === value) ?? known.find((candidate) => candidate.name.toLowerCase() === value.toLowerCase());
2693
+ if (label === undefined) {
2694
+ throw new CliError(`No label "${value}" in this project.`, "validation_failed", EXIT.validation);
2695
+ }
2696
+ return label.id;
2697
+ });
2698
+ }
2699
+ const parent = str6(context.flags.parent);
2700
+ if (parent !== undefined)
2701
+ body.parentId = await workItemId(context, parent);
2702
+ const startDate = date(context.flags["start-date"], "start-date");
2703
+ if (startDate !== undefined)
2704
+ body.startDate = startDate;
2705
+ const targetDate = date(context.flags["target-date"], "target-date");
2706
+ if (targetDate !== undefined)
2707
+ body.targetDate = targetDate;
2708
+ const created = await client.createWorkItem(projectId, body, context.mutationOptions());
2709
+ return { data: created, text: `${created.key} ${created.name}`, ids: [created.key] };
2710
+ };
2711
+ var updateWork = async (context) => {
2712
+ const client = await context.client();
2713
+ const id = await workItemId(context, str6(context.args.workItem));
2714
+ const body = {};
2715
+ const title = str6(context.flags.title);
2716
+ if (title !== undefined)
2717
+ body.name = title;
2718
+ const text = await description(context);
2719
+ if (text !== undefined)
2720
+ body.description = text;
2721
+ const priority = str6(context.flags.priority);
2722
+ if (priority !== undefined)
2723
+ body.priority = priority;
2724
+ const stateValue = str6(context.flags.state);
2725
+ if (stateValue !== undefined) {
2726
+ body.stateId = isUuid(stateValue) ? stateValue : findState(await listStates(client, await context.projectId()), stateValue).id;
2727
+ }
2728
+ if (context.flags["no-parent"] === true)
2729
+ body.parentId = null;
2730
+ const parent = str6(context.flags.parent);
2731
+ if (parent !== undefined)
2732
+ body.parentId = await workItemId(context, parent);
2733
+ const startDate = date(context.flags["start-date"], "start-date");
2734
+ if (startDate !== undefined)
2735
+ body.startDate = startDate;
2736
+ const targetDate = date(context.flags["target-date"], "target-date");
2737
+ if (targetDate !== undefined)
2738
+ body.targetDate = targetDate;
2739
+ if (Object.keys(body).length === 0) {
2740
+ throw new UsageError("Nothing to update. Pass at least one of --title, --description, --priority, --state, --parent, --start-date, --target-date.");
2741
+ }
2742
+ const version = context.flags["expect-version"];
2743
+ const updated = await client.updateWorkItem(id, body, context.mutationOptions(version === undefined ? {} : { expectedVersion: version }));
2744
+ return { data: updated, text: `Updated ${updated.key}.`, ids: [updated.key] };
2745
+ };
2746
+ async function moveTo(context, value, choose, verb) {
2747
+ const client = await context.client();
2748
+ const id = await workItemId(context, value);
2749
+ const item = await client.getWorkItem(id);
2750
+ const projectId = projectOf(item) ?? await context.projectId();
2751
+ const target = choose(await listStates(client, projectId));
2752
+ if (item.stateId === target.id) {
2753
+ return {
2754
+ data: { ...item, changed: false },
2755
+ text: `${item.key} is already in ${target.name}.`,
2756
+ ids: [item.key]
2757
+ };
2758
+ }
2759
+ const updated = await client.updateWorkItem(id, { stateId: target.id }, context.mutationOptions(item.version === undefined ? {} : { expectedVersion: item.version }));
2760
+ return {
2761
+ data: { ...updated, changed: true },
2762
+ text: verb === "close" ? `Closed ${updated.key}.` : `Moved ${updated.key} to ${target.name}.`,
2763
+ ids: [updated.key]
2764
+ };
443
2765
  }
2766
+ var closeWork = (context) => moveTo(context, str6(context.args.workItem), closingState, "close");
2767
+ var transitionWork = (context) => {
2768
+ const to = str6(context.flags.to);
2769
+ if (to === undefined)
2770
+ throw new UsageError("Pass --to with the state name or id.");
2771
+ return moveTo(context, str6(context.args.workItem), (states) => findState(states, to), "transition");
2772
+ };
2773
+ var deleteWork = async (context) => {
2774
+ const value = str6(context.args.workItem);
2775
+ const id = await workItemId(context, value);
2776
+ if (!await context.confirm(`Delete work item ${value}? This cannot be undone.`)) {
2777
+ throw new CliError("Cancelled.", "cancelled", EXIT.failure);
2778
+ }
2779
+ await (await context.client()).deleteWorkItem(id, context.mutationOptions());
2780
+ return { data: { id, deleted: true }, text: `Deleted ${value}.`, ids: [id] };
2781
+ };
444
2782
 
445
- // src/bin.ts
446
- var url = readEnv("URL");
447
- var key = readEnv("API_KEY");
448
- var argv = process.argv.slice(2);
449
- var needsConfig = argv[0] !== undefined && argv[0] !== "help" && argv[0] !== "doctor";
450
- if (needsConfig && (url === undefined || key === undefined)) {
451
- process.stderr.write(`Set YCADENCE_URL and YCADENCE_API_KEY first.
452
-
453
- ${USAGE}
2783
+ // src/tree.ts
2784
+ var workItemArg = {
2785
+ name: "workItem",
2786
+ required: true,
2787
+ description: "Work item id, or key such as DP-42 (a key needs --project)"
2788
+ };
2789
+ var importFlags = [
2790
+ {
2791
+ name: "source",
2792
+ type: "string",
2793
+ placeholder: "<csv|github|jira|linear>",
2794
+ description: "What the file was exported from (default csv)"
2795
+ },
2796
+ { name: "file", type: "string", placeholder: "<path>", description: "The export to import" },
2797
+ {
2798
+ name: "repository",
2799
+ type: "string",
2800
+ placeholder: "<org/repo>",
2801
+ description: "GitHub repository the export came from"
2802
+ },
2803
+ {
2804
+ name: "base-url",
2805
+ type: "string",
2806
+ placeholder: "<url>",
2807
+ description: "Jira site the export came from"
2808
+ }
2809
+ ];
2810
+ var legacyProjectArg = {
2811
+ name: "projectId",
2812
+ description: "Project id (the older positional form of --project)"
2813
+ };
2814
+ var LOGIN_FLAGS = [
2815
+ { name: "url", type: "string", placeholder: "<url>", description: "The instance URL" },
2816
+ { name: "api-key-stdin", type: "boolean", description: "Read the API key from stdin" },
2817
+ { name: "device", type: "boolean", description: "Use the device flow (no local browser)" },
2818
+ {
2819
+ name: "no-browser",
2820
+ type: "boolean",
2821
+ description: "Print the sign-in URL instead of opening it"
2822
+ },
2823
+ {
2824
+ name: "oauth-client-id",
2825
+ type: "string",
2826
+ placeholder: "<id>",
2827
+ description: "Desktop OAuth client id (saved to the profile)"
2828
+ },
2829
+ {
2830
+ name: "iap-audience",
2831
+ type: "string",
2832
+ placeholder: "<id>",
2833
+ description: "IAP OAuth client id (saved to the profile)"
2834
+ },
2835
+ { name: "skip-oauth", type: "boolean", description: "Do not sign in for IAP" }
2836
+ ];
2837
+ var help = async (context) => {
2838
+ const words = context.args.command ?? [];
2839
+ let node = context.root;
2840
+ const path = [];
2841
+ for (const word of words) {
2842
+ const child = findChild(node, word);
2843
+ if (child === undefined)
2844
+ throw new UsageError(`Unknown command: ${words.join(" ")}.`);
2845
+ node = child;
2846
+ path.push(child.name);
2847
+ }
2848
+ return {
2849
+ data: { help: renderHelp(context.root, path, node) },
2850
+ text: renderHelp(context.root, path, node)
2851
+ };
2852
+ };
2853
+ var completion = async (context) => {
2854
+ const shell = context.args.shell;
2855
+ const script = completionScript(context.root, shell);
2856
+ return { data: { shell, script }, text: script.trimEnd() };
2857
+ };
2858
+ var version = async () => ({
2859
+ data: { version: CLI_VERSION },
2860
+ text: `ycadence ${CLI_VERSION}`
2861
+ });
2862
+ var TREE = {
2863
+ name: "ycadence",
2864
+ summary: "Project and work management from the command line",
2865
+ children: [
2866
+ {
2867
+ name: "work",
2868
+ summary: "Work items: list, read, create, update, close, move",
2869
+ children: [
2870
+ {
2871
+ name: "list",
2872
+ aliases: ["ls"],
2873
+ summary: "List work items in a project",
2874
+ flags: [
2875
+ {
2876
+ name: "state",
2877
+ type: "string",
2878
+ placeholder: "<name|id>",
2879
+ description: "Only items in this state"
2880
+ },
2881
+ WORK_FIELD_FLAGS.priority,
2882
+ {
2883
+ name: "assignee",
2884
+ type: "string",
2885
+ multiple: true,
2886
+ placeholder: "<user id>",
2887
+ description: "Only items assigned to this user"
2888
+ },
2889
+ {
2890
+ name: "label",
2891
+ type: "string",
2892
+ multiple: true,
2893
+ placeholder: "<name|id>",
2894
+ description: "Only items with this label"
2895
+ },
2896
+ {
2897
+ name: "filter",
2898
+ type: "string",
2899
+ placeholder: "<json>",
2900
+ description: "A filter expression in the query language, passed through"
2901
+ }
2902
+ ],
2903
+ examples: [
2904
+ "ycadence work list --project <projectId>",
2905
+ 'ycadence work list --state "In Progress" --priority high --format json',
2906
+ "ycadence work list --all --quiet"
2907
+ ],
2908
+ run: listWork
2909
+ },
2910
+ {
2911
+ name: "get",
2912
+ aliases: ["show"],
2913
+ summary: "Read one work item",
2914
+ args: [workItemArg],
2915
+ run: getWork
2916
+ },
2917
+ {
2918
+ name: "create",
2919
+ summary: "Create a work item",
2920
+ mutates: true,
2921
+ flags: [
2922
+ { ...WORK_FIELD_FLAGS.title, required: true },
2923
+ WORK_FIELD_FLAGS.description,
2924
+ WORK_FIELD_FLAGS.descriptionFile,
2925
+ WORK_FIELD_FLAGS.priority,
2926
+ WORK_FIELD_FLAGS.state,
2927
+ {
2928
+ name: "assignee",
2929
+ type: "string",
2930
+ multiple: true,
2931
+ placeholder: "<user id>",
2932
+ description: "Assign a user, by id"
2933
+ },
2934
+ {
2935
+ name: "label",
2936
+ type: "string",
2937
+ multiple: true,
2938
+ placeholder: "<name|id>",
2939
+ description: "Add a label, by name or id"
2940
+ },
2941
+ WORK_FIELD_FLAGS.parent,
2942
+ WORK_FIELD_FLAGS.startDate,
2943
+ WORK_FIELD_FLAGS.targetDate
2944
+ ],
2945
+ examples: [
2946
+ 'ycadence work create --project <projectId> --title "Fix login redirect" --priority high',
2947
+ 'ycadence work create --title "Spike" --description-file notes.md --idempotency-key spike-1'
2948
+ ],
2949
+ run: createWork
2950
+ },
2951
+ {
2952
+ name: "update",
2953
+ summary: "Change a work item",
2954
+ mutates: true,
2955
+ args: [workItemArg],
2956
+ flags: [
2957
+ WORK_FIELD_FLAGS.title,
2958
+ WORK_FIELD_FLAGS.description,
2959
+ WORK_FIELD_FLAGS.descriptionFile,
2960
+ WORK_FIELD_FLAGS.priority,
2961
+ WORK_FIELD_FLAGS.state,
2962
+ WORK_FIELD_FLAGS.parent,
2963
+ { name: "no-parent", type: "boolean", description: "Remove the parent" },
2964
+ WORK_FIELD_FLAGS.startDate,
2965
+ WORK_FIELD_FLAGS.targetDate,
2966
+ {
2967
+ name: "expect-version",
2968
+ type: "number",
2969
+ placeholder: "<n>",
2970
+ description: "Fail if the item changed since this version (sent as If-Match)"
2971
+ }
2972
+ ],
2973
+ run: updateWork
2974
+ },
2975
+ {
2976
+ name: "close",
2977
+ summary: "Move a work item to the project's first completed state",
2978
+ description: "The project comes from the work item; --project is only needed against a server older than Slice 2A.",
2979
+ mutates: true,
2980
+ args: [workItemArg],
2981
+ run: closeWork
2982
+ },
2983
+ {
2984
+ name: "transition",
2985
+ aliases: ["move"],
2986
+ summary: "Move a work item to a state",
2987
+ mutates: true,
2988
+ args: [workItemArg],
2989
+ flags: [
2990
+ {
2991
+ name: "to",
2992
+ type: "string",
2993
+ required: true,
2994
+ placeholder: "<state name|id>",
2995
+ description: "The state to move to"
2996
+ }
2997
+ ],
2998
+ examples: ['ycadence work transition DP-42 --to "In Review" --project <projectId>'],
2999
+ run: transitionWork
3000
+ },
3001
+ {
3002
+ name: "delete",
3003
+ summary: "Delete a work item",
3004
+ mutates: true,
3005
+ destructive: true,
3006
+ args: [workItemArg],
3007
+ run: deleteWork
3008
+ }
3009
+ ]
3010
+ },
3011
+ {
3012
+ name: "states",
3013
+ aliases: ["state"],
3014
+ summary: "A project's workflow states",
3015
+ args: [legacyProjectArg],
3016
+ run: listStatesHandler,
3017
+ children: [
3018
+ {
3019
+ name: "list",
3020
+ aliases: ["ls"],
3021
+ summary: "List a project's states, in order",
3022
+ run: listStatesHandler
3023
+ }
3024
+ ]
3025
+ },
3026
+ {
3027
+ name: "labels",
3028
+ aliases: ["label"],
3029
+ summary: "A project's labels",
3030
+ children: [
3031
+ {
3032
+ name: "list",
3033
+ aliases: ["ls"],
3034
+ summary: "List a project's labels",
3035
+ run: listLabelsHandler
3036
+ },
3037
+ {
3038
+ name: "create",
3039
+ summary: "Create a label",
3040
+ mutates: true,
3041
+ flags: [
3042
+ {
3043
+ name: "name",
3044
+ type: "string",
3045
+ required: true,
3046
+ placeholder: "<name>",
3047
+ description: "Label name"
3048
+ },
3049
+ {
3050
+ name: "color",
3051
+ type: "string",
3052
+ required: true,
3053
+ placeholder: "<token|css colour>",
3054
+ description: "A theme token or CSS colour"
3055
+ },
3056
+ {
3057
+ name: "description",
3058
+ type: "string",
3059
+ placeholder: "<text>",
3060
+ description: "Description"
3061
+ }
3062
+ ],
3063
+ run: createLabelHandler
3064
+ }
3065
+ ]
3066
+ },
3067
+ {
3068
+ name: "cycles",
3069
+ aliases: ["cycle", "sprint"],
3070
+ summary: "A project's cycles (sprints)",
3071
+ args: [legacyProjectArg],
3072
+ run: listCyclesHandler,
3073
+ children: [
3074
+ {
3075
+ name: "list",
3076
+ aliases: ["ls"],
3077
+ summary: "List a project's cycles",
3078
+ run: listCyclesHandler
3079
+ },
3080
+ {
3081
+ name: "show",
3082
+ summary: "Progress and burndown for a cycle",
3083
+ args: [{ name: "cycle", required: true, description: "Cycle id" }],
3084
+ run: showCycleHandler
3085
+ },
3086
+ {
3087
+ name: "create",
3088
+ summary: "Create a cycle",
3089
+ mutates: true,
3090
+ flags: [
3091
+ {
3092
+ name: "name",
3093
+ type: "string",
3094
+ required: true,
3095
+ placeholder: "<name>",
3096
+ description: "Cycle name"
3097
+ },
3098
+ {
3099
+ name: "description",
3100
+ type: "string",
3101
+ placeholder: "<text>",
3102
+ description: "Description"
3103
+ },
3104
+ WORK_FIELD_FLAGS.startDate,
3105
+ {
3106
+ name: "end-date",
3107
+ type: "string",
3108
+ placeholder: "<YYYY-MM-DD>",
3109
+ description: "End date"
3110
+ }
3111
+ ],
3112
+ run: createCycleHandler
3113
+ },
3114
+ {
3115
+ name: "add",
3116
+ summary: "Add work items to a cycle, moving them out of any other",
3117
+ mutates: true,
3118
+ args: [
3119
+ { name: "cycle", required: true, description: "Cycle id" },
3120
+ {
3121
+ name: "workItems",
3122
+ required: true,
3123
+ variadic: true,
3124
+ description: "Work item ids or keys"
3125
+ }
3126
+ ],
3127
+ run: addToCycleHandler
3128
+ }
3129
+ ]
3130
+ },
3131
+ {
3132
+ name: "views",
3133
+ aliases: ["view"],
3134
+ summary: "Saved views",
3135
+ children: [
3136
+ {
3137
+ name: "list",
3138
+ aliases: ["ls"],
3139
+ summary: "List the views you can see",
3140
+ run: listViewsHandler
3141
+ },
3142
+ {
3143
+ name: "create",
3144
+ summary: "Save a view",
3145
+ mutates: true,
3146
+ flags: [
3147
+ {
3148
+ name: "name",
3149
+ type: "string",
3150
+ required: true,
3151
+ placeholder: "<name>",
3152
+ description: "View name"
3153
+ },
3154
+ {
3155
+ name: "description",
3156
+ type: "string",
3157
+ placeholder: "<text>",
3158
+ description: "Description"
3159
+ },
3160
+ {
3161
+ name: "filter",
3162
+ type: "string",
3163
+ placeholder: "<json>",
3164
+ description: "Filter expression"
3165
+ },
3166
+ {
3167
+ name: "access",
3168
+ type: "string",
3169
+ values: ["private", "public"],
3170
+ placeholder: "<private|public>",
3171
+ description: "Who can see it (default private)"
3172
+ }
3173
+ ],
3174
+ run: createViewHandler
3175
+ }
3176
+ ]
3177
+ },
3178
+ {
3179
+ name: "types",
3180
+ aliases: ["type"],
3181
+ summary: "Work item types",
3182
+ children: [
3183
+ {
3184
+ name: "list",
3185
+ aliases: ["ls"],
3186
+ summary: "Work item types and their properties",
3187
+ run: listTypesHandler
3188
+ }
3189
+ ]
3190
+ },
3191
+ {
3192
+ name: "intake",
3193
+ summary: "The triage queue",
3194
+ children: [
3195
+ {
3196
+ name: "list",
3197
+ aliases: ["ls"],
3198
+ summary: "Pending submissions, and snoozed ones that are due",
3199
+ flags: [
3200
+ {
3201
+ name: "include-decided",
3202
+ type: "boolean",
3203
+ description: "Include decided submissions"
3204
+ }
3205
+ ],
3206
+ run: listIntakeHandler
3207
+ },
3208
+ {
3209
+ name: "submit",
3210
+ summary: "Submit a request for triage",
3211
+ mutates: true,
3212
+ flags: [
3213
+ { ...WORK_FIELD_FLAGS.title, required: true },
3214
+ WORK_FIELD_FLAGS.description,
3215
+ WORK_FIELD_FLAGS.descriptionFile,
3216
+ {
3217
+ name: "email",
3218
+ type: "string",
3219
+ placeholder: "<email>",
3220
+ description: "Requester email"
3221
+ }
3222
+ ],
3223
+ run: submitIntakeHandler
3224
+ },
3225
+ {
3226
+ name: "triage",
3227
+ summary: "Accept, decline, snooze or mark a submission as a duplicate",
3228
+ mutates: true,
3229
+ args: [{ name: "intakeItem", required: true, description: "Intake item id" }],
3230
+ flags: [
3231
+ {
3232
+ name: "decision",
3233
+ type: "string",
3234
+ required: true,
3235
+ values: TRIAGE_DECISIONS,
3236
+ placeholder: "<decision>",
3237
+ description: `One of ${TRIAGE_DECISIONS.join(", ")}`
3238
+ },
3239
+ {
3240
+ name: "until",
3241
+ type: "string",
3242
+ placeholder: "<date>",
3243
+ description: "Snooze until (with snoozed)"
3244
+ },
3245
+ {
3246
+ name: "duplicate-of",
3247
+ type: "string",
3248
+ placeholder: "<work item>",
3249
+ description: "The item it duplicates (with duplicate)"
3250
+ }
3251
+ ],
3252
+ run: triageIntakeHandler
3253
+ }
3254
+ ]
3255
+ },
3256
+ {
3257
+ name: "import",
3258
+ summary: "Import from CSV, GitHub, Jira or Linear exports",
3259
+ description: "`preview` and `run` are separate commands, so the safe one is the one you can type by accident.",
3260
+ children: [
3261
+ {
3262
+ name: "preview",
3263
+ summary: "Show what an import would do, without importing",
3264
+ args: [legacyProjectArg],
3265
+ flags: importFlags,
3266
+ examples: ["ycadence import preview --project <projectId> --file issues.csv"],
3267
+ run: previewImportHandler
3268
+ },
3269
+ {
3270
+ name: "run",
3271
+ summary: "Start an import",
3272
+ mutates: true,
3273
+ args: [legacyProjectArg],
3274
+ flags: [
3275
+ ...importFlags,
3276
+ {
3277
+ name: "map",
3278
+ type: "string",
3279
+ multiple: true,
3280
+ placeholder: "<State=stateId>",
3281
+ description: "Map a source state to a state id"
3282
+ }
3283
+ ],
3284
+ run: runImportHandler
3285
+ },
3286
+ {
3287
+ name: "status",
3288
+ summary: "Where an import got to",
3289
+ args: [{ name: "runId", required: true, description: "The run id `import run` printed" }],
3290
+ run: importStatusHandler
3291
+ },
3292
+ {
3293
+ name: "list",
3294
+ aliases: ["ls"],
3295
+ summary: "A project's imports",
3296
+ args: [legacyProjectArg],
3297
+ run: listImportsHandler
3298
+ }
3299
+ ]
3300
+ },
3301
+ {
3302
+ name: "login",
3303
+ summary: "Save a profile, sign in for IAP, and store an API key",
3304
+ description: "Signs in with Google (loopback, or --device) against the configured Desktop OAuth client for an IAP identity token, when one is configured. The API key is read from stdin and kept in the OS keychain.",
3305
+ needsClient: false,
3306
+ flags: LOGIN_FLAGS,
3307
+ examples: [
3308
+ "ycadence login --url https://ycadence.example.com --api-key-stdin < key.txt",
3309
+ "ycadence login --profile yarlis --oauth-client-id <desktop client id> --iap-audience <iap client id>"
3310
+ ],
3311
+ run: loginHandler
3312
+ },
3313
+ {
3314
+ name: "auth",
3315
+ summary: "Credentials for the current profile",
3316
+ children: [
3317
+ {
3318
+ name: "login",
3319
+ summary: "Same as `ycadence login`",
3320
+ needsClient: false,
3321
+ flags: LOGIN_FLAGS,
3322
+ run: loginHandler
3323
+ },
3324
+ {
3325
+ name: "status",
3326
+ summary: "Who the stored credential is",
3327
+ run: statusHandler,
3328
+ needsClient: false
3329
+ },
3330
+ {
3331
+ name: "logout",
3332
+ summary: "Remove the profile's stored credentials",
3333
+ run: logoutHandler,
3334
+ needsClient: false
3335
+ },
3336
+ {
3337
+ name: "token",
3338
+ summary: "Print a fresh IAP identity token, for other tools",
3339
+ needsClient: false,
3340
+ flags: [
3341
+ {
3342
+ name: "identity",
3343
+ type: "boolean",
3344
+ description: "The IAP identity token (the default)"
3345
+ }
3346
+ ],
3347
+ run: tokenHandler
3348
+ }
3349
+ ]
3350
+ },
3351
+ {
3352
+ name: "config",
3353
+ summary: "Profiles and settings",
3354
+ children: [
3355
+ {
3356
+ name: "list",
3357
+ aliases: ["ls"],
3358
+ summary: "Settings for the current profile, and where each comes from",
3359
+ run: configListHandler,
3360
+ needsClient: false
3361
+ },
3362
+ {
3363
+ name: "get",
3364
+ summary: "Read a setting",
3365
+ args: [{ name: "key", required: true, description: "Setting name" }],
3366
+ run: configGetHandler,
3367
+ needsClient: false
3368
+ },
3369
+ {
3370
+ name: "set",
3371
+ summary: "Change a setting",
3372
+ args: [
3373
+ {
3374
+ name: "key",
3375
+ required: true,
3376
+ description: "url, org, project, format, oauthClientId or iapAudience"
3377
+ },
3378
+ { name: "value", required: true, description: "The value" }
3379
+ ],
3380
+ run: configSetHandler,
3381
+ needsClient: false
3382
+ },
3383
+ {
3384
+ name: "unset",
3385
+ summary: "Remove a setting",
3386
+ args: [{ name: "key", required: true, description: "Setting name" }],
3387
+ run: configUnsetHandler,
3388
+ needsClient: false
3389
+ },
3390
+ {
3391
+ name: "profiles",
3392
+ summary: "List profiles",
3393
+ run: configProfilesHandler,
3394
+ needsClient: false
3395
+ },
3396
+ {
3397
+ name: "use",
3398
+ summary: "Switch the default profile",
3399
+ args: [{ name: "profile", required: true, description: "Profile name" }],
3400
+ run: configUseHandler,
3401
+ needsClient: false
3402
+ },
3403
+ {
3404
+ name: "path",
3405
+ summary: "Where the config file is",
3406
+ run: configPathHandler,
3407
+ needsClient: false
3408
+ }
3409
+ ]
3410
+ },
3411
+ {
3412
+ name: "doctor",
3413
+ summary: "Check configuration, credentials and connectivity",
3414
+ needsClient: false,
3415
+ run: doctorHandler
3416
+ },
3417
+ {
3418
+ name: "completion",
3419
+ summary: "Print a shell completion script",
3420
+ description: "For a short name, also add `alias yc=ycadence` to your shell.",
3421
+ needsClient: false,
3422
+ args: [{ name: "shell", required: true, description: `One of ${SHELLS.join(", ")}` }],
3423
+ examples: [
3424
+ "source <(ycadence completion bash)",
3425
+ "source <(ycadence completion zsh)",
3426
+ "ycadence completion fish > ~/.config/fish/completions/ycadence.fish"
3427
+ ],
3428
+ run: async (context) => {
3429
+ if (!SHELLS.includes(context.args.shell)) {
3430
+ throw new UsageError(`Shell is one of ${SHELLS.join(", ")}.`);
3431
+ }
3432
+ return completion(context);
3433
+ }
3434
+ },
3435
+ {
3436
+ name: "help",
3437
+ summary: "Help for a command",
3438
+ needsClient: false,
3439
+ args: [{ name: "command", variadic: true, description: "The command, e.g. `work create`" }],
3440
+ run: help
3441
+ },
3442
+ { name: "version", summary: "Print the version", needsClient: false, run: version },
3443
+ {
3444
+ name: "work-item",
3445
+ summary: "Old spelling of `work`",
3446
+ hidden: true,
3447
+ deprecatedFor: "ycadence work",
3448
+ children: [
3449
+ {
3450
+ name: "list",
3451
+ summary: "Old spelling of `work list`",
3452
+ deprecatedFor: "ycadence work list --project <projectId>",
3453
+ args: [legacyProjectArg],
3454
+ run: listWork
3455
+ },
3456
+ {
3457
+ name: "create",
3458
+ summary: "Old spelling of `work create`",
3459
+ deprecatedFor: "ycadence work create --project <projectId> --title …",
3460
+ mutates: true,
3461
+ args: [legacyProjectArg],
3462
+ flags: [WORK_FIELD_FLAGS.title, WORK_FIELD_FLAGS.description, WORK_FIELD_FLAGS.priority],
3463
+ run: createWork
3464
+ },
3465
+ {
3466
+ name: "show",
3467
+ summary: "Old spelling of `work get`",
3468
+ deprecatedFor: "ycadence work get <workItem>",
3469
+ args: [workItemArg],
3470
+ run: getWork
3471
+ },
3472
+ {
3473
+ name: "close",
3474
+ summary: "Old spelling of `work close`",
3475
+ deprecatedFor: "ycadence work close <workItem>",
3476
+ mutates: true,
3477
+ args: [workItemArg],
3478
+ run: closeWork
3479
+ }
3480
+ ]
3481
+ }
3482
+ ]
3483
+ };
3484
+
3485
+ // src/runner.ts
3486
+ var IDENTIFIER = /^[A-Z][A-Z0-9_]{0,15}$/;
3487
+ function lazySecretStore(create) {
3488
+ let store;
3489
+ let pending;
3490
+ const ready = async () => {
3491
+ if (store !== undefined)
3492
+ return store;
3493
+ pending ??= create();
3494
+ store = await pending;
3495
+ return store;
3496
+ };
3497
+ return {
3498
+ get kind() {
3499
+ return store?.kind ?? "memory";
3500
+ },
3501
+ get description() {
3502
+ return store?.description ?? "not yet opened";
3503
+ },
3504
+ get: async (account) => (await ready()).get(account),
3505
+ set: async (account, value) => (await ready()).set(account, value),
3506
+ delete: async (account) => (await ready()).delete(account)
3507
+ };
3508
+ }
3509
+ function redactHeaders(headers) {
3510
+ const result = {};
3511
+ for (const [name, value] of Object.entries(headers ?? {})) {
3512
+ const lower = name.toLowerCase();
3513
+ result[name] = lower === "x-api-key" || lower === "authorization" || lower === "cookie" ? "[redacted]" : value;
3514
+ }
3515
+ return result;
3516
+ }
3517
+ function dryRunFetch(inner) {
3518
+ return async (input, init) => {
3519
+ const method = (init?.method ?? "GET").toUpperCase();
3520
+ if (method === "GET" || method === "HEAD")
3521
+ return inner(input, init);
3522
+ let body;
3523
+ if (typeof init?.body === "string") {
3524
+ try {
3525
+ body = JSON.parse(init.body);
3526
+ } catch {
3527
+ body = init.body;
3528
+ }
3529
+ }
3530
+ throw new RequestIntercepted({
3531
+ method,
3532
+ url: String(input),
3533
+ headers: redactHeaders(init?.headers),
3534
+ body
3535
+ });
3536
+ };
3537
+ }
3538
+ function errorCode(error) {
3539
+ if (error instanceof YCadenceError)
3540
+ return error.code;
3541
+ if (error instanceof UsageError || error instanceof ConfigError || error instanceof CliError) {
3542
+ return error.code;
3543
+ }
3544
+ return "error";
3545
+ }
3546
+ async function run(argv, context) {
3547
+ const tree = context.tree ?? TREE;
3548
+ const stdout = [];
3549
+ const stderr = [];
3550
+ let globals = {
3551
+ format: "table",
3552
+ quiet: false,
3553
+ verbose: false
3554
+ };
3555
+ let requestId;
3556
+ let path = [];
3557
+ const finish = (exitCode) => {
3558
+ const out = stdout.join(`
454
3559
  `);
455
- process.exit(1);
456
- }
457
- var result = await run(argv, {
458
- client: new YCadenceClient({
459
- baseUrl: url ?? "http://localhost:3000",
460
- apiKey: key ?? "",
461
- identityToken: readEnv("IDENTITY_TOKEN")
462
- }),
3560
+ const err = stderr.join(`
3561
+ `);
3562
+ return { stdout: out, stderr: err, exitCode, output: exitCode === 0 ? out : err || out };
3563
+ };
3564
+ try {
3565
+ if (argv.includes("--json") || argv.includes("--format=json"))
3566
+ globals.format = "json";
3567
+ const formatAt = argv.indexOf("--format");
3568
+ if (formatAt !== -1 && argv[formatAt + 1] === "json")
3569
+ globals.format = "json";
3570
+ const parsed = parse(tree, argv);
3571
+ path = parsed.path;
3572
+ if (parsed.help || parsed.command.run === undefined) {
3573
+ const text = renderHelp(tree, parsed.path, parsed.command);
3574
+ if (parsed.help || parsed.path.length === 0) {
3575
+ stdout.push(text);
3576
+ return finish(EXIT.ok);
3577
+ }
3578
+ throw new UsageError(`"${commandName(parsed.path)}" needs a command.
3579
+
3580
+ ${text}`);
3581
+ }
3582
+ const directory = configDirectory(context.env);
3583
+ const configStore = context.configStore ?? (directory === undefined ? memoryConfigStore() : fileConfigStore(directory));
3584
+ const flagsForConfig = {
3585
+ profile: parsed.flags.profile,
3586
+ org: parsed.flags.org,
3587
+ project: parsed.flags.project
3588
+ };
3589
+ let config = resolveConfig(await configStore.load(), context.env, flagsForConfig);
3590
+ const options = globalOptions(parsed.flags, { format: config.format });
3591
+ globals = options;
3592
+ const secrets = context.secrets ?? (directory === undefined ? lazySecretStore(async () => detectSecretStore({ directory: undefined, env: { YCADENCE_CREDENTIAL_STORE: "file" } })) : lazySecretStore(() => detectSecretStore({ directory, env: context.env })));
3593
+ const notice = (message) => {
3594
+ if (options.quiet)
3595
+ return;
3596
+ if (context.notice === undefined)
3597
+ stderr.push(message);
3598
+ else
3599
+ context.notice(message);
3600
+ };
3601
+ const debug = (message) => {
3602
+ if (options.verbose)
3603
+ stderr.push(message);
3604
+ };
3605
+ if (parsed.command.deprecatedFor !== undefined) {
3606
+ notice(`"${commandName(parsed.path)}" is deprecated; use "${parsed.command.deprecatedFor}".`);
3607
+ }
3608
+ const isTTY = context.isTTY ?? false;
3609
+ const fetchImpl = context.fetch ?? ((input, init) => globalThis.fetch(input, init));
3610
+ const now = context.now ?? Date.now;
3611
+ const sleep = context.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3612
+ const onResponse = (info) => {
3613
+ if (info.requestId !== undefined)
3614
+ requestId = info.requestId;
3615
+ debug(`→ ${info.method} ${info.path} ${info.status} ${info.durationMs}ms` + `${info.requestId === undefined ? "" : ` request-id=${info.requestId}`}` + `${info.attempt > 0 ? ` (retry ${info.attempt})` : ""}${info.replayed ? " (replayed)" : ""}`);
3616
+ };
3617
+ let client;
3618
+ const getClient = async () => {
3619
+ if (client !== undefined)
3620
+ return client;
3621
+ let base = context.client;
3622
+ if (base === undefined) {
3623
+ const apiKey = readEnv("API_KEY", context.env) ?? await secrets.get(secretAccount.apiKey(config.profile));
3624
+ if (config.url === undefined || apiKey === undefined) {
3625
+ throw new ConfigError("Set YCADENCE_URL and YCADENCE_API_KEY first, or run `ycadence login`.");
3626
+ }
3627
+ debug(`profile "${config.profile}", ${config.url}, key ${apiKey.slice(0, 12)}…`);
3628
+ const envToken = readEnv("IDENTITY_TOKEN", context.env);
3629
+ base = new YCadenceClient({
3630
+ baseUrl: config.url,
3631
+ apiKey,
3632
+ identityToken: envToken ?? storedIdentityToken({ config, secrets, env: context.env, fetch: fetchImpl, now }),
3633
+ fetch: fetchImpl,
3634
+ sleep
3635
+ });
3636
+ }
3637
+ const inner = base.fetchImplementation;
3638
+ client = base.withOptions({
3639
+ userAgent: `ycadence-cli/${CLI_VERSION}`,
3640
+ onResponse,
3641
+ fetch: options.dryRun ? dryRunFetch(inner) : inner,
3642
+ ...options.retry === undefined ? {} : { retry: { retries: options.retry } }
3643
+ });
3644
+ return client;
3645
+ };
3646
+ const generatedKey = options.idempotencyKey === undefined && (options.retry ?? 0) > 0 && parsed.command.mutates === true ? randomUUID() : undefined;
3647
+ if (generatedKey !== undefined)
3648
+ debug(`Idempotency-Key ${generatedKey} (generated for --retry)`);
3649
+ const handlerContext = {
3650
+ root: tree,
3651
+ path: parsed.path,
3652
+ args: parsed.args,
3653
+ flags: parsed.flags,
3654
+ rest: parsed.rest,
3655
+ globals: options,
3656
+ env: context.env,
3657
+ get config() {
3658
+ return config;
3659
+ },
3660
+ configStore,
3661
+ secrets,
3662
+ isTTY,
3663
+ async reloadConfig() {
3664
+ config = resolveConfig(await configStore.load(), context.env, flagsForConfig);
3665
+ client = undefined;
3666
+ },
3667
+ client: getClient,
3668
+ async projectId(positional) {
3669
+ const value = positional ?? options.project ?? config.project;
3670
+ if (value === undefined || value === "") {
3671
+ throw new ConfigError("This command needs a project: pass --project <id>, or set a default with `ycadence config set project <id>`.");
3672
+ }
3673
+ if (isUuid(value) || !IDENTIFIER.test(value))
3674
+ return value;
3675
+ return resolveProjectId(await getClient(), value);
3676
+ },
3677
+ mutationOptions(extra = {}) {
3678
+ const key = options.idempotencyKey ?? generatedKey;
3679
+ return {
3680
+ ...extra,
3681
+ ...key === undefined ? {} : { idempotencyKey: key },
3682
+ ...options.retry === undefined ? {} : { retries: options.retry }
3683
+ };
3684
+ },
3685
+ readFile: context.readFile,
3686
+ readStdin: context.readStdin ?? (async () => {
3687
+ throw new UsageError("Nothing to read on stdin.");
3688
+ }),
3689
+ async confirm(question) {
3690
+ if (options.yes || options.dryRun)
3691
+ return true;
3692
+ if (!isTTY || context.confirm === undefined) {
3693
+ throw new UsageError(`"${commandName(parsed.path)}" needs --yes when not run in a terminal.`);
3694
+ }
3695
+ return context.confirm(question);
3696
+ },
3697
+ notice,
3698
+ debug,
3699
+ fetch: fetchImpl,
3700
+ openUrl: context.openUrl ?? (async () => false),
3701
+ now,
3702
+ sleep
3703
+ };
3704
+ try {
3705
+ const output = await parsed.command.run(handlerContext);
3706
+ const text = formatSuccess(output, options, requestId);
3707
+ if (text !== "")
3708
+ stdout.push(text);
3709
+ return finish(output.exitCode ?? EXIT.ok);
3710
+ } catch (error) {
3711
+ if (error instanceof RequestIntercepted) {
3712
+ stdout.push(formatDryRun(error.request, options));
3713
+ return finish(EXIT.ok);
3714
+ }
3715
+ throw error;
3716
+ }
3717
+ } catch (error) {
3718
+ const code = errorCode(error);
3719
+ let message = formatError(error, code, globals, requestId);
3720
+ if (error instanceof UsageError && globals.format === "table" && !message.includes("Usage:") && !message.includes(`
3721
+
3722
+ `)) {
3723
+ message += `
3724
+ Run \`${commandName(path)} --help\` for usage.`;
3725
+ }
3726
+ if (!(error instanceof YCadenceError || error instanceof UsageError || error instanceof ConfigError || error instanceof CliError)) {
3727
+ if (globals.verbose && error instanceof Error && error.stack)
3728
+ stderr.push(error.stack);
3729
+ }
3730
+ stderr.push(message);
3731
+ return finish(exitCodeFor(error));
3732
+ }
3733
+ }
3734
+
3735
+ // src/bin.ts
3736
+ async function readStdin() {
3737
+ if (process.stdin.isTTY)
3738
+ return "";
3739
+ const chunks = [];
3740
+ for await (const chunk of process.stdin)
3741
+ chunks.push(chunk);
3742
+ return Buffer.concat(chunks).toString("utf8");
3743
+ }
3744
+ function confirm(question) {
3745
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
3746
+ return new Promise((resolve) => {
3747
+ prompt.question(`${question} [y/N] `, (answer) => {
3748
+ prompt.close();
3749
+ resolve(/^y(es)?$/i.test(answer.trim()));
3750
+ });
3751
+ });
3752
+ }
3753
+ function openUrl(url) {
3754
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
3755
+ return new Promise((resolve) => {
3756
+ try {
3757
+ const child = spawn2(command, args, { stdio: "ignore", detached: true });
3758
+ child.on("error", () => resolve(false));
3759
+ child.on("spawn", () => {
3760
+ child.unref();
3761
+ resolve(true);
3762
+ });
3763
+ } catch {
3764
+ resolve(false);
3765
+ }
3766
+ });
3767
+ }
3768
+ var result = await run(process.argv.slice(2), {
463
3769
  env: process.env,
464
- readFile: (path) => readFile(path, "utf8")
3770
+ readFile: (path) => readFile3(path, "utf8"),
3771
+ readStdin,
3772
+ isTTY: process.stdin.isTTY === true && process.stderr.isTTY === true,
3773
+ confirm,
3774
+ openUrl,
3775
+ notice: (message) => process.stderr.write(`${message}
3776
+ `)
465
3777
  });
466
- var stream = result.exitCode === 0 ? process.stdout : process.stderr;
467
- stream.write(`${result.output}
3778
+ if (result.stdout !== "")
3779
+ process.stdout.write(`${result.stdout}
3780
+ `);
3781
+ if (result.stderr !== "")
3782
+ process.stderr.write(`${result.stderr}
468
3783
  `);
469
- process.exit(result.exitCode);
3784
+ process.exitCode = result.exitCode;