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,827 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SafariAdapter = void 0;
|
|
4
|
+
exports.parseSafariInspectionSnapshot = parseSafariInspectionSnapshot;
|
|
5
|
+
exports.isValidSafariWindowBounds = isValidSafariWindowBounds;
|
|
6
|
+
exports.buildSafariPreflight = buildSafariPreflight;
|
|
7
|
+
exports.classifySafariRuntimeError = classifySafariRuntimeError;
|
|
8
|
+
exports.classifySafariTabResolutionError = classifySafariTabResolutionError;
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
const node_util_1 = require("node:util");
|
|
14
|
+
const errors_1 = require("../errors");
|
|
15
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
16
|
+
const inspectWindowsScript = `
|
|
17
|
+
function enumerateInspectableSafariWindows() {
|
|
18
|
+
const safari = Application("Safari");
|
|
19
|
+
if (!safari.running()) {
|
|
20
|
+
throw new Error("Safari is not running.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const windows = safari.windows();
|
|
24
|
+
if (!windows || windows.length === 0) {
|
|
25
|
+
throw new Error("Safari has no open windows.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const payload = [];
|
|
29
|
+
let inspectableWindowCount = 0;
|
|
30
|
+
|
|
31
|
+
for (let windowOffset = 0; windowOffset < windows.length; windowOffset += 1) {
|
|
32
|
+
const safariWindow = windows[windowOffset];
|
|
33
|
+
let currentTab = null;
|
|
34
|
+
let currentTabIndex = -1;
|
|
35
|
+
let tabs = null;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
currentTab = safariWindow.currentTab();
|
|
39
|
+
currentTabIndex = currentTab ? Number(currentTab.index()) : -1;
|
|
40
|
+
tabs = safariWindow.tabs();
|
|
41
|
+
} catch (_error) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!tabs || typeof tabs.length !== "number") {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
inspectableWindowCount += 1;
|
|
50
|
+
for (let tabOffset = 0; tabOffset < tabs.length; tabOffset += 1) {
|
|
51
|
+
const tab = tabs[tabOffset];
|
|
52
|
+
payload.push({
|
|
53
|
+
browser: "safari",
|
|
54
|
+
windowIndex: windowOffset + 1,
|
|
55
|
+
tabIndex: Number(tab.index()),
|
|
56
|
+
title: String(tab.name() || ""),
|
|
57
|
+
url: String(tab.url() || ""),
|
|
58
|
+
isFrontWindow: windowOffset === 0,
|
|
59
|
+
isActiveInWindow: Number(tab.index()) === currentTabIndex
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
tabs: payload,
|
|
66
|
+
windowCount: windows.length,
|
|
67
|
+
inspectableWindowCount,
|
|
68
|
+
tabCount: payload.length
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function run() {
|
|
73
|
+
return JSON.stringify(enumerateInspectableSafariWindows());
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
const focusWindowScript = `
|
|
77
|
+
function normalizeWindowBounds(rawBounds) {
|
|
78
|
+
if (!rawBounds) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const toFiniteNumber = (value) => {
|
|
83
|
+
const numeric = Number(value);
|
|
84
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const fromArray = Array.isArray(rawBounds)
|
|
88
|
+
? rawBounds
|
|
89
|
+
: typeof rawBounds.length === "number"
|
|
90
|
+
? Array.prototype.slice.call(rawBounds)
|
|
91
|
+
: null;
|
|
92
|
+
|
|
93
|
+
if (fromArray && fromArray.length >= 4) {
|
|
94
|
+
const left = toFiniteNumber(fromArray[0]);
|
|
95
|
+
const top = toFiniteNumber(fromArray[1]);
|
|
96
|
+
const right = toFiniteNumber(fromArray[2]);
|
|
97
|
+
const bottom = toFiniteNumber(fromArray[3]);
|
|
98
|
+
|
|
99
|
+
if (left !== null && top !== null && right !== null && bottom !== null) {
|
|
100
|
+
return {
|
|
101
|
+
x: left,
|
|
102
|
+
y: top,
|
|
103
|
+
width: right - left,
|
|
104
|
+
height: bottom - top
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const x = toFiniteNumber(rawBounds.x);
|
|
110
|
+
const y = toFiniteNumber(rawBounds.y);
|
|
111
|
+
const width = toFiniteNumber(rawBounds.width);
|
|
112
|
+
const height = toFiniteNumber(rawBounds.height);
|
|
113
|
+
if (x !== null && y !== null && width !== null && height !== null) {
|
|
114
|
+
return { x, y, width, height };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const left = toFiniteNumber(rawBounds.left);
|
|
118
|
+
const top = toFiniteNumber(rawBounds.top);
|
|
119
|
+
const right = toFiniteNumber(rawBounds.right);
|
|
120
|
+
const bottom = toFiniteNumber(rawBounds.bottom);
|
|
121
|
+
if (left !== null && top !== null && right !== null && bottom !== null) {
|
|
122
|
+
return {
|
|
123
|
+
x: left,
|
|
124
|
+
y: top,
|
|
125
|
+
width: right - left,
|
|
126
|
+
height: bottom - top
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function run(argv) {
|
|
134
|
+
const requestedWindowIndex = Number(argv[0]);
|
|
135
|
+
const requestedTabIndex = Number(argv[1]);
|
|
136
|
+
const preferredWindowOrder = String(argv[2] || "front");
|
|
137
|
+
const safari = Application("Safari");
|
|
138
|
+
if (!safari.running()) {
|
|
139
|
+
throw new Error("Safari is not running.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const windows = safari.windows();
|
|
143
|
+
if (!windows || windows.length === 0) {
|
|
144
|
+
throw new Error("Safari has no open windows.");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const safariWindow = windows[requestedWindowIndex - 1];
|
|
148
|
+
if (!safariWindow) {
|
|
149
|
+
throw new Error("Safari target window is no longer available.");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const tabs = safariWindow.tabs();
|
|
153
|
+
let targetTab = null;
|
|
154
|
+
for (let index = 0; index < tabs.length; index += 1) {
|
|
155
|
+
const candidate = tabs[index];
|
|
156
|
+
if (Number(candidate.index()) === requestedTabIndex) {
|
|
157
|
+
targetTab = candidate;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!targetTab) {
|
|
163
|
+
throw new Error("Safari target tab is no longer available.");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
safari.activate();
|
|
167
|
+
safariWindow.currentTab = targetTab;
|
|
168
|
+
|
|
169
|
+
let reorderedWindowToFront = false;
|
|
170
|
+
if (preferredWindowOrder !== "preserve") {
|
|
171
|
+
safariWindow.index = 1;
|
|
172
|
+
reorderedWindowToFront = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
delay(0.2);
|
|
176
|
+
|
|
177
|
+
const normalizedBounds = normalizeWindowBounds(safariWindow.bounds());
|
|
178
|
+
if (!normalizedBounds || !(normalizedBounds.width > 0) || !(normalizedBounds.height > 0)) {
|
|
179
|
+
throw new Error("Safari target window bounds are unavailable or invalid for screenshot capture.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return JSON.stringify({
|
|
183
|
+
x: normalizedBounds.x,
|
|
184
|
+
y: normalizedBounds.y,
|
|
185
|
+
width: normalizedBounds.width,
|
|
186
|
+
height: normalizedBounds.height,
|
|
187
|
+
reorderedWindowToFront
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
const navigateWindowScript = `
|
|
192
|
+
function run(argv) {
|
|
193
|
+
const requestedWindowIndex = Number(argv[0]);
|
|
194
|
+
const requestedTabIndex = Number(argv[1]);
|
|
195
|
+
const requestedUrl = String(argv[2] || "");
|
|
196
|
+
const preferredWindowOrder = String(argv[3] || "front");
|
|
197
|
+
if (!requestedUrl) {
|
|
198
|
+
throw new Error("navigate requires a URL.");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const safari = Application("Safari");
|
|
202
|
+
if (!safari.running()) {
|
|
203
|
+
throw new Error("Safari is not running.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const windows = safari.windows();
|
|
207
|
+
if (!windows || windows.length === 0) {
|
|
208
|
+
throw new Error("Safari has no open windows.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const safariWindow = windows[requestedWindowIndex - 1];
|
|
212
|
+
if (!safariWindow) {
|
|
213
|
+
throw new Error("Safari target window is no longer available.");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tabs = safariWindow.tabs();
|
|
217
|
+
let targetTab = null;
|
|
218
|
+
for (let index = 0; index < tabs.length; index += 1) {
|
|
219
|
+
const candidate = tabs[index];
|
|
220
|
+
if (Number(candidate.index()) === requestedTabIndex) {
|
|
221
|
+
targetTab = candidate;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!targetTab) {
|
|
227
|
+
throw new Error("Safari target tab is no longer available.");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
safari.activate();
|
|
231
|
+
safariWindow.currentTab = targetTab;
|
|
232
|
+
|
|
233
|
+
let reorderedWindowToFront = false;
|
|
234
|
+
if (preferredWindowOrder !== "preserve") {
|
|
235
|
+
safariWindow.index = 1;
|
|
236
|
+
reorderedWindowToFront = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
targetTab.url = requestedUrl;
|
|
240
|
+
delay(0.2);
|
|
241
|
+
|
|
242
|
+
return JSON.stringify({
|
|
243
|
+
browser: "safari",
|
|
244
|
+
windowIndex: requestedWindowIndex,
|
|
245
|
+
tabIndex: requestedTabIndex,
|
|
246
|
+
title: String(targetTab.name() || ""),
|
|
247
|
+
url: String(targetTab.url() || requestedUrl),
|
|
248
|
+
isFrontWindow: safariWindow.index() === 1,
|
|
249
|
+
isActiveInWindow: Number(safariWindow.currentTab().index()) === requestedTabIndex,
|
|
250
|
+
reorderedWindowToFront
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
const safariWindowProbeScript = `
|
|
255
|
+
function enumerateInspectableSafariWindows() {
|
|
256
|
+
const safari = Application("Safari");
|
|
257
|
+
const windows = safari.windows();
|
|
258
|
+
const payload = [];
|
|
259
|
+
let inspectableWindowCount = 0;
|
|
260
|
+
|
|
261
|
+
for (let windowOffset = 0; windowOffset < windows.length; windowOffset += 1) {
|
|
262
|
+
const safariWindow = windows[windowOffset];
|
|
263
|
+
let currentTab = null;
|
|
264
|
+
let currentTabIndex = -1;
|
|
265
|
+
let tabs = null;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
currentTab = safariWindow.currentTab();
|
|
269
|
+
currentTabIndex = currentTab ? Number(currentTab.index()) : -1;
|
|
270
|
+
tabs = safariWindow.tabs();
|
|
271
|
+
} catch (_error) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!tabs || typeof tabs.length !== "number") {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
inspectableWindowCount += 1;
|
|
280
|
+
for (let tabOffset = 0; tabOffset < tabs.length; tabOffset += 1) {
|
|
281
|
+
const tab = tabs[tabOffset];
|
|
282
|
+
payload.push({
|
|
283
|
+
browser: "safari",
|
|
284
|
+
windowIndex: windowOffset + 1,
|
|
285
|
+
tabIndex: Number(tab.index()),
|
|
286
|
+
title: String(tab.name() || ""),
|
|
287
|
+
url: String(tab.url() || ""),
|
|
288
|
+
isFrontWindow: windowOffset === 0,
|
|
289
|
+
isActiveInWindow: Number(tab.index()) === currentTabIndex
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
tabs: payload,
|
|
296
|
+
windowCount: windows.length,
|
|
297
|
+
inspectableWindowCount,
|
|
298
|
+
tabCount: payload.length
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function run() {
|
|
303
|
+
const safari = Application("Safari");
|
|
304
|
+
if (!safari.running()) {
|
|
305
|
+
return JSON.stringify({ running: false, windowCount: 0, inspectableWindowCount: 0, tabCount: 0 });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
return JSON.stringify({ running: true, ...enumerateInspectableSafariWindows() });
|
|
310
|
+
} catch (error) {
|
|
311
|
+
const windows = safari.windows();
|
|
312
|
+
return JSON.stringify({
|
|
313
|
+
running: true,
|
|
314
|
+
windowCount: windows ? windows.length : 0,
|
|
315
|
+
inspectableWindowCount: 0,
|
|
316
|
+
tabCount: 0,
|
|
317
|
+
probeError: String(error && error.message ? error.message : error)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
322
|
+
function normalizeUrl(rawUrl) {
|
|
323
|
+
try {
|
|
324
|
+
return new URL(rawUrl);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function normalizeTitle(title) {
|
|
331
|
+
return title.trim().replace(/\s+/g, " ").toLowerCase();
|
|
332
|
+
}
|
|
333
|
+
function createIdentity(url, title) {
|
|
334
|
+
const parsedUrl = normalizeUrl(url);
|
|
335
|
+
const origin = parsedUrl?.origin ?? "";
|
|
336
|
+
const pathname = parsedUrl?.pathname ?? "";
|
|
337
|
+
const urlKey = parsedUrl ? `${parsedUrl.origin}${parsedUrl.pathname}${parsedUrl.search}` : url.trim();
|
|
338
|
+
const titleKey = normalizeTitle(title);
|
|
339
|
+
const signature = (0, node_crypto_1.createHash)("sha256")
|
|
340
|
+
.update(JSON.stringify({ browser: "safari", urlKey, titleKey }))
|
|
341
|
+
.digest("hex")
|
|
342
|
+
.slice(0, 24);
|
|
343
|
+
return {
|
|
344
|
+
signature,
|
|
345
|
+
urlKey,
|
|
346
|
+
titleKey,
|
|
347
|
+
origin,
|
|
348
|
+
pathname
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function withAttachedAt(tab) {
|
|
352
|
+
return {
|
|
353
|
+
...tab,
|
|
354
|
+
identity: createIdentity(tab.url, tab.title),
|
|
355
|
+
attachedAt: new Date().toISOString()
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function parseSafariInspectionSnapshot(raw) {
|
|
359
|
+
const parsed = JSON.parse(raw.trim());
|
|
360
|
+
if (Array.isArray(parsed)) {
|
|
361
|
+
return {
|
|
362
|
+
tabs: parsed,
|
|
363
|
+
windowCount: 0,
|
|
364
|
+
inspectableWindowCount: 0,
|
|
365
|
+
tabCount: parsed.length
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
tabs: Array.isArray(parsed.tabs) ? parsed.tabs : [],
|
|
370
|
+
windowCount: typeof parsed.windowCount === "number" ? parsed.windowCount : 0,
|
|
371
|
+
inspectableWindowCount: typeof parsed.inspectableWindowCount === "number" ? parsed.inspectableWindowCount : 0,
|
|
372
|
+
tabCount: typeof parsed.tabCount === "number" ? parsed.tabCount : Array.isArray(parsed.tabs) ? parsed.tabs.length : 0
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function isValidSafariWindowBounds(bounds) {
|
|
376
|
+
return Boolean(bounds &&
|
|
377
|
+
Number.isFinite(bounds.x) &&
|
|
378
|
+
Number.isFinite(bounds.y) &&
|
|
379
|
+
Number.isFinite(bounds.width) &&
|
|
380
|
+
Number.isFinite(bounds.height) &&
|
|
381
|
+
Number(bounds.width) > 0 &&
|
|
382
|
+
Number(bounds.height) > 0);
|
|
383
|
+
}
|
|
384
|
+
async function activateSafariTarget(tab, preferredWindowOrder = "front") {
|
|
385
|
+
const { stdout } = await execFileAsync("osascript", [
|
|
386
|
+
"-l",
|
|
387
|
+
"JavaScript",
|
|
388
|
+
"-e",
|
|
389
|
+
focusWindowScript,
|
|
390
|
+
String(tab.windowIndex),
|
|
391
|
+
String(tab.tabIndex),
|
|
392
|
+
preferredWindowOrder
|
|
393
|
+
]);
|
|
394
|
+
const parsed = JSON.parse(stdout.trim());
|
|
395
|
+
if (!isValidSafariWindowBounds(parsed)) {
|
|
396
|
+
throw new Error("Safari target window bounds are unavailable or invalid for screenshot capture.");
|
|
397
|
+
}
|
|
398
|
+
return parsed;
|
|
399
|
+
}
|
|
400
|
+
async function navigateSafariTarget(tab, url, preferredWindowOrder = "front") {
|
|
401
|
+
const { stdout } = await execFileAsync("osascript", [
|
|
402
|
+
"-l",
|
|
403
|
+
"JavaScript",
|
|
404
|
+
"-e",
|
|
405
|
+
navigateWindowScript,
|
|
406
|
+
String(tab.windowIndex),
|
|
407
|
+
String(tab.tabIndex),
|
|
408
|
+
url,
|
|
409
|
+
preferredWindowOrder
|
|
410
|
+
]);
|
|
411
|
+
const parsed = JSON.parse(stdout.trim());
|
|
412
|
+
const { reorderedWindowToFront, ...nextTab } = parsed;
|
|
413
|
+
return {
|
|
414
|
+
tab: withAttachedAt(nextTab),
|
|
415
|
+
reorderedWindowToFront
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function commandAvailable(command) {
|
|
419
|
+
try {
|
|
420
|
+
await execFileAsync("sh", ["-lc", `command -v ${command}`]);
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function safariApplicationAvailable() {
|
|
428
|
+
try {
|
|
429
|
+
await execFileAsync("open", ["-Ra", "Safari"]);
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function safariRunning() {
|
|
437
|
+
try {
|
|
438
|
+
const { stdout } = await execFileAsync("osascript", [
|
|
439
|
+
"-l",
|
|
440
|
+
"JavaScript",
|
|
441
|
+
"-e",
|
|
442
|
+
'Application("Safari").running() ? "true" : "false"'
|
|
443
|
+
]);
|
|
444
|
+
return stdout.trim() === "true";
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function screenRecordingPermissionGranted() {
|
|
451
|
+
if (process.platform !== "darwin") {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const { stdout } = await execFileAsync("swift", [
|
|
456
|
+
"-e",
|
|
457
|
+
'import CoreGraphics\nprint(CGPreflightScreenCaptureAccess() ? "true" : "false")'
|
|
458
|
+
]);
|
|
459
|
+
const normalized = stdout.trim().toLowerCase();
|
|
460
|
+
if (normalized === "true") {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
if (normalized === "false") {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function safariWindowProbe() {
|
|
473
|
+
const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", "-e", safariWindowProbeScript]);
|
|
474
|
+
return JSON.parse(stdout.trim());
|
|
475
|
+
}
|
|
476
|
+
function readinessBlocker(code, message, scope, checks) {
|
|
477
|
+
return { code, message, scope, checks };
|
|
478
|
+
}
|
|
479
|
+
function readinessStatus(checks, blockers) {
|
|
480
|
+
const relevantBlockers = blockers.filter((blocker) => blocker.checks.some((check) => checks.includes(check)));
|
|
481
|
+
return {
|
|
482
|
+
ready: relevantBlockers.length === 0,
|
|
483
|
+
checks,
|
|
484
|
+
blockers: relevantBlockers
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function buildSafariPreflight(args) {
|
|
488
|
+
const blockers = [];
|
|
489
|
+
if (!args.osascriptAvailable) {
|
|
490
|
+
blockers.push(readinessBlocker("host_tool_missing", "The host macOS runtime does not have osascript available, so Safari inspection and automation cannot run.", "host", ["inspect", "automation", "screenshot"]));
|
|
491
|
+
}
|
|
492
|
+
if (!args.applicationAvailable) {
|
|
493
|
+
blockers.push(readinessBlocker("browser_application_missing", "Safari.app is not discoverable on this host.", "host", ["inspect", "automation", "screenshot"]));
|
|
494
|
+
}
|
|
495
|
+
if (!args.safariRunning) {
|
|
496
|
+
blockers.push(readinessBlocker("browser_not_running", "Safari is not running, so attach and runtime actions are unavailable until Safari is opened.", "runtime", ["inspect", "automation", "screenshot"]));
|
|
497
|
+
}
|
|
498
|
+
if (typeof args.windowCount === "number" && args.safariRunning && args.windowCount < 1) {
|
|
499
|
+
blockers.push(readinessBlocker("browser_no_windows", "Safari is running but has no open windows, so there is no tab to inspect or act on.", "runtime", ["inspect", "automation", "screenshot"]));
|
|
500
|
+
}
|
|
501
|
+
const hasInspectableWindows = typeof args.inspectableWindowCount === "number" ? args.inspectableWindowCount > 0 : undefined;
|
|
502
|
+
const hasInspectableTabs = typeof args.tabCount === "number" ? args.tabCount > 0 : undefined;
|
|
503
|
+
const windowsWithoutInspectableTabs = typeof args.windowCount === "number" &&
|
|
504
|
+
args.windowCount > 0 &&
|
|
505
|
+
(hasInspectableTabs === false || hasInspectableWindows === false) &&
|
|
506
|
+
!blockers.some((blocker) => blocker.code === "browser_no_windows");
|
|
507
|
+
if (windowsWithoutInspectableTabs) {
|
|
508
|
+
blockers.push(readinessBlocker("browser_no_tabs", hasInspectableWindows === false
|
|
509
|
+
? "Safari has open windows, but none expose inspectable tabs to Apple Events right now. Safari special or transient windows are being skipped until a normal browser tab is available."
|
|
510
|
+
: "Safari has open windows, but no inspectable tabs are currently available to attach or act on.", "runtime", ["inspect", "automation", "screenshot"]));
|
|
511
|
+
}
|
|
512
|
+
if (args.probeError) {
|
|
513
|
+
const classified = classifySafariRuntimeError("inspect", args.probeError);
|
|
514
|
+
if (classified.code === "automation_permission_denied") {
|
|
515
|
+
blockers.push(readinessBlocker("automation_permission_denied", "Safari Apple Events permission is currently denied for the host process.", "permission", ["inspect", "automation", "screenshot"]));
|
|
516
|
+
}
|
|
517
|
+
else if (!blockers.some((blocker) => blocker.code === "browser_not_running" || blocker.code === "browser_no_windows" || blocker.code === "browser_no_tabs")) {
|
|
518
|
+
blockers.push(readinessBlocker("runtime_error", `Safari preflight probe failed: ${classified.message}`, "runtime", ["inspect", "automation", "screenshot"]));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (!args.screencaptureAvailable) {
|
|
522
|
+
blockers.push(readinessBlocker("host_tool_missing", "The host macOS runtime does not have screencapture available, so Safari screenshots cannot be captured.", "host", ["screenshot"]));
|
|
523
|
+
}
|
|
524
|
+
if (args.screenRecordingPermissionGranted === false) {
|
|
525
|
+
blockers.push(readinessBlocker("screen_recording_permission_denied", "macOS Screen Recording permission is currently denied for the host process, so Safari screenshots cannot be captured until access is granted.", "permission", ["screenshot"]));
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
inspect: readinessStatus(["inspect"], blockers),
|
|
529
|
+
automation: readinessStatus(["automation"], blockers),
|
|
530
|
+
screenshot: readinessStatus(["screenshot"], blockers)
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function safariErrorText(error) {
|
|
534
|
+
if (error instanceof Error) {
|
|
535
|
+
return error.message;
|
|
536
|
+
}
|
|
537
|
+
return String(error ?? "Unknown Safari adapter error.");
|
|
538
|
+
}
|
|
539
|
+
function classifySafariRuntimeError(operation, error) {
|
|
540
|
+
const message = safariErrorText(error);
|
|
541
|
+
const normalized = message.toLowerCase();
|
|
542
|
+
if (normalized.includes("not authorized") ||
|
|
543
|
+
normalized.includes("not permitted") ||
|
|
544
|
+
normalized.includes("apple event") ||
|
|
545
|
+
normalized.includes("automation") ||
|
|
546
|
+
normalized.includes("screen recording permission") ||
|
|
547
|
+
normalized.includes("cgpreflightscreencaptureaccess returned false")) {
|
|
548
|
+
const code = operation === "screenshot" ? "screen_recording_permission_denied" : "automation_permission_denied";
|
|
549
|
+
const guidance = operation === "screenshot"
|
|
550
|
+
? "Grant Screen Recording permission for the host process, then retry."
|
|
551
|
+
: "Grant Automation/Apple Events permission for the host process to control Safari, then retry.";
|
|
552
|
+
return new errors_1.AppError(`Safari ${operation} permission denied. ${guidance} Raw error: ${message}`, 403, code);
|
|
553
|
+
}
|
|
554
|
+
if (normalized.includes("safari is not running")) {
|
|
555
|
+
return new errors_1.AppError(`Safari is not running, so ${operation} is unavailable until Safari is opened with at least one tab or window. Raw error: ${message}`, 503, "browser_not_running");
|
|
556
|
+
}
|
|
557
|
+
if (normalized.includes("has no open windows")) {
|
|
558
|
+
return new errors_1.AppError(`Safari has no open windows, so ${operation} is unavailable until a real Safari window exists. Raw error: ${message}`, 503, "browser_unavailable");
|
|
559
|
+
}
|
|
560
|
+
if (normalized.includes("target window is no longer available") || normalized.includes("target tab is no longer available")) {
|
|
561
|
+
return new errors_1.AppError(`Safari target disappeared before ${operation} could complete. Attach or resume the tab again and retry. Raw error: ${message}`, 404, "tab_not_found");
|
|
562
|
+
}
|
|
563
|
+
if (normalized.includes("bounds are unavailable or invalid")) {
|
|
564
|
+
return new errors_1.AppError(`Safari reported invalid window bounds for ${operation}, so screenshot capture was aborted before calling screencapture. Focus a normal Safari window and retry. Raw error: ${message}`, 503, "window_bounds_unavailable");
|
|
565
|
+
}
|
|
566
|
+
if (normalized.includes("could not create image from rect")) {
|
|
567
|
+
return new errors_1.AppError(`macOS screencapture rejected the Safari window region even after activation. This usually means the requested capture rect is not currently capturable on this host/display. Raw error: ${message}`, 503, "screenshot_capture_failed");
|
|
568
|
+
}
|
|
569
|
+
const genericCodeByOperation = {
|
|
570
|
+
inspect: "browser_unavailable",
|
|
571
|
+
activate: "activation_unavailable",
|
|
572
|
+
navigate: "navigation_unavailable",
|
|
573
|
+
screenshot: "screenshot_unavailable"
|
|
574
|
+
};
|
|
575
|
+
return new errors_1.AppError(`Unable to ${operation} Safari tab. Raw error: ${message}`, 503, genericCodeByOperation[operation]);
|
|
576
|
+
}
|
|
577
|
+
function hasSafariInspectableTabs(snapshot) {
|
|
578
|
+
return snapshot.tabCount > 0 && snapshot.inspectableWindowCount > 0;
|
|
579
|
+
}
|
|
580
|
+
function classifySafariTabResolutionError(target, snapshot) {
|
|
581
|
+
if (snapshot.windowCount === 0) {
|
|
582
|
+
if (target.type === "front") {
|
|
583
|
+
return new errors_1.AppError("Safari has no open windows, so the front tab is unavailable until a real Safari window exists.", 503, "browser_no_windows");
|
|
584
|
+
}
|
|
585
|
+
return new errors_1.AppError("Safari has no open windows, so there is no tab to resolve yet.", 503, "browser_no_windows");
|
|
586
|
+
}
|
|
587
|
+
if (!hasSafariInspectableTabs(snapshot)) {
|
|
588
|
+
if (target.type === "front") {
|
|
589
|
+
return new errors_1.AppError("Safari has open windows, but no inspectable tabs are currently available for the front window. Open or focus a normal Safari tab and retry.", 503, "browser_no_tabs");
|
|
590
|
+
}
|
|
591
|
+
return new errors_1.AppError("Safari has open windows, but no inspectable tabs are currently available to resolve. Open or focus a normal Safari tab and retry.", 503, "browser_no_tabs");
|
|
592
|
+
}
|
|
593
|
+
if (target.type === "front") {
|
|
594
|
+
return new errors_1.AppError("Unable to read the front Safari tab. Safari front window has no active tab.", 503, "browser_unavailable");
|
|
595
|
+
}
|
|
596
|
+
if (target.type === "indexed") {
|
|
597
|
+
return new errors_1.AppError(`Safari tab not found for window ${target.windowIndex}, tab ${target.tabIndex}.`, 404, "tab_not_found");
|
|
598
|
+
}
|
|
599
|
+
return new errors_1.AppError(`Safari tab not found for signature ${target.signature}.`, 404, "tab_not_found");
|
|
600
|
+
}
|
|
601
|
+
class SafariAdapter {
|
|
602
|
+
browser = "safari";
|
|
603
|
+
async inspectTabs() {
|
|
604
|
+
try {
|
|
605
|
+
const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", "-e", inspectWindowsScript]);
|
|
606
|
+
return parseSafariInspectionSnapshot(stdout);
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
throw classifySafariRuntimeError("inspect", error);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async listTabs() {
|
|
613
|
+
const snapshot = await this.inspectTabs();
|
|
614
|
+
return snapshot.tabs.map(withAttachedAt);
|
|
615
|
+
}
|
|
616
|
+
async resolveTab(target) {
|
|
617
|
+
const snapshot = await this.inspectTabs();
|
|
618
|
+
const tabs = snapshot.tabs.map(withAttachedAt);
|
|
619
|
+
if (target.type === "front") {
|
|
620
|
+
const frontTab = tabs.find((tab) => tab.isFrontWindow && tab.isActiveInWindow);
|
|
621
|
+
if (!frontTab) {
|
|
622
|
+
throw classifySafariTabResolutionError(target, snapshot);
|
|
623
|
+
}
|
|
624
|
+
return frontTab;
|
|
625
|
+
}
|
|
626
|
+
if (target.type === "indexed") {
|
|
627
|
+
const matchedTab = tabs.find((tab) => tab.windowIndex === target.windowIndex && tab.tabIndex === target.tabIndex);
|
|
628
|
+
if (!matchedTab) {
|
|
629
|
+
throw classifySafariTabResolutionError(target, snapshot);
|
|
630
|
+
}
|
|
631
|
+
return matchedTab;
|
|
632
|
+
}
|
|
633
|
+
const exactSignature = tabs.find((tab) => tab.identity.signature === target.signature);
|
|
634
|
+
if (exactSignature) {
|
|
635
|
+
return exactSignature;
|
|
636
|
+
}
|
|
637
|
+
const exactUrlTitle = tabs.find((tab) => tab.url === (target.url ?? "") &&
|
|
638
|
+
normalizeTitle(tab.title) === normalizeTitle(target.title ?? ""));
|
|
639
|
+
if (exactUrlTitle) {
|
|
640
|
+
return exactUrlTitle;
|
|
641
|
+
}
|
|
642
|
+
const exactUrl = target.url ? tabs.find((tab) => tab.url === target.url) : undefined;
|
|
643
|
+
if (exactUrl) {
|
|
644
|
+
return exactUrl;
|
|
645
|
+
}
|
|
646
|
+
const lastKnown = target.lastKnownWindowIndex && target.lastKnownTabIndex
|
|
647
|
+
? tabs.find((tab) => tab.windowIndex === target.lastKnownWindowIndex && tab.tabIndex === target.lastKnownTabIndex)
|
|
648
|
+
: undefined;
|
|
649
|
+
if (lastKnown) {
|
|
650
|
+
return lastKnown;
|
|
651
|
+
}
|
|
652
|
+
throw classifySafariTabResolutionError(target, snapshot);
|
|
653
|
+
}
|
|
654
|
+
async performSessionAction(action) {
|
|
655
|
+
const tab = await this.resolveTab(action.target);
|
|
656
|
+
if (action.action === "activate") {
|
|
657
|
+
const activationOptions = action.options && typeof action.options === "object" && "preferredWindowOrder" in action.options
|
|
658
|
+
? { preferredWindowOrder: action.options.preferredWindowOrder }
|
|
659
|
+
: undefined;
|
|
660
|
+
try {
|
|
661
|
+
const activation = await activateSafariTarget(tab, activationOptions?.preferredWindowOrder);
|
|
662
|
+
const result = {
|
|
663
|
+
action: "activate",
|
|
664
|
+
browser: this.browser,
|
|
665
|
+
tab,
|
|
666
|
+
activatedAt: new Date().toISOString(),
|
|
667
|
+
implementation: {
|
|
668
|
+
browserNative: false,
|
|
669
|
+
engine: "macos-osascript",
|
|
670
|
+
selectedTarget: true,
|
|
671
|
+
broughtAppToFront: true,
|
|
672
|
+
reorderedWindowToFront: activation.reorderedWindowToFront
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
throw classifySafariRuntimeError("activate", error);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (action.action === "navigate") {
|
|
682
|
+
const navigateActionOptions = action.options;
|
|
683
|
+
const navigateOptions = navigateActionOptions?.url
|
|
684
|
+
? {
|
|
685
|
+
url: String(navigateActionOptions.url),
|
|
686
|
+
preferredWindowOrder: navigateActionOptions.preferredWindowOrder
|
|
687
|
+
}
|
|
688
|
+
: undefined;
|
|
689
|
+
if (!navigateOptions?.url) {
|
|
690
|
+
throw new errors_1.AppError("url is required for Safari navigation.", 400, "invalid_action");
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
const navigation = await navigateSafariTarget(tab, navigateOptions.url, navigateOptions.preferredWindowOrder);
|
|
694
|
+
const result = {
|
|
695
|
+
action: "navigate",
|
|
696
|
+
browser: this.browser,
|
|
697
|
+
requestedUrl: navigateOptions.url,
|
|
698
|
+
previousTab: tab,
|
|
699
|
+
tab: navigation.tab,
|
|
700
|
+
navigatedAt: new Date().toISOString(),
|
|
701
|
+
implementation: {
|
|
702
|
+
browserNative: false,
|
|
703
|
+
engine: "macos-osascript",
|
|
704
|
+
selectedTarget: true,
|
|
705
|
+
broughtAppToFront: true,
|
|
706
|
+
reusedExistingTab: true
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
throw classifySafariRuntimeError("navigate", error);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (action.action !== "screenshot") {
|
|
716
|
+
throw new errors_1.AppError(`Unsupported Safari session action: ${action.action}`, 400, "unsupported_action");
|
|
717
|
+
}
|
|
718
|
+
const screenshotOptions = action.options;
|
|
719
|
+
const outputPath = screenshotOptions?.outputPath;
|
|
720
|
+
if (!outputPath) {
|
|
721
|
+
throw new errors_1.AppError("outputPath is required for Safari screenshots.", 500, "invalid_action");
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
await (0, promises_1.mkdir)((0, node_path_1.dirname)(outputPath), { recursive: true });
|
|
725
|
+
const screenRecordingAllowed = await screenRecordingPermissionGranted();
|
|
726
|
+
if (screenRecordingAllowed === false) {
|
|
727
|
+
throw new Error("CGPreflightScreenCaptureAccess returned false. Screen recording permission is required before Safari screenshots can be captured.");
|
|
728
|
+
}
|
|
729
|
+
const bounds = await activateSafariTarget(tab, screenshotOptions?.preferredWindowOrder);
|
|
730
|
+
const region = `${bounds.x},${bounds.y},${bounds.width},${bounds.height}`;
|
|
731
|
+
await execFileAsync("screencapture", ["-x", "-R", region, outputPath]);
|
|
732
|
+
const result = {
|
|
733
|
+
action: "screenshot",
|
|
734
|
+
browser: this.browser,
|
|
735
|
+
tab,
|
|
736
|
+
outputPath,
|
|
737
|
+
format: "png",
|
|
738
|
+
capturedAt: new Date().toISOString(),
|
|
739
|
+
implementation: {
|
|
740
|
+
browserNative: false,
|
|
741
|
+
engine: "macos-osascript-screencapture",
|
|
742
|
+
scope: "window",
|
|
743
|
+
activatedTarget: true,
|
|
744
|
+
includesBrowserChrome: true
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
throw classifySafariRuntimeError("screenshot", error);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async getDiagnostics() {
|
|
754
|
+
const [osascriptAvailable, screencaptureAvailable, applicationAvailable, isRunning, screenRecordingAllowed] = await Promise.all([
|
|
755
|
+
commandAvailable("osascript"),
|
|
756
|
+
commandAvailable("screencapture"),
|
|
757
|
+
safariApplicationAvailable(),
|
|
758
|
+
safariRunning(),
|
|
759
|
+
screenRecordingPermissionGranted()
|
|
760
|
+
]);
|
|
761
|
+
let windowCount;
|
|
762
|
+
let inspectableWindowCount;
|
|
763
|
+
let tabCount;
|
|
764
|
+
let probeError;
|
|
765
|
+
if (osascriptAvailable && applicationAvailable && isRunning) {
|
|
766
|
+
try {
|
|
767
|
+
const probe = await safariWindowProbe();
|
|
768
|
+
windowCount = probe.windowCount;
|
|
769
|
+
inspectableWindowCount = probe.inspectableWindowCount;
|
|
770
|
+
tabCount = probe.tabCount;
|
|
771
|
+
if (probe.probeError) {
|
|
772
|
+
probeError = probe.probeError;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
probeError = error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const constraints = [
|
|
780
|
+
"Safari automation depends on macOS Apple Events permissions.",
|
|
781
|
+
"Screenshots depend on macOS screen recording permissions.",
|
|
782
|
+
"Navigation, activation, and screenshots visibly focus Safari."
|
|
783
|
+
];
|
|
784
|
+
const preflight = buildSafariPreflight({
|
|
785
|
+
osascriptAvailable,
|
|
786
|
+
screencaptureAvailable,
|
|
787
|
+
applicationAvailable,
|
|
788
|
+
safariRunning: isRunning,
|
|
789
|
+
screenRecordingPermissionGranted: screenRecordingAllowed,
|
|
790
|
+
windowCount,
|
|
791
|
+
inspectableWindowCount,
|
|
792
|
+
tabCount,
|
|
793
|
+
probeError
|
|
794
|
+
});
|
|
795
|
+
return {
|
|
796
|
+
browser: this.browser,
|
|
797
|
+
checkedAt: new Date().toISOString(),
|
|
798
|
+
runtime: {
|
|
799
|
+
platform: process.platform,
|
|
800
|
+
arch: process.arch,
|
|
801
|
+
nodeVersion: process.version,
|
|
802
|
+
safariRunning: isRunning
|
|
803
|
+
},
|
|
804
|
+
host: {
|
|
805
|
+
osascriptAvailable,
|
|
806
|
+
screencaptureAvailable,
|
|
807
|
+
safariApplicationAvailable: applicationAvailable
|
|
808
|
+
},
|
|
809
|
+
supportedFeatures: {
|
|
810
|
+
inspectTabs: osascriptAvailable && applicationAvailable,
|
|
811
|
+
attach: true,
|
|
812
|
+
activate: osascriptAvailable && applicationAvailable,
|
|
813
|
+
navigate: osascriptAvailable && applicationAvailable,
|
|
814
|
+
screenshot: osascriptAvailable && screencaptureAvailable && applicationAvailable,
|
|
815
|
+
savedSessions: true,
|
|
816
|
+
cli: true,
|
|
817
|
+
httpApi: true
|
|
818
|
+
},
|
|
819
|
+
constraints,
|
|
820
|
+
preflight,
|
|
821
|
+
adapter: {
|
|
822
|
+
mode: "apple-events"
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
exports.SafariAdapter = SafariAdapter;
|