@gonovus-ai/spec-flow-cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +807 -2
  2. package/dist/authentication-command-topology.d.ts +7 -0
  3. package/dist/authentication-command-topology.js +9 -0
  4. package/dist/authentication-command-topology.js.map +1 -0
  5. package/dist/bin.d.ts +2 -0
  6. package/dist/bin.js +52 -0
  7. package/dist/bin.js.map +1 -0
  8. package/dist/cli.d.ts +16 -0
  9. package/dist/cli.js +3339 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/client-identity.d.ts +12 -0
  12. package/dist/client-identity.js +29 -0
  13. package/dist/client-identity.js.map +1 -0
  14. package/dist/credential-store.d.ts +20 -0
  15. package/dist/credential-store.js +146 -0
  16. package/dist/credential-store.js.map +1 -0
  17. package/dist/default-api-origin.d.ts +1 -0
  18. package/dist/default-api-origin.js +1 -0
  19. package/dist/default-api-origin.js.map +1 -0
  20. package/dist/github-account-commands.d.ts +25 -0
  21. package/dist/github-account-commands.js +117 -0
  22. package/dist/github-account-commands.js.map +1 -0
  23. package/dist/github-connection-commands.d.ts +4 -0
  24. package/dist/github-connection-commands.js +95 -0
  25. package/dist/github-connection-commands.js.map +1 -0
  26. package/dist/github-import-commands.d.ts +3 -0
  27. package/dist/github-import-commands.js +59 -0
  28. package/dist/github-import-commands.js.map +1 -0
  29. package/dist/http-origin.d.ts +13 -0
  30. package/dist/http-origin.js +39 -0
  31. package/dist/http-origin.js.map +1 -0
  32. package/dist/index.d.ts +22 -0
  33. package/dist/index.js +238 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/node-version.d.ts +1 -0
  36. package/dist/node-version.js +8 -0
  37. package/dist/node-version.js.map +1 -0
  38. package/dist/safe-cli-error.d.ts +2 -0
  39. package/dist/safe-cli-error.js +3 -0
  40. package/dist/safe-cli-error.js.map +1 -0
  41. package/dist/user-agent.d.ts +3 -0
  42. package/dist/user-agent.js +10 -0
  43. package/dist/user-agent.js.map +1 -0
  44. package/dist/user-api-key-commands.d.ts +29 -0
  45. package/dist/user-api-key-commands.js +128 -0
  46. package/dist/user-api-key-commands.js.map +1 -0
  47. package/dist/work-item-asset-commands.d.ts +24 -0
  48. package/dist/work-item-asset-commands.js +211 -0
  49. package/dist/work-item-asset-commands.js.map +1 -0
  50. package/dist/work-item-exchange-file.d.ts +4 -0
  51. package/dist/work-item-exchange-file.js +66 -0
  52. package/dist/work-item-exchange-file.js.map +1 -0
  53. package/dist/work-item-resource-link-commands.d.ts +23 -0
  54. package/dist/work-item-resource-link-commands.js +85 -0
  55. package/dist/work-item-resource-link-commands.js.map +1 -0
  56. package/dist/work-item-time-entry-input.d.ts +4 -0
  57. package/dist/work-item-time-entry-input.js +27 -0
  58. package/dist/work-item-time-entry-input.js.map +1 -0
  59. package/package.json +53 -3
package/dist/cli.js ADDED
@@ -0,0 +1,3339 @@
1
+ import { Command, CommanderError, InvalidArgumentError } from "commander";
2
+ import { randomUUID } from "node:crypto";
3
+ import createClient from "openapi-fetch";
4
+ import { z } from "zod";
5
+ import { addAuthenticationCommandTopology } from "./authentication-command-topology.js";
6
+ import { registerGitHubAccountCommands } from "./github-account-commands.js";
7
+ import { parseHttpOrigin, unresolvedSpecFlowApiOrigin, } from "./http-origin.js";
8
+ import { specFlowCliUserAgent } from "./user-agent.js";
9
+ import { registerUserApiKeyCommands } from "./user-api-key-commands.js";
10
+ import { registerWorkItemAssetCommands } from "./work-item-asset-commands.js";
11
+ import { readWorkItemImportFile } from "./work-item-exchange-file.js";
12
+ import { registerWorkItemResourceLinkCommands } from "./work-item-resource-link-commands.js";
13
+ import { parseTimeEntryTimestamp, parseTimeEntryUuid, parseTimeEntryDescription } from "./work-item-time-entry-input.js";
14
+ export function formatResourceDiagnostic(message) {
15
+ return `${JSON.stringify({ diagnostic: { level: "warning", message } })}\n`;
16
+ }
17
+ const workItemBulkActions = [
18
+ "updateStatus",
19
+ "updatePriority",
20
+ "updateAssignee",
21
+ "delete",
22
+ "addLabel",
23
+ "removeLabel",
24
+ "updateDueDate",
25
+ ];
26
+ const glossaryEntryInputSchema = z
27
+ .object({
28
+ canonicalTerm: z.string().trim().min(1).max(200),
29
+ definition: z.string().trim().min(1).max(4_000),
30
+ id: z.string().trim().min(1).max(200),
31
+ usageNote: z.string().trim().min(1).max(2_000).nullable(),
32
+ })
33
+ .strict();
34
+ const glossaryEntriesInputSchema = z.array(glossaryEntryInputSchema).max(500);
35
+ const sourceReferenceInputSchema = z
36
+ .object({
37
+ externalRevision: z.string().trim().min(1).max(200).optional(),
38
+ section: z.string().trim().min(1).max(500).optional(),
39
+ title: z.string().trim().min(1).max(300),
40
+ url: z.url().max(2_000).refine((value) => {
41
+ const url = new URL(value);
42
+ return ((url.protocol === "https:" || url.protocol === "http:") &&
43
+ url.username.length === 0 &&
44
+ url.password.length === 0);
45
+ }),
46
+ })
47
+ .strict();
48
+ const sourceReferencesInputSchema = z.array(sourceReferenceInputSchema).max(20);
49
+ const roleStatementsInputSchema = z.record(z.string().trim().min(1).max(200), z.array(z.string().trim().min(1).max(200)).min(1).max(50));
50
+ const findingReferenceSchema = z
51
+ .object({
52
+ kind: z.enum(["SharedPrecondition", "ExampleTable", "Title", "Rule", "Scenario", "Line"]),
53
+ text: z.string().trim().min(1).max(20_000),
54
+ })
55
+ .strict();
56
+ const findingTargetSchema = z
57
+ .object({
58
+ references: z.array(findingReferenceSchema).min(1).max(50),
59
+ specId: z.string().trim().toUpperCase().regex(/^[A-Z]{2,10}-[1-9]\d*$/u),
60
+ })
61
+ .strict();
62
+ const findingEvidenceSchema = z.discriminatedUnion("kind", [
63
+ z
64
+ .object({
65
+ incompatibleClaims: z.tuple([
66
+ z.string().trim().min(1).max(4_000),
67
+ z.string().trim().min(1).max(4_000),
68
+ ]),
69
+ kind: z.literal("Contradiction"),
70
+ })
71
+ .strict(),
72
+ z
73
+ .object({
74
+ incompatibleRequirements: z.tuple([
75
+ z.string().trim().min(1).max(4_000),
76
+ z.string().trim().min(1).max(4_000),
77
+ ]),
78
+ kind: z.literal("Conflict"),
79
+ })
80
+ .strict(),
81
+ z
82
+ .object({
83
+ duplicatedBehavior: z.string().trim().min(1).max(4_000),
84
+ kind: z.literal("Duplicate"),
85
+ })
86
+ .strict(),
87
+ z
88
+ .object({
89
+ interpretations: z.tuple([
90
+ z
91
+ .object({
92
+ behavior: z.string().trim().min(1).max(4_000),
93
+ meaning: z.string().trim().min(1).max(4_000),
94
+ })
95
+ .strict(),
96
+ z
97
+ .object({
98
+ behavior: z.string().trim().min(1).max(4_000),
99
+ meaning: z.string().trim().min(1).max(4_000),
100
+ })
101
+ .strict(),
102
+ ]),
103
+ kind: z.literal("Ambiguity"),
104
+ })
105
+ .strict(),
106
+ z
107
+ .object({
108
+ glossaryEntryIds: z.array(z.string().trim().min(1).max(200)).max(20),
109
+ issue: z.enum(["MissingDefinition", "GlossaryConflict", "MultipleMeanings"]),
110
+ kind: z.literal("Terminology"),
111
+ term: z.string().trim().min(1).max(200),
112
+ })
113
+ .strict(),
114
+ z
115
+ .object({
116
+ kind: z.literal("Unverifiable"),
117
+ missingCriterion: z.string().trim().min(1).max(4_000),
118
+ })
119
+ .strict(),
120
+ ]);
121
+ const findingCreateInputSchema = z
122
+ .object({
123
+ affectedScenarios: z.array(z.string().trim().min(1).max(300)).max(50),
124
+ conclusion: z.string().trim().min(1).max(4_000),
125
+ evidence: findingEvidenceSchema,
126
+ explanation: z.string().trim().min(1).max(8_000),
127
+ targets: z.array(findingTargetSchema).min(1).max(10),
128
+ })
129
+ .strict();
130
+ export async function runCli(arguments_, dependencies) {
131
+ let exitCode = 0;
132
+ const program = new Command()
133
+ .name("specflow")
134
+ .description("Read and govern SpecFlow through supported commands")
135
+ .option("--allow-file-fallback", "use a warned owner-only local credential file if the system store is unavailable")
136
+ .option("--server <url>", "SpecFlow server origin")
137
+ .option("--api-key-id <id>", "Use an origin-scoped User API Key from the credential store")
138
+ .exitOverride()
139
+ .configureOutput({
140
+ writeErr: () => undefined,
141
+ writeOut: dependencies.stdout,
142
+ });
143
+ const run = (request, options = {}) => executeRequest(dependencies, request, options, program.getOptionValue("server"), program.getOptionValue("apiKeyId"));
144
+ registerUserApiKeyCommands(program, { dependencies, run, setExitCode: (value) => { exitCode = value; }, parseScopes: parseRoleStatements, parseLimit: parseSearchLimit });
145
+ registerGitHubAccountCommands(program, { dependencies, run, setExitCode: (value) => { exitCode = value; } });
146
+ const authentication = addAuthenticationCommandTopology(program);
147
+ program.hook("preAction", (_command, actionCommand) => {
148
+ const options = actionCommand.opts();
149
+ if (typeof options.projectId === "string") {
150
+ actionCommand.setOptionValue("projectId", parseProjectId(options.projectId));
151
+ }
152
+ if (typeof options.specId === "string") {
153
+ actionCommand.setOptionValue("specId", parseSpecId(options.specId));
154
+ }
155
+ if (typeof options.workItemId === "string") {
156
+ actionCommand.setOptionValue("workItemId", parseWorkItemId(options.workItemId));
157
+ }
158
+ });
159
+ for (const command of [
160
+ authentication.login,
161
+ authentication.logout,
162
+ authentication.status,
163
+ ]) {
164
+ command.action(() => {
165
+ throw new CommanderError(2, "specflow.authenticationCommand", "Authentication commands use the human-facing CLI boundary");
166
+ });
167
+ }
168
+ const workspace = program.command("workspace");
169
+ workspace
170
+ .command("list")
171
+ .action(async () => {
172
+ exitCode = await run((client) => client.GET("/v1/workspaces"), { rejectProjectReadToken: true });
173
+ });
174
+ workspace
175
+ .command("create")
176
+ .requiredOption("--name <name>")
177
+ .action(async (options) => {
178
+ if (hasProjectReadToken(dependencies.environment)) {
179
+ exitCode = rejectTokenWrite(dependencies);
180
+ return;
181
+ }
182
+ const idempotencyKey = randomUUID();
183
+ exitCode = await run((client) => client.POST("/v1/workspaces", {
184
+ body: { name: options.name },
185
+ params: { header: { "idempotency-key": idempotencyKey } },
186
+ }), { retryTransportOnce: true });
187
+ });
188
+ const workspaceMember = workspace.command("member");
189
+ workspaceMember
190
+ .command("list")
191
+ .requiredOption("--workspace-id <workspaceId>")
192
+ .action(async (options) => {
193
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/members", {
194
+ params: { path: { workspaceId: options.workspaceId } },
195
+ }), { rejectProjectReadToken: true });
196
+ });
197
+ workspaceMember
198
+ .command("search")
199
+ .requiredOption("--workspace-id <workspaceId>")
200
+ .requiredOption("--query <query>")
201
+ .option("--limit <limit>", "Maximum directory candidates", parseSearchLimit, 10)
202
+ .action(async (options) => {
203
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/members/directory", {
204
+ params: {
205
+ path: { workspaceId: options.workspaceId },
206
+ query: { limit: options.limit, query: options.query },
207
+ },
208
+ }), { rejectProjectReadToken: true });
209
+ });
210
+ workspaceMember
211
+ .command("add")
212
+ .requiredOption("--workspace-id <workspaceId>")
213
+ .requiredOption("--issuer <issuer>", "Authentik issuer URL from `workspace member search`")
214
+ .requiredOption("--subject <subject>", "Authentik subject from `workspace member search`")
215
+ .requiredOption("--role <role>", "Workspace Role name")
216
+ .action(async (options) => {
217
+ if (hasProjectReadToken(dependencies.environment)) {
218
+ exitCode = rejectTokenWrite(dependencies);
219
+ return;
220
+ }
221
+ const idempotencyKey = randomUUID();
222
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/members", {
223
+ body: {
224
+ issuer: options.issuer,
225
+ role: options.role,
226
+ subject: options.subject,
227
+ },
228
+ params: {
229
+ header: { "idempotency-key": idempotencyKey },
230
+ path: { workspaceId: options.workspaceId },
231
+ },
232
+ }), { retryTransportOnce: true });
233
+ });
234
+ workspaceMember
235
+ .command("set-role")
236
+ .requiredOption("--workspace-id <workspaceId>")
237
+ .requiredOption("--user-id <userId>", "Member User ID from `workspace member list`")
238
+ .requiredOption("--role <role>", "Workspace Role name")
239
+ .requiredOption("--expected-version <version>", "Opaque Member version from `workspace member list`", parseOpaqueVersion)
240
+ .action(async (options) => {
241
+ if (hasProjectReadToken(dependencies.environment)) {
242
+ exitCode = rejectTokenWrite(dependencies);
243
+ return;
244
+ }
245
+ const idempotencyKey = randomUUID();
246
+ exitCode = await run((client) => client.PATCH("/v1/workspaces/{workspaceId}/members/{userId}", {
247
+ body: { role: options.role },
248
+ params: {
249
+ header: {
250
+ "idempotency-key": idempotencyKey,
251
+ "if-match": options.expectedVersion,
252
+ },
253
+ path: {
254
+ userId: options.userId,
255
+ workspaceId: options.workspaceId,
256
+ },
257
+ },
258
+ }), {
259
+ ambiguousOutcomeMessage: "Member Role change outcome is unknown; read `workspace member list` before retrying",
260
+ retryTransportOnce: true,
261
+ });
262
+ });
263
+ workspaceMember
264
+ .command("remove")
265
+ .requiredOption("--workspace-id <workspaceId>")
266
+ .requiredOption("--user-id <userId>", "Member User ID from `workspace member list`")
267
+ .requiredOption("--expected-version <version>", "Opaque Member version from `workspace member list`", parseOpaqueVersion)
268
+ .action(async (options) => {
269
+ if (hasProjectReadToken(dependencies.environment)) {
270
+ exitCode = rejectTokenWrite(dependencies);
271
+ return;
272
+ }
273
+ const idempotencyKey = randomUUID();
274
+ exitCode = await run((client) => client.DELETE("/v1/workspaces/{workspaceId}/members/{userId}", {
275
+ params: {
276
+ header: {
277
+ "idempotency-key": idempotencyKey,
278
+ "if-match": options.expectedVersion,
279
+ },
280
+ path: {
281
+ userId: options.userId,
282
+ workspaceId: options.workspaceId,
283
+ },
284
+ },
285
+ }), {
286
+ ambiguousOutcomeMessage: "Member removal outcome is unknown; read `workspace member list` before retrying",
287
+ retryTransportOnce: true,
288
+ });
289
+ });
290
+ const workspaceRole = workspace.command("role");
291
+ workspaceRole
292
+ .command("list")
293
+ .requiredOption("--workspace-id <workspaceId>")
294
+ .action(async (options) => {
295
+ exitCode = await run((client) => client.GET("/v1/workspaces/{workspaceId}/roles", {
296
+ params: { path: { workspaceId: options.workspaceId } },
297
+ }), { rejectProjectReadToken: true });
298
+ });
299
+ workspaceRole
300
+ .command("create")
301
+ .requiredOption("--workspace-id <workspaceId>")
302
+ .requiredOption("--name <name>", "Custom Workspace Role name")
303
+ .requiredOption("--statements <statements>", "JSON object mapping permission resources to granted actions", parseRoleStatements)
304
+ .action(async (options) => {
305
+ if (hasProjectReadToken(dependencies.environment)) {
306
+ exitCode = rejectTokenWrite(dependencies);
307
+ return;
308
+ }
309
+ const idempotencyKey = randomUUID();
310
+ exitCode = await run((client) => client.POST("/v1/workspaces/{workspaceId}/roles", {
311
+ body: { name: options.name, statements: options.statements },
312
+ params: {
313
+ header: { "idempotency-key": idempotencyKey },
314
+ path: { workspaceId: options.workspaceId },
315
+ },
316
+ }), { retryTransportOnce: true });
317
+ });
318
+ workspaceRole
319
+ .command("update")
320
+ .requiredOption("--workspace-id <workspaceId>")
321
+ .requiredOption("--name <name>", "Workspace Role name from `workspace role list`")
322
+ .requiredOption("--statements <statements>", "JSON object mapping permission resources to granted actions", parseRoleStatements)
323
+ .requiredOption("--expected-version <version>", "Opaque Role version from `workspace role list`", parseOpaqueVersion)
324
+ .action(async (options) => {
325
+ if (hasProjectReadToken(dependencies.environment)) {
326
+ exitCode = rejectTokenWrite(dependencies);
327
+ return;
328
+ }
329
+ const idempotencyKey = randomUUID();
330
+ exitCode = await run((client) => client.PUT("/v1/workspaces/{workspaceId}/roles/{roleName}", {
331
+ body: { statements: options.statements },
332
+ params: {
333
+ header: {
334
+ "idempotency-key": idempotencyKey,
335
+ "if-match": options.expectedVersion,
336
+ },
337
+ path: {
338
+ roleName: options.name,
339
+ workspaceId: options.workspaceId,
340
+ },
341
+ },
342
+ }), {
343
+ ambiguousOutcomeMessage: "Role update outcome is unknown; read `workspace role list` before retrying",
344
+ retryTransportOnce: true,
345
+ });
346
+ });
347
+ workspaceRole
348
+ .command("delete")
349
+ .requiredOption("--workspace-id <workspaceId>")
350
+ .requiredOption("--name <name>", "Custom Workspace Role name")
351
+ .requiredOption("--expected-version <version>", "Opaque Role version from `workspace role list`", parseOpaqueVersion)
352
+ .action(async (options) => {
353
+ if (hasProjectReadToken(dependencies.environment)) {
354
+ exitCode = rejectTokenWrite(dependencies);
355
+ return;
356
+ }
357
+ const idempotencyKey = randomUUID();
358
+ exitCode = await run((client) => client.DELETE("/v1/workspaces/{workspaceId}/roles/{roleName}", {
359
+ params: {
360
+ header: {
361
+ "idempotency-key": idempotencyKey,
362
+ "if-match": options.expectedVersion,
363
+ },
364
+ path: {
365
+ roleName: options.name,
366
+ workspaceId: options.workspaceId,
367
+ },
368
+ },
369
+ }), {
370
+ ambiguousOutcomeMessage: "Role deletion outcome is unknown; read `workspace role list` before retrying",
371
+ retryTransportOnce: true,
372
+ });
373
+ });
374
+ const project = program.command("project");
375
+ project
376
+ .command("list")
377
+ .option("--status <status>", "Filter by Project lifecycle status", parseProjectStatus)
378
+ .option("--workspace-id <workspaceId>", "Filter by owning Workspace ID from `workspace list`")
379
+ .action(async (options) => {
380
+ exitCode = await run((client) => client.GET("/v1/projects", {
381
+ params: {
382
+ query: {
383
+ ...(options.status === undefined
384
+ ? {}
385
+ : { status: options.status }),
386
+ ...(options.workspaceId === undefined
387
+ ? {}
388
+ : { workspaceId: options.workspaceId }),
389
+ },
390
+ },
391
+ }), { rejectProjectReadToken: true });
392
+ });
393
+ project
394
+ .command("create")
395
+ .requiredOption("--name <name>")
396
+ .requiredOption("--project-id <projectId>", "Immutable Project ID (2-10 letters)", parseProjectId)
397
+ .requiredOption("--workspace-id <workspaceId>", "Workspace ID returned by `workspace list` or `workspace create`")
398
+ .option("--spec-language <language>", "Spec Language: en or zh-CN (default: en)")
399
+ .action(async (options) => {
400
+ if (hasProjectReadToken(dependencies.environment)) {
401
+ exitCode = rejectTokenWrite(dependencies);
402
+ return;
403
+ }
404
+ const idempotencyKey = randomUUID();
405
+ exitCode = await run((client) => client.POST("/v1/projects", {
406
+ body: {
407
+ name: options.name,
408
+ projectId: options.projectId,
409
+ workspaceId: options.workspaceId,
410
+ ...(options.specLanguage === undefined
411
+ ? {}
412
+ : { specLanguage: options.specLanguage }),
413
+ },
414
+ params: { header: { "idempotency-key": idempotencyKey } },
415
+ }), { retryTransportOnce: true });
416
+ });
417
+ for (const operation of ["archive", "restore", "trash"]) {
418
+ project
419
+ .command(operation)
420
+ .requiredOption("--project-id <projectId>")
421
+ .requiredOption("--expected-version <version>", "Opaque Project version", parseOpaqueVersion)
422
+ .action(async (options) => {
423
+ const idempotencyKey = randomUUID();
424
+ exitCode = await run((client) => client.POST(`/v1/projects/{projectId}/${operation}`, {
425
+ params: {
426
+ header: {
427
+ "idempotency-key": idempotencyKey,
428
+ "if-match": options.expectedVersion,
429
+ },
430
+ path: { projectId: options.projectId },
431
+ },
432
+ }), {
433
+ ambiguousOutcomeMessage: `Project ${operation} outcome is unknown; read Project context before retrying`,
434
+ rejectProjectReadToken: true,
435
+ retryTransportOnce: true,
436
+ });
437
+ });
438
+ }
439
+ project
440
+ .command("context")
441
+ .requiredOption("--project-id <projectId>")
442
+ .action(async (options) => {
443
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/context", {
444
+ params: { path: { projectId: options.projectId } },
445
+ }));
446
+ });
447
+ project
448
+ .command("directory")
449
+ .requiredOption("--project-id <projectId>")
450
+ .action(async (options) => {
451
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/spec-directory", {
452
+ params: { path: { projectId: options.projectId } },
453
+ }));
454
+ });
455
+ const sharing = project.command("sharing").description("Manage anonymous task-only Project sharing");
456
+ sharing.command("read")
457
+ .requiredOption("--project-id <projectId>")
458
+ .action(async (options) => {
459
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/sharing", {
460
+ params: { path: { projectId: options.projectId } },
461
+ }), { rejectProjectReadToken: true });
462
+ });
463
+ sharing.command("set")
464
+ .requiredOption("--project-id <projectId>")
465
+ .requiredOption("--public <boolean>", "true or false", (value) => {
466
+ if (value !== "true" && value !== "false")
467
+ throw new InvalidArgumentError("--public must be true or false");
468
+ return value === "true";
469
+ })
470
+ .requiredOption("--expected-version <version>", "Opaque sharing version from project sharing read", parseOpaqueVersion)
471
+ .requiredOption("--idempotency-key <key>", "UUID; reuse the same key and intent after an uncertain response", (value) => {
472
+ if (!z.uuid().safeParse(value).success)
473
+ throw new InvalidArgumentError("The sharing command key must be a UUID");
474
+ return value;
475
+ })
476
+ .action(async (options) => {
477
+ if (hasProjectReadToken(dependencies.environment)) {
478
+ exitCode = rejectTokenWrite(dependencies);
479
+ return;
480
+ }
481
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/sharing", {
482
+ body: { isPublic: options.public },
483
+ params: { path: { projectId: options.projectId }, header: { "if-match": options.expectedVersion, "idempotency-key": options.idempotencyKey } },
484
+ }), { rejectProjectReadToken: true, retryTransportOnce: true, ambiguousOutcomeMessage: "Sharing outcome is unknown; reread project sharing and retry the same command key and intent" });
485
+ });
486
+ const spec = program.command("spec");
487
+ spec
488
+ .command("read")
489
+ .requiredOption("--project-id <projectId>")
490
+ .requiredOption("--spec-id <specId>")
491
+ .action(async (options) => {
492
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/specs/{specId}", {
493
+ params: { path: options },
494
+ }), { rejectProjectReadToken: true });
495
+ });
496
+ spec
497
+ .command("changelog")
498
+ .requiredOption("--project-id <projectId>")
499
+ .requiredOption("--spec-id <specId>")
500
+ .action(async (options) => {
501
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/specs/{specId}/changelog", { params: { path: options } }), { rejectProjectReadToken: true });
502
+ });
503
+ spec
504
+ .command("search")
505
+ .requiredOption("--project-id <projectId>")
506
+ .requiredOption("--query <query>")
507
+ .option("--limit <limit>", "Maximum results", parseSearchLimit, 20)
508
+ .option("--cursor <cursor>")
509
+ .action(async (options) => {
510
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/spec-search", {
511
+ params: {
512
+ path: { projectId: options.projectId },
513
+ query: {
514
+ ...(options.cursor === undefined
515
+ ? {}
516
+ : { cursor: options.cursor }),
517
+ limit: options.limit,
518
+ query: options.query,
519
+ },
520
+ },
521
+ }));
522
+ });
523
+ spec
524
+ .command("current")
525
+ .requiredOption("--project-id <projectId>")
526
+ .requiredOption("--spec-id <specId>")
527
+ .action(async (options) => {
528
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/specs/{specId}/current-revision", { params: { path: options } }));
529
+ });
530
+ spec
531
+ .command("revision")
532
+ .requiredOption("--project-id <projectId>")
533
+ .requiredOption("--spec-id <specId>")
534
+ .requiredOption("--revision-id <revisionId>")
535
+ .action(async (options) => {
536
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/specs/{specId}/revisions/{revisionId}", { params: { path: options } }));
537
+ });
538
+ spec
539
+ .command("edit")
540
+ .requiredOption("--project-id <projectId>")
541
+ .requiredOption("--spec-id <specId>")
542
+ .requiredOption("--content <content>")
543
+ .option("--agent-revision <agentRevision>")
544
+ .option("--agent-model <agentModel>")
545
+ .option("--source-reference-ids <sourceReferenceIds>")
546
+ .action(async (options) => {
547
+ if (hasProjectReadToken(dependencies.environment)) {
548
+ exitCode = rejectTokenWrite(dependencies);
549
+ return;
550
+ }
551
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/specs/{specId}/draft", {
552
+ body: {
553
+ content: options.content,
554
+ provenance: toAgentProvenance(options),
555
+ },
556
+ params: { path: options },
557
+ }));
558
+ });
559
+ spec
560
+ .command("assign-scenario-ids")
561
+ .requiredOption("--project-id <projectId>")
562
+ .requiredOption("--spec-id <specId>")
563
+ .option("--agent-revision <agentRevision>")
564
+ .option("--agent-model <agentModel>")
565
+ .action(async (options) => {
566
+ if (hasProjectReadToken(dependencies.environment)) {
567
+ exitCode = rejectTokenWrite(dependencies);
568
+ return;
569
+ }
570
+ const idempotencyKey = randomUUID();
571
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/specs/{specId}/draft/scenario-ids", {
572
+ body: { provenance: toAgentProvenance(options) },
573
+ params: {
574
+ header: { "idempotency-key": idempotencyKey },
575
+ path: options,
576
+ },
577
+ }), {
578
+ ambiguousOutcomeMessage: "Scenario ID assignment outcome is unknown; inspect the Working Draft before retrying",
579
+ retryTransportOnce: true,
580
+ });
581
+ });
582
+ spec
583
+ .command("publish")
584
+ .requiredOption("--project-id <projectId>")
585
+ .requiredOption("--spec-id <specId>")
586
+ .action(async (options) => {
587
+ if (hasProjectReadToken(dependencies.environment)) {
588
+ exitCode = rejectTokenWrite(dependencies);
589
+ return;
590
+ }
591
+ const idempotencyKey = randomUUID();
592
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/specs/{specId}/publish", {
593
+ params: {
594
+ header: { "idempotency-key": idempotencyKey },
595
+ path: options,
596
+ },
597
+ }), {
598
+ ambiguousOutcomeMessage: "Spec Publish outcome is unknown; inspect the Spec Changelog before retrying",
599
+ retryTransportOnce: true,
600
+ });
601
+ });
602
+ spec
603
+ .command("create")
604
+ .requiredOption("--project-id <projectId>")
605
+ .option("--capability-id <capabilityId>")
606
+ .requiredOption("--feature-name <featureName>")
607
+ .option("--source-references <sourceReferences>")
608
+ .action(async (options) => {
609
+ if (hasProjectReadToken(dependencies.environment)) {
610
+ exitCode = rejectTokenWrite(dependencies);
611
+ return;
612
+ }
613
+ const idempotencyKey = randomUUID();
614
+ const sourceReferences = parseSourceReferences(options.sourceReferences ?? "[]");
615
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/specs", {
616
+ body: {
617
+ capabilityId: options.capabilityId ?? null,
618
+ featureName: options.featureName,
619
+ sourceReferences,
620
+ },
621
+ params: {
622
+ header: { "idempotency-key": idempotencyKey },
623
+ path: { projectId: options.projectId },
624
+ },
625
+ }), {
626
+ ambiguousOutcomeMessage: "Spec Creation outcome is unknown; inspect the Project directory before retrying",
627
+ retryTransportOnce: true,
628
+ });
629
+ });
630
+ spec
631
+ .command("update")
632
+ .requiredOption("--project-id <projectId>")
633
+ .requiredOption("--spec-id <specId>")
634
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
635
+ .option("--capability-id <capabilityId>")
636
+ .option("--source-references <sourceReferences>")
637
+ .action(async (options) => {
638
+ if (hasProjectReadToken(dependencies.environment)) {
639
+ exitCode = rejectTokenWrite(dependencies);
640
+ return;
641
+ }
642
+ if (options.capabilityId === undefined &&
643
+ options.sourceReferences === undefined) {
644
+ throw invalidArgument("Spec update requires Metadata to change");
645
+ }
646
+ const sourceReferences = options.sourceReferences === undefined
647
+ ? undefined
648
+ : parseSourceReferences(options.sourceReferences);
649
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/specs/{specId}", {
650
+ body: {
651
+ ...(options.capabilityId === undefined
652
+ ? {}
653
+ : { capabilityId: options.capabilityId === "none" ? null : options.capabilityId }),
654
+ ...(sourceReferences === undefined
655
+ ? {}
656
+ : { sourceReferences }),
657
+ },
658
+ params: {
659
+ header: { "if-match": options.expectedVersion },
660
+ path: options,
661
+ },
662
+ }));
663
+ });
664
+ spec
665
+ .command("draft")
666
+ .requiredOption("--project-id <projectId>")
667
+ .requiredOption("--spec-id <specId>")
668
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
669
+ .action(async (options) => {
670
+ if (hasProjectReadToken(dependencies.environment)) {
671
+ exitCode = rejectTokenWrite(dependencies);
672
+ return;
673
+ }
674
+ const idempotencyKey = randomUUID();
675
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/specs/{specId}/draft-session", {
676
+ params: {
677
+ header: {
678
+ "idempotency-key": idempotencyKey,
679
+ "if-match": options.expectedVersion,
680
+ },
681
+ path: options,
682
+ },
683
+ }), {
684
+ ambiguousOutcomeMessage: "Spec Draft Session outcome is unknown; read the Spec before retrying",
685
+ retryTransportOnce: true,
686
+ });
687
+ });
688
+ spec
689
+ .command("state")
690
+ .requiredOption("--project-id <projectId>")
691
+ .requiredOption("--spec-id <specId>")
692
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
693
+ .requiredOption("--state <state>", "Ready or Unavailable", parseSpecState)
694
+ .option("--unavailability-reason <unavailabilityReason>")
695
+ .action(async (options) => {
696
+ if (hasProjectReadToken(dependencies.environment)) {
697
+ exitCode = rejectTokenWrite(dependencies);
698
+ return;
699
+ }
700
+ if (options.state === "Unavailable" &&
701
+ options.unavailabilityReason === undefined) {
702
+ throw invalidArgument("Unavailable requires --unavailability-reason");
703
+ }
704
+ if (options.state === "Ready" &&
705
+ options.unavailabilityReason !== undefined) {
706
+ throw invalidArgument("Ready does not accept --unavailability-reason");
707
+ }
708
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/specs/{specId}/state", {
709
+ body: options.state === "Ready"
710
+ ? { state: "Ready" }
711
+ : {
712
+ state: "Unavailable",
713
+ unavailabilityReason: options.unavailabilityReason ?? "",
714
+ },
715
+ params: {
716
+ header: { "if-match": options.expectedVersion },
717
+ path: options,
718
+ },
719
+ }));
720
+ });
721
+ spec
722
+ .command("rollback")
723
+ .requiredOption("--project-id <projectId>")
724
+ .requiredOption("--spec-id <specId>")
725
+ .requiredOption("--revision-id <revisionId>")
726
+ .action(async (options) => {
727
+ if (hasProjectReadToken(dependencies.environment)) {
728
+ exitCode = rejectTokenWrite(dependencies);
729
+ return;
730
+ }
731
+ const idempotencyKey = randomUUID();
732
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/specs/{specId}/rollback", {
733
+ body: { revisionId: options.revisionId },
734
+ params: {
735
+ header: { "idempotency-key": idempotencyKey },
736
+ path: options,
737
+ },
738
+ }), {
739
+ ambiguousOutcomeMessage: "Spec Rollback outcome is unknown; inspect the Spec Changelog before retrying",
740
+ retryTransportOnce: true,
741
+ });
742
+ });
743
+ spec
744
+ .command("delete")
745
+ .requiredOption("--project-id <projectId>")
746
+ .requiredOption("--spec-id <specId>")
747
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
748
+ .action(async (options) => {
749
+ if (hasProjectReadToken(dependencies.environment)) {
750
+ exitCode = rejectTokenWrite(dependencies);
751
+ return;
752
+ }
753
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/specs/{specId}", {
754
+ params: {
755
+ header: { "if-match": options.expectedVersion },
756
+ path: options,
757
+ },
758
+ }), { deleted: { specId: options.specId } }));
759
+ });
760
+ const glossary = program.command("glossary");
761
+ glossary
762
+ .command("revision")
763
+ .requiredOption("--project-id <projectId>")
764
+ .requiredOption("--revision-id <revisionId>")
765
+ .action(async (options) => {
766
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/glossary/revisions/{revisionId}", { params: { path: options } }));
767
+ });
768
+ glossary
769
+ .command("read")
770
+ .requiredOption("--project-id <projectId>")
771
+ .action(async (options) => {
772
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/glossary", {
773
+ params: { path: { projectId: options.projectId } },
774
+ }), { rejectProjectReadToken: true });
775
+ });
776
+ glossary
777
+ .command("changelog")
778
+ .requiredOption("--project-id <projectId>")
779
+ .action(async (options) => {
780
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/glossary/changelog", {
781
+ params: { path: { projectId: options.projectId } },
782
+ }), { rejectProjectReadToken: true });
783
+ });
784
+ glossary
785
+ .command("draft")
786
+ .requiredOption("--project-id <projectId>")
787
+ .action(async (options) => {
788
+ if (hasProjectReadToken(dependencies.environment)) {
789
+ exitCode = rejectTokenWrite(dependencies);
790
+ return;
791
+ }
792
+ const idempotencyKey = randomUUID();
793
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/glossary/draft-session", {
794
+ params: {
795
+ header: { "idempotency-key": idempotencyKey },
796
+ path: { projectId: options.projectId },
797
+ },
798
+ }), {
799
+ ambiguousOutcomeMessage: "Glossary Draft Session outcome is unknown; read the Glossary before retrying",
800
+ retryTransportOnce: true,
801
+ });
802
+ });
803
+ glossary
804
+ .command("edit")
805
+ .requiredOption("--project-id <projectId>")
806
+ .requiredOption("--entries <entries>")
807
+ .option("--agent-revision <agentRevision>")
808
+ .option("--agent-model <agentModel>")
809
+ .option("--source-reference-ids <sourceReferenceIds>")
810
+ .action(async (options) => {
811
+ if (hasProjectReadToken(dependencies.environment)) {
812
+ exitCode = rejectTokenWrite(dependencies);
813
+ return;
814
+ }
815
+ const entries = parseGlossaryEntries(options.entries);
816
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/glossary/draft", {
817
+ body: { entries, provenance: toAgentProvenance(options) },
818
+ params: { path: { projectId: options.projectId } },
819
+ }));
820
+ });
821
+ glossary
822
+ .command("publish")
823
+ .requiredOption("--project-id <projectId>")
824
+ .action(async (options) => {
825
+ if (hasProjectReadToken(dependencies.environment)) {
826
+ exitCode = rejectTokenWrite(dependencies);
827
+ return;
828
+ }
829
+ const idempotencyKey = randomUUID();
830
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/glossary/publish", {
831
+ body: {},
832
+ params: {
833
+ header: { "idempotency-key": idempotencyKey },
834
+ path: { projectId: options.projectId },
835
+ },
836
+ }), {
837
+ ambiguousOutcomeMessage: "Glossary Publish outcome is unknown; inspect the Glossary Changelog before retrying",
838
+ retryTransportOnce: true,
839
+ });
840
+ });
841
+ glossary
842
+ .command("rollback")
843
+ .requiredOption("--project-id <projectId>")
844
+ .requiredOption("--revision-id <revisionId>")
845
+ .action(async (options) => {
846
+ if (hasProjectReadToken(dependencies.environment)) {
847
+ exitCode = rejectTokenWrite(dependencies);
848
+ return;
849
+ }
850
+ const idempotencyKey = randomUUID();
851
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/glossary/rollback", {
852
+ body: { revisionId: options.revisionId },
853
+ params: {
854
+ header: { "idempotency-key": idempotencyKey },
855
+ path: { projectId: options.projectId },
856
+ },
857
+ }), {
858
+ ambiguousOutcomeMessage: "Glossary Rollback outcome is unknown; inspect the Glossary Changelog before retrying",
859
+ retryTransportOnce: true,
860
+ });
861
+ });
862
+ const proposal = program.command("proposal");
863
+ proposal
864
+ .command("create-spec")
865
+ .requiredOption("--project-id <projectId>")
866
+ .requiredOption("--spec-id <specId>")
867
+ .requiredOption("--baseline-content <baselineContent>")
868
+ .requiredOption("--proposed-content <proposedContent>")
869
+ .option("--agent-revision <agentRevision>")
870
+ .option("--agent-model <agentModel>")
871
+ .option("--source-reference-ids <sourceReferenceIds>")
872
+ .action(async (options) => {
873
+ if (hasProjectReadToken(dependencies.environment)) {
874
+ exitCode = rejectTokenWrite(dependencies);
875
+ return;
876
+ }
877
+ const idempotencyKey = randomUUID();
878
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/draft-proposals", {
879
+ body: {
880
+ baselineContent: options.baselineContent,
881
+ kind: "Spec",
882
+ proposedContent: options.proposedContent,
883
+ provenance: toAgentProvenance(options),
884
+ specId: options.specId,
885
+ },
886
+ params: {
887
+ header: { "idempotency-key": idempotencyKey },
888
+ path: { projectId: options.projectId },
889
+ },
890
+ }), {
891
+ ambiguousOutcomeMessage: "Draft Proposal Creation outcome is unknown; list open Proposals before retrying",
892
+ retryTransportOnce: true,
893
+ });
894
+ });
895
+ proposal
896
+ .command("create-glossary")
897
+ .requiredOption("--project-id <projectId>")
898
+ .requiredOption("--baseline-entries <baselineEntries>")
899
+ .requiredOption("--proposed-entries <proposedEntries>")
900
+ .option("--agent-revision <agentRevision>")
901
+ .option("--agent-model <agentModel>")
902
+ .option("--source-reference-ids <sourceReferenceIds>")
903
+ .action(async (options) => {
904
+ if (hasProjectReadToken(dependencies.environment)) {
905
+ exitCode = rejectTokenWrite(dependencies);
906
+ return;
907
+ }
908
+ const idempotencyKey = randomUUID();
909
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/draft-proposals", {
910
+ body: {
911
+ baselineEntries: parseGlossaryEntries(options.baselineEntries),
912
+ kind: "Glossary",
913
+ proposedEntries: parseGlossaryEntries(options.proposedEntries),
914
+ provenance: toAgentProvenance(options),
915
+ },
916
+ params: {
917
+ header: { "idempotency-key": idempotencyKey },
918
+ path: { projectId: options.projectId },
919
+ },
920
+ }), {
921
+ ambiguousOutcomeMessage: "Draft Proposal Creation outcome is unknown; list open Proposals before retrying",
922
+ retryTransportOnce: true,
923
+ });
924
+ });
925
+ proposal
926
+ .command("list")
927
+ .requiredOption("--project-id <projectId>")
928
+ .option("--target-kind <targetKind>", "Spec or Glossary", parseProposalTargetKind)
929
+ .option("--spec-id <specId>")
930
+ .option("--status <status>", "Open, Applied, or Discarded", parseProposalStatus)
931
+ .action(async (options) => {
932
+ if ((options.targetKind === "Spec" && options.specId === undefined) ||
933
+ (options.targetKind !== "Spec" && options.specId !== undefined)) {
934
+ throw invalidArgument("--spec-id requires --target-kind Spec");
935
+ }
936
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/draft-proposals", {
937
+ params: {
938
+ path: { projectId: options.projectId },
939
+ query: {
940
+ ...(options.specId === undefined ? {} : { specId: options.specId }),
941
+ ...(options.status === undefined ? {} : { status: options.status }),
942
+ ...(options.targetKind === undefined
943
+ ? {}
944
+ : { targetKind: options.targetKind }),
945
+ },
946
+ },
947
+ }));
948
+ });
949
+ proposal
950
+ .command("apply")
951
+ .requiredOption("--project-id <projectId>")
952
+ .requiredOption("--proposal-id <proposalId>")
953
+ .option("--change-ids <changeIds>")
954
+ .action(async (options) => {
955
+ if (hasProjectReadToken(dependencies.environment)) {
956
+ exitCode = rejectTokenWrite(dependencies);
957
+ return;
958
+ }
959
+ const idempotencyKey = randomUUID();
960
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/draft-proposals/{proposalId}/apply", {
961
+ body: {
962
+ changeIds: options.changeIds === undefined
963
+ ? null
964
+ : parseStringArray(options.changeIds, "change IDs"),
965
+ },
966
+ params: {
967
+ header: { "idempotency-key": idempotencyKey },
968
+ path: {
969
+ projectId: options.projectId,
970
+ proposalId: options.proposalId,
971
+ },
972
+ },
973
+ }), {
974
+ ambiguousOutcomeMessage: "Draft Proposal Apply outcome is unknown; reread the Proposal and Working Draft",
975
+ retryTransportOnce: true,
976
+ });
977
+ });
978
+ proposal
979
+ .command("discard")
980
+ .requiredOption("--project-id <projectId>")
981
+ .requiredOption("--proposal-id <proposalId>")
982
+ .action(async (options) => {
983
+ if (hasProjectReadToken(dependencies.environment)) {
984
+ exitCode = rejectTokenWrite(dependencies);
985
+ return;
986
+ }
987
+ const idempotencyKey = randomUUID();
988
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/draft-proposals/{proposalId}/discard", {
989
+ params: {
990
+ header: { "idempotency-key": idempotencyKey },
991
+ path: {
992
+ projectId: options.projectId,
993
+ proposalId: options.proposalId,
994
+ },
995
+ },
996
+ }), {
997
+ ambiguousOutcomeMessage: "Draft Proposal Discard outcome is unknown; reread the Proposal",
998
+ retryTransportOnce: true,
999
+ });
1000
+ });
1001
+ program
1002
+ .command("authoring-history")
1003
+ .requiredOption("--project-id <projectId>")
1004
+ .action(async (options) => {
1005
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/authoring-history", {
1006
+ params: { path: { projectId: options.projectId } },
1007
+ }));
1008
+ });
1009
+ const capability = program.command("capability");
1010
+ capability
1011
+ .command("create")
1012
+ .requiredOption("--project-id <projectId>")
1013
+ .requiredOption("--name <name>")
1014
+ .option("--parent-id <parentId>")
1015
+ .action(async (options) => {
1016
+ if (hasProjectReadToken(dependencies.environment)) {
1017
+ exitCode = rejectTokenWrite(dependencies);
1018
+ return;
1019
+ }
1020
+ const idempotencyKey = randomUUID();
1021
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/capabilities", {
1022
+ body: {
1023
+ name: options.name,
1024
+ parentId: options.parentId ?? null,
1025
+ },
1026
+ params: {
1027
+ header: { "idempotency-key": idempotencyKey },
1028
+ path: { projectId: options.projectId },
1029
+ },
1030
+ }), {
1031
+ ambiguousOutcomeMessage: "Capability Creation outcome is unknown; inspect the Project directory before retrying",
1032
+ retryTransportOnce: true,
1033
+ });
1034
+ });
1035
+ capability
1036
+ .command("update")
1037
+ .requiredOption("--project-id <projectId>")
1038
+ .requiredOption("--capability-id <capabilityId>")
1039
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1040
+ .option("--name <name>")
1041
+ .option("--parent-id <parentId>")
1042
+ .option("--move-to-root")
1043
+ .action(async (options) => {
1044
+ if (hasProjectReadToken(dependencies.environment)) {
1045
+ exitCode = rejectTokenWrite(dependencies);
1046
+ return;
1047
+ }
1048
+ if (options.parentId !== undefined && options.moveToRoot === true) {
1049
+ throw invalidArgument("Capability update cannot combine --parent-id and --move-to-root");
1050
+ }
1051
+ if (options.name === undefined &&
1052
+ options.parentId === undefined &&
1053
+ options.moveToRoot !== true) {
1054
+ throw invalidArgument("Capability update requires a change");
1055
+ }
1056
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/capabilities/{capabilityId}", {
1057
+ body: {
1058
+ ...(options.name === undefined ? {} : { name: options.name }),
1059
+ ...(options.moveToRoot === true
1060
+ ? { parentId: null }
1061
+ : options.parentId === undefined
1062
+ ? {}
1063
+ : { parentId: options.parentId }),
1064
+ },
1065
+ params: {
1066
+ header: { "if-match": options.expectedVersion },
1067
+ path: options,
1068
+ },
1069
+ }));
1070
+ });
1071
+ capability
1072
+ .command("delete")
1073
+ .option("--idempotency-key <key>", "Reuse this key when retrying the same deletion")
1074
+ .requiredOption("--project-id <projectId>")
1075
+ .requiredOption("--capability-id <capabilityId>")
1076
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1077
+ .action(async (options) => {
1078
+ if (hasProjectReadToken(dependencies.environment)) {
1079
+ exitCode = rejectTokenWrite(dependencies);
1080
+ return;
1081
+ }
1082
+ const idempotencyKey = options.idempotencyKey ?? randomUUID();
1083
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/capabilities/{capabilityId}", {
1084
+ params: {
1085
+ header: { "if-match": options.expectedVersion, "idempotency-key": idempotencyKey },
1086
+ path: options,
1087
+ },
1088
+ }), { deleted: { capabilityId: options.capabilityId } }));
1089
+ });
1090
+ const finding = program.command("finding");
1091
+ finding
1092
+ .command("list")
1093
+ .requiredOption("--project-id <projectId>")
1094
+ .requiredOption("--spec-id <specId>")
1095
+ .option("--status <status>", "Finding status", parseFindingStatus)
1096
+ .option("--limit <limit>", "Maximum results", parseSearchLimit, 20)
1097
+ .option("--cursor <cursor>")
1098
+ .action(async (options) => {
1099
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/specs/{specId}/findings", {
1100
+ params: {
1101
+ path: { projectId: options.projectId, specId: options.specId },
1102
+ query: {
1103
+ ...(options.cursor === undefined
1104
+ ? {}
1105
+ : { cursor: options.cursor }),
1106
+ limit: options.limit,
1107
+ ...(options.status === undefined
1108
+ ? {}
1109
+ : { status: options.status }),
1110
+ },
1111
+ },
1112
+ }), { rejectProjectReadToken: true });
1113
+ });
1114
+ finding
1115
+ .command("read")
1116
+ .requiredOption("--project-id <projectId>")
1117
+ .requiredOption("--finding-id <findingId>")
1118
+ .action(async (options) => {
1119
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/findings/{findingId}", {
1120
+ params: { path: options },
1121
+ }), { rejectProjectReadToken: true });
1122
+ });
1123
+ finding
1124
+ .command("create")
1125
+ .requiredOption("--project-id <projectId>")
1126
+ .requiredOption("--input <input>")
1127
+ .action(async (options) => {
1128
+ if (hasProjectReadToken(dependencies.environment)) {
1129
+ exitCode = rejectTokenWrite(dependencies);
1130
+ return;
1131
+ }
1132
+ const idempotencyKey = randomUUID();
1133
+ const input = parseFindingInput(options.input);
1134
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/findings", {
1135
+ body: input,
1136
+ params: {
1137
+ header: { "idempotency-key": idempotencyKey },
1138
+ path: { projectId: options.projectId },
1139
+ },
1140
+ }), {
1141
+ ambiguousOutcomeMessage: "Finding Creation outcome is unknown; inspect the Finding list before retrying",
1142
+ retryTransportOnce: true,
1143
+ });
1144
+ });
1145
+ for (const [commandName, status] of [
1146
+ ["confirm", "Confirmed"],
1147
+ ["dismiss", "Dismissed"],
1148
+ ["resolve", "Resolved"],
1149
+ ]) {
1150
+ finding
1151
+ .command(commandName)
1152
+ .requiredOption("--project-id <projectId>")
1153
+ .requiredOption("--finding-id <findingId>")
1154
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1155
+ .action(async (options) => {
1156
+ if (hasProjectReadToken(dependencies.environment)) {
1157
+ exitCode = rejectTokenWrite(dependencies);
1158
+ return;
1159
+ }
1160
+ const idempotencyKey = randomUUID();
1161
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/findings/{findingId}/status", {
1162
+ body: { status },
1163
+ params: {
1164
+ header: {
1165
+ "idempotency-key": idempotencyKey,
1166
+ "if-match": options.expectedVersion,
1167
+ },
1168
+ path: options,
1169
+ },
1170
+ }), {
1171
+ ambiguousOutcomeMessage: "Finding disposition outcome is unknown; read the Finding before retrying",
1172
+ retryTransportOnce: true,
1173
+ });
1174
+ });
1175
+ }
1176
+ const notification = program.command("notification").description("Read and manage your personal task notifications");
1177
+ notification.command("list").action(async () => {
1178
+ if (hasProjectReadToken(dependencies.environment)) {
1179
+ exitCode = rejectProjectReadTokenOperation(dependencies);
1180
+ return;
1181
+ }
1182
+ exitCode = await run((client) => client.GET("/v1/notifications"));
1183
+ });
1184
+ notification.command("create")
1185
+ .option("--title <title>")
1186
+ .option("--message <message>")
1187
+ .option("--type <type>", "Notification type", "info")
1188
+ .option("--event-data <json>", "JSON object for presentation", parseNotificationEventData)
1189
+ .option("--related-entity-id <id>", "Public task ID or Workspace ID")
1190
+ .option("--related-entity-type <type>", "task or workspace", (value) => { if (value !== "task" && value !== "workspace")
1191
+ throw invalidArgument("Notification target must be task or workspace"); return value; })
1192
+ .requiredOption("--idempotency-key <key>", "Reuse the same command key and inputs when retrying", parseNotificationUuid)
1193
+ .action(async (options) => {
1194
+ if (hasProjectReadToken(dependencies.environment)) {
1195
+ exitCode = rejectProjectReadTokenOperation(dependencies);
1196
+ return;
1197
+ }
1198
+ if ((options.relatedEntityId === undefined) !== (options.relatedEntityType === undefined))
1199
+ throw invalidArgument("Notification target identity and type must be provided together");
1200
+ exitCode = await run((client) => client.POST("/v1/notifications", { body: { type: options.type, ...(options.title === undefined ? {} : { title: options.title }), ...(options.message === undefined ? {} : { message: options.message }), ...(options.eventData === undefined ? {} : { eventData: options.eventData }), ...(options.relatedEntityId === undefined ? {} : { relatedEntityId: options.relatedEntityId }), ...(options.relatedEntityType === undefined ? {} : { relatedEntityType: options.relatedEntityType }) }, params: { header: { "idempotency-key": options.idempotencyKey } } }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Notification creation outcome is unknown; retry with the same inputs and command key" });
1201
+ });
1202
+ notification.command("read")
1203
+ .requiredOption("--notification-id <id>", "Notification UUID returned by the list", parseNotificationUuid)
1204
+ .requiredOption("--idempotency-key <key>", "Reuse the same key when retrying", parseNotificationUuid)
1205
+ .action(async (options) => {
1206
+ if (hasProjectReadToken(dependencies.environment)) {
1207
+ exitCode = rejectProjectReadTokenOperation(dependencies);
1208
+ return;
1209
+ }
1210
+ exitCode = await run((client) => client.PATCH("/v1/notifications/{notificationId}/read", { params: { path: { notificationId: options.notificationId }, header: { "idempotency-key": options.idempotencyKey } } }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Notification read outcome is unknown; retry with the same notification and command key" });
1211
+ });
1212
+ for (const action of ["read-all", "clear"]) {
1213
+ notification.command(action).requiredOption("--idempotency-key <key>", "Reuse the same key to avoid changing notifications that arrived after the first command", parseNotificationUuid)
1214
+ .action(async (options) => {
1215
+ if (hasProjectReadToken(dependencies.environment)) {
1216
+ exitCode = rejectProjectReadTokenOperation(dependencies);
1217
+ return;
1218
+ }
1219
+ const params = { header: { "idempotency-key": options.idempotencyKey } };
1220
+ exitCode = await run((client) => action === "read-all" ? client.PATCH("/v1/notifications/read", { params }) : client.DELETE("/v1/notifications", { params }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Notification bulk command outcome is unknown; retry with the same command key" });
1221
+ });
1222
+ }
1223
+ const notificationPreferences = notification.command("preferences");
1224
+ notificationPreferences.command("get").action(async () => {
1225
+ if (hasProjectReadToken(dependencies.environment)) {
1226
+ exitCode = rejectProjectReadTokenOperation(dependencies);
1227
+ return;
1228
+ }
1229
+ exitCode = await run((client) => client.GET("/v1/notification-preferences"));
1230
+ });
1231
+ notificationPreferences.command("update")
1232
+ .requiredOption("--expected-version <version>", "Opaque preference version returned by get")
1233
+ .requiredOption("--preferences <json>", "JSON category settings; omitted settings remain unchanged", parseNotificationPreferences)
1234
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and settings when retrying", parseNotificationUuid)
1235
+ .action(async (options) => {
1236
+ if (hasProjectReadToken(dependencies.environment)) {
1237
+ exitCode = rejectProjectReadTokenOperation(dependencies);
1238
+ return;
1239
+ }
1240
+ exitCode = await run((client) => client.PATCH("/v1/notification-preferences", { body: options.preferences, params: { header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } } }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Notification preference outcome is unknown; retry with the same settings and command key" });
1241
+ });
1242
+ const workItem = program.command("work-item");
1243
+ registerWorkItemAssetCommands(workItem, { dependencies, run, setExitCode: (value) => { exitCode = value; }, rejectWrite: () => rejectTokenWrite(dependencies) });
1244
+ registerWorkItemResourceLinkCommands(workItem, spec, { run, setExitCode: (value) => { exitCode = value; }, canWrite: () => !hasProjectReadToken(dependencies.environment), rejectWrite: () => rejectTokenWrite(dependencies), parseOpaqueVersion, parseLimit: parseSearchLimit });
1245
+ const timeEntry = workItem.command("time-entry").description("Record and read task time entries");
1246
+ timeEntry.command("list")
1247
+ .requiredOption("--project-id <projectId>")
1248
+ .requiredOption("--work-item-id <workItemId>")
1249
+ .option("--limit <limit>", "Maximum results, 1 through 100", parseSearchLimit, 50)
1250
+ .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
1251
+ .action(async (options) => {
1252
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/time-entries", { params: { path: { projectId: options.projectId, workItemId: options.workItemId }, query: { limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }) } } }));
1253
+ });
1254
+ timeEntry.command("get")
1255
+ .requiredOption("--project-id <projectId>")
1256
+ .requiredOption("--work-item-id <workItemId>")
1257
+ .requiredOption("--entry-id <entryId>", "Time Entry UUID", (value) => { try {
1258
+ return parseTimeEntryUuid(value);
1259
+ }
1260
+ catch {
1261
+ throw invalidArgument("Time Entry ID must be a UUID");
1262
+ } })
1263
+ .action(async (options) => {
1264
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/time-entries/{entryId}", { params: { path: options } }));
1265
+ });
1266
+ for (const action of ["create", "update"]) {
1267
+ const command = timeEntry.command(action)
1268
+ .requiredOption("--project-id <projectId>")
1269
+ .requiredOption("--work-item-id <workItemId>")
1270
+ .requiredOption("--start-time <timestamp>", "ISO date-time with an offset", (value) => { try {
1271
+ return parseTimeEntryTimestamp(value);
1272
+ }
1273
+ catch {
1274
+ throw invalidArgument("Start time must be a valid ISO 8601 timestamp with an offset");
1275
+ } })
1276
+ .option("--end-time <timestamp>", "Omit to start running, or preserve the stored end on update", (value) => { try {
1277
+ return parseTimeEntryTimestamp(value);
1278
+ }
1279
+ catch {
1280
+ throw invalidArgument("End time must be a valid ISO 8601 timestamp with an offset");
1281
+ } })
1282
+ .option("--description <description>", "Optional recorded work description", (value) => { try {
1283
+ return parseTimeEntryDescription(value);
1284
+ }
1285
+ catch {
1286
+ throw invalidArgument("Time Entry description exceeds the allowed limit");
1287
+ } })
1288
+ .requiredOption("--idempotency-key <key>", "Reuse this key and inputs for the same action", (value) => { try {
1289
+ return parseTimeEntryUuid(value);
1290
+ }
1291
+ catch {
1292
+ throw invalidArgument("Command key must be a UUID");
1293
+ } });
1294
+ if (action === "update")
1295
+ command.requiredOption("--entry-id <entryId>", "Time Entry UUID", (value) => { try {
1296
+ return parseTimeEntryUuid(value);
1297
+ }
1298
+ catch {
1299
+ throw invalidArgument("Time Entry ID must be a UUID");
1300
+ } }).requiredOption("--expected-version <version>", "Opaque version returned by SpecFlow", parseOpaqueVersion);
1301
+ command.action(async (options) => {
1302
+ if (hasProjectReadToken(dependencies.environment)) {
1303
+ exitCode = rejectTokenWrite(dependencies);
1304
+ return;
1305
+ }
1306
+ const input = { startTime: options.startTime, ...(options.endTime === undefined ? {} : { endTime: options.endTime }), ...(options.description === undefined ? {} : { description: options.description }) };
1307
+ if (options.endTime !== undefined && Date.parse(options.startTime) > Date.parse(options.endTime))
1308
+ throw invalidArgument("Start time cannot follow end time");
1309
+ const path = { projectId: options.projectId, workItemId: options.workItemId };
1310
+ const header = { "idempotency-key": options.idempotencyKey };
1311
+ exitCode = await run((client) => {
1312
+ if (action === "create")
1313
+ return client.POST("/v1/projects/{projectId}/work-items/{workItemId}/time-entries", { body: input, params: { path, header } });
1314
+ if (options.entryId === undefined || options.expectedVersion === undefined)
1315
+ throw new Error("Time Entry update requires its identity and observed version");
1316
+ return client.PATCH("/v1/projects/{projectId}/work-items/{workItemId}/time-entries/{entryId}", { body: input, params: { path: { ...path, entryId: options.entryId }, header: { ...header, "if-match": options.expectedVersion } } });
1317
+ }, { retryTransportOnce: true, ambiguousOutcomeMessage: "Time Entry outcome is unknown; retry the same inputs and command key" });
1318
+ });
1319
+ }
1320
+ const workItemComment = workItem.command("comment").description("Read and manage authored task comments");
1321
+ workItemComment.command("list")
1322
+ .requiredOption("--project-id <projectId>")
1323
+ .requiredOption("--work-item-id <workItemId>")
1324
+ .option("--limit <limit>", "Maximum results, 1 through 100", parseSearchLimit, 50)
1325
+ .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
1326
+ .action(async (options) => {
1327
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/comments", {
1328
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId }, query: { limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }) } },
1329
+ }));
1330
+ });
1331
+ workItem.command("discussion").description("Read comments and immutable activity newest first")
1332
+ .requiredOption("--project-id <projectId>")
1333
+ .requiredOption("--work-item-id <workItemId>")
1334
+ .option("--limit <limit>", "Maximum results, 1 through 100", parseSearchLimit, 50)
1335
+ .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
1336
+ .action(async (options) => {
1337
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/discussion", {
1338
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId }, query: { limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }) } },
1339
+ }));
1340
+ });
1341
+ workItemComment.command("create")
1342
+ .requiredOption("--project-id <projectId>")
1343
+ .requiredOption("--work-item-id <workItemId>")
1344
+ .requiredOption("--content <markdown>", "Markdown comment body, up to 20000 characters", parseCommentContent)
1345
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseCommentCommandKey)
1346
+ .action(async (options) => {
1347
+ if (hasProjectReadToken(dependencies.environment)) {
1348
+ exitCode = rejectTokenWrite(dependencies);
1349
+ return;
1350
+ }
1351
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/comments", {
1352
+ body: { content: options.content },
1353
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId }, header: { "idempotency-key": options.idempotencyKey } },
1354
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Comment creation outcome is unknown; retry with the same inputs and command key" });
1355
+ });
1356
+ workItemComment.command("update")
1357
+ .requiredOption("--project-id <projectId>")
1358
+ .requiredOption("--work-item-id <workItemId>")
1359
+ .requiredOption("--comment-id <commentId>", "Identifier returned by comments", parseCommentId)
1360
+ .requiredOption("--content <markdown>", "Markdown comment body, up to 20000 characters", parseCommentContent)
1361
+ .requiredOption("--expected-version <version>", "Opaque comment version returned by SpecFlow", parseOpaqueVersion)
1362
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseCommentCommandKey)
1363
+ .action(async (options) => {
1364
+ if (hasProjectReadToken(dependencies.environment)) {
1365
+ exitCode = rejectTokenWrite(dependencies);
1366
+ return;
1367
+ }
1368
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/work-items/{workItemId}/comments/{commentId}", {
1369
+ body: { content: options.content },
1370
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId, commentId: options.commentId },
1371
+ header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } },
1372
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Comment edit outcome is unknown; reread the comment or retry the same inputs and command key" });
1373
+ });
1374
+ workItemComment.command("delete")
1375
+ .requiredOption("--project-id <projectId>")
1376
+ .requiredOption("--work-item-id <workItemId>")
1377
+ .requiredOption("--comment-id <commentId>", "Identifier returned by comments", parseCommentId)
1378
+ .requiredOption("--expected-version <version>", "Opaque comment version returned by SpecFlow", parseOpaqueVersion)
1379
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseCommentCommandKey)
1380
+ .action(async (options) => {
1381
+ if (hasProjectReadToken(dependencies.environment)) {
1382
+ exitCode = rejectTokenWrite(dependencies);
1383
+ return;
1384
+ }
1385
+ exitCode = await run((client) => client.DELETE("/v1/projects/{projectId}/work-items/{workItemId}/comments/{commentId}", {
1386
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId, commentId: options.commentId },
1387
+ header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } },
1388
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Comment deletion outcome is unknown; reread the comment or retry the same inputs and command key" });
1389
+ });
1390
+ const workItemRelation = workItem.command("relation").description("Manage subtask, blocking and related task links");
1391
+ workItemRelation.command("list")
1392
+ .requiredOption("--project-id <projectId>")
1393
+ .requiredOption("--work-item-id <workItemId>")
1394
+ .action(async (options) => {
1395
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/relations", { params: { path: options } }));
1396
+ });
1397
+ workItemRelation.command("create")
1398
+ .requiredOption("--project-id <projectId>")
1399
+ .requiredOption("--work-item-id <workItemId>")
1400
+ .requiredOption("--target-project-id <projectId>", "Target task's current Project", parseProjectId)
1401
+ .requiredOption("--target-work-item-id <workItemId>", "Target task's public identity", parseWorkItemId)
1402
+ .requiredOption("--type <type>", "subtask, blocks or related", (value) => {
1403
+ const result = z.enum(["subtask", "blocks", "related"]).safeParse(value);
1404
+ if (!result.success)
1405
+ throw invalidArgument("Relation type must be subtask, blocks or related");
1406
+ return result.data;
1407
+ })
1408
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseRelationCommandKey)
1409
+ .action(async (options) => {
1410
+ if (hasProjectReadToken(dependencies.environment)) {
1411
+ exitCode = rejectTokenWrite(dependencies);
1412
+ return;
1413
+ }
1414
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/relations", {
1415
+ body: { targetProjectId: options.targetProjectId, targetWorkItemId: options.targetWorkItemId, relationType: options.type },
1416
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId }, header: { "idempotency-key": options.idempotencyKey } },
1417
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Relation creation outcome is unknown; retry with the same inputs and command key" });
1418
+ });
1419
+ workItemRelation.command("delete")
1420
+ .requiredOption("--project-id <projectId>")
1421
+ .requiredOption("--work-item-id <workItemId>")
1422
+ .requiredOption("--peer-project-id <projectId>", "Other endpoint's current Project", parseProjectId)
1423
+ .requiredOption("--relation-id <relationId>", "Relation identifier returned by list", (value) => {
1424
+ if (!z.uuid().safeParse(value).success)
1425
+ throw invalidArgument("Relation ID must be a UUID returned by SpecFlow");
1426
+ return value;
1427
+ })
1428
+ .requiredOption("--expected-version <expectedVersion>", "Opaque relation version returned by SpecFlow", parseOpaqueVersion)
1429
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseRelationCommandKey)
1430
+ .action(async (options) => {
1431
+ if (hasProjectReadToken(dependencies.environment)) {
1432
+ exitCode = rejectTokenWrite(dependencies);
1433
+ return;
1434
+ }
1435
+ exitCode = await run((client) => client.DELETE("/v1/projects/{projectId}/work-items/{workItemId}/relations/{relationId}", {
1436
+ params: { path: { projectId: options.projectId, workItemId: options.workItemId, relationId: options.relationId },
1437
+ query: { peerProjectId: options.peerProjectId }, header: { "idempotency-key": options.idempotencyKey, "if-match": options.expectedVersion } },
1438
+ }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Relation removal outcome is unknown; retry with the same inputs and command key" });
1439
+ });
1440
+ workItemSelectionFlags(workItem.command("list"))
1441
+ .requiredOption("--project-id <projectId>")
1442
+ .option("--limit <limit>", "Maximum results", parseSearchLimit, 20)
1443
+ .option("--cursor <cursor>")
1444
+ .option("--group-by <field>", "Group the page by field (status)", (value) => {
1445
+ if (value === "status") {
1446
+ return "status";
1447
+ }
1448
+ throw invalidArgument("Work Items can only be grouped by status");
1449
+ })
1450
+ .action(async (options) => {
1451
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items", {
1452
+ params: {
1453
+ path: { projectId: options.projectId },
1454
+ query: {
1455
+ ...workItemSelectionQuery(options),
1456
+ ...(options.cursor === undefined
1457
+ ? {}
1458
+ : { cursor: options.cursor }),
1459
+ ...(options.groupBy === undefined
1460
+ ? {}
1461
+ : { groupBy: options.groupBy }),
1462
+ limit: options.limit,
1463
+ },
1464
+ },
1465
+ }));
1466
+ });
1467
+ workItem
1468
+ .command("read")
1469
+ .requiredOption("--project-id <projectId>")
1470
+ .requiredOption("--work-item-id <workItemId>")
1471
+ .action(async (options) => {
1472
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}", {
1473
+ params: { path: options },
1474
+ }));
1475
+ });
1476
+ workItem
1477
+ .command("assignees")
1478
+ .requiredOption("--project-id <projectId>")
1479
+ .action(async (options) => {
1480
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-assignees", {
1481
+ params: { path: options },
1482
+ }));
1483
+ });
1484
+ workItem
1485
+ .command("create")
1486
+ .requiredOption("--project-id <projectId>")
1487
+ .requiredOption("--title <title>")
1488
+ .option("--description <description>")
1489
+ .option("--label <labelId>", "Workspace label definition ID; repeat for several labels", collectOption, [])
1490
+ .option("--assignee <assignee>", "Workspace member User ID")
1491
+ .option("--custom-field <fieldId=value>", "Custom Field value as fieldId=value; repeat for several fields", collectOption, [])
1492
+ .option("--priority <priority>", "Work Item priority", parseWorkItemPriority)
1493
+ .option("--start-date <startDate>", "ISO 8601 date", parseWorkItemDate)
1494
+ .option("--due-date <dueDate>", "ISO 8601 date", parseWorkItemDate)
1495
+ .option("--status <status>", "Workflow Column slug or virtual status to create the Work Item in")
1496
+ .action(async (options) => {
1497
+ if (hasProjectReadToken(dependencies.environment)) {
1498
+ exitCode = rejectTokenWrite(dependencies);
1499
+ return;
1500
+ }
1501
+ const idempotencyKey = randomUUID();
1502
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items", {
1503
+ body: {
1504
+ ...(options.assignee === undefined
1505
+ ? {}
1506
+ : { assigneeId: options.assignee }),
1507
+ ...(options.customField.length === 0
1508
+ ? {}
1509
+ : {
1510
+ customFields: options.customField.map(parseCustomFieldAssignment),
1511
+ }),
1512
+ ...(options.description === undefined
1513
+ ? {}
1514
+ : { description: options.description }),
1515
+ ...(options.label.length === 0 ? {} : { labelIds: [...new Set(options.label)] }),
1516
+ ...(options.dueDate === undefined
1517
+ ? {}
1518
+ : { dueDate: options.dueDate }),
1519
+ ...(options.priority === undefined
1520
+ ? {}
1521
+ : { priority: options.priority }),
1522
+ ...(options.startDate === undefined
1523
+ ? {}
1524
+ : { startDate: options.startDate }),
1525
+ ...(options.status === undefined
1526
+ ? {}
1527
+ : { status: options.status }),
1528
+ title: options.title,
1529
+ },
1530
+ params: {
1531
+ header: { "idempotency-key": idempotencyKey },
1532
+ path: { projectId: options.projectId },
1533
+ },
1534
+ }), {
1535
+ ambiguousOutcomeMessage: "Work Item creation outcome is unknown; list Work Items before retrying",
1536
+ retryTransportOnce: true,
1537
+ });
1538
+ });
1539
+ workItem
1540
+ .command("update")
1541
+ .requiredOption("--project-id <projectId>")
1542
+ .requiredOption("--work-item-id <workItemId>")
1543
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1544
+ .option("--title <title>")
1545
+ .option("--description <description>")
1546
+ .option("--clear-description", "Remove the Work Item description")
1547
+ .option("--priority <priority>", "Work Item priority", parseWorkItemPriority)
1548
+ .option("--start-date <startDate>", "ISO 8601 date", parseWorkItemDate)
1549
+ .option("--due-date <dueDate>", "ISO 8601 date", parseWorkItemDate)
1550
+ .option("--clear-start-date", "Remove the Work Item start date")
1551
+ .option("--clear-due-date", "Remove the Work Item due date")
1552
+ .action(async (options) => {
1553
+ if (hasProjectReadToken(dependencies.environment)) {
1554
+ exitCode = rejectTokenWrite(dependencies);
1555
+ return;
1556
+ }
1557
+ if (options.title === undefined &&
1558
+ options.description === undefined &&
1559
+ options.clearDescription !== true &&
1560
+ options.priority === undefined &&
1561
+ options.startDate === undefined &&
1562
+ options.dueDate === undefined &&
1563
+ options.clearStartDate !== true &&
1564
+ options.clearDueDate !== true) {
1565
+ throw invalidArgument("Work Item update requires a field change");
1566
+ }
1567
+ if (options.clearDescription === true &&
1568
+ options.description !== undefined) {
1569
+ throw invalidArgument("Work Item update cannot set and clear the description together");
1570
+ }
1571
+ if (options.clearStartDate === true && options.startDate !== undefined) {
1572
+ throw invalidArgument("Work Item update cannot set and clear the start date together");
1573
+ }
1574
+ if (options.clearDueDate === true && options.dueDate !== undefined) {
1575
+ throw invalidArgument("Work Item update cannot set and clear the due date together");
1576
+ }
1577
+ const idempotencyKey = randomUUID();
1578
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/work-items/{workItemId}", {
1579
+ body: {
1580
+ ...(options.clearDescription === true
1581
+ ? { description: null }
1582
+ : options.description === undefined
1583
+ ? {}
1584
+ : { description: options.description }),
1585
+ ...(options.clearDueDate === true
1586
+ ? { dueDate: null }
1587
+ : options.dueDate === undefined
1588
+ ? {}
1589
+ : { dueDate: options.dueDate }),
1590
+ ...(options.clearStartDate === true
1591
+ ? { startDate: null }
1592
+ : options.startDate === undefined
1593
+ ? {}
1594
+ : { startDate: options.startDate }),
1595
+ ...(options.priority === undefined
1596
+ ? {}
1597
+ : { priority: options.priority }),
1598
+ ...(options.title === undefined
1599
+ ? {}
1600
+ : { title: options.title }),
1601
+ },
1602
+ params: {
1603
+ header: {
1604
+ "idempotency-key": idempotencyKey,
1605
+ "if-match": options.expectedVersion,
1606
+ },
1607
+ path: {
1608
+ projectId: options.projectId,
1609
+ workItemId: options.workItemId,
1610
+ },
1611
+ },
1612
+ }), {
1613
+ ambiguousOutcomeMessage: "Work Item update outcome is unknown; read the Work Item before retrying",
1614
+ retryTransportOnce: true,
1615
+ });
1616
+ });
1617
+ workItem
1618
+ .command("assign")
1619
+ .requiredOption("--project-id <projectId>")
1620
+ .requiredOption("--work-item-id <workItemId>")
1621
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1622
+ .option("--assignee <assignee>", "Workspace member User ID")
1623
+ .option("--unassign", "Remove the Work Item assignee")
1624
+ .action(async (options) => {
1625
+ if (hasProjectReadToken(dependencies.environment)) {
1626
+ exitCode = rejectTokenWrite(dependencies);
1627
+ return;
1628
+ }
1629
+ if (options.assignee === undefined && options.unassign !== true) {
1630
+ throw invalidArgument("Work Item assign requires an assignee or --unassign");
1631
+ }
1632
+ if (options.assignee !== undefined && options.unassign === true) {
1633
+ throw invalidArgument("Work Item assign cannot set and clear the assignee together");
1634
+ }
1635
+ const idempotencyKey = randomUUID();
1636
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-items/{workItemId}/assignee", {
1637
+ body: {
1638
+ assigneeId: options.unassign === true ? null : (options.assignee ?? null),
1639
+ },
1640
+ params: {
1641
+ header: {
1642
+ "idempotency-key": idempotencyKey,
1643
+ "if-match": options.expectedVersion,
1644
+ },
1645
+ path: {
1646
+ projectId: options.projectId,
1647
+ workItemId: options.workItemId,
1648
+ },
1649
+ },
1650
+ }), {
1651
+ ambiguousOutcomeMessage: "Work Item assignment outcome is unknown; read the Work Item before retrying",
1652
+ retryTransportOnce: true,
1653
+ });
1654
+ });
1655
+ workItem
1656
+ .command("delete")
1657
+ .requiredOption("--project-id <projectId>")
1658
+ .requiredOption("--work-item-id <workItemId>")
1659
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1660
+ .action(async (options) => {
1661
+ if (hasProjectReadToken(dependencies.environment)) {
1662
+ exitCode = rejectTokenWrite(dependencies);
1663
+ return;
1664
+ }
1665
+ const idempotencyKey = randomUUID();
1666
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/work-items/{workItemId}", {
1667
+ params: {
1668
+ header: {
1669
+ "idempotency-key": idempotencyKey,
1670
+ "if-match": options.expectedVersion,
1671
+ },
1672
+ path: {
1673
+ projectId: options.projectId,
1674
+ workItemId: options.workItemId,
1675
+ },
1676
+ },
1677
+ }), { deleted: { workItemId: options.workItemId } }), {
1678
+ ambiguousOutcomeMessage: "Work Item deletion outcome is unknown; list Work Items before retrying",
1679
+ retryTransportOnce: true,
1680
+ });
1681
+ });
1682
+ workItemSelectionFlags(workItem.command("board"))
1683
+ .requiredOption("--project-id <projectId>")
1684
+ .action(async (options) => {
1685
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-board", {
1686
+ params: {
1687
+ path: { projectId: options.projectId },
1688
+ query: workItemSelectionQuery(options),
1689
+ },
1690
+ }));
1691
+ });
1692
+ workItem
1693
+ .command("bulk")
1694
+ .requiredOption("--project-id <projectId>")
1695
+ .requiredOption("--action <action>", `Bulk action: ${workItemBulkActions.join(", ")}`, parseWorkItemBulkAction)
1696
+ .requiredOption("--target <id@version>", "Target Work Item ID with its observed version, e.g. PAY-1@3; repeat for several Projects in the same Workspace", parseWorkItemBulkTarget, [])
1697
+ .option("--status <status>", "Destination Workflow Column slug or virtual status (updateStatus)")
1698
+ .option("--priority <priority>", "Priority (updatePriority)", parseWorkItemPriority)
1699
+ .option("--assignee <assignee>", "Assignee User ID (updateAssignee)")
1700
+ .option("--unassign", "Clear the assignee (updateAssignee)")
1701
+ .option("--label-id <labelId>", "Work Item Label ID (addLabel/removeLabel)")
1702
+ .option("--due-date <dueDate>", "Due date (updateDueDate)", parseWorkItemDate)
1703
+ .option("--clear-due-date", "Clear the due date (updateDueDate)")
1704
+ .description("Apply one action to several Work Items, which may span Projects inside the same Workspace")
1705
+ .action(async (options) => {
1706
+ if (hasProjectReadToken(dependencies.environment)) {
1707
+ exitCode = rejectTokenWrite(dependencies);
1708
+ return;
1709
+ }
1710
+ if (options.unassign === true && options.assignee !== undefined) {
1711
+ writeSkillError(dependencies, {
1712
+ message: "--assignee and --unassign cannot be combined for updateAssignee",
1713
+ reason: "Invalid Operation",
1714
+ retryable: false,
1715
+ });
1716
+ exitCode = 1;
1717
+ return;
1718
+ }
1719
+ if (options.clearDueDate === true && options.dueDate !== undefined) {
1720
+ writeSkillError(dependencies, {
1721
+ message: "--due-date and --clear-due-date cannot be combined for updateDueDate",
1722
+ reason: "Invalid Operation",
1723
+ retryable: false,
1724
+ });
1725
+ exitCode = 1;
1726
+ return;
1727
+ }
1728
+ const idempotencyKey = randomUUID();
1729
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/bulk", {
1730
+ body: {
1731
+ action: options.action,
1732
+ ...(options.action === "updateAssignee"
1733
+ ? options.unassign === true
1734
+ ? { assigneeId: null }
1735
+ : options.assignee === undefined
1736
+ ? {}
1737
+ : { assigneeId: options.assignee }
1738
+ : {}),
1739
+ ...(options.action === "updateDueDate"
1740
+ ? options.clearDueDate === true
1741
+ ? { dueDate: null }
1742
+ : options.dueDate === undefined
1743
+ ? {}
1744
+ : { dueDate: options.dueDate }
1745
+ : {}),
1746
+ ...((options.action === "addLabel" ||
1747
+ options.action === "removeLabel") &&
1748
+ options.labelId !== undefined
1749
+ ? { labelId: options.labelId }
1750
+ : {}),
1751
+ ...(options.action === "updatePriority" &&
1752
+ options.priority !== undefined
1753
+ ? { priority: options.priority }
1754
+ : {}),
1755
+ ...(options.action === "updateStatus" &&
1756
+ options.status !== undefined
1757
+ ? { status: options.status }
1758
+ : {}),
1759
+ workItems: options.target.map((target) => ({
1760
+ id: target.id,
1761
+ version: target.expectedVersion,
1762
+ })),
1763
+ },
1764
+ params: {
1765
+ header: { "idempotency-key": idempotencyKey },
1766
+ path: { projectId: options.projectId },
1767
+ },
1768
+ }), {
1769
+ ambiguousOutcomeMessage: "Work Item bulk outcome is unknown; list Work Items before retrying",
1770
+ retryTransportOnce: true,
1771
+ });
1772
+ });
1773
+ workItem
1774
+ .command("import")
1775
+ .description("Import JSON tasks into the explicit Project; imported labels are reported as skipped")
1776
+ .requiredOption("--project-id <projectId>")
1777
+ .requiredOption("--file <path>", "JSON task file, at most 1 MiB and 1000 tasks")
1778
+ .requiredOption("--idempotency-key <key>", "Reuse this key with the same file when retrying")
1779
+ .action(async (options) => {
1780
+ if (hasProjectReadToken(dependencies.environment)) {
1781
+ exitCode = rejectTokenWrite(dependencies);
1782
+ return;
1783
+ }
1784
+ const key = options.idempotencyKey.trim();
1785
+ if (key.length === 0 || key.length > 200) {
1786
+ throw invalidArgument("The import idempotency key must contain 1 to 200 characters");
1787
+ }
1788
+ const body = await readWorkItemImportFile(options.file);
1789
+ if (body === null) {
1790
+ writeSkillError(dependencies, {
1791
+ message: "The import file must be valid task JSON within 1 MiB and 1000 tasks",
1792
+ reason: "Invalid Operation", retryable: false,
1793
+ });
1794
+ exitCode = 2;
1795
+ return;
1796
+ }
1797
+ let failed = 0;
1798
+ exitCode = await run(async (client) => {
1799
+ const result = await client.POST("/v1/projects/{projectId}/work-items/import", {
1800
+ body, params: { header: { "idempotency-key": key }, path: { projectId: options.projectId } },
1801
+ });
1802
+ if (result.data !== undefined) {
1803
+ failed = result.data.results.failed;
1804
+ }
1805
+ return result;
1806
+ }, {
1807
+ ambiguousOutcomeMessage: "Import outcome is unknown; retry the same file and idempotency key",
1808
+ retryTransportOnce: true,
1809
+ });
1810
+ if (exitCode === 0 && failed > 0) {
1811
+ writeSkillError(dependencies, {
1812
+ message: "Some tasks were refused; inspect the per-row results and import only corrected failed rows with a new key",
1813
+ reason: "Invalid Operation", retryable: false,
1814
+ });
1815
+ exitCode = 2;
1816
+ }
1817
+ });
1818
+ workItemSelectionFlags(workItem.command("export"))
1819
+ .description("Export selected readable Project tasks as JSON on stdout")
1820
+ .requiredOption("--project-id <projectId>")
1821
+ .action(async (options) => {
1822
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/export", {
1823
+ params: { path: { projectId: options.projectId }, query: workItemSelectionQuery(options) },
1824
+ }));
1825
+ });
1826
+ workItem
1827
+ .command("transfer")
1828
+ .description("Move a Work Item into another Project in its Workspace")
1829
+ .requiredOption("--project-id <projectId>")
1830
+ .requiredOption("--work-item-id <workItemId>")
1831
+ .requiredOption("--destination-project-id <projectId>")
1832
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1833
+ .option("--destination-status <status>", "Destination Workflow Column slug; defaults to the matching or first column")
1834
+ .action(async (options) => {
1835
+ if (hasProjectReadToken(dependencies.environment)) {
1836
+ exitCode = rejectTokenWrite(dependencies);
1837
+ return;
1838
+ }
1839
+ const idempotencyKey = randomUUID();
1840
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/transfer", {
1841
+ body: {
1842
+ destinationProjectId: options.destinationProjectId,
1843
+ ...(options.destinationStatus === undefined ? {} : { destinationStatus: options.destinationStatus }),
1844
+ },
1845
+ params: {
1846
+ header: { "idempotency-key": idempotencyKey, "if-match": options.expectedVersion },
1847
+ path: { projectId: options.projectId, workItemId: options.workItemId },
1848
+ },
1849
+ }), {
1850
+ ambiguousOutcomeMessage: "Work Item transfer outcome is unknown; inspect both Projects before retrying",
1851
+ retryTransportOnce: true,
1852
+ });
1853
+ });
1854
+ workItem
1855
+ .command("move")
1856
+ .requiredOption("--project-id <projectId>")
1857
+ .requiredOption("--work-item-id <workItemId>")
1858
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1859
+ .requiredOption("--status <status>", "Destination Workflow Column slug or virtual status")
1860
+ .option("--position <position>", "Board index inside the destination column", parseBoardPosition)
1861
+ .action(async (options) => {
1862
+ if (hasProjectReadToken(dependencies.environment)) {
1863
+ exitCode = rejectTokenWrite(dependencies);
1864
+ return;
1865
+ }
1866
+ const idempotencyKey = randomUUID();
1867
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/move", {
1868
+ body: {
1869
+ ...(options.position === undefined
1870
+ ? {}
1871
+ : { position: options.position }),
1872
+ status: options.status,
1873
+ },
1874
+ params: {
1875
+ header: {
1876
+ "idempotency-key": idempotencyKey,
1877
+ "if-match": options.expectedVersion,
1878
+ },
1879
+ path: {
1880
+ projectId: options.projectId,
1881
+ workItemId: options.workItemId,
1882
+ },
1883
+ },
1884
+ }), {
1885
+ ambiguousOutcomeMessage: "Work Item move outcome is unknown; read the Work Item board before retrying",
1886
+ retryTransportOnce: true,
1887
+ });
1888
+ });
1889
+ workItem
1890
+ .command("complete")
1891
+ .requiredOption("--project-id <projectId>")
1892
+ .requiredOption("--work-item-id <workItemId>")
1893
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1894
+ .description("Move a Work Item into the Project's first final Workflow Column")
1895
+ .action(async (options) => {
1896
+ if (hasProjectReadToken(dependencies.environment)) {
1897
+ exitCode = rejectTokenWrite(dependencies);
1898
+ return;
1899
+ }
1900
+ const idempotencyKey = randomUUID();
1901
+ exitCode = await run(async (client) => {
1902
+ const board = await client.GET("/v1/projects/{projectId}/work-item-board", { params: { path: { projectId: options.projectId } } });
1903
+ if (!board.response.ok) {
1904
+ return board;
1905
+ }
1906
+ const finalColumn = board.data?.workflowColumns.find((column) => column.isFinal);
1907
+ if (finalColumn === undefined) {
1908
+ return {
1909
+ error: {
1910
+ code: "WORKFLOW_COLUMN_NO_FINAL",
1911
+ message: "The Project has no final Workflow Column; mark a column final or move the Work Item to its slug directly",
1912
+ },
1913
+ response: new Response(null, { status: 422 }),
1914
+ };
1915
+ }
1916
+ return await client.POST("/v1/projects/{projectId}/work-items/{workItemId}/move", {
1917
+ body: { status: finalColumn.slug },
1918
+ params: {
1919
+ header: {
1920
+ "idempotency-key": idempotencyKey,
1921
+ "if-match": options.expectedVersion,
1922
+ },
1923
+ path: {
1924
+ projectId: options.projectId,
1925
+ workItemId: options.workItemId,
1926
+ },
1927
+ },
1928
+ });
1929
+ }, {
1930
+ ambiguousOutcomeMessage: "Work Item completion outcome is unknown; read the Work Item board before retrying",
1931
+ retryTransportOnce: true,
1932
+ });
1933
+ });
1934
+ const workItemLabel = program.command("work-item-label");
1935
+ workItemLabel
1936
+ .command("list")
1937
+ .requiredOption("--project-id <projectId>")
1938
+ .option("--work-item-id <workItemId>", "List only the labels attached to one Work Item")
1939
+ .action(async (options) => {
1940
+ exitCode = await run((client) => options.workItemId === undefined
1941
+ ? client.GET("/v1/projects/{projectId}/work-item-labels", {
1942
+ params: { path: { projectId: options.projectId } },
1943
+ })
1944
+ : client.GET("/v1/projects/{projectId}/work-items/{workItemId}/labels", {
1945
+ params: {
1946
+ path: {
1947
+ projectId: options.projectId,
1948
+ workItemId: options.workItemId,
1949
+ },
1950
+ },
1951
+ }));
1952
+ });
1953
+ workItemLabel
1954
+ .command("read")
1955
+ .requiredOption("--project-id <projectId>")
1956
+ .requiredOption("--label-id <labelId>")
1957
+ .action(async (options) => {
1958
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-labels/{labelId}", { params: { path: options } }));
1959
+ });
1960
+ workItemLabel
1961
+ .command("create")
1962
+ .requiredOption("--project-id <projectId>")
1963
+ .requiredOption("--name <name>")
1964
+ .requiredOption("--color <color>")
1965
+ .option("--work-item-id <workItemId>", "Create the label directly on the named Work Item instead of the Workspace definition")
1966
+ .action(async (options) => {
1967
+ if (hasProjectReadToken(dependencies.environment)) {
1968
+ exitCode = rejectTokenWrite(dependencies);
1969
+ return;
1970
+ }
1971
+ const idempotencyKey = randomUUID();
1972
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-item-labels", {
1973
+ body: {
1974
+ color: options.color,
1975
+ name: options.name,
1976
+ ...(options.workItemId === undefined
1977
+ ? {}
1978
+ : { workItemId: options.workItemId }),
1979
+ },
1980
+ params: {
1981
+ header: { "idempotency-key": idempotencyKey },
1982
+ path: { projectId: options.projectId },
1983
+ },
1984
+ }), {
1985
+ ambiguousOutcomeMessage: "Work Item Label creation outcome is unknown; list Work Item Labels before retrying",
1986
+ retryTransportOnce: true,
1987
+ });
1988
+ });
1989
+ workItemLabel
1990
+ .command("update")
1991
+ .requiredOption("--project-id <projectId>")
1992
+ .requiredOption("--label-id <labelId>")
1993
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1994
+ .option("--name <name>")
1995
+ .option("--color <color>")
1996
+ .action(async (options) => {
1997
+ if (hasProjectReadToken(dependencies.environment)) {
1998
+ exitCode = rejectTokenWrite(dependencies);
1999
+ return;
2000
+ }
2001
+ if (options.name === undefined && options.color === undefined) {
2002
+ throw invalidArgument("Work Item Label update requires a field change");
2003
+ }
2004
+ const idempotencyKey = randomUUID();
2005
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/work-item-labels/{labelId}", {
2006
+ body: {
2007
+ ...(options.color === undefined
2008
+ ? {}
2009
+ : { color: options.color }),
2010
+ ...(options.name === undefined ? {} : { name: options.name }),
2011
+ },
2012
+ params: {
2013
+ header: {
2014
+ "idempotency-key": idempotencyKey,
2015
+ "if-match": options.expectedVersion,
2016
+ },
2017
+ path: {
2018
+ labelId: options.labelId,
2019
+ projectId: options.projectId,
2020
+ },
2021
+ },
2022
+ }), {
2023
+ ambiguousOutcomeMessage: "Work Item Label update outcome is unknown; read the Work Item Label before retrying",
2024
+ retryTransportOnce: true,
2025
+ });
2026
+ });
2027
+ workItemLabel
2028
+ .command("delete")
2029
+ .requiredOption("--project-id <projectId>")
2030
+ .requiredOption("--label-id <labelId>")
2031
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2032
+ .description("Delete a Work Item Label; a Workspace definition's task-copy cascade resumes automatically across bounded batches")
2033
+ .action(async (options) => {
2034
+ if (hasProjectReadToken(dependencies.environment)) {
2035
+ exitCode = rejectTokenWrite(dependencies);
2036
+ return;
2037
+ }
2038
+ const idempotencyKey = randomUUID();
2039
+ exitCode = await run(async (client) => {
2040
+ const request = () => client.DELETE("/v1/projects/{projectId}/work-item-labels/{labelId}", {
2041
+ params: {
2042
+ header: {
2043
+ "idempotency-key": idempotencyKey,
2044
+ "if-match": options.expectedVersion,
2045
+ },
2046
+ path: {
2047
+ labelId: options.labelId,
2048
+ projectId: options.projectId,
2049
+ },
2050
+ },
2051
+ });
2052
+ let result = await request();
2053
+ // HTTP 202 marks a definition cascade still in flight; the same
2054
+ // request resumes it, bounded so a runaway copy count stops.
2055
+ for (let attempt = 0; result.response.status === 202 && attempt < 40; attempt += 1) {
2056
+ result = await request();
2057
+ }
2058
+ return withSuccessData(result, {
2059
+ deleted: { labelId: options.labelId },
2060
+ });
2061
+ }, {
2062
+ ambiguousOutcomeMessage: "Work Item Label deletion outcome is unknown; list Work Item Labels before retrying",
2063
+ retryTransportOnce: true,
2064
+ // A bounded 202 stream that never completes is not a success —
2065
+ // report the resumable deletion as retryable instead of printing
2066
+ // the pending payload as if it were done.
2067
+ validateSuccess: (data) => isWorkItemLabelDeletionPending(data)
2068
+ ? {
2069
+ message: "Work Item Label deletion is still in progress; run the same command again to resume it",
2070
+ reason: "Temporary Failure",
2071
+ retryable: true,
2072
+ }
2073
+ : null,
2074
+ });
2075
+ });
2076
+ workItemLabel
2077
+ .command("attach")
2078
+ .requiredOption("--project-id <projectId>")
2079
+ .requiredOption("--work-item-id <workItemId>")
2080
+ .requiredOption("--label-id <labelId>")
2081
+ .option("--expected-version <expectedVersion>", "Opaque version of the task copy being moved; required when the Label is already attached elsewhere", parseOpaqueVersion)
2082
+ .description("Attach a Work Item Label to a Work Item as a task-level copy")
2083
+ .action(async (options) => {
2084
+ if (hasProjectReadToken(dependencies.environment)) {
2085
+ exitCode = rejectTokenWrite(dependencies);
2086
+ return;
2087
+ }
2088
+ const idempotencyKey = randomUUID();
2089
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-items/{workItemId}/labels/{labelId}", {
2090
+ params: {
2091
+ header: {
2092
+ "idempotency-key": idempotencyKey,
2093
+ ...(options.expectedVersion === undefined
2094
+ ? {}
2095
+ : { "if-match": options.expectedVersion }),
2096
+ },
2097
+ path: {
2098
+ labelId: options.labelId,
2099
+ projectId: options.projectId,
2100
+ workItemId: options.workItemId,
2101
+ },
2102
+ },
2103
+ }), {
2104
+ ambiguousOutcomeMessage: "Work Item Label attachment outcome is unknown; list the Work Item's labels before retrying",
2105
+ retryTransportOnce: true,
2106
+ });
2107
+ });
2108
+ workItemLabel
2109
+ .command("detach")
2110
+ .requiredOption("--project-id <projectId>")
2111
+ .requiredOption("--work-item-id <workItemId>")
2112
+ .requiredOption("--label-id <labelId>")
2113
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow for the attached copy", parseOpaqueVersion)
2114
+ .description("Detach a Work Item Label copy from a Work Item")
2115
+ .action(async (options) => {
2116
+ if (hasProjectReadToken(dependencies.environment)) {
2117
+ exitCode = rejectTokenWrite(dependencies);
2118
+ return;
2119
+ }
2120
+ const idempotencyKey = randomUUID();
2121
+ exitCode = await run((client) => client.DELETE("/v1/projects/{projectId}/work-items/{workItemId}/labels/{labelId}", {
2122
+ params: {
2123
+ header: {
2124
+ "idempotency-key": idempotencyKey,
2125
+ "if-match": options.expectedVersion,
2126
+ },
2127
+ path: {
2128
+ labelId: options.labelId,
2129
+ projectId: options.projectId,
2130
+ workItemId: options.workItemId,
2131
+ },
2132
+ },
2133
+ }), {
2134
+ ambiguousOutcomeMessage: "Work Item Label detachment outcome is unknown; list the Work Item's labels before retrying",
2135
+ retryTransportOnce: true,
2136
+ });
2137
+ });
2138
+ const workItemCustomField = program.command("work-item-custom-field");
2139
+ workItemCustomField
2140
+ .command("list")
2141
+ .requiredOption("--project-id <projectId>")
2142
+ .action(async (options) => {
2143
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-custom-fields", {
2144
+ params: { path: options },
2145
+ }));
2146
+ });
2147
+ workItemCustomField
2148
+ .command("create")
2149
+ .requiredOption("--project-id <projectId>")
2150
+ .requiredOption("--name <name>")
2151
+ .requiredOption("--type <type>", "text, number, date, dropdown, or boolean", parseWorkItemCustomFieldType)
2152
+ .option("--option <option>", "Dropdown option; repeat for every choice", collectOption, [])
2153
+ .option("--default-value <defaultValue>")
2154
+ .option("--required", "Reject Work Items that leave the field blank")
2155
+ .action(async (options) => {
2156
+ if (hasProjectReadToken(dependencies.environment)) {
2157
+ exitCode = rejectTokenWrite(dependencies);
2158
+ return;
2159
+ }
2160
+ const idempotencyKey = randomUUID();
2161
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-item-custom-fields", {
2162
+ body: {
2163
+ ...(options.defaultValue === undefined
2164
+ ? {}
2165
+ : { defaultValue: options.defaultValue }),
2166
+ name: options.name,
2167
+ ...(options.option.length === 0
2168
+ ? {}
2169
+ : { options: options.option }),
2170
+ ...(options.required === undefined
2171
+ ? {}
2172
+ : { required: options.required }),
2173
+ type: options.type,
2174
+ },
2175
+ params: {
2176
+ header: { "idempotency-key": idempotencyKey },
2177
+ path: { projectId: options.projectId },
2178
+ },
2179
+ }), {
2180
+ ambiguousOutcomeMessage: "Work Item Custom Field creation outcome is unknown; list Work Item Custom Fields before retrying",
2181
+ retryTransportOnce: true,
2182
+ });
2183
+ });
2184
+ workItemCustomField
2185
+ .command("delete")
2186
+ .requiredOption("--project-id <projectId>")
2187
+ .requiredOption("--field-id <fieldId>")
2188
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2189
+ .description("Delete a Work Item Custom Field; its stored values cascade with the definition")
2190
+ .action(async (options) => {
2191
+ if (hasProjectReadToken(dependencies.environment)) {
2192
+ exitCode = rejectTokenWrite(dependencies);
2193
+ return;
2194
+ }
2195
+ const idempotencyKey = randomUUID();
2196
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/work-item-custom-fields/{fieldId}", {
2197
+ params: {
2198
+ header: {
2199
+ "idempotency-key": idempotencyKey,
2200
+ "if-match": options.expectedVersion,
2201
+ },
2202
+ path: {
2203
+ fieldId: options.fieldId,
2204
+ projectId: options.projectId,
2205
+ },
2206
+ },
2207
+ }), { deleted: { fieldId: options.fieldId } }), {
2208
+ ambiguousOutcomeMessage: "Work Item Custom Field deletion outcome is unknown; list Work Item Custom Fields before retrying",
2209
+ retryTransportOnce: true,
2210
+ });
2211
+ });
2212
+ workItemCustomField
2213
+ .command("reorder")
2214
+ .requiredOption("--project-id <projectId>")
2215
+ .requiredOption("--order <order>", "Comma-separated id@version pairs in display order — version is the integer inside the field's opaque \"N\" version")
2216
+ .action(async (options) => {
2217
+ if (hasProjectReadToken(dependencies.environment)) {
2218
+ exitCode = rejectTokenWrite(dependencies);
2219
+ return;
2220
+ }
2221
+ const fields = parseFieldOrder(options.order);
2222
+ const idempotencyKey = randomUUID();
2223
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-item-custom-fields/reorder", {
2224
+ body: { fields },
2225
+ params: {
2226
+ header: { "idempotency-key": idempotencyKey },
2227
+ path: { projectId: options.projectId },
2228
+ },
2229
+ }), {
2230
+ ambiguousOutcomeMessage: "Work Item Custom Field reorder outcome is unknown; list Work Item Custom Fields before retrying",
2231
+ retryTransportOnce: true,
2232
+ });
2233
+ });
2234
+ workItemCustomField
2235
+ .command("values")
2236
+ .requiredOption("--project-id <projectId>")
2237
+ .option("--work-item-id <workItemId>", "List only the values stored on one Work Item")
2238
+ .action(async (options) => {
2239
+ exitCode = await run((client) => options.workItemId === undefined
2240
+ ? client.GET("/v1/projects/{projectId}/work-item-custom-field-values", {
2241
+ params: { path: { projectId: options.projectId } },
2242
+ })
2243
+ : client.GET("/v1/projects/{projectId}/work-items/{workItemId}/custom-field-values", {
2244
+ params: {
2245
+ path: {
2246
+ projectId: options.projectId,
2247
+ workItemId: options.workItemId,
2248
+ },
2249
+ },
2250
+ }));
2251
+ });
2252
+ workItemCustomField
2253
+ .command("filter-values")
2254
+ .requiredOption("--project-id <projectId>")
2255
+ .action(async (options) => {
2256
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-item-custom-field-filter-values", { params: { path: options } }));
2257
+ });
2258
+ workItemCustomField
2259
+ .command("set")
2260
+ .requiredOption("--project-id <projectId>")
2261
+ .requiredOption("--work-item-id <workItemId>")
2262
+ .requiredOption("--field-id <fieldId>")
2263
+ .requiredOption("--expected-version <version>", "The opaque Work Item version returned by SpecFlow")
2264
+ .requiredOption("--value <value>", "Stored value; an empty string clears a non-required field")
2265
+ .action(async (options) => {
2266
+ if (hasProjectReadToken(dependencies.environment)) {
2267
+ exitCode = rejectTokenWrite(dependencies);
2268
+ return;
2269
+ }
2270
+ const expectedVersion = parseOpaqueVersion(options.expectedVersion);
2271
+ const idempotencyKey = randomUUID();
2272
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-items/{workItemId}/custom-field-values/{fieldId}", {
2273
+ body: { value: options.value },
2274
+ params: {
2275
+ header: {
2276
+ "idempotency-key": idempotencyKey,
2277
+ "if-match": expectedVersion,
2278
+ },
2279
+ path: {
2280
+ fieldId: options.fieldId,
2281
+ projectId: options.projectId,
2282
+ workItemId: options.workItemId,
2283
+ },
2284
+ },
2285
+ }), {
2286
+ ambiguousOutcomeMessage: "Work Item Custom Field value outcome is unknown; list the Work Item's Custom Field values before retrying",
2287
+ retryTransportOnce: true,
2288
+ });
2289
+ });
2290
+ const workflowColumn = program.command("workflow-column");
2291
+ workflowColumn
2292
+ .command("list")
2293
+ .requiredOption("--project-id <projectId>")
2294
+ .action(async (options) => {
2295
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/workflow-columns", {
2296
+ params: { path: options },
2297
+ }));
2298
+ });
2299
+ workflowColumn
2300
+ .command("create")
2301
+ .requiredOption("--project-id <projectId>")
2302
+ .requiredOption("--name <name>")
2303
+ .option("--icon <icon>", "Column icon name")
2304
+ .option("--color <color>", "Column color")
2305
+ .option("--final", "Mark the column as an explicit completion lane")
2306
+ .action(async (options) => {
2307
+ if (hasProjectReadToken(dependencies.environment)) {
2308
+ exitCode = rejectTokenWrite(dependencies);
2309
+ return;
2310
+ }
2311
+ const idempotencyKey = randomUUID();
2312
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/workflow-columns", {
2313
+ body: {
2314
+ ...(options.color === undefined
2315
+ ? {}
2316
+ : { color: options.color }),
2317
+ ...(options.final === undefined
2318
+ ? {}
2319
+ : { isFinal: options.final }),
2320
+ ...(options.icon === undefined ? {} : { icon: options.icon }),
2321
+ name: options.name,
2322
+ },
2323
+ params: {
2324
+ header: { "idempotency-key": idempotencyKey },
2325
+ path: { projectId: options.projectId },
2326
+ },
2327
+ }), {
2328
+ ambiguousOutcomeMessage: "Workflow Column creation outcome is unknown; list Workflow Columns before retrying",
2329
+ retryTransportOnce: true,
2330
+ });
2331
+ });
2332
+ workflowColumn
2333
+ .command("update")
2334
+ .requiredOption("--project-id <projectId>")
2335
+ .requiredOption("--column-slug <columnSlug>")
2336
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2337
+ .option("--name <name>")
2338
+ .option("--icon <icon>", "Column icon name")
2339
+ .option("--color <color>", "Column color")
2340
+ .option("--clear-icon", "Remove the column icon")
2341
+ .option("--clear-color", "Remove the column color")
2342
+ .option("--final", "Mark the column as an explicit completion lane")
2343
+ .option("--not-final", "Remove the column's completion marker")
2344
+ .action(async (options) => {
2345
+ if (hasProjectReadToken(dependencies.environment)) {
2346
+ exitCode = rejectTokenWrite(dependencies);
2347
+ return;
2348
+ }
2349
+ if (options.name === undefined &&
2350
+ options.icon === undefined &&
2351
+ options.clearIcon !== true &&
2352
+ options.color === undefined &&
2353
+ options.clearColor !== true &&
2354
+ options.final !== true &&
2355
+ options.notFinal !== true) {
2356
+ throw invalidArgument("Workflow Column update requires a field change");
2357
+ }
2358
+ if (options.clearIcon === true && options.icon !== undefined) {
2359
+ throw invalidArgument("Workflow Column update cannot set and clear the icon together");
2360
+ }
2361
+ if (options.clearColor === true && options.color !== undefined) {
2362
+ throw invalidArgument("Workflow Column update cannot set and clear the color together");
2363
+ }
2364
+ if (options.final === true && options.notFinal === true) {
2365
+ throw invalidArgument("Workflow Column update cannot set and clear the final marker together");
2366
+ }
2367
+ const idempotencyKey = randomUUID();
2368
+ exitCode = await run((client) => client.PATCH("/v1/projects/{projectId}/workflow-columns/{columnSlug}", {
2369
+ body: {
2370
+ ...(options.clearColor === true
2371
+ ? { color: null }
2372
+ : options.color === undefined
2373
+ ? {}
2374
+ : { color: options.color }),
2375
+ ...(options.clearIcon === true
2376
+ ? { icon: null }
2377
+ : options.icon === undefined
2378
+ ? {}
2379
+ : { icon: options.icon }),
2380
+ ...(options.final === true
2381
+ ? { isFinal: true }
2382
+ : options.notFinal === true
2383
+ ? { isFinal: false }
2384
+ : {}),
2385
+ ...(options.name === undefined
2386
+ ? {}
2387
+ : { name: options.name }),
2388
+ },
2389
+ params: {
2390
+ header: {
2391
+ "idempotency-key": idempotencyKey,
2392
+ "if-match": options.expectedVersion,
2393
+ },
2394
+ path: {
2395
+ columnSlug: options.columnSlug,
2396
+ projectId: options.projectId,
2397
+ },
2398
+ },
2399
+ }), {
2400
+ ambiguousOutcomeMessage: "Workflow Column update outcome is unknown; list Workflow Columns before retrying",
2401
+ retryTransportOnce: true,
2402
+ });
2403
+ });
2404
+ workflowColumn
2405
+ .command("delete")
2406
+ .requiredOption("--project-id <projectId>")
2407
+ .requiredOption("--column-slug <columnSlug>")
2408
+ .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
2409
+ .action(async (options) => {
2410
+ if (hasProjectReadToken(dependencies.environment)) {
2411
+ exitCode = rejectTokenWrite(dependencies);
2412
+ return;
2413
+ }
2414
+ const idempotencyKey = randomUUID();
2415
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/workflow-columns/{columnSlug}", {
2416
+ params: {
2417
+ header: {
2418
+ "idempotency-key": idempotencyKey,
2419
+ "if-match": options.expectedVersion,
2420
+ },
2421
+ path: {
2422
+ columnSlug: options.columnSlug,
2423
+ projectId: options.projectId,
2424
+ },
2425
+ },
2426
+ }), { deleted: { columnSlug: options.columnSlug } }), {
2427
+ ambiguousOutcomeMessage: "Workflow Column deletion outcome is unknown; list Workflow Columns before retrying",
2428
+ retryTransportOnce: true,
2429
+ });
2430
+ });
2431
+ workflowColumn
2432
+ .command("reorder")
2433
+ .requiredOption("--project-id <projectId>")
2434
+ .requiredOption("--order <order>", "Comma-separated slug@version pairs in board order — version is the integer inside the column's opaque \"N\" version")
2435
+ .action(async (options) => {
2436
+ if (hasProjectReadToken(dependencies.environment)) {
2437
+ exitCode = rejectTokenWrite(dependencies);
2438
+ return;
2439
+ }
2440
+ const columns = parseColumnOrder(options.order);
2441
+ const idempotencyKey = randomUUID();
2442
+ exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/workflow-column-order", {
2443
+ body: { columns },
2444
+ params: {
2445
+ header: { "idempotency-key": idempotencyKey },
2446
+ path: { projectId: options.projectId },
2447
+ },
2448
+ }), {
2449
+ ambiguousOutcomeMessage: "Workflow Column reorder outcome is unknown; list Workflow Columns before retrying",
2450
+ retryTransportOnce: true,
2451
+ });
2452
+ });
2453
+ const token = program.command("token");
2454
+ token
2455
+ .command("issue")
2456
+ .requiredOption("--project-id <projectId>")
2457
+ .requiredOption("--name <name>")
2458
+ .option("--expires-at <expiresAt>", "ISO 8601 expiry date-time", parseIsoDateTime)
2459
+ .action(async (options) => {
2460
+ if (hasProjectReadToken(dependencies.environment)) {
2461
+ exitCode = rejectTokenWrite(dependencies);
2462
+ return;
2463
+ }
2464
+ const idempotencyKey = randomUUID();
2465
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/read-tokens", {
2466
+ body: {
2467
+ expiresAt: options.expiresAt ?? null,
2468
+ name: options.name,
2469
+ },
2470
+ params: {
2471
+ header: { "idempotency-key": idempotencyKey },
2472
+ path: { projectId: options.projectId },
2473
+ },
2474
+ }), {
2475
+ ambiguousOutcomeMessage: "Project Read Token issuance outcome is unknown; list tokens before retrying",
2476
+ retryTransportOnce: true,
2477
+ validateSuccess: validateIssuedProjectReadToken,
2478
+ });
2479
+ });
2480
+ token
2481
+ .command("list")
2482
+ .requiredOption("--project-id <projectId>")
2483
+ .action(async (options) => {
2484
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/read-tokens", {
2485
+ params: { path: { projectId: options.projectId } },
2486
+ }), { rejectProjectReadToken: true });
2487
+ });
2488
+ token
2489
+ .command("revoke")
2490
+ .requiredOption("--project-id <projectId>")
2491
+ .requiredOption("--token-id <tokenId>")
2492
+ .action(async (options) => {
2493
+ if (hasProjectReadToken(dependencies.environment)) {
2494
+ exitCode = rejectTokenWrite(dependencies);
2495
+ return;
2496
+ }
2497
+ exitCode = await run(async (client) => withSuccessData(await client.DELETE("/v1/projects/{projectId}/read-tokens/{tokenId}", { params: { path: options } }), { revoked: { tokenId: options.tokenId } }), {
2498
+ ambiguousOutcomeMessage: "Project Read Token revocation outcome is unknown; list tokens before retrying",
2499
+ retryTransportOnce: true,
2500
+ });
2501
+ });
2502
+ const review = program.command("review");
2503
+ review
2504
+ .command("request")
2505
+ .requiredOption("--project-id <projectId>")
2506
+ .requiredOption("--spec-id <specId>")
2507
+ .action(async (options) => {
2508
+ if (hasProjectReadToken(dependencies.environment)) {
2509
+ exitCode = rejectTokenWrite(dependencies);
2510
+ return;
2511
+ }
2512
+ const idempotencyKey = randomUUID();
2513
+ exitCode = await run((client) => client.POST("/v1/projects/{projectId}/specs/{specId}/reviews", {
2514
+ params: {
2515
+ header: { "idempotency-key": idempotencyKey },
2516
+ path: options,
2517
+ },
2518
+ }), {
2519
+ ambiguousOutcomeMessage: "Manual Review request outcome is unknown; inspect Review Records before retrying",
2520
+ retryTransportOnce: true,
2521
+ });
2522
+ });
2523
+ review
2524
+ .command("read")
2525
+ .requiredOption("--project-id <projectId>")
2526
+ .requiredOption("--review-id <reviewId>")
2527
+ .action(async (options) => {
2528
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/reviews/{reviewId}", {
2529
+ params: { path: options },
2530
+ }), { rejectProjectReadToken: true });
2531
+ });
2532
+ try {
2533
+ await program.parseAsync([...arguments_], { from: "user" });
2534
+ return exitCode;
2535
+ }
2536
+ catch (cause) {
2537
+ if (cause instanceof CommanderError) {
2538
+ if (cause.code === "commander.helpDisplayed") {
2539
+ return cause.exitCode;
2540
+ }
2541
+ writeSkillError(dependencies, {
2542
+ message: "The command does not match the supported CLI contract",
2543
+ reason: "Invalid Operation",
2544
+ retryable: false,
2545
+ });
2546
+ return 2;
2547
+ }
2548
+ throw cause;
2549
+ }
2550
+ }
2551
+ async function executeRequest(dependencies, request, options = {}, serverFlag, apiKeyIdFlag) {
2552
+ const environmentCredential = dependencies.environment.SPEC_FLOW_PROJECT_READ_TOKEN;
2553
+ const apiKeyEnvironment = dependencies.environment.SPEC_FLOW_API_KEY;
2554
+ const usesApiKey = apiKeyEnvironment !== undefined || apiKeyIdFlag !== undefined;
2555
+ if (options.requireUserSession === true && (usesApiKey || hasProjectReadToken(dependencies.environment))) {
2556
+ writeSkillError(dependencies, { message: "A User Session is required for account management", reason: "Permission Denied", retryable: false });
2557
+ return 3;
2558
+ }
2559
+ if ((apiKeyEnvironment !== undefined && apiKeyIdFlag !== undefined) || (usesApiKey && hasProjectReadToken(dependencies.environment))) {
2560
+ writeSkillError(dependencies, { message: "Select exactly one API Key or Project Read Token credential", reason: "Invalid Operation", retryable: false });
2561
+ return 2;
2562
+ }
2563
+ if (apiKeyIdFlag !== undefined && !z.uuid().safeParse(apiKeyIdFlag).success) {
2564
+ writeSkillError(dependencies, { message: "API Key ID must be a UUID", reason: "Invalid Operation", retryable: false });
2565
+ return 2;
2566
+ }
2567
+ if (apiKeyEnvironment !== undefined && !/^sfk_[A-Za-z0-9_-]{43}$/u.test(apiKeyEnvironment)) {
2568
+ writeSkillError(dependencies, { message: "SPEC_FLOW_API_KEY does not contain a User API Key", reason: "Authentication Failed", retryable: false });
2569
+ return 2;
2570
+ }
2571
+ if (options.rejectProjectReadToken === true &&
2572
+ hasProjectReadToken(dependencies.environment)) {
2573
+ return rejectProjectReadTokenOperation(dependencies);
2574
+ }
2575
+ const baseUrl = resolveApiOrigin(dependencies, serverFlag);
2576
+ if (baseUrl === null) {
2577
+ return 2;
2578
+ }
2579
+ let credential;
2580
+ try {
2581
+ if (apiKeyEnvironment !== undefined)
2582
+ credential = apiKeyEnvironment;
2583
+ else if (typeof apiKeyIdFlag === "string") {
2584
+ if (dependencies.credentialStore.readApiKey === undefined)
2585
+ throw new Error("API Key storage is unavailable");
2586
+ credential = await dependencies.credentialStore.readApiKey(apiKeyIdFlag.toLowerCase());
2587
+ }
2588
+ else {
2589
+ credential = environmentCredential === undefined || environmentCredential.trim().length === 0
2590
+ ? await dependencies.credentialStore.read() : environmentCredential;
2591
+ }
2592
+ }
2593
+ catch {
2594
+ writeSkillError(dependencies, {
2595
+ message: "The CLI credential store is temporarily unavailable",
2596
+ reason: "Temporary Failure",
2597
+ retryable: true,
2598
+ });
2599
+ return 4;
2600
+ }
2601
+ if (credential === null || credential.trim().length === 0) {
2602
+ writeSkillError(dependencies, {
2603
+ message: "An authenticated CLI Session, User API Key or Project Read Token is required",
2604
+ reason: "Authentication Failed",
2605
+ retryable: false,
2606
+ });
2607
+ return 2;
2608
+ }
2609
+ const client = createClient({
2610
+ baseUrl,
2611
+ ...(usesApiKey || options.requireUserSession === true ? { redirect: "error" } : {}),
2612
+ fetch: options.retryTransportOnce === true
2613
+ ? retryTransportOnce(dependencies.fetch)
2614
+ : dependencies.fetch,
2615
+ headers: {
2616
+ ...(usesApiKey ? { "x-api-key": credential } : { authorization: `Bearer ${credential}` }),
2617
+ "user-agent": specFlowCliUserAgent,
2618
+ },
2619
+ });
2620
+ try {
2621
+ const result = await request(client);
2622
+ if (result.response.ok && result.data !== undefined) {
2623
+ const validationError = options.validateSuccess?.(result.data) ?? null;
2624
+ if (validationError !== null) {
2625
+ writeSkillError(dependencies, validationError);
2626
+ return skillErrorExitCode(validationError);
2627
+ }
2628
+ dependencies.stdout(`${JSON.stringify(result.data)}\n`);
2629
+ return 0;
2630
+ }
2631
+ if (result.response.ok && options.retryTransportOnce === true) {
2632
+ const error = ambiguousMutationError(options.ambiguousOutcomeMessage);
2633
+ writeSkillError(dependencies, error);
2634
+ return skillErrorExitCode(error);
2635
+ }
2636
+ const error = mapHttpFailure(result.response, result.error);
2637
+ writeSkillError(dependencies, error);
2638
+ return skillErrorExitCode(error);
2639
+ }
2640
+ catch {
2641
+ const error = options.retryTransportOnce === true
2642
+ ? ambiguousMutationError(options.ambiguousOutcomeMessage)
2643
+ : {
2644
+ message: "SpecFlow is temporarily unavailable",
2645
+ reason: "Temporary Failure",
2646
+ retryable: true,
2647
+ };
2648
+ writeSkillError(dependencies, error);
2649
+ return skillErrorExitCode(error);
2650
+ }
2651
+ }
2652
+ function retryTransportOnce(fetch) {
2653
+ return async (request) => {
2654
+ const retryRequest = request.clone();
2655
+ try {
2656
+ return await fetch(request);
2657
+ }
2658
+ catch {
2659
+ return fetch(retryRequest);
2660
+ }
2661
+ };
2662
+ }
2663
+ function ambiguousMutationError(message) {
2664
+ return {
2665
+ message: message ??
2666
+ "Project creation outcome is unknown; run project list before trying a new create",
2667
+ reason: "Temporary Failure",
2668
+ retryable: false,
2669
+ };
2670
+ }
2671
+ function validateIssuedProjectReadToken(data) {
2672
+ if (typeof data === "object" &&
2673
+ data !== null &&
2674
+ "credential" in data &&
2675
+ data.credential === null) {
2676
+ return {
2677
+ message: "Project Read Token issuance completed, but its credential cannot be recovered; list or revoke the token before deliberately issuing another",
2678
+ reason: "Temporary Failure",
2679
+ retryable: false,
2680
+ };
2681
+ }
2682
+ return null;
2683
+ }
2684
+ function resolveApiOrigin(dependencies, serverFlag) {
2685
+ const flag = typeof serverFlag === "string" ? serverFlag : undefined;
2686
+ const result = parseHttpOrigin(unresolvedSpecFlowApiOrigin(flag, dependencies.environment.SPEC_FLOW_API_URL));
2687
+ if (result.valid) {
2688
+ return result.origin;
2689
+ }
2690
+ writeSkillError(dependencies, {
2691
+ message: flag === undefined
2692
+ ? "SPEC_FLOW_API_URL must be an HTTP(S) origin without a path"
2693
+ : "--server must be an HTTP(S) origin without a path",
2694
+ reason: "Invalid Operation",
2695
+ retryable: false,
2696
+ });
2697
+ return null;
2698
+ }
2699
+ function parseRelationCommandKey(value) {
2700
+ const key = value.trim();
2701
+ if (key.length === 0 || key.length > 200)
2702
+ throw invalidArgument("Relation command key must contain 1 to 200 characters");
2703
+ return key;
2704
+ }
2705
+ function parseSearchLimit(value) {
2706
+ const limit = Number(value);
2707
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
2708
+ throw new CommanderError(1, "commander.invalidArgument", "Search limit must be an integer from 1 to 100");
2709
+ }
2710
+ return limit;
2711
+ }
2712
+ function parseFindingStatus(value) {
2713
+ if (value === "Confirmed" ||
2714
+ value === "Dismissed" ||
2715
+ value === "Proposed" ||
2716
+ value === "Resolved" ||
2717
+ value === "Stale") {
2718
+ return value;
2719
+ }
2720
+ throw invalidArgument("Finding status is not supported");
2721
+ }
2722
+ function parseProjectStatus(value) {
2723
+ if (value === "Active" || value === "Archived" || value === "Trashed") {
2724
+ return value;
2725
+ }
2726
+ throw new CommanderError(1, "commander.invalidArgument", "Project status must be Active, Archived, or Trashed");
2727
+ }
2728
+ function parseProjectId(value) {
2729
+ const normalized = value.trim().toUpperCase();
2730
+ if (/^[A-Z]{2,10}$/u.test(normalized)) {
2731
+ return normalized;
2732
+ }
2733
+ throw invalidArgument("Project ID must contain 2-10 letters");
2734
+ }
2735
+ function parseSpecId(value) {
2736
+ const normalized = value.trim().toUpperCase();
2737
+ if (/^[A-Z]{2,10}-[1-9]\d*$/u.test(normalized)) {
2738
+ return normalized;
2739
+ }
2740
+ throw invalidArgument("Spec ID must be a Project ID followed by a positive number");
2741
+ }
2742
+ function parseWorkItemId(value) {
2743
+ const normalized = value.trim().toUpperCase();
2744
+ if (/^[A-Z]{2,10}-[1-9]\d*$/u.test(normalized)) {
2745
+ return normalized;
2746
+ }
2747
+ throw invalidArgument("Work Item ID must be a Project ID followed by a positive number");
2748
+ }
2749
+ const workItemPriorities = [
2750
+ "no-priority",
2751
+ "low",
2752
+ "medium",
2753
+ "high",
2754
+ "urgent",
2755
+ ];
2756
+ function parseWorkItemPriority(value) {
2757
+ const normalized = value.trim().toLowerCase();
2758
+ if (workItemPriorities.includes(normalized)) {
2759
+ return normalized;
2760
+ }
2761
+ throw invalidArgument("Work Item priority must be no-priority, low, medium, high, or urgent");
2762
+ }
2763
+ function parseWorkItemDate(value) {
2764
+ if (z.iso.date().safeParse(value).success ||
2765
+ z.iso.datetime().safeParse(value).success) {
2766
+ return value;
2767
+ }
2768
+ throw invalidArgument("Work Item dates must be an ISO 8601 date or date-time");
2769
+ }
2770
+ const workItemDueDateFilters = [
2771
+ "dueThisWeek",
2772
+ "dueNextWeek",
2773
+ "noDueDate",
2774
+ ];
2775
+ const workItemQuerySortFields = [
2776
+ "createdAt",
2777
+ "dueDate",
2778
+ "number",
2779
+ "position",
2780
+ "priority",
2781
+ "title",
2782
+ ];
2783
+ const workItemSortOrders = ["asc", "desc"];
2784
+ function parseWorkItemDueFilter(value) {
2785
+ if (workItemDueDateFilters.includes(value)) {
2786
+ return value;
2787
+ }
2788
+ throw invalidArgument("Due filter must be dueThisWeek, dueNextWeek, or noDueDate");
2789
+ }
2790
+ function parseWorkItemSortField(value) {
2791
+ if (workItemQuerySortFields.includes(value)) {
2792
+ return value;
2793
+ }
2794
+ throw invalidArgument("Work Item sort must be createdAt, dueDate, number, position, priority, or title");
2795
+ }
2796
+ function parseWorkItemSortOrder(value) {
2797
+ if (workItemSortOrders.includes(value)) {
2798
+ return value;
2799
+ }
2800
+ throw invalidArgument("Work Item sort order must be asc or desc");
2801
+ }
2802
+ /**
2803
+ * Builds the wire selection query shared by `work-item list` and
2804
+ * `work-item board`: repeatable flags become the API's CSV fields and
2805
+ * repeated `fieldId=value` pairs merge into the `customFields` JSON map.
2806
+ */
2807
+ function workItemSelectionQuery(options) {
2808
+ const customFields = {};
2809
+ for (const entry of options.customField) {
2810
+ const { fieldId, value } = parseCustomFieldAssignment(entry);
2811
+ customFields[fieldId] = [...(customFields[fieldId] ?? []), value];
2812
+ }
2813
+ return {
2814
+ ...(options.assignee.length === 0
2815
+ ? {}
2816
+ : { assignee: options.assignee.join(",") }),
2817
+ ...(Object.keys(customFields).length === 0
2818
+ ? {}
2819
+ : { customFields: JSON.stringify(customFields) }),
2820
+ ...(options.due.length === 0 ? {} : { due: options.due.join(",") }),
2821
+ ...(options.dueAfter === undefined ? {} : { dueAfter: options.dueAfter }),
2822
+ ...(options.dueBefore === undefined
2823
+ ? {}
2824
+ : { dueBefore: options.dueBefore }),
2825
+ ...(options.label.length === 0 ? {} : { label: options.label.join(",") }),
2826
+ ...(options.priority.length === 0
2827
+ ? {}
2828
+ : { priority: options.priority.join(",") }),
2829
+ ...(options.search === undefined ? {} : { search: options.search }),
2830
+ ...(options.sort === undefined ? {} : { sortBy: options.sort }),
2831
+ ...(options.order === undefined ? {} : { sortOrder: options.order }),
2832
+ ...(options.startAfter === undefined
2833
+ ? {}
2834
+ : { startAfter: options.startAfter }),
2835
+ ...(options.startBefore === undefined
2836
+ ? {}
2837
+ : { startBefore: options.startBefore }),
2838
+ ...(options.status.length === 0
2839
+ ? {}
2840
+ : { status: options.status.join(",") }),
2841
+ };
2842
+ }
2843
+ /**
2844
+ * The selection options `work-item list` and `work-item board` share.
2845
+ * Attach with `workItemSelectionFlags(command)`.
2846
+ */
2847
+ function workItemSelectionFlags(command) {
2848
+ return command
2849
+ .option("--assignee <assignee>", "Filter by assignee User ID, or unassigned for items with none; repeat for several", collectOption, [])
2850
+ .option("--custom-field <fieldId=value>", "Filter by Custom Field value; repeat to OR values or combine fields", collectOption, [])
2851
+ .option("--due <filter>", "Filter by due bucket: dueThisWeek, dueNextWeek, or noDueDate; repeat for several", (value, previous) => [
2852
+ ...previous,
2853
+ parseWorkItemDueFilter(value),
2854
+ ], [])
2855
+ .option("--due-after <date>", "Filter due on/after", parseWorkItemDate)
2856
+ .option("--due-before <date>", "Filter due on/before", parseWorkItemDate)
2857
+ .option("--label <labelId>", "Filter by Work Item Label ID; repeat for several", collectOption, [])
2858
+ .option("--priority <priority>", "Filter by priority; repeat for several", (value, previous) => [
2859
+ ...previous,
2860
+ parseWorkItemPriority(value),
2861
+ ], [])
2862
+ .option("--search <search>", "Filter by title search text")
2863
+ .option("--sort <field>", "Sort by field", parseWorkItemSortField)
2864
+ .option("--order <order>", "Sort order", parseWorkItemSortOrder)
2865
+ .option("--start-after <date>", "Filter start on/after", parseWorkItemDate)
2866
+ .option("--start-before <date>", "Filter start on/before", parseWorkItemDate)
2867
+ .option("--status <status>", "Filter by Workflow Column slug or virtual status; repeat for several", collectOption, []);
2868
+ }
2869
+ function parseWorkItemBulkAction(value) {
2870
+ if (workItemBulkActions.includes(value)) {
2871
+ return value;
2872
+ }
2873
+ throw invalidArgument(`Bulk action must be ${workItemBulkActions.join(", ")}`);
2874
+ }
2875
+ /**
2876
+ * A bulk target is `WORKITEM@version` — the public id and its observed
2877
+ * version, echoed back as the `"N"` precondition each item carries.
2878
+ */
2879
+ function parseWorkItemBulkTarget(value, previous) {
2880
+ const [id, version, extra] = value.split("@");
2881
+ if (id === undefined ||
2882
+ version === undefined ||
2883
+ extra !== undefined ||
2884
+ !/^[A-Z]{2,10}-[1-9]\d*$/u.test(id.trim().toUpperCase()) ||
2885
+ !/^[1-9]\d*$/u.test(version)) {
2886
+ throw invalidArgument(`Bulk target "${value}" must be a WORK-ITEM-ID@version pair`);
2887
+ }
2888
+ const numericVersion = Number(version);
2889
+ if (!Number.isSafeInteger(numericVersion) ||
2890
+ numericVersion > 2_147_483_647) {
2891
+ throw invalidArgument(`Bulk target "${value}" has a version outside the supported range`);
2892
+ }
2893
+ return [
2894
+ ...previous,
2895
+ { expectedVersion: `"${version}"`, id: id.trim().toUpperCase() },
2896
+ ];
2897
+ }
2898
+ function parseBoardPosition(value) {
2899
+ const position = Number(value);
2900
+ if (!Number.isInteger(position) ||
2901
+ position < 0 ||
2902
+ position > 2_147_483_646) {
2903
+ throw invalidArgument("Board position must be a nonnegative integer within the supported range");
2904
+ }
2905
+ return position;
2906
+ }
2907
+ /**
2908
+ * Comma-separated `slug@version` pairs become the verbatim board order —
2909
+ * position equals list index, matching the reorder contract, and each
2910
+ * version is the column's observed version sent as its reorder
2911
+ * precondition.
2912
+ */
2913
+ function parseColumnOrder(value) {
2914
+ const entries = value
2915
+ .split(",")
2916
+ .map((entry) => entry.trim())
2917
+ .filter((entry) => entry.length > 0);
2918
+ if (entries.length === 0 || entries.length > 200) {
2919
+ throw invalidArgument("Workflow Column order must list between 1 and 200 slug@version pairs");
2920
+ }
2921
+ const parsed = entries.map((entry, position) => {
2922
+ const [slug, version, extra] = entry.split("@");
2923
+ if (slug === undefined ||
2924
+ version === undefined ||
2925
+ extra !== undefined ||
2926
+ !/^[1-9]\d*$/u.test(version)) {
2927
+ throw invalidArgument(`Workflow Column order entry "${entry}" must be a slug@version pair`);
2928
+ }
2929
+ return { expectedVersion: `"${version}"`, position, slug };
2930
+ });
2931
+ const slugs = parsed.map((entry) => entry.slug);
2932
+ if (new Set(slugs).size !== slugs.length) {
2933
+ throw invalidArgument("Workflow Column order cannot repeat a slug");
2934
+ }
2935
+ return parsed;
2936
+ }
2937
+ const workItemCustomFieldTypes = [
2938
+ "text",
2939
+ "number",
2940
+ "date",
2941
+ "dropdown",
2942
+ "boolean",
2943
+ ];
2944
+ function parseWorkItemCustomFieldType(value) {
2945
+ const normalized = value.trim().toLowerCase();
2946
+ if (workItemCustomFieldTypes.includes(normalized)) {
2947
+ return normalized;
2948
+ }
2949
+ throw invalidArgument("Custom Field type must be text, number, date, dropdown, or boolean");
2950
+ }
2951
+ function collectOption(value, previous) {
2952
+ return [...previous, value];
2953
+ }
2954
+ /**
2955
+ * A `fieldId=value` assignment splits on the first `=` so values may carry
2956
+ * the separator; the field id itself is an opaque server identity.
2957
+ */
2958
+ function parseCustomFieldAssignment(value) {
2959
+ const separator = value.indexOf("=");
2960
+ const fieldId = separator === -1 ? "" : value.slice(0, separator).trim();
2961
+ if (fieldId.length === 0 || fieldId.length > 200) {
2962
+ throw invalidArgument("Custom Field values must be written as fieldId=value");
2963
+ }
2964
+ const fieldValue = value.slice(separator + 1);
2965
+ if (fieldValue.length > 2_000) {
2966
+ throw invalidArgument("Custom Field values must stay within the supported length");
2967
+ }
2968
+ return { fieldId, value: fieldValue };
2969
+ }
2970
+ /**
2971
+ * Comma-separated `id@version` pairs become the verbatim display order —
2972
+ * position equals list index, matching the reorder contract, and each
2973
+ * version is the field's observed version sent as its reorder
2974
+ * precondition.
2975
+ */
2976
+ function parseFieldOrder(value) {
2977
+ const entries = value
2978
+ .split(",")
2979
+ .map((entry) => entry.trim())
2980
+ .filter((entry) => entry.length > 0);
2981
+ if (entries.length === 0 || entries.length > 500) {
2982
+ throw invalidArgument("Custom Field order must list between 1 and 500 id@version pairs");
2983
+ }
2984
+ const parsed = entries.map((entry, position) => {
2985
+ const [id, version, extra] = entry.split("@");
2986
+ if (id === undefined ||
2987
+ version === undefined ||
2988
+ extra !== undefined ||
2989
+ !/^[1-9]\d*$/u.test(version)) {
2990
+ throw invalidArgument(`Custom Field order entry "${entry}" must be an id@version pair`);
2991
+ }
2992
+ return { expectedVersion: `"${version}"`, id, position };
2993
+ });
2994
+ const ids = parsed.map((entry) => entry.id);
2995
+ if (new Set(ids).size !== ids.length) {
2996
+ throw invalidArgument("Custom Field order cannot repeat a field id");
2997
+ }
2998
+ return parsed;
2999
+ }
3000
+ function parseOpaqueVersion(value) {
3001
+ if (/^"[1-9]\d*"$/u.test(value)) {
3002
+ return value;
3003
+ }
3004
+ throw invalidArgument("Expected version must be the opaque version returned by SpecFlow");
3005
+ }
3006
+ function parseIsoDateTime(value) {
3007
+ if (z.iso.datetime().safeParse(value).success) {
3008
+ return value;
3009
+ }
3010
+ throw invalidArgument("Expiry must be an ISO 8601 date-time");
3011
+ }
3012
+ function parseSpecState(value) {
3013
+ if (value === "Ready" || value === "Unavailable") {
3014
+ return value;
3015
+ }
3016
+ throw invalidArgument("Spec State must be Ready or Unavailable");
3017
+ }
3018
+ function parseSourceReferences(value) {
3019
+ let parsed;
3020
+ try {
3021
+ parsed = JSON.parse(value);
3022
+ }
3023
+ catch {
3024
+ throw invalidArgument("Source References must be valid JSON");
3025
+ }
3026
+ const sourceReferences = sourceReferencesInputSchema.safeParse(parsed);
3027
+ if (!sourceReferences.success) {
3028
+ throw invalidArgument("Source References do not match the supported bounded contract");
3029
+ }
3030
+ return sourceReferences.data.map((reference) => ({
3031
+ ...(reference.externalRevision === undefined
3032
+ ? {}
3033
+ : { externalRevision: reference.externalRevision }),
3034
+ ...(reference.section === undefined ? {} : { section: reference.section }),
3035
+ title: reference.title,
3036
+ url: reference.url,
3037
+ }));
3038
+ }
3039
+ function invalidArgument(message) {
3040
+ return new CommanderError(1, "commander.invalidArgument", message);
3041
+ }
3042
+ function parseGlossaryEntries(value) {
3043
+ let parsed;
3044
+ try {
3045
+ parsed = JSON.parse(value);
3046
+ }
3047
+ catch {
3048
+ throw new CommanderError(1, "commander.invalidArgument", "Glossary Entries must be valid JSON");
3049
+ }
3050
+ const entries = glossaryEntriesInputSchema.safeParse(parsed);
3051
+ if (!entries.success) {
3052
+ throw new CommanderError(1, "commander.invalidArgument", "Glossary Entries do not match the supported contract");
3053
+ }
3054
+ return entries.data;
3055
+ }
3056
+ function parseStringArray(value, label) {
3057
+ let parsed;
3058
+ try {
3059
+ parsed = JSON.parse(value);
3060
+ }
3061
+ catch {
3062
+ throw invalidArgument(`${label} must be valid JSON`);
3063
+ }
3064
+ const result = z.array(z.string().trim().min(1).max(200)).max(500).safeParse(parsed);
3065
+ if (!result.success) {
3066
+ throw invalidArgument(`${label} must be a bounded JSON string array`);
3067
+ }
3068
+ return result.data;
3069
+ }
3070
+ function toAgentProvenance(options) {
3071
+ return {
3072
+ ...(options.agentRevision === undefined
3073
+ ? {}
3074
+ : { agentRevision: options.agentRevision }),
3075
+ ...(options.agentModel === undefined ? {} : { model: options.agentModel }),
3076
+ sourceReferenceIds: options.sourceReferenceIds === undefined
3077
+ ? []
3078
+ : parseStringArray(options.sourceReferenceIds, "source reference IDs"),
3079
+ };
3080
+ }
3081
+ function parseRoleStatements(value) {
3082
+ let parsed;
3083
+ try {
3084
+ parsed = JSON.parse(value);
3085
+ }
3086
+ catch {
3087
+ throw invalidArgument("Role statements must be valid JSON");
3088
+ }
3089
+ const statements = roleStatementsInputSchema.safeParse(parsed);
3090
+ if (!statements.success) {
3091
+ throw invalidArgument("Role statements must map permission resources to non-empty action arrays");
3092
+ }
3093
+ return statements.data;
3094
+ }
3095
+ function parseProposalTargetKind(value) {
3096
+ if (value === "Glossary" || value === "Spec") {
3097
+ return value;
3098
+ }
3099
+ throw invalidArgument("Proposal target kind must be Spec or Glossary");
3100
+ }
3101
+ function parseProposalStatus(value) {
3102
+ if (value === "Applied" || value === "Discarded" || value === "Open") {
3103
+ return value;
3104
+ }
3105
+ throw invalidArgument("Proposal status must be Open, Applied, or Discarded");
3106
+ }
3107
+ function parseFindingInput(value) {
3108
+ let parsed;
3109
+ try {
3110
+ parsed = JSON.parse(value);
3111
+ }
3112
+ catch {
3113
+ throw invalidArgument("Finding input must be valid JSON");
3114
+ }
3115
+ const finding = findingCreateInputSchema.safeParse(parsed);
3116
+ if (!finding.success) {
3117
+ throw invalidArgument("Finding input does not match the supported contract");
3118
+ }
3119
+ return finding.data;
3120
+ }
3121
+ function withSuccessData(result, data) {
3122
+ return result.response.ok && result.data === undefined
3123
+ ? { ...result, data }
3124
+ : result;
3125
+ }
3126
+ /**
3127
+ * Whether a `work-item-label delete` payload is the still-running cascade —
3128
+ * the 202 body carries the definition with its deletion boundary set.
3129
+ */
3130
+ function isWorkItemLabelDeletionPending(data) {
3131
+ if (typeof data !== "object" || data === null || !("workItemLabel" in data)) {
3132
+ return false;
3133
+ }
3134
+ const label = data.workItemLabel;
3135
+ return (typeof label === "object" &&
3136
+ label !== null &&
3137
+ label.deletionStartedAt !==
3138
+ null &&
3139
+ label.deletionStartedAt !==
3140
+ undefined);
3141
+ }
3142
+ function rejectTokenWrite(dependencies) {
3143
+ if (hasProjectReadToken(dependencies.environment)) {
3144
+ writeSkillError(dependencies, {
3145
+ message: "Project Read Token does not permit write operations",
3146
+ reason: "Permission Denied",
3147
+ retryable: false,
3148
+ });
3149
+ return 3;
3150
+ }
3151
+ writeSkillError(dependencies, {
3152
+ message: "An authenticated CLI Session is required",
3153
+ reason: "Authentication Failed",
3154
+ retryable: false,
3155
+ });
3156
+ return 2;
3157
+ }
3158
+ function rejectProjectReadTokenOperation(dependencies) {
3159
+ writeSkillError(dependencies, {
3160
+ message: "Project Read Token does not permit this operation",
3161
+ reason: "Permission Denied",
3162
+ retryable: false,
3163
+ });
3164
+ return 3;
3165
+ }
3166
+ function hasProjectReadToken(environment) {
3167
+ const credential = environment.SPEC_FLOW_PROJECT_READ_TOKEN;
3168
+ return credential !== undefined && credential.trim().length > 0;
3169
+ }
3170
+ function mapHttpFailure(response, payload) {
3171
+ const { status } = response;
3172
+ const serverMessage = readServerMessage(payload);
3173
+ const serverCode = readServerCode(payload);
3174
+ if (status === 401) {
3175
+ return {
3176
+ message: serverMessage ?? "Project Read Token is invalid, expired, or revoked",
3177
+ reason: "Authentication Failed",
3178
+ retryable: false,
3179
+ };
3180
+ }
3181
+ if (status === 403) {
3182
+ return {
3183
+ message: serverMessage ?? "Project Read Token does not permit this operation",
3184
+ reason: "Permission Denied",
3185
+ retryable: false,
3186
+ };
3187
+ }
3188
+ if (status === 404) {
3189
+ if (serverCode === "PROJECT_NOT_ACCESSIBLE") {
3190
+ return {
3191
+ message: serverMessage ?? "The Project is not accessible",
3192
+ reason: "Project Not Accessible",
3193
+ retryable: false,
3194
+ };
3195
+ }
3196
+ return {
3197
+ message: serverMessage ?? "The requested Project resource was not found",
3198
+ reason: "Resource Not Found",
3199
+ retryable: false,
3200
+ };
3201
+ }
3202
+ if (status === 409 && serverCode === "RESOURCE_CHANGED") {
3203
+ return {
3204
+ message: serverMessage ?? "The Working Draft changed after the Proposal baseline",
3205
+ reason: "Resource Changed",
3206
+ retryable: false,
3207
+ };
3208
+ }
3209
+ if (status === 400 || status === 409 || status === 422) {
3210
+ const diagnostics = readServerDiagnostics(payload);
3211
+ return {
3212
+ ...(diagnostics === undefined ? {} : { diagnostics }),
3213
+ message: serverMessage ?? "The requested operation is invalid",
3214
+ reason: "Invalid Operation",
3215
+ retryable: false,
3216
+ };
3217
+ }
3218
+ if (status === 412) {
3219
+ return {
3220
+ message: serverMessage ?? "The resource changed after it was read",
3221
+ reason: "Resource Changed",
3222
+ retryable: false,
3223
+ };
3224
+ }
3225
+ if (status === 429) {
3226
+ const retryAfter = response.headers.get("retry-after");
3227
+ return {
3228
+ message: serverMessage ?? "SpecFlow is temporarily throttling this client",
3229
+ reason: "Temporary Failure",
3230
+ ...(retryAfter === null ? {} : { retryAfter }),
3231
+ retryable: true,
3232
+ };
3233
+ }
3234
+ return {
3235
+ message: serverMessage ?? "SpecFlow is temporarily unavailable",
3236
+ reason: "Temporary Failure",
3237
+ retryable: status >= 500,
3238
+ };
3239
+ }
3240
+ function readServerMessage(payload) {
3241
+ return typeof payload === "object" &&
3242
+ payload !== null &&
3243
+ "message" in payload &&
3244
+ typeof payload.message === "string"
3245
+ ? payload.message
3246
+ : null;
3247
+ }
3248
+ function readServerCode(payload) {
3249
+ return typeof payload === "object" &&
3250
+ payload !== null &&
3251
+ "code" in payload &&
3252
+ typeof payload.code === "string"
3253
+ ? payload.code
3254
+ : null;
3255
+ }
3256
+ function readServerDiagnostics(payload) {
3257
+ if (typeof payload !== "object" ||
3258
+ payload === null ||
3259
+ !("diagnostics" in payload) ||
3260
+ !Array.isArray(payload.diagnostics)) {
3261
+ return undefined;
3262
+ }
3263
+ const diagnostics = payload.diagnostics.flatMap((entry) => {
3264
+ if (typeof entry !== "object" || entry === null) {
3265
+ return [];
3266
+ }
3267
+ const { column, line, message, reason } = entry;
3268
+ if (typeof reason !== "string" || typeof message !== "string") {
3269
+ return [];
3270
+ }
3271
+ return [
3272
+ {
3273
+ ...(typeof column === "number" ? { column } : {}),
3274
+ ...(typeof line === "number" ? { line } : {}),
3275
+ message,
3276
+ reason,
3277
+ },
3278
+ ];
3279
+ });
3280
+ return diagnostics.length === 0 ? undefined : diagnostics;
3281
+ }
3282
+ function skillErrorExitCode(error) {
3283
+ if (error.reason === "Permission Denied") {
3284
+ return 3;
3285
+ }
3286
+ return error.reason === "Temporary Failure" ? 4 : 2;
3287
+ }
3288
+ function writeSkillError(dependencies, error) {
3289
+ dependencies.stderr(`${JSON.stringify({ error })}\n`);
3290
+ }
3291
+ function parseCommentContent(value) {
3292
+ const result = z.string().trim().min(1).max(20_000).refine((content) => !content.includes("\0")).safeParse(value);
3293
+ if (!result.success)
3294
+ throw invalidArgument("Comment content must contain 1 through 20000 characters without null characters");
3295
+ return result.data;
3296
+ }
3297
+ function parseCommentId(value) {
3298
+ if (!z.uuid().safeParse(value).success)
3299
+ throw invalidArgument("Comment ID must be a UUID returned by SpecFlow");
3300
+ return value;
3301
+ }
3302
+ function parseCommentCommandKey(value) {
3303
+ if (!z.uuid().safeParse(value).success)
3304
+ throw invalidArgument("Comment command key must be a UUID; reuse the same key and inputs when retrying");
3305
+ return value;
3306
+ }
3307
+ function parseNotificationUuid(value) {
3308
+ if (!z.uuid().safeParse(value).success)
3309
+ throw invalidArgument("Notification identity and command keys must be UUIDs");
3310
+ return value;
3311
+ }
3312
+ function parseNotificationEventData(value) {
3313
+ let parsed;
3314
+ try {
3315
+ parsed = JSON.parse(value);
3316
+ }
3317
+ catch {
3318
+ throw invalidArgument("Notification event data must be a JSON object");
3319
+ }
3320
+ const result = z.record(z.string(), z.json()).safeParse(parsed);
3321
+ if (!result.success)
3322
+ throw invalidArgument("Notification event data must be a JSON object");
3323
+ return result.data;
3324
+ }
3325
+ function parseNotificationPreferences(value) {
3326
+ let parsed;
3327
+ try {
3328
+ parsed = JSON.parse(value);
3329
+ }
3330
+ catch {
3331
+ throw invalidArgument("Notification preferences must be a JSON object");
3332
+ }
3333
+ const result = z.object({ taskAssignmentEnabled: z.boolean().optional(), taskCommentEnabled: z.boolean().optional(), taskStatusChangeEnabled: z.boolean().optional(), dueDateReminderEnabled: z.boolean().optional(), dueDateReminderMinutes: z.number().int().min(5).max(43200).optional() }).strict().safeParse(parsed);
3334
+ if (!result.success)
3335
+ throw invalidArgument("Notification preferences contain unsupported settings or invalid values");
3336
+ const data = result.data;
3337
+ return { ...(data.taskAssignmentEnabled === undefined ? {} : { taskAssignmentEnabled: data.taskAssignmentEnabled }), ...(data.taskCommentEnabled === undefined ? {} : { taskCommentEnabled: data.taskCommentEnabled }), ...(data.taskStatusChangeEnabled === undefined ? {} : { taskStatusChangeEnabled: data.taskStatusChangeEnabled }), ...(data.dueDateReminderEnabled === undefined ? {} : { dueDateReminderEnabled: data.dueDateReminderEnabled }), ...(data.dueDateReminderMinutes === undefined ? {} : { dueDateReminderMinutes: data.dueDateReminderMinutes }) };
3338
+ }
3339
+ //# sourceMappingURL=cli.js.map