@narumitw/pi-chrome-devtools 0.53.2 → 0.53.3

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.
Files changed (40) hide show
  1. package/README.md +1 -0
  2. package/dist/chunks/{browser-settings-menu-IMB6HXSU.js → browser-settings-menu-SR7KLZT6.js} +16 -42
  3. package/dist/chunks/browser-settings-menu-SR7KLZT6.js.map +7 -0
  4. package/dist/chunks/{chunk-GTTYS5GT.js → chunk-4KMN4SFF.js} +2 -2
  5. package/dist/chunks/chunk-4KMN4SFF.js.map +7 -0
  6. package/dist/chunks/{chunk-SRT3UUIP.js → chunk-F5VG35UI.js} +23 -74
  7. package/dist/chunks/chunk-F5VG35UI.js.map +7 -0
  8. package/dist/chunks/{chunk-LLXOCRNF.js → chunk-MZCGM6D3.js} +12 -33
  9. package/dist/chunks/chunk-MZCGM6D3.js.map +7 -0
  10. package/dist/chunks/{menu-UJETYMSB.js → menu-YR4VRT7D.js} +10 -23
  11. package/dist/chunks/menu-YR4VRT7D.js.map +7 -0
  12. package/dist/chunks/{tools-6P7EUX37.js → tools-6R7Z6HWJ.js} +17 -59
  13. package/dist/chunks/tools-6R7Z6HWJ.js.map +7 -0
  14. package/dist/index.ts +18 -42
  15. package/dist/index.ts.map +2 -2
  16. package/package.json +54 -55
  17. package/src/browser-manager.ts +872 -942
  18. package/src/browser-settings-menu.ts +315 -350
  19. package/src/cdp-client.ts +381 -398
  20. package/src/chrome-devtools.ts +205 -232
  21. package/src/display.ts +26 -29
  22. package/src/lazy-tools.ts +158 -181
  23. package/src/menu.ts +409 -446
  24. package/src/render.ts +72 -81
  25. package/src/runtime.ts +137 -140
  26. package/src/screenshot.ts +170 -186
  27. package/src/settings.ts +472 -523
  28. package/src/tool-names.ts +8 -14
  29. package/src/tool-selector.ts +242 -271
  30. package/src/tools.ts +218 -226
  31. package/src/webmcp/discovery.ts +184 -188
  32. package/src/webmcp/policy.ts +296 -329
  33. package/src/webmcp/protocol.ts +324 -337
  34. package/src/webmcp/tools.ts +136 -147
  35. package/dist/chunks/browser-settings-menu-IMB6HXSU.js.map +0 -7
  36. package/dist/chunks/chunk-GTTYS5GT.js.map +0 -7
  37. package/dist/chunks/chunk-LLXOCRNF.js.map +0 -7
  38. package/dist/chunks/chunk-SRT3UUIP.js.map +0 -7
  39. package/dist/chunks/menu-UJETYMSB.js.map +0 -7
  40. package/dist/chunks/tools-6P7EUX37.js.map +0 -7
package/src/settings.ts CHANGED
@@ -1,16 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
- import {
4
- access,
5
- lstat,
6
- mkdir,
7
- readFile,
8
- realpath,
9
- rename,
10
- rm,
11
- stat,
12
- writeFile,
13
- } from "node:fs/promises";
3
+ import { access, lstat, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
14
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
15
5
  import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
16
6
  import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
@@ -19,8 +9,8 @@ const NEW_SETTINGS_FILE_NAME = "pi-chrome-devtools.json";
19
9
  const LEGACY_SETTINGS_FILE_NAME = "pi-chrome-devtools-settings.json";
20
10
 
21
11
  export interface ChromeDevToolsSettings {
22
- tools: ChromeDevToolsToolName[];
23
- updatedAt: number;
12
+ tools: ChromeDevToolsToolName[];
13
+ updatedAt: number;
24
14
  }
25
15
 
26
16
  export const DEFAULT_BROWSER_HOST = "127.0.0.1";
@@ -30,639 +20,598 @@ export const DEFAULT_BROWSER_ENDPOINT = `http://${DEFAULT_BROWSER_HOST}:${DEFAUL
30
20
  export type BrowserSettingsSource = "default" | "environment" | "project" | "user";
31
21
 
32
22
  export interface UserBrowserSettings {
33
- endpoint?: string;
34
- autoLaunch?: boolean;
35
- executablePath?: string;
36
- extensionPaths?: string[];
23
+ endpoint?: string;
24
+ autoLaunch?: boolean;
25
+ executablePath?: string;
26
+ extensionPaths?: string[];
37
27
  }
38
28
 
39
29
  export interface BrowserSettingsPatch {
40
- endpoint?: string | null;
41
- autoLaunch?: boolean | null;
42
- executablePath?: string | null;
30
+ endpoint?: string | null;
31
+ autoLaunch?: boolean | null;
32
+ executablePath?: string | null;
43
33
  }
44
34
 
45
35
  export interface EffectiveBrowserSettings {
46
- endpoint: string;
47
- host: string;
48
- port: number;
49
- hostConfigured: boolean;
50
- portConfigured: boolean;
51
- autoLaunchEnabled: boolean;
52
- executablePath?: string;
53
- extensionPaths: string[];
54
- endpointSource: BrowserSettingsSource;
55
- autoLaunchSource: BrowserSettingsSource;
56
- executablePathSource: BrowserSettingsSource;
57
- extensionPathsSource: BrowserSettingsSource;
36
+ endpoint: string;
37
+ host: string;
38
+ port: number;
39
+ hostConfigured: boolean;
40
+ portConfigured: boolean;
41
+ autoLaunchEnabled: boolean;
42
+ executablePath?: string;
43
+ extensionPaths: string[];
44
+ endpointSource: BrowserSettingsSource;
45
+ autoLaunchSource: BrowserSettingsSource;
46
+ executablePathSource: BrowserSettingsSource;
47
+ extensionPathsSource: BrowserSettingsSource;
58
48
  }
59
49
 
60
50
  export interface ResolvedChromeDevToolsSettings {
61
- tools?: ChromeDevToolsToolName[];
62
- updatedAt?: number;
63
- browser: EffectiveBrowserSettings;
64
- webmcp: { enabled: boolean };
51
+ tools?: ChromeDevToolsToolName[];
52
+ updatedAt?: number;
53
+ browser: EffectiveBrowserSettings;
54
+ webmcp: { enabled: boolean };
65
55
  }
66
56
 
67
57
  export interface SettingsLoadOptions {
68
- cwd?: string;
69
- projectTrusted?: boolean;
58
+ cwd?: string;
59
+ projectTrusted?: boolean;
70
60
  }
71
61
 
72
- export type UserSettingsFileStatus =
73
- | { kind: "missing" | "valid" }
74
- | { kind: "invalid"; reason: string };
62
+ export type UserSettingsFileStatus = { kind: "missing" | "valid" } | { kind: "invalid"; reason: string };
75
63
 
76
64
  interface SettingsLoadBase {
77
- effectiveBrowser: EffectiveBrowserSettings;
78
- effectiveWebMcpEnabled: boolean;
79
- paths: { user: string; project?: string };
80
- userFile: UserSettingsFileStatus;
81
- warnings: string[];
82
- notice?: string;
65
+ effectiveBrowser: EffectiveBrowserSettings;
66
+ effectiveWebMcpEnabled: boolean;
67
+ paths: { user: string; project?: string };
68
+ userFile: UserSettingsFileStatus;
69
+ warnings: string[];
70
+ notice?: string;
83
71
  }
84
72
 
85
73
  export type SettingsLoadResult =
86
- | (SettingsLoadBase & { kind: "missing"; settings?: undefined })
87
- | (SettingsLoadBase & { kind: "invalid"; reason: string; settings?: undefined })
88
- | (SettingsLoadBase & { kind: "loaded"; settings: ResolvedChromeDevToolsSettings });
74
+ | (SettingsLoadBase & { kind: "missing"; settings?: undefined })
75
+ | (SettingsLoadBase & { kind: "invalid"; reason: string; settings?: undefined })
76
+ | (SettingsLoadBase & { kind: "loaded"; settings: ResolvedChromeDevToolsSettings });
89
77
 
90
78
  export interface SettingsFileOperations {
91
- write(path: string, data: string): Promise<void>;
92
- rename(source: string, destination: string): Promise<void>;
79
+ write(path: string, data: string): Promise<void>;
80
+ rename(source: string, destination: string): Promise<void>;
93
81
  }
94
82
 
95
83
  interface NormalizedSettingsDocument {
96
- tools?: ChromeDevToolsToolName[];
97
- updatedAt?: number;
98
- browser?: UserBrowserSettings;
99
- webmcp?: { enabled: boolean };
84
+ tools?: ChromeDevToolsToolName[];
85
+ updatedAt?: number;
86
+ browser?: UserBrowserSettings;
87
+ webmcp?: { enabled: boolean };
100
88
  }
101
89
 
102
90
  interface SettingsDocumentResult {
103
- kind: "missing" | "invalid" | "valid";
104
- reason?: string;
105
- document?: Record<string, unknown>;
106
- normalized?: NormalizedSettingsDocument;
107
- warnings: string[];
91
+ kind: "missing" | "invalid" | "valid";
92
+ reason?: string;
93
+ document?: Record<string, unknown>;
94
+ normalized?: NormalizedSettingsDocument;
95
+ warnings: string[];
108
96
  }
109
97
 
110
98
  const DEFAULT_FILE_OPERATIONS: SettingsFileOperations = {
111
- write: (path, data) => writeFile(path, data, "utf8").then(() => undefined),
112
- rename,
99
+ write: (path, data) => writeFile(path, data, "utf8").then(() => undefined),
100
+ rename,
113
101
  };
114
102
 
115
103
  let settingsSaveQueue = Promise.resolve();
116
104
 
117
105
  export async function waitForSettingsWrites() {
118
- await settingsSaveQueue;
106
+ await settingsSaveQueue;
119
107
  }
120
108
 
121
109
  export async function loadSettings(options: SettingsLoadOptions = {}): Promise<SettingsLoadResult> {
122
- await settingsSaveQueue;
123
- const userPath = settingsFilePath();
124
- const projectPath = options.cwd ? projectSettingsFilePath(options.cwd) : undefined;
125
- const paths = { user: userPath, ...(projectPath ? { project: projectPath } : {}) };
126
- const warnings: string[] = [];
127
-
128
- let user = await readSettingsDocument(userPath, "user");
129
- const legacyExists = await fileExists(legacySettingsFilePath());
130
- if (user.kind !== "missing" && legacyExists) {
131
- warnings.push(
132
- `Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${userPath} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
133
- );
134
- }
135
- if (user.kind === "missing") {
136
- const legacy = await readSettingsDocument(legacySettingsFilePath(), "user");
137
- const concurrentlyCreated = await readSettingsDocument(userPath, "user");
138
- if (concurrentlyCreated.kind !== "missing") {
139
- user = concurrentlyCreated;
140
- if (legacy.kind !== "missing") {
141
- warnings.push(
142
- `Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${userPath} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
143
- );
144
- }
145
- } else if (legacy.kind !== "missing") {
146
- user = legacy;
147
- if (legacy.kind === "valid") {
148
- warnings.push(
149
- `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.`,
150
- );
151
- }
152
- }
153
- }
154
- warnings.push(...user.warnings);
155
-
156
- let project: SettingsDocumentResult = { kind: "missing", warnings: [] };
157
- if (projectPath && options.projectTrusted) {
158
- project = await readSettingsDocument(projectPath, "project", options.cwd);
159
- warnings.push(...project.warnings);
160
- }
161
-
162
- const environmentWarning = deprecatedEnvironmentWarning();
163
- if (environmentWarning) warnings.push(environmentWarning);
164
-
165
- const userBrowser = user.normalized?.browser ?? {};
166
- const effectiveBrowser = resolveEffectiveBrowser(userBrowser, project.normalized?.browser);
167
- const settings = resolveSettings(user.normalized, project.normalized, effectiveBrowser);
168
- const recognized =
169
- settings.tools !== undefined ||
170
- user.normalized?.browser !== undefined ||
171
- project.normalized?.browser !== undefined ||
172
- user.normalized?.webmcp !== undefined;
173
- const invalidReasons = [user, project]
174
- .filter((result) => result.kind === "invalid")
175
- .map((result) => result.reason)
176
- .filter((reason): reason is string => Boolean(reason));
177
- const userFile: UserSettingsFileStatus =
178
- user.kind === "invalid"
179
- ? {
180
- kind: "invalid",
181
- reason: user.reason ?? `${userPath}: invalid settings`,
182
- }
183
- : { kind: user.kind };
184
- const base = {
185
- effectiveBrowser,
186
- effectiveWebMcpEnabled: settings.webmcp.enabled,
187
- paths,
188
- userFile,
189
- warnings,
190
- ...(warnings.length > 0 ? { notice: warnings.join("\n") } : {}),
191
- };
192
-
193
- if (recognized) return { ...base, kind: "loaded", settings };
194
- if (invalidReasons.length > 0) {
195
- return { ...base, kind: "invalid", reason: invalidReasons.join("; ") };
196
- }
197
- return { ...base, kind: "missing" };
110
+ await settingsSaveQueue;
111
+ const userPath = settingsFilePath();
112
+ const projectPath = options.cwd ? projectSettingsFilePath(options.cwd) : undefined;
113
+ const paths = { user: userPath, ...(projectPath ? { project: projectPath } : {}) };
114
+ const warnings: string[] = [];
115
+
116
+ let user = await readSettingsDocument(userPath, "user");
117
+ const legacyExists = await fileExists(legacySettingsFilePath());
118
+ if (user.kind !== "missing" && legacyExists) {
119
+ warnings.push(
120
+ `Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${userPath} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
121
+ );
122
+ }
123
+ if (user.kind === "missing") {
124
+ const legacy = await readSettingsDocument(legacySettingsFilePath(), "user");
125
+ const concurrentlyCreated = await readSettingsDocument(userPath, "user");
126
+ if (concurrentlyCreated.kind !== "missing") {
127
+ user = concurrentlyCreated;
128
+ if (legacy.kind !== "missing") {
129
+ warnings.push(
130
+ `Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${userPath} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
131
+ );
132
+ }
133
+ } else if (legacy.kind !== "missing") {
134
+ user = legacy;
135
+ if (legacy.kind === "valid") {
136
+ warnings.push(
137
+ `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.`,
138
+ );
139
+ }
140
+ }
141
+ }
142
+ warnings.push(...user.warnings);
143
+
144
+ let project: SettingsDocumentResult = { kind: "missing", warnings: [] };
145
+ if (projectPath && options.projectTrusted) {
146
+ project = await readSettingsDocument(projectPath, "project", options.cwd);
147
+ warnings.push(...project.warnings);
148
+ }
149
+
150
+ const environmentWarning = deprecatedEnvironmentWarning();
151
+ if (environmentWarning) warnings.push(environmentWarning);
152
+
153
+ const userBrowser = user.normalized?.browser ?? {};
154
+ const effectiveBrowser = resolveEffectiveBrowser(userBrowser, project.normalized?.browser);
155
+ const settings = resolveSettings(user.normalized, project.normalized, effectiveBrowser);
156
+ const recognized =
157
+ settings.tools !== undefined ||
158
+ user.normalized?.browser !== undefined ||
159
+ project.normalized?.browser !== undefined ||
160
+ user.normalized?.webmcp !== undefined;
161
+ const invalidReasons = [user, project]
162
+ .filter((result) => result.kind === "invalid")
163
+ .map((result) => result.reason)
164
+ .filter((reason): reason is string => Boolean(reason));
165
+ const userFile: UserSettingsFileStatus =
166
+ user.kind === "invalid"
167
+ ? {
168
+ kind: "invalid",
169
+ reason: user.reason ?? `${userPath}: invalid settings`,
170
+ }
171
+ : { kind: user.kind };
172
+ const base = {
173
+ effectiveBrowser,
174
+ effectiveWebMcpEnabled: settings.webmcp.enabled,
175
+ paths,
176
+ userFile,
177
+ warnings,
178
+ ...(warnings.length > 0 ? { notice: warnings.join("\n") } : {}),
179
+ };
180
+
181
+ if (recognized) return { ...base, kind: "loaded", settings };
182
+ if (invalidReasons.length > 0) {
183
+ return { ...base, kind: "invalid", reason: invalidReasons.join("; ") };
184
+ }
185
+ return { ...base, kind: "missing" };
198
186
  }
199
187
 
200
188
  function resolveSettings(
201
- user: NormalizedSettingsDocument | undefined,
202
- _project: NormalizedSettingsDocument | undefined,
203
- browser: EffectiveBrowserSettings,
189
+ user: NormalizedSettingsDocument | undefined,
190
+ _project: NormalizedSettingsDocument | undefined,
191
+ browser: EffectiveBrowserSettings,
204
192
  ): ResolvedChromeDevToolsSettings {
205
- return {
206
- ...(user?.tools ? { tools: user.tools, updatedAt: user.updatedAt } : {}),
207
- browser,
208
- webmcp: { enabled: user?.webmcp?.enabled ?? false },
209
- };
193
+ return {
194
+ ...(user?.tools ? { tools: user.tools, updatedAt: user.updatedAt } : {}),
195
+ browser,
196
+ webmcp: { enabled: user?.webmcp?.enabled ?? false },
197
+ };
210
198
  }
211
199
 
212
200
  function resolveEffectiveBrowser(
213
- user: UserBrowserSettings,
214
- project: UserBrowserSettings | undefined,
201
+ user: UserBrowserSettings,
202
+ project: UserBrowserSettings | undefined,
215
203
  ): EffectiveBrowserSettings {
216
- const configuredEndpoint = parseBrowserEndpoint(user.endpoint ?? DEFAULT_BROWSER_ENDPOINT);
217
- const environmentHost = process.env.PI_CHROME_DEVTOOLS_HOST;
218
- const environmentPortValue = process.env.PI_CHROME_DEVTOOLS_PORT;
219
- const environmentPort = parseConfiguredPort(environmentPortValue);
220
- const environmentEndpointConfigured =
221
- environmentHost !== undefined || environmentPort !== undefined;
222
- const host = environmentHost ?? configuredEndpoint.host;
223
- const port = environmentPort ?? configuredEndpoint.port;
224
- const environmentAutoLaunch = process.env.PI_CHROME_DEVTOOLS_AUTO_LAUNCH;
225
- const environmentExecutable = process.env.PI_CHROME_DEVTOOLS_BROWSER;
226
- const executablePath = environmentExecutable || user.executablePath;
227
- const extensionPaths = project?.extensionPaths ?? user.extensionPaths ?? [];
228
- const endpointSource: BrowserSettingsSource = environmentEndpointConfigured
229
- ? "environment"
230
- : user.endpoint
231
- ? "user"
232
- : "default";
233
- return {
234
- endpoint: formatBrowserEndpoint(host, port),
235
- host,
236
- port,
237
- hostConfigured: environmentHost !== undefined || user.endpoint !== undefined,
238
- portConfigured: environmentPort !== undefined || user.endpoint !== undefined,
239
- autoLaunchEnabled:
240
- environmentAutoLaunch === undefined
241
- ? (user.autoLaunch ?? true)
242
- : environmentAutoLaunch !== "0",
243
- ...(executablePath ? { executablePath } : {}),
244
- extensionPaths: [...extensionPaths],
245
- endpointSource,
246
- autoLaunchSource:
247
- environmentAutoLaunch !== undefined
248
- ? "environment"
249
- : user.autoLaunch !== undefined
250
- ? "user"
251
- : "default",
252
- executablePathSource: environmentExecutable
253
- ? "environment"
254
- : user.executablePath
255
- ? "user"
256
- : "default",
257
- extensionPathsSource: project?.extensionPaths
258
- ? "project"
259
- : user.extensionPaths
260
- ? "user"
261
- : "default",
262
- };
204
+ const configuredEndpoint = parseBrowserEndpoint(user.endpoint ?? DEFAULT_BROWSER_ENDPOINT);
205
+ const environmentHost = process.env.PI_CHROME_DEVTOOLS_HOST;
206
+ const environmentPortValue = process.env.PI_CHROME_DEVTOOLS_PORT;
207
+ const environmentPort = parseConfiguredPort(environmentPortValue);
208
+ const environmentEndpointConfigured = environmentHost !== undefined || environmentPort !== undefined;
209
+ const host = environmentHost ?? configuredEndpoint.host;
210
+ const port = environmentPort ?? configuredEndpoint.port;
211
+ const environmentAutoLaunch = process.env.PI_CHROME_DEVTOOLS_AUTO_LAUNCH;
212
+ const environmentExecutable = process.env.PI_CHROME_DEVTOOLS_BROWSER;
213
+ const executablePath = environmentExecutable || user.executablePath;
214
+ const extensionPaths = project?.extensionPaths ?? user.extensionPaths ?? [];
215
+ const endpointSource: BrowserSettingsSource = environmentEndpointConfigured
216
+ ? "environment"
217
+ : user.endpoint
218
+ ? "user"
219
+ : "default";
220
+ return {
221
+ endpoint: formatBrowserEndpoint(host, port),
222
+ host,
223
+ port,
224
+ hostConfigured: environmentHost !== undefined || user.endpoint !== undefined,
225
+ portConfigured: environmentPort !== undefined || user.endpoint !== undefined,
226
+ autoLaunchEnabled: environmentAutoLaunch === undefined ? (user.autoLaunch ?? true) : environmentAutoLaunch !== "0",
227
+ ...(executablePath ? { executablePath } : {}),
228
+ extensionPaths: [...extensionPaths],
229
+ endpointSource,
230
+ autoLaunchSource:
231
+ environmentAutoLaunch !== undefined ? "environment" : user.autoLaunch !== undefined ? "user" : "default",
232
+ executablePathSource: environmentExecutable ? "environment" : user.executablePath ? "user" : "default",
233
+ extensionPathsSource: project?.extensionPaths ? "project" : user.extensionPaths ? "user" : "default",
234
+ };
263
235
  }
264
236
 
265
237
  function deprecatedEnvironmentWarning() {
266
- const configuredNames = [
267
- "PI_CHROME_DEVTOOLS_HOST",
268
- "PI_CHROME_DEVTOOLS_PORT",
269
- "PI_CHROME_DEVTOOLS_AUTO_LAUNCH",
270
- "PI_CHROME_DEVTOOLS_BROWSER",
271
- ].filter((name) => process.env[name] !== undefined);
272
- if (configuredNames.length === 0) return undefined;
273
- 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.`;
238
+ const configuredNames = [
239
+ "PI_CHROME_DEVTOOLS_HOST",
240
+ "PI_CHROME_DEVTOOLS_PORT",
241
+ "PI_CHROME_DEVTOOLS_AUTO_LAUNCH",
242
+ "PI_CHROME_DEVTOOLS_BROWSER",
243
+ ].filter((name) => process.env[name] !== undefined);
244
+ if (configuredNames.length === 0) return undefined;
245
+ 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.`;
274
246
  }
275
247
 
276
248
  async function readSettingsDocument(
277
- filePath: string,
278
- scope: "project" | "user",
279
- cwd?: string,
249
+ filePath: string,
250
+ scope: "project" | "user",
251
+ cwd?: string,
280
252
  ): Promise<SettingsDocumentResult> {
281
- let text: string;
282
- try {
283
- text = await readFile(filePath, "utf8");
284
- } catch (error) {
285
- if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing", warnings: [] };
286
- const reason = `${filePath}: ${formatError(error)}`;
287
- return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
288
- }
289
-
290
- let parsed: unknown;
291
- try {
292
- parsed = JSON.parse(text) as unknown;
293
- } catch {
294
- const reason = `${filePath}: invalid JSON`;
295
- return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
296
- }
297
- if (!isRecord(parsed)) {
298
- const reason = `${filePath}: expected a JSON object`;
299
- return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
300
- }
301
-
302
- const scopeWarnings = projectOwnedSettingsWarnings(parsed, scope, filePath);
303
- try {
304
- const normalized = await normalizeSettingsDocument(parsed, scope, filePath, cwd);
305
- return { kind: "valid", document: { ...parsed }, normalized, warnings: scopeWarnings };
306
- } catch (error) {
307
- const reason = `${filePath}: ${formatError(error)}`;
308
- return {
309
- kind: "invalid",
310
- reason,
311
- warnings: [...scopeWarnings, `Chrome DevTools settings ignored: ${reason}`],
312
- };
313
- }
314
- }
315
-
316
- function projectOwnedSettingsWarnings(
317
- document: Record<string, unknown>,
318
- scope: "project" | "user",
319
- filePath: string,
320
- ) {
321
- if (scope !== "project") return [];
322
- const browser = isRecord(document.browser) ? document.browser : undefined;
323
- const warnings = browser
324
- ? ["endpoint", "autoLaunch", "executablePath"]
325
- .filter((field) => browser[field] !== undefined)
326
- .map(
327
- (field) =>
328
- `Chrome DevTools project browser.${field} ignored in ${filePath}; configure this machine-owned setting in ${settingsFilePath()}.`,
329
- )
330
- : [];
331
- if (document.webmcp !== undefined) {
332
- warnings.push(
333
- `Chrome DevTools project webmcp settings ignored in ${filePath}; only the user settings file may enable experimental WebMCP.`,
334
- );
335
- }
336
- return warnings;
253
+ let text: string;
254
+ try {
255
+ text = await readFile(filePath, "utf8");
256
+ } catch (error) {
257
+ if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing", warnings: [] };
258
+ const reason = `${filePath}: ${formatError(error)}`;
259
+ return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
260
+ }
261
+
262
+ let parsed: unknown;
263
+ try {
264
+ parsed = JSON.parse(text) as unknown;
265
+ } catch {
266
+ const reason = `${filePath}: invalid JSON`;
267
+ return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
268
+ }
269
+ if (!isRecord(parsed)) {
270
+ const reason = `${filePath}: expected a JSON object`;
271
+ return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
272
+ }
273
+
274
+ const scopeWarnings = projectOwnedSettingsWarnings(parsed, scope, filePath);
275
+ try {
276
+ const normalized = await normalizeSettingsDocument(parsed, scope, filePath, cwd);
277
+ return { kind: "valid", document: { ...parsed }, normalized, warnings: scopeWarnings };
278
+ } catch (error) {
279
+ const reason = `${filePath}: ${formatError(error)}`;
280
+ return {
281
+ kind: "invalid",
282
+ reason,
283
+ warnings: [...scopeWarnings, `Chrome DevTools settings ignored: ${reason}`],
284
+ };
285
+ }
286
+ }
287
+
288
+ function projectOwnedSettingsWarnings(document: Record<string, unknown>, scope: "project" | "user", filePath: string) {
289
+ if (scope !== "project") return [];
290
+ const browser = isRecord(document.browser) ? document.browser : undefined;
291
+ const warnings = browser
292
+ ? ["endpoint", "autoLaunch", "executablePath"]
293
+ .filter((field) => browser[field] !== undefined)
294
+ .map(
295
+ (field) =>
296
+ `Chrome DevTools project browser.${field} ignored in ${filePath}; configure this machine-owned setting in ${settingsFilePath()}.`,
297
+ )
298
+ : [];
299
+ if (document.webmcp !== undefined) {
300
+ warnings.push(
301
+ `Chrome DevTools project webmcp settings ignored in ${filePath}; only the user settings file may enable experimental WebMCP.`,
302
+ );
303
+ }
304
+ return warnings;
337
305
  }
338
306
 
339
307
  async function normalizeSettingsDocument(
340
- document: Record<string, unknown>,
341
- scope: "project" | "user",
342
- filePath: string,
343
- cwd?: string,
308
+ document: Record<string, unknown>,
309
+ scope: "project" | "user",
310
+ filePath: string,
311
+ cwd?: string,
344
312
  ): Promise<NormalizedSettingsDocument> {
345
- const normalized: NormalizedSettingsDocument = {};
346
-
347
- if (scope === "user" && (document.tools !== undefined || document.updatedAt !== undefined)) {
348
- const toolSettings = normalizeChromeDevtoolsSettings(document);
349
- if (!toolSettings) {
350
- throw new Error(
351
- "expected tools to be an array of Chrome DevTools tool names with numeric updatedAt",
352
- );
353
- }
354
- normalized.tools = toolSettings.tools;
355
- normalized.updatedAt = toolSettings.updatedAt;
356
- }
357
-
358
- if (document.browser !== undefined) {
359
- if (!isRecord(document.browser)) throw new Error("expected browser to be an object");
360
- normalized.browser = await normalizeBrowserSection(document.browser, scope, filePath, cwd);
361
- }
362
-
363
- if (scope === "user" && document.webmcp !== undefined) {
364
- if (!isRecord(document.webmcp) || typeof document.webmcp.enabled !== "boolean") {
365
- throw new Error("expected webmcp.enabled to be a boolean");
366
- }
367
- normalized.webmcp = { enabled: document.webmcp.enabled };
368
- }
369
-
370
- return normalized;
313
+ const normalized: NormalizedSettingsDocument = {};
314
+
315
+ if (scope === "user" && (document.tools !== undefined || document.updatedAt !== undefined)) {
316
+ const toolSettings = normalizeChromeDevtoolsSettings(document);
317
+ if (!toolSettings) {
318
+ throw new Error("expected tools to be an array of Chrome DevTools tool names with numeric updatedAt");
319
+ }
320
+ normalized.tools = toolSettings.tools;
321
+ normalized.updatedAt = toolSettings.updatedAt;
322
+ }
323
+
324
+ if (document.browser !== undefined) {
325
+ if (!isRecord(document.browser)) throw new Error("expected browser to be an object");
326
+ normalized.browser = await normalizeBrowserSection(document.browser, scope, filePath, cwd);
327
+ }
328
+
329
+ if (scope === "user" && document.webmcp !== undefined) {
330
+ if (!isRecord(document.webmcp) || typeof document.webmcp.enabled !== "boolean") {
331
+ throw new Error("expected webmcp.enabled to be a boolean");
332
+ }
333
+ normalized.webmcp = { enabled: document.webmcp.enabled };
334
+ }
335
+
336
+ return normalized;
371
337
  }
372
338
 
373
339
  async function normalizeBrowserSection(
374
- browser: Record<string, unknown>,
375
- scope: "project" | "user",
376
- _filePath: string,
377
- cwd?: string,
340
+ browser: Record<string, unknown>,
341
+ scope: "project" | "user",
342
+ _filePath: string,
343
+ cwd?: string,
378
344
  ): Promise<UserBrowserSettings> {
379
- const normalized: UserBrowserSettings = {};
380
- if (scope === "user" && browser.endpoint !== undefined) {
381
- if (typeof browser.endpoint !== "string") {
382
- throw new Error("expected browser.endpoint to be an HTTP URL with an explicit port");
383
- }
384
- normalized.endpoint = parseBrowserEndpoint(browser.endpoint).endpoint;
385
- }
386
- if (scope === "user" && browser.autoLaunch !== undefined) {
387
- if (typeof browser.autoLaunch !== "boolean") {
388
- throw new Error("expected browser.autoLaunch to be a boolean");
389
- }
390
- normalized.autoLaunch = browser.autoLaunch;
391
- }
392
- if (scope === "user" && browser.executablePath !== undefined) {
393
- if (typeof browser.executablePath !== "string" || browser.executablePath.length === 0) {
394
- throw new Error("expected browser.executablePath to be a non-empty absolute path");
395
- }
396
- if (!isAbsolute(browser.executablePath)) {
397
- throw new Error("browser.executablePath in user settings must be absolute");
398
- }
399
- normalized.executablePath = resolve(browser.executablePath);
400
- }
401
-
402
- if (browser.extensionPaths !== undefined) {
403
- if (
404
- !Array.isArray(browser.extensionPaths) ||
405
- !browser.extensionPaths.every((entry) => typeof entry === "string" && entry.length > 0)
406
- ) {
407
- throw new Error(`expected ${scope} browser.extensionPaths to be an array of non-empty paths`);
408
- }
409
- const resolvedPaths: string[] = [];
410
- for (const configuredPath of browser.extensionPaths as string[]) {
411
- if (scope === "user" && !isAbsolute(configuredPath)) {
412
- throw new Error("browser.extensionPaths in user settings must contain only absolute paths");
413
- }
414
- const absolutePath = isAbsolute(configuredPath)
415
- ? resolve(configuredPath)
416
- : resolve(cwd ?? process.cwd(), configuredPath);
417
- resolvedPaths.push(await validateUnpackedExtensionPath(absolutePath));
418
- }
419
- normalized.extensionPaths = orderedUnique(resolvedPaths);
420
- }
421
- return normalized;
345
+ const normalized: UserBrowserSettings = {};
346
+ if (scope === "user" && browser.endpoint !== undefined) {
347
+ if (typeof browser.endpoint !== "string") {
348
+ throw new Error("expected browser.endpoint to be an HTTP URL with an explicit port");
349
+ }
350
+ normalized.endpoint = parseBrowserEndpoint(browser.endpoint).endpoint;
351
+ }
352
+ if (scope === "user" && browser.autoLaunch !== undefined) {
353
+ if (typeof browser.autoLaunch !== "boolean") {
354
+ throw new Error("expected browser.autoLaunch to be a boolean");
355
+ }
356
+ normalized.autoLaunch = browser.autoLaunch;
357
+ }
358
+ if (scope === "user" && browser.executablePath !== undefined) {
359
+ if (typeof browser.executablePath !== "string" || browser.executablePath.length === 0) {
360
+ throw new Error("expected browser.executablePath to be a non-empty absolute path");
361
+ }
362
+ if (!isAbsolute(browser.executablePath)) {
363
+ throw new Error("browser.executablePath in user settings must be absolute");
364
+ }
365
+ normalized.executablePath = resolve(browser.executablePath);
366
+ }
367
+
368
+ if (browser.extensionPaths !== undefined) {
369
+ if (
370
+ !Array.isArray(browser.extensionPaths) ||
371
+ !browser.extensionPaths.every((entry) => typeof entry === "string" && entry.length > 0)
372
+ ) {
373
+ throw new Error(`expected ${scope} browser.extensionPaths to be an array of non-empty paths`);
374
+ }
375
+ const resolvedPaths: string[] = [];
376
+ for (const configuredPath of browser.extensionPaths as string[]) {
377
+ if (scope === "user" && !isAbsolute(configuredPath)) {
378
+ throw new Error("browser.extensionPaths in user settings must contain only absolute paths");
379
+ }
380
+ const absolutePath = isAbsolute(configuredPath)
381
+ ? resolve(configuredPath)
382
+ : resolve(cwd ?? process.cwd(), configuredPath);
383
+ resolvedPaths.push(await validateUnpackedExtensionPath(absolutePath));
384
+ }
385
+ normalized.extensionPaths = orderedUnique(resolvedPaths);
386
+ }
387
+ return normalized;
422
388
  }
423
389
 
424
390
  async function validateUnpackedExtensionPath(extensionPath: string) {
425
- let canonicalPath: string;
426
- try {
427
- canonicalPath = await realpath(extensionPath);
428
- const extensionStat = await stat(canonicalPath);
429
- if (!extensionStat.isDirectory()) throw new Error("not a directory");
430
- } catch (error) {
431
- throw new Error(`unpacked extension path ${extensionPath} is invalid: ${formatError(error)}`);
432
- }
433
-
434
- const manifestPath = join(canonicalPath, "manifest.json");
435
- let manifest: unknown;
436
- try {
437
- const manifestStat = await lstat(manifestPath);
438
- if (!manifestStat.isFile()) throw new Error("not a regular file");
439
- manifest = JSON.parse(await readFile(manifestPath, "utf8")) as unknown;
440
- } catch (error) {
441
- throw new Error(
442
- `unpacked extension ${canonicalPath} has invalid manifest.json: ${formatError(error)}`,
443
- );
444
- }
445
- if (canonicalPath.includes(",")) {
446
- throw new Error(
447
- `unpacked extension path ${canonicalPath} cannot contain a comma because Chrome separates multiple --load-extension paths with commas`,
448
- );
449
- }
450
- if (
451
- !isRecord(manifest) ||
452
- ![2, 3].includes(manifest.manifest_version as number) ||
453
- typeof manifest.name !== "string" ||
454
- typeof manifest.version !== "string"
455
- ) {
456
- throw new Error(
457
- `unpacked extension ${canonicalPath} has invalid manifest.json: expected manifest_version, name, and version`,
458
- );
459
- }
460
- return canonicalPath;
391
+ let canonicalPath: string;
392
+ try {
393
+ canonicalPath = await realpath(extensionPath);
394
+ const extensionStat = await stat(canonicalPath);
395
+ if (!extensionStat.isDirectory()) throw new Error("not a directory");
396
+ } catch (error) {
397
+ throw new Error(`unpacked extension path ${extensionPath} is invalid: ${formatError(error)}`);
398
+ }
399
+
400
+ const manifestPath = join(canonicalPath, "manifest.json");
401
+ let manifest: unknown;
402
+ try {
403
+ const manifestStat = await lstat(manifestPath);
404
+ if (!manifestStat.isFile()) throw new Error("not a regular file");
405
+ manifest = JSON.parse(await readFile(manifestPath, "utf8")) as unknown;
406
+ } catch (error) {
407
+ throw new Error(`unpacked extension ${canonicalPath} has invalid manifest.json: ${formatError(error)}`);
408
+ }
409
+ if (canonicalPath.includes(",")) {
410
+ throw new Error(
411
+ `unpacked extension path ${canonicalPath} cannot contain a comma because Chrome separates multiple --load-extension paths with commas`,
412
+ );
413
+ }
414
+ if (
415
+ !isRecord(manifest) ||
416
+ ![2, 3].includes(manifest.manifest_version as number) ||
417
+ typeof manifest.name !== "string" ||
418
+ typeof manifest.version !== "string"
419
+ ) {
420
+ throw new Error(
421
+ `unpacked extension ${canonicalPath} has invalid manifest.json: expected manifest_version, name, and version`,
422
+ );
423
+ }
424
+ return canonicalPath;
461
425
  }
462
426
 
463
427
  export function parseBrowserEndpoint(value: string) {
464
- const trimmedValue = value.trim();
465
- const explicitPort = trimmedValue.match(/^http:\/\/(?:\[[^\]]+\]|[^/:?#]+):(\d+)\/?$/i)?.[1];
466
- let endpoint: URL;
467
- try {
468
- endpoint = new URL(trimmedValue);
469
- } catch {
470
- throw new Error("browser.endpoint must be a valid HTTP URL with an explicit port");
471
- }
472
- if (
473
- endpoint.protocol !== "http:" ||
474
- endpoint.username ||
475
- endpoint.password ||
476
- !endpoint.hostname ||
477
- !explicitPort ||
478
- (endpoint.pathname !== "/" && endpoint.pathname !== "") ||
479
- endpoint.search ||
480
- endpoint.hash
481
- ) {
482
- throw new Error(
483
- "browser.endpoint must be an HTTP origin with an explicit port and no credentials, path, query, or fragment",
484
- );
485
- }
486
- const port = parseConfiguredPort(explicitPort);
487
- if (port === undefined) {
488
- throw new Error("browser.endpoint must use a port from 1 through 65535");
489
- }
490
- const host = endpoint.hostname;
491
- return { endpoint: formatBrowserEndpoint(host, port), host, port };
428
+ const trimmedValue = value.trim();
429
+ const explicitPort = trimmedValue.match(/^http:\/\/(?:\[[^\]]+\]|[^/:?#]+):(\d+)\/?$/i)?.[1];
430
+ let endpoint: URL;
431
+ try {
432
+ endpoint = new URL(trimmedValue);
433
+ } catch {
434
+ throw new Error("browser.endpoint must be a valid HTTP URL with an explicit port");
435
+ }
436
+ if (
437
+ endpoint.protocol !== "http:" ||
438
+ endpoint.username ||
439
+ endpoint.password ||
440
+ !endpoint.hostname ||
441
+ !explicitPort ||
442
+ (endpoint.pathname !== "/" && endpoint.pathname !== "") ||
443
+ endpoint.search ||
444
+ endpoint.hash
445
+ ) {
446
+ throw new Error(
447
+ "browser.endpoint must be an HTTP origin with an explicit port and no credentials, path, query, or fragment",
448
+ );
449
+ }
450
+ const port = parseConfiguredPort(explicitPort);
451
+ if (port === undefined) {
452
+ throw new Error("browser.endpoint must use a port from 1 through 65535");
453
+ }
454
+ const host = endpoint.hostname;
455
+ return { endpoint: formatBrowserEndpoint(host, port), host, port };
492
456
  }
493
457
 
494
458
  export function parseConfiguredPort(value: string | undefined) {
495
- if (value === undefined) return undefined;
496
- const trimmedValue = value.trim();
497
- if (!/^\d+$/.test(trimmedValue)) return undefined;
498
- const port = Number(trimmedValue);
499
- if (!Number.isInteger(port) || port < 1 || port > 65_535) return undefined;
500
- return port;
459
+ if (value === undefined) return undefined;
460
+ const trimmedValue = value.trim();
461
+ if (!/^\d+$/.test(trimmedValue)) return undefined;
462
+ const port = Number(trimmedValue);
463
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) return undefined;
464
+ return port;
501
465
  }
502
466
 
503
467
  function formatBrowserEndpoint(host: string, port: number) {
504
- const formattedHost =
505
- host.startsWith("[") && host.endsWith("]") ? host : host.includes(":") ? `[${host}]` : host;
506
- return `http://${formattedHost}:${port}`;
468
+ const formattedHost = host.startsWith("[") && host.endsWith("]") ? host : host.includes(":") ? `[${host}]` : host;
469
+ return `http://${formattedHost}:${port}`;
507
470
  }
508
471
 
509
- export function normalizeChromeDevtoolsSettings(
510
- value: unknown,
511
- ): ChromeDevToolsSettings | undefined {
512
- if (!isRecord(value)) return undefined;
513
- if (typeof value.updatedAt !== "number" || !Number.isFinite(value.updatedAt)) return undefined;
472
+ export function normalizeChromeDevtoolsSettings(value: unknown): ChromeDevToolsSettings | undefined {
473
+ if (!isRecord(value)) return undefined;
474
+ if (typeof value.updatedAt !== "number" || !Number.isFinite(value.updatedAt)) return undefined;
514
475
 
515
- if (value.tools === "enabled") {
516
- return { tools: [...CHROME_DEVTOOLS_TOOL_NAMES], updatedAt: value.updatedAt };
517
- }
518
- if (value.tools === "disabled") return { tools: [], updatedAt: value.updatedAt };
519
- if (!Array.isArray(value.tools) || !value.tools.every(isChromeDevtoolsToolName)) return undefined;
520
- return { tools: orderedUniqueChromeDevtoolsTools(value.tools), updatedAt: value.updatedAt };
476
+ if (value.tools === "enabled") {
477
+ return { tools: [...CHROME_DEVTOOLS_TOOL_NAMES], updatedAt: value.updatedAt };
478
+ }
479
+ if (value.tools === "disabled") return { tools: [], updatedAt: value.updatedAt };
480
+ if (!Array.isArray(value.tools) || !value.tools.every(isChromeDevtoolsToolName)) return undefined;
481
+ return { tools: orderedUniqueChromeDevtoolsTools(value.tools), updatedAt: value.updatedAt };
521
482
  }
522
483
 
523
484
  function isChromeDevtoolsToolName(value: unknown): value is ChromeDevToolsToolName {
524
- return typeof value === "string" && CHROME_DEVTOOLS_TOOL_NAMES.includes(value as never);
485
+ return typeof value === "string" && CHROME_DEVTOOLS_TOOL_NAMES.includes(value as never);
525
486
  }
526
487
 
527
488
  function orderedUniqueChromeDevtoolsTools(tools: readonly ChromeDevToolsToolName[]) {
528
- const selectedTools = new Set(tools);
529
- return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => selectedTools.has(toolName));
489
+ const selectedTools = new Set(tools);
490
+ return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => selectedTools.has(toolName));
530
491
  }
531
492
 
532
493
  function orderedUnique(values: readonly string[]) {
533
- return [...new Set(values)];
494
+ return [...new Set(values)];
534
495
  }
535
496
 
536
497
  export function saveSettings(
537
- settings: ChromeDevToolsSettings,
538
- operations: Partial<SettingsFileOperations> = {},
539
- ): Promise<void> {
540
- return queueSettingsMutation(
541
- (current) => ({
542
- ...current,
543
- tools: [...settings.tools],
544
- updatedAt: settings.updatedAt,
545
- }),
546
- operations,
547
- );
548
- }
549
-
550
- export function saveWebMcpSettings(
551
- enabled: boolean,
552
- operations: Partial<SettingsFileOperations> = {},
498
+ settings: ChromeDevToolsSettings,
499
+ operations: Partial<SettingsFileOperations> = {},
553
500
  ): Promise<void> {
554
- return queueSettingsMutation(
555
- (current) => ({
556
- ...current,
557
- webmcp: {
558
- ...(isRecord(current.webmcp) ? current.webmcp : {}),
559
- enabled,
560
- },
561
- }),
562
- operations,
563
- );
501
+ return queueSettingsMutation(
502
+ (current) => ({
503
+ ...current,
504
+ tools: [...settings.tools],
505
+ updatedAt: settings.updatedAt,
506
+ }),
507
+ operations,
508
+ );
509
+ }
510
+
511
+ export function saveWebMcpSettings(enabled: boolean, operations: Partial<SettingsFileOperations> = {}): Promise<void> {
512
+ return queueSettingsMutation(
513
+ (current) => ({
514
+ ...current,
515
+ webmcp: {
516
+ ...(isRecord(current.webmcp) ? current.webmcp : {}),
517
+ enabled,
518
+ },
519
+ }),
520
+ operations,
521
+ );
564
522
  }
565
523
 
566
524
  export function saveBrowserSettings(
567
- patch: BrowserSettingsPatch,
568
- operations: Partial<SettingsFileOperations> = {},
525
+ patch: BrowserSettingsPatch,
526
+ operations: Partial<SettingsFileOperations> = {},
569
527
  ): Promise<void> {
570
- return queueSettingsMutation(async (current) => {
571
- const browser = isRecord(current.browser) ? { ...current.browser } : {};
572
- for (const field of ["endpoint", "autoLaunch", "executablePath"] as const) {
573
- const value = patch[field];
574
- if (value === undefined) continue;
575
- if (value === null) delete browser[field];
576
- else browser[field] = value;
577
- }
578
- await normalizeBrowserSection(browser, "user", settingsFilePath());
579
- return { ...current, browser };
580
- }, operations);
528
+ return queueSettingsMutation(async (current) => {
529
+ const browser = isRecord(current.browser) ? { ...current.browser } : {};
530
+ for (const field of ["endpoint", "autoLaunch", "executablePath"] as const) {
531
+ const value = patch[field];
532
+ if (value === undefined) continue;
533
+ if (value === null) delete browser[field];
534
+ else browser[field] = value;
535
+ }
536
+ await normalizeBrowserSection(browser, "user", settingsFilePath());
537
+ return { ...current, browser };
538
+ }, operations);
581
539
  }
582
540
 
583
541
  function queueSettingsMutation(
584
- mutate: (
585
- current: Record<string, unknown>,
586
- ) => Record<string, unknown> | Promise<Record<string, unknown>>,
587
- operations: Partial<SettingsFileOperations>,
542
+ mutate: (current: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>,
543
+ operations: Partial<SettingsFileOperations>,
588
544
  ) {
589
- const operation = settingsSaveQueue.then(() => saveSettingsMutationNow(mutate, operations));
590
- settingsSaveQueue = operation.catch(() => undefined);
591
- return operation;
545
+ const operation = settingsSaveQueue.then(() => saveSettingsMutationNow(mutate, operations));
546
+ settingsSaveQueue = operation.catch(() => undefined);
547
+ return operation;
592
548
  }
593
549
 
594
550
  async function saveSettingsMutationNow(
595
- mutate: (
596
- current: Record<string, unknown>,
597
- ) => Record<string, unknown> | Promise<Record<string, unknown>>,
598
- operations: Partial<SettingsFileOperations>,
551
+ mutate: (current: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>,
552
+ operations: Partial<SettingsFileOperations>,
599
553
  ): Promise<void> {
600
- const filePath = settingsFilePath();
601
- let current = await readSettingsDocument(filePath, "user");
602
- const replaceCanonical = current.kind !== "missing";
603
- if (!replaceCanonical) current = await readSettingsDocument(legacySettingsFilePath(), "user");
604
- if (current.kind === "invalid") {
605
- throw new Error(`Cannot save Chrome DevTools settings until you repair ${current.reason}`);
606
- }
607
- const nextDocument = await mutate(current.document ?? {});
608
- await mkdir(dirname(filePath), { recursive: true });
609
- const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
610
- try {
611
- await (operations.write ?? DEFAULT_FILE_OPERATIONS.write)(
612
- tempFile,
613
- `${JSON.stringify(nextDocument, null, 2)}\n`,
614
- );
615
- if (!replaceCanonical && (await pathEntryExists(filePath))) {
616
- throw new Error(
617
- `${NEW_SETTINGS_FILE_NAME} was created concurrently; reopen settings and retry.`,
618
- );
619
- }
620
- await (operations.rename ?? DEFAULT_FILE_OPERATIONS.rename)(tempFile, filePath);
621
- } catch (error) {
622
- await rm(tempFile, { force: true }).catch(() => undefined);
623
- throw error;
624
- }
554
+ const filePath = settingsFilePath();
555
+ let current = await readSettingsDocument(filePath, "user");
556
+ const replaceCanonical = current.kind !== "missing";
557
+ if (!replaceCanonical) current = await readSettingsDocument(legacySettingsFilePath(), "user");
558
+ if (current.kind === "invalid") {
559
+ throw new Error(`Cannot save Chrome DevTools settings until you repair ${current.reason}`);
560
+ }
561
+ const nextDocument = await mutate(current.document ?? {});
562
+ await mkdir(dirname(filePath), { recursive: true });
563
+ const tempFile = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
564
+ try {
565
+ await (operations.write ?? DEFAULT_FILE_OPERATIONS.write)(tempFile, `${JSON.stringify(nextDocument, null, 2)}\n`);
566
+ if (!replaceCanonical && (await pathEntryExists(filePath))) {
567
+ throw new Error(`${NEW_SETTINGS_FILE_NAME} was created concurrently; reopen settings and retry.`);
568
+ }
569
+ await (operations.rename ?? DEFAULT_FILE_OPERATIONS.rename)(tempFile, filePath);
570
+ } catch (error) {
571
+ await rm(tempFile, { force: true }).catch(() => undefined);
572
+ throw error;
573
+ }
625
574
  }
626
575
 
627
576
  export function settingsFilePath() {
628
- return join(getAgentDir(), NEW_SETTINGS_FILE_NAME);
577
+ return join(getAgentDir(), NEW_SETTINGS_FILE_NAME);
629
578
  }
630
579
 
631
580
  export function projectSettingsFilePath(cwd: string) {
632
- return join(cwd, CONFIG_DIR_NAME, NEW_SETTINGS_FILE_NAME);
581
+ return join(cwd, CONFIG_DIR_NAME, NEW_SETTINGS_FILE_NAME);
633
582
  }
634
583
 
635
584
  function legacySettingsFilePath() {
636
- return join(getAgentDir(), LEGACY_SETTINGS_FILE_NAME);
585
+ return join(getAgentDir(), LEGACY_SETTINGS_FILE_NAME);
637
586
  }
638
587
 
639
588
  async function fileExists(filePath: string) {
640
- try {
641
- await access(filePath, constants.F_OK);
642
- return true;
643
- } catch {
644
- return false;
645
- }
589
+ try {
590
+ await access(filePath, constants.F_OK);
591
+ return true;
592
+ } catch {
593
+ return false;
594
+ }
646
595
  }
647
596
 
648
597
  async function pathEntryExists(filePath: string) {
649
- try {
650
- await lstat(filePath);
651
- return true;
652
- } catch (error) {
653
- if (isNodeError(error) && error.code === "ENOENT") return false;
654
- throw error;
655
- }
598
+ try {
599
+ await lstat(filePath);
600
+ return true;
601
+ } catch (error) {
602
+ if (isNodeError(error) && error.code === "ENOENT") return false;
603
+ throw error;
604
+ }
656
605
  }
657
606
 
658
607
  function isRecord(value: unknown): value is Record<string, unknown> {
659
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
608
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
660
609
  }
661
610
 
662
611
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
663
- return error instanceof Error && "code" in error;
612
+ return error instanceof Error && "code" in error;
664
613
  }
665
614
 
666
615
  function formatError(error: unknown) {
667
- return error instanceof Error ? error.message : String(error);
616
+ return error instanceof Error ? error.message : String(error);
668
617
  }