@autifyhq/aximo-cli 0.5.0 → 0.7.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 +196 -64
- package/dist/cli.js +4 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +33 -0
- package/dist/commands/mobile/connect-core.d.ts +2 -1
- package/dist/commands/mobile/connect-core.js +127 -15
- package/dist/commands/mobile/connect.js +3 -1
- package/dist/commands/mobile/doctor.d.ts +3 -0
- package/dist/commands/mobile/doctor.js +55 -6
- package/dist/commands/mobile/doctor.test.d.ts +1 -0
- package/dist/commands/mobile/doctor.test.js +14 -0
- package/dist/commands/mobile/index.js +1 -0
- package/dist/commands/mobile/install.d.ts +50 -0
- package/dist/commands/mobile/install.js +107 -0
- package/dist/commands/mobile/install.test.d.ts +1 -0
- package/dist/commands/mobile/install.test.js +142 -0
- package/dist/commands/mobile/list.d.ts +2 -0
- package/dist/commands/mobile/list.js +17 -11
- package/dist/commands/mobile/register.d.ts +6 -1
- package/dist/commands/mobile/register.js +12 -6
- package/dist/commands/mobile/register.test.js +34 -3
- package/dist/commands/mobile/unregister.js +11 -6
- package/dist/commands/schema.js +15 -8
- package/dist/mobile-control/app-install.d.ts +34 -0
- package/dist/mobile-control/app-install.js +55 -0
- package/dist/mobile-control/app-install.test.d.ts +1 -0
- package/dist/mobile-control/app-install.test.js +113 -0
- package/dist/mobile-control/appium-controller.d.ts +11 -0
- package/dist/mobile-control/appium-controller.js +24 -4
- package/dist/mobile-control/bundle-id-map.d.ts +9 -0
- package/dist/mobile-control/bundle-id-map.js +27 -0
- package/dist/mobile-control/devices.d.ts +8 -3
- package/dist/mobile-control/devices.js +57 -5
- package/dist/mobile-control/devices.test.js +41 -1
- package/dist/mobile-control/env-flags.d.ts +5 -0
- package/dist/mobile-control/env-flags.js +10 -0
- package/dist/mobile-control/env-flags.test.d.ts +1 -0
- package/dist/mobile-control/env-flags.test.js +21 -0
- package/dist/mobile-control/ios-auto-resign.d.ts +45 -0
- package/dist/mobile-control/ios-auto-resign.js +108 -0
- package/dist/mobile-control/ios-auto-resign.test.d.ts +1 -0
- package/dist/mobile-control/ios-auto-resign.test.js +150 -0
- package/dist/mobile-control/ios-devicectl.d.ts +39 -0
- package/dist/mobile-control/ios-devicectl.js +87 -0
- package/dist/mobile-control/ios-devicectl.test.d.ts +1 -0
- package/dist/mobile-control/ios-devicectl.test.js +110 -0
- package/dist/mobile-control/ios-identity.d.ts +27 -0
- package/dist/mobile-control/ios-identity.js +49 -0
- package/dist/mobile-control/ios-identity.test.d.ts +1 -0
- package/dist/mobile-control/ios-identity.test.js +102 -0
- package/dist/mobile-control/ios-mint.d.ts +39 -0
- package/dist/mobile-control/ios-mint.js +78 -0
- package/dist/mobile-control/ios-mint.test.d.ts +1 -0
- package/dist/mobile-control/ios-mint.test.js +94 -0
- package/dist/mobile-control/ios-profiles.d.ts +46 -0
- package/dist/mobile-control/ios-profiles.js +106 -0
- package/dist/mobile-control/ios-profiles.test.d.ts +1 -0
- package/dist/mobile-control/ios-profiles.test.js +154 -0
- package/dist/mobile-control/ios-resign.d.ts +37 -0
- package/dist/mobile-control/ios-resign.js +101 -0
- package/dist/mobile-control/ios-resign.test.d.ts +1 -0
- package/dist/mobile-control/ios-resign.test.js +84 -0
- package/dist/mobile-control/ios-session-error.d.ts +10 -0
- package/dist/mobile-control/ios-session-error.js +34 -0
- package/dist/mobile-control/ios-session-error.test.d.ts +1 -0
- package/dist/mobile-control/ios-session-error.test.js +30 -0
- package/dist/mobile-control/ios-signing.d.ts +56 -0
- package/dist/mobile-control/ios-signing.js +94 -0
- package/dist/mobile-control/ios-signing.test.d.ts +1 -0
- package/dist/mobile-control/ios-signing.test.js +160 -0
- package/dist/mobile-control/ios-tunnel.d.ts +54 -0
- package/dist/mobile-control/ios-tunnel.js +130 -0
- package/dist/mobile-control/ios-tunnel.test.d.ts +1 -0
- package/dist/mobile-control/ios-tunnel.test.js +115 -0
- package/dist/mobile-control/ios-wda-cache.d.ts +39 -0
- package/dist/mobile-control/ios-wda-cache.js +157 -0
- package/dist/mobile-control/ios-wda-cache.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-cache.test.js +128 -0
- package/dist/mobile-control/ios-wda-diagnostics.d.ts +14 -0
- package/dist/mobile-control/ios-wda-diagnostics.js +138 -0
- package/dist/mobile-control/ios-wda-diagnostics.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-diagnostics.test.js +77 -0
- package/dist/mobile-control/ios-wda-preinstall.d.ts +64 -0
- package/dist/mobile-control/ios-wda-preinstall.js +162 -0
- package/dist/mobile-control/ios-wda-preinstall.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-preinstall.test.js +107 -0
- package/dist/mobile-control/mobile-errors.d.ts +12 -0
- package/dist/mobile-control/mobile-errors.js +91 -0
- package/dist/mobile-control/mobile-errors.test.d.ts +1 -0
- package/dist/mobile-control/mobile-errors.test.js +48 -0
- package/dist/mobile-control/tunnel.d.ts +5 -1
- package/dist/mobile-control/tunnel.js +60 -3
- package/dist/mobile-control/tunnel.test.js +1 -1
- package/dist/proxy.d.ts +11 -0
- package/dist/proxy.js +85 -0
- package/dist/proxy.test.d.ts +1 -0
- package/dist/proxy.test.js +72 -0
- package/package.json +4 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mobileErrorHint, withMobileHint } from "./mobile-errors.js";
|
|
3
|
+
describe("mobileErrorHint (iOS)", () => {
|
|
4
|
+
it("guides trusting the developer cert", () => {
|
|
5
|
+
expect(mobileErrorHint("...its profile has not been explicitly trusted by the user", "ios")).toContain("VPN & Device Management");
|
|
6
|
+
});
|
|
7
|
+
it("guides setting a unique WDA bundle id when the id is taken", () => {
|
|
8
|
+
expect(mobileErrorHint('The app identifier "com.facebook.WebDriverAgentRunner.xctrunner" cannot be registered to your development team', "ios")).toContain("AXIMO_IOS_WDA_BUNDLE_ID");
|
|
9
|
+
});
|
|
10
|
+
it("guides installing the iOS platform components", () => {
|
|
11
|
+
expect(mobileErrorHint("error: iOS 26.5 is not installed. Please download", "ios")).toContain("downloadPlatform");
|
|
12
|
+
});
|
|
13
|
+
it("explains that a sideloaded .ipa must be device-signed", () => {
|
|
14
|
+
expect(mobileErrorHint("Last event ApplicationVerificationFailed", "ios")).toContain("signed for THIS device");
|
|
15
|
+
});
|
|
16
|
+
it("falls back to a general WDA hint for a bare xcodebuild failure", () => {
|
|
17
|
+
expect(mobileErrorHint("xcodebuild failed with code 65", "ios")).toContain("aximo mobile doctor");
|
|
18
|
+
});
|
|
19
|
+
it("matches a refused connection on a dynamic wdaLocalPort", () => {
|
|
20
|
+
expect(mobileErrorHint("Connection refused to port 51234", "ios")).toContain("device tunnel");
|
|
21
|
+
});
|
|
22
|
+
it("matches the driver's RemoteXPC tunnel errors", () => {
|
|
23
|
+
expect(mobileErrorHint("RemoteXPC tunnel is not available for this session", "ios")).toContain("device tunnel");
|
|
24
|
+
});
|
|
25
|
+
it("hints unlocking for a locked device but not for 'unlocked' messages", () => {
|
|
26
|
+
expect(mobileErrorHint("The device 'iPhone' is locked", "ios")).toContain("Unlock");
|
|
27
|
+
expect(mobileErrorHint("the device was not, or could not be, unlocked", "ios")).toContain("Unlock");
|
|
28
|
+
expect(mobileErrorHint("the device screen is unlocked and ready", "ios")).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
it("returns undefined for an unrecognized message", () => {
|
|
31
|
+
expect(mobileErrorHint("some unrelated failure", "ios")).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("mobileErrorHint (Android)", () => {
|
|
35
|
+
it("guides uninstalling on a signature mismatch", () => {
|
|
36
|
+
expect(mobileErrorHint("Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE]", "android")).toContain("adb uninstall");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("withMobileHint", () => {
|
|
40
|
+
it("appends the hint to the message when one matches", () => {
|
|
41
|
+
const out = withMobileHint("Failure [INSTALL_FAILED_INSUFFICIENT_STORAGE]", "android");
|
|
42
|
+
expect(out).toContain("Failure [INSTALL_FAILED_INSUFFICIENT_STORAGE] — ");
|
|
43
|
+
expect(out).toContain("out of storage");
|
|
44
|
+
});
|
|
45
|
+
it("returns the message unchanged when nothing matches", () => {
|
|
46
|
+
expect(withMobileHint("weird error", "ios")).toBe("weird error");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -5,7 +5,11 @@ export interface RunControlTunnelOptions {
|
|
|
5
5
|
deviceToken: string;
|
|
6
6
|
deviceName: string;
|
|
7
7
|
controller: MobileController;
|
|
8
|
+
/** Installs the app under test on the device (fetch by upload id -> adb/idb install). */
|
|
9
|
+
installApp?: (args: {
|
|
10
|
+
uploadId: string;
|
|
11
|
+
}) => Promise<void>;
|
|
8
12
|
connect?: (wsUrl: string, deviceToken: string) => Socket;
|
|
9
13
|
}
|
|
10
|
-
export declare function registerHandlers(socket: Socket, controller: MobileController): void;
|
|
14
|
+
export declare function registerHandlers(socket: Socket, controller: MobileController, installApp?: RunControlTunnelOptions["installApp"]): void;
|
|
11
15
|
export declare function runControlTunnel(options: RunControlTunnelOptions): Promise<void>;
|
|
@@ -49,7 +49,7 @@ function defaultConnect(wsUrl, deviceToken) {
|
|
|
49
49
|
timeout: 10000,
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
-
export function registerHandlers(socket, controller) {
|
|
52
|
+
export function registerHandlers(socket, controller, installApp) {
|
|
53
53
|
const on = (event, handler) => {
|
|
54
54
|
socket.on(event, (rawArgs, ack) => {
|
|
55
55
|
const args = (rawArgs ?? {});
|
|
@@ -100,6 +100,15 @@ export function registerHandlers(socket, controller) {
|
|
|
100
100
|
on("input", async (args) => asActionResult(await controller.typeText(String(args["text"] ?? ""))));
|
|
101
101
|
on("key", async (args) => asActionResult(await controller.pressKey(String(args["key"] ?? ""))));
|
|
102
102
|
on("launch_app", async (args) => asActionResult(await controller.launchApp(String(args["app"] ?? ""))));
|
|
103
|
+
if (installApp) {
|
|
104
|
+
on("install_app", async (args) => {
|
|
105
|
+
const uploadId = String(args["uploadId"] ?? "");
|
|
106
|
+
if (!uploadId)
|
|
107
|
+
return { ok: false, error: "install_app requires an uploadId" };
|
|
108
|
+
await installApp({ uploadId });
|
|
109
|
+
return ok();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
103
112
|
on("find_element", async (args) => asActionResult(await controller.findElement(String(args["text"] ?? ""))));
|
|
104
113
|
on("clear_text", async () => asActionResult(await controller.clearText()));
|
|
105
114
|
on("hide_keyboard", async () => asActionResult(await controller.hideKeyboard()));
|
|
@@ -107,9 +116,10 @@ export function registerHandlers(socket, controller) {
|
|
|
107
116
|
export async function runControlTunnel(options) {
|
|
108
117
|
const connect = options.connect ?? defaultConnect;
|
|
109
118
|
const socket = connect(options.wsUrl, options.deviceToken);
|
|
110
|
-
registerHandlers(socket, options.controller);
|
|
119
|
+
registerHandlers(socket, options.controller, options.installApp);
|
|
111
120
|
await new Promise((resolve, reject) => {
|
|
112
121
|
let shuttingDown = false;
|
|
122
|
+
let serverKicks = [];
|
|
113
123
|
const cleanup = () => {
|
|
114
124
|
if (shuttingDown)
|
|
115
125
|
return;
|
|
@@ -117,12 +127,27 @@ export async function runControlTunnel(options) {
|
|
|
117
127
|
socket.close();
|
|
118
128
|
resolve();
|
|
119
129
|
};
|
|
130
|
+
// Handshake rejections that re-trying can never fix (bad/revoked token,
|
|
131
|
+
// feature disabled). Everything else is treated as transient.
|
|
132
|
+
const isTerminalAuthError = (message) => /device token|not enabled for this organization/i.test(message);
|
|
120
133
|
socket.on("connect", () => {
|
|
121
134
|
console.log(`Connected to Aximo as "${options.deviceName}" (${options.wsUrl}${SOCKET_PATH})`);
|
|
135
|
+
// Once a session has been established, outlive server deploys/restarts:
|
|
136
|
+
// retry forever instead of giving up after the initial-connect budget.
|
|
137
|
+
socket.io.opts.reconnectionAttempts = Infinity;
|
|
122
138
|
});
|
|
123
139
|
socket.on("connect_error", (err) => {
|
|
140
|
+
if (isTerminalAuthError(err.message)) {
|
|
141
|
+
// Mark as shutting down BEFORE close(): close emits a client-initiated
|
|
142
|
+
// "disconnect" that would otherwise double-handle this terminal error.
|
|
143
|
+
shuttingDown = true;
|
|
144
|
+
socket.close();
|
|
145
|
+
reject(new Error(`Connection rejected: ${err.message}. Re-register the device with \`aximo mobile register\`.`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
124
148
|
// socket.active is false once reconnection is given up; reject so the CLI exits non-zero.
|
|
125
149
|
if (!socket.active) {
|
|
150
|
+
shuttingDown = true;
|
|
126
151
|
socket.close();
|
|
127
152
|
reject(new Error(`Connection error: ${err.message}`));
|
|
128
153
|
return;
|
|
@@ -130,8 +155,34 @@ export async function runControlTunnel(options) {
|
|
|
130
155
|
console.error(`Connection error: ${err.message}`);
|
|
131
156
|
});
|
|
132
157
|
socket.on("disconnect", (reason) => {
|
|
158
|
+
// A SIGINT/SIGTERM teardown closes the socket itself; cleanup() already
|
|
159
|
+
// resolved the promise, so don't report our own close as an error.
|
|
160
|
+
if (shuttingDown)
|
|
161
|
+
return;
|
|
162
|
+
// A server-initiated disconnect (gateway deploy/restart kicking the socket)
|
|
163
|
+
// does NOT auto-reconnect per socket.io semantics — reconnect explicitly so
|
|
164
|
+
// a routine deploy doesn't strand the device offline. But rapid repeated
|
|
165
|
+
// kicks mean another `connect` for this device keeps displacing us (the
|
|
166
|
+
// gateway kicks the old socket on every re-registration): back off and exit
|
|
167
|
+
// instead of fighting it for the slot.
|
|
168
|
+
if (reason === "io server disconnect") {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
serverKicks = serverKicks.filter((t) => now - t < 30_000);
|
|
171
|
+
serverKicks.push(now);
|
|
172
|
+
if (serverKicks.length >= 3) {
|
|
173
|
+
shuttingDown = true;
|
|
174
|
+
socket.close();
|
|
175
|
+
reject(new Error("Disconnected repeatedly by the server — another `aximo mobile connect` " +
|
|
176
|
+
"is likely running for this device. Stop the other instance first."));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
console.log("Server closed the connection (restart/deploy); reconnecting...");
|
|
180
|
+
socket.connect();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
133
183
|
// socket.active is false when the disconnect is terminal (the client won't reconnect).
|
|
134
184
|
if (!socket.active) {
|
|
185
|
+
shuttingDown = true;
|
|
135
186
|
socket.close();
|
|
136
187
|
reject(new Error(`Disconnected: ${reason}`));
|
|
137
188
|
return;
|
|
@@ -142,7 +193,13 @@ export async function runControlTunnel(options) {
|
|
|
142
193
|
console.log("Server is shutting down; awaiting reconnect...");
|
|
143
194
|
});
|
|
144
195
|
process.on("SIGINT", () => {
|
|
145
|
-
|
|
196
|
+
// Second Ctrl+C: teardown is hanging (e.g. Appium closing a session
|
|
197
|
+
// mid-build) — force-quit instead of becoming un-interruptible.
|
|
198
|
+
if (shuttingDown) {
|
|
199
|
+
console.error("Force exit.");
|
|
200
|
+
process.exit(130);
|
|
201
|
+
}
|
|
202
|
+
console.log("\nDisconnecting... (press Ctrl+C again to force quit)");
|
|
146
203
|
cleanup();
|
|
147
204
|
});
|
|
148
205
|
process.on("SIGTERM", () => {
|
|
@@ -127,7 +127,7 @@ describe("registerHandlers", () => {
|
|
|
127
127
|
const result = (await socket.emit("datetime", {}));
|
|
128
128
|
expect(typeof result.datetime).toBe("string");
|
|
129
129
|
expect(result.datetime.length).toBeGreaterThan(0);
|
|
130
|
-
expect(result.datetime).not.
|
|
130
|
+
expect(result.datetime).not.toMatch(/\dT\d/); // humanized, not raw ISO (e.g. 2026-06-30T14:41)
|
|
131
131
|
expect(typeof result.timezone).toBe("string");
|
|
132
132
|
expect(result.timezone.length).toBeGreaterThan(0);
|
|
133
133
|
});
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** True for loopback, RFC1918, link-local, or IPv6 ULA hosts — never proxied. */
|
|
2
|
+
export declare function isPrivateHost(host: string): boolean;
|
|
3
|
+
/** Merge any existing NO_PROXY with the hosts we always force direct. */
|
|
4
|
+
export declare function resolveNoProxy(env: NodeJS.ProcessEnv, host: string | null): string;
|
|
5
|
+
/**
|
|
6
|
+
* Route the CLI's global fetch() through any configured HTTP(S) proxy. No-op
|
|
7
|
+
* when no proxy env var is set. Writes the resolved NO_PROXY back onto the
|
|
8
|
+
* environment so the pip subprocess and the spawned Python agent inherit the
|
|
9
|
+
* same bypass rules. Returns whether a proxy was installed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function installProxySupport(env?: NodeJS.ProcessEnv): Promise<boolean>;
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getBaseUrl } from "./client.js";
|
|
2
|
+
// Make every fetch() the CLI issues honor HTTP_PROXY / HTTPS_PROXY / NO_PROXY,
|
|
3
|
+
// the same way npm, curl and pip do. Node's global fetch (undici) ignores these
|
|
4
|
+
// by default, so the embedded-Python download and the API calls would bypass a
|
|
5
|
+
// corporate proxy and fail on a locked-down on-prem network.
|
|
6
|
+
//
|
|
7
|
+
// Two hosts are always forced direct, even when a proxy is set:
|
|
8
|
+
// - loopback: the local Appium / WebDriver servers the mobile commands drive.
|
|
9
|
+
// - a private-range Aximo server: an on-prem box (e.g. 172.30.x.x) is reached
|
|
10
|
+
// directly, and an internet proxy usually cannot route to an internal IP —
|
|
11
|
+
// proxying it would break a link that currently works. NO_PROXY can't
|
|
12
|
+
// express CIDRs reliably, so we match the base-URL host against the private
|
|
13
|
+
// ranges ourselves and force it direct when it's an internal IP literal.
|
|
14
|
+
const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
15
|
+
/** True for loopback, RFC1918, link-local, or IPv6 ULA hosts — never proxied. */
|
|
16
|
+
export function isPrivateHost(host) {
|
|
17
|
+
const h = host.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
18
|
+
if (LOOPBACK_HOSTS.includes(h))
|
|
19
|
+
return true;
|
|
20
|
+
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
21
|
+
if (v4) {
|
|
22
|
+
const a = Number(v4[1]);
|
|
23
|
+
const b = Number(v4[2]);
|
|
24
|
+
if (a === 10 || a === 127)
|
|
25
|
+
return true;
|
|
26
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
27
|
+
return true;
|
|
28
|
+
if (a === 192 && b === 168)
|
|
29
|
+
return true;
|
|
30
|
+
if (a === 169 && b === 254)
|
|
31
|
+
return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// IPv6 ULA / link-local — only for IPv6 literals (which contain a colon), so
|
|
35
|
+
// ordinary DNS names like "fd.example.com" or "fc00.example.com" stay public.
|
|
36
|
+
if (h.includes(":")) {
|
|
37
|
+
if (/^fe[89ab]/.test(h))
|
|
38
|
+
return true; // fe80::/10 link-local
|
|
39
|
+
if (/^f[cd]/.test(h))
|
|
40
|
+
return true; // fc00::/7 unique-local
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
function hasProxyEnv(env) {
|
|
45
|
+
return Boolean(env["HTTP_PROXY"] || env["http_proxy"] || env["HTTPS_PROXY"] || env["https_proxy"]);
|
|
46
|
+
}
|
|
47
|
+
function serverHost() {
|
|
48
|
+
try {
|
|
49
|
+
return new URL(getBaseUrl()).hostname || null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Merge any existing NO_PROXY with the hosts we always force direct. */
|
|
56
|
+
export function resolveNoProxy(env, host) {
|
|
57
|
+
const parts = (env["NO_PROXY"] ?? env["no_proxy"] ?? "")
|
|
58
|
+
.split(",")
|
|
59
|
+
.map((s) => s.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
const direct = [...LOOPBACK_HOSTS];
|
|
62
|
+
if (host && isPrivateHost(host))
|
|
63
|
+
direct.push(host);
|
|
64
|
+
for (const d of direct) {
|
|
65
|
+
if (!parts.includes(d))
|
|
66
|
+
parts.push(d);
|
|
67
|
+
}
|
|
68
|
+
return parts.join(",");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Route the CLI's global fetch() through any configured HTTP(S) proxy. No-op
|
|
72
|
+
* when no proxy env var is set. Writes the resolved NO_PROXY back onto the
|
|
73
|
+
* environment so the pip subprocess and the spawned Python agent inherit the
|
|
74
|
+
* same bypass rules. Returns whether a proxy was installed.
|
|
75
|
+
*/
|
|
76
|
+
export async function installProxySupport(env = process.env) {
|
|
77
|
+
if (!hasProxyEnv(env))
|
|
78
|
+
return false;
|
|
79
|
+
const noProxy = resolveNoProxy(env, serverHost());
|
|
80
|
+
env["NO_PROXY"] = noProxy;
|
|
81
|
+
env["no_proxy"] = noProxy;
|
|
82
|
+
const { EnvHttpProxyAgent, setGlobalDispatcher } = await import("undici");
|
|
83
|
+
setGlobalDispatcher(new EnvHttpProxyAgent({ noProxy }));
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { getGlobalDispatcher, setGlobalDispatcher } from "undici";
|
|
3
|
+
import { isPrivateHost, installProxySupport, resolveNoProxy } from "./proxy.js";
|
|
4
|
+
describe("isPrivateHost", () => {
|
|
5
|
+
it("treats loopback as private", () => {
|
|
6
|
+
expect(isPrivateHost("localhost")).toBe(true);
|
|
7
|
+
expect(isPrivateHost("127.0.0.1")).toBe(true);
|
|
8
|
+
expect(isPrivateHost("::1")).toBe(true);
|
|
9
|
+
expect(isPrivateHost("[::1]")).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it("treats RFC1918 / link-local IPv4 as private", () => {
|
|
12
|
+
expect(isPrivateHost("10.1.2.3")).toBe(true);
|
|
13
|
+
expect(isPrivateHost("172.30.193.138")).toBe(true); // the on-prem repro
|
|
14
|
+
expect(isPrivateHost("192.168.0.5")).toBe(true);
|
|
15
|
+
expect(isPrivateHost("169.254.10.10")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it("treats 172.x outside 16-31 as public", () => {
|
|
18
|
+
expect(isPrivateHost("172.15.0.1")).toBe(false);
|
|
19
|
+
expect(isPrivateHost("172.32.0.1")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it("treats IPv6 ULA / link-local as private", () => {
|
|
22
|
+
expect(isPrivateHost("fd00::1")).toBe(true);
|
|
23
|
+
expect(isPrivateHost("fe80::1")).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it("treats public hosts as not private", () => {
|
|
26
|
+
expect(isPrivateHost("aximo.autify.com")).toBe(false);
|
|
27
|
+
expect(isPrivateHost("8.8.8.8")).toBe(false);
|
|
28
|
+
expect(isPrivateHost("2606:4700::1")).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("treats DNS names that start like IPv6 ULA/link-local as public", () => {
|
|
31
|
+
expect(isPrivateHost("fd.example.com")).toBe(false);
|
|
32
|
+
expect(isPrivateHost("fc00.example.com")).toBe(false);
|
|
33
|
+
expect(isPrivateHost("fe80.example.com")).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("resolveNoProxy", () => {
|
|
37
|
+
it("always forces loopback direct", () => {
|
|
38
|
+
expect(resolveNoProxy({}, null).split(",")).toEqual(["localhost", "127.0.0.1", "::1"]);
|
|
39
|
+
});
|
|
40
|
+
it("adds a private server host so the on-prem link stays direct", () => {
|
|
41
|
+
const result = resolveNoProxy({}, "172.30.193.138").split(",");
|
|
42
|
+
expect(result).toContain("172.30.193.138");
|
|
43
|
+
});
|
|
44
|
+
it("leaves a public server host to the proxy", () => {
|
|
45
|
+
expect(resolveNoProxy({}, "aximo.autify.com")).not.toContain("aximo.autify.com");
|
|
46
|
+
});
|
|
47
|
+
it("preserves an existing NO_PROXY and dedupes", () => {
|
|
48
|
+
const result = resolveNoProxy({ NO_PROXY: "example.com, localhost" }, "10.0.0.1");
|
|
49
|
+
const parts = result.split(",");
|
|
50
|
+
expect(parts).toContain("example.com");
|
|
51
|
+
expect(parts).toContain("10.0.0.1");
|
|
52
|
+
expect(parts.filter((p) => p === "localhost")).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("installProxySupport", () => {
|
|
56
|
+
// installProxySupport sets a global undici dispatcher; restore it so the
|
|
57
|
+
// proxy agent doesn't leak into other tests' fetch() calls.
|
|
58
|
+
const original = getGlobalDispatcher();
|
|
59
|
+
afterEach(() => setGlobalDispatcher(original));
|
|
60
|
+
it("is a no-op when no proxy env is set", async () => {
|
|
61
|
+
const env = {};
|
|
62
|
+
expect(await installProxySupport(env)).toBe(false);
|
|
63
|
+
expect(env["NO_PROXY"]).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
it("installs a dispatcher and writes NO_PROXY when a proxy is set", async () => {
|
|
66
|
+
const env = { HTTPS_PROXY: "http://proxy.local:8080" };
|
|
67
|
+
expect(await installProxySupport(env)).toBe(true);
|
|
68
|
+
const noProxy = env["NO_PROXY"] ?? "";
|
|
69
|
+
expect(noProxy.split(",")).toContain("127.0.0.1");
|
|
70
|
+
expect(env["no_proxy"]).toBe(noProxy);
|
|
71
|
+
});
|
|
72
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autifyhq/aximo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Autify Aximo Command Line Interface (CLI)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"autify",
|
|
@@ -38,8 +38,10 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
40
|
"appium": "^3.5.2",
|
|
41
|
+
"applesign": "^5.0.1",
|
|
41
42
|
"citty": "0.2.2",
|
|
42
43
|
"socket.io-client": "^4.8.1",
|
|
44
|
+
"undici": "^7.28.0",
|
|
43
45
|
"ws": "^8.18.0",
|
|
44
46
|
"zod": "^3.25.76"
|
|
45
47
|
},
|
|
@@ -61,7 +63,7 @@
|
|
|
61
63
|
"vitest": "4.1.4"
|
|
62
64
|
},
|
|
63
65
|
"engines": {
|
|
64
|
-
"node": ">=20"
|
|
66
|
+
"node": ">=20.18.1"
|
|
65
67
|
},
|
|
66
68
|
"scripts": {
|
|
67
69
|
"build": "tsc",
|