@gonovus-ai/spec-flow-cli 0.5.0 → 0.6.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 (38) hide show
  1. package/README.md +57 -28
  2. package/dist/bin.js +6 -50
  3. package/dist/bin.js.map +1 -1
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.js +103 -397
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/work-item-output.d.ts +390 -0
  8. package/dist/commands/work-item-output.js +36 -0
  9. package/dist/commands/work-item-output.js.map +1 -0
  10. package/dist/http-origin.js +3 -0
  11. package/dist/http-origin.js.map +1 -1
  12. package/dist/index.js +50 -28
  13. package/dist/index.js.map +1 -1
  14. package/dist/input/text-source.d.ts +9 -0
  15. package/dist/input/text-source.js +74 -0
  16. package/dist/input/text-source.js.map +1 -0
  17. package/dist/program.d.ts +3 -0
  18. package/dist/program.js +19 -0
  19. package/dist/program.js.map +1 -0
  20. package/dist/runtime/errors.d.ts +28 -0
  21. package/dist/runtime/errors.js +133 -0
  22. package/dist/runtime/errors.js.map +1 -0
  23. package/dist/runtime/execute.d.ts +23 -0
  24. package/dist/runtime/execute.js +25 -0
  25. package/dist/runtime/execute.js.map +1 -0
  26. package/dist/runtime/identity.d.ts +183 -0
  27. package/dist/runtime/identity.js +17 -0
  28. package/dist/runtime/identity.js.map +1 -0
  29. package/dist/runtime/output.d.ts +3 -0
  30. package/dist/runtime/output.js +13 -0
  31. package/dist/runtime/output.js.map +1 -0
  32. package/dist/runtime/session.d.ts +16 -0
  33. package/dist/runtime/session.js +123 -0
  34. package/dist/runtime/session.js.map +1 -0
  35. package/package.json +1 -1
  36. package/dist/work-item-time-entry-input.d.ts +0 -4
  37. package/dist/work-item-time-entry-input.js +0 -27
  38. package/dist/work-item-time-entry-input.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,22 +1,26 @@
1
- import { Command, CommanderError, InvalidArgumentError } from "commander";
1
+ import { CommanderError, InvalidArgumentError } from "commander";
2
2
  import { randomUUID } from "node:crypto";
3
- import createClient from "openapi-fetch";
4
3
  import { z } from "zod";
5
4
  import { addAuthenticationCommandTopology } from "./authentication-command-topology.js";
5
+ import { itemOutput, itemListOutput, relationListOutput, relationReceiptOutput } from "./commands/work-item-output.js";
6
6
  import { registerCustomFieldColorCommands, parseOptionColors } from "./custom-field-color-commands.js";
7
7
  import { registerGitHubAccountCommands } from "./github-account-commands.js";
8
- import { parseHttpOrigin, unresolvedSpecFlowApiOrigin, } from "./http-origin.js";
8
+ import { readDescription, singleDescriptionSource } from "./input/text-source.js";
9
9
  import { registerProductMapCommands } from "./product-map-commands.js";
10
+ import { createProgram } from "./program.js";
10
11
  import { registerProjectAdministrationCommands } from "./project-administration-commands.js";
12
+ import { writeSkillError, skillErrorExitCode } from "./runtime/errors.js";
13
+ import { execute } from "./runtime/execute.js";
14
+ import { readIdentity } from "./runtime/identity.js";
15
+ import { writeResult } from "./runtime/output.js";
16
+ import { selectSession, hasWorkspaceReadToken, rejectWorkspaceReadTokenOperation } from "./runtime/session.js";
11
17
  import { registerSpecDiscussionCommands } from "./spec-discussion-commands.js";
12
18
  import { registerSpecOwnerCommands } from "./spec-owner-commands.js";
13
- import { specFlowCliUserAgent } from "./user-agent.js";
14
19
  import { registerUserApiKeyCommands } from "./user-api-key-commands.js";
15
20
  import { registerWorkItemAssetCommands } from "./work-item-asset-commands.js";
16
21
  import { readWorkItemImportFile } from "./work-item-exchange-file.js";
17
22
  import { registerWorkItemQueryCommands } from "./work-item-query-commands.js";
18
23
  import { registerWorkItemResourceLinkCommands } from "./work-item-resource-link-commands.js";
19
- import { parseTimeEntryTimestamp, parseTimeEntryUuid, parseTimeEntryDescription } from "./work-item-time-entry-input.js";
20
24
  export function formatResourceDiagnostic(message) {
21
25
  return `${JSON.stringify({ diagnostic: { level: "warning", message } })}\n`;
22
26
  }
@@ -135,22 +139,47 @@ const findingCreateInputSchema = z
135
139
  .strict();
136
140
  export async function runCli(arguments_, dependencies) {
137
141
  let exitCode = 0;
138
- const program = new Command()
139
- .name("specflow")
140
- .description("Read and govern SpecFlow through supported commands")
141
- .option("--allow-file-fallback", "use a warned owner-only local credential file if the system store is unavailable")
142
- .option("--server <url>", "SpecFlow server origin")
143
- .option("--api-key-id <id>", "Use an origin-scoped User API Key from the credential store")
144
- .exitOverride()
145
- .configureOutput({
146
- writeErr: () => undefined,
147
- writeOut: dependencies.stdout,
148
- });
149
- const run = (request, options = {}) => executeRequest(dependencies, request, options, program.getOptionValue("server"), program.getOptionValue("apiKeyId"));
142
+ let outputMode = "full";
143
+ const warnMissingUrl = () => { dependencies.stderr(formatResourceDiagnostic("This server version does not support Work Item URLs; upgrade the server. The business operation succeeded.")); };
144
+ const program = createProgram(dependencies);
145
+ const run = async (request, options = {}) => {
146
+ const selected = await selectSession(dependencies, options, program.getOptionValue("server"), program.getOptionValue("apiKeyId"));
147
+ if ("exitCode" in selected)
148
+ return selected.exitCode;
149
+ let me;
150
+ if (options.resolveMe === true) {
151
+ if (selected.credentialKind === "workspace-read-token")
152
+ return rejectWorkspaceReadTokenOperation(dependencies);
153
+ const identity = await execute(readIdentity, selected.identityClient);
154
+ if (!identity.ok) {
155
+ writeSkillError(dependencies, identity.error);
156
+ return skillErrorExitCode(identity.error);
157
+ }
158
+ me = identity.data.user.id;
159
+ }
160
+ const result = await execute(client => request(client, me, selected.credentialKind), selected.client, options);
161
+ if (!result.ok) {
162
+ writeSkillError(dependencies, result.error);
163
+ return skillErrorExitCode(result.error);
164
+ }
165
+ return writeResult(dependencies, () => options.project === undefined ? result.data : options.project(result.data));
166
+ };
150
167
  registerUserApiKeyCommands(program, { dependencies, run, setExitCode: (value) => { exitCode = value; }, parseScopes: parseRoleStatements, parseLimit: parseSearchLimit });
151
168
  registerGitHubAccountCommands(program, { dependencies, run, setExitCode: (value) => { exitCode = value; } });
152
169
  const authentication = addAuthenticationCommandTopology(program);
153
170
  program.hook("preAction", (_command, actionCommand) => {
171
+ const names = [];
172
+ for (let command = actionCommand; command.parent !== null; command = command.parent)
173
+ names.unshift(command.name());
174
+ const path = names.join(" ");
175
+ outputMode = program.opts().output ?? (path === "work-item subtask link" ? "summary" : "full");
176
+ if (outputMode === "summary" && !["work-item create", "work-item read", "work-item update", "work-item assign", "work-item list", "work-item relation create", "work-item relation list", "work-item subtask link"].includes(path))
177
+ throw invalidArgument("Summary output is not supported for this command");
178
+ if (!["work-item create", "work-item assign"].includes(path)) {
179
+ const assignee = actionCommand.getOptionValue("assignee");
180
+ if (assignee === "me" || (Array.isArray(assignee) && assignee.some(value => typeof value === "string" && value.split(",").includes("me"))))
181
+ throw invalidArgument("This command accepts User IDs only; me is supported by work-item create and assign");
182
+ }
154
183
  const options = actionCommand.opts();
155
184
  if (typeof options.projectId === "string") {
156
185
  actionCommand.setOptionValue("projectId", parseProjectId(options.projectId));
@@ -171,6 +200,13 @@ export async function runCli(arguments_, dependencies) {
171
200
  throw new CommanderError(2, "specflow.authenticationCommand", "Authentication commands use the human-facing CLI boundary");
172
201
  });
173
202
  }
203
+ dependencies.registerAuthentication?.(program, authentication);
204
+ authentication.status.parent?.command("whoami").description("Read the current User ID and selected credential kind").action(async () => {
205
+ exitCode = await run(async (client, _me, credentialKind) => {
206
+ const result = await readIdentity(client);
207
+ return { ...result, ...(result.data === undefined ? {} : { data: { user: { id: result.data.user.id }, credentialKind } }) };
208
+ }, { rejectWorkspaceReadToken: true });
209
+ });
174
210
  const workspace = program.command("workspace");
175
211
  workspace
176
212
  .command("list")
@@ -1289,81 +1325,6 @@ export async function runCli(arguments_, dependencies) {
1289
1325
  registerWorkItemQueryCommands(workItem, workspace, resourceRuntime);
1290
1326
  registerProductMapCommands(program, { dependencies, run, setExitCode: value => { exitCode = value; }, canWrite: () => !hasWorkspaceReadToken(dependencies.environment), rejectWrite: () => rejectTokenWrite(dependencies), parseOpaqueVersion });
1291
1327
  registerWorkItemResourceLinkCommands(workItem, spec, { run, setExitCode: (value) => { exitCode = value; }, canWrite: () => !hasWorkspaceReadToken(dependencies.environment), rejectWrite: () => rejectTokenWrite(dependencies), parseOpaqueVersion, parseLimit: parseSearchLimit });
1292
- const timeEntry = workItem.command("time-entry").description("Record and read task time entries");
1293
- timeEntry.command("list")
1294
- .requiredOption("--project-id <projectId>")
1295
- .requiredOption("--work-item-id <workItemId>")
1296
- .option("--limit <limit>", "Maximum results, 1 through 100", parseSearchLimit, 50)
1297
- .option("--cursor <cursor>", "Opaque cursor returned by the previous page")
1298
- .action(async (options) => {
1299
- 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 }) } } }));
1300
- });
1301
- timeEntry.command("get")
1302
- .requiredOption("--project-id <projectId>")
1303
- .requiredOption("--work-item-id <workItemId>")
1304
- .requiredOption("--entry-id <entryId>", "Time Entry UUID", (value) => { try {
1305
- return parseTimeEntryUuid(value);
1306
- }
1307
- catch {
1308
- throw invalidArgument("Time Entry ID must be a UUID");
1309
- } })
1310
- .action(async (options) => {
1311
- exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/time-entries/{entryId}", { params: { path: options } }));
1312
- });
1313
- for (const action of ["create", "update"]) {
1314
- const command = timeEntry.command(action)
1315
- .requiredOption("--project-id <projectId>")
1316
- .requiredOption("--work-item-id <workItemId>")
1317
- .requiredOption("--start-time <timestamp>", "ISO date-time with an offset", (value) => { try {
1318
- return parseTimeEntryTimestamp(value);
1319
- }
1320
- catch {
1321
- throw invalidArgument("Start time must be a valid ISO 8601 timestamp with an offset");
1322
- } })
1323
- .option("--end-time <timestamp>", "Omit to start running, or preserve the stored end on update", (value) => { try {
1324
- return parseTimeEntryTimestamp(value);
1325
- }
1326
- catch {
1327
- throw invalidArgument("End time must be a valid ISO 8601 timestamp with an offset");
1328
- } })
1329
- .option("--description <description>", "Optional recorded work description", (value) => { try {
1330
- return parseTimeEntryDescription(value);
1331
- }
1332
- catch {
1333
- throw invalidArgument("Time Entry description exceeds the allowed limit");
1334
- } })
1335
- .requiredOption("--idempotency-key <key>", "Reuse this key and inputs for the same action", (value) => { try {
1336
- return parseTimeEntryUuid(value);
1337
- }
1338
- catch {
1339
- throw invalidArgument("Command key must be a UUID");
1340
- } });
1341
- if (action === "update")
1342
- command.requiredOption("--entry-id <entryId>", "Time Entry UUID", (value) => { try {
1343
- return parseTimeEntryUuid(value);
1344
- }
1345
- catch {
1346
- throw invalidArgument("Time Entry ID must be a UUID");
1347
- } }).requiredOption("--expected-version <version>", "Opaque version returned by SpecFlow", parseOpaqueVersion);
1348
- command.action(async (options) => {
1349
- if (hasWorkspaceReadToken(dependencies.environment)) {
1350
- exitCode = rejectTokenWrite(dependencies);
1351
- return;
1352
- }
1353
- const input = { startTime: options.startTime, ...(options.endTime === undefined ? {} : { endTime: options.endTime }), ...(options.description === undefined ? {} : { description: options.description }) };
1354
- if (options.endTime !== undefined && Date.parse(options.startTime) > Date.parse(options.endTime))
1355
- throw invalidArgument("Start time cannot follow end time");
1356
- const path = { projectId: options.projectId, workItemId: options.workItemId };
1357
- const header = { "idempotency-key": options.idempotencyKey };
1358
- exitCode = await run((client) => {
1359
- if (action === "create")
1360
- return client.POST("/v1/projects/{projectId}/work-items/{workItemId}/time-entries", { body: input, params: { path, header } });
1361
- if (options.entryId === undefined || options.expectedVersion === undefined)
1362
- throw new Error("Time Entry update requires its identity and observed version");
1363
- 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 } } });
1364
- }, { retryTransportOnce: true, ambiguousOutcomeMessage: "Time Entry outcome is unknown; retry the same inputs and command key" });
1365
- });
1366
- }
1367
1328
  const workItemComment = workItem.command("comment").description("Read and manage authored task comments");
1368
1329
  workItemComment.command("list")
1369
1330
  .requiredOption("--project-id <projectId>")
@@ -1439,14 +1400,15 @@ export async function runCli(arguments_, dependencies) {
1439
1400
  .requiredOption("--project-id <projectId>")
1440
1401
  .requiredOption("--work-item-id <workItemId>")
1441
1402
  .action(async (options) => {
1442
- exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/relations", { params: { path: options } }));
1403
+ exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}/relations", { params: { path: options } }), { project: data => relationListOutput(data, outputMode, warnMissingUrl) });
1443
1404
  });
1444
1405
  workItemRelation.command("create")
1406
+ .addHelpText("after", "\nExample: specflow work-item relation create --project-id PROJECT --work-item-id GON-78 --target-project-id PROJECT --target-work-item-id GON-79 --type subtask --idempotency-key KEY\n")
1445
1407
  .requiredOption("--project-id <projectId>")
1446
1408
  .requiredOption("--work-item-id <workItemId>")
1447
1409
  .requiredOption("--target-project-id <projectId>", "Target task's current Project", parseProjectId)
1448
1410
  .requiredOption("--target-work-item-id <workItemId>", "Target task's public identity", parseWorkItemId)
1449
- .requiredOption("--type <type>", "subtask, blocks or related", (value) => {
1411
+ .requiredOption("--type <type>", "subtask (source=parent, target=child), blocks or related", (value) => {
1450
1412
  const result = z.enum(["subtask", "blocks", "related"]).safeParse(value);
1451
1413
  if (!result.success)
1452
1414
  throw invalidArgument("Relation type must be subtask, blocks or related");
@@ -1461,7 +1423,24 @@ export async function runCli(arguments_, dependencies) {
1461
1423
  exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/relations", {
1462
1424
  body: { targetProjectId: options.targetProjectId, targetWorkItemId: options.targetWorkItemId, relationType: options.type },
1463
1425
  params: { path: { projectId: options.projectId, workItemId: options.workItemId }, header: { "idempotency-key": options.idempotencyKey } },
1464
- }), { retryTransportOnce: true, ambiguousOutcomeMessage: "Relation creation outcome is unknown; retry with the same inputs and command key" });
1426
+ }), { project: data => relationReceiptOutput(data, outputMode, warnMissingUrl), retryTransportOnce: true, ambiguousOutcomeMessage: "Relation creation outcome is unknown; retry with the same inputs and command key" });
1427
+ });
1428
+ workItem.command("subtask").command("link")
1429
+ .description("Link parent (source) to child (target), including across Projects")
1430
+ .requiredOption("--project-id <projectId>", "Parent Project")
1431
+ .requiredOption("--parent <workItemId>", "Parent public Work Item ID", parseWorkItemId)
1432
+ .requiredOption("--child <workItemId>", "Child public Work Item ID", parseWorkItemId)
1433
+ .option("--child-project-id <projectId>", "Child Project, defaults to parent Project", parseProjectId)
1434
+ .requiredOption("--idempotency-key <key>", "Reuse the same key and inputs when retrying", parseRelationCommandKey)
1435
+ .action(async (options) => {
1436
+ if (hasWorkspaceReadToken(dependencies.environment)) {
1437
+ exitCode = rejectTokenWrite(dependencies);
1438
+ return;
1439
+ }
1440
+ exitCode = await run(client => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/relations", {
1441
+ body: { relationType: "subtask", targetProjectId: options.childProjectId ?? options.projectId, targetWorkItemId: options.child },
1442
+ params: { path: { projectId: options.projectId, workItemId: options.parent }, header: { "idempotency-key": options.idempotencyKey } },
1443
+ }), { retryTransportOnce: true, project: data => relationReceiptOutput(data, outputMode, warnMissingUrl), ambiguousOutcomeMessage: "Relation creation outcome is unknown; retry with the same inputs and command key" });
1465
1444
  });
1466
1445
  workItemRelation.command("delete")
1467
1446
  .requiredOption("--project-id <projectId>")
@@ -1509,7 +1488,7 @@ export async function runCli(arguments_, dependencies) {
1509
1488
  limit: options.limit,
1510
1489
  },
1511
1490
  },
1512
- }));
1491
+ }), { project: data => itemListOutput(data, outputMode, warnMissingUrl) });
1513
1492
  });
1514
1493
  workItem
1515
1494
  .command("read")
@@ -1518,7 +1497,7 @@ export async function runCli(arguments_, dependencies) {
1518
1497
  .action(async (options) => {
1519
1498
  exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}", {
1520
1499
  params: { path: options },
1521
- }));
1500
+ }), { project: data => itemOutput(data, outputMode, warnMissingUrl) });
1522
1501
  });
1523
1502
  workItem
1524
1503
  .command("assignees")
@@ -1532,33 +1511,36 @@ export async function runCli(arguments_, dependencies) {
1532
1511
  .command("create")
1533
1512
  .requiredOption("--project-id <projectId>")
1534
1513
  .requiredOption("--title <title>")
1535
- .option("--description <description>")
1514
+ .option("--description <description>", "Inline description text", singleDescriptionSource)
1515
+ .option("--description-file <path>", "Read description from a UTF-8 regular file, or - for piped stdin", singleDescriptionSource)
1536
1516
  .option("--label <labelId>", "Workspace label definition ID; repeat for several labels", collectOption, [])
1537
- .option("--assignee <assignee>", "Workspace member User ID")
1517
+ .option("--assignee <assignee>", "Workspace member User ID, or me for the selected credential")
1538
1518
  .option("--custom-field <fieldId=value>", "Custom Field value as fieldId=value; repeat for several fields", collectOption, [])
1539
1519
  .option("--priority <priority>", "Work Item priority", parseWorkItemPriority)
1540
1520
  .option("--start-date <startDate>", "ISO 8601 date", parseWorkItemDate)
1541
1521
  .option("--due-date <dueDate>", "ISO 8601 date", parseWorkItemDate)
1542
1522
  .option("--status <status>", "Workflow Column slug or virtual status to create the Work Item in")
1543
1523
  .action(async (options) => {
1524
+ const description = (await readDescription(options)) ?? undefined;
1544
1525
  if (hasWorkspaceReadToken(dependencies.environment)) {
1545
1526
  exitCode = rejectTokenWrite(dependencies);
1546
1527
  return;
1547
1528
  }
1529
+ const customFields = options.customField.map(parseCustomFieldAssignment);
1548
1530
  const idempotencyKey = randomUUID();
1549
- exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items", {
1531
+ exitCode = await run((client, me) => client.POST("/v1/projects/{projectId}/work-items", {
1550
1532
  body: {
1551
1533
  ...(options.assignee === undefined
1552
1534
  ? {}
1553
- : { assigneeId: options.assignee }),
1535
+ : { assigneeId: me ?? options.assignee }),
1554
1536
  ...(options.customField.length === 0
1555
1537
  ? {}
1556
1538
  : {
1557
- customFields: options.customField.map(parseCustomFieldAssignment),
1539
+ customFields,
1558
1540
  }),
1559
- ...(options.description === undefined
1541
+ ...(description === undefined
1560
1542
  ? {}
1561
- : { description: options.description }),
1543
+ : { description }),
1562
1544
  ...(options.label.length === 0 ? {} : { labelIds: [...new Set(options.label)] }),
1563
1545
  ...(options.dueDate === undefined
1564
1546
  ? {}
@@ -1579,6 +1561,8 @@ export async function runCli(arguments_, dependencies) {
1579
1561
  path: { projectId: options.projectId },
1580
1562
  },
1581
1563
  }), {
1564
+ resolveMe: options.assignee === "me",
1565
+ project: data => itemOutput(data, outputMode, warnMissingUrl),
1582
1566
  ambiguousOutcomeMessage: "Work Item creation outcome is unknown; list Work Items before retrying",
1583
1567
  retryTransportOnce: true,
1584
1568
  });
@@ -1589,7 +1573,8 @@ export async function runCli(arguments_, dependencies) {
1589
1573
  .requiredOption("--work-item-id <workItemId>")
1590
1574
  .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1591
1575
  .option("--title <title>")
1592
- .option("--description <description>")
1576
+ .option("--description <description>", "Inline description text", singleDescriptionSource)
1577
+ .option("--description-file <path>", "Read description from a UTF-8 regular file, or - for piped stdin", singleDescriptionSource)
1593
1578
  .option("--clear-description", "Remove the Work Item description")
1594
1579
  .option("--priority <priority>", "Work Item priority", parseWorkItemPriority)
1595
1580
  .option("--start-date <startDate>", "ISO 8601 date", parseWorkItemDate)
@@ -1597,12 +1582,13 @@ export async function runCli(arguments_, dependencies) {
1597
1582
  .option("--clear-start-date", "Remove the Work Item start date")
1598
1583
  .option("--clear-due-date", "Remove the Work Item due date")
1599
1584
  .action(async (options) => {
1585
+ const description = (await readDescription(options)) ?? undefined;
1600
1586
  if (hasWorkspaceReadToken(dependencies.environment)) {
1601
1587
  exitCode = rejectTokenWrite(dependencies);
1602
1588
  return;
1603
1589
  }
1604
1590
  if (options.title === undefined &&
1605
- options.description === undefined &&
1591
+ description === undefined &&
1606
1592
  options.clearDescription !== true &&
1607
1593
  options.priority === undefined &&
1608
1594
  options.startDate === undefined &&
@@ -1611,10 +1597,6 @@ export async function runCli(arguments_, dependencies) {
1611
1597
  options.clearDueDate !== true) {
1612
1598
  throw invalidArgument("Work Item update requires a field change");
1613
1599
  }
1614
- if (options.clearDescription === true &&
1615
- options.description !== undefined) {
1616
- throw invalidArgument("Work Item update cannot set and clear the description together");
1617
- }
1618
1600
  if (options.clearStartDate === true && options.startDate !== undefined) {
1619
1601
  throw invalidArgument("Work Item update cannot set and clear the start date together");
1620
1602
  }
@@ -1626,9 +1608,9 @@ export async function runCli(arguments_, dependencies) {
1626
1608
  body: {
1627
1609
  ...(options.clearDescription === true
1628
1610
  ? { description: null }
1629
- : options.description === undefined
1611
+ : description === undefined
1630
1612
  ? {}
1631
- : { description: options.description }),
1613
+ : { description }),
1632
1614
  ...(options.clearDueDate === true
1633
1615
  ? { dueDate: null }
1634
1616
  : options.dueDate === undefined
@@ -1657,6 +1639,7 @@ export async function runCli(arguments_, dependencies) {
1657
1639
  },
1658
1640
  },
1659
1641
  }), {
1642
+ project: data => itemOutput(data, outputMode, warnMissingUrl),
1660
1643
  ambiguousOutcomeMessage: "Work Item update outcome is unknown; read the Work Item before retrying",
1661
1644
  retryTransportOnce: true,
1662
1645
  });
@@ -1666,7 +1649,7 @@ export async function runCli(arguments_, dependencies) {
1666
1649
  .requiredOption("--project-id <projectId>")
1667
1650
  .requiredOption("--work-item-id <workItemId>")
1668
1651
  .requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
1669
- .option("--assignee <assignee>", "Workspace member User ID")
1652
+ .option("--assignee <assignee>", "Workspace member User ID, or me for the selected credential")
1670
1653
  .option("--unassign", "Remove the Work Item assignee")
1671
1654
  .action(async (options) => {
1672
1655
  if (hasWorkspaceReadToken(dependencies.environment)) {
@@ -1680,9 +1663,9 @@ export async function runCli(arguments_, dependencies) {
1680
1663
  throw invalidArgument("Work Item assign cannot set and clear the assignee together");
1681
1664
  }
1682
1665
  const idempotencyKey = randomUUID();
1683
- exitCode = await run((client) => client.PUT("/v1/projects/{projectId}/work-items/{workItemId}/assignee", {
1666
+ exitCode = await run((client, me) => client.PUT("/v1/projects/{projectId}/work-items/{workItemId}/assignee", {
1684
1667
  body: {
1685
- assigneeId: options.unassign === true ? null : (options.assignee ?? null),
1668
+ assigneeId: options.unassign === true ? null : (me ?? options.assignee ?? null),
1686
1669
  },
1687
1670
  params: {
1688
1671
  header: {
@@ -1695,6 +1678,8 @@ export async function runCli(arguments_, dependencies) {
1695
1678
  },
1696
1679
  },
1697
1680
  }), {
1681
+ resolveMe: options.assignee === "me",
1682
+ project: data => itemOutput(data, outputMode, warnMissingUrl),
1698
1683
  ambiguousOutcomeMessage: "Work Item assignment outcome is unknown; read the Work Item before retrying",
1699
1684
  retryTransportOnce: true,
1700
1685
  });
@@ -1743,7 +1728,7 @@ export async function runCli(arguments_, dependencies) {
1743
1728
  .requiredOption("--target <[projectId/]id@version>", "Target with its observed version, e.g. PAY-1@3 or project-id/PAY-2@4; repeat within the same Workspace", parseWorkItemBulkTarget, [])
1744
1729
  .option("--status <status>", "Destination Workflow Column slug or virtual status (updateStatus)")
1745
1730
  .option("--priority <priority>", "Priority (updatePriority)", parseWorkItemPriority)
1746
- .option("--assignee <assignee>", "Assignee User ID (updateAssignee)")
1731
+ .option("--assignee <assignee>", "Assignee User ID only (updateAssignee); me is not supported")
1747
1732
  .option("--unassign", "Clear the assignee (updateAssignee)")
1748
1733
  .option("--label-id <labelId>", "Work Item Label ID (addLabel/removeLabel)")
1749
1734
  .option("--due-date <dueDate>", "Due date (updateDueDate)", parseWorkItemDate)
@@ -2556,7 +2541,7 @@ export async function runCli(arguments_, dependencies) {
2556
2541
  }
2557
2542
  catch (cause) {
2558
2543
  if (cause instanceof CommanderError) {
2559
- if (cause.code === "commander.helpDisplayed") {
2544
+ if (cause.exitCode === 0) {
2560
2545
  return cause.exitCode;
2561
2546
  }
2562
2547
  writeSkillError(dependencies, {
@@ -2570,130 +2555,6 @@ export async function runCli(arguments_, dependencies) {
2570
2555
  throw cause;
2571
2556
  }
2572
2557
  }
2573
- async function executeRequest(dependencies, request, options = {}, serverFlag, apiKeyIdFlag) {
2574
- if (dependencies.environment.SPEC_FLOW_PROJECT_READ_TOKEN !== undefined) {
2575
- writeSkillError(dependencies, { message: "Project Read Tokens are retired; use a Workspace read-only Token", reason: "Authentication Failed", retryable: false });
2576
- return 2;
2577
- }
2578
- const environmentCredential = dependencies.environment.SPEC_FLOW_WORKSPACE_READ_TOKEN;
2579
- const apiKeyEnvironment = dependencies.environment.SPEC_FLOW_API_KEY;
2580
- const usesApiKey = apiKeyEnvironment !== undefined || apiKeyIdFlag !== undefined;
2581
- if (options.requireUserSession === true && (usesApiKey || hasWorkspaceReadToken(dependencies.environment))) {
2582
- writeSkillError(dependencies, { message: "A User Session is required for account management", reason: "Permission Denied", retryable: false });
2583
- return 3;
2584
- }
2585
- if ((apiKeyEnvironment !== undefined && apiKeyIdFlag !== undefined) || (usesApiKey && hasWorkspaceReadToken(dependencies.environment))) {
2586
- writeSkillError(dependencies, { message: "Select exactly one API Key or Workspace read-only Token credential", reason: "Invalid Operation", retryable: false });
2587
- return 2;
2588
- }
2589
- if (apiKeyIdFlag !== undefined && !z.uuid().safeParse(apiKeyIdFlag).success) {
2590
- writeSkillError(dependencies, { message: "API Key ID must be a UUID", reason: "Invalid Operation", retryable: false });
2591
- return 2;
2592
- }
2593
- if (apiKeyEnvironment !== undefined && !/^sfk_[A-Za-z0-9_-]{43}$/u.test(apiKeyEnvironment)) {
2594
- writeSkillError(dependencies, { message: "SPEC_FLOW_API_KEY does not contain a User API Key", reason: "Authentication Failed", retryable: false });
2595
- return 2;
2596
- }
2597
- if (options.rejectWorkspaceReadToken === true &&
2598
- hasWorkspaceReadToken(dependencies.environment)) {
2599
- return rejectWorkspaceReadTokenOperation(dependencies);
2600
- }
2601
- const baseUrl = resolveApiOrigin(dependencies, serverFlag);
2602
- if (baseUrl === null) {
2603
- return 2;
2604
- }
2605
- let credential;
2606
- try {
2607
- if (apiKeyEnvironment !== undefined)
2608
- credential = apiKeyEnvironment;
2609
- else if (typeof apiKeyIdFlag === "string") {
2610
- if (dependencies.credentialStore.readApiKey === undefined)
2611
- throw new Error("API Key storage is unavailable");
2612
- credential = await dependencies.credentialStore.readApiKey(apiKeyIdFlag.toLowerCase());
2613
- }
2614
- else {
2615
- credential = environmentCredential ?? await dependencies.credentialStore.read();
2616
- }
2617
- }
2618
- catch {
2619
- writeSkillError(dependencies, {
2620
- message: "The CLI credential store is temporarily unavailable",
2621
- reason: "Temporary Failure",
2622
- retryable: true,
2623
- });
2624
- return 4;
2625
- }
2626
- if (credential === null || credential.trim().length === 0) {
2627
- writeSkillError(dependencies, {
2628
- message: "An authenticated CLI Session, User API Key or Workspace read-only Token is required",
2629
- reason: "Authentication Failed",
2630
- retryable: false,
2631
- });
2632
- return 2;
2633
- }
2634
- const client = createClient({
2635
- baseUrl,
2636
- ...(usesApiKey || options.requireUserSession === true ? { redirect: "error" } : {}),
2637
- fetch: options.retryTransportOnce === true
2638
- ? retryTransportOnce(dependencies.fetch)
2639
- : dependencies.fetch,
2640
- headers: {
2641
- ...(usesApiKey ? { "x-api-key": credential } : { authorization: `Bearer ${credential}` }),
2642
- "user-agent": specFlowCliUserAgent,
2643
- },
2644
- });
2645
- try {
2646
- const result = await request(client);
2647
- if (result.response.ok && result.data !== undefined) {
2648
- const validationError = options.validateSuccess?.(result.data) ?? null;
2649
- if (validationError !== null) {
2650
- writeSkillError(dependencies, validationError);
2651
- return skillErrorExitCode(validationError);
2652
- }
2653
- dependencies.stdout(`${JSON.stringify(result.data)}\n`);
2654
- return 0;
2655
- }
2656
- if (result.response.ok && options.retryTransportOnce === true) {
2657
- const error = ambiguousMutationError(options.ambiguousOutcomeMessage);
2658
- writeSkillError(dependencies, error);
2659
- return skillErrorExitCode(error);
2660
- }
2661
- const error = mapHttpFailure(result.response, result.error);
2662
- writeSkillError(dependencies, error);
2663
- return skillErrorExitCode(error);
2664
- }
2665
- catch {
2666
- const error = options.retryTransportOnce === true
2667
- ? ambiguousMutationError(options.ambiguousOutcomeMessage)
2668
- : {
2669
- message: "SpecFlow is temporarily unavailable",
2670
- reason: "Temporary Failure",
2671
- retryable: true,
2672
- };
2673
- writeSkillError(dependencies, error);
2674
- return skillErrorExitCode(error);
2675
- }
2676
- }
2677
- function retryTransportOnce(fetch) {
2678
- return async (request) => {
2679
- const retryRequest = request.clone();
2680
- try {
2681
- return await fetch(request);
2682
- }
2683
- catch {
2684
- return fetch(retryRequest);
2685
- }
2686
- };
2687
- }
2688
- function ambiguousMutationError(message) {
2689
- return {
2690
- message: message ??
2691
- "Write outcome is unknown; read the target resource before trying a new command identity",
2692
- code: "EXECUTION_OUTCOME_UNKNOWN",
2693
- reason: "Temporary Failure",
2694
- retryable: false,
2695
- };
2696
- }
2697
2558
  function validateIssuedWorkspaceReadToken(data) {
2698
2559
  if (typeof data === "object" &&
2699
2560
  data !== null &&
@@ -2707,21 +2568,6 @@ function validateIssuedWorkspaceReadToken(data) {
2707
2568
  }
2708
2569
  return null;
2709
2570
  }
2710
- function resolveApiOrigin(dependencies, serverFlag) {
2711
- const flag = typeof serverFlag === "string" ? serverFlag : undefined;
2712
- const result = parseHttpOrigin(unresolvedSpecFlowApiOrigin(flag, dependencies.environment.SPEC_FLOW_API_URL));
2713
- if (result.valid) {
2714
- return result.origin;
2715
- }
2716
- writeSkillError(dependencies, {
2717
- message: flag === undefined
2718
- ? "SPEC_FLOW_API_URL must be an HTTP(S) origin without a path"
2719
- : "--server must be an HTTP(S) origin without a path",
2720
- reason: "Invalid Operation",
2721
- retryable: false,
2722
- });
2723
- return null;
2724
- }
2725
2571
  function parseRelationCommandKey(value) {
2726
2572
  const key = value.trim();
2727
2573
  if (key.length === 0 || key.length > 200)
@@ -2872,7 +2718,7 @@ function workItemSelectionQuery(options) {
2872
2718
  */
2873
2719
  function workItemSelectionFlags(command) {
2874
2720
  return command
2875
- .option("--assignee <assignee>", "Filter by assignee User ID, or unassigned for items with none; repeat for several", collectOption, [])
2721
+ .option("--assignee <assignee>", "Filter by assignee User ID only (me is not supported), or unassigned for items with none; repeat for several", collectOption, [])
2876
2722
  .option("--custom-field <fieldId=value>", "Filter by Custom Field value; repeat to OR values or combine fields", collectOption, [])
2877
2723
  .option("--due <filter>", "Filter by due bucket: dueThisWeek, dueNextWeek, or noDueDate; repeat for several", (value, previous) => [
2878
2724
  ...previous,
@@ -3186,146 +3032,6 @@ function rejectTokenWrite(dependencies) {
3186
3032
  });
3187
3033
  return 2;
3188
3034
  }
3189
- function rejectWorkspaceReadTokenOperation(dependencies) {
3190
- writeSkillError(dependencies, {
3191
- message: "Workspace read-only Token does not permit this operation",
3192
- reason: "Permission Denied",
3193
- retryable: false,
3194
- });
3195
- return 3;
3196
- }
3197
- function hasWorkspaceReadToken(environment) {
3198
- const credential = environment.SPEC_FLOW_WORKSPACE_READ_TOKEN;
3199
- return credential !== undefined && credential.trim().length > 0;
3200
- }
3201
- function mapHttpFailure(response, payload) {
3202
- const failure = classifyHttpFailure(response, payload);
3203
- const issues = z.array(z.object({ location: z.enum(["body", "query", "path", "headers", "tool"]), path: z.array(z.union([z.string().max(100), z.number()])), rule: z.string().max(100), message: z.string().max(500) })).max(20).safeParse(z.object({ issues: z.unknown() }).safeParse(payload).data?.issues);
3204
- const code = readServerCode(payload);
3205
- const requestId = response.headers.get("x-request-id");
3206
- return { ...failure, ...(requestId !== null && /^[A-Za-z0-9._:-]{1,100}$/u.test(requestId) ? { requestId } : {}), status: response.status, ...(code !== null && /^[A-Z][A-Z0-9_]{0,79}$/u.test(code) ? { code } : {}), ...(issues.success ? { issues: issues.data } : {}) };
3207
- }
3208
- function classifyHttpFailure(response, payload) {
3209
- const { status } = response;
3210
- const serverMessage = readServerMessage(payload);
3211
- const serverCode = readServerCode(payload);
3212
- if (status === 401) {
3213
- return {
3214
- message: serverMessage ?? "Workspace read-only Token is invalid, expired, or revoked",
3215
- reason: "Authentication Failed",
3216
- retryable: false,
3217
- };
3218
- }
3219
- if (status === 403) {
3220
- return {
3221
- message: serverMessage ?? "Workspace read-only Token does not permit this operation",
3222
- reason: "Permission Denied",
3223
- retryable: false,
3224
- };
3225
- }
3226
- if (status === 404) {
3227
- if (serverCode === "PROJECT_NOT_ACCESSIBLE") {
3228
- return {
3229
- message: serverMessage ?? "The Project is not accessible",
3230
- reason: "Project Not Accessible",
3231
- retryable: false,
3232
- };
3233
- }
3234
- return {
3235
- message: serverMessage ?? "The requested Project resource was not found",
3236
- reason: "Resource Not Found",
3237
- retryable: false,
3238
- };
3239
- }
3240
- if (status === 409 && serverCode === "RESOURCE_CHANGED") {
3241
- return {
3242
- message: serverMessage ?? "The Working Draft changed after the Proposal baseline",
3243
- reason: "Resource Changed",
3244
- retryable: false,
3245
- };
3246
- }
3247
- if (status === 400 || status === 409 || status === 413 || status === 422 || status === 428) {
3248
- const diagnostics = readServerDiagnostics(payload);
3249
- return {
3250
- ...(diagnostics === undefined ? {} : { diagnostics }),
3251
- message: serverMessage ?? "The requested operation is invalid",
3252
- reason: "Invalid Operation",
3253
- retryable: false,
3254
- };
3255
- }
3256
- if (status === 412) {
3257
- return {
3258
- message: serverMessage ?? "The resource changed after it was read",
3259
- reason: "Resource Changed",
3260
- retryable: false,
3261
- };
3262
- }
3263
- if (status === 429) {
3264
- const retryAfter = response.headers.get("retry-after");
3265
- return {
3266
- message: serverMessage ?? "SpecFlow is temporarily throttling this client",
3267
- reason: "Temporary Failure",
3268
- ...(retryAfter === null ? {} : { retryAfter }),
3269
- retryable: true,
3270
- };
3271
- }
3272
- return {
3273
- message: serverMessage ?? "SpecFlow is temporarily unavailable",
3274
- reason: "Temporary Failure",
3275
- retryable: status >= 500,
3276
- };
3277
- }
3278
- function readServerMessage(payload) {
3279
- return typeof payload === "object" &&
3280
- payload !== null &&
3281
- "message" in payload &&
3282
- typeof payload.message === "string"
3283
- ? payload.message
3284
- : null;
3285
- }
3286
- function readServerCode(payload) {
3287
- return typeof payload === "object" &&
3288
- payload !== null &&
3289
- "code" in payload &&
3290
- typeof payload.code === "string"
3291
- ? payload.code
3292
- : null;
3293
- }
3294
- function readServerDiagnostics(payload) {
3295
- if (typeof payload !== "object" ||
3296
- payload === null ||
3297
- !("diagnostics" in payload) ||
3298
- !Array.isArray(payload.diagnostics)) {
3299
- return undefined;
3300
- }
3301
- const diagnostics = payload.diagnostics.flatMap((entry) => {
3302
- if (typeof entry !== "object" || entry === null) {
3303
- return [];
3304
- }
3305
- const { column, line, message, reason } = entry;
3306
- if (typeof reason !== "string" || typeof message !== "string") {
3307
- return [];
3308
- }
3309
- return [
3310
- {
3311
- ...(typeof column === "number" ? { column } : {}),
3312
- ...(typeof line === "number" ? { line } : {}),
3313
- message,
3314
- reason,
3315
- },
3316
- ];
3317
- });
3318
- return diagnostics.length === 0 ? undefined : diagnostics;
3319
- }
3320
- function skillErrorExitCode(error) {
3321
- if (error.reason === "Permission Denied") {
3322
- return 3;
3323
- }
3324
- return error.reason === "Temporary Failure" ? 4 : 2;
3325
- }
3326
- function writeSkillError(dependencies, error) {
3327
- dependencies.stderr(`${JSON.stringify({ error })}\n`);
3328
- }
3329
3035
  function parseCommentContent(value) {
3330
3036
  const result = z.string().trim().min(1).max(20_000).refine((content) => !content.includes("\0")).safeParse(value);
3331
3037
  if (!result.success)