@autifyhq/aximo-cli 0.4.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.
- package/README.md +98 -34
- package/dist/cli.js +1 -0
- package/dist/commands/device/device-config.d.ts +14 -0
- package/dist/commands/device/device-config.js +46 -0
- package/dist/commands/device/device-config.test.d.ts +1 -0
- package/dist/commands/device/device-config.test.js +56 -0
- package/dist/commands/device/doctor.d.ts +13 -0
- package/dist/commands/device/doctor.js +142 -25
- package/dist/commands/device/doctor.test.js +61 -1
- package/dist/commands/device/register.d.ts +2 -0
- package/dist/commands/device/register.js +50 -32
- package/dist/commands/device/register.test.d.ts +1 -0
- package/dist/commands/device/register.test.js +95 -0
- package/dist/commands/device/unregister.d.ts +5 -3
- package/dist/commands/device/unregister.js +32 -33
- package/dist/commands/device/unregister.test.d.ts +1 -0
- package/dist/commands/device/unregister.test.js +73 -0
- package/dist/commands/mobile/connect-core.d.ts +12 -0
- package/dist/commands/mobile/connect-core.js +74 -0
- package/dist/commands/mobile/connect.d.ts +9 -0
- package/dist/commands/mobile/connect.js +35 -0
- package/dist/commands/mobile/connect.test.d.ts +1 -0
- package/dist/commands/mobile/connect.test.js +87 -0
- package/dist/commands/mobile/doctor.d.ts +28 -0
- package/dist/commands/mobile/doctor.js +143 -0
- package/dist/commands/mobile/index.d.ts +2 -0
- package/dist/commands/mobile/index.js +15 -0
- package/dist/commands/mobile/list.d.ts +10 -0
- package/dist/commands/mobile/list.js +30 -0
- package/dist/commands/mobile/paths.d.ts +14 -0
- package/dist/commands/mobile/paths.js +71 -0
- package/dist/commands/mobile/register-core.d.ts +19 -0
- package/dist/commands/mobile/register-core.js +46 -0
- package/dist/commands/mobile/register.d.ts +32 -0
- package/dist/commands/mobile/register.js +51 -0
- package/dist/commands/mobile/register.test.d.ts +1 -0
- package/dist/commands/mobile/register.test.js +60 -0
- package/dist/commands/mobile/setup.d.ts +27 -0
- package/dist/commands/mobile/setup.js +100 -0
- package/dist/commands/mobile/setup.test.d.ts +1 -0
- package/dist/commands/mobile/setup.test.js +49 -0
- package/dist/commands/mobile/unregister.d.ts +14 -0
- package/dist/commands/mobile/unregister.js +37 -0
- package/dist/commands/schema.js +22 -0
- package/dist/commands/session/create.d.ts +5 -0
- package/dist/commands/session/create.js +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mobile-control/appium-controller.d.ts +39 -0
- package/dist/mobile-control/appium-controller.js +367 -0
- package/dist/mobile-control/appium-controller.test.d.ts +1 -0
- package/dist/mobile-control/appium-controller.test.js +252 -0
- package/dist/mobile-control/appium-import.d.ts +7 -0
- package/dist/mobile-control/appium-import.js +6 -0
- package/dist/mobile-control/appium-import.test.d.ts +1 -0
- package/dist/mobile-control/appium-import.test.js +9 -0
- package/dist/mobile-control/appium-server.d.ts +30 -0
- package/dist/mobile-control/appium-server.js +110 -0
- package/dist/mobile-control/appium-server.test.d.ts +1 -0
- package/dist/mobile-control/appium-server.test.js +108 -0
- package/dist/mobile-control/controller.d.ts +22 -0
- package/dist/mobile-control/controller.js +1 -0
- package/dist/mobile-control/devices.d.ts +21 -0
- package/dist/mobile-control/devices.js +99 -0
- package/dist/mobile-control/devices.test.d.ts +1 -0
- package/dist/mobile-control/devices.test.js +35 -0
- package/dist/mobile-control/exec.d.ts +6 -0
- package/dist/mobile-control/exec.js +13 -0
- package/dist/mobile-control/tunnel.d.ts +11 -0
- package/dist/mobile-control/tunnel.js +152 -0
- package/dist/mobile-control/tunnel.test.d.ts +1 -0
- package/dist/mobile-control/tunnel.test.js +155 -0
- package/dist/mobile-control/webdriver.d.ts +31 -0
- package/dist/mobile-control/webdriver.js +137 -0
- package/dist/mobile-control/webdriver.test.d.ts +1 -0
- package/dist/mobile-control/webdriver.test.js +45 -0
- package/dist/types.d.ts +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { platform } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { formatSuccess, printResult } from "../../output.js";
|
|
7
|
+
import { importAppium } from "../../mobile-control/appium-import.js";
|
|
8
|
+
import { getAppiumHome, resolveAndroidSdk } from "../../mobile-control/appium-server.js";
|
|
9
|
+
function which(cmd) {
|
|
10
|
+
const probe = platform() === "win32" ? "where" : "which";
|
|
11
|
+
const result = spawnSync(probe, [cmd], { encoding: "utf-8" });
|
|
12
|
+
return result.status === 0 ? (result.stdout || "").split("\n")[0]?.trim() || cmd : null;
|
|
13
|
+
}
|
|
14
|
+
function driverInstalled(driver) {
|
|
15
|
+
return existsSync(join(getAppiumHome(), "node_modules", `appium-${driver}-driver`));
|
|
16
|
+
}
|
|
17
|
+
function adbInstallHint() {
|
|
18
|
+
switch (platform()) {
|
|
19
|
+
case "darwin":
|
|
20
|
+
return "Install Android platform-tools (brew install --cask android-platform-tools).";
|
|
21
|
+
case "linux":
|
|
22
|
+
return "Install Android platform-tools (apt install android-tools-adb, or your distro's adb package).";
|
|
23
|
+
case "win32":
|
|
24
|
+
return "Install platform-tools via the Android Studio SDK Manager.";
|
|
25
|
+
default:
|
|
26
|
+
return "Install Android platform-tools (adb).";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function checkAppium() {
|
|
30
|
+
try {
|
|
31
|
+
const mod = await importAppium();
|
|
32
|
+
if (typeof mod.main === "function") {
|
|
33
|
+
return { name: "appium", status: "pass", message: "bundled Appium server is importable" };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
name: "appium",
|
|
37
|
+
status: "fail",
|
|
38
|
+
message: "appium is installed but exposes no programmatic `main`",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return {
|
|
43
|
+
name: "appium",
|
|
44
|
+
status: "fail",
|
|
45
|
+
message: err instanceof Error ? err.message : String(err),
|
|
46
|
+
hint: "Reinstall the CLI; the bundled Appium dependency appears to be missing.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function checkAndroid() {
|
|
51
|
+
const checks = [];
|
|
52
|
+
checks.push(driverInstalled("uiautomator2")
|
|
53
|
+
? { name: "uiautomator2 driver", status: "pass", message: "installed" }
|
|
54
|
+
: {
|
|
55
|
+
name: "uiautomator2 driver",
|
|
56
|
+
status: "warn",
|
|
57
|
+
message: "Appium UiAutomator2 driver not installed",
|
|
58
|
+
hint: "Run `aximo mobile setup` to install Appium drivers.",
|
|
59
|
+
});
|
|
60
|
+
const adb = which("adb");
|
|
61
|
+
checks.push(adb
|
|
62
|
+
? { name: "adb", status: "pass", message: adb }
|
|
63
|
+
: {
|
|
64
|
+
name: "adb",
|
|
65
|
+
status: "fail",
|
|
66
|
+
message: "adb not found in PATH",
|
|
67
|
+
hint: `${adbInstallHint()} Appium delegates to adb.`,
|
|
68
|
+
});
|
|
69
|
+
const sdk = resolveAndroidSdk();
|
|
70
|
+
checks.push(sdk
|
|
71
|
+
? { name: "android sdk", status: "pass", message: sdk }
|
|
72
|
+
: {
|
|
73
|
+
name: "android sdk",
|
|
74
|
+
status: "fail",
|
|
75
|
+
message: "Android SDK not found (ANDROID_HOME/ANDROID_SDK_ROOT unset)",
|
|
76
|
+
hint: "Set ANDROID_HOME to your Android SDK — Appium's UiAutomator2 driver requires it even though adb is on PATH — or install it via Android Studio.",
|
|
77
|
+
});
|
|
78
|
+
return checks;
|
|
79
|
+
}
|
|
80
|
+
function checkIos() {
|
|
81
|
+
if (platform() !== "darwin") {
|
|
82
|
+
return [
|
|
83
|
+
{ name: "ios", status: "info", message: "iOS device control is only available on macOS" },
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
const checks = [];
|
|
87
|
+
checks.push(driverInstalled("xcuitest")
|
|
88
|
+
? { name: "xcuitest driver", status: "pass", message: "installed" }
|
|
89
|
+
: {
|
|
90
|
+
name: "xcuitest driver",
|
|
91
|
+
status: "warn",
|
|
92
|
+
message: "Appium XCUITest driver not installed",
|
|
93
|
+
hint: "Run `aximo mobile setup` to install Appium drivers.",
|
|
94
|
+
});
|
|
95
|
+
const xcrun = which("xcrun");
|
|
96
|
+
checks.push(xcrun
|
|
97
|
+
? { name: "xcrun", status: "pass", message: xcrun }
|
|
98
|
+
: {
|
|
99
|
+
name: "xcrun",
|
|
100
|
+
status: "warn",
|
|
101
|
+
message: "xcrun not found",
|
|
102
|
+
hint: "Install Xcode command line tools: xcode-select --install. Appium's XCUITest driver delegates to Xcode.",
|
|
103
|
+
});
|
|
104
|
+
checks.push({
|
|
105
|
+
name: "ios webdriveragent",
|
|
106
|
+
status: "info",
|
|
107
|
+
message: "Physical iPhones are driven via Appium's XCUITest driver, which builds and installs a WebDriverAgent runner on the device.",
|
|
108
|
+
hint: "WDA needs an Apple Developer signing identity; enable Developer Mode (Settings → Privacy & Security) and trust the device.",
|
|
109
|
+
});
|
|
110
|
+
return checks;
|
|
111
|
+
}
|
|
112
|
+
export async function runMobileDoctor(options = {}) {
|
|
113
|
+
const checks = [await checkAppium()];
|
|
114
|
+
if (options.platform !== "ios") {
|
|
115
|
+
checks.push(...checkAndroid());
|
|
116
|
+
}
|
|
117
|
+
if (options.platform !== "android") {
|
|
118
|
+
checks.push(...checkIos());
|
|
119
|
+
}
|
|
120
|
+
const summary = { pass: 0, warn: 0, fail: 0, info: 0 };
|
|
121
|
+
for (const c of checks)
|
|
122
|
+
summary[c.status]++;
|
|
123
|
+
return formatSuccess({ checks, summary });
|
|
124
|
+
}
|
|
125
|
+
export default defineCommand({
|
|
126
|
+
meta: {
|
|
127
|
+
name: "doctor",
|
|
128
|
+
description: "Diagnose Appium prerequisites (bundled server, drivers, adb/Xcode delegates)",
|
|
129
|
+
},
|
|
130
|
+
args: {
|
|
131
|
+
platform: { type: "string", description: "Limit checks to ios | android" },
|
|
132
|
+
},
|
|
133
|
+
async run({ args }) {
|
|
134
|
+
const options = {};
|
|
135
|
+
if (args.platform === "ios" || args.platform === "android")
|
|
136
|
+
options.platform = args.platform;
|
|
137
|
+
const result = await runMobileDoctor(options);
|
|
138
|
+
printResult(result);
|
|
139
|
+
if (result.ok && result.data.summary.fail > 0) {
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
export default defineCommand({
|
|
3
|
+
meta: {
|
|
4
|
+
name: "mobile",
|
|
5
|
+
description: "Register and connect a phone to Aximo via a CLI-managed Appium server",
|
|
6
|
+
},
|
|
7
|
+
subCommands: {
|
|
8
|
+
list: () => import("./list.js").then((m) => m.default),
|
|
9
|
+
register: () => import("./register.js").then((m) => m.default),
|
|
10
|
+
unregister: () => import("./unregister.js").then((m) => m.default),
|
|
11
|
+
connect: () => import("./connect.js").then((m) => m.default),
|
|
12
|
+
setup: () => import("./setup.js").then((m) => m.default),
|
|
13
|
+
doctor: () => import("./doctor.js").then((m) => m.default),
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Envelope } from "../../types.js";
|
|
2
|
+
import { type AndroidDevice, type IosPhysicalDevice } from "../../mobile-control/devices.js";
|
|
3
|
+
import type { ExecFn } from "../../mobile-control/exec.js";
|
|
4
|
+
export interface MobileListResult {
|
|
5
|
+
androidDevices: AndroidDevice[];
|
|
6
|
+
iosDevices: IosPhysicalDevice[];
|
|
7
|
+
}
|
|
8
|
+
export declare function runMobileList(exec?: ExecFn): Promise<Envelope<MobileListResult>>;
|
|
9
|
+
declare const _default: import("citty").CommandDef<{}>;
|
|
10
|
+
export default _default;
|
|
@@ -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 {};
|