@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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/device/connect.js +19 -1
  3. package/dist/commands/device/doctor.d.ts +2 -0
  4. package/dist/commands/device/doctor.js +37 -10
  5. package/dist/commands/device/doctor.test.js +26 -1
  6. package/dist/commands/mcp/server.test.js +3 -1
  7. package/dist/commands/mcp/tools.js +28 -0
  8. package/dist/commands/mcp/tools.test.js +90 -0
  9. package/dist/commands/mobile/connect-core.d.ts +2 -2
  10. package/dist/commands/mobile/connect-core.js +35 -2
  11. package/dist/commands/mobile/signing-consent.js +5 -11
  12. package/dist/desktop-server/preflight.d.ts +43 -0
  13. package/dist/desktop-server/preflight.js +78 -0
  14. package/dist/desktop-server/preflight.test.d.ts +1 -0
  15. package/dist/desktop-server/preflight.test.js +115 -0
  16. package/dist/mobile-control/appium-controller.d.ts +24 -0
  17. package/dist/mobile-control/appium-controller.js +218 -127
  18. package/dist/mobile-control/appium-controller.test.js +102 -0
  19. package/dist/mobile-control/frame-sender.d.ts +34 -0
  20. package/dist/mobile-control/frame-sender.js +96 -0
  21. package/dist/mobile-control/frame-sender.test.d.ts +1 -0
  22. package/dist/mobile-control/frame-sender.test.js +108 -0
  23. package/dist/mobile-control/mjpeg-stream.d.ts +43 -0
  24. package/dist/mobile-control/mjpeg-stream.js +160 -0
  25. package/dist/mobile-control/mjpeg-stream.test.d.ts +1 -0
  26. package/dist/mobile-control/mjpeg-stream.test.js +93 -0
  27. package/dist/mobile-control/tunnel.d.ts +10 -1
  28. package/dist/mobile-control/tunnel.js +20 -2
  29. package/dist/mobile-control/tunnel.test.js +33 -0
  30. package/dist/mobile-control/webdriver.d.ts +2 -0
  31. package/dist/mobile-control/webdriver.js +6 -0
  32. package/dist/prompt.d.ts +11 -0
  33. package/dist/prompt.js +19 -0
  34. package/dist/types.d.ts +17 -0
  35. 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) {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autifyhq/aximo-cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Autify Aximo Command Line Interface (CLI)",
5
5
  "keywords": [
6
6
  "autify",