@autifyhq/aximo-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.
Files changed (96) hide show
  1. package/README.md +219 -38
  2. package/dist/cli.js +5 -0
  3. package/dist/client.d.ts +2 -0
  4. package/dist/client.js +18 -0
  5. package/dist/commands/device/device-config.d.ts +14 -0
  6. package/dist/commands/device/device-config.js +46 -0
  7. package/dist/commands/device/device-config.test.d.ts +1 -0
  8. package/dist/commands/device/device-config.test.js +56 -0
  9. package/dist/commands/device/doctor.d.ts +13 -0
  10. package/dist/commands/device/doctor.js +142 -25
  11. package/dist/commands/device/doctor.test.js +61 -1
  12. package/dist/commands/device/register.d.ts +2 -0
  13. package/dist/commands/device/register.js +50 -32
  14. package/dist/commands/device/register.test.d.ts +1 -0
  15. package/dist/commands/device/register.test.js +95 -0
  16. package/dist/commands/device/unregister.d.ts +5 -3
  17. package/dist/commands/device/unregister.js +32 -33
  18. package/dist/commands/device/unregister.test.d.ts +1 -0
  19. package/dist/commands/device/unregister.test.js +73 -0
  20. package/dist/commands/mobile/connect-core.d.ts +12 -0
  21. package/dist/commands/mobile/connect-core.js +88 -0
  22. package/dist/commands/mobile/connect.d.ts +9 -0
  23. package/dist/commands/mobile/connect.js +35 -0
  24. package/dist/commands/mobile/connect.test.d.ts +1 -0
  25. package/dist/commands/mobile/connect.test.js +87 -0
  26. package/dist/commands/mobile/doctor.d.ts +28 -0
  27. package/dist/commands/mobile/doctor.js +162 -0
  28. package/dist/commands/mobile/index.d.ts +2 -0
  29. package/dist/commands/mobile/index.js +16 -0
  30. package/dist/commands/mobile/install.d.ts +43 -0
  31. package/dist/commands/mobile/install.js +76 -0
  32. package/dist/commands/mobile/install.test.d.ts +1 -0
  33. package/dist/commands/mobile/install.test.js +104 -0
  34. package/dist/commands/mobile/list.d.ts +10 -0
  35. package/dist/commands/mobile/list.js +30 -0
  36. package/dist/commands/mobile/paths.d.ts +14 -0
  37. package/dist/commands/mobile/paths.js +71 -0
  38. package/dist/commands/mobile/register-core.d.ts +19 -0
  39. package/dist/commands/mobile/register-core.js +46 -0
  40. package/dist/commands/mobile/register.d.ts +37 -0
  41. package/dist/commands/mobile/register.js +57 -0
  42. package/dist/commands/mobile/register.test.d.ts +1 -0
  43. package/dist/commands/mobile/register.test.js +91 -0
  44. package/dist/commands/mobile/setup.d.ts +27 -0
  45. package/dist/commands/mobile/setup.js +100 -0
  46. package/dist/commands/mobile/setup.test.d.ts +1 -0
  47. package/dist/commands/mobile/setup.test.js +49 -0
  48. package/dist/commands/mobile/unregister.d.ts +14 -0
  49. package/dist/commands/mobile/unregister.js +37 -0
  50. package/dist/commands/schema.js +29 -0
  51. package/dist/commands/session/create.d.ts +5 -0
  52. package/dist/commands/session/create.js +6 -0
  53. package/dist/index.d.ts +3 -0
  54. package/dist/index.js +3 -0
  55. package/dist/mobile-control/app-install.d.ts +21 -0
  56. package/dist/mobile-control/app-install.js +28 -0
  57. package/dist/mobile-control/app-install.test.d.ts +1 -0
  58. package/dist/mobile-control/app-install.test.js +78 -0
  59. package/dist/mobile-control/appium-controller.d.ts +42 -0
  60. package/dist/mobile-control/appium-controller.js +372 -0
  61. package/dist/mobile-control/appium-controller.test.d.ts +1 -0
  62. package/dist/mobile-control/appium-controller.test.js +252 -0
  63. package/dist/mobile-control/appium-import.d.ts +7 -0
  64. package/dist/mobile-control/appium-import.js +6 -0
  65. package/dist/mobile-control/appium-import.test.d.ts +1 -0
  66. package/dist/mobile-control/appium-import.test.js +9 -0
  67. package/dist/mobile-control/appium-server.d.ts +30 -0
  68. package/dist/mobile-control/appium-server.js +110 -0
  69. package/dist/mobile-control/appium-server.test.d.ts +1 -0
  70. package/dist/mobile-control/appium-server.test.js +108 -0
  71. package/dist/mobile-control/controller.d.ts +22 -0
  72. package/dist/mobile-control/controller.js +1 -0
  73. package/dist/mobile-control/devices.d.ts +23 -0
  74. package/dist/mobile-control/devices.js +129 -0
  75. package/dist/mobile-control/devices.test.d.ts +1 -0
  76. package/dist/mobile-control/devices.test.js +75 -0
  77. package/dist/mobile-control/exec.d.ts +6 -0
  78. package/dist/mobile-control/exec.js +13 -0
  79. package/dist/mobile-control/ios-signing.d.ts +28 -0
  80. package/dist/mobile-control/ios-signing.js +38 -0
  81. package/dist/mobile-control/ios-signing.test.d.ts +1 -0
  82. package/dist/mobile-control/ios-signing.test.js +73 -0
  83. package/dist/mobile-control/tunnel.d.ts +15 -0
  84. package/dist/mobile-control/tunnel.js +165 -0
  85. package/dist/mobile-control/tunnel.test.d.ts +1 -0
  86. package/dist/mobile-control/tunnel.test.js +155 -0
  87. package/dist/mobile-control/webdriver.d.ts +31 -0
  88. package/dist/mobile-control/webdriver.js +137 -0
  89. package/dist/mobile-control/webdriver.test.d.ts +1 -0
  90. package/dist/mobile-control/webdriver.test.js +45 -0
  91. package/dist/proxy.d.ts +11 -0
  92. package/dist/proxy.js +85 -0
  93. package/dist/proxy.test.d.ts +1 -0
  94. package/dist/proxy.test.js +72 -0
  95. package/dist/types.d.ts +1 -0
  96. package/package.json +5 -2
@@ -0,0 +1,14 @@
1
+ import { type ClientDeps } from "../../client.js";
2
+ import type { Envelope } from "../../types.js";
3
+ export declare function runMobileUnregister(options: {
4
+ deviceId?: string;
5
+ }, deps?: ClientDeps): Promise<Envelope<{
6
+ success: boolean;
7
+ }>>;
8
+ declare const _default: import("citty").CommandDef<{
9
+ readonly "device-id": {
10
+ readonly type: "string";
11
+ readonly description: "Device ID (defaults to the sole registered local device)";
12
+ };
13
+ }>;
14
+ export default _default;
@@ -0,0 +1,37 @@
1
+ import { defineCommand } from "citty";
2
+ import { existsSync, unlinkSync } from "node:fs";
3
+ import { request } from "../../client.js";
4
+ import { handleApiError } from "../../errors.js";
5
+ import { formatSuccess, printResult } from "../../output.js";
6
+ import { resolveDeviceConfig } from "./connect-core.js";
7
+ import { getMobileDeviceConfigPath } from "./paths.js";
8
+ export async function runMobileUnregister(options, deps) {
9
+ const resolved = resolveDeviceConfig(options.deviceId);
10
+ if (!resolved.ok)
11
+ return resolved;
12
+ const { deviceId } = resolved.config;
13
+ try {
14
+ await request(`/api/devices/user/${deviceId}`, { method: "DELETE" }, deps ?? {});
15
+ const configPath = getMobileDeviceConfigPath(deviceId);
16
+ if (existsSync(configPath)) {
17
+ unlinkSync(configPath);
18
+ }
19
+ return formatSuccess({ success: true });
20
+ }
21
+ catch (err) {
22
+ return handleApiError(err);
23
+ }
24
+ }
25
+ export default defineCommand({
26
+ meta: { name: "unregister", description: "Unregister a local mobile device" },
27
+ args: {
28
+ "device-id": {
29
+ type: "string",
30
+ description: "Device ID (defaults to the sole registered local device)",
31
+ },
32
+ },
33
+ async run({ args }) {
34
+ const deviceId = args["device-id"];
35
+ printResult(await runMobileUnregister(deviceId ? { deviceId } : {}));
36
+ },
37
+ });
@@ -36,6 +36,7 @@ export function getSchema() {
36
36
  arg("upload-type", "string", "ANDROID_APP | IOS_APP"),
37
37
  arg("app-name", "string", ""),
38
38
  arg("bundle-id", "string", ""),
39
+ arg("user-device-id", "string", "Run on a registered local device"),
39
40
  ...MUTATE_ARGS,
40
41
  ]),
41
42
  cmd("get", "GET /api/sessions/{sessionId}", [
@@ -119,6 +120,34 @@ export function getSchema() {
119
120
  arg("available-only", "boolean", ""),
120
121
  ]),
121
122
  ]),
123
+ cmd("mobile", "Register and connect a physically-connected Android phone (adb) or iPhone (idb/Xcode)", [], [
124
+ cmd("list", "List physically-connected phones (Android via adb, iOS via idb)"),
125
+ cmd("register", "Register a physically-connected Android phone or iPhone as an Aximo device", [
126
+ arg("name", "string", "Device name", true),
127
+ arg("platform", "string", "Device platform (`android` or `ios`)"),
128
+ arg("serial", "string", "Android adb serial (defaults to the connected one)"),
129
+ arg("udid", "string", "iOS device udid (defaults to the connected one)"),
130
+ arg("os", "string", "Optional OS version label"),
131
+ ]),
132
+ cmd("install", "Install an app on the connected phone (.apk via adb, .ipa via idb)", [
133
+ arg("app", "string", "Path to the .apk or .ipa to install", true),
134
+ arg("platform", "string", "android | ios (inferred from the file extension)"),
135
+ arg("serial", "string", "Android adb serial (defaults to the connected one)"),
136
+ arg("udid", "string", "iOS device udid (defaults to the connected one)"),
137
+ ]),
138
+ cmd("unregister", "Unregister a local mobile device", [
139
+ arg("device-id", "string", "Device ID (defaults to the sole registered local device)"),
140
+ ]),
141
+ cmd("connect", "Connect a registered phone via a CLI-managed Appium server", [
142
+ arg("device-id", "string", "Registered device id (required if multiple)"),
143
+ ]),
144
+ cmd("setup", "Install the Appium drivers used to drive registered phones", [
145
+ arg("platform", "string", "Limit to ios | android driver"),
146
+ ]),
147
+ cmd("doctor", "Diagnose physical-device tooling (adb/idb, Xcode, Appium setup)", [
148
+ arg("platform", "string", "Limit checks to ios | android"),
149
+ ]),
150
+ ]),
122
151
  cmd("upload", "Upload commands", [], [
123
152
  cmd("create", "POST /api/uploads", [
124
153
  arg("file-name", "string", "File name"),
@@ -16,6 +16,7 @@ export interface CreateSessionOptions {
16
16
  uploadType?: unknown;
17
17
  appName?: unknown;
18
18
  bundleId?: unknown;
19
+ userDeviceId?: unknown;
19
20
  json?: unknown;
20
21
  dryRun?: unknown;
21
22
  }
@@ -66,6 +67,10 @@ declare const _default: import("citty").CommandDef<{
66
67
  readonly "bundle-id": {
67
68
  readonly type: "string";
68
69
  };
70
+ readonly "user-device-id": {
71
+ readonly type: "string";
72
+ readonly description: "Run on a registered local device (BYOD desktop or local mobile)";
73
+ };
69
74
  readonly json: {
70
75
  readonly type: "string";
71
76
  readonly description: "Raw JSON request body (replaces individual flags)";
@@ -29,6 +29,7 @@ export async function runSessionCreate(options, deps) {
29
29
  uploadType: parseFileType(options.uploadType),
30
30
  appName: optionalString("appName", options.appName),
31
31
  bundleId: optionalString("bundleId", options.bundleId),
32
+ userDeviceId: optionalString("userDeviceId", options.userDeviceId),
32
33
  };
33
34
  }
34
35
  catch (err) {
@@ -63,6 +64,10 @@ export default defineCommand({
63
64
  "upload-type": { type: "string", description: "ANDROID_APP | IOS_APP" },
64
65
  "app-name": { type: "string" },
65
66
  "bundle-id": { type: "string" },
67
+ "user-device-id": {
68
+ type: "string",
69
+ description: "Run on a registered local device (BYOD desktop or local mobile)",
70
+ },
66
71
  json: { type: "string", description: "Raw JSON request body (replaces individual flags)" },
67
72
  "dry-run": { type: "boolean", description: "Validate and show request without calling API" },
68
73
  },
@@ -82,6 +87,7 @@ export default defineCommand({
82
87
  uploadType: args["upload-type"],
83
88
  appName: args["app-name"],
84
89
  bundleId: args["bundle-id"],
90
+ userDeviceId: args["user-device-id"],
85
91
  json: args.json,
86
92
  dryRun: args["dry-run"],
87
93
  }));
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ export { getApiKey, getBaseUrl, request } from "./client.js";
3
3
  export { runCaseCreate } from "./commands/case/create.js";
4
4
  export { runDeviceList } from "./commands/device/list.js";
5
5
  export { runHealthCheck } from "./commands/health/check.js";
6
+ export { runMobileList } from "./commands/mobile/list.js";
7
+ export { runMobileRegister } from "./commands/mobile/register.js";
8
+ export { runMobileDoctor } from "./commands/mobile/doctor.js";
6
9
  export { runPlanAddCase } from "./commands/plan/add-case.js";
7
10
  export { runPlanCreate } from "./commands/plan/create.js";
8
11
  export { runPlanRemoveCase } from "./commands/plan/remove-case.js";
package/dist/index.js CHANGED
@@ -2,6 +2,9 @@ export { getApiKey, getBaseUrl, request } from "./client.js";
2
2
  export { runCaseCreate } from "./commands/case/create.js";
3
3
  export { runDeviceList } from "./commands/device/list.js";
4
4
  export { runHealthCheck } from "./commands/health/check.js";
5
+ export { runMobileList } from "./commands/mobile/list.js";
6
+ export { runMobileRegister } from "./commands/mobile/register.js";
7
+ export { runMobileDoctor } from "./commands/mobile/doctor.js";
5
8
  export { runPlanAddCase } from "./commands/plan/add-case.js";
6
9
  export { runPlanCreate } from "./commands/plan/create.js";
7
10
  export { runPlanRemoveCase } from "./commands/plan/remove-case.js";
@@ -0,0 +1,21 @@
1
+ import { type ClientDeps } from "../client.js";
2
+ import { type ExecFn } from "./exec.js";
3
+ export interface InstallUploadedAppOptions {
4
+ /** Aximo upload id of the app under test. */
5
+ uploadId: string;
6
+ platform: "ios" | "android";
7
+ /** adb serial (Android) or udid (iOS) of the registered device. */
8
+ target: string;
9
+ exec?: ExecFn;
10
+ /** Client overrides (apiKey/baseUrl/fetchImpl) — used in tests. */
11
+ deps?: ClientDeps;
12
+ }
13
+ /**
14
+ * Download the app under test from Aximo (authenticated via the org API key, which
15
+ * redirects to short-lived storage) and install it on the connected device —
16
+ * Android `.apk` via `adb install -r`, iOS `.ipa` via `idb install`. The app binary
17
+ * never crosses the control tunnel; only the opaque upload id does.
18
+ */
19
+ export declare function installUploadedApp(opts: InstallUploadedAppOptions): Promise<{
20
+ output: string;
21
+ }>;
@@ -0,0 +1,28 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { requestToFile } from "../client.js";
6
+ import { execBinary } from "./exec.js";
7
+ /**
8
+ * Download the app under test from Aximo (authenticated via the org API key, which
9
+ * redirects to short-lived storage) and install it on the connected device —
10
+ * Android `.apk` via `adb install -r`, iOS `.ipa` via `idb install`. The app binary
11
+ * never crosses the control tunnel; only the opaque upload id does.
12
+ */
13
+ export async function installUploadedApp(opts) {
14
+ const exec = opts.exec ?? execBinary;
15
+ const ext = opts.platform === "ios" ? ".ipa" : ".apk";
16
+ const tmpPath = join(tmpdir(), `aximo-install-${randomBytes(8).toString("hex")}${ext}`);
17
+ try {
18
+ // Stream the (authenticated) download straight to disk — apps can be large.
19
+ await requestToFile(`/api/devices/apps/${encodeURIComponent(opts.uploadId)}/download`, tmpPath, opts.deps ?? {});
20
+ const { stdout } = opts.platform === "ios"
21
+ ? await exec("idb", ["install", "--udid", opts.target, tmpPath])
22
+ : await exec("adb", ["-s", opts.target, "install", "-r", tmpPath]);
23
+ return { output: stdout.toString("utf-8").trim() };
24
+ }
25
+ finally {
26
+ await rm(tmpPath, { force: true }).catch(() => { });
27
+ }
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { installUploadedApp } from "./app-install.js";
3
+ const DEPS = { apiKey: "test-key", baseUrl: "https://aximo.test" };
4
+ function fakeFetch(ok, bytes = "apk-bytes") {
5
+ let seen = "";
6
+ const impl = vi.fn(async (url) => {
7
+ seen = url;
8
+ return {
9
+ ok,
10
+ status: ok ? 200 : 403,
11
+ statusText: ok ? "OK" : "Forbidden",
12
+ text: async () => (ok ? "" : "forbidden"),
13
+ body: ok
14
+ ? new ReadableStream({
15
+ start(controller) {
16
+ controller.enqueue(new TextEncoder().encode(bytes));
17
+ controller.close();
18
+ },
19
+ })
20
+ : null,
21
+ headers: { get: () => null },
22
+ };
23
+ });
24
+ return { impl, url: () => seen };
25
+ }
26
+ function captureExec() {
27
+ const calls = [];
28
+ const exec = async (file, args) => {
29
+ calls.push({ file, args });
30
+ return { stdout: Buffer.from("Success"), stderr: "" };
31
+ };
32
+ return { exec, calls: () => calls };
33
+ }
34
+ describe("installUploadedApp", () => {
35
+ it("fetches the app authenticated by upload id and installs via adb", async () => {
36
+ const { exec, calls } = captureExec();
37
+ const { impl, url } = fakeFetch(true);
38
+ const res = await installUploadedApp({
39
+ uploadId: "up-123",
40
+ platform: "android",
41
+ target: "R1",
42
+ exec,
43
+ deps: { ...DEPS, fetchImpl: impl },
44
+ });
45
+ expect(res.output).toContain("Success");
46
+ expect(url()).toBe("https://aximo.test/api/devices/apps/up-123/download");
47
+ const call = calls()[0];
48
+ expect(call?.file).toBe("adb");
49
+ expect(call?.args.slice(0, 4)).toEqual(["-s", "R1", "install", "-r"]);
50
+ expect(call?.args[4]).toMatch(/\.apk$/);
51
+ });
52
+ it("installs an iOS app via idb", async () => {
53
+ const { exec, calls } = captureExec();
54
+ const { impl } = fakeFetch(true);
55
+ await installUploadedApp({
56
+ uploadId: "up-9",
57
+ platform: "ios",
58
+ target: "00008030-ABC",
59
+ exec,
60
+ deps: { ...DEPS, fetchImpl: impl },
61
+ });
62
+ const call = calls()[0];
63
+ expect(call?.file).toBe("idb");
64
+ expect(call?.args.slice(0, 3)).toEqual(["install", "--udid", "00008030-ABC"]);
65
+ expect(call?.args[3]).toMatch(/\.ipa$/);
66
+ });
67
+ it("throws when the authenticated download fails", async () => {
68
+ const { exec } = captureExec();
69
+ const { impl } = fakeFetch(false);
70
+ await expect(installUploadedApp({
71
+ uploadId: "missing",
72
+ platform: "android",
73
+ target: "R1",
74
+ exec,
75
+ deps: { ...DEPS, fetchImpl: impl },
76
+ })).rejects.toThrow();
77
+ });
78
+ });
@@ -0,0 +1,42 @@
1
+ import type { ActionResult, MobileController } from "./controller.js";
2
+ import { type IosSigningConfig } from "./ios-signing.js";
3
+ import { type WebDriverTransport } from "./webdriver.js";
4
+ export interface AppiumControllerOptions {
5
+ endpoint: string;
6
+ platform: "ios" | "android";
7
+ target: string;
8
+ iosSigning?: IosSigningConfig;
9
+ transport?: WebDriverTransport;
10
+ }
11
+ export declare class AppiumController implements MobileController {
12
+ private readonly session;
13
+ private readonly platform;
14
+ private readonly target;
15
+ private readonly iosSigning;
16
+ private sessionReady;
17
+ private scaling;
18
+ constructor(opts: AppiumControllerOptions);
19
+ private capabilities;
20
+ private ensureSession;
21
+ private scale;
22
+ private toPoint;
23
+ private pointerAction;
24
+ screenshot(): Promise<Buffer>;
25
+ tap(x: number, y: number): Promise<void>;
26
+ doubleTap(x: number, y: number): Promise<void>;
27
+ longPress(x: number, y: number, durationMs: number): Promise<void>;
28
+ swipe(x1: number, y1: number, x2: number, y2: number, durationMs: number): Promise<void>;
29
+ private performPointer;
30
+ typeText(text: string): Promise<ActionResult>;
31
+ clearText(): Promise<ActionResult>;
32
+ pressKey(key: string): Promise<ActionResult>;
33
+ launchApp(app: string): Promise<ActionResult>;
34
+ findElement(text: string): Promise<ActionResult>;
35
+ private locate;
36
+ hideKeyboard(): Promise<ActionResult>;
37
+ getDeviceTime(): Promise<{
38
+ datetime: string;
39
+ timezone: string;
40
+ } | null>;
41
+ close(): Promise<void>;
42
+ }