@avasapp/agent-bridge 0.4.2 → 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-TA2OLPPT.js → chunk-3QIG7A5W.js} +247 -142
- package/dist/cli.js +1198 -12
- package/dist/client/index.cjs +210 -111
- package/dist/client/index.js +1 -1
- package/dist/runtime/index.cjs +69 -16
- package/dist/runtime/index.d.cts +2 -0
- package/package.json +3 -2
- package/skills/agent-bridge/SKILL.md +32 -15
- package/skills/agent-bridge/references/flows.md +6 -5
- package/skills/agent-bridge/references/large-data.md +6 -6
- package/skills/agent-bridge/references/network.md +7 -7
- package/skills/agent-bridge/references/realtime.md +5 -5
- package/skills/agent-bridge/references/restore.md +4 -4
- package/skills/agent-bridge/references/screen.md +18 -10
- package/skills/agent-bridge/references/state.md +11 -11
- package/skills/agent-bridge/references/webview.md +9 -9
- package/skills/agent-bridge/references/writing-scenarios.md +3 -3
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 });
|
|
@@ -636,7 +639,9 @@ async function runSessionDaemon(options) {
|
|
|
636
639
|
`${c.device.name} already belongs to session "${owner.name}". Use --session ${owner.name}, or stop it first.`
|
|
637
640
|
);
|
|
638
641
|
};
|
|
639
|
-
let conn = await reach()
|
|
642
|
+
let conn = await reach().catch(async (error) => {
|
|
643
|
+
throw await explainConnectError(error, metro, options.device);
|
|
644
|
+
});
|
|
640
645
|
claim(conn);
|
|
641
646
|
const pin = conn.pin;
|
|
642
647
|
const deviceOf = (c) => ({
|
|
@@ -953,11 +958,11 @@ async function runSessionDaemon(options) {
|
|
|
953
958
|
});
|
|
954
959
|
if (process.platform !== "win32") rmSync(files.socket, { force: true });
|
|
955
960
|
try {
|
|
956
|
-
await new Promise((
|
|
961
|
+
await new Promise((resolve3, reject) => {
|
|
957
962
|
server.once("error", reject);
|
|
958
963
|
server.listen(files.socket, () => {
|
|
959
964
|
server.off("error", reject);
|
|
960
|
-
|
|
965
|
+
resolve3();
|
|
961
966
|
});
|
|
962
967
|
});
|
|
963
968
|
} catch (error) {
|
|
@@ -1044,7 +1049,7 @@ async function start(flags) {
|
|
|
1044
1049
|
[...process.execArgv, process.argv[1], ...args],
|
|
1045
1050
|
{ detached: true, stdio: "ignore", windowsHide: true }
|
|
1046
1051
|
);
|
|
1047
|
-
const state = await new Promise((
|
|
1052
|
+
const state = await new Promise((resolve3, reject) => {
|
|
1048
1053
|
const deadline = Date.now() + 3e4;
|
|
1049
1054
|
let exited = false;
|
|
1050
1055
|
child.once("exit", () => {
|
|
@@ -1055,7 +1060,7 @@ async function start(flags) {
|
|
|
1055
1060
|
const failed = existsSync(files.error);
|
|
1056
1061
|
if (ready && ready.pid === child.pid) {
|
|
1057
1062
|
clearInterval(tick);
|
|
1058
|
-
|
|
1063
|
+
resolve3(ready);
|
|
1059
1064
|
} else if (failed || exited || Date.now() > deadline) {
|
|
1060
1065
|
clearInterval(tick);
|
|
1061
1066
|
if (!exited) child.kill();
|
|
@@ -1177,10 +1182,1151 @@ async function daemonMain(flags) {
|
|
|
1177
1182
|
process.exit(0);
|
|
1178
1183
|
}
|
|
1179
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
|
+
|
|
1180
2324
|
// src/cli.ts
|
|
1181
2325
|
var HELP2 = `agent-bridge: drive a running React Native app from an agent
|
|
1182
2326
|
|
|
1183
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
|
|
1184
2330
|
agent-bridge devices Apps connected to Metro
|
|
1185
2331
|
agent-bridge tools Tools the app exposes
|
|
1186
2332
|
agent-bridge call <tool> [args] Call a tool. args: a JSON array, or one JSON value
|
|
@@ -1191,7 +2337,7 @@ Usage
|
|
|
1191
2337
|
agent-bridge call --batch Read \`tool args\` lines from stdin (args may be @file); one JSON line per call, one connection
|
|
1192
2338
|
agent-bridge repl Interactive prompt (history, tab completion, .help). With
|
|
1193
2339
|
stdin not a terminal it behaves like call --batch
|
|
1194
|
-
agent-bridge run <flow.mjs
|
|
2340
|
+
agent-bridge run <flow.mjs> Run a flow: export default async ({ step, call }) => {}
|
|
1195
2341
|
export const scenario = 'signedIn' applies it first
|
|
1196
2342
|
and runs bridge.restore after, even on failure
|
|
1197
2343
|
agent-bridge scenarios Scenarios the app defines (scenario.apply applies one)
|
|
@@ -1219,6 +2365,17 @@ Options
|
|
|
1219
2365
|
JSON after "=": --scenario 'signedIn={"user":{"name":"Ada"}}'
|
|
1220
2366
|
--session <name> Use this session (env AGENT_BRIDGE_SESSION)
|
|
1221
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
|
|
1222
2379
|
`;
|
|
1223
2380
|
var failedLogs = (error) => error instanceof AgentBridgeCallError ? error.logs : [];
|
|
1224
2381
|
async function readStdin() {
|
|
@@ -1251,6 +2408,14 @@ async function main() {
|
|
|
1251
2408
|
"dry-run": { type: "boolean" },
|
|
1252
2409
|
session: { type: "string" },
|
|
1253
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" },
|
|
1254
2419
|
help: { type: "boolean", short: "h" }
|
|
1255
2420
|
}
|
|
1256
2421
|
});
|
|
@@ -1290,9 +2455,30 @@ async function main() {
|
|
|
1290
2455
|
if (failed) process.exitCode = 1;
|
|
1291
2456
|
};
|
|
1292
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;
|
|
1293
2474
|
case "devices": {
|
|
1294
2475
|
const devices = await listDevices(options);
|
|
1295
|
-
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
|
+
}
|
|
1296
2482
|
for (const d of devices) {
|
|
1297
2483
|
const extra = d.tools !== void 0 ? ` ${d.tools} tools ${d.deviceId}` : "";
|
|
1298
2484
|
console.log(`${d.transport.padEnd(5)} ${d.name}${extra}`);
|
|
@@ -1353,7 +2539,7 @@ async function main() {
|
|
|
1353
2539
|
case "run": {
|
|
1354
2540
|
const [file] = rest;
|
|
1355
2541
|
if (!file) throw new Error("Usage: agent-bridge run <flow file>");
|
|
1356
|
-
const flow = await import(pathToFileURL(
|
|
2542
|
+
const flow = await import(pathToFileURL(resolve2(file)).href);
|
|
1357
2543
|
flowScenarios(flow);
|
|
1358
2544
|
const extra = (values.scenario ?? []).map(parseScenarioFlag);
|
|
1359
2545
|
return withBridge(async (bridge) => {
|