@gonovus-ai/spec-flow-cli 0.4.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 +190 -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 +126 -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/custom-field-color-commands.d.ts +7 -0
- package/dist/custom-field-color-commands.js +22 -0
- package/dist/custom-field-color-commands.js.map +1 -0
- package/dist/github-connection-commands.js +23 -3
- package/dist/github-connection-commands.js.map +1 -1
- 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/product-map-commands.d.ts +2 -0
- package/dist/product-map-commands.js +2 -0
- package/dist/product-map-commands.js.map +1 -1
- package/dist/program.d.ts +3 -0
- package/dist/program.js +19 -0
- package/dist/program.js.map +1 -0
- package/dist/project-administration-commands.d.ts +3 -0
- package/dist/project-administration-commands.js +31 -0
- package/dist/project-administration-commands.js.map +1 -0
- package/dist/resource-command-runtime.d.ts +16 -0
- package/dist/resource-command-runtime.js +2 -0
- package/dist/resource-command-runtime.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/dist/screen-image-commands.d.ts +10 -0
- package/dist/screen-image-commands.js +77 -0
- package/dist/screen-image-commands.js.map +1 -0
- package/dist/spec-discussion-commands.d.ts +3 -0
- package/dist/spec-discussion-commands.js +56 -0
- package/dist/spec-discussion-commands.js.map +1 -0
- package/dist/spec-owner-commands.d.ts +3 -0
- package/dist/spec-owner-commands.js +37 -0
- package/dist/spec-owner-commands.js.map +1 -0
- package/dist/work-item-asset-commands.d.ts +13 -0
- package/dist/work-item-asset-commands.js +10 -9
- package/dist/work-item-asset-commands.js.map +1 -1
- package/dist/work-item-query-commands.d.ts +3 -0
- package/dist/work-item-query-commands.js +29 -0
- package/dist/work-item-query-commands.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,17 +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
|
+
import { registerCustomFieldColorCommands, parseOptionColors } from "./custom-field-color-commands.js";
|
|
6
7
|
import { registerGitHubAccountCommands } from "./github-account-commands.js";
|
|
7
|
-
import {
|
|
8
|
+
import { readDescription, singleDescriptionSource } from "./input/text-source.js";
|
|
8
9
|
import { registerProductMapCommands } from "./product-map-commands.js";
|
|
9
|
-
import {
|
|
10
|
+
import { createProgram } from "./program.js";
|
|
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";
|
|
17
|
+
import { registerSpecDiscussionCommands } from "./spec-discussion-commands.js";
|
|
18
|
+
import { registerSpecOwnerCommands } from "./spec-owner-commands.js";
|
|
10
19
|
import { registerUserApiKeyCommands } from "./user-api-key-commands.js";
|
|
11
20
|
import { registerWorkItemAssetCommands } from "./work-item-asset-commands.js";
|
|
12
21
|
import { readWorkItemImportFile } from "./work-item-exchange-file.js";
|
|
22
|
+
import { registerWorkItemQueryCommands } from "./work-item-query-commands.js";
|
|
13
23
|
import { registerWorkItemResourceLinkCommands } from "./work-item-resource-link-commands.js";
|
|
14
|
-
import { parseTimeEntryTimestamp, parseTimeEntryUuid, parseTimeEntryDescription } from "./work-item-time-entry-input.js";
|
|
15
24
|
export function formatResourceDiagnostic(message) {
|
|
16
25
|
return `${JSON.stringify({ diagnostic: { level: "warning", message } })}\n`;
|
|
17
26
|
}
|
|
@@ -130,22 +139,47 @@ const findingCreateInputSchema = z
|
|
|
130
139
|
.strict();
|
|
131
140
|
export async function runCli(arguments_, dependencies) {
|
|
132
141
|
let exitCode = 0;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
};
|
|
145
167
|
registerUserApiKeyCommands(program, { dependencies, run, setExitCode: (value) => { exitCode = value; }, parseScopes: parseRoleStatements, parseLimit: parseSearchLimit });
|
|
146
168
|
registerGitHubAccountCommands(program, { dependencies, run, setExitCode: (value) => { exitCode = value; } });
|
|
147
169
|
const authentication = addAuthenticationCommandTopology(program);
|
|
148
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
|
+
}
|
|
149
183
|
const options = actionCommand.opts();
|
|
150
184
|
if (typeof options.projectId === "string") {
|
|
151
185
|
actionCommand.setOptionValue("projectId", parseProjectId(options.projectId));
|
|
@@ -166,6 +200,13 @@ export async function runCli(arguments_, dependencies) {
|
|
|
166
200
|
throw new CommanderError(2, "specflow.authenticationCommand", "Authentication commands use the human-facing CLI boundary");
|
|
167
201
|
});
|
|
168
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
|
+
});
|
|
169
210
|
const workspace = program.command("workspace");
|
|
170
211
|
workspace
|
|
171
212
|
.command("list")
|
|
@@ -1267,83 +1308,23 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1267
1308
|
});
|
|
1268
1309
|
const workItem = program.command("work-item");
|
|
1269
1310
|
registerWorkItemAssetCommands(workItem, { dependencies, run, setExitCode: (value) => { exitCode = value; }, rejectWrite: () => rejectTokenWrite(dependencies) });
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
.requiredOption("--
|
|
1276
|
-
.option("--
|
|
1277
|
-
.option("--
|
|
1278
|
-
.
|
|
1279
|
-
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 }) } } }));
|
|
1280
|
-
});
|
|
1281
|
-
timeEntry.command("get")
|
|
1282
|
-
.requiredOption("--project-id <projectId>")
|
|
1283
|
-
.requiredOption("--work-item-id <workItemId>")
|
|
1284
|
-
.requiredOption("--entry-id <entryId>", "Time Entry UUID", (value) => { try {
|
|
1285
|
-
return parseTimeEntryUuid(value);
|
|
1286
|
-
}
|
|
1287
|
-
catch {
|
|
1288
|
-
throw invalidArgument("Time Entry ID must be a UUID");
|
|
1289
|
-
} })
|
|
1311
|
+
const resourceRuntime = { run, setExitCode: (value) => { exitCode = value; }, parseOpaqueVersion, parseLimit: parseSearchLimit };
|
|
1312
|
+
registerSpecOwnerCommands(spec, resourceRuntime);
|
|
1313
|
+
registerSpecDiscussionCommands(spec, resourceRuntime);
|
|
1314
|
+
registerProjectAdministrationCommands(project, workspace, resourceRuntime);
|
|
1315
|
+
program.command("search-global").description("Search all currently accessible objects; preserves server totals and cursor")
|
|
1316
|
+
.requiredOption("--query <query>")
|
|
1317
|
+
.option("--types <types>", "Comma-separated kinds: project,work-item,capability,spec,glossary,screen,journey,user")
|
|
1318
|
+
.option("--limit <limit>", "Server page size (1–100)", resourceRuntime.parseLimit, 20)
|
|
1319
|
+
.option("--cursor <cursor>")
|
|
1290
1320
|
.action(async (options) => {
|
|
1291
|
-
|
|
1321
|
+
resourceRuntime.setExitCode(await resourceRuntime.run(client => client.GET("/v1/search", {
|
|
1322
|
+
params: { query: { query: options.query, limit: options.limit, ...(options.cursor === undefined ? {} : { cursor: options.cursor }), ...(options.types === undefined ? {} : { types: options.types }) } },
|
|
1323
|
+
}), { rejectWorkspaceReadToken: true }));
|
|
1292
1324
|
});
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
.requiredOption("--work-item-id <workItemId>")
|
|
1297
|
-
.requiredOption("--start-time <timestamp>", "ISO date-time with an offset", (value) => { try {
|
|
1298
|
-
return parseTimeEntryTimestamp(value);
|
|
1299
|
-
}
|
|
1300
|
-
catch {
|
|
1301
|
-
throw invalidArgument("Start time must be a valid ISO 8601 timestamp with an offset");
|
|
1302
|
-
} })
|
|
1303
|
-
.option("--end-time <timestamp>", "Omit to start running, or preserve the stored end on update", (value) => { try {
|
|
1304
|
-
return parseTimeEntryTimestamp(value);
|
|
1305
|
-
}
|
|
1306
|
-
catch {
|
|
1307
|
-
throw invalidArgument("End time must be a valid ISO 8601 timestamp with an offset");
|
|
1308
|
-
} })
|
|
1309
|
-
.option("--description <description>", "Optional recorded work description", (value) => { try {
|
|
1310
|
-
return parseTimeEntryDescription(value);
|
|
1311
|
-
}
|
|
1312
|
-
catch {
|
|
1313
|
-
throw invalidArgument("Time Entry description exceeds the allowed limit");
|
|
1314
|
-
} })
|
|
1315
|
-
.requiredOption("--idempotency-key <key>", "Reuse this key and inputs for the same action", (value) => { try {
|
|
1316
|
-
return parseTimeEntryUuid(value);
|
|
1317
|
-
}
|
|
1318
|
-
catch {
|
|
1319
|
-
throw invalidArgument("Command key must be a UUID");
|
|
1320
|
-
} });
|
|
1321
|
-
if (action === "update")
|
|
1322
|
-
command.requiredOption("--entry-id <entryId>", "Time Entry UUID", (value) => { try {
|
|
1323
|
-
return parseTimeEntryUuid(value);
|
|
1324
|
-
}
|
|
1325
|
-
catch {
|
|
1326
|
-
throw invalidArgument("Time Entry ID must be a UUID");
|
|
1327
|
-
} }).requiredOption("--expected-version <version>", "Opaque version returned by SpecFlow", parseOpaqueVersion);
|
|
1328
|
-
command.action(async (options) => {
|
|
1329
|
-
if (hasWorkspaceReadToken(dependencies.environment)) {
|
|
1330
|
-
exitCode = rejectTokenWrite(dependencies);
|
|
1331
|
-
return;
|
|
1332
|
-
}
|
|
1333
|
-
const input = { startTime: options.startTime, ...(options.endTime === undefined ? {} : { endTime: options.endTime }), ...(options.description === undefined ? {} : { description: options.description }) };
|
|
1334
|
-
if (options.endTime !== undefined && Date.parse(options.startTime) > Date.parse(options.endTime))
|
|
1335
|
-
throw invalidArgument("Start time cannot follow end time");
|
|
1336
|
-
const path = { projectId: options.projectId, workItemId: options.workItemId };
|
|
1337
|
-
const header = { "idempotency-key": options.idempotencyKey };
|
|
1338
|
-
exitCode = await run((client) => {
|
|
1339
|
-
if (action === "create")
|
|
1340
|
-
return client.POST("/v1/projects/{projectId}/work-items/{workItemId}/time-entries", { body: input, params: { path, header } });
|
|
1341
|
-
if (options.entryId === undefined || options.expectedVersion === undefined)
|
|
1342
|
-
throw new Error("Time Entry update requires its identity and observed version");
|
|
1343
|
-
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 } } });
|
|
1344
|
-
}, { retryTransportOnce: true, ambiguousOutcomeMessage: "Time Entry outcome is unknown; retry the same inputs and command key" });
|
|
1345
|
-
});
|
|
1346
|
-
}
|
|
1325
|
+
registerWorkItemQueryCommands(workItem, workspace, resourceRuntime);
|
|
1326
|
+
registerProductMapCommands(program, { dependencies, run, setExitCode: value => { exitCode = value; }, canWrite: () => !hasWorkspaceReadToken(dependencies.environment), rejectWrite: () => rejectTokenWrite(dependencies), parseOpaqueVersion });
|
|
1327
|
+
registerWorkItemResourceLinkCommands(workItem, spec, { run, setExitCode: (value) => { exitCode = value; }, canWrite: () => !hasWorkspaceReadToken(dependencies.environment), rejectWrite: () => rejectTokenWrite(dependencies), parseOpaqueVersion, parseLimit: parseSearchLimit });
|
|
1347
1328
|
const workItemComment = workItem.command("comment").description("Read and manage authored task comments");
|
|
1348
1329
|
workItemComment.command("list")
|
|
1349
1330
|
.requiredOption("--project-id <projectId>")
|
|
@@ -1419,14 +1400,15 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1419
1400
|
.requiredOption("--project-id <projectId>")
|
|
1420
1401
|
.requiredOption("--work-item-id <workItemId>")
|
|
1421
1402
|
.action(async (options) => {
|
|
1422
|
-
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) });
|
|
1423
1404
|
});
|
|
1424
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")
|
|
1425
1407
|
.requiredOption("--project-id <projectId>")
|
|
1426
1408
|
.requiredOption("--work-item-id <workItemId>")
|
|
1427
1409
|
.requiredOption("--target-project-id <projectId>", "Target task's current Project", parseProjectId)
|
|
1428
1410
|
.requiredOption("--target-work-item-id <workItemId>", "Target task's public identity", parseWorkItemId)
|
|
1429
|
-
.requiredOption("--type <type>", "subtask, blocks or related", (value) => {
|
|
1411
|
+
.requiredOption("--type <type>", "subtask (source=parent, target=child), blocks or related", (value) => {
|
|
1430
1412
|
const result = z.enum(["subtask", "blocks", "related"]).safeParse(value);
|
|
1431
1413
|
if (!result.success)
|
|
1432
1414
|
throw invalidArgument("Relation type must be subtask, blocks or related");
|
|
@@ -1441,7 +1423,24 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1441
1423
|
exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items/{workItemId}/relations", {
|
|
1442
1424
|
body: { targetProjectId: options.targetProjectId, targetWorkItemId: options.targetWorkItemId, relationType: options.type },
|
|
1443
1425
|
params: { path: { projectId: options.projectId, workItemId: options.workItemId }, header: { "idempotency-key": options.idempotencyKey } },
|
|
1444
|
-
}), { 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" });
|
|
1445
1444
|
});
|
|
1446
1445
|
workItemRelation.command("delete")
|
|
1447
1446
|
.requiredOption("--project-id <projectId>")
|
|
@@ -1489,7 +1488,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1489
1488
|
limit: options.limit,
|
|
1490
1489
|
},
|
|
1491
1490
|
},
|
|
1492
|
-
}));
|
|
1491
|
+
}), { project: data => itemListOutput(data, outputMode, warnMissingUrl) });
|
|
1493
1492
|
});
|
|
1494
1493
|
workItem
|
|
1495
1494
|
.command("read")
|
|
@@ -1498,7 +1497,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1498
1497
|
.action(async (options) => {
|
|
1499
1498
|
exitCode = await run((client) => client.GET("/v1/projects/{projectId}/work-items/{workItemId}", {
|
|
1500
1499
|
params: { path: options },
|
|
1501
|
-
}));
|
|
1500
|
+
}), { project: data => itemOutput(data, outputMode, warnMissingUrl) });
|
|
1502
1501
|
});
|
|
1503
1502
|
workItem
|
|
1504
1503
|
.command("assignees")
|
|
@@ -1512,33 +1511,36 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1512
1511
|
.command("create")
|
|
1513
1512
|
.requiredOption("--project-id <projectId>")
|
|
1514
1513
|
.requiredOption("--title <title>")
|
|
1515
|
-
.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)
|
|
1516
1516
|
.option("--label <labelId>", "Workspace label definition ID; repeat for several labels", collectOption, [])
|
|
1517
|
-
.option("--assignee <assignee>", "Workspace member User ID")
|
|
1517
|
+
.option("--assignee <assignee>", "Workspace member User ID, or me for the selected credential")
|
|
1518
1518
|
.option("--custom-field <fieldId=value>", "Custom Field value as fieldId=value; repeat for several fields", collectOption, [])
|
|
1519
1519
|
.option("--priority <priority>", "Work Item priority", parseWorkItemPriority)
|
|
1520
1520
|
.option("--start-date <startDate>", "ISO 8601 date", parseWorkItemDate)
|
|
1521
1521
|
.option("--due-date <dueDate>", "ISO 8601 date", parseWorkItemDate)
|
|
1522
1522
|
.option("--status <status>", "Workflow Column slug or virtual status to create the Work Item in")
|
|
1523
1523
|
.action(async (options) => {
|
|
1524
|
+
const description = (await readDescription(options)) ?? undefined;
|
|
1524
1525
|
if (hasWorkspaceReadToken(dependencies.environment)) {
|
|
1525
1526
|
exitCode = rejectTokenWrite(dependencies);
|
|
1526
1527
|
return;
|
|
1527
1528
|
}
|
|
1529
|
+
const customFields = options.customField.map(parseCustomFieldAssignment);
|
|
1528
1530
|
const idempotencyKey = randomUUID();
|
|
1529
|
-
exitCode = await run((client) => client.POST("/v1/projects/{projectId}/work-items", {
|
|
1531
|
+
exitCode = await run((client, me) => client.POST("/v1/projects/{projectId}/work-items", {
|
|
1530
1532
|
body: {
|
|
1531
1533
|
...(options.assignee === undefined
|
|
1532
1534
|
? {}
|
|
1533
|
-
: { assigneeId: options.assignee }),
|
|
1535
|
+
: { assigneeId: me ?? options.assignee }),
|
|
1534
1536
|
...(options.customField.length === 0
|
|
1535
1537
|
? {}
|
|
1536
1538
|
: {
|
|
1537
|
-
customFields
|
|
1539
|
+
customFields,
|
|
1538
1540
|
}),
|
|
1539
|
-
...(
|
|
1541
|
+
...(description === undefined
|
|
1540
1542
|
? {}
|
|
1541
|
-
: { description
|
|
1543
|
+
: { description }),
|
|
1542
1544
|
...(options.label.length === 0 ? {} : { labelIds: [...new Set(options.label)] }),
|
|
1543
1545
|
...(options.dueDate === undefined
|
|
1544
1546
|
? {}
|
|
@@ -1559,6 +1561,8 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1559
1561
|
path: { projectId: options.projectId },
|
|
1560
1562
|
},
|
|
1561
1563
|
}), {
|
|
1564
|
+
resolveMe: options.assignee === "me",
|
|
1565
|
+
project: data => itemOutput(data, outputMode, warnMissingUrl),
|
|
1562
1566
|
ambiguousOutcomeMessage: "Work Item creation outcome is unknown; list Work Items before retrying",
|
|
1563
1567
|
retryTransportOnce: true,
|
|
1564
1568
|
});
|
|
@@ -1569,7 +1573,8 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1569
1573
|
.requiredOption("--work-item-id <workItemId>")
|
|
1570
1574
|
.requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
|
|
1571
1575
|
.option("--title <title>")
|
|
1572
|
-
.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)
|
|
1573
1578
|
.option("--clear-description", "Remove the Work Item description")
|
|
1574
1579
|
.option("--priority <priority>", "Work Item priority", parseWorkItemPriority)
|
|
1575
1580
|
.option("--start-date <startDate>", "ISO 8601 date", parseWorkItemDate)
|
|
@@ -1577,12 +1582,13 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1577
1582
|
.option("--clear-start-date", "Remove the Work Item start date")
|
|
1578
1583
|
.option("--clear-due-date", "Remove the Work Item due date")
|
|
1579
1584
|
.action(async (options) => {
|
|
1585
|
+
const description = (await readDescription(options)) ?? undefined;
|
|
1580
1586
|
if (hasWorkspaceReadToken(dependencies.environment)) {
|
|
1581
1587
|
exitCode = rejectTokenWrite(dependencies);
|
|
1582
1588
|
return;
|
|
1583
1589
|
}
|
|
1584
1590
|
if (options.title === undefined &&
|
|
1585
|
-
|
|
1591
|
+
description === undefined &&
|
|
1586
1592
|
options.clearDescription !== true &&
|
|
1587
1593
|
options.priority === undefined &&
|
|
1588
1594
|
options.startDate === undefined &&
|
|
@@ -1591,10 +1597,6 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1591
1597
|
options.clearDueDate !== true) {
|
|
1592
1598
|
throw invalidArgument("Work Item update requires a field change");
|
|
1593
1599
|
}
|
|
1594
|
-
if (options.clearDescription === true &&
|
|
1595
|
-
options.description !== undefined) {
|
|
1596
|
-
throw invalidArgument("Work Item update cannot set and clear the description together");
|
|
1597
|
-
}
|
|
1598
1600
|
if (options.clearStartDate === true && options.startDate !== undefined) {
|
|
1599
1601
|
throw invalidArgument("Work Item update cannot set and clear the start date together");
|
|
1600
1602
|
}
|
|
@@ -1606,9 +1608,9 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1606
1608
|
body: {
|
|
1607
1609
|
...(options.clearDescription === true
|
|
1608
1610
|
? { description: null }
|
|
1609
|
-
:
|
|
1611
|
+
: description === undefined
|
|
1610
1612
|
? {}
|
|
1611
|
-
: { description
|
|
1613
|
+
: { description }),
|
|
1612
1614
|
...(options.clearDueDate === true
|
|
1613
1615
|
? { dueDate: null }
|
|
1614
1616
|
: options.dueDate === undefined
|
|
@@ -1637,6 +1639,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1637
1639
|
},
|
|
1638
1640
|
},
|
|
1639
1641
|
}), {
|
|
1642
|
+
project: data => itemOutput(data, outputMode, warnMissingUrl),
|
|
1640
1643
|
ambiguousOutcomeMessage: "Work Item update outcome is unknown; read the Work Item before retrying",
|
|
1641
1644
|
retryTransportOnce: true,
|
|
1642
1645
|
});
|
|
@@ -1646,7 +1649,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1646
1649
|
.requiredOption("--project-id <projectId>")
|
|
1647
1650
|
.requiredOption("--work-item-id <workItemId>")
|
|
1648
1651
|
.requiredOption("--expected-version <expectedVersion>", "Opaque version returned by SpecFlow", parseOpaqueVersion)
|
|
1649
|
-
.option("--assignee <assignee>", "Workspace member User ID")
|
|
1652
|
+
.option("--assignee <assignee>", "Workspace member User ID, or me for the selected credential")
|
|
1650
1653
|
.option("--unassign", "Remove the Work Item assignee")
|
|
1651
1654
|
.action(async (options) => {
|
|
1652
1655
|
if (hasWorkspaceReadToken(dependencies.environment)) {
|
|
@@ -1660,9 +1663,9 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1660
1663
|
throw invalidArgument("Work Item assign cannot set and clear the assignee together");
|
|
1661
1664
|
}
|
|
1662
1665
|
const idempotencyKey = randomUUID();
|
|
1663
|
-
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", {
|
|
1664
1667
|
body: {
|
|
1665
|
-
assigneeId: options.unassign === true ? null : (options.assignee ?? null),
|
|
1668
|
+
assigneeId: options.unassign === true ? null : (me ?? options.assignee ?? null),
|
|
1666
1669
|
},
|
|
1667
1670
|
params: {
|
|
1668
1671
|
header: {
|
|
@@ -1675,6 +1678,8 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1675
1678
|
},
|
|
1676
1679
|
},
|
|
1677
1680
|
}), {
|
|
1681
|
+
resolveMe: options.assignee === "me",
|
|
1682
|
+
project: data => itemOutput(data, outputMode, warnMissingUrl),
|
|
1678
1683
|
ambiguousOutcomeMessage: "Work Item assignment outcome is unknown; read the Work Item before retrying",
|
|
1679
1684
|
retryTransportOnce: true,
|
|
1680
1685
|
});
|
|
@@ -1723,7 +1728,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
1723
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, [])
|
|
1724
1729
|
.option("--status <status>", "Destination Workflow Column slug or virtual status (updateStatus)")
|
|
1725
1730
|
.option("--priority <priority>", "Priority (updatePriority)", parseWorkItemPriority)
|
|
1726
|
-
.option("--assignee <assignee>", "Assignee User ID (updateAssignee)")
|
|
1731
|
+
.option("--assignee <assignee>", "Assignee User ID only (updateAssignee); me is not supported")
|
|
1727
1732
|
.option("--unassign", "Clear the assignee (updateAssignee)")
|
|
1728
1733
|
.option("--label-id <labelId>", "Work Item Label ID (addLabel/removeLabel)")
|
|
1729
1734
|
.option("--due-date <dueDate>", "Due date (updateDueDate)", parseWorkItemDate)
|
|
@@ -2164,6 +2169,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
2164
2169
|
});
|
|
2165
2170
|
});
|
|
2166
2171
|
const workItemCustomField = program.command("work-item-custom-field");
|
|
2172
|
+
registerCustomFieldColorCommands(workItemCustomField, resourceRuntime);
|
|
2167
2173
|
workItemCustomField
|
|
2168
2174
|
.command("list")
|
|
2169
2175
|
.requiredOption("--project-id <projectId>")
|
|
@@ -2180,6 +2186,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
2180
2186
|
.option("--option <option>", "Dropdown option; repeat for every choice", collectOption, [])
|
|
2181
2187
|
.option("--default-value <defaultValue>")
|
|
2182
2188
|
.option("--required", "Reject Work Items that leave the field blank")
|
|
2189
|
+
.option("--option-colors <json>", "One color or null per dropdown option", parseOptionColors)
|
|
2183
2190
|
.action(async (options) => {
|
|
2184
2191
|
if (hasWorkspaceReadToken(dependencies.environment)) {
|
|
2185
2192
|
exitCode = rejectTokenWrite(dependencies);
|
|
@@ -2192,6 +2199,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
2192
2199
|
? {}
|
|
2193
2200
|
: { defaultValue: options.defaultValue }),
|
|
2194
2201
|
name: options.name,
|
|
2202
|
+
...(options.optionColors === undefined ? {} : { optionColors: options.optionColors }),
|
|
2195
2203
|
...(options.option.length === 0
|
|
2196
2204
|
? {}
|
|
2197
2205
|
: { options: options.option }),
|
|
@@ -2533,7 +2541,7 @@ export async function runCli(arguments_, dependencies) {
|
|
|
2533
2541
|
}
|
|
2534
2542
|
catch (cause) {
|
|
2535
2543
|
if (cause instanceof CommanderError) {
|
|
2536
|
-
if (cause.
|
|
2544
|
+
if (cause.exitCode === 0) {
|
|
2537
2545
|
return cause.exitCode;
|
|
2538
2546
|
}
|
|
2539
2547
|
writeSkillError(dependencies, {
|
|
@@ -2547,130 +2555,6 @@ export async function runCli(arguments_, dependencies) {
|
|
|
2547
2555
|
throw cause;
|
|
2548
2556
|
}
|
|
2549
2557
|
}
|
|
2550
|
-
async function executeRequest(dependencies, request, options = {}, serverFlag, apiKeyIdFlag) {
|
|
2551
|
-
if (dependencies.environment.SPEC_FLOW_PROJECT_READ_TOKEN !== undefined) {
|
|
2552
|
-
writeSkillError(dependencies, { message: "Project Read Tokens are retired; use a Workspace read-only Token", reason: "Authentication Failed", retryable: false });
|
|
2553
|
-
return 2;
|
|
2554
|
-
}
|
|
2555
|
-
const environmentCredential = dependencies.environment.SPEC_FLOW_WORKSPACE_READ_TOKEN;
|
|
2556
|
-
const apiKeyEnvironment = dependencies.environment.SPEC_FLOW_API_KEY;
|
|
2557
|
-
const usesApiKey = apiKeyEnvironment !== undefined || apiKeyIdFlag !== undefined;
|
|
2558
|
-
if (options.requireUserSession === true && (usesApiKey || hasWorkspaceReadToken(dependencies.environment))) {
|
|
2559
|
-
writeSkillError(dependencies, { message: "A User Session is required for account management", reason: "Permission Denied", retryable: false });
|
|
2560
|
-
return 3;
|
|
2561
|
-
}
|
|
2562
|
-
if ((apiKeyEnvironment !== undefined && apiKeyIdFlag !== undefined) || (usesApiKey && hasWorkspaceReadToken(dependencies.environment))) {
|
|
2563
|
-
writeSkillError(dependencies, { message: "Select exactly one API Key or Workspace read-only Token credential", reason: "Invalid Operation", retryable: false });
|
|
2564
|
-
return 2;
|
|
2565
|
-
}
|
|
2566
|
-
if (apiKeyIdFlag !== undefined && !z.uuid().safeParse(apiKeyIdFlag).success) {
|
|
2567
|
-
writeSkillError(dependencies, { message: "API Key ID must be a UUID", reason: "Invalid Operation", retryable: false });
|
|
2568
|
-
return 2;
|
|
2569
|
-
}
|
|
2570
|
-
if (apiKeyEnvironment !== undefined && !/^sfk_[A-Za-z0-9_-]{43}$/u.test(apiKeyEnvironment)) {
|
|
2571
|
-
writeSkillError(dependencies, { message: "SPEC_FLOW_API_KEY does not contain a User API Key", reason: "Authentication Failed", retryable: false });
|
|
2572
|
-
return 2;
|
|
2573
|
-
}
|
|
2574
|
-
if (options.rejectWorkspaceReadToken === true &&
|
|
2575
|
-
hasWorkspaceReadToken(dependencies.environment)) {
|
|
2576
|
-
return rejectWorkspaceReadTokenOperation(dependencies);
|
|
2577
|
-
}
|
|
2578
|
-
const baseUrl = resolveApiOrigin(dependencies, serverFlag);
|
|
2579
|
-
if (baseUrl === null) {
|
|
2580
|
-
return 2;
|
|
2581
|
-
}
|
|
2582
|
-
let credential;
|
|
2583
|
-
try {
|
|
2584
|
-
if (apiKeyEnvironment !== undefined)
|
|
2585
|
-
credential = apiKeyEnvironment;
|
|
2586
|
-
else if (typeof apiKeyIdFlag === "string") {
|
|
2587
|
-
if (dependencies.credentialStore.readApiKey === undefined)
|
|
2588
|
-
throw new Error("API Key storage is unavailable");
|
|
2589
|
-
credential = await dependencies.credentialStore.readApiKey(apiKeyIdFlag.toLowerCase());
|
|
2590
|
-
}
|
|
2591
|
-
else {
|
|
2592
|
-
credential = environmentCredential ?? await dependencies.credentialStore.read();
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
catch {
|
|
2596
|
-
writeSkillError(dependencies, {
|
|
2597
|
-
message: "The CLI credential store is temporarily unavailable",
|
|
2598
|
-
reason: "Temporary Failure",
|
|
2599
|
-
retryable: true,
|
|
2600
|
-
});
|
|
2601
|
-
return 4;
|
|
2602
|
-
}
|
|
2603
|
-
if (credential === null || credential.trim().length === 0) {
|
|
2604
|
-
writeSkillError(dependencies, {
|
|
2605
|
-
message: "An authenticated CLI Session, User API Key or Workspace read-only Token is required",
|
|
2606
|
-
reason: "Authentication Failed",
|
|
2607
|
-
retryable: false,
|
|
2608
|
-
});
|
|
2609
|
-
return 2;
|
|
2610
|
-
}
|
|
2611
|
-
const client = createClient({
|
|
2612
|
-
baseUrl,
|
|
2613
|
-
...(usesApiKey || options.requireUserSession === true ? { redirect: "error" } : {}),
|
|
2614
|
-
fetch: options.retryTransportOnce === true
|
|
2615
|
-
? retryTransportOnce(dependencies.fetch)
|
|
2616
|
-
: dependencies.fetch,
|
|
2617
|
-
headers: {
|
|
2618
|
-
...(usesApiKey ? { "x-api-key": credential } : { authorization: `Bearer ${credential}` }),
|
|
2619
|
-
"user-agent": specFlowCliUserAgent,
|
|
2620
|
-
},
|
|
2621
|
-
});
|
|
2622
|
-
try {
|
|
2623
|
-
const result = await request(client);
|
|
2624
|
-
if (result.response.ok && result.data !== undefined) {
|
|
2625
|
-
const validationError = options.validateSuccess?.(result.data) ?? null;
|
|
2626
|
-
if (validationError !== null) {
|
|
2627
|
-
writeSkillError(dependencies, validationError);
|
|
2628
|
-
return skillErrorExitCode(validationError);
|
|
2629
|
-
}
|
|
2630
|
-
dependencies.stdout(`${JSON.stringify(result.data)}\n`);
|
|
2631
|
-
return 0;
|
|
2632
|
-
}
|
|
2633
|
-
if (result.response.ok && options.retryTransportOnce === true) {
|
|
2634
|
-
const error = ambiguousMutationError(options.ambiguousOutcomeMessage);
|
|
2635
|
-
writeSkillError(dependencies, error);
|
|
2636
|
-
return skillErrorExitCode(error);
|
|
2637
|
-
}
|
|
2638
|
-
const error = mapHttpFailure(result.response, result.error);
|
|
2639
|
-
writeSkillError(dependencies, error);
|
|
2640
|
-
return skillErrorExitCode(error);
|
|
2641
|
-
}
|
|
2642
|
-
catch {
|
|
2643
|
-
const error = options.retryTransportOnce === true
|
|
2644
|
-
? ambiguousMutationError(options.ambiguousOutcomeMessage)
|
|
2645
|
-
: {
|
|
2646
|
-
message: "SpecFlow is temporarily unavailable",
|
|
2647
|
-
reason: "Temporary Failure",
|
|
2648
|
-
retryable: true,
|
|
2649
|
-
};
|
|
2650
|
-
writeSkillError(dependencies, error);
|
|
2651
|
-
return skillErrorExitCode(error);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
function retryTransportOnce(fetch) {
|
|
2655
|
-
return async (request) => {
|
|
2656
|
-
const retryRequest = request.clone();
|
|
2657
|
-
try {
|
|
2658
|
-
return await fetch(request);
|
|
2659
|
-
}
|
|
2660
|
-
catch {
|
|
2661
|
-
return fetch(retryRequest);
|
|
2662
|
-
}
|
|
2663
|
-
};
|
|
2664
|
-
}
|
|
2665
|
-
function ambiguousMutationError(message) {
|
|
2666
|
-
return {
|
|
2667
|
-
message: message ??
|
|
2668
|
-
"Write outcome is unknown; read the target resource before trying a new command identity",
|
|
2669
|
-
code: "EXECUTION_OUTCOME_UNKNOWN",
|
|
2670
|
-
reason: "Temporary Failure",
|
|
2671
|
-
retryable: false,
|
|
2672
|
-
};
|
|
2673
|
-
}
|
|
2674
2558
|
function validateIssuedWorkspaceReadToken(data) {
|
|
2675
2559
|
if (typeof data === "object" &&
|
|
2676
2560
|
data !== null &&
|
|
@@ -2684,21 +2568,6 @@ function validateIssuedWorkspaceReadToken(data) {
|
|
|
2684
2568
|
}
|
|
2685
2569
|
return null;
|
|
2686
2570
|
}
|
|
2687
|
-
function resolveApiOrigin(dependencies, serverFlag) {
|
|
2688
|
-
const flag = typeof serverFlag === "string" ? serverFlag : undefined;
|
|
2689
|
-
const result = parseHttpOrigin(unresolvedSpecFlowApiOrigin(flag, dependencies.environment.SPEC_FLOW_API_URL));
|
|
2690
|
-
if (result.valid) {
|
|
2691
|
-
return result.origin;
|
|
2692
|
-
}
|
|
2693
|
-
writeSkillError(dependencies, {
|
|
2694
|
-
message: flag === undefined
|
|
2695
|
-
? "SPEC_FLOW_API_URL must be an HTTP(S) origin without a path"
|
|
2696
|
-
: "--server must be an HTTP(S) origin without a path",
|
|
2697
|
-
reason: "Invalid Operation",
|
|
2698
|
-
retryable: false,
|
|
2699
|
-
});
|
|
2700
|
-
return null;
|
|
2701
|
-
}
|
|
2702
2571
|
function parseRelationCommandKey(value) {
|
|
2703
2572
|
const key = value.trim();
|
|
2704
2573
|
if (key.length === 0 || key.length > 200)
|
|
@@ -2849,7 +2718,7 @@ function workItemSelectionQuery(options) {
|
|
|
2849
2718
|
*/
|
|
2850
2719
|
function workItemSelectionFlags(command) {
|
|
2851
2720
|
return command
|
|
2852
|
-
.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, [])
|
|
2853
2722
|
.option("--custom-field <fieldId=value>", "Filter by Custom Field value; repeat to OR values or combine fields", collectOption, [])
|
|
2854
2723
|
.option("--due <filter>", "Filter by due bucket: dueThisWeek, dueNextWeek, or noDueDate; repeat for several", (value, previous) => [
|
|
2855
2724
|
...previous,
|
|
@@ -3163,146 +3032,6 @@ function rejectTokenWrite(dependencies) {
|
|
|
3163
3032
|
});
|
|
3164
3033
|
return 2;
|
|
3165
3034
|
}
|
|
3166
|
-
function rejectWorkspaceReadTokenOperation(dependencies) {
|
|
3167
|
-
writeSkillError(dependencies, {
|
|
3168
|
-
message: "Workspace read-only Token does not permit this operation",
|
|
3169
|
-
reason: "Permission Denied",
|
|
3170
|
-
retryable: false,
|
|
3171
|
-
});
|
|
3172
|
-
return 3;
|
|
3173
|
-
}
|
|
3174
|
-
function hasWorkspaceReadToken(environment) {
|
|
3175
|
-
const credential = environment.SPEC_FLOW_WORKSPACE_READ_TOKEN;
|
|
3176
|
-
return credential !== undefined && credential.trim().length > 0;
|
|
3177
|
-
}
|
|
3178
|
-
function mapHttpFailure(response, payload) {
|
|
3179
|
-
const failure = classifyHttpFailure(response, payload);
|
|
3180
|
-
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);
|
|
3181
|
-
const code = readServerCode(payload);
|
|
3182
|
-
const requestId = response.headers.get("x-request-id");
|
|
3183
|
-
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 } : {}) };
|
|
3184
|
-
}
|
|
3185
|
-
function classifyHttpFailure(response, payload) {
|
|
3186
|
-
const { status } = response;
|
|
3187
|
-
const serverMessage = readServerMessage(payload);
|
|
3188
|
-
const serverCode = readServerCode(payload);
|
|
3189
|
-
if (status === 401) {
|
|
3190
|
-
return {
|
|
3191
|
-
message: serverMessage ?? "Workspace read-only Token is invalid, expired, or revoked",
|
|
3192
|
-
reason: "Authentication Failed",
|
|
3193
|
-
retryable: false,
|
|
3194
|
-
};
|
|
3195
|
-
}
|
|
3196
|
-
if (status === 403) {
|
|
3197
|
-
return {
|
|
3198
|
-
message: serverMessage ?? "Workspace read-only Token does not permit this operation",
|
|
3199
|
-
reason: "Permission Denied",
|
|
3200
|
-
retryable: false,
|
|
3201
|
-
};
|
|
3202
|
-
}
|
|
3203
|
-
if (status === 404) {
|
|
3204
|
-
if (serverCode === "PROJECT_NOT_ACCESSIBLE") {
|
|
3205
|
-
return {
|
|
3206
|
-
message: serverMessage ?? "The Project is not accessible",
|
|
3207
|
-
reason: "Project Not Accessible",
|
|
3208
|
-
retryable: false,
|
|
3209
|
-
};
|
|
3210
|
-
}
|
|
3211
|
-
return {
|
|
3212
|
-
message: serverMessage ?? "The requested Project resource was not found",
|
|
3213
|
-
reason: "Resource Not Found",
|
|
3214
|
-
retryable: false,
|
|
3215
|
-
};
|
|
3216
|
-
}
|
|
3217
|
-
if (status === 409 && serverCode === "RESOURCE_CHANGED") {
|
|
3218
|
-
return {
|
|
3219
|
-
message: serverMessage ?? "The Working Draft changed after the Proposal baseline",
|
|
3220
|
-
reason: "Resource Changed",
|
|
3221
|
-
retryable: false,
|
|
3222
|
-
};
|
|
3223
|
-
}
|
|
3224
|
-
if (status === 400 || status === 409 || status === 413 || status === 422 || status === 428) {
|
|
3225
|
-
const diagnostics = readServerDiagnostics(payload);
|
|
3226
|
-
return {
|
|
3227
|
-
...(diagnostics === undefined ? {} : { diagnostics }),
|
|
3228
|
-
message: serverMessage ?? "The requested operation is invalid",
|
|
3229
|
-
reason: "Invalid Operation",
|
|
3230
|
-
retryable: false,
|
|
3231
|
-
};
|
|
3232
|
-
}
|
|
3233
|
-
if (status === 412) {
|
|
3234
|
-
return {
|
|
3235
|
-
message: serverMessage ?? "The resource changed after it was read",
|
|
3236
|
-
reason: "Resource Changed",
|
|
3237
|
-
retryable: false,
|
|
3238
|
-
};
|
|
3239
|
-
}
|
|
3240
|
-
if (status === 429) {
|
|
3241
|
-
const retryAfter = response.headers.get("retry-after");
|
|
3242
|
-
return {
|
|
3243
|
-
message: serverMessage ?? "SpecFlow is temporarily throttling this client",
|
|
3244
|
-
reason: "Temporary Failure",
|
|
3245
|
-
...(retryAfter === null ? {} : { retryAfter }),
|
|
3246
|
-
retryable: true,
|
|
3247
|
-
};
|
|
3248
|
-
}
|
|
3249
|
-
return {
|
|
3250
|
-
message: serverMessage ?? "SpecFlow is temporarily unavailable",
|
|
3251
|
-
reason: "Temporary Failure",
|
|
3252
|
-
retryable: status >= 500,
|
|
3253
|
-
};
|
|
3254
|
-
}
|
|
3255
|
-
function readServerMessage(payload) {
|
|
3256
|
-
return typeof payload === "object" &&
|
|
3257
|
-
payload !== null &&
|
|
3258
|
-
"message" in payload &&
|
|
3259
|
-
typeof payload.message === "string"
|
|
3260
|
-
? payload.message
|
|
3261
|
-
: null;
|
|
3262
|
-
}
|
|
3263
|
-
function readServerCode(payload) {
|
|
3264
|
-
return typeof payload === "object" &&
|
|
3265
|
-
payload !== null &&
|
|
3266
|
-
"code" in payload &&
|
|
3267
|
-
typeof payload.code === "string"
|
|
3268
|
-
? payload.code
|
|
3269
|
-
: null;
|
|
3270
|
-
}
|
|
3271
|
-
function readServerDiagnostics(payload) {
|
|
3272
|
-
if (typeof payload !== "object" ||
|
|
3273
|
-
payload === null ||
|
|
3274
|
-
!("diagnostics" in payload) ||
|
|
3275
|
-
!Array.isArray(payload.diagnostics)) {
|
|
3276
|
-
return undefined;
|
|
3277
|
-
}
|
|
3278
|
-
const diagnostics = payload.diagnostics.flatMap((entry) => {
|
|
3279
|
-
if (typeof entry !== "object" || entry === null) {
|
|
3280
|
-
return [];
|
|
3281
|
-
}
|
|
3282
|
-
const { column, line, message, reason } = entry;
|
|
3283
|
-
if (typeof reason !== "string" || typeof message !== "string") {
|
|
3284
|
-
return [];
|
|
3285
|
-
}
|
|
3286
|
-
return [
|
|
3287
|
-
{
|
|
3288
|
-
...(typeof column === "number" ? { column } : {}),
|
|
3289
|
-
...(typeof line === "number" ? { line } : {}),
|
|
3290
|
-
message,
|
|
3291
|
-
reason,
|
|
3292
|
-
},
|
|
3293
|
-
];
|
|
3294
|
-
});
|
|
3295
|
-
return diagnostics.length === 0 ? undefined : diagnostics;
|
|
3296
|
-
}
|
|
3297
|
-
function skillErrorExitCode(error) {
|
|
3298
|
-
if (error.reason === "Permission Denied") {
|
|
3299
|
-
return 3;
|
|
3300
|
-
}
|
|
3301
|
-
return error.reason === "Temporary Failure" ? 4 : 2;
|
|
3302
|
-
}
|
|
3303
|
-
function writeSkillError(dependencies, error) {
|
|
3304
|
-
dependencies.stderr(`${JSON.stringify({ error })}\n`);
|
|
3305
|
-
}
|
|
3306
3035
|
function parseCommentContent(value) {
|
|
3307
3036
|
const result = z.string().trim().min(1).max(20_000).refine((content) => !content.includes("\0")).safeParse(value);
|
|
3308
3037
|
if (!result.success)
|