@autifyhq/aximo-cli 0.1.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/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +21 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +90 -0
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +127 -0
- package/dist/commands/configure.d.ts +24 -0
- package/dist/commands/configure.js +103 -0
- package/dist/commands/configure.test.d.ts +1 -0
- package/dist/commands/configure.test.js +96 -0
- package/dist/commands/device/connect.d.ts +2 -0
- package/dist/commands/device/connect.js +84 -0
- package/dist/commands/device/doctor.d.ts +34 -0
- package/dist/commands/device/doctor.js +421 -0
- package/dist/commands/device/doctor.test.d.ts +1 -0
- package/dist/commands/device/doctor.test.js +89 -0
- package/dist/commands/device/index.d.ts +2 -0
- package/dist/commands/device/index.js +16 -0
- package/dist/commands/device/list.d.ts +20 -0
- package/dist/commands/device/list.js +40 -0
- package/dist/commands/device/list.test.d.ts +1 -0
- package/dist/commands/device/list.test.js +38 -0
- package/dist/commands/device/logs.d.ts +34 -0
- package/dist/commands/device/logs.js +160 -0
- package/dist/commands/device/logs.test.d.ts +1 -0
- package/dist/commands/device/logs.test.js +70 -0
- package/dist/commands/device/paths.d.ts +12 -0
- package/dist/commands/device/paths.js +40 -0
- package/dist/commands/device/ping.d.ts +21 -0
- package/dist/commands/device/ping.js +66 -0
- package/dist/commands/device/ping.test.d.ts +1 -0
- package/dist/commands/device/ping.test.js +68 -0
- package/dist/commands/device/register.d.ts +23 -0
- package/dist/commands/device/register.js +83 -0
- package/dist/commands/device/reset.d.ts +30 -0
- package/dist/commands/device/reset.js +67 -0
- package/dist/commands/device/reset.test.d.ts +1 -0
- package/dist/commands/device/reset.test.js +69 -0
- package/dist/commands/device/setup.d.ts +25 -0
- package/dist/commands/device/setup.js +105 -0
- package/dist/commands/device/setup.test.d.ts +1 -0
- package/dist/commands/device/setup.test.js +64 -0
- package/dist/commands/device/status.d.ts +16 -0
- package/dist/commands/device/status.js +20 -0
- package/dist/commands/device/unregister.d.ts +14 -0
- package/dist/commands/device/unregister.js +60 -0
- package/dist/commands/health/check.d.ts +5 -0
- package/dist/commands/health/check.js +20 -0
- package/dist/commands/health/check.test.d.ts +1 -0
- package/dist/commands/health/check.test.js +26 -0
- package/dist/commands/health/index.d.ts +2 -0
- package/dist/commands/health/index.js +7 -0
- package/dist/commands/project/index.d.ts +2 -0
- package/dist/commands/project/index.js +7 -0
- package/dist/commands/project/list.d.ts +7 -0
- package/dist/commands/project/list.js +20 -0
- package/dist/commands/project/list.test.d.ts +1 -0
- package/dist/commands/project/list.test.js +28 -0
- package/dist/commands/schema.d.ts +27 -0
- package/dist/commands/schema.js +146 -0
- package/dist/commands/schema.test.d.ts +1 -0
- package/dist/commands/schema.test.js +38 -0
- package/dist/commands/session/create.d.ts +78 -0
- package/dist/commands/session/create.js +89 -0
- package/dist/commands/session/get.d.ts +11 -0
- package/dist/commands/session/get.js +30 -0
- package/dist/commands/session/index.d.ts +2 -0
- package/dist/commands/session/index.js +8 -0
- package/dist/commands/session/session.test.d.ts +1 -0
- package/dist/commands/session/session.test.js +128 -0
- package/dist/commands/trigger/case/get.d.ts +14 -0
- package/dist/commands/trigger/case/get.js +33 -0
- package/dist/commands/trigger/case/index.d.ts +2 -0
- package/dist/commands/trigger/case/index.js +8 -0
- package/dist/commands/trigger/case/run.d.ts +58 -0
- package/dist/commands/trigger/case/run.js +81 -0
- package/dist/commands/trigger/index.d.ts +2 -0
- package/dist/commands/trigger/index.js +8 -0
- package/dist/commands/trigger/plan/get.d.ts +14 -0
- package/dist/commands/trigger/plan/get.js +33 -0
- package/dist/commands/trigger/plan/index.d.ts +2 -0
- package/dist/commands/trigger/plan/index.js +8 -0
- package/dist/commands/trigger/plan/run.d.ts +33 -0
- package/dist/commands/trigger/plan/run.js +63 -0
- package/dist/commands/trigger/trigger.test.d.ts +1 -0
- package/dist/commands/trigger/trigger.test.js +107 -0
- package/dist/commands/upload/create.d.ts +32 -0
- package/dist/commands/upload/create.js +65 -0
- package/dist/commands/upload/get.d.ts +10 -0
- package/dist/commands/upload/get.js +30 -0
- package/dist/commands/upload/index.d.ts +2 -0
- package/dist/commands/upload/index.js +11 -0
- package/dist/commands/upload/list.d.ts +15 -0
- package/dist/commands/upload/list.js +31 -0
- package/dist/commands/upload/put.d.ts +24 -0
- package/dist/commands/upload/put.js +62 -0
- package/dist/commands/upload/submit.d.ts +19 -0
- package/dist/commands/upload/submit.js +36 -0
- package/dist/commands/upload/upload.test.d.ts +1 -0
- package/dist/commands/upload/upload.test.js +133 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +36 -0
- package/dist/desktop-server/agent-sync.d.ts +19 -0
- package/dist/desktop-server/agent-sync.js +141 -0
- package/dist/desktop-server/agent-sync.test.d.ts +1 -0
- package/dist/desktop-server/agent-sync.test.js +130 -0
- package/dist/desktop-server/setup.d.ts +19 -0
- package/dist/desktop-server/setup.js +200 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +71 -0
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +88 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/mutate.d.ts +13 -0
- package/dist/mutate.js +25 -0
- package/dist/mutate.test.d.ts +1 -0
- package/dist/mutate.test.js +49 -0
- package/dist/output.d.ts +5 -0
- package/dist/output.js +23 -0
- package/dist/output.test.d.ts +1 -0
- package/dist/output.test.js +42 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/test-utils.d.ts +4 -0
- package/dist/test-utils.js +5 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.js +2 -0
- package/dist/validate.d.ts +14 -0
- package/dist/validate.js +103 -0
- package/dist/validate.test.d.ts +1 -0
- package/dist/validate.test.js +96 -0
- package/package.json +72 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { formatSuccess, printResult, validationError } from "../../../output.js";
|
|
3
|
+
import { handleApiError } from "../../../errors.js";
|
|
4
|
+
import { request } from "../../../client.js";
|
|
5
|
+
import { requireId, optionalString, parsePlatform, parseFileType, parseStringList, } from "../../../validate.js";
|
|
6
|
+
import { dryRunEnvelope, resolveJsonOverride } from "../../../mutate.js";
|
|
7
|
+
export async function runTriggerCaseRun(options, deps) {
|
|
8
|
+
let caseId;
|
|
9
|
+
let body;
|
|
10
|
+
const override = resolveJsonOverride(options.json);
|
|
11
|
+
if (override && "error" in override)
|
|
12
|
+
return override.error;
|
|
13
|
+
try {
|
|
14
|
+
caseId = requireId("caseId", options.caseId);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return validationError(err);
|
|
18
|
+
}
|
|
19
|
+
if (override) {
|
|
20
|
+
body = override.body;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
try {
|
|
24
|
+
body = {
|
|
25
|
+
model: optionalString("model", options.model),
|
|
26
|
+
hostnameOverrideIds: parseStringList("hostnameOverrideIds", options.hostnameOverrideIds),
|
|
27
|
+
deviceArn: optionalString("deviceArn", options.deviceArn),
|
|
28
|
+
deviceName: optionalString("deviceName", options.deviceName),
|
|
29
|
+
platform: parsePlatform(options.platform),
|
|
30
|
+
uploadArn: optionalString("uploadArn", options.uploadArn),
|
|
31
|
+
uploadType: parseFileType(options.uploadType),
|
|
32
|
+
appName: optionalString("appName", options.appName),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return validationError(err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const path = `/api/triggers/cases/${encodeURIComponent(caseId)}/run`;
|
|
40
|
+
if (options.dryRun) {
|
|
41
|
+
return dryRunEnvelope("POST", path, body);
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const res = await request(path, { method: "POST", body }, deps ?? {});
|
|
45
|
+
return formatSuccess(res);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return handleApiError(err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export default defineCommand({
|
|
52
|
+
meta: { name: "run", description: "Trigger a case run" },
|
|
53
|
+
args: {
|
|
54
|
+
caseId: { type: "positional", description: "Case ID", required: true },
|
|
55
|
+
model: { type: "string" },
|
|
56
|
+
"hostname-override-ids": { type: "string", description: "comma-separated" },
|
|
57
|
+
"device-arn": { type: "string" },
|
|
58
|
+
"device-name": { type: "string" },
|
|
59
|
+
platform: { type: "string" },
|
|
60
|
+
"upload-arn": { type: "string" },
|
|
61
|
+
"upload-type": { type: "string" },
|
|
62
|
+
"app-name": { type: "string" },
|
|
63
|
+
json: { type: "string", description: "Raw JSON request body" },
|
|
64
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
65
|
+
},
|
|
66
|
+
async run({ args }) {
|
|
67
|
+
printResult(await runTriggerCaseRun({
|
|
68
|
+
caseId: args.caseId,
|
|
69
|
+
model: args.model,
|
|
70
|
+
hostnameOverrideIds: args["hostname-override-ids"],
|
|
71
|
+
deviceArn: args["device-arn"],
|
|
72
|
+
deviceName: args["device-name"],
|
|
73
|
+
platform: args.platform,
|
|
74
|
+
uploadArn: args["upload-arn"],
|
|
75
|
+
uploadType: args["upload-type"],
|
|
76
|
+
appName: args["app-name"],
|
|
77
|
+
json: args.json,
|
|
78
|
+
dryRun: args["dry-run"],
|
|
79
|
+
}));
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
export default defineCommand({
|
|
3
|
+
meta: { name: "trigger", description: "Trigger case/plan runs" },
|
|
4
|
+
subCommands: {
|
|
5
|
+
case: () => import("./case/index.js").then((m) => m.default),
|
|
6
|
+
plan: () => import("./plan/index.js").then((m) => m.default),
|
|
7
|
+
},
|
|
8
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Envelope, PlanRun } from "../../../types.js";
|
|
2
|
+
import { type ClientDeps } from "../../../client.js";
|
|
3
|
+
export declare function runTriggerPlanGet(planIdInput: unknown, planRunIdInput: unknown, deps?: ClientDeps): Promise<Envelope<PlanRun>>;
|
|
4
|
+
declare const _default: import("citty").CommandDef<{
|
|
5
|
+
readonly planId: {
|
|
6
|
+
readonly type: "positional";
|
|
7
|
+
readonly required: true;
|
|
8
|
+
};
|
|
9
|
+
readonly planRunId: {
|
|
10
|
+
readonly type: "positional";
|
|
11
|
+
readonly required: true;
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { formatSuccess, printResult, validationError } from "../../../output.js";
|
|
3
|
+
import { handleApiError } from "../../../errors.js";
|
|
4
|
+
import { request } from "../../../client.js";
|
|
5
|
+
import { requireId } from "../../../validate.js";
|
|
6
|
+
export async function runTriggerPlanGet(planIdInput, planRunIdInput, deps) {
|
|
7
|
+
let planId;
|
|
8
|
+
let planRunId;
|
|
9
|
+
try {
|
|
10
|
+
planId = requireId("planId", planIdInput);
|
|
11
|
+
planRunId = requireId("planRunId", planRunIdInput);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
return validationError(err);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const res = await request(`/api/triggers/plans/${encodeURIComponent(planId)}/runs/${encodeURIComponent(planRunId)}`, {}, deps ?? {});
|
|
18
|
+
return formatSuccess(res);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
return handleApiError(err);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export default defineCommand({
|
|
25
|
+
meta: { name: "get", description: "Get status of a plan run" },
|
|
26
|
+
args: {
|
|
27
|
+
planId: { type: "positional", required: true },
|
|
28
|
+
planRunId: { type: "positional", required: true },
|
|
29
|
+
},
|
|
30
|
+
async run({ args }) {
|
|
31
|
+
printResult(await runTriggerPlanGet(args.planId, args.planRunId));
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
export default defineCommand({
|
|
3
|
+
meta: { name: "plan", description: "Plan trigger commands" },
|
|
4
|
+
subCommands: {
|
|
5
|
+
run: () => import("./run.js").then((m) => m.default),
|
|
6
|
+
get: () => import("./get.js").then((m) => m.default),
|
|
7
|
+
},
|
|
8
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Envelope, PlanRunResult } from "../../../types.js";
|
|
2
|
+
import { type ClientDeps } from "../../../client.js";
|
|
3
|
+
import { type DryRunResult } from "../../../mutate.js";
|
|
4
|
+
export interface TriggerPlanRunOptions {
|
|
5
|
+
planId?: unknown;
|
|
6
|
+
model?: unknown;
|
|
7
|
+
mobileConfig?: unknown;
|
|
8
|
+
json?: unknown;
|
|
9
|
+
dryRun?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare function runTriggerPlanRun(options: TriggerPlanRunOptions, deps?: ClientDeps): Promise<Envelope<PlanRunResult | DryRunResult>>;
|
|
12
|
+
declare const _default: import("citty").CommandDef<{
|
|
13
|
+
readonly planId: {
|
|
14
|
+
readonly type: "positional";
|
|
15
|
+
readonly required: true;
|
|
16
|
+
};
|
|
17
|
+
readonly model: {
|
|
18
|
+
readonly type: "string";
|
|
19
|
+
};
|
|
20
|
+
readonly "mobile-config": {
|
|
21
|
+
readonly type: "string";
|
|
22
|
+
readonly description: "JSON object string";
|
|
23
|
+
};
|
|
24
|
+
readonly json: {
|
|
25
|
+
readonly type: "string";
|
|
26
|
+
readonly description: "Raw JSON request body";
|
|
27
|
+
};
|
|
28
|
+
readonly "dry-run": {
|
|
29
|
+
readonly type: "boolean";
|
|
30
|
+
readonly description: "Validate and show request without calling API";
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
export default _default;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { formatSuccess, printResult, validationError } from "../../../output.js";
|
|
3
|
+
import { handleApiError } from "../../../errors.js";
|
|
4
|
+
import { request } from "../../../client.js";
|
|
5
|
+
import { requireId, optionalString, parseJsonObject } from "../../../validate.js";
|
|
6
|
+
import { dryRunEnvelope, resolveJsonOverride } from "../../../mutate.js";
|
|
7
|
+
export async function runTriggerPlanRun(options, deps) {
|
|
8
|
+
let planId;
|
|
9
|
+
let body;
|
|
10
|
+
const override = resolveJsonOverride(options.json);
|
|
11
|
+
if (override && "error" in override)
|
|
12
|
+
return override.error;
|
|
13
|
+
try {
|
|
14
|
+
planId = requireId("planId", options.planId);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return validationError(err);
|
|
18
|
+
}
|
|
19
|
+
if (override) {
|
|
20
|
+
body = override.body;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
try {
|
|
24
|
+
body = {
|
|
25
|
+
model: optionalString("model", options.model),
|
|
26
|
+
mobileConfig: parseJsonObject("mobileConfig", options.mobileConfig),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return validationError(err);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const path = `/api/triggers/plans/${encodeURIComponent(planId)}/run`;
|
|
34
|
+
if (options.dryRun) {
|
|
35
|
+
return dryRunEnvelope("POST", path, body);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const res = await request(path, { method: "POST", body }, deps ?? {});
|
|
39
|
+
return formatSuccess(res);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return handleApiError(err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export default defineCommand({
|
|
46
|
+
meta: { name: "run", description: "Trigger a plan run" },
|
|
47
|
+
args: {
|
|
48
|
+
planId: { type: "positional", required: true },
|
|
49
|
+
model: { type: "string" },
|
|
50
|
+
"mobile-config": { type: "string", description: "JSON object string" },
|
|
51
|
+
json: { type: "string", description: "Raw JSON request body" },
|
|
52
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
53
|
+
},
|
|
54
|
+
async run({ args }) {
|
|
55
|
+
printResult(await runTriggerPlanRun({
|
|
56
|
+
planId: args.planId,
|
|
57
|
+
model: args.model,
|
|
58
|
+
mobileConfig: args["mobile-config"],
|
|
59
|
+
json: args.json,
|
|
60
|
+
dryRun: args["dry-run"],
|
|
61
|
+
}));
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { server, http, HttpResponse, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
|
+
import { runTriggerCaseRun } from "./case/run.js";
|
|
4
|
+
import { runTriggerCaseGet } from "./case/get.js";
|
|
5
|
+
import { runTriggerPlanRun } from "./plan/run.js";
|
|
6
|
+
import { runTriggerPlanGet } from "./plan/get.js";
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
process.env["AXIMO_API_KEY"] = "test-key";
|
|
9
|
+
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
10
|
+
server.listen();
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => server.resetHandlers());
|
|
13
|
+
afterAll(() => server.close());
|
|
14
|
+
describe("trigger case", () => {
|
|
15
|
+
it("runs and returns sessionId", async () => {
|
|
16
|
+
server.use(http.post(`${TEST_BASE_URL}/api/triggers/cases/c1/run`, () => HttpResponse.json({ success: true, sessionId: "s1" }, { status: 201 })));
|
|
17
|
+
const result = await runTriggerCaseRun({ caseId: "c1", platform: "ANDROID" });
|
|
18
|
+
expect(result.ok).toBe(true);
|
|
19
|
+
if (result.ok && !("dryRun" in result.data))
|
|
20
|
+
expect(result.data.sessionId).toBe("s1");
|
|
21
|
+
});
|
|
22
|
+
it("rejects missing caseId", async () => {
|
|
23
|
+
const result = await runTriggerCaseRun({});
|
|
24
|
+
expect(result.ok).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it("rejects caseId with forbidden chars", async () => {
|
|
27
|
+
const result = await runTriggerCaseRun({ caseId: "c1/../x" });
|
|
28
|
+
expect(result.ok).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("--json replaces body", async () => {
|
|
31
|
+
let seen;
|
|
32
|
+
server.use(http.post(`${TEST_BASE_URL}/api/triggers/cases/c1/run`, async ({ request: req }) => {
|
|
33
|
+
seen = await req.json();
|
|
34
|
+
return HttpResponse.json({ success: true, sessionId: "s1" }, { status: 201 });
|
|
35
|
+
}));
|
|
36
|
+
const result = await runTriggerCaseRun({ caseId: "c1", json: '{"foo":1}' });
|
|
37
|
+
expect(result.ok).toBe(true);
|
|
38
|
+
expect(seen).toEqual({ foo: 1 });
|
|
39
|
+
});
|
|
40
|
+
it("--dry-run skips API call", async () => {
|
|
41
|
+
const result = await runTriggerCaseRun({ caseId: "c1", dryRun: true });
|
|
42
|
+
expect(result.ok).toBe(true);
|
|
43
|
+
if (result.ok && "dryRun" in result.data) {
|
|
44
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/triggers/cases/c1/run`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it("gets status by caseId + sessionId", async () => {
|
|
48
|
+
server.use(http.get(`${TEST_BASE_URL}/api/triggers/cases/c1/sessions/s1`, () => HttpResponse.json({
|
|
49
|
+
id: "s1",
|
|
50
|
+
status: "completed",
|
|
51
|
+
platform: "IOS",
|
|
52
|
+
screenshotsCount: 3,
|
|
53
|
+
})));
|
|
54
|
+
const result = await runTriggerCaseGet("c1", "s1");
|
|
55
|
+
expect(result.ok).toBe(true);
|
|
56
|
+
if (result.ok)
|
|
57
|
+
expect(result.data.status).toBe("completed");
|
|
58
|
+
});
|
|
59
|
+
it("rejects empty sessionId", async () => {
|
|
60
|
+
const result = await runTriggerCaseGet("c1", "");
|
|
61
|
+
expect(result.ok).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("trigger plan", () => {
|
|
65
|
+
it("runs with mobileConfig JSON", async () => {
|
|
66
|
+
let seen;
|
|
67
|
+
server.use(http.post(`${TEST_BASE_URL}/api/triggers/plans/p1/run`, async ({ request: req }) => {
|
|
68
|
+
seen = await req.json();
|
|
69
|
+
return HttpResponse.json({ success: true, planRunId: "pr1", totalCases: 2 }, { status: 201 });
|
|
70
|
+
}));
|
|
71
|
+
const result = await runTriggerPlanRun({
|
|
72
|
+
planId: "p1",
|
|
73
|
+
mobileConfig: '{"device":"ios"}',
|
|
74
|
+
});
|
|
75
|
+
expect(result.ok).toBe(true);
|
|
76
|
+
if (result.ok && !("dryRun" in result.data))
|
|
77
|
+
expect(result.data.totalCases).toBe(2);
|
|
78
|
+
expect(seen).toMatchObject({ mobileConfig: { device: "ios" } });
|
|
79
|
+
});
|
|
80
|
+
it("plan run --dry-run skips call", async () => {
|
|
81
|
+
const result = await runTriggerPlanRun({ planId: "p1", dryRun: true });
|
|
82
|
+
expect(result.ok).toBe(true);
|
|
83
|
+
if (result.ok && "dryRun" in result.data) {
|
|
84
|
+
expect(result.data.method).toBe("POST");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it("rejects invalid mobileConfig JSON", async () => {
|
|
88
|
+
const result = await runTriggerPlanRun({ planId: "p1", mobileConfig: "{bad" });
|
|
89
|
+
expect(result.ok).toBe(false);
|
|
90
|
+
if (!result.ok)
|
|
91
|
+
expect(result.error.code).toBe("validation_error");
|
|
92
|
+
});
|
|
93
|
+
it("gets plan run", async () => {
|
|
94
|
+
server.use(http.get(`${TEST_BASE_URL}/api/triggers/plans/p1/runs/pr1`, () => HttpResponse.json({ id: "pr1", status: "running" })));
|
|
95
|
+
const result = await runTriggerPlanGet("p1", "pr1");
|
|
96
|
+
expect(result.ok).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it("maps 500 on plan get", async () => {
|
|
99
|
+
server.use(http.get(`${TEST_BASE_URL}/api/triggers/plans/p1/runs/pr1`, () => HttpResponse.json({ error: "boom" }, { status: 500 })));
|
|
100
|
+
const result = await runTriggerPlanGet("p1", "pr1");
|
|
101
|
+
expect(result.ok).toBe(false);
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
expect(result.error.code).toBe("api_error");
|
|
104
|
+
expect(result.error.retryable).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Envelope, CreateUploadResult } from "../../types.js";
|
|
2
|
+
import { type ClientDeps } from "../../client.js";
|
|
3
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
4
|
+
export interface CreateUploadOptions {
|
|
5
|
+
fileName?: unknown;
|
|
6
|
+
fileSize?: unknown;
|
|
7
|
+
platform?: unknown;
|
|
8
|
+
json?: unknown;
|
|
9
|
+
dryRun?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare function runUploadCreate(options: CreateUploadOptions, deps?: ClientDeps): Promise<Envelope<CreateUploadResult | DryRunResult>>;
|
|
12
|
+
declare const _default: import("citty").CommandDef<{
|
|
13
|
+
readonly "file-name": {
|
|
14
|
+
readonly type: "string";
|
|
15
|
+
};
|
|
16
|
+
readonly "file-size": {
|
|
17
|
+
readonly type: "string";
|
|
18
|
+
readonly description: "File size in bytes";
|
|
19
|
+
};
|
|
20
|
+
readonly platform: {
|
|
21
|
+
readonly type: "string";
|
|
22
|
+
};
|
|
23
|
+
readonly json: {
|
|
24
|
+
readonly type: "string";
|
|
25
|
+
readonly description: "Raw JSON request body";
|
|
26
|
+
};
|
|
27
|
+
readonly "dry-run": {
|
|
28
|
+
readonly type: "boolean";
|
|
29
|
+
readonly description: "Validate and show request without calling API";
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
export default _default;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { request } from "../../client.js";
|
|
5
|
+
import { requireString, parsePlatform } from "../../validate.js";
|
|
6
|
+
import { dryRunEnvelope, resolveJsonOverride } from "../../mutate.js";
|
|
7
|
+
function parseFileSize(v) {
|
|
8
|
+
if (v === undefined || v === "")
|
|
9
|
+
return undefined;
|
|
10
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
11
|
+
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) {
|
|
12
|
+
throw new Error("fileSize must be a non-negative integer");
|
|
13
|
+
}
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
export async function runUploadCreate(options, deps) {
|
|
17
|
+
let body;
|
|
18
|
+
const override = resolveJsonOverride(options.json);
|
|
19
|
+
if (override && "error" in override)
|
|
20
|
+
return override.error;
|
|
21
|
+
if (override) {
|
|
22
|
+
body = override.body;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
try {
|
|
26
|
+
body = {
|
|
27
|
+
fileName: requireString("fileName", options.fileName),
|
|
28
|
+
fileSize: parseFileSize(options.fileSize),
|
|
29
|
+
platform: parsePlatform(options.platform),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return validationError(err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (options.dryRun) {
|
|
37
|
+
return dryRunEnvelope("POST", "/api/uploads", body);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const res = await request("/api/uploads", { method: "POST", body }, deps ?? {});
|
|
41
|
+
return formatSuccess(res);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return handleApiError(err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export default defineCommand({
|
|
48
|
+
meta: { name: "create", description: "Create an upload (returns presignedUrl)" },
|
|
49
|
+
args: {
|
|
50
|
+
"file-name": { type: "string" },
|
|
51
|
+
"file-size": { type: "string", description: "File size in bytes" },
|
|
52
|
+
platform: { type: "string" },
|
|
53
|
+
json: { type: "string", description: "Raw JSON request body" },
|
|
54
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
55
|
+
},
|
|
56
|
+
async run({ args }) {
|
|
57
|
+
printResult(await runUploadCreate({
|
|
58
|
+
fileName: args["file-name"],
|
|
59
|
+
fileSize: args["file-size"],
|
|
60
|
+
platform: args.platform,
|
|
61
|
+
json: args.json,
|
|
62
|
+
dryRun: args["dry-run"],
|
|
63
|
+
}));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Envelope, Upload } from "../../types.js";
|
|
2
|
+
import { type ClientDeps } from "../../client.js";
|
|
3
|
+
export declare function runUploadGet(uploadIdInput: unknown, deps?: ClientDeps): Promise<Envelope<Upload>>;
|
|
4
|
+
declare const _default: import("citty").CommandDef<{
|
|
5
|
+
readonly uploadId: {
|
|
6
|
+
readonly type: "positional";
|
|
7
|
+
readonly required: true;
|
|
8
|
+
};
|
|
9
|
+
}>;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { request } from "../../client.js";
|
|
5
|
+
import { requireId } from "../../validate.js";
|
|
6
|
+
export async function runUploadGet(uploadIdInput, deps) {
|
|
7
|
+
let uploadId;
|
|
8
|
+
try {
|
|
9
|
+
uploadId = requireId("uploadId", uploadIdInput);
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
return validationError(err);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const res = await request(`/api/uploads/${encodeURIComponent(uploadId)}`, {}, deps ?? {});
|
|
16
|
+
return formatSuccess(res);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return handleApiError(err);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export default defineCommand({
|
|
23
|
+
meta: { name: "get", description: "Get an upload by id" },
|
|
24
|
+
args: {
|
|
25
|
+
uploadId: { type: "positional", required: true },
|
|
26
|
+
},
|
|
27
|
+
async run({ args }) {
|
|
28
|
+
printResult(await runUploadGet(args.uploadId));
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
export default defineCommand({
|
|
3
|
+
meta: { name: "upload", description: "Upload commands" },
|
|
4
|
+
subCommands: {
|
|
5
|
+
create: () => import("./create.js").then((m) => m.default),
|
|
6
|
+
list: () => import("./list.js").then((m) => m.default),
|
|
7
|
+
get: () => import("./get.js").then((m) => m.default),
|
|
8
|
+
put: () => import("./put.js").then((m) => m.default),
|
|
9
|
+
submit: () => import("./submit.js").then((m) => m.default),
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Envelope, Upload } from "../../types.js";
|
|
2
|
+
import { type ClientDeps } from "../../client.js";
|
|
3
|
+
export interface UploadListOptions {
|
|
4
|
+
fileType?: unknown;
|
|
5
|
+
}
|
|
6
|
+
export declare function runUploadList(options?: UploadListOptions, deps?: ClientDeps): Promise<Envelope<{
|
|
7
|
+
uploads: Upload[];
|
|
8
|
+
}>>;
|
|
9
|
+
declare const _default: import("citty").CommandDef<{
|
|
10
|
+
readonly "file-type": {
|
|
11
|
+
readonly type: "string";
|
|
12
|
+
readonly description: "ANDROID_APP | IOS_APP";
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { request } from "../../client.js";
|
|
5
|
+
import { parseFileType } from "../../validate.js";
|
|
6
|
+
export async function runUploadList(options = {}, deps) {
|
|
7
|
+
let fileType;
|
|
8
|
+
try {
|
|
9
|
+
fileType = parseFileType(options.fileType);
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
return validationError(err);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const res = await request("/api/uploads", { query: { fileType } }, deps ?? {});
|
|
16
|
+
const uploads = res.uploads ?? [];
|
|
17
|
+
return formatSuccess({ uploads }, { total: uploads.length });
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return handleApiError(err);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export default defineCommand({
|
|
24
|
+
meta: { name: "list", description: "List uploads" },
|
|
25
|
+
args: {
|
|
26
|
+
"file-type": { type: "string", description: "ANDROID_APP | IOS_APP" },
|
|
27
|
+
},
|
|
28
|
+
async run({ args }) {
|
|
29
|
+
printResult(await runUploadList({ fileType: args["file-type"] }));
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Envelope, UploadPutResult } from "../../types.js";
|
|
2
|
+
import { type ClientDeps } from "../../client.js";
|
|
3
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
4
|
+
export interface UploadPutOptions {
|
|
5
|
+
uploadId?: unknown;
|
|
6
|
+
bytes?: Uint8Array | undefined;
|
|
7
|
+
dryRun?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export declare function runUploadPut(options: UploadPutOptions, deps?: ClientDeps): Promise<Envelope<UploadPutResult | DryRunResult>>;
|
|
10
|
+
declare const _default: import("citty").CommandDef<{
|
|
11
|
+
readonly uploadId: {
|
|
12
|
+
readonly type: "positional";
|
|
13
|
+
readonly required: true;
|
|
14
|
+
};
|
|
15
|
+
readonly file: {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
readonly description: "Local path to the file";
|
|
18
|
+
};
|
|
19
|
+
readonly "dry-run": {
|
|
20
|
+
readonly type: "boolean";
|
|
21
|
+
readonly description: "Validate and show request without calling API";
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
24
|
+
export default _default;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { defineCommand } from "citty";
|
|
3
|
+
import { formatSuccess, formatError, printResult, validationError } from "../../output.js";
|
|
4
|
+
import { handleApiError } from "../../errors.js";
|
|
5
|
+
import { request } from "../../client.js";
|
|
6
|
+
import { requireId } from "../../validate.js";
|
|
7
|
+
import { dryRunEnvelope } from "../../mutate.js";
|
|
8
|
+
export async function runUploadPut(options, deps) {
|
|
9
|
+
let uploadId;
|
|
10
|
+
try {
|
|
11
|
+
uploadId = requireId("uploadId", options.uploadId);
|
|
12
|
+
if (!options.dryRun && !options.bytes)
|
|
13
|
+
throw new Error("file bytes are required");
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
return validationError(err);
|
|
17
|
+
}
|
|
18
|
+
const path = `/api/uploads/${encodeURIComponent(uploadId)}`;
|
|
19
|
+
if (options.dryRun) {
|
|
20
|
+
return dryRunEnvelope("PUT", path);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await request(path, {
|
|
24
|
+
method: "PUT",
|
|
25
|
+
rawBody: options.bytes ? new Blob([new Uint8Array(options.bytes)]) : undefined,
|
|
26
|
+
contentType: "application/octet-stream",
|
|
27
|
+
expectJson: false,
|
|
28
|
+
}, deps ?? {});
|
|
29
|
+
return formatSuccess({ success: true });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return handleApiError(err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export default defineCommand({
|
|
36
|
+
meta: { name: "put", description: "Upload binary file bytes to the upload slot" },
|
|
37
|
+
args: {
|
|
38
|
+
uploadId: { type: "positional", required: true },
|
|
39
|
+
file: { type: "string", description: "Local path to the file" },
|
|
40
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
41
|
+
},
|
|
42
|
+
async run({ args }) {
|
|
43
|
+
const dryRun = args["dry-run"];
|
|
44
|
+
if (dryRun) {
|
|
45
|
+
printResult(await runUploadPut({ uploadId: args.uploadId, dryRun: true }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!args.file) {
|
|
49
|
+
printResult(formatError("validation_error", "file is required", false));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let bytes;
|
|
53
|
+
try {
|
|
54
|
+
bytes = await readFile(args.file);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
printResult(formatError("validation_error", `Cannot read file: ${args.file}`, false, err instanceof Error ? err.message : undefined));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
printResult(await runUploadPut({ uploadId: args.uploadId, bytes }));
|
|
61
|
+
},
|
|
62
|
+
});
|