@flowriki/agent 0.1.0-beta.52 → 0.1.0-beta.53

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.
@@ -47,6 +47,7 @@ import {
47
47
  nodeAt,
48
48
  normalizeStartUrl,
49
49
  openMobileSession,
50
+ openSessionOn,
50
51
  parseFlowDocument,
51
52
  performMobileStep,
52
53
  platformsOf,
@@ -67,7 +68,7 @@ import {
67
68
  textIndependent,
68
69
  validateActors,
69
70
  webCloudOf
70
- } from "./chunk-KDW65RBE.js";
71
+ } from "./chunk-FIR6NOLA.js";
71
72
 
72
73
  // src/engine/machine.ts
73
74
  import fs from "fs";
@@ -75,7 +76,7 @@ import os from "os";
75
76
  import path from "path";
76
77
  import { createRequire } from "module";
77
78
  import { spawnSync } from "child_process";
78
- var AGENT_VERSION = "0.1.0-beta.52";
79
+ var AGENT_VERSION = "0.1.0-beta.53";
79
80
  async function prepareBrowsers() {
80
81
  if (browsersReady()) {
81
82
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -135,7 +136,7 @@ async function checkMachine() {
135
136
  }
136
137
  } catch {
137
138
  }
138
- const { mobileDoctor: mobileDoctor2 } = await import("./dist-R2I4D66B.js");
139
+ const { mobileDoctor: mobileDoctor2 } = await import("./dist-6GKVE5CL.js");
139
140
  return {
140
141
  agentVersion: AGENT_VERSION,
141
142
  node: process.versions.node,
@@ -234,7 +235,7 @@ function forgetConnection(service, env = process.env) {
234
235
  }
235
236
  var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
236
237
 
237
- // ../contracts/dist/chunk-S3GLB3QV.js
238
+ // ../contracts/dist/chunk-NTBXG5EZ.js
238
239
  import { z } from "zod";
239
240
  var PROTOCOL_VERSION = 1;
240
241
  var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
@@ -283,6 +284,9 @@ var CommandNameSchema = z.enum([
283
284
  // Reading a live phone session: its screen as a picture, and what is under
284
285
  // a point. Not effectful — a picture is far too big to keep as a result.
285
286
  "mobile.screen",
287
+ // The screen of whatever phone session is open on a platform — a run's,
288
+ // a recording's — as a picture, for the live mirror. Reading.
289
+ "mobile.peek",
286
290
  // What this computer has installed for web and phone testing. Reading.
287
291
  "machine.check"
288
292
  ]);
@@ -3962,7 +3966,7 @@ async function replayCase(opts) {
3962
3966
  let status2 = "passed";
3963
3967
  let error = null;
3964
3968
  const browserRows = occurrences.filter((o) => o.platform !== "android" && o.platform !== "ios");
3965
- const laneOf = new Map(sessionLanes(browserRows, opts.defaultActorId).map((plan, i) => [browserRows[i].occurrenceId, plan]));
3969
+ const laneOf2 = new Map(sessionLanes(browserRows, opts.defaultActorId).map((plan, i) => [browserRows[i].occurrenceId, plan]));
3966
3970
  try {
3967
3971
  for (const [index, occurrence] of occurrences.entries()) {
3968
3972
  if (status2 !== "passed" || opts.signal?.aborted) {
@@ -4014,7 +4018,7 @@ async function replayCase(opts) {
4014
4018
  const navigates = navigatesFirst(occurrence);
4015
4019
  const who = occurrence.actorId ?? opts.defaultActorId ?? "default";
4016
4020
  const entry = resolveStartUrl(occurrence.baseUrl ?? baseUrl, occurrence.startPath ?? "/");
4017
- const plan = laneOf.get(occurrence.occurrenceId);
4021
+ const plan = laneOf2.get(occurrence.occurrenceId);
4018
4022
  try {
4019
4023
  if (plan.opens && startOf(occurrence) === "fresh") await actors.restart(who, entry);
4020
4024
  else if (plan.opens) await actors.select(who, entry);
@@ -7990,6 +7994,105 @@ var BrowserBusy = class extends Error {
7990
7994
  this.name = "BrowserBusy";
7991
7995
  }
7992
7996
  };
7997
+ var LANES = ["web", "android", "ios"];
7998
+ var OneLane = class {
7999
+ owner = null;
8000
+ phase = "idle";
8001
+ since = null;
8002
+ /** Take the lane, or report who has it. Never awaits — that is the point. */
8003
+ acquire(owner) {
8004
+ if (this.owner !== null) {
8005
+ return { ok: false, heldBy: this.owner, phase: this.phase };
8006
+ }
8007
+ this.owner = owner;
8008
+ this.phase = "starting";
8009
+ this.since = Date.now();
8010
+ return { ok: true };
8011
+ }
8012
+ /** The holder finished starting and is now doing the thing. */
8013
+ activate(owner) {
8014
+ if (this.owner === owner) this.phase = "active";
8015
+ }
8016
+ /** The holder has begun shutting down. Still not idle: a second stop must
8017
+ * join the first rather than starting its own. */
8018
+ beginRelease(owner) {
8019
+ if (this.owner !== owner || this.phase === "stopping") return false;
8020
+ this.phase = "stopping";
8021
+ return true;
8022
+ }
8023
+ release(owner) {
8024
+ if (this.owner !== owner) return;
8025
+ this.owner = null;
8026
+ this.phase = "idle";
8027
+ this.since = null;
8028
+ }
8029
+ forceRelease() {
8030
+ this.owner = null;
8031
+ this.phase = "idle";
8032
+ this.since = null;
8033
+ }
8034
+ state() {
8035
+ return { owner: this.owner, phase: this.phase, since: this.since };
8036
+ }
8037
+ };
8038
+ var BrowserLease = class {
8039
+ lanes = { web: new OneLane(), android: new OneLane(), ios: new OneLane() };
8040
+ /** The web lane's methods, as they always were: the browser coordinators
8041
+ * drive one platform and need not know about lanes. */
8042
+ acquire(owner, lane = "web") {
8043
+ return this.lanes[lane].acquire(owner);
8044
+ }
8045
+ activate(owner, lane = "web") {
8046
+ this.lanes[lane].activate(owner);
8047
+ }
8048
+ beginRelease(owner, lane = "web") {
8049
+ return this.lanes[lane].beginRelease(owner);
8050
+ }
8051
+ release(owner, lane = "web") {
8052
+ this.lanes[lane].release(owner);
8053
+ }
8054
+ state(lane = "web") {
8055
+ return this.lanes[lane].state();
8056
+ }
8057
+ /** One lane, bound: what a coordinator for that platform is handed. */
8058
+ lane(lane) {
8059
+ return this.lanes[lane];
8060
+ }
8061
+ /** Take several lanes at once — all or none, so a job on two platforms
8062
+ * never holds one while refused the other. */
8063
+ acquireAll(owner, lanes) {
8064
+ const taken = [];
8065
+ for (const lane of new Set(lanes)) {
8066
+ const claim = this.lanes[lane].acquire(owner);
8067
+ if (!claim.ok) {
8068
+ for (const had of taken) this.lanes[had].release(owner);
8069
+ return { ok: false, lane, heldBy: claim.heldBy, phase: claim.phase };
8070
+ }
8071
+ taken.push(lane);
8072
+ }
8073
+ return { ok: true };
8074
+ }
8075
+ activateAll(owner, lanes) {
8076
+ for (const lane of new Set(lanes)) this.lanes[lane].activate(owner);
8077
+ }
8078
+ releaseAll(owner, lanes) {
8079
+ for (const lane of new Set(lanes)) this.lanes[lane].release(owner);
8080
+ }
8081
+ /** Drop every lane whoever holds it — shutdown, and nothing else. */
8082
+ forceRelease() {
8083
+ for (const lane of LANES) this.lanes[lane].forceRelease();
8084
+ }
8085
+ /** The sentence a refused caller is shown. Names what is happening and on
8086
+ * what, because "busy" leaves someone hunting for a window they cannot find. */
8087
+ static refusal(heldBy, phase, lane = "web") {
8088
+ const what = heldBy === "recording" ? "a recording" : heldBy === "debug" ? "a debug session" : "a run";
8089
+ const where = lane === "web" ? "the browser" : lane === "android" ? "the Android phone" : "the iPhone";
8090
+ if (phase === "starting") return `${what} is starting on ${where} \u2014 wait for it to open, or stop it first`;
8091
+ if (phase === "stopping") return `${what} is still closing on ${where} \u2014 try again in a moment`;
8092
+ return `${what} is already using ${where} \u2014 stop it before starting another`;
8093
+ }
8094
+ };
8095
+ var laneOf = (platform) => platform === "android" || platform === "ios" ? platform : "web";
7993
8096
  var COMMANDS = /* @__PURE__ */ new Set([
7994
8097
  "play",
7995
8098
  "pause",
@@ -8166,11 +8269,12 @@ async function verifyFlowSteps(deps, opts) {
8166
8269
  const steps = (opts.steps ?? flow.steps).map((s2) => DraftStepSchema.parse(s2));
8167
8270
  if (!opts.verifyIds.length) return { results: [], error: null, failedAtStepId: null, ok: true };
8168
8271
  const baseUrl = store.baseUrlFor(flow);
8169
- const claim = deps.lease?.acquire("run");
8272
+ const lane = laneOf(flow.platform);
8273
+ const claim = deps.lease?.acquire("run", lane);
8170
8274
  if (claim && !claim.ok) {
8171
- throw new BrowserBusy(`the browser on this machine is in use (${claim.heldBy}, ${claim.phase})`);
8275
+ throw new BrowserBusy(`${lane === "web" ? "the browser" : lane === "android" ? "the Android phone" : "the iPhone"} on this machine is in use (${claim.heldBy}, ${claim.phase})`);
8172
8276
  }
8173
- deps.lease?.activate("run");
8277
+ deps.lease?.activate("run", lane);
8174
8278
  try {
8175
8279
  const secrets = store.readSecrets();
8176
8280
  if (isMobile(flow.platform)) {
@@ -8215,7 +8319,7 @@ async function verifyFlowSteps(deps, opts) {
8215
8319
  resolveSecret: (ref) => secrets[ref]
8216
8320
  });
8217
8321
  } finally {
8218
- deps.lease?.release("run");
8322
+ deps.lease?.release("run", lane);
8219
8323
  }
8220
8324
  }
8221
8325
  function deliveredRun(delivered) {
@@ -8235,55 +8339,6 @@ function deliveredRun(delivered) {
8235
8339
  resolveFile: () => void 0
8236
8340
  };
8237
8341
  }
8238
- var BrowserLease = class {
8239
- owner = null;
8240
- phase = "idle";
8241
- since = null;
8242
- /** Take the lease, or report who has it. Never awaits — that is the point. */
8243
- acquire(owner) {
8244
- if (this.owner !== null) {
8245
- return { ok: false, heldBy: this.owner, phase: this.phase };
8246
- }
8247
- this.owner = owner;
8248
- this.phase = "starting";
8249
- this.since = Date.now();
8250
- return { ok: true };
8251
- }
8252
- /** The holder finished starting and is now doing the thing. */
8253
- activate(owner) {
8254
- if (this.owner === owner) this.phase = "active";
8255
- }
8256
- /** The holder has begun shutting down. Still not idle: a second stop must
8257
- * join the first rather than starting its own. */
8258
- beginRelease(owner) {
8259
- if (this.owner !== owner || this.phase === "stopping") return false;
8260
- this.phase = "stopping";
8261
- return true;
8262
- }
8263
- release(owner) {
8264
- if (this.owner !== owner) return;
8265
- this.owner = null;
8266
- this.phase = "idle";
8267
- this.since = null;
8268
- }
8269
- /** Drop the lease whoever holds it — shutdown, and nothing else. */
8270
- forceRelease() {
8271
- this.owner = null;
8272
- this.phase = "idle";
8273
- this.since = null;
8274
- }
8275
- state() {
8276
- return { owner: this.owner, phase: this.phase, since: this.since };
8277
- }
8278
- /** The sentence a refused caller is shown. Names what is happening, because
8279
- * "busy" leaves someone hunting for a window they cannot find. */
8280
- static refusal(heldBy, phase) {
8281
- const what = heldBy === "recording" ? "a recording" : heldBy === "debug" ? "a debug session" : "a run";
8282
- if (phase === "starting") return `${what} is starting \u2014 wait for it to open, or stop it first`;
8283
- if (phase === "stopping") return `${what} is still closing \u2014 try again in a moment`;
8284
- return `${what} is already using the browser \u2014 stop it before starting another`;
8285
- }
8286
- };
8287
8342
 
8288
8343
  // src/engine/checkouts.ts
8289
8344
  function resolveCheckouts(dirs) {
@@ -9240,11 +9295,12 @@ function executionHandlers(ctx) {
9240
9295
  detail: `this flow needs ${missing.length} secret value(s) that are not on this machine (${missing.map((m) => m.ref || m.field).join(", ")}) \u2014 set them on the machine that runs it`
9241
9296
  };
9242
9297
  }
9243
- const claim = lease?.acquire("run");
9298
+ const lanes = [laneOf(flow.platform)];
9299
+ const claim = lease?.acquireAll("run", lanes);
9244
9300
  if (claim && !claim.ok) {
9245
- return { ok: false, reason: "busy", detail: `the browser on this machine is in use (${claim.heldBy})` };
9301
+ return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, claim.lane) };
9246
9302
  }
9247
- lease?.activate("run");
9303
+ lease?.activateAll("run", lanes);
9248
9304
  onActivity?.({ kind: "run", projectId: project.id, detail: flow.name });
9249
9305
  const aborter = new AbortController();
9250
9306
  cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
@@ -9279,6 +9335,8 @@ function executionHandlers(ctx) {
9279
9335
  ok: true,
9280
9336
  result: {
9281
9337
  runId: outcome.run?.id ?? null,
9338
+ // What it was called, for a service that only knew its id.
9339
+ flowName: flow.name,
9282
9340
  // The WHOLE outcome, not a summary of it. A dashboard renders the
9283
9341
  // per-step report exactly as the local app does, and a result
9284
9342
  // carrying only counts makes that screen crash rather than degrade.
@@ -9294,7 +9352,7 @@ function executionHandlers(ctx) {
9294
9352
  if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
9295
9353
  if (runId) cancels.delete(runId);
9296
9354
  onActivity?.(null);
9297
- lease?.release("run");
9355
+ lease?.releaseAll("run", lanes);
9298
9356
  }
9299
9357
  },
9300
9358
  /** Run a test case: several flows, one browser, in order.
@@ -9312,11 +9370,12 @@ function executionHandlers(ctx) {
9312
9370
  if (testCase.occurrences.length === 0) {
9313
9371
  return { ok: false, reason: "failed", detail: `"${testCase.name}" has no flows in it to run` };
9314
9372
  }
9315
- const claim = lease?.acquire("run");
9373
+ const lanes = [...new Set(testCase.occurrences.map((o) => laneOf(o.platform)))];
9374
+ const claim = lease?.acquireAll("run", lanes);
9316
9375
  if (claim && !claim.ok) {
9317
- return { ok: false, reason: "busy", detail: `the browser on this machine is in use (${claim.heldBy})` };
9376
+ return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, claim.lane) };
9318
9377
  }
9319
- lease?.activate("run");
9378
+ lease?.activateAll("run", lanes);
9320
9379
  onActivity?.({ kind: "run", projectId: project.id, detail: testCase.name });
9321
9380
  const aborter = new AbortController();
9322
9381
  cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
@@ -9355,6 +9414,8 @@ function executionHandlers(ctx) {
9355
9414
  ok: true,
9356
9415
  result: {
9357
9416
  runId: run?.id ?? null,
9417
+ // What it was called, for a service that only knew its id.
9418
+ caseName: testCase.name,
9358
9419
  // The whole outcome, per-flow groups included: a dashboard renders
9359
9420
  // the report from this, and a result carrying only counts makes
9360
9421
  // that screen crash rather than degrade.
@@ -9366,7 +9427,7 @@ function executionHandlers(ctx) {
9366
9427
  if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
9367
9428
  if (runId) cancels.delete(runId);
9368
9429
  onActivity?.(null);
9369
- lease?.release("run");
9430
+ lease?.releaseAll("run", lanes);
9370
9431
  }
9371
9432
  },
9372
9433
  "run.cancel": async (command, project) => {
@@ -9779,7 +9840,9 @@ var runFile = (file2, args, timeoutMs) => new Promise((resolve2, reject) => {
9779
9840
  resolve2({ code, stdout: String(stdout), stderr: String(stderr) });
9780
9841
  });
9781
9842
  });
9782
- var live = null;
9843
+ var live = /* @__PURE__ */ new Map();
9844
+ var liveById = (sessionId) => [...live.values()].find((l) => l.id === sessionId);
9845
+ var ownerOf = (l) => l.draft ? "recording" : "debug";
9783
9846
  var SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|\bpin\b|cvv|cvc/i;
9784
9847
  async function screenOf(l, withImage = false) {
9785
9848
  l.session.changed();
@@ -9805,8 +9868,9 @@ async function screenOf(l, withImage = false) {
9805
9868
  }
9806
9869
  function mobileHandlers(ctx) {
9807
9870
  const require2 = (command) => {
9808
- if (!live || command.args["sessionId"] !== live.id) throw new Error("that phone session has ended \u2014 open it again");
9809
- return live;
9871
+ const l = liveById(command.args["sessionId"]);
9872
+ if (!l) throw new Error("that phone session has ended \u2014 open it again");
9873
+ return l;
9810
9874
  };
9811
9875
  return {
9812
9876
  // Everything this computer has for testing — web and phones — for the Guide.
@@ -9817,6 +9881,21 @@ function mobileHandlers(ctx) {
9817
9881
  const devices = await listDevices();
9818
9882
  return { ok: true, result: { devices, ...command.args["doctor"] === true ? { doctor: await mobileDoctor() } : {} } };
9819
9883
  },
9884
+ // THE PHONE A RUN IS DRIVING, as a picture: whatever session is open on
9885
+ // that platform — a run's, a recording's. Not effectful, never kept, and
9886
+ // nothing when no phone is open: the mirror then shows that.
9887
+ "mobile.peek": async (command) => {
9888
+ const platform = command.args["platform"];
9889
+ if (!isMobile(platform)) return { ok: false, reason: "failed", detail: "say which phone: android or ios" };
9890
+ const session = live.get(platform)?.session ?? openSessionOn(platform);
9891
+ if (!session) return { ok: true, result: { image: null } };
9892
+ try {
9893
+ const png = await session.screenshot();
9894
+ return { ok: true, result: { image: png.toString("base64"), device: session.deviceName } };
9895
+ } catch {
9896
+ return { ok: true, result: { image: null } };
9897
+ }
9898
+ },
9820
9899
  "mobile.screen": async (command) => {
9821
9900
  try {
9822
9901
  const l = require2(command);
@@ -9833,14 +9912,21 @@ function mobileHandlers(ctx) {
9833
9912
  },
9834
9913
  "mobile.session": async (command, project) => {
9835
9914
  const op = String(command.args["op"] ?? "");
9915
+ let claimed = null;
9836
9916
  try {
9837
9917
  if (op === "start") {
9838
- if (live) {
9839
- await live.session.close().catch(() => void 0);
9840
- live = null;
9841
- }
9842
9918
  const platform = command.args["platform"];
9843
9919
  if (!isMobile(platform)) return { ok: false, reason: "failed", detail: "say which phone: android or ios" };
9920
+ const prior = live.get(platform);
9921
+ if (prior) {
9922
+ live.delete(prior.platform);
9923
+ await prior.session.close().catch(() => void 0);
9924
+ ctx.lease?.release(ownerOf(prior), prior.platform);
9925
+ }
9926
+ const owner = command.args["record"] === true ? "recording" : "debug";
9927
+ const claim = ctx.lease?.acquire(owner, platform);
9928
+ if (claim && !claim.ok) return { ok: false, reason: "busy", detail: BrowserLease.refusal(claim.heldBy, claim.phase, platform) };
9929
+ claimed = { owner, platform };
9844
9930
  const config = command.args["config"] ?? {};
9845
9931
  const secrets = command.args["secrets"] ?? {};
9846
9932
  const device = command.args["device"] ? DeviceChoiceSchema.parse(command.args["device"]) : void 0;
@@ -9875,7 +9961,8 @@ function mobileHandlers(ctx) {
9875
9961
  ...auth?.profileId ? { signIn: auth } : {}
9876
9962
  };
9877
9963
  }
9878
- live = l2;
9964
+ live.set(l2.platform, l2);
9965
+ ctx.lease?.activate(owner, l2.platform);
9879
9966
  if (/^[a-z][\w+.-]*:\/\//i.test(startPath)) await session.openLink(startPath);
9880
9967
  const opening = [
9881
9968
  ...l2.draft?.signIn ? [RecordedStepSchema.parse({ id: "sign-in", type: "signIn", auth: l2.draft.signIn, role: "", name: "Sign in", scope: "", value: "", secret: false, legacySelector: "", proposals: [] })] : [],
@@ -9958,7 +10045,8 @@ function mobileHandlers(ctx) {
9958
10045
  return { ok: true, result: { steps: l.draft.steps } };
9959
10046
  }
9960
10047
  if (op === "stop" || op === "discard") {
9961
- live = null;
10048
+ live.delete(l.platform);
10049
+ ctx.lease?.release(ownerOf(l), l.platform);
9962
10050
  await l.session.close().catch(() => void 0);
9963
10051
  if (op === "discard" || !l.draft) return { ok: true, result: { ended: true } };
9964
10052
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -9982,6 +10070,7 @@ function mobileHandlers(ctx) {
9982
10070
  }
9983
10071
  return { ok: false, reason: "failed", detail: `unknown phone session operation "${op}"` };
9984
10072
  } catch (error) {
10073
+ if (claimed && !live.has(claimed.platform)) ctx.lease?.release(claimed.owner, claimed.platform);
9985
10074
  return { ok: false, reason: "failed", detail: error.message.split("\n")[0] ?? "the phone session failed" };
9986
10075
  }
9987
10076
  }
@@ -2568,6 +2568,10 @@ async function localCapabilities(target, udid, opts = {}) {
2568
2568
  "appium:appPackage": app.id,
2569
2569
  ...app.activity ? { "appium:appActivity": app.activity } : {},
2570
2570
  "appium:appWaitActivity": "*",
2571
+ // Its own ports: two phones on one Appium each need their own driver
2572
+ // server and picture stream, or the second session steals the first's.
2573
+ ...opts.ports?.system ? { "appium:systemPort": opts.ports.system } : {},
2574
+ ...opts.ports?.mjpeg ? { "appium:mjpegServerPort": opts.ports.mjpeg } : {},
2571
2575
  "appium:autoGrantPermissions": false,
2572
2576
  "appium:disableWindowAnimation": true,
2573
2577
  "appium:uiautomator2ServerLaunchTimeout": 12e4,
@@ -2584,6 +2588,8 @@ async function localCapabilities(target, udid, opts = {}) {
2584
2588
  "appium:bundleId": app.id,
2585
2589
  "appium:wdaLaunchTimeout": 24e4,
2586
2590
  "appium:wdaConnectionTimeout": 24e4,
2591
+ ...opts.ports?.wda ? { "appium:wdaLocalPort": opts.ports.wda } : {},
2592
+ ...opts.ports?.mjpeg ? { "appium:mjpegServerPort": opts.ports.mjpeg } : {},
2587
2593
  ...target.mobile.ios?.teamId ? { "appium:xcodeOrgId": target.mobile.ios.teamId, "appium:xcodeSigningId": "Apple Development" } : {}
2588
2594
  };
2589
2595
  }
@@ -3203,6 +3209,8 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
3203
3209
  throw last;
3204
3210
  }
3205
3211
  var uploaded = /* @__PURE__ */ new Map();
3212
+ var OPEN = /* @__PURE__ */ new Map();
3213
+ var openSessionOn = (platform) => OPEN.get(platform) ?? null;
3206
3214
  var MobileSession = class _MobileSession {
3207
3215
  constructor(driver, platform, appId, deviceName, cloud) {
3208
3216
  this.driver = driver;
@@ -3243,6 +3251,7 @@ var MobileSession = class _MobileSession {
3243
3251
  throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
3244
3252
  });
3245
3253
  const cloudSession = new _MobileSession(driver2, target.platform, app.id, choice2.device, true);
3254
+ OPEN.set(target.platform, cloudSession);
3246
3255
  await cloudSession.settle(2e4);
3247
3256
  return cloudSession;
3248
3257
  }
@@ -3256,12 +3265,14 @@ var MobileSession = class _MobileSession {
3256
3265
  const endpoint = await appiumServer();
3257
3266
  const buildFile = app.build ? await localBuild(app.build) : null;
3258
3267
  say(`Opening the app on ${known.name}\u2026`);
3259
- const capabilities = await localCapabilities(target, udid, { buildFile });
3268
+ const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
3269
+ const capabilities = await localCapabilities(target, udid, { buildFile, ports });
3260
3270
  const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
3261
3271
  const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
3262
3272
  throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
3263
3273
  });
3264
3274
  const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
3275
+ OPEN.set(target.platform, session);
3265
3276
  await session.terminateApp().catch(() => {
3266
3277
  });
3267
3278
  await session.launchApp().catch(() => {
@@ -3494,6 +3505,7 @@ var MobileSession = class _MobileSession {
3494
3505
  return { relPath: path4.basename(file), bytes: bytes.length };
3495
3506
  }
3496
3507
  async close() {
3508
+ if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
3497
3509
  if (this.recording) await this.stopVideo(null);
3498
3510
  await this.driver.close();
3499
3511
  }
@@ -4152,6 +4164,7 @@ export {
4152
4164
  CheckError,
4153
4165
  textOf,
4154
4166
  checkMobileAssertion,
4167
+ openSessionOn,
4155
4168
  MobileSession,
4156
4169
  MOBILE_TIMEOUTS,
4157
4170
  performMobileStep,
package/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  runtimeValue,
24
24
  start,
25
25
  status
26
- } from "./chunk-Z3SL46OX.js";
26
+ } from "./chunk-6AR63WTN.js";
27
27
  import {
28
28
  CLOUD_PROVIDERS,
29
29
  CLOUD_PROVIDER_NAME,
@@ -49,7 +49,7 @@ import {
49
49
  screenLines,
50
50
  stepRefusedOn,
51
51
  targetFor
52
- } from "./chunk-KDW65RBE.js";
52
+ } from "./chunk-FIR6NOLA.js";
53
53
 
54
54
  // src/cli.ts
55
55
  import { parseArgs } from "util";
@@ -31,6 +31,7 @@ import {
31
31
  mobileSetup,
32
32
  nodeAt,
33
33
  openMobileSession,
34
+ openSessionOn,
34
35
  performMobileStep,
35
36
  readScreen,
36
37
  replayMobileCase,
@@ -44,7 +45,7 @@ import {
44
45
  textOf,
45
46
  toolEnv,
46
47
  typesSecrets
47
- } from "./chunk-KDW65RBE.js";
48
+ } from "./chunk-FIR6NOLA.js";
48
49
  export {
49
50
  AGENT_HOME,
50
51
  APPIUM_DIR,
@@ -77,6 +78,7 @@ export {
77
78
  mobileSetup,
78
79
  nodeAt,
79
80
  openMobileSession,
81
+ openSessionOn,
80
82
  performMobileStep,
81
83
  readScreen,
82
84
  replayMobileCase,
package/dist/engine.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-Z3SL46OX.js";
13
- import "./chunk-KDW65RBE.js";
12
+ } from "./chunk-6AR63WTN.js";
13
+ import "./chunk-FIR6NOLA.js";
14
14
  export {
15
15
  AGENT_VERSION,
16
16
  browsersReady,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.52",
3
+ "version": "0.1.0-beta.53",
4
4
  "description": "Run Flowriki UI tests on your own computer. Connects this machine to your Flowriki account.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",