@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/README.md +19 -11
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +12 -3
- package/dist/{chunk-AJD3XTNT.js → chunk-3QIG7A5W.js} +292 -161
- package/dist/cli.js +1216 -22
- package/dist/client/index.cjs +255 -130
- package/dist/client/index.js +1 -1
- package/dist/runtime/index.cjs +102 -32
- package/dist/runtime/index.d.cts +2 -0
- package/package.json +3 -2
- package/skills/agent-bridge/SKILL.md +67 -252
- package/skills/agent-bridge/references/flows.md +66 -0
- package/skills/agent-bridge/references/large-data.md +38 -0
- package/skills/agent-bridge/references/network.md +34 -0
- package/skills/agent-bridge/references/realtime.md +20 -0
- package/skills/agent-bridge/references/restore.md +52 -0
- package/skills/agent-bridge/references/screen.md +38 -0
- package/skills/agent-bridge/references/state.md +48 -0
- package/skills/agent-bridge/references/webview.md +42 -0
- package/skills/agent-bridge/references/writing-scenarios.md +97 -0
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-
|
|
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((
|
|
597
|
+
var pause = (ms, signal) => new Promise((resolve3) => {
|
|
595
598
|
const done = () => {
|
|
596
599
|
clearTimeout(timer);
|
|
597
600
|
signal.removeEventListener("abort", done);
|
|
598
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
`${
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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(
|
|
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) => {
|