@flowriki/agent 0.1.0-beta.64 → 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
 
@@ -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(),
@@ -1832,7 +1840,7 @@ function decodeEntities(text) {
1832
1840
  var nameKey = (name) => decodeEntities(name).toLowerCase().replace(/&/g, " and ").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
1833
1841
  var PLATFORM_PART = "(web|android|ios)";
1834
1842
  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);
1843
+ var isLearnedPath = (path9) => LEARNED_PATH.test(path9);
1836
1844
  var LearnedSchema = z11.object({
1837
1845
  id: z11.string().regex(/^lrn_[a-z0-9_-]+$/),
1838
1846
  projectId: z11.string().min(1),
@@ -1873,6 +1881,29 @@ var WhatToDoSchema = z13.object({
1873
1881
  skipped: z13.array(z13.string().min(1).max(80)).max(100),
1874
1882
  updatedAt: z13.string()
1875
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
+ ]);
1876
1907
  var CaseRegistrySchema = z14.object({
1877
1908
  id: z14.string().regex(/^creg_/),
1878
1909
  projectId: z14.string().min(1),
@@ -1880,11 +1911,83 @@ var CaseRegistrySchema = z14.object({
1880
1911
  cases: z14.record(z14.string(), z14.number().int().positive()),
1881
1912
  features: z14.record(z14.string(), z14.number().int().positive()),
1882
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(),
1883
1916
  updatedAt: z14.string()
1884
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();
1885
1988
 
1886
1989
  // ../engine-appium/dist/index.js
1887
- import path5 from "path";
1990
+ import path6 from "path";
1888
1991
 
1889
1992
  // ../core/dist/index.js
1890
1993
  function proposeAssertions(before, after) {
@@ -2217,9 +2320,9 @@ var STEP_PRESENTATION = {
2217
2320
  };
2218
2321
 
2219
2322
  // ../engine-appium/dist/index.js
2220
- import path6 from "path";
2221
- import fs5 from "fs";
2222
2323
  import path7 from "path";
2324
+ import fs6 from "fs";
2325
+ import path8 from "path";
2223
2326
  import { spawn as spawn3 } from "child_process";
2224
2327
  var WebDriverError = class extends Error {
2225
2328
  constructor(message, error, status) {
@@ -2231,10 +2334,10 @@ var WebDriverError = class extends Error {
2231
2334
  status;
2232
2335
  };
2233
2336
  var ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecc";
2234
- async function call(endpoint, method, path8, body, timeoutMs = 12e4) {
2337
+ async function call(endpoint, method, path9, body, timeoutMs = 12e4) {
2235
2338
  const headers = { "content-type": "application/json; charset=utf-8" };
2236
2339
  if (endpoint.username) headers["authorization"] = `Basic ${Buffer.from(`${endpoint.username}:${endpoint.password ?? ""}`).toString("base64")}`;
2237
- const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path8}`, {
2340
+ const res = await fetch(`${endpoint.url.replace(/\/+$/, "")}${path9}`, {
2238
2341
  method,
2239
2342
  headers,
2240
2343
  ...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {},
@@ -2273,11 +2376,11 @@ var WebDriverSession = class _WebDriverSession {
2273
2376
  return new _WebDriverSession(endpoint, value.sessionId, value.capabilities ?? {});
2274
2377
  }
2275
2378
  /** Any other command — Appium's own extensions live under /appium/. */
2276
- raw(method, path8, body, timeoutMs) {
2277
- return this.req(method, path8, body, timeoutMs);
2379
+ raw(method, path9, body, timeoutMs) {
2380
+ return this.req(method, path9, body, timeoutMs);
2278
2381
  }
2279
- req(method, path8, body, timeoutMs) {
2280
- 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);
2281
2384
  }
2282
2385
  source() {
2283
2386
  return this.req("GET", "/source");
@@ -2380,8 +2483,8 @@ function appiumPath() {
2380
2483
  }
2381
2484
  function javaMajor(home) {
2382
2485
  try {
2383
- const release = fs.readFileSync(path.join(home, "release"), "utf8");
2384
- 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);
2385
2488
  if (!v) return 0;
2386
2489
  return v[1] === "1" ? Number(v[2] ?? 0) : Number(v[1]);
2387
2490
  } catch {
@@ -3304,6 +3407,88 @@ async function checkMobileAssertion(session, a, timeoutMs = 3e3) {
3304
3407
  }
3305
3408
  throw last;
3306
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
+ });
3307
3492
  var uploaded = /* @__PURE__ */ new Map();
3308
3493
  var OPEN = /* @__PURE__ */ new Map();
3309
3494
  var openSessionOn = (platform) => OPEN.get(platform) ?? null;
@@ -3321,6 +3506,8 @@ var MobileSession = class _MobileSession {
3321
3506
  deviceName;
3322
3507
  cloud;
3323
3508
  cached = null;
3509
+ /** Lets go of this phone for other Flowriki processes — see device-lock. */
3510
+ releaseDevice = null;
3324
3511
  static async open(target, opts = {}) {
3325
3512
  const app = appOf(target);
3326
3513
  const say = opts.onProgress ?? (() => {
@@ -3342,11 +3529,11 @@ var MobileSession = class _MobileSession {
3342
3529
  }
3343
3530
  }
3344
3531
  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) => {
3532
+ const { endpoint, capabilities } = cloudCapabilities(target, account, key, appRef);
3533
+ const driver = await WebDriverSession.create(endpoint, capabilities).catch((e) => {
3347
3534
  throw new Error(`${CLOUD_PROVIDER_NAME[choice2.provider]} could not open ${choice2.device}: ${e.message}`);
3348
3535
  });
3349
- 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);
3350
3537
  OPEN.set(target.platform, cloudSession);
3351
3538
  await cloudSession.settle(2e4);
3352
3539
  return cloudSession;
@@ -3358,23 +3545,30 @@ var MobileSession = class _MobileSession {
3358
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"}.`);
3359
3546
  if (known.state !== "ready") say(`Starting ${known.name}\u2026`);
3360
3547
  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;
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
+ }
3378
3572
  }
3379
3573
  // ── Reading ────────────────────────────────────────────────────────────
3380
3574
  async screen() {
@@ -3405,8 +3599,8 @@ var MobileSession = class _MobileSession {
3405
3599
  async screenshot(file) {
3406
3600
  const png = Buffer.from(await this.driver.screenshot(), "base64");
3407
3601
  if (file) {
3408
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
3409
- fs4.writeFileSync(file, png);
3602
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
3603
+ fs5.writeFileSync(file, png);
3410
3604
  }
3411
3605
  return png;
3412
3606
  }
@@ -3607,14 +3801,19 @@ var MobileSession = class _MobileSession {
3607
3801
  const b64 = await this.driver.raw("POST", "/appium/stop_recording_screen", { options: {} }, 12e4).catch(() => "");
3608
3802
  if (!file || !b64) return null;
3609
3803
  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 };
3804
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
3805
+ fs5.writeFileSync(file, bytes);
3806
+ return { relPath: path5.basename(file), bytes: bytes.length };
3613
3807
  }
3614
3808
  async close() {
3615
3809
  if (OPEN.get(this.platform) === this) OPEN.delete(this.platform);
3616
- if (this.recording) await this.stopVideo(null);
3617
- 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
+ }
3618
3817
  }
3619
3818
  };
3620
3819
  async function launchActivityOf(udid, appPackage) {
@@ -3630,18 +3829,18 @@ async function launchActivityOf(udid, appPackage) {
3630
3829
  }
3631
3830
  async function shrink(png, maxPx) {
3632
3831
  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");
3832
+ const dir = fs5.mkdtempSync(path5.join(os4.tmpdir(), "flowriki-shot-"));
3833
+ const file = path5.join(dir, "s.png");
3635
3834
  try {
3636
- fs4.writeFileSync(file, png);
3835
+ fs5.writeFileSync(file, png);
3637
3836
  await new Promise((resolve, reject) => {
3638
3837
  execFile2("/usr/bin/sips", ["-Z", String(maxPx), file], { timeout: 5e3 }, (error) => error ? reject(error) : resolve());
3639
3838
  });
3640
- return fs4.readFileSync(file);
3839
+ return fs5.readFileSync(file);
3641
3840
  } catch {
3642
3841
  return png;
3643
3842
  } finally {
3644
- fs4.rmSync(dir, { recursive: true, force: true });
3843
+ fs5.rmSync(dir, { recursive: true, force: true });
3645
3844
  }
3646
3845
  }
3647
3846
  var ANDROID_KEYS = {
@@ -3901,7 +4100,7 @@ async function runMobileSteps(session, steps, opts) {
3901
4100
  if (opts.evidenceDir && (shots === "always" || shots === "on-failure" && result.status === "failed")) {
3902
4101
  const file = `step-${String((opts.stepOffset ?? 0) + results.length + 1).padStart(2, "0")}.png`;
3903
4102
  try {
3904
- await session.screenshot(path5.join(opts.evidenceDir, file));
4103
+ await session.screenshot(path6.join(opts.evidenceDir, file));
3905
4104
  result.evidence = { screenshot: file, url: await session.currentScreenName(), title: session.deviceName };
3906
4105
  } catch {
3907
4106
  }
@@ -3925,7 +4124,7 @@ var typesSecrets = (steps) => steps.some((s) => s.secret || Boolean(s.secretRef)
3925
4124
  var filmsThis = (policy, steps, evidenceDir) => Boolean(evidenceDir) && Boolean(policy) && policy.mode !== "off" && (policy.allowUnmasked === true || !typesSecrets(steps));
3926
4125
  async function finishVideo(session, policy, evidenceDir, passed, name = "phone") {
3927
4126
  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);
4127
+ const saved = await session.stopVideo(keep ? path7.join(evidenceDir, `video-${name}.mp4`) : null).catch(() => null);
3929
4128
  return saved ? [{ kind: "video", relPath: saved.relPath, bytes: saved.bytes }] : [];
3930
4129
  }
3931
4130
  async function chooseDevice(platform, mobile, asked, where) {
@@ -4054,20 +4253,20 @@ function stream(file, args, env, cwd, say) {
4054
4253
  child.stdout.on("data", each);
4055
4254
  child.stderr.on("data", each);
4056
4255
  child.on("error", reject);
4057
- 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})`)));
4058
4257
  });
4059
4258
  }
4060
4259
  async function mobileSetup(say = console.log) {
4061
4260
  const npm = onPath("npm");
4062
4261
  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));
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));
4066
4265
  }
4067
- const env = { ...await toolEnv(), APPIUM_HOME: path7.join(APPIUM_DIR, "home") };
4266
+ const env = { ...await toolEnv(), APPIUM_HOME: path8.join(APPIUM_DIR, "home") };
4068
4267
  say("Installing Appium\u2026");
4069
4268
  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");
4269
+ const bin = path8.join(APPIUM_DIR, "node_modules", ".bin", process.platform === "win32" ? "appium.cmd" : "appium");
4071
4270
  const have = await installedDrivers();
4072
4271
  const wanted = ["uiautomator2", ...process.platform === "darwin" ? ["xcuitest"] : []];
4073
4272
  for (const driver of wanted) {
@@ -4261,6 +4460,7 @@ export {
4261
4460
  WEB_CLOUD_BROWSER_NAME,
4262
4461
  webCloudOf,
4263
4462
  nameKey,
4463
+ usageKindOf,
4264
4464
  proposeAssertions,
4265
4465
  hasObservableChange,
4266
4466
  normalizeStartUrl,
@@ -4307,6 +4507,8 @@ export {
4307
4507
  CheckError,
4308
4508
  textOf,
4309
4509
  checkMobileAssertion,
4510
+ DeviceInUse,
4511
+ lockDevice,
4310
4512
  openSessionOn,
4311
4513
  MobileSession,
4312
4514
  shrink,