@narumitw/pi-chrome-devtools 0.49.3 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -40
- package/package.json +7 -4
- package/src/browser-manager.ts +366 -53
- package/src/chrome-devtools.ts +46 -81
- package/src/menu.ts +483 -0
- package/src/runtime.ts +27 -0
- package/src/settings.ts +337 -82
- package/src/tool-selector.ts +145 -130
package/src/runtime.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
import type { EffectiveBrowserSettings } from "./settings.js";
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_HOST = "127.0.0.1";
|
|
4
5
|
export const DEFAULT_PORT = 9222;
|
|
@@ -26,6 +27,12 @@ export interface ChromeDevToolsState {
|
|
|
26
27
|
portConfigured: boolean;
|
|
27
28
|
autoLaunchEnabled: boolean;
|
|
28
29
|
browserExecutable?: string;
|
|
30
|
+
extensionPaths: string[];
|
|
31
|
+
browserExecutableSource: EffectiveBrowserSettings["executablePathSource"];
|
|
32
|
+
extensionPathsSource: EffectiveBrowserSettings["extensionPathsSource"];
|
|
33
|
+
settingsFilePath?: string;
|
|
34
|
+
projectSettingsFilePath?: string;
|
|
35
|
+
projectSettingsTrusted: boolean;
|
|
29
36
|
activePageId?: string;
|
|
30
37
|
managedBrowser?: ManagedBrowser;
|
|
31
38
|
launchPromise?: Promise<void>;
|
|
@@ -42,6 +49,8 @@ export interface ManagedBrowser {
|
|
|
42
49
|
port?: number;
|
|
43
50
|
exited: boolean;
|
|
44
51
|
ready: boolean;
|
|
52
|
+
ownerGeneration: number;
|
|
53
|
+
cleanupPromise?: Promise<void>;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
export interface BrowserLaunchAttempt {
|
|
@@ -83,7 +92,25 @@ export const state: ChromeDevToolsState = {
|
|
|
83
92
|
portConfigured: configuredPortOverride !== undefined,
|
|
84
93
|
autoLaunchEnabled: process.env.PI_CHROME_DEVTOOLS_AUTO_LAUNCH !== "0",
|
|
85
94
|
browserExecutable: process.env.PI_CHROME_DEVTOOLS_BROWSER,
|
|
95
|
+
extensionPaths: [],
|
|
96
|
+
browserExecutableSource: process.env.PI_CHROME_DEVTOOLS_BROWSER ? "environment" : "default",
|
|
97
|
+
extensionPathsSource: "default",
|
|
98
|
+
projectSettingsTrusted: false,
|
|
86
99
|
shuttingDown: false,
|
|
87
100
|
sessionGeneration: 0,
|
|
88
101
|
sessionController: new AbortController(),
|
|
89
102
|
};
|
|
103
|
+
|
|
104
|
+
export function applyRuntimeBrowserSettings(
|
|
105
|
+
browser: EffectiveBrowserSettings,
|
|
106
|
+
paths: { user: string; project?: string },
|
|
107
|
+
projectTrusted: boolean,
|
|
108
|
+
) {
|
|
109
|
+
state.browserExecutable = browser.executablePath;
|
|
110
|
+
state.extensionPaths = [...browser.extensionPaths];
|
|
111
|
+
state.browserExecutableSource = browser.executablePathSource;
|
|
112
|
+
state.extensionPathsSource = browser.extensionPathsSource;
|
|
113
|
+
state.settingsFilePath = paths.user;
|
|
114
|
+
state.projectSettingsFilePath = paths.project;
|
|
115
|
+
state.projectSettingsTrusted = projectTrusted;
|
|
116
|
+
}
|
package/src/settings.ts
CHANGED
|
@@ -1,135 +1,367 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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";
|
|
14
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
16
|
import { CHROME_DEVTOOLS_TOOL_NAMES, type ChromeDevToolsToolName } from "./tool-names.js";
|
|
7
17
|
|
|
8
18
|
const NEW_SETTINGS_FILE_NAME = "pi-chrome-devtools.json";
|
|
9
19
|
const LEGACY_SETTINGS_FILE_NAME = "pi-chrome-devtools-settings.json";
|
|
20
|
+
|
|
10
21
|
export interface ChromeDevToolsSettings {
|
|
11
22
|
tools: ChromeDevToolsToolName[];
|
|
12
23
|
updatedAt: number;
|
|
13
24
|
}
|
|
14
25
|
|
|
26
|
+
export type BrowserSettingsSource = "default" | "environment" | "project" | "user";
|
|
27
|
+
|
|
28
|
+
export interface EffectiveBrowserSettings {
|
|
29
|
+
executablePath?: string;
|
|
30
|
+
extensionPaths: string[];
|
|
31
|
+
executablePathSource: BrowserSettingsSource;
|
|
32
|
+
extensionPathsSource: BrowserSettingsSource;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ResolvedChromeDevToolsSettings {
|
|
36
|
+
tools?: ChromeDevToolsToolName[];
|
|
37
|
+
updatedAt?: number;
|
|
38
|
+
browser: EffectiveBrowserSettings;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SettingsLoadOptions {
|
|
42
|
+
cwd?: string;
|
|
43
|
+
projectTrusted?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SettingsLoadBase {
|
|
47
|
+
effectiveBrowser: EffectiveBrowserSettings;
|
|
48
|
+
paths: { user: string; project?: string };
|
|
49
|
+
warnings: string[];
|
|
50
|
+
notice?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type SettingsLoadResult =
|
|
54
|
+
| (SettingsLoadBase & { kind: "missing"; settings?: undefined })
|
|
55
|
+
| (SettingsLoadBase & { kind: "invalid"; reason: string; settings?: undefined })
|
|
56
|
+
| (SettingsLoadBase & { kind: "loaded"; settings: ResolvedChromeDevToolsSettings });
|
|
57
|
+
|
|
15
58
|
export interface SettingsFileOperations {
|
|
16
59
|
write(path: string, data: string): Promise<void>;
|
|
17
60
|
rename(source: string, destination: string): Promise<void>;
|
|
18
61
|
}
|
|
19
62
|
|
|
63
|
+
interface NormalizedBrowserSection {
|
|
64
|
+
executablePath?: string;
|
|
65
|
+
extensionPaths?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface NormalizedSettingsDocument {
|
|
69
|
+
tools?: ChromeDevToolsToolName[];
|
|
70
|
+
updatedAt?: number;
|
|
71
|
+
browser?: NormalizedBrowserSection;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface SettingsDocumentResult {
|
|
75
|
+
kind: "missing" | "invalid" | "valid";
|
|
76
|
+
reason?: string;
|
|
77
|
+
document?: Record<string, unknown>;
|
|
78
|
+
normalized?: NormalizedSettingsDocument;
|
|
79
|
+
warnings: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
20
82
|
const DEFAULT_FILE_OPERATIONS: SettingsFileOperations = {
|
|
21
83
|
write: (path, data) => writeFile(path, data, "utf8").then(() => undefined),
|
|
22
84
|
rename,
|
|
23
85
|
};
|
|
24
86
|
|
|
25
|
-
|
|
26
|
-
| { kind: "missing"; notice?: string }
|
|
27
|
-
| { kind: "invalid"; reason: string; notice?: string }
|
|
28
|
-
| { kind: "loaded"; settings: ChromeDevToolsSettings; notice?: string };
|
|
87
|
+
let settingsSaveQueue = Promise.resolve();
|
|
29
88
|
|
|
30
|
-
export async function loadSettings(): Promise<SettingsLoadResult> {
|
|
89
|
+
export async function loadSettings(options: SettingsLoadOptions = {}): Promise<SettingsLoadResult> {
|
|
31
90
|
await settingsSaveQueue;
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
91
|
+
const userPath = settingsFilePath();
|
|
92
|
+
const projectPath = options.cwd ? projectSettingsFilePath(options.cwd) : undefined;
|
|
93
|
+
const paths = { user: userPath, ...(projectPath ? { project: projectPath } : {}) };
|
|
94
|
+
const warnings: string[] = [];
|
|
95
|
+
|
|
96
|
+
let user = await readSettingsDocument(userPath, "user");
|
|
97
|
+
const legacyExists = await fileExists(legacySettingsFilePath());
|
|
98
|
+
if (user.kind !== "missing" && legacyExists) {
|
|
99
|
+
warnings.push(
|
|
100
|
+
`Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${userPath} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
|
|
101
|
+
);
|
|
36
102
|
}
|
|
103
|
+
if (user.kind === "missing") {
|
|
104
|
+
const legacy = await readSettingsDocument(legacySettingsFilePath(), "user");
|
|
105
|
+
const concurrentlyCreated = await readSettingsDocument(userPath, "user");
|
|
106
|
+
if (concurrentlyCreated.kind !== "missing") {
|
|
107
|
+
user = concurrentlyCreated;
|
|
108
|
+
if (legacy.kind !== "missing") {
|
|
109
|
+
warnings.push(
|
|
110
|
+
`Chrome DevTools legacy settings ignored: ${legacySettingsFilePath()} exists, but ${userPath} takes precedence. Delete ${LEGACY_SETTINGS_FILE_NAME} after confirming your settings.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
} else if (legacy.kind !== "missing") {
|
|
114
|
+
user = legacy;
|
|
115
|
+
if (legacy.kind === "valid") {
|
|
116
|
+
warnings.push(
|
|
117
|
+
`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.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
warnings.push(...user.warnings);
|
|
37
123
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return withLegacyIgnoredNotice(concurrentlyCreatedSettings);
|
|
124
|
+
let project: SettingsDocumentResult = { kind: "missing", warnings: [] };
|
|
125
|
+
if (projectPath && options.projectTrusted) {
|
|
126
|
+
project = await readSettingsDocument(projectPath, "project", options.cwd);
|
|
127
|
+
warnings.push(...project.warnings);
|
|
43
128
|
}
|
|
44
|
-
if (legacySettings.kind === "missing") return { kind: "missing" };
|
|
45
|
-
if (legacySettings.kind === "invalid") return legacySettings;
|
|
46
129
|
|
|
130
|
+
const effectiveBrowser = resolveEffectiveBrowser(
|
|
131
|
+
user.normalized?.browser,
|
|
132
|
+
project.normalized?.browser,
|
|
133
|
+
);
|
|
134
|
+
const settings = resolveSettings(user.normalized, project.normalized, effectiveBrowser);
|
|
135
|
+
const recognized =
|
|
136
|
+
settings.tools !== undefined ||
|
|
137
|
+
user.normalized?.browser !== undefined ||
|
|
138
|
+
project.normalized?.browser !== undefined;
|
|
139
|
+
const invalidReasons = [user, project]
|
|
140
|
+
.filter((result) => result.kind === "invalid")
|
|
141
|
+
.map((result) => result.reason)
|
|
142
|
+
.filter((reason): reason is string => Boolean(reason));
|
|
143
|
+
const base = {
|
|
144
|
+
effectiveBrowser,
|
|
145
|
+
paths,
|
|
146
|
+
warnings,
|
|
147
|
+
...(warnings.length > 0 ? { notice: warnings.join("\n") } : {}),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (recognized) return { ...base, kind: "loaded", settings };
|
|
151
|
+
if (invalidReasons.length > 0) {
|
|
152
|
+
return { ...base, kind: "invalid", reason: invalidReasons.join("; ") };
|
|
153
|
+
}
|
|
154
|
+
return { ...base, kind: "missing" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveSettings(
|
|
158
|
+
user: NormalizedSettingsDocument | undefined,
|
|
159
|
+
_project: NormalizedSettingsDocument | undefined,
|
|
160
|
+
browser: EffectiveBrowserSettings,
|
|
161
|
+
): ResolvedChromeDevToolsSettings {
|
|
47
162
|
return {
|
|
48
|
-
...
|
|
49
|
-
|
|
163
|
+
...(user?.tools ? { tools: user.tools, updatedAt: user.updatedAt } : {}),
|
|
164
|
+
browser,
|
|
50
165
|
};
|
|
51
166
|
}
|
|
52
167
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
168
|
+
function resolveEffectiveBrowser(
|
|
169
|
+
user: NormalizedBrowserSection | undefined,
|
|
170
|
+
project: NormalizedBrowserSection | undefined,
|
|
171
|
+
): EffectiveBrowserSettings {
|
|
172
|
+
const environmentExecutable = process.env.PI_CHROME_DEVTOOLS_BROWSER;
|
|
173
|
+
const executablePath = environmentExecutable || user?.executablePath;
|
|
174
|
+
const extensionPaths = project?.extensionPaths ?? user?.extensionPaths ?? [];
|
|
175
|
+
return {
|
|
176
|
+
...(executablePath ? { executablePath } : {}),
|
|
177
|
+
extensionPaths: [...extensionPaths],
|
|
178
|
+
executablePathSource: environmentExecutable
|
|
179
|
+
? "environment"
|
|
180
|
+
: user?.executablePath
|
|
181
|
+
? "user"
|
|
182
|
+
: "default",
|
|
183
|
+
extensionPathsSource: project?.extensionPaths
|
|
184
|
+
? "project"
|
|
185
|
+
: user?.extensionPaths
|
|
186
|
+
? "user"
|
|
187
|
+
: "default",
|
|
188
|
+
};
|
|
56
189
|
}
|
|
57
190
|
|
|
58
|
-
async function readSettingsDocument(
|
|
191
|
+
async function readSettingsDocument(
|
|
192
|
+
filePath: string,
|
|
193
|
+
scope: "project" | "user",
|
|
194
|
+
cwd?: string,
|
|
195
|
+
): Promise<SettingsDocumentResult> {
|
|
59
196
|
let text: string;
|
|
60
197
|
try {
|
|
61
198
|
text = await readFile(filePath, "utf8");
|
|
62
199
|
} catch (error) {
|
|
63
|
-
if (isNodeError(error) && error.code === "ENOENT") return {
|
|
64
|
-
|
|
200
|
+
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing", warnings: [] };
|
|
201
|
+
const reason = `${filePath}: ${formatError(error)}`;
|
|
202
|
+
return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
|
|
65
203
|
}
|
|
66
204
|
|
|
205
|
+
let parsed: unknown;
|
|
67
206
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
207
|
+
parsed = JSON.parse(text) as unknown;
|
|
208
|
+
} catch {
|
|
209
|
+
const reason = `${filePath}: invalid JSON`;
|
|
210
|
+
return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
|
|
211
|
+
}
|
|
212
|
+
if (!isRecord(parsed)) {
|
|
213
|
+
const reason = `${filePath}: expected a JSON object`;
|
|
214
|
+
return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const scopeWarnings = projectExecutableWarnings(parsed, scope, filePath);
|
|
218
|
+
try {
|
|
219
|
+
const normalized = await normalizeSettingsDocument(parsed, scope, filePath, cwd);
|
|
220
|
+
return { kind: "valid", document: { ...parsed }, normalized, warnings: scopeWarnings };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const reason = `${filePath}: ${formatError(error)}`;
|
|
76
223
|
return {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
},
|
|
224
|
+
kind: "invalid",
|
|
225
|
+
reason,
|
|
226
|
+
warnings: [...scopeWarnings, `Chrome DevTools settings ignored: ${reason}`],
|
|
81
227
|
};
|
|
82
|
-
} catch (error) {
|
|
83
|
-
return { result: { kind: "invalid", reason: `${filePath}: ${formatError(error)}` } };
|
|
84
228
|
}
|
|
85
229
|
}
|
|
86
230
|
|
|
87
|
-
|
|
88
|
-
|
|
231
|
+
function projectExecutableWarnings(
|
|
232
|
+
document: Record<string, unknown>,
|
|
233
|
+
scope: "project" | "user",
|
|
234
|
+
filePath: string,
|
|
235
|
+
) {
|
|
236
|
+
if (
|
|
237
|
+
scope !== "project" ||
|
|
238
|
+
!isRecord(document.browser) ||
|
|
239
|
+
document.browser.executablePath === undefined
|
|
240
|
+
) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
return [
|
|
244
|
+
`Chrome DevTools project browser.executablePath ignored in ${filePath}; configure the machine-owned executable in ${settingsFilePath()}.`,
|
|
245
|
+
];
|
|
89
246
|
}
|
|
90
247
|
|
|
91
|
-
async function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
248
|
+
async function normalizeSettingsDocument(
|
|
249
|
+
document: Record<string, unknown>,
|
|
250
|
+
scope: "project" | "user",
|
|
251
|
+
filePath: string,
|
|
252
|
+
cwd?: string,
|
|
253
|
+
): Promise<NormalizedSettingsDocument> {
|
|
254
|
+
const normalized: NormalizedSettingsDocument = {};
|
|
255
|
+
|
|
256
|
+
if (scope === "user" && (document.tools !== undefined || document.updatedAt !== undefined)) {
|
|
257
|
+
const toolSettings = normalizeChromeDevtoolsSettings(document);
|
|
258
|
+
if (!toolSettings) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
"expected tools to be an array of Chrome DevTools tool names with numeric updatedAt",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
normalized.tools = toolSettings.tools;
|
|
264
|
+
normalized.updatedAt = toolSettings.updatedAt;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (document.browser !== undefined) {
|
|
268
|
+
if (!isRecord(document.browser)) throw new Error("expected browser to be an object");
|
|
269
|
+
normalized.browser = await normalizeBrowserSection(document.browser, scope, filePath, cwd);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return normalized;
|
|
97
273
|
}
|
|
98
274
|
|
|
99
|
-
async function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
275
|
+
async function normalizeBrowserSection(
|
|
276
|
+
browser: Record<string, unknown>,
|
|
277
|
+
scope: "project" | "user",
|
|
278
|
+
_filePath: string,
|
|
279
|
+
cwd?: string,
|
|
280
|
+
): Promise<NormalizedBrowserSection> {
|
|
281
|
+
const normalized: NormalizedBrowserSection = {};
|
|
282
|
+
if (scope === "user" && browser.executablePath !== undefined) {
|
|
283
|
+
if (typeof browser.executablePath !== "string" || browser.executablePath.length === 0) {
|
|
284
|
+
throw new Error("expected browser.executablePath to be a non-empty absolute path");
|
|
285
|
+
}
|
|
286
|
+
if (!isAbsolute(browser.executablePath)) {
|
|
287
|
+
throw new Error("browser.executablePath in user settings must be absolute");
|
|
288
|
+
}
|
|
289
|
+
normalized.executablePath = resolve(browser.executablePath);
|
|
105
290
|
}
|
|
291
|
+
|
|
292
|
+
if (browser.extensionPaths !== undefined) {
|
|
293
|
+
if (
|
|
294
|
+
!Array.isArray(browser.extensionPaths) ||
|
|
295
|
+
!browser.extensionPaths.every((entry) => typeof entry === "string" && entry.length > 0)
|
|
296
|
+
) {
|
|
297
|
+
throw new Error(`expected ${scope} browser.extensionPaths to be an array of non-empty paths`);
|
|
298
|
+
}
|
|
299
|
+
const resolvedPaths: string[] = [];
|
|
300
|
+
for (const configuredPath of browser.extensionPaths as string[]) {
|
|
301
|
+
if (scope === "user" && !isAbsolute(configuredPath)) {
|
|
302
|
+
throw new Error("browser.extensionPaths in user settings must contain only absolute paths");
|
|
303
|
+
}
|
|
304
|
+
const absolutePath = isAbsolute(configuredPath)
|
|
305
|
+
? resolve(configuredPath)
|
|
306
|
+
: resolve(cwd ?? process.cwd(), configuredPath);
|
|
307
|
+
resolvedPaths.push(await validateUnpackedExtensionPath(absolutePath));
|
|
308
|
+
}
|
|
309
|
+
normalized.extensionPaths = orderedUnique(resolvedPaths);
|
|
310
|
+
}
|
|
311
|
+
return normalized;
|
|
106
312
|
}
|
|
107
313
|
|
|
108
|
-
async function
|
|
314
|
+
async function validateUnpackedExtensionPath(extensionPath: string) {
|
|
315
|
+
let canonicalPath: string;
|
|
109
316
|
try {
|
|
110
|
-
await
|
|
111
|
-
|
|
317
|
+
canonicalPath = await realpath(extensionPath);
|
|
318
|
+
const extensionStat = await stat(canonicalPath);
|
|
319
|
+
if (!extensionStat.isDirectory()) throw new Error("not a directory");
|
|
112
320
|
} catch (error) {
|
|
113
|
-
|
|
114
|
-
|
|
321
|
+
throw new Error(`unpacked extension path ${extensionPath} is invalid: ${formatError(error)}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const manifestPath = join(canonicalPath, "manifest.json");
|
|
325
|
+
let manifest: unknown;
|
|
326
|
+
try {
|
|
327
|
+
const manifestStat = await lstat(manifestPath);
|
|
328
|
+
if (!manifestStat.isFile()) throw new Error("not a regular file");
|
|
329
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8")) as unknown;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`unpacked extension ${canonicalPath} has invalid manifest.json: ${formatError(error)}`,
|
|
333
|
+
);
|
|
115
334
|
}
|
|
335
|
+
if (canonicalPath.includes(",")) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`unpacked extension path ${canonicalPath} cannot contain a comma because Chrome separates multiple --load-extension paths with commas`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (
|
|
341
|
+
!isRecord(manifest) ||
|
|
342
|
+
![2, 3].includes(manifest.manifest_version as number) ||
|
|
343
|
+
typeof manifest.name !== "string" ||
|
|
344
|
+
typeof manifest.version !== "string"
|
|
345
|
+
) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`unpacked extension ${canonicalPath} has invalid manifest.json: expected manifest_version, name, and version`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return canonicalPath;
|
|
116
351
|
}
|
|
117
352
|
|
|
118
353
|
export function normalizeChromeDevtoolsSettings(
|
|
119
354
|
value: unknown,
|
|
120
355
|
): ChromeDevToolsSettings | undefined {
|
|
121
|
-
if (!value
|
|
122
|
-
|
|
123
|
-
if (typeof settings.updatedAt !== "number") return undefined;
|
|
356
|
+
if (!isRecord(value)) return undefined;
|
|
357
|
+
if (typeof value.updatedAt !== "number" || !Number.isFinite(value.updatedAt)) return undefined;
|
|
124
358
|
|
|
125
|
-
if (
|
|
126
|
-
return { tools: [...CHROME_DEVTOOLS_TOOL_NAMES], updatedAt:
|
|
359
|
+
if (value.tools === "enabled") {
|
|
360
|
+
return { tools: [...CHROME_DEVTOOLS_TOOL_NAMES], updatedAt: value.updatedAt };
|
|
127
361
|
}
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!settings.tools.every(isChromeDevtoolsToolName)) return undefined;
|
|
132
|
-
return { tools: orderedUniqueChromeDevtoolsTools(settings.tools), updatedAt: settings.updatedAt };
|
|
362
|
+
if (value.tools === "disabled") return { tools: [], updatedAt: value.updatedAt };
|
|
363
|
+
if (!Array.isArray(value.tools) || !value.tools.every(isChromeDevtoolsToolName)) return undefined;
|
|
364
|
+
return { tools: orderedUniqueChromeDevtoolsTools(value.tools), updatedAt: value.updatedAt };
|
|
133
365
|
}
|
|
134
366
|
|
|
135
367
|
function isChromeDevtoolsToolName(value: unknown): value is ChromeDevToolsToolName {
|
|
@@ -141,7 +373,9 @@ function orderedUniqueChromeDevtoolsTools(tools: readonly ChromeDevToolsToolName
|
|
|
141
373
|
return CHROME_DEVTOOLS_TOOL_NAMES.filter((toolName) => selectedTools.has(toolName));
|
|
142
374
|
}
|
|
143
375
|
|
|
144
|
-
|
|
376
|
+
function orderedUnique(values: readonly string[]) {
|
|
377
|
+
return [...new Set(values)];
|
|
378
|
+
}
|
|
145
379
|
|
|
146
380
|
export function saveSettings(
|
|
147
381
|
settings: ChromeDevToolsSettings,
|
|
@@ -157,13 +391,11 @@ async function saveSettingsNow(
|
|
|
157
391
|
operations: Partial<SettingsFileOperations>,
|
|
158
392
|
): Promise<void> {
|
|
159
393
|
const filePath = settingsFilePath();
|
|
160
|
-
let current = await readSettingsDocument(filePath);
|
|
161
|
-
const replaceCanonical = current.
|
|
162
|
-
if (!replaceCanonical) current = await readSettingsDocument(legacySettingsFilePath());
|
|
163
|
-
if (current.
|
|
164
|
-
throw new Error(
|
|
165
|
-
`Cannot save Chrome DevTools settings until you repair ${current.result.reason}`,
|
|
166
|
-
);
|
|
394
|
+
let current = await readSettingsDocument(filePath, "user");
|
|
395
|
+
const replaceCanonical = current.kind !== "missing";
|
|
396
|
+
if (!replaceCanonical) current = await readSettingsDocument(legacySettingsFilePath(), "user");
|
|
397
|
+
if (current.kind === "invalid") {
|
|
398
|
+
throw new Error(`Cannot save Chrome DevTools settings until you repair ${current.reason}`);
|
|
167
399
|
}
|
|
168
400
|
const nextDocument = {
|
|
169
401
|
...(current.document ?? {}),
|
|
@@ -190,15 +422,38 @@ async function saveSettingsNow(
|
|
|
190
422
|
}
|
|
191
423
|
|
|
192
424
|
export function settingsFilePath() {
|
|
193
|
-
return join(
|
|
425
|
+
return join(getAgentDir(), NEW_SETTINGS_FILE_NAME);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function projectSettingsFilePath(cwd: string) {
|
|
429
|
+
return join(cwd, CONFIG_DIR_NAME, NEW_SETTINGS_FILE_NAME);
|
|
194
430
|
}
|
|
195
431
|
|
|
196
432
|
function legacySettingsFilePath() {
|
|
197
|
-
return join(
|
|
433
|
+
return join(getAgentDir(), LEGACY_SETTINGS_FILE_NAME);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function fileExists(filePath: string) {
|
|
437
|
+
try {
|
|
438
|
+
await access(filePath, constants.F_OK);
|
|
439
|
+
return true;
|
|
440
|
+
} catch {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function pathEntryExists(filePath: string) {
|
|
446
|
+
try {
|
|
447
|
+
await lstat(filePath);
|
|
448
|
+
return true;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
198
453
|
}
|
|
199
454
|
|
|
200
|
-
function
|
|
201
|
-
return
|
|
455
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
456
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
202
457
|
}
|
|
203
458
|
|
|
204
459
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|