@autifyhq/aximo-cli 0.10.1 → 0.11.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 +39 -2
- package/dist/cli.js +14 -4
- package/dist/commands/mcp/server.test.js +4 -1
- package/dist/commands/mcp/tools.js +43 -0
- package/dist/commands/mcp/tools.test.js +104 -0
- package/dist/commands/mobile/doctor.d.ts +10 -0
- package/dist/commands/mobile/doctor.js +149 -1
- package/dist/commands/mobile/doctor.test.js +66 -1
- package/dist/desktop-server/agent-sync.d.ts +21 -1
- package/dist/desktop-server/agent-sync.js +6 -2
- package/dist/desktop-server/bundle-sync.d.ts +25 -0
- package/dist/desktop-server/bundle-sync.js +128 -0
- package/dist/desktop-server/bundle-sync.test.d.ts +1 -0
- package/dist/desktop-server/bundle-sync.test.js +189 -0
- package/dist/desktop-server/cli-version.d.ts +21 -0
- package/dist/desktop-server/cli-version.js +41 -0
- package/dist/desktop-server/cli-version.test.d.ts +1 -0
- package/dist/desktop-server/cli-version.test.js +58 -0
- package/dist/desktop-server/offline.d.ts +6 -0
- package/dist/desktop-server/offline.js +44 -0
- package/dist/desktop-server/offline.test.d.ts +1 -0
- package/dist/desktop-server/offline.test.js +74 -0
- package/dist/desktop-server/setup-deps.test.d.ts +1 -0
- package/dist/desktop-server/setup-deps.test.js +112 -0
- package/dist/desktop-server/setup-python.test.d.ts +1 -0
- package/dist/desktop-server/setup-python.test.js +272 -0
- package/dist/desktop-server/setup.d.ts +26 -1
- package/dist/desktop-server/setup.js +136 -29
- package/dist/mobile-control/android-recovery.d.ts +5 -0
- package/dist/mobile-control/android-recovery.js +19 -0
- package/dist/mobile-control/android-recovery.test.d.ts +1 -0
- package/dist/mobile-control/android-recovery.test.js +33 -0
- package/dist/mobile-control/appium-controller.d.ts +4 -0
- package/dist/mobile-control/appium-controller.js +15 -1
- package/dist/mobile-control/appium-controller.test.js +78 -0
- package/dist/mobile-control/appium-import.js +19 -3
- package/dist/types.d.ts +49 -0
- package/package.json +4 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { execBinary } from "./exec.js";
|
|
2
|
+
// Helper APKs left by an older driver version can wedge instrumentation: the
|
|
3
|
+
// packages are installed but Android refuses to register the test runner, so
|
|
4
|
+
// every session fails with one of these until the stale APKs are removed.
|
|
5
|
+
const WEDGED_INSTRUMENTATION_PATTERN = /instrumentation process cannot be initialized|Unable to find instrumentation info|INSTRUMENTATION_FAILED/i;
|
|
6
|
+
export function isWedgedInstrumentationError(err) {
|
|
7
|
+
return err instanceof Error && WEDGED_INSTRUMENTATION_PATTERN.test(err.message);
|
|
8
|
+
}
|
|
9
|
+
export const APPIUM_HELPER_PACKAGES = [
|
|
10
|
+
"io.appium.uiautomator2.server.test",
|
|
11
|
+
"io.appium.uiautomator2.server",
|
|
12
|
+
"io.appium.settings",
|
|
13
|
+
];
|
|
14
|
+
/** Remove stale Appium helper APKs (best-effort) so the driver reinstalls fresh ones. */
|
|
15
|
+
export async function uninstallAppiumHelpers(serial, exec = execBinary) {
|
|
16
|
+
for (const pkg of APPIUM_HELPER_PACKAGES) {
|
|
17
|
+
await exec("adb", ["-s", serial, "uninstall", pkg]).catch(() => { });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { APPIUM_HELPER_PACKAGES, isWedgedInstrumentationError, uninstallAppiumHelpers, } from "./android-recovery.js";
|
|
3
|
+
describe("isWedgedInstrumentationError", () => {
|
|
4
|
+
it("matches the driver's wedged-instrumentation signatures", () => {
|
|
5
|
+
expect(isWedgedInstrumentationError(new Error("WebDriver POST /session failed: unknown error: The instrumentation process cannot be initialized."))).toBe(true);
|
|
6
|
+
expect(isWedgedInstrumentationError(new Error("Unable to find instrumentation info for: ComponentInfo{io.appium.uiautomator2.server.test/androidx.test.runner.AndroidJUnitRunner}"))).toBe(true);
|
|
7
|
+
expect(isWedgedInstrumentationError(new Error("INSTRUMENTATION_FAILED: io.appium.uiautomator2.server.test/..."))).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it("does not match unrelated session errors or non-errors", () => {
|
|
10
|
+
expect(isWedgedInstrumentationError(new Error("Could not find a connected Android device"))).toBe(false);
|
|
11
|
+
expect(isWedgedInstrumentationError("instrumentation process cannot be initialized")).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe("uninstallAppiumHelpers", () => {
|
|
15
|
+
it("uninstalls all helper packages for the given serial", async () => {
|
|
16
|
+
const calls = [];
|
|
17
|
+
const exec = async (_file, args) => {
|
|
18
|
+
calls.push(args);
|
|
19
|
+
return { stdout: Buffer.from("Success"), stderr: "" };
|
|
20
|
+
};
|
|
21
|
+
await uninstallAppiumHelpers("serial-1", exec);
|
|
22
|
+
expect(calls).toEqual(APPIUM_HELPER_PACKAGES.map((pkg) => ["-s", "serial-1", "uninstall", pkg]));
|
|
23
|
+
});
|
|
24
|
+
it("keeps going when an uninstall fails (package not installed)", async () => {
|
|
25
|
+
const calls = [];
|
|
26
|
+
const exec = async (_file, args) => {
|
|
27
|
+
calls.push(args);
|
|
28
|
+
throw new Error("Failure [DELETE_FAILED_INTERNAL_ERROR]");
|
|
29
|
+
};
|
|
30
|
+
await uninstallAppiumHelpers("serial-1", exec);
|
|
31
|
+
expect(calls).toHaveLength(APPIUM_HELPER_PACKAGES.length);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionResult, MobileController } from "./controller.js";
|
|
2
|
+
import type { ExecFn } from "./exec.js";
|
|
2
3
|
import { type IosSigningConfig } from "./ios-signing.js";
|
|
3
4
|
import { type WebDriverTransport } from "./webdriver.js";
|
|
4
5
|
export interface AppiumControllerOptions {
|
|
@@ -19,6 +20,8 @@ export interface AppiumControllerOptions {
|
|
|
19
20
|
*/
|
|
20
21
|
mjpegServerPort?: number | (() => Promise<number>);
|
|
21
22
|
transport?: WebDriverTransport;
|
|
23
|
+
/** adb runner override for tests (used by the wedged-instrumentation repair). */
|
|
24
|
+
exec?: ExecFn;
|
|
22
25
|
}
|
|
23
26
|
export declare class AppiumController implements MobileController {
|
|
24
27
|
private readonly session;
|
|
@@ -27,6 +30,7 @@ export declare class AppiumController implements MobileController {
|
|
|
27
30
|
private readonly iosSigning;
|
|
28
31
|
private readonly wdaLocalPort;
|
|
29
32
|
private readonly mjpegServerPort;
|
|
33
|
+
private readonly exec;
|
|
30
34
|
private resolvedWdaPort;
|
|
31
35
|
private resolvedMjpegPort;
|
|
32
36
|
private sessionReady;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isWedgedInstrumentationError, uninstallAppiumHelpers } from "./android-recovery.js";
|
|
1
2
|
import { enrichIosSessionError } from "./ios-session-error.js";
|
|
2
3
|
import { iosSigningCapabilities } from "./ios-signing.js";
|
|
3
4
|
import { fetchTransport, WebDriverSession } from "./webdriver.js";
|
|
@@ -38,6 +39,7 @@ export class AppiumController {
|
|
|
38
39
|
iosSigning;
|
|
39
40
|
wdaLocalPort;
|
|
40
41
|
mjpegServerPort;
|
|
42
|
+
exec;
|
|
41
43
|
resolvedWdaPort;
|
|
42
44
|
resolvedMjpegPort;
|
|
43
45
|
sessionReady = null;
|
|
@@ -48,6 +50,7 @@ export class AppiumController {
|
|
|
48
50
|
this.iosSigning = opts.iosSigning;
|
|
49
51
|
this.wdaLocalPort = opts.wdaLocalPort;
|
|
50
52
|
this.mjpegServerPort = opts.mjpegServerPort;
|
|
53
|
+
this.exec = opts.exec;
|
|
51
54
|
const transport = opts.transport ?? fetchTransport(opts.endpoint);
|
|
52
55
|
this.session = new WebDriverSession(transport);
|
|
53
56
|
}
|
|
@@ -90,7 +93,18 @@ export class AppiumController {
|
|
|
90
93
|
typeof this.mjpegServerPort === "function"
|
|
91
94
|
? await this.mjpegServerPort()
|
|
92
95
|
: this.mjpegServerPort;
|
|
93
|
-
|
|
96
|
+
try {
|
|
97
|
+
await this.session.createSession(this.capabilities());
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// Stale helper APKs from an older driver wedge instrumentation until
|
|
101
|
+
// they are removed; wipe them and retry once so the driver reinstalls.
|
|
102
|
+
if (this.platform !== "android" || !isWedgedInstrumentationError(err))
|
|
103
|
+
throw err;
|
|
104
|
+
console.error("Stale Appium helper apps detected on the phone — removing them and retrying once.");
|
|
105
|
+
await uninstallAppiumHelpers(this.target, this.exec);
|
|
106
|
+
await this.session.createSession(this.capabilities());
|
|
107
|
+
}
|
|
94
108
|
await this.applyMjpegSettings();
|
|
95
109
|
})().catch(async (err) => {
|
|
96
110
|
this.resetSession();
|
|
@@ -95,6 +95,84 @@ function lastPointerActions(calls) {
|
|
|
95
95
|
const groups = body["actions"];
|
|
96
96
|
return groups[0]?.actions ?? [];
|
|
97
97
|
}
|
|
98
|
+
describe("AppiumController wedged-instrumentation recovery", () => {
|
|
99
|
+
function wedgedTransport(failures) {
|
|
100
|
+
const calls = [];
|
|
101
|
+
const transport = {
|
|
102
|
+
async request(method, path, body) {
|
|
103
|
+
calls.push({ method, path, body });
|
|
104
|
+
if (method === "POST" && path === "/session") {
|
|
105
|
+
const failure = failures.shift();
|
|
106
|
+
if (failure)
|
|
107
|
+
throw new Error(failure);
|
|
108
|
+
return { sessionId: "sess-1" };
|
|
109
|
+
}
|
|
110
|
+
if (path.endsWith("/screenshot"))
|
|
111
|
+
return pngWithWidth(4);
|
|
112
|
+
return {};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
return { transport, calls };
|
|
116
|
+
}
|
|
117
|
+
const WEDGED = "WebDriver POST /session failed: The instrumentation process cannot be initialized.";
|
|
118
|
+
it("uninstalls the helper APKs and retries once when instrumentation is wedged", async () => {
|
|
119
|
+
const adbCalls = [];
|
|
120
|
+
const exec = async (_file, args) => {
|
|
121
|
+
adbCalls.push(args);
|
|
122
|
+
return { stdout: Buffer.from(""), stderr: "" };
|
|
123
|
+
};
|
|
124
|
+
const { transport, calls } = wedgedTransport([WEDGED]);
|
|
125
|
+
const controller = new AppiumController({
|
|
126
|
+
endpoint: "http://x",
|
|
127
|
+
platform: "android",
|
|
128
|
+
target: "udid-1",
|
|
129
|
+
transport,
|
|
130
|
+
exec,
|
|
131
|
+
});
|
|
132
|
+
await controller.screenshot();
|
|
133
|
+
expect(calls.filter((c) => c.path === "/session" && c.method === "POST")).toHaveLength(2);
|
|
134
|
+
expect(adbCalls.map((a) => a.at(-1))).toEqual([
|
|
135
|
+
"io.appium.uiautomator2.server.test",
|
|
136
|
+
"io.appium.uiautomator2.server",
|
|
137
|
+
"io.appium.settings",
|
|
138
|
+
]);
|
|
139
|
+
expect(adbCalls.every((a) => a[0] === "-s" && a[1] === "udid-1")).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
it("does not attempt recovery for unrelated session errors", async () => {
|
|
142
|
+
const adbCalls = [];
|
|
143
|
+
const exec = async (_file, args) => {
|
|
144
|
+
adbCalls.push(args);
|
|
145
|
+
return { stdout: Buffer.from(""), stderr: "" };
|
|
146
|
+
};
|
|
147
|
+
const { transport } = wedgedTransport(["Could not find a connected Android device"]);
|
|
148
|
+
const controller = new AppiumController({
|
|
149
|
+
endpoint: "http://x",
|
|
150
|
+
platform: "android",
|
|
151
|
+
target: "udid-1",
|
|
152
|
+
transport,
|
|
153
|
+
exec,
|
|
154
|
+
});
|
|
155
|
+
await expect(controller.screenshot()).rejects.toThrow("connected Android device");
|
|
156
|
+
expect(adbCalls).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
it("does not attempt adb recovery on iOS", async () => {
|
|
159
|
+
const adbCalls = [];
|
|
160
|
+
const exec = async (_file, args) => {
|
|
161
|
+
adbCalls.push(args);
|
|
162
|
+
return { stdout: Buffer.from(""), stderr: "" };
|
|
163
|
+
};
|
|
164
|
+
const { transport } = wedgedTransport([WEDGED]);
|
|
165
|
+
const controller = new AppiumController({
|
|
166
|
+
endpoint: "http://x",
|
|
167
|
+
platform: "ios",
|
|
168
|
+
target: "udid-1",
|
|
169
|
+
transport,
|
|
170
|
+
exec,
|
|
171
|
+
});
|
|
172
|
+
await expect(controller.screenshot()).rejects.toThrow();
|
|
173
|
+
expect(adbCalls).toHaveLength(0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
98
176
|
describe("AppiumController session capabilities", () => {
|
|
99
177
|
it("sends Android UiAutomator2 caps", async () => {
|
|
100
178
|
const { controller, calls } = makeController("android");
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
const APPIUM = "appium";
|
|
2
2
|
export async function importAppium() {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
try {
|
|
4
|
+
// Appium 3 is CJS, so its `main` is under `.default` when imported from ESM.
|
|
5
|
+
const mod = (await import(APPIUM));
|
|
6
|
+
return mod.default ?? mod;
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
// appium is left out of the single-file build — it pulls in native code the
|
|
10
|
+
// executable cannot carry — but `aximo mobile` still appears in --help.
|
|
11
|
+
//
|
|
12
|
+
// Both codes are needed: a CJS require reports MODULE_NOT_FOUND, while the
|
|
13
|
+
// dynamic ESM import used here reports ERR_MODULE_NOT_FOUND. The raw message
|
|
14
|
+
// also names the bundle's build-time path, which means nothing to a customer.
|
|
15
|
+
const code = err?.code;
|
|
16
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
|
|
17
|
+
throw new Error("Mobile commands are unavailable in the single-file aximo build. " +
|
|
18
|
+
"Install the CLI from npm to use them: npm i -g @autifyhq/aximo-cli");
|
|
19
|
+
}
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
6
22
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -109,6 +109,55 @@ export interface PlanRun {
|
|
|
109
109
|
status?: string | undefined;
|
|
110
110
|
[key: string]: unknown;
|
|
111
111
|
}
|
|
112
|
+
export interface PlanListItem {
|
|
113
|
+
id: string;
|
|
114
|
+
projectId: string;
|
|
115
|
+
title: string;
|
|
116
|
+
description: string | null;
|
|
117
|
+
apiEnabled: boolean;
|
|
118
|
+
caseCount: number;
|
|
119
|
+
hasMobileCases: boolean;
|
|
120
|
+
createdAt: string;
|
|
121
|
+
updatedAt: string;
|
|
122
|
+
createdBy: {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string | null;
|
|
125
|
+
email: string;
|
|
126
|
+
} | null;
|
|
127
|
+
}
|
|
128
|
+
export interface PlanDetail {
|
|
129
|
+
id: string;
|
|
130
|
+
projectId: string;
|
|
131
|
+
title: string;
|
|
132
|
+
description: string | null;
|
|
133
|
+
apiEnabled: boolean;
|
|
134
|
+
executionMode: ExecutionMode;
|
|
135
|
+
stopOnFailure: boolean;
|
|
136
|
+
variables: {
|
|
137
|
+
name: string;
|
|
138
|
+
value: string;
|
|
139
|
+
isSecret: boolean;
|
|
140
|
+
}[];
|
|
141
|
+
caseCount: number;
|
|
142
|
+
createdAt: string;
|
|
143
|
+
updatedAt: string;
|
|
144
|
+
createdBy: {
|
|
145
|
+
id: string;
|
|
146
|
+
name: string | null;
|
|
147
|
+
email: string;
|
|
148
|
+
} | null;
|
|
149
|
+
project: {
|
|
150
|
+
id: string;
|
|
151
|
+
name: string;
|
|
152
|
+
} | null;
|
|
153
|
+
cases: {
|
|
154
|
+
id: string;
|
|
155
|
+
order: number;
|
|
156
|
+
type: string;
|
|
157
|
+
title: string | null;
|
|
158
|
+
lastStatus: string | null;
|
|
159
|
+
}[];
|
|
160
|
+
}
|
|
112
161
|
export interface PlanRunSummary {
|
|
113
162
|
id: string;
|
|
114
163
|
status: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autifyhq/aximo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Autify Aximo Command Line Interface (CLI)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"autify",
|
|
@@ -52,11 +52,13 @@
|
|
|
52
52
|
"@types/node": "^25.6.0",
|
|
53
53
|
"@types/ws": "^8.18.0",
|
|
54
54
|
"@vitest/coverage-v8": "4.1.4",
|
|
55
|
+
"esbuild": "0.27.7",
|
|
55
56
|
"husky": "9.1.7",
|
|
56
57
|
"msw": "2.13.2",
|
|
57
58
|
"nano-staged": "1.0.2",
|
|
58
59
|
"oxfmt": "0.45.0",
|
|
59
60
|
"oxlint": "1.60.0",
|
|
61
|
+
"postject": "1.0.0-alpha.6",
|
|
60
62
|
"secretlint": "11.7.1",
|
|
61
63
|
"tsx": "4.21.0",
|
|
62
64
|
"typescript": "6.0.2",
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
},
|
|
68
70
|
"scripts": {
|
|
69
71
|
"build": "tsc",
|
|
72
|
+
"build:sea": "node scripts/build-sea.mjs",
|
|
70
73
|
"cli": "tsx src/cli.ts",
|
|
71
74
|
"format": "oxfmt --write .",
|
|
72
75
|
"format:check": "oxfmt --check .",
|