@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.
- package/README.md +19 -5
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
- package/dist/cli.js +1336 -255
- package/dist/client/index.cjs +233 -27
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +785 -128
- package/dist/runtime/index.d.cts +2 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +32 -1
- package/skills/agent-bridge/SKILL.md +39 -8
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +13 -3
- package/skills/agent-bridge/references/network.md +19 -1
- package/skills/agent-bridge/references/realtime.md +1 -1
- package/skills/agent-bridge/references/restore.md +21 -3
- package/skills/agent-bridge/references/screen.md +8 -2
- package/skills/agent-bridge/references/state.md +21 -1
package/dist/client/index.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
743
|
-
|
|
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
|
-
|
|
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})${
|
|
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
|
-
|
|
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
|
|
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
|
|
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 })
|
|
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
|
-
|
|
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
|
});
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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
|
|
75
|
-
*
|
|
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 };
|
package/dist/client/index.js
CHANGED
|
@@ -4,13 +4,15 @@ import {
|
|
|
4
4
|
connectSession,
|
|
5
5
|
listDevices,
|
|
6
6
|
listSessions,
|
|
7
|
-
runFlow
|
|
8
|
-
|
|
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
|
};
|
package/dist/expo/index.d.cts
CHANGED