@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.
- package/README.md +57 -28
- package/dist/bin.js +6 -50
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +103 -397
- package/dist/cli.js.map +1 -1
- package/dist/commands/work-item-output.d.ts +390 -0
- package/dist/commands/work-item-output.js +36 -0
- package/dist/commands/work-item-output.js.map +1 -0
- package/dist/http-origin.js +3 -0
- package/dist/http-origin.js.map +1 -1
- package/dist/index.js +50 -28
- package/dist/index.js.map +1 -1
- package/dist/input/text-source.d.ts +9 -0
- package/dist/input/text-source.js +74 -0
- package/dist/input/text-source.js.map +1 -0
- package/dist/program.d.ts +3 -0
- package/dist/program.js +19 -0
- package/dist/program.js.map +1 -0
- package/dist/runtime/errors.d.ts +28 -0
- package/dist/runtime/errors.js +133 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/execute.d.ts +23 -0
- package/dist/runtime/execute.js +25 -0
- package/dist/runtime/execute.js.map +1 -0
- package/dist/runtime/identity.d.ts +183 -0
- package/dist/runtime/identity.js +17 -0
- package/dist/runtime/identity.js.map +1 -0
- package/dist/runtime/output.d.ts +3 -0
- package/dist/runtime/output.js +13 -0
- package/dist/runtime/output.js.map +1 -0
- package/dist/runtime/session.d.ts +16 -0
- package/dist/runtime/session.js +123 -0
- package/dist/runtime/session.js.map +1 -0
- package/package.json +1 -1
- package/dist/work-item-time-entry-input.d.ts +0 -4
- package/dist/work-item-time-entry-input.js +0 -27
- package/dist/work-item-time-entry-input.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
1539
|
+
customFields,
|
|
1558
1540
|
}),
|
|
1559
|
-
...(
|
|
1541
|
+
...(description === undefined
|
|
1560
1542
|
? {}
|
|
1561
|
-
: { 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
|
-
|
|
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
|
-
:
|
|
1611
|
+
: description === undefined
|
|
1630
1612
|
? {}
|
|
1631
|
-
: { 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.
|
|
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)
|