@flowriki/agent 0.1.0-beta.64 → 0.1.0-beta.66

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.
@@ -16,10 +16,12 @@ import os3 from "os";
16
16
  import path3 from "path";
17
17
  import { execFile as execFile2 } from "child_process";
18
18
  import fs4 from "fs";
19
- import os4 from "os";
20
19
  import path4 from "path";
20
+ import fs5 from "fs";
21
+ import os4 from "os";
22
+ import path5 from "path";
21
23
 
22
- // ../test-model/dist/chunk-I5COYCQ5.js
24
+ // ../test-model/dist/chunk-6JIZJVPN.js
23
25
  import { z as z2 } from "zod";
24
26
  import { z } from "zod";
25
27
  var PLATFORMS = ["web", "android", "ios"];
@@ -69,7 +71,10 @@ var CloudAccountSchema = z.object({
69
71
  provider: z.enum(CLOUD_PROVIDERS),
70
72
  username: z.string().trim().min(1).max(200),
71
73
  /** Sauce Labs data centre: "us-west-1", "eu-central-1", "us-east-4". */
72
- region: z.string().trim().max(40).optional(),
74
+ /** A Sauce Labs region — a word like us-west-1, never an address: it is
75
+ * spelled into the host the farm's key is sent to. Anything else is
76
+ * dropped, and the default region used. */
77
+ region: z.string().trim().max(40).optional().transform((v) => v && /^[a-z]{2}-[a-z]+-\d$/.test(v) ? v : void 0),
73
78
  keyRef: z.string().regex(/^[a-z][a-z0-9_]*$/)
74
79
  }).strict();
75
80
  var DeviceChoiceSchema = z.discriminatedUnion("kind", [
@@ -629,19 +634,19 @@ var CHECK_TARGET_FIELDS = [
629
634
  var unset = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
630
635
  function mirrorCheckTargets(steps) {
631
636
  return steps.map((step) => {
632
- const held = step;
633
- if (held["type"] !== "assert") return step;
634
- const proposals = held["proposals"];
637
+ const held2 = step;
638
+ if (held2["type"] !== "assert") return step;
639
+ const proposals = held2["proposals"];
635
640
  if (!Array.isArray(proposals)) return step;
636
641
  const first = proposals[0];
637
642
  const target = first?.["target"];
638
643
  if (!target) return step;
639
644
  const fill = {};
640
645
  for (const field of CHECK_TARGET_FIELDS) {
641
- if (!unset(held[field]) || unset(target[field])) continue;
646
+ if (!unset(held2[field]) || unset(target[field])) continue;
642
647
  fill[field] = target[field];
643
648
  }
644
- return Object.keys(fill).length === 0 ? step : { ...held, ...fill };
649
+ return Object.keys(fill).length === 0 ? step : { ...held2, ...fill };
645
650
  });
646
651
  }
647
652
 
@@ -660,6 +665,9 @@ import { z as z11 } from "zod";
660
665
  import { z as z12 } from "zod";
661
666
  import { z as z13 } from "zod";
662
667
  import { z as z14 } from "zod";
668
+ import { z as z15 } from "zod";
669
+ import { z as z16 } from "zod";
670
+ import { z as z17 } from "zod";
663
671
  var EngineSchema = z3.enum(["chromium", "firefox", "webkit", "android", "ios"]);
664
672
  var CapabilitySchema = z3.object({
665
673
  id: z3.string(),
@@ -1446,11 +1454,52 @@ var HttpHelperSchema = z5.object({
1446
1454
  pattern: z5.string().max(500).default("")
1447
1455
  }).strict().default({})
1448
1456
  }).strict();
1457
+ var CommandLineSchema = z5.object({
1458
+ program: z5.string().trim().min(1).max(500),
1459
+ args: z5.array(z5.string().max(2e3)).max(50).default([])
1460
+ }).strict();
1461
+ var EnvName = z5.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "an environment variable name is letters, digits and _");
1462
+ var CommandHelperSchema = z5.object({
1463
+ kind: z5.literal("command"),
1464
+ /** What runs everywhere, unless a system has its own below. */
1465
+ run: CommandLineSchema,
1466
+ /** The same job on another system — a different program, or a .cmd on
1467
+ * Windows. A tester's computer uses its own when there is one. */
1468
+ perSystem: z5.object({
1469
+ macos: CommandLineSchema.optional(),
1470
+ windows: CommandLineSchema.optional(),
1471
+ linux: CommandLineSchema.optional()
1472
+ }).strict().default({}),
1473
+ /** Where it runs: a folder inside the project's checkout, or empty for the
1474
+ * project's folder (the agent's own folder when there is no checkout). */
1475
+ folder: z5.string().max(500).default(""),
1476
+ /** Plain environment variables, in the repository. */
1477
+ env: z5.record(EnvName, z5.string().max(2e3)).default({}),
1478
+ /** Environment variable → vault reference: how a token reaches the command
1479
+ * without ever being on its command line. */
1480
+ secretEnv: z5.record(EnvName, SecretRef).default({}),
1481
+ /** `run` waits for it to finish; `background` starts it, waits until it is
1482
+ * ready, and stops it when the test ends — a mock server, a simulator. */
1483
+ mode: z5.enum(["run", "background"]).default("run"),
1484
+ /** Background: the output line that says it is ready. Empty waits a
1485
+ * couple of seconds instead. */
1486
+ readyPattern: z5.string().max(500).default(""),
1487
+ timeoutSeconds: z5.number().int().min(1).max(600).default(60),
1488
+ /** Refused unless it ends with this exit code. */
1489
+ expectExit: z5.number().int().min(0).max(255).default(0),
1490
+ extract: z5.object({
1491
+ take: z5.enum(["output", "pattern", "json", "nothing"]).default("output"),
1492
+ pattern: z5.string().max(500).default(""),
1493
+ /** For json: a dot path — `device.id`. */
1494
+ path: z5.string().max(300).default("")
1495
+ }).strict().default({})
1496
+ }).strict();
1449
1497
  var HelperConfigSchema = z5.discriminatedUnion("kind", [
1450
1498
  MailboxHelperSchema,
1451
1499
  TotpHelperSchema,
1452
1500
  SmsHelperSchema,
1453
- HttpHelperSchema
1501
+ HttpHelperSchema,
1502
+ CommandHelperSchema
1454
1503
  ]);
1455
1504
  var HelperSchema = z5.object({
1456
1505
  schemaVersion: z5.literal(HELPER_SCHEMA_VERSION),
@@ -1467,7 +1516,9 @@ var HelperSchema = z5.object({
1467
1516
  const patterns = [
1468
1517
  helper.config.kind === "mailbox" && helper.config.extract.take === "code" ? helper.config.extract.pattern : "",
1469
1518
  helper.config.kind === "sms" ? helper.config.extract.pattern : "",
1470
- helper.config.kind === "http" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : ""
1519
+ helper.config.kind === "http" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : "",
1520
+ helper.config.kind === "command" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : "",
1521
+ helper.config.kind === "command" ? helper.config.readyPattern : ""
1471
1522
  ];
1472
1523
  for (const pattern of patterns) {
1473
1524
  if (pattern && !PatternSchema.safeParse(pattern).success) {
@@ -1475,6 +1526,12 @@ var HelperSchema = z5.object({
1475
1526
  }
1476
1527
  }
1477
1528
  });
1529
+ function commandFor(config, system) {
1530
+ return config.perSystem[system] ?? config.run;
1531
+ }
1532
+ function commandSystemOf(platform) {
1533
+ return platform === "win32" ? "windows" : platform === "darwin" ? "macos" : "linux";
1534
+ }
1478
1535
  var DeliveredHelperSchema = z5.object({
1479
1536
  id: z5.string().min(1),
1480
1537
  name: z5.string(),
@@ -1832,7 +1889,7 @@ function decodeEntities(text) {
1832
1889
  var nameKey = (name) => decodeEntities(name).toLowerCase().replace(/&/g, " and ").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
1833
1890
  var PLATFORM_PART = "(web|android|ios)";
1834
1891
  var LEARNED_PATH = new RegExp(`^(explore\\/${PLATFORM_PART}\\.md|memory\\/sessions\\/[a-z0-9][a-z0-9-]{0,120}\\.md)$`);
1835
- var isLearnedPath = (path8) => LEARNED_PATH.test(path8);
1892
+ var isLearnedPath = (path9) => LEARNED_PATH.test(path9);
1836
1893
  var LearnedSchema = z11.object({
1837
1894
  id: z11.string().regex(/^lrn_[a-z0-9_-]+$/),
1838
1895
  projectId: z11.string().min(1),
@@ -1873,6 +1930,29 @@ var WhatToDoSchema = z13.object({
1873
1930
  skipped: z13.array(z13.string().min(1).max(80)).max(100),
1874
1931
  updatedAt: z13.string()
1875
1932
  }).strict();
1933
+ var BLOCKER_KINDS = ["app-bug", "needs-person", "unclear-step", "not-found", "stuck"];
1934
+ var CaseStandingSchema = z14.discriminatedUnion("state", [
1935
+ z14.object({
1936
+ state: z14.literal("blocked"),
1937
+ kind: z14.enum(BLOCKER_KINDS),
1938
+ /** The business step it stopped at, counted from 1 — when it was one. */
1939
+ line: z14.number().int().positive().max(500).optional(),
1940
+ lineText: z14.string().max(500).optional(),
1941
+ detail: z14.string().min(1).max(1500),
1942
+ /** What would unblock it, said for a person. */
1943
+ needs: z14.string().max(600).optional(),
1944
+ platform: z14.enum(["web", "android", "ios"]).optional(),
1945
+ by: z14.string().max(200),
1946
+ at: z14.string()
1947
+ }).strict(),
1948
+ z14.object({
1949
+ state: z14.literal("manual"),
1950
+ /** Why it is run by hand, when somebody said. */
1951
+ reason: z14.string().max(600).optional(),
1952
+ by: z14.string().max(200),
1953
+ at: z14.string()
1954
+ }).strict()
1955
+ ]);
1876
1956
  var CaseRegistrySchema = z14.object({
1877
1957
  id: z14.string().regex(/^creg_/),
1878
1958
  projectId: z14.string().min(1),
@@ -1880,11 +1960,83 @@ var CaseRegistrySchema = z14.object({
1880
1960
  cases: z14.record(z14.string(), z14.number().int().positive()),
1881
1961
  features: z14.record(z14.string(), z14.number().int().positive()),
1882
1962
  reviews: z14.record(z14.string(), z14.object({ hash: z14.string(), by: z14.string().max(200), at: z14.string() }).strict()),
1963
+ /** Blocked and manual cases, by case id. Absent on a project with none. */
1964
+ standing: z14.record(z14.string(), CaseStandingSchema).optional(),
1883
1965
  updatedAt: z14.string()
1884
1966
  }).strict();
1967
+ var BranchChoiceSchema = z15.object({
1968
+ id: z15.string().regex(/^bch_/),
1969
+ projectId: z15.string().min(1),
1970
+ userId: z15.string().min(1),
1971
+ /** The branch (a change id), or null for the published version. */
1972
+ changeId: z15.string().min(1).max(100).nullable(),
1973
+ at: z15.string()
1974
+ }).strict();
1975
+ var TokensSchema = z16.object({
1976
+ /** Read fresh — the request, minus what came from the cache. */
1977
+ input: z16.number().int().nonnegative(),
1978
+ output: z16.number().int().nonnegative(),
1979
+ /** Read from the prompt cache: a tenth of the price of fresh input. */
1980
+ cacheRead: z16.number().int().nonnegative(),
1981
+ /** Written to the prompt cache, so later turns can read it cheaply. */
1982
+ cacheWrite: z16.number().int().nonnegative()
1983
+ }).strict();
1984
+ var USAGE_STAGES = ["write", "automate", "setup", "knowledge", "reply"];
1985
+ var USAGE_KINDS = ["automate", "reautomate", "heal", "update", "write", "explore", "chat"];
1986
+ function usageKindOf(shown) {
1987
+ const s = shown.trim();
1988
+ if (/^Heal\b/i.test(s)) return "heal";
1989
+ if (/^Re-automate\b/i.test(s)) return "reautomate";
1990
+ if (/^Automate\b/i.test(s)) return "automate";
1991
+ if (/business steps of .* (just )?changed/i.test(s) || /^Update\b/i.test(s)) return "update";
1992
+ if (/^Explore\b|\bexplore (the|this) (app|site|website)\b/i.test(s)) return "explore";
1993
+ if (/^(Write|Import)\b.*test cases?/i.test(s)) return "write";
1994
+ return "chat";
1995
+ }
1996
+ var UsagePartSchema = TokensSchema.extend({
1997
+ stage: z16.enum(USAGE_STAGES),
1998
+ /** The tool the turn called; null for a turn that called none. */
1999
+ tool: z16.string().max(80).nullable(),
2000
+ /** The test case the work was for, when one was being worked on. */
2001
+ caseName: z16.string().max(300).nullable(),
2002
+ folder: z16.string().max(300).nullable(),
2003
+ /** Its id — TC-12 — when the assistant named it by id. */
2004
+ caseKey: z16.string().max(40).nullable(),
2005
+ /** The flow being recorded, when one was. */
2006
+ flow: z16.string().max(300).nullable(),
2007
+ /** How many turns are in this line. */
2008
+ turns: z16.number().int().nonnegative()
2009
+ }).strict();
2010
+ var UsageRecordSchema = z16.object({
2011
+ id: z16.string().regex(/^use_/),
2012
+ projectId: z16.string().min(1),
2013
+ userId: z16.string().min(1),
2014
+ jobId: z16.string().min(1),
2015
+ kind: z16.enum(USAGE_KINDS),
2016
+ /** The start of what was asked, so a list of jobs reads as one. */
2017
+ asked: z16.string().max(200),
2018
+ platform: z16.enum(["web", "android", "ios"]).nullable(),
2019
+ startedAt: z16.string(),
2020
+ finishedAt: z16.string().nullable(),
2021
+ status: z16.enum(["running", "done", "failed", "stopped"]),
2022
+ /** Every turn added up, from the turns as they happened. */
2023
+ totals: TokensSchema,
2024
+ turns: z16.number().int().nonnegative(),
2025
+ /** What the model said the whole job cost, in US dollars — at API prices;
2026
+ * on a Claude plan it is what the same work would cost there. */
2027
+ costUsd: z16.number().nonnegative().nullable(),
2028
+ durationMs: z16.number().int().nonnegative().nullable(),
2029
+ parts: z16.array(UsagePartSchema).max(600)
2030
+ }).strict();
2031
+ var StepRenameSchema = z17.object({
2032
+ occurrenceId: z17.string().min(1).max(200),
2033
+ stepId: z17.string().min(1).max(200),
2034
+ from: z17.string().min(1).max(500),
2035
+ to: z17.string().trim().min(1).max(200)
2036
+ }).strict();
1885
2037
 
1886
2038
  // ../engine-appium/dist/index.js
1887
- import path5 from "path";
2039
+ import path6 from "path";
1888
2040
 
1889
2041
  // ../core/dist/index.js
1890
2042
  function proposeAssertions(before, after) {
@@ -2217,9 +2369,9 @@ var STEP_PRESENTATION = {
2217
2369
  };
2218
2370
 
2219
2371
  // ../engine-appium/dist/index.js
2220
- import path6 from "path";
2221
- import fs5 from "fs";
2222
2372
  import path7 from "path";
2373
+ import fs6 from "fs";
2374
+ import path8 from "path";
2223
2375
  import { spawn as spawn3 } from "child_process";
2224
2376
  var WebDriverError = class extends Error {
2225
2377
  constructor(message, error, status) {
@@ -2231,10 +2383,10 @@ var WebDriverError = class extends Error {
2231
2383
  status;
2232
2384
  };
2233
2385
  var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecc";
2234
- async function call(endpoint, method, path8, body, timeoutMs = 12e4) {
2386
+ async function call(endpoint, method, path9, body, timeoutMs = 12e4) {
2235
2387
  const headers = { "content-type": "application/json; charset=utf-8" };
2236
2388
  if (endpoint.username) headers["authorization"] = `Basic ${Buffer.from(`${endpoint.username}:${endpoint.password ?? ""}`).toString("base64")}`;
2237
- const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path8}`, {
2389
+ const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path9}`, {
2238
2390
  method,
2239
2391
  headers,
2240
2392
  ...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {},
@@ -2273,11 +2425,11 @@ var WebDriverSession = class _WebDriverSession {
2273
2425
  return new _WebDriverSession(endpoint, value.sessionId, value.capabilities ?? {});
2274
2426
  }
2275
2427
  /** Any other command — Appium's own extensions live under /appium/. */
2276
- raw(method, path8, body, timeoutMs) {
2277
- return this.req(method, path8, body, timeoutMs);
2428
+ raw(method, path9, body, timeoutMs) {
2429
+ return this.req(method, path9, body, timeoutMs);
2278
2430
  }
2279
- req(method, path8, body, timeoutMs) {
2280
- return call(this.endpoint, method, `/session/${this.id}${path8}`, body, timeoutMs);
2431
+ req(method, path9, body, timeoutMs) {
2432
+ return call(this.endpoint, method, `/session/${this.id}${path9}`, body, timeoutMs);
2281
2433
  }
2282
2434
  source() {
2283
2435
  return this.req("GET", "/source");
@@ -2380,8 +2532,8 @@ function appiumPath() {
2380
2532
  }
2381
2533
  function javaMajor(home) {
2382
2534
  try {
2383
- const release = fs.readFileSync(path.join(home, "release"), "utf8");
2384
- const v = /JAVA_VERSION="(\d+)(?:\.(\d+))?/.exec(release);
2535
+ const release2 = fs.readFileSync(path.join(home, "release"), "utf8");
2536
+ const v = /JAVA_VERSION="(\d+)(?:\.(\d+))?/.exec(release2);
2385
2537
  if (!v) return 0;
2386
2538
  return v[1] === "1" ? Number(v[2] ?? 0) : Number(v[1]);
2387
2539
  } catch {
@@ -3304,6 +3456,88 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
3304
3456
  }
3305
3457
  throw last;
3306
3458
  }
3459
+ var held = /* @__PURE__ */ new Map();
3460
+ var lockDir = () => path4.join(process.env["FLOWRIKI_HOME"] || AGENT_HOME, "locks");
3461
+ var fileFor = (udid) => path4.join(lockDir(), `device-${udid.replace(/[^A-Za-z0-9._-]/g, "_")}.lock`);
3462
+ var alive = (pid) => {
3463
+ try {
3464
+ process.kill(pid, 0);
3465
+ return true;
3466
+ } catch (error) {
3467
+ return error.code === "EPERM";
3468
+ }
3469
+ };
3470
+ var DeviceInUse = class extends Error {
3471
+ constructor(device, pid) {
3472
+ super(`${device} is being used by another Flowriki job on this computer \u2014 wait for it to finish, or stop it first`);
3473
+ this.device = device;
3474
+ this.pid = pid;
3475
+ this.name = "DeviceInUse";
3476
+ }
3477
+ device;
3478
+ pid;
3479
+ };
3480
+ function lockDevice(udid, name = udid) {
3481
+ const file = fileFor(udid);
3482
+ const mine = held.get(file);
3483
+ if (mine) {
3484
+ held.set(file, mine + 1);
3485
+ return once(() => release(file));
3486
+ }
3487
+ fs4.mkdirSync(lockDir(), { recursive: true, mode: 448 });
3488
+ for (let attempt = 0; attempt < 2; attempt++) {
3489
+ try {
3490
+ const fd = fs4.openSync(file, "wx", 384);
3491
+ fs4.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
3492
+ fs4.closeSync(fd);
3493
+ held.set(file, 1);
3494
+ return once(() => release(file));
3495
+ } catch (error) {
3496
+ if (error.code !== "EEXIST") throw error;
3497
+ let owner = 0;
3498
+ try {
3499
+ owner = Number(JSON.parse(fs4.readFileSync(file, "utf8")).pid) || 0;
3500
+ } catch {
3501
+ }
3502
+ if (owner && owner !== process.pid && alive(owner)) throw new DeviceInUse(name, owner);
3503
+ try {
3504
+ fs4.unlinkSync(file);
3505
+ } catch {
3506
+ }
3507
+ }
3508
+ }
3509
+ throw new DeviceInUse(name, 0);
3510
+ }
3511
+ function release(file) {
3512
+ const count = held.get(file) ?? 0;
3513
+ if (count > 1) {
3514
+ held.set(file, count - 1);
3515
+ return;
3516
+ }
3517
+ held.delete(file);
3518
+ try {
3519
+ const owner = Number(JSON.parse(fs4.readFileSync(file, "utf8")).pid);
3520
+ if (owner === process.pid) fs4.unlinkSync(file);
3521
+ } catch {
3522
+ }
3523
+ }
3524
+ function once(fn) {
3525
+ let done = false;
3526
+ return () => {
3527
+ if (!done) {
3528
+ done = true;
3529
+ fn();
3530
+ }
3531
+ };
3532
+ }
3533
+ process.once("exit", () => {
3534
+ for (const file of held.keys()) {
3535
+ try {
3536
+ fs4.unlinkSync(file);
3537
+ } catch {
3538
+ }
3539
+ }
3540
+ });
3307
3541
  var uploaded = /* @__PURE__ */ new Map();
3308
3542
  var OPEN = /* @__PURE__ */ new Map();
3309
3543
  var openSessionOn = (platform) => OPEN.get(platform) ?? null;
@@ -3321,6 +3555,8 @@ var MobileSession = class _MobileSession {
3321
3555
  deviceName;
3322
3556
  cloud;
3323
3557
  cached = null;
3558
+ /** Lets go of this phone for other Flowriki processes — see device-lock. */
3559
+ releaseDevice = null;
3324
3560
  static async open(target, opts = {}) {
3325
3561
  const app = appOf(target);
3326
3562
  const say = opts.onProgress ?? (() => {
@@ -3342,11 +3578,11 @@ var MobileSession = class _MobileSession {
3342
3578
  }
3343
3579
  }
3344
3580
  say(`Opening ${choice2.device} on ${CLOUD_PROVIDER_NAME[choice2.provider]}\u2026`);
3345
- const { endpoint: endpoint2, capabilities: capabilities2 } = cloudCapabilities(target, account, key, appRef);
3346
- const driver2 = await WebDriverSession.create(endpoint2, capabilities2).catch((e) => {
3581
+ const { endpoint, capabilities } = cloudCapabilities(target, account, key, appRef);
3582
+ const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
3347
3583
  throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
3348
3584
  });
3349
- const cloudSession = new _MobileSession(driver2, target.platform, app.id, choice2.device, true);
3585
+ const cloudSession = new _MobileSession(driver, target.platform, app.id, choice2.device, true);
3350
3586
  OPEN.set(target.platform, cloudSession);
3351
3587
  await cloudSession.settle(2e4);
3352
3588
  return cloudSession;
@@ -3358,23 +3594,30 @@ var MobileSession = class _MobileSession {
3358
3594
  if (known.platform !== target.platform) throw new Error(`${known.name} is ${known.platform === "ios" ? "an iOS" : "an Android"} device; this flow runs on ${target.platform === "ios" ? "iOS" : "Android"}.`);
3359
3595
  if (known.state !== "ready") say(`Starting ${known.name}\u2026`);
3360
3596
  const udid = await bootDevice(known);
3361
- const endpoint = await appiumServer();
3362
- const buildFile = app.build ? await localBuild(app.build) : null;
3363
- say(`Opening the app on ${known.name}\u2026`);
3364
- const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
3365
- const activity = target.platform === "android" && !app.activity && !buildFile ? await launchActivityOf(udid, app.id) : null;
3366
- const capabilities = await localCapabilities(target, udid, { buildFile, ports, ...activity ? { activity } : {} });
3367
- const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
3368
- const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
3369
- throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
3370
- });
3371
- const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
3372
- OPEN.set(target.platform, session);
3373
- await session.terminateApp().catch(() => {
3374
- });
3375
- await session.launchApp().catch(() => {
3376
- });
3377
- return session;
3597
+ const releaseDevice = lockDevice(udid, known.name);
3598
+ try {
3599
+ const endpoint = await appiumServer();
3600
+ const buildFile = app.build ? await localBuild(app.build) : null;
3601
+ say(`Opening the app on ${known.name}\u2026`);
3602
+ const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
3603
+ const activity = target.platform === "android" && !app.activity && !buildFile ? await launchActivityOf(udid, app.id) : null;
3604
+ const capabilities = await localCapabilities(target, udid, { buildFile, ports, ...activity ? { activity } : {} });
3605
+ const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
3606
+ const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
3607
+ throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
3608
+ });
3609
+ const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
3610
+ session.releaseDevice = releaseDevice;
3611
+ OPEN.set(target.platform, session);
3612
+ await session.terminateApp().catch(() => {
3613
+ });
3614
+ await session.launchApp().catch(() => {
3615
+ });
3616
+ return session;
3617
+ } catch (error) {
3618
+ releaseDevice();
3619
+ throw error;
3620
+ }
3378
3621
  }
3379
3622
  // ── Reading ────────────────────────────────────────────────────────────
3380
3623
  async screen() {
@@ -3405,8 +3648,8 @@ var MobileSession = class _MobileSession {
3405
3648
  async screenshot(file) {
3406
3649
  const png = Buffer.from(await this.driver.screenshot(), "base64");
3407
3650
  if (file) {
3408
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
3409
- fs4.writeFileSync(file, png);
3651
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
3652
+ fs5.writeFileSync(file, png);
3410
3653
  }
3411
3654
  return png;
3412
3655
  }
@@ -3607,14 +3850,19 @@ var MobileSession = class _MobileSession {
3607
3850
  const b64 = await this.driver.raw("POST", "/appium/stop_recording_screen", { options: {} }, 12e4).catch(() => "");
3608
3851
  if (!file || !b64) return null;
3609
3852
  const bytes = Buffer.from(b64, "base64");
3610
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
3611
- fs4.writeFileSync(file, bytes);
3612
- return { relPath: path4.basename(file), bytes: bytes.length };
3853
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
3854
+ fs5.writeFileSync(file, bytes);
3855
+ return { relPath: path5.basename(file), bytes: bytes.length };
3613
3856
  }
3614
3857
  async close() {
3615
3858
  if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
3616
- if (this.recording) await this.stopVideo(null);
3617
- await this.driver.close();
3859
+ try {
3860
+ if (this.recording) await this.stopVideo(null);
3861
+ await this.driver.close();
3862
+ } finally {
3863
+ this.releaseDevice?.();
3864
+ this.releaseDevice = null;
3865
+ }
3618
3866
  }
3619
3867
  };
3620
3868
  async function launchActivityOf(udid, appPackage) {
@@ -3630,18 +3878,18 @@ async function launchActivityOf(udid, appPackage) {
3630
3878
  }
3631
3879
  async function shrink(png, maxPx) {
3632
3880
  if (process.platform !== "darwin") return png;
3633
- const dir = fs4.mkdtempSync(path4.join(os4.tmpdir(), "flowriki-shot-"));
3634
- const file = path4.join(dir, "s.png");
3881
+ const dir = fs5.mkdtempSync(path5.join(os4.tmpdir(), "flowriki-shot-"));
3882
+ const file = path5.join(dir, "s.png");
3635
3883
  try {
3636
- fs4.writeFileSync(file, png);
3884
+ fs5.writeFileSync(file, png);
3637
3885
  await new Promise((resolve, reject) => {
3638
3886
  execFile2("/usr/bin/sips", ["-Z", String(maxPx), file], { timeout: 5e3 }, (error) => error ? reject(error) : resolve());
3639
3887
  });
3640
- return fs4.readFileSync(file);
3888
+ return fs5.readFileSync(file);
3641
3889
  } catch {
3642
3890
  return png;
3643
3891
  } finally {
3644
- fs4.rmSync(dir, { recursive: true, force: true });
3892
+ fs5.rmSync(dir, { recursive: true, force: true });
3645
3893
  }
3646
3894
  }
3647
3895
  var ANDROID_KEYS = {
@@ -3901,7 +4149,7 @@ async function runMobileSteps(session, steps, opts) {
3901
4149
  if (opts.evidenceDir && (shots === "always" || shots === "on-failure" && result.status === "failed")) {
3902
4150
  const file = `step-${String((opts.stepOffset ?? 0) + results.length + 1).padStart(2, "0")}.png`;
3903
4151
  try {
3904
- await session.screenshot(path5.join(opts.evidenceDir, file));
4152
+ await session.screenshot(path6.join(opts.evidenceDir, file));
3905
4153
  result.evidence = { screenshot: file, url: await session.currentScreenName(), title: session.deviceName };
3906
4154
  } catch {
3907
4155
  }
@@ -3925,7 +4173,7 @@ var typesSecrets = (steps) => steps.some((s) => s.secret || Boolean(s.secretRef)
3925
4173
  var filmsThis = (policy, steps, evidenceDir) => Boolean(evidenceDir) && Boolean(policy) && policy.mode !== "off" && (policy.allowUnmasked === true || !typesSecrets(steps));
3926
4174
  async function finishVideo(session, policy, evidenceDir, passed, name = "phone") {
3927
4175
  const keep = Boolean(evidenceDir) && (policy?.mode === "always" || policy?.mode === "on-failure" && !passed);
3928
- const saved = await session.stopVideo(keep ? path6.join(evidenceDir, `video-${name}.mp4`) : null).catch(() => null);
4176
+ const saved = await session.stopVideo(keep ? path7.join(evidenceDir, `video-${name}.mp4`) : null).catch(() => null);
3929
4177
  return saved ? [{ kind: "video", relPath: saved.relPath, bytes: saved.bytes }] : [];
3930
4178
  }
3931
4179
  async function chooseDevice(platform, mobile, asked, where) {
@@ -4054,20 +4302,20 @@ function stream(file, args, env, cwd, say) {
4054
4302
  child.stdout.on("data", each);
4055
4303
  child.stderr.on("data", each);
4056
4304
  child.on("error", reject);
4057
- child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${path7.basename(file)} ${args.join(" ")} failed (${code})`)));
4305
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${path8.basename(file)} ${args.join(" ")} failed (${code})`)));
4058
4306
  });
4059
4307
  }
4060
4308
  async function mobileSetup(say = console.log) {
4061
4309
  const npm = onPath("npm");
4062
4310
  if (!npm) throw new Error("npm was not found \u2014 install Node.js 20 or newer.");
4063
- fs5.mkdirSync(path7.join(APPIUM_DIR, "home"), { recursive: true });
4064
- if (!fs5.existsSync(path7.join(APPIUM_DIR, "package.json"))) {
4065
- fs5.writeFileSync(path7.join(APPIUM_DIR, "package.json"), JSON.stringify({ name: "flowriki-appium", private: true }, null, 2));
4311
+ fs6.mkdirSync(path8.join(APPIUM_DIR, "home"), { recursive: true });
4312
+ if (!fs6.existsSync(path8.join(APPIUM_DIR, "package.json"))) {
4313
+ fs6.writeFileSync(path8.join(APPIUM_DIR, "package.json"), JSON.stringify({ name: "flowriki-appium", private: true }, null, 2));
4066
4314
  }
4067
- const env = { ...await toolEnv(), APPIUM_HOME: path7.join(APPIUM_DIR, "home") };
4315
+ const env = { ...await toolEnv(), APPIUM_HOME: path8.join(APPIUM_DIR, "home") };
4068
4316
  say("Installing Appium\u2026");
4069
4317
  await stream(npm, ["install", "--no-audit", "--no-fund", "appium@2"], env, APPIUM_DIR, say);
4070
- const bin = path7.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
4318
+ const bin = path8.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
4071
4319
  const have = await installedDrivers();
4072
4320
  const wanted = ["uiautomator2", ...process.platform === "darwin" ? ["xcuitest"] : []];
4073
4321
  for (const driver of wanted) {
@@ -4253,6 +4501,8 @@ export {
4253
4501
  sameIdentity,
4254
4502
  PrerequisiteProblem,
4255
4503
  expandForRun,
4504
+ commandFor,
4505
+ commandSystemOf,
4256
4506
  DeliveredHelperSchema,
4257
4507
  exploreSummary,
4258
4508
  CODEBASE_TAG_NAME,
@@ -4261,6 +4511,7 @@ export {
4261
4511
  WEB_CLOUD_BROWSER_NAME,
4262
4512
  webCloudOf,
4263
4513
  nameKey,
4514
+ usageKindOf,
4264
4515
  proposeAssertions,
4265
4516
  hasObservableChange,
4266
4517
  normalizeStartUrl,
@@ -4307,6 +4558,8 @@ export {
4307
4558
  CheckError,
4308
4559
  textOf,
4309
4560
  checkMobileAssertion,
4561
+ DeviceInUse,
4562
+ lockDevice,
4310
4563
  openSessionOn,
4311
4564
  MobileSession,
4312
4565
  shrink,