@autifyhq/aximo-cli 0.9.0 → 0.10.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 +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 +218 -127
- package/dist/mobile-control/appium-controller.test.js +102 -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 +20 -2
- package/dist/mobile-control/tunnel.test.js +33 -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
|
|
@@ -172,7 +186,7 @@ export function registerHandlers(socket, controller, installApp) {
|
|
|
172
186
|
export async function runControlTunnel(options) {
|
|
173
187
|
const connect = options.connect ?? defaultConnect;
|
|
174
188
|
const socket = connect(options.wsUrl, options.deviceToken);
|
|
175
|
-
registerHandlers(socket, options.controller, options.installApp);
|
|
189
|
+
registerHandlers(socket, options.controller, options.installApp, options.stream);
|
|
176
190
|
await new Promise((resolve, reject) => {
|
|
177
191
|
let shuttingDown = false;
|
|
178
192
|
let serverKicks = [];
|
|
@@ -180,6 +194,7 @@ export async function runControlTunnel(options) {
|
|
|
180
194
|
if (shuttingDown)
|
|
181
195
|
return;
|
|
182
196
|
shuttingDown = true;
|
|
197
|
+
options.stream?.stop();
|
|
183
198
|
socket.close();
|
|
184
199
|
resolve();
|
|
185
200
|
};
|
|
@@ -211,6 +226,9 @@ export async function runControlTunnel(options) {
|
|
|
211
226
|
console.error(`Connection error: ${err.message}`);
|
|
212
227
|
});
|
|
213
228
|
socket.on("disconnect", (reason) => {
|
|
229
|
+
// Whatever the reason, a disconnected tunnel means no viewer can be
|
|
230
|
+
// watching — stop capture; a reconnect re-arms via stream_start.
|
|
231
|
+
options.stream?.stop();
|
|
214
232
|
// A SIGINT/SIGTERM teardown closes the socket itself; cleanup() already
|
|
215
233
|
// resolved the promise, so don't report our own close as an error.
|
|
216
234
|
if (shuttingDown)
|
|
@@ -310,3 +310,36 @@ describe("registerHandlers session guard", () => {
|
|
|
310
310
|
expect(tap).toHaveBeenCalledTimes(2);
|
|
311
311
|
});
|
|
312
312
|
});
|
|
313
|
+
describe("stream lifecycle handlers", () => {
|
|
314
|
+
function wireWithStream() {
|
|
315
|
+
const socket = new FakeSocket();
|
|
316
|
+
const stream = { start: vi.fn(), stop: vi.fn() };
|
|
317
|
+
registerHandlers(socket, stubController(), undefined, stream);
|
|
318
|
+
return { socket, stream };
|
|
319
|
+
}
|
|
320
|
+
it("starts and stops capture on gateway demand signals while a session owns the device", () => {
|
|
321
|
+
const { socket, stream } = wireWithStream();
|
|
322
|
+
socket.handlers.get("session_acquired")?.({ sessionId: "s1" }, undefined);
|
|
323
|
+
socket.handlers.get("stream_start")?.({}, undefined);
|
|
324
|
+
expect(stream.start).toHaveBeenCalledTimes(1);
|
|
325
|
+
socket.handlers.get("stream_stop")?.({}, undefined);
|
|
326
|
+
expect(stream.stop).toHaveBeenCalledTimes(1);
|
|
327
|
+
});
|
|
328
|
+
it("drops stream_start when no session owns the device", () => {
|
|
329
|
+
const { socket, stream } = wireWithStream();
|
|
330
|
+
socket.handlers.get("stream_start")?.({}, undefined);
|
|
331
|
+
expect(stream.start).not.toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
it("stops capture when the owning session releases the device", () => {
|
|
334
|
+
const { socket, stream } = wireWithStream();
|
|
335
|
+
socket.handlers.get("session_acquired")?.({ sessionId: "s1" }, undefined);
|
|
336
|
+
socket.handlers.get("session_released")?.({ sessionId: "s1" }, undefined);
|
|
337
|
+
expect(stream.stop).toHaveBeenCalledTimes(1);
|
|
338
|
+
});
|
|
339
|
+
it("ignores a release from a session that does not own the device", () => {
|
|
340
|
+
const { socket, stream } = wireWithStream();
|
|
341
|
+
socket.handlers.get("session_acquired")?.({ sessionId: "s1" }, undefined);
|
|
342
|
+
socket.handlers.get("session_released")?.({ sessionId: "other" }, undefined);
|
|
343
|
+
expect(stream.stop).not.toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -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;
|