@avasapp/agent-bridge 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +19 -5
  2. package/dist/adapters/ably.cjs +79 -3
  3. package/dist/adapters/ably.d.cts +2 -1
  4. package/dist/adapters/async-storage.cjs +158 -0
  5. package/dist/adapters/async-storage.d.cts +14 -0
  6. package/dist/adapters/device.cjs +1110 -0
  7. package/dist/adapters/device.d.cts +49 -0
  8. package/dist/adapters/expo-router.cjs +112 -6
  9. package/dist/adapters/expo-router.d.cts +1 -1
  10. package/dist/adapters/jotai.cjs +266 -0
  11. package/dist/adapters/jotai.d.cts +19 -0
  12. package/dist/adapters/react-native-mmkv.cjs +80 -0
  13. package/dist/adapters/react-native-mmkv.d.cts +3 -1
  14. package/dist/adapters/react-native-webview.d.cts +1 -1
  15. package/dist/adapters/react-navigation.cjs +293 -0
  16. package/dist/adapters/react-navigation.d.cts +55 -0
  17. package/dist/adapters/redux.cjs +322 -0
  18. package/dist/adapters/redux.d.cts +28 -0
  19. package/dist/adapters/secure-store.cjs +169 -0
  20. package/dist/adapters/secure-store.d.cts +28 -0
  21. package/dist/adapters/socket-io.cjs +77 -3
  22. package/dist/adapters/socket-io.d.cts +2 -1
  23. package/dist/adapters/tanstack-query.d.cts +1 -1
  24. package/dist/adapters/zustand.cjs +193 -22
  25. package/dist/adapters/zustand.d.cts +11 -15
  26. package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
  27. package/dist/cli.js +1336 -255
  28. package/dist/client/index.cjs +233 -27
  29. package/dist/client/index.d.ts +67 -4
  30. package/dist/client/index.js +5 -3
  31. package/dist/expo/index.d.cts +1 -1
  32. package/dist/network/index.cjs +320 -73
  33. package/dist/network/index.d.cts +18 -3
  34. package/dist/noop/async-storage.cjs +26 -0
  35. package/dist/noop/device.cjs +26 -0
  36. package/dist/noop/jotai.cjs +26 -0
  37. package/dist/noop/react-navigation.cjs +26 -0
  38. package/dist/noop/realtime.cjs +6 -1
  39. package/dist/noop/redux.cjs +28 -0
  40. package/dist/noop/secure-store.cjs +26 -0
  41. package/dist/realtime/index.cjs +71 -4
  42. package/dist/realtime/index.d.cts +21 -2
  43. package/dist/runtime/index.cjs +785 -128
  44. package/dist/runtime/index.d.cts +2 -2
  45. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  46. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  47. package/entries/async-storage.cjs +9 -0
  48. package/entries/device.cjs +9 -0
  49. package/entries/jotai.cjs +9 -0
  50. package/entries/react-navigation.cjs +9 -0
  51. package/entries/redux.cjs +9 -0
  52. package/entries/secure-store.cjs +9 -0
  53. package/package.json +32 -1
  54. package/skills/agent-bridge/SKILL.md +39 -8
  55. package/skills/agent-bridge/references/device.md +21 -0
  56. package/skills/agent-bridge/references/flows.md +13 -3
  57. package/skills/agent-bridge/references/network.md +19 -1
  58. package/skills/agent-bridge/references/realtime.md +1 -1
  59. package/skills/agent-bridge/references/restore.md +21 -3
  60. package/skills/agent-bridge/references/screen.md +8 -2
  61. package/skills/agent-bridge/references/state.md +21 -1
@@ -35,10 +35,26 @@ __export(client_exports, {
35
35
  connectSession: () => connectSession,
36
36
  listDevices: () => listDevices,
37
37
  listSessions: () => listSessions,
38
- runFlow: () => runFlow
38
+ runFlow: () => runFlow,
39
+ runFlows: () => runFlows
39
40
  });
40
41
  module.exports = __toCommonJS(client_exports);
41
42
 
43
+ // src/client/call-timeout.ts
44
+ var WAITERS = {
45
+ "screen.waitFor": 2e3,
46
+ // The page helper adds 5 s to timeoutMs before it gives up.
47
+ "webview.waitFor": 7e3
48
+ };
49
+ var MAX_MS = 10 * 6e4;
50
+ function callTimeout(base, tool, args) {
51
+ const margin = WAITERS[tool];
52
+ const t = args[1]?.timeoutMs;
53
+ if (margin === void 0 || typeof t !== "number" || !(t > 0) || !Number.isFinite(t))
54
+ return base;
55
+ return Math.max(base, Math.min(t + margin, MAX_MS));
56
+ }
57
+
42
58
  // src/client/discover.ts
43
59
  var within = (ms, signal) => signal ? AbortSignal.any([AbortSignal.timeout(ms), signal]) : AbortSignal.timeout(ms);
44
60
  var DEFAULT_METRO = "localhost:8081";
@@ -272,7 +288,7 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
272
288
  transport: "expo",
273
289
  device: info,
274
290
  pin: { name: info.name, platform: info.platform },
275
- call(tool, args, timeoutMs) {
291
+ call(tool, args, timeoutMs, extra) {
276
292
  if (superseded)
277
293
  return Promise.resolve({
278
294
  id: "",
@@ -285,7 +301,8 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
285
301
  id: newCallId(),
286
302
  tool,
287
303
  args,
288
- to: info.deviceId
304
+ to: info.deviceId,
305
+ ...extra
289
306
  };
290
307
  const reply = pending.wait(call.id, tool, timeoutMs);
291
308
  send("call", call);
@@ -306,6 +323,17 @@ function projectRoot(from = process.cwd()) {
306
323
  if ((0, import_node_path.dirname)(dir) === dir) return (0, import_node_path.resolve)(from);
307
324
  }
308
325
  }
326
+ var isPackage = (dir) => (0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"));
327
+ function inProject(session, project) {
328
+ if (!session) return false;
329
+ if (session === project) return true;
330
+ if (!session.startsWith(project + import_node_path.sep) || !isPackage(project)) return false;
331
+ for (let dir = session; dir !== project; dir = (0, import_node_path.dirname)(dir)) {
332
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, ".git"))) return false;
333
+ if (dir !== session && isPackage(dir)) return false;
334
+ }
335
+ return true;
336
+ }
309
337
  var uid = () => process.getuid?.() ?? (0, import_node_os.userInfo)().username;
310
338
  function privateDir(dir) {
311
339
  (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
@@ -389,8 +417,10 @@ function pickSession(filter) {
389
417
  const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
390
418
  const project = projectRoot();
391
419
  const matching = listSessions().filter(
392
- (s) => (explicit || s.project === project) && (!filter.metro || s.metro === filter.metro) && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
420
+ (s) => !filter.skip?.(s) && (explicit || inProject(s.project, project)) && (!filter.metro || s.metro === filter.metro) && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
393
421
  );
422
+ if (!explicit && matching.length > 1 && matching.every((s) => s.project !== project))
423
+ return null;
394
424
  if (matching.length > 1)
395
425
  throw new Error(
396
426
  `Several sessions match (${matching.map((s) => `${s.name}: ${s.device.name} on ${s.metro}`).join("; ")}); pick one by name (--session, or --name for session stop/status), or narrow with --metro/--device`
@@ -599,8 +629,8 @@ async function connectCdp(metro, device, signal, pin) {
599
629
  target: targetLabel(target)
600
630
  },
601
631
  device: { ...info, name: `${info.name} (${targetLabel(target)})` },
602
- call(tool, args, timeoutMs) {
603
- const call = { id: newCallId(), tool, args };
632
+ call(tool, args, timeoutMs, extra) {
633
+ const call = { id: newCallId(), tool, args, ...extra };
604
634
  const reply = pending.wait(call.id, tool, timeoutMs);
605
635
  evaluate(
606
636
  `${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
@@ -683,6 +713,40 @@ function logLine(entry) {
683
713
  }
684
714
 
685
715
  // src/client/flow.ts
716
+ function deepMatches(actual, expected) {
717
+ if (expected instanceof RegExp) return typeof actual === "string" && expected.test(actual);
718
+ if (Array.isArray(expected))
719
+ return Array.isArray(actual) && actual.length === expected.length && expected.every((e, i) => deepMatches(actual[i], e));
720
+ if (expected && typeof expected === "object") {
721
+ const proto = Object.getPrototypeOf(expected);
722
+ if (proto !== Object.prototype && proto !== null)
723
+ throw new Error(
724
+ `A match can hold plain objects, arrays, values and RegExps, not a ${expected.constructor?.name ?? "class instance"}`
725
+ );
726
+ return !!actual && typeof actual === "object" && Object.entries(expected).every(
727
+ ([k, v]) => deepMatches(actual[k], v)
728
+ );
729
+ }
730
+ return Object.is(actual, expected);
731
+ }
732
+ var MAX_SCREEN_LINES = 40;
733
+ function compactScreen(snapshot) {
734
+ const elements = snapshot?.elements;
735
+ if (!Array.isArray(elements)) return [];
736
+ const lines = elements.slice(0, MAX_SCREEN_LINES).map((raw) => {
737
+ const e = raw;
738
+ const parts = [String(e.kind ?? "element")];
739
+ if (e.testID) parts.push(`#${String(e.testID)}`);
740
+ for (const key of ["text", "label", "placeholder", "value"])
741
+ if (typeof e[key] === "string" && e[key]) parts.push(JSON.stringify(e[key]));
742
+ if (e.disabled) parts.push("disabled");
743
+ if (e.checked) parts.push(`checked=${String(e.checked)}`);
744
+ return parts.join(" ");
745
+ });
746
+ if (elements.length > MAX_SCREEN_LINES)
747
+ lines.push(`... ${elements.length - MAX_SCREEN_LINES} more`);
748
+ return lines;
749
+ }
686
750
  function flowScenarios(flow) {
687
751
  const declared = [
688
752
  ...flow.scenario === void 0 ? [] : [flow.scenario],
@@ -710,6 +774,10 @@ async function runFlow(bridge, flow, options = {}) {
710
774
  let errors = 0;
711
775
  const t0 = performance.now();
712
776
  const notices = [];
777
+ let lost = false;
778
+ const noteOutcome = (error) => {
779
+ lost = error !== void 0 && !(error instanceof AgentBridgeCallError);
780
+ };
713
781
  const report = (logs, notice) => {
714
782
  errors += logs.length;
715
783
  for (const e of logs) print(` ${logLine(e)}`);
@@ -721,28 +789,82 @@ async function runFlow(bridge, flow, options = {}) {
721
789
  const timed = async (tool, ...args) => {
722
790
  try {
723
791
  const result = await bridge.timed(tool, ...args);
792
+ noteOutcome();
724
793
  report(result.logs, result.notice);
725
794
  return result;
726
795
  } catch (error) {
796
+ noteOutcome(error);
727
797
  report(failedLogs(error), failedNotice(error));
728
798
  throw error;
729
799
  }
730
800
  };
731
801
  const call = async (tool, ...args) => (await timed(tool, ...args)).value;
732
- const step = async (label2, tool, ...given) => {
733
- const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
734
- const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
735
- report(failedLogs(error), failedNotice(error));
736
- throw error;
737
- });
802
+ const record = (label2, ms) => {
738
803
  total += ms;
739
804
  print(
740
805
  `${String(++n).padStart(2, "0")} ${label2.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
741
806
  );
742
- report(logs, notice);
743
- return value;
807
+ };
808
+ const step = async (label2, tool, ...given) => {
809
+ const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
810
+ const result = await timed(tool, ...args);
811
+ record(label2, result.ms);
812
+ return result.value;
813
+ };
814
+ const message = (error) => error instanceof Error ? error.message : String(error);
815
+ const expectApi = {
816
+ text: async (target, o) => {
817
+ await step(`expect text ${JSON.stringify(target)}`, "screen.waitFor", target, o ?? {}).catch(
818
+ (error) => {
819
+ throw new Error(`expected ${JSON.stringify(target)} on screen: ${message(error)}`);
820
+ }
821
+ );
822
+ },
823
+ gone: async (target, o) => {
824
+ await step(`expect gone ${JSON.stringify(target)}`, "screen.waitFor", target, {
825
+ ...o,
826
+ gone: true
827
+ }).catch((error) => {
828
+ throw new Error(`expected ${JSON.stringify(target)} to be off screen: ${message(error)}`);
829
+ });
830
+ },
831
+ route: async (pathname, o) => {
832
+ const label2 = `expect route ${pathname}`;
833
+ const t02 = performance.now();
834
+ const deadline = t02 + (o?.timeoutMs ?? 2e3);
835
+ for (; ; ) {
836
+ const route = await call("router.current").catch((error) => {
837
+ record(label2, performance.now() - t02);
838
+ throw error;
839
+ });
840
+ if (route?.pathname === pathname) return record(label2, performance.now() - t02);
841
+ if (performance.now() >= deadline) {
842
+ record(label2, performance.now() - t02);
843
+ throw new Error(`expected route ${pathname}, got ${route?.pathname ?? JSON.stringify(route)}`);
844
+ }
845
+ await new Promise((r) => setTimeout(r, 50));
846
+ }
847
+ },
848
+ result: async (tool, args, match) => {
849
+ const value = await step(`expect ${tool}`, tool, args);
850
+ if (!deepMatches(value, match))
851
+ throw new Error(
852
+ `expected ${tool} to match ${JSON.stringify(match)}, got ${JSON.stringify(value)}`
853
+ );
854
+ }
855
+ };
856
+ const failureState = async () => {
857
+ const has = (name) => bridge.tools().some((t) => t.name === name);
858
+ const state = {};
859
+ if (has("screen.snapshot"))
860
+ state.screen = await call("screen.snapshot").then(compactScreen).catch(() => void 0);
861
+ if (has("router.current") && !lost)
862
+ state.route = await call("router.current").catch(() => void 0);
863
+ return state;
744
864
  };
745
865
  const restoreErrors = [];
866
+ let flowErrors = 0;
867
+ let unreachable = false;
746
868
  const applied = {};
747
869
  if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
748
870
  throw new Error(
@@ -757,10 +879,22 @@ async function runFlow(bridge, flow, options = {}) {
757
879
  bridge: { ...bridge, timed, call },
758
880
  call,
759
881
  step,
760
- scenarios: applied
882
+ scenarios: applied,
883
+ expect: expectApi
761
884
  });
885
+ } catch (error) {
886
+ const state = lost ? {} : await failureState();
887
+ if (state.route !== void 0) print(` route: ${JSON.stringify(state.route)}`);
888
+ if (state.screen?.length) {
889
+ print(" on screen:");
890
+ for (const line of state.screen) print(` ${line}`);
891
+ }
892
+ options.onFailure?.(state);
893
+ throw error;
762
894
  } finally {
763
- if (needs.length) {
895
+ flowErrors = errors;
896
+ unreachable = lost;
897
+ if (!lost && !options.keep && (needs.length || bridge.tools().some((t) => t.name === "bridge.restore"))) {
764
898
  try {
765
899
  const result = await step("restore", "bridge.restore");
766
900
  for (const [name, value] of Object.entries(result ?? {})) {
@@ -768,17 +902,70 @@ async function runFlow(bridge, flow, options = {}) {
768
902
  if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
769
903
  }
770
904
  } catch (error) {
905
+ unreachable = lost;
771
906
  restoreErrors.push(
772
907
  `bridge.restore: ${error instanceof Error ? error.message : String(error)}`
773
908
  );
774
909
  }
775
910
  for (const e of restoreErrors) print(` ! restore failed: ${e}`);
776
911
  }
912
+ options.onFinish?.({ steps: n, errors: flowErrors, restoreErrors, notices, unreachable });
777
913
  }
778
914
  print(
779
- `${n} steps, ${total.toFixed(1)} ms in calls, ${(performance.now() - t0).toFixed(0)} ms wall (${bridge.transport})${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}${notices.length ? ", the app reloaded" : ""}`
915
+ `${n} steps, ${total.toFixed(1)} ms in calls, ${(performance.now() - t0).toFixed(0)} ms wall (${bridge.transport})${flowErrors ? `, ${flowErrors} error${flowErrors === 1 ? "" : "s"}` : ""}${notices.length ? ", the app reloaded" : ""}`
780
916
  );
781
- return { steps: n, errors, restoreErrors, notices };
917
+ return { steps: n, errors: flowErrors, restoreErrors, notices, unreachable };
918
+ }
919
+ async function runFlows(bridge, entries, options = {}) {
920
+ const print = options.print ?? console.log;
921
+ const printError = options.single ? options.printError ?? console.error : print;
922
+ const runs = [];
923
+ let unreachable = false;
924
+ for (const entry of entries) {
925
+ const base = { file: entry.file, name: entry.name, restoreErrors: [], notices: [] };
926
+ const skip = unreachable ? "skipped: the app stopped answering in an earlier flow" : options.bail && runs.some((r) => r.status === "failed") ? "skipped: an earlier flow failed (--bail)" : void 0;
927
+ if (skip) {
928
+ runs.push({ ...base, status: "skipped", ms: 0, steps: 0, errors: 0, error: skip });
929
+ continue;
930
+ }
931
+ if (!options.single) print(`
932
+ ${entry.name}`);
933
+ const t0 = performance.now();
934
+ let state;
935
+ let done = { steps: 0, errors: 0, restoreErrors: [], notices: [], unreachable: false };
936
+ const reasons = [];
937
+ try {
938
+ await runFlow(bridge, entry.module, {
939
+ ...options,
940
+ onFailure: (s) => {
941
+ state = s;
942
+ },
943
+ onFinish: (r) => {
944
+ done = r;
945
+ }
946
+ });
947
+ } catch (error) {
948
+ const why = error instanceof Error ? error.message : String(error);
949
+ printError(` FAILED: ${why}`);
950
+ reasons.push(why);
951
+ }
952
+ if (done.restoreErrors.length) reasons.push(`restore failed: ${done.restoreErrors.join("; ")}`);
953
+ if (options.strict && done.errors)
954
+ reasons.push(`the app logged ${done.errors} error${done.errors === 1 ? "" : "s"}`);
955
+ if (done.notices.length)
956
+ print(" ! the app reloaded: changes made before the reload could not be undone");
957
+ const { unreachable: gone, ...result } = done;
958
+ runs.push({
959
+ ...base,
960
+ ...result,
961
+ status: reasons.length ? "failed" : "passed",
962
+ ms: performance.now() - t0,
963
+ ...reasons.length && { error: reasons.join("; ") },
964
+ ...state && { state }
965
+ });
966
+ if (gone) unreachable = true;
967
+ }
968
+ return runs;
782
969
  }
783
970
 
784
971
  // src/client/session/client.ts
@@ -790,19 +977,26 @@ var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]
790
977
  `;
791
978
 
792
979
  // src/client/session/client.ts
980
+ var SessionLinkError = class extends Error {
981
+ constructor(message) {
982
+ super(message);
983
+ this.name = "SessionLinkError";
984
+ }
985
+ };
793
986
  function openSessionLink(state) {
794
987
  return new Promise((resolve2, reject) => {
795
988
  const socket = (0, import_node_net.createConnection)(state.socket);
796
989
  const waiting = /* @__PURE__ */ new Map();
797
990
  let seq = 0;
798
991
  let open = false;
799
- const gone = `Session "${state.name}" closed the connection`;
992
+ let lastError = "";
993
+ const gone = () => `Session "${state.name}" closed the connection${lastError && `: ${lastError}`}`;
800
994
  socket.once("connect", () => {
801
995
  open = true;
802
996
  resolve2({
803
997
  request: (req) => new Promise((done, fail) => {
804
998
  if (socket.destroyed || socket.writableEnded) {
805
- fail(new Error(gone));
999
+ fail(new SessionLinkError(gone()));
806
1000
  return;
807
1001
  }
808
1002
  const id = ++seq;
@@ -813,18 +1007,22 @@ function openSessionLink(state) {
813
1007
  });
814
1008
  });
815
1009
  socket.on("error", (error) => {
1010
+ lastError = error.message;
816
1011
  if (!open)
817
1012
  reject(
818
- new Error(
1013
+ new SessionLinkError(
819
1014
  `Session "${state.name}" isn't answering on ${state.socket}: ${error.message}`
820
1015
  )
821
1016
  );
822
1017
  });
823
1018
  socket.on("close", () => {
824
- for (const [id, done] of waiting) done({ id, error: gone });
1019
+ for (const [id, done] of waiting) done({ id, error: gone(), linkLost: true });
825
1020
  waiting.clear();
826
1021
  });
827
- (0, import_node_readline.createInterface)({ input: socket }).on("line", (line) => {
1022
+ const lines = (0, import_node_readline.createInterface)({ input: socket });
1023
+ lines.on("error", () => {
1024
+ });
1025
+ lines.on("line", (line) => {
828
1026
  const res = JSON.parse(line);
829
1027
  waiting.get(res.id)?.(res);
830
1028
  waiting.delete(res.id);
@@ -838,10 +1036,16 @@ async function connectSession(options = {}) {
838
1036
  device: options.device,
839
1037
  transport: options.transport
840
1038
  });
841
- if (!state)
1039
+ if (!state) {
1040
+ const where = [
1041
+ explicitMetro(options.metro) && `on Metro ${explicitMetro(options.metro)}`,
1042
+ options.device && `on a device matching "${options.device}"`,
1043
+ options.transport && options.transport !== "auto" && `using ${options.transport}`
1044
+ ].filter(Boolean);
842
1045
  throw new Error(
843
- "No single session matches. Start one with `agent-bridge session start`, or name it."
1046
+ `No single session matches (looked for sessions ${where.length ? where.join(", ") : `started in ${projectRoot()}, or in an app folder under it`}). Start one with \`agent-bridge session start\`, or name it.`
844
1047
  );
1048
+ }
845
1049
  const link = await openSessionLink(state);
846
1050
  const info = await link.request({ op: "info" });
847
1051
  if (info.error || !info.device || !info.transport) {
@@ -858,6 +1062,7 @@ async function connectSession(options = {}) {
858
1062
  });
859
1063
  if (res.error || !res.result) {
860
1064
  const why = res.error ?? "No result";
1065
+ if (res.linkLost) throw new SessionLinkError(why);
861
1066
  throw res.notice ? new AgentBridgeCallError(tool, why, [], res.notice) : new Error(why);
862
1067
  }
863
1068
  const result = res.result;
@@ -915,7 +1120,7 @@ async function connect(options = {}) {
915
1120
  });
916
1121
  const timed = async (tool, ...args) => {
917
1122
  const t0 = performance.now();
918
- const result = await conn.call(tool, args, timeoutMs);
1123
+ const result = await conn.call(tool, args, callTimeout(timeoutMs, tool, args));
919
1124
  const ms = performance.now() - t0;
920
1125
  const logs = result.logs ?? [];
921
1126
  if (!result.ok) throw new AgentBridgeCallError(tool, result.error, logs);
@@ -956,5 +1161,6 @@ async function listDevices(options = {}) {
956
1161
  connectSession,
957
1162
  listDevices,
958
1163
  listSessions,
959
- runFlow
1164
+ runFlow,
1165
+ runFlows
960
1166
  });
@@ -44,9 +44,29 @@ type FlowApi = {
44
44
  step: (label: string, tool: string, ...args: unknown[]) => Promise<unknown>;
45
45
  /** What each declared scenario's apply returned, by name. */
46
46
  scenarios: Record<string, unknown>;
47
+ expect: FlowExpect;
48
+ };
49
+ /** Checks that throw a readable message; each runs as a timed step. */
50
+ type FlowExpect = {
51
+ /** Text, label or testID is on screen (waits up to `timeoutMs`, default 5000). */
52
+ text: (target: string, options?: {
53
+ timeoutMs?: number;
54
+ }) => Promise<void>;
55
+ /** The target is not on screen (waits for it to leave). */
56
+ gone: (target: string, options?: {
57
+ timeoutMs?: number;
58
+ }) => Promise<void>;
59
+ /** `router.current` has this pathname (waits up to `timeoutMs`, default 2000). */
60
+ route: (pathname: string, options?: {
61
+ timeoutMs?: number;
62
+ }) => Promise<void>;
63
+ /** A tool's result contains `match`: objects by the keys given, arrays and values exactly. */
64
+ result: (tool: string, args: unknown[], match: unknown) => Promise<void>;
47
65
  };
48
66
  /** A flow file: `export default async ({ step, call }) => {}`, plus what it needs. */
49
67
  type FlowModule = {
68
+ /** Shown in the run summary and matched by `--grep`; default: the file name. */
69
+ name?: string;
50
70
  default: (api: FlowApi) => Promise<void>;
51
71
  /** Applied before the flow and undone after it, even when it fails. */
52
72
  scenario?: FlowScenario;
@@ -60,6 +80,13 @@ type FlowResult = {
60
80
  restoreErrors: string[];
61
81
  /** "app reloaded; N pending restores lost" warnings seen during the flow. */
62
82
  notices: string[];
83
+ /** The app stopped answering (a timeout or closed connection), so no restore was tried. */
84
+ unreachable: boolean;
85
+ };
86
+ /** What was on screen when a flow failed. */
87
+ type FailureState = {
88
+ screen?: string[];
89
+ route?: unknown;
63
90
  };
64
91
  type RunFlowOptions = {
65
92
  /** Where step lines go. Default: console.log. */
@@ -69,14 +96,50 @@ type RunFlowOptions = {
69
96
  name: string;
70
97
  options?: unknown;
71
98
  }>;
99
+ /** Leave the app as the flow left it: no closing bridge.restore (for debugging). */
100
+ keep?: boolean;
101
+ /** Called with what was on screen when the flow failed. */
102
+ onFailure?: (state: FailureState) => void;
103
+ /** Called once the flow and its restore are done, also when it failed. */
104
+ onFinish?: (result: FlowResult) => void;
72
105
  };
73
106
  /**
74
- * Runs a flow module you imported: applies its scenarios, runs it, and when
75
- * there were any, undoes everything with bridge.restore afterwards, even
107
+ * Runs a flow module you imported: applies its scenarios, runs it, and
108
+ * undoes everything with bridge.restore afterwards (unless `keep`), even
76
109
  * when it fails. Throws what the flow threw; otherwise returns how it went.
77
110
  * It runs only the code you hand it, in this process.
78
111
  */
79
112
  declare function runFlow(bridge: AgentBridge, flow: FlowModule, options?: RunFlowOptions): Promise<FlowResult>;
113
+ type FlowEntry = {
114
+ file: string;
115
+ name: string;
116
+ module: FlowModule;
117
+ };
118
+ type FlowRun = {
119
+ file: string;
120
+ name: string;
121
+ status: 'passed' | 'failed' | 'skipped';
122
+ ms: number;
123
+ steps: number;
124
+ errors: number;
125
+ /** Why it failed: what the flow threw, restore failures, or app errors under `strict`. */
126
+ error?: string;
127
+ state?: FailureState;
128
+ restoreErrors: string[];
129
+ notices: string[];
130
+ };
131
+ type RunFlowsOptions = RunFlowOptions & {
132
+ /** Stop at the first failure; the rest are reported as skipped. */
133
+ bail?: boolean;
134
+ /** Count an error the app logged as a failure. */
135
+ strict?: boolean;
136
+ /** One flow, text output: no name header, and the failure goes to `printError`. */
137
+ single?: boolean;
138
+ /** Where a failed flow's reason goes when `single`. Default: console.error. */
139
+ printError?: (line: string) => void;
140
+ };
141
+ /** Runs flows in order on one connection, each followed by a restore (see `keep`), and never throws for a failing flow. */
142
+ declare function runFlows(bridge: AgentBridge, entries: FlowEntry[], options?: RunFlowsOptions): Promise<FlowRun[]>;
80
143
 
81
144
  /** What a running session writes next to its socket. Readable only by the user. */
82
145
  type SessionState = {
@@ -126,7 +189,7 @@ type ConnectOptions = {
126
189
  device?: string;
127
190
  /** "auto" tries Expo's socket first and falls back to CDP. */
128
191
  transport?: 'auto' | TransportName;
129
- /** Per-call timeout. Default 10 s. */
192
+ /** Per-call timeout. Default 10 s; stretched when a call's own `timeoutMs` argument is longer. */
130
193
  timeoutMs?: number;
131
194
  };
132
195
  declare class AgentBridgeCallError extends Error {
@@ -170,4 +233,4 @@ type ListedDevice = {
170
233
  /** Apps with the bridge on Expo's socket, plus every React Native page Metro knows. */
171
234
  declare function listDevices(options?: Pick<ConnectOptions, 'metro'>): Promise<ListedDevice[]>;
172
235
 
173
- export { type AgentBridge, AgentBridgeCallError, type ConnectOptions, type DeviceInfo, type FlowApi, type FlowModule, type FlowResult, type FlowScenario, type ListedDevice, type LogEntry, type RunFlowOptions, type SessionConnectOptions, type SessionState, type Timed, type ToolInfo, type TransportName, connect, connectSession, listDevices, listSessions, runFlow };
236
+ export { type AgentBridge, AgentBridgeCallError, type ConnectOptions, type DeviceInfo, type FlowApi, type FlowExpect, type FlowModule, type FlowResult, type FlowRun, type FlowScenario, type ListedDevice, type LogEntry, type RunFlowOptions, type SessionConnectOptions, type SessionState, type Timed, type ToolInfo, type TransportName, connect, connectSession, listDevices, listSessions, runFlow, runFlows };
@@ -4,13 +4,15 @@ import {
4
4
  connectSession,
5
5
  listDevices,
6
6
  listSessions,
7
- runFlow
8
- } from "../chunk-3QIG7A5W.js";
7
+ runFlow,
8
+ runFlows
9
+ } from "../chunk-Y6QFU5ON.js";
9
10
  export {
10
11
  AgentBridgeCallError,
11
12
  connect,
12
13
  connectSession,
13
14
  listDevices,
14
15
  listSessions,
15
- runFlow
16
+ runFlow,
17
+ runFlows
16
18
  };
@@ -1,4 +1,4 @@
1
- import { a as Transport } from '../types-BmpykP1P.cjs';
1
+ import { a as Transport } from '../types-qlwdcGjt.cjs';
2
2
 
3
3
  /**
4
4
  * Plain JSON over Expo's dev-tools plugin socket: no string evaluation, emoji