@autifyhq/aximo-cli 0.13.0 → 0.14.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 +15 -5
- package/dist/client.d.ts +5 -0
- package/dist/client.js +23 -22
- package/dist/client.test.js +28 -0
- package/dist/commands/case/archive.d.ts +20 -0
- package/dist/commands/case/archive.js +42 -0
- package/dist/commands/case/case.test.js +181 -0
- package/dist/commands/case/delete.js +4 -1
- package/dist/commands/case/index.js +2 -0
- package/dist/commands/case/restore.d.ts +20 -0
- package/dist/commands/case/restore.js +42 -0
- package/dist/commands/case/update.d.ts +22 -0
- package/dist/commands/case/update.js +34 -3
- package/dist/commands/device/status.d.ts +1 -10
- package/dist/commands/mcp/server.test.js +9 -1
- package/dist/commands/mcp/tools.js +140 -7
- package/dist/commands/mcp/tools.test.js +310 -0
- package/dist/commands/plan/archive.d.ts +20 -0
- package/dist/commands/plan/archive.js +42 -0
- package/dist/commands/plan/delete.d.ts +20 -0
- package/dist/commands/plan/delete.js +42 -0
- package/dist/commands/plan/index.js +3 -0
- package/dist/commands/plan/plan.test.js +115 -3
- package/dist/commands/plan/restore.d.ts +20 -0
- package/dist/commands/plan/restore.js +42 -0
- package/dist/commands/upload/upload.test.js +5 -2
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +110 -2
- package/dist/errors.test.js +106 -3
- package/dist/types.d.ts +41 -3
- package/dist/validate.d.ts +1 -0
- package/dist/validate.js +15 -3
- package/dist/validate.test.js +18 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { request } from "../../client.js";
|
|
|
3
3
|
import { handleApiError } from "../../errors.js";
|
|
4
4
|
import { dryRunEnvelope, resolveJsonOverride } from "../../mutate.js";
|
|
5
5
|
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
6
|
-
import { optionalString, parseCaseFormat, parseCaseType, requireId } from "../../validate.js";
|
|
6
|
+
import { optionalString, parseCaseFormat, parseCaseType, parseJsonArray, parseJsonObject, requireId, } from "../../validate.js";
|
|
7
7
|
export async function runCaseUpdate(options, deps) {
|
|
8
8
|
let caseId;
|
|
9
9
|
let body;
|
|
@@ -21,6 +21,7 @@ export async function runCaseUpdate(options, deps) {
|
|
|
21
21
|
}
|
|
22
22
|
else {
|
|
23
23
|
try {
|
|
24
|
+
const desktopDevice = optionalString("desktopDevice", options.desktopDevice);
|
|
24
25
|
body = {
|
|
25
26
|
title: optionalString("title", options.title),
|
|
26
27
|
description: optionalString("description", options.description),
|
|
@@ -28,6 +29,10 @@ export async function runCaseUpdate(options, deps) {
|
|
|
28
29
|
type: parseCaseType(options.type),
|
|
29
30
|
format: parseCaseFormat(options.format),
|
|
30
31
|
startUrl: optionalString("startUrl", options.startUrl),
|
|
32
|
+
apiMobileConfig: parseJsonObject("mobileConfig", options.mobileConfig),
|
|
33
|
+
apiDesktopConfig: desktopDevice === undefined ? undefined : { userDeviceId: desktopDevice },
|
|
34
|
+
apiMultiDeviceConfig: parseJsonArray("multiDeviceConfig", options.multiDeviceConfig),
|
|
35
|
+
apiWebConfig: parseJsonObject("webConfig", options.webConfig),
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
catch (err) {
|
|
@@ -54,11 +59,33 @@ export default defineCommand({
|
|
|
54
59
|
args: {
|
|
55
60
|
caseId: { type: "positional", required: true },
|
|
56
61
|
title: { type: "string" },
|
|
57
|
-
description: {
|
|
62
|
+
description: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Cannot clear; use --json '{\"description\": null}' for that",
|
|
65
|
+
},
|
|
58
66
|
scenario: { type: "string" },
|
|
59
67
|
type: { type: "string", description: "web | mobile | desktop | multi_device" },
|
|
60
68
|
format: { type: "string", description: "gherkin | markdown | plain_text" },
|
|
61
|
-
"start-url": {
|
|
69
|
+
"start-url": {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Cannot clear; use --json '{\"startUrl\": null}' for that",
|
|
72
|
+
},
|
|
73
|
+
"mobile-config": {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "JSON object for apiMobileConfig ({deviceArn, deviceName, platform?, uploadArn?, uploadType?, appName?, bundleId?, apps?}); replaces the whole config. Clear with --json '{\"apiMobileConfig\":null}'",
|
|
76
|
+
},
|
|
77
|
+
"desktop-device": {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Registered BYOD desktop device id (aximo device status) for apiDesktopConfig. Clear with --json '{\"apiDesktopConfig\":null}'",
|
|
80
|
+
},
|
|
81
|
+
"multi-device-config": {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "JSON array of 2-3 device entries ({kind: web|mobile, name, ...}) for apiMultiDeviceConfig; replaces the whole config. Clear with --json '{\"apiMultiDeviceConfig\":null}'",
|
|
84
|
+
},
|
|
85
|
+
"web-config": {
|
|
86
|
+
type: "string",
|
|
87
|
+
description: "JSON object for apiWebConfig ({hostnameOverrideIds: [...]}); replaces the whole config. Clear with --json '{\"apiWebConfig\":null}'",
|
|
88
|
+
},
|
|
62
89
|
json: { type: "string", description: "Raw JSON request body (replaces individual flags)" },
|
|
63
90
|
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
64
91
|
},
|
|
@@ -71,6 +98,10 @@ export default defineCommand({
|
|
|
71
98
|
type: args.type,
|
|
72
99
|
format: args.format,
|
|
73
100
|
startUrl: args["start-url"],
|
|
101
|
+
mobileConfig: args["mobile-config"],
|
|
102
|
+
desktopDevice: args["desktop-device"],
|
|
103
|
+
multiDeviceConfig: args["multi-device-config"],
|
|
104
|
+
webConfig: args["web-config"],
|
|
74
105
|
json: args.json,
|
|
75
106
|
dryRun: args["dry-run"],
|
|
76
107
|
}));
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { type ClientDeps } from "../../client.js";
|
|
2
|
-
import type { Envelope } from "../../types.js";
|
|
3
|
-
interface UserDevice {
|
|
4
|
-
id: string;
|
|
5
|
-
name: string;
|
|
6
|
-
platform: string;
|
|
7
|
-
os?: string;
|
|
8
|
-
status: string;
|
|
9
|
-
lastSeenAt?: string;
|
|
10
|
-
createdAt: string;
|
|
11
|
-
}
|
|
2
|
+
import type { Envelope, UserDevice } from "../../types.js";
|
|
12
3
|
export declare function runDeviceStatus(deps?: ClientDeps): Promise<Envelope<{
|
|
13
4
|
devices: UserDevice[];
|
|
14
5
|
}>>;
|
|
@@ -24,7 +24,7 @@ async function connect() {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
describe("createMcpServer wiring", () => {
|
|
27
|
-
it("exposes all
|
|
27
|
+
it("exposes all 29 tools through tools/list with valid JSON Schema", async () => {
|
|
28
28
|
const { client, cleanup } = await connect();
|
|
29
29
|
try {
|
|
30
30
|
const { tools } = await client.listTools();
|
|
@@ -37,10 +37,15 @@ describe("createMcpServer wiring", () => {
|
|
|
37
37
|
"aximo_get_session_screenshots",
|
|
38
38
|
"aximo_get_session_timeline",
|
|
39
39
|
"aximo_list_cases",
|
|
40
|
+
"aximo_list_devices",
|
|
40
41
|
"aximo_list_projects",
|
|
41
42
|
"aximo_list_sessions",
|
|
43
|
+
"aximo_list_uploads",
|
|
44
|
+
"aximo_list_user_devices",
|
|
42
45
|
"aximo_create_case",
|
|
43
46
|
"aximo_update_case",
|
|
47
|
+
"aximo_archive_case",
|
|
48
|
+
"aximo_restore_case",
|
|
44
49
|
"aximo_delete_case",
|
|
45
50
|
"aximo_run_case",
|
|
46
51
|
"aximo_create_plan",
|
|
@@ -49,6 +54,9 @@ describe("createMcpServer wiring", () => {
|
|
|
49
54
|
"aximo_list_plans",
|
|
50
55
|
"aximo_get_plan",
|
|
51
56
|
"aximo_update_plan",
|
|
57
|
+
"aximo_archive_plan",
|
|
58
|
+
"aximo_restore_plan",
|
|
59
|
+
"aximo_delete_plan",
|
|
52
60
|
"aximo_run_plan",
|
|
53
61
|
"aximo_list_plan_runs",
|
|
54
62
|
"aximo_get_plan_run",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { request } from "../../client.js";
|
|
3
|
+
import { ARCHIVE_FIRST_HINT, featureHint, isArchiveFirstConflict, withDetails, } from "../../errors.js";
|
|
3
4
|
function ok(payload) {
|
|
4
5
|
return {
|
|
5
6
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
@@ -23,22 +24,31 @@ async function call(fn) {
|
|
|
23
24
|
catch (e) {
|
|
24
25
|
if (!(e instanceof Error))
|
|
25
26
|
return err(String(e));
|
|
26
|
-
const status = e
|
|
27
|
+
const { status, details } = e;
|
|
28
|
+
const message = withDetails(e.message, details);
|
|
27
29
|
if (status === 401)
|
|
28
30
|
return err("Unauthorized: check AXIMO_API_KEY", "Run `aximo configure`.");
|
|
29
31
|
if (status === 403) {
|
|
30
|
-
return err(
|
|
32
|
+
return err(message || "Forbidden", featureHint(e.message, details) ?? "API key may not have access to this resource.");
|
|
31
33
|
}
|
|
32
34
|
if (status === 404)
|
|
33
|
-
return err(
|
|
35
|
+
return err(message || "Not found");
|
|
34
36
|
if (status === 429) {
|
|
35
37
|
const retryAfter = e.retryAfter;
|
|
36
38
|
return err("Rate limited", retryAfter
|
|
37
39
|
? `Back off and retry after ${retryAfter} seconds.`
|
|
38
40
|
: "Back off before retrying; Aximo rate-limits requests per organization.");
|
|
39
41
|
}
|
|
42
|
+
// Mirrors the 409 branch in handleApiError. Without this, an archive-first
|
|
43
|
+
// 409 falls through to the bare `HTTP 409: ...` case below and the agent is
|
|
44
|
+
// told to call a REST endpoint it has no way to reach, while the tool that
|
|
45
|
+
// would work goes unmentioned. Keyed on the details-enriched message, the
|
|
46
|
+
// same input handleApiError feeds the predicate, so the two cannot disagree.
|
|
47
|
+
if (status === 409) {
|
|
48
|
+
return err(message || "Conflict", isArchiveFirstConflict(message) ? ARCHIVE_FIRST_HINT : undefined);
|
|
49
|
+
}
|
|
40
50
|
if (typeof status === "number")
|
|
41
|
-
return err(`HTTP ${status}: ${
|
|
51
|
+
return err(`HTTP ${status}: ${message}`);
|
|
42
52
|
if (e.message.startsWith("No API key configured")) {
|
|
43
53
|
return err("API key not configured", "Run `aximo configure` and restart the MCP server.");
|
|
44
54
|
}
|
|
@@ -82,6 +92,53 @@ export function defineTools(deps = {}) {
|
|
|
82
92
|
return res;
|
|
83
93
|
}),
|
|
84
94
|
},
|
|
95
|
+
{
|
|
96
|
+
name: "aximo_list_devices",
|
|
97
|
+
title: "List Device Farm devices",
|
|
98
|
+
description: "List the cloud (AWS Device Farm) mobile devices available to your organization — not BYOD devices (see aximo_list_user_devices). Each device's arn is the encrypted token to pass as deviceArn, and its name as deviceName, in a case's apiMobileConfig or multi-device mobile entries (aximo_update_case) and in per-run overrides (aximo_run_case, aximo_run_plan). Filter by platform (ANDROID | IOS) or availableOnly.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
platform: z.enum(["ANDROID", "IOS"]).optional(),
|
|
101
|
+
availableOnly: z
|
|
102
|
+
.boolean()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Only devices that are currently free to run on."),
|
|
105
|
+
},
|
|
106
|
+
handler: async ({ platform, availableOnly }) => call(async () => {
|
|
107
|
+
return await request("/api/devices", { query: { platform, availableOnly: availableOnly || undefined } }, deps);
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "aximo_list_user_devices",
|
|
112
|
+
title: "List registered BYOD devices",
|
|
113
|
+
description: "List the BYOD (bring-your-own) desktop and mobile devices registered with `aximo device register` by the owner of this API key — other members' devices are not included. Each device's id is the userDeviceId to pass in a case's apiDesktopConfig or multi-device local entries (aximo_update_case) and in per-run overrides. status shows whether the device is online. Returns 404 when BYOD is not enabled for the organization.",
|
|
114
|
+
inputSchema: {},
|
|
115
|
+
handler: async () => call(async () => {
|
|
116
|
+
try {
|
|
117
|
+
return await request("/api/devices/user", {}, deps);
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
// The route answers a bare 404 when the org has no BYOD feature.
|
|
121
|
+
if (e.status === 404) {
|
|
122
|
+
throw Object.assign(new Error("BYOD is not enabled for this organization"), {
|
|
123
|
+
status: 403,
|
|
124
|
+
details: { feature: "byodEnabled" },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "aximo_list_uploads",
|
|
133
|
+
title: "List app uploads",
|
|
134
|
+
description: "List the mobile app binaries (APK / IPA) uploaded to your organization. Each upload's uploadArn is the encrypted token to pass as uploadArn in a case's apiMobileConfig or multi-device mobile entries (aximo_update_case) and in per-run overrides; only uploads with status succeeded can be installed. identifier is the app's bundleId. Filter by fileType (ANDROID_APP | IOS_APP).",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
fileType: z.enum(["ANDROID_APP", "IOS_APP"]).optional(),
|
|
137
|
+
},
|
|
138
|
+
handler: async ({ fileType }) => call(async () => {
|
|
139
|
+
return await request("/api/uploads", { query: { fileType } }, deps);
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
85
142
|
{
|
|
86
143
|
name: "aximo_list_sessions",
|
|
87
144
|
title: "List sessions",
|
|
@@ -231,15 +288,36 @@ export function defineTools(deps = {}) {
|
|
|
231
288
|
{
|
|
232
289
|
name: "aximo_update_case",
|
|
233
290
|
title: "Update test case",
|
|
234
|
-
description: "Update a test case's title, description, scenario, type, format, or startUrl (pass null to clear startUrl). Omitted fields are left unchanged; at least one field is required.
|
|
291
|
+
description: "Update a test case's title, description, scenario, type, format, or startUrl (pass null to clear description or startUrl; an empty description string is stored as-is, not a clear). Omitted fields are left unchanged; at least one field is required. Pass apiMobileConfig / apiDesktopConfig / apiMultiDeviceConfig / apiWebConfig to set the case's default targets — the same shapes aximo_get_case returns; null clears one, omit leaves it unchanged. Each config value is replaced whole — send the complete config object you want stored (never a partial), and omit a config key entirely to leave that target unchanged. deviceArn = aximo_list_devices devices[].arn and deviceName = devices[].name; uploadArn = aximo_list_uploads uploads[].uploadArn (status succeeded), bundleId = uploads[].identifier, uploadType is usually \"own_app\"; userDeviceId = aximo_list_user_devices devices[].id. apiMobileConfig.platform is ANDROID|IOS; multi-device mobilePlatform is android|ios. There is no top-level userDeviceId/deviceArn here — they live inside the config objects. Desktop and multi-device require the organization's BYOD / multi-device feature. You can set type in the same call.",
|
|
235
292
|
inputSchema: {
|
|
236
293
|
caseId: z.string().uuid(),
|
|
237
294
|
title: z.string().min(1).max(500).optional(),
|
|
238
|
-
description: z.string().optional(),
|
|
295
|
+
description: z.string().nullable().optional(),
|
|
239
296
|
scenario: z.string().min(1).optional(),
|
|
240
297
|
type: z.enum(["web", "mobile", "desktop", "multi_device"]).optional(),
|
|
241
298
|
format: z.enum(["gherkin", "markdown", "plain_text"]).optional(),
|
|
242
299
|
startUrl: z.string().url().nullable().optional(),
|
|
300
|
+
apiMobileConfig: z
|
|
301
|
+
.record(z.unknown())
|
|
302
|
+
.nullable()
|
|
303
|
+
.optional()
|
|
304
|
+
.describe("{ deviceArn, deviceName, platform?: ANDROID|IOS, uploadArn?, uploadType?: own_app|existing|sample, appName?, bundleId?, apps?: [{ uploadArn, uploadType, appName, bundleId? }] (max 3) }. null clears."),
|
|
305
|
+
apiDesktopConfig: z
|
|
306
|
+
.object({ userDeviceId: z.string().uuid() })
|
|
307
|
+
.passthrough()
|
|
308
|
+
.nullable()
|
|
309
|
+
.optional()
|
|
310
|
+
.describe("{ userDeviceId } of a registered BYOD desktop device (aximo_list_user_devices); name/platform/os are resolved server-side. null clears."),
|
|
311
|
+
apiMultiDeviceConfig: z
|
|
312
|
+
.array(z.record(z.unknown()))
|
|
313
|
+
.nullable()
|
|
314
|
+
.optional()
|
|
315
|
+
.describe('2-3 entries, each { kind: "web", name, startUrl?, userDeviceId? } or { kind: "mobile", name, mobilePlatform: android|ios, deviceArn?, deviceName?, userDeviceId?, uploadArn?, uploadType?, appName?, bundleId?, apps? }. null clears.'),
|
|
316
|
+
apiWebConfig: z
|
|
317
|
+
.record(z.unknown())
|
|
318
|
+
.nullable()
|
|
319
|
+
.optional()
|
|
320
|
+
.describe("{ hostnameOverrideIds?: string[] }. null clears."),
|
|
243
321
|
},
|
|
244
322
|
handler: async ({ caseId, ...body }) => call(async () => {
|
|
245
323
|
if (Object.keys(body).length === 0) {
|
|
@@ -248,10 +326,32 @@ export function defineTools(deps = {}) {
|
|
|
248
326
|
return await request(`/api/cases/${encodeURIComponent(caseId)}`, { method: "PATCH", body }, deps);
|
|
249
327
|
}),
|
|
250
328
|
},
|
|
329
|
+
{
|
|
330
|
+
name: "aximo_archive_case",
|
|
331
|
+
title: "Archive test case",
|
|
332
|
+
description: "Archive a test case: hidden from lists, plans, and new runs; run history kept. Idempotent. Required before aximo_delete_case can permanently delete it.",
|
|
333
|
+
inputSchema: {
|
|
334
|
+
caseId: z.string().uuid(),
|
|
335
|
+
},
|
|
336
|
+
handler: async ({ caseId }) => call(async () => {
|
|
337
|
+
return await request(`/api/cases/${encodeURIComponent(caseId)}/archive`, { method: "POST" }, deps);
|
|
338
|
+
}),
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "aximo_restore_case",
|
|
342
|
+
title: "Restore test case",
|
|
343
|
+
description: "Restore an archived test case: it returns to lists, directory counts, and plan membership. Idempotent. A case whose directory was deleted while archived returns to the project root.",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
caseId: z.string().uuid(),
|
|
346
|
+
},
|
|
347
|
+
handler: async ({ caseId }) => call(async () => {
|
|
348
|
+
return await request(`/api/cases/${encodeURIComponent(caseId)}/restore`, { method: "POST" }, deps);
|
|
349
|
+
}),
|
|
350
|
+
},
|
|
251
351
|
{
|
|
252
352
|
name: "aximo_delete_case",
|
|
253
353
|
title: "Delete test case",
|
|
254
|
-
description: "Permanently delete
|
|
354
|
+
description: "Permanently delete an already-archived test case. Archive it first with aximo_archive_case. Requires an organization admin or owner API key. It is removed from any plans it belongs to, and its rows in past plan runs go with it, so those runs list fewer cases than their stored totals; the agent sessions themselves are kept but detached. This cannot be undone.",
|
|
255
355
|
inputSchema: {
|
|
256
356
|
caseId: z.string().uuid(),
|
|
257
357
|
},
|
|
@@ -378,6 +478,39 @@ export function defineTools(deps = {}) {
|
|
|
378
478
|
return await request(`/api/plans/${encodeURIComponent(planId)}`, { method: "PATCH", body }, deps);
|
|
379
479
|
}),
|
|
380
480
|
},
|
|
481
|
+
{
|
|
482
|
+
name: "aximo_archive_plan",
|
|
483
|
+
title: "Archive test plan",
|
|
484
|
+
description: "Archive a test plan: hidden from lists and blocked from new runs; its cases, run history, and schedule are all kept. The scheduler skips archived plans, so restoring one resumes its normal cadence. Idempotent. Required before aximo_delete_plan can permanently delete it.",
|
|
485
|
+
inputSchema: {
|
|
486
|
+
planId: z.string().uuid(),
|
|
487
|
+
},
|
|
488
|
+
handler: async ({ planId }) => call(async () => {
|
|
489
|
+
return await request(`/api/plans/${encodeURIComponent(planId)}/archive`, { method: "POST" }, deps);
|
|
490
|
+
}),
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "aximo_restore_plan",
|
|
494
|
+
title: "Restore test plan",
|
|
495
|
+
description: "Restore an archived test plan: it returns to lists and can be run again, and its schedule resumes exactly as configured. Idempotent.",
|
|
496
|
+
inputSchema: {
|
|
497
|
+
planId: z.string().uuid(),
|
|
498
|
+
},
|
|
499
|
+
handler: async ({ planId }) => call(async () => {
|
|
500
|
+
return await request(`/api/plans/${encodeURIComponent(planId)}/restore`, { method: "POST" }, deps);
|
|
501
|
+
}),
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: "aximo_delete_plan",
|
|
505
|
+
title: "Delete test plan",
|
|
506
|
+
description: "Permanently delete an already-archived test plan. Archive it first with aximo_archive_plan. Requires an organization admin or owner API key. Its member cases are kept, and so is its run history: those runs are detached from the plan and remain readable as standalone results. This cannot be undone.",
|
|
507
|
+
inputSchema: {
|
|
508
|
+
planId: z.string().uuid(),
|
|
509
|
+
},
|
|
510
|
+
handler: async ({ planId }) => call(async () => {
|
|
511
|
+
return await request(`/api/plans/${encodeURIComponent(planId)}`, { method: "DELETE" }, deps);
|
|
512
|
+
}),
|
|
513
|
+
},
|
|
381
514
|
{
|
|
382
515
|
name: "aximo_run_plan",
|
|
383
516
|
title: "Run test plan",
|