@autifyhq/aximo-cli 0.6.0 → 0.8.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 +88 -70
- package/dist/client.js +16 -1
- package/dist/commands/mobile/connect-core.d.ts +2 -1
- package/dist/commands/mobile/connect-core.js +112 -9
- package/dist/commands/mobile/connect.js +6 -3
- package/dist/commands/mobile/connect.test.js +7 -1
- package/dist/commands/mobile/doctor.d.ts +4 -0
- package/dist/commands/mobile/doctor.js +62 -16
- package/dist/commands/mobile/doctor.test.d.ts +1 -0
- package/dist/commands/mobile/doctor.test.js +28 -0
- package/dist/commands/mobile/install.d.ts +10 -2
- package/dist/commands/mobile/install.js +57 -15
- package/dist/commands/mobile/install.test.js +44 -6
- package/dist/commands/mobile/list.d.ts +3 -0
- package/dist/commands/mobile/list.js +40 -14
- package/dist/commands/mobile/list.test.d.ts +1 -0
- package/dist/commands/mobile/list.test.js +24 -0
- package/dist/commands/mobile/output.d.ts +13 -0
- package/dist/commands/mobile/output.js +21 -0
- package/dist/commands/mobile/output.test.d.ts +1 -0
- package/dist/commands/mobile/output.test.js +54 -0
- package/dist/commands/mobile/register.d.ts +1 -0
- package/dist/commands/mobile/register.js +11 -3
- package/dist/commands/mobile/setup.d.ts +1 -0
- package/dist/commands/mobile/setup.js +7 -2
- package/dist/commands/mobile/unregister.d.ts +7 -3
- package/dist/commands/mobile/unregister.js +20 -9
- package/dist/mobile-control/app-install.d.ts +18 -5
- package/dist/mobile-control/app-install.js +33 -6
- package/dist/mobile-control/app-install.test.js +42 -7
- package/dist/mobile-control/appium-controller.d.ts +8 -0
- package/dist/mobile-control/appium-controller.js +19 -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 +6 -3
- package/dist/mobile-control/devices.js +26 -4
- 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/exec.d.ts +5 -1
- package/dist/mobile-control/exec.js +2 -2
- 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 +33 -5
- package/dist/mobile-control/ios-signing.js +56 -0
- package/dist/mobile-control/ios-signing.test.js +87 -0
- package/dist/mobile-control/ios-tunnel.d.ts +57 -0
- package/dist/mobile-control/ios-tunnel.js +150 -0
- package/dist/mobile-control/ios-tunnel.test.d.ts +1 -0
- package/dist/mobile-control/ios-tunnel.test.js +120 -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 +165 -0
- package/dist/mobile-control/ios-wda-preinstall.test.d.ts +1 -0
- package/dist/mobile-control/ios-wda-preinstall.test.js +111 -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.js +79 -2
- package/dist/mobile-control/tunnel.test.js +82 -0
- package/package.json +2 -1
|
@@ -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
|
+
});
|
|
@@ -49,10 +49,43 @@ function defaultConnect(wsUrl, deviceToken) {
|
|
|
49
49
|
timeout: 10000,
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
// Mirrors the gateway's lock lease: a session quiet for longer than this has lost
|
|
53
|
+
// its server-side lock, so a new session id is a legitimate takeover, not a stray.
|
|
54
|
+
const SESSION_LEASE_MS = 25 * 60 * 1000;
|
|
52
55
|
export function registerHandlers(socket, controller, installApp) {
|
|
56
|
+
// Device-side guard against interleaved sessions: the gateway announces owner
|
|
57
|
+
// changes (session_acquired/session_released) and tags each command with its
|
|
58
|
+
// session id, so commands from a session that no longer owns the device are
|
|
59
|
+
// dropped even if a gateway instance lost its in-memory lock.
|
|
60
|
+
let activeSession = null;
|
|
61
|
+
socket.on("session_acquired", (args) => {
|
|
62
|
+
const sessionId = args?.sessionId;
|
|
63
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
64
|
+
activeSession = { id: sessionId, lastSeenAt: Date.now() };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
socket.on("session_released", (args) => {
|
|
68
|
+
// Only the owner's release clears the guard; a takeover by a new session is
|
|
69
|
+
// announced via session_acquired, so an unmatched release is ignored.
|
|
70
|
+
const sessionId = args?.sessionId;
|
|
71
|
+
if (activeSession && activeSession.id === sessionId) {
|
|
72
|
+
activeSession = null;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
53
75
|
const on = (event, handler) => {
|
|
54
76
|
socket.on(event, (rawArgs, ack) => {
|
|
55
|
-
|
|
77
|
+
// Destructure the routing-only tag off a clone so the caller's payload
|
|
78
|
+
// object is never mutated.
|
|
79
|
+
const { __sessionId: sessionId, ...args } = (rawArgs ?? {});
|
|
80
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
81
|
+
if (activeSession &&
|
|
82
|
+
activeSession.id !== sessionId &&
|
|
83
|
+
Date.now() - activeSession.lastSeenAt < SESSION_LEASE_MS) {
|
|
84
|
+
ack?.(fail(new Error("Device is busy with another session")));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
activeSession = { id: sessionId, lastSeenAt: Date.now() };
|
|
88
|
+
}
|
|
56
89
|
void handler(args)
|
|
57
90
|
.then((result) => ack?.(result))
|
|
58
91
|
.catch((err) => ack?.(fail(err)));
|
|
@@ -119,6 +152,7 @@ export async function runControlTunnel(options) {
|
|
|
119
152
|
registerHandlers(socket, options.controller, options.installApp);
|
|
120
153
|
await new Promise((resolve, reject) => {
|
|
121
154
|
let shuttingDown = false;
|
|
155
|
+
let serverKicks = [];
|
|
122
156
|
const cleanup = () => {
|
|
123
157
|
if (shuttingDown)
|
|
124
158
|
return;
|
|
@@ -126,12 +160,27 @@ export async function runControlTunnel(options) {
|
|
|
126
160
|
socket.close();
|
|
127
161
|
resolve();
|
|
128
162
|
};
|
|
163
|
+
// Handshake rejections that re-trying can never fix (bad/revoked token,
|
|
164
|
+
// feature disabled). Everything else is treated as transient.
|
|
165
|
+
const isTerminalAuthError = (message) => /device token|not enabled for this organization/i.test(message);
|
|
129
166
|
socket.on("connect", () => {
|
|
130
167
|
console.log(`Connected to Aximo as "${options.deviceName}" (${options.wsUrl}${SOCKET_PATH})`);
|
|
168
|
+
// Once a session has been established, outlive server deploys/restarts:
|
|
169
|
+
// retry forever instead of giving up after the initial-connect budget.
|
|
170
|
+
socket.io.opts.reconnectionAttempts = Infinity;
|
|
131
171
|
});
|
|
132
172
|
socket.on("connect_error", (err) => {
|
|
173
|
+
if (isTerminalAuthError(err.message)) {
|
|
174
|
+
// Mark as shutting down BEFORE close(): close emits a client-initiated
|
|
175
|
+
// "disconnect" that would otherwise double-handle this terminal error.
|
|
176
|
+
shuttingDown = true;
|
|
177
|
+
socket.close();
|
|
178
|
+
reject(new Error(`Connection rejected: ${err.message}. Re-register the device with \`aximo mobile register\`.`));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
133
181
|
// socket.active is false once reconnection is given up; reject so the CLI exits non-zero.
|
|
134
182
|
if (!socket.active) {
|
|
183
|
+
shuttingDown = true;
|
|
135
184
|
socket.close();
|
|
136
185
|
reject(new Error(`Connection error: ${err.message}`));
|
|
137
186
|
return;
|
|
@@ -143,8 +192,30 @@ export async function runControlTunnel(options) {
|
|
|
143
192
|
// resolved the promise, so don't report our own close as an error.
|
|
144
193
|
if (shuttingDown)
|
|
145
194
|
return;
|
|
195
|
+
// A server-initiated disconnect (gateway deploy/restart kicking the socket)
|
|
196
|
+
// does NOT auto-reconnect per socket.io semantics — reconnect explicitly so
|
|
197
|
+
// a routine deploy doesn't strand the device offline. But rapid repeated
|
|
198
|
+
// kicks mean another `connect` for this device keeps displacing us (the
|
|
199
|
+
// gateway kicks the old socket on every re-registration): back off and exit
|
|
200
|
+
// instead of fighting it for the slot.
|
|
201
|
+
if (reason === "io server disconnect") {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
serverKicks = serverKicks.filter((t) => now - t < 30_000);
|
|
204
|
+
serverKicks.push(now);
|
|
205
|
+
if (serverKicks.length >= 3) {
|
|
206
|
+
shuttingDown = true;
|
|
207
|
+
socket.close();
|
|
208
|
+
reject(new Error("Disconnected repeatedly by the server — another `aximo mobile connect` " +
|
|
209
|
+
"is likely running for this device. Stop the other instance first."));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
console.log("Server closed the connection (restart/deploy); reconnecting...");
|
|
213
|
+
socket.connect();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
146
216
|
// socket.active is false when the disconnect is terminal (the client won't reconnect).
|
|
147
217
|
if (!socket.active) {
|
|
218
|
+
shuttingDown = true;
|
|
148
219
|
socket.close();
|
|
149
220
|
reject(new Error(`Disconnected: ${reason}`));
|
|
150
221
|
return;
|
|
@@ -155,7 +226,13 @@ export async function runControlTunnel(options) {
|
|
|
155
226
|
console.log("Server is shutting down; awaiting reconnect...");
|
|
156
227
|
});
|
|
157
228
|
process.on("SIGINT", () => {
|
|
158
|
-
|
|
229
|
+
// Second Ctrl+C: teardown is hanging (e.g. Appium closing a session
|
|
230
|
+
// mid-build) — force-quit instead of becoming un-interruptible.
|
|
231
|
+
if (shuttingDown) {
|
|
232
|
+
console.error("Force exit.");
|
|
233
|
+
process.exit(130);
|
|
234
|
+
}
|
|
235
|
+
console.log("\nDisconnecting... (press Ctrl+C again to force quit)");
|
|
159
236
|
cleanup();
|
|
160
237
|
});
|
|
161
238
|
process.on("SIGTERM", () => {
|
|
@@ -153,3 +153,85 @@ describe("registerHandlers", () => {
|
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
|
+
describe("registerHandlers session guard", () => {
|
|
157
|
+
const BUSY = { ok: false, error: "Device is busy with another session" };
|
|
158
|
+
function fire(socket, event, args) {
|
|
159
|
+
socket.handlers.get(event)?.(args);
|
|
160
|
+
}
|
|
161
|
+
it("strips __sessionId for the handler without mutating the incoming payload", async () => {
|
|
162
|
+
const tap = vi.fn(async () => { });
|
|
163
|
+
const socket = wire(stubController({ tap }));
|
|
164
|
+
const payload = { x: 5, y: 9, __sessionId: "s1" };
|
|
165
|
+
await expect(socket.emit("tap", payload)).resolves.toEqual({ ok: true });
|
|
166
|
+
expect(tap).toHaveBeenCalledWith(5, 9);
|
|
167
|
+
expect(payload.__sessionId).toBe("s1");
|
|
168
|
+
});
|
|
169
|
+
it("rejects a second session's command while the first is active", async () => {
|
|
170
|
+
const tap = vi.fn(async () => { });
|
|
171
|
+
const socket = wire(stubController({ tap }));
|
|
172
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
173
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual(BUSY);
|
|
174
|
+
expect(tap).toHaveBeenCalledTimes(1);
|
|
175
|
+
});
|
|
176
|
+
it("accepts a new session after session_released for the active one", async () => {
|
|
177
|
+
const tap = vi.fn(async () => { });
|
|
178
|
+
const socket = wire(stubController({ tap }));
|
|
179
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
180
|
+
fire(socket, "session_released", { sessionId: "s1" });
|
|
181
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual({
|
|
182
|
+
ok: true,
|
|
183
|
+
});
|
|
184
|
+
expect(tap).toHaveBeenCalledTimes(2);
|
|
185
|
+
});
|
|
186
|
+
it("ignores session_released for a session that is not active", async () => {
|
|
187
|
+
const tap = vi.fn(async () => { });
|
|
188
|
+
const socket = wire(stubController({ tap }));
|
|
189
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
190
|
+
fire(socket, "session_released", { sessionId: "other" });
|
|
191
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual(BUSY);
|
|
192
|
+
expect(tap).toHaveBeenCalledTimes(1);
|
|
193
|
+
});
|
|
194
|
+
it("ignores session_released without a sessionId (malformed release)", async () => {
|
|
195
|
+
const tap = vi.fn(async () => { });
|
|
196
|
+
const socket = wire(stubController({ tap }));
|
|
197
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
198
|
+
fire(socket, "session_released", {});
|
|
199
|
+
fire(socket, "session_released", { sessionId: null });
|
|
200
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual(BUSY);
|
|
201
|
+
expect(tap).toHaveBeenCalledTimes(1);
|
|
202
|
+
});
|
|
203
|
+
it("switches owner on session_acquired and rejects the superseded session", async () => {
|
|
204
|
+
const tap = vi.fn(async () => { });
|
|
205
|
+
const socket = wire(stubController({ tap }));
|
|
206
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
207
|
+
fire(socket, "session_acquired", { sessionId: "s2" });
|
|
208
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s1" })).resolves.toEqual(BUSY);
|
|
209
|
+
await expect(socket.emit("tap", { x: 5, y: 6, __sessionId: "s2" })).resolves.toEqual({
|
|
210
|
+
ok: true,
|
|
211
|
+
});
|
|
212
|
+
expect(tap).toHaveBeenCalledTimes(2);
|
|
213
|
+
});
|
|
214
|
+
it("allows takeover once the active session is idle past the lease", async () => {
|
|
215
|
+
vi.useFakeTimers();
|
|
216
|
+
try {
|
|
217
|
+
const tap = vi.fn(async () => { });
|
|
218
|
+
const socket = wire(stubController({ tap }));
|
|
219
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
220
|
+
vi.advanceTimersByTime(26 * 60 * 1000);
|
|
221
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual({
|
|
222
|
+
ok: true,
|
|
223
|
+
});
|
|
224
|
+
expect(tap).toHaveBeenCalledTimes(2);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
vi.useRealTimers();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
it("leaves untagged commands ungated (pre-tagging gateways)", async () => {
|
|
231
|
+
const tap = vi.fn(async () => { });
|
|
232
|
+
const socket = wire(stubController({ tap }));
|
|
233
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
234
|
+
await expect(socket.emit("tap", { x: 3, y: 4 })).resolves.toEqual({ ok: true });
|
|
235
|
+
expect(tap).toHaveBeenCalledTimes(2);
|
|
236
|
+
});
|
|
237
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autifyhq/aximo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Autify Aximo Command Line Interface (CLI)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"autify",
|
|
@@ -38,6 +38,7 @@
|
|
|
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",
|
|
43
44
|
"undici": "^7.28.0",
|