@avasapp/agent-bridge 0.1.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/adapters/expo-router.cjs +144 -0
- package/dist/adapters/expo-router.d.cts +38 -0
- package/dist/adapters/react-native-mmkv.cjs +102 -0
- package/dist/adapters/react-native-mmkv.d.cts +15 -0
- package/dist/adapters/tanstack-query.cjs +161 -0
- package/dist/adapters/tanstack-query.d.cts +10 -0
- package/dist/adapters/zustand.cjs +97 -0
- package/dist/adapters/zustand.d.cts +15 -0
- package/dist/chunk-UEWFQWCY.js +575 -0
- package/dist/cli.js +657 -0
- package/dist/client/index.cjs +568 -0
- package/dist/client/index.d.ts +112 -0
- package/dist/client/index.js +14 -0
- package/dist/expo/index.cjs +62 -0
- package/dist/expo/index.d.cts +9 -0
- package/dist/network/index.cjs +568 -0
- package/dist/network/index.d.cts +85 -0
- package/dist/noop/expo-router.cjs +26 -0
- package/dist/noop/expo.cjs +27 -0
- package/dist/noop/index.cjs +36 -0
- package/dist/noop/network.cjs +34 -0
- package/dist/noop/react-native-mmkv.cjs +26 -0
- package/dist/noop/tanstack-query.cjs +26 -0
- package/dist/noop/zustand.cjs +26 -0
- package/dist/runtime/index.cjs +933 -0
- package/dist/runtime/index.d.cts +69 -0
- package/dist/types-C6DUUHnB.d.cts +76 -0
- package/entries/expo-router.cjs +9 -0
- package/entries/expo.cjs +9 -0
- package/entries/index.cjs +9 -0
- package/entries/network.cjs +9 -0
- package/entries/react-native-mmkv.cjs +9 -0
- package/entries/tanstack-query.cjs +9 -0
- package/entries/zustand.cjs +9 -0
- package/package.json +120 -0
- package/skills/agent-bridge/SKILL.md +89 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/expo/index.ts
|
|
21
|
+
var expo_exports = {};
|
|
22
|
+
__export(expo_exports, {
|
|
23
|
+
expoTransport: () => expoTransport
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(expo_exports);
|
|
26
|
+
var import_devtools = require("expo/devtools");
|
|
27
|
+
|
|
28
|
+
// src/shared/protocol.ts
|
|
29
|
+
var PLUGIN_NAME = "agent-bridge";
|
|
30
|
+
|
|
31
|
+
// src/expo/index.ts
|
|
32
|
+
function expoTransport() {
|
|
33
|
+
return {
|
|
34
|
+
name: "expo",
|
|
35
|
+
start(context) {
|
|
36
|
+
let closed = false;
|
|
37
|
+
let client = null;
|
|
38
|
+
const subscriptions = [];
|
|
39
|
+
void (0, import_devtools.getDevToolsPluginClientAsync)(PLUGIN_NAME).then((c) => {
|
|
40
|
+
if (closed) {
|
|
41
|
+
void c.closeAsync();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
client = c;
|
|
45
|
+
const announce = () => c.sendMessage("hello:reply", context.info());
|
|
46
|
+
subscriptions.push(c.addMessageListener("hello", announce));
|
|
47
|
+
subscriptions.push(
|
|
48
|
+
c.addMessageListener("call", async (call) => {
|
|
49
|
+
if (call.to && call.to !== context.info().deviceId) return;
|
|
50
|
+
c.sendMessage("result", await context.dispatch(call));
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
announce();
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
closed = true;
|
|
57
|
+
for (const s of subscriptions) s.remove();
|
|
58
|
+
void client?.closeAsync();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { a as Transport } from '../types-C6DUUHnB.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plain JSON over Expo's dev-tools plugin socket: no string evaluation, emoji
|
|
5
|
+
* and async tools just work. Needs the Expo CLI dev server.
|
|
6
|
+
*/
|
|
7
|
+
declare function expoTransport(): Transport;
|
|
8
|
+
|
|
9
|
+
export { Transport, expoTransport };
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/network/index.ts
|
|
21
|
+
var network_exports = {};
|
|
22
|
+
__export(network_exports, {
|
|
23
|
+
installNetwork: () => installNetwork,
|
|
24
|
+
mock: () => mock,
|
|
25
|
+
mockRequests: () => mockRequests,
|
|
26
|
+
networkTools: () => networkTools
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(network_exports);
|
|
29
|
+
|
|
30
|
+
// src/network/body.ts
|
|
31
|
+
var MAX_BODY = 2048;
|
|
32
|
+
function formatBody(text) {
|
|
33
|
+
if (text === void 0 || text === "") return void 0;
|
|
34
|
+
let out = text;
|
|
35
|
+
try {
|
|
36
|
+
out = JSON.stringify(JSON.parse(text));
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return out.length > MAX_BODY ? `${out.slice(0, MAX_BODY)}\u2026 (+${out.length - MAX_BODY} chars)` : out;
|
|
40
|
+
}
|
|
41
|
+
function bodyText(body) {
|
|
42
|
+
if (body === void 0 || body === null) return void 0;
|
|
43
|
+
if (typeof body === "string") return body;
|
|
44
|
+
if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams)
|
|
45
|
+
return body.toString();
|
|
46
|
+
if (typeof FormData !== "undefined" && body instanceof FormData)
|
|
47
|
+
return "[FormData]";
|
|
48
|
+
if (typeof Blob !== "undefined" && body instanceof Blob)
|
|
49
|
+
return `[Blob ${body.size} bytes]`;
|
|
50
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
|
|
51
|
+
return `[binary ${body.byteLength} bytes]`;
|
|
52
|
+
return String(body);
|
|
53
|
+
}
|
|
54
|
+
function isTextual(contentType) {
|
|
55
|
+
if (!contentType) return true;
|
|
56
|
+
if (/event-stream/i.test(contentType)) return false;
|
|
57
|
+
return /json|text|xml|javascript|x-www-form-urlencoded/i.test(contentType);
|
|
58
|
+
}
|
|
59
|
+
function errorMessage(error) {
|
|
60
|
+
return error instanceof Error ? error.message : String(error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/network/state.ts
|
|
64
|
+
var LOG_SIZE = 100;
|
|
65
|
+
var KEY = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/network");
|
|
66
|
+
function networkState() {
|
|
67
|
+
const g = globalThis;
|
|
68
|
+
g[KEY] ?? (g[KEY] = {
|
|
69
|
+
installed: false,
|
|
70
|
+
log: [],
|
|
71
|
+
nextLogId: 1,
|
|
72
|
+
mocks: [],
|
|
73
|
+
nextSeq: 1,
|
|
74
|
+
insideFetch: 0,
|
|
75
|
+
devHost: null,
|
|
76
|
+
skip: [],
|
|
77
|
+
uninstall: []
|
|
78
|
+
});
|
|
79
|
+
return g[KEY];
|
|
80
|
+
}
|
|
81
|
+
var URL_PARTS = /^(?:[a-z][a-z0-9+.-]*:)?(?:\/\/([^/?#]*))?([^?#]*)/i;
|
|
82
|
+
var hostOf = (url) => URL_PARTS.exec(url)?.[1] ?? "";
|
|
83
|
+
var pathOf = (url) => URL_PARTS.exec(url)?.[2] ?? "";
|
|
84
|
+
var DEV_PATH = /^\/(symbolicate|logs|status|hot|message|inspector|expo-dev-plugins|_expo)(\/.*)?$|\.(bundle|map)$/;
|
|
85
|
+
function isSkipped(state, url) {
|
|
86
|
+
const host = hostOf(url);
|
|
87
|
+
if (state.devHost && host === state.devHost) return true;
|
|
88
|
+
if (DEV_PATH.test(pathOf(url))) return true;
|
|
89
|
+
return state.skip.some(
|
|
90
|
+
(rule) => typeof rule === "string" ? url.includes(rule) : rule.test(url)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
function deriveDevHost() {
|
|
94
|
+
try {
|
|
95
|
+
const rn = require("react-native");
|
|
96
|
+
const module2 = rn.TurboModuleRegistry?.get?.("SourceCode") ?? rn.NativeModules?.SourceCode;
|
|
97
|
+
const scriptURL = module2?.getConstants?.().scriptURL ?? module2?.scriptURL;
|
|
98
|
+
return typeof scriptURL === "string" && /^https?:/i.test(scriptURL) ? hostOf(scriptURL) || null : null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function startEntry(state, fields) {
|
|
104
|
+
const entry = {
|
|
105
|
+
id: state.nextLogId++,
|
|
106
|
+
...fields,
|
|
107
|
+
ms: 0,
|
|
108
|
+
pending: true,
|
|
109
|
+
started: Date.now()
|
|
110
|
+
};
|
|
111
|
+
if (entry.requestBody === void 0) delete entry.requestBody;
|
|
112
|
+
state.log.push(entry);
|
|
113
|
+
if (state.log.length > LOG_SIZE)
|
|
114
|
+
state.log.splice(0, state.log.length - LOG_SIZE);
|
|
115
|
+
return entry;
|
|
116
|
+
}
|
|
117
|
+
function finishEntry(entry, fields) {
|
|
118
|
+
entry.ms = Date.now() - entry.started;
|
|
119
|
+
delete entry.pending;
|
|
120
|
+
for (const [key, value] of Object.entries(fields))
|
|
121
|
+
if (value !== void 0) entry[key] = value;
|
|
122
|
+
}
|
|
123
|
+
function publicEntry(entry) {
|
|
124
|
+
const { started, ...rest } = entry;
|
|
125
|
+
return rest.pending && started !== void 0 ? { ...rest, ms: Date.now() - started } : rest;
|
|
126
|
+
}
|
|
127
|
+
function urlTest(url) {
|
|
128
|
+
if (typeof url === "string") return (u) => u.includes(url);
|
|
129
|
+
const regex = url instanceof RegExp ? url : new RegExp(url.regex, url.flags);
|
|
130
|
+
return (u) => {
|
|
131
|
+
regex.lastIndex = 0;
|
|
132
|
+
return regex.test(u);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function compileMatch(match) {
|
|
136
|
+
const spec = typeof match === "string" ? { url: match } : match;
|
|
137
|
+
if (!spec || spec.url === void 0)
|
|
138
|
+
throw new Error("A mock needs a match: a URL substring or { url, method? }");
|
|
139
|
+
const testUrl = urlTest(spec.url);
|
|
140
|
+
const method = spec.method?.toUpperCase();
|
|
141
|
+
return (m, u) => (!method || method === m) && testUrl(u);
|
|
142
|
+
}
|
|
143
|
+
function addMock(state, source, match, response, options = {}) {
|
|
144
|
+
if (!response || typeof response !== "object" && typeof response !== "function")
|
|
145
|
+
throw new Error("A mock needs a response: { status?, json?, body?, headers? }, { offline: true } or a handler");
|
|
146
|
+
const seq = state.nextSeq++;
|
|
147
|
+
const id = options.id ?? `${source}-${seq}`;
|
|
148
|
+
removeMocks(state, (m) => m.id === id);
|
|
149
|
+
const mock2 = {
|
|
150
|
+
id,
|
|
151
|
+
source,
|
|
152
|
+
match,
|
|
153
|
+
response,
|
|
154
|
+
times: options.times,
|
|
155
|
+
delayMs: options.delayMs ?? 0,
|
|
156
|
+
hits: 0,
|
|
157
|
+
seq,
|
|
158
|
+
test: compileMatch(match)
|
|
159
|
+
};
|
|
160
|
+
state.mocks.push(mock2);
|
|
161
|
+
return mock2;
|
|
162
|
+
}
|
|
163
|
+
function removeMocks(state, which) {
|
|
164
|
+
const before = state.mocks.length;
|
|
165
|
+
state.mocks = state.mocks.filter((m) => !which(m));
|
|
166
|
+
return before - state.mocks.length;
|
|
167
|
+
}
|
|
168
|
+
var mockOrder = (a, b) => Number(b.source === "agent") - Number(a.source === "agent") || b.seq - a.seq;
|
|
169
|
+
function matchingMocks(state, method, url) {
|
|
170
|
+
return state.mocks.filter((m) => m.test(method, url)).sort(mockOrder);
|
|
171
|
+
}
|
|
172
|
+
async function answer(state, candidates, request) {
|
|
173
|
+
for (const mock2 of candidates) {
|
|
174
|
+
if (!state.mocks.includes(mock2)) continue;
|
|
175
|
+
const response = typeof mock2.response === "function" ? await mock2.response(await request()) : mock2.response;
|
|
176
|
+
if (!response) continue;
|
|
177
|
+
mock2.hits += 1;
|
|
178
|
+
if (mock2.times !== void 0 && mock2.hits >= mock2.times)
|
|
179
|
+
removeMocks(state, (m) => m === mock2);
|
|
180
|
+
return { mock: mock2, response };
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function mockRequest(method, url, headers, body) {
|
|
185
|
+
return {
|
|
186
|
+
method,
|
|
187
|
+
url,
|
|
188
|
+
headers,
|
|
189
|
+
body,
|
|
190
|
+
json: () => {
|
|
191
|
+
try {
|
|
192
|
+
return body === void 0 ? void 0 : JSON.parse(body);
|
|
193
|
+
} catch {
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function responseParts(response) {
|
|
200
|
+
const r = response;
|
|
201
|
+
const status = r.status ?? 200;
|
|
202
|
+
if (r.json !== void 0)
|
|
203
|
+
return {
|
|
204
|
+
status,
|
|
205
|
+
text: JSON.stringify(r.json),
|
|
206
|
+
headers: { "content-type": "application/json", ...r.headers }
|
|
207
|
+
};
|
|
208
|
+
return { status, text: r.body ?? "", headers: { ...r.headers } };
|
|
209
|
+
}
|
|
210
|
+
var isOffline = (response) => response.offline === true;
|
|
211
|
+
var OFFLINE_MESSAGE = "Network request failed";
|
|
212
|
+
var sleep = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
213
|
+
|
|
214
|
+
// src/network/fetch.ts
|
|
215
|
+
function headersRecord(headers) {
|
|
216
|
+
const out = {};
|
|
217
|
+
if (!headers) return out;
|
|
218
|
+
const h = headers;
|
|
219
|
+
if (Array.isArray(headers)) {
|
|
220
|
+
for (const [key, value] of headers) out[String(key).toLowerCase()] = String(value);
|
|
221
|
+
} else if (typeof h.forEach === "function") {
|
|
222
|
+
h.forEach((value, key) => {
|
|
223
|
+
out[key.toLowerCase()] = value;
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
for (const [key, value] of Object.entries(headers))
|
|
227
|
+
out[key.toLowerCase()] = String(value);
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
var nullBody = (status) => [101, 204, 205, 304].includes(status);
|
|
232
|
+
function mockedResponse(response) {
|
|
233
|
+
const { status, text, headers } = responseParts(response);
|
|
234
|
+
const body = nullBody(status) ? null : text;
|
|
235
|
+
if (typeof Response !== "undefined")
|
|
236
|
+
return new Response(body, { status, headers });
|
|
237
|
+
return {
|
|
238
|
+
ok: status >= 200 && status < 300,
|
|
239
|
+
status,
|
|
240
|
+
statusText: "",
|
|
241
|
+
url: "",
|
|
242
|
+
headers: { get: (name) => headers[name.toLowerCase()] ?? null },
|
|
243
|
+
text: async () => text,
|
|
244
|
+
json: async () => JSON.parse(text),
|
|
245
|
+
clone() {
|
|
246
|
+
return mockedResponse(response);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function abortError() {
|
|
251
|
+
const error = new Error("Aborted");
|
|
252
|
+
error.name = "AbortError";
|
|
253
|
+
return error;
|
|
254
|
+
}
|
|
255
|
+
function patchFetch(state) {
|
|
256
|
+
const g = globalThis;
|
|
257
|
+
const original = g.fetch;
|
|
258
|
+
if (typeof original !== "function") return null;
|
|
259
|
+
const callOriginal = (input, init) => {
|
|
260
|
+
state.insideFetch += 1;
|
|
261
|
+
try {
|
|
262
|
+
return original.call(globalThis, input, init);
|
|
263
|
+
} finally {
|
|
264
|
+
state.insideFetch -= 1;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const wrapped = function fetch(input, init) {
|
|
268
|
+
const req = typeof input === "object" && input ? input : {};
|
|
269
|
+
const url = typeof input === "string" ? input : req.url ?? String(input);
|
|
270
|
+
if (isSkipped(state, url)) return callOriginal(input, init);
|
|
271
|
+
const method = String(init?.method ?? req.method ?? "GET").toUpperCase();
|
|
272
|
+
const requestBody = bodyText(init?.body);
|
|
273
|
+
const entry = startEntry(state, {
|
|
274
|
+
method,
|
|
275
|
+
url,
|
|
276
|
+
requestBody: formatBody(requestBody)
|
|
277
|
+
});
|
|
278
|
+
const real = () => {
|
|
279
|
+
let promise;
|
|
280
|
+
try {
|
|
281
|
+
promise = callOriginal(input, init);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
finishEntry(entry, { error: errorMessage(error) });
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
return promise.then(
|
|
287
|
+
(res) => {
|
|
288
|
+
finishEntry(entry, { status: res.status });
|
|
289
|
+
const type = res.headers?.get?.("content-type");
|
|
290
|
+
if (isTextual(type) && typeof res.clone === "function") {
|
|
291
|
+
res.clone().text().then((text) => {
|
|
292
|
+
const body = formatBody(text);
|
|
293
|
+
if (body !== void 0) entry.responseBody = body;
|
|
294
|
+
}).catch(() => {
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return res;
|
|
298
|
+
},
|
|
299
|
+
(error) => {
|
|
300
|
+
finishEntry(entry, { error: errorMessage(error) });
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
const candidates = matchingMocks(state, method, url);
|
|
306
|
+
if (!candidates.length) return real();
|
|
307
|
+
const request = async () => {
|
|
308
|
+
let body = requestBody;
|
|
309
|
+
if (body === void 0 && init?.body === void 0 && req.clone) {
|
|
310
|
+
body = await req.clone().text().catch(() => void 0);
|
|
311
|
+
}
|
|
312
|
+
return mockRequest(
|
|
313
|
+
method,
|
|
314
|
+
url,
|
|
315
|
+
headersRecord(init?.headers ?? req.headers),
|
|
316
|
+
body || void 0
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
const answered = answer(state, candidates, request).catch((error) => {
|
|
320
|
+
entry.mocked = true;
|
|
321
|
+
finishEntry(entry, { error: errorMessage(error) });
|
|
322
|
+
throw error;
|
|
323
|
+
});
|
|
324
|
+
return answered.then(async (hit) => {
|
|
325
|
+
if (!hit) return real();
|
|
326
|
+
entry.mocked = true;
|
|
327
|
+
await sleep(hit.mock.delayMs);
|
|
328
|
+
if (init?.signal?.aborted) {
|
|
329
|
+
finishEntry(entry, { error: "aborted" });
|
|
330
|
+
throw abortError();
|
|
331
|
+
}
|
|
332
|
+
if (isOffline(hit.response)) {
|
|
333
|
+
finishEntry(entry, { error: `${OFFLINE_MESSAGE} (mocked offline)` });
|
|
334
|
+
throw new TypeError(OFFLINE_MESSAGE);
|
|
335
|
+
}
|
|
336
|
+
const { status, text } = responseParts(hit.response);
|
|
337
|
+
finishEntry(entry, { status, responseBody: formatBody(text) });
|
|
338
|
+
return mockedResponse(hit.response);
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
g.fetch = wrapped;
|
|
342
|
+
return () => {
|
|
343
|
+
if (g.fetch === wrapped) g.fetch = original;
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/network/xhr.ts
|
|
348
|
+
var META = /* @__PURE__ */ Symbol("agent-bridge.xhr");
|
|
349
|
+
var isRN = (xhr) => typeof xhr.__didReceiveResponse === "function" && typeof xhr.__didCompleteResponse === "function";
|
|
350
|
+
function responseTextOf(xhr) {
|
|
351
|
+
try {
|
|
352
|
+
if (xhr.responseType === "" || xhr.responseType === "text") return xhr.responseText;
|
|
353
|
+
if (xhr.responseType === "json") return JSON.stringify(xhr.response);
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
function track(xhr, entry) {
|
|
359
|
+
let error;
|
|
360
|
+
xhr.addEventListener("error", () => error = OFFLINE_MESSAGE);
|
|
361
|
+
xhr.addEventListener("timeout", () => error = "timed out");
|
|
362
|
+
xhr.addEventListener("abort", () => error = "aborted");
|
|
363
|
+
xhr.addEventListener("loadend", () => {
|
|
364
|
+
finishEntry(entry, {
|
|
365
|
+
status: error ? void 0 : xhr.status,
|
|
366
|
+
error,
|
|
367
|
+
responseBody: error ? void 0 : formatBody(responseTextOf(xhr))
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
var fakeRequestId = 0;
|
|
372
|
+
function fire(xhr, type) {
|
|
373
|
+
const Ctor = (type === "readystatechange" ? void 0 : globalThis.ProgressEvent) ?? globalThis.Event;
|
|
374
|
+
xhr.dispatchEvent(new Ctor(type));
|
|
375
|
+
}
|
|
376
|
+
function respond(xhr, hit, url) {
|
|
377
|
+
const offline = isOffline(hit.response);
|
|
378
|
+
const { status, text, headers } = responseParts(hit.response);
|
|
379
|
+
if (isRN(xhr)) {
|
|
380
|
+
const id = -++fakeRequestId;
|
|
381
|
+
xhr.__didCreateRequest(id);
|
|
382
|
+
if (offline) return xhr.__didCompleteResponse(id, OFFLINE_MESSAGE, false);
|
|
383
|
+
xhr.__didReceiveResponse(id, status, headers, url);
|
|
384
|
+
if (xhr.responseType === "arraybuffer" && typeof btoa === "function") {
|
|
385
|
+
xhr.__didReceiveData(id, btoa(unescape(encodeURIComponent(text))));
|
|
386
|
+
} else if (xhr.responseType !== "blob") {
|
|
387
|
+
xhr.__didReceiveData(id, text);
|
|
388
|
+
}
|
|
389
|
+
return xhr.__didCompleteResponse(id, "", false);
|
|
390
|
+
}
|
|
391
|
+
const define = (values) => {
|
|
392
|
+
for (const [key, value] of Object.entries(values))
|
|
393
|
+
Object.defineProperty(xhr, key, { value, configurable: true });
|
|
394
|
+
};
|
|
395
|
+
const lower = Object.fromEntries(
|
|
396
|
+
Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])
|
|
397
|
+
);
|
|
398
|
+
let response = text;
|
|
399
|
+
if (xhr.responseType === "json") {
|
|
400
|
+
try {
|
|
401
|
+
response = JSON.parse(text);
|
|
402
|
+
} catch {
|
|
403
|
+
response = null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
define({
|
|
407
|
+
readyState: 4,
|
|
408
|
+
status: offline ? 0 : status,
|
|
409
|
+
statusText: "",
|
|
410
|
+
responseURL: url,
|
|
411
|
+
responseText: offline ? "" : text,
|
|
412
|
+
response: offline ? "" : response,
|
|
413
|
+
getAllResponseHeaders: () => offline ? "" : Object.entries(lower).map(([k, v]) => `${k}: ${v}\r
|
|
414
|
+
`).join(""),
|
|
415
|
+
getResponseHeader: (name) => lower[name.toLowerCase()] ?? null
|
|
416
|
+
});
|
|
417
|
+
fire(xhr, "readystatechange");
|
|
418
|
+
fire(xhr, offline ? "error" : "load");
|
|
419
|
+
fire(xhr, "loadend");
|
|
420
|
+
}
|
|
421
|
+
function patchXhr(state) {
|
|
422
|
+
const XHR = globalThis.XMLHttpRequest;
|
|
423
|
+
if (typeof XHR !== "function") return null;
|
|
424
|
+
const proto = XHR.prototype;
|
|
425
|
+
const { open, send, setRequestHeader, abort } = proto;
|
|
426
|
+
proto.open = function(method, url, ...rest) {
|
|
427
|
+
this[META] = {
|
|
428
|
+
method: String(method).toUpperCase(),
|
|
429
|
+
url: String(url),
|
|
430
|
+
// Opened by the patched fetch's own original: fetch logs it already.
|
|
431
|
+
internal: state.insideFetch > 0,
|
|
432
|
+
headers: {}
|
|
433
|
+
};
|
|
434
|
+
return open.call(this, method, url, ...rest);
|
|
435
|
+
};
|
|
436
|
+
proto.setRequestHeader = function(name, value) {
|
|
437
|
+
const meta = this[META];
|
|
438
|
+
if (meta) meta.headers[name.toLowerCase()] = value;
|
|
439
|
+
return setRequestHeader.call(this, name, value);
|
|
440
|
+
};
|
|
441
|
+
proto.abort = function() {
|
|
442
|
+
const meta = this[META];
|
|
443
|
+
if (meta) meta.aborted = true;
|
|
444
|
+
return abort.call(this);
|
|
445
|
+
};
|
|
446
|
+
proto.send = function(body) {
|
|
447
|
+
const meta = this[META];
|
|
448
|
+
if (!meta || meta.internal || isSkipped(state, meta.url))
|
|
449
|
+
return send.call(this, body);
|
|
450
|
+
const { method, url } = meta;
|
|
451
|
+
const text = bodyText(body);
|
|
452
|
+
const entry = startEntry(state, {
|
|
453
|
+
method,
|
|
454
|
+
url,
|
|
455
|
+
requestBody: formatBody(text)
|
|
456
|
+
});
|
|
457
|
+
const candidates = matchingMocks(state, method, url);
|
|
458
|
+
if (!candidates.length) {
|
|
459
|
+
track(this, entry);
|
|
460
|
+
return send.call(this, body);
|
|
461
|
+
}
|
|
462
|
+
const request = async () => mockRequest(method, url, meta.headers, text);
|
|
463
|
+
answer(state, candidates, request).then(
|
|
464
|
+
async (hit) => {
|
|
465
|
+
if (!hit) {
|
|
466
|
+
track(this, entry);
|
|
467
|
+
return send.call(this, body);
|
|
468
|
+
}
|
|
469
|
+
entry.mocked = true;
|
|
470
|
+
await sleep(hit.mock.delayMs);
|
|
471
|
+
if (meta.aborted) {
|
|
472
|
+
finishEntry(entry, { error: "aborted" });
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const offline = isOffline(hit.response);
|
|
476
|
+
const { status, text: out } = responseParts(hit.response);
|
|
477
|
+
finishEntry(
|
|
478
|
+
entry,
|
|
479
|
+
offline ? { error: `${OFFLINE_MESSAGE} (mocked offline)` } : { status, responseBody: formatBody(out) }
|
|
480
|
+
);
|
|
481
|
+
respond(this, hit, url);
|
|
482
|
+
},
|
|
483
|
+
(error) => {
|
|
484
|
+
entry.mocked = true;
|
|
485
|
+
finishEntry(entry, { error: errorMessage(error) });
|
|
486
|
+
respond(this, { mock: candidates[0], response: { offline: true } }, url);
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
};
|
|
490
|
+
return () => {
|
|
491
|
+
Object.assign(proto, { open, send, setRequestHeader, abort });
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/network/index.ts
|
|
496
|
+
function installNetwork() {
|
|
497
|
+
const state = networkState();
|
|
498
|
+
if (state.installed) return;
|
|
499
|
+
state.installed = true;
|
|
500
|
+
state.devHost = deriveDevHost();
|
|
501
|
+
for (const undo of [patchXhr(state), patchFetch(state)])
|
|
502
|
+
if (undo) state.uninstall.push(undo);
|
|
503
|
+
}
|
|
504
|
+
function mock(match, response, options) {
|
|
505
|
+
installNetwork();
|
|
506
|
+
const state = networkState();
|
|
507
|
+
const { id } = addMock(state, "app", match, response, options);
|
|
508
|
+
return { id, remove: () => removeMocks(state, (m) => m.id === id) > 0 };
|
|
509
|
+
}
|
|
510
|
+
function mockRequests(routes) {
|
|
511
|
+
const added = routes.map((r) => mock(r.match, r.response, r.options));
|
|
512
|
+
return () => {
|
|
513
|
+
for (const m of added) m.remove();
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
var describeMock = (m) => ({
|
|
517
|
+
id: m.id,
|
|
518
|
+
source: m.source,
|
|
519
|
+
match: m.match instanceof RegExp ? String(m.match) : m.match,
|
|
520
|
+
response: typeof m.response === "function" ? "[handler]" : m.response,
|
|
521
|
+
times: m.times,
|
|
522
|
+
hits: m.hits,
|
|
523
|
+
delayMs: m.delayMs || void 0
|
|
524
|
+
});
|
|
525
|
+
function networkTools(options = {}) {
|
|
526
|
+
installNetwork();
|
|
527
|
+
const state = networkState();
|
|
528
|
+
if (options.skip) state.skip = options.skip;
|
|
529
|
+
const isAgent = (m) => m.source === "agent";
|
|
530
|
+
return {
|
|
531
|
+
"net.log": {
|
|
532
|
+
description: "Recent requests, newest first: method, url, status, ms, bodies (~2 KB), error, mocked. Filter by url substring or method; clear: true empties the log after reading.",
|
|
533
|
+
run: (filter = {}) => {
|
|
534
|
+
const method = filter.method?.toUpperCase();
|
|
535
|
+
const entries = state.log.filter(
|
|
536
|
+
(e) => (!filter.url || e.url.includes(filter.url)) && (!method || e.method === method)
|
|
537
|
+
).reverse().slice(0, filter.limit ?? 20).map(publicEntry);
|
|
538
|
+
if (filter.clear) state.log.length = 0;
|
|
539
|
+
return entries;
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
"net.mock": {
|
|
543
|
+
description: 'Answer matching requests with a canned response until unmocked. match: "/path" or { url: string | { regex }, method? }. response: { status?, json?, body?, headers? } or { offline: true }. options: { times?, delayMs? }.',
|
|
544
|
+
run: (match, response, mockOptions = {}) => {
|
|
545
|
+
if (typeof response !== "object" || response === null)
|
|
546
|
+
throw new Error("response must be an object, e.g. { status: 500 }");
|
|
547
|
+
const { times, delayMs } = mockOptions;
|
|
548
|
+
return { id: addMock(state, "agent", match, response, { times, delayMs }).id };
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
"net.mocks": {
|
|
552
|
+
description: "Active mocks, agent and app, in the order they are tried.",
|
|
553
|
+
run: () => [...state.mocks].sort(mockOrder).map(describeMock)
|
|
554
|
+
},
|
|
555
|
+
"net.unmock": {
|
|
556
|
+
description: "Remove one agent mock by id, or every agent mock when no id. Returns how many.",
|
|
557
|
+
run: (id) => removeMocks(state, (m) => isAgent(m) && (id === void 0 || m.id === id))
|
|
558
|
+
},
|
|
559
|
+
"net.clear": {
|
|
560
|
+
description: "Empty the request log. Returns how many entries it held.",
|
|
561
|
+
run: () => state.log.splice(0).length
|
|
562
|
+
},
|
|
563
|
+
"net.restore": {
|
|
564
|
+
description: "Undo the agent's network changes: remove agent mocks, keep the app's. Returns how many were removed.",
|
|
565
|
+
run: () => removeMocks(state, isAgent)
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|