@avasapp/agent-bridge 0.4.1 → 0.5.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/dist/cli.js CHANGED
@@ -2,10 +2,12 @@
2
2
  import {
3
3
  AgentBridgeCallError,
4
4
  RUNTIME_MARKER,
5
+ assertListening,
5
6
  checkName,
6
7
  connect,
7
8
  connectSession,
8
9
  encodeLine,
10
+ explainConnectError,
9
11
  explicitMetro,
10
12
  flowScenarios,
11
13
  formatDuration,
@@ -14,6 +16,7 @@ import {
14
16
  listSessions,
15
17
  logLine,
16
18
  metroHost,
19
+ noAppHint,
17
20
  openConnection,
18
21
  parseDuration,
19
22
  parseScenarioFlag,
@@ -26,11 +29,11 @@ import {
26
29
  sessionRequest,
27
30
  stateDir,
28
31
  writePrivate
29
- } from "./chunk-AJD3XTNT.js";
32
+ } from "./chunk-3QIG7A5W.js";
30
33
 
31
34
  // src/cli.ts
32
35
  import { readFile } from "fs/promises";
33
- import { resolve } from "path";
36
+ import { resolve as resolve2 } from "path";
34
37
  import { pathToFileURL } from "url";
35
38
  import { parseArgs } from "util";
36
39
 
@@ -591,11 +594,11 @@ import { appendFileSync as appendFileSync2, chmodSync, rmSync } from "fs";
591
594
  import { createServer } from "net";
592
595
  import { createInterface as createInterface3 } from "readline";
593
596
  var RELOAD = "app.reload";
594
- var pause = (ms, signal) => new Promise((resolve2) => {
597
+ var pause = (ms, signal) => new Promise((resolve3) => {
595
598
  const done = () => {
596
599
  clearTimeout(timer);
597
600
  signal.removeEventListener("abort", done);
598
- resolve2();
601
+ resolve3();
599
602
  };
600
603
  const timer = setTimeout(done, ms);
601
604
  signal.addEventListener("abort", done, { once: true });
@@ -619,22 +622,28 @@ async function runSessionDaemon(options) {
619
622
  const attemptMs = options.attemptMs ?? 12e3;
620
623
  const stopping = new AbortController();
621
624
  const cutOff = new AbortController();
622
- const reach = (signal) => openConnection({
625
+ const reach = (signal, pin2) => openConnection({
623
626
  metro,
624
627
  device: options.device,
628
+ pin: pin2,
625
629
  transport: options.transport,
626
630
  ...signal && { signal, timeoutMs: attemptMs }
627
631
  });
628
- let conn = await reach();
629
- const owner = listSessions(dir).find(
630
- (s) => s.metro === metro && s.device.deviceId === conn.device.deviceId
631
- );
632
- if (owner) {
633
- conn.close();
632
+ const claim = (c) => {
633
+ const owner = listSessions(dir).find(
634
+ (s) => s.name !== name && s.metro === metro && s.device.deviceId === c.device.deviceId
635
+ );
636
+ if (!owner) return;
637
+ c.close();
634
638
  throw new Error(
635
- `${conn.device.name} already belongs to session "${owner.name}". Use --session ${owner.name}, or stop it first.`
639
+ `${c.device.name} already belongs to session "${owner.name}". Use --session ${owner.name}, or stop it first.`
636
640
  );
637
- }
641
+ };
642
+ let conn = await reach().catch(async (error) => {
643
+ throw await explainConnectError(error, metro, options.device);
644
+ });
645
+ claim(conn);
646
+ const pin = conn.pin;
638
647
  const deviceOf = (c) => ({
639
648
  name: c.device.name,
640
649
  deviceId: c.device.deviceId,
@@ -690,7 +699,8 @@ async function runSessionDaemon(options) {
690
699
  const deadline = Date.now() + (patient ? reloadWaitMs : 0);
691
700
  for (; ; ) {
692
701
  try {
693
- const next = await reach(cutOff.signal);
702
+ const next = await reach(cutOff.signal, pin);
703
+ claim(next);
694
704
  if (patient && knownLoad && next.device.loadId === knownLoad) {
695
705
  next.close();
696
706
  throw new Error("the old runtime still answers");
@@ -948,11 +958,11 @@ async function runSessionDaemon(options) {
948
958
  });
949
959
  if (process.platform !== "win32") rmSync(files.socket, { force: true });
950
960
  try {
951
- await new Promise((resolve2, reject) => {
961
+ await new Promise((resolve3, reject) => {
952
962
  server.once("error", reject);
953
963
  server.listen(files.socket, () => {
954
964
  server.off("error", reject);
955
- resolve2();
965
+ resolve3();
956
966
  });
957
967
  });
958
968
  } catch (error) {
@@ -1039,7 +1049,7 @@ async function start(flags) {
1039
1049
  [...process.execArgv, process.argv[1], ...args],
1040
1050
  { detached: true, stdio: "ignore", windowsHide: true }
1041
1051
  );
1042
- const state = await new Promise((resolve2, reject) => {
1052
+ const state = await new Promise((resolve3, reject) => {
1043
1053
  const deadline = Date.now() + 3e4;
1044
1054
  let exited = false;
1045
1055
  child.once("exit", () => {
@@ -1050,7 +1060,7 @@ async function start(flags) {
1050
1060
  const failed = existsSync(files.error);
1051
1061
  if (ready && ready.pid === child.pid) {
1052
1062
  clearInterval(tick);
1053
- resolve2(ready);
1063
+ resolve3(ready);
1054
1064
  } else if (failed || exited || Date.now() > deadline) {
1055
1065
  clearInterval(tick);
1056
1066
  if (!exited) child.kill();
@@ -1172,10 +1182,1151 @@ async function daemonMain(flags) {
1172
1182
  process.exit(0);
1173
1183
  }
1174
1184
 
1185
+ // src/init/index.ts
1186
+ import { spawn as spawn2 } from "child_process";
1187
+ import {
1188
+ appendFileSync as appendFileSync3,
1189
+ existsSync as existsSync2,
1190
+ mkdirSync as mkdirSync2,
1191
+ readdirSync,
1192
+ readFileSync as readFileSync4,
1193
+ writeFileSync as writeFileSync2
1194
+ } from "fs";
1195
+ import { dirname as dirname5, relative as relative4, resolve, sep as sep2 } from "path";
1196
+ import { createInterface as createInterface4 } from "readline/promises";
1197
+
1198
+ // package.json
1199
+ var version = "0.5.0";
1200
+
1201
+ // src/init/detect.ts
1202
+ import { dirname as dirname3, join as join4, relative } from "path";
1203
+
1204
+ // src/init/package-manager.ts
1205
+ import { dirname as dirname2, join as join3 } from "path";
1206
+ var LOCKFILES = [
1207
+ ["bun.lock", "bun"],
1208
+ ["bun.lockb", "bun"],
1209
+ ["pnpm-lock.yaml", "pnpm"],
1210
+ ["yarn.lock", "yarn"],
1211
+ ["package-lock.json", "npm"]
1212
+ ];
1213
+ var fromName = (value) => {
1214
+ const name = typeof value === "string" ? value.split("@")[0] : void 0;
1215
+ return name === "bun" || name === "pnpm" || name === "yarn" || name === "npm" ? name : void 0;
1216
+ };
1217
+ function detectPackageManager(dir, root, fs, userAgent = "") {
1218
+ for (const d of [dir, root]) {
1219
+ const pm = fromName(fs.readJson(join3(d, "package.json"))?.packageManager);
1220
+ if (pm) return pm;
1221
+ }
1222
+ for (let d = dir; ; d = dirname2(d)) {
1223
+ for (const [file, pm] of LOCKFILES) if (fs.exists(join3(d, file))) return pm;
1224
+ if (d === root || dirname2(d) === d) break;
1225
+ }
1226
+ return fromName(userAgent.split("/")[0]) ?? "npm";
1227
+ }
1228
+ function installCommand(pm, spec) {
1229
+ switch (pm) {
1230
+ case "bun":
1231
+ return ["bun", "add", "-d", spec];
1232
+ case "pnpm":
1233
+ return ["pnpm", "add", "-D", spec];
1234
+ case "yarn":
1235
+ return ["yarn", "add", "-D", spec];
1236
+ case "npm":
1237
+ return ["npm", "install", "-D", spec];
1238
+ }
1239
+ }
1240
+ function binFallback(pm) {
1241
+ switch (pm) {
1242
+ case "bun":
1243
+ return "bunx @avasapp/agent-bridge";
1244
+ case "pnpm":
1245
+ return "pnpm exec agent-bridge";
1246
+ case "yarn":
1247
+ return "yarn agent-bridge";
1248
+ case "npm":
1249
+ return "./node_modules/.bin/agent-bridge";
1250
+ }
1251
+ }
1252
+
1253
+ // src/init/detect.ts
1254
+ var PACKAGE = "@avasapp/agent-bridge";
1255
+ var LIBRARIES = {
1256
+ expo: "expo",
1257
+ expoRouter: "expo-router",
1258
+ reactQuery: "@tanstack/react-query",
1259
+ webview: "react-native-webview",
1260
+ zustand: "zustand",
1261
+ mmkv: "react-native-mmkv",
1262
+ ably: "ably",
1263
+ socketIo: "socket.io-client",
1264
+ reactNavigation: "@react-navigation/native",
1265
+ typescript: "typescript"
1266
+ };
1267
+ var SOURCE_EXTS = ["tsx", "ts", "jsx", "js"];
1268
+ var depsOf = (pkg) => ({
1269
+ ...pkg?.peerDependencies,
1270
+ ...pkg?.devDependencies,
1271
+ ...pkg?.dependencies
1272
+ });
1273
+ var isApp = (dir, fs) => !!fs.readJson(join4(dir, "package.json"))?.dependencies?.["react-native"] || ["app.json", "app.config.js", "app.config.ts", "ios", "android"].some((f) => fs.exists(join4(dir, f)));
1274
+ var isWorkspaceRoot = (dir, fs) => !!fs.readJson(join4(dir, "package.json"))?.workspaces || fs.exists(join4(dir, "pnpm-workspace.yaml"));
1275
+ function findUp(dir, fs, test) {
1276
+ for (let d = dir; ; d = dirname3(d)) {
1277
+ if (test(d)) return d;
1278
+ if (fs.exists(join4(d, ".git")) || dirname3(d) === d) return void 0;
1279
+ }
1280
+ }
1281
+ function workspacePatterns(root, fs) {
1282
+ const ws = fs.readJson(join4(root, "package.json"))?.workspaces;
1283
+ const fromPkg = Array.isArray(ws) ? ws : ws?.packages ?? [];
1284
+ const yaml = fs.read(join4(root, "pnpm-workspace.yaml")) ?? "";
1285
+ const block = yaml.match(/^packages:\s*\n((?:[ \t]*(?:-.*|#.*)?\n?)*)/m)?.[1] ?? "";
1286
+ const fromYaml = [...block.matchAll(/^[ \t]*-[ \t]*['"]?([^'"#\n]+?)['"]?[ \t]*$/gm)].map(
1287
+ (m) => m[1]
1288
+ );
1289
+ return [...fromPkg, ...fromYaml].filter(
1290
+ (p) => typeof p === "string" && !p.startsWith("!")
1291
+ );
1292
+ }
1293
+ function expandWorkspaces(root, patterns, fs) {
1294
+ const found = /* @__PURE__ */ new Set();
1295
+ const walk = (dir, segments, depth) => {
1296
+ if (!segments.length) {
1297
+ if (fs.exists(join4(dir, "package.json"))) found.add(dir);
1298
+ return;
1299
+ }
1300
+ const [head, ...rest] = segments;
1301
+ const children = () => fs.list(dir).filter((e) => e.dir && e.name !== "node_modules" && !e.name.startsWith("."));
1302
+ if (head === "**") {
1303
+ walk(dir, rest, depth);
1304
+ if (depth < 4)
1305
+ for (const c of children()) walk(join4(dir, c.name), segments, depth + 1);
1306
+ } else if (head.includes("*")) {
1307
+ const re = new RegExp(
1308
+ `^${head.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`
1309
+ );
1310
+ for (const c of children())
1311
+ if (re.test(c.name)) walk(join4(dir, c.name), rest, depth);
1312
+ } else {
1313
+ walk(join4(dir, head), rest, depth);
1314
+ }
1315
+ };
1316
+ for (const p of patterns)
1317
+ walk(root, p.replace(/\/+$/, "").split("/").filter((s) => s && s !== "."), 0);
1318
+ return [...found].sort();
1319
+ }
1320
+ var firstExisting = (dir, fs, candidates) => candidates.find((c) => fs.exists(join4(dir, c)));
1321
+ var withExts = (base) => SOURCE_EXTS.map((e) => `${base}.${e}`);
1322
+ function detect(dir, fs, env = {}) {
1323
+ const pkg = fs.readJson(join4(dir, "package.json"));
1324
+ if (!pkg) return { error: `No package.json in ${dir}.` };
1325
+ const deps = depsOf(pkg);
1326
+ if (!deps["react-native"]) {
1327
+ if (isWorkspaceRoot(dir, fs)) {
1328
+ const apps = expandWorkspaces(dir, workspacePatterns(dir, fs), fs).filter((d) => isApp(d, fs)).map((d) => relative(dir, d));
1329
+ return {
1330
+ error: apps.length ? `${dir} is a monorepo root. Rerun with --dir pointing at the app: ${apps.map((a) => `--dir ${a}`).join(", ")}` : `${dir} is a monorepo root and no workspace package depends on react-native.`,
1331
+ workspaces: apps
1332
+ };
1333
+ }
1334
+ return {
1335
+ error: `${dir} isn't a React Native app: its package.json has no react-native dependency. Pass --dir <app>.`
1336
+ };
1337
+ }
1338
+ const root = findUp(dir, fs, (d) => isWorkspaceRoot(d, fs)) ?? dir;
1339
+ const repoRoot = findUp(dir, fs, (d) => fs.exists(join4(d, ".git"))) ?? root;
1340
+ const has = Object.fromEntries(
1341
+ Object.entries(LIBRARIES).map(([k, dep]) => [k, !!deps[dep]])
1342
+ );
1343
+ const appJson = fs.readJson(join4(dir, "app.json"));
1344
+ const appName = [appJson?.expo?.name, appJson?.name, pkg.name].find(
1345
+ (n) => typeof n === "string" && !!n
1346
+ ) ?? "app";
1347
+ const tsconfig = firstExisting(dir, fs, ["tsconfig.json"]);
1348
+ const rootFile = firstExisting(
1349
+ dir,
1350
+ fs,
1351
+ has.expoRouter ? [...withExts("app/_layout"), ...withExts("src/app/_layout")] : [...withExts("App"), ...withExts("src/App")]
1352
+ );
1353
+ const agentsFile = [.../* @__PURE__ */ new Set([dir, repoRoot])].flatMap((d) => [join4(d, "AGENTS.md"), join4(d, "CLAUDE.md")]).find((f) => fs.exists(f));
1354
+ let bin;
1355
+ for (let d = dir; ; d = dirname3(d)) {
1356
+ const candidate = join4(d, "node_modules", ".bin", "agent-bridge");
1357
+ if (fs.exists(candidate)) {
1358
+ bin = candidate;
1359
+ break;
1360
+ }
1361
+ if (d === repoRoot || dirname3(d) === d) break;
1362
+ }
1363
+ return {
1364
+ dir,
1365
+ root,
1366
+ repoRoot,
1367
+ appName,
1368
+ pm: detectPackageManager(dir, root, fs, env.userAgent),
1369
+ has,
1370
+ installed: !!deps[PACKAGE],
1371
+ typescript: !!tsconfig || has.typescript,
1372
+ srcDir: fs.exists(join4(dir, "src")),
1373
+ rootFile,
1374
+ tsconfig,
1375
+ oxlint: firstExisting(dir, fs, [".oxlintrc.json"]),
1376
+ eslint: firstExisting(dir, fs, [
1377
+ "eslint.config.js",
1378
+ "eslint.config.mjs",
1379
+ "eslint.config.cjs",
1380
+ "eslint.config.ts"
1381
+ ]),
1382
+ agentsFile,
1383
+ bin
1384
+ };
1385
+ }
1386
+
1387
+ // src/init/plan.ts
1388
+ import { dirname as dirname4, isAbsolute, join as join5, relative as relative2, sep } from "path";
1389
+
1390
+ // src/init/jsonc.ts
1391
+ function strip(text) {
1392
+ let out = "";
1393
+ let i = 0;
1394
+ while (i < text.length) {
1395
+ const c = text[i];
1396
+ if (c === '"') {
1397
+ let j = i + 1;
1398
+ while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
1399
+ out += text.slice(i, j + 1);
1400
+ i = j + 1;
1401
+ } else if (c === "/" && text[i + 1] === "/") {
1402
+ const end = text.indexOf("\n", i);
1403
+ const j = end === -1 ? text.length : end;
1404
+ out += " ".repeat(j - i);
1405
+ i = j;
1406
+ } else if (c === "/" && text[i + 1] === "*") {
1407
+ const end = text.indexOf("*/", i + 2);
1408
+ const j = end === -1 ? text.length : end + 2;
1409
+ out += text.slice(i, j).replace(/[^\n]/g, " ");
1410
+ i = j;
1411
+ } else {
1412
+ out += c;
1413
+ i++;
1414
+ }
1415
+ }
1416
+ return out.replace(/,(\s*[}\]])/g, " $1");
1417
+ }
1418
+ function parseJsonc(text) {
1419
+ try {
1420
+ return JSON.parse(strip(text));
1421
+ } catch {
1422
+ return void 0;
1423
+ }
1424
+ }
1425
+ function addToArray(text, key, value, initial = []) {
1426
+ const json = parseJsonc(text);
1427
+ if (!json || typeof json !== "object" || Array.isArray(json)) return void 0;
1428
+ const current = json[key];
1429
+ const clean = strip(text);
1430
+ const item = JSON.stringify(value);
1431
+ if (Array.isArray(current)) {
1432
+ const matches = [...clean.matchAll(new RegExp(`"${key}"\\s*:\\s*\\[`, "g"))];
1433
+ if (matches.length !== 1) return void 0;
1434
+ const at = matches[0].index + matches[0][0].length;
1435
+ const after = text.slice(at);
1436
+ const multiline = after.match(/^[ \t]*\n([ \t]*)/);
1437
+ if (!current.length) return `${text.slice(0, at)}${item}${after}`;
1438
+ if (multiline) return `${text.slice(0, at)}
1439
+ ${multiline[1]}${item},${after}`;
1440
+ return `${text.slice(0, at)}${item}, ${after}`;
1441
+ }
1442
+ if (current !== void 0) return void 0;
1443
+ const end = clean.slice(0, clean.lastIndexOf("}")).trimEnd().length;
1444
+ const indent = text.match(/^([ \t]+)"/m)?.[1] ?? " ";
1445
+ const comma = clean.slice(0, end).endsWith("{") ? "" : ",";
1446
+ const list2 = [...initial, value].map((v) => JSON.stringify(v)).join(", ");
1447
+ return `${text.slice(0, end)}${comma}
1448
+ ${indent}"${key}": [${list2}]${text.slice(end)}`;
1449
+ }
1450
+
1451
+ // src/init/render.ts
1452
+ import { createHash } from "crypto";
1453
+ var DOCS = "https://github.com/avas-app/agent-bridge/blob/main/docs";
1454
+ var FLOW_GLOB = "**/*.flow.*";
1455
+ var REALTIME = {
1456
+ "socket.io": { fn: "socketIoTools", from: "@avasapp/agent-bridge/socket.io", client: "socket" },
1457
+ ably: { fn: "ablyTools", from: "@avasapp/agent-bridge/ably", client: "ably" }
1458
+ };
1459
+ var STORES_WHY = "stores often hold tokens: add `redact` for those paths before wiring them";
1460
+ var MMKV_WHY = "MMKV often holds tokens and mmkv.get returns values as they are: wire only instances without secrets";
1461
+ var REALTIME_WHY = "the adapter must wrap the client where it is created, before the app subscribes";
1462
+ var NETWORK_PROMPT = "Wire network tools? In dev builds they patch fetch and XMLHttpRequest so the agent can log and mock requests.";
1463
+ var NETWORK_WHY = "networkTools patches fetch and XMLHttpRequest in dev builds so the agent can log and mock requests (net.*); it also enables the offline scenario";
1464
+ var storeKey = (name) => {
1465
+ const bare = name.replace(/^use/, "").replace(/Store$/, "") || name;
1466
+ return bare[0].toLowerCase() + bare.slice(1);
1467
+ };
1468
+ function realtimeLine(todo) {
1469
+ const r = REALTIME[todo.kind];
1470
+ return `export const realtimeDevTools = ${r.fn}(${todo.name ?? r.client})`;
1471
+ }
1472
+ function realtimeTodoText(todo) {
1473
+ const r = REALTIME[todo.kind];
1474
+ const where = todo.file ? `In ${todo.file}, right after \`${todo.name}\` is created` : `Where your ${todo.kind} client is created`;
1475
+ return `${where}, add \`${realtimeLine(todo)}\` (import { ${r.fn} } from '${r.from}'), then spread realtimeDevTools into tools`;
1476
+ }
1477
+ function renderBridgeFile(o) {
1478
+ const L = [];
1479
+ const core = ["cdpTransport", ...o.ts ? ["type Scenarios"] : [], "useAgentBridge"];
1480
+ L.push(
1481
+ `// Generated by agent-bridge init ${o.version}. Yours to edit: once changed, init never overwrites it.`,
1482
+ `import { ${core.join(", ")} } from '@avasapp/agent-bridge'`
1483
+ );
1484
+ if (o.expo) L.push(`import { expoTransport } from '@avasapp/agent-bridge/expo'`);
1485
+ if (o.expoRouter) L.push(`import { routerTools } from '@avasapp/agent-bridge/expo-router'`);
1486
+ if (o.network)
1487
+ L.push(`import { networkTools, strictNetwork } from '@avasapp/agent-bridge/network'`);
1488
+ if (o.webview)
1489
+ L.push(`import { webviewTools } from '@avasapp/agent-bridge/react-native-webview'`);
1490
+ if (o.reactQuery) {
1491
+ L.push(
1492
+ `import { queryTools } from '@avasapp/agent-bridge/tanstack-query'`,
1493
+ `import { QueryClientContext } from '@tanstack/react-query'`
1494
+ );
1495
+ }
1496
+ if (o.expoRouter) L.push(`import { router, useNavigationContainerRef } from 'expo-router'`);
1497
+ if (o.reactQuery) L.push(`import { useContext } from 'react'`);
1498
+ if (o.realtimeTools?.length)
1499
+ L.push("", ...o.realtimeTools.map((t) => `import { ${t.name} } from '${t.from}'`));
1500
+ if (o.stores) {
1501
+ const stores = o.stores.length ? o.stores : [{ name: "useAuthStore", from: "../stores/auth" }];
1502
+ L.push(
1503
+ "",
1504
+ "// Zustand stores are left out: they often hold tokens. Add `redact` for those",
1505
+ "// paths, then uncomment these and storeTools below. See",
1506
+ `// ${DOCS}/adapters.md`,
1507
+ `// import { storeTools } from '@avasapp/agent-bridge/zustand'`,
1508
+ ...stores.map((s) => `// import { ${s.name} } from '${s.from}'`)
1509
+ );
1510
+ }
1511
+ if (o.mmkv) {
1512
+ const mmkv = o.mmkv.length ? o.mmkv : [{ name: "storage", from: "../storage" }];
1513
+ L.push(
1514
+ "",
1515
+ "// MMKV is left out: it often holds tokens, and mmkv.get returns values as they",
1516
+ "// are. Wire only instances without secrets: uncomment these and mmkvTools below.",
1517
+ `// See ${DOCS}/adapters.md`,
1518
+ `// import { mmkvTools } from '@avasapp/agent-bridge/react-native-mmkv'`,
1519
+ ...mmkv.map((m) => `// import { ${m.name} } from '${m.from}'`)
1520
+ );
1521
+ }
1522
+ for (const todo of o.realtime) {
1523
+ const r = REALTIME[todo.kind];
1524
+ L.push(
1525
+ "",
1526
+ `// TODO ${todo.kind}: the adapter must wrap the client where it is created,`,
1527
+ `// before the app subscribes (${DOCS}/realtime.md).`,
1528
+ todo.file ? `// In ${todo.file}, right after \`${todo.name}\` is created, add:` : `// Where your ${todo.kind} client is created, add:`,
1529
+ `// import { ${r.fn} } from '${r.from}'`,
1530
+ `// ${realtimeLine(todo)}`,
1531
+ `// then import realtimeDevTools here and uncomment it in tools.`
1532
+ );
1533
+ }
1534
+ L.push("");
1535
+ if (o.ts) L.push("const scenarios: Scenarios = {");
1536
+ else L.push(`/** @type {import('@avasapp/agent-bridge').Scenarios} */`, "const scenarios = {");
1537
+ if (o.network) {
1538
+ L.push(
1539
+ " offline: {",
1540
+ ` description: 'Every request fails like a network failure; Metro still gets through.',`,
1541
+ " apply: ({ onUndo }) => {",
1542
+ " onUndo(strictNetwork({ offline: true }))",
1543
+ " },",
1544
+ " },"
1545
+ );
1546
+ }
1547
+ L.push(
1548
+ " // A user signed in locally with every request mocked: the state most flows",
1549
+ ` // start from. See ${DOCS}/scenarios.md`,
1550
+ " // signedIn: {",
1551
+ ` // description: 'Signed in locally; no request reaches a server.',`,
1552
+ " // apply: async ({ call, onUndo }) => {",
1553
+ ` // onUndo(strictNetwork()) // and mockApi, both from '@avasapp/agent-bridge/network'`,
1554
+ ` // onUndo(mockApi(API_URL, { 'GET /me': { json: user } }))`,
1555
+ ` // await call('store.set', 'auth', { token: 'local', user })`,
1556
+ " // return { user }",
1557
+ " // },",
1558
+ " // },",
1559
+ "}",
1560
+ "",
1561
+ "export function AgentBridge() {"
1562
+ );
1563
+ if (o.reactQuery)
1564
+ L.push(
1565
+ " // Undefined outside a QueryClientProvider: mount this inside one for query.* tools.",
1566
+ " const queryClient = useContext(QueryClientContext)"
1567
+ );
1568
+ L.push(
1569
+ " useAgentBridge({",
1570
+ ` name: '${o.appName.replace(/[\\']/g, "\\$&")}',`,
1571
+ ` transports: [${o.expo ? "expoTransport(), " : ""}cdpTransport()],`,
1572
+ " tools: {"
1573
+ );
1574
+ if (o.reactQuery) L.push(" ...(queryClient ? queryTools(queryClient) : {}),");
1575
+ if (o.expoRouter)
1576
+ L.push(" ...routerTools(router, { navigation: useNavigationContainerRef() }),");
1577
+ if (o.webview)
1578
+ L.push(
1579
+ ` // Each WebView also needs useWebViewTools (${DOCS}/webviews.md).`,
1580
+ " ...webviewTools(),"
1581
+ );
1582
+ if (o.network) L.push(" ...networkTools(),");
1583
+ for (const t of o.realtimeTools ?? []) L.push(` ...${t.name},`);
1584
+ if (o.stores) {
1585
+ const stores = o.stores.length ? o.stores : [{ name: "useAuthStore", from: "" }];
1586
+ const map = stores.map((s) => `${storeKey(s.name)}: ${s.name}`).join(", ");
1587
+ const first = storeKey(stores[0].name);
1588
+ L.push(` // ...storeTools({ ${map} }, { redact: { ${first}: ['token'] } }),`);
1589
+ }
1590
+ if (o.mmkv) {
1591
+ const mmkv = o.mmkv.length ? o.mmkv : [{ name: "storage", from: "" }];
1592
+ L.push(` // ...mmkvTools({ ${mmkv.map((m) => m.name).join(", ")} }),`);
1593
+ }
1594
+ if (o.realtime.length) L.push(" // ...realtimeDevTools,");
1595
+ if (o.reactNavigation) {
1596
+ const params = o.ts ? "name: string, params?: object" : "name, params";
1597
+ L.push(
1598
+ " // React Navigation has no adapter. With your container ref",
1599
+ " // (createNavigationContainerRef from @react-navigation/native), tools like these work:",
1600
+ ` // 'nav.navigate': (${params}) => navigationRef.navigate(name, params),`,
1601
+ ` // 'nav.current': () => navigationRef.getCurrentRoute(),`
1602
+ );
1603
+ }
1604
+ L.push(
1605
+ ` // Your own tools: any function, JSON in and out, e.g. 'cart.clear': () => clearCart(),`,
1606
+ " },",
1607
+ " scenarios,",
1608
+ " })",
1609
+ " return null",
1610
+ "}",
1611
+ ""
1612
+ );
1613
+ return stamp(L.join("\n"));
1614
+ }
1615
+ function renderMountSnippet(importPath2) {
1616
+ return `import { AgentBridge } from '${importPath2}'
1617
+ {__DEV__ ? <AgentBridge /> : null}`;
1618
+ }
1619
+ function renderSmokeFlow(o) {
1620
+ return stamp([
1621
+ `// Generated by agent-bridge init ${o.version}. Run it with:`,
1622
+ `// ${o.bin} run flows/smoke.flow.mjs`,
1623
+ `/** @type {import('@avasapp/agent-bridge/client').FlowModule['default']} */`,
1624
+ "const smoke = async ({ step }) => {",
1625
+ ` await step('snapshot', 'screen.snapshot')`,
1626
+ ...o.expoRouter ? [` await step('open home', 'router.navigate', '/')`] : [],
1627
+ " // TODO: wait for text your first screen always shows, for example:",
1628
+ ` // await step('home shown', 'screen.waitFor', 'Welcome')`,
1629
+ ` await step('undo', 'bridge.restore')`,
1630
+ "}",
1631
+ "",
1632
+ "export default smoke",
1633
+ ""
1634
+ ].join("\n"));
1635
+ }
1636
+ function renderFlowsReadme(bin) {
1637
+ return `Flows for \`${bin} run flows/<name>.flow.mjs\`: plain .mjs files named \`*.flow.mjs\`, kept out of \`app/\` and test globs. See ${DOCS}/cli.md#flows-where-they-go.
1638
+ `;
1639
+ }
1640
+ function agentsLine(appDir, bin) {
1641
+ const where = appDir ? `${appDir}/flows` : "flows";
1642
+ const run = `\`${bin} run flows/smoke.flow.mjs\``;
1643
+ return `- Flows for agent-bridge live in \`${where}/<name>.flow.mjs\` (never under \`app/\`, never \`*.test.*\`); run one ${appDir ? `from \`${appDir}\` with ${run}` : `with ${run}`}.`;
1644
+ }
1645
+ var HEADER = /^(\/\/ Generated by agent-bridge init \d+\.\d+\.\d+(?:-[\w.]+)?)( \(#([0-9a-f]{12})\))?\./;
1646
+ var hash = (body) => createHash("sha256").update(body).digest("hex").slice(0, 12);
1647
+ function stamp(content) {
1648
+ const nl = content.indexOf("\n");
1649
+ return content.slice(0, nl).replace(HEADER, `$1 (#${hash(content.slice(nl))}).`) + content.slice(nl);
1650
+ }
1651
+ function isPristine(text) {
1652
+ const nl = text.indexOf("\n");
1653
+ const recorded = text.slice(0, nl).match(HEADER)?.[3];
1654
+ return !!recorded && recorded === hash(text.slice(nl));
1655
+ }
1656
+ var NETWORK_LINES = [
1657
+ `import { networkTools } from '@avasapp/agent-bridge/network'`,
1658
+ "...networkTools(), // in useAgentBridge({ tools })"
1659
+ ];
1660
+ var indentOf = (text) => text.match(/^([ \t]+)"/m)?.[1] ?? " ";
1661
+ function patchJson(text, edit, manual) {
1662
+ let json;
1663
+ try {
1664
+ json = JSON.parse(text);
1665
+ } catch {
1666
+ return { manual };
1667
+ }
1668
+ if (!json || typeof json !== "object" || Array.isArray(json)) return { manual };
1669
+ if (!edit(json)) return { unchanged: true };
1670
+ return {
1671
+ text: JSON.stringify(json, null, indentOf(text)) + (text.endsWith("\n") ? "\n" : "")
1672
+ };
1673
+ }
1674
+ function patchPackageScripts(text) {
1675
+ return patchJson(
1676
+ text,
1677
+ (pkg) => {
1678
+ if (pkg.scripts?.flow !== void 0) return false;
1679
+ pkg.scripts = { ...pkg.scripts, flow: "agent-bridge run" };
1680
+ return true;
1681
+ },
1682
+ `Add to package.json scripts: "flow": "agent-bridge run"`
1683
+ );
1684
+ }
1685
+ var globRe = (glob) => new RegExp(
1686
+ `^${glob.replace(/^\.\//, "").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?")}$`
1687
+ );
1688
+ function includesFlows(pattern) {
1689
+ const last = pattern.split("/").pop() ?? "";
1690
+ const glob = last.includes(".") ? pattern : `${pattern.replace(/\/+$/, "")}${last === "*" ? "" : "/**/*"}`;
1691
+ return globRe(glob).test("flows/smoke.flow.mjs");
1692
+ }
1693
+ function patchTsconfig(text, baseAllowJs) {
1694
+ const manual = `Add "${FLOW_GLOB}" to "exclude" in tsconfig.json`;
1695
+ const json = parseJsonc(text);
1696
+ if (!json || typeof json !== "object") return { manual };
1697
+ if (Array.isArray(json.exclude) && json.exclude.includes(FLOW_GLOB)) return { unchanged: true };
1698
+ const allowJs = json.compilerOptions?.allowJs ?? baseAllowJs ?? true;
1699
+ const include = Array.isArray(json.include) ? json.include : json.files ? [] : ["**/*"];
1700
+ if (!allowJs || !include.some((p) => typeof p === "string" && includesFlows(p)))
1701
+ return { unchanged: true, note: "doesn't include .mjs files" };
1702
+ if (json.extends && !Array.isArray(json.exclude))
1703
+ return {
1704
+ manual: `${manual}, along with what it inherits from ${JSON.stringify(json.extends)}: an "exclude" here replaces the inherited one`
1705
+ };
1706
+ const patched = addToArray(text, "exclude", FLOW_GLOB, ["node_modules"]);
1707
+ return patched ? { text: patched } : { manual };
1708
+ }
1709
+ function patchOxlint(text) {
1710
+ const ignore = parseJsonc(text)?.ignorePatterns;
1711
+ if (Array.isArray(ignore) && ignore.includes(FLOW_GLOB)) return { unchanged: true };
1712
+ const patched = addToArray(text, "ignorePatterns", FLOW_GLOB);
1713
+ return patched ? { text: patched } : { manual: `Add "${FLOW_GLOB}" to "ignorePatterns" in .oxlintrc.json` };
1714
+ }
1715
+ var ESLINT_SNIPPET = `{ ignores: ['${FLOW_GLOB}'] },`;
1716
+ function patchEslint(text) {
1717
+ if (text.includes(FLOW_GLOB)) return { unchanged: true };
1718
+ const manual = `Add to the start of your ESLint flat config array: ${ESLINT_SNIPPET}`;
1719
+ const re = /^((?:module\.exports\s*=|export\s+default)\s*(?:defineConfig\(\s*)?\[)/gm;
1720
+ const matches = [...text.matchAll(re)];
1721
+ if (matches.length !== 1) return { manual };
1722
+ const at = matches[0].index + matches[0][1].length;
1723
+ return { text: `${text.slice(0, at)}
1724
+ ${ESLINT_SNIPPET}${text.slice(at)}` };
1725
+ }
1726
+
1727
+ // src/init/plan.ts
1728
+ var SKILL_COMMAND = ["npx", "skills", "add", "avas-app/agent-bridge"];
1729
+ var SKILL_RUN = ["npx", "--yes", "skills", "add", "avas-app/agent-bridge"];
1730
+ var PLUGIN_LINES = [
1731
+ "/plugin marketplace add avas-app/agent-bridge",
1732
+ "/plugin install agent-bridge@agent-bridge"
1733
+ ];
1734
+ var MARKERS = [
1735
+ ["expo", /expoTransport/, "expo"],
1736
+ ["expoRouter", /routerTools/, "expo-router"],
1737
+ ["reactQuery", /queryTools/, "@tanstack/react-query"],
1738
+ ["webview", /webviewTools/, "react-native-webview"],
1739
+ ["zustand", /storeTools/, "zustand"],
1740
+ ["mmkv", /mmkvTools/, "react-native-mmkv"],
1741
+ ["socketIo", /realtimeDevTools|socketIoTools|realtime/i, "socket.io-client"],
1742
+ ["ably", /realtimeDevTools|ablyTools|realtime/i, "ably"]
1743
+ ];
1744
+ var posix = (p) => p.split(sep).join("/");
1745
+ function importPath(fromFile, toFile) {
1746
+ const rel = posix(relative2(dirname4(fromFile), toFile)).replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
1747
+ return rel.startsWith(".") ? rel : `./${rel}`;
1748
+ }
1749
+ function binFor(d) {
1750
+ if (d.bin) {
1751
+ const rel = posix(relative2(d.dir, d.bin));
1752
+ return rel.startsWith(".") ? rel : `./${rel}`;
1753
+ }
1754
+ if (d.pm === "npm" && d.root !== d.dir)
1755
+ return `${posix(relative2(d.dir, d.root))}/node_modules/.bin/agent-bridge`;
1756
+ return binFallback(d.pm);
1757
+ }
1758
+ function bridgePath(d, out) {
1759
+ if (out) return isAbsolute(out) ? out : join5(d.dir, out);
1760
+ return join5(d.dir, d.srcDir ? "src/dev" : "dev", `agent-bridge.${d.typescript ? "tsx" : "js"}`);
1761
+ }
1762
+ function nextSteps(bin, o) {
1763
+ const { offline, runFrom } = o;
1764
+ const next = [];
1765
+ if (!o.mounted)
1766
+ next.push({
1767
+ step: o.elsewhere ? "Make sure the root component renders the component that calls useAgentBridge" : "Mount it: paste the snippet above"
1768
+ });
1769
+ next.push(
1770
+ { step: "Reload the app (press r in Metro)" },
1771
+ { step: "Start a session", command: `${bin} session start` },
1772
+ { step: "List the tools", command: `${bin} tools` },
1773
+ { step: "See what's on screen", command: `${bin} call screen.snapshot` }
1774
+ );
1775
+ if (offline)
1776
+ next.push({ step: "Try a scenario", command: `${bin} call scenario.apply '"offline"'` });
1777
+ next.push({ step: "Undo everything and disconnect", command: `${bin} session stop` });
1778
+ const from = runFrom === "." ? "" : `From \`${runFrom}\`: `;
1779
+ return {
1780
+ next,
1781
+ prompt: `This app has agent-bridge. ${from ? `${from}s` : "S"}tart a session with \`${bin} session start\`, see the screen with \`${bin} call screen.snapshot\`, and check changes through the bridge instead of tapping; stop the session when done.`
1782
+ };
1783
+ }
1784
+ var stripExt = (p) => p.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "").replace(/\/index$/, "");
1785
+ function readTsconfig(d, fs) {
1786
+ if (!d.tsconfig) return { aliases: [] };
1787
+ const file = join5(d.dir, d.tsconfig);
1788
+ const json = parseJsonc(fs.read(file) ?? "");
1789
+ if (!json) return { aliases: [] };
1790
+ const findExtended = (from2, ext2) => {
1791
+ const names = ext2.endsWith(".json") ? [ext2] : [ext2, `${ext2}.json`];
1792
+ const roots = ext2.startsWith(".") ? [dirname4(from2)] : ancestors(dirname4(from2), d.repoRoot).map((a) => join5(a, "node_modules"));
1793
+ for (const r of roots) for (const n of names) if (fs.exists(join5(r, n)) && !fs.list(join5(r, n)).length) return join5(r, n);
1794
+ return void 0;
1795
+ };
1796
+ let baseAllowJs;
1797
+ let ext = json.extends;
1798
+ let from = file;
1799
+ for (let depth = 0; typeof ext === "string" && depth < 4; depth++) {
1800
+ const path = findExtended(from, ext);
1801
+ const base = path ? parseJsonc(fs.read(path) ?? "") : void 0;
1802
+ if (!base) break;
1803
+ if (typeof base.compilerOptions?.allowJs === "boolean") {
1804
+ baseAllowJs = base.compilerOptions.allowJs;
1805
+ break;
1806
+ }
1807
+ ext = base.extends;
1808
+ from = path;
1809
+ }
1810
+ const baseDir = join5(d.dir, json.compilerOptions?.baseUrl ?? ".");
1811
+ const paths = json.compilerOptions?.paths ?? {};
1812
+ const aliases = Object.entries(paths).flatMap(
1813
+ ([pattern, targets]) => Array.isArray(targets) && typeof targets[0] === "string" ? [[pattern.replace(/\*$/, ""), join5(baseDir, targets[0].replace(/\*$/, ""))]] : []
1814
+ );
1815
+ return { json, baseAllowJs, aliases };
1816
+ }
1817
+ function ancestors(dir, stop2) {
1818
+ const out = [];
1819
+ for (let d = dir; ; d = dirname4(d)) {
1820
+ out.push(d);
1821
+ if (d === stop2 || dirname4(d) === d) return out;
1822
+ }
1823
+ }
1824
+ function importedFiles(file, text, aliases) {
1825
+ const specs = [...text.matchAll(/(?:from\s+|import\s+|require\(\s*)['"]([^'"]+)['"]/g)].map((m) => m[1]);
1826
+ return specs.flatMap((spec) => {
1827
+ if (spec.startsWith(".")) return [stripExt(join5(dirname4(file), spec))];
1828
+ const alias = aliases.find(([prefix]) => prefix && spec.startsWith(prefix));
1829
+ return alias ? [stripExt(join5(alias[1], spec.slice(alias[0].length)))] : [];
1830
+ });
1831
+ }
1832
+ function plan(d, found, fs, o, cwd) {
1833
+ const rel = (p) => posix(relative2(d.dir, p));
1834
+ const out = bridgePath(d, o.out);
1835
+ if (rel(out).startsWith("..") || isAbsolute(rel(out)))
1836
+ return { error: `--out ${o.out} is outside the app (${d.dir}).` };
1837
+ if (/^(src\/)?app\//.test(rel(out)))
1838
+ return {
1839
+ error: `${rel(out)} is under app/, where expo-router treats every file as a route. Pick another --out.`
1840
+ };
1841
+ const ts = /\.tsx?$/.test(out);
1842
+ const bin = binFor(d);
1843
+ const runFrom = posix(relative2(cwd, d.dir)) || ".";
1844
+ const tsconfig = readTsconfig(d, fs);
1845
+ const actions = [];
1846
+ const files = [];
1847
+ const commented = [];
1848
+ const todos = [];
1849
+ const generate = (path, content) => {
1850
+ const current = fs.read(path);
1851
+ if (current === void 0) {
1852
+ actions.push({ kind: "write", path, content });
1853
+ files.push({ path: rel(path), status: "written" });
1854
+ return "written";
1855
+ }
1856
+ const pristine = isPristine(current);
1857
+ if (!o.force) {
1858
+ files.push({
1859
+ path: rel(path),
1860
+ status: "skipped",
1861
+ note: pristine ? "exists; --force regenerates it" : "exists, edited since init wrote it"
1862
+ });
1863
+ return pristine ? "kept" : "edited";
1864
+ }
1865
+ if (pristine) {
1866
+ const same = current === content;
1867
+ if (!same) actions.push({ kind: "write", path, content });
1868
+ files.push({ path: rel(path), status: same ? "unchanged" : "written" });
1869
+ return "written";
1870
+ }
1871
+ actions.push({ kind: "write", path: `${path}.new`, content });
1872
+ files.push({
1873
+ path: rel(path),
1874
+ status: "skipped",
1875
+ note: `edited since init wrote it, so --force left it; wrote ${rel(path)}.new to compare`
1876
+ });
1877
+ files.push({ path: `${rel(path)}.new`, status: "written" });
1878
+ return "edited";
1879
+ };
1880
+ const imported = (list2) => list2.map((f) => ({ name: f.name, from: importPath(out, join5(d.dir, f.file)) }));
1881
+ const realtimeTools = imported(found.realtimeTools);
1882
+ const realtime = realtimeTools.length ? [] : [
1883
+ ...d.has.socketIo ? found.socketIo.length ? found.socketIo.map((f) => ({ kind: "socket.io", ...f })) : [{ kind: "socket.io" }] : [],
1884
+ ...d.has.ably ? found.ably.length ? found.ably.map((f) => ({ kind: "ably", ...f })) : [{ kind: "ably" }] : []
1885
+ ];
1886
+ const existingText = fs.read(out);
1887
+ const existing = found.bridgeUsers.filter((f) => stripExt(join5(d.dir, f)) !== stripExt(out));
1888
+ const elsewhere = existingText === void 0 && existing.length > 0 && !o.out;
1889
+ let state = "elsewhere";
1890
+ if (elsewhere) {
1891
+ files.push({
1892
+ path: rel(out),
1893
+ status: "skipped",
1894
+ note: `the app already uses the bridge in ${existing.join(", ")}; pass --out to write one anyway`
1895
+ });
1896
+ } else {
1897
+ state = generate(
1898
+ out,
1899
+ renderBridgeFile({
1900
+ version: o.version,
1901
+ appName: d.appName,
1902
+ ts,
1903
+ expo: d.has.expo,
1904
+ expoRouter: d.has.expoRouter,
1905
+ reactQuery: d.has.reactQuery,
1906
+ webview: d.has.webview,
1907
+ network: o.network.enabled,
1908
+ reactNavigation: d.has.reactNavigation && !d.has.expoRouter,
1909
+ stores: d.has.zustand ? imported(found.stores) : void 0,
1910
+ mmkv: d.has.mmkv ? imported(found.mmkv) : void 0,
1911
+ realtime,
1912
+ realtimeTools
1913
+ })
1914
+ );
1915
+ }
1916
+ const wrote = state === "written";
1917
+ const liveText = wrote ? void 0 : elsewhere ? existing.map((f) => fs.read(join5(d.dir, f)) ?? "").join("\n") : existingText;
1918
+ const network = wrote ? o.network.enabled : /networkTools\s*\(/.test(liveText ?? "");
1919
+ const offline = wrote ? o.network.enabled : /\boffline\s*:/.test(liveText ?? "");
1920
+ if (wrote) {
1921
+ if (d.has.zustand)
1922
+ commented.push({
1923
+ adapter: "zustand",
1924
+ items: found.stores.map((s) => `${s.name} (${s.file})`),
1925
+ why: `${STORES_WHY} (${DOCS}/adapters.md)`
1926
+ });
1927
+ if (d.has.mmkv)
1928
+ commented.push({
1929
+ adapter: "react-native-mmkv",
1930
+ items: found.mmkv.map((s) => `${s.name} (${s.file})`),
1931
+ why: `${MMKV_WHY} (${DOCS}/adapters.md)`
1932
+ });
1933
+ if (d.has.reactNavigation && !d.has.expoRouter)
1934
+ commented.push({
1935
+ adapter: "@react-navigation/native",
1936
+ items: ["nav.navigate", "nav.current"],
1937
+ why: "there is no React Navigation adapter; the sample tools need your container ref"
1938
+ });
1939
+ for (const t of realtime) todos.push(`${realtimeTodoText(t)}: ${REALTIME_WHY} (${DOCS}/realtime.md).`);
1940
+ }
1941
+ const missing = !wrote && liveText !== void 0 ? MARKERS.filter(([lib, marker]) => d.has[lib] && !marker.test(liveText)).map(([, , dep]) => dep) : [];
1942
+ const flows = join5(d.dir, "flows");
1943
+ generate(
1944
+ join5(flows, "smoke.flow.mjs"),
1945
+ renderSmokeFlow({ version: o.version, expoRouter: d.has.expoRouter, bin })
1946
+ );
1947
+ const readme = join5(flows, "README.md");
1948
+ if (fs.exists(readme)) files.push({ path: rel(readme), status: "skipped", note: "exists" });
1949
+ else {
1950
+ actions.push({ kind: "write", path: readme, content: renderFlowsReadme(bin) });
1951
+ files.push({ path: rel(readme), status: "written" });
1952
+ }
1953
+ const patch = (path, fn) => {
1954
+ if (!path) return;
1955
+ const abs = join5(d.dir, path);
1956
+ const result = fn(fs.read(abs) ?? "");
1957
+ if ("text" in result) {
1958
+ actions.push({ kind: "patch", path: abs, patch: fn });
1959
+ files.push({ path, status: "patched" });
1960
+ } else if ("manual" in result) {
1961
+ files.push({ path, status: "manual", note: result.manual });
1962
+ } else {
1963
+ files.push({ path, status: "unchanged", ...result.note && { note: result.note } });
1964
+ }
1965
+ };
1966
+ patch("package.json", patchPackageScripts);
1967
+ patch(d.tsconfig, (text) => patchTsconfig(text, tsconfig.baseAllowJs));
1968
+ patch(d.oxlint, patchOxlint);
1969
+ patch(d.eslint, patchEslint);
1970
+ if (d.agentsFile) {
1971
+ const text = fs.read(d.agentsFile) ?? "";
1972
+ const path = posix(relative2(d.dir, d.agentsFile));
1973
+ if (text.includes(".flow.mjs")) files.push({ path, status: "unchanged" });
1974
+ else {
1975
+ const appDir = posix(relative2(dirname4(d.agentsFile), d.dir));
1976
+ actions.push({ kind: "append", path: d.agentsFile, line: agentsLine(appDir, bin) });
1977
+ files.push({ path, status: "appended" });
1978
+ }
1979
+ }
1980
+ const spec = `${PACKAGE}@^${o.version}`;
1981
+ const install = installCommand(d.pm, spec);
1982
+ if (!d.installed && o.install) actions.unshift({ kind: "install", command: install, cwd: d.dir });
1983
+ const skill = o.skillYes ? [...SKILL_RUN, "-y"] : SKILL_RUN;
1984
+ if (o.skill) actions.push({ kind: "skill", command: skill, cwd: d.repoRoot });
1985
+ const rootFile = d.rootFile ? join5(d.dir, d.rootFile) : void 0;
1986
+ const rootImports = rootFile ? importedFiles(rootFile, fs.read(rootFile) ?? "", tsconfig.aliases) : [];
1987
+ const reachedFromRoot = (file) => stripExt(file) === stripExt(rootFile ?? "") || rootImports.includes(stripExt(file));
1988
+ const mounted = elsewhere ? existing.some((f) => reachedFromRoot(join5(d.dir, f))) : rootImports.includes(stripExt(out));
1989
+ const snippet = renderMountSnippet(importPath(rootFile ?? join5(d.dir, "App.tsx"), out));
1990
+ const where = elsewhere ? `The app already uses the bridge in ${existing.join(", ")}; make sure the root component renders it` : d.rootFile ? `Paste the import into ${d.rootFile} and the element into the root component's JSX` : `No root component found (looked for ${d.has.expoRouter ? "app/_layout or src/app/_layout" : "App or src/App"}): paste it into yours, fixing the import path`;
1991
+ const note = `${where}${d.has.reactQuery && !elsewhere ? ", inside QueryClientProvider so query.* tools work" : ""}.`;
1992
+ const regenerable = state === "written" || state === "kept";
1993
+ const ignored = o.network.chosen && !wrote && o.network.enabled !== network ? `${o.network.enabled ? "--network" : "--no-network"} had no effect: ${elsewhere ? existing.join(", ") : rel(out)} already exists${regenerable ? "; add --force to regenerate it" : "; edit it by hand"}.` : void 0;
1994
+ const choice = {
1995
+ enabled: network,
1996
+ open: !network && !(o.network.chosen && wrote),
1997
+ why: NETWORK_WHY,
1998
+ ...regenerable && { flag: network ? "--no-network --force" : "--network --force" },
1999
+ ...!network && { lines: NETWORK_LINES },
2000
+ ...ignored && { note: ignored }
2001
+ };
2002
+ const detected = MARKERS.filter(([lib]) => d.has[lib]).map(([, , dep]) => dep);
2003
+ if (d.has.reactNavigation && !d.has.expoRouter) detected.push("@react-navigation/native");
2004
+ return {
2005
+ actions,
2006
+ report: {
2007
+ ok: true,
2008
+ version: o.version,
2009
+ dryRun: false,
2010
+ app: { dir: d.dir, name: d.appName, packageManager: d.pm, detected },
2011
+ files,
2012
+ install: {
2013
+ status: d.installed ? "present" : "not-run",
2014
+ command: install.join(" ")
2015
+ },
2016
+ skill: { status: "not-run", command: SKILL_COMMAND.join(" "), plugin: PLUGIN_LINES },
2017
+ mount: { file: d.rootFile ?? null, mounted, snippet, note },
2018
+ choices: { network: choice },
2019
+ commented,
2020
+ todos,
2021
+ missing,
2022
+ existing,
2023
+ runFrom,
2024
+ scenarios: offline ? ["offline"] : [],
2025
+ ...nextSteps(bin, { mounted, offline, runFrom, elsewhere })
2026
+ }
2027
+ };
2028
+ }
2029
+
2030
+ // src/init/scan.ts
2031
+ import { join as join6, relative as relative3 } from "path";
2032
+ var CODE = /\.(tsx?|jsx?|mjs|cjs)$/;
2033
+ var TESTS = /\.(test|spec)\.|(^|\/)__tests__\//;
2034
+ var imports = (text, module) => new RegExp(
2035
+ `(from\\s+|require\\(\\s*)['"]${module.replace(/[.]/g, "\\.")}(/[\\w./-]+)?['"]`
2036
+ ).test(text);
2037
+ var TYPED = "(?:\\s*:[^=]+)?";
2038
+ var PATTERNS = [
2039
+ ["stores", "zustand", new RegExp(`export\\s+const\\s+(use\\w+)${TYPED}\\s*=\\s*create\\w*\\s*[<(]`, "g")],
2040
+ ["mmkv", "react-native-mmkv", new RegExp(`export\\s+const\\s+(\\w+)${TYPED}\\s*=\\s*(?:new\\s+MMKV|createMMKV)\\s*[<(]`, "g")],
2041
+ ["socketIo", "socket.io-client", new RegExp(`(?:const|let)\\s+(\\w+)${TYPED}\\s*=\\s*io\\s*\\(`, "g")],
2042
+ ["ably", "ably", new RegExp(`(?:const|let)\\s+(\\w+)${TYPED}\\s*=\\s*new\\s+(?:Ably\\.)?Realtime(?:\\.Promise)?\\s*\\(`, "g")],
2043
+ ["realtimeTools", "@avasapp/agent-bridge", /export\s+const\s+(\w+)\s*=\s*(?:socketIoTools|ablyTools)\s*\(/g]
2044
+ ];
2045
+ function scan(dir, fs, { maxFiles = 2e3, maxBytes = 512 * 1024 } = {}) {
2046
+ const files = [];
2047
+ const walk = (d, deep) => {
2048
+ const entries = fs.list(d).sort((a, b) => a.name < b.name ? -1 : 1);
2049
+ for (const e of entries) {
2050
+ if (files.length >= maxFiles) return;
2051
+ if (e.name.startsWith(".") || e.name === "node_modules") continue;
2052
+ const path = join6(d, e.name);
2053
+ if (e.dir) {
2054
+ if (deep) walk(path, true);
2055
+ } else if (CODE.test(e.name) && !TESTS.test(relative3(dir, path))) {
2056
+ files.push(path);
2057
+ }
2058
+ }
2059
+ };
2060
+ walk(dir, false);
2061
+ for (const sub of ["src", "app", "lib"]) walk(join6(dir, sub), true);
2062
+ const result = { stores: [], mmkv: [], socketIo: [], ably: [], realtimeTools: [], bridgeUsers: [] };
2063
+ for (const path of files) {
2064
+ const text = fs.read(path);
2065
+ if (!text || text.length > maxBytes) continue;
2066
+ if (/\buseAgentBridge\s*\(|(from\s+|require\(\s*)['"]@avasapp\/agent-bridge['"]/.test(text))
2067
+ result.bridgeUsers.push(relative3(dir, path));
2068
+ for (const [key, module, re] of PATTERNS) {
2069
+ if (!imports(text, module)) continue;
2070
+ for (const m of text.matchAll(re))
2071
+ result[key].push({ name: m[1], file: relative3(dir, path) });
2072
+ }
2073
+ }
2074
+ return result;
2075
+ }
2076
+
2077
+ // src/init/index.ts
2078
+ var INSTALL_TIMEOUT_MS = 5 * 6e4;
2079
+ var SKILL_TIMEOUT_MS = 2 * 6e4;
2080
+ var posix2 = (p) => p.split(sep2).join("/");
2081
+ var nodeFs = {
2082
+ exists: existsSync2,
2083
+ read: (path) => {
2084
+ try {
2085
+ return readFileSync4(path, "utf8");
2086
+ } catch {
2087
+ return void 0;
2088
+ }
2089
+ },
2090
+ readJson: (path) => {
2091
+ try {
2092
+ return JSON.parse(readFileSync4(path, "utf8"));
2093
+ } catch {
2094
+ return void 0;
2095
+ }
2096
+ },
2097
+ list: (dir) => {
2098
+ try {
2099
+ return readdirSync(dir, { withFileTypes: true }).map((e) => ({
2100
+ name: e.name,
2101
+ dir: e.isDirectory()
2102
+ }));
2103
+ } catch {
2104
+ return [];
2105
+ }
2106
+ }
2107
+ };
2108
+ function nodeIo() {
2109
+ return {
2110
+ fs: nodeFs,
2111
+ write: (path, content) => {
2112
+ mkdirSync2(dirname5(path), { recursive: true });
2113
+ writeFileSync2(path, content);
2114
+ },
2115
+ append: (path, text) => appendFileSync3(path, text),
2116
+ exec: (command, cwd, interactive, timeoutMs) => new Promise((done) => {
2117
+ const [cmd, ...args] = command;
2118
+ const child = spawn2(cmd, args, {
2119
+ cwd,
2120
+ stdio: [interactive ? "inherit" : "ignore", 2, 2],
2121
+ shell: process.platform === "win32"
2122
+ });
2123
+ const timer = setTimeout(() => {
2124
+ child.kill("SIGTERM");
2125
+ done("timeout");
2126
+ }, timeoutMs);
2127
+ child.on("error", () => {
2128
+ clearTimeout(timer);
2129
+ done(127);
2130
+ });
2131
+ child.on("close", (code) => {
2132
+ clearTimeout(timer);
2133
+ done(code ?? 1);
2134
+ });
2135
+ }),
2136
+ prompt: async (question) => {
2137
+ const rl = createInterface4({ input: process.stdin, output: process.stderr });
2138
+ try {
2139
+ return await rl.question(question);
2140
+ } finally {
2141
+ rl.close();
2142
+ }
2143
+ },
2144
+ print: (text) => process.stdout.write(`${text}
2145
+ `),
2146
+ warn: (text) => process.stderr.write(`${text}
2147
+ `),
2148
+ interactive: !!process.stdin.isTTY && !!process.stdout.isTTY && !/^(1|true)$/i.test(process.env.CI ?? ""),
2149
+ cwd: process.cwd(),
2150
+ userAgent: process.env.npm_config_user_agent
2151
+ };
2152
+ }
2153
+ async function apply(actions, report, io, interactive) {
2154
+ const why = (code, ms) => code === "timeout" ? `timed out after ${ms / 6e4} min` : `exit code ${code}`;
2155
+ for (const a of actions) {
2156
+ switch (a.kind) {
2157
+ case "install": {
2158
+ const code = await io.exec(a.command, a.cwd, false, INSTALL_TIMEOUT_MS);
2159
+ report.install.status = code === 0 ? "installed" : "failed";
2160
+ if (code !== 0) {
2161
+ report.ok = false;
2162
+ report.install.error = why(code, INSTALL_TIMEOUT_MS);
2163
+ io.warn(`Install failed (${a.command.join(" ")}): ${report.install.error}`);
2164
+ }
2165
+ break;
2166
+ }
2167
+ case "skill": {
2168
+ const code = await io.exec(a.command, a.cwd, interactive, SKILL_TIMEOUT_MS);
2169
+ report.skill.status = code === 0 ? "added" : "failed";
2170
+ if (code !== 0) {
2171
+ report.skill.error = why(code, SKILL_TIMEOUT_MS);
2172
+ io.warn(`Adding the skill failed (${report.skill.error}); add it later with ${report.skill.command}`);
2173
+ }
2174
+ break;
2175
+ }
2176
+ case "write":
2177
+ io.write(a.path, a.content);
2178
+ break;
2179
+ case "patch": {
2180
+ const result = a.patch(io.fs.read(a.path) ?? "");
2181
+ if ("text" in result) io.write(a.path, result.text);
2182
+ break;
2183
+ }
2184
+ case "append": {
2185
+ const text = io.fs.read(a.path) ?? "";
2186
+ io.append(a.path, `${text.endsWith("\n") || !text ? "" : "\n"}${a.line}
2187
+ `);
2188
+ break;
2189
+ }
2190
+ }
2191
+ }
2192
+ }
2193
+ var describe = (a, dir) => {
2194
+ const rel = (p) => relative4(dir, p) || ".";
2195
+ switch (a.kind) {
2196
+ case "install":
2197
+ return `install ${a.command.join(" ")}`;
2198
+ case "skill":
2199
+ return `skill ${a.command.join(" ")} (in ${a.cwd})`;
2200
+ case "write":
2201
+ return `write ${rel(a.path)}`;
2202
+ case "patch":
2203
+ return `patch ${rel(a.path)}`;
2204
+ case "append":
2205
+ return `append ${rel(a.path)}: ${a.line}`;
2206
+ }
2207
+ };
2208
+ function formatReport(r, actions) {
2209
+ const L = [];
2210
+ const detected = r.app.detected.length ? ` (${r.app.detected.join(", ")})` : "";
2211
+ L.push(
2212
+ `agent-bridge init ${r.version}${r.dryRun ? " --dry-run: nothing changed" : ""}`,
2213
+ `App "${r.app.name}"${detected}, ${r.app.packageManager}`,
2214
+ ""
2215
+ );
2216
+ if (r.dryRun) {
2217
+ L.push("Would run:", ...actions.map((a) => ` ${describe(a, r.app.dir)}`), "");
2218
+ for (const a of actions)
2219
+ if (a.kind === "write")
2220
+ L.push(`--- ${relative4(r.app.dir, a.path)}`, a.content.trimEnd(), "");
2221
+ }
2222
+ L.push("Files");
2223
+ for (const f of r.files)
2224
+ L.push(` ${f.status.padEnd(9)} ${f.path}${f.note ? `: ${f.note}` : ""}`);
2225
+ L.push(
2226
+ "",
2227
+ `Install: ${r.install.status} (${r.install.command})`,
2228
+ `Skill: ${r.skill.status} (${r.skill.command})`,
2229
+ " In Claude Code, install it as a plugin instead:",
2230
+ ...r.skill.plugin.map((l) => ` ${l}`)
2231
+ );
2232
+ if (r.existing.length)
2233
+ L.push("", `Already uses the bridge: ${r.existing.join(", ")}`);
2234
+ if (r.missing.length)
2235
+ L.push("", `Detected but not wired in the bridge file: ${r.missing.join(", ")} (see ${DOCS}/adapters.md)`);
2236
+ if (r.commented.length) {
2237
+ L.push("", "Left as comments in the bridge file:");
2238
+ for (const c of r.commented)
2239
+ L.push(` ${c.adapter}${c.items.length ? `: ${c.items.join(", ")}` : ""}. Why: ${c.why}`);
2240
+ }
2241
+ if (r.todos.length) L.push("", "TODO:", ...r.todos.map((t) => ` ${t}`));
2242
+ const n = r.choices.network;
2243
+ L.push("");
2244
+ if (n.enabled) L.push("Network tools: on (net.*).");
2245
+ else {
2246
+ const how = [n.flag && `rerun with ${n.flag}`, n.lines && "add these lines"].filter(Boolean).join(", or ");
2247
+ L.push(`Network tools: off. ${n.open ? "Open choice: " : ""}${n.why}. To wire them, ${how}:`);
2248
+ L.push(...(n.lines ?? []).map((l) => ` ${l}`));
2249
+ }
2250
+ if (n.note) L.push(n.note);
2251
+ L.push(
2252
+ "",
2253
+ r.mount.mounted ? `Mounted: yes (from ${r.mount.file}).` : `Mounted: no. ${r.mount.note}`
2254
+ );
2255
+ if (!r.mount.mounted && !r.existing.length)
2256
+ L.push(...r.mount.snippet.split("\n").map((l) => ` ${l}`));
2257
+ L.push("", `First run${r.runFrom === "." ? "" : ` (from ${r.runFrom})`}:`);
2258
+ r.next.forEach(
2259
+ (s, i) => L.push(` ${i + 1}. ${s.step}${s.command ? `
2260
+ ${s.command}` : ""}`)
2261
+ );
2262
+ L.push("", "Prompt for your agent:", ` ${r.prompt}`);
2263
+ return L.join("\n");
2264
+ }
2265
+ async function runInit(args, io) {
2266
+ const dir = resolve(io.cwd, args.dir ?? ".");
2267
+ const fail = (error) => {
2268
+ if (args.json) io.print(JSON.stringify({ ok: false, ...error }, null, 2));
2269
+ else io.warn(error.error);
2270
+ return 1;
2271
+ };
2272
+ let d = detect(dir, io.fs, { userAgent: io.userAgent });
2273
+ if ("error" in d) return fail(d);
2274
+ const interactive = io.interactive && !args.json && !args.yes;
2275
+ const options = (network2) => ({
2276
+ version,
2277
+ out: args.out === void 0 ? void 0 : resolve(io.cwd, args.out),
2278
+ force: args.force,
2279
+ install: args.install !== false,
2280
+ skill: args.skill !== false,
2281
+ network: network2,
2282
+ skillYes: !interactive
2283
+ });
2284
+ const planFor = (det, network2, install2 = true) => plan(det, scan(dir, io.fs), io.fs, { ...options(network2), install: install2 && args.install !== false }, io.cwd);
2285
+ let network = { enabled: args.network ?? false, chosen: args.network !== void 0 };
2286
+ const first = planFor(d, network);
2287
+ if ("error" in first) return fail(first);
2288
+ if (args.dryRun) {
2289
+ const { actions: actions2, report: report2 } = first;
2290
+ report2.dryRun = true;
2291
+ if (args.json) {
2292
+ const contents = Object.fromEntries(
2293
+ actions2.flatMap((a) => a.kind === "write" ? [[relative4(dir, a.path), a.content]] : [])
2294
+ );
2295
+ io.print(JSON.stringify({ ...report2, contents }, null, 2));
2296
+ } else io.print(formatReport(report2, actions2));
2297
+ return 0;
2298
+ }
2299
+ const bridge = posix2(relative4(dir, bridgePath(d, options(network).out)));
2300
+ const writesBridge = first.report.files.some((f) => f.path === bridge && f.status === "written");
2301
+ if (!network.chosen && interactive && writesBridge) {
2302
+ const answer = await io.prompt(`${NETWORK_PROMPT} [y/N] `);
2303
+ network = { enabled: /^y(es)?$/i.test(answer.trim()), chosen: true };
2304
+ }
2305
+ const install = first.actions.find((a) => a.kind === "install");
2306
+ const installReport = first.report.install;
2307
+ if (install) {
2308
+ await apply([install], first.report, io, interactive);
2309
+ const after = detect(dir, io.fs, { userAgent: io.userAgent });
2310
+ if (!("error" in after)) d = after;
2311
+ }
2312
+ const p = planFor(d, network, false);
2313
+ if ("error" in p) return fail(p);
2314
+ const { actions, report } = p;
2315
+ if (install) {
2316
+ report.install = installReport;
2317
+ report.ok = first.report.ok;
2318
+ }
2319
+ await apply(actions, report, io, interactive);
2320
+ io.print(args.json ? JSON.stringify(report, null, 2) : formatReport(report, actions));
2321
+ return report.ok ? 0 : 1;
2322
+ }
2323
+
1175
2324
  // src/cli.ts
1176
2325
  var HELP2 = `agent-bridge: drive a running React Native app from an agent
1177
2326
 
1178
2327
  Usage
2328
+ agent-bridge init Set up this app: install, write the dev-only bridge
2329
+ file and a smoke flow, print the line to mount it
1179
2330
  agent-bridge devices Apps connected to Metro
1180
2331
  agent-bridge tools Tools the app exposes
1181
2332
  agent-bridge call <tool> [args] Call a tool. args: a JSON array, or one JSON value
@@ -1186,7 +2337,7 @@ Usage
1186
2337
  agent-bridge call --batch Read \`tool args\` lines from stdin (args may be @file); one JSON line per call, one connection
1187
2338
  agent-bridge repl Interactive prompt (history, tab completion, .help). With
1188
2339
  stdin not a terminal it behaves like call --batch
1189
- agent-bridge run <flow.mjs|.ts> Run a flow: export default async ({ step, call }) => {}
2340
+ agent-bridge run <flow.mjs> Run a flow: export default async ({ step, call }) => {}
1190
2341
  export const scenario = 'signedIn' applies it first
1191
2342
  and runs bridge.restore after, even on failure
1192
2343
  agent-bridge scenarios Scenarios the app defines (scenario.apply applies one)
@@ -1214,6 +2365,17 @@ Options
1214
2365
  JSON after "=": --scenario 'signedIn={"user":{"name":"Ada"}}'
1215
2366
  --session <name> Use this session (env AGENT_BRIDGE_SESSION)
1216
2367
  --no-session Connect directly even if a session is running
2368
+
2369
+ init options
2370
+ --dir <path> The app (default: the current directory)
2371
+ --out <file> Bridge file path, inside the app (default src/dev/agent-bridge.tsx, or dev/)
2372
+ --dry-run Print what it would do and the files; change nothing
2373
+ --force Regenerate files init wrote that are still unedited (edited ones get a .new copy)
2374
+ --no-install Print the install command instead of running it
2375
+ --no-skill Don't run \`npx skills add avas-app/agent-bridge\`
2376
+ --network, --no-network Wire networkTools into a new bridge file (asked in a terminal; off otherwise)
2377
+ --json Machine-readable report on stdout; never prompts
2378
+ --yes, -y Never prompt
1217
2379
  `;
1218
2380
  var failedLogs = (error) => error instanceof AgentBridgeCallError ? error.logs : [];
1219
2381
  async function readStdin() {
@@ -1246,6 +2408,14 @@ async function main() {
1246
2408
  "dry-run": { type: "boolean" },
1247
2409
  session: { type: "string" },
1248
2410
  "no-session": { type: "boolean" },
2411
+ dir: { type: "string" },
2412
+ force: { type: "boolean" },
2413
+ "no-install": { type: "boolean" },
2414
+ "no-skill": { type: "boolean" },
2415
+ network: { type: "boolean" },
2416
+ "no-network": { type: "boolean" },
2417
+ json: { type: "boolean" },
2418
+ yes: { type: "boolean", short: "y" },
1249
2419
  help: { type: "boolean", short: "h" }
1250
2420
  }
1251
2421
  });
@@ -1262,7 +2432,10 @@ async function main() {
1262
2432
  };
1263
2433
  const withBridge = async (fn) => {
1264
2434
  const session = sessionFor(values);
1265
- if (session) console.error(`Using session "${session.name}"`);
2435
+ if (session)
2436
+ console.error(
2437
+ `Using session "${session.name}" (${session.device.name} on ${session.metro})`
2438
+ );
1266
2439
  const bridge = session ? await connectSession({
1267
2440
  name: session.name,
1268
2441
  timeoutMs: options.timeoutMs
@@ -1282,9 +2455,30 @@ async function main() {
1282
2455
  if (failed) process.exitCode = 1;
1283
2456
  };
1284
2457
  switch (command) {
2458
+ case "init":
2459
+ process.exitCode = await runInit(
2460
+ {
2461
+ dir: values.dir,
2462
+ out: values.out,
2463
+ dryRun: values["dry-run"],
2464
+ force: values.force,
2465
+ install: !values["no-install"],
2466
+ skill: !values["no-skill"],
2467
+ network: values.network ? true : values["no-network"] ? false : void 0,
2468
+ json: values.json,
2469
+ yes: values.yes
2470
+ },
2471
+ nodeIo()
2472
+ );
2473
+ return;
1285
2474
  case "devices": {
1286
2475
  const devices = await listDevices(options);
1287
- if (!devices.length) console.log("No apps connected.");
2476
+ if (!devices.length) {
2477
+ const metro = metroHost(options.metro);
2478
+ await assertListening(metro);
2479
+ console.log("No apps connected.");
2480
+ console.error(await noAppHint(metro));
2481
+ }
1288
2482
  for (const d of devices) {
1289
2483
  const extra = d.tools !== void 0 ? ` ${d.tools} tools ${d.deviceId}` : "";
1290
2484
  console.log(`${d.transport.padEnd(5)} ${d.name}${extra}`);
@@ -1345,7 +2539,7 @@ async function main() {
1345
2539
  case "run": {
1346
2540
  const [file] = rest;
1347
2541
  if (!file) throw new Error("Usage: agent-bridge run <flow file>");
1348
- const flow = await import(pathToFileURL(resolve(file)).href);
2542
+ const flow = await import(pathToFileURL(resolve2(file)).href);
1349
2543
  flowScenarios(flow);
1350
2544
  const extra = (values.scenario ?? []).map(parseScenarioFlag);
1351
2545
  return withBridge(async (bridge) => {