@avasapp/agent-bridge 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -11
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +12 -3
- package/dist/{chunk-AJD3XTNT.js → chunk-3QIG7A5W.js} +292 -161
- package/dist/cli.js +1216 -22
- package/dist/client/index.cjs +255 -130
- package/dist/client/index.js +1 -1
- package/dist/runtime/index.cjs +102 -32
- package/dist/runtime/index.d.cts +2 -0
- package/package.json +3 -2
- package/skills/agent-bridge/SKILL.md +67 -252
- package/skills/agent-bridge/references/flows.md +66 -0
- package/skills/agent-bridge/references/large-data.md +38 -0
- package/skills/agent-bridge/references/network.md +34 -0
- package/skills/agent-bridge/references/realtime.md +20 -0
- package/skills/agent-bridge/references/restore.md +52 -0
- package/skills/agent-bridge/references/screen.md +38 -0
- package/skills/agent-bridge/references/state.md +48 -0
- package/skills/agent-bridge/references/webview.md +42 -0
- package/skills/agent-bridge/references/writing-scenarios.md +97 -0
package/dist/client/index.cjs
CHANGED
|
@@ -76,11 +76,11 @@ async function listCdpTargets(metro, signal) {
|
|
|
76
76
|
);
|
|
77
77
|
return await res.json();
|
|
78
78
|
}
|
|
79
|
-
function pickOne(items, filter,
|
|
79
|
+
function pickOne(items, filter, label2) {
|
|
80
80
|
const matching = filter ? items.filter(
|
|
81
|
-
(item) =>
|
|
81
|
+
(item) => label2(item).toLowerCase().includes(filter.toLowerCase())
|
|
82
82
|
) : items;
|
|
83
|
-
const connected = items.map(
|
|
83
|
+
const connected = items.map(label2).join("; ") || "none";
|
|
84
84
|
if (matching.length === 0) {
|
|
85
85
|
throw new Error(
|
|
86
86
|
filter ? `No app matches "${filter}". Connected: ${connected}` : "No app is connected to Metro."
|
|
@@ -91,6 +91,15 @@ function pickOne(items, filter, label) {
|
|
|
91
91
|
`${matching.length} apps are connected; pick one with --device. Connected: ${connected}`
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
|
+
if (matching.length > 1) {
|
|
95
|
+
const exact = matching.filter(
|
|
96
|
+
(item) => label2(item).toLowerCase() === filter?.toLowerCase()
|
|
97
|
+
);
|
|
98
|
+
if (exact.length === 1) return exact[0];
|
|
99
|
+
throw new Error(
|
|
100
|
+
`${matching.length} apps match "${filter}"; narrow --device. Matching: ${matching.map(label2).join("; ")}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
94
103
|
return matching[0];
|
|
95
104
|
}
|
|
96
105
|
|
|
@@ -107,6 +116,7 @@ function toAsciiJson(value) {
|
|
|
107
116
|
|
|
108
117
|
// src/client/connection.ts
|
|
109
118
|
var import_ws = __toESM(require("ws"), 1);
|
|
119
|
+
var describePin = (pin) => `${pin.name} (${pin.platform}${pin.target ? `, ${pin.target}` : ""})`;
|
|
110
120
|
var OPEN_SOCKET_MS = 5e3;
|
|
111
121
|
var OpenGaveUp = class extends Error {
|
|
112
122
|
};
|
|
@@ -232,7 +242,7 @@ async function listExpoDevices(metro, discoveryMs = 600) {
|
|
|
232
242
|
ws.close();
|
|
233
243
|
return [...devices.values()];
|
|
234
244
|
}
|
|
235
|
-
async function connectExpo(metro, device, discoveryMs = 600, signal) {
|
|
245
|
+
async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
|
|
236
246
|
const { ws, devices, pending, send, announced } = await openBroadcast(
|
|
237
247
|
metro,
|
|
238
248
|
discoveryMs,
|
|
@@ -240,7 +250,13 @@ async function connectExpo(metro, device, discoveryMs = 600, signal) {
|
|
|
240
250
|
);
|
|
241
251
|
let info;
|
|
242
252
|
try {
|
|
243
|
-
|
|
253
|
+
const all = [...devices.values()];
|
|
254
|
+
const candidates = pin ? all.filter((d) => d.name === pin.name && d.platform === pin.platform) : all;
|
|
255
|
+
if (pin && !candidates.length)
|
|
256
|
+
throw new Error(
|
|
257
|
+
`${describePin(pin)} is not connected; connected: ${all.map(deviceLabel).join("; ") || "none"}`
|
|
258
|
+
);
|
|
259
|
+
info = pickOne(candidates, device, deviceLabel);
|
|
244
260
|
} catch (error) {
|
|
245
261
|
ws.close();
|
|
246
262
|
throw error;
|
|
@@ -255,6 +271,7 @@ async function connectExpo(metro, device, discoveryMs = 600, signal) {
|
|
|
255
271
|
return {
|
|
256
272
|
transport: "expo",
|
|
257
273
|
device: info,
|
|
274
|
+
pin: { name: info.name, platform: info.platform },
|
|
258
275
|
call(tool, args, timeoutMs) {
|
|
259
276
|
if (superseded)
|
|
260
277
|
return Promise.resolve({
|
|
@@ -278,17 +295,219 @@ async function connectExpo(metro, device, discoveryMs = 600, signal) {
|
|
|
278
295
|
};
|
|
279
296
|
}
|
|
280
297
|
|
|
298
|
+
// src/client/session/state.ts
|
|
299
|
+
var import_node_crypto = require("crypto");
|
|
300
|
+
var import_node_fs = require("fs");
|
|
301
|
+
var import_node_os = require("os");
|
|
302
|
+
var import_node_path = require("path");
|
|
303
|
+
function projectRoot(from = process.cwd()) {
|
|
304
|
+
for (let dir = (0, import_node_path.resolve)(from); ; dir = (0, import_node_path.dirname)(dir)) {
|
|
305
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
|
|
306
|
+
if ((0, import_node_path.dirname)(dir) === dir) return (0, import_node_path.resolve)(from);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
var uid = () => process.getuid?.() ?? (0, import_node_os.userInfo)().username;
|
|
310
|
+
function privateDir(dir) {
|
|
311
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
312
|
+
const stat = (0, import_node_fs.lstatSync)(dir);
|
|
313
|
+
if (!stat.isDirectory())
|
|
314
|
+
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
315
|
+
if (process.getuid && stat.uid !== process.getuid())
|
|
316
|
+
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
317
|
+
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
318
|
+
(0, import_node_fs.chmodSync)(dir, 448);
|
|
319
|
+
return dir;
|
|
320
|
+
}
|
|
321
|
+
function stateDir() {
|
|
322
|
+
return privateDir(
|
|
323
|
+
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? (0, import_node_path.join)(process.env.XDG_RUNTIME_DIR, "agent-bridge") : (0, import_node_path.join)((0, import_node_os.tmpdir)(), `agent-bridge-${uid()}`))
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
var shortHash = (text) => (0, import_node_crypto.createHash)("sha256").update(text).digest("hex").slice(0, 16);
|
|
327
|
+
function socketPath(name, dir) {
|
|
328
|
+
if (process.platform === "win32")
|
|
329
|
+
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
330
|
+
const path = (0, import_node_path.join)(dir, `${name}.sock`);
|
|
331
|
+
if (Buffer.byteLength(path) <= 100) return path;
|
|
332
|
+
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
333
|
+
return (0, import_node_path.join)(short, `${shortHash(path)}.sock`);
|
|
334
|
+
}
|
|
335
|
+
var sessionFiles = (name, dir = stateDir()) => ({
|
|
336
|
+
state: (0, import_node_path.join)(dir, `${name}.json`),
|
|
337
|
+
error: (0, import_node_path.join)(dir, `${name}.error`),
|
|
338
|
+
log: (0, import_node_path.join)(dir, `${name}.log`),
|
|
339
|
+
socket: socketPath(name, dir)
|
|
340
|
+
});
|
|
341
|
+
function checkName(name) {
|
|
342
|
+
if (!/^[\w.-]{1,40}$/.test(name))
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
345
|
+
);
|
|
346
|
+
return name;
|
|
347
|
+
}
|
|
348
|
+
function isAlive(pid) {
|
|
349
|
+
try {
|
|
350
|
+
process.kill(pid, 0);
|
|
351
|
+
return true;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return error.code === "EPERM";
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function removeSessionFiles(name, dir = stateDir()) {
|
|
357
|
+
const files = sessionFiles(name, dir);
|
|
358
|
+
(0, import_node_fs.rmSync)(files.state, { force: true });
|
|
359
|
+
if (process.platform !== "win32") (0, import_node_fs.rmSync)(files.socket, { force: true });
|
|
360
|
+
}
|
|
361
|
+
function readState(file) {
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function listSessions(dir = stateDir()) {
|
|
369
|
+
const live = [];
|
|
370
|
+
for (const entry of (0, import_node_fs.readdirSync)(dir)) {
|
|
371
|
+
if (!entry.endsWith(".json")) continue;
|
|
372
|
+
const state = readState((0, import_node_path.join)(dir, entry));
|
|
373
|
+
if (!state) continue;
|
|
374
|
+
if (isAlive(state.pid)) live.push(state);
|
|
375
|
+
else removeSessionFiles(state.name, dir);
|
|
376
|
+
}
|
|
377
|
+
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
378
|
+
}
|
|
379
|
+
function readSession(name, dir = stateDir()) {
|
|
380
|
+
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
381
|
+
}
|
|
382
|
+
function pickSession(filter) {
|
|
383
|
+
if (filter.name) {
|
|
384
|
+
const named = readSession(checkName(filter.name));
|
|
385
|
+
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
386
|
+
return named;
|
|
387
|
+
}
|
|
388
|
+
const device = filter.device?.toLowerCase();
|
|
389
|
+
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
390
|
+
const project = projectRoot();
|
|
391
|
+
const matching = listSessions().filter(
|
|
392
|
+
(s) => (explicit || s.project === project) && (!filter.metro || s.metro === filter.metro) && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
|
|
393
|
+
);
|
|
394
|
+
if (matching.length > 1)
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Several sessions match (${matching.map((s) => `${s.name}: ${s.device.name} on ${s.metro}`).join("; ")}); pick one by name (--session, or --name for session stop/status), or narrow with --metro/--device`
|
|
397
|
+
);
|
|
398
|
+
return matching[0] ?? null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/client/metro-hint.ts
|
|
402
|
+
var range = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
403
|
+
var probe = {
|
|
404
|
+
ports: [...range(8081, 8090), ...range(8100, 8199)],
|
|
405
|
+
eachMs: 300,
|
|
406
|
+
totalMs: 1e3
|
|
407
|
+
};
|
|
408
|
+
var hostOf = (metro) => {
|
|
409
|
+
const colon = metro.lastIndexOf(":");
|
|
410
|
+
return colon > 0 && !metro.endsWith("]") ? metro.slice(0, colon) : metro;
|
|
411
|
+
};
|
|
412
|
+
async function answers(metro, signal) {
|
|
413
|
+
try {
|
|
414
|
+
const res = await fetch(`http://${metro}/status`, { signal });
|
|
415
|
+
return (await res.text()).includes("packager-status:running");
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function appsOn(metro, signal) {
|
|
421
|
+
try {
|
|
422
|
+
const res = await fetch(`http://${metro}/json/list`, { signal });
|
|
423
|
+
const targets = await res.json();
|
|
424
|
+
const names = targets.map(
|
|
425
|
+
(t) => `${t.title ?? "?"}${t.deviceName ? ` [${t.deviceName}]` : ""}`
|
|
426
|
+
);
|
|
427
|
+
return [...new Set(names)];
|
|
428
|
+
} catch {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function findMetros(metro) {
|
|
433
|
+
const host = hostOf(metro);
|
|
434
|
+
const candidates = new Set(probe.ports.map((port) => `${host}:${port}`));
|
|
435
|
+
try {
|
|
436
|
+
for (const s of listSessions()) candidates.add(s.metro);
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
candidates.delete(metro);
|
|
440
|
+
const deadline = AbortSignal.timeout(probe.totalMs);
|
|
441
|
+
const within2 = () => AbortSignal.any([deadline, AbortSignal.timeout(probe.eachMs)]);
|
|
442
|
+
const running = (await Promise.all(
|
|
443
|
+
[...candidates].map(async (m) => await answers(m, within2()) ? m : null)
|
|
444
|
+
)).filter((m) => m !== null);
|
|
445
|
+
return Promise.all(
|
|
446
|
+
running.map(async (m) => ({ metro: m, apps: await appsOn(m, within2()) }))
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
var label = (found) => `${found.metro} (${found.apps.length ? `${found.apps.length === 1 ? "app" : "apps"}: ${found.apps.join(", ")}` : "no app connected"})`;
|
|
450
|
+
var howToSet = (metro) => `Pass --metro ${metro} or set AGENT_BRIDGE_METRO=${metro}.`;
|
|
451
|
+
function elsewhere(found) {
|
|
452
|
+
const [only] = found;
|
|
453
|
+
if (found.length === 1 && only)
|
|
454
|
+
return `Metro is running on ${label(only)}. ${howToSet(only.metro)}`;
|
|
455
|
+
return `Metro is running on ${found.map(label).join("; ")}. ${howToSet("<host:port>")}`;
|
|
456
|
+
}
|
|
457
|
+
var errorText = (error) => {
|
|
458
|
+
if (!(error instanceof Error)) return String(error);
|
|
459
|
+
const code = error.code ?? "";
|
|
460
|
+
return `${error.message} ${code} ${error.cause ? errorText(error.cause) : ""}`;
|
|
461
|
+
};
|
|
462
|
+
var isRefused = (error) => /ECONNREFUSED|ConnectionRefused/.test(errorText(error));
|
|
463
|
+
var NO_APP = /No app is connected to Metro|No app matches/;
|
|
464
|
+
async function refusedMessage(metro) {
|
|
465
|
+
const found = await findMetros(metro);
|
|
466
|
+
if (found.length)
|
|
467
|
+
return `Nothing is listening on ${metro}. ${elsewhere(found)}`;
|
|
468
|
+
const host = hostOf(metro);
|
|
469
|
+
return `Nothing is listening on ${metro}, and no Metro answered on ${host} ports 8081-8090 or 8100-8199. Start Metro, or if it runs elsewhere, pass --metro <host:port> or set AGENT_BRIDGE_METRO=<host:port>.`;
|
|
470
|
+
}
|
|
471
|
+
async function noAppHint(metro, device) {
|
|
472
|
+
const causes = `Likely: the app isn't open or hasn't reloaded since the bridge was mounted, it uses another Metro${device ? `, or --device "${device}" doesn't match` : ""}.`;
|
|
473
|
+
const withApps = (await findMetros(metro)).filter((f) => f.apps.length);
|
|
474
|
+
return withApps.length ? `${causes} ${elsewhere(withApps)}` : causes;
|
|
475
|
+
}
|
|
476
|
+
async function refused(metro) {
|
|
477
|
+
try {
|
|
478
|
+
await fetch(`http://${metro}/status`, {
|
|
479
|
+
signal: AbortSignal.timeout(probe.eachMs)
|
|
480
|
+
});
|
|
481
|
+
return false;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return isRefused(error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function explainConnectError(error, metro, device) {
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
if (NO_APP.test(message))
|
|
489
|
+
return new Error(`${message}
|
|
490
|
+
${await noAppHint(metro, device)}`, {
|
|
491
|
+
cause: error
|
|
492
|
+
});
|
|
493
|
+
if (await refused(metro))
|
|
494
|
+
return new Error(await refusedMessage(metro), { cause: error });
|
|
495
|
+
return error;
|
|
496
|
+
}
|
|
497
|
+
|
|
281
498
|
// src/client/cdp.ts
|
|
282
499
|
var import_ws2 = __toESM(require("ws"), 1);
|
|
283
500
|
var CannotEvaluate = class extends Error {
|
|
284
501
|
};
|
|
285
502
|
var targetLabel = (t) => `${t.title}${t.deviceName ? ` [${t.deviceName}]` : ""}`;
|
|
286
|
-
async function connectCdp(metro, device, signal) {
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
503
|
+
async function connectCdp(metro, device, signal, pin) {
|
|
504
|
+
const targets = await listCdpTargets(metro, signal);
|
|
505
|
+
const candidates = pin?.target ? targets.filter((t) => targetLabel(t) === pin.target) : targets;
|
|
506
|
+
if (pin && !candidates.length)
|
|
507
|
+
throw new Error(
|
|
508
|
+
`${describePin(pin)} is not connected; connected: ${targets.map(targetLabel).join("; ") || "none"}`
|
|
509
|
+
);
|
|
510
|
+
const target = pickOne(candidates, device, targetLabel);
|
|
292
511
|
const hostUri = await expoHostUri(metro, signal);
|
|
293
512
|
const port = metro.split(":")[1] ?? "8081";
|
|
294
513
|
const origin = hostUri ? `http://${hostUri}` : `http://localhost:${port}`;
|
|
@@ -366,27 +585,34 @@ async function connectCdp(metro, device, signal) {
|
|
|
366
585
|
);
|
|
367
586
|
}
|
|
368
587
|
signal?.removeEventListener("abort", dropOnAbort);
|
|
588
|
+
if (pin && (info.name !== pin.name || info.platform !== pin.platform)) {
|
|
589
|
+
ws.terminate();
|
|
590
|
+
throw new Error(
|
|
591
|
+
`${describePin(pin)} is not connected; connected: ${targetLabel(target)} runs ${info.name} (${info.platform})`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
369
594
|
return {
|
|
370
595
|
transport: "cdp",
|
|
596
|
+
pin: {
|
|
597
|
+
name: info.name,
|
|
598
|
+
platform: info.platform,
|
|
599
|
+
target: targetLabel(target)
|
|
600
|
+
},
|
|
371
601
|
device: { ...info, name: `${info.name} (${targetLabel(target)})` },
|
|
372
|
-
|
|
602
|
+
call(tool, args, timeoutMs) {
|
|
373
603
|
const call = { id: newCallId(), tool, args };
|
|
374
604
|
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
`${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
|
|
380
|
-
);
|
|
381
|
-
} catch (error) {
|
|
382
|
-
pending.settle({
|
|
605
|
+
evaluate(
|
|
606
|
+
`${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
|
|
607
|
+
).catch(
|
|
608
|
+
(error) => pending.settle({
|
|
383
609
|
id: call.id,
|
|
384
610
|
from: info.deviceId,
|
|
385
611
|
ok: false,
|
|
386
612
|
error: String(error),
|
|
387
613
|
ms: 0
|
|
388
|
-
})
|
|
389
|
-
|
|
614
|
+
})
|
|
615
|
+
);
|
|
390
616
|
return reply;
|
|
391
617
|
},
|
|
392
618
|
close: () => ws.close()
|
|
@@ -429,7 +655,7 @@ async function openConnection(options = {}) {
|
|
|
429
655
|
try {
|
|
430
656
|
return await phase(
|
|
431
657
|
options,
|
|
432
|
-
(signal) => connectExpo(metro, options.device, void 0, signal)
|
|
658
|
+
(signal) => connectExpo(metro, options.device, void 0, signal, options.pin)
|
|
433
659
|
);
|
|
434
660
|
} catch (error) {
|
|
435
661
|
if (want === "expo" || options.signal?.aborted) throw error;
|
|
@@ -439,7 +665,7 @@ async function openConnection(options = {}) {
|
|
|
439
665
|
try {
|
|
440
666
|
return await phase(
|
|
441
667
|
options,
|
|
442
|
-
(signal) => connectCdp(metro, options.device, signal)
|
|
668
|
+
(signal) => connectCdp(metro, options.device, signal, options.pin)
|
|
443
669
|
);
|
|
444
670
|
} catch (error) {
|
|
445
671
|
const expoNote = expoError ? ` (Expo socket: ${String(expoError)})` : "";
|
|
@@ -503,7 +729,7 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
503
729
|
}
|
|
504
730
|
};
|
|
505
731
|
const call = async (tool, ...args) => (await timed(tool, ...args)).value;
|
|
506
|
-
const step = async (
|
|
732
|
+
const step = async (label2, tool, ...given) => {
|
|
507
733
|
const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
|
|
508
734
|
const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
|
|
509
735
|
report(failedLogs(error), failedNotice(error));
|
|
@@ -511,7 +737,7 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
511
737
|
});
|
|
512
738
|
total += ms;
|
|
513
739
|
print(
|
|
514
|
-
`${String(++n).padStart(2, "0")} ${
|
|
740
|
+
`${String(++n).padStart(2, "0")} ${label2.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
|
|
515
741
|
);
|
|
516
742
|
report(logs, notice);
|
|
517
743
|
return value;
|
|
@@ -563,109 +789,6 @@ var import_node_readline = require("readline");
|
|
|
563
789
|
var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16)}`)}
|
|
564
790
|
`;
|
|
565
791
|
|
|
566
|
-
// src/client/session/state.ts
|
|
567
|
-
var import_node_crypto = require("crypto");
|
|
568
|
-
var import_node_fs = require("fs");
|
|
569
|
-
var import_node_os = require("os");
|
|
570
|
-
var import_node_path = require("path");
|
|
571
|
-
function projectRoot(from = process.cwd()) {
|
|
572
|
-
for (let dir = (0, import_node_path.resolve)(from); ; dir = (0, import_node_path.dirname)(dir)) {
|
|
573
|
-
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
|
|
574
|
-
if ((0, import_node_path.dirname)(dir) === dir) return (0, import_node_path.resolve)(from);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
var uid = () => process.getuid?.() ?? (0, import_node_os.userInfo)().username;
|
|
578
|
-
function privateDir(dir) {
|
|
579
|
-
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
580
|
-
const stat = (0, import_node_fs.lstatSync)(dir);
|
|
581
|
-
if (!stat.isDirectory())
|
|
582
|
-
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
583
|
-
if (process.getuid && stat.uid !== process.getuid())
|
|
584
|
-
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
585
|
-
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
586
|
-
(0, import_node_fs.chmodSync)(dir, 448);
|
|
587
|
-
return dir;
|
|
588
|
-
}
|
|
589
|
-
function stateDir() {
|
|
590
|
-
return privateDir(
|
|
591
|
-
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? (0, import_node_path.join)(process.env.XDG_RUNTIME_DIR, "agent-bridge") : (0, import_node_path.join)((0, import_node_os.tmpdir)(), `agent-bridge-${uid()}`))
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
var shortHash = (text) => (0, import_node_crypto.createHash)("sha256").update(text).digest("hex").slice(0, 16);
|
|
595
|
-
function socketPath(name, dir) {
|
|
596
|
-
if (process.platform === "win32")
|
|
597
|
-
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
598
|
-
const path = (0, import_node_path.join)(dir, `${name}.sock`);
|
|
599
|
-
if (Buffer.byteLength(path) <= 100) return path;
|
|
600
|
-
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
601
|
-
return (0, import_node_path.join)(short, `${shortHash(path)}.sock`);
|
|
602
|
-
}
|
|
603
|
-
var sessionFiles = (name, dir = stateDir()) => ({
|
|
604
|
-
state: (0, import_node_path.join)(dir, `${name}.json`),
|
|
605
|
-
error: (0, import_node_path.join)(dir, `${name}.error`),
|
|
606
|
-
log: (0, import_node_path.join)(dir, `${name}.log`),
|
|
607
|
-
socket: socketPath(name, dir)
|
|
608
|
-
});
|
|
609
|
-
function checkName(name) {
|
|
610
|
-
if (!/^[\w.-]{1,40}$/.test(name))
|
|
611
|
-
throw new Error(
|
|
612
|
-
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
613
|
-
);
|
|
614
|
-
return name;
|
|
615
|
-
}
|
|
616
|
-
function isAlive(pid) {
|
|
617
|
-
try {
|
|
618
|
-
process.kill(pid, 0);
|
|
619
|
-
return true;
|
|
620
|
-
} catch (error) {
|
|
621
|
-
return error.code === "EPERM";
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
function removeSessionFiles(name, dir = stateDir()) {
|
|
625
|
-
const files = sessionFiles(name, dir);
|
|
626
|
-
(0, import_node_fs.rmSync)(files.state, { force: true });
|
|
627
|
-
if (process.platform !== "win32") (0, import_node_fs.rmSync)(files.socket, { force: true });
|
|
628
|
-
}
|
|
629
|
-
function readState(file) {
|
|
630
|
-
try {
|
|
631
|
-
return JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
632
|
-
} catch {
|
|
633
|
-
return null;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
function listSessions(dir = stateDir()) {
|
|
637
|
-
const live = [];
|
|
638
|
-
for (const entry of (0, import_node_fs.readdirSync)(dir)) {
|
|
639
|
-
if (!entry.endsWith(".json")) continue;
|
|
640
|
-
const state = readState((0, import_node_path.join)(dir, entry));
|
|
641
|
-
if (!state) continue;
|
|
642
|
-
if (isAlive(state.pid)) live.push(state);
|
|
643
|
-
else removeSessionFiles(state.name, dir);
|
|
644
|
-
}
|
|
645
|
-
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
646
|
-
}
|
|
647
|
-
function readSession(name, dir = stateDir()) {
|
|
648
|
-
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
649
|
-
}
|
|
650
|
-
function pickSession(filter) {
|
|
651
|
-
if (filter.name) {
|
|
652
|
-
const named = readSession(checkName(filter.name));
|
|
653
|
-
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
654
|
-
return named;
|
|
655
|
-
}
|
|
656
|
-
const device = filter.device?.toLowerCase();
|
|
657
|
-
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
658
|
-
const project = projectRoot();
|
|
659
|
-
const matching = listSessions().filter(
|
|
660
|
-
(s) => (explicit || s.project === project) && (!filter.metro || s.metro === filter.metro) && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
|
|
661
|
-
);
|
|
662
|
-
if (matching.length > 1)
|
|
663
|
-
throw new Error(
|
|
664
|
-
`Several sessions match (${matching.map((s) => `${s.name}: ${s.device.name} on ${s.metro}`).join("; ")}); pick one by name (--session, or --name for session stop/status), or narrow with --metro/--device`
|
|
665
|
-
);
|
|
666
|
-
return matching[0] ?? null;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
792
|
// src/client/session/client.ts
|
|
670
793
|
function openSessionLink(state) {
|
|
671
794
|
return new Promise((resolve2, reject) => {
|
|
@@ -787,7 +910,9 @@ Warning: ${notice}` : ""}`);
|
|
|
787
910
|
};
|
|
788
911
|
async function connect(options = {}) {
|
|
789
912
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
790
|
-
const conn = await openConnection(options)
|
|
913
|
+
const conn = await openConnection(options).catch(async (error) => {
|
|
914
|
+
throw await explainConnectError(error, metroHost(options.metro), options.device);
|
|
915
|
+
});
|
|
791
916
|
const timed = async (tool, ...args) => {
|
|
792
917
|
const t0 = performance.now();
|
|
793
918
|
const result = await conn.call(tool, args, timeoutMs);
|