@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.
- package/README.md +1 -0
- package/dist/chunks/{browser-settings-menu-IMB6HXSU.js → browser-settings-menu-SR7KLZT6.js} +16 -42
- package/dist/chunks/browser-settings-menu-SR7KLZT6.js.map +7 -0
- package/dist/chunks/{chunk-GTTYS5GT.js → chunk-4KMN4SFF.js} +2 -2
- package/dist/chunks/chunk-4KMN4SFF.js.map +7 -0
- package/dist/chunks/{chunk-SRT3UUIP.js → chunk-F5VG35UI.js} +23 -74
- package/dist/chunks/chunk-F5VG35UI.js.map +7 -0
- package/dist/chunks/{chunk-LLXOCRNF.js → chunk-MZCGM6D3.js} +12 -33
- package/dist/chunks/chunk-MZCGM6D3.js.map +7 -0
- package/dist/chunks/{menu-UJETYMSB.js → menu-YR4VRT7D.js} +10 -23
- package/dist/chunks/menu-YR4VRT7D.js.map +7 -0
- package/dist/chunks/{tools-6P7EUX37.js → tools-6R7Z6HWJ.js} +17 -59
- package/dist/chunks/tools-6R7Z6HWJ.js.map +7 -0
- package/dist/index.ts +18 -42
- package/dist/index.ts.map +2 -2
- package/package.json +54 -55
- package/src/browser-manager.ts +872 -942
- package/src/browser-settings-menu.ts +315 -350
- package/src/cdp-client.ts +381 -398
- package/src/chrome-devtools.ts +205 -232
- package/src/display.ts +26 -29
- package/src/lazy-tools.ts +158 -181
- package/src/menu.ts +409 -446
- package/src/render.ts +72 -81
- package/src/runtime.ts +137 -140
- package/src/screenshot.ts +170 -186
- package/src/settings.ts +472 -523
- package/src/tool-names.ts +8 -14
- package/src/tool-selector.ts +242 -271
- package/src/tools.ts +218 -226
- package/src/webmcp/discovery.ts +184 -188
- package/src/webmcp/policy.ts +296 -329
- package/src/webmcp/protocol.ts +324 -337
- package/src/webmcp/tools.ts +136 -147
- package/dist/chunks/browser-settings-menu-IMB6HXSU.js.map +0 -7
- package/dist/chunks/chunk-GTTYS5GT.js.map +0 -7
- package/dist/chunks/chunk-LLXOCRNF.js.map +0 -7
- package/dist/chunks/chunk-SRT3UUIP.js.map +0 -7
- package/dist/chunks/menu-UJETYMSB.js.map +0 -7
- 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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
autoLaunch?: boolean;
|
|
25
|
+
executablePath?: string;
|
|
26
|
+
extensionPaths?: string[];
|
|
37
27
|
}
|
|
38
28
|
|
|
39
29
|
export interface BrowserSettingsPatch {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
endpoint?: string | null;
|
|
31
|
+
autoLaunch?: boolean | null;
|
|
32
|
+
executablePath?: string | null;
|
|
43
33
|
}
|
|
44
34
|
|
|
45
35
|
export interface EffectiveBrowserSettings {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
tools?: ChromeDevToolsToolName[];
|
|
52
|
+
updatedAt?: number;
|
|
53
|
+
browser: EffectiveBrowserSettings;
|
|
54
|
+
webmcp: { enabled: boolean };
|
|
65
55
|
}
|
|
66
56
|
|
|
67
57
|
export interface SettingsLoadOptions {
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
tools?: ChromeDevToolsToolName[];
|
|
85
|
+
updatedAt?: number;
|
|
86
|
+
browser?: UserBrowserSettings;
|
|
87
|
+
webmcp?: { enabled: boolean };
|
|
100
88
|
}
|
|
101
89
|
|
|
102
90
|
interface SettingsDocumentResult {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
106
|
+
await settingsSaveQueue;
|
|
119
107
|
}
|
|
120
108
|
|
|
121
109
|
export async function loadSettings(options: SettingsLoadOptions = {}): Promise<SettingsLoadResult> {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
189
|
+
user: NormalizedSettingsDocument | undefined,
|
|
190
|
+
_project: NormalizedSettingsDocument | undefined,
|
|
191
|
+
browser: EffectiveBrowserSettings,
|
|
204
192
|
): ResolvedChromeDevToolsSettings {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
201
|
+
user: UserBrowserSettings,
|
|
202
|
+
project: UserBrowserSettings | undefined,
|
|
215
203
|
): EffectiveBrowserSettings {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
249
|
+
filePath: string,
|
|
250
|
+
scope: "project" | "user",
|
|
251
|
+
cwd?: string,
|
|
280
252
|
): Promise<SettingsDocumentResult> {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function projectOwnedSettingsWarnings(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
308
|
+
document: Record<string, unknown>,
|
|
309
|
+
scope: "project" | "user",
|
|
310
|
+
filePath: string,
|
|
311
|
+
cwd?: string,
|
|
344
312
|
): Promise<NormalizedSettingsDocument> {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
340
|
+
browser: Record<string, unknown>,
|
|
341
|
+
scope: "project" | "user",
|
|
342
|
+
_filePath: string,
|
|
343
|
+
cwd?: string,
|
|
378
344
|
): Promise<UserBrowserSettings> {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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
|
-
|
|
511
|
-
)
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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
|
-
|
|
494
|
+
return [...new Set(values)];
|
|
534
495
|
}
|
|
535
496
|
|
|
536
497
|
export function saveSettings(
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
568
|
-
|
|
525
|
+
patch: BrowserSettingsPatch,
|
|
526
|
+
operations: Partial<SettingsFileOperations> = {},
|
|
569
527
|
): Promise<void> {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
585
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
596
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
577
|
+
return join(getAgentDir(), NEW_SETTINGS_FILE_NAME);
|
|
629
578
|
}
|
|
630
579
|
|
|
631
580
|
export function projectSettingsFilePath(cwd: string) {
|
|
632
|
-
|
|
581
|
+
return join(cwd, CONFIG_DIR_NAME, NEW_SETTINGS_FILE_NAME);
|
|
633
582
|
}
|
|
634
583
|
|
|
635
584
|
function legacySettingsFilePath() {
|
|
636
|
-
|
|
585
|
+
return join(getAgentDir(), LEGACY_SETTINGS_FILE_NAME);
|
|
637
586
|
}
|
|
638
587
|
|
|
639
588
|
async function fileExists(filePath: string) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
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
|
-
|
|
612
|
+
return error instanceof Error && "code" in error;
|
|
664
613
|
}
|
|
665
614
|
|
|
666
615
|
function formatError(error: unknown) {
|
|
667
|
-
|
|
616
|
+
return error instanceof Error ? error.message : String(error);
|
|
668
617
|
}
|