@narumitw/pi-chrome-devtools 0.51.0 → 0.52.1
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 +74 -42
- package/dist/chunks/browser-settings-menu-L4CZINWO.ts +266 -0
- package/dist/chunks/browser-settings-menu-L4CZINWO.ts.map +7 -0
- package/dist/chunks/chunk-RM7OGXVL.ts +1607 -0
- package/dist/chunks/chunk-RM7OGXVL.ts.map +7 -0
- package/dist/chunks/menu-U4AIAN7V.ts +397 -0
- package/dist/chunks/menu-U4AIAN7V.ts.map +7 -0
- package/dist/index.ts +762 -0
- package/dist/index.ts.map +7 -0
- package/package.json +11 -7
- package/src/browser-manager.ts +3 -5
- package/src/browser-settings-menu.ts +311 -0
- package/src/chrome-devtools.ts +22 -2
- package/src/menu.ts +12 -14
- package/src/runtime.ts +28 -23
- package/src/settings.ts +204 -38
- package/src/tool-selector.ts +3 -1
|
@@ -0,0 +1,1607 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
|
|
4
|
+
// src/tool-names.ts
|
|
5
|
+
var CHROME_DEVTOOLS_TOOL_NAMES = [
|
|
6
|
+
"chrome_devtools_list_pages",
|
|
7
|
+
"chrome_devtools_select_page",
|
|
8
|
+
"chrome_devtools_navigate",
|
|
9
|
+
"chrome_devtools_evaluate",
|
|
10
|
+
"chrome_devtools_screenshot"
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
// src/settings.ts
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { constants } from "node:fs";
|
|
16
|
+
import {
|
|
17
|
+
access,
|
|
18
|
+
lstat,
|
|
19
|
+
mkdir,
|
|
20
|
+
readFile,
|
|
21
|
+
realpath,
|
|
22
|
+
rename,
|
|
23
|
+
rm,
|
|
24
|
+
stat,
|
|
25
|
+
writeFile
|
|
26
|
+
} from "node:fs/promises";
|
|
27
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
28
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
var NEW_SETTINGS_FILE_NAME = "pi-chrome-devtools.json";
|
|
30
|
+
var LEGACY_SETTINGS_FILE_NAME = "pi-chrome-devtools-settings.json";
|
|
31
|
+
var DEFAULT_BROWSER_HOST = "127.0.0.1";
|
|
32
|
+
var DEFAULT_BROWSER_PORT = 9222;
|
|
33
|
+
var DEFAULT_BROWSER_ENDPOINT = `http://${DEFAULT_BROWSER_HOST}:${DEFAULT_BROWSER_PORT}`;
|
|
34
|
+
var DEFAULT_FILE_OPERATIONS = {
|
|
35
|
+
write: (path, data) => writeFile(path, data, "utf8").then(() => void 0),
|
|
36
|
+
rename
|
|
37
|
+
};
|
|
38
|
+
var settingsSaveQueue = Promise.resolve();
|
|
39
|
+
async function waitForSettingsWrites() {
|
|
40
|
+
await settingsSaveQueue;
|
|
41
|
+
}
|
|
42
|
+
async function loadSettings(options = {}) {
|
|
43
|
+
await settingsSaveQueue;
|
|
44
|
+
const userPath = settingsFilePath();
|
|
45
|
+
const projectPath = options.cwd ? projectSettingsFilePath(options.cwd) : void 0;
|
|
46
|
+
const paths = { user: userPath, ...projectPath ? { project: projectPath } : {} };
|
|
47
|
+
const warnings = [];
|
|
48
|
+
let user = await readSettingsDocument(userPath, "user");
|
|
49
|
+
const legacyExists = await fileExists(legacySettingsFilePath());
|
|
50
|
+
if (user.kind !== "missing" && legacyExists) {
|
|
51
|
+
warnings.push(
|
|
52
|
+
`Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${userPath} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (user.kind === "missing") {
|
|
56
|
+
const legacy = await readSettingsDocument(legacySettingsFilePath(), "user");
|
|
57
|
+
const concurrentlyCreated = await readSettingsDocument(userPath, "user");
|
|
58
|
+
if (concurrentlyCreated.kind !== "missing") {
|
|
59
|
+
user = concurrentlyCreated;
|
|
60
|
+
if (legacy.kind !== "missing") {
|
|
61
|
+
warnings.push(
|
|
62
|
+
`Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${userPath} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
} else if (legacy.kind !== "missing") {
|
|
66
|
+
user = legacy;
|
|
67
|
+
if (legacy.kind === "valid") {
|
|
68
|
+
warnings.push(
|
|
69
|
+
`Using legacy ${LEGACY_SETTINGS_FILE_NAME}; rename it to ${NEW_SETTINGS_FILE_NAME}. Future saves write ${NEW_SETTINGS_FILE_NAME} without modifying the legacy file.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
warnings.push(...user.warnings);
|
|
75
|
+
let project = { kind: "missing", warnings: [] };
|
|
76
|
+
if (projectPath && options.projectTrusted) {
|
|
77
|
+
project = await readSettingsDocument(projectPath, "project", options.cwd);
|
|
78
|
+
warnings.push(...project.warnings);
|
|
79
|
+
}
|
|
80
|
+
const environmentWarning = deprecatedEnvironmentWarning();
|
|
81
|
+
if (environmentWarning) warnings.push(environmentWarning);
|
|
82
|
+
const userBrowser = user.normalized?.browser ?? {};
|
|
83
|
+
const effectiveBrowser = resolveEffectiveBrowser(userBrowser, project.normalized?.browser);
|
|
84
|
+
const settings = resolveSettings(user.normalized, project.normalized, effectiveBrowser);
|
|
85
|
+
const recognized = settings.tools !== void 0 || user.normalized?.browser !== void 0 || project.normalized?.browser !== void 0;
|
|
86
|
+
const invalidReasons = [user, project].filter((result) => result.kind === "invalid").map((result) => result.reason).filter((reason) => Boolean(reason));
|
|
87
|
+
const userFile = user.kind === "invalid" ? {
|
|
88
|
+
kind: "invalid",
|
|
89
|
+
reason: user.reason ?? `${userPath}: invalid settings`
|
|
90
|
+
} : { kind: user.kind };
|
|
91
|
+
const base = {
|
|
92
|
+
effectiveBrowser,
|
|
93
|
+
paths,
|
|
94
|
+
userFile,
|
|
95
|
+
warnings,
|
|
96
|
+
...warnings.length > 0 ? { notice: warnings.join("\n") } : {}
|
|
97
|
+
};
|
|
98
|
+
if (recognized) return { ...base, kind: "loaded", settings };
|
|
99
|
+
if (invalidReasons.length > 0) {
|
|
100
|
+
return { ...base, kind: "invalid", reason: invalidReasons.join("; ") };
|
|
101
|
+
}
|
|
102
|
+
return { ...base, kind: "missing" };
|
|
103
|
+
}
|
|
104
|
+
function resolveSettings(user, _project, browser) {
|
|
105
|
+
return {
|
|
106
|
+
...user?.tools ? { tools: user.tools, updatedAt: user.updatedAt } : {},
|
|
107
|
+
browser
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function resolveEffectiveBrowser(user, project) {
|
|
111
|
+
const configuredEndpoint = parseBrowserEndpoint(user.endpoint ?? DEFAULT_BROWSER_ENDPOINT);
|
|
112
|
+
const environmentHost = process.env.PI_CHROME_DEVTOOLS_HOST;
|
|
113
|
+
const environmentPortValue = process.env.PI_CHROME_DEVTOOLS_PORT;
|
|
114
|
+
const environmentPort = parseConfiguredPort(environmentPortValue);
|
|
115
|
+
const environmentEndpointConfigured = environmentHost !== void 0 || environmentPort !== void 0;
|
|
116
|
+
const host = environmentHost ?? configuredEndpoint.host;
|
|
117
|
+
const port = environmentPort ?? configuredEndpoint.port;
|
|
118
|
+
const environmentAutoLaunch = process.env.PI_CHROME_DEVTOOLS_AUTO_LAUNCH;
|
|
119
|
+
const environmentExecutable = process.env.PI_CHROME_DEVTOOLS_BROWSER;
|
|
120
|
+
const executablePath = environmentExecutable || user.executablePath;
|
|
121
|
+
const extensionPaths = project?.extensionPaths ?? user.extensionPaths ?? [];
|
|
122
|
+
const endpointSource = environmentEndpointConfigured ? "environment" : user.endpoint ? "user" : "default";
|
|
123
|
+
return {
|
|
124
|
+
endpoint: formatBrowserEndpoint(host, port),
|
|
125
|
+
host,
|
|
126
|
+
port,
|
|
127
|
+
hostConfigured: environmentHost !== void 0 || user.endpoint !== void 0,
|
|
128
|
+
portConfigured: environmentPort !== void 0 || user.endpoint !== void 0,
|
|
129
|
+
autoLaunchEnabled: environmentAutoLaunch === void 0 ? user.autoLaunch ?? true : environmentAutoLaunch !== "0",
|
|
130
|
+
...executablePath ? { executablePath } : {},
|
|
131
|
+
extensionPaths: [...extensionPaths],
|
|
132
|
+
endpointSource,
|
|
133
|
+
autoLaunchSource: environmentAutoLaunch !== void 0 ? "environment" : user.autoLaunch !== void 0 ? "user" : "default",
|
|
134
|
+
executablePathSource: environmentExecutable ? "environment" : user.executablePath ? "user" : "default",
|
|
135
|
+
extensionPathsSource: project?.extensionPaths ? "project" : user.extensionPaths ? "user" : "default"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function deprecatedEnvironmentWarning() {
|
|
139
|
+
const configuredNames = [
|
|
140
|
+
"PI_CHROME_DEVTOOLS_HOST",
|
|
141
|
+
"PI_CHROME_DEVTOOLS_PORT",
|
|
142
|
+
"PI_CHROME_DEVTOOLS_AUTO_LAUNCH",
|
|
143
|
+
"PI_CHROME_DEVTOOLS_BROWSER"
|
|
144
|
+
].filter((name) => process.env[name] !== void 0);
|
|
145
|
+
if (configuredNames.length === 0) return void 0;
|
|
146
|
+
return `Chrome DevTools environment settings are deprecated and will be removed in a future version: ${configuredNames.join(", ")}. Move them to browser.endpoint, browser.autoLaunch, and browser.executablePath in ${settingsFilePath()}. Environment values remain active overrides during the deprecation period.`;
|
|
147
|
+
}
|
|
148
|
+
async function readSettingsDocument(filePath, scope, cwd) {
|
|
149
|
+
let text;
|
|
150
|
+
try {
|
|
151
|
+
text = await readFile(filePath, "utf8");
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing", warnings: [] };
|
|
154
|
+
const reason = `${filePath}: ${formatError(error)}`;
|
|
155
|
+
return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
|
|
156
|
+
}
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(text);
|
|
160
|
+
} catch {
|
|
161
|
+
const reason = `${filePath}: invalid JSON`;
|
|
162
|
+
return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
|
|
163
|
+
}
|
|
164
|
+
if (!isRecord(parsed)) {
|
|
165
|
+
const reason = `${filePath}: expected a JSON object`;
|
|
166
|
+
return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
|
|
167
|
+
}
|
|
168
|
+
const scopeWarnings = projectMachineSettingsWarnings(parsed, scope, filePath);
|
|
169
|
+
try {
|
|
170
|
+
const normalized = await normalizeSettingsDocument(parsed, scope, filePath, cwd);
|
|
171
|
+
return { kind: "valid", document: { ...parsed }, normalized, warnings: scopeWarnings };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const reason = `${filePath}: ${formatError(error)}`;
|
|
174
|
+
return {
|
|
175
|
+
kind: "invalid",
|
|
176
|
+
reason,
|
|
177
|
+
warnings: [...scopeWarnings, `Chrome DevTools settings ignored: ${reason}`]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function projectMachineSettingsWarnings(document, scope, filePath) {
|
|
182
|
+
if (scope !== "project" || !isRecord(document.browser)) return [];
|
|
183
|
+
const browser = document.browser;
|
|
184
|
+
return ["endpoint", "autoLaunch", "executablePath"].filter((field) => browser[field] !== void 0).map(
|
|
185
|
+
(field) => `Chrome DevTools project browser.${field} ignored in ${filePath}; configure this machine-owned setting in ${settingsFilePath()}.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
async function normalizeSettingsDocument(document, scope, filePath, cwd) {
|
|
189
|
+
const normalized = {};
|
|
190
|
+
if (scope === "user" && (document.tools !== void 0 || document.updatedAt !== void 0)) {
|
|
191
|
+
const toolSettings = normalizeChromeDevtoolsSettings(document);
|
|
192
|
+
if (!toolSettings) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"expected tools to be an array of Chrome DevTools tool names with numeric updatedAt"
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
normalized.tools = toolSettings.tools;
|
|
198
|
+
normalized.updatedAt = toolSettings.updatedAt;
|
|
199
|
+
}
|
|
200
|
+
if (document.browser !== void 0) {
|
|
201
|
+
if (!isRecord(document.browser)) throw new Error("expected browser to be an object");
|
|
202
|
+
normalized.browser = await normalizeBrowserSection(document.browser, scope, filePath, cwd);
|
|
203
|
+
}
|
|
204
|
+
return normalized;
|
|
205
|
+
}
|
|
206
|
+
async function normalizeBrowserSection(browser, scope, _filePath, cwd) {
|
|
207
|
+
const normalized = {};
|
|
208
|
+
if (scope === "user" && browser.endpoint !== void 0) {
|
|
209
|
+
if (typeof browser.endpoint !== "string") {
|
|
210
|
+
throw new Error("expected browser.endpoint to be an HTTP URL with an explicit port");
|
|
211
|
+
}
|
|
212
|
+
normalized.endpoint = parseBrowserEndpoint(browser.endpoint).endpoint;
|
|
213
|
+
}
|
|
214
|
+
if (scope === "user" && browser.autoLaunch !== void 0) {
|
|
215
|
+
if (typeof browser.autoLaunch !== "boolean") {
|
|
216
|
+
throw new Error("expected browser.autoLaunch to be a boolean");
|
|
217
|
+
}
|
|
218
|
+
normalized.autoLaunch = browser.autoLaunch;
|
|
219
|
+
}
|
|
220
|
+
if (scope === "user" && browser.executablePath !== void 0) {
|
|
221
|
+
if (typeof browser.executablePath !== "string" || browser.executablePath.length === 0) {
|
|
222
|
+
throw new Error("expected browser.executablePath to be a non-empty absolute path");
|
|
223
|
+
}
|
|
224
|
+
if (!isAbsolute(browser.executablePath)) {
|
|
225
|
+
throw new Error("browser.executablePath in user settings must be absolute");
|
|
226
|
+
}
|
|
227
|
+
normalized.executablePath = resolve(browser.executablePath);
|
|
228
|
+
}
|
|
229
|
+
if (browser.extensionPaths !== void 0) {
|
|
230
|
+
if (!Array.isArray(browser.extensionPaths) || !browser.extensionPaths.every((entry) => typeof entry === "string" && entry.length > 0)) {
|
|
231
|
+
throw new Error(`expected ${scope} browser.extensionPaths to be an array of non-empty paths`);
|
|
232
|
+
}
|
|
233
|
+
const resolvedPaths = [];
|
|
234
|
+
for (const configuredPath of browser.extensionPaths) {
|
|
235
|
+
if (scope === "user" && !isAbsolute(configuredPath)) {
|
|
236
|
+
throw new Error("browser.extensionPaths in user settings must contain only absolute paths");
|
|
237
|
+
}
|
|
238
|
+
const absolutePath = isAbsolute(configuredPath) ? resolve(configuredPath) : resolve(cwd ?? process.cwd(), configuredPath);
|
|
239
|
+
resolvedPaths.push(await validateUnpackedExtensionPath(absolutePath));
|
|
240
|
+
}
|
|
241
|
+
normalized.extensionPaths = orderedUnique(resolvedPaths);
|
|
242
|
+
}
|
|
243
|
+
return normalized;
|
|
244
|
+
}
|
|
245
|
+
async function validateUnpackedExtensionPath(extensionPath) {
|
|
246
|
+
let canonicalPath;
|
|
247
|
+
try {
|
|
248
|
+
canonicalPath = await realpath(extensionPath);
|
|
249
|
+
const extensionStat = await stat(canonicalPath);
|
|
250
|
+
if (!extensionStat.isDirectory()) throw new Error("not a directory");
|
|
251
|
+
} catch (error) {
|
|
252
|
+
throw new Error(`unpacked extension path ${extensionPath} is invalid: ${formatError(error)}`);
|
|
253
|
+
}
|
|
254
|
+
const manifestPath = join(canonicalPath, "manifest.json");
|
|
255
|
+
let manifest;
|
|
256
|
+
try {
|
|
257
|
+
const manifestStat = await lstat(manifestPath);
|
|
258
|
+
if (!manifestStat.isFile()) throw new Error("not a regular file");
|
|
259
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`unpacked extension ${canonicalPath} has invalid manifest.json: ${formatError(error)}`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (canonicalPath.includes(",")) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`unpacked extension path ${canonicalPath} cannot contain a comma because Chrome separates multiple --load-extension paths with commas`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (!isRecord(manifest) || ![2, 3].includes(manifest.manifest_version) || typeof manifest.name !== "string" || typeof manifest.version !== "string") {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`unpacked extension ${canonicalPath} has invalid manifest.json: expected manifest_version, name, and version`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return canonicalPath;
|
|
276
|
+
}
|
|
277
|
+
function parseBrowserEndpoint(value) {
|
|
278
|
+
const trimmedValue = value.trim();
|
|
279
|
+
const explicitPort = trimmedValue.match(/^http:\/\/(?:\[[^\]]+\]|[^/:?#]+):(\d+)\/?$/i)?.[1];
|
|
280
|
+
let endpoint;
|
|
281
|
+
try {
|
|
282
|
+
endpoint = new URL(trimmedValue);
|
|
283
|
+
} catch {
|
|
284
|
+
throw new Error("browser.endpoint must be a valid HTTP URL with an explicit port");
|
|
285
|
+
}
|
|
286
|
+
if (endpoint.protocol !== "http:" || endpoint.username || endpoint.password || !endpoint.hostname || !explicitPort || endpoint.pathname !== "/" && endpoint.pathname !== "" || endpoint.search || endpoint.hash) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
"browser.endpoint must be an HTTP origin with an explicit port and no credentials, path, query, or fragment"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const port = parseConfiguredPort(explicitPort);
|
|
292
|
+
if (port === void 0) {
|
|
293
|
+
throw new Error("browser.endpoint must use a port from 1 through 65535");
|
|
294
|
+
}
|
|
295
|
+
const host = endpoint.hostname;
|
|
296
|
+
return { endpoint: formatBrowserEndpoint(host, port), host, port };
|
|
297
|
+
}
|
|
298
|
+
function parseConfiguredPort(value) {
|
|
299
|
+
if (value === void 0) return void 0;
|
|
300
|
+
const trimmedValue = value.trim();
|
|
301
|
+
if (!/^\d+$/.test(trimmedValue)) return void 0;
|
|
302
|
+
const port = Number(trimmedValue);
|
|
303
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return void 0;
|
|
304
|
+
return port;
|
|
305
|
+
}
|
|
306
|
+
function formatBrowserEndpoint(host, port) {
|
|
307
|
+
const formattedHost = host.startsWith("[") && host.endsWith("]") ? host : host.includes(":") ? `[${host}]` : host;
|
|
308
|
+
return `http://${formattedHost}:${port}`;
|
|
309
|
+
}
|
|
310
|
+
function normalizeChromeDevtoolsSettings(value) {
|
|
311
|
+
if (!isRecord(value)) return void 0;
|
|
312
|
+
if (typeof value.updatedAt !== "number" || !Number.isFinite(value.updatedAt)) return void 0;
|
|
313
|
+
if (value.tools === "enabled") {
|
|
314
|
+
return { tools: [...CHROME_DEVTOOLS_TOOL_NAMES], updatedAt: value.updatedAt };
|
|
315
|
+
}
|
|
316
|
+
if (value.tools === "disabled") return { tools: [], updatedAt: value.updatedAt };
|
|
317
|
+
if (!Array.isArray(value.tools) || !value.tools.every(isChromeDevtoolsToolName)) return void 0;
|
|
318
|
+
return { tools: orderedUniqueChromeDevtoolsTools(value.tools), updatedAt: value.updatedAt };
|
|
319
|
+
}
|
|
320
|
+
function isChromeDevtoolsToolName(value) {
|
|
321
|
+
return typeof value === "string" && CHROME_DEVTOOLS_TOOL_NAMES.includes(value);
|
|
322
|
+
}
|
|
323
|
+
function orderedUniqueChromeDevtoolsTools(tools) {
|
|
324
|
+
const selectedTools = new Set(tools);
|
|
325
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => selectedTools.has(toolName));
|
|
326
|
+
}
|
|
327
|
+
function orderedUnique(values) {
|
|
328
|
+
return [...new Set(values)];
|
|
329
|
+
}
|
|
330
|
+
function saveSettings(settings, operations = {}) {
|
|
331
|
+
return queueSettingsMutation(
|
|
332
|
+
(current) => ({
|
|
333
|
+
...current,
|
|
334
|
+
tools: [...settings.tools],
|
|
335
|
+
updatedAt: settings.updatedAt
|
|
336
|
+
}),
|
|
337
|
+
operations
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function saveBrowserSettings(patch, operations = {}) {
|
|
341
|
+
return queueSettingsMutation(async (current) => {
|
|
342
|
+
const browser = isRecord(current.browser) ? { ...current.browser } : {};
|
|
343
|
+
for (const field of ["endpoint", "autoLaunch", "executablePath"]) {
|
|
344
|
+
const value = patch[field];
|
|
345
|
+
if (value === void 0) continue;
|
|
346
|
+
if (value === null) delete browser[field];
|
|
347
|
+
else browser[field] = value;
|
|
348
|
+
}
|
|
349
|
+
await normalizeBrowserSection(browser, "user", settingsFilePath());
|
|
350
|
+
return { ...current, browser };
|
|
351
|
+
}, operations);
|
|
352
|
+
}
|
|
353
|
+
function queueSettingsMutation(mutate, operations) {
|
|
354
|
+
const operation = settingsSaveQueue.then(() => saveSettingsMutationNow(mutate, operations));
|
|
355
|
+
settingsSaveQueue = operation.catch(() => void 0);
|
|
356
|
+
return operation;
|
|
357
|
+
}
|
|
358
|
+
async function saveSettingsMutationNow(mutate, operations) {
|
|
359
|
+
const filePath = settingsFilePath();
|
|
360
|
+
let current = await readSettingsDocument(filePath, "user");
|
|
361
|
+
const replaceCanonical = current.kind !== "missing";
|
|
362
|
+
if (!replaceCanonical) current = await readSettingsDocument(legacySettingsFilePath(), "user");
|
|
363
|
+
if (current.kind === "invalid") {
|
|
364
|
+
throw new Error(`Cannot save Chrome DevTools settings until you repair ${current.reason}`);
|
|
365
|
+
}
|
|
366
|
+
const nextDocument = await mutate(current.document ?? {});
|
|
367
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
368
|
+
const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
369
|
+
try {
|
|
370
|
+
await (operations.write ?? DEFAULT_FILE_OPERATIONS.write)(
|
|
371
|
+
tempFile,
|
|
372
|
+
`${JSON.stringify(nextDocument, null, 2)}
|
|
373
|
+
`
|
|
374
|
+
);
|
|
375
|
+
if (!replaceCanonical && await pathEntryExists(filePath)) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`${NEW_SETTINGS_FILE_NAME} was created concurrently; reopen settings and retry.`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
await (operations.rename ?? DEFAULT_FILE_OPERATIONS.rename)(tempFile, filePath);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
await rm(tempFile, { force: true }).catch(() => void 0);
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function settingsFilePath() {
|
|
387
|
+
return join(getAgentDir(), NEW_SETTINGS_FILE_NAME);
|
|
388
|
+
}
|
|
389
|
+
function projectSettingsFilePath(cwd) {
|
|
390
|
+
return join(cwd, CONFIG_DIR_NAME, NEW_SETTINGS_FILE_NAME);
|
|
391
|
+
}
|
|
392
|
+
function legacySettingsFilePath() {
|
|
393
|
+
return join(getAgentDir(), LEGACY_SETTINGS_FILE_NAME);
|
|
394
|
+
}
|
|
395
|
+
async function fileExists(filePath) {
|
|
396
|
+
try {
|
|
397
|
+
await access(filePath, constants.F_OK);
|
|
398
|
+
return true;
|
|
399
|
+
} catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function pathEntryExists(filePath) {
|
|
404
|
+
try {
|
|
405
|
+
await lstat(filePath);
|
|
406
|
+
return true;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function isRecord(value) {
|
|
413
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
414
|
+
}
|
|
415
|
+
function isNodeError(error) {
|
|
416
|
+
return error instanceof Error && "code" in error;
|
|
417
|
+
}
|
|
418
|
+
function formatError(error) {
|
|
419
|
+
return error instanceof Error ? error.message : String(error);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/runtime.ts
|
|
423
|
+
var DEFAULT_HOST = DEFAULT_BROWSER_HOST;
|
|
424
|
+
var DEFAULT_PORT = DEFAULT_BROWSER_PORT;
|
|
425
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
426
|
+
var DEFAULT_HTTP_TIMEOUT_MS = 1e3;
|
|
427
|
+
var DEFAULT_ENDPOINT_WAIT_MS = 5e3;
|
|
428
|
+
var DEFAULT_ENDPOINT_RETRY_MS = 250;
|
|
429
|
+
var MANAGED_BROWSER_PROFILE_PREFIX = "pi-chrome-devtools-profile-";
|
|
430
|
+
var DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
|
|
431
|
+
var BROWSER_SHUTDOWN_WAIT_MS = 1500;
|
|
432
|
+
var state = {
|
|
433
|
+
host: DEFAULT_HOST,
|
|
434
|
+
port: DEFAULT_PORT,
|
|
435
|
+
configuredPort: DEFAULT_PORT,
|
|
436
|
+
hostConfigured: false,
|
|
437
|
+
portConfigured: false,
|
|
438
|
+
autoLaunchEnabled: true,
|
|
439
|
+
endpointSource: "default",
|
|
440
|
+
autoLaunchSource: "default",
|
|
441
|
+
extensionPaths: [],
|
|
442
|
+
browserExecutableSource: "default",
|
|
443
|
+
extensionPathsSource: "default",
|
|
444
|
+
projectSettingsTrusted: false,
|
|
445
|
+
shuttingDown: false,
|
|
446
|
+
sessionGeneration: 0,
|
|
447
|
+
sessionController: new AbortController()
|
|
448
|
+
};
|
|
449
|
+
function applyRuntimeBrowserSettings(browser, paths, projectTrusted) {
|
|
450
|
+
state.host = browser.host;
|
|
451
|
+
state.port = browser.port;
|
|
452
|
+
state.configuredPort = browser.port;
|
|
453
|
+
state.hostConfigured = browser.hostConfigured;
|
|
454
|
+
state.portConfigured = browser.portConfigured;
|
|
455
|
+
state.autoLaunchEnabled = browser.autoLaunchEnabled;
|
|
456
|
+
state.endpointSource = browser.endpointSource;
|
|
457
|
+
state.autoLaunchSource = browser.autoLaunchSource;
|
|
458
|
+
state.browserExecutable = browser.executablePath;
|
|
459
|
+
state.extensionPaths = [...browser.extensionPaths];
|
|
460
|
+
state.browserExecutableSource = browser.executablePathSource;
|
|
461
|
+
state.extensionPathsSource = browser.extensionPathsSource;
|
|
462
|
+
state.settingsFilePath = paths.user;
|
|
463
|
+
state.projectSettingsFilePath = paths.project;
|
|
464
|
+
state.projectSettingsTrusted = projectTrusted;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/browser-manager.ts
|
|
468
|
+
import { execFile, spawn } from "node:child_process";
|
|
469
|
+
import { constants as constants2 } from "node:fs";
|
|
470
|
+
import { access as access2, mkdtemp, readFile as readFile2, rm as rm2 } from "node:fs/promises";
|
|
471
|
+
import { createServer } from "node:net";
|
|
472
|
+
import { tmpdir } from "node:os";
|
|
473
|
+
import { delimiter, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
|
|
474
|
+
var DEFAULT_BROWSER_MANAGER_OPERATIONS = {
|
|
475
|
+
access: access2,
|
|
476
|
+
mkdtemp,
|
|
477
|
+
readFile: readFile2,
|
|
478
|
+
rm: (path, options) => rm2(path, options),
|
|
479
|
+
fetch: (input, init) => fetch(input, init),
|
|
480
|
+
spawn,
|
|
481
|
+
inspectBrowserVersion,
|
|
482
|
+
isPortAvailable,
|
|
483
|
+
sleep: abortableSleep
|
|
484
|
+
};
|
|
485
|
+
var browserManagerOperations = DEFAULT_BROWSER_MANAGER_OPERATIONS;
|
|
486
|
+
function formatError2(error) {
|
|
487
|
+
return error instanceof Error ? error.message : String(error);
|
|
488
|
+
}
|
|
489
|
+
function isNodeError2(error) {
|
|
490
|
+
return error instanceof Error && "code" in error;
|
|
491
|
+
}
|
|
492
|
+
function normalizePathForComparison(value) {
|
|
493
|
+
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
494
|
+
}
|
|
495
|
+
async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS) {
|
|
496
|
+
if (extensionsConfigured()) {
|
|
497
|
+
validateExtensionLaunchMode();
|
|
498
|
+
await ensureManagedBrowserLaunched(waitMs);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (canAutoLaunchBrowser()) {
|
|
502
|
+
try {
|
|
503
|
+
await withEndpointRetry(() => fetchDevToolsJson("/json/version"), waitMs);
|
|
504
|
+
return;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (shouldAutoLaunchAfterEndpointError(error)) {
|
|
507
|
+
await ensureManagedBrowserLaunched(waitMs);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
await fetchDevToolsJson("/json/version");
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (isRetryableEndpointError(error)) return;
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function validateExtensionLaunchMode() {
|
|
521
|
+
if (!isLocalDevToolsHost(state.host)) {
|
|
522
|
+
throw new DevToolsEndpointError(
|
|
523
|
+
"Unpacked extensions require a local extension-owned managed browser; remote CDP endpoints cannot be modified."
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (!state.autoLaunchEnabled) {
|
|
527
|
+
throw new DevToolsEndpointError(
|
|
528
|
+
"Auto-launch is required for unpacked extensions so Pi can create an isolated managed browser."
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (!state.browserExecutable) {
|
|
532
|
+
throw new DevToolsEndpointError(
|
|
533
|
+
"Unpacked extensions require browser.executablePath for Chrome for Testing or Chromium in pi-chrome-devtools.json."
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function ensureManagedBrowserLaunched(waitMs) {
|
|
538
|
+
if (state.launchPromise) return state.launchPromise;
|
|
539
|
+
if (state.managedBrowser && !state.managedBrowser.exited && state.managedBrowser.ready) return;
|
|
540
|
+
const generation = state.sessionGeneration;
|
|
541
|
+
const signal = state.sessionController.signal;
|
|
542
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
543
|
+
const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal);
|
|
544
|
+
const wrappedPromise = launchPromise.finally(() => {
|
|
545
|
+
if (state.launchPromise === wrappedPromise) state.launchPromise = void 0;
|
|
546
|
+
});
|
|
547
|
+
state.launchPromise = wrappedPromise;
|
|
548
|
+
return wrappedPromise;
|
|
549
|
+
}
|
|
550
|
+
async function prepareManagedBrowserLaunch(waitMs, generation, signal) {
|
|
551
|
+
if (state.managedBrowser) {
|
|
552
|
+
await shutdownManagedBrowser(state.managedBrowser, { awaitLaunch: false });
|
|
553
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
554
|
+
}
|
|
555
|
+
if (state.portConfigured) {
|
|
556
|
+
const available = await browserManagerOperations.isPortAvailable(state.host, state.port);
|
|
557
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
558
|
+
if (!available) {
|
|
559
|
+
throw new DevToolsEndpointError(
|
|
560
|
+
`Cannot launch the extension-owned browser because explicit port ${state.port} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return launchManagedBrowser(waitMs, generation, signal);
|
|
565
|
+
}
|
|
566
|
+
async function launchManagedBrowser(waitMs, generation, signal) {
|
|
567
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
568
|
+
const candidateDefinitions = browserCandidateDefinitions();
|
|
569
|
+
const candidates = await resolveBrowserCandidates(candidateDefinitions, generation, signal);
|
|
570
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
571
|
+
state.lastLaunchAttempt = {
|
|
572
|
+
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
573
|
+
mode: state.portConfigured ? "explicit-port" : "dynamic-port"
|
|
574
|
+
};
|
|
575
|
+
if (candidates.length === 0) {
|
|
576
|
+
throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions));
|
|
577
|
+
}
|
|
578
|
+
let lastError;
|
|
579
|
+
for (const candidate of candidates) {
|
|
580
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
581
|
+
try {
|
|
582
|
+
if (extensionsConfigured())
|
|
583
|
+
await requireSupportedExtensionBrowser(candidate, generation, signal);
|
|
584
|
+
await launchBrowserCandidate(candidate, waitMs, generation, signal);
|
|
585
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
586
|
+
state.lastLaunchAttempt = {
|
|
587
|
+
...state.lastLaunchAttempt,
|
|
588
|
+
selectedCandidate: formatBrowserCandidate(candidate),
|
|
589
|
+
userDataDir: state.managedBrowser?.userDataDir
|
|
590
|
+
};
|
|
591
|
+
return;
|
|
592
|
+
} catch (error) {
|
|
593
|
+
try {
|
|
594
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
595
|
+
} catch (cancellation) {
|
|
596
|
+
state.lastLaunchAttempt = {
|
|
597
|
+
...state.lastLaunchAttempt,
|
|
598
|
+
lastError: formatError2(cancellation)
|
|
599
|
+
};
|
|
600
|
+
throw cancellation;
|
|
601
|
+
}
|
|
602
|
+
lastError = error;
|
|
603
|
+
state.lastLaunchAttempt = {
|
|
604
|
+
...state.lastLaunchAttempt,
|
|
605
|
+
lastError: formatError2(error)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (lastError instanceof DevToolsEndpointError && extensionsConfigured()) throw lastError;
|
|
610
|
+
throw new DevToolsEndpointError(
|
|
611
|
+
[
|
|
612
|
+
"Unable to auto-launch a Chromium-family browser for Chrome DevTools.",
|
|
613
|
+
`Tried: ${candidates.map(formatBrowserCandidate).join(", ")}`,
|
|
614
|
+
lastError ? `Last error: ${formatError2(lastError)}` : void 0,
|
|
615
|
+
launchHint(),
|
|
616
|
+
endpointConfigHint()
|
|
617
|
+
].filter(Boolean).join("\n")
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
async function requireSupportedExtensionBrowser(candidate, generation, signal) {
|
|
621
|
+
let version;
|
|
622
|
+
try {
|
|
623
|
+
version = (await browserManagerOperations.inspectBrowserVersion(candidate.resolvedExecutable, signal)).trim();
|
|
624
|
+
} catch (error) {
|
|
625
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
626
|
+
throw new DevToolsEndpointError(
|
|
627
|
+
`Unable to identify the configured extension browser ${candidate.resolvedExecutable}: ${formatError2(error)}. Configure Chrome for Testing or Chromium.`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
631
|
+
const classification = classifyExtensionBrowserVersion(version);
|
|
632
|
+
if (!classification.supported) {
|
|
633
|
+
throw new DevToolsEndpointError(
|
|
634
|
+
`Unsupported browser for unpacked extensions: ${version || "unknown product"}. Configure Chrome for Testing or Chromium; branded Chrome may silently ignore extension startup flags.`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function classifyExtensionBrowserVersion(version) {
|
|
639
|
+
if (/^(?:Google )?Chrome for Testing\b/i.test(version)) {
|
|
640
|
+
return { supported: true, product: "Chrome for Testing" };
|
|
641
|
+
}
|
|
642
|
+
if (/^Chromium\b/i.test(version)) return { supported: true, product: "Chromium" };
|
|
643
|
+
const product = version.trim().replace(/\s+\d+(?:\.\d+)*.*$/, "") || "Unknown";
|
|
644
|
+
return { supported: false, product };
|
|
645
|
+
}
|
|
646
|
+
async function launchBrowserCandidate(candidate, waitMs, generation, signal) {
|
|
647
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
648
|
+
const userDataDir = await browserManagerOperations.mkdtemp(
|
|
649
|
+
join2(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX)
|
|
650
|
+
);
|
|
651
|
+
let managedBrowser;
|
|
652
|
+
try {
|
|
653
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
654
|
+
const portArgument = state.portConfigured ? String(state.port) : "0";
|
|
655
|
+
const args = buildManagedBrowserLaunchArguments(
|
|
656
|
+
userDataDir,
|
|
657
|
+
portArgument,
|
|
658
|
+
state.extensionPaths
|
|
659
|
+
);
|
|
660
|
+
const child = browserManagerOperations.spawn(candidate.resolvedExecutable, args, {
|
|
661
|
+
shell: false,
|
|
662
|
+
stdio: "ignore"
|
|
663
|
+
});
|
|
664
|
+
const launchedBrowser = {
|
|
665
|
+
process: child,
|
|
666
|
+
userDataDir,
|
|
667
|
+
exited: false,
|
|
668
|
+
ready: false,
|
|
669
|
+
ownerGeneration: generation
|
|
670
|
+
};
|
|
671
|
+
managedBrowser = launchedBrowser;
|
|
672
|
+
state.managedBrowser = launchedBrowser;
|
|
673
|
+
child.once("exit", () => {
|
|
674
|
+
launchedBrowser.exited = true;
|
|
675
|
+
launchedBrowser.ready = false;
|
|
676
|
+
if (!state.portConfigured && launchedBrowser.port === state.port) {
|
|
677
|
+
state.port = state.configuredPort;
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
await waitForBrowserSpawn(child);
|
|
681
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
682
|
+
if (state.portConfigured) {
|
|
683
|
+
launchedBrowser.port = state.port;
|
|
684
|
+
} else {
|
|
685
|
+
launchedBrowser.port = await readManagedBrowserPort(
|
|
686
|
+
userDataDir,
|
|
687
|
+
launchedBrowser,
|
|
688
|
+
waitMs,
|
|
689
|
+
generation,
|
|
690
|
+
signal
|
|
691
|
+
);
|
|
692
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
693
|
+
state.port = launchedBrowser.port;
|
|
694
|
+
}
|
|
695
|
+
await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal);
|
|
696
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
697
|
+
launchedBrowser.ready = true;
|
|
698
|
+
} catch (error) {
|
|
699
|
+
if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false });
|
|
700
|
+
else {
|
|
701
|
+
await browserManagerOperations.rm(userDataDir, { recursive: true, force: true }).catch(() => void 0);
|
|
702
|
+
}
|
|
703
|
+
throw error;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function buildManagedBrowserLaunchArguments(userDataDir, portArgument, extensionPaths) {
|
|
707
|
+
const extensionList = extensionPaths.join(",");
|
|
708
|
+
return [
|
|
709
|
+
`--remote-debugging-port=${portArgument}`,
|
|
710
|
+
`--user-data-dir=${userDataDir}`,
|
|
711
|
+
...extensionPaths.length > 0 ? [`--disable-extensions-except=${extensionList}`, `--load-extension=${extensionList}`] : [],
|
|
712
|
+
// Chrome 138+ de-elevates when launched from an elevated process on Windows: the
|
|
713
|
+
// spawned process relaunches a de-elevated child and exits immediately, which the
|
|
714
|
+
// exit watchdog misreads as a failed launch.
|
|
715
|
+
...process.platform === "win32" ? ["--do-not-de-elevate"] : [],
|
|
716
|
+
"--no-first-run",
|
|
717
|
+
"--no-default-browser-check",
|
|
718
|
+
"about:blank"
|
|
719
|
+
];
|
|
720
|
+
}
|
|
721
|
+
function waitForBrowserSpawn(child) {
|
|
722
|
+
return new Promise((resolve3, reject) => {
|
|
723
|
+
let settled = false;
|
|
724
|
+
const settle = (callback) => {
|
|
725
|
+
if (settled) return;
|
|
726
|
+
settled = true;
|
|
727
|
+
child.off("error", onError);
|
|
728
|
+
child.off("spawn", onSpawn);
|
|
729
|
+
callback();
|
|
730
|
+
};
|
|
731
|
+
const onError = (error) => settle(() => reject(error));
|
|
732
|
+
const onSpawn = () => settle(resolve3);
|
|
733
|
+
child.once("error", onError);
|
|
734
|
+
child.once("spawn", onSpawn);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, generation, signal) {
|
|
738
|
+
const activePortFile = join2(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
|
|
739
|
+
const deadline = Date.now() + waitMs;
|
|
740
|
+
while (true) {
|
|
741
|
+
throwIfManagedBrowserExited(managedBrowser);
|
|
742
|
+
const text = await browserManagerOperations.readFile(activePortFile, "utf8").catch((error) => {
|
|
743
|
+
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
744
|
+
throw error;
|
|
745
|
+
});
|
|
746
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
747
|
+
const portText = text?.split(/\r?\n/, 1)[0]?.trim();
|
|
748
|
+
const port = Number(portText);
|
|
749
|
+
if (Number.isInteger(port) && port > 0) return port;
|
|
750
|
+
const remainingMs = deadline - Date.now();
|
|
751
|
+
if (remainingMs <= 0) {
|
|
752
|
+
throw new DevToolsEndpointError(
|
|
753
|
+
[
|
|
754
|
+
"Timed out waiting for auto-launched browser DevToolsActivePort.",
|
|
755
|
+
`Expected file: ${activePortFile}`,
|
|
756
|
+
launchHint()
|
|
757
|
+
].join("\n")
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function waitForDevToolsEndpoint(waitMs, managedBrowser, generation, signal) {
|
|
764
|
+
const deadline = Date.now() + waitMs;
|
|
765
|
+
while (true) {
|
|
766
|
+
throwIfManagedBrowserExited(managedBrowser);
|
|
767
|
+
try {
|
|
768
|
+
await fetchDevToolsJson("/json/version", { signal });
|
|
769
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
770
|
+
return;
|
|
771
|
+
} catch (error) {
|
|
772
|
+
if (!isRetryableEndpointError(error)) throw error;
|
|
773
|
+
}
|
|
774
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
775
|
+
const remainingMs = deadline - Date.now();
|
|
776
|
+
if (remainingMs <= 0) {
|
|
777
|
+
throw new DevToolsEndpointError(
|
|
778
|
+
[
|
|
779
|
+
`Timed out waiting for auto-launched browser at ${devToolsEndpoint()}.`,
|
|
780
|
+
launchHint()
|
|
781
|
+
].join("\n")
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function throwIfManagedBrowserExited(managedBrowser) {
|
|
788
|
+
if (!managedBrowser.exited) return;
|
|
789
|
+
throw new DevToolsEndpointError("Auto-launched browser exited before DevTools became available.");
|
|
790
|
+
}
|
|
791
|
+
function throwIfBrowserLaunchCancelled(generation, signal) {
|
|
792
|
+
if (!state.shuttingDown && generation === state.sessionGeneration && !signal.aborted) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
throw new DevToolsEndpointError(
|
|
796
|
+
generation === state.sessionGeneration ? "Chrome DevTools browser launch cancelled during shutdown." : "Chrome DevTools browser launch cancelled because the session was replaced."
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
async function shutdownManagedBrowser(managedBrowser = state.managedBrowser, options = {}) {
|
|
800
|
+
if (options.cancelLaunch) state.shuttingDown = true;
|
|
801
|
+
if (options.awaitLaunch !== false) {
|
|
802
|
+
await state.launchPromise?.catch(() => void 0);
|
|
803
|
+
managedBrowser = managedBrowser ?? state.managedBrowser;
|
|
804
|
+
}
|
|
805
|
+
if (!managedBrowser) return;
|
|
806
|
+
if (managedBrowser.cleanupPromise) return managedBrowser.cleanupPromise;
|
|
807
|
+
const cleanup = cleanupManagedBrowser(managedBrowser);
|
|
808
|
+
managedBrowser.cleanupPromise = cleanup;
|
|
809
|
+
return cleanup;
|
|
810
|
+
}
|
|
811
|
+
async function cleanupManagedBrowser(managedBrowser) {
|
|
812
|
+
if (state.managedBrowser === managedBrowser) state.managedBrowser = void 0;
|
|
813
|
+
if (!managedBrowser.exited) {
|
|
814
|
+
killManagedBrowserProcess(managedBrowser);
|
|
815
|
+
await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(async () => {
|
|
816
|
+
killManagedBrowserProcess(managedBrowser, "SIGKILL");
|
|
817
|
+
await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(
|
|
818
|
+
() => void 0
|
|
819
|
+
);
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
await browserManagerOperations.rm(managedBrowser.userDataDir, { recursive: true, force: true }).catch(() => void 0);
|
|
823
|
+
if (!state.portConfigured && managedBrowser.port === state.port) {
|
|
824
|
+
state.port = state.configuredPort;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function killManagedBrowserProcess(managedBrowser, signal) {
|
|
828
|
+
try {
|
|
829
|
+
managedBrowser.process.kill(signal);
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function waitForManagedBrowserExit(managedBrowser, waitMs) {
|
|
834
|
+
if (managedBrowser.exited) return Promise.resolve();
|
|
835
|
+
return new Promise((resolve3, reject) => {
|
|
836
|
+
const settle = (callback) => {
|
|
837
|
+
clearTimeout(timeout);
|
|
838
|
+
managedBrowser.process.off("exit", onExitOrClose);
|
|
839
|
+
managedBrowser.process.off("close", onExitOrClose);
|
|
840
|
+
callback();
|
|
841
|
+
};
|
|
842
|
+
const onExitOrClose = () => {
|
|
843
|
+
managedBrowser.exited = true;
|
|
844
|
+
settle(resolve3);
|
|
845
|
+
};
|
|
846
|
+
const timeout = setTimeout(
|
|
847
|
+
() => settle(() => reject(new Error("Timed out waiting for browser shutdown."))),
|
|
848
|
+
waitMs
|
|
849
|
+
);
|
|
850
|
+
managedBrowser.process.once("exit", onExitOrClose);
|
|
851
|
+
managedBrowser.process.once("close", onExitOrClose);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
async function fetchDevToolsJson(path, init) {
|
|
855
|
+
const url = `${devToolsEndpoint()}${path}`;
|
|
856
|
+
let response;
|
|
857
|
+
try {
|
|
858
|
+
const timeoutSignal = AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS);
|
|
859
|
+
response = await browserManagerOperations.fetch(url, {
|
|
860
|
+
...init,
|
|
861
|
+
signal: init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
862
|
+
});
|
|
863
|
+
} catch (error) {
|
|
864
|
+
throw new DevToolsEndpointError(endpointConnectionErrorMessage(error), {
|
|
865
|
+
launchable: true,
|
|
866
|
+
retryable: true
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (!response.ok) {
|
|
870
|
+
const body = (await response.text().catch(() => "")).trim();
|
|
871
|
+
const suffix = body ? `: ${body.slice(0, 200)}` : "";
|
|
872
|
+
throw new DevToolsEndpointError(
|
|
873
|
+
[
|
|
874
|
+
`Chrome DevTools endpoint ${url} returned ${response.status} ${response.statusText}${suffix}.`,
|
|
875
|
+
endpointConfigHint()
|
|
876
|
+
].join("\n"),
|
|
877
|
+
{ retryable: response.status === 429 || response.status >= 500 }
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
return await response.json();
|
|
882
|
+
} catch (error) {
|
|
883
|
+
throw new DevToolsEndpointError(
|
|
884
|
+
[
|
|
885
|
+
`Chrome DevTools endpoint ${url} returned invalid JSON: ${formatError2(error)}.`,
|
|
886
|
+
endpointConfigHint()
|
|
887
|
+
].join("\n")
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async function withEndpointRetry(operation, waitMs) {
|
|
892
|
+
const deadline = Date.now() + waitMs;
|
|
893
|
+
while (true) {
|
|
894
|
+
try {
|
|
895
|
+
return await operation();
|
|
896
|
+
} catch (error) {
|
|
897
|
+
if (!isRetryableEndpointError(error)) throw error;
|
|
898
|
+
const remainingMs = deadline - Date.now();
|
|
899
|
+
if (remainingMs <= 0) throw error;
|
|
900
|
+
await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function isRetryableEndpointError(error) {
|
|
905
|
+
return error instanceof DevToolsEndpointError && error.retryable;
|
|
906
|
+
}
|
|
907
|
+
function isLaunchableEndpointError(error) {
|
|
908
|
+
return error instanceof DevToolsEndpointError && error.launchable;
|
|
909
|
+
}
|
|
910
|
+
function shouldAutoLaunchAfterEndpointError(error) {
|
|
911
|
+
if (!canAutoLaunchBrowser()) return false;
|
|
912
|
+
if (isLaunchableEndpointError(error)) return true;
|
|
913
|
+
return !state.portConfigured && error instanceof DevToolsEndpointError;
|
|
914
|
+
}
|
|
915
|
+
function canAutoLaunchBrowser() {
|
|
916
|
+
return state.autoLaunchEnabled && isLocalDevToolsHost(state.host);
|
|
917
|
+
}
|
|
918
|
+
function extensionsConfigured() {
|
|
919
|
+
return state.extensionPaths.length > 0;
|
|
920
|
+
}
|
|
921
|
+
function endpointConnectionErrorMessage(error) {
|
|
922
|
+
const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
|
|
923
|
+
return [
|
|
924
|
+
`Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint()} (${reason}).`,
|
|
925
|
+
launchHint(),
|
|
926
|
+
endpointConfigHint()
|
|
927
|
+
].join("\n");
|
|
928
|
+
}
|
|
929
|
+
function isTimeoutError(error) {
|
|
930
|
+
return error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
|
|
931
|
+
}
|
|
932
|
+
function devToolsEndpoint() {
|
|
933
|
+
return `http://${formatHostForUrl(state.host)}:${state.port}`;
|
|
934
|
+
}
|
|
935
|
+
function formatHostForUrl(host) {
|
|
936
|
+
if (host.startsWith("[") && host.endsWith("]")) return host;
|
|
937
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
938
|
+
}
|
|
939
|
+
function endpointSourceLabel() {
|
|
940
|
+
return state.endpointSource;
|
|
941
|
+
}
|
|
942
|
+
function browserLifecycleState() {
|
|
943
|
+
if (state.launchPromise) return "starting";
|
|
944
|
+
if (state.managedBrowser?.exited) return "exited";
|
|
945
|
+
if (state.managedBrowser?.ready) return "running";
|
|
946
|
+
if (state.lastLaunchAttempt?.lastError) return "failed";
|
|
947
|
+
return "unobserved";
|
|
948
|
+
}
|
|
949
|
+
function launchModeLabel() {
|
|
950
|
+
if (extensionsConfigured()) {
|
|
951
|
+
if (!isLocalDevToolsHost(state.host)) return "invalid; extensions require a local endpoint";
|
|
952
|
+
if (!state.autoLaunchEnabled) return "invalid; extensions require auto-launch";
|
|
953
|
+
if (state.managedBrowser && !state.managedBrowser.exited) {
|
|
954
|
+
return state.portConfigured ? "extension-owned browser on explicit port" : "extension-owned browser on dynamic port";
|
|
955
|
+
}
|
|
956
|
+
return state.portConfigured ? "force extension-owned launch on explicit port" : "force extension-owned launch on dynamic port";
|
|
957
|
+
}
|
|
958
|
+
if (!isLocalDevToolsHost(state.host)) return "manual remote endpoint";
|
|
959
|
+
if (!state.autoLaunchEnabled) return "manual; auto-launch disabled";
|
|
960
|
+
if (state.managedBrowser && !state.managedBrowser.exited) {
|
|
961
|
+
return state.portConfigured ? "auto-launched on explicit port" : "auto-launched on dynamic port";
|
|
962
|
+
}
|
|
963
|
+
return state.portConfigured ? "attach first; auto-launch explicit port" : "attach first; auto-launch dynamic port";
|
|
964
|
+
}
|
|
965
|
+
function launchAttemptLines() {
|
|
966
|
+
if (!state.lastLaunchAttempt) return [];
|
|
967
|
+
const lines = [`Last launch attempt: ${state.lastLaunchAttempt.mode}`];
|
|
968
|
+
if (state.lastLaunchAttempt.selectedCandidate) {
|
|
969
|
+
lines.push(`Launched browser: ${state.lastLaunchAttempt.selectedCandidate}`);
|
|
970
|
+
} else {
|
|
971
|
+
lines.push(`Tried browser candidates: ${state.lastLaunchAttempt.candidateLabels.join(", ")}`);
|
|
972
|
+
}
|
|
973
|
+
if (state.lastLaunchAttempt.userDataDir) {
|
|
974
|
+
lines.push(`Managed browser profile: ${state.lastLaunchAttempt.userDataDir}`);
|
|
975
|
+
}
|
|
976
|
+
if (state.lastLaunchAttempt.lastError) {
|
|
977
|
+
lines.push(`Last launch error: ${state.lastLaunchAttempt.lastError}`);
|
|
978
|
+
}
|
|
979
|
+
return lines;
|
|
980
|
+
}
|
|
981
|
+
function launchHint() {
|
|
982
|
+
if (extensionsConfigured()) {
|
|
983
|
+
return "Configured unpacked extensions force an isolated extension-owned launch; existing CDP browsers are never reused, modified, or closed.";
|
|
984
|
+
}
|
|
985
|
+
if (!isLocalDevToolsHost(state.host)) {
|
|
986
|
+
return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint()}.`;
|
|
987
|
+
}
|
|
988
|
+
if (!state.autoLaunchEnabled) {
|
|
989
|
+
return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand()}`;
|
|
990
|
+
}
|
|
991
|
+
const managedMode = state.portConfigured ? `port ${state.port}` : "a dynamic DevTools port";
|
|
992
|
+
return `If no endpoint is available, Pi will auto-launch a Chromium-family browser with ${managedMode} and an isolated temp profile. Manual command: ${chromeLaunchCommand()}`;
|
|
993
|
+
}
|
|
994
|
+
function browserCandidateHint() {
|
|
995
|
+
if (extensionsConfigured()) {
|
|
996
|
+
return "Extension browser requirement: configure Chrome for Testing or Chromium with browser.executablePath; branded Chrome is rejected because it may ignore unpacked-extension flags.";
|
|
997
|
+
}
|
|
998
|
+
return `Browser candidates: ${browserCandidateDefinitions().map((candidate) => candidate.label).join(", ")}`;
|
|
999
|
+
}
|
|
1000
|
+
function chromeLaunchCommand() {
|
|
1001
|
+
const executable = state.browserExecutable ?? defaultManualBrowserExecutable();
|
|
1002
|
+
const dataDir = process.platform === "win32" ? "%TEMP%\\pi-chrome-devtools" : "/tmp/pi-chrome-devtools";
|
|
1003
|
+
return `${quoteCommandPart(executable)} --remote-debugging-port=${state.port} --user-data-dir=${dataDir}`;
|
|
1004
|
+
}
|
|
1005
|
+
function defaultManualBrowserExecutable() {
|
|
1006
|
+
return process.platform === "darwin" ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" : process.platform === "win32" ? "chrome.exe" : "google-chrome";
|
|
1007
|
+
}
|
|
1008
|
+
function quoteCommandPart(value) {
|
|
1009
|
+
return /\s/.test(value) ? `"${value.replaceAll('"', '\\"')}"` : value;
|
|
1010
|
+
}
|
|
1011
|
+
function endpointConfigHint() {
|
|
1012
|
+
return "Configure browser.endpoint, browser.executablePath, and browser.autoLaunch in pi-chrome-devtools.json or the /chrome-devtools Settings flow.";
|
|
1013
|
+
}
|
|
1014
|
+
function isLocalDevToolsHost(host) {
|
|
1015
|
+
const normalizedHost = host.toLowerCase().replace(/^\[(.*)]$/, "$1");
|
|
1016
|
+
return ["localhost", "127.0.0.1", "::1", "0:0:0:0:0:0:0:1"].includes(normalizedHost);
|
|
1017
|
+
}
|
|
1018
|
+
function browserCandidateDefinitions() {
|
|
1019
|
+
const explicitCandidate = explicitBrowserCandidateDefinition();
|
|
1020
|
+
if (explicitCandidate.length > 0) return explicitCandidate;
|
|
1021
|
+
return uniqueBrowserCandidates(platformBrowserCandidateDefinitions());
|
|
1022
|
+
}
|
|
1023
|
+
function explicitBrowserCandidateDefinition() {
|
|
1024
|
+
if (!state.browserExecutable) return [];
|
|
1025
|
+
return [
|
|
1026
|
+
{
|
|
1027
|
+
label: state.browserExecutableSource === "environment" ? "PI_CHROME_DEVTOOLS_BROWSER" : "browser.executablePath",
|
|
1028
|
+
executable: state.browserExecutable,
|
|
1029
|
+
source: "env"
|
|
1030
|
+
}
|
|
1031
|
+
];
|
|
1032
|
+
}
|
|
1033
|
+
function platformBrowserCandidateDefinitions() {
|
|
1034
|
+
if (process.platform === "darwin") {
|
|
1035
|
+
return [
|
|
1036
|
+
{
|
|
1037
|
+
label: "Google Chrome",
|
|
1038
|
+
executable: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1039
|
+
source: "wellKnownPath"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
label: "Chromium",
|
|
1043
|
+
executable: "/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1044
|
+
source: "wellKnownPath"
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
label: "Brave Browser",
|
|
1048
|
+
executable: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
1049
|
+
source: "wellKnownPath"
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
label: "Microsoft Edge",
|
|
1053
|
+
executable: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1054
|
+
source: "wellKnownPath"
|
|
1055
|
+
}
|
|
1056
|
+
];
|
|
1057
|
+
}
|
|
1058
|
+
if (process.platform === "win32") {
|
|
1059
|
+
return windowsBrowserCandidateDefinitions();
|
|
1060
|
+
}
|
|
1061
|
+
return [
|
|
1062
|
+
{ label: "Google Chrome", executable: "google-chrome", source: "path" },
|
|
1063
|
+
{ label: "Google Chrome Stable", executable: "google-chrome-stable", source: "path" },
|
|
1064
|
+
{ label: "Chromium", executable: "chromium", source: "path" },
|
|
1065
|
+
{ label: "Chromium Browser", executable: "chromium-browser", source: "path" },
|
|
1066
|
+
{ label: "Brave Browser", executable: "brave-browser", source: "path" },
|
|
1067
|
+
{ label: "Brave", executable: "brave", source: "path" },
|
|
1068
|
+
{ label: "Microsoft Edge", executable: "microsoft-edge", source: "path" },
|
|
1069
|
+
{ label: "Microsoft Edge Stable", executable: "microsoft-edge-stable", source: "path" }
|
|
1070
|
+
];
|
|
1071
|
+
}
|
|
1072
|
+
function windowsBrowserCandidateDefinitions() {
|
|
1073
|
+
const programFiles = [
|
|
1074
|
+
process.env.PROGRAMFILES,
|
|
1075
|
+
process.env["PROGRAMFILES(X86)"],
|
|
1076
|
+
process.env.LOCALAPPDATA
|
|
1077
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
1078
|
+
const wellKnownPaths = programFiles.flatMap((root) => [
|
|
1079
|
+
join2(root, "Google", "Chrome", "Application", "chrome.exe"),
|
|
1080
|
+
join2(root, "Chromium", "Application", "chrome.exe"),
|
|
1081
|
+
join2(root, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
1082
|
+
join2(root, "Microsoft", "Edge", "Application", "msedge.exe")
|
|
1083
|
+
]);
|
|
1084
|
+
return [
|
|
1085
|
+
...wellKnownPaths.map((executable) => ({
|
|
1086
|
+
label: browserLabelFromExecutable(executable),
|
|
1087
|
+
executable,
|
|
1088
|
+
source: "wellKnownPath"
|
|
1089
|
+
})),
|
|
1090
|
+
{ label: "Google Chrome", executable: "chrome.exe", source: "path" },
|
|
1091
|
+
{ label: "Chromium", executable: "chromium.exe", source: "path" },
|
|
1092
|
+
{ label: "Brave Browser", executable: "brave.exe", source: "path" },
|
|
1093
|
+
{ label: "Microsoft Edge", executable: "msedge.exe", source: "path" }
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
function browserLabelFromExecutable(executable) {
|
|
1097
|
+
const normalizedExecutable = normalizePathForComparison(executable);
|
|
1098
|
+
if (normalizedExecutable.includes("brave")) return "Brave Browser";
|
|
1099
|
+
if (normalizedExecutable.includes("edge") || normalizedExecutable.includes("msedge")) {
|
|
1100
|
+
return "Microsoft Edge";
|
|
1101
|
+
}
|
|
1102
|
+
if (normalizedExecutable.includes("chromium")) return "Chromium";
|
|
1103
|
+
return "Google Chrome";
|
|
1104
|
+
}
|
|
1105
|
+
function uniqueBrowserCandidates(candidates) {
|
|
1106
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1107
|
+
return candidates.filter((candidate) => {
|
|
1108
|
+
const key = normalizePathForComparison(candidate.executable);
|
|
1109
|
+
if (seen.has(key)) return false;
|
|
1110
|
+
seen.add(key);
|
|
1111
|
+
return true;
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
async function resolveBrowserCandidates(definitions, generation, signal) {
|
|
1115
|
+
const candidates = [];
|
|
1116
|
+
for (const definition of definitions) {
|
|
1117
|
+
const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
|
|
1118
|
+
throwIfBrowserLaunchCancelled(generation, signal);
|
|
1119
|
+
if (!resolvedExecutable) continue;
|
|
1120
|
+
candidates.push({ ...definition, resolvedExecutable });
|
|
1121
|
+
}
|
|
1122
|
+
return uniqueBrowserCandidatesByResolvedPath(candidates);
|
|
1123
|
+
}
|
|
1124
|
+
function uniqueBrowserCandidatesByResolvedPath(candidates) {
|
|
1125
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1126
|
+
return candidates.filter((candidate) => {
|
|
1127
|
+
const key = normalizePathForComparison(resolve2(candidate.resolvedExecutable));
|
|
1128
|
+
if (seen.has(key)) return false;
|
|
1129
|
+
seen.add(key);
|
|
1130
|
+
return true;
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
async function resolveBrowserExecutable(executable) {
|
|
1134
|
+
if (hasPathSeparator(executable) || isAbsolute2(executable)) {
|
|
1135
|
+
const resolvedExecutable = isAbsolute2(executable) ? executable : resolve2(executable);
|
|
1136
|
+
return await canAccessExecutable(resolvedExecutable) ? resolvedExecutable : void 0;
|
|
1137
|
+
}
|
|
1138
|
+
for (const directory of executableSearchPath()) {
|
|
1139
|
+
for (const executableName of executableSearchNames(executable)) {
|
|
1140
|
+
const candidate = join2(directory, executableName);
|
|
1141
|
+
if (await canAccessExecutable(candidate)) return candidate;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return void 0;
|
|
1145
|
+
}
|
|
1146
|
+
function hasPathSeparator(path) {
|
|
1147
|
+
return path.includes("/") || path.includes("\\");
|
|
1148
|
+
}
|
|
1149
|
+
function executableSearchPath() {
|
|
1150
|
+
return (process.env.PATH ?? "").split(delimiter).filter((part) => part.length > 0);
|
|
1151
|
+
}
|
|
1152
|
+
function executableSearchNames(executable) {
|
|
1153
|
+
if (process.platform !== "win32" || /\.[a-z0-9]+$/i.test(executable)) return [executable];
|
|
1154
|
+
return [executable, `${executable}.exe`, `${executable}.cmd`, `${executable}.bat`];
|
|
1155
|
+
}
|
|
1156
|
+
async function canAccessExecutable(path) {
|
|
1157
|
+
try {
|
|
1158
|
+
await browserManagerOperations.access(
|
|
1159
|
+
path,
|
|
1160
|
+
process.platform === "win32" ? constants2.F_OK : constants2.X_OK
|
|
1161
|
+
);
|
|
1162
|
+
return true;
|
|
1163
|
+
} catch {
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function formatBrowserCandidate(candidate) {
|
|
1168
|
+
return `${candidate.label} (${candidate.resolvedExecutable})`;
|
|
1169
|
+
}
|
|
1170
|
+
function formatBrowserCandidateDefinition(candidate) {
|
|
1171
|
+
return `${candidate.label} (${candidate.executable})`;
|
|
1172
|
+
}
|
|
1173
|
+
function noBrowserCandidateMessage(candidateDefinitions) {
|
|
1174
|
+
if (extensionsConfigured()) {
|
|
1175
|
+
return [
|
|
1176
|
+
"Cannot launch unpacked extensions because the configured browser executable was not found or is not executable.",
|
|
1177
|
+
`Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,
|
|
1178
|
+
"Set browser.executablePath to an executable Chrome for Testing or Chromium binary."
|
|
1179
|
+
].join("\n");
|
|
1180
|
+
}
|
|
1181
|
+
return [
|
|
1182
|
+
"Cannot auto-launch Chrome DevTools because no Chromium-family browser executable was found.",
|
|
1183
|
+
`Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,
|
|
1184
|
+
endpointConfigHint()
|
|
1185
|
+
].join("\n");
|
|
1186
|
+
}
|
|
1187
|
+
function formatPageListItem(page) {
|
|
1188
|
+
return `- ${page.id}: ${page.title || "(untitled)"} ${page.url}`;
|
|
1189
|
+
}
|
|
1190
|
+
function abortableSleep(ms, signal) {
|
|
1191
|
+
return new Promise((resolveSleep, reject) => {
|
|
1192
|
+
if (signal?.aborted) {
|
|
1193
|
+
reject(signal.reason);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const timeout = setTimeout(settleResolve, ms);
|
|
1197
|
+
const onAbort = () => settleReject(signal?.reason);
|
|
1198
|
+
function cleanup() {
|
|
1199
|
+
clearTimeout(timeout);
|
|
1200
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1201
|
+
}
|
|
1202
|
+
function settleResolve() {
|
|
1203
|
+
cleanup();
|
|
1204
|
+
resolveSleep();
|
|
1205
|
+
}
|
|
1206
|
+
function settleReject(reason) {
|
|
1207
|
+
cleanup();
|
|
1208
|
+
reject(reason);
|
|
1209
|
+
}
|
|
1210
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
function inspectBrowserVersion(executable, signal) {
|
|
1214
|
+
return new Promise((resolveVersion, reject) => {
|
|
1215
|
+
execFile(executable, ["--version"], { signal, timeout: 5e3 }, (error, stdout, stderr) => {
|
|
1216
|
+
if (error) {
|
|
1217
|
+
reject(error);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
resolveVersion(`${stdout}${stderr}`.trim());
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function isPortAvailable(host, port) {
|
|
1225
|
+
return new Promise((resolveAvailability, reject) => {
|
|
1226
|
+
const server = createServer();
|
|
1227
|
+
const normalizedHost = host.replace(/^\[(.*)]$/, "$1");
|
|
1228
|
+
server.unref();
|
|
1229
|
+
server.once("error", (error) => {
|
|
1230
|
+
if (error.code === "EADDRINUSE") resolveAvailability(false);
|
|
1231
|
+
else reject(error);
|
|
1232
|
+
});
|
|
1233
|
+
server.listen(port, normalizedHost, () => {
|
|
1234
|
+
server.close((error) => {
|
|
1235
|
+
if (error) reject(error);
|
|
1236
|
+
else resolveAvailability(true);
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
var DevToolsEndpointError = class extends Error {
|
|
1242
|
+
retryable;
|
|
1243
|
+
launchable;
|
|
1244
|
+
constructor(message, options = {}) {
|
|
1245
|
+
super(message);
|
|
1246
|
+
this.name = "DevToolsEndpointError";
|
|
1247
|
+
this.retryable = options.retryable ?? false;
|
|
1248
|
+
this.launchable = options.launchable ?? false;
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// src/lazy-tools.ts
|
|
1253
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
1254
|
+
import { Type } from "typebox";
|
|
1255
|
+
var CHROME_DEVTOOLS_LOAD_TOOL_NAME = "chrome_devtools_load";
|
|
1256
|
+
var AVAILABLE_TOOLS_STORE = /* @__PURE__ */ Symbol.for("@narumitw/pi-chrome-devtools.available-tools-store");
|
|
1257
|
+
var sharedGlobal = globalThis;
|
|
1258
|
+
var existingAvailableToolsStore = sharedGlobal[AVAILABLE_TOOLS_STORE];
|
|
1259
|
+
var availableToolsByApi = existingAvailableToolsStore ?? /* @__PURE__ */ new WeakMap();
|
|
1260
|
+
if (!existingAvailableToolsStore) sharedGlobal[AVAILABLE_TOOLS_STORE] = availableToolsByApi;
|
|
1261
|
+
var SEARCH_TEXT = {
|
|
1262
|
+
chrome_devtools_list_pages: "list open inspectable chrome browser pages tabs targets",
|
|
1263
|
+
chrome_devtools_select_page: "select choose active chrome browser page tab target",
|
|
1264
|
+
chrome_devtools_navigate: "navigate open create chrome browser page url website",
|
|
1265
|
+
chrome_devtools_evaluate: "evaluate run javascript expression dom inspect chrome browser page",
|
|
1266
|
+
chrome_devtools_screenshot: "capture screenshot png image visual chrome browser page"
|
|
1267
|
+
};
|
|
1268
|
+
function initializeAvailableChromeDevtoolsTools(pi) {
|
|
1269
|
+
if (availableToolsByApi.has(pi)) return;
|
|
1270
|
+
const activeTools = new Set(pi.getActiveTools());
|
|
1271
|
+
setAvailableTools(
|
|
1272
|
+
pi,
|
|
1273
|
+
CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => activeTools.has(name))
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
function configureLazyChromeDevtoolsTools(pi, availableTools) {
|
|
1277
|
+
setAvailableTools(pi, availableTools);
|
|
1278
|
+
const nonCapabilityTools = pi.getActiveTools().filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name));
|
|
1279
|
+
pi.setActiveTools(unique([...nonCapabilityTools, CHROME_DEVTOOLS_LOAD_TOOL_NAME]));
|
|
1280
|
+
}
|
|
1281
|
+
function applyAvailableChromeDevtoolsTools(pi, availableTools) {
|
|
1282
|
+
const available = setAvailableTools(pi, availableTools);
|
|
1283
|
+
const active = pi.getActiveTools().filter(
|
|
1284
|
+
(name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name) || available.has(name)
|
|
1285
|
+
);
|
|
1286
|
+
pi.setActiveTools(unique([...active, CHROME_DEVTOOLS_LOAD_TOOL_NAME]));
|
|
1287
|
+
}
|
|
1288
|
+
function availableChromeDevtoolsTools(pi) {
|
|
1289
|
+
const available = availableToolsByApi.get(pi) ?? /* @__PURE__ */ new Set();
|
|
1290
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
1291
|
+
}
|
|
1292
|
+
function createChromeDevtoolsLoadTool(pi) {
|
|
1293
|
+
return defineTool({
|
|
1294
|
+
name: CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
1295
|
+
label: "Chrome DevTools: Load Tools",
|
|
1296
|
+
description: "Find and enable Chrome DevTools browser tools relevant to a task. Loaded tools remain available for the session.",
|
|
1297
|
+
promptSnippet: "Load Chrome DevTools browser capabilities on demand",
|
|
1298
|
+
promptGuidelines: [
|
|
1299
|
+
"Use chrome_devtools_load when a task requires inspecting or controlling a Chrome browser and the needed chrome_devtools_* capability is not active."
|
|
1300
|
+
],
|
|
1301
|
+
parameters: Type.Object({
|
|
1302
|
+
query: Type.String({
|
|
1303
|
+
description: "Browser capability or task to find tools for.",
|
|
1304
|
+
maxLength: 500
|
|
1305
|
+
}),
|
|
1306
|
+
limit: Type.Optional(
|
|
1307
|
+
Type.Integer({
|
|
1308
|
+
description: "Maximum tools to load. Defaults to 3.",
|
|
1309
|
+
minimum: 1,
|
|
1310
|
+
maximum: 5
|
|
1311
|
+
})
|
|
1312
|
+
)
|
|
1313
|
+
}),
|
|
1314
|
+
async execute(_toolCallId, params, signal) {
|
|
1315
|
+
signal?.throwIfAborted();
|
|
1316
|
+
const available = new Set(availableChromeDevtoolsTools(pi));
|
|
1317
|
+
const matches = matchChromeDevtoolsTools(params.query, params.limit ?? 3, available);
|
|
1318
|
+
const active = pi.getActiveTools();
|
|
1319
|
+
const activeSet = new Set(active);
|
|
1320
|
+
const added = matches.filter((name) => !activeSet.has(name));
|
|
1321
|
+
if (added.length > 0) {
|
|
1322
|
+
pi.setActiveTools(unique([...active, ...added]));
|
|
1323
|
+
}
|
|
1324
|
+
const text = matches.length === 0 ? "No available Chrome DevTools tools matched the query." : added.length > 0 ? `Loaded Chrome DevTools tools: ${added.join(", ")}` : `Matching Chrome DevTools tools are already loaded: ${matches.join(", ")}`;
|
|
1325
|
+
return {
|
|
1326
|
+
content: [{ type: "text", text }],
|
|
1327
|
+
details: { matches, added }
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
function matchChromeDevtoolsTools(query, limit, available) {
|
|
1333
|
+
const terms = query.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 2);
|
|
1334
|
+
if (terms.length === 0) return [];
|
|
1335
|
+
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name)).map((name, index) => ({
|
|
1336
|
+
name,
|
|
1337
|
+
index,
|
|
1338
|
+
score: terms.reduce((score, term) => score + (SEARCH_TEXT[name].includes(term) ? 1 : 0), 0)
|
|
1339
|
+
})).filter((match) => match.score > 0).sort((left, right) => right.score - left.score || left.index - right.index).slice(0, limit).map((match) => match.name);
|
|
1340
|
+
}
|
|
1341
|
+
function setAvailableTools(pi, availableTools) {
|
|
1342
|
+
const available = new Set(availableTools);
|
|
1343
|
+
availableToolsByApi.set(pi, available);
|
|
1344
|
+
return available;
|
|
1345
|
+
}
|
|
1346
|
+
function unique(values) {
|
|
1347
|
+
return [...new Set(values)];
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/tool-selector.ts
|
|
1351
|
+
import { stripVTControlCharacters } from "node:util";
|
|
1352
|
+
function unique2(values) {
|
|
1353
|
+
return Array.from(new Set(values));
|
|
1354
|
+
}
|
|
1355
|
+
function formatError3(error) {
|
|
1356
|
+
return error instanceof Error ? error.message : String(error);
|
|
1357
|
+
}
|
|
1358
|
+
async function updateChromeDevtoolsTools(pi, ctx, selectedTools, action) {
|
|
1359
|
+
const generation = state.sessionGeneration;
|
|
1360
|
+
const result = await transactSelectedTools(pi, ctx, selectedTools, generation);
|
|
1361
|
+
if (result !== "saved" || generation !== state.sessionGeneration) return;
|
|
1362
|
+
const status = await buildToolStatusMessage(pi);
|
|
1363
|
+
if (generation !== state.sessionGeneration) return;
|
|
1364
|
+
ctx.ui.notify(`Chrome DevTools lazy catalog ${action}.
|
|
1365
|
+
|
|
1366
|
+
${status}`, "info");
|
|
1367
|
+
}
|
|
1368
|
+
async function setSelectedChromeDevtoolsTools(pi, ctx, selectedTools, expectedActiveTools) {
|
|
1369
|
+
return transactSelectedTools(
|
|
1370
|
+
pi,
|
|
1371
|
+
ctx,
|
|
1372
|
+
selectedTools,
|
|
1373
|
+
state.sessionGeneration,
|
|
1374
|
+
expectedActiveTools
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
var toolTransactionQueue = Promise.resolve();
|
|
1378
|
+
async function waitForChromeDevtoolsSettings() {
|
|
1379
|
+
await toolTransactionQueue;
|
|
1380
|
+
}
|
|
1381
|
+
function transactSelectedTools(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools) {
|
|
1382
|
+
const operation = toolTransactionQueue.then(
|
|
1383
|
+
() => transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools)
|
|
1384
|
+
);
|
|
1385
|
+
toolTransactionQueue = operation.then(
|
|
1386
|
+
() => void 0,
|
|
1387
|
+
() => void 0
|
|
1388
|
+
);
|
|
1389
|
+
return operation;
|
|
1390
|
+
}
|
|
1391
|
+
async function transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools) {
|
|
1392
|
+
if (expectedGeneration !== state.sessionGeneration) return "failed";
|
|
1393
|
+
if (expectedActiveTools && !arraysEqual(availableChromeDevtoolsTools(pi), expectedActiveTools)) {
|
|
1394
|
+
ctx.ui.notify(
|
|
1395
|
+
"Browser tool selection changed while review was open. Review the current state, then apply again.",
|
|
1396
|
+
"warning"
|
|
1397
|
+
);
|
|
1398
|
+
return "active-tools-changed";
|
|
1399
|
+
}
|
|
1400
|
+
const previousActiveTools = pi.getActiveTools();
|
|
1401
|
+
const previousAvailableTools = availableChromeDevtoolsTools(pi);
|
|
1402
|
+
try {
|
|
1403
|
+
applyChromeDevtoolsTools(pi, selectedTools);
|
|
1404
|
+
await persistSettings(selectedTools);
|
|
1405
|
+
return expectedGeneration === state.sessionGeneration ? "saved" : "failed";
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
let rollbackError;
|
|
1408
|
+
try {
|
|
1409
|
+
applyAvailableChromeDevtoolsTools(pi, previousAvailableTools);
|
|
1410
|
+
const currentNonChromeTools = pi.getActiveTools().filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name));
|
|
1411
|
+
const previousLoadedChromeTools = previousActiveTools.filter(
|
|
1412
|
+
(name) => CHROME_DEVTOOLS_TOOL_NAMES.includes(name)
|
|
1413
|
+
);
|
|
1414
|
+
pi.setActiveTools(unique2([...currentNonChromeTools, ...previousLoadedChromeTools]));
|
|
1415
|
+
} catch (caught) {
|
|
1416
|
+
rollbackError = caught;
|
|
1417
|
+
}
|
|
1418
|
+
if (expectedGeneration !== state.sessionGeneration) return "failed";
|
|
1419
|
+
ctx.ui.notify(
|
|
1420
|
+
sanitizeChromeDevtoolsDisplay(
|
|
1421
|
+
rollbackError ? `Chrome DevTools settings save failed: ${formatError3(error)}; active-tool rollback failed: ${formatError3(rollbackError)}` : `Chrome DevTools settings save failed; active tools restored: ${formatError3(error)}`
|
|
1422
|
+
),
|
|
1423
|
+
"warning"
|
|
1424
|
+
);
|
|
1425
|
+
return "failed";
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function arraysEqual(left, right) {
|
|
1429
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1430
|
+
}
|
|
1431
|
+
function applyChromeDevtoolsTools(pi, selectedTools) {
|
|
1432
|
+
applyAvailableChromeDevtoolsTools(pi, selectedTools);
|
|
1433
|
+
}
|
|
1434
|
+
function getToolStatusSummary(pi) {
|
|
1435
|
+
const chromeToolNames = new Set(CHROME_DEVTOOLS_TOOL_NAMES);
|
|
1436
|
+
const activeToolNames = new Set(pi.getActiveTools());
|
|
1437
|
+
const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter(
|
|
1438
|
+
(name) => activeToolNames.has(name)
|
|
1439
|
+
).length;
|
|
1440
|
+
const availableChromeToolCount = availableChromeDevtoolsTools(pi).length;
|
|
1441
|
+
const activeNonChromeToolCount = Array.from(activeToolNames).filter(
|
|
1442
|
+
(name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME
|
|
1443
|
+
).length;
|
|
1444
|
+
const availabilityStatus = availableChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length ? "enabled" : availableChromeToolCount === 0 ? "disabled" : "partial";
|
|
1445
|
+
return {
|
|
1446
|
+
availabilityStatus,
|
|
1447
|
+
availableChromeToolCount,
|
|
1448
|
+
loadedChromeToolCount,
|
|
1449
|
+
activeNonChromeToolCount
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
async function buildToolStatusMessage(pi) {
|
|
1453
|
+
const summary = getToolStatusSummary(pi);
|
|
1454
|
+
const persistedSetting = await persistedSettingLabel();
|
|
1455
|
+
return sanitizeChromeDevtoolsDisplay(
|
|
1456
|
+
[
|
|
1457
|
+
`Chrome DevTools tools available to lazy-load: ${formatRuntimeStatus(summary)}`,
|
|
1458
|
+
`Loaded capability tools this session: ${summary.loadedChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
1459
|
+
`Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
|
|
1460
|
+
`Persisted lazy catalog: ${persistedSetting}`,
|
|
1461
|
+
...browserSettingsStatusLines(),
|
|
1462
|
+
...state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : [],
|
|
1463
|
+
`Other active tools preserved: ${summary.activeNonChromeToolCount}`,
|
|
1464
|
+
`Endpoint: ${devToolsEndpoint()}`,
|
|
1465
|
+
`Endpoint source: ${endpointSourceLabel()}`,
|
|
1466
|
+
`Launch mode: ${launchModeLabel()}`,
|
|
1467
|
+
...launchAttemptLines()
|
|
1468
|
+
].join("\n")
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
function buildQuickstartMessage() {
|
|
1472
|
+
return buildSettingsSetupMessage();
|
|
1473
|
+
}
|
|
1474
|
+
function buildBrowserStatusMessage() {
|
|
1475
|
+
const lifecycle = browserLifecycleState();
|
|
1476
|
+
const browserState = lifecycle === "starting" ? "starting managed browser" : lifecycle === "running" ? "managed browser running" : lifecycle === "exited" ? "managed browser exited" : lifecycle === "failed" ? "last launch failed" : "not started; connection has not been checked";
|
|
1477
|
+
const needsRecovery = lifecycle === "exited" || lifecycle === "failed";
|
|
1478
|
+
return sanitizeChromeDevtoolsDisplay(
|
|
1479
|
+
[
|
|
1480
|
+
`Browser: ${browserState}`,
|
|
1481
|
+
"Viewing this status does not probe the endpoint or launch Chrome.",
|
|
1482
|
+
`Endpoint: ${devToolsEndpoint()}`,
|
|
1483
|
+
`Endpoint source: ${endpointSourceLabel()}`,
|
|
1484
|
+
`Launch mode: ${launchModeLabel()}`,
|
|
1485
|
+
`Unpacked extensions: ${state.extensionPaths.length} (${state.extensionPathsSource})`,
|
|
1486
|
+
...state.extensionPaths.length > 0 ? ["Unpacked extensions execute trusted browser code in an isolated managed browser."] : [],
|
|
1487
|
+
...launchAttemptLines(),
|
|
1488
|
+
...needsRecovery ? [launchHint(), endpointConfigHint()] : []
|
|
1489
|
+
].join("\n")
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
function buildSettingsSetupMessage() {
|
|
1493
|
+
return sanitizeChromeDevtoolsDisplay(
|
|
1494
|
+
[
|
|
1495
|
+
`Chrome DevTools endpoint: ${devToolsEndpoint()}`,
|
|
1496
|
+
`Endpoint source: ${endpointSourceLabel()}`,
|
|
1497
|
+
`Launch mode: ${launchModeLabel()}`,
|
|
1498
|
+
...browserSettingsStatusLines(),
|
|
1499
|
+
launchHint(),
|
|
1500
|
+
browserCandidateHint(),
|
|
1501
|
+
...launchAttemptLines(),
|
|
1502
|
+
endpointConfigHint()
|
|
1503
|
+
].join("\n")
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
function sanitizeChromeDevtoolsDisplay(value, maxCharacters = 5e4) {
|
|
1507
|
+
const sanitized = Array.from(stripVTControlCharacters(value), (character) => {
|
|
1508
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
1509
|
+
const unsafeControl = codePoint >= 0 && codePoint <= 8 || codePoint >= 11 && codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
|
|
1510
|
+
return unsafeControl ? "\uFFFD" : character;
|
|
1511
|
+
}).join("");
|
|
1512
|
+
if (sanitized.length <= maxCharacters) return sanitized;
|
|
1513
|
+
return `${sanitized.slice(0, Math.max(0, maxCharacters - 1))}\u2026`;
|
|
1514
|
+
}
|
|
1515
|
+
function browserSettingsStatusLines() {
|
|
1516
|
+
const extensionLines = state.extensionPaths.length > 0 ? state.extensionPaths.map((extensionPath) => ` - ${extensionPath}`) : [" - none"];
|
|
1517
|
+
return [
|
|
1518
|
+
`Settings file: ${state.settingsFilePath ?? settingsFilePath()} (user)`,
|
|
1519
|
+
...state.projectSettingsFilePath ? [
|
|
1520
|
+
`Project settings: ${state.projectSettingsFilePath} (${state.projectSettingsTrusted ? "trusted" : "untrusted; ignored"})`
|
|
1521
|
+
] : [],
|
|
1522
|
+
`Auto-launch: ${state.autoLaunchEnabled ? "on" : "off"} (${state.autoLaunchSource})`,
|
|
1523
|
+
`Browser executable: ${state.browserExecutable ?? "automatic discovery"} (${state.browserExecutableSource})`,
|
|
1524
|
+
`Unpacked extensions (${state.extensionPathsSource}):`,
|
|
1525
|
+
...extensionLines,
|
|
1526
|
+
"Confirmed menu settings apply before the next browser connection; manual JSON edits require /reload or session replacement.",
|
|
1527
|
+
...state.extensionPaths.length > 0 ? [
|
|
1528
|
+
"Unpacked extensions require Chrome for Testing or Chromium and execute trusted browser code."
|
|
1529
|
+
] : []
|
|
1530
|
+
];
|
|
1531
|
+
}
|
|
1532
|
+
function buildCommandGuide() {
|
|
1533
|
+
return [
|
|
1534
|
+
"Chrome DevTools commands:",
|
|
1535
|
+
"/chrome-devtools \u2014 open this menu",
|
|
1536
|
+
"/chrome-devtools help \u2014 show command usage",
|
|
1537
|
+
"/chrome-devtools quickstart \u2014 show endpoint and launch help",
|
|
1538
|
+
"/chrome-devtools status \u2014 show tool and settings status",
|
|
1539
|
+
"/chrome-devtools settings \u2014 edit browser connection settings",
|
|
1540
|
+
"/chrome-devtools tools \u2014 choose tools available to lazy-load",
|
|
1541
|
+
"/chrome-devtools toggle|select \u2014 compatibility aliases for tools",
|
|
1542
|
+
"/chrome-devtools enable|on \u2014 make all Chrome DevTools tools available to lazy-load",
|
|
1543
|
+
"/chrome-devtools disable|off \u2014 make all Chrome DevTools capability tools unavailable"
|
|
1544
|
+
].join("\n");
|
|
1545
|
+
}
|
|
1546
|
+
function allChromeDevtoolsTools() {
|
|
1547
|
+
return [...CHROME_DEVTOOLS_TOOL_NAMES];
|
|
1548
|
+
}
|
|
1549
|
+
function formatRuntimeStatus(summary) {
|
|
1550
|
+
return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length} available)`;
|
|
1551
|
+
}
|
|
1552
|
+
async function persistedSettingLabel() {
|
|
1553
|
+
const settings = await loadSettings();
|
|
1554
|
+
if (settings.kind === "loaded" && settings.settings.tools) {
|
|
1555
|
+
return formatPersistedSelection(settings.settings.tools);
|
|
1556
|
+
}
|
|
1557
|
+
if (settings.kind === "invalid") {
|
|
1558
|
+
return `none; current active-tool policy preserved (invalid settings ignored: ${settings.reason})`;
|
|
1559
|
+
}
|
|
1560
|
+
return "none; current active-tool policy preserved";
|
|
1561
|
+
}
|
|
1562
|
+
function formatPersistedSelection(tools) {
|
|
1563
|
+
if (tools.length === CHROME_DEVTOOLS_TOOL_NAMES.length) {
|
|
1564
|
+
return `all available (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
|
|
1565
|
+
}
|
|
1566
|
+
if (tools.length === 0)
|
|
1567
|
+
return `all unavailable (0/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
|
|
1568
|
+
return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
|
|
1569
|
+
}
|
|
1570
|
+
async function persistSettings(selectedTools) {
|
|
1571
|
+
await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
export {
|
|
1575
|
+
CHROME_DEVTOOLS_TOOL_NAMES,
|
|
1576
|
+
waitForSettingsWrites,
|
|
1577
|
+
loadSettings,
|
|
1578
|
+
parseBrowserEndpoint,
|
|
1579
|
+
saveBrowserSettings,
|
|
1580
|
+
DEFAULT_TIMEOUT_MS,
|
|
1581
|
+
DEFAULT_ENDPOINT_WAIT_MS,
|
|
1582
|
+
state,
|
|
1583
|
+
applyRuntimeBrowserSettings,
|
|
1584
|
+
ensureDevToolsEndpoint,
|
|
1585
|
+
shutdownManagedBrowser,
|
|
1586
|
+
fetchDevToolsJson,
|
|
1587
|
+
withEndpointRetry,
|
|
1588
|
+
devToolsEndpoint,
|
|
1589
|
+
browserLifecycleState,
|
|
1590
|
+
launchModeLabel,
|
|
1591
|
+
launchHint,
|
|
1592
|
+
formatPageListItem,
|
|
1593
|
+
initializeAvailableChromeDevtoolsTools,
|
|
1594
|
+
configureLazyChromeDevtoolsTools,
|
|
1595
|
+
availableChromeDevtoolsTools,
|
|
1596
|
+
createChromeDevtoolsLoadTool,
|
|
1597
|
+
updateChromeDevtoolsTools,
|
|
1598
|
+
setSelectedChromeDevtoolsTools,
|
|
1599
|
+
waitForChromeDevtoolsSettings,
|
|
1600
|
+
buildToolStatusMessage,
|
|
1601
|
+
buildQuickstartMessage,
|
|
1602
|
+
buildBrowserStatusMessage,
|
|
1603
|
+
sanitizeChromeDevtoolsDisplay,
|
|
1604
|
+
buildCommandGuide,
|
|
1605
|
+
allChromeDevtoolsTools
|
|
1606
|
+
};
|
|
1607
|
+
//# sourceMappingURL=chunk-RM7OGXVL.ts.map
|