@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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { T as Tools } from '../types-
|
|
1
|
+
import { T as Tools } from '../types-qlwdcGjt.cjs';
|
|
2
|
+
import { R as Redact } from '../store-common-j0cHHvBr.cjs';
|
|
2
3
|
|
|
3
4
|
/** Anything with zustand's store shape. */
|
|
4
5
|
type StoreLike = {
|
|
@@ -8,24 +9,19 @@ type StoreLike = {
|
|
|
8
9
|
* syntax, so zustand's overloads (replace: false | true) still fit.
|
|
9
10
|
*/
|
|
10
11
|
setState(partial: Record<string, unknown>, replace?: boolean): void;
|
|
12
|
+
/** Present on stores made with zustand's `persist` middleware. */
|
|
13
|
+
persist?: {
|
|
14
|
+
getOptions?: () => {
|
|
15
|
+
partialize?: (state: never) => unknown;
|
|
16
|
+
};
|
|
17
|
+
hasHydrated?: () => boolean;
|
|
18
|
+
onFinishHydration?: (fn: () => void) => () => void;
|
|
19
|
+
};
|
|
11
20
|
};
|
|
12
|
-
/**
|
|
13
|
-
* Keeps secrets out of what store.* prints. Either dotted paths per store
|
|
14
|
-
* (`{ auth: ['accessToken', 'user.phone'] }`), or a hook called for the store
|
|
15
|
-
* value and every nested value with its dotted path; what it returns is
|
|
16
|
-
* printed instead.
|
|
17
|
-
*
|
|
18
|
-
* Paths are store paths. A `store.call` result that is a piece of the store's
|
|
19
|
-
* state (same object, e.g. `() => get().user`) is matched by where it lives; a
|
|
20
|
-
* copy or derived value (`() => ({ ...get().user })`) is not, so its paths are
|
|
21
|
-
* matched relative to the result and store paths like `user.phone` won't apply.
|
|
22
|
-
* Use the hook, or don't return secrets from actions.
|
|
23
|
-
*/
|
|
24
|
-
type Redact = Record<string, string[]> | ((store: string, path: string, value: unknown) => unknown);
|
|
25
21
|
type StoreToolsOptions = {
|
|
26
22
|
redact?: Redact;
|
|
27
23
|
};
|
|
28
24
|
/** Read, merge into, and call actions on named stores. */
|
|
29
25
|
declare function storeTools(stores: Record<string, StoreLike>, options?: StoreToolsOptions): Tools;
|
|
30
26
|
|
|
31
|
-
export {
|
|
27
|
+
export { Redact, type StoreLike, type StoreToolsOptions, storeTools };
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
// src/client/call-timeout.ts
|
|
2
|
+
var WAITERS = {
|
|
3
|
+
"screen.waitFor": 2e3,
|
|
4
|
+
// The page helper adds 5 s to timeoutMs before it gives up.
|
|
5
|
+
"webview.waitFor": 7e3
|
|
6
|
+
};
|
|
7
|
+
var MAX_MS = 10 * 6e4;
|
|
8
|
+
function callTimeout(base, tool, args) {
|
|
9
|
+
const margin = WAITERS[tool];
|
|
10
|
+
const t = args[1]?.timeoutMs;
|
|
11
|
+
if (margin === void 0 || typeof t !== "number" || !(t > 0) || !Number.isFinite(t))
|
|
12
|
+
return base;
|
|
13
|
+
return Math.max(base, Math.min(t + margin, MAX_MS));
|
|
14
|
+
}
|
|
15
|
+
|
|
1
16
|
// src/client/discover.ts
|
|
2
17
|
var within = (ms, signal) => signal ? AbortSignal.any([AbortSignal.timeout(ms), signal]) : AbortSignal.timeout(ms);
|
|
3
18
|
var DEFAULT_METRO = "localhost:8081";
|
|
@@ -232,7 +247,7 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
|
|
|
232
247
|
transport: "expo",
|
|
233
248
|
device: info,
|
|
234
249
|
pin: { name: info.name, platform: info.platform },
|
|
235
|
-
call(tool, args, timeoutMs) {
|
|
250
|
+
call(tool, args, timeoutMs, extra) {
|
|
236
251
|
if (superseded)
|
|
237
252
|
return Promise.resolve({
|
|
238
253
|
id: "",
|
|
@@ -245,7 +260,8 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
|
|
|
245
260
|
id: newCallId(),
|
|
246
261
|
tool,
|
|
247
262
|
args,
|
|
248
|
-
to: info.deviceId
|
|
263
|
+
to: info.deviceId,
|
|
264
|
+
...extra
|
|
249
265
|
};
|
|
250
266
|
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
251
267
|
send("call", call);
|
|
@@ -268,13 +284,24 @@ import {
|
|
|
268
284
|
writeFileSync
|
|
269
285
|
} from "fs";
|
|
270
286
|
import { tmpdir, userInfo } from "os";
|
|
271
|
-
import { dirname, join, resolve } from "path";
|
|
287
|
+
import { dirname, join, resolve, sep } from "path";
|
|
272
288
|
function projectRoot(from = process.cwd()) {
|
|
273
289
|
for (let dir = resolve(from); ; dir = dirname(dir)) {
|
|
274
290
|
if (existsSync(join(dir, "package.json"))) return dir;
|
|
275
291
|
if (dirname(dir) === dir) return resolve(from);
|
|
276
292
|
}
|
|
277
293
|
}
|
|
294
|
+
var isPackage = (dir) => existsSync(join(dir, "package.json"));
|
|
295
|
+
function inProject(session, project) {
|
|
296
|
+
if (!session) return false;
|
|
297
|
+
if (session === project) return true;
|
|
298
|
+
if (!session.startsWith(project + sep) || !isPackage(project)) return false;
|
|
299
|
+
for (let dir = session; dir !== project; dir = dirname(dir)) {
|
|
300
|
+
if (existsSync(join(dir, ".git"))) return false;
|
|
301
|
+
if (dir !== session && isPackage(dir)) return false;
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
278
305
|
var uid = () => process.getuid?.() ?? userInfo().username;
|
|
279
306
|
function privateDir(dir) {
|
|
280
307
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
@@ -374,8 +401,10 @@ function pickSession(filter) {
|
|
|
374
401
|
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
375
402
|
const project = projectRoot();
|
|
376
403
|
const matching = listSessions().filter(
|
|
377
|
-
(s) => (explicit || s.project
|
|
404
|
+
(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)
|
|
378
405
|
);
|
|
406
|
+
if (!explicit && matching.length > 1 && matching.every((s) => s.project !== project))
|
|
407
|
+
return null;
|
|
379
408
|
if (matching.length > 1)
|
|
380
409
|
throw new Error(
|
|
381
410
|
`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`
|
|
@@ -587,8 +616,8 @@ async function connectCdp(metro, device, signal, pin) {
|
|
|
587
616
|
target: targetLabel(target)
|
|
588
617
|
},
|
|
589
618
|
device: { ...info, name: `${info.name} (${targetLabel(target)})` },
|
|
590
|
-
call(tool, args, timeoutMs) {
|
|
591
|
-
const call = { id: newCallId(), tool, args };
|
|
619
|
+
call(tool, args, timeoutMs, extra) {
|
|
620
|
+
const call = { id: newCallId(), tool, args, ...extra };
|
|
592
621
|
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
593
622
|
evaluate(
|
|
594
623
|
`${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
|
|
@@ -671,6 +700,40 @@ function logLine(entry) {
|
|
|
671
700
|
}
|
|
672
701
|
|
|
673
702
|
// src/client/flow.ts
|
|
703
|
+
function deepMatches(actual, expected) {
|
|
704
|
+
if (expected instanceof RegExp) return typeof actual === "string" && expected.test(actual);
|
|
705
|
+
if (Array.isArray(expected))
|
|
706
|
+
return Array.isArray(actual) && actual.length === expected.length && expected.every((e, i) => deepMatches(actual[i], e));
|
|
707
|
+
if (expected && typeof expected === "object") {
|
|
708
|
+
const proto = Object.getPrototypeOf(expected);
|
|
709
|
+
if (proto !== Object.prototype && proto !== null)
|
|
710
|
+
throw new Error(
|
|
711
|
+
`A match can hold plain objects, arrays, values and RegExps, not a ${expected.constructor?.name ?? "class instance"}`
|
|
712
|
+
);
|
|
713
|
+
return !!actual && typeof actual === "object" && Object.entries(expected).every(
|
|
714
|
+
([k, v]) => deepMatches(actual[k], v)
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return Object.is(actual, expected);
|
|
718
|
+
}
|
|
719
|
+
var MAX_SCREEN_LINES = 40;
|
|
720
|
+
function compactScreen(snapshot) {
|
|
721
|
+
const elements = snapshot?.elements;
|
|
722
|
+
if (!Array.isArray(elements)) return [];
|
|
723
|
+
const lines = elements.slice(0, MAX_SCREEN_LINES).map((raw) => {
|
|
724
|
+
const e = raw;
|
|
725
|
+
const parts = [String(e.kind ?? "element")];
|
|
726
|
+
if (e.testID) parts.push(`#${String(e.testID)}`);
|
|
727
|
+
for (const key of ["text", "label", "placeholder", "value"])
|
|
728
|
+
if (typeof e[key] === "string" && e[key]) parts.push(JSON.stringify(e[key]));
|
|
729
|
+
if (e.disabled) parts.push("disabled");
|
|
730
|
+
if (e.checked) parts.push(`checked=${String(e.checked)}`);
|
|
731
|
+
return parts.join(" ");
|
|
732
|
+
});
|
|
733
|
+
if (elements.length > MAX_SCREEN_LINES)
|
|
734
|
+
lines.push(`... ${elements.length - MAX_SCREEN_LINES} more`);
|
|
735
|
+
return lines;
|
|
736
|
+
}
|
|
674
737
|
function flowScenarios(flow) {
|
|
675
738
|
const declared = [
|
|
676
739
|
...flow.scenario === void 0 ? [] : [flow.scenario],
|
|
@@ -711,6 +774,10 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
711
774
|
let errors = 0;
|
|
712
775
|
const t0 = performance.now();
|
|
713
776
|
const notices = [];
|
|
777
|
+
let lost = false;
|
|
778
|
+
const noteOutcome = (error) => {
|
|
779
|
+
lost = error !== void 0 && !(error instanceof AgentBridgeCallError);
|
|
780
|
+
};
|
|
714
781
|
const report = (logs, notice) => {
|
|
715
782
|
errors += logs.length;
|
|
716
783
|
for (const e of logs) print(` ${logLine(e)}`);
|
|
@@ -722,28 +789,82 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
722
789
|
const timed = async (tool, ...args) => {
|
|
723
790
|
try {
|
|
724
791
|
const result = await bridge.timed(tool, ...args);
|
|
792
|
+
noteOutcome();
|
|
725
793
|
report(result.logs, result.notice);
|
|
726
794
|
return result;
|
|
727
795
|
} catch (error) {
|
|
796
|
+
noteOutcome(error);
|
|
728
797
|
report(failedLogs(error), failedNotice(error));
|
|
729
798
|
throw error;
|
|
730
799
|
}
|
|
731
800
|
};
|
|
732
801
|
const call = async (tool, ...args) => (await timed(tool, ...args)).value;
|
|
733
|
-
const
|
|
734
|
-
const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
|
|
735
|
-
const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
|
|
736
|
-
report(failedLogs(error), failedNotice(error));
|
|
737
|
-
throw error;
|
|
738
|
-
});
|
|
802
|
+
const record = (label2, ms) => {
|
|
739
803
|
total += ms;
|
|
740
804
|
print(
|
|
741
805
|
`${String(++n).padStart(2, "0")} ${label2.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
|
|
742
806
|
);
|
|
743
|
-
|
|
744
|
-
|
|
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;
|
|
745
864
|
};
|
|
746
865
|
const restoreErrors = [];
|
|
866
|
+
let flowErrors = 0;
|
|
867
|
+
let unreachable = false;
|
|
747
868
|
const applied = {};
|
|
748
869
|
if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
|
|
749
870
|
throw new Error(
|
|
@@ -758,10 +879,22 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
758
879
|
bridge: { ...bridge, timed, call },
|
|
759
880
|
call,
|
|
760
881
|
step,
|
|
761
|
-
scenarios: applied
|
|
882
|
+
scenarios: applied,
|
|
883
|
+
expect: expectApi
|
|
762
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;
|
|
763
894
|
} finally {
|
|
764
|
-
|
|
895
|
+
flowErrors = errors;
|
|
896
|
+
unreachable = lost;
|
|
897
|
+
if (!lost && !options.keep && (needs.length || bridge.tools().some((t) => t.name === "bridge.restore"))) {
|
|
765
898
|
try {
|
|
766
899
|
const result = await step("restore", "bridge.restore");
|
|
767
900
|
for (const [name, value] of Object.entries(result ?? {})) {
|
|
@@ -769,17 +902,70 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
769
902
|
if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
|
|
770
903
|
}
|
|
771
904
|
} catch (error) {
|
|
905
|
+
unreachable = lost;
|
|
772
906
|
restoreErrors.push(
|
|
773
907
|
`bridge.restore: ${error instanceof Error ? error.message : String(error)}`
|
|
774
908
|
);
|
|
775
909
|
}
|
|
776
910
|
for (const e of restoreErrors) print(` ! restore failed: ${e}`);
|
|
777
911
|
}
|
|
912
|
+
options.onFinish?.({ steps: n, errors: flowErrors, restoreErrors, notices, unreachable });
|
|
778
913
|
}
|
|
779
914
|
print(
|
|
780
|
-
`${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" : ""}`
|
|
781
916
|
);
|
|
782
|
-
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;
|
|
783
969
|
}
|
|
784
970
|
|
|
785
971
|
// src/client/session/client.ts
|
|
@@ -791,19 +977,26 @@ var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]
|
|
|
791
977
|
`;
|
|
792
978
|
|
|
793
979
|
// src/client/session/client.ts
|
|
980
|
+
var SessionLinkError = class extends Error {
|
|
981
|
+
constructor(message) {
|
|
982
|
+
super(message);
|
|
983
|
+
this.name = "SessionLinkError";
|
|
984
|
+
}
|
|
985
|
+
};
|
|
794
986
|
function openSessionLink(state) {
|
|
795
987
|
return new Promise((resolve2, reject) => {
|
|
796
988
|
const socket = createConnection(state.socket);
|
|
797
989
|
const waiting = /* @__PURE__ */ new Map();
|
|
798
990
|
let seq = 0;
|
|
799
991
|
let open = false;
|
|
800
|
-
|
|
992
|
+
let lastError = "";
|
|
993
|
+
const gone = () => `Session "${state.name}" closed the connection${lastError && `: ${lastError}`}`;
|
|
801
994
|
socket.once("connect", () => {
|
|
802
995
|
open = true;
|
|
803
996
|
resolve2({
|
|
804
997
|
request: (req) => new Promise((done, fail) => {
|
|
805
998
|
if (socket.destroyed || socket.writableEnded) {
|
|
806
|
-
fail(new
|
|
999
|
+
fail(new SessionLinkError(gone()));
|
|
807
1000
|
return;
|
|
808
1001
|
}
|
|
809
1002
|
const id = ++seq;
|
|
@@ -814,18 +1007,22 @@ function openSessionLink(state) {
|
|
|
814
1007
|
});
|
|
815
1008
|
});
|
|
816
1009
|
socket.on("error", (error) => {
|
|
1010
|
+
lastError = error.message;
|
|
817
1011
|
if (!open)
|
|
818
1012
|
reject(
|
|
819
|
-
new
|
|
1013
|
+
new SessionLinkError(
|
|
820
1014
|
`Session "${state.name}" isn't answering on ${state.socket}: ${error.message}`
|
|
821
1015
|
)
|
|
822
1016
|
);
|
|
823
1017
|
});
|
|
824
1018
|
socket.on("close", () => {
|
|
825
|
-
for (const [id, done] of waiting) done({ id, error: gone });
|
|
1019
|
+
for (const [id, done] of waiting) done({ id, error: gone(), linkLost: true });
|
|
826
1020
|
waiting.clear();
|
|
827
1021
|
});
|
|
828
|
-
createInterface({ input: socket })
|
|
1022
|
+
const lines = createInterface({ input: socket });
|
|
1023
|
+
lines.on("error", () => {
|
|
1024
|
+
});
|
|
1025
|
+
lines.on("line", (line) => {
|
|
829
1026
|
const res = JSON.parse(line);
|
|
830
1027
|
waiting.get(res.id)?.(res);
|
|
831
1028
|
waiting.delete(res.id);
|
|
@@ -847,10 +1044,16 @@ async function connectSession(options = {}) {
|
|
|
847
1044
|
device: options.device,
|
|
848
1045
|
transport: options.transport
|
|
849
1046
|
});
|
|
850
|
-
if (!state)
|
|
1047
|
+
if (!state) {
|
|
1048
|
+
const where = [
|
|
1049
|
+
explicitMetro(options.metro) && `on Metro ${explicitMetro(options.metro)}`,
|
|
1050
|
+
options.device && `on a device matching "${options.device}"`,
|
|
1051
|
+
options.transport && options.transport !== "auto" && `using ${options.transport}`
|
|
1052
|
+
].filter(Boolean);
|
|
851
1053
|
throw new Error(
|
|
852
|
-
|
|
1054
|
+
`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.`
|
|
853
1055
|
);
|
|
1056
|
+
}
|
|
854
1057
|
const link = await openSessionLink(state);
|
|
855
1058
|
const info = await link.request({ op: "info" });
|
|
856
1059
|
if (info.error || !info.device || !info.transport) {
|
|
@@ -867,6 +1070,7 @@ async function connectSession(options = {}) {
|
|
|
867
1070
|
});
|
|
868
1071
|
if (res.error || !res.result) {
|
|
869
1072
|
const why = res.error ?? "No result";
|
|
1073
|
+
if (res.linkLost) throw new SessionLinkError(why);
|
|
870
1074
|
throw res.notice ? new AgentBridgeCallError(tool, why, [], res.notice) : new Error(why);
|
|
871
1075
|
}
|
|
872
1076
|
const result = res.result;
|
|
@@ -924,7 +1128,7 @@ async function connect(options = {}) {
|
|
|
924
1128
|
});
|
|
925
1129
|
const timed = async (tool, ...args) => {
|
|
926
1130
|
const t0 = performance.now();
|
|
927
|
-
const result = await conn.call(tool, args, timeoutMs);
|
|
1131
|
+
const result = await conn.call(tool, args, callTimeout(timeoutMs, tool, args));
|
|
928
1132
|
const ms = performance.now() - t0;
|
|
929
1133
|
const logs = result.logs ?? [];
|
|
930
1134
|
if (!result.ok) throw new AgentBridgeCallError(tool, result.error, logs);
|
|
@@ -960,6 +1164,7 @@ async function listDevices(options = {}) {
|
|
|
960
1164
|
}
|
|
961
1165
|
|
|
962
1166
|
export {
|
|
1167
|
+
callTimeout,
|
|
963
1168
|
explicitMetro,
|
|
964
1169
|
metroHost,
|
|
965
1170
|
RUNTIME_MARKER,
|
|
@@ -983,7 +1188,9 @@ export {
|
|
|
983
1188
|
flowScenarios,
|
|
984
1189
|
parseScenarioFlag,
|
|
985
1190
|
runFlow,
|
|
1191
|
+
runFlows,
|
|
986
1192
|
encodeLine,
|
|
1193
|
+
SessionLinkError,
|
|
987
1194
|
sessionRequest,
|
|
988
1195
|
connectSession,
|
|
989
1196
|
AgentBridgeCallError,
|