@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,30 @@
1
+ import { defineCommand } from "citty";
2
+ import { formatSuccess, printResult } from "../../output.js";
3
+ import { listAndroidDevices, listIosPhysicalDevices, } from "../../mobile-control/devices.js";
4
+ async function safe(fn, fallback) {
5
+ try {
6
+ return await fn();
7
+ }
8
+ catch {
9
+ return fallback;
10
+ }
11
+ }
12
+ export async function runMobileList(exec) {
13
+ const [allAndroid, iosDevices] = await Promise.all([
14
+ safe(() => listAndroidDevices(exec), []),
15
+ safe(() => listIosPhysicalDevices(exec), []),
16
+ ]);
17
+ // Physical phones only: drop emulator serials.
18
+ const androidDevices = allAndroid.filter((d) => !d.serial.startsWith("emulator-"));
19
+ return formatSuccess({ androidDevices, iosDevices });
20
+ }
21
+ export default defineCommand({
22
+ meta: {
23
+ name: "list",
24
+ description: "List physically-connected phones (Android via adb, iOS via idb)",
25
+ },
26
+ args: {},
27
+ async run() {
28
+ printResult(await runMobileList());
29
+ },
30
+ });
@@ -0,0 +1,14 @@
1
+ export type MobilePlatform = "ios" | "android";
2
+ export interface MobileDeviceConfig {
3
+ deviceId: string;
4
+ deviceToken: string;
5
+ name: string;
6
+ platform: MobilePlatform;
7
+ /** udid for iOS, adb serial for Android. */
8
+ target: string;
9
+ }
10
+ export declare function getMobileDevicesDir(): string;
11
+ export declare function getMobileDeviceConfigPath(deviceId: string): string;
12
+ export declare function writeMobileDeviceConfig(config: MobileDeviceConfig): string;
13
+ export declare function readMobileDeviceConfig(deviceId: string): MobileDeviceConfig | null;
14
+ export declare function listMobileDeviceConfigs(): MobileDeviceConfig[];
@@ -0,0 +1,71 @@
1
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAximoDir } from "../device/paths.js";
4
+ export function getMobileDevicesDir() {
5
+ return join(getAximoDir(), "mobile-devices");
6
+ }
7
+ export function getMobileDeviceConfigPath(deviceId) {
8
+ return join(getMobileDevicesDir(), `${deviceId}.json`);
9
+ }
10
+ export function writeMobileDeviceConfig(config) {
11
+ const dir = getMobileDevicesDir();
12
+ mkdirSync(dir, { recursive: true });
13
+ const path = getMobileDeviceConfigPath(config.deviceId);
14
+ writeFileSync(path, JSON.stringify(config, null, 2), { mode: 0o600 });
15
+ try {
16
+ chmodSync(path, 0o600);
17
+ }
18
+ catch {
19
+ // chmod may fail on Windows; file-level ACLs apply instead.
20
+ }
21
+ return path;
22
+ }
23
+ function parseConfig(raw) {
24
+ if (!raw || typeof raw !== "object")
25
+ return null;
26
+ const c = raw;
27
+ if (typeof c.deviceId !== "string" ||
28
+ typeof c.deviceToken !== "string" ||
29
+ typeof c.name !== "string" ||
30
+ (c.platform !== "ios" && c.platform !== "android") ||
31
+ typeof c.target !== "string") {
32
+ return null;
33
+ }
34
+ return {
35
+ deviceId: c.deviceId,
36
+ deviceToken: c.deviceToken,
37
+ name: c.name,
38
+ platform: c.platform,
39
+ target: c.target,
40
+ };
41
+ }
42
+ export function readMobileDeviceConfig(deviceId) {
43
+ const path = getMobileDeviceConfigPath(deviceId);
44
+ if (!existsSync(path))
45
+ return null;
46
+ try {
47
+ return parseConfig(JSON.parse(readFileSync(path, "utf-8")));
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ export function listMobileDeviceConfigs() {
54
+ const dir = getMobileDevicesDir();
55
+ if (!existsSync(dir))
56
+ return [];
57
+ const result = [];
58
+ for (const file of readdirSync(dir)) {
59
+ if (!file.endsWith(".json"))
60
+ continue;
61
+ try {
62
+ const parsed = parseConfig(JSON.parse(readFileSync(join(dir, file), "utf-8")));
63
+ if (parsed)
64
+ result.push(parsed);
65
+ }
66
+ catch {
67
+ // skip unreadable entries
68
+ }
69
+ }
70
+ return result;
71
+ }
@@ -0,0 +1,19 @@
1
+ import { type ClientDeps } from "../../client.js";
2
+ import type { Envelope } from "../../types.js";
3
+ import { type MobilePlatform } from "./paths.js";
4
+ export interface MobileRegisterResult {
5
+ deviceId: string;
6
+ name: string;
7
+ platform: MobilePlatform;
8
+ target: string;
9
+ configPath: string;
10
+ }
11
+ export interface MobileRegisterOptions {
12
+ name: string;
13
+ platform: MobilePlatform;
14
+ target: string;
15
+ os?: string;
16
+ }
17
+ /** Shared registration: POST /api/devices/register and persist the config. */
18
+ export declare function registerMobileDevice(options: MobileRegisterOptions, deps?: ClientDeps): Promise<Envelope<MobileRegisterResult>>;
19
+ export declare function parsePlatformArg(value: string | undefined): MobilePlatform | null;
@@ -0,0 +1,46 @@
1
+ import { request } from "../../client.js";
2
+ import { handleApiError } from "../../errors.js";
3
+ import { formatError, formatSuccess } from "../../output.js";
4
+ import { writeMobileDeviceConfig } from "./paths.js";
5
+ /** Shared registration: POST /api/devices/register and persist the config. */
6
+ export async function registerMobileDevice(options, deps) {
7
+ if (!options.name) {
8
+ return formatError("validation_error", "Device name is required (--name).", false);
9
+ }
10
+ if (!options.target) {
11
+ return formatError("validation_error", options.platform === "ios"
12
+ ? "Could not resolve a device target (--udid)."
13
+ : "Could not resolve a device target (--serial).", false);
14
+ }
15
+ try {
16
+ const body = {
17
+ name: options.name,
18
+ platform: options.platform,
19
+ };
20
+ if (options.os)
21
+ body["os"] = options.os;
22
+ const res = await request("/api/devices/register", { method: "POST", body }, deps ?? {});
23
+ const configPath = writeMobileDeviceConfig({
24
+ deviceId: res.deviceId,
25
+ deviceToken: res.deviceToken,
26
+ name: options.name,
27
+ platform: options.platform,
28
+ target: options.target,
29
+ });
30
+ return formatSuccess({
31
+ deviceId: res.deviceId,
32
+ name: options.name,
33
+ platform: options.platform,
34
+ target: options.target,
35
+ configPath,
36
+ });
37
+ }
38
+ catch (err) {
39
+ return handleApiError(err);
40
+ }
41
+ }
42
+ export function parsePlatformArg(value) {
43
+ if (value === "ios" || value === "android")
44
+ return value;
45
+ return null;
46
+ }
@@ -0,0 +1,32 @@
1
+ import { type ClientDeps } from "../../client.js";
2
+ import type { Envelope } from "../../types.js";
3
+ import type { ExecFn } from "../../mobile-control/exec.js";
4
+ import { type MobileRegisterResult } from "./register-core.js";
5
+ export interface MobileRegisterCmdOptions {
6
+ name: string;
7
+ platform: string;
8
+ serial?: string | undefined;
9
+ os?: string | undefined;
10
+ }
11
+ export declare function runMobileRegister(options: MobileRegisterCmdOptions, deps?: ClientDeps, exec?: ExecFn): Promise<Envelope<MobileRegisterResult>>;
12
+ declare const _default: import("citty").CommandDef<{
13
+ readonly name: {
14
+ readonly type: "string";
15
+ readonly description: "Device name";
16
+ readonly required: true;
17
+ };
18
+ readonly platform: {
19
+ readonly type: "string";
20
+ readonly description: "Device platform (only `android` is supported today)";
21
+ readonly default: "android";
22
+ };
23
+ readonly serial: {
24
+ readonly type: "string";
25
+ readonly description: "Android adb serial (defaults to the connected one)";
26
+ };
27
+ readonly os: {
28
+ readonly type: "string";
29
+ readonly description: "Optional OS version label";
30
+ };
31
+ }>;
32
+ export default _default;
@@ -0,0 +1,51 @@
1
+ import { defineCommand } from "citty";
2
+ import {} from "../../client.js";
3
+ import { formatError, printResult } from "../../output.js";
4
+ import { resolveAndroidSerial } from "../../mobile-control/devices.js";
5
+ import { parsePlatformArg, registerMobileDevice, } from "./register-core.js";
6
+ export async function runMobileRegister(options, deps, exec) {
7
+ const platform = parsePlatformArg(options.platform);
8
+ if (platform !== "android") {
9
+ return formatError("validation_error", "Only --platform android is supported (iOS local devices are coming in a future release).", false);
10
+ }
11
+ if (!options.name) {
12
+ return formatError("validation_error", "Device name is required (--name).", false);
13
+ }
14
+ let target;
15
+ try {
16
+ target = await resolveAndroidSerial(options.serial, exec);
17
+ }
18
+ catch (err) {
19
+ return formatError("validation_error", err instanceof Error ? err.message : String(err), false);
20
+ }
21
+ return registerMobileDevice({
22
+ name: options.name,
23
+ platform,
24
+ target,
25
+ ...(options.os ? { os: options.os } : {}),
26
+ }, deps);
27
+ }
28
+ export default defineCommand({
29
+ meta: {
30
+ name: "register",
31
+ description: "Register a physically-connected Android phone as an Aximo device",
32
+ },
33
+ args: {
34
+ name: { type: "string", description: "Device name", required: true },
35
+ platform: {
36
+ type: "string",
37
+ description: "Device platform (only `android` is supported today)",
38
+ default: "android",
39
+ },
40
+ serial: { type: "string", description: "Android adb serial (defaults to the connected one)" },
41
+ os: { type: "string", description: "Optional OS version label" },
42
+ },
43
+ async run({ args }) {
44
+ printResult(await runMobileRegister({
45
+ name: args.name,
46
+ platform: args.platform,
47
+ serial: args.serial,
48
+ os: args.os,
49
+ }));
50
+ },
51
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { server, http, HttpResponse, TEST_BASE_URL } from "../../test-utils.js";
6
+ import { runMobileRegister } from "./register.js";
7
+ let tempHome;
8
+ beforeAll(() => {
9
+ tempHome = mkdtempSync(join(tmpdir(), "aximo-mob-reg-"));
10
+ process.env["AXIMO_HOME"] = tempHome;
11
+ process.env["AXIMO_API_KEY"] = "test-key";
12
+ process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
13
+ server.listen();
14
+ });
15
+ afterAll(() => {
16
+ delete process.env["AXIMO_HOME"];
17
+ server.close();
18
+ rmSync(tempHome, { recursive: true, force: true });
19
+ });
20
+ beforeEach(() => {
21
+ rmSync(tempHome, { recursive: true, force: true });
22
+ mkdirSync(tempHome, { recursive: true });
23
+ });
24
+ afterEach(() => server.resetHandlers());
25
+ const oneAndroidDevice = async () => ({
26
+ stdout: Buffer.from("List of devices attached\nR58M12345 device model:Pixel_7\n"),
27
+ stderr: "",
28
+ });
29
+ describe("runMobileRegister", () => {
30
+ it("posts the resolved android serial and writes config", async () => {
31
+ let body;
32
+ server.use(http.post(`${TEST_BASE_URL}/api/devices/register`, async ({ request }) => {
33
+ body = (await request.json());
34
+ return HttpResponse.json({ deviceId: "dev-9", deviceToken: "tok-9" }, { status: 201 });
35
+ }));
36
+ const result = await runMobileRegister({ name: "My Pixel", platform: "android" }, undefined, oneAndroidDevice);
37
+ expect(result.ok).toBe(true);
38
+ if (!result.ok)
39
+ return;
40
+ expect(body).toMatchObject({
41
+ name: "My Pixel",
42
+ platform: "android",
43
+ });
44
+ expect(body).not.toHaveProperty("category");
45
+ expect(result.data.target).toBe("R58M12345");
46
+ const configPath = join(tempHome, "mobile-devices", "dev-9.json");
47
+ expect(existsSync(configPath)).toBe(true);
48
+ const saved = JSON.parse(readFileSync(configPath, "utf-8"));
49
+ expect(saved).toMatchObject({ platform: "android", target: "R58M12345" });
50
+ expect(saved).not.toHaveProperty("category");
51
+ });
52
+ it("rejects iOS registration as unsupported", async () => {
53
+ const result = await runMobileRegister({ name: "iPhone", platform: "ios" });
54
+ expect(result.ok).toBe(false);
55
+ if (!result.ok) {
56
+ expect(result.error.code).toBe("validation_error");
57
+ expect(result.error.message).toContain("Only --platform android is supported");
58
+ }
59
+ });
60
+ });
@@ -0,0 +1,27 @@
1
+ import type { Envelope } from "../../types.js";
2
+ import { type MobileDoctorReport } from "./doctor.js";
3
+ export interface DriverSetupResult {
4
+ driver: string;
5
+ alreadyInstalled: boolean;
6
+ }
7
+ export interface SetupResult {
8
+ appiumHome: string;
9
+ drivers: DriverSetupResult[];
10
+ }
11
+ export type DriverInstaller = (driver: string, appiumHome: string) => Promise<void>;
12
+ export interface RunMobileSetupOptions {
13
+ platform?: "ios" | "android" | undefined;
14
+ install?: DriverInstaller;
15
+ appiumHome?: string;
16
+ doctor?: (options: {
17
+ platform?: "ios" | "android" | undefined;
18
+ }) => Promise<Envelope<MobileDoctorReport>>;
19
+ }
20
+ export declare function runMobileSetup(options?: RunMobileSetupOptions): Promise<Envelope<SetupResult>>;
21
+ declare const _default: import("citty").CommandDef<{
22
+ readonly platform: {
23
+ readonly type: "string";
24
+ readonly description: "Limit to ios | android driver";
25
+ };
26
+ }>;
27
+ export default _default;
@@ -0,0 +1,100 @@
1
+ import { defineCommand } from "citty";
2
+ import { execFile } from "node:child_process";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { join } from "node:path";
6
+ import { formatError, formatSuccess, printResult } from "../../output.js";
7
+ import { getAppiumHome } from "../../mobile-control/appium-server.js";
8
+ import { runMobileDoctor } from "./doctor.js";
9
+ const DRIVER_PACKAGE = {
10
+ uiautomator2: "appium-uiautomator2-driver",
11
+ xcuitest: "appium-xcuitest-driver",
12
+ };
13
+ function resolveAppiumBin() {
14
+ const require = createRequire(import.meta.url);
15
+ return require.resolve("appium");
16
+ }
17
+ const defaultInstaller = (driver, appiumHome) => new Promise((resolve, reject) => {
18
+ execFile(process.execPath, [resolveAppiumBin(), "driver", "install", driver], { env: { ...process.env, APPIUM_HOME: appiumHome }, maxBuffer: 16 * 1024 * 1024 }, (error, _stdout, stderr) => {
19
+ if (error) {
20
+ reject(new Error(stderr?.toString().trim() || error.message));
21
+ return;
22
+ }
23
+ resolve();
24
+ });
25
+ });
26
+ function isInstalled(driver, appiumHome) {
27
+ const pkg = DRIVER_PACKAGE[driver];
28
+ return pkg ? existsSync(join(appiumHome, "node_modules", pkg)) : false;
29
+ }
30
+ // After installing drivers, tell the user what is still required to drive a real
31
+ // device (adb / Xcode / Developer Mode) rather than implying setup is complete.
32
+ function printNextSteps(checks) {
33
+ const blockers = checks.filter((c) => c.status === "fail" || c.status === "warn");
34
+ const notes = checks.filter((c) => c.status === "info");
35
+ if (blockers.length === 0) {
36
+ console.error("\nAll required prerequisites are satisfied.");
37
+ }
38
+ else {
39
+ console.error("\nDrivers installed, but you still need to set up:");
40
+ for (const c of blockers) {
41
+ console.error(` - ${c.message}`);
42
+ if (c.hint)
43
+ console.error(` ${c.hint}`);
44
+ }
45
+ }
46
+ for (const c of notes) {
47
+ console.error(`\nNote: ${c.message}`);
48
+ if (c.hint)
49
+ console.error(` ${c.hint}`);
50
+ }
51
+ console.error("\nNext: aximo mobile register --platform android -> aximo mobile connect (recheck: aximo mobile doctor)");
52
+ }
53
+ export async function runMobileSetup(options = {}) {
54
+ const appiumHome = options.appiumHome ?? getAppiumHome();
55
+ const install = options.install ?? defaultInstaller;
56
+ mkdirSync(appiumHome, { recursive: true });
57
+ const drivers = [];
58
+ if (options.platform !== "ios")
59
+ drivers.push("uiautomator2");
60
+ if (options.platform !== "android")
61
+ drivers.push("xcuitest");
62
+ const results = [];
63
+ for (const driver of drivers) {
64
+ if (isInstalled(driver, appiumHome)) {
65
+ console.error(`Appium driver "${driver}" already installed.`);
66
+ results.push({ driver, alreadyInstalled: true });
67
+ continue;
68
+ }
69
+ console.error(`Installing Appium driver "${driver}"...`);
70
+ try {
71
+ await install(driver, appiumHome);
72
+ }
73
+ catch (err) {
74
+ return formatError("api_error", `Failed to install Appium driver "${driver}": ${err instanceof Error ? err.message : String(err)}`, true);
75
+ }
76
+ console.error(`Installed Appium driver "${driver}".`);
77
+ results.push({ driver, alreadyInstalled: false });
78
+ }
79
+ // Surface remaining prerequisites as concise guidance; `doctor` owns the full report.
80
+ const runDoctor = options.doctor ?? runMobileDoctor;
81
+ const report = await runDoctor({ platform: options.platform });
82
+ printNextSteps(report.ok ? report.data.checks : []);
83
+ return formatSuccess({ appiumHome, drivers: results });
84
+ }
85
+ export default defineCommand({
86
+ meta: {
87
+ name: "setup",
88
+ description: "Install the Appium drivers used to drive registered phones",
89
+ },
90
+ args: {
91
+ platform: { type: "string", description: "Limit to ios | android driver" },
92
+ },
93
+ async run({ args }) {
94
+ const options = {};
95
+ if (args.platform === "ios" || args.platform === "android")
96
+ options.platform = args.platform;
97
+ const result = await runMobileSetup(options);
98
+ printResult(result);
99
+ },
100
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { runMobileSetup } from "./setup.js";
6
+ function fakeDoctor(checks) {
7
+ return async () => ({
8
+ ok: true,
9
+ data: { checks, summary: { pass: 0, warn: 0, fail: 0, info: 0 } },
10
+ });
11
+ }
12
+ describe("runMobileSetup", () => {
13
+ it("installs both drivers and runs the prerequisite check", async () => {
14
+ const appiumHome = mkdtempSync(join(tmpdir(), "aximo-setup-"));
15
+ const installed = [];
16
+ let doctorCalled = false;
17
+ const res = await runMobileSetup({
18
+ appiumHome,
19
+ install: async (driver) => {
20
+ installed.push(driver);
21
+ },
22
+ doctor: async () => {
23
+ doctorCalled = true;
24
+ return fakeDoctor([])();
25
+ },
26
+ });
27
+ expect(installed).toEqual(["uiautomator2", "xcuitest"]);
28
+ expect(doctorCalled).toBe(true);
29
+ expect(res.ok).toBe(true);
30
+ if (res.ok) {
31
+ expect(res.data.drivers.map((d) => d.driver)).toEqual(["uiautomator2", "xcuitest"]);
32
+ // checks are surfaced as guidance, not embedded in setup's JSON result
33
+ expect(res.data).not.toHaveProperty("checks");
34
+ }
35
+ });
36
+ it("limits to one driver with a platform filter", async () => {
37
+ const appiumHome = mkdtempSync(join(tmpdir(), "aximo-setup-"));
38
+ const installed = [];
39
+ await runMobileSetup({
40
+ appiumHome,
41
+ platform: "android",
42
+ install: async (driver) => {
43
+ installed.push(driver);
44
+ },
45
+ doctor: fakeDoctor([]),
46
+ });
47
+ expect(installed).toEqual(["uiautomator2"]);
48
+ });
49
+ });
@@ -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,27 @@ export function getSchema() {
119
120
  arg("available-only", "boolean", ""),
120
121
  ]),
121
122
  ]),
123
+ cmd("mobile", "Register and connect a physically-connected Android phone (via adb)", [], [
124
+ cmd("list", "List physically-connected phones (adb)"),
125
+ cmd("register", "Register a physically-connected Android phone as an Aximo device", [
126
+ arg("name", "string", "Device name", true),
127
+ arg("platform", "string", "Device platform (only `android` is supported today)"),
128
+ arg("serial", "string", "Android adb serial (defaults to the connected one)"),
129
+ arg("os", "string", "Optional OS version label"),
130
+ ]),
131
+ cmd("unregister", "Unregister a local mobile device", [
132
+ arg("device-id", "string", "Device ID (defaults to the sole registered local device)"),
133
+ ]),
134
+ cmd("connect", "Connect a registered Android phone via a CLI-managed Appium server", [
135
+ arg("device-id", "string", "Registered device id (required if multiple)"),
136
+ ]),
137
+ cmd("setup", "Install the Appium drivers used to drive registered phones", [
138
+ arg("platform", "string", "Limit checks to android"),
139
+ ]),
140
+ cmd("doctor", "Diagnose physical-device tooling (adb authorization, Appium setup)", [
141
+ arg("platform", "string", "Limit checks to android"),
142
+ ]),
143
+ ]),
122
144
  cmd("upload", "Upload commands", [], [
123
145
  cmd("create", "POST /api/uploads", [
124
146
  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
  }));
@@ -0,0 +1 @@
1
+ export {};