@avasapp/agent-bridge 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
- package/dist/cli.js +1336 -255
- package/dist/client/index.cjs +233 -27
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +785 -128
- package/dist/runtime/index.d.cts +2 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +32 -1
- package/skills/agent-bridge/SKILL.md +39 -8
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +13 -3
- package/skills/agent-bridge/references/network.md +19 -1
- package/skills/agent-bridge/references/realtime.md +1 -1
- package/skills/agent-bridge/references/restore.md +21 -3
- package/skills/agent-bridge/references/screen.md +8 -2
- package/skills/agent-bridge/references/state.md +21 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
AgentBridgeCallError,
|
|
4
4
|
RUNTIME_MARKER,
|
|
5
|
+
SessionLinkError,
|
|
5
6
|
assertListening,
|
|
7
|
+
callTimeout,
|
|
6
8
|
checkName,
|
|
7
9
|
connect,
|
|
8
10
|
connectSession,
|
|
@@ -24,27 +26,201 @@ import {
|
|
|
24
26
|
projectRoot,
|
|
25
27
|
readSession,
|
|
26
28
|
removeSessionFiles,
|
|
27
|
-
|
|
29
|
+
runFlows,
|
|
28
30
|
sessionFiles,
|
|
29
31
|
sessionRequest,
|
|
30
32
|
stateDir,
|
|
31
33
|
writePrivate
|
|
32
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-Y6QFU5ON.js";
|
|
33
35
|
|
|
34
36
|
// src/cli.ts
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
37
|
+
import { createRequire } from "module";
|
|
38
|
+
import { mkdir as mkdir3, readFile } from "fs/promises";
|
|
39
|
+
import { dirname as dirname7 } from "path";
|
|
38
40
|
import { parseArgs } from "util";
|
|
39
41
|
|
|
42
|
+
// src/client/flow-cli.ts
|
|
43
|
+
import { mkdir, realpath as realpath2, rename, writeFile } from "fs/promises";
|
|
44
|
+
import { dirname, join as join2, relative as relative2 } from "path";
|
|
45
|
+
import { pathToFileURL } from "url";
|
|
46
|
+
|
|
47
|
+
// src/client/flow-files.ts
|
|
48
|
+
import { readdir, realpath, stat } from "fs/promises";
|
|
49
|
+
import { basename, join, relative, resolve } from "path";
|
|
50
|
+
var FLOW_SUFFIX = /\.flow\.mjs$/;
|
|
51
|
+
function globRegExp(glob) {
|
|
52
|
+
let re = "";
|
|
53
|
+
for (let i = 0; i < glob.length; i++) {
|
|
54
|
+
const c = glob[i];
|
|
55
|
+
if (c === "*" && glob[i + 1] === "*") {
|
|
56
|
+
i++;
|
|
57
|
+
if (glob[i + 1] === "/") {
|
|
58
|
+
i++;
|
|
59
|
+
re += "(?:.*/)?";
|
|
60
|
+
} else re += ".*";
|
|
61
|
+
} else if (c === "*") re += "[^/]*";
|
|
62
|
+
else if (c === "?") re += "[^/]";
|
|
63
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
64
|
+
}
|
|
65
|
+
return new RegExp(`^${re}$`);
|
|
66
|
+
}
|
|
67
|
+
var collator = new Intl.Collator("en", { numeric: true });
|
|
68
|
+
var posix = (p) => p.split("\\").join("/");
|
|
69
|
+
var byPath = (a, b) => collator.compare(posix(a), posix(b));
|
|
70
|
+
async function walk(dir, seen = /* @__PURE__ */ new Set()) {
|
|
71
|
+
const real = await realpath(dir);
|
|
72
|
+
if (seen.has(real)) return [];
|
|
73
|
+
seen.add(real);
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
76
|
+
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
77
|
+
const full = join(dir, e.name);
|
|
78
|
+
const isDir = e.isDirectory() || e.isSymbolicLink() && (await stat(full).catch(() => null))?.isDirectory();
|
|
79
|
+
if (isDir) out.push(...await walk(full, seen));
|
|
80
|
+
else out.push(full);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
async function resolveFlowFiles(args, cwd = process.cwd()) {
|
|
85
|
+
const files = [];
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
for (const given of args) {
|
|
88
|
+
const arg = process.platform === "win32" ? given.split("\\").join("/") : given;
|
|
89
|
+
const path = resolve(cwd, arg);
|
|
90
|
+
const info = await stat(path).catch(() => null);
|
|
91
|
+
let found;
|
|
92
|
+
if (info?.isDirectory())
|
|
93
|
+
found = (await walk(path)).filter((f) => FLOW_SUFFIX.test(f)).sort(byPath);
|
|
94
|
+
else if (info) found = [path];
|
|
95
|
+
else if (/[*?]/.test(arg)) {
|
|
96
|
+
const fixed2 = arg.split("/").findIndex((seg) => /[*?]/.test(seg));
|
|
97
|
+
const base = resolve(cwd, arg.split("/").slice(0, fixed2).join("/") || ".");
|
|
98
|
+
const re = globRegExp(posix(relative(cwd, resolve(cwd, arg))));
|
|
99
|
+
found = (await walk(base).catch(() => [])).filter((f) => FLOW_SUFFIX.test(f) && re.test(posix(relative(cwd, f)))).sort(byPath);
|
|
100
|
+
} else throw new Error(`No such flow file or directory: ${given}`);
|
|
101
|
+
if (!found.length) throw new Error(`No flow files match ${given} (a directory or glob only finds *.flow.mjs)`);
|
|
102
|
+
for (const f of found) {
|
|
103
|
+
const real = await realpath(f);
|
|
104
|
+
if (!seen.has(real)) {
|
|
105
|
+
seen.add(real);
|
|
106
|
+
files.push(real);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return files;
|
|
111
|
+
}
|
|
112
|
+
var flowName = (file, exported) => exported ?? basename(file).replace(FLOW_SUFFIX, "").replace(/\.[mc]?[jt]s$/, "");
|
|
113
|
+
|
|
114
|
+
// src/client/flow-cli.ts
|
|
115
|
+
var REPORTERS = ["text", "json", "junit"];
|
|
116
|
+
function checkRunFlags(flags) {
|
|
117
|
+
const reporter = flags.reporter ?? "text";
|
|
118
|
+
if (!REPORTERS.includes(reporter))
|
|
119
|
+
throw new Error(`--reporter is text, json or junit, not ${reporter}`);
|
|
120
|
+
if (flags.out && reporter === "text")
|
|
121
|
+
throw new Error("--out needs --reporter json or junit (run: --out is the report file)");
|
|
122
|
+
return reporter !== "text";
|
|
123
|
+
}
|
|
124
|
+
var failedRun = (file, name, error) => ({
|
|
125
|
+
file,
|
|
126
|
+
name,
|
|
127
|
+
status: "failed",
|
|
128
|
+
ms: 0,
|
|
129
|
+
steps: 0,
|
|
130
|
+
errors: 0,
|
|
131
|
+
error,
|
|
132
|
+
restoreErrors: [],
|
|
133
|
+
notices: []
|
|
134
|
+
});
|
|
135
|
+
var reason = (error) => error instanceof Error ? error.message : String(error);
|
|
136
|
+
async function loadFlows(files, cwd = process.cwd()) {
|
|
137
|
+
const root = await realpath2(cwd).catch(() => cwd);
|
|
138
|
+
const entries = [];
|
|
139
|
+
const failures = [];
|
|
140
|
+
for (const abs of files) {
|
|
141
|
+
const file = relative2(root, abs).split("\\").join("/");
|
|
142
|
+
try {
|
|
143
|
+
const module = await import(pathToFileURL(abs).href);
|
|
144
|
+
if (typeof module.default !== "function")
|
|
145
|
+
throw new Error(`${file} has no \`export default async ({ step }) => {}\``);
|
|
146
|
+
if (module.name !== void 0 && typeof module.name !== "string")
|
|
147
|
+
throw new Error(`${file}: \`export const name\` must be a string, not ${typeof module.name}`);
|
|
148
|
+
flowScenarios(module);
|
|
149
|
+
entries.push({ file, name: flowName(abs, module.name), module });
|
|
150
|
+
} catch (error) {
|
|
151
|
+
failures.push(failedRun(file, flowName(abs), reason(error)));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { entries, failures };
|
|
155
|
+
}
|
|
156
|
+
function selectFlows(entries, grep) {
|
|
157
|
+
if (!grep) return entries;
|
|
158
|
+
const needle = grep.toLowerCase();
|
|
159
|
+
return entries.filter((e) => `${e.name}
|
|
160
|
+
${e.file}`.toLowerCase().includes(needle));
|
|
161
|
+
}
|
|
162
|
+
async function writeReport(out, doc) {
|
|
163
|
+
await mkdir(dirname(out), { recursive: true });
|
|
164
|
+
const tmp = join2(dirname(out), `.${process.pid}-${Date.now()}.tmp`);
|
|
165
|
+
await writeFile(tmp, doc.endsWith("\n") ? doc : `${doc}
|
|
166
|
+
`);
|
|
167
|
+
await rename(tmp, out);
|
|
168
|
+
}
|
|
169
|
+
async function logToStderr(fn) {
|
|
170
|
+
const log = console.log;
|
|
171
|
+
console.log = (...args) => console.error(...args);
|
|
172
|
+
try {
|
|
173
|
+
return await fn();
|
|
174
|
+
} finally {
|
|
175
|
+
console.log = log;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/client/flow-report.ts
|
|
180
|
+
var ANSI = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
|
|
181
|
+
var escape = (s) => s.replace(ANSI, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff]/g, "").replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
182
|
+
var classname = (file) => {
|
|
183
|
+
const at = file.lastIndexOf("/");
|
|
184
|
+
return at > 0 ? file.slice(0, at) : "agent-bridge";
|
|
185
|
+
};
|
|
186
|
+
function renderJson(runs) {
|
|
187
|
+
const count = (s) => runs.filter((r) => r.status === s).length;
|
|
188
|
+
return JSON.stringify(
|
|
189
|
+
{ passed: count("passed"), failed: count("failed"), skipped: count("skipped"), flows: runs },
|
|
190
|
+
null,
|
|
191
|
+
2
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
function renderJunit(runs) {
|
|
195
|
+
const failed = runs.filter((r) => r.status === "failed").length;
|
|
196
|
+
const skipped = runs.filter((r) => r.status === "skipped").length;
|
|
197
|
+
const time = runs.reduce((t, r) => t + r.ms, 0) / 1e3;
|
|
198
|
+
const cases = runs.map((r) => {
|
|
199
|
+
const open = `<testcase classname="${escape(classname(r.file))}" name="${escape(r.name)}" file="${escape(r.file)}" time="${(r.ms / 1e3).toFixed(3)}"`;
|
|
200
|
+
if (r.status === "skipped") return `${open}><skipped/></testcase>`;
|
|
201
|
+
if (r.status === "passed") return `${open}/>`;
|
|
202
|
+
const screen = r.state?.screen?.length ? `
|
|
203
|
+
on screen:
|
|
204
|
+
${r.state.screen.join("\n")}` : "";
|
|
205
|
+
const route = r.state?.route !== void 0 ? `
|
|
206
|
+
route: ${JSON.stringify(r.state.route)}` : "";
|
|
207
|
+
return `${open}><failure message="${escape(r.error ?? "failed")}">${escape((r.error ?? "") + route + screen)}</failure></testcase>`;
|
|
208
|
+
});
|
|
209
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
210
|
+
<testsuite name="agent-bridge" tests="${runs.length}" failures="${failed}" skipped="${skipped}" time="${time.toFixed(3)}">
|
|
211
|
+
${cases.join("\n")}
|
|
212
|
+
</testsuite>
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
|
|
40
216
|
// src/client/batch.ts
|
|
41
217
|
import { readFileSync } from "fs";
|
|
42
|
-
import { mkdir, readdir, stat } from "fs/promises";
|
|
43
|
-
import { join } from "path";
|
|
218
|
+
import { mkdir as mkdir2, readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
219
|
+
import { join as join3 } from "path";
|
|
44
220
|
import { createInterface } from "readline";
|
|
45
221
|
|
|
46
222
|
// src/client/output.ts
|
|
47
|
-
import { writeFile } from "fs/promises";
|
|
223
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
48
224
|
var OUTPUT_LIMIT_BYTES = 32 * 1024;
|
|
49
225
|
var kb = (bytes) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
50
226
|
function shapeOf(value) {
|
|
@@ -76,7 +252,7 @@ async function shapedResult(value, options = {}) {
|
|
|
76
252
|
const json = JSON.stringify(value, null, 2) ?? "null";
|
|
77
253
|
const bytes = Buffer.byteLength(json);
|
|
78
254
|
if (options.out) {
|
|
79
|
-
await
|
|
255
|
+
await writeFile2(options.out, `${json}
|
|
80
256
|
`);
|
|
81
257
|
return fileSummary(options.out, bytes, value);
|
|
82
258
|
}
|
|
@@ -88,7 +264,7 @@ async function renderResult(value, options = {}) {
|
|
|
88
264
|
const json = JSON.stringify(value, null, 2) ?? "null";
|
|
89
265
|
const bytes = Buffer.byteLength(json);
|
|
90
266
|
if (options.out) {
|
|
91
|
-
await
|
|
267
|
+
await writeFile2(options.out, `${json}
|
|
92
268
|
`);
|
|
93
269
|
return JSON.stringify(fileSummary(options.out, bytes, value), null, 2);
|
|
94
270
|
}
|
|
@@ -96,6 +272,16 @@ async function renderResult(value, options = {}) {
|
|
|
96
272
|
return json;
|
|
97
273
|
return JSON.stringify(tooLarge(bytes, value), null, 2);
|
|
98
274
|
}
|
|
275
|
+
async function renderText(text2, options = {}) {
|
|
276
|
+
const bytes = Buffer.byteLength(text2);
|
|
277
|
+
if (options.out) {
|
|
278
|
+
await writeFile2(options.out, `${text2}
|
|
279
|
+
`);
|
|
280
|
+
return JSON.stringify(fileSummary(options.out, bytes, text2), null, 2);
|
|
281
|
+
}
|
|
282
|
+
if (options.full || bytes <= (options.limit ?? OUTPUT_LIMIT_BYTES)) return text2;
|
|
283
|
+
return JSON.stringify(tooLarge(bytes, text2), null, 2);
|
|
284
|
+
}
|
|
99
285
|
|
|
100
286
|
// src/client/batch.ts
|
|
101
287
|
function parseCallArgs(raw) {
|
|
@@ -123,9 +309,9 @@ function fileJson(ref, read) {
|
|
|
123
309
|
throw new Error(
|
|
124
310
|
`An argument that is just "@" needs a path (@file, or @- for stdin). To pass the text itself, quote it as JSON: '"@"'`
|
|
125
311
|
);
|
|
126
|
-
const
|
|
312
|
+
const text2 = read(path);
|
|
127
313
|
try {
|
|
128
|
-
return JSON.parse(
|
|
314
|
+
return JSON.parse(text2);
|
|
129
315
|
} catch (error) {
|
|
130
316
|
throw new Error(
|
|
131
317
|
`${path === "-" ? "stdin" : path} is not valid JSON (${error instanceof Error ? error.message : error})`
|
|
@@ -149,19 +335,19 @@ function parseCallArgv(argv, read = readArgFile) {
|
|
|
149
335
|
});
|
|
150
336
|
}
|
|
151
337
|
var BatchParseError = class extends Error {
|
|
152
|
-
constructor(tool,
|
|
153
|
-
super(
|
|
338
|
+
constructor(tool, message3) {
|
|
339
|
+
super(message3);
|
|
154
340
|
this.tool = tool;
|
|
155
341
|
}
|
|
156
342
|
tool;
|
|
157
343
|
};
|
|
158
344
|
function parseBatchLine(line) {
|
|
159
|
-
const
|
|
160
|
-
if (!
|
|
161
|
-
const at =
|
|
162
|
-
if (at < 0) return { tool:
|
|
163
|
-
const tool =
|
|
164
|
-
const raw =
|
|
345
|
+
const text2 = line.trim();
|
|
346
|
+
if (!text2 || text2.startsWith("#")) return null;
|
|
347
|
+
const at = text2.search(/\s/);
|
|
348
|
+
if (at < 0) return { tool: text2, args: [] };
|
|
349
|
+
const tool = text2.slice(0, at);
|
|
350
|
+
const raw = text2.slice(at).trim();
|
|
165
351
|
if (/^[[{"]/.test(raw)) {
|
|
166
352
|
try {
|
|
167
353
|
JSON.parse(raw);
|
|
@@ -292,11 +478,11 @@ async function dotLine(bridge, line) {
|
|
|
292
478
|
}
|
|
293
479
|
}
|
|
294
480
|
async function checkOutDir(dir) {
|
|
295
|
-
const info = await
|
|
296
|
-
if (!info) return
|
|
481
|
+
const info = await stat2(dir).catch(() => null);
|
|
482
|
+
if (!info) return mkdir2(dir, { recursive: true });
|
|
297
483
|
if (!info.isDirectory())
|
|
298
484
|
throw new Error(`--out ${dir} is a file; with --batch it must be a directory`);
|
|
299
|
-
if ((await
|
|
485
|
+
if ((await readdir2(dir)).length)
|
|
300
486
|
throw new Error(
|
|
301
487
|
`--out ${dir} is not empty; use a new or empty directory so old files aren't mistaken for this run's`
|
|
302
488
|
);
|
|
@@ -309,9 +495,9 @@ async function runBatch(bridge, input, options = {}) {
|
|
|
309
495
|
let stopped = false;
|
|
310
496
|
try {
|
|
311
497
|
for await (const line of rl) {
|
|
312
|
-
const
|
|
313
|
-
const outcome =
|
|
314
|
-
if (
|
|
498
|
+
const text2 = line.trim();
|
|
499
|
+
const outcome = text2.startsWith(".") ? await dotLine(bridge, text2) : await runLine(bridge, line);
|
|
500
|
+
if (text2.startsWith(".") && outcome === null) break;
|
|
315
501
|
if (!outcome) continue;
|
|
316
502
|
result.calls++;
|
|
317
503
|
const raw = outcome;
|
|
@@ -326,7 +512,7 @@ async function runBatch(bridge, input, options = {}) {
|
|
|
326
512
|
record.value = await shapedResult(raw.value, {
|
|
327
513
|
full: options.full,
|
|
328
514
|
outKind: "dir",
|
|
329
|
-
out: options.outDir ?
|
|
515
|
+
out: options.outDir ? join3(
|
|
330
516
|
options.outDir,
|
|
331
517
|
`${result.calls}-${String(raw.tool).replace(/[^\w.-]/g, "_")}.json`
|
|
332
518
|
) : void 0
|
|
@@ -355,13 +541,27 @@ async function runBatch(bridge, input, options = {}) {
|
|
|
355
541
|
return result;
|
|
356
542
|
}
|
|
357
543
|
|
|
544
|
+
// src/client/snapshot-format.ts
|
|
545
|
+
function withCompactSnapshot(tool, args, json = false) {
|
|
546
|
+
if (tool !== "screen.snapshot" || args.length > 1) return args;
|
|
547
|
+
const options = args[0];
|
|
548
|
+
if (options !== void 0 && (options === null || typeof options !== "object" || Array.isArray(options)))
|
|
549
|
+
return args;
|
|
550
|
+
const given = options ?? {};
|
|
551
|
+
if (given.format === "text" && json)
|
|
552
|
+
throw new Error('--json and {"format":"text"} ask for different formats; drop one');
|
|
553
|
+
if (given.format !== void 0) return args;
|
|
554
|
+
return [{ ...given, format: json ? "json" : "text" }];
|
|
555
|
+
}
|
|
556
|
+
var isSnapshotText = (tool, value) => tool === "screen.snapshot" && typeof value === "string";
|
|
557
|
+
|
|
358
558
|
// src/client/repl.ts
|
|
359
559
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
360
560
|
import { homedir } from "os";
|
|
361
|
-
import { dirname, join as
|
|
561
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
362
562
|
import { createInterface as createInterface2 } from "readline";
|
|
363
563
|
var HISTORY_LIMIT = 500;
|
|
364
|
-
var DEFAULT_HISTORY_FILE =
|
|
564
|
+
var DEFAULT_HISTORY_FILE = join4(
|
|
365
565
|
homedir(),
|
|
366
566
|
".agent-bridge",
|
|
367
567
|
"repl_history"
|
|
@@ -413,7 +613,7 @@ function loadHistory(file) {
|
|
|
413
613
|
}
|
|
414
614
|
function saveHistory(file, lines) {
|
|
415
615
|
try {
|
|
416
|
-
mkdirSync(
|
|
616
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
417
617
|
writeFileSync(file, `${lines.slice(-HISTORY_LIMIT).join("\n")}
|
|
418
618
|
`, {
|
|
419
619
|
mode: 384
|
|
@@ -423,7 +623,7 @@ function saveHistory(file, lines) {
|
|
|
423
623
|
}
|
|
424
624
|
function appendHistory(file, line) {
|
|
425
625
|
try {
|
|
426
|
-
mkdirSync(
|
|
626
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
427
627
|
appendFileSync(file, `${line}
|
|
428
628
|
`, { mode: 384 });
|
|
429
629
|
} catch {
|
|
@@ -435,9 +635,9 @@ async function runRepl(bridge, options) {
|
|
|
435
635
|
const { output } = options;
|
|
436
636
|
const tty = !!options.tty;
|
|
437
637
|
const { historyFile } = options;
|
|
438
|
-
const paint = (code,
|
|
439
|
-
const print = (
|
|
440
|
-
output.write(`${
|
|
638
|
+
const paint = (code, text2) => tty && !process.env.NO_COLOR ? `\x1B[${code}m${text2}\x1B[0m` : text2;
|
|
639
|
+
const print = (text2 = "") => {
|
|
640
|
+
output.write(`${text2}
|
|
441
641
|
`);
|
|
442
642
|
};
|
|
443
643
|
const toolNames = () => bridge.tools().map((t) => t.name);
|
|
@@ -590,21 +790,201 @@ import { spawn } from "child_process";
|
|
|
590
790
|
import { existsSync, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
|
|
591
791
|
|
|
592
792
|
// src/client/session/daemon.ts
|
|
793
|
+
import { randomBytes } from "crypto";
|
|
593
794
|
import { appendFileSync as appendFileSync2, chmodSync, rmSync } from "fs";
|
|
594
795
|
import { createServer } from "net";
|
|
595
796
|
import { createInterface as createInterface3 } from "readline";
|
|
797
|
+
|
|
798
|
+
// src/client/session/journal.ts
|
|
799
|
+
var MAX_TRIES = 3;
|
|
800
|
+
var lostConnection = (r) => !r.ok && (r.from === "" || /socket closed|closed the debugger|not open/i.test(r.error));
|
|
801
|
+
var plain = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
802
|
+
var isStore = (v) => plain(v.original) && plain(v.after);
|
|
803
|
+
function mergeStore(old, next) {
|
|
804
|
+
const known = /* @__PURE__ */ new Set([...Object.keys(old.original), ...Object.keys(old.after), ...old.skipped ?? []]);
|
|
805
|
+
const original = { ...old.original };
|
|
806
|
+
const after = { ...old.after };
|
|
807
|
+
const touched = /* @__PURE__ */ new Set([...Object.keys(next.original), ...Object.keys(next.after), ...next.skipped ?? []]);
|
|
808
|
+
for (const k of touched) {
|
|
809
|
+
if (!known.has(k) && k in next.original) original[k] = next.original[k];
|
|
810
|
+
if (k in next.after) after[k] = next.after[k];
|
|
811
|
+
else delete after[k];
|
|
812
|
+
}
|
|
813
|
+
const skipped = [.../* @__PURE__ */ new Set([...old.skipped ?? [], ...next.skipped ?? []])];
|
|
814
|
+
return { original, after, ...skipped.length && { skipped } };
|
|
815
|
+
}
|
|
816
|
+
function merge(carry, data) {
|
|
817
|
+
for (const [area, part] of Object.entries(data)) {
|
|
818
|
+
const old = carry[area];
|
|
819
|
+
if (!plain(old) || !plain(part)) {
|
|
820
|
+
carry[area] = part;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const next = { ...old };
|
|
824
|
+
for (const [name, value] of Object.entries(part)) {
|
|
825
|
+
const was = old[name];
|
|
826
|
+
next[name] = plain(was) && plain(value) ? isStore(was) && isStore(value) ? mergeStore(was, value) : { ...value, ...was } : value;
|
|
827
|
+
}
|
|
828
|
+
carry[area] = next;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
var names = (part) => plain(part) ? Object.keys(part) : [];
|
|
832
|
+
function createJournal(call, firstLoad, alive = async () => false) {
|
|
833
|
+
let data = {};
|
|
834
|
+
let tag = "";
|
|
835
|
+
let load = firstLoad;
|
|
836
|
+
const carry = {};
|
|
837
|
+
let tries = 0;
|
|
838
|
+
let lastWaiting = "";
|
|
839
|
+
const why = (area, part, reason2) => {
|
|
840
|
+
const keys = names(part);
|
|
841
|
+
return keys.length ? keys.map((n) => `${area}:${n} (${reason2(n)})`) : [`${area} (${reason2(area)})`];
|
|
842
|
+
};
|
|
843
|
+
const gaveUp = (reasons) => {
|
|
844
|
+
const left = Object.entries(carry).flatMap(
|
|
845
|
+
([area, part]) => why(area, part, () => reasons[area] ?? "not ready")
|
|
846
|
+
);
|
|
847
|
+
for (const k of Object.keys(carry)) delete carry[k];
|
|
848
|
+
return left.length ? `could not re-arm after the reload: ${left.join(", ")}` : "";
|
|
849
|
+
};
|
|
850
|
+
return {
|
|
851
|
+
/** The tag to send with every call. */
|
|
852
|
+
ack: () => tag,
|
|
853
|
+
/** Takes the journal a reply carries; one from another load is dropped. */
|
|
854
|
+
accept(r) {
|
|
855
|
+
if (r.journalData === void 0 || !r.loadId || r.loadId !== load) return;
|
|
856
|
+
data = r.journalData;
|
|
857
|
+
tag = `${r.loadId}:${r.journal ?? 0}`;
|
|
858
|
+
},
|
|
859
|
+
/** The app reloaded into `newLoad`: what the old load held moves to `carry`. Returns the pending areas that had nothing persistent. */
|
|
860
|
+
reloaded(lastPending, newLoad) {
|
|
861
|
+
merge(carry, data);
|
|
862
|
+
data = {};
|
|
863
|
+
tag = "";
|
|
864
|
+
load = newLoad;
|
|
865
|
+
tries = 0;
|
|
866
|
+
lastWaiting = "";
|
|
867
|
+
return lastPending.filter((area) => !(area in carry));
|
|
868
|
+
},
|
|
869
|
+
hasCarry: () => Object.keys(carry).length > 0,
|
|
870
|
+
/** The load the journal is for now. */
|
|
871
|
+
currentLoad: () => load,
|
|
872
|
+
/** Hands `carry` to the new load. Resolves to a note ('' when there was nothing to say). */
|
|
873
|
+
async adopt() {
|
|
874
|
+
if (!Object.keys(carry).length) return "";
|
|
875
|
+
const sent = structuredClone(carry);
|
|
876
|
+
const sentLoad = load;
|
|
877
|
+
let r;
|
|
878
|
+
try {
|
|
879
|
+
r = await call("bridge.adopt", [sent]);
|
|
880
|
+
} catch {
|
|
881
|
+
if (!await alive().catch(() => false)) return "";
|
|
882
|
+
r = { id: "", from: "app", ok: false, error: "timed out", ms: 0 };
|
|
883
|
+
}
|
|
884
|
+
if (lostConnection({ ...r })) return "";
|
|
885
|
+
if (load !== sentLoad || r.loadId && sentLoad && r.loadId !== sentLoad) return "";
|
|
886
|
+
this.accept(r);
|
|
887
|
+
tries += 1;
|
|
888
|
+
const bits = [];
|
|
889
|
+
const reasons = {};
|
|
890
|
+
if (!r.ok) {
|
|
891
|
+
for (const area of Object.keys(sent)) reasons[area] = r.error;
|
|
892
|
+
} else {
|
|
893
|
+
const reply = r.value;
|
|
894
|
+
const adopted = [];
|
|
895
|
+
const already = [];
|
|
896
|
+
const changed = [];
|
|
897
|
+
const invalid = [];
|
|
898
|
+
const skipped = [];
|
|
899
|
+
const notKept = [];
|
|
900
|
+
for (const area of Object.keys(sent)) {
|
|
901
|
+
const part = reply[area];
|
|
902
|
+
if (!part) continue;
|
|
903
|
+
adopted.push(...part.adopted ?? []);
|
|
904
|
+
already.push(...part.already ?? []);
|
|
905
|
+
changed.push(...part.changed ?? []);
|
|
906
|
+
invalid.push(...part.invalid ?? []);
|
|
907
|
+
skipped.push(...part.skipped ?? []);
|
|
908
|
+
notKept.push(...part.notKept ?? []);
|
|
909
|
+
if (part.retry !== void 0) {
|
|
910
|
+
carry[area] = part.retry;
|
|
911
|
+
reasons[area] = part.error ?? "not ready";
|
|
912
|
+
} else delete carry[area];
|
|
913
|
+
}
|
|
914
|
+
if (adopted.length)
|
|
915
|
+
bits.push(`kept ${adopted.length} undo${adopted.length === 1 ? "" : "s"} across the reload (${adopted.join(", ")}); bridge.restore still puts them back`);
|
|
916
|
+
if (already.length) bits.push(`already back to the original: ${already.join(", ")}`);
|
|
917
|
+
if (changed.length) bits.push(`left alone, the app changed them since: ${changed.join(", ")}`);
|
|
918
|
+
if (skipped.length) bits.push(`left alone, a different storage instance than the one you wrote: ${skipped.join(", ")}`);
|
|
919
|
+
if (notKept.length) bits.push(`not kept across reload (not plain JSON, renamed by partialize, or hidden by redact): ${notKept.join(", ")}`);
|
|
920
|
+
if (invalid.length) bits.push(`ignored, not a value this session saved: ${invalid.join(", ")}`);
|
|
921
|
+
}
|
|
922
|
+
if (Object.keys(carry).length) {
|
|
923
|
+
if (tries >= MAX_TRIES) {
|
|
924
|
+
bits.push(gaveUp(reasons));
|
|
925
|
+
lastWaiting = "";
|
|
926
|
+
} else {
|
|
927
|
+
const waiting = Object.entries(carry).flatMap(([area, part]) => why(area, part, () => reasons[area] ?? "not ready")).join(", ");
|
|
928
|
+
if (waiting !== lastWaiting) bits.push(`not ready yet, will retry: ${waiting}`);
|
|
929
|
+
lastWaiting = waiting;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return bits.join("; ");
|
|
933
|
+
},
|
|
934
|
+
/** At session end: drops what is still waiting and says so. */
|
|
935
|
+
giveUp: () => gaveUp({})
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/client/session/daemon.ts
|
|
596
940
|
var RELOAD = "app.reload";
|
|
597
|
-
var
|
|
941
|
+
var REPEATABLE = /* @__PURE__ */ new Set([
|
|
942
|
+
"bridge.ping",
|
|
943
|
+
"bridge.pending",
|
|
944
|
+
"bridge.tools",
|
|
945
|
+
"screen.snapshot",
|
|
946
|
+
"screen.findText",
|
|
947
|
+
"screen.waitFor",
|
|
948
|
+
"screen.capture",
|
|
949
|
+
"store.get",
|
|
950
|
+
"store.list",
|
|
951
|
+
"query.get",
|
|
952
|
+
"query.list",
|
|
953
|
+
"mmkv.get",
|
|
954
|
+
"mmkv.keys",
|
|
955
|
+
"mmkv.list",
|
|
956
|
+
"net.entry",
|
|
957
|
+
"net.mocks",
|
|
958
|
+
"router.current",
|
|
959
|
+
"scenario.list",
|
|
960
|
+
"webview.list",
|
|
961
|
+
"webview.url",
|
|
962
|
+
"webview.messages",
|
|
963
|
+
"webview.message",
|
|
964
|
+
"webview.snapshot",
|
|
965
|
+
"webview.waitFor",
|
|
966
|
+
"realtime.channels"
|
|
967
|
+
]);
|
|
968
|
+
var LOG_TOOLS = /* @__PURE__ */ new Set(["net.log", "realtime.log", "bridge.logs"]);
|
|
969
|
+
var WAIT_TOOLS = /* @__PURE__ */ new Set(["screen.waitFor", "webview.waitFor"]);
|
|
970
|
+
var WAIT_DEFAULT_MS = 5e3;
|
|
971
|
+
var repeatable = (tool, args) => REPEATABLE.has(tool) || LOG_TOOLS.has(tool) && !args.some((a) => a?.clear);
|
|
972
|
+
var withTimeLeft = (args, leftMs) => {
|
|
973
|
+
const next = [...args];
|
|
974
|
+
const options = next[1] ?? {};
|
|
975
|
+
next[1] = { ...options, timeoutMs: Math.max(1, Math.round(leftMs)) };
|
|
976
|
+
return next;
|
|
977
|
+
};
|
|
978
|
+
var pause = (ms, signal) => new Promise((resolve4) => {
|
|
598
979
|
const done = () => {
|
|
599
980
|
clearTimeout(timer);
|
|
600
981
|
signal.removeEventListener("abort", done);
|
|
601
|
-
|
|
982
|
+
resolve4();
|
|
602
983
|
};
|
|
603
984
|
const timer = setTimeout(done, ms);
|
|
604
985
|
signal.addEventListener("abort", done, { once: true });
|
|
605
986
|
});
|
|
606
987
|
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
607
|
-
var lostConnection = (r) => !r.ok && (r.from === "" || /socket closed|closed the debugger|not open/i.test(r.error));
|
|
608
988
|
async function runSessionDaemon(options) {
|
|
609
989
|
const name = checkName(options.name);
|
|
610
990
|
const dir = stateDir();
|
|
@@ -612,7 +992,7 @@ async function runSessionDaemon(options) {
|
|
|
612
992
|
const metro = metroHost(options.metro);
|
|
613
993
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
614
994
|
const reloadWaitMs = options.reloadWaitMs ?? 2e4;
|
|
615
|
-
const log = (
|
|
995
|
+
const log = (text2) => appendFileSync2(files.log, `${(/* @__PURE__ */ new Date()).toISOString()} ${text2}
|
|
616
996
|
`, {
|
|
617
997
|
mode: 384
|
|
618
998
|
});
|
|
@@ -669,20 +1049,68 @@ async function runSessionDaemon(options) {
|
|
|
669
1049
|
let lastPending = [];
|
|
670
1050
|
const reloads = [];
|
|
671
1051
|
let unseen = [];
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1052
|
+
const journalKey = randomBytes(16).toString("hex");
|
|
1053
|
+
const send = (tool, args, ms) => conn.call(tool, args, ms, { journalAck: journal.ack(), journalKey });
|
|
1054
|
+
const adoptTimeoutMs = options.adoptTimeoutMs ?? Math.max(timeoutMs, 3500);
|
|
1055
|
+
const journal = createJournal(
|
|
1056
|
+
(tool, args) => send(tool, args, adoptTimeoutMs),
|
|
1057
|
+
conn.device.loadId,
|
|
1058
|
+
() => ping()
|
|
1059
|
+
);
|
|
1060
|
+
const addNote = (note) => {
|
|
676
1061
|
reloads.push(note);
|
|
677
1062
|
unseen.push(note);
|
|
678
1063
|
log(`warning: ${note}`);
|
|
679
1064
|
};
|
|
1065
|
+
const adoptRetryMs = options.adoptRetryMs ?? 1e3;
|
|
1066
|
+
let adoptRun = null;
|
|
1067
|
+
let adoptLoad;
|
|
1068
|
+
let nextAdopt = 0;
|
|
1069
|
+
const adoptJournal = async (force = false) => {
|
|
1070
|
+
if (adoptRun) {
|
|
1071
|
+
const startedFor = adoptLoad;
|
|
1072
|
+
await adoptRun;
|
|
1073
|
+
if (!force || startedFor === journal.currentLoad()) return;
|
|
1074
|
+
if (adoptRun) return adoptRun;
|
|
1075
|
+
}
|
|
1076
|
+
if (!journal.hasCarry() || !force && Date.now() < nextAdopt) return;
|
|
1077
|
+
inflight++;
|
|
1078
|
+
adoptLoad = journal.currentLoad();
|
|
1079
|
+
adoptRun = journal.adopt().then(
|
|
1080
|
+
(note) => {
|
|
1081
|
+
if (note) addNote(`app reloaded; ${note}`);
|
|
1082
|
+
},
|
|
1083
|
+
() => {
|
|
1084
|
+
}
|
|
1085
|
+
).finally(() => {
|
|
1086
|
+
inflight--;
|
|
1087
|
+
nextAdopt = Date.now() + adoptRetryMs;
|
|
1088
|
+
adoptRun = null;
|
|
1089
|
+
});
|
|
1090
|
+
return adoptRun;
|
|
1091
|
+
};
|
|
1092
|
+
const noteReload = (newLoad) => {
|
|
1093
|
+
const lost = journal.reloaded(lastPending, newLoad);
|
|
1094
|
+
lastPending = [];
|
|
1095
|
+
if (lost.length || !journal.hasCarry())
|
|
1096
|
+
addNote(
|
|
1097
|
+
`app reloaded; ${lost.length} pending restore${lost.length === 1 ? "" : "s"} lost${lost.length ? `: ${lost.join(", ")}` : ""}`
|
|
1098
|
+
);
|
|
1099
|
+
nextAdopt = 0;
|
|
1100
|
+
void adoptJournal(true);
|
|
1101
|
+
};
|
|
680
1102
|
const observe = (r) => {
|
|
681
1103
|
if (!r.loadId) return;
|
|
682
|
-
if (knownLoad && r.loadId !== knownLoad) noteReload();
|
|
1104
|
+
if (knownLoad && r.loadId !== knownLoad) noteReload(r.loadId);
|
|
683
1105
|
knownLoad = r.loadId;
|
|
684
1106
|
lastPending = r.pending ?? [];
|
|
685
1107
|
};
|
|
1108
|
+
const seen = (r) => {
|
|
1109
|
+
observe(r);
|
|
1110
|
+
journal.accept(r);
|
|
1111
|
+
const { journalData: _, ...rest } = r;
|
|
1112
|
+
return rest;
|
|
1113
|
+
};
|
|
686
1114
|
const takeNotice = () => {
|
|
687
1115
|
const notice = unseen.join("; ");
|
|
688
1116
|
unseen = [];
|
|
@@ -691,9 +1119,9 @@ async function runSessionDaemon(options) {
|
|
|
691
1119
|
let reloadingUntil = 0;
|
|
692
1120
|
let stale = false;
|
|
693
1121
|
let reconnecting = null;
|
|
694
|
-
const reconnect = (
|
|
1122
|
+
const reconnect = (reason2) => {
|
|
695
1123
|
reconnecting ??= (async () => {
|
|
696
|
-
log(`reconnecting: ${
|
|
1124
|
+
log(`reconnecting: ${reason2}`);
|
|
697
1125
|
conn.close();
|
|
698
1126
|
const patient = Date.now() < reloadingUntil;
|
|
699
1127
|
const deadline = Date.now() + (patient ? reloadWaitMs : 0);
|
|
@@ -712,7 +1140,7 @@ async function runSessionDaemon(options) {
|
|
|
712
1140
|
stale = true;
|
|
713
1141
|
log(`reconnect failed: ${message(error)}`);
|
|
714
1142
|
throw new Error(
|
|
715
|
-
patient ? `The app is reloading; its bridge isn't ready yet (${message(error)})` : `The app is gone (${
|
|
1143
|
+
patient ? `The app is reloading; its bridge isn't ready yet (${message(error)})` : `The app is gone (${reason2}). Reconnecting failed: ${message(error)}`
|
|
716
1144
|
);
|
|
717
1145
|
}
|
|
718
1146
|
await pause(300, stopping.signal);
|
|
@@ -721,7 +1149,7 @@ async function runSessionDaemon(options) {
|
|
|
721
1149
|
stale = false;
|
|
722
1150
|
reloadingUntil = 0;
|
|
723
1151
|
if (conn.device.loadId) {
|
|
724
|
-
if (knownLoad && conn.device.loadId !== knownLoad) noteReload();
|
|
1152
|
+
if (knownLoad && conn.device.loadId !== knownLoad) noteReload(conn.device.loadId);
|
|
725
1153
|
knownLoad = conn.device.loadId;
|
|
726
1154
|
}
|
|
727
1155
|
state.device = deviceOf(conn);
|
|
@@ -733,8 +1161,8 @@ async function runSessionDaemon(options) {
|
|
|
733
1161
|
});
|
|
734
1162
|
return reconnecting;
|
|
735
1163
|
};
|
|
736
|
-
const connected = async (
|
|
737
|
-
const attempt2 = reconnecting ?? reconnect(
|
|
1164
|
+
const connected = async (reason2 = "app stopped answering") => {
|
|
1165
|
+
const attempt2 = reconnecting ?? reconnect(reason2);
|
|
738
1166
|
const wait = Date.now() < reloadingUntil ? reloadWaitMs + 2 * attemptMs + 500 : 2 * attemptMs + 500;
|
|
739
1167
|
let timer2;
|
|
740
1168
|
try {
|
|
@@ -755,16 +1183,16 @@ async function runSessionDaemon(options) {
|
|
|
755
1183
|
clearTimeout(timer2);
|
|
756
1184
|
}
|
|
757
1185
|
};
|
|
758
|
-
const ping = (ms = 1500) =>
|
|
1186
|
+
const ping = (ms = 1500) => send("bridge.ping", [], Math.min(ms, 1500)).then(
|
|
759
1187
|
(r) => {
|
|
760
|
-
if (r.ok)
|
|
1188
|
+
if (r.ok) seen(r);
|
|
761
1189
|
return r.ok;
|
|
762
1190
|
},
|
|
763
1191
|
() => false
|
|
764
1192
|
);
|
|
765
1193
|
const attempt = async (tool, args, ms) => {
|
|
766
1194
|
try {
|
|
767
|
-
const result = await
|
|
1195
|
+
const result = await send(tool, args, ms);
|
|
768
1196
|
return lostConnection(result) ? { lost: result.error } : { result };
|
|
769
1197
|
} catch (error) {
|
|
770
1198
|
if (await ping(ms)) throw error;
|
|
@@ -773,12 +1201,15 @@ async function runSessionDaemon(options) {
|
|
|
773
1201
|
};
|
|
774
1202
|
const callApp = async (tool, args, ms) => {
|
|
775
1203
|
if (stale || reconnecting) await connected();
|
|
1204
|
+
await adoptJournal(tool === "bridge.restore");
|
|
1205
|
+
if (tool === RELOAD) await ping();
|
|
776
1206
|
let t0 = performance.now();
|
|
1207
|
+
const start2 = t0;
|
|
777
1208
|
const first = await attempt(tool, args, ms);
|
|
778
1209
|
if (first.result) {
|
|
779
|
-
|
|
780
|
-
if (tool === RELOAD &&
|
|
781
|
-
return { result
|
|
1210
|
+
const result = seen(first.result);
|
|
1211
|
+
if (tool === RELOAD && result.ok) waitForReload();
|
|
1212
|
+
return { result, ms: performance.now() - t0 };
|
|
782
1213
|
}
|
|
783
1214
|
if (tool === RELOAD) {
|
|
784
1215
|
waitForReload();
|
|
@@ -791,20 +1222,48 @@ async function runSessionDaemon(options) {
|
|
|
791
1222
|
};
|
|
792
1223
|
return { result, ms: performance.now() - t0 };
|
|
793
1224
|
}
|
|
794
|
-
|
|
1225
|
+
const before = knownLoad;
|
|
1226
|
+
const goneWait = WAIT_TOOLS.has(tool) && args[1]?.gone;
|
|
1227
|
+
let reconnectFailure = "";
|
|
1228
|
+
try {
|
|
1229
|
+
await connected(first.lost);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
reconnectFailure = message(error);
|
|
1232
|
+
}
|
|
1233
|
+
const reloaded = knownLoad !== before;
|
|
1234
|
+
if (!reconnectFailure) await sync();
|
|
1235
|
+
const again = repeatable(tool, args) && !(goneWait && reloaded);
|
|
1236
|
+
if (again && reconnectFailure) throw new Error(reconnectFailure);
|
|
1237
|
+
if (!again) {
|
|
1238
|
+
if ((reloaded || reconnectFailure) && /^(mmkv|store)\./.test(tool))
|
|
1239
|
+
addNote(
|
|
1240
|
+
`${tool} was cut off; if it wrote to MMKV or a persisted store, that undo may be lost`
|
|
1241
|
+
);
|
|
1242
|
+
const how = reloaded ? "the app reloaded" : before ? "the app disconnected" : "the connection dropped";
|
|
1243
|
+
throw new Error(
|
|
1244
|
+
`${how} during ${tool}; it may or may not have run (${first.lost})${reconnectFailure ? `; reconnecting failed: ${reconnectFailure}` : ""}`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
795
1247
|
t0 = performance.now();
|
|
796
|
-
const
|
|
1248
|
+
const retryArgs = WAIT_TOOLS.has(tool) ? withTimeLeft(
|
|
1249
|
+
args,
|
|
1250
|
+
(args[1]?.timeoutMs ?? WAIT_DEFAULT_MS) - (t0 - start2)
|
|
1251
|
+
) : args;
|
|
1252
|
+
const second = await attempt(tool, retryArgs, ms);
|
|
797
1253
|
if (!second.result)
|
|
798
1254
|
throw new Error(
|
|
799
1255
|
`The app stopped answering after a reconnect: ${second.lost}`
|
|
800
1256
|
);
|
|
801
|
-
observe(second.result);
|
|
802
1257
|
return {
|
|
803
|
-
result: second.result,
|
|
1258
|
+
result: seen(second.result),
|
|
804
1259
|
ms: performance.now() - t0,
|
|
805
1260
|
reconnected: true
|
|
806
1261
|
};
|
|
807
1262
|
};
|
|
1263
|
+
const sync = async () => {
|
|
1264
|
+
if (journal.hasCarry()) await adoptJournal(true);
|
|
1265
|
+
else await ping();
|
|
1266
|
+
};
|
|
808
1267
|
const waitForReload = () => {
|
|
809
1268
|
reloadingUntil = Date.now() + reloadWaitMs;
|
|
810
1269
|
stale = true;
|
|
@@ -827,19 +1286,47 @@ async function runSessionDaemon(options) {
|
|
|
827
1286
|
};
|
|
828
1287
|
const health = options.healthMs === 0 ? void 0 : setInterval(async () => {
|
|
829
1288
|
if (inflight || reconnecting || finishing) return;
|
|
830
|
-
|
|
1289
|
+
const pinged = conn;
|
|
1290
|
+
const answered = await ping();
|
|
1291
|
+
if (inflight || reconnecting || finishing || conn !== pinged) return;
|
|
1292
|
+
if (stale || !answered)
|
|
831
1293
|
await reconnect("no answer to bridge.ping").catch(() => {
|
|
832
1294
|
});
|
|
833
1295
|
}, options.healthMs ?? 5e3);
|
|
834
1296
|
const timer = health;
|
|
835
1297
|
timer?.unref?.();
|
|
1298
|
+
const restoreOnce = async () => {
|
|
1299
|
+
const fail = (error) => ({ id: "restore", from: "", ok: false, error, ms: 0 });
|
|
1300
|
+
const run = (tool) => attempt(tool, [], timeoutMs);
|
|
1301
|
+
try {
|
|
1302
|
+
if (stale || reconnecting || !await ping()) await connected("no answer before restore");
|
|
1303
|
+
for (let i = 0; i < 3 && journal.hasCarry(); i++) {
|
|
1304
|
+
if (i) await new Promise((r) => setTimeout(r, Math.min(adoptRetryMs, 500)));
|
|
1305
|
+
await adoptJournal(true);
|
|
1306
|
+
}
|
|
1307
|
+
const first = await run("bridge.restore");
|
|
1308
|
+
if (first.result) return seen(first.result);
|
|
1309
|
+
await connected(first.lost);
|
|
1310
|
+
await adoptJournal(true);
|
|
1311
|
+
const check = await run("bridge.pending");
|
|
1312
|
+
if (!check.result) return fail(`restore may or may not have run (${first.lost})`);
|
|
1313
|
+
const left = seen(check.result);
|
|
1314
|
+
if (!left.ok) return fail(`restore may or may not have run (${first.lost}); ${left.error}`);
|
|
1315
|
+
if (!Object.keys(left.value).length)
|
|
1316
|
+
return { ...left, id: "restore", value: { note: "the reply was lost; nothing is pending now" } };
|
|
1317
|
+
const second = await run("bridge.restore");
|
|
1318
|
+
return second.result ? seen(second.result) : fail(`The app stopped answering after a reconnect: ${second.lost}`);
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
return fail(message(error));
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
836
1323
|
let finishing = null;
|
|
837
1324
|
let resolveDone = () => {
|
|
838
1325
|
};
|
|
839
1326
|
const done = new Promise((r) => {
|
|
840
1327
|
resolveDone = r;
|
|
841
1328
|
});
|
|
842
|
-
const teardown = (
|
|
1329
|
+
const teardown = (reason2, keep) => {
|
|
843
1330
|
finishing ??= (async () => {
|
|
844
1331
|
clearTimeout(idleTimer);
|
|
845
1332
|
clearInterval(health);
|
|
@@ -847,16 +1334,9 @@ async function runSessionDaemon(options) {
|
|
|
847
1334
|
const cut = setTimeout(() => cutOff.abort(), Math.min(attemptMs, 5e3));
|
|
848
1335
|
let restore = null;
|
|
849
1336
|
if (!keep) {
|
|
850
|
-
restore = await
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
id: "restore",
|
|
854
|
-
from: "",
|
|
855
|
-
ok: false,
|
|
856
|
-
error: message(error),
|
|
857
|
-
ms: 0
|
|
858
|
-
})
|
|
859
|
-
);
|
|
1337
|
+
restore = await restoreOnce();
|
|
1338
|
+
const left = journal.giveUp();
|
|
1339
|
+
if (left) addNote(`app reloaded; ${left}`);
|
|
860
1340
|
}
|
|
861
1341
|
clearTimeout(cut);
|
|
862
1342
|
cutOff.abort();
|
|
@@ -864,10 +1344,10 @@ async function runSessionDaemon(options) {
|
|
|
864
1344
|
server.close();
|
|
865
1345
|
removeSessionFiles(name, dir);
|
|
866
1346
|
const outcome = !restore ? "kept app state" : restore.ok ? `restore: ${JSON.stringify(restore.value)}` : `restore: ${restore.error}`;
|
|
867
|
-
log(`stopped (${
|
|
1347
|
+
log(`stopped (${reason2}); ${outcome}`);
|
|
868
1348
|
const deadline = Date.now() + 2e3;
|
|
869
1349
|
const settle = () => {
|
|
870
|
-
if (!clients.size || Date.now() > deadline) resolveDone(
|
|
1350
|
+
if (!clients.size || Date.now() > deadline) resolveDone(reason2);
|
|
871
1351
|
else setTimeout(settle, 10);
|
|
872
1352
|
};
|
|
873
1353
|
settle();
|
|
@@ -889,16 +1369,21 @@ async function runSessionDaemon(options) {
|
|
|
889
1369
|
switch (req.op) {
|
|
890
1370
|
case "call": {
|
|
891
1371
|
if (!req.tool) return { id: req.id, error: "call needs a tool" };
|
|
1372
|
+
if (req.tool === "bridge.adopt")
|
|
1373
|
+
return { id: req.id, error: "bridge.adopt is for a session's own use" };
|
|
892
1374
|
inflight++;
|
|
893
1375
|
touch();
|
|
894
1376
|
try {
|
|
1377
|
+
const args = req.args ?? [];
|
|
895
1378
|
const answer = await callApp(
|
|
896
1379
|
req.tool,
|
|
897
|
-
|
|
898
|
-
req.timeoutMs ?? timeoutMs
|
|
1380
|
+
args,
|
|
1381
|
+
callTimeout(req.timeoutMs ?? timeoutMs, req.tool, args)
|
|
899
1382
|
);
|
|
1383
|
+
await adoptRun;
|
|
900
1384
|
return { id: req.id, ...answer, ...takeNotice() };
|
|
901
1385
|
} catch (error) {
|
|
1386
|
+
await adoptRun;
|
|
902
1387
|
return { id: req.id, error: message(error), ...takeNotice() };
|
|
903
1388
|
} finally {
|
|
904
1389
|
inflight--;
|
|
@@ -941,7 +1426,10 @@ async function runSessionDaemon(options) {
|
|
|
941
1426
|
socket.on("close", () => clients.delete(socket));
|
|
942
1427
|
socket.on("error", () => {
|
|
943
1428
|
});
|
|
944
|
-
createInterface3({ input: socket })
|
|
1429
|
+
const lines = createInterface3({ input: socket });
|
|
1430
|
+
lines.on("error", () => {
|
|
1431
|
+
});
|
|
1432
|
+
lines.on("line", async (line) => {
|
|
945
1433
|
let req;
|
|
946
1434
|
try {
|
|
947
1435
|
req = JSON.parse(line);
|
|
@@ -958,11 +1446,11 @@ async function runSessionDaemon(options) {
|
|
|
958
1446
|
});
|
|
959
1447
|
if (process.platform !== "win32") rmSync(files.socket, { force: true });
|
|
960
1448
|
try {
|
|
961
|
-
await new Promise((
|
|
1449
|
+
await new Promise((resolve4, reject) => {
|
|
962
1450
|
server.once("error", reject);
|
|
963
1451
|
server.listen(files.socket, () => {
|
|
964
1452
|
server.off("error", reject);
|
|
965
|
-
|
|
1453
|
+
resolve4();
|
|
966
1454
|
});
|
|
967
1455
|
});
|
|
968
1456
|
} catch (error) {
|
|
@@ -989,13 +1477,14 @@ async function runSessionDaemon(options) {
|
|
|
989
1477
|
|
|
990
1478
|
// src/client/session/cli.ts
|
|
991
1479
|
var DAEMON_COMMAND = "__session-daemon";
|
|
992
|
-
function sessionFor(flags) {
|
|
1480
|
+
function sessionFor(flags, skip) {
|
|
993
1481
|
if (flags["no-session"]) return null;
|
|
994
1482
|
return pickSession({
|
|
995
1483
|
name: flags.session ?? process.env.AGENT_BRIDGE_SESSION,
|
|
996
1484
|
metro: explicitMetro(flags.metro),
|
|
997
1485
|
device: flags.device,
|
|
998
|
-
transport: flags.transport
|
|
1486
|
+
transport: flags.transport,
|
|
1487
|
+
skip
|
|
999
1488
|
});
|
|
1000
1489
|
}
|
|
1001
1490
|
var describeRestore = (restore) => {
|
|
@@ -1049,7 +1538,7 @@ async function start(flags) {
|
|
|
1049
1538
|
[...process.execArgv, process.argv[1], ...args],
|
|
1050
1539
|
{ detached: true, stdio: "ignore", windowsHide: true }
|
|
1051
1540
|
);
|
|
1052
|
-
const state = await new Promise((
|
|
1541
|
+
const state = await new Promise((resolve4, reject) => {
|
|
1053
1542
|
const deadline = Date.now() + 3e4;
|
|
1054
1543
|
let exited = false;
|
|
1055
1544
|
child.once("exit", () => {
|
|
@@ -1060,13 +1549,13 @@ async function start(flags) {
|
|
|
1060
1549
|
const failed = existsSync(files.error);
|
|
1061
1550
|
if (ready && ready.pid === child.pid) {
|
|
1062
1551
|
clearInterval(tick);
|
|
1063
|
-
|
|
1552
|
+
resolve4(ready);
|
|
1064
1553
|
} else if (failed || exited || Date.now() > deadline) {
|
|
1065
1554
|
clearInterval(tick);
|
|
1066
1555
|
if (!exited) child.kill();
|
|
1067
|
-
const
|
|
1556
|
+
const reason2 = failed ? readFileSync3(files.error, "utf8") : exited ? `The session exited early; see ${files.log}` : `The session didn't connect within 30 s; see ${files.log}`;
|
|
1068
1557
|
rmSync2(files.error, { force: true });
|
|
1069
|
-
reject(new Error(
|
|
1558
|
+
reject(new Error(reason2));
|
|
1070
1559
|
}
|
|
1071
1560
|
}, 50);
|
|
1072
1561
|
});
|
|
@@ -1192,17 +1681,17 @@ import {
|
|
|
1192
1681
|
readFileSync as readFileSync4,
|
|
1193
1682
|
writeFileSync as writeFileSync2
|
|
1194
1683
|
} from "fs";
|
|
1195
|
-
import { dirname as
|
|
1684
|
+
import { dirname as dirname6, relative as relative6, resolve as resolve2, sep as sep2 } from "path";
|
|
1196
1685
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
1197
1686
|
|
|
1198
1687
|
// package.json
|
|
1199
|
-
var version = "0.
|
|
1688
|
+
var version = "0.6.0";
|
|
1200
1689
|
|
|
1201
1690
|
// src/init/detect.ts
|
|
1202
|
-
import { dirname as
|
|
1691
|
+
import { dirname as dirname4, join as join6, relative as relative3 } from "path";
|
|
1203
1692
|
|
|
1204
1693
|
// src/init/package-manager.ts
|
|
1205
|
-
import { dirname as
|
|
1694
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1206
1695
|
var LOCKFILES = [
|
|
1207
1696
|
["bun.lock", "bun"],
|
|
1208
1697
|
["bun.lockb", "bun"],
|
|
@@ -1216,12 +1705,12 @@ var fromName = (value) => {
|
|
|
1216
1705
|
};
|
|
1217
1706
|
function detectPackageManager(dir, root, fs, userAgent = "") {
|
|
1218
1707
|
for (const d of [dir, root]) {
|
|
1219
|
-
const pm = fromName(fs.readJson(
|
|
1708
|
+
const pm = fromName(fs.readJson(join5(d, "package.json"))?.packageManager);
|
|
1220
1709
|
if (pm) return pm;
|
|
1221
1710
|
}
|
|
1222
|
-
for (let d = dir; ; d =
|
|
1223
|
-
for (const [file, pm] of LOCKFILES) if (fs.exists(
|
|
1224
|
-
if (d === root ||
|
|
1711
|
+
for (let d = dir; ; d = dirname3(d)) {
|
|
1712
|
+
for (const [file, pm] of LOCKFILES) if (fs.exists(join5(d, file))) return pm;
|
|
1713
|
+
if (d === root || dirname3(d) === d) break;
|
|
1225
1714
|
}
|
|
1226
1715
|
return fromName(userAgent.split("/")[0]) ?? "npm";
|
|
1227
1716
|
}
|
|
@@ -1258,7 +1747,12 @@ var LIBRARIES = {
|
|
|
1258
1747
|
reactQuery: "@tanstack/react-query",
|
|
1259
1748
|
webview: "react-native-webview",
|
|
1260
1749
|
zustand: "zustand",
|
|
1750
|
+
redux: "@reduxjs/toolkit",
|
|
1751
|
+
reduxCore: "redux",
|
|
1752
|
+
jotai: "jotai",
|
|
1261
1753
|
mmkv: "react-native-mmkv",
|
|
1754
|
+
asyncStorage: "@react-native-async-storage/async-storage",
|
|
1755
|
+
secureStore: "expo-secure-store",
|
|
1262
1756
|
ably: "ably",
|
|
1263
1757
|
socketIo: "socket.io-client",
|
|
1264
1758
|
reactNavigation: "@react-navigation/native",
|
|
@@ -1270,18 +1764,18 @@ var depsOf = (pkg) => ({
|
|
|
1270
1764
|
...pkg?.devDependencies,
|
|
1271
1765
|
...pkg?.dependencies
|
|
1272
1766
|
});
|
|
1273
|
-
var isApp = (dir, fs) => !!fs.readJson(
|
|
1274
|
-
var isWorkspaceRoot = (dir, fs) => !!fs.readJson(
|
|
1767
|
+
var isApp = (dir, fs) => !!fs.readJson(join6(dir, "package.json"))?.dependencies?.["react-native"] || ["app.json", "app.config.js", "app.config.ts", "ios", "android"].some((f) => fs.exists(join6(dir, f)));
|
|
1768
|
+
var isWorkspaceRoot = (dir, fs) => !!fs.readJson(join6(dir, "package.json"))?.workspaces || fs.exists(join6(dir, "pnpm-workspace.yaml"));
|
|
1275
1769
|
function findUp(dir, fs, test) {
|
|
1276
|
-
for (let d = dir; ; d =
|
|
1770
|
+
for (let d = dir; ; d = dirname4(d)) {
|
|
1277
1771
|
if (test(d)) return d;
|
|
1278
|
-
if (fs.exists(
|
|
1772
|
+
if (fs.exists(join6(d, ".git")) || dirname4(d) === d) return void 0;
|
|
1279
1773
|
}
|
|
1280
1774
|
}
|
|
1281
1775
|
function workspacePatterns(root, fs) {
|
|
1282
|
-
const ws = fs.readJson(
|
|
1776
|
+
const ws = fs.readJson(join6(root, "package.json"))?.workspaces;
|
|
1283
1777
|
const fromPkg = Array.isArray(ws) ? ws : ws?.packages ?? [];
|
|
1284
|
-
const yaml = fs.read(
|
|
1778
|
+
const yaml = fs.read(join6(root, "pnpm-workspace.yaml")) ?? "";
|
|
1285
1779
|
const block = yaml.match(/^packages:\s*\n((?:[ \t]*(?:-.*|#.*)?\n?)*)/m)?.[1] ?? "";
|
|
1286
1780
|
const fromYaml = [...block.matchAll(/^[ \t]*-[ \t]*['"]?([^'"#\n]+?)['"]?[ \t]*$/gm)].map(
|
|
1287
1781
|
(m) => m[1]
|
|
@@ -1292,40 +1786,40 @@ function workspacePatterns(root, fs) {
|
|
|
1292
1786
|
}
|
|
1293
1787
|
function expandWorkspaces(root, patterns, fs) {
|
|
1294
1788
|
const found = /* @__PURE__ */ new Set();
|
|
1295
|
-
const
|
|
1789
|
+
const walk2 = (dir, segments, depth) => {
|
|
1296
1790
|
if (!segments.length) {
|
|
1297
|
-
if (fs.exists(
|
|
1791
|
+
if (fs.exists(join6(dir, "package.json"))) found.add(dir);
|
|
1298
1792
|
return;
|
|
1299
1793
|
}
|
|
1300
1794
|
const [head, ...rest] = segments;
|
|
1301
1795
|
const children = () => fs.list(dir).filter((e) => e.dir && e.name !== "node_modules" && !e.name.startsWith("."));
|
|
1302
1796
|
if (head === "**") {
|
|
1303
|
-
|
|
1797
|
+
walk2(dir, rest, depth);
|
|
1304
1798
|
if (depth < 4)
|
|
1305
|
-
for (const c of children())
|
|
1799
|
+
for (const c of children()) walk2(join6(dir, c.name), segments, depth + 1);
|
|
1306
1800
|
} else if (head.includes("*")) {
|
|
1307
1801
|
const re = new RegExp(
|
|
1308
1802
|
`^${head.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`
|
|
1309
1803
|
);
|
|
1310
1804
|
for (const c of children())
|
|
1311
|
-
if (re.test(c.name))
|
|
1805
|
+
if (re.test(c.name)) walk2(join6(dir, c.name), rest, depth);
|
|
1312
1806
|
} else {
|
|
1313
|
-
|
|
1807
|
+
walk2(join6(dir, head), rest, depth);
|
|
1314
1808
|
}
|
|
1315
1809
|
};
|
|
1316
1810
|
for (const p of patterns)
|
|
1317
|
-
|
|
1811
|
+
walk2(root, p.replace(/\/+$/, "").split("/").filter((s) => s && s !== "."), 0);
|
|
1318
1812
|
return [...found].sort();
|
|
1319
1813
|
}
|
|
1320
|
-
var firstExisting = (dir, fs, candidates) => candidates.find((c) => fs.exists(
|
|
1814
|
+
var firstExisting = (dir, fs, candidates) => candidates.find((c) => fs.exists(join6(dir, c)));
|
|
1321
1815
|
var withExts = (base) => SOURCE_EXTS.map((e) => `${base}.${e}`);
|
|
1322
1816
|
function detect(dir, fs, env = {}) {
|
|
1323
|
-
const pkg = fs.readJson(
|
|
1817
|
+
const pkg = fs.readJson(join6(dir, "package.json"));
|
|
1324
1818
|
if (!pkg) return { error: `No package.json in ${dir}.` };
|
|
1325
1819
|
const deps = depsOf(pkg);
|
|
1326
1820
|
if (!deps["react-native"]) {
|
|
1327
1821
|
if (isWorkspaceRoot(dir, fs)) {
|
|
1328
|
-
const apps = expandWorkspaces(dir, workspacePatterns(dir, fs), fs).filter((d) => isApp(d, fs)).map((d) =>
|
|
1822
|
+
const apps = expandWorkspaces(dir, workspacePatterns(dir, fs), fs).filter((d) => isApp(d, fs)).map((d) => relative3(dir, d));
|
|
1329
1823
|
return {
|
|
1330
1824
|
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
1825
|
workspaces: apps
|
|
@@ -1336,11 +1830,11 @@ function detect(dir, fs, env = {}) {
|
|
|
1336
1830
|
};
|
|
1337
1831
|
}
|
|
1338
1832
|
const root = findUp(dir, fs, (d) => isWorkspaceRoot(d, fs)) ?? dir;
|
|
1339
|
-
const repoRoot = findUp(dir, fs, (d) => fs.exists(
|
|
1833
|
+
const repoRoot = findUp(dir, fs, (d) => fs.exists(join6(d, ".git"))) ?? root;
|
|
1340
1834
|
const has = Object.fromEntries(
|
|
1341
1835
|
Object.entries(LIBRARIES).map(([k, dep]) => [k, !!deps[dep]])
|
|
1342
1836
|
);
|
|
1343
|
-
const appJson = fs.readJson(
|
|
1837
|
+
const appJson = fs.readJson(join6(dir, "app.json"));
|
|
1344
1838
|
const appName = [appJson?.expo?.name, appJson?.name, pkg.name].find(
|
|
1345
1839
|
(n) => typeof n === "string" && !!n
|
|
1346
1840
|
) ?? "app";
|
|
@@ -1350,15 +1844,15 @@ function detect(dir, fs, env = {}) {
|
|
|
1350
1844
|
fs,
|
|
1351
1845
|
has.expoRouter ? [...withExts("app/_layout"), ...withExts("src/app/_layout")] : [...withExts("App"), ...withExts("src/App")]
|
|
1352
1846
|
);
|
|
1353
|
-
const agentsFile = [.../* @__PURE__ */ new Set([dir, repoRoot])].flatMap((d) => [
|
|
1847
|
+
const agentsFile = [.../* @__PURE__ */ new Set([dir, repoRoot])].flatMap((d) => [join6(d, "AGENTS.md"), join6(d, "CLAUDE.md")]).find((f) => fs.exists(f));
|
|
1354
1848
|
let bin;
|
|
1355
|
-
for (let d = dir; ; d =
|
|
1356
|
-
const candidate =
|
|
1849
|
+
for (let d = dir; ; d = dirname4(d)) {
|
|
1850
|
+
const candidate = join6(d, "node_modules", ".bin", "agent-bridge");
|
|
1357
1851
|
if (fs.exists(candidate)) {
|
|
1358
1852
|
bin = candidate;
|
|
1359
1853
|
break;
|
|
1360
1854
|
}
|
|
1361
|
-
if (d === repoRoot ||
|
|
1855
|
+
if (d === repoRoot || dirname4(d) === d) break;
|
|
1362
1856
|
}
|
|
1363
1857
|
return {
|
|
1364
1858
|
dir,
|
|
@@ -1369,7 +1863,7 @@ function detect(dir, fs, env = {}) {
|
|
|
1369
1863
|
has,
|
|
1370
1864
|
installed: !!deps[PACKAGE],
|
|
1371
1865
|
typescript: !!tsconfig || has.typescript,
|
|
1372
|
-
srcDir: fs.exists(
|
|
1866
|
+
srcDir: fs.exists(join6(dir, "src")),
|
|
1373
1867
|
rootFile,
|
|
1374
1868
|
tsconfig,
|
|
1375
1869
|
oxlint: firstExisting(dir, fs, [".oxlintrc.json"]),
|
|
@@ -1385,28 +1879,28 @@ function detect(dir, fs, env = {}) {
|
|
|
1385
1879
|
}
|
|
1386
1880
|
|
|
1387
1881
|
// src/init/plan.ts
|
|
1388
|
-
import { dirname as
|
|
1882
|
+
import { dirname as dirname5, isAbsolute, join as join7, relative as relative4, sep } from "path";
|
|
1389
1883
|
|
|
1390
1884
|
// src/init/jsonc.ts
|
|
1391
|
-
function strip(
|
|
1885
|
+
function strip(text2) {
|
|
1392
1886
|
let out = "";
|
|
1393
1887
|
let i = 0;
|
|
1394
|
-
while (i <
|
|
1395
|
-
const c =
|
|
1888
|
+
while (i < text2.length) {
|
|
1889
|
+
const c = text2[i];
|
|
1396
1890
|
if (c === '"') {
|
|
1397
1891
|
let j = i + 1;
|
|
1398
|
-
while (j <
|
|
1399
|
-
out +=
|
|
1892
|
+
while (j < text2.length && text2[j] !== '"') j += text2[j] === "\\" ? 2 : 1;
|
|
1893
|
+
out += text2.slice(i, j + 1);
|
|
1400
1894
|
i = j + 1;
|
|
1401
|
-
} else if (c === "/" &&
|
|
1402
|
-
const end =
|
|
1403
|
-
const j = end === -1 ?
|
|
1895
|
+
} else if (c === "/" && text2[i + 1] === "/") {
|
|
1896
|
+
const end = text2.indexOf("\n", i);
|
|
1897
|
+
const j = end === -1 ? text2.length : end;
|
|
1404
1898
|
out += " ".repeat(j - i);
|
|
1405
1899
|
i = j;
|
|
1406
|
-
} else if (c === "/" &&
|
|
1407
|
-
const end =
|
|
1408
|
-
const j = end === -1 ?
|
|
1409
|
-
out +=
|
|
1900
|
+
} else if (c === "/" && text2[i + 1] === "*") {
|
|
1901
|
+
const end = text2.indexOf("*/", i + 2);
|
|
1902
|
+
const j = end === -1 ? text2.length : end + 2;
|
|
1903
|
+
out += text2.slice(i, j).replace(/[^\n]/g, " ");
|
|
1410
1904
|
i = j;
|
|
1411
1905
|
} else {
|
|
1412
1906
|
out += c;
|
|
@@ -1415,37 +1909,37 @@ function strip(text) {
|
|
|
1415
1909
|
}
|
|
1416
1910
|
return out.replace(/,(\s*[}\]])/g, " $1");
|
|
1417
1911
|
}
|
|
1418
|
-
function parseJsonc(
|
|
1912
|
+
function parseJsonc(text2) {
|
|
1419
1913
|
try {
|
|
1420
|
-
return JSON.parse(strip(
|
|
1914
|
+
return JSON.parse(strip(text2));
|
|
1421
1915
|
} catch {
|
|
1422
1916
|
return void 0;
|
|
1423
1917
|
}
|
|
1424
1918
|
}
|
|
1425
|
-
function addToArray(
|
|
1426
|
-
const json = parseJsonc(
|
|
1919
|
+
function addToArray(text2, key, value, initial = []) {
|
|
1920
|
+
const json = parseJsonc(text2);
|
|
1427
1921
|
if (!json || typeof json !== "object" || Array.isArray(json)) return void 0;
|
|
1428
1922
|
const current = json[key];
|
|
1429
|
-
const clean = strip(
|
|
1923
|
+
const clean = strip(text2);
|
|
1430
1924
|
const item = JSON.stringify(value);
|
|
1431
1925
|
if (Array.isArray(current)) {
|
|
1432
1926
|
const matches = [...clean.matchAll(new RegExp(`"${key}"\\s*:\\s*\\[`, "g"))];
|
|
1433
1927
|
if (matches.length !== 1) return void 0;
|
|
1434
1928
|
const at = matches[0].index + matches[0][0].length;
|
|
1435
|
-
const after =
|
|
1929
|
+
const after = text2.slice(at);
|
|
1436
1930
|
const multiline = after.match(/^[ \t]*\n([ \t]*)/);
|
|
1437
|
-
if (!current.length) return `${
|
|
1438
|
-
if (multiline) return `${
|
|
1931
|
+
if (!current.length) return `${text2.slice(0, at)}${item}${after}`;
|
|
1932
|
+
if (multiline) return `${text2.slice(0, at)}
|
|
1439
1933
|
${multiline[1]}${item},${after}`;
|
|
1440
|
-
return `${
|
|
1934
|
+
return `${text2.slice(0, at)}${item}, ${after}`;
|
|
1441
1935
|
}
|
|
1442
1936
|
if (current !== void 0) return void 0;
|
|
1443
1937
|
const end = clean.slice(0, clean.lastIndexOf("}")).trimEnd().length;
|
|
1444
|
-
const indent =
|
|
1938
|
+
const indent = text2.match(/^([ \t]+)"/m)?.[1] ?? " ";
|
|
1445
1939
|
const comma = clean.slice(0, end).endsWith("{") ? "" : ",";
|
|
1446
1940
|
const list2 = [...initial, value].map((v) => JSON.stringify(v)).join(", ");
|
|
1447
|
-
return `${
|
|
1448
|
-
${indent}"${key}": [${list2}]${
|
|
1941
|
+
return `${text2.slice(0, end)}${comma}
|
|
1942
|
+
${indent}"${key}": [${list2}]${text2.slice(end)}`;
|
|
1449
1943
|
}
|
|
1450
1944
|
|
|
1451
1945
|
// src/init/render.ts
|
|
@@ -1458,6 +1952,10 @@ var REALTIME = {
|
|
|
1458
1952
|
};
|
|
1459
1953
|
var STORES_WHY = "stores often hold tokens: add `redact` for those paths before wiring them";
|
|
1460
1954
|
var MMKV_WHY = "MMKV often holds tokens and mmkv.get returns values as they are: wire only instances without secrets";
|
|
1955
|
+
var ASYNC_STORAGE_WHY = "AsyncStorage often holds tokens and asyncStorage.get returns values as they are: wire it only if it holds no secrets";
|
|
1956
|
+
var SECURE_STORE_WHY = "SecureStore holds secrets: secureStore.get hides values unless the agent asks for {full:true}, but secureStore.set can still overwrite them";
|
|
1957
|
+
var REDUX_WHY = "the Redux state often holds tokens: add `redact` for those slices and paths, and wrap the root reducer in bridgeReducer() (inside persistReducer) before wiring it";
|
|
1958
|
+
var JOTAI_WHY = "atoms often hold tokens: list only the atoms you want the agent to read and set, and add `redact` for secret ones";
|
|
1461
1959
|
var REALTIME_WHY = "the adapter must wrap the client where it is created, before the app subscribes";
|
|
1462
1960
|
var NETWORK_PROMPT = "Wire network tools? In dev builds they patch fetch and XMLHttpRequest so the agent can log and mock requests.";
|
|
1463
1961
|
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";
|
|
@@ -1495,6 +1993,13 @@ function renderBridgeFile(o) {
|
|
|
1495
1993
|
}
|
|
1496
1994
|
if (o.expoRouter) L.push(`import { router, useNavigationContainerRef } from 'expo-router'`);
|
|
1497
1995
|
if (o.reactQuery) L.push(`import { useContext } from 'react'`);
|
|
1996
|
+
if (o.reactNavigation)
|
|
1997
|
+
L.push(
|
|
1998
|
+
"",
|
|
1999
|
+
"// Pass the ref you give to <NavigationContainer ref={\u2026}>, then uncomment navTools below.",
|
|
2000
|
+
`// import { navTools } from '@avasapp/agent-bridge/react-navigation'`,
|
|
2001
|
+
`// import { navigationRef } from './path-to-your-navigation-ref'`
|
|
2002
|
+
);
|
|
1498
2003
|
if (o.realtimeTools?.length)
|
|
1499
2004
|
L.push("", ...o.realtimeTools.map((t) => `import { ${t.name} } from '${t.from}'`));
|
|
1500
2005
|
if (o.stores) {
|
|
@@ -1519,6 +2024,48 @@ function renderBridgeFile(o) {
|
|
|
1519
2024
|
...mmkv.map((m) => `// import { ${m.name} } from '${m.from}'`)
|
|
1520
2025
|
);
|
|
1521
2026
|
}
|
|
2027
|
+
if (o.asyncStorage)
|
|
2028
|
+
L.push(
|
|
2029
|
+
"",
|
|
2030
|
+
"// AsyncStorage is left out: it often holds tokens, and asyncStorage.get returns",
|
|
2031
|
+
"// values as they are. Uncomment these and asyncStorageTools below if it holds none,",
|
|
2032
|
+
"// passing the storage the app uses (a createAsyncStorage(name) instance, if it has one).",
|
|
2033
|
+
`// See ${DOCS}/adapters.md`,
|
|
2034
|
+
`// import { asyncStorageTools } from '@avasapp/agent-bridge/async-storage'`,
|
|
2035
|
+
`// import AsyncStorage from '@react-native-async-storage/async-storage'`
|
|
2036
|
+
);
|
|
2037
|
+
if (o.secureStore)
|
|
2038
|
+
L.push(
|
|
2039
|
+
"",
|
|
2040
|
+
"// SecureStore is left out: it holds secrets. secureStore.get hides values unless",
|
|
2041
|
+
"// the agent asks for {full:true}. Uncomment these and secureStoreTools below,",
|
|
2042
|
+
"// passing the keys the app uses so secureStore.list can show them, and the same",
|
|
2043
|
+
"// options the app passes to SecureStore (keychainService, requireAuthentication).",
|
|
2044
|
+
`// See ${DOCS}/adapters.md`,
|
|
2045
|
+
`// import { secureStoreTools } from '@avasapp/agent-bridge/secure-store'`,
|
|
2046
|
+
`// import * as SecureStore from 'expo-secure-store'`
|
|
2047
|
+
);
|
|
2048
|
+
if (o.redux)
|
|
2049
|
+
L.push(
|
|
2050
|
+
"",
|
|
2051
|
+
"// Redux is left out: the state often holds tokens. Add `redact` for those slices,",
|
|
2052
|
+
"// wrap the root reducer in bridgeReducer() (inside persistReducer) where the store",
|
|
2053
|
+
"// is created, then uncomment these and reduxTools below.",
|
|
2054
|
+
`// See ${DOCS}/adapters.md`,
|
|
2055
|
+
`// import { reduxTools } from '@avasapp/agent-bridge/redux'`,
|
|
2056
|
+
`// import { store } from '../store'`
|
|
2057
|
+
);
|
|
2058
|
+
if (o.jotai)
|
|
2059
|
+
L.push(
|
|
2060
|
+
"",
|
|
2061
|
+
"// Jotai is left out: atoms often hold tokens. List the atoms the agent may read and",
|
|
2062
|
+
"// set, add `redact` for secret ones, then uncomment these and jotaiTools below.",
|
|
2063
|
+
"// Use the store your app uses: a <Provider> without a store prop has its own, out of reach here.",
|
|
2064
|
+
`// See ${DOCS}/adapters.md`,
|
|
2065
|
+
`// import { jotaiTools } from '@avasapp/agent-bridge/jotai'`,
|
|
2066
|
+
`// import { getDefaultStore } from 'jotai'`,
|
|
2067
|
+
`// import { userAtom } from '../atoms'`
|
|
2068
|
+
);
|
|
1522
2069
|
for (const todo of o.realtime) {
|
|
1523
2070
|
const r = REALTIME[todo.kind];
|
|
1524
2071
|
L.push(
|
|
@@ -1591,16 +2138,17 @@ function renderBridgeFile(o) {
|
|
|
1591
2138
|
const mmkv = o.mmkv.length ? o.mmkv : [{ name: "storage", from: "" }];
|
|
1592
2139
|
L.push(` // ...mmkvTools({ ${mmkv.map((m) => m.name).join(", ")} }),`);
|
|
1593
2140
|
}
|
|
2141
|
+
if (o.asyncStorage) L.push(" // ...asyncStorageTools(AsyncStorage),");
|
|
2142
|
+
if (o.secureStore) L.push(` // ...secureStoreTools(SecureStore, ['authToken']),`);
|
|
2143
|
+
if (o.redux) L.push(` // ...reduxTools(store, { redact: { auth: ['token'] } }),`);
|
|
2144
|
+
if (o.jotai) L.push(" // ...jotaiTools(getDefaultStore(), { user: userAtom }),");
|
|
1594
2145
|
if (o.realtime.length) L.push(" // ...realtimeDevTools,");
|
|
1595
|
-
if (o.reactNavigation)
|
|
1596
|
-
const params = o.ts ? "name: string, params?: object" : "name, params";
|
|
2146
|
+
if (o.reactNavigation)
|
|
1597
2147
|
L.push(
|
|
1598
|
-
" // React Navigation
|
|
1599
|
-
" // (createNavigationContainerRef from @react-navigation/native)
|
|
1600
|
-
|
|
1601
|
-
` // 'nav.current': () => navigationRef.getCurrentRoute(),`
|
|
2148
|
+
" // React Navigation: uncomment this and the imports above, with the ref you pass to",
|
|
2149
|
+
" // <NavigationContainer ref={\u2026}> (createNavigationContainerRef from @react-navigation/native).",
|
|
2150
|
+
" // ...navTools(navigationRef),"
|
|
1602
2151
|
);
|
|
1603
|
-
}
|
|
1604
2152
|
L.push(
|
|
1605
2153
|
` // Your own tools: any function, JSON in and out, e.g. 'cart.clear': () => clearCart(),`,
|
|
1606
2154
|
" },",
|
|
@@ -1621,12 +2169,11 @@ function renderSmokeFlow(o) {
|
|
|
1621
2169
|
`// Generated by agent-bridge init ${o.version}. Run it with:`,
|
|
1622
2170
|
`// ${o.bin} run flows/smoke.flow.mjs`,
|
|
1623
2171
|
`/** @type {import('@avasapp/agent-bridge/client').FlowModule['default']} */`,
|
|
1624
|
-
"const smoke = async ({ step }) => {",
|
|
2172
|
+
"const smoke = async ({ step, expect }) => {",
|
|
1625
2173
|
` await step('snapshot', 'screen.snapshot')`,
|
|
1626
2174
|
...o.expoRouter ? [` await step('open home', 'router.navigate', '/')`] : [],
|
|
1627
|
-
" // TODO:
|
|
1628
|
-
` // await
|
|
1629
|
-
` await step('undo', 'bridge.restore')`,
|
|
2175
|
+
" // TODO: check text your first screen always shows, for example:",
|
|
2176
|
+
` // await expect.text('Welcome')`,
|
|
1630
2177
|
"}",
|
|
1631
2178
|
"",
|
|
1632
2179
|
"export default smoke",
|
|
@@ -1648,32 +2195,32 @@ function stamp(content) {
|
|
|
1648
2195
|
const nl = content.indexOf("\n");
|
|
1649
2196
|
return content.slice(0, nl).replace(HEADER, `$1 (#${hash(content.slice(nl))}).`) + content.slice(nl);
|
|
1650
2197
|
}
|
|
1651
|
-
function isPristine(
|
|
1652
|
-
const nl =
|
|
1653
|
-
const recorded =
|
|
1654
|
-
return !!recorded && recorded === hash(
|
|
2198
|
+
function isPristine(text2) {
|
|
2199
|
+
const nl = text2.indexOf("\n");
|
|
2200
|
+
const recorded = text2.slice(0, nl).match(HEADER)?.[3];
|
|
2201
|
+
return !!recorded && recorded === hash(text2.slice(nl));
|
|
1655
2202
|
}
|
|
1656
2203
|
var NETWORK_LINES = [
|
|
1657
2204
|
`import { networkTools } from '@avasapp/agent-bridge/network'`,
|
|
1658
2205
|
"...networkTools(), // in useAgentBridge({ tools })"
|
|
1659
2206
|
];
|
|
1660
|
-
var indentOf = (
|
|
1661
|
-
function patchJson(
|
|
2207
|
+
var indentOf = (text2) => text2.match(/^([ \t]+)"/m)?.[1] ?? " ";
|
|
2208
|
+
function patchJson(text2, edit, manual) {
|
|
1662
2209
|
let json;
|
|
1663
2210
|
try {
|
|
1664
|
-
json = JSON.parse(
|
|
2211
|
+
json = JSON.parse(text2);
|
|
1665
2212
|
} catch {
|
|
1666
2213
|
return { manual };
|
|
1667
2214
|
}
|
|
1668
2215
|
if (!json || typeof json !== "object" || Array.isArray(json)) return { manual };
|
|
1669
2216
|
if (!edit(json)) return { unchanged: true };
|
|
1670
2217
|
return {
|
|
1671
|
-
text: JSON.stringify(json, null, indentOf(
|
|
2218
|
+
text: JSON.stringify(json, null, indentOf(text2)) + (text2.endsWith("\n") ? "\n" : "")
|
|
1672
2219
|
};
|
|
1673
2220
|
}
|
|
1674
|
-
function patchPackageScripts(
|
|
2221
|
+
function patchPackageScripts(text2) {
|
|
1675
2222
|
return patchJson(
|
|
1676
|
-
|
|
2223
|
+
text2,
|
|
1677
2224
|
(pkg) => {
|
|
1678
2225
|
if (pkg.scripts?.flow !== void 0) return false;
|
|
1679
2226
|
pkg.scripts = { ...pkg.scripts, flow: "agent-bridge run" };
|
|
@@ -1690,9 +2237,9 @@ function includesFlows(pattern) {
|
|
|
1690
2237
|
const glob = last.includes(".") ? pattern : `${pattern.replace(/\/+$/, "")}${last === "*" ? "" : "/**/*"}`;
|
|
1691
2238
|
return globRe(glob).test("flows/smoke.flow.mjs");
|
|
1692
2239
|
}
|
|
1693
|
-
function patchTsconfig(
|
|
2240
|
+
function patchTsconfig(text2, baseAllowJs) {
|
|
1694
2241
|
const manual = `Add "${FLOW_GLOB}" to "exclude" in tsconfig.json`;
|
|
1695
|
-
const json = parseJsonc(
|
|
2242
|
+
const json = parseJsonc(text2);
|
|
1696
2243
|
if (!json || typeof json !== "object") return { manual };
|
|
1697
2244
|
if (Array.isArray(json.exclude) && json.exclude.includes(FLOW_GLOB)) return { unchanged: true };
|
|
1698
2245
|
const allowJs = json.compilerOptions?.allowJs ?? baseAllowJs ?? true;
|
|
@@ -1703,25 +2250,25 @@ function patchTsconfig(text, baseAllowJs) {
|
|
|
1703
2250
|
return {
|
|
1704
2251
|
manual: `${manual}, along with what it inherits from ${JSON.stringify(json.extends)}: an "exclude" here replaces the inherited one`
|
|
1705
2252
|
};
|
|
1706
|
-
const patched = addToArray(
|
|
2253
|
+
const patched = addToArray(text2, "exclude", FLOW_GLOB, ["node_modules"]);
|
|
1707
2254
|
return patched ? { text: patched } : { manual };
|
|
1708
2255
|
}
|
|
1709
|
-
function patchOxlint(
|
|
1710
|
-
const ignore = parseJsonc(
|
|
2256
|
+
function patchOxlint(text2) {
|
|
2257
|
+
const ignore = parseJsonc(text2)?.ignorePatterns;
|
|
1711
2258
|
if (Array.isArray(ignore) && ignore.includes(FLOW_GLOB)) return { unchanged: true };
|
|
1712
|
-
const patched = addToArray(
|
|
2259
|
+
const patched = addToArray(text2, "ignorePatterns", FLOW_GLOB);
|
|
1713
2260
|
return patched ? { text: patched } : { manual: `Add "${FLOW_GLOB}" to "ignorePatterns" in .oxlintrc.json` };
|
|
1714
2261
|
}
|
|
1715
2262
|
var ESLINT_SNIPPET = `{ ignores: ['${FLOW_GLOB}'] },`;
|
|
1716
|
-
function patchEslint(
|
|
1717
|
-
if (
|
|
2263
|
+
function patchEslint(text2) {
|
|
2264
|
+
if (text2.includes(FLOW_GLOB)) return { unchanged: true };
|
|
1718
2265
|
const manual = `Add to the start of your ESLint flat config array: ${ESLINT_SNIPPET}`;
|
|
1719
2266
|
const re = /^((?:module\.exports\s*=|export\s+default)\s*(?:defineConfig\(\s*)?\[)/gm;
|
|
1720
|
-
const matches = [...
|
|
2267
|
+
const matches = [...text2.matchAll(re)];
|
|
1721
2268
|
if (matches.length !== 1) return { manual };
|
|
1722
2269
|
const at = matches[0].index + matches[0][1].length;
|
|
1723
|
-
return { text: `${
|
|
1724
|
-
${ESLINT_SNIPPET}${
|
|
2270
|
+
return { text: `${text2.slice(0, at)}
|
|
2271
|
+
${ESLINT_SNIPPET}${text2.slice(at)}` };
|
|
1725
2272
|
}
|
|
1726
2273
|
|
|
1727
2274
|
// src/init/plan.ts
|
|
@@ -1737,27 +2284,32 @@ var MARKERS = [
|
|
|
1737
2284
|
["reactQuery", /queryTools/, "@tanstack/react-query"],
|
|
1738
2285
|
["webview", /webviewTools/, "react-native-webview"],
|
|
1739
2286
|
["zustand", /storeTools/, "zustand"],
|
|
2287
|
+
["redux", /reduxTools/, "@reduxjs/toolkit"],
|
|
2288
|
+
["reduxCore", /reduxTools/, "redux"],
|
|
2289
|
+
["jotai", /jotaiTools/, "jotai"],
|
|
1740
2290
|
["mmkv", /mmkvTools/, "react-native-mmkv"],
|
|
2291
|
+
["asyncStorage", /asyncStorageTools/, "@react-native-async-storage/async-storage"],
|
|
2292
|
+
["secureStore", /secureStoreTools/, "expo-secure-store"],
|
|
1741
2293
|
["socketIo", /realtimeDevTools|socketIoTools|realtime/i, "socket.io-client"],
|
|
1742
2294
|
["ably", /realtimeDevTools|ablyTools|realtime/i, "ably"]
|
|
1743
2295
|
];
|
|
1744
|
-
var
|
|
2296
|
+
var posix2 = (p) => p.split(sep).join("/");
|
|
1745
2297
|
function importPath(fromFile, toFile) {
|
|
1746
|
-
const rel =
|
|
2298
|
+
const rel = posix2(relative4(dirname5(fromFile), toFile)).replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
1747
2299
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1748
2300
|
}
|
|
1749
2301
|
function binFor(d) {
|
|
1750
2302
|
if (d.bin) {
|
|
1751
|
-
const rel =
|
|
2303
|
+
const rel = posix2(relative4(d.dir, d.bin));
|
|
1752
2304
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1753
2305
|
}
|
|
1754
2306
|
if (d.pm === "npm" && d.root !== d.dir)
|
|
1755
|
-
return `${
|
|
2307
|
+
return `${posix2(relative4(d.dir, d.root))}/node_modules/.bin/agent-bridge`;
|
|
1756
2308
|
return binFallback(d.pm);
|
|
1757
2309
|
}
|
|
1758
2310
|
function bridgePath(d, out) {
|
|
1759
|
-
if (out) return isAbsolute(out) ? out :
|
|
1760
|
-
return
|
|
2311
|
+
if (out) return isAbsolute(out) ? out : join7(d.dir, out);
|
|
2312
|
+
return join7(d.dir, d.srcDir ? "src/dev" : "dev", `agent-bridge.${d.typescript ? "tsx" : "js"}`);
|
|
1761
2313
|
}
|
|
1762
2314
|
function nextSteps(bin, o) {
|
|
1763
2315
|
const { offline, runFrom } = o;
|
|
@@ -1784,13 +2336,13 @@ function nextSteps(bin, o) {
|
|
|
1784
2336
|
var stripExt = (p) => p.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "").replace(/\/index$/, "");
|
|
1785
2337
|
function readTsconfig(d, fs) {
|
|
1786
2338
|
if (!d.tsconfig) return { aliases: [] };
|
|
1787
|
-
const file =
|
|
2339
|
+
const file = join7(d.dir, d.tsconfig);
|
|
1788
2340
|
const json = parseJsonc(fs.read(file) ?? "");
|
|
1789
2341
|
if (!json) return { aliases: [] };
|
|
1790
2342
|
const findExtended = (from2, ext2) => {
|
|
1791
|
-
const
|
|
1792
|
-
const roots = ext2.startsWith(".") ? [
|
|
1793
|
-
for (const r of roots) for (const n of
|
|
2343
|
+
const names2 = ext2.endsWith(".json") ? [ext2] : [ext2, `${ext2}.json`];
|
|
2344
|
+
const roots = ext2.startsWith(".") ? [dirname5(from2)] : ancestors(dirname5(from2), d.repoRoot).map((a) => join7(a, "node_modules"));
|
|
2345
|
+
for (const r of roots) for (const n of names2) if (fs.exists(join7(r, n)) && !fs.list(join7(r, n)).length) return join7(r, n);
|
|
1794
2346
|
return void 0;
|
|
1795
2347
|
};
|
|
1796
2348
|
let baseAllowJs;
|
|
@@ -1807,30 +2359,30 @@ function readTsconfig(d, fs) {
|
|
|
1807
2359
|
ext = base.extends;
|
|
1808
2360
|
from = path;
|
|
1809
2361
|
}
|
|
1810
|
-
const baseDir =
|
|
2362
|
+
const baseDir = join7(d.dir, json.compilerOptions?.baseUrl ?? ".");
|
|
1811
2363
|
const paths = json.compilerOptions?.paths ?? {};
|
|
1812
2364
|
const aliases = Object.entries(paths).flatMap(
|
|
1813
|
-
([pattern, targets]) => Array.isArray(targets) && typeof targets[0] === "string" ? [[pattern.replace(/\*$/, ""),
|
|
2365
|
+
([pattern, targets]) => Array.isArray(targets) && typeof targets[0] === "string" ? [[pattern.replace(/\*$/, ""), join7(baseDir, targets[0].replace(/\*$/, ""))]] : []
|
|
1814
2366
|
);
|
|
1815
2367
|
return { json, baseAllowJs, aliases };
|
|
1816
2368
|
}
|
|
1817
2369
|
function ancestors(dir, stop2) {
|
|
1818
2370
|
const out = [];
|
|
1819
|
-
for (let d = dir; ; d =
|
|
2371
|
+
for (let d = dir; ; d = dirname5(d)) {
|
|
1820
2372
|
out.push(d);
|
|
1821
|
-
if (d === stop2 ||
|
|
2373
|
+
if (d === stop2 || dirname5(d) === d) return out;
|
|
1822
2374
|
}
|
|
1823
2375
|
}
|
|
1824
|
-
function importedFiles(file,
|
|
1825
|
-
const specs = [...
|
|
2376
|
+
function importedFiles(file, text2, aliases) {
|
|
2377
|
+
const specs = [...text2.matchAll(/(?:from\s+|import\s+|require\(\s*)['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
1826
2378
|
return specs.flatMap((spec) => {
|
|
1827
|
-
if (spec.startsWith(".")) return [stripExt(
|
|
2379
|
+
if (spec.startsWith(".")) return [stripExt(join7(dirname5(file), spec))];
|
|
1828
2380
|
const alias = aliases.find(([prefix]) => prefix && spec.startsWith(prefix));
|
|
1829
|
-
return alias ? [stripExt(
|
|
2381
|
+
return alias ? [stripExt(join7(alias[1], spec.slice(alias[0].length)))] : [];
|
|
1830
2382
|
});
|
|
1831
2383
|
}
|
|
1832
2384
|
function plan(d, found, fs, o, cwd) {
|
|
1833
|
-
const rel = (p) =>
|
|
2385
|
+
const rel = (p) => posix2(relative4(d.dir, p));
|
|
1834
2386
|
const out = bridgePath(d, o.out);
|
|
1835
2387
|
if (rel(out).startsWith("..") || isAbsolute(rel(out)))
|
|
1836
2388
|
return { error: `--out ${o.out} is outside the app (${d.dir}).` };
|
|
@@ -1840,7 +2392,7 @@ function plan(d, found, fs, o, cwd) {
|
|
|
1840
2392
|
};
|
|
1841
2393
|
const ts = /\.tsx?$/.test(out);
|
|
1842
2394
|
const bin = binFor(d);
|
|
1843
|
-
const runFrom =
|
|
2395
|
+
const runFrom = posix2(relative4(cwd, d.dir)) || ".";
|
|
1844
2396
|
const tsconfig = readTsconfig(d, fs);
|
|
1845
2397
|
const actions = [];
|
|
1846
2398
|
const files = [];
|
|
@@ -1877,14 +2429,14 @@ function plan(d, found, fs, o, cwd) {
|
|
|
1877
2429
|
files.push({ path: `${rel(path)}.new`, status: "written" });
|
|
1878
2430
|
return "edited";
|
|
1879
2431
|
};
|
|
1880
|
-
const imported = (list2) => list2.map((f) => ({ name: f.name, from: importPath(out,
|
|
2432
|
+
const imported = (list2) => list2.map((f) => ({ name: f.name, from: importPath(out, join7(d.dir, f.file)) }));
|
|
1881
2433
|
const realtimeTools = imported(found.realtimeTools);
|
|
1882
2434
|
const realtime = realtimeTools.length ? [] : [
|
|
1883
2435
|
...d.has.socketIo ? found.socketIo.length ? found.socketIo.map((f) => ({ kind: "socket.io", ...f })) : [{ kind: "socket.io" }] : [],
|
|
1884
2436
|
...d.has.ably ? found.ably.length ? found.ably.map((f) => ({ kind: "ably", ...f })) : [{ kind: "ably" }] : []
|
|
1885
2437
|
];
|
|
1886
2438
|
const existingText = fs.read(out);
|
|
1887
|
-
const existing = found.bridgeUsers.filter((f) => stripExt(
|
|
2439
|
+
const existing = found.bridgeUsers.filter((f) => stripExt(join7(d.dir, f)) !== stripExt(out));
|
|
1888
2440
|
const elsewhere = existingText === void 0 && existing.length > 0 && !o.out;
|
|
1889
2441
|
let state = "elsewhere";
|
|
1890
2442
|
if (elsewhere) {
|
|
@@ -1908,13 +2460,17 @@ function plan(d, found, fs, o, cwd) {
|
|
|
1908
2460
|
reactNavigation: d.has.reactNavigation && !d.has.expoRouter,
|
|
1909
2461
|
stores: d.has.zustand ? imported(found.stores) : void 0,
|
|
1910
2462
|
mmkv: d.has.mmkv ? imported(found.mmkv) : void 0,
|
|
2463
|
+
asyncStorage: d.has.asyncStorage,
|
|
2464
|
+
secureStore: d.has.secureStore,
|
|
2465
|
+
redux: d.has.redux || d.has.reduxCore,
|
|
2466
|
+
jotai: d.has.jotai,
|
|
1911
2467
|
realtime,
|
|
1912
2468
|
realtimeTools
|
|
1913
2469
|
})
|
|
1914
2470
|
);
|
|
1915
2471
|
}
|
|
1916
2472
|
const wrote = state === "written";
|
|
1917
|
-
const liveText = wrote ? void 0 : elsewhere ? existing.map((f) => fs.read(
|
|
2473
|
+
const liveText = wrote ? void 0 : elsewhere ? existing.map((f) => fs.read(join7(d.dir, f)) ?? "").join("\n") : existingText;
|
|
1918
2474
|
const network = wrote ? o.network.enabled : /networkTools\s*\(/.test(liveText ?? "");
|
|
1919
2475
|
const offline = wrote ? o.network.enabled : /\boffline\s*:/.test(liveText ?? "");
|
|
1920
2476
|
if (wrote) {
|
|
@@ -1930,21 +2486,45 @@ function plan(d, found, fs, o, cwd) {
|
|
|
1930
2486
|
items: found.mmkv.map((s) => `${s.name} (${s.file})`),
|
|
1931
2487
|
why: `${MMKV_WHY} (${DOCS}/adapters.md)`
|
|
1932
2488
|
});
|
|
2489
|
+
if (d.has.asyncStorage)
|
|
2490
|
+
commented.push({
|
|
2491
|
+
adapter: "async-storage",
|
|
2492
|
+
items: ["AsyncStorage"],
|
|
2493
|
+
why: `${ASYNC_STORAGE_WHY} (${DOCS}/adapters.md)`
|
|
2494
|
+
});
|
|
2495
|
+
if (d.has.secureStore)
|
|
2496
|
+
commented.push({
|
|
2497
|
+
adapter: "secure-store",
|
|
2498
|
+
items: ["SecureStore"],
|
|
2499
|
+
why: `${SECURE_STORE_WHY} (${DOCS}/adapters.md)`
|
|
2500
|
+
});
|
|
2501
|
+
if (d.has.redux || d.has.reduxCore)
|
|
2502
|
+
commented.push({
|
|
2503
|
+
adapter: "redux",
|
|
2504
|
+
items: ["reduxTools(store)"],
|
|
2505
|
+
why: `${REDUX_WHY} (${DOCS}/adapters.md)`
|
|
2506
|
+
});
|
|
2507
|
+
if (d.has.jotai)
|
|
2508
|
+
commented.push({
|
|
2509
|
+
adapter: "jotai",
|
|
2510
|
+
items: ["jotaiTools(store, atoms)"],
|
|
2511
|
+
why: `${JOTAI_WHY} (${DOCS}/adapters.md)`
|
|
2512
|
+
});
|
|
1933
2513
|
if (d.has.reactNavigation && !d.has.expoRouter)
|
|
1934
2514
|
commented.push({
|
|
1935
2515
|
adapter: "@react-navigation/native",
|
|
1936
|
-
items: ["
|
|
1937
|
-
why:
|
|
2516
|
+
items: ["navTools(navigationRef)"],
|
|
2517
|
+
why: `navTools needs the container ref you pass to <NavigationContainer> (${DOCS}/adapters.md)`
|
|
1938
2518
|
});
|
|
1939
2519
|
for (const t of realtime) todos.push(`${realtimeTodoText(t)}: ${REALTIME_WHY} (${DOCS}/realtime.md).`);
|
|
1940
2520
|
}
|
|
1941
2521
|
const missing = !wrote && liveText !== void 0 ? MARKERS.filter(([lib, marker]) => d.has[lib] && !marker.test(liveText)).map(([, , dep]) => dep) : [];
|
|
1942
|
-
const flows =
|
|
2522
|
+
const flows = join7(d.dir, "flows");
|
|
1943
2523
|
generate(
|
|
1944
|
-
|
|
2524
|
+
join7(flows, "smoke.flow.mjs"),
|
|
1945
2525
|
renderSmokeFlow({ version: o.version, expoRouter: d.has.expoRouter, bin })
|
|
1946
2526
|
);
|
|
1947
|
-
const readme =
|
|
2527
|
+
const readme = join7(flows, "README.md");
|
|
1948
2528
|
if (fs.exists(readme)) files.push({ path: rel(readme), status: "skipped", note: "exists" });
|
|
1949
2529
|
else {
|
|
1950
2530
|
actions.push({ kind: "write", path: readme, content: renderFlowsReadme(bin) });
|
|
@@ -1952,7 +2532,7 @@ function plan(d, found, fs, o, cwd) {
|
|
|
1952
2532
|
}
|
|
1953
2533
|
const patch = (path, fn) => {
|
|
1954
2534
|
if (!path) return;
|
|
1955
|
-
const abs =
|
|
2535
|
+
const abs = join7(d.dir, path);
|
|
1956
2536
|
const result = fn(fs.read(abs) ?? "");
|
|
1957
2537
|
if ("text" in result) {
|
|
1958
2538
|
actions.push({ kind: "patch", path: abs, patch: fn });
|
|
@@ -1964,15 +2544,15 @@ function plan(d, found, fs, o, cwd) {
|
|
|
1964
2544
|
}
|
|
1965
2545
|
};
|
|
1966
2546
|
patch("package.json", patchPackageScripts);
|
|
1967
|
-
patch(d.tsconfig, (
|
|
2547
|
+
patch(d.tsconfig, (text2) => patchTsconfig(text2, tsconfig.baseAllowJs));
|
|
1968
2548
|
patch(d.oxlint, patchOxlint);
|
|
1969
2549
|
patch(d.eslint, patchEslint);
|
|
1970
2550
|
if (d.agentsFile) {
|
|
1971
|
-
const
|
|
1972
|
-
const path =
|
|
1973
|
-
if (
|
|
2551
|
+
const text2 = fs.read(d.agentsFile) ?? "";
|
|
2552
|
+
const path = posix2(relative4(d.dir, d.agentsFile));
|
|
2553
|
+
if (text2.includes(".flow.mjs")) files.push({ path, status: "unchanged" });
|
|
1974
2554
|
else {
|
|
1975
|
-
const appDir =
|
|
2555
|
+
const appDir = posix2(relative4(dirname5(d.agentsFile), d.dir));
|
|
1976
2556
|
actions.push({ kind: "append", path: d.agentsFile, line: agentsLine(appDir, bin) });
|
|
1977
2557
|
files.push({ path, status: "appended" });
|
|
1978
2558
|
}
|
|
@@ -1982,11 +2562,11 @@ function plan(d, found, fs, o, cwd) {
|
|
|
1982
2562
|
if (!d.installed && o.install) actions.unshift({ kind: "install", command: install, cwd: d.dir });
|
|
1983
2563
|
const skill = o.skillYes ? [...SKILL_RUN, "-y"] : SKILL_RUN;
|
|
1984
2564
|
if (o.skill) actions.push({ kind: "skill", command: skill, cwd: d.repoRoot });
|
|
1985
|
-
const rootFile = d.rootFile ?
|
|
2565
|
+
const rootFile = d.rootFile ? join7(d.dir, d.rootFile) : void 0;
|
|
1986
2566
|
const rootImports = rootFile ? importedFiles(rootFile, fs.read(rootFile) ?? "", tsconfig.aliases) : [];
|
|
1987
2567
|
const reachedFromRoot = (file) => stripExt(file) === stripExt(rootFile ?? "") || rootImports.includes(stripExt(file));
|
|
1988
|
-
const mounted = elsewhere ? existing.some((f) => reachedFromRoot(
|
|
1989
|
-
const snippet = renderMountSnippet(importPath(rootFile ??
|
|
2568
|
+
const mounted = elsewhere ? existing.some((f) => reachedFromRoot(join7(d.dir, f))) : rootImports.includes(stripExt(out));
|
|
2569
|
+
const snippet = renderMountSnippet(importPath(rootFile ?? join7(d.dir, "App.tsx"), out));
|
|
1990
2570
|
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
2571
|
const note = `${where}${d.has.reactQuery && !elsewhere ? ", inside QueryClientProvider so query.* tools work" : ""}.`;
|
|
1992
2572
|
const regenerable = state === "written" || state === "kept";
|
|
@@ -2028,12 +2608,12 @@ function plan(d, found, fs, o, cwd) {
|
|
|
2028
2608
|
}
|
|
2029
2609
|
|
|
2030
2610
|
// src/init/scan.ts
|
|
2031
|
-
import { join as
|
|
2611
|
+
import { join as join8, relative as relative5 } from "path";
|
|
2032
2612
|
var CODE = /\.(tsx?|jsx?|mjs|cjs)$/;
|
|
2033
2613
|
var TESTS = /\.(test|spec)\.|(^|\/)__tests__\//;
|
|
2034
|
-
var imports = (
|
|
2614
|
+
var imports = (text2, module) => new RegExp(
|
|
2035
2615
|
`(from\\s+|require\\(\\s*)['"]${module.replace(/[.]/g, "\\.")}(/[\\w./-]+)?['"]`
|
|
2036
|
-
).test(
|
|
2616
|
+
).test(text2);
|
|
2037
2617
|
var TYPED = "(?:\\s*:[^=]+)?";
|
|
2038
2618
|
var PATTERNS = [
|
|
2039
2619
|
["stores", "zustand", new RegExp(`export\\s+const\\s+(use\\w+)${TYPED}\\s*=\\s*create\\w*\\s*[<(]`, "g")],
|
|
@@ -2044,31 +2624,31 @@ var PATTERNS = [
|
|
|
2044
2624
|
];
|
|
2045
2625
|
function scan(dir, fs, { maxFiles = 2e3, maxBytes = 512 * 1024 } = {}) {
|
|
2046
2626
|
const files = [];
|
|
2047
|
-
const
|
|
2627
|
+
const walk2 = (d, deep) => {
|
|
2048
2628
|
const entries = fs.list(d).sort((a, b) => a.name < b.name ? -1 : 1);
|
|
2049
2629
|
for (const e of entries) {
|
|
2050
2630
|
if (files.length >= maxFiles) return;
|
|
2051
2631
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
2052
|
-
const path =
|
|
2632
|
+
const path = join8(d, e.name);
|
|
2053
2633
|
if (e.dir) {
|
|
2054
|
-
if (deep)
|
|
2055
|
-
} else if (CODE.test(e.name) && !TESTS.test(
|
|
2634
|
+
if (deep) walk2(path, true);
|
|
2635
|
+
} else if (CODE.test(e.name) && !TESTS.test(relative5(dir, path))) {
|
|
2056
2636
|
files.push(path);
|
|
2057
2637
|
}
|
|
2058
2638
|
}
|
|
2059
2639
|
};
|
|
2060
|
-
|
|
2061
|
-
for (const sub of ["src", "app", "lib"])
|
|
2640
|
+
walk2(dir, false);
|
|
2641
|
+
for (const sub of ["src", "app", "lib"]) walk2(join8(dir, sub), true);
|
|
2062
2642
|
const result = { stores: [], mmkv: [], socketIo: [], ably: [], realtimeTools: [], bridgeUsers: [] };
|
|
2063
2643
|
for (const path of files) {
|
|
2064
|
-
const
|
|
2065
|
-
if (!
|
|
2066
|
-
if (/\buseAgentBridge\s*\(|(from\s+|require\(\s*)['"]@avasapp\/agent-bridge['"]/.test(
|
|
2067
|
-
result.bridgeUsers.push(
|
|
2644
|
+
const text2 = fs.read(path);
|
|
2645
|
+
if (!text2 || text2.length > maxBytes) continue;
|
|
2646
|
+
if (/\buseAgentBridge\s*\(|(from\s+|require\(\s*)['"]@avasapp\/agent-bridge['"]/.test(text2))
|
|
2647
|
+
result.bridgeUsers.push(relative5(dir, path));
|
|
2068
2648
|
for (const [key, module, re] of PATTERNS) {
|
|
2069
|
-
if (!imports(
|
|
2070
|
-
for (const m of
|
|
2071
|
-
result[key].push({ name: m[1], file:
|
|
2649
|
+
if (!imports(text2, module)) continue;
|
|
2650
|
+
for (const m of text2.matchAll(re))
|
|
2651
|
+
result[key].push({ name: m[1], file: relative5(dir, path) });
|
|
2072
2652
|
}
|
|
2073
2653
|
}
|
|
2074
2654
|
return result;
|
|
@@ -2077,7 +2657,7 @@ function scan(dir, fs, { maxFiles = 2e3, maxBytes = 512 * 1024 } = {}) {
|
|
|
2077
2657
|
// src/init/index.ts
|
|
2078
2658
|
var INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
2079
2659
|
var SKILL_TIMEOUT_MS = 2 * 6e4;
|
|
2080
|
-
var
|
|
2660
|
+
var posix3 = (p) => p.split(sep2).join("/");
|
|
2081
2661
|
var nodeFs = {
|
|
2082
2662
|
exists: existsSync2,
|
|
2083
2663
|
read: (path) => {
|
|
@@ -2109,10 +2689,10 @@ function nodeIo() {
|
|
|
2109
2689
|
return {
|
|
2110
2690
|
fs: nodeFs,
|
|
2111
2691
|
write: (path, content) => {
|
|
2112
|
-
mkdirSync2(
|
|
2692
|
+
mkdirSync2(dirname6(path), { recursive: true });
|
|
2113
2693
|
writeFileSync2(path, content);
|
|
2114
2694
|
},
|
|
2115
|
-
append: (path,
|
|
2695
|
+
append: (path, text2) => appendFileSync3(path, text2),
|
|
2116
2696
|
exec: (command, cwd, interactive, timeoutMs) => new Promise((done) => {
|
|
2117
2697
|
const [cmd, ...args] = command;
|
|
2118
2698
|
const child = spawn2(cmd, args, {
|
|
@@ -2141,9 +2721,9 @@ function nodeIo() {
|
|
|
2141
2721
|
rl.close();
|
|
2142
2722
|
}
|
|
2143
2723
|
},
|
|
2144
|
-
print: (
|
|
2724
|
+
print: (text2) => process.stdout.write(`${text2}
|
|
2145
2725
|
`),
|
|
2146
|
-
warn: (
|
|
2726
|
+
warn: (text2) => process.stderr.write(`${text2}
|
|
2147
2727
|
`),
|
|
2148
2728
|
interactive: !!process.stdin.isTTY && !!process.stdout.isTTY && !/^(1|true)$/i.test(process.env.CI ?? ""),
|
|
2149
2729
|
cwd: process.cwd(),
|
|
@@ -2182,8 +2762,8 @@ async function apply(actions, report, io, interactive) {
|
|
|
2182
2762
|
break;
|
|
2183
2763
|
}
|
|
2184
2764
|
case "append": {
|
|
2185
|
-
const
|
|
2186
|
-
io.append(a.path, `${
|
|
2765
|
+
const text2 = io.fs.read(a.path) ?? "";
|
|
2766
|
+
io.append(a.path, `${text2.endsWith("\n") || !text2 ? "" : "\n"}${a.line}
|
|
2187
2767
|
`);
|
|
2188
2768
|
break;
|
|
2189
2769
|
}
|
|
@@ -2191,7 +2771,7 @@ async function apply(actions, report, io, interactive) {
|
|
|
2191
2771
|
}
|
|
2192
2772
|
}
|
|
2193
2773
|
var describe = (a, dir) => {
|
|
2194
|
-
const rel = (p) =>
|
|
2774
|
+
const rel = (p) => relative6(dir, p) || ".";
|
|
2195
2775
|
switch (a.kind) {
|
|
2196
2776
|
case "install":
|
|
2197
2777
|
return `install ${a.command.join(" ")}`;
|
|
@@ -2217,7 +2797,7 @@ function formatReport(r, actions) {
|
|
|
2217
2797
|
L.push("Would run:", ...actions.map((a) => ` ${describe(a, r.app.dir)}`), "");
|
|
2218
2798
|
for (const a of actions)
|
|
2219
2799
|
if (a.kind === "write")
|
|
2220
|
-
L.push(`--- ${
|
|
2800
|
+
L.push(`--- ${relative6(r.app.dir, a.path)}`, a.content.trimEnd(), "");
|
|
2221
2801
|
}
|
|
2222
2802
|
L.push("Files");
|
|
2223
2803
|
for (const f of r.files)
|
|
@@ -2263,7 +2843,7 @@ function formatReport(r, actions) {
|
|
|
2263
2843
|
return L.join("\n");
|
|
2264
2844
|
}
|
|
2265
2845
|
async function runInit(args, io) {
|
|
2266
|
-
const dir =
|
|
2846
|
+
const dir = resolve2(io.cwd, args.dir ?? ".");
|
|
2267
2847
|
const fail = (error) => {
|
|
2268
2848
|
if (args.json) io.print(JSON.stringify({ ok: false, ...error }, null, 2));
|
|
2269
2849
|
else io.warn(error.error);
|
|
@@ -2274,7 +2854,7 @@ async function runInit(args, io) {
|
|
|
2274
2854
|
const interactive = io.interactive && !args.json && !args.yes;
|
|
2275
2855
|
const options = (network2) => ({
|
|
2276
2856
|
version,
|
|
2277
|
-
out: args.out === void 0 ? void 0 :
|
|
2857
|
+
out: args.out === void 0 ? void 0 : resolve2(io.cwd, args.out),
|
|
2278
2858
|
force: args.force,
|
|
2279
2859
|
install: args.install !== false,
|
|
2280
2860
|
skill: args.skill !== false,
|
|
@@ -2290,13 +2870,13 @@ async function runInit(args, io) {
|
|
|
2290
2870
|
report2.dryRun = true;
|
|
2291
2871
|
if (args.json) {
|
|
2292
2872
|
const contents = Object.fromEntries(
|
|
2293
|
-
actions2.flatMap((a) => a.kind === "write" ? [[
|
|
2873
|
+
actions2.flatMap((a) => a.kind === "write" ? [[relative6(dir, a.path), a.content]] : [])
|
|
2294
2874
|
);
|
|
2295
2875
|
io.print(JSON.stringify({ ...report2, contents }, null, 2));
|
|
2296
2876
|
} else io.print(formatReport(report2, actions2));
|
|
2297
2877
|
return 0;
|
|
2298
2878
|
}
|
|
2299
|
-
const bridge =
|
|
2879
|
+
const bridge = posix3(relative6(dir, bridgePath(d, options(network).out)));
|
|
2300
2880
|
const writesBridge = first.report.files.some((f) => f.path === bridge && f.status === "written");
|
|
2301
2881
|
if (!network.chosen && interactive && writesBridge) {
|
|
2302
2882
|
const answer = await io.prompt(`${NETWORK_PROMPT} [y/N] `);
|
|
@@ -2321,6 +2901,439 @@ async function runInit(args, io) {
|
|
|
2321
2901
|
return report.ok ? 0 : 1;
|
|
2322
2902
|
}
|
|
2323
2903
|
|
|
2904
|
+
// src/mcp/server.ts
|
|
2905
|
+
import { createInterface as createInterface5 } from "readline";
|
|
2906
|
+
var SUPPORTED = ["2025-06-18", "2024-11-05"];
|
|
2907
|
+
var STATUS_TOOL = "agent_bridge_status";
|
|
2908
|
+
var MAX_NAME = 64;
|
|
2909
|
+
function mcpNames(tools, reserved = []) {
|
|
2910
|
+
const out = /* @__PURE__ */ new Map();
|
|
2911
|
+
const taken = new Set(reserved);
|
|
2912
|
+
const sorted = [...tools].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
2913
|
+
for (const { name } of sorted) {
|
|
2914
|
+
const base = name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, MAX_NAME - 4) || "_";
|
|
2915
|
+
let mcp = base;
|
|
2916
|
+
for (let n = 2; taken.has(mcp); n++) mcp = `${base}_${n}`;
|
|
2917
|
+
taken.add(mcp);
|
|
2918
|
+
out.set(mcp, name);
|
|
2919
|
+
}
|
|
2920
|
+
return out;
|
|
2921
|
+
}
|
|
2922
|
+
var INPUT_SCHEMA = {
|
|
2923
|
+
type: "object",
|
|
2924
|
+
properties: {
|
|
2925
|
+
args: {
|
|
2926
|
+
type: "array",
|
|
2927
|
+
items: {},
|
|
2928
|
+
description: "The tool's arguments, in order (each any JSON value). Leave out for none."
|
|
2929
|
+
},
|
|
2930
|
+
full: {
|
|
2931
|
+
type: "boolean",
|
|
2932
|
+
description: `Return a result over ${OUTPUT_LIMIT_BYTES / 1024} KB whole instead of a summary of its size and shape.`
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2935
|
+
additionalProperties: false
|
|
2936
|
+
};
|
|
2937
|
+
var text = (t) => ({ type: "text", text: t });
|
|
2938
|
+
var logBlock = (logs) => logs.length ? [text(`App errors:
|
|
2939
|
+
${logs.map(logLine).join("\n")}`)] : [];
|
|
2940
|
+
var IMAGE_MAGIC = [
|
|
2941
|
+
["iVBORw0KGgo", "image/png"],
|
|
2942
|
+
["/9j/", "image/jpeg"],
|
|
2943
|
+
["UklGR", "image/webp"],
|
|
2944
|
+
["R0lGOD", "image/gif"]
|
|
2945
|
+
];
|
|
2946
|
+
function withImage(tool, value) {
|
|
2947
|
+
if (tool !== "screen.capture" || !value || typeof value !== "object") return null;
|
|
2948
|
+
const { base64, ...rest } = value;
|
|
2949
|
+
if (typeof base64 !== "string" || !base64) return null;
|
|
2950
|
+
const url = /^data:([^;,]*)[^,]*,/.exec(base64);
|
|
2951
|
+
const data = url ? base64.slice(url[0].length) : base64;
|
|
2952
|
+
const mimeType = IMAGE_MAGIC.find(([magic]) => data.startsWith(magic))?.[1];
|
|
2953
|
+
if (!mimeType) return null;
|
|
2954
|
+
const image = { type: "image", data, mimeType };
|
|
2955
|
+
return Object.keys(rest).length ? [image, text(JSON.stringify(rest, null, 2))] : [image];
|
|
2956
|
+
}
|
|
2957
|
+
async function resultContent(tool, value, full) {
|
|
2958
|
+
const image = withImage(tool, value);
|
|
2959
|
+
if (image) return image;
|
|
2960
|
+
if (isSnapshotText(tool, value) && (full || Buffer.byteLength(value) <= OUTPUT_LIMIT_BYTES)) return [text(value)];
|
|
2961
|
+
const shaped = await shapedResult(value, { full });
|
|
2962
|
+
const json = JSON.stringify(shaped, null, 2) ?? "null";
|
|
2963
|
+
if (!full && shaped && typeof shaped === "object" && shaped.resultTooLarge)
|
|
2964
|
+
return [
|
|
2965
|
+
text(
|
|
2966
|
+
JSON.stringify(
|
|
2967
|
+
{
|
|
2968
|
+
...shaped,
|
|
2969
|
+
hint: "Not returned: the result is large. Call again with full: true to get it whole, or narrow the call (a path, a filter, a smaller page)."
|
|
2970
|
+
},
|
|
2971
|
+
null,
|
|
2972
|
+
2
|
|
2973
|
+
)
|
|
2974
|
+
)
|
|
2975
|
+
];
|
|
2976
|
+
return [text(json)];
|
|
2977
|
+
}
|
|
2978
|
+
var message2 = (error) => error instanceof Error ? error.message : String(error);
|
|
2979
|
+
function startMcpServer(options) {
|
|
2980
|
+
const pollMs = options.pollMs ?? 5e3;
|
|
2981
|
+
let bridge = null;
|
|
2982
|
+
let opening = null;
|
|
2983
|
+
let tools = [];
|
|
2984
|
+
let names2 = /* @__PURE__ */ new Map();
|
|
2985
|
+
let signature = "";
|
|
2986
|
+
let listed = null;
|
|
2987
|
+
let announced = "";
|
|
2988
|
+
let initialized = false;
|
|
2989
|
+
let closed = false;
|
|
2990
|
+
let inflight = 0;
|
|
2991
|
+
let lastError;
|
|
2992
|
+
const send = (message3) => {
|
|
2993
|
+
if (!closed) options.output.write(encodeLine(message3));
|
|
2994
|
+
};
|
|
2995
|
+
const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
2996
|
+
const fail = (id, code, error) => send({ jsonrpc: "2.0", id, error: { code, message: error } });
|
|
2997
|
+
const noteError = (error) => {
|
|
2998
|
+
const now = message2(error);
|
|
2999
|
+
if (now === lastError) return;
|
|
3000
|
+
lastError = now;
|
|
3001
|
+
options.log?.(`agent-bridge mcp: not connected: ${now}`);
|
|
3002
|
+
};
|
|
3003
|
+
const announce = () => {
|
|
3004
|
+
if (!initialized || listed === null || signature === listed || signature === announced)
|
|
3005
|
+
return;
|
|
3006
|
+
announced = signature;
|
|
3007
|
+
send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
3008
|
+
};
|
|
3009
|
+
const adopt = (next) => {
|
|
3010
|
+
const list2 = next.tools();
|
|
3011
|
+
const sig = JSON.stringify([next.device.deviceId, list2]);
|
|
3012
|
+
bridge = next;
|
|
3013
|
+
lastError = void 0;
|
|
3014
|
+
if (sig !== signature) {
|
|
3015
|
+
signature = sig;
|
|
3016
|
+
names2 = mcpNames(list2, [STATUS_TOOL]);
|
|
3017
|
+
tools = [...names2].map(([mcp, name]) => ({
|
|
3018
|
+
name,
|
|
3019
|
+
mcp,
|
|
3020
|
+
description: list2.find((t) => t.name === name)?.description
|
|
3021
|
+
}));
|
|
3022
|
+
announce();
|
|
3023
|
+
}
|
|
3024
|
+
};
|
|
3025
|
+
const ensure = () => {
|
|
3026
|
+
if (bridge) return Promise.resolve(bridge);
|
|
3027
|
+
opening ??= options.connect().then((next) => {
|
|
3028
|
+
adopt(next);
|
|
3029
|
+
return next;
|
|
3030
|
+
}).catch((error) => {
|
|
3031
|
+
noteError(error);
|
|
3032
|
+
throw error;
|
|
3033
|
+
}).finally(() => {
|
|
3034
|
+
opening = null;
|
|
3035
|
+
});
|
|
3036
|
+
return opening;
|
|
3037
|
+
};
|
|
3038
|
+
const drop = () => {
|
|
3039
|
+
const old = bridge;
|
|
3040
|
+
bridge = null;
|
|
3041
|
+
try {
|
|
3042
|
+
old?.close();
|
|
3043
|
+
} catch {
|
|
3044
|
+
}
|
|
3045
|
+
};
|
|
3046
|
+
let polling = false;
|
|
3047
|
+
const poll = async () => {
|
|
3048
|
+
if (polling || inflight || closed) return;
|
|
3049
|
+
polling = true;
|
|
3050
|
+
try {
|
|
3051
|
+
if (!bridge) {
|
|
3052
|
+
await ensure();
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
const next = await options.connect();
|
|
3056
|
+
if (inflight || closed) return next.close();
|
|
3057
|
+
const old = bridge;
|
|
3058
|
+
adopt(next);
|
|
3059
|
+
if (old !== next) old.close();
|
|
3060
|
+
} catch (error) {
|
|
3061
|
+
noteError(error);
|
|
3062
|
+
} finally {
|
|
3063
|
+
polling = false;
|
|
3064
|
+
}
|
|
3065
|
+
};
|
|
3066
|
+
const timer = pollMs > 0 ? setInterval(() => void poll(), pollMs) : void 0;
|
|
3067
|
+
timer?.unref?.();
|
|
3068
|
+
const statusTool = {
|
|
3069
|
+
name: STATUS_TOOL,
|
|
3070
|
+
title: "agent-bridge status",
|
|
3071
|
+
description: "Whether this server is connected to the app, which app and session, how many tools it has, and why the last connect failed. Works with no app connected.",
|
|
3072
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
3073
|
+
};
|
|
3074
|
+
const listTools = () => {
|
|
3075
|
+
listed = signature;
|
|
3076
|
+
return {
|
|
3077
|
+
tools: [
|
|
3078
|
+
...tools.map((t) => ({
|
|
3079
|
+
name: t.mcp,
|
|
3080
|
+
title: t.name,
|
|
3081
|
+
description: t.description ?? t.name,
|
|
3082
|
+
inputSchema: INPUT_SCHEMA
|
|
3083
|
+
})),
|
|
3084
|
+
statusTool
|
|
3085
|
+
]
|
|
3086
|
+
};
|
|
3087
|
+
};
|
|
3088
|
+
const statusResult = async () => {
|
|
3089
|
+
await ensure().catch(() => {
|
|
3090
|
+
});
|
|
3091
|
+
const json = {
|
|
3092
|
+
connected: !!bridge,
|
|
3093
|
+
...bridge && {
|
|
3094
|
+
app: { name: bridge.device.name, deviceId: bridge.device.deviceId },
|
|
3095
|
+
tools: tools.length
|
|
3096
|
+
},
|
|
3097
|
+
...options.sessionName?.() && { session: options.sessionName() },
|
|
3098
|
+
...lastError && { lastError }
|
|
3099
|
+
};
|
|
3100
|
+
return { content: [text(JSON.stringify(json, null, 2))] };
|
|
3101
|
+
};
|
|
3102
|
+
const callTool = async (params) => {
|
|
3103
|
+
const failure = (...content) => ({ isError: true, content });
|
|
3104
|
+
if (params.name === STATUS_TOOL) return statusResult();
|
|
3105
|
+
const notice = options.takeNotice?.();
|
|
3106
|
+
if (notice) {
|
|
3107
|
+
drop();
|
|
3108
|
+
return failure(text(notice));
|
|
3109
|
+
}
|
|
3110
|
+
inflight++;
|
|
3111
|
+
try {
|
|
3112
|
+
let b;
|
|
3113
|
+
try {
|
|
3114
|
+
b = await ensure();
|
|
3115
|
+
} catch (error) {
|
|
3116
|
+
return failure(
|
|
3117
|
+
text(
|
|
3118
|
+
`Not connected to the app: ${message2(error)}
|
|
3119
|
+
Start the app and call again; the server reconnects on the next call. The ${STATUS_TOOL} tool says what state it is in.`
|
|
3120
|
+
)
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
const tool = typeof params.name === "string" ? names2.get(params.name) : void 0;
|
|
3124
|
+
if (!tool) return null;
|
|
3125
|
+
const input = params.arguments ?? {};
|
|
3126
|
+
let args = input.args ?? [];
|
|
3127
|
+
if (typeof args === "string") {
|
|
3128
|
+
try {
|
|
3129
|
+
args = JSON.parse(args);
|
|
3130
|
+
} catch {
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
if (!Array.isArray(args))
|
|
3134
|
+
return failure(text("`args` must be an array of the tool's arguments."));
|
|
3135
|
+
try {
|
|
3136
|
+
const timed = await b.timed(tool, ...withCompactSnapshot(tool, args));
|
|
3137
|
+
const content = await resultContent(tool, timed.value, input.full === true);
|
|
3138
|
+
content.push(...logBlock(timed.logs));
|
|
3139
|
+
if (timed.notice) content.push(text(`Warning: ${timed.notice}`));
|
|
3140
|
+
return { content };
|
|
3141
|
+
} catch (error) {
|
|
3142
|
+
if (error instanceof AgentBridgeCallError)
|
|
3143
|
+
return failure(text(error.message), ...logBlock(error.logs));
|
|
3144
|
+
if (error instanceof SessionLinkError) {
|
|
3145
|
+
drop();
|
|
3146
|
+
return failure(
|
|
3147
|
+
text(
|
|
3148
|
+
`${error.message}
|
|
3149
|
+
The call may or may not have run; check before repeating one that changes state. The server reconnects on the next call.`
|
|
3150
|
+
)
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
if (/^No reply to "/.test(message2(error))) drop();
|
|
3154
|
+
return failure(text(message2(error)));
|
|
3155
|
+
}
|
|
3156
|
+
} finally {
|
|
3157
|
+
inflight--;
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
const active = /* @__PURE__ */ new Map();
|
|
3161
|
+
const dispatch = async (method, params, id, isRequest) => {
|
|
3162
|
+
switch (method) {
|
|
3163
|
+
case "initialize": {
|
|
3164
|
+
if (!isRequest) return null;
|
|
3165
|
+
const asked = params?.protocolVersion;
|
|
3166
|
+
void ensure().catch(() => {
|
|
3167
|
+
});
|
|
3168
|
+
return {
|
|
3169
|
+
result: {
|
|
3170
|
+
protocolVersion: SUPPORTED.includes(asked) ? asked : SUPPORTED[0],
|
|
3171
|
+
capabilities: { tools: { listChanged: true } },
|
|
3172
|
+
serverInfo: { name: "agent-bridge", version: options.version ?? "0.0.0" },
|
|
3173
|
+
instructions: `Tools of the connected React Native app. Each takes \`args\`, an array of the tool's arguments. Results over 32 KB come back as a summary unless \`full\` is true. If the list is empty or calls fail, call ${STATUS_TOOL}.`
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
case "notifications/initialized":
|
|
3178
|
+
initialized = true;
|
|
3179
|
+
return null;
|
|
3180
|
+
case "ping":
|
|
3181
|
+
return isRequest ? { result: {} } : null;
|
|
3182
|
+
case "tools/list":
|
|
3183
|
+
if (!isRequest) return null;
|
|
3184
|
+
await Promise.race([
|
|
3185
|
+
ensure().catch(() => {
|
|
3186
|
+
}),
|
|
3187
|
+
new Promise((r) => {
|
|
3188
|
+
const t = setTimeout(r, 5e3);
|
|
3189
|
+
if (typeof t === "object") t.unref();
|
|
3190
|
+
})
|
|
3191
|
+
]);
|
|
3192
|
+
return { result: listTools() };
|
|
3193
|
+
case "tools/call": {
|
|
3194
|
+
if (!isRequest) return null;
|
|
3195
|
+
const result = await callTool(params ?? {});
|
|
3196
|
+
return result ? { result } : { code: -32602, error: `Unknown tool: ${String(params?.name)}` };
|
|
3197
|
+
}
|
|
3198
|
+
default:
|
|
3199
|
+
return isRequest ? { code: -32601, error: `Method not found: ${method}` } : null;
|
|
3200
|
+
}
|
|
3201
|
+
};
|
|
3202
|
+
const handle = async (raw) => {
|
|
3203
|
+
let req;
|
|
3204
|
+
try {
|
|
3205
|
+
req = JSON.parse(raw);
|
|
3206
|
+
} catch {
|
|
3207
|
+
return void fail(null, -32700, "Parse error");
|
|
3208
|
+
}
|
|
3209
|
+
if (!req || typeof req !== "object" || Array.isArray(req))
|
|
3210
|
+
return void fail(null, -32600, "Invalid request");
|
|
3211
|
+
if (req.method === void 0 && req.id !== void 0) return;
|
|
3212
|
+
if (typeof req.method !== "string")
|
|
3213
|
+
return void fail(req.id ?? null, -32600, "Invalid request");
|
|
3214
|
+
const { id, method, params } = req;
|
|
3215
|
+
const isRequest = id !== void 0 && id !== null;
|
|
3216
|
+
if (method === "notifications/cancelled") {
|
|
3217
|
+
active.get(JSON.stringify(params?.requestId))?.();
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
const key = JSON.stringify(id);
|
|
3221
|
+
let cancel = () => {
|
|
3222
|
+
};
|
|
3223
|
+
const cancelled = new Promise((r) => {
|
|
3224
|
+
cancel = () => r("cancelled");
|
|
3225
|
+
});
|
|
3226
|
+
if (isRequest) active.set(key, cancel);
|
|
3227
|
+
try {
|
|
3228
|
+
const outcome = await Promise.race([dispatch(method, params, id, isRequest), cancelled]);
|
|
3229
|
+
if (outcome === "cancelled" || !outcome) return;
|
|
3230
|
+
if ("result" in outcome) reply(id, outcome.result);
|
|
3231
|
+
else fail(id, outcome.code, outcome.error);
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
if (isRequest) fail(id, -32603, message2(error));
|
|
3234
|
+
} finally {
|
|
3235
|
+
if (isRequest) active.delete(key);
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
const lines = createInterface5({ input: options.input });
|
|
3239
|
+
const pending = /* @__PURE__ */ new Set();
|
|
3240
|
+
lines.on("line", (line) => {
|
|
3241
|
+
if (!line.trim()) return;
|
|
3242
|
+
const p = handle(line).finally(() => pending.delete(p));
|
|
3243
|
+
pending.add(p);
|
|
3244
|
+
});
|
|
3245
|
+
const done = new Promise((resolve4) => {
|
|
3246
|
+
lines.once("close", () => {
|
|
3247
|
+
void Promise.allSettled([...pending]).then(() => {
|
|
3248
|
+
close();
|
|
3249
|
+
resolve4();
|
|
3250
|
+
});
|
|
3251
|
+
});
|
|
3252
|
+
});
|
|
3253
|
+
function close() {
|
|
3254
|
+
if (closed) return;
|
|
3255
|
+
closed = true;
|
|
3256
|
+
if (timer) clearInterval(timer);
|
|
3257
|
+
lines.close();
|
|
3258
|
+
drop();
|
|
3259
|
+
}
|
|
3260
|
+
return { done, close };
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
// src/mcp/run.ts
|
|
3264
|
+
function createConnector(flags, options) {
|
|
3265
|
+
let daemon;
|
|
3266
|
+
let pinned;
|
|
3267
|
+
let ending = false;
|
|
3268
|
+
let notice;
|
|
3269
|
+
const ownName = `mcp-${process.pid}`;
|
|
3270
|
+
const stopped = (name) => {
|
|
3271
|
+
notice = `The session "${name}" this server was using stopped (session stop, or its idle time ran out). Stopping runs bridge.restore, so state changed through it was most likely undone. This call did not run; call again and the server uses a new session.`;
|
|
3272
|
+
};
|
|
3273
|
+
const connectBridge = async () => {
|
|
3274
|
+
if (flags["no-session"]) return connect(options);
|
|
3275
|
+
const timeoutMs = options.timeoutMs;
|
|
3276
|
+
if (daemon) return connectSession({ name: daemon.state.name, timeoutMs });
|
|
3277
|
+
if (pinned) {
|
|
3278
|
+
if (readSession(pinned)) return connectSession({ name: pinned, timeoutMs });
|
|
3279
|
+
stopped(pinned);
|
|
3280
|
+
pinned = void 0;
|
|
3281
|
+
}
|
|
3282
|
+
const existing = sessionFor(flags, (s) => s.name.startsWith("mcp-") && s.name !== ownName);
|
|
3283
|
+
if (existing) {
|
|
3284
|
+
pinned = existing.name;
|
|
3285
|
+
return connectSession({ name: pinned, timeoutMs });
|
|
3286
|
+
}
|
|
3287
|
+
const started = await runSessionDaemon({
|
|
3288
|
+
name: ownName,
|
|
3289
|
+
metro: options.metro,
|
|
3290
|
+
device: options.device,
|
|
3291
|
+
transport: options.transport,
|
|
3292
|
+
idleMs: 0,
|
|
3293
|
+
timeoutMs
|
|
3294
|
+
});
|
|
3295
|
+
daemon = started;
|
|
3296
|
+
void started.done.then(() => {
|
|
3297
|
+
if (daemon !== started) return;
|
|
3298
|
+
daemon = void 0;
|
|
3299
|
+
if (!ending) stopped(started.state.name);
|
|
3300
|
+
});
|
|
3301
|
+
return connectSession({ name: started.state.name, timeoutMs });
|
|
3302
|
+
};
|
|
3303
|
+
return {
|
|
3304
|
+
connect: connectBridge,
|
|
3305
|
+
takeNotice: () => {
|
|
3306
|
+
const n = notice;
|
|
3307
|
+
notice = void 0;
|
|
3308
|
+
return n;
|
|
3309
|
+
},
|
|
3310
|
+
stop: async () => {
|
|
3311
|
+
ending = true;
|
|
3312
|
+
return daemon?.stop(flags.keep).catch(() => null);
|
|
3313
|
+
},
|
|
3314
|
+
sessionName: () => daemon?.state.name ?? pinned
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
async function mcpMain(flags, options, version2) {
|
|
3318
|
+
const connector = createConnector(flags, options);
|
|
3319
|
+
const server = startMcpServer({
|
|
3320
|
+
input: process.stdin,
|
|
3321
|
+
output: process.stdout,
|
|
3322
|
+
connect: connector.connect,
|
|
3323
|
+
takeNotice: connector.takeNotice,
|
|
3324
|
+
sessionName: connector.sessionName,
|
|
3325
|
+
log: (line) => console.error(line),
|
|
3326
|
+
pollMs: flags["no-session"] ? 0 : void 0,
|
|
3327
|
+
version: version2
|
|
3328
|
+
});
|
|
3329
|
+
const quit = () => server.close();
|
|
3330
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"])
|
|
3331
|
+
process.on(signal, quit);
|
|
3332
|
+
await server.done;
|
|
3333
|
+
const restore = await connector.stop();
|
|
3334
|
+
if (restore && !restore.ok) console.error(`bridge.restore failed: ${restore.error}`);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
2324
3337
|
// src/cli.ts
|
|
2325
3338
|
var HELP2 = `agent-bridge: drive a running React Native app from an agent
|
|
2326
3339
|
|
|
@@ -2337,10 +3350,15 @@ Usage
|
|
|
2337
3350
|
agent-bridge call --batch Read \`tool args\` lines from stdin (args may be @file); one JSON line per call, one connection
|
|
2338
3351
|
agent-bridge repl Interactive prompt (history, tab completion, .help). With
|
|
2339
3352
|
stdin not a terminal it behaves like call --batch
|
|
2340
|
-
agent-bridge run <flow
|
|
2341
|
-
|
|
2342
|
-
|
|
3353
|
+
agent-bridge run <flow|dir|glob>... Run flows in order on one connection: export default
|
|
3354
|
+
async ({ step, call, expect }) => {}. A directory means
|
|
3355
|
+
its *.flow.mjs. Each flow ends with bridge.restore, even
|
|
3356
|
+
on failure (--keep skips it); a scenario export is
|
|
3357
|
+
applied first. Exits 1 if any flow failed
|
|
2343
3358
|
agent-bridge scenarios Scenarios the app defines (scenario.apply applies one)
|
|
3359
|
+
agent-bridge mcp Serve the app's tools as an MCP server on stdio (add to your agent's MCP
|
|
3360
|
+
config). Uses this project's session, or starts one and
|
|
3361
|
+
runs bridge.restore on exit (--keep: don't)
|
|
2344
3362
|
agent-bridge assert-absent <files...> Fail if a release bundle contains the bridge
|
|
2345
3363
|
|
|
2346
3364
|
Sessions: one connection for all of an agent's calls
|
|
@@ -2356,11 +3374,18 @@ Options
|
|
|
2356
3374
|
--device <text> Pick an app when several are connected
|
|
2357
3375
|
--transport <name> auto (default), expo or cdp
|
|
2358
3376
|
--timeout <ms> Per-call timeout (default 10000)
|
|
2359
|
-
--out <file> call: write the result to a file
|
|
3377
|
+
--out <file> call: write the result to a file and print its size and shape
|
|
2360
3378
|
(--batch: a directory, one <n>-<tool>.json per call)
|
|
3379
|
+
run: the report file (with --reporter json|junit)
|
|
2361
3380
|
--stop-on-error call --batch: stop after the first failed call
|
|
2362
3381
|
--full call: print results over 32 KB instead of a summary
|
|
2363
|
-
--
|
|
3382
|
+
--json call screen.snapshot: print JSON with rects, not one line per element (call --batch and repl stay JSON)
|
|
3383
|
+
(--out also writes JSON)
|
|
3384
|
+
--strict run: fail a flow if the app logged an error
|
|
3385
|
+
--bail run: stop at the first failed flow
|
|
3386
|
+
--grep <text> run: only flows whose name or path (relative to here) contains text
|
|
3387
|
+
--keep run: no bridge.restore after each flow (session stop: keep the session)
|
|
3388
|
+
--reporter <name> run: text (default), json or junit; step lines and flow output go to stderr
|
|
2364
3389
|
--scenario <name> run: also apply this scenario (repeatable). Options as
|
|
2365
3390
|
JSON after "=": --scenario 'signedIn={"user":{"name":"Ada"}}'
|
|
2366
3391
|
--session <name> Use this session (env AGENT_BRIDGE_SESSION)
|
|
@@ -2402,6 +3427,9 @@ async function main() {
|
|
|
2402
3427
|
batch: { type: "boolean" },
|
|
2403
3428
|
"stop-on-error": { type: "boolean" },
|
|
2404
3429
|
scenario: { type: "string", multiple: true },
|
|
3430
|
+
bail: { type: "boolean" },
|
|
3431
|
+
grep: { type: "string" },
|
|
3432
|
+
reporter: { type: "string" },
|
|
2405
3433
|
name: { type: "string" },
|
|
2406
3434
|
idle: { type: "string" },
|
|
2407
3435
|
keep: { type: "boolean" },
|
|
@@ -2507,12 +3535,12 @@ async function main() {
|
|
|
2507
3535
|
(path) => path === "-" ? stdin : readArgFile(path)
|
|
2508
3536
|
);
|
|
2509
3537
|
return withBridge(async (bridge) => {
|
|
2510
|
-
const { value, ms, appMs, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
|
|
3538
|
+
const { value, ms, appMs, logs, notice } = await bridge.timed(tool, ...withCompactSnapshot(tool, args, values.json || !!values.out)).catch((error) => {
|
|
2511
3539
|
for (const e of failedLogs(error)) console.error(logLine(e));
|
|
2512
3540
|
throw error;
|
|
2513
3541
|
});
|
|
2514
3542
|
console.log(
|
|
2515
|
-
await renderResult(value, { out: values.out, full: values.full })
|
|
3543
|
+
isSnapshotText(tool, value) ? await renderText(value, { out: values.out, full: values.full }) : await renderResult(value, { out: values.out, full: values.full })
|
|
2516
3544
|
);
|
|
2517
3545
|
for (const e of logs) console.error(logLine(e));
|
|
2518
3546
|
if (notice) console.error(`Warning: ${notice}`);
|
|
@@ -2537,18 +3565,65 @@ async function main() {
|
|
|
2537
3565
|
});
|
|
2538
3566
|
});
|
|
2539
3567
|
case "run": {
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
const
|
|
2543
|
-
|
|
3568
|
+
if (!rest.length)
|
|
3569
|
+
throw new Error("Usage: agent-bridge run <flow file | directory | glob>...");
|
|
3570
|
+
const machine = checkRunFlags({ reporter: values.reporter, out: values.out });
|
|
3571
|
+
const reporter = values.reporter ?? "text";
|
|
3572
|
+
const files = await resolveFlowFiles(rest);
|
|
3573
|
+
const { entries: loaded, failures } = await loadFlows(files);
|
|
3574
|
+
const entries = selectFlows(loaded, values.grep);
|
|
3575
|
+
if (!entries.length && !failures.length) {
|
|
3576
|
+
if (!machine) throw new Error(`--grep ${values.grep}: no flow matches`);
|
|
3577
|
+
failures.push(failedRun("", `--grep ${values.grep}`, "no flow matches"));
|
|
3578
|
+
}
|
|
3579
|
+
if (machine && values.out) await mkdir3(dirname7(values.out), { recursive: true });
|
|
2544
3580
|
const extra = (values.scenario ?? []).map(parseScenarioFlag);
|
|
2545
|
-
|
|
2546
|
-
const
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
3581
|
+
const finish = async (runs2) => {
|
|
3582
|
+
const failed = runs2.filter((r) => r.status === "failed");
|
|
3583
|
+
if (machine) {
|
|
3584
|
+
const doc = reporter === "json" ? renderJson(runs2) : renderJunit(runs2);
|
|
3585
|
+
if (values.out) await writeReport(values.out, doc);
|
|
3586
|
+
else console.log(doc);
|
|
3587
|
+
}
|
|
3588
|
+
const log = machine ? console.error : console.log;
|
|
3589
|
+
if (runs2.length > 1 || machine) {
|
|
3590
|
+
log("");
|
|
3591
|
+
for (const r of runs2)
|
|
3592
|
+
log(
|
|
3593
|
+
`${r.status === "passed" ? "PASS" : r.status === "failed" ? "FAIL" : "SKIP"} ${r.name}${r.status === "skipped" ? "" : ` (${(r.ms / 1e3).toFixed(1)} s)`}${r.error ? `: ${r.error}` : ""}`
|
|
3594
|
+
);
|
|
3595
|
+
const count = (st) => runs2.filter((r) => r.status === st).length;
|
|
3596
|
+
log(
|
|
3597
|
+
`${count("passed")} passed, ${failed.length} failed${count("skipped") ? `, ${count("skipped")} skipped` : ""}`
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
if (failed.length) process.exitCode = 1;
|
|
3601
|
+
};
|
|
3602
|
+
const execute = (bridge) => runFlows(bridge, entries, {
|
|
3603
|
+
scenarios: extra,
|
|
3604
|
+
strict: values.strict,
|
|
3605
|
+
bail: values.bail,
|
|
3606
|
+
keep: values.keep,
|
|
3607
|
+
single: entries.length === 1 && !machine,
|
|
3608
|
+
print: machine ? (line) => console.error(line) : void 0
|
|
2551
3609
|
});
|
|
3610
|
+
let runs;
|
|
3611
|
+
if (!entries.length) runs = [];
|
|
3612
|
+
else {
|
|
3613
|
+
try {
|
|
3614
|
+
const run = (bridge) => machine ? logToStderr(() => execute(bridge)) : execute(bridge);
|
|
3615
|
+
let result = [];
|
|
3616
|
+
await withBridge(async (bridge) => {
|
|
3617
|
+
result = await run(bridge);
|
|
3618
|
+
});
|
|
3619
|
+
runs = result;
|
|
3620
|
+
} catch (error) {
|
|
3621
|
+
if (!machine) throw error;
|
|
3622
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
3623
|
+
runs = entries.map((e) => failedRun(e.file, e.name, why));
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
return finish([...failures, ...runs]);
|
|
2552
3627
|
}
|
|
2553
3628
|
case "scenarios":
|
|
2554
3629
|
return withBridge(async (bridge) => {
|
|
@@ -2568,6 +3643,12 @@ async function main() {
|
|
|
2568
3643
|
console.log(` ${"".padEnd(20)} options: ${JSON.stringify(s.options)}`);
|
|
2569
3644
|
}
|
|
2570
3645
|
});
|
|
3646
|
+
case "mcp":
|
|
3647
|
+
return mcpMain(
|
|
3648
|
+
values,
|
|
3649
|
+
options,
|
|
3650
|
+
createRequire(import.meta.url)("../package.json").version
|
|
3651
|
+
);
|
|
2571
3652
|
case "session":
|
|
2572
3653
|
return sessionCommand(rest[0], values);
|
|
2573
3654
|
case DAEMON_COMMAND:
|