@autifyhq/aximo-cli 0.3.0 → 0.5.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 (91) hide show
  1. package/README.md +129 -35
  2. package/dist/cli.js +1 -0
  3. package/dist/commands/case/case.test.js +34 -0
  4. package/dist/commands/case/delete.d.ts +20 -0
  5. package/dist/commands/case/delete.js +39 -0
  6. package/dist/commands/case/index.js +1 -0
  7. package/dist/commands/device/device-config.d.ts +14 -0
  8. package/dist/commands/device/device-config.js +46 -0
  9. package/dist/commands/device/device-config.test.d.ts +1 -0
  10. package/dist/commands/device/device-config.test.js +56 -0
  11. package/dist/commands/device/doctor.d.ts +14 -0
  12. package/dist/commands/device/doctor.js +155 -29
  13. package/dist/commands/device/doctor.test.js +69 -3
  14. package/dist/commands/device/paths.d.ts +1 -0
  15. package/dist/commands/device/paths.js +3 -0
  16. package/dist/commands/device/register.d.ts +2 -0
  17. package/dist/commands/device/register.js +50 -32
  18. package/dist/commands/device/register.test.d.ts +1 -0
  19. package/dist/commands/device/register.test.js +95 -0
  20. package/dist/commands/device/unregister.d.ts +5 -3
  21. package/dist/commands/device/unregister.js +32 -33
  22. package/dist/commands/device/unregister.test.d.ts +1 -0
  23. package/dist/commands/device/unregister.test.js +73 -0
  24. package/dist/commands/mcp/server.test.js +2 -1
  25. package/dist/commands/mcp/tools.js +11 -0
  26. package/dist/commands/mcp/tools.test.js +12 -0
  27. package/dist/commands/mobile/connect-core.d.ts +12 -0
  28. package/dist/commands/mobile/connect-core.js +74 -0
  29. package/dist/commands/mobile/connect.d.ts +9 -0
  30. package/dist/commands/mobile/connect.js +35 -0
  31. package/dist/commands/mobile/connect.test.d.ts +1 -0
  32. package/dist/commands/mobile/connect.test.js +87 -0
  33. package/dist/commands/mobile/doctor.d.ts +28 -0
  34. package/dist/commands/mobile/doctor.js +143 -0
  35. package/dist/commands/mobile/index.d.ts +2 -0
  36. package/dist/commands/mobile/index.js +15 -0
  37. package/dist/commands/mobile/list.d.ts +10 -0
  38. package/dist/commands/mobile/list.js +30 -0
  39. package/dist/commands/mobile/paths.d.ts +14 -0
  40. package/dist/commands/mobile/paths.js +71 -0
  41. package/dist/commands/mobile/register-core.d.ts +19 -0
  42. package/dist/commands/mobile/register-core.js +46 -0
  43. package/dist/commands/mobile/register.d.ts +32 -0
  44. package/dist/commands/mobile/register.js +51 -0
  45. package/dist/commands/mobile/register.test.d.ts +1 -0
  46. package/dist/commands/mobile/register.test.js +60 -0
  47. package/dist/commands/mobile/setup.d.ts +27 -0
  48. package/dist/commands/mobile/setup.js +100 -0
  49. package/dist/commands/mobile/setup.test.d.ts +1 -0
  50. package/dist/commands/mobile/setup.test.js +49 -0
  51. package/dist/commands/mobile/unregister.d.ts +14 -0
  52. package/dist/commands/mobile/unregister.js +37 -0
  53. package/dist/commands/schema.js +22 -0
  54. package/dist/commands/session/create.d.ts +5 -0
  55. package/dist/commands/session/create.js +6 -0
  56. package/dist/desktop-server/setup-tls.test.d.ts +1 -0
  57. package/dist/desktop-server/setup-tls.test.js +83 -0
  58. package/dist/desktop-server/setup.d.ts +17 -0
  59. package/dist/desktop-server/setup.js +53 -1
  60. package/dist/index.d.ts +3 -0
  61. package/dist/index.js +3 -0
  62. package/dist/mobile-control/appium-controller.d.ts +39 -0
  63. package/dist/mobile-control/appium-controller.js +367 -0
  64. package/dist/mobile-control/appium-controller.test.d.ts +1 -0
  65. package/dist/mobile-control/appium-controller.test.js +252 -0
  66. package/dist/mobile-control/appium-import.d.ts +7 -0
  67. package/dist/mobile-control/appium-import.js +6 -0
  68. package/dist/mobile-control/appium-import.test.d.ts +1 -0
  69. package/dist/mobile-control/appium-import.test.js +9 -0
  70. package/dist/mobile-control/appium-server.d.ts +30 -0
  71. package/dist/mobile-control/appium-server.js +110 -0
  72. package/dist/mobile-control/appium-server.test.d.ts +1 -0
  73. package/dist/mobile-control/appium-server.test.js +108 -0
  74. package/dist/mobile-control/controller.d.ts +22 -0
  75. package/dist/mobile-control/controller.js +1 -0
  76. package/dist/mobile-control/devices.d.ts +21 -0
  77. package/dist/mobile-control/devices.js +99 -0
  78. package/dist/mobile-control/devices.test.d.ts +1 -0
  79. package/dist/mobile-control/devices.test.js +35 -0
  80. package/dist/mobile-control/exec.d.ts +6 -0
  81. package/dist/mobile-control/exec.js +13 -0
  82. package/dist/mobile-control/tunnel.d.ts +11 -0
  83. package/dist/mobile-control/tunnel.js +152 -0
  84. package/dist/mobile-control/tunnel.test.d.ts +1 -0
  85. package/dist/mobile-control/tunnel.test.js +155 -0
  86. package/dist/mobile-control/webdriver.d.ts +31 -0
  87. package/dist/mobile-control/webdriver.js +137 -0
  88. package/dist/mobile-control/webdriver.test.d.ts +1 -0
  89. package/dist/mobile-control/webdriver.test.js +45 -0
  90. package/dist/types.d.ts +4 -0
  91. package/package.json +3 -1
@@ -0,0 +1,83 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { resolveAgentSslCertFile, writeCertBundle } from "./setup.js";
6
+ let tempDir;
7
+ const savedEnv = {};
8
+ beforeEach(() => {
9
+ tempDir = mkdtempSync(join(tmpdir(), "aximo-tls-"));
10
+ for (const key of ["SSL_CERT_FILE", "NODE_EXTRA_CA_CERTS", "AXIMO_HOME"]) {
11
+ savedEnv[key] = process.env[key];
12
+ delete process.env[key];
13
+ }
14
+ process.env["AXIMO_HOME"] = tempDir;
15
+ });
16
+ afterEach(() => {
17
+ for (const [key, value] of Object.entries(savedEnv)) {
18
+ if (value === undefined)
19
+ delete process.env[key];
20
+ else
21
+ process.env[key] = value;
22
+ }
23
+ rmSync(tempDir, { recursive: true, force: true });
24
+ vi.doUnmock("node:child_process");
25
+ vi.resetModules();
26
+ });
27
+ describe("writeCertBundle", () => {
28
+ it("concatenates certifi roots and the custom CA into one bundle", () => {
29
+ const certifi = join(tempDir, "certifi.pem");
30
+ const customCa = join(tempDir, "ca.pem");
31
+ const out = join(tempDir, "bundle.pem");
32
+ writeFileSync(certifi, "-----CERTIFI ROOT-----\n");
33
+ writeFileSync(customCa, "-----AXIMO CA-----\n");
34
+ writeCertBundle(out, certifi, customCa);
35
+ const result = readFileSync(out, "utf-8");
36
+ expect(result).toContain("-----CERTIFI ROOT-----");
37
+ expect(result).toContain("-----AXIMO CA-----");
38
+ // certifi first, custom CA second, single trailing newline
39
+ expect(result).toBe("-----CERTIFI ROOT-----\n-----AXIMO CA-----\n");
40
+ });
41
+ });
42
+ describe("resolveAgentSslCertFile", () => {
43
+ it("respects an explicit SSL_CERT_FILE override", () => {
44
+ process.env["SSL_CERT_FILE"] = "/custom/bundle.pem";
45
+ process.env["NODE_EXTRA_CA_CERTS"] = join(tempDir, "ca.pem");
46
+ writeFileSync(join(tempDir, "ca.pem"), "ca");
47
+ expect(resolveAgentSslCertFile("/no/python")).toBe("/custom/bundle.pem");
48
+ });
49
+ it("returns undefined when no custom CA is configured (public-CA servers)", () => {
50
+ expect(resolveAgentSslCertFile("/no/python")).toBeUndefined();
51
+ });
52
+ it("returns undefined when NODE_EXTRA_CA_CERTS points at a missing file", () => {
53
+ process.env["NODE_EXTRA_CA_CERTS"] = join(tempDir, "does-not-exist.pem");
54
+ expect(resolveAgentSslCertFile("/no/python")).toBeUndefined();
55
+ });
56
+ it("degrades gracefully (no throw) when the embedded certifi store can't be located", () => {
57
+ process.env["NODE_EXTRA_CA_CERTS"] = join(tempDir, "ca.pem");
58
+ writeFileSync(join(tempDir, "ca.pem"), "-----AXIMO CA-----\n");
59
+ const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
60
+ // A non-existent python makes `python -m certifi` fail, so no bundle is built.
61
+ expect(resolveAgentSslCertFile(join(tempDir, "no-such-python"))).toBeUndefined();
62
+ expect(errSpy).toHaveBeenCalled();
63
+ errSpy.mockRestore();
64
+ });
65
+ it("builds and returns the certifi+CA bundle when a custom CA and certifi store are present", async () => {
66
+ const certifiPath = join(tempDir, "certifi.pem");
67
+ const customCaPath = join(tempDir, "ca.pem");
68
+ writeFileSync(certifiPath, "-----CERTIFI ROOT-----\n");
69
+ writeFileSync(customCaPath, "-----AXIMO CA-----\n");
70
+ process.env["NODE_EXTRA_CA_CERTS"] = customCaPath;
71
+ // Stub `python -m certifi` so it resolves to the fixture certifi store above.
72
+ vi.resetModules();
73
+ vi.doMock("node:child_process", () => ({
74
+ spawn: vi.fn(),
75
+ spawnSync: vi.fn(() => ({ status: 0, stdout: `${certifiPath}\n` })),
76
+ }));
77
+ const { resolveAgentSslCertFile: resolve } = await import("./setup.js");
78
+ const bundlePath = resolve("/fake/python");
79
+ expect(bundlePath).toBe(join(tempDir, "agent-ca-bundle.pem"));
80
+ // The bundle is the certifi roots followed by the custom CA, in that order.
81
+ expect(readFileSync(bundlePath, "utf-8")).toBe("-----CERTIFI ROOT-----\n-----AXIMO CA-----\n");
82
+ });
83
+ });
@@ -15,5 +15,22 @@ interface RemoteOptions {
15
15
  deviceToken: string;
16
16
  deviceName: string;
17
17
  }
18
+ /** Path to the embedded interpreter's certifi CA store, or undefined if unavailable. */
19
+ export declare function embeddedCertifiPath(pythonPath: string): string | undefined;
20
+ /** Write a combined trust store (certifi roots followed by the custom CA) to outPath. */
21
+ export declare function writeCertBundle(outPath: string, certifiPath: string, customCaPath: string): void;
22
+ /**
23
+ * Resolve the SSL_CERT_FILE for the Python desktop agent.
24
+ *
25
+ * The Node CLI trusts a custom CA (e.g. an on-prem self-signed server) via
26
+ * NODE_EXTRA_CA_CERTS, but Python ignores that variable, so the agent's wss://
27
+ * relay connection would fail verification. Mirror the trust by building a
28
+ * bundle of the embedded interpreter's certifi roots PLUS the custom CA and
29
+ * pointing SSL_CERT_FILE at it. SSL_CERT_FILE replaces the whole trust store, so
30
+ * the bundle must contain both — the custom CA alone would break every other TLS
31
+ * the agent does. No-op when no custom CA is configured (public-CA servers
32
+ * already verify via certifi).
33
+ */
34
+ export declare function resolveAgentSslCertFile(pythonPath: string): string | undefined;
18
35
  export declare function startDesktopRemote(pythonPath: string, scriptPath: string, options: RemoteOptions): ChildProcess;
19
36
  export {};
@@ -5,7 +5,7 @@ import { arch as osArch, platform } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { Readable } from "node:stream";
7
7
  import { pipeline } from "node:stream/promises";
8
- import { getAximoDir, getCacheDir, getDepsMarkerPath, getEmbeddedPythonBinary, getPythonDir, getPythonMarkerPath, getRemoteLogPath, } from "../commands/device/paths.js";
8
+ import { getAgentCaBundlePath, getAximoDir, getCacheDir, getDepsMarkerPath, getEmbeddedPythonBinary, getPythonDir, getPythonMarkerPath, getRemoteLogPath, } from "../commands/device/paths.js";
9
9
  import { ensureAgent } from "./agent-sync.js";
10
10
  export const PYTHON_RELEASE = "20260414";
11
11
  export const PYTHON_VERSION = "3.12.13";
@@ -174,7 +174,58 @@ export async function ensurePythonReady(deps) {
174
174
  agentChanged: agent.changed,
175
175
  };
176
176
  }
177
+ /** Path to the embedded interpreter's certifi CA store, or undefined if unavailable. */
178
+ export function embeddedCertifiPath(pythonPath) {
179
+ const result = spawnSync(pythonPath, ["-m", "certifi"], { encoding: "utf-8" });
180
+ if (result.status !== 0)
181
+ return undefined;
182
+ const path = (result.stdout ?? "").trim();
183
+ return path && existsSync(path) ? path : undefined;
184
+ }
185
+ /** Write a combined trust store (certifi roots followed by the custom CA) to outPath. */
186
+ export function writeCertBundle(outPath, certifiPath, customCaPath) {
187
+ const certifi = readFileSync(certifiPath, "utf-8");
188
+ const customCa = readFileSync(customCaPath, "utf-8");
189
+ writeFileSync(outPath, `${certifi.trimEnd()}\n${customCa.trimEnd()}\n`, { mode: 0o644 });
190
+ }
191
+ /**
192
+ * Resolve the SSL_CERT_FILE for the Python desktop agent.
193
+ *
194
+ * The Node CLI trusts a custom CA (e.g. an on-prem self-signed server) via
195
+ * NODE_EXTRA_CA_CERTS, but Python ignores that variable, so the agent's wss://
196
+ * relay connection would fail verification. Mirror the trust by building a
197
+ * bundle of the embedded interpreter's certifi roots PLUS the custom CA and
198
+ * pointing SSL_CERT_FILE at it. SSL_CERT_FILE replaces the whole trust store, so
199
+ * the bundle must contain both — the custom CA alone would break every other TLS
200
+ * the agent does. No-op when no custom CA is configured (public-CA servers
201
+ * already verify via certifi).
202
+ */
203
+ export function resolveAgentSslCertFile(pythonPath) {
204
+ // Respect an explicit override — the user may already point Python at a bundle.
205
+ if (process.env["SSL_CERT_FILE"])
206
+ return process.env["SSL_CERT_FILE"];
207
+ const customCa = process.env["NODE_EXTRA_CA_CERTS"];
208
+ if (!customCa || !existsSync(customCa))
209
+ return undefined;
210
+ const certifiPath = embeddedCertifiPath(pythonPath);
211
+ if (!certifiPath) {
212
+ console.error("[remote] NODE_EXTRA_CA_CERTS is set but the embedded Python's certifi store " +
213
+ "could not be located; the desktop agent may fail TLS verification against a " +
214
+ "custom-CA server. Set SSL_CERT_FILE to a certifi+CA bundle to override.");
215
+ return undefined;
216
+ }
217
+ try {
218
+ const bundlePath = getAgentCaBundlePath();
219
+ writeCertBundle(bundlePath, certifiPath, customCa);
220
+ return bundlePath;
221
+ }
222
+ catch (err) {
223
+ console.error(`[remote] Failed to build the agent CA bundle: ${err instanceof Error ? err.message : String(err)}`);
224
+ return undefined;
225
+ }
226
+ }
177
227
  export function startDesktopRemote(pythonPath, scriptPath, options) {
228
+ const sslCertFile = resolveAgentSslCertFile(pythonPath);
178
229
  const child = spawn(pythonPath, [scriptPath], {
179
230
  env: {
180
231
  ...process.env,
@@ -183,6 +234,7 @@ export function startDesktopRemote(pythonPath, scriptPath, options) {
183
234
  RELAY_URL: options.relayUrl,
184
235
  DEVICE_TOKEN: options.deviceToken,
185
236
  DEVICE_NAME: options.deviceName,
237
+ ...(sslCertFile ? { SSL_CERT_FILE: sslCertFile } : {}),
186
238
  },
187
239
  stdio: "pipe",
188
240
  });
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,39 @@
1
+ import type { ActionResult, MobileController } from "./controller.js";
2
+ import { type WebDriverTransport } from "./webdriver.js";
3
+ export interface AppiumControllerOptions {
4
+ endpoint: string;
5
+ platform: "ios" | "android";
6
+ target: string;
7
+ transport?: WebDriverTransport;
8
+ }
9
+ export declare class AppiumController implements MobileController {
10
+ private readonly session;
11
+ private readonly platform;
12
+ private readonly target;
13
+ private sessionReady;
14
+ private scaling;
15
+ constructor(opts: AppiumControllerOptions);
16
+ private capabilities;
17
+ private ensureSession;
18
+ private scale;
19
+ private toPoint;
20
+ private pointerAction;
21
+ screenshot(): Promise<Buffer>;
22
+ tap(x: number, y: number): Promise<void>;
23
+ doubleTap(x: number, y: number): Promise<void>;
24
+ longPress(x: number, y: number, durationMs: number): Promise<void>;
25
+ swipe(x1: number, y1: number, x2: number, y2: number, durationMs: number): Promise<void>;
26
+ private performPointer;
27
+ typeText(text: string): Promise<ActionResult>;
28
+ clearText(): Promise<ActionResult>;
29
+ pressKey(key: string): Promise<ActionResult>;
30
+ launchApp(app: string): Promise<ActionResult>;
31
+ findElement(text: string): Promise<ActionResult>;
32
+ private locate;
33
+ hideKeyboard(): Promise<ActionResult>;
34
+ getDeviceTime(): Promise<{
35
+ datetime: string;
36
+ timezone: string;
37
+ } | null>;
38
+ close(): Promise<void>;
39
+ }
@@ -0,0 +1,367 @@
1
+ import { fetchTransport, WebDriverSession } from "./webdriver.js";
2
+ const ANDROID_KEY_CODES = {
3
+ enter: 66,
4
+ delete: 67,
5
+ tab: 61,
6
+ space: 62,
7
+ escape: 111,
8
+ back: 4,
9
+ home: 3,
10
+ };
11
+ const ANDROID_KEYCODE_A = 29;
12
+ const ANDROID_META_CTRL_ON = 4096;
13
+ const IOS_BUTTONS = {
14
+ home: "home",
15
+ };
16
+ const W3C_KEYS = {
17
+ enter: "\uE007",
18
+ delete: "\uE003",
19
+ tab: "\uE004",
20
+ escape: "\uE00C",
21
+ space: " ",
22
+ };
23
+ export class AppiumController {
24
+ session;
25
+ platform;
26
+ target;
27
+ sessionReady = null;
28
+ scaling = null;
29
+ constructor(opts) {
30
+ this.platform = opts.platform;
31
+ this.target = opts.target;
32
+ const transport = opts.transport ?? fetchTransport(opts.endpoint);
33
+ this.session = new WebDriverSession(transport);
34
+ }
35
+ capabilities() {
36
+ if (this.platform === "android") {
37
+ return {
38
+ platformName: "Android",
39
+ "appium:automationName": "UiAutomator2",
40
+ "appium:udid": this.target,
41
+ "appium:noReset": true,
42
+ "appium:autoLaunch": false,
43
+ "appium:newCommandTimeout": 0,
44
+ };
45
+ }
46
+ return {
47
+ platformName: "iOS",
48
+ "appium:automationName": "XCUITest",
49
+ "appium:udid": this.target,
50
+ "appium:newCommandTimeout": 0,
51
+ };
52
+ }
53
+ ensureSession() {
54
+ if (!this.sessionReady) {
55
+ this.sessionReady = this.session
56
+ .createSession(this.capabilities())
57
+ .then(() => undefined)
58
+ .catch((err) => {
59
+ this.sessionReady = null;
60
+ throw err;
61
+ });
62
+ }
63
+ return this.sessionReady;
64
+ }
65
+ // Incoming pixel coords scale to logical points: iOS divides by DPR, Android ~= 1.
66
+ async scale() {
67
+ if (this.scaling)
68
+ return this.scaling.scale;
69
+ const [rect, shot] = await Promise.all([this.session.windowRect(), this.session.screenshot()]);
70
+ const pixelWidth = readPngWidth(shot);
71
+ const scale = rect.width > 0 && pixelWidth > 0 ? pixelWidth / rect.width : 1;
72
+ this.scaling = { scale: scale > 0 ? scale : 1 };
73
+ return this.scaling.scale;
74
+ }
75
+ async toPoint(x, y) {
76
+ const s = await this.scale();
77
+ return [Math.round(x / s), Math.round(y / s)];
78
+ }
79
+ pointerAction(actions) {
80
+ return [
81
+ {
82
+ type: "pointer",
83
+ id: "finger1",
84
+ parameters: { pointerType: "touch" },
85
+ actions,
86
+ },
87
+ ];
88
+ }
89
+ async screenshot() {
90
+ await this.ensureSession();
91
+ return this.session.screenshot();
92
+ }
93
+ async tap(x, y) {
94
+ await this.ensureSession();
95
+ const [px, py] = await this.toPoint(x, y);
96
+ await this.performPointer([
97
+ { type: "pointerMove", duration: 0, x: px, y: py },
98
+ { type: "pointerDown", button: 0 },
99
+ { type: "pause", duration: 50 },
100
+ { type: "pointerUp", button: 0 },
101
+ ]);
102
+ }
103
+ async doubleTap(x, y) {
104
+ await this.ensureSession();
105
+ const [px, py] = await this.toPoint(x, y);
106
+ await this.performPointer([
107
+ { type: "pointerMove", duration: 0, x: px, y: py },
108
+ { type: "pointerDown", button: 0 },
109
+ { type: "pause", duration: 50 },
110
+ { type: "pointerUp", button: 0 },
111
+ { type: "pause", duration: 50 },
112
+ { type: "pointerDown", button: 0 },
113
+ { type: "pause", duration: 50 },
114
+ { type: "pointerUp", button: 0 },
115
+ ]);
116
+ }
117
+ async longPress(x, y, durationMs) {
118
+ await this.ensureSession();
119
+ const [px, py] = await this.toPoint(x, y);
120
+ await this.performPointer([
121
+ { type: "pointerMove", duration: 0, x: px, y: py },
122
+ { type: "pointerDown", button: 0 },
123
+ { type: "pause", duration: Math.max(0, Math.round(durationMs)) },
124
+ { type: "pointerUp", button: 0 },
125
+ ]);
126
+ }
127
+ async swipe(x1, y1, x2, y2, durationMs) {
128
+ await this.ensureSession();
129
+ const [px1, py1] = await this.toPoint(x1, y1);
130
+ const [px2, py2] = await this.toPoint(x2, y2);
131
+ await this.performPointer([
132
+ { type: "pointerMove", duration: 0, x: px1, y: py1 },
133
+ { type: "pointerDown", button: 0 },
134
+ { type: "pointerMove", duration: Math.max(0, Math.round(durationMs)), x: px2, y: py2 },
135
+ { type: "pointerUp", button: 0 },
136
+ ]);
137
+ }
138
+ async performPointer(actions) {
139
+ await this.session.performActions(this.pointerAction(actions));
140
+ await this.session.releaseActions();
141
+ }
142
+ async typeText(text) {
143
+ await this.ensureSession();
144
+ try {
145
+ if (this.platform === "android") {
146
+ await this.session.execute("mobile: type", [{ text }]);
147
+ return { ok: true };
148
+ }
149
+ await this.session.setValueOnActiveElement(text);
150
+ return { ok: true };
151
+ }
152
+ catch (error) {
153
+ return fail(error);
154
+ }
155
+ }
156
+ async clearText() {
157
+ await this.ensureSession();
158
+ try {
159
+ const active = await this.session.getActiveElement();
160
+ if (active) {
161
+ await this.session.elementClear(active);
162
+ return { ok: true };
163
+ }
164
+ }
165
+ catch { }
166
+ if (this.platform === "android") {
167
+ try {
168
+ await this.session.execute("mobile: pressKey", [
169
+ { keycode: ANDROID_KEYCODE_A, metastate: ANDROID_META_CTRL_ON },
170
+ ]);
171
+ await this.session.execute("mobile: pressKey", [{ keycode: ANDROID_KEY_CODES["delete"] }]);
172
+ return { ok: true };
173
+ }
174
+ catch (error) {
175
+ return fail(error);
176
+ }
177
+ }
178
+ return {
179
+ ok: false,
180
+ error: "Could not clear the field: no focused input element was found. " +
181
+ "Tap the field to focus it before calling clear_text.",
182
+ };
183
+ }
184
+ async pressKey(key) {
185
+ await this.ensureSession();
186
+ const normalized = key.toLowerCase().trim();
187
+ try {
188
+ if (this.platform === "android") {
189
+ const code = ANDROID_KEY_CODES[normalized];
190
+ if (code === undefined)
191
+ return { ok: false, error: `Unsupported key: ${key}` };
192
+ await this.session.execute("mobile: pressKey", [{ keycode: code }]);
193
+ return { ok: true };
194
+ }
195
+ if (normalized === "back") {
196
+ return {
197
+ ok: false,
198
+ error: '"back" key is Android-only. On iOS, tap the back button in the UI instead.',
199
+ };
200
+ }
201
+ const button = IOS_BUTTONS[normalized];
202
+ if (button) {
203
+ await this.session.execute("mobile: pressButton", [{ name: button }]);
204
+ return { ok: true };
205
+ }
206
+ const w3c = W3C_KEYS[normalized];
207
+ if (w3c) {
208
+ await this.session.performActions([
209
+ {
210
+ type: "key",
211
+ id: "keyboard",
212
+ actions: [
213
+ { type: "keyDown", value: w3c },
214
+ { type: "keyUp", value: w3c },
215
+ ],
216
+ },
217
+ ]);
218
+ await this.session.releaseActions();
219
+ return { ok: true };
220
+ }
221
+ return { ok: false, error: `Unsupported key: ${key}` };
222
+ }
223
+ catch (error) {
224
+ return fail(error);
225
+ }
226
+ }
227
+ async launchApp(app) {
228
+ if (!app)
229
+ return { ok: false, error: "launch_app requires an app id" };
230
+ await this.ensureSession();
231
+ try {
232
+ const param = this.platform === "android" ? { appId: app } : { bundleId: app };
233
+ await this.session.execute("mobile: activateApp", [param]);
234
+ return { ok: true };
235
+ }
236
+ catch (error) {
237
+ return fail(error);
238
+ }
239
+ }
240
+ async findElement(text) {
241
+ if (!text)
242
+ return { ok: false, error: "find_element requires text" };
243
+ await this.ensureSession();
244
+ try {
245
+ const ref = await this.locate(text);
246
+ if (!ref)
247
+ return { ok: false, error: `Element with text "${text}" not found` };
248
+ await this.session.elementClick(ref);
249
+ return { ok: true };
250
+ }
251
+ catch (error) {
252
+ return fail(error);
253
+ }
254
+ }
255
+ async locate(text) {
256
+ const strategies = this.platform === "android" ? androidStrategies(text) : iosStrategies(text);
257
+ for (const [using, value] of strategies) {
258
+ try {
259
+ const ref = await this.session.findElement(using, value);
260
+ if (ref)
261
+ return ref;
262
+ }
263
+ catch { }
264
+ }
265
+ return null;
266
+ }
267
+ async hideKeyboard() {
268
+ await this.ensureSession();
269
+ try {
270
+ await this.session.execute("mobile: hideKeyboard", []);
271
+ return { ok: true };
272
+ }
273
+ catch {
274
+ return { ok: true };
275
+ }
276
+ }
277
+ async getDeviceTime() {
278
+ await this.ensureSession();
279
+ try {
280
+ const raw = await this.session.execute("mobile: getDeviceTime", []);
281
+ if (typeof raw !== "string" || raw.trim().length === 0)
282
+ return null;
283
+ const parsed = new Date(raw);
284
+ if (Number.isNaN(parsed.getTime()))
285
+ return null;
286
+ return {
287
+ datetime: humanizeDateTime(parsed),
288
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
289
+ };
290
+ }
291
+ catch {
292
+ return null;
293
+ }
294
+ }
295
+ async close() {
296
+ try {
297
+ await this.session.deleteSession();
298
+ }
299
+ catch { }
300
+ }
301
+ }
302
+ // Builds an XPath string literal that is safe even when `s` contains single quotes.
303
+ function xpathLiteral(s) {
304
+ if (!s.includes("'"))
305
+ return `'${s}'`;
306
+ const parts = s.split("'").map((part) => `'${part}'`);
307
+ return `concat(${parts.join(`, "'", `)})`;
308
+ }
309
+ // Escapes backslashes and double quotes for a Java UiSelector string argument.
310
+ function uiSelectorLiteral(s) {
311
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
312
+ }
313
+ // Escapes single quotes for an iOS NSPredicate string literal.
314
+ function iosPredicateLiteral(s) {
315
+ return s.replace(/'/g, "\\'");
316
+ }
317
+ function androidStrategies(text) {
318
+ const lower = text.toLowerCase();
319
+ return [
320
+ ["accessibility id", text],
321
+ ["id", text],
322
+ ["xpath", `//*[@text=${xpathLiteral(text)}]`],
323
+ [
324
+ "xpath",
325
+ `//*[contains(translate(@text, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ${xpathLiteral(lower)})]`,
326
+ ],
327
+ ["xpath", `//*[@content-desc=${xpathLiteral(text)}]`],
328
+ ["xpath", `//*[contains(@content-desc, ${xpathLiteral(text)})]`],
329
+ ["-android uiautomator", `new UiSelector().textContains("${uiSelectorLiteral(text)}")`],
330
+ ];
331
+ }
332
+ function iosStrategies(text) {
333
+ const v = iosPredicateLiteral(text);
334
+ return [
335
+ ["-ios predicate string", `type == 'XCUIElementTypeButton' AND label == '${v}'`],
336
+ ["-ios predicate string", `type == 'XCUIElementTypeButton' AND name == '${v}'`],
337
+ ["accessibility id", text],
338
+ ["name", text],
339
+ ["-ios predicate string", `label == '${v}'`],
340
+ ["-ios predicate string", `name == '${v}'`],
341
+ ["-ios predicate string", `label CONTAINS '${v}'`],
342
+ ];
343
+ }
344
+ function humanizeDateTime(date) {
345
+ return date.toLocaleString("en-US", {
346
+ weekday: "long",
347
+ year: "numeric",
348
+ month: "long",
349
+ day: "numeric",
350
+ hour: "numeric",
351
+ minute: "2-digit",
352
+ hour12: true,
353
+ });
354
+ }
355
+ function fail(error) {
356
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
357
+ }
358
+ function readPngWidth(buffer) {
359
+ if (buffer.length < 24)
360
+ return 0;
361
+ try {
362
+ return buffer.readUInt32BE(16);
363
+ }
364
+ catch {
365
+ return 0;
366
+ }
367
+ }
@@ -0,0 +1 @@
1
+ export {};