@avasapp/agent-bridge 0.4.2 → 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 +36 -14
- 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.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +13 -4
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
- package/dist/cli.js +2370 -103
- package/dist/client/index.cjs +441 -136
- 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 +838 -128
- package/dist/runtime/index.d.cts +4 -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 +34 -2
- package/skills/agent-bridge/SKILL.md +70 -22
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +19 -8
- package/skills/agent-bridge/references/large-data.md +6 -6
- package/skills/agent-bridge/references/network.md +25 -7
- package/skills/agent-bridge/references/realtime.md +5 -5
- package/skills/agent-bridge/references/restore.md +25 -7
- package/skills/agent-bridge/references/screen.md +25 -11
- package/skills/agent-bridge/references/state.md +31 -11
- package/skills/agent-bridge/references/webview.md +9 -9
- package/skills/agent-bridge/references/writing-scenarios.md +3 -3
|
@@ -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";
|
|
@@ -35,11 +50,11 @@ async function listCdpTargets(metro, signal) {
|
|
|
35
50
|
);
|
|
36
51
|
return await res.json();
|
|
37
52
|
}
|
|
38
|
-
function pickOne(items, filter,
|
|
53
|
+
function pickOne(items, filter, label2) {
|
|
39
54
|
const matching = filter ? items.filter(
|
|
40
|
-
(item) =>
|
|
55
|
+
(item) => label2(item).toLowerCase().includes(filter.toLowerCase())
|
|
41
56
|
) : items;
|
|
42
|
-
const connected = items.map(
|
|
57
|
+
const connected = items.map(label2).join("; ") || "none";
|
|
43
58
|
if (matching.length === 0) {
|
|
44
59
|
throw new Error(
|
|
45
60
|
filter ? `No app matches "${filter}". Connected: ${connected}` : "No app is connected to Metro."
|
|
@@ -52,11 +67,11 @@ function pickOne(items, filter, label) {
|
|
|
52
67
|
}
|
|
53
68
|
if (matching.length > 1) {
|
|
54
69
|
const exact = matching.filter(
|
|
55
|
-
(item) =>
|
|
70
|
+
(item) => label2(item).toLowerCase() === filter?.toLowerCase()
|
|
56
71
|
);
|
|
57
72
|
if (exact.length === 1) return exact[0];
|
|
58
73
|
throw new Error(
|
|
59
|
-
`${matching.length} apps match "${filter}"; narrow --device. Matching: ${matching.map(
|
|
74
|
+
`${matching.length} apps match "${filter}"; narrow --device. Matching: ${matching.map(label2).join("; ")}`
|
|
60
75
|
);
|
|
61
76
|
}
|
|
62
77
|
return matching[0];
|
|
@@ -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);
|
|
@@ -255,6 +271,247 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
|
|
|
255
271
|
};
|
|
256
272
|
}
|
|
257
273
|
|
|
274
|
+
// src/client/session/state.ts
|
|
275
|
+
import { createHash } from "crypto";
|
|
276
|
+
import {
|
|
277
|
+
chmodSync,
|
|
278
|
+
existsSync,
|
|
279
|
+
lstatSync,
|
|
280
|
+
mkdirSync,
|
|
281
|
+
readFileSync,
|
|
282
|
+
readdirSync,
|
|
283
|
+
rmSync,
|
|
284
|
+
writeFileSync
|
|
285
|
+
} from "fs";
|
|
286
|
+
import { tmpdir, userInfo } from "os";
|
|
287
|
+
import { dirname, join, resolve, sep } from "path";
|
|
288
|
+
function projectRoot(from = process.cwd()) {
|
|
289
|
+
for (let dir = resolve(from); ; dir = dirname(dir)) {
|
|
290
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
291
|
+
if (dirname(dir) === dir) return resolve(from);
|
|
292
|
+
}
|
|
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
|
+
}
|
|
305
|
+
var uid = () => process.getuid?.() ?? userInfo().username;
|
|
306
|
+
function privateDir(dir) {
|
|
307
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
308
|
+
const stat = lstatSync(dir);
|
|
309
|
+
if (!stat.isDirectory())
|
|
310
|
+
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
311
|
+
if (process.getuid && stat.uid !== process.getuid())
|
|
312
|
+
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
313
|
+
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
314
|
+
chmodSync(dir, 448);
|
|
315
|
+
return dir;
|
|
316
|
+
}
|
|
317
|
+
function stateDir() {
|
|
318
|
+
return privateDir(
|
|
319
|
+
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? join(process.env.XDG_RUNTIME_DIR, "agent-bridge") : join(tmpdir(), `agent-bridge-${uid()}`))
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
var shortHash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
323
|
+
function socketPath(name, dir) {
|
|
324
|
+
if (process.platform === "win32")
|
|
325
|
+
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
326
|
+
const path = join(dir, `${name}.sock`);
|
|
327
|
+
if (Buffer.byteLength(path) <= 100) return path;
|
|
328
|
+
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
329
|
+
return join(short, `${shortHash(path)}.sock`);
|
|
330
|
+
}
|
|
331
|
+
var sessionFiles = (name, dir = stateDir()) => ({
|
|
332
|
+
state: join(dir, `${name}.json`),
|
|
333
|
+
error: join(dir, `${name}.error`),
|
|
334
|
+
log: join(dir, `${name}.log`),
|
|
335
|
+
socket: socketPath(name, dir)
|
|
336
|
+
});
|
|
337
|
+
function checkName(name) {
|
|
338
|
+
if (!/^[\w.-]{1,40}$/.test(name))
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
341
|
+
);
|
|
342
|
+
return name;
|
|
343
|
+
}
|
|
344
|
+
function parseDuration(text) {
|
|
345
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/.exec(text.trim());
|
|
346
|
+
if (!match) throw new Error(`Can't read duration "${text}"; try 15m or 90s`);
|
|
347
|
+
const units = { s: 1e3, m: 6e4, h: 36e5 };
|
|
348
|
+
return Math.round(Number(match[1]) * (units[match[2] ?? "ms"] ?? 1));
|
|
349
|
+
}
|
|
350
|
+
function formatDuration(ms) {
|
|
351
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
352
|
+
if (s < 60) return `${s}s`;
|
|
353
|
+
if (s < 3600)
|
|
354
|
+
return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
355
|
+
return `${Math.floor(s / 3600)}h${String(Math.floor(s / 60) % 60).padStart(2, "0")}m`;
|
|
356
|
+
}
|
|
357
|
+
function isAlive(pid) {
|
|
358
|
+
try {
|
|
359
|
+
process.kill(pid, 0);
|
|
360
|
+
return true;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
return error.code === "EPERM";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function writePrivate(file, text) {
|
|
366
|
+
writeFileSync(file, text, { mode: 384 });
|
|
367
|
+
}
|
|
368
|
+
function removeSessionFiles(name, dir = stateDir()) {
|
|
369
|
+
const files = sessionFiles(name, dir);
|
|
370
|
+
rmSync(files.state, { force: true });
|
|
371
|
+
if (process.platform !== "win32") rmSync(files.socket, { force: true });
|
|
372
|
+
}
|
|
373
|
+
function readState(file) {
|
|
374
|
+
try {
|
|
375
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function listSessions(dir = stateDir()) {
|
|
381
|
+
const live = [];
|
|
382
|
+
for (const entry of readdirSync(dir)) {
|
|
383
|
+
if (!entry.endsWith(".json")) continue;
|
|
384
|
+
const state = readState(join(dir, entry));
|
|
385
|
+
if (!state) continue;
|
|
386
|
+
if (isAlive(state.pid)) live.push(state);
|
|
387
|
+
else removeSessionFiles(state.name, dir);
|
|
388
|
+
}
|
|
389
|
+
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
390
|
+
}
|
|
391
|
+
function readSession(name, dir = stateDir()) {
|
|
392
|
+
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
393
|
+
}
|
|
394
|
+
function pickSession(filter) {
|
|
395
|
+
if (filter.name) {
|
|
396
|
+
const named = readSession(checkName(filter.name));
|
|
397
|
+
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
398
|
+
return named;
|
|
399
|
+
}
|
|
400
|
+
const device = filter.device?.toLowerCase();
|
|
401
|
+
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
402
|
+
const project = projectRoot();
|
|
403
|
+
const matching = listSessions().filter(
|
|
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)
|
|
405
|
+
);
|
|
406
|
+
if (!explicit && matching.length > 1 && matching.every((s) => s.project !== project))
|
|
407
|
+
return null;
|
|
408
|
+
if (matching.length > 1)
|
|
409
|
+
throw new Error(
|
|
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`
|
|
411
|
+
);
|
|
412
|
+
return matching[0] ?? null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/client/metro-hint.ts
|
|
416
|
+
var range = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
417
|
+
var probe = {
|
|
418
|
+
ports: [...range(8081, 8090), ...range(8100, 8199)],
|
|
419
|
+
eachMs: 300,
|
|
420
|
+
totalMs: 1e3
|
|
421
|
+
};
|
|
422
|
+
var hostOf = (metro) => {
|
|
423
|
+
const colon = metro.lastIndexOf(":");
|
|
424
|
+
return colon > 0 && !metro.endsWith("]") ? metro.slice(0, colon) : metro;
|
|
425
|
+
};
|
|
426
|
+
async function answers(metro, signal) {
|
|
427
|
+
try {
|
|
428
|
+
const res = await fetch(`http://${metro}/status`, { signal });
|
|
429
|
+
return (await res.text()).includes("packager-status:running");
|
|
430
|
+
} catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function appsOn(metro, signal) {
|
|
435
|
+
try {
|
|
436
|
+
const res = await fetch(`http://${metro}/json/list`, { signal });
|
|
437
|
+
const targets = await res.json();
|
|
438
|
+
const names = targets.map(
|
|
439
|
+
(t) => `${t.title ?? "?"}${t.deviceName ? ` [${t.deviceName}]` : ""}`
|
|
440
|
+
);
|
|
441
|
+
return [...new Set(names)];
|
|
442
|
+
} catch {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function findMetros(metro) {
|
|
447
|
+
const host = hostOf(metro);
|
|
448
|
+
const candidates = new Set(probe.ports.map((port) => `${host}:${port}`));
|
|
449
|
+
try {
|
|
450
|
+
for (const s of listSessions()) candidates.add(s.metro);
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
candidates.delete(metro);
|
|
454
|
+
const deadline = AbortSignal.timeout(probe.totalMs);
|
|
455
|
+
const within2 = () => AbortSignal.any([deadline, AbortSignal.timeout(probe.eachMs)]);
|
|
456
|
+
const running = (await Promise.all(
|
|
457
|
+
[...candidates].map(async (m) => await answers(m, within2()) ? m : null)
|
|
458
|
+
)).filter((m) => m !== null);
|
|
459
|
+
return Promise.all(
|
|
460
|
+
running.map(async (m) => ({ metro: m, apps: await appsOn(m, within2()) }))
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
var label = (found) => `${found.metro} (${found.apps.length ? `${found.apps.length === 1 ? "app" : "apps"}: ${found.apps.join(", ")}` : "no app connected"})`;
|
|
464
|
+
var howToSet = (metro) => `Pass --metro ${metro} or set AGENT_BRIDGE_METRO=${metro}.`;
|
|
465
|
+
function elsewhere(found) {
|
|
466
|
+
const [only] = found;
|
|
467
|
+
if (found.length === 1 && only)
|
|
468
|
+
return `Metro is running on ${label(only)}. ${howToSet(only.metro)}`;
|
|
469
|
+
return `Metro is running on ${found.map(label).join("; ")}. ${howToSet("<host:port>")}`;
|
|
470
|
+
}
|
|
471
|
+
var errorText = (error) => {
|
|
472
|
+
if (!(error instanceof Error)) return String(error);
|
|
473
|
+
const code = error.code ?? "";
|
|
474
|
+
return `${error.message} ${code} ${error.cause ? errorText(error.cause) : ""}`;
|
|
475
|
+
};
|
|
476
|
+
var isRefused = (error) => /ECONNREFUSED|ConnectionRefused/.test(errorText(error));
|
|
477
|
+
var NO_APP = /No app is connected to Metro|No app matches/;
|
|
478
|
+
async function refusedMessage(metro) {
|
|
479
|
+
const found = await findMetros(metro);
|
|
480
|
+
if (found.length)
|
|
481
|
+
return `Nothing is listening on ${metro}. ${elsewhere(found)}`;
|
|
482
|
+
const host = hostOf(metro);
|
|
483
|
+
return `Nothing is listening on ${metro}, and no Metro answered on ${host} ports 8081-8090 or 8100-8199. Start Metro, or if it runs elsewhere, pass --metro <host:port> or set AGENT_BRIDGE_METRO=<host:port>.`;
|
|
484
|
+
}
|
|
485
|
+
async function noAppHint(metro, device) {
|
|
486
|
+
const causes = `Likely: the app isn't open or hasn't reloaded since the bridge was mounted, it uses another Metro${device ? `, or --device "${device}" doesn't match` : ""}.`;
|
|
487
|
+
const withApps = (await findMetros(metro)).filter((f) => f.apps.length);
|
|
488
|
+
return withApps.length ? `${causes} ${elsewhere(withApps)}` : causes;
|
|
489
|
+
}
|
|
490
|
+
async function refused(metro) {
|
|
491
|
+
try {
|
|
492
|
+
await fetch(`http://${metro}/status`, {
|
|
493
|
+
signal: AbortSignal.timeout(probe.eachMs)
|
|
494
|
+
});
|
|
495
|
+
return false;
|
|
496
|
+
} catch (error) {
|
|
497
|
+
return isRefused(error);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function explainConnectError(error, metro, device) {
|
|
501
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
502
|
+
if (NO_APP.test(message))
|
|
503
|
+
return new Error(`${message}
|
|
504
|
+
${await noAppHint(metro, device)}`, {
|
|
505
|
+
cause: error
|
|
506
|
+
});
|
|
507
|
+
if (await refused(metro))
|
|
508
|
+
return new Error(await refusedMessage(metro), { cause: error });
|
|
509
|
+
return error;
|
|
510
|
+
}
|
|
511
|
+
async function assertListening(metro) {
|
|
512
|
+
if (await refused(metro)) throw new Error(await refusedMessage(metro));
|
|
513
|
+
}
|
|
514
|
+
|
|
258
515
|
// src/client/cdp.ts
|
|
259
516
|
import WebSocket2 from "ws";
|
|
260
517
|
var CannotEvaluate = class extends Error {
|
|
@@ -359,8 +616,8 @@ async function connectCdp(metro, device, signal, pin) {
|
|
|
359
616
|
target: targetLabel(target)
|
|
360
617
|
},
|
|
361
618
|
device: { ...info, name: `${info.name} (${targetLabel(target)})` },
|
|
362
|
-
call(tool, args, timeoutMs) {
|
|
363
|
-
const call = { id: newCallId(), tool, args };
|
|
619
|
+
call(tool, args, timeoutMs, extra) {
|
|
620
|
+
const call = { id: newCallId(), tool, args, ...extra };
|
|
364
621
|
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
365
622
|
evaluate(
|
|
366
623
|
`${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
|
|
@@ -443,6 +700,40 @@ function logLine(entry) {
|
|
|
443
700
|
}
|
|
444
701
|
|
|
445
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
|
+
}
|
|
446
737
|
function flowScenarios(flow) {
|
|
447
738
|
const declared = [
|
|
448
739
|
...flow.scenario === void 0 ? [] : [flow.scenario],
|
|
@@ -483,6 +774,10 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
483
774
|
let errors = 0;
|
|
484
775
|
const t0 = performance.now();
|
|
485
776
|
const notices = [];
|
|
777
|
+
let lost = false;
|
|
778
|
+
const noteOutcome = (error) => {
|
|
779
|
+
lost = error !== void 0 && !(error instanceof AgentBridgeCallError);
|
|
780
|
+
};
|
|
486
781
|
const report = (logs, notice) => {
|
|
487
782
|
errors += logs.length;
|
|
488
783
|
for (const e of logs) print(` ${logLine(e)}`);
|
|
@@ -494,28 +789,82 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
494
789
|
const timed = async (tool, ...args) => {
|
|
495
790
|
try {
|
|
496
791
|
const result = await bridge.timed(tool, ...args);
|
|
792
|
+
noteOutcome();
|
|
497
793
|
report(result.logs, result.notice);
|
|
498
794
|
return result;
|
|
499
795
|
} catch (error) {
|
|
796
|
+
noteOutcome(error);
|
|
500
797
|
report(failedLogs(error), failedNotice(error));
|
|
501
798
|
throw error;
|
|
502
799
|
}
|
|
503
800
|
};
|
|
504
801
|
const call = async (tool, ...args) => (await timed(tool, ...args)).value;
|
|
505
|
-
const
|
|
506
|
-
const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
|
|
507
|
-
const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
|
|
508
|
-
report(failedLogs(error), failedNotice(error));
|
|
509
|
-
throw error;
|
|
510
|
-
});
|
|
802
|
+
const record = (label2, ms) => {
|
|
511
803
|
total += ms;
|
|
512
804
|
print(
|
|
513
|
-
`${String(++n).padStart(2, "0")} ${
|
|
805
|
+
`${String(++n).padStart(2, "0")} ${label2.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
|
|
514
806
|
);
|
|
515
|
-
|
|
516
|
-
|
|
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;
|
|
517
864
|
};
|
|
518
865
|
const restoreErrors = [];
|
|
866
|
+
let flowErrors = 0;
|
|
867
|
+
let unreachable = false;
|
|
519
868
|
const applied = {};
|
|
520
869
|
if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
|
|
521
870
|
throw new Error(
|
|
@@ -530,10 +879,22 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
530
879
|
bridge: { ...bridge, timed, call },
|
|
531
880
|
call,
|
|
532
881
|
step,
|
|
533
|
-
scenarios: applied
|
|
882
|
+
scenarios: applied,
|
|
883
|
+
expect: expectApi
|
|
534
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;
|
|
535
894
|
} finally {
|
|
536
|
-
|
|
895
|
+
flowErrors = errors;
|
|
896
|
+
unreachable = lost;
|
|
897
|
+
if (!lost && !options.keep && (needs.length || bridge.tools().some((t) => t.name === "bridge.restore"))) {
|
|
537
898
|
try {
|
|
538
899
|
const result = await step("restore", "bridge.restore");
|
|
539
900
|
for (const [name, value] of Object.entries(result ?? {})) {
|
|
@@ -541,17 +902,70 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
541
902
|
if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
|
|
542
903
|
}
|
|
543
904
|
} catch (error) {
|
|
905
|
+
unreachable = lost;
|
|
544
906
|
restoreErrors.push(
|
|
545
907
|
`bridge.restore: ${error instanceof Error ? error.message : String(error)}`
|
|
546
908
|
);
|
|
547
909
|
}
|
|
548
910
|
for (const e of restoreErrors) print(` ! restore failed: ${e}`);
|
|
549
911
|
}
|
|
912
|
+
options.onFinish?.({ steps: n, errors: flowErrors, restoreErrors, notices, unreachable });
|
|
550
913
|
}
|
|
551
914
|
print(
|
|
552
|
-
`${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" : ""}`
|
|
553
916
|
);
|
|
554
|
-
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;
|
|
555
969
|
}
|
|
556
970
|
|
|
557
971
|
// src/client/session/client.ts
|
|
@@ -562,148 +976,27 @@ import { createInterface } from "readline";
|
|
|
562
976
|
var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16)}`)}
|
|
563
977
|
`;
|
|
564
978
|
|
|
565
|
-
// src/client/session/state.ts
|
|
566
|
-
import { createHash } from "crypto";
|
|
567
|
-
import {
|
|
568
|
-
chmodSync,
|
|
569
|
-
existsSync,
|
|
570
|
-
lstatSync,
|
|
571
|
-
mkdirSync,
|
|
572
|
-
readFileSync,
|
|
573
|
-
readdirSync,
|
|
574
|
-
rmSync,
|
|
575
|
-
writeFileSync
|
|
576
|
-
} from "fs";
|
|
577
|
-
import { tmpdir, userInfo } from "os";
|
|
578
|
-
import { dirname, join, resolve } from "path";
|
|
579
|
-
function projectRoot(from = process.cwd()) {
|
|
580
|
-
for (let dir = resolve(from); ; dir = dirname(dir)) {
|
|
581
|
-
if (existsSync(join(dir, "package.json"))) return dir;
|
|
582
|
-
if (dirname(dir) === dir) return resolve(from);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
var uid = () => process.getuid?.() ?? userInfo().username;
|
|
586
|
-
function privateDir(dir) {
|
|
587
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
588
|
-
const stat = lstatSync(dir);
|
|
589
|
-
if (!stat.isDirectory())
|
|
590
|
-
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
591
|
-
if (process.getuid && stat.uid !== process.getuid())
|
|
592
|
-
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
593
|
-
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
594
|
-
chmodSync(dir, 448);
|
|
595
|
-
return dir;
|
|
596
|
-
}
|
|
597
|
-
function stateDir() {
|
|
598
|
-
return privateDir(
|
|
599
|
-
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? join(process.env.XDG_RUNTIME_DIR, "agent-bridge") : join(tmpdir(), `agent-bridge-${uid()}`))
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
var shortHash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
603
|
-
function socketPath(name, dir) {
|
|
604
|
-
if (process.platform === "win32")
|
|
605
|
-
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
606
|
-
const path = join(dir, `${name}.sock`);
|
|
607
|
-
if (Buffer.byteLength(path) <= 100) return path;
|
|
608
|
-
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
609
|
-
return join(short, `${shortHash(path)}.sock`);
|
|
610
|
-
}
|
|
611
|
-
var sessionFiles = (name, dir = stateDir()) => ({
|
|
612
|
-
state: join(dir, `${name}.json`),
|
|
613
|
-
error: join(dir, `${name}.error`),
|
|
614
|
-
log: join(dir, `${name}.log`),
|
|
615
|
-
socket: socketPath(name, dir)
|
|
616
|
-
});
|
|
617
|
-
function checkName(name) {
|
|
618
|
-
if (!/^[\w.-]{1,40}$/.test(name))
|
|
619
|
-
throw new Error(
|
|
620
|
-
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
621
|
-
);
|
|
622
|
-
return name;
|
|
623
|
-
}
|
|
624
|
-
function parseDuration(text) {
|
|
625
|
-
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/.exec(text.trim());
|
|
626
|
-
if (!match) throw new Error(`Can't read duration "${text}"; try 15m or 90s`);
|
|
627
|
-
const units = { s: 1e3, m: 6e4, h: 36e5 };
|
|
628
|
-
return Math.round(Number(match[1]) * (units[match[2] ?? "ms"] ?? 1));
|
|
629
|
-
}
|
|
630
|
-
function formatDuration(ms) {
|
|
631
|
-
const s = Math.max(0, Math.round(ms / 1e3));
|
|
632
|
-
if (s < 60) return `${s}s`;
|
|
633
|
-
if (s < 3600)
|
|
634
|
-
return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
635
|
-
return `${Math.floor(s / 3600)}h${String(Math.floor(s / 60) % 60).padStart(2, "0")}m`;
|
|
636
|
-
}
|
|
637
|
-
function isAlive(pid) {
|
|
638
|
-
try {
|
|
639
|
-
process.kill(pid, 0);
|
|
640
|
-
return true;
|
|
641
|
-
} catch (error) {
|
|
642
|
-
return error.code === "EPERM";
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
function writePrivate(file, text) {
|
|
646
|
-
writeFileSync(file, text, { mode: 384 });
|
|
647
|
-
}
|
|
648
|
-
function removeSessionFiles(name, dir = stateDir()) {
|
|
649
|
-
const files = sessionFiles(name, dir);
|
|
650
|
-
rmSync(files.state, { force: true });
|
|
651
|
-
if (process.platform !== "win32") rmSync(files.socket, { force: true });
|
|
652
|
-
}
|
|
653
|
-
function readState(file) {
|
|
654
|
-
try {
|
|
655
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
656
|
-
} catch {
|
|
657
|
-
return null;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
function listSessions(dir = stateDir()) {
|
|
661
|
-
const live = [];
|
|
662
|
-
for (const entry of readdirSync(dir)) {
|
|
663
|
-
if (!entry.endsWith(".json")) continue;
|
|
664
|
-
const state = readState(join(dir, entry));
|
|
665
|
-
if (!state) continue;
|
|
666
|
-
if (isAlive(state.pid)) live.push(state);
|
|
667
|
-
else removeSessionFiles(state.name, dir);
|
|
668
|
-
}
|
|
669
|
-
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
670
|
-
}
|
|
671
|
-
function readSession(name, dir = stateDir()) {
|
|
672
|
-
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
673
|
-
}
|
|
674
|
-
function pickSession(filter) {
|
|
675
|
-
if (filter.name) {
|
|
676
|
-
const named = readSession(checkName(filter.name));
|
|
677
|
-
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
678
|
-
return named;
|
|
679
|
-
}
|
|
680
|
-
const device = filter.device?.toLowerCase();
|
|
681
|
-
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
682
|
-
const project = projectRoot();
|
|
683
|
-
const matching = listSessions().filter(
|
|
684
|
-
(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)
|
|
685
|
-
);
|
|
686
|
-
if (matching.length > 1)
|
|
687
|
-
throw new Error(
|
|
688
|
-
`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`
|
|
689
|
-
);
|
|
690
|
-
return matching[0] ?? null;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
979
|
// src/client/session/client.ts
|
|
980
|
+
var SessionLinkError = class extends Error {
|
|
981
|
+
constructor(message) {
|
|
982
|
+
super(message);
|
|
983
|
+
this.name = "SessionLinkError";
|
|
984
|
+
}
|
|
985
|
+
};
|
|
694
986
|
function openSessionLink(state) {
|
|
695
987
|
return new Promise((resolve2, reject) => {
|
|
696
988
|
const socket = createConnection(state.socket);
|
|
697
989
|
const waiting = /* @__PURE__ */ new Map();
|
|
698
990
|
let seq = 0;
|
|
699
991
|
let open = false;
|
|
700
|
-
|
|
992
|
+
let lastError = "";
|
|
993
|
+
const gone = () => `Session "${state.name}" closed the connection${lastError && `: ${lastError}`}`;
|
|
701
994
|
socket.once("connect", () => {
|
|
702
995
|
open = true;
|
|
703
996
|
resolve2({
|
|
704
997
|
request: (req) => new Promise((done, fail) => {
|
|
705
998
|
if (socket.destroyed || socket.writableEnded) {
|
|
706
|
-
fail(new
|
|
999
|
+
fail(new SessionLinkError(gone()));
|
|
707
1000
|
return;
|
|
708
1001
|
}
|
|
709
1002
|
const id = ++seq;
|
|
@@ -714,18 +1007,22 @@ function openSessionLink(state) {
|
|
|
714
1007
|
});
|
|
715
1008
|
});
|
|
716
1009
|
socket.on("error", (error) => {
|
|
1010
|
+
lastError = error.message;
|
|
717
1011
|
if (!open)
|
|
718
1012
|
reject(
|
|
719
|
-
new
|
|
1013
|
+
new SessionLinkError(
|
|
720
1014
|
`Session "${state.name}" isn't answering on ${state.socket}: ${error.message}`
|
|
721
1015
|
)
|
|
722
1016
|
);
|
|
723
1017
|
});
|
|
724
1018
|
socket.on("close", () => {
|
|
725
|
-
for (const [id, done] of waiting) done({ id, error: gone });
|
|
1019
|
+
for (const [id, done] of waiting) done({ id, error: gone(), linkLost: true });
|
|
726
1020
|
waiting.clear();
|
|
727
1021
|
});
|
|
728
|
-
createInterface({ input: socket })
|
|
1022
|
+
const lines = createInterface({ input: socket });
|
|
1023
|
+
lines.on("error", () => {
|
|
1024
|
+
});
|
|
1025
|
+
lines.on("line", (line) => {
|
|
729
1026
|
const res = JSON.parse(line);
|
|
730
1027
|
waiting.get(res.id)?.(res);
|
|
731
1028
|
waiting.delete(res.id);
|
|
@@ -747,10 +1044,16 @@ async function connectSession(options = {}) {
|
|
|
747
1044
|
device: options.device,
|
|
748
1045
|
transport: options.transport
|
|
749
1046
|
});
|
|
750
|
-
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);
|
|
751
1053
|
throw new Error(
|
|
752
|
-
|
|
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.`
|
|
753
1055
|
);
|
|
1056
|
+
}
|
|
754
1057
|
const link = await openSessionLink(state);
|
|
755
1058
|
const info = await link.request({ op: "info" });
|
|
756
1059
|
if (info.error || !info.device || !info.transport) {
|
|
@@ -767,6 +1070,7 @@ async function connectSession(options = {}) {
|
|
|
767
1070
|
});
|
|
768
1071
|
if (res.error || !res.result) {
|
|
769
1072
|
const why = res.error ?? "No result";
|
|
1073
|
+
if (res.linkLost) throw new SessionLinkError(why);
|
|
770
1074
|
throw res.notice ? new AgentBridgeCallError(tool, why, [], res.notice) : new Error(why);
|
|
771
1075
|
}
|
|
772
1076
|
const result = res.result;
|
|
@@ -819,10 +1123,12 @@ Warning: ${notice}` : ""}`);
|
|
|
819
1123
|
};
|
|
820
1124
|
async function connect(options = {}) {
|
|
821
1125
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
822
|
-
const conn = await openConnection(options)
|
|
1126
|
+
const conn = await openConnection(options).catch(async (error) => {
|
|
1127
|
+
throw await explainConnectError(error, metroHost(options.metro), options.device);
|
|
1128
|
+
});
|
|
823
1129
|
const timed = async (tool, ...args) => {
|
|
824
1130
|
const t0 = performance.now();
|
|
825
|
-
const result = await conn.call(tool, args, timeoutMs);
|
|
1131
|
+
const result = await conn.call(tool, args, callTimeout(timeoutMs, tool, args));
|
|
826
1132
|
const ms = performance.now() - t0;
|
|
827
1133
|
const logs = result.logs ?? [];
|
|
828
1134
|
if (!result.ok) throw new AgentBridgeCallError(tool, result.error, logs);
|
|
@@ -858,15 +1164,10 @@ async function listDevices(options = {}) {
|
|
|
858
1164
|
}
|
|
859
1165
|
|
|
860
1166
|
export {
|
|
1167
|
+
callTimeout,
|
|
861
1168
|
explicitMetro,
|
|
862
1169
|
metroHost,
|
|
863
1170
|
RUNTIME_MARKER,
|
|
864
|
-
openConnection,
|
|
865
|
-
logLine,
|
|
866
|
-
flowScenarios,
|
|
867
|
-
parseScenarioFlag,
|
|
868
|
-
runFlow,
|
|
869
|
-
encodeLine,
|
|
870
1171
|
projectRoot,
|
|
871
1172
|
stateDir,
|
|
872
1173
|
sessionFiles,
|
|
@@ -879,6 +1180,17 @@ export {
|
|
|
879
1180
|
listSessions,
|
|
880
1181
|
readSession,
|
|
881
1182
|
pickSession,
|
|
1183
|
+
noAppHint,
|
|
1184
|
+
explainConnectError,
|
|
1185
|
+
assertListening,
|
|
1186
|
+
openConnection,
|
|
1187
|
+
logLine,
|
|
1188
|
+
flowScenarios,
|
|
1189
|
+
parseScenarioFlag,
|
|
1190
|
+
runFlow,
|
|
1191
|
+
runFlows,
|
|
1192
|
+
encodeLine,
|
|
1193
|
+
SessionLinkError,
|
|
882
1194
|
sessionRequest,
|
|
883
1195
|
connectSession,
|
|
884
1196
|
AgentBridgeCallError,
|