@flowriki/agent 0.1.0-beta.63 → 0.1.0-beta.65

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
 
@@ -659,6 +664,10 @@ import { z as z10 } from "zod";
659
664
  import { z as z11 } from "zod";
660
665
  import { z as z12 } from "zod";
661
666
  import { z as z13 } from "zod";
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";
662
671
  var EngineSchema = z3.enum(["chromium", "firefox", "webkit", "android", "ios"]);
663
672
  var CapabilitySchema = z3.object({
664
673
  id: z3.string(),
@@ -867,6 +876,10 @@ var TestCaseSchema = z32.object({
867
876
  createdAt: z32.string(),
868
877
  createdBy: z32.string().default(""),
869
878
  updatedAt: z32.string(),
879
+ /** WHO LAST CHANGED IT — a person's id, or "Claude" for the assistant — so
880
+ * a change the assistant made can be told from one a person made. Absent
881
+ * on cases saved before this was kept. Never sent to a machine. */
882
+ updatedBy: z32.string().max(200).optional(),
870
883
  /** The same optimistic-concurrency counter a flow carries, so an edit made
871
884
  * against a stale copy is refused rather than overwriting. */
872
885
  revision: z32.number().int().nonnegative().default(0)
@@ -1768,6 +1781,8 @@ var CaseUpdateRequestSchema = z9.object({
1768
1781
  instruction: z9.string().min(1).max(4e3),
1769
1782
  cases: z9.array(z9.object({
1770
1783
  id: z9.string().min(1),
1784
+ /** Its id as people say it — TC-12. */
1785
+ key: z9.string().max(20).optional(),
1771
1786
  name: z9.string(),
1772
1787
  feature: z9.string(),
1773
1788
  businessSteps: z9.array(BusinessStepSchema)
@@ -1825,7 +1840,7 @@ function decodeEntities(text) {
1825
1840
  var nameKey = (name) => decodeEntities(name).toLowerCase().replace(/&/g, " and ").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
1826
1841
  var PLATFORM_PART = "(web|android|ios)";
1827
1842
  var LEARNED_PATH = new RegExp(`^(explore\\/${PLATFORM_PART}\\.md|memory\\/sessions\\/[a-z0-9][a-z0-9-]{0,120}\\.md)$`);
1828
- var isLearnedPath = (path8) => LEARNED_PATH.test(path8);
1843
+ var isLearnedPath = (path9) => LEARNED_PATH.test(path9);
1829
1844
  var LearnedSchema = z11.object({
1830
1845
  id: z11.string().regex(/^lrn_[a-z0-9_-]+$/),
1831
1846
  projectId: z11.string().min(1),
@@ -1866,9 +1881,113 @@ var WhatToDoSchema = z13.object({
1866
1881
  skipped: z13.array(z13.string().min(1).max(80)).max(100),
1867
1882
  updatedAt: z13.string()
1868
1883
  }).strict();
1884
+ var BLOCKER_KINDS = ["app-bug", "needs-person", "unclear-step", "not-found", "stuck"];
1885
+ var CaseStandingSchema = z14.discriminatedUnion("state", [
1886
+ z14.object({
1887
+ state: z14.literal("blocked"),
1888
+ kind: z14.enum(BLOCKER_KINDS),
1889
+ /** The business step it stopped at, counted from 1 — when it was one. */
1890
+ line: z14.number().int().positive().max(500).optional(),
1891
+ lineText: z14.string().max(500).optional(),
1892
+ detail: z14.string().min(1).max(1500),
1893
+ /** What would unblock it, said for a person. */
1894
+ needs: z14.string().max(600).optional(),
1895
+ platform: z14.enum(["web", "android", "ios"]).optional(),
1896
+ by: z14.string().max(200),
1897
+ at: z14.string()
1898
+ }).strict(),
1899
+ z14.object({
1900
+ state: z14.literal("manual"),
1901
+ /** Why it is run by hand, when somebody said. */
1902
+ reason: z14.string().max(600).optional(),
1903
+ by: z14.string().max(200),
1904
+ at: z14.string()
1905
+ }).strict()
1906
+ ]);
1907
+ var CaseRegistrySchema = z14.object({
1908
+ id: z14.string().regex(/^creg_/),
1909
+ projectId: z14.string().min(1),
1910
+ next: z14.object({ case: z14.number().int().positive(), feature: z14.number().int().positive() }).strict(),
1911
+ cases: z14.record(z14.string(), z14.number().int().positive()),
1912
+ features: z14.record(z14.string(), z14.number().int().positive()),
1913
+ reviews: z14.record(z14.string(), z14.object({ hash: z14.string(), by: z14.string().max(200), at: z14.string() }).strict()),
1914
+ /** Blocked and manual cases, by case id. Absent on a project with none. */
1915
+ standing: z14.record(z14.string(), CaseStandingSchema).optional(),
1916
+ updatedAt: z14.string()
1917
+ }).strict();
1918
+ var BranchChoiceSchema = z15.object({
1919
+ id: z15.string().regex(/^bch_/),
1920
+ projectId: z15.string().min(1),
1921
+ userId: z15.string().min(1),
1922
+ /** The branch (a change id), or null for the published version. */
1923
+ changeId: z15.string().min(1).max(100).nullable(),
1924
+ at: z15.string()
1925
+ }).strict();
1926
+ var TokensSchema = z16.object({
1927
+ /** Read fresh — the request, minus what came from the cache. */
1928
+ input: z16.number().int().nonnegative(),
1929
+ output: z16.number().int().nonnegative(),
1930
+ /** Read from the prompt cache: a tenth of the price of fresh input. */
1931
+ cacheRead: z16.number().int().nonnegative(),
1932
+ /** Written to the prompt cache, so later turns can read it cheaply. */
1933
+ cacheWrite: z16.number().int().nonnegative()
1934
+ }).strict();
1935
+ var USAGE_STAGES = ["write", "automate", "setup", "knowledge", "reply"];
1936
+ var USAGE_KINDS = ["automate", "reautomate", "heal", "update", "write", "explore", "chat"];
1937
+ function usageKindOf(shown) {
1938
+ const s = shown.trim();
1939
+ if (/^Heal\b/i.test(s)) return "heal";
1940
+ if (/^Re-automate\b/i.test(s)) return "reautomate";
1941
+ if (/^Automate\b/i.test(s)) return "automate";
1942
+ if (/business steps of .* (just )?changed/i.test(s) || /^Update\b/i.test(s)) return "update";
1943
+ if (/^Explore\b|\bexplore (the|this) (app|site|website)\b/i.test(s)) return "explore";
1944
+ if (/^(Write|Import)\b.*test cases?/i.test(s)) return "write";
1945
+ return "chat";
1946
+ }
1947
+ var UsagePartSchema = TokensSchema.extend({
1948
+ stage: z16.enum(USAGE_STAGES),
1949
+ /** The tool the turn called; null for a turn that called none. */
1950
+ tool: z16.string().max(80).nullable(),
1951
+ /** The test case the work was for, when one was being worked on. */
1952
+ caseName: z16.string().max(300).nullable(),
1953
+ folder: z16.string().max(300).nullable(),
1954
+ /** Its id — TC-12 — when the assistant named it by id. */
1955
+ caseKey: z16.string().max(40).nullable(),
1956
+ /** The flow being recorded, when one was. */
1957
+ flow: z16.string().max(300).nullable(),
1958
+ /** How many turns are in this line. */
1959
+ turns: z16.number().int().nonnegative()
1960
+ }).strict();
1961
+ var UsageRecordSchema = z16.object({
1962
+ id: z16.string().regex(/^use_/),
1963
+ projectId: z16.string().min(1),
1964
+ userId: z16.string().min(1),
1965
+ jobId: z16.string().min(1),
1966
+ kind: z16.enum(USAGE_KINDS),
1967
+ /** The start of what was asked, so a list of jobs reads as one. */
1968
+ asked: z16.string().max(200),
1969
+ platform: z16.enum(["web", "android", "ios"]).nullable(),
1970
+ startedAt: z16.string(),
1971
+ finishedAt: z16.string().nullable(),
1972
+ status: z16.enum(["running", "done", "failed", "stopped"]),
1973
+ /** Every turn added up, from the turns as they happened. */
1974
+ totals: TokensSchema,
1975
+ turns: z16.number().int().nonnegative(),
1976
+ /** What the model said the whole job cost, in US dollars — at API prices;
1977
+ * on a Claude plan it is what the same work would cost there. */
1978
+ costUsd: z16.number().nonnegative().nullable(),
1979
+ durationMs: z16.number().int().nonnegative().nullable(),
1980
+ parts: z16.array(UsagePartSchema).max(600)
1981
+ }).strict();
1982
+ var StepRenameSchema = z17.object({
1983
+ occurrenceId: z17.string().min(1).max(200),
1984
+ stepId: z17.string().min(1).max(200),
1985
+ from: z17.string().min(1).max(500),
1986
+ to: z17.string().trim().min(1).max(200)
1987
+ }).strict();
1869
1988
 
1870
1989
  // ../engine-appium/dist/index.js
1871
- import path5 from "path";
1990
+ import path6 from "path";
1872
1991
 
1873
1992
  // ../core/dist/index.js
1874
1993
  function proposeAssertions(before, after) {
@@ -2201,9 +2320,9 @@ var STEP_PRESENTATION = {
2201
2320
  };
2202
2321
 
2203
2322
  // ../engine-appium/dist/index.js
2204
- import path6 from "path";
2205
- import fs5 from "fs";
2206
2323
  import path7 from "path";
2324
+ import fs6 from "fs";
2325
+ import path8 from "path";
2207
2326
  import { spawn as spawn3 } from "child_process";
2208
2327
  var WebDriverError = class extends Error {
2209
2328
  constructor(message, error, status) {
@@ -2215,10 +2334,10 @@ var WebDriverError = class extends Error {
2215
2334
  status;
2216
2335
  };
2217
2336
  var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecc";
2218
- async function call(endpoint, method, path8, body, timeoutMs = 12e4) {
2337
+ async function call(endpoint, method, path9, body, timeoutMs = 12e4) {
2219
2338
  const headers = { "content-type": "application/json; charset=utf-8" };
2220
2339
  if (endpoint.username) headers["authorization"] = `Basic ${Buffer.from(`${endpoint.username}:${endpoint.password ?? ""}`).toString("base64")}`;
2221
- const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path8}`, {
2340
+ const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path9}`, {
2222
2341
  method,
2223
2342
  headers,
2224
2343
  ...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {},
@@ -2257,11 +2376,11 @@ var WebDriverSession = class _WebDriverSession {
2257
2376
  return new _WebDriverSession(endpoint, value.sessionId, value.capabilities ?? {});
2258
2377
  }
2259
2378
  /** Any other command — Appium's own extensions live under /appium/. */
2260
- raw(method, path8, body, timeoutMs) {
2261
- return this.req(method, path8, body, timeoutMs);
2379
+ raw(method, path9, body, timeoutMs) {
2380
+ return this.req(method, path9, body, timeoutMs);
2262
2381
  }
2263
- req(method, path8, body, timeoutMs) {
2264
- return call(this.endpoint, method, `/session/${this.id}${path8}`, body, timeoutMs);
2382
+ req(method, path9, body, timeoutMs) {
2383
+ return call(this.endpoint, method, `/session/${this.id}${path9}`, body, timeoutMs);
2265
2384
  }
2266
2385
  source() {
2267
2386
  return this.req("GET", "/source");
@@ -2364,8 +2483,8 @@ function appiumPath() {
2364
2483
  }
2365
2484
  function javaMajor(home) {
2366
2485
  try {
2367
- const release = fs.readFileSync(path.join(home, "release"), "utf8");
2368
- const v = /JAVA_VERSION="(\d+)(?:\.(\d+))?/.exec(release);
2486
+ const release2 = fs.readFileSync(path.join(home, "release"), "utf8");
2487
+ const v = /JAVA_VERSION="(\d+)(?:\.(\d+))?/.exec(release2);
2369
2488
  if (!v) return 0;
2370
2489
  return v[1] === "1" ? Number(v[2] ?? 0) : Number(v[1]);
2371
2490
  } catch {
@@ -3288,6 +3407,88 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
3288
3407
  }
3289
3408
  throw last;
3290
3409
  }
3410
+ var held = /* @__PURE__ */ new Map();
3411
+ var lockDir = () => path4.join(process.env["FLOWRIKI_HOME"] || AGENT_HOME, "locks");
3412
+ var fileFor = (udid) => path4.join(lockDir(), `device-${udid.replace(/[^A-Za-z0-9._-]/g, "_")}.lock`);
3413
+ var alive = (pid) => {
3414
+ try {
3415
+ process.kill(pid, 0);
3416
+ return true;
3417
+ } catch (error) {
3418
+ return error.code === "EPERM";
3419
+ }
3420
+ };
3421
+ var DeviceInUse = class extends Error {
3422
+ constructor(device, pid) {
3423
+ super(`${device} is being used by another Flowriki job on this computer \u2014 wait for it to finish, or stop it first`);
3424
+ this.device = device;
3425
+ this.pid = pid;
3426
+ this.name = "DeviceInUse";
3427
+ }
3428
+ device;
3429
+ pid;
3430
+ };
3431
+ function lockDevice(udid, name = udid) {
3432
+ const file = fileFor(udid);
3433
+ const mine = held.get(file);
3434
+ if (mine) {
3435
+ held.set(file, mine + 1);
3436
+ return once(() => release(file));
3437
+ }
3438
+ fs4.mkdirSync(lockDir(), { recursive: true, mode: 448 });
3439
+ for (let attempt = 0; attempt < 2; attempt++) {
3440
+ try {
3441
+ const fd = fs4.openSync(file, "wx", 384);
3442
+ fs4.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
3443
+ fs4.closeSync(fd);
3444
+ held.set(file, 1);
3445
+ return once(() => release(file));
3446
+ } catch (error) {
3447
+ if (error.code !== "EEXIST") throw error;
3448
+ let owner = 0;
3449
+ try {
3450
+ owner = Number(JSON.parse(fs4.readFileSync(file, "utf8")).pid) || 0;
3451
+ } catch {
3452
+ }
3453
+ if (owner && owner !== process.pid && alive(owner)) throw new DeviceInUse(name, owner);
3454
+ try {
3455
+ fs4.unlinkSync(file);
3456
+ } catch {
3457
+ }
3458
+ }
3459
+ }
3460
+ throw new DeviceInUse(name, 0);
3461
+ }
3462
+ function release(file) {
3463
+ const count = held.get(file) ?? 0;
3464
+ if (count > 1) {
3465
+ held.set(file, count - 1);
3466
+ return;
3467
+ }
3468
+ held.delete(file);
3469
+ try {
3470
+ const owner = Number(JSON.parse(fs4.readFileSync(file, "utf8")).pid);
3471
+ if (owner === process.pid) fs4.unlinkSync(file);
3472
+ } catch {
3473
+ }
3474
+ }
3475
+ function once(fn) {
3476
+ let done = false;
3477
+ return () => {
3478
+ if (!done) {
3479
+ done = true;
3480
+ fn();
3481
+ }
3482
+ };
3483
+ }
3484
+ process.once("exit", () => {
3485
+ for (const file of held.keys()) {
3486
+ try {
3487
+ fs4.unlinkSync(file);
3488
+ } catch {
3489
+ }
3490
+ }
3491
+ });
3291
3492
  var uploaded = /* @__PURE__ */ new Map();
3292
3493
  var OPEN = /* @__PURE__ */ new Map();
3293
3494
  var openSessionOn = (platform) => OPEN.get(platform) ?? null;
@@ -3305,6 +3506,8 @@ var MobileSession = class _MobileSession {
3305
3506
  deviceName;
3306
3507
  cloud;
3307
3508
  cached = null;
3509
+ /** Lets go of this phone for other Flowriki processes — see device-lock. */
3510
+ releaseDevice = null;
3308
3511
  static async open(target, opts = {}) {
3309
3512
  const app = appOf(target);
3310
3513
  const say = opts.onProgress ?? (() => {
@@ -3326,11 +3529,11 @@ var MobileSession = class _MobileSession {
3326
3529
  }
3327
3530
  }
3328
3531
  say(`Opening ${choice2.device} on ${CLOUD_PROVIDER_NAME[choice2.provider]}\u2026`);
3329
- const { endpoint: endpoint2, capabilities: capabilities2 } = cloudCapabilities(target, account, key, appRef);
3330
- const driver2 = await WebDriverSession.create(endpoint2, capabilities2).catch((e) => {
3532
+ const { endpoint, capabilities } = cloudCapabilities(target, account, key, appRef);
3533
+ const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
3331
3534
  throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
3332
3535
  });
3333
- const cloudSession = new _MobileSession(driver2, target.platform, app.id, choice2.device, true);
3536
+ const cloudSession = new _MobileSession(driver, target.platform, app.id, choice2.device, true);
3334
3537
  OPEN.set(target.platform, cloudSession);
3335
3538
  await cloudSession.settle(2e4);
3336
3539
  return cloudSession;
@@ -3342,23 +3545,30 @@ var MobileSession = class _MobileSession {
3342
3545
  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"}.`);
3343
3546
  if (known.state !== "ready") say(`Starting ${known.name}\u2026`);
3344
3547
  const udid = await bootDevice(known);
3345
- const endpoint = await appiumServer();
3346
- const buildFile = app.build ? await localBuild(app.build) : null;
3347
- say(`Opening the app on ${known.name}\u2026`);
3348
- const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
3349
- const activity = target.platform === "android" && !app.activity && !buildFile ? await launchActivityOf(udid, app.id) : null;
3350
- const capabilities = await localCapabilities(target, udid, { buildFile, ports, ...activity ? { activity } : {} });
3351
- const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
3352
- const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
3353
- throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
3354
- });
3355
- const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
3356
- OPEN.set(target.platform, session);
3357
- await session.terminateApp().catch(() => {
3358
- });
3359
- await session.launchApp().catch(() => {
3360
- });
3361
- return session;
3548
+ const releaseDevice = lockDevice(udid, known.name);
3549
+ try {
3550
+ const endpoint = await appiumServer();
3551
+ const buildFile = app.build ? await localBuild(app.build) : null;
3552
+ say(`Opening the app on ${known.name}\u2026`);
3553
+ const ports = target.platform === "android" ? { system: await freePort(), mjpeg: await freePort() } : { wda: await freePort(), mjpeg: await freePort() };
3554
+ const activity = target.platform === "android" && !app.activity && !buildFile ? await launchActivityOf(udid, app.id) : null;
3555
+ const capabilities = await localCapabilities(target, udid, { buildFile, ports, ...activity ? { activity } : {} });
3556
+ const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
3557
+ const tail = appiumLog().filter((l) => /error/i.test(l)).slice(-2).join(" ");
3558
+ throw new Error(explainStartFailure(e.message, target.platform) + (tail ? ` (${tail.slice(0, 300)})` : ""));
3559
+ });
3560
+ const session = new _MobileSession(driver, target.platform, app.id, known.name, false);
3561
+ session.releaseDevice = releaseDevice;
3562
+ OPEN.set(target.platform, session);
3563
+ await session.terminateApp().catch(() => {
3564
+ });
3565
+ await session.launchApp().catch(() => {
3566
+ });
3567
+ return session;
3568
+ } catch (error) {
3569
+ releaseDevice();
3570
+ throw error;
3571
+ }
3362
3572
  }
3363
3573
  // ── Reading ────────────────────────────────────────────────────────────
3364
3574
  async screen() {
@@ -3389,8 +3599,8 @@ var MobileSession = class _MobileSession {
3389
3599
  async screenshot(file) {
3390
3600
  const png = Buffer.from(await this.driver.screenshot(), "base64");
3391
3601
  if (file) {
3392
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
3393
- fs4.writeFileSync(file, png);
3602
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
3603
+ fs5.writeFileSync(file, png);
3394
3604
  }
3395
3605
  return png;
3396
3606
  }
@@ -3591,14 +3801,19 @@ var MobileSession = class _MobileSession {
3591
3801
  const b64 = await this.driver.raw("POST", "/appium/stop_recording_screen", { options: {} }, 12e4).catch(() => "");
3592
3802
  if (!file || !b64) return null;
3593
3803
  const bytes = Buffer.from(b64, "base64");
3594
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
3595
- fs4.writeFileSync(file, bytes);
3596
- return { relPath: path4.basename(file), bytes: bytes.length };
3804
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
3805
+ fs5.writeFileSync(file, bytes);
3806
+ return { relPath: path5.basename(file), bytes: bytes.length };
3597
3807
  }
3598
3808
  async close() {
3599
3809
  if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
3600
- if (this.recording) await this.stopVideo(null);
3601
- await this.driver.close();
3810
+ try {
3811
+ if (this.recording) await this.stopVideo(null);
3812
+ await this.driver.close();
3813
+ } finally {
3814
+ this.releaseDevice?.();
3815
+ this.releaseDevice = null;
3816
+ }
3602
3817
  }
3603
3818
  };
3604
3819
  async function launchActivityOf(udid, appPackage) {
@@ -3614,18 +3829,18 @@ async function launchActivityOf(udid, appPackage) {
3614
3829
  }
3615
3830
  async function shrink(png, maxPx) {
3616
3831
  if (process.platform !== "darwin") return png;
3617
- const dir = fs4.mkdtempSync(path4.join(os4.tmpdir(), "flowriki-shot-"));
3618
- const file = path4.join(dir, "s.png");
3832
+ const dir = fs5.mkdtempSync(path5.join(os4.tmpdir(), "flowriki-shot-"));
3833
+ const file = path5.join(dir, "s.png");
3619
3834
  try {
3620
- fs4.writeFileSync(file, png);
3835
+ fs5.writeFileSync(file, png);
3621
3836
  await new Promise((resolve, reject) => {
3622
3837
  execFile2("/usr/bin/sips", ["-Z", String(maxPx), file], { timeout: 5e3 }, (error) => error ? reject(error) : resolve());
3623
3838
  });
3624
- return fs4.readFileSync(file);
3839
+ return fs5.readFileSync(file);
3625
3840
  } catch {
3626
3841
  return png;
3627
3842
  } finally {
3628
- fs4.rmSync(dir, { recursive: true, force: true });
3843
+ fs5.rmSync(dir, { recursive: true, force: true });
3629
3844
  }
3630
3845
  }
3631
3846
  var ANDROID_KEYS = {
@@ -3885,7 +4100,7 @@ async function runMobileSteps(session, steps, opts) {
3885
4100
  if (opts.evidenceDir && (shots === "always" || shots === "on-failure" && result.status === "failed")) {
3886
4101
  const file = `step-${String((opts.stepOffset ?? 0) + results.length + 1).padStart(2, "0")}.png`;
3887
4102
  try {
3888
- await session.screenshot(path5.join(opts.evidenceDir, file));
4103
+ await session.screenshot(path6.join(opts.evidenceDir, file));
3889
4104
  result.evidence = { screenshot: file, url: await session.currentScreenName(), title: session.deviceName };
3890
4105
  } catch {
3891
4106
  }
@@ -3909,7 +4124,7 @@ var typesSecrets = (steps) => steps.some((s) => s.secret || Boolean(s.secretRef)
3909
4124
  var filmsThis = (policy, steps, evidenceDir) => Boolean(evidenceDir) && Boolean(policy) && policy.mode !== "off" && (policy.allowUnmasked === true || !typesSecrets(steps));
3910
4125
  async function finishVideo(session, policy, evidenceDir, passed, name = "phone") {
3911
4126
  const keep = Boolean(evidenceDir) && (policy?.mode === "always" || policy?.mode === "on-failure" && !passed);
3912
- const saved = await session.stopVideo(keep ? path6.join(evidenceDir, `video-${name}.mp4`) : null).catch(() => null);
4127
+ const saved = await session.stopVideo(keep ? path7.join(evidenceDir, `video-${name}.mp4`) : null).catch(() => null);
3913
4128
  return saved ? [{ kind: "video", relPath: saved.relPath, bytes: saved.bytes }] : [];
3914
4129
  }
3915
4130
  async function chooseDevice(platform, mobile, asked, where) {
@@ -4038,20 +4253,20 @@ function stream(file, args, env, cwd, say) {
4038
4253
  child.stdout.on("data", each);
4039
4254
  child.stderr.on("data", each);
4040
4255
  child.on("error", reject);
4041
- child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${path7.basename(file)} ${args.join(" ")} failed (${code})`)));
4256
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${path8.basename(file)} ${args.join(" ")} failed (${code})`)));
4042
4257
  });
4043
4258
  }
4044
4259
  async function mobileSetup(say = console.log) {
4045
4260
  const npm = onPath("npm");
4046
4261
  if (!npm) throw new Error("npm was not found \u2014 install Node.js 20 or newer.");
4047
- fs5.mkdirSync(path7.join(APPIUM_DIR, "home"), { recursive: true });
4048
- if (!fs5.existsSync(path7.join(APPIUM_DIR, "package.json"))) {
4049
- fs5.writeFileSync(path7.join(APPIUM_DIR, "package.json"), JSON.stringify({ name: "flowriki-appium", private: true }, null, 2));
4262
+ fs6.mkdirSync(path8.join(APPIUM_DIR, "home"), { recursive: true });
4263
+ if (!fs6.existsSync(path8.join(APPIUM_DIR, "package.json"))) {
4264
+ fs6.writeFileSync(path8.join(APPIUM_DIR, "package.json"), JSON.stringify({ name: "flowriki-appium", private: true }, null, 2));
4050
4265
  }
4051
- const env = { ...await toolEnv(), APPIUM_HOME: path7.join(APPIUM_DIR, "home") };
4266
+ const env = { ...await toolEnv(), APPIUM_HOME: path8.join(APPIUM_DIR, "home") };
4052
4267
  say("Installing Appium\u2026");
4053
4268
  await stream(npm, ["install", "--no-audit", "--no-fund", "appium@2"], env, APPIUM_DIR, say);
4054
- const bin = path7.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
4269
+ const bin = path8.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
4055
4270
  const have = await installedDrivers();
4056
4271
  const wanted = ["uiautomator2", ...process.platform === "darwin" ? ["xcuitest"] : []];
4057
4272
  for (const driver of wanted) {
@@ -4245,6 +4460,7 @@ export {
4245
4460
  WEB_CLOUD_BROWSER_NAME,
4246
4461
  webCloudOf,
4247
4462
  nameKey,
4463
+ usageKindOf,
4248
4464
  proposeAssertions,
4249
4465
  hasObservableChange,
4250
4466
  normalizeStartUrl,
@@ -4291,6 +4507,8 @@ export {
4291
4507
  CheckError,
4292
4508
  textOf,
4293
4509
  checkMobileAssertion,
4510
+ DeviceInUse,
4511
+ lockDevice,
4294
4512
  openSessionOn,
4295
4513
  MobileSession,
4296
4514
  shrink,