dsh-desktop-kit 2.2.1 → 2.7.2

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 (125) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +113 -27
  3. package/client.js +3121 -32
  4. package/docs/AGENT-HARNESS.md +144 -0
  5. package/docs/ARCHITECTURE.md +38 -7
  6. package/docs/COMPATIBILITY.md +4 -2
  7. package/docs/DEVELOPMENT.md +56 -0
  8. package/docs/INSTALL.md +20 -11
  9. package/docs/LAPTOP-INSTALL.md +96 -0
  10. package/docs/ROADMAP-3.0.md +353 -0
  11. package/docs/ROADMAP.md +1 -1
  12. package/docs/SUBAGENTS.md +25 -0
  13. package/docs/TEST-0.2.2.md +5 -5
  14. package/docs/TEST-2.3.0.md +49 -0
  15. package/docs/TEST-2.7.2.md +46 -0
  16. package/docs/TOKENS.md +2 -2
  17. package/docs/TROUBLESHOOTING.md +27 -0
  18. package/docs/UI-QA.md +28 -0
  19. package/docs/images/peak-toast-peak.png +0 -0
  20. package/docs/images/peak-toast-soon.png +0 -0
  21. package/docs/preview/peak-toast.html +155 -0
  22. package/index.js +18 -1
  23. package/package.json +26 -6
  24. package/plugins/dsh-agent-extensions/README.md +99 -0
  25. package/plugins/dsh-agent-extensions/index.js +167 -0
  26. package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
  27. package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
  28. package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
  29. package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
  30. package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
  31. package/plugins/dsh-agent-extensions/lib/project.js +144 -0
  32. package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
  33. package/plugins/dsh-agent-extensions/package.json +9 -0
  34. package/plugins/dsh-browser-qa/README.md +221 -0
  35. package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
  36. package/plugins/dsh-browser-qa/index.js +165 -0
  37. package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
  38. package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
  39. package/plugins/dsh-browser-qa/lib/pages.js +552 -0
  40. package/plugins/dsh-browser-qa/lib/qa.js +827 -0
  41. package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
  42. package/plugins/dsh-browser-qa/package.json +31 -0
  43. package/plugins/dsh-checkpoints/README.md +22 -0
  44. package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
  45. package/plugins/dsh-checkpoints/index.js +80 -0
  46. package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
  47. package/plugins/dsh-checkpoints/package.json +25 -0
  48. package/plugins/dsh-github-review/README.md +131 -0
  49. package/plugins/dsh-github-review/cordis.patch.yml +3 -0
  50. package/plugins/dsh-github-review/index.js +536 -0
  51. package/plugins/dsh-github-review/lib/gh.js +494 -0
  52. package/plugins/dsh-github-review/lib/normalize.js +662 -0
  53. package/plugins/dsh-github-review/package.json +28 -0
  54. package/plugins/dsh-guardrails/README.md +264 -0
  55. package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
  56. package/plugins/dsh-guardrails/index.js +389 -0
  57. package/plugins/dsh-guardrails/lib/hooks.js +262 -0
  58. package/plugins/dsh-guardrails/lib/modes.js +77 -0
  59. package/plugins/dsh-guardrails/lib/policy.js +592 -0
  60. package/plugins/dsh-guardrails/lib/scan.js +451 -0
  61. package/plugins/dsh-guardrails/lib/secrets.js +274 -0
  62. package/plugins/dsh-guardrails/package.json +26 -0
  63. package/plugins/dsh-mcp-manager/README.md +78 -0
  64. package/plugins/dsh-mcp-manager/client.js +373 -0
  65. package/plugins/dsh-mcp-manager/index.js +159 -0
  66. package/plugins/dsh-mcp-manager/lib/api.js +250 -0
  67. package/plugins/dsh-mcp-manager/lib/client.js +219 -0
  68. package/plugins/dsh-mcp-manager/lib/config.js +217 -0
  69. package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
  70. package/plugins/dsh-mcp-manager/package.json +18 -0
  71. package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
  72. package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
  73. package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
  74. package/plugins/dsh-memory-bridge/README.md +161 -0
  75. package/plugins/dsh-memory-bridge/client.js +2264 -0
  76. package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
  77. package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
  78. package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
  79. package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
  80. package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
  81. package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
  82. package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
  83. package/plugins/dsh-memory-bridge/icon.svg +13 -0
  84. package/plugins/dsh-memory-bridge/index.js +378 -0
  85. package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
  86. package/plugins/dsh-memory-bridge/lib/github.js +473 -0
  87. package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
  88. package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
  89. package/plugins/dsh-memory-bridge/lib/state.js +565 -0
  90. package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
  91. package/plugins/dsh-memory-bridge/locale/en.json +6 -0
  92. package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
  93. package/plugins/dsh-memory-bridge/package.json +67 -0
  94. package/plugins/dsh-peak-guard/README.md +16 -3
  95. package/plugins/dsh-peak-guard/client.js +223 -8
  96. package/plugins/dsh-peak-guard/package.json +1 -1
  97. package/plugins/dsh-plan-mode/README.md +24 -0
  98. package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
  99. package/plugins/dsh-plan-mode/index.js +103 -0
  100. package/plugins/dsh-plan-mode/lib/plan.js +89 -0
  101. package/plugins/dsh-plan-mode/package.json +18 -0
  102. package/plugins/dsh-profile-transfer/client.js +277 -17
  103. package/plugins/dsh-profile-transfer/index.js +10 -0
  104. package/plugins/dsh-profile-transfer/package.json +3 -1
  105. package/plugins/dsh-profile-transfer/transfer.js +101 -2
  106. package/plugins/dsh-project-memory/index.js +92 -2
  107. package/plugins/dsh-project-memory/package.json +1 -1
  108. package/plugins/dsh-repo-map/README.md +26 -0
  109. package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
  110. package/plugins/dsh-repo-map/index.js +99 -0
  111. package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
  112. package/plugins/dsh-repo-map/package.json +25 -0
  113. package/plugins/dsh-skill-autopilot/index.js +135 -11
  114. package/skills/delivery-gate/SKILL.md +31 -0
  115. package/skills/git-and-ci/SKILL.md +5 -1
  116. package/skills/manifest.json +31 -6
  117. package/skills/requirements-and-planning/SKILL.md +3 -0
  118. package/skills/research-first/SKILL.md +35 -0
  119. package/skills/research-first/references/query-patterns.md +20 -0
  120. package/skills/subagent-playbook/SKILL.md +36 -0
  121. package/skills/subagent-playbook/references/prompts.md +39 -0
  122. package/skills/ui-craft/SKILL.md +43 -0
  123. package/skills/visual-qa/SKILL.md +29 -0
  124. package/skills/visual-qa/references/commands.md +32 -0
  125. package/skills-baseline.json +219 -0
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Chrome/Chromium detection and a short-lived headless session.
3
+ *
4
+ * Every run gets its own temp `--user-data-dir` and `--remote-debugging-port=0`
5
+ * (the port is read back from `DevToolsActivePort`), so it never touches the
6
+ * user's browsing profile. `close()` always kills the browser and removes the
7
+ * profile; an `exit` guard force-kills sessions if the host process goes away
8
+ * for any reason. Only Node built-ins are used.
9
+ */
10
+ import fs from "node:fs";
11
+ import fsp from "node:fs/promises";
12
+ import http from "node:http";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import { spawn, spawnSync } from "node:child_process";
16
+
17
+ import { Cdp } from "./cdp.js";
18
+
19
+ export const CHROME_ARGS = Object.freeze([
20
+ "--headless=new",
21
+ "--disable-gpu",
22
+ "--hide-scrollbars",
23
+ "--no-first-run",
24
+ "--no-default-browser-check",
25
+ "--disable-extensions",
26
+ "--disable-background-networking",
27
+ "--disable-background-timer-throttling",
28
+ "--disable-renderer-backgrounding",
29
+ "--disable-sync",
30
+ "--disable-default-apps",
31
+ "--disable-component-update",
32
+ "--mute-audio",
33
+ "--force-device-scale-factor=1",
34
+ "--allow-file-access-from-files",
35
+ "--remote-allow-origins=*",
36
+ ]);
37
+
38
+ const VERSION_CACHE_MS = 5 * 60_000;
39
+ const versionCache = new Map();
40
+ const activeSessions = new Set();
41
+ let exitGuardInstalled = false;
42
+
43
+ function sleep(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ function errorMessage(error) {
48
+ return error instanceof Error ? error.message : String(error);
49
+ }
50
+
51
+ /* ------------------------------------------------------------------ */
52
+ /* detection */
53
+ /* ------------------------------------------------------------------ */
54
+
55
+ /**
56
+ * Ordered candidate executables: CHROME_PATH, installed Chrome builds, then
57
+ * Edge (Chromium, same DevTools protocol) as a fallback.
58
+ */
59
+ export function candidatePaths(env = process.env, platform = process.platform) {
60
+ const candidates = [];
61
+ const add = (file, kind, source) => {
62
+ if (typeof file !== "string") return;
63
+ const cleaned = file.trim().replace(/^["']+|["']+$/gu, "");
64
+ if (cleaned.length === 0) return;
65
+ let resolved;
66
+ try {
67
+ resolved = path.resolve(cleaned);
68
+ } catch {
69
+ return;
70
+ }
71
+ if (candidates.some((candidate) => candidate.path.toLowerCase() === resolved.toLowerCase())) return;
72
+ candidates.push({ path: resolved, kind, source });
73
+ };
74
+
75
+ add(env.CHROME_PATH, "custom", "CHROME_PATH");
76
+ if (platform === "win32") {
77
+ const programFiles = env.ProgramFiles || "C:\\Program Files";
78
+ const programFilesX86 = env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
79
+ const localAppData = env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
80
+ add(path.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"), "chrome", "Program Files");
81
+ add(path.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"), "chrome", "Program Files (x86)");
82
+ add(path.join(localAppData, "Google", "Chrome", "Application", "chrome.exe"), "chrome", "LOCALAPPDATA");
83
+ add(path.join(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe"), "chrome", "Canary");
84
+ add(path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"), "edge", "Edge");
85
+ add(path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"), "edge", "Edge");
86
+ add(path.join(localAppData, "Microsoft", "Edge", "Application", "msedge.exe"), "edge", "Edge");
87
+ } else if (platform === "darwin") {
88
+ add("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "chrome", "Applications");
89
+ add(path.join(os.homedir(), "Applications", "Google Chrome.app", "Contents", "MacOS", "Google Chrome"), "chrome", "~/Applications");
90
+ add("/Applications/Chromium.app/Contents/MacOS/Chromium", "chromium", "Applications");
91
+ } else {
92
+ for (const file of [
93
+ "/usr/bin/google-chrome",
94
+ "/usr/bin/google-chrome-stable",
95
+ "/usr/bin/chromium",
96
+ "/usr/bin/chromium-browser",
97
+ "/snap/bin/chromium",
98
+ ]) {
99
+ add(file, "chrome", "system");
100
+ }
101
+ }
102
+ return candidates;
103
+ }
104
+
105
+ export function detectChrome(options = {}) {
106
+ const candidates = candidatePaths(options.env, options.platform);
107
+ for (const candidate of candidates) {
108
+ try {
109
+ if (fs.statSync(candidate.path).isFile()) {
110
+ return {
111
+ found: true,
112
+ path: candidate.path,
113
+ kind: candidate.kind,
114
+ source: candidate.source,
115
+ candidates: candidates.map((entry) => entry.path),
116
+ };
117
+ }
118
+ } catch {
119
+ /* not installed / not readable */
120
+ }
121
+ }
122
+ return { found: false, path: null, kind: null, source: null, candidates: candidates.map((entry) => entry.path) };
123
+ }
124
+
125
+ export function parseBrowserInfo(info = {}) {
126
+ const browser = typeof info?.Browser === "string" ? info.Browser : "";
127
+ const slash = browser.indexOf("/");
128
+ return {
129
+ browser,
130
+ product: slash >= 0 ? browser.slice(0, slash) : browser || null,
131
+ version: slash >= 0 ? browser.slice(slash + 1) : null,
132
+ };
133
+ }
134
+
135
+ /* ------------------------------------------------------------------ */
136
+ /* low-level helpers */
137
+ /* ------------------------------------------------------------------ */
138
+
139
+ function httpGetJson(port, pathname, timeoutMs = 2_000) {
140
+ return new Promise((resolve, reject) => {
141
+ const request = http.get(
142
+ { host: "127.0.0.1", port, path: pathname, timeout: timeoutMs, headers: { accept: "application/json" } },
143
+ (response) => {
144
+ const chunks = [];
145
+ let size = 0;
146
+ response.on("data", (chunk) => {
147
+ size += chunk.length;
148
+ if (size > 2_000_000) {
149
+ request.destroy(new Error(`DevTools response too large from ${pathname}`));
150
+ return;
151
+ }
152
+ chunks.push(chunk);
153
+ });
154
+ response.on("end", () => {
155
+ try {
156
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
157
+ } catch (error) {
158
+ reject(new Error(`invalid DevTools JSON from ${pathname}: ${errorMessage(error)}`));
159
+ }
160
+ });
161
+ response.on("error", reject);
162
+ },
163
+ );
164
+ request.on("timeout", () => request.destroy(new Error(`DevTools endpoint timeout: ${pathname}`)));
165
+ request.on("error", reject);
166
+ });
167
+ }
168
+
169
+ function spawnChrome(executablePath, profileDir) {
170
+ const state = { child: null, spawnError: null };
171
+ const child = spawn(
172
+ executablePath,
173
+ [...CHROME_ARGS, `--user-data-dir=${profileDir}`, "--remote-debugging-port=0", "about:blank"],
174
+ { stdio: "ignore", windowsHide: true },
175
+ );
176
+ state.child = child;
177
+ child.once("error", (error) => {
178
+ state.spawnError = new Error(`cannot start ${executablePath}: ${errorMessage(error)}`);
179
+ });
180
+ return state;
181
+ }
182
+
183
+ async function waitForDevToolsPort(profileDir, state, timeoutMs) {
184
+ const file = path.join(profileDir, "DevToolsActivePort");
185
+ const deadline = Date.now() + Math.max(1_000, timeoutMs);
186
+ while (Date.now() < deadline) {
187
+ if (state.spawnError) throw state.spawnError;
188
+ if (state.child && state.child.exitCode !== null) {
189
+ throw new Error(`Chrome exited before DevTools was ready (code ${state.child.exitCode})`);
190
+ }
191
+ try {
192
+ const text = await fsp.readFile(file, "utf8");
193
+ const lines = text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
194
+ const port = Number(lines[0]);
195
+ if (Number.isInteger(port) && port > 0) return { port, browserWebSocketPath: lines[1] ?? "" };
196
+ } catch {
197
+ /* not written yet */
198
+ }
199
+ await sleep(80);
200
+ }
201
+ throw new Error(`Chrome did not expose a DevTools port within ${Math.round(timeoutMs)} ms`);
202
+ }
203
+
204
+ async function waitForVersion(port, deadline) {
205
+ let lastError = null;
206
+ while (Date.now() < deadline) {
207
+ try {
208
+ const info = await httpGetJson(port, "/json/version", 1_500);
209
+ if (info && typeof info === "object") return info;
210
+ } catch (error) {
211
+ lastError = error;
212
+ }
213
+ await sleep(80);
214
+ }
215
+ throw new Error(`Chrome DevTools endpoint did not respond: ${errorMessage(lastError ?? new Error("timeout"))}`);
216
+ }
217
+
218
+ function forceKillSync(pid) {
219
+ if (!Number.isInteger(pid) || pid <= 0) return;
220
+ try {
221
+ if (process.platform === "win32") {
222
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true, timeout: 5_000 });
223
+ } else {
224
+ try {
225
+ process.kill(-pid, "SIGKILL");
226
+ } catch {
227
+ process.kill(pid, "SIGKILL");
228
+ }
229
+ }
230
+ } catch {
231
+ /* already gone */
232
+ }
233
+ }
234
+
235
+ async function terminateChild(state) {
236
+ const child = state?.child;
237
+ if (!child) return;
238
+ if (state.spawnError) {
239
+ try {
240
+ child.kill();
241
+ } catch {
242
+ /* never started */
243
+ }
244
+ return;
245
+ }
246
+ if (child.exitCode !== null || child.signalCode !== null) return;
247
+ await new Promise((resolve) => {
248
+ let finished = false;
249
+ let forceTimer = null;
250
+ let safetyTimer = null;
251
+ const finish = () => {
252
+ if (finished) return;
253
+ finished = true;
254
+ clearTimeout(forceTimer);
255
+ clearTimeout(safetyTimer);
256
+ resolve();
257
+ };
258
+ child.once("exit", finish);
259
+ try {
260
+ child.kill();
261
+ } catch {
262
+ /* ignore */
263
+ }
264
+ if (child.exitCode !== null) {
265
+ finish();
266
+ return;
267
+ }
268
+ forceTimer = setTimeout(() => forceKillSync(child.pid), 1_500);
269
+ safetyTimer = setTimeout(finish, 5_000);
270
+ if (typeof forceTimer.unref === "function") forceTimer.unref();
271
+ if (typeof safetyTimer.unref === "function") safetyTimer.unref();
272
+ });
273
+ }
274
+
275
+ async function removeProfileDir(dir) {
276
+ if (typeof dir !== "string" || dir.length === 0) return null;
277
+ try {
278
+ await fsp.rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 120 });
279
+ return null;
280
+ } catch (error) {
281
+ return `temp profile not fully removed: ${errorMessage(error)}`;
282
+ }
283
+ }
284
+
285
+ function installExitGuard() {
286
+ if (exitGuardInstalled) return;
287
+ exitGuardInstalled = true;
288
+ process.once("exit", () => {
289
+ for (const session of activeSessions) {
290
+ try {
291
+ const pid = session?.state?.child?.pid;
292
+ if (Number.isInteger(pid) && session.state.child.exitCode === null) forceKillSync(pid);
293
+ } catch {
294
+ /* ignore */
295
+ }
296
+ try {
297
+ if (typeof session?.profileDir === "string") fs.rmSync(session.profileDir, { recursive: true, force: true, maxRetries: 2 });
298
+ } catch {
299
+ /* best effort on process exit */
300
+ }
301
+ }
302
+ activeSessions.clear();
303
+ });
304
+ }
305
+
306
+ /* ------------------------------------------------------------------ */
307
+ /* one-shot version probe */
308
+ /* ------------------------------------------------------------------ */
309
+
310
+ /**
311
+ * Launch a throwaway headless browser, read `/json/version`, kill it. Used by
312
+ * the status action; the answer is cached for five minutes per executable.
313
+ */
314
+ export async function probeChrome(executablePath, options = {}) {
315
+ const pathKey = String(executablePath ?? "");
316
+ if (pathKey.length === 0) throw new Error("Chrome path is empty");
317
+ const cached = versionCache.get(pathKey);
318
+ if (cached && Date.now() - cached.at < VERSION_CACHE_MS) {
319
+ return { ...cached.info, found: true, path: pathKey, cached: true };
320
+ }
321
+ installExitGuard();
322
+ const profileDir = await fsp.mkdtemp(path.join(os.tmpdir(), "dsh-browser-qa-probe-"));
323
+ const state = spawnChrome(pathKey, profileDir);
324
+ const guardEntry = { state, profileDir };
325
+ activeSessions.add(guardEntry);
326
+ const timeoutMs = Math.max(2_000, Math.min(20_000, Number(options.timeoutMs) || 8_000));
327
+ const deadline = Date.now() + timeoutMs;
328
+ try {
329
+ const portInfo = await waitForDevToolsPort(profileDir, state, Math.max(1_000, deadline - Date.now()));
330
+ const info = await waitForVersion(portInfo.port, deadline);
331
+ const parsed = parseBrowserInfo(info);
332
+ const result = {
333
+ found: true,
334
+ path: pathKey,
335
+ kind: options.kind ?? "chrome",
336
+ product: parsed.product,
337
+ version: parsed.version,
338
+ browser: parsed.browser || null,
339
+ protocolVersion: info?.["Protocol-Version"] ?? null,
340
+ userAgent: info?.["User-Agent"] ?? null,
341
+ };
342
+ versionCache.set(pathKey, { at: Date.now(), info: result });
343
+ return { ...result, cached: false };
344
+ } finally {
345
+ activeSessions.delete(guardEntry);
346
+ await terminateChild(state);
347
+ await removeProfileDir(profileDir);
348
+ }
349
+ }
350
+
351
+ /* ------------------------------------------------------------------ */
352
+ /* long-lived (per tool call) session */
353
+ /* ------------------------------------------------------------------ */
354
+
355
+ export class ChromeSession {
356
+ static async start(options = {}) {
357
+ const executablePath = options.executablePath;
358
+ if (typeof executablePath !== "string" || executablePath.length === 0) {
359
+ throw new Error("Chrome executable path is empty");
360
+ }
361
+ installExitGuard();
362
+ const timeoutMs = Math.max(2_000, Math.min(25_000, Number(options.timeoutMs) || 15_000));
363
+ const profileDir = await fsp.mkdtemp(path.join(os.tmpdir(), "dsh-browser-qa-"));
364
+ const state = spawnChrome(executablePath, profileDir);
365
+ const session = new ChromeSession({ executablePath, kind: options.kind ?? "chrome", profileDir, state });
366
+ activeSessions.add(session);
367
+ try {
368
+ await session.boot(timeoutMs);
369
+ return session;
370
+ } catch (error) {
371
+ await session.close();
372
+ throw error;
373
+ }
374
+ }
375
+
376
+ constructor({ executablePath, kind, profileDir, state }) {
377
+ this.executablePath = executablePath;
378
+ this.kind = kind;
379
+ this.profileDir = profileDir;
380
+ this.state = state;
381
+ this.port = null;
382
+ this.browserWebSocketPath = "";
383
+ this.versionInfo = null;
384
+ this.product = null;
385
+ this.version = null;
386
+ this.protocolVersion = null;
387
+ this.cdp = null;
388
+ this.cleanupWarning = null;
389
+ this.closed = false;
390
+ }
391
+
392
+ chromeInfo() {
393
+ return {
394
+ path: this.executablePath,
395
+ kind: this.kind,
396
+ product: this.product,
397
+ version: this.version,
398
+ protocolVersion: this.protocolVersion,
399
+ };
400
+ }
401
+
402
+ async boot(timeoutMs) {
403
+ const deadline = Date.now() + timeoutMs;
404
+ const portInfo = await waitForDevToolsPort(this.profileDir, this.state, Math.max(1_000, deadline - Date.now()));
405
+ this.port = portInfo.port;
406
+ this.browserWebSocketPath = portInfo.browserWebSocketPath;
407
+ this.versionInfo = await waitForVersion(this.port, deadline);
408
+ const parsed = parseBrowserInfo(this.versionInfo);
409
+ this.product = parsed.product;
410
+ this.version = parsed.version;
411
+ this.protocolVersion = this.versionInfo?.["Protocol-Version"] ?? null;
412
+ versionCache.set(this.executablePath, {
413
+ at: Date.now(),
414
+ info: {
415
+ found: true,
416
+ path: this.executablePath,
417
+ kind: this.kind,
418
+ product: this.product,
419
+ version: this.version,
420
+ browser: parsed.browser || null,
421
+ protocolVersion: this.protocolVersion,
422
+ userAgent: this.versionInfo?.["User-Agent"] ?? null,
423
+ },
424
+ });
425
+ const target = await this.pageTarget(deadline);
426
+ const connectTimeout = Math.max(1_000, Math.min(10_000, deadline - Date.now()));
427
+ this.cdp = await new Cdp(target.webSocketDebuggerUrl).connect(connectTimeout);
428
+ return this;
429
+ }
430
+
431
+ async pageTarget(deadline) {
432
+ let lastError = null;
433
+ while (Date.now() < deadline) {
434
+ if (this.state.spawnError) throw this.state.spawnError;
435
+ try {
436
+ const targets = await httpGetJson(this.port, "/json/list", 1_500);
437
+ if (Array.isArray(targets)) {
438
+ const page = targets.find(
439
+ (target) => target && target.type === "page" && typeof target.webSocketDebuggerUrl === "string" && target.webSocketDebuggerUrl.length > 0,
440
+ );
441
+ if (page) return page;
442
+ }
443
+ } catch (error) {
444
+ lastError = error;
445
+ }
446
+ await sleep(80);
447
+ }
448
+ throw new Error(`Chrome did not expose a debuggable page target: ${errorMessage(lastError ?? new Error("timeout"))}`);
449
+ }
450
+
451
+ /** Kill the browser and delete the temp profile. Safe to call twice. */
452
+ async close() {
453
+ if (this.closed) return;
454
+ this.closed = true;
455
+ try {
456
+ this.cdp?.close();
457
+ } catch {
458
+ /* ignore */
459
+ }
460
+ this.cdp = null;
461
+ activeSessions.delete(this);
462
+ await terminateChild(this.state);
463
+ const warning = await removeProfileDir(this.profileDir);
464
+ this.cleanupWarning = warning;
465
+ }
466
+ }