local-browser-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/README.md +724 -0
- package/dist/package.json +61 -0
- package/dist/src/browser/chrome.d.ts +19 -0
- package/dist/src/browser/chrome.js +778 -0
- package/dist/src/browser/index.d.ts +3 -0
- package/dist/src/browser/index.js +25 -0
- package/dist/src/browser/safari.d.ts +41 -0
- package/dist/src/browser/safari.js +827 -0
- package/dist/src/browser-attach-ux-helper.d.ts +39 -0
- package/dist/src/browser-attach-ux-helper.js +157 -0
- package/dist/src/capabilities.d.ts +3 -0
- package/dist/src/capabilities.js +182 -0
- package/dist/src/chrome-relay-error-helper.d.ts +19 -0
- package/dist/src/chrome-relay-error-helper.js +78 -0
- package/dist/src/chrome-relay-helper-cli.d.ts +2 -0
- package/dist/src/chrome-relay-helper-cli.js +97 -0
- package/dist/src/chrome-relay-helper.d.ts +29 -0
- package/dist/src/chrome-relay-helper.js +151 -0
- package/dist/src/chrome-relay-state.d.ts +23 -0
- package/dist/src/chrome-relay-state.js +108 -0
- package/dist/src/claude-code.d.ts +20 -0
- package/dist/src/claude-code.js +66 -0
- package/dist/src/cli-reference-adapter.d.ts +13 -0
- package/dist/src/cli-reference-adapter.js +48 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.js +200 -0
- package/dist/src/codex.d.ts +17 -0
- package/dist/src/codex.js +25 -0
- package/dist/src/connection-ux.d.ts +61 -0
- package/dist/src/connection-ux.js +256 -0
- package/dist/src/errors.d.ts +12 -0
- package/dist/src/errors.js +58 -0
- package/dist/src/http-reference-adapter.d.ts +34 -0
- package/dist/src/http-reference-adapter.js +61 -0
- package/dist/src/http.d.ts +3 -0
- package/dist/src/http.js +161 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +43 -0
- package/dist/src/mcp-stdio.d.ts +2 -0
- package/dist/src/mcp-stdio.js +10 -0
- package/dist/src/mcp.d.ts +25 -0
- package/dist/src/mcp.js +483 -0
- package/dist/src/reference-adapter.d.ts +32 -0
- package/dist/src/reference-adapter.js +42 -0
- package/dist/src/service/attach-service.d.ts +28 -0
- package/dist/src/service/attach-service.js +272 -0
- package/dist/src/session-metadata.d.ts +4 -0
- package/dist/src/session-metadata.js +88 -0
- package/dist/src/store/session-store.d.ts +14 -0
- package/dist/src/store/session-store.js +52 -0
- package/dist/src/target.d.ts +9 -0
- package/dist/src/target.js +61 -0
- package/dist/src/types.d.ts +397 -0
- package/dist/src/types.js +2 -0
- package/dist/tests/attach-service.test.d.ts +1 -0
- package/dist/tests/attach-service.test.js +1367 -0
- package/dist/tests/browser-attach-ux-helper.test.d.ts +1 -0
- package/dist/tests/browser-attach-ux-helper.test.js +139 -0
- package/dist/tests/chrome-relay-error-helper.test.d.ts +1 -0
- package/dist/tests/chrome-relay-error-helper.test.js +67 -0
- package/dist/tests/chrome-relay-helper.test.d.ts +1 -0
- package/dist/tests/chrome-relay-helper.test.js +142 -0
- package/dist/tests/chrome-relay-state-schema.test.d.ts +1 -0
- package/dist/tests/chrome-relay-state-schema.test.js +96 -0
- package/dist/tests/claude-code-wrapper.test.d.ts +1 -0
- package/dist/tests/claude-code-wrapper.test.js +170 -0
- package/dist/tests/codex.test.d.ts +1 -0
- package/dist/tests/codex.test.js +210 -0
- package/dist/tests/demo-client-smoke.test.d.ts +1 -0
- package/dist/tests/demo-client-smoke.test.js +405 -0
- package/dist/tests/docs-fixtures.test.d.ts +1 -0
- package/dist/tests/docs-fixtures.test.js +255 -0
- package/dist/tests/doctor-connect-wrapper.test.d.ts +1 -0
- package/dist/tests/doctor-connect-wrapper.test.js +62 -0
- package/dist/tests/fixtures/doctor-connect-cli-stub.d.ts +1 -0
- package/dist/tests/fixtures/doctor-connect-cli-stub.js +93 -0
- package/dist/tests/fixtures/public-root-cli-stub.d.ts +210 -0
- package/dist/tests/fixtures/public-root-cli-stub.js +143 -0
- package/dist/tests/fixtures/public-root-consumer.js +67 -0
- package/dist/tests/mcp.test.d.ts +1 -0
- package/dist/tests/mcp.test.js +345 -0
- package/dist/tests/public-consumer-helpers.test.d.ts +1 -0
- package/dist/tests/public-consumer-helpers.test.js +33 -0
- package/dist/tests/public-package-git-consumption.test.d.ts +1 -0
- package/dist/tests/public-package-git-consumption.test.js +56 -0
- package/dist/tests/public-root-consumer-smoke.test.d.ts +1 -0
- package/dist/tests/public-root-consumer-smoke.test.js +214 -0
- package/dist/tests/reference-adapter.test.d.ts +1 -0
- package/dist/tests/reference-adapter.test.js +220 -0
- package/dist/tests/transport-reference-adapters.test.d.ts +1 -0
- package/dist/tests/transport-reference-adapters.test.js +214 -0
- package/package.json +61 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChromeAdapter = void 0;
|
|
4
|
+
exports.resolveChromeRelayAttach = resolveChromeRelayAttach;
|
|
5
|
+
exports.resumeChromeRelaySession = resumeChromeRelaySession;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_os_1 = require("node:os");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const promises_1 = require("node:fs/promises");
|
|
11
|
+
const node_util_1 = require("node:util");
|
|
12
|
+
const errors_1 = require("../errors");
|
|
13
|
+
const session_metadata_1 = require("../session-metadata");
|
|
14
|
+
const target_1 = require("../target");
|
|
15
|
+
const chrome_relay_state_1 = require("../chrome-relay-state");
|
|
16
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
17
|
+
const CHROME_PREFIX = "Chrome/Chromium session actions are not implemented in this phase.";
|
|
18
|
+
const DEFAULT_DEBUG_PORTS = [9222, 9223, 9333];
|
|
19
|
+
const DEVTOOLS_VERSION_PATH = "/json/version";
|
|
20
|
+
const DEVTOOLS_LIST_PATH = "/json/list";
|
|
21
|
+
const DEBUG_URL_ENV = "LOCAL_BROWSER_BRIDGE_CHROME_DEBUG_URL";
|
|
22
|
+
const RELAY_STATE_PATH_ENV = "LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH";
|
|
23
|
+
const DEFAULT_RELAY_STATE_PATHS = [
|
|
24
|
+
(0, node_path_1.join)(process.cwd(), ".local-browser-bridge", "chrome-relay-state.json"),
|
|
25
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), ".local-browser-bridge", "chrome-relay-state.json")
|
|
26
|
+
];
|
|
27
|
+
function normalizeTitle(title) {
|
|
28
|
+
return title.trim().replace(/\s+/g, " ").toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
function normalizeUrl(rawUrl) {
|
|
31
|
+
try {
|
|
32
|
+
return new URL(rawUrl);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function createIdentity(native, url, title) {
|
|
39
|
+
const parsedUrl = normalizeUrl(url);
|
|
40
|
+
const origin = parsedUrl?.origin ?? "";
|
|
41
|
+
const pathname = parsedUrl?.pathname ?? "";
|
|
42
|
+
const urlKey = parsedUrl ? `${parsedUrl.origin}${parsedUrl.pathname}${parsedUrl.search}` : url.trim();
|
|
43
|
+
const titleKey = normalizeTitle(title);
|
|
44
|
+
const signature = (0, node_crypto_1.createHash)("sha256")
|
|
45
|
+
.update(JSON.stringify({ browser: "chrome", targetId: native.targetId, urlKey, titleKey }))
|
|
46
|
+
.digest("hex")
|
|
47
|
+
.slice(0, 24);
|
|
48
|
+
return {
|
|
49
|
+
signature,
|
|
50
|
+
urlKey,
|
|
51
|
+
titleKey,
|
|
52
|
+
origin,
|
|
53
|
+
pathname,
|
|
54
|
+
native
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function refreshRelaySession(session, tab, probe) {
|
|
58
|
+
return (0, session_metadata_1.normalizeAttachmentSession)({
|
|
59
|
+
...session,
|
|
60
|
+
target: (0, target_1.buildSignatureTargetFromTab)(tab),
|
|
61
|
+
tab,
|
|
62
|
+
frontTab: tab,
|
|
63
|
+
attach: {
|
|
64
|
+
...session.attach,
|
|
65
|
+
mode: "relay",
|
|
66
|
+
source: "extension-relay",
|
|
67
|
+
scope: "tab",
|
|
68
|
+
trustedAt: probe.updatedAt,
|
|
69
|
+
resumable: probe.resumable,
|
|
70
|
+
expiresAt: probe.expiresAt,
|
|
71
|
+
resumeRequiresUserGesture: probe.resumeRequiresUserGesture
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function toTabMetadata(target, index) {
|
|
76
|
+
const title = String(target.title ?? "");
|
|
77
|
+
const url = String(target.url ?? "");
|
|
78
|
+
const targetId = String(target.id ?? `tab-${index + 1}`);
|
|
79
|
+
const native = {
|
|
80
|
+
kind: "chrome-devtools-target",
|
|
81
|
+
targetId,
|
|
82
|
+
targetType: typeof target.type === "string" ? target.type : undefined,
|
|
83
|
+
attached: typeof target.attached === "boolean" ? target.attached : undefined,
|
|
84
|
+
openerId: typeof target.openerId === "string" ? target.openerId : undefined,
|
|
85
|
+
browserContextId: typeof target.browserContextId === "string" ? target.browserContextId : undefined
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
browser: "chrome",
|
|
89
|
+
windowIndex: 1,
|
|
90
|
+
tabIndex: index + 1,
|
|
91
|
+
title,
|
|
92
|
+
url,
|
|
93
|
+
attachedAt: new Date().toISOString(),
|
|
94
|
+
identity: createIdentity(native, url, title),
|
|
95
|
+
isFrontWindow: index === 0,
|
|
96
|
+
isActiveInWindow: index === 0
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function commandAvailable(command) {
|
|
100
|
+
try {
|
|
101
|
+
await execFileAsync("sh", ["-lc", `command -v ${command}`]);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function fetchJson(url) {
|
|
109
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1200) });
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(`HTTP ${response.status} from ${url}`);
|
|
112
|
+
}
|
|
113
|
+
return response.json();
|
|
114
|
+
}
|
|
115
|
+
function pushCandidate(target, candidate) {
|
|
116
|
+
const key = JSON.stringify({
|
|
117
|
+
kind: candidate.kind,
|
|
118
|
+
label: candidate.label,
|
|
119
|
+
baseUrl: candidate.baseUrl,
|
|
120
|
+
devtoolsActivePortPath: candidate.devtoolsActivePortPath,
|
|
121
|
+
pid: candidate.pid,
|
|
122
|
+
port: candidate.port
|
|
123
|
+
});
|
|
124
|
+
if (!target.some((item) => JSON.stringify({
|
|
125
|
+
kind: item.kind,
|
|
126
|
+
label: item.label,
|
|
127
|
+
baseUrl: item.baseUrl,
|
|
128
|
+
devtoolsActivePortPath: item.devtoolsActivePortPath,
|
|
129
|
+
pid: item.pid,
|
|
130
|
+
port: item.port
|
|
131
|
+
}) === key)) {
|
|
132
|
+
target.push(candidate);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function chromeProfilePaths() {
|
|
136
|
+
const home = (0, node_os_1.homedir)();
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
label: "Google Chrome DevToolsActivePort",
|
|
140
|
+
path: (0, node_path_1.join)(home, "Library", "Application Support", "Google", "Chrome", "DevToolsActivePort")
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: "Google Chrome Canary DevToolsActivePort",
|
|
144
|
+
path: (0, node_path_1.join)(home, "Library", "Application Support", "Google", "Chrome Canary", "DevToolsActivePort")
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: "Chromium DevToolsActivePort",
|
|
148
|
+
path: (0, node_path_1.join)(home, "Library", "Application Support", "Chromium", "DevToolsActivePort")
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
async function discoverFromEnv() {
|
|
153
|
+
const baseUrl = process.env[DEBUG_URL_ENV]?.trim();
|
|
154
|
+
if (!baseUrl) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return [{
|
|
158
|
+
kind: "devtools-http",
|
|
159
|
+
label: `${DEBUG_URL_ENV} override`,
|
|
160
|
+
baseUrl,
|
|
161
|
+
chosen: false,
|
|
162
|
+
notes: ["Explicit override from environment variable."]
|
|
163
|
+
}];
|
|
164
|
+
}
|
|
165
|
+
async function discoverFromDevtoolsFiles() {
|
|
166
|
+
const candidates = [];
|
|
167
|
+
for (const profile of chromeProfilePaths()) {
|
|
168
|
+
try {
|
|
169
|
+
const raw = await (0, promises_1.readFile)(profile.path, "utf8");
|
|
170
|
+
const [portLine] = raw.split(/\r?\n/);
|
|
171
|
+
const port = Number(portLine?.trim());
|
|
172
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
173
|
+
pushCandidate(candidates, {
|
|
174
|
+
kind: "profile-devtools-file",
|
|
175
|
+
label: profile.label,
|
|
176
|
+
devtoolsActivePortPath: profile.path,
|
|
177
|
+
notes: ["DevToolsActivePort exists but did not contain a usable port."]
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
pushCandidate(candidates, {
|
|
182
|
+
kind: "profile-devtools-file",
|
|
183
|
+
label: profile.label,
|
|
184
|
+
devtoolsActivePortPath: profile.path,
|
|
185
|
+
port,
|
|
186
|
+
host: "127.0.0.1",
|
|
187
|
+
baseUrl: `http://127.0.0.1:${port}`
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Ignore missing/unreadable files.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return candidates;
|
|
195
|
+
}
|
|
196
|
+
function relayStateCandidatePaths() {
|
|
197
|
+
const configured = process.env[RELAY_STATE_PATH_ENV]?.trim();
|
|
198
|
+
if (configured) {
|
|
199
|
+
return [{ path: configured, source: "configured" }];
|
|
200
|
+
}
|
|
201
|
+
return DEFAULT_RELAY_STATE_PATHS.map((path) => ({ path, source: "conventional" }));
|
|
202
|
+
}
|
|
203
|
+
function toRelayProbe(raw) {
|
|
204
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const record = raw;
|
|
208
|
+
const sharedTabRaw = record.sharedTab;
|
|
209
|
+
const sharedTab = sharedTabRaw && typeof sharedTabRaw === "object" && !Array.isArray(sharedTabRaw)
|
|
210
|
+
? (() => {
|
|
211
|
+
const sharedTabRecord = sharedTabRaw;
|
|
212
|
+
return {
|
|
213
|
+
id: typeof sharedTabRecord.id === "string" ? sharedTabRecord.id : undefined,
|
|
214
|
+
url: typeof sharedTabRecord.url === "string" ? sharedTabRecord.url : undefined,
|
|
215
|
+
title: typeof sharedTabRecord.title === "string" ? sharedTabRecord.title : undefined
|
|
216
|
+
};
|
|
217
|
+
})()
|
|
218
|
+
: sharedTabRaw === null
|
|
219
|
+
? null
|
|
220
|
+
: undefined;
|
|
221
|
+
return {
|
|
222
|
+
version: typeof record.version === "string" ? record.version : undefined,
|
|
223
|
+
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : undefined,
|
|
224
|
+
extensionInstalled: typeof record.extensionInstalled === "boolean" ? record.extensionInstalled : undefined,
|
|
225
|
+
connected: typeof record.connected === "boolean" ? record.connected : undefined,
|
|
226
|
+
userGestureRequired: typeof record.userGestureRequired === "boolean" ? record.userGestureRequired : undefined,
|
|
227
|
+
shareRequired: typeof record.shareRequired === "boolean" ? record.shareRequired : undefined,
|
|
228
|
+
resumable: typeof record.resumable === "boolean" ? record.resumable : undefined,
|
|
229
|
+
expiresAt: typeof record.expiresAt === "string" ? record.expiresAt : undefined,
|
|
230
|
+
resumeRequiresUserGesture: typeof record.resumeRequiresUserGesture === "boolean" ? record.resumeRequiresUserGesture : undefined,
|
|
231
|
+
sharedTab
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function loadRelayProbe() {
|
|
235
|
+
const candidates = relayStateCandidatePaths();
|
|
236
|
+
const checkedPaths = candidates.map((candidate) => candidate.path);
|
|
237
|
+
for (const candidate of candidates) {
|
|
238
|
+
try {
|
|
239
|
+
const raw = JSON.parse(await (0, promises_1.readFile)(candidate.path, "utf8"));
|
|
240
|
+
const validation = (0, chrome_relay_state_1.validateChromeRelayState)(raw);
|
|
241
|
+
if (!validation.ok || !validation.probe) {
|
|
242
|
+
return {
|
|
243
|
+
checkedPaths,
|
|
244
|
+
sourcePath: candidate.path,
|
|
245
|
+
source: candidate.source,
|
|
246
|
+
error: "invalid"
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
checkedPaths,
|
|
251
|
+
sourcePath: candidate.path,
|
|
252
|
+
source: candidate.source,
|
|
253
|
+
probe: validation.probe
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
const message = error instanceof Error ? error.message : "";
|
|
258
|
+
if (message.includes("ENOENT")) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
checkedPaths,
|
|
263
|
+
sourcePath: candidate.path,
|
|
264
|
+
source: candidate.source,
|
|
265
|
+
error: "invalid"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
checkedPaths,
|
|
271
|
+
source: process.env[RELAY_STATE_PATH_ENV]?.trim() ? "configured" : "conventional"
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function buildRelayDiagnostics(probeResult) {
|
|
275
|
+
const blockers = [];
|
|
276
|
+
const notes = [];
|
|
277
|
+
const probe = probeResult.probe;
|
|
278
|
+
if (probeResult.sourcePath) {
|
|
279
|
+
notes.push(`Relay state probe loaded from ${probeResult.sourcePath}.`);
|
|
280
|
+
}
|
|
281
|
+
else if (probeResult.checkedPaths.length > 0) {
|
|
282
|
+
notes.push(`Relay state probe not found. Checked: ${probeResult.checkedPaths.join(", ")}.`);
|
|
283
|
+
}
|
|
284
|
+
if (probe?.version) {
|
|
285
|
+
notes.push(`Relay probe version: ${probe.version}.`);
|
|
286
|
+
}
|
|
287
|
+
if (probe?.updatedAt) {
|
|
288
|
+
notes.push(`Relay probe updatedAt: ${probe.updatedAt}.`);
|
|
289
|
+
}
|
|
290
|
+
if (probe?.expiresAt) {
|
|
291
|
+
notes.push(`Relay scope expiresAt: ${probe.expiresAt}.`);
|
|
292
|
+
}
|
|
293
|
+
if (probe?.sharedTab?.url || probe?.sharedTab?.title) {
|
|
294
|
+
notes.push(`Relay shared tab detected: ${probe.sharedTab.title ?? "untitled"} ${probe.sharedTab.url ?? ""}`.trim());
|
|
295
|
+
}
|
|
296
|
+
if (probe?.resumeRequiresUserGesture === true) {
|
|
297
|
+
notes.push("Relay resume requires the user to share the tab again.");
|
|
298
|
+
}
|
|
299
|
+
let state = "unavailable";
|
|
300
|
+
let ready = false;
|
|
301
|
+
if (probeResult.error === "invalid") {
|
|
302
|
+
blockers.push({
|
|
303
|
+
code: "relay_probe_invalid",
|
|
304
|
+
message: "The local Chrome relay state probe file exists but could not be parsed as a supported JSON object."
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
else if (!probe) {
|
|
308
|
+
blockers.push({
|
|
309
|
+
code: "relay_probe_not_configured",
|
|
310
|
+
message: "No local Chrome relay state probe was found. Set LOCAL_BROWSER_BRIDGE_CHROME_RELAY_STATE_PATH or write .local-browser-bridge/chrome-relay-state.json."
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
else if (probe.extensionInstalled === false) {
|
|
314
|
+
blockers.push({
|
|
315
|
+
code: "relay_extension_not_installed",
|
|
316
|
+
message: "The local relay probe reports that the Chrome relay extension is not installed."
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else if (probe.connected === false) {
|
|
320
|
+
blockers.push({
|
|
321
|
+
code: "relay_extension_disconnected",
|
|
322
|
+
message: "The local relay probe reports that the extension is installed but not currently connected to the bridge."
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
else if (probe.userGestureRequired === true || probe.shareRequired === true) {
|
|
326
|
+
state = "attention-required";
|
|
327
|
+
blockers.push({
|
|
328
|
+
code: probe.userGestureRequired === true ? "relay_toolbar_not_clicked" : "relay_share_required",
|
|
329
|
+
message: probe.userGestureRequired === true
|
|
330
|
+
? "The user must click the relay extension toolbar button before a tab can be shared."
|
|
331
|
+
: "The relay extension is connected, but the current tab still needs to be explicitly shared."
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
else if (!probe.sharedTab) {
|
|
335
|
+
blockers.push({
|
|
336
|
+
code: "relay_no_shared_tab",
|
|
337
|
+
message: "The relay extension is connected, but no shared tab is currently available."
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else if (isExpiredIsoTimestamp(probe.expiresAt)) {
|
|
341
|
+
blockers.push({
|
|
342
|
+
code: "relay_attach_scope_expired",
|
|
343
|
+
message: "The relayed tab scope has expired and must be shared again before attach can succeed."
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
state = "ready";
|
|
348
|
+
ready = true;
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
mode: "relay",
|
|
352
|
+
source: "extension-relay",
|
|
353
|
+
scope: "tab",
|
|
354
|
+
supported: true,
|
|
355
|
+
ready,
|
|
356
|
+
state,
|
|
357
|
+
blockers,
|
|
358
|
+
notes
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function isExpiredIsoTimestamp(value) {
|
|
362
|
+
if (!value) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
const timestamp = Date.parse(value);
|
|
366
|
+
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
|
367
|
+
}
|
|
368
|
+
function toRelayTabMetadata(probe) {
|
|
369
|
+
const title = probe.sharedTab?.title ?? "";
|
|
370
|
+
const url = probe.sharedTab?.url ?? "";
|
|
371
|
+
const targetId = probe.sharedTab?.id ?? `relay-${(0, node_crypto_1.createHash)("sha256").update(`${url}:${title}`).digest("hex").slice(0, 12)}`;
|
|
372
|
+
const native = {
|
|
373
|
+
kind: "chrome-devtools-target",
|
|
374
|
+
targetId,
|
|
375
|
+
targetType: "page",
|
|
376
|
+
attached: true
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
browser: "chrome",
|
|
380
|
+
windowIndex: 1,
|
|
381
|
+
tabIndex: 1,
|
|
382
|
+
title,
|
|
383
|
+
url,
|
|
384
|
+
attachedAt: new Date().toISOString(),
|
|
385
|
+
identity: createIdentity(native, url, title),
|
|
386
|
+
isFrontWindow: true,
|
|
387
|
+
isActiveInWindow: true
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function buildChromeRelayFailureDetails(code, operation, overrides) {
|
|
391
|
+
const branchByCode = {
|
|
392
|
+
direct_unavailable_attach_endpoint_missing: "unsupported",
|
|
393
|
+
direct_degraded_discovery_partial: "unsupported",
|
|
394
|
+
relay_probe_not_configured: "configure-relay-probe",
|
|
395
|
+
relay_probe_invalid: "repair-relay-probe",
|
|
396
|
+
relay_extension_not_installed: "install-extension",
|
|
397
|
+
relay_extension_disconnected: "reconnect-extension",
|
|
398
|
+
relay_toolbar_not_clicked: "click-toolbar-button",
|
|
399
|
+
relay_share_required: operation === "resumeSession" ? "share-original-tab-again" : "share-tab",
|
|
400
|
+
relay_no_shared_tab: operation === "resumeSession" ? "share-original-tab-again" : "share-tab",
|
|
401
|
+
relay_attach_target_out_of_scope: operation === "resumeSession" ? "share-original-tab-again" : "use-current-shared-tab",
|
|
402
|
+
relay_attach_scope_expired: "share-original-tab-again",
|
|
403
|
+
relay_transport_not_implemented: "unsupported"
|
|
404
|
+
};
|
|
405
|
+
const retryableByCode = {
|
|
406
|
+
direct_unavailable_attach_endpoint_missing: false,
|
|
407
|
+
direct_degraded_discovery_partial: false,
|
|
408
|
+
relay_probe_not_configured: false,
|
|
409
|
+
relay_probe_invalid: false,
|
|
410
|
+
relay_extension_not_installed: true,
|
|
411
|
+
relay_extension_disconnected: true,
|
|
412
|
+
relay_toolbar_not_clicked: true,
|
|
413
|
+
relay_share_required: true,
|
|
414
|
+
relay_no_shared_tab: true,
|
|
415
|
+
relay_attach_target_out_of_scope: true,
|
|
416
|
+
relay_attach_scope_expired: true,
|
|
417
|
+
relay_transport_not_implemented: false
|
|
418
|
+
};
|
|
419
|
+
const userActionRequiredByCode = {
|
|
420
|
+
direct_unavailable_attach_endpoint_missing: false,
|
|
421
|
+
direct_degraded_discovery_partial: false,
|
|
422
|
+
relay_probe_not_configured: false,
|
|
423
|
+
relay_probe_invalid: false,
|
|
424
|
+
relay_extension_not_installed: true,
|
|
425
|
+
relay_extension_disconnected: true,
|
|
426
|
+
relay_toolbar_not_clicked: true,
|
|
427
|
+
relay_share_required: true,
|
|
428
|
+
relay_no_shared_tab: true,
|
|
429
|
+
relay_attach_target_out_of_scope: true,
|
|
430
|
+
relay_attach_scope_expired: true,
|
|
431
|
+
relay_transport_not_implemented: false
|
|
432
|
+
};
|
|
433
|
+
return {
|
|
434
|
+
context: {
|
|
435
|
+
browser: "chrome",
|
|
436
|
+
attachMode: "relay",
|
|
437
|
+
operation
|
|
438
|
+
},
|
|
439
|
+
relay: {
|
|
440
|
+
branch: overrides?.branch ?? branchByCode[code],
|
|
441
|
+
retryable: overrides?.retryable ?? retryableByCode[code],
|
|
442
|
+
userActionRequired: overrides?.userActionRequired ?? userActionRequiredByCode[code],
|
|
443
|
+
phase: overrides?.phase ?? "diagnostics",
|
|
444
|
+
sharedTabScope: "current-shared-tab",
|
|
445
|
+
currentSharedTabMatches: overrides?.currentSharedTabMatches,
|
|
446
|
+
resumable: overrides?.resumable,
|
|
447
|
+
resumeRequiresUserGesture: overrides?.resumeRequiresUserGesture,
|
|
448
|
+
expiresAt: overrides?.expiresAt,
|
|
449
|
+
sessionId: overrides?.sessionId
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function createChromeRelayFailure(message, code, statusCode, operation, overrides) {
|
|
454
|
+
return new errors_1.AppError(message, statusCode, code, buildChromeRelayFailureDetails(code, operation, overrides));
|
|
455
|
+
}
|
|
456
|
+
function toRelayAttachError(result, operation = "attach") {
|
|
457
|
+
const diagnostics = buildRelayDiagnostics(result);
|
|
458
|
+
const blocker = diagnostics.blockers[0];
|
|
459
|
+
const code = blocker?.code ?? "relay_no_shared_tab";
|
|
460
|
+
return createChromeRelayFailure(blocker?.message ?? "Chrome relay attach is not available for the current tab.", code, code === "relay_attach_target_out_of_scope" ? 409 : 503, operation, { phase: "diagnostics" });
|
|
461
|
+
}
|
|
462
|
+
async function resolveChromeRelayAttach(target) {
|
|
463
|
+
const result = await loadRelayProbe();
|
|
464
|
+
const probe = result.probe;
|
|
465
|
+
if (!probe || result.error === "invalid") {
|
|
466
|
+
throw toRelayAttachError(result);
|
|
467
|
+
}
|
|
468
|
+
if (target.type !== "front") {
|
|
469
|
+
throw createChromeRelayFailure("Chrome relay attach is scoped to the currently shared tab only; use the front tab target or omit an explicit target.", "relay_attach_target_out_of_scope", 409, "attach", {
|
|
470
|
+
phase: "target-selection",
|
|
471
|
+
branch: "use-current-shared-tab",
|
|
472
|
+
currentSharedTabMatches: false
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const diagnostics = buildRelayDiagnostics(result);
|
|
476
|
+
if (!diagnostics.ready) {
|
|
477
|
+
throw toRelayAttachError(result);
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
tab: toRelayTabMetadata(probe),
|
|
481
|
+
trustedAt: probe.updatedAt,
|
|
482
|
+
resumable: probe.resumable,
|
|
483
|
+
expiresAt: probe.expiresAt,
|
|
484
|
+
resumeRequiresUserGesture: probe.resumeRequiresUserGesture
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
async function resumeChromeRelaySession(session) {
|
|
488
|
+
const result = await loadRelayProbe();
|
|
489
|
+
const probe = result.probe;
|
|
490
|
+
if (!probe || result.error === "invalid") {
|
|
491
|
+
throw toRelayAttachError(result, "resumeSession");
|
|
492
|
+
}
|
|
493
|
+
if (session.attach.expiresAt && isExpiredIsoTimestamp(session.attach.expiresAt)) {
|
|
494
|
+
throw createChromeRelayFailure("The saved Chrome relay session has expired and the tab must be shared again before it can be resumed.", "relay_attach_scope_expired", 409, "resumeSession", {
|
|
495
|
+
phase: "session-precondition",
|
|
496
|
+
expiresAt: session.attach.expiresAt,
|
|
497
|
+
resumable: session.attach.resumable,
|
|
498
|
+
resumeRequiresUserGesture: session.attach.resumeRequiresUserGesture,
|
|
499
|
+
sessionId: session.id
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (session.attach.resumable === false || session.attach.resumeRequiresUserGesture === true) {
|
|
503
|
+
throw createChromeRelayFailure("The saved Chrome relay session is not resumable without the user sharing the tab again.", "relay_share_required", 409, "resumeSession", {
|
|
504
|
+
phase: "session-precondition",
|
|
505
|
+
resumable: session.attach.resumable,
|
|
506
|
+
resumeRequiresUserGesture: session.attach.resumeRequiresUserGesture,
|
|
507
|
+
expiresAt: session.attach.expiresAt,
|
|
508
|
+
sessionId: session.id
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const diagnostics = buildRelayDiagnostics(result);
|
|
512
|
+
if (!diagnostics.ready) {
|
|
513
|
+
throw toRelayAttachError(result, "resumeSession");
|
|
514
|
+
}
|
|
515
|
+
const tab = toRelayTabMetadata(probe);
|
|
516
|
+
if (session.target.type === "signature" && session.target.signature !== tab.identity.signature) {
|
|
517
|
+
throw createChromeRelayFailure("The currently shared relay tab does not match the saved Chrome relay session. Share the original tab again before resuming.", "relay_attach_target_out_of_scope", 409, "resumeSession", {
|
|
518
|
+
phase: "shared-tab-match",
|
|
519
|
+
currentSharedTabMatches: false,
|
|
520
|
+
resumable: session.attach.resumable,
|
|
521
|
+
resumeRequiresUserGesture: session.attach.resumeRequiresUserGesture,
|
|
522
|
+
expiresAt: session.attach.expiresAt,
|
|
523
|
+
sessionId: session.id
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const refreshedSession = refreshRelaySession(session, tab, probe);
|
|
527
|
+
return {
|
|
528
|
+
session: refreshedSession,
|
|
529
|
+
tab,
|
|
530
|
+
resumedAt: new Date().toISOString(),
|
|
531
|
+
resolution: {
|
|
532
|
+
strategy: "signature",
|
|
533
|
+
matched: true,
|
|
534
|
+
attachMode: session.attach.mode,
|
|
535
|
+
semantics: session.semantics.resume
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async function discoverFromProcessFlags() {
|
|
540
|
+
const candidates = [];
|
|
541
|
+
try {
|
|
542
|
+
const { stdout } = await execFileAsync("ps", ["-ax", "-o", "pid=,command="]);
|
|
543
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
544
|
+
const line = rawLine.trim();
|
|
545
|
+
if (!line || !/(chrome|chromium)/i.test(line)) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const match = line.match(/^(\d+)\s+(.*)$/);
|
|
549
|
+
if (!match) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const pid = Number(match[1]);
|
|
553
|
+
const command = match[2];
|
|
554
|
+
const portMatch = command.match(/--remote-debugging-port=(\d+)/);
|
|
555
|
+
if (!portMatch) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const host = command.match(/--remote-debugging-address=([^\s]+)/)?.[1] ?? "127.0.0.1";
|
|
559
|
+
const port = Number(portMatch[1]);
|
|
560
|
+
pushCandidate(candidates, {
|
|
561
|
+
kind: "process-flag",
|
|
562
|
+
label: `Process flag pid ${pid}`,
|
|
563
|
+
pid,
|
|
564
|
+
command,
|
|
565
|
+
host,
|
|
566
|
+
port,
|
|
567
|
+
baseUrl: `http://${host}:${port}`
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
// Ignore ps failures.
|
|
573
|
+
}
|
|
574
|
+
return candidates;
|
|
575
|
+
}
|
|
576
|
+
function fallbackPortCandidates() {
|
|
577
|
+
return DEFAULT_DEBUG_PORTS.map((port) => ({
|
|
578
|
+
kind: "fallback-port",
|
|
579
|
+
label: `Fallback localhost port ${port}`,
|
|
580
|
+
host: "127.0.0.1",
|
|
581
|
+
port,
|
|
582
|
+
baseUrl: `http://127.0.0.1:${port}`
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
async function discoverChromeEndpoint() {
|
|
586
|
+
const candidates = [];
|
|
587
|
+
for (const candidate of await discoverFromEnv()) {
|
|
588
|
+
pushCandidate(candidates, candidate);
|
|
589
|
+
}
|
|
590
|
+
for (const candidate of await discoverFromProcessFlags()) {
|
|
591
|
+
pushCandidate(candidates, candidate);
|
|
592
|
+
}
|
|
593
|
+
for (const candidate of await discoverFromDevtoolsFiles()) {
|
|
594
|
+
pushCandidate(candidates, candidate);
|
|
595
|
+
}
|
|
596
|
+
for (const candidate of fallbackPortCandidates()) {
|
|
597
|
+
pushCandidate(candidates, candidate);
|
|
598
|
+
}
|
|
599
|
+
let selectedBaseUrl;
|
|
600
|
+
let selectedSourceLabel;
|
|
601
|
+
for (const candidate of candidates) {
|
|
602
|
+
if (!candidate.baseUrl) {
|
|
603
|
+
candidate.reachable = false;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
await fetchJson(`${candidate.baseUrl}${DEVTOOLS_VERSION_PATH}`);
|
|
608
|
+
candidate.reachable = true;
|
|
609
|
+
candidate.chosen = true;
|
|
610
|
+
selectedBaseUrl = candidate.baseUrl;
|
|
611
|
+
selectedSourceLabel = candidate.label;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
candidate.reachable = false;
|
|
616
|
+
candidate.notes = [error instanceof Error ? error.message : "Endpoint probe failed."];
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return { candidates, selectedBaseUrl, selectedSourceLabel };
|
|
620
|
+
}
|
|
621
|
+
function inspectUnavailableError(candidates) {
|
|
622
|
+
const attempted = candidates
|
|
623
|
+
.map((candidate) => candidate.baseUrl ?? candidate.devtoolsActivePortPath ?? candidate.label)
|
|
624
|
+
.slice(0, 6)
|
|
625
|
+
.join(", ");
|
|
626
|
+
return new errors_1.AppError(`Chrome/Chromium tab inspection needs an existing local DevTools HTTP endpoint. No reachable endpoint was found${attempted ? ` (checked: ${attempted}).` : "."}`, 503, "inspect_unavailable");
|
|
627
|
+
}
|
|
628
|
+
function unsupportedRuntimeOperation(operation, code) {
|
|
629
|
+
return new errors_1.AppError(`${CHROME_PREFIX} ${operation} is not implemented yet in the current Chrome adapter.`, 501, code);
|
|
630
|
+
}
|
|
631
|
+
class ChromeAdapter {
|
|
632
|
+
browser = "chrome";
|
|
633
|
+
async listInspectableTabs() {
|
|
634
|
+
const discovery = await discoverChromeEndpoint();
|
|
635
|
+
if (!discovery.selectedBaseUrl) {
|
|
636
|
+
throw inspectUnavailableError(discovery.candidates);
|
|
637
|
+
}
|
|
638
|
+
let payload;
|
|
639
|
+
try {
|
|
640
|
+
payload = (await fetchJson(`${discovery.selectedBaseUrl}${DEVTOOLS_LIST_PATH}`));
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
throw new errors_1.AppError(`Chrome/Chromium DevTools endpoint was found at ${discovery.selectedBaseUrl}, but tab listing failed. ${error instanceof Error ? error.message : "Unknown error."}`, 503, "inspect_unavailable");
|
|
644
|
+
}
|
|
645
|
+
const tabs = payload
|
|
646
|
+
.filter((target) => target.type === "page")
|
|
647
|
+
.map((target, index) => toTabMetadata(target, index));
|
|
648
|
+
return { tabs, discovery };
|
|
649
|
+
}
|
|
650
|
+
async listTabs() {
|
|
651
|
+
const { tabs } = await this.listInspectableTabs();
|
|
652
|
+
return tabs;
|
|
653
|
+
}
|
|
654
|
+
async resolveTab(target) {
|
|
655
|
+
const { tabs } = await this.listInspectableTabs();
|
|
656
|
+
if (tabs.length === 0) {
|
|
657
|
+
throw new errors_1.AppError("Chrome/Chromium DevTools endpoint is reachable, but it reported no inspectable page targets.", 404, "tab_not_found");
|
|
658
|
+
}
|
|
659
|
+
if (target.type === "front") {
|
|
660
|
+
return tabs[0];
|
|
661
|
+
}
|
|
662
|
+
if (target.type === "indexed") {
|
|
663
|
+
const matchedTab = tabs.find((tab) => tab.windowIndex === target.windowIndex && tab.tabIndex === target.tabIndex);
|
|
664
|
+
if (!matchedTab) {
|
|
665
|
+
throw new errors_1.AppError(`Chrome tab not found for window ${target.windowIndex}, tab ${target.tabIndex}.`, 404, "tab_not_found");
|
|
666
|
+
}
|
|
667
|
+
return matchedTab;
|
|
668
|
+
}
|
|
669
|
+
const exactSignature = tabs.find((tab) => tab.identity.signature === target.signature);
|
|
670
|
+
if (exactSignature) {
|
|
671
|
+
return exactSignature;
|
|
672
|
+
}
|
|
673
|
+
const exactUrlTitle = tabs.find((tab) => tab.url === (target.url ?? "") && normalizeTitle(tab.title) === normalizeTitle(target.title ?? ""));
|
|
674
|
+
if (exactUrlTitle) {
|
|
675
|
+
return exactUrlTitle;
|
|
676
|
+
}
|
|
677
|
+
const exactUrl = target.url ? tabs.find((tab) => tab.url === target.url) : undefined;
|
|
678
|
+
if (exactUrl) {
|
|
679
|
+
return exactUrl;
|
|
680
|
+
}
|
|
681
|
+
const lastKnown = target.lastKnownWindowIndex && target.lastKnownTabIndex
|
|
682
|
+
? tabs.find((tab) => tab.windowIndex === target.lastKnownWindowIndex && tab.tabIndex === target.lastKnownTabIndex)
|
|
683
|
+
: undefined;
|
|
684
|
+
if (lastKnown) {
|
|
685
|
+
return lastKnown;
|
|
686
|
+
}
|
|
687
|
+
throw new errors_1.AppError(`Chrome tab not found for signature ${target.signature}.`, 404, "tab_not_found");
|
|
688
|
+
}
|
|
689
|
+
async performSessionAction(action) {
|
|
690
|
+
if (action.action === "activate") {
|
|
691
|
+
throw unsupportedRuntimeOperation("Activation", "activation_unavailable");
|
|
692
|
+
}
|
|
693
|
+
if (action.action === "navigate") {
|
|
694
|
+
throw unsupportedRuntimeOperation("Navigation", "navigation_unavailable");
|
|
695
|
+
}
|
|
696
|
+
if (action.action === "screenshot") {
|
|
697
|
+
throw unsupportedRuntimeOperation("Screenshot capture", "screenshot_unavailable");
|
|
698
|
+
}
|
|
699
|
+
throw new errors_1.AppError(`Unsupported Chrome session action: ${action.action}`, 400, "unsupported_action");
|
|
700
|
+
}
|
|
701
|
+
async getDiagnostics() {
|
|
702
|
+
const [osascriptAvailable, screencaptureAvailable, discovery, relayProbe] = await Promise.all([
|
|
703
|
+
commandAvailable("osascript"),
|
|
704
|
+
commandAvailable("screencapture"),
|
|
705
|
+
discoverChromeEndpoint(),
|
|
706
|
+
loadRelayProbe()
|
|
707
|
+
]);
|
|
708
|
+
const inspectTabs = Boolean(discovery.selectedBaseUrl);
|
|
709
|
+
const reachableCandidates = discovery.candidates.filter((candidate) => candidate.reachable).length;
|
|
710
|
+
const direct = {
|
|
711
|
+
mode: "direct",
|
|
712
|
+
source: "user-browser",
|
|
713
|
+
scope: "browser",
|
|
714
|
+
supported: true,
|
|
715
|
+
ready: inspectTabs,
|
|
716
|
+
state: inspectTabs ? (reachableCandidates > 1 ? "degraded" : "ready") : "unavailable",
|
|
717
|
+
blockers: inspectTabs
|
|
718
|
+
? reachableCandidates > 1
|
|
719
|
+
? [{
|
|
720
|
+
code: "direct_degraded_discovery_partial",
|
|
721
|
+
message: "A usable DevTools endpoint was found, but other discovered candidates were unreachable or incomplete."
|
|
722
|
+
}]
|
|
723
|
+
: []
|
|
724
|
+
: [{
|
|
725
|
+
code: "direct_unavailable_attach_endpoint_missing",
|
|
726
|
+
message: "No running local Chrome/Chromium DevTools HTTP endpoint could be discovered for direct user-browser attach."
|
|
727
|
+
}],
|
|
728
|
+
notes: inspectTabs
|
|
729
|
+
? ["Direct attach currently uses read-only DevTools discovery and preserves Chrome as chrome-readonly in emitted sessions."]
|
|
730
|
+
: ["Start Chrome/Chromium with a discoverable local DevTools endpoint to make direct attach ready."]
|
|
731
|
+
};
|
|
732
|
+
const relay = buildRelayDiagnostics(relayProbe);
|
|
733
|
+
return {
|
|
734
|
+
browser: this.browser,
|
|
735
|
+
checkedAt: new Date().toISOString(),
|
|
736
|
+
runtime: {
|
|
737
|
+
platform: process.platform,
|
|
738
|
+
arch: process.arch,
|
|
739
|
+
nodeVersion: process.version,
|
|
740
|
+
safariRunning: false
|
|
741
|
+
},
|
|
742
|
+
host: {
|
|
743
|
+
osascriptAvailable,
|
|
744
|
+
screencaptureAvailable,
|
|
745
|
+
safariApplicationAvailable: false
|
|
746
|
+
},
|
|
747
|
+
supportedFeatures: {
|
|
748
|
+
inspectTabs,
|
|
749
|
+
attach: inspectTabs || relay.ready,
|
|
750
|
+
activate: false,
|
|
751
|
+
navigate: false,
|
|
752
|
+
screenshot: false,
|
|
753
|
+
savedSessions: inspectTabs || relay.ready,
|
|
754
|
+
cli: true,
|
|
755
|
+
httpApi: true
|
|
756
|
+
},
|
|
757
|
+
constraints: [
|
|
758
|
+
"Safari remains the primary production adapter in this phase.",
|
|
759
|
+
"Chrome/Chromium direct attach currently depends on an already-running local DevTools HTTP endpoint.",
|
|
760
|
+
"This Chrome adapter is intentionally read-only: sessions can be attached and resumed, but activate/navigate/screenshot remain unavailable.",
|
|
761
|
+
"Chrome relay attach is limited to the currently shared tab surfaced by the local relay probe."
|
|
762
|
+
],
|
|
763
|
+
attach: {
|
|
764
|
+
direct,
|
|
765
|
+
relay
|
|
766
|
+
},
|
|
767
|
+
adapter: {
|
|
768
|
+
mode: inspectTabs ? "chrome-devtools-readonly" : "stub",
|
|
769
|
+
discovery: {
|
|
770
|
+
selectedBaseUrl: discovery.selectedBaseUrl,
|
|
771
|
+
selectedSourceLabel: discovery.selectedSourceLabel,
|
|
772
|
+
candidates: discovery.candidates
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
exports.ChromeAdapter = ChromeAdapter;
|