@autifyhq/aximo-cli 0.9.0 → 0.10.1
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 +1 -1
- package/dist/commands/device/connect.js +19 -1
- package/dist/commands/device/doctor.d.ts +2 -0
- package/dist/commands/device/doctor.js +37 -10
- package/dist/commands/device/doctor.test.js +26 -1
- package/dist/commands/mcp/server.test.js +3 -1
- package/dist/commands/mcp/tools.js +28 -0
- package/dist/commands/mcp/tools.test.js +90 -0
- package/dist/commands/mobile/connect-core.d.ts +2 -2
- package/dist/commands/mobile/connect-core.js +35 -2
- package/dist/commands/mobile/signing-consent.js +5 -11
- package/dist/desktop-server/preflight.d.ts +43 -0
- package/dist/desktop-server/preflight.js +78 -0
- package/dist/desktop-server/preflight.test.d.ts +1 -0
- package/dist/desktop-server/preflight.test.js +115 -0
- package/dist/mobile-control/appium-controller.d.ts +24 -0
- package/dist/mobile-control/appium-controller.js +221 -127
- package/dist/mobile-control/appium-controller.test.js +105 -0
- package/dist/mobile-control/frame-sender.d.ts +34 -0
- package/dist/mobile-control/frame-sender.js +96 -0
- package/dist/mobile-control/frame-sender.test.d.ts +1 -0
- package/dist/mobile-control/frame-sender.test.js +108 -0
- package/dist/mobile-control/mjpeg-stream.d.ts +43 -0
- package/dist/mobile-control/mjpeg-stream.js +160 -0
- package/dist/mobile-control/mjpeg-stream.test.d.ts +1 -0
- package/dist/mobile-control/mjpeg-stream.test.js +93 -0
- package/dist/mobile-control/tunnel.d.ts +10 -1
- package/dist/mobile-control/tunnel.js +27 -3
- package/dist/mobile-control/tunnel.test.js +50 -0
- package/dist/mobile-control/webdriver.d.ts +2 -0
- package/dist/mobile-control/webdriver.js +6 -0
- package/dist/prompt.d.ts +11 -0
- package/dist/prompt.js +19 -0
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
|
@@ -52,7 +52,7 @@ function defaultConnect(wsUrl, deviceToken) {
|
|
|
52
52
|
// Mirrors the gateway's lock lease: a session quiet for longer than this has lost
|
|
53
53
|
// its server-side lock, so a new session id is a legitimate takeover, not a stray.
|
|
54
54
|
const SESSION_LEASE_MS = 25 * 60 * 1000;
|
|
55
|
-
export function registerHandlers(socket, controller, installApp) {
|
|
55
|
+
export function registerHandlers(socket, controller, installApp, stream) {
|
|
56
56
|
// Device-side guard against interleaved sessions: the gateway announces owner
|
|
57
57
|
// changes (session_acquired/session_released) and tags each command with its
|
|
58
58
|
// session id, so commands from a session that no longer owns the device are
|
|
@@ -70,8 +70,22 @@ export function registerHandlers(socket, controller, installApp) {
|
|
|
70
70
|
const sessionId = args?.sessionId;
|
|
71
71
|
if (activeSession && activeSession.id === sessionId) {
|
|
72
72
|
activeSession = null;
|
|
73
|
+
// No owning session -> nobody may watch the phone.
|
|
74
|
+
stream?.stop();
|
|
73
75
|
}
|
|
74
76
|
});
|
|
77
|
+
// Demand gating: the gateway asks for frames only while a session owns the
|
|
78
|
+
// device AND at least one viewer is reading them; it signals idle explicitly
|
|
79
|
+
// (and the frame acks carry the same signal as a backstop). The device-side
|
|
80
|
+
// owner check mirrors the command guard: a stray start from a gateway
|
|
81
|
+
// instance with stale state must not stream an idle phone. A start dropped
|
|
82
|
+
// here self-heals — the gateway re-signals while viewers keep reading, and
|
|
83
|
+
// the owner becomes known on session_acquired or the next tagged command.
|
|
84
|
+
socket.on("stream_start", () => {
|
|
85
|
+
if (activeSession)
|
|
86
|
+
stream?.start();
|
|
87
|
+
});
|
|
88
|
+
socket.on("stream_stop", () => stream?.stop());
|
|
75
89
|
const on = (event, handler) => {
|
|
76
90
|
socket.on(event, (rawArgs, ack) => {
|
|
77
91
|
// Destructure the routing-only tag off a clone so the caller's payload
|
|
@@ -88,7 +102,13 @@ export function registerHandlers(socket, controller, installApp) {
|
|
|
88
102
|
}
|
|
89
103
|
void handler(args)
|
|
90
104
|
.then((result) => ack?.(result))
|
|
91
|
-
.catch((err) =>
|
|
105
|
+
.catch((err) => {
|
|
106
|
+
const failure = fail(err);
|
|
107
|
+
// The server may collapse this ack into a generic error; the terminal
|
|
108
|
+
// is the only place the device-side cause is guaranteed to be visible.
|
|
109
|
+
console.error(`Command "${event}" failed: ${failure.error}`);
|
|
110
|
+
ack?.(failure);
|
|
111
|
+
});
|
|
92
112
|
});
|
|
93
113
|
};
|
|
94
114
|
on("ping", async () => ok());
|
|
@@ -172,7 +192,7 @@ export function registerHandlers(socket, controller, installApp) {
|
|
|
172
192
|
export async function runControlTunnel(options) {
|
|
173
193
|
const connect = options.connect ?? defaultConnect;
|
|
174
194
|
const socket = connect(options.wsUrl, options.deviceToken);
|
|
175
|
-
registerHandlers(socket, options.controller, options.installApp);
|
|
195
|
+
registerHandlers(socket, options.controller, options.installApp, options.stream);
|
|
176
196
|
await new Promise((resolve, reject) => {
|
|
177
197
|
let shuttingDown = false;
|
|
178
198
|
let serverKicks = [];
|
|
@@ -180,6 +200,7 @@ export async function runControlTunnel(options) {
|
|
|
180
200
|
if (shuttingDown)
|
|
181
201
|
return;
|
|
182
202
|
shuttingDown = true;
|
|
203
|
+
options.stream?.stop();
|
|
183
204
|
socket.close();
|
|
184
205
|
resolve();
|
|
185
206
|
};
|
|
@@ -211,6 +232,9 @@ export async function runControlTunnel(options) {
|
|
|
211
232
|
console.error(`Connection error: ${err.message}`);
|
|
212
233
|
});
|
|
213
234
|
socket.on("disconnect", (reason) => {
|
|
235
|
+
// Whatever the reason, a disconnected tunnel means no viewer can be
|
|
236
|
+
// watching — stop capture; a reconnect re-arms via stream_start.
|
|
237
|
+
options.stream?.stop();
|
|
214
238
|
// A SIGINT/SIGTERM teardown closes the socket itself; cleanup() already
|
|
215
239
|
// resolved the promise, so don't report our own close as an error.
|
|
216
240
|
if (shuttingDown)
|
|
@@ -49,6 +49,23 @@ describe("registerHandlers", () => {
|
|
|
49
49
|
expect(Buffer.isBuffer(result)).toBe(true);
|
|
50
50
|
expect(result.equals(Buffer.from([1, 2, 3]))).toBe(true);
|
|
51
51
|
});
|
|
52
|
+
it("a throwing handler acks the failure and logs it to the terminal", async () => {
|
|
53
|
+
const logSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
54
|
+
try {
|
|
55
|
+
const screenshot = vi.fn(async () => {
|
|
56
|
+
throw new Error("Hidden API policy cannot be enabled");
|
|
57
|
+
});
|
|
58
|
+
const socket = wire(stubController({ screenshot }));
|
|
59
|
+
await expect(socket.emit("screenshot", {})).resolves.toEqual({
|
|
60
|
+
ok: false,
|
|
61
|
+
error: "Hidden API policy cannot be enabled",
|
|
62
|
+
});
|
|
63
|
+
expect(logSpy).toHaveBeenCalledWith('Command "screenshot" failed: Hidden API policy cannot be enabled');
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
logSpy.mockRestore();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
52
69
|
it("tap forwards coordinates and acks ok", async () => {
|
|
53
70
|
const tap = vi.fn(async () => { });
|
|
54
71
|
const socket = wire(stubController({ tap }));
|
|
@@ -310,3 +327,36 @@ describe("registerHandlers session guard", () => {
|
|
|
310
327
|
expect(tap).toHaveBeenCalledTimes(2);
|
|
311
328
|
});
|
|
312
329
|
});
|
|
330
|
+
describe("stream lifecycle handlers", () => {
|
|
331
|
+
function wireWithStream() {
|
|
332
|
+
const socket = new FakeSocket();
|
|
333
|
+
const stream = { start: vi.fn(), stop: vi.fn() };
|
|
334
|
+
registerHandlers(socket, stubController(), undefined, stream);
|
|
335
|
+
return { socket, stream };
|
|
336
|
+
}
|
|
337
|
+
it("starts and stops capture on gateway demand signals while a session owns the device", () => {
|
|
338
|
+
const { socket, stream } = wireWithStream();
|
|
339
|
+
socket.handlers.get("session_acquired")?.({ sessionId: "s1" }, undefined);
|
|
340
|
+
socket.handlers.get("stream_start")?.({}, undefined);
|
|
341
|
+
expect(stream.start).toHaveBeenCalledTimes(1);
|
|
342
|
+
socket.handlers.get("stream_stop")?.({}, undefined);
|
|
343
|
+
expect(stream.stop).toHaveBeenCalledTimes(1);
|
|
344
|
+
});
|
|
345
|
+
it("drops stream_start when no session owns the device", () => {
|
|
346
|
+
const { socket, stream } = wireWithStream();
|
|
347
|
+
socket.handlers.get("stream_start")?.({}, undefined);
|
|
348
|
+
expect(stream.start).not.toHaveBeenCalled();
|
|
349
|
+
});
|
|
350
|
+
it("stops capture when the owning session releases the device", () => {
|
|
351
|
+
const { socket, stream } = wireWithStream();
|
|
352
|
+
socket.handlers.get("session_acquired")?.({ sessionId: "s1" }, undefined);
|
|
353
|
+
socket.handlers.get("session_released")?.({ sessionId: "s1" }, undefined);
|
|
354
|
+
expect(stream.stop).toHaveBeenCalledTimes(1);
|
|
355
|
+
});
|
|
356
|
+
it("ignores a release from a session that does not own the device", () => {
|
|
357
|
+
const { socket, stream } = wireWithStream();
|
|
358
|
+
socket.handlers.get("session_acquired")?.({ sessionId: "s1" }, undefined);
|
|
359
|
+
socket.handlers.get("session_released")?.({ sessionId: "other" }, undefined);
|
|
360
|
+
expect(stream.stop).not.toHaveBeenCalled();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
@@ -33,5 +33,7 @@ export declare class WebDriverSession {
|
|
|
33
33
|
elementSendKeys(elementRef: string, text: string): Promise<void>;
|
|
34
34
|
elementDisplayed(elementRef: string): Promise<boolean>;
|
|
35
35
|
execute(script: string, args?: unknown[]): Promise<unknown>;
|
|
36
|
+
/** Appium extension endpoint: update driver settings for the current session. */
|
|
37
|
+
updateSettings(settings: Record<string, unknown>): Promise<void>;
|
|
36
38
|
}
|
|
37
39
|
export declare const ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
|
|
@@ -128,6 +128,12 @@ export class WebDriverSession {
|
|
|
128
128
|
args,
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
+
/** Appium extension endpoint: update driver settings for the current session. */
|
|
132
|
+
async updateSettings(settings) {
|
|
133
|
+
await this.transport.request("POST", `/session/${this.requireId()}/appium/settings`, {
|
|
134
|
+
settings,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
131
137
|
}
|
|
132
138
|
export const ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
|
|
133
139
|
function elementId(value) {
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ConfirmOptions {
|
|
2
|
+
/** Whether a human can answer; defaults to stdin being a TTY. */
|
|
3
|
+
interactive?: boolean;
|
|
4
|
+
ask?: (question: string) => Promise<string>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Yes-by-default confirmation. Enter proceeds; only an explicit n/no declines.
|
|
8
|
+
* Unattended callers (no TTY) proceed without asking, so CI and daemonized
|
|
9
|
+
* devices keep working instead of hanging on a prompt nobody can see.
|
|
10
|
+
*/
|
|
11
|
+
export declare function confirmYesNo(question: string, options?: ConfirmOptions): Promise<boolean>;
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
function defaultAsk(question) {
|
|
3
|
+
// stderr, so prompts never contaminate the JSON envelope on stdout.
|
|
4
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
5
|
+
return rl.question(question).finally(() => rl.close());
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Yes-by-default confirmation. Enter proceeds; only an explicit n/no declines.
|
|
9
|
+
* Unattended callers (no TTY) proceed without asking, so CI and daemonized
|
|
10
|
+
* devices keep working instead of hanging on a prompt nobody can see.
|
|
11
|
+
*/
|
|
12
|
+
export async function confirmYesNo(question, options = {}) {
|
|
13
|
+
const interactive = options.interactive ?? process.stdin.isTTY === true;
|
|
14
|
+
if (!interactive)
|
|
15
|
+
return true;
|
|
16
|
+
const ask = options.ask ?? defaultAsk;
|
|
17
|
+
const answer = (await ask(`${question} [Y/n] `)).trim().toLowerCase();
|
|
18
|
+
return answer !== "n" && answer !== "no";
|
|
19
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -109,6 +109,23 @@ export interface PlanRun {
|
|
|
109
109
|
status?: string | undefined;
|
|
110
110
|
[key: string]: unknown;
|
|
111
111
|
}
|
|
112
|
+
export interface PlanRunSummary {
|
|
113
|
+
id: string;
|
|
114
|
+
status: string;
|
|
115
|
+
triggerSource: string | null;
|
|
116
|
+
executionMode: string;
|
|
117
|
+
totalCases: number;
|
|
118
|
+
completedCases: number;
|
|
119
|
+
passedCases: number;
|
|
120
|
+
failedCases: number;
|
|
121
|
+
startedAt: string | null;
|
|
122
|
+
completedAt: string | null;
|
|
123
|
+
createdAt: string;
|
|
124
|
+
}
|
|
125
|
+
export interface PlanRunList {
|
|
126
|
+
items: PlanRunSummary[];
|
|
127
|
+
nextCursor?: string | undefined;
|
|
128
|
+
}
|
|
112
129
|
export interface Device {
|
|
113
130
|
arn?: string | undefined;
|
|
114
131
|
name?: string | undefined;
|