@comecaramelos/dsh-chrome-mcp 0.0.1 → 1.0.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.
Files changed (171) hide show
  1. package/README.md +22 -78
  2. package/assets/dsh-chrome-mcp-social.png +0 -0
  3. package/assets/icon.svg +71 -0
  4. package/assets/prelaunch-windows-chrome.html +277 -0
  5. package/lib/client.js +1433 -568
  6. package/lib/client.js.map +7 -0
  7. package/lib/host/apply.d.ts +3 -0
  8. package/lib/host/apply.d.ts.map +1 -0
  9. package/lib/host/apply.js +31 -0
  10. package/lib/host/apply.js.map +1 -0
  11. package/lib/host/bridge.d.ts +65 -0
  12. package/lib/host/bridge.d.ts.map +1 -0
  13. package/lib/host/bridge.js +176 -0
  14. package/lib/host/bridge.js.map +1 -0
  15. package/lib/host/catalog.d.ts +31 -0
  16. package/lib/host/catalog.d.ts.map +1 -0
  17. package/lib/host/catalog.js +92 -0
  18. package/lib/host/catalog.js.map +1 -0
  19. package/lib/host/constants.d.ts +229 -0
  20. package/lib/host/constants.d.ts.map +1 -0
  21. package/lib/host/constants.js +242 -0
  22. package/lib/host/constants.js.map +1 -0
  23. package/lib/host/controller/bridge-run.d.ts +38 -0
  24. package/lib/host/controller/bridge-run.d.ts.map +1 -0
  25. package/lib/host/controller/bridge-run.js +69 -0
  26. package/lib/host/controller/bridge-run.js.map +1 -0
  27. package/lib/host/controller/captures.d.ts +22 -0
  28. package/lib/host/controller/captures.d.ts.map +1 -0
  29. package/lib/host/controller/captures.js +60 -0
  30. package/lib/host/controller/captures.js.map +1 -0
  31. package/lib/host/controller/commits.d.ts +4 -0
  32. package/lib/host/controller/commits.d.ts.map +1 -0
  33. package/lib/host/controller/commits.js +75 -0
  34. package/lib/host/controller/commits.js.map +1 -0
  35. package/lib/host/controller/controller.d.ts +40 -0
  36. package/lib/host/controller/controller.d.ts.map +1 -0
  37. package/lib/host/controller/controller.js +167 -0
  38. package/lib/host/controller/controller.js.map +1 -0
  39. package/lib/host/controller/index.d.ts +26 -0
  40. package/lib/host/controller/index.d.ts.map +1 -0
  41. package/lib/host/controller/index.js +25 -0
  42. package/lib/host/controller/index.js.map +1 -0
  43. package/lib/host/controller/notes.d.ts +26 -0
  44. package/lib/host/controller/notes.d.ts.map +1 -0
  45. package/lib/host/controller/notes.js +49 -0
  46. package/lib/host/controller/notes.js.map +1 -0
  47. package/lib/host/controller/persist.d.ts +9 -0
  48. package/lib/host/controller/persist.d.ts.map +1 -0
  49. package/lib/host/controller/persist.js +51 -0
  50. package/lib/host/controller/persist.js.map +1 -0
  51. package/lib/host/controller/runs.d.ts +56 -0
  52. package/lib/host/controller/runs.d.ts.map +1 -0
  53. package/lib/host/controller/runs.js +211 -0
  54. package/lib/host/controller/runs.js.map +1 -0
  55. package/lib/host/controller/state.d.ts +70 -0
  56. package/lib/host/controller/state.d.ts.map +1 -0
  57. package/lib/host/controller/state.js +2 -0
  58. package/lib/host/controller/state.js.map +1 -0
  59. package/lib/host/controller/windows-run.d.ts +9 -0
  60. package/lib/host/controller/windows-run.d.ts.map +1 -0
  61. package/lib/host/controller/windows-run.js +78 -0
  62. package/lib/host/controller/windows-run.js.map +1 -0
  63. package/lib/host/discovery/candidates.d.ts +37 -0
  64. package/lib/host/discovery/candidates.d.ts.map +1 -0
  65. package/lib/host/discovery/candidates.js +56 -0
  66. package/lib/host/discovery/candidates.js.map +1 -0
  67. package/lib/host/discovery/index.d.ts +18 -0
  68. package/lib/host/discovery/index.d.ts.map +1 -0
  69. package/lib/host/discovery/index.js +18 -0
  70. package/lib/host/discovery/index.js.map +1 -0
  71. package/lib/host/discovery/probe.d.ts +18 -0
  72. package/lib/host/discovery/probe.d.ts.map +1 -0
  73. package/lib/host/discovery/probe.js +97 -0
  74. package/lib/host/discovery/probe.js.map +1 -0
  75. package/lib/host/discovery/scan.d.ts +67 -0
  76. package/lib/host/discovery/scan.d.ts.map +1 -0
  77. package/lib/host/discovery/scan.js +169 -0
  78. package/lib/host/discovery/scan.js.map +1 -0
  79. package/lib/host/flags.d.ts +42 -0
  80. package/lib/host/flags.d.ts.map +1 -0
  81. package/lib/host/flags.js +118 -0
  82. package/lib/host/flags.js.map +1 -0
  83. package/lib/host/platform.d.ts +38 -0
  84. package/lib/host/platform.d.ts.map +1 -0
  85. package/lib/host/platform.js +64 -0
  86. package/lib/host/platform.js.map +1 -0
  87. package/lib/host/plugin-meta.d.ts +12 -0
  88. package/lib/host/plugin-meta.d.ts.map +1 -0
  89. package/lib/host/plugin-meta.js +12 -0
  90. package/lib/host/plugin-meta.js.map +1 -0
  91. package/lib/host/schema.d.ts +307 -0
  92. package/lib/host/schema.d.ts.map +1 -0
  93. package/lib/host/schema.js +171 -0
  94. package/lib/host/schema.js.map +1 -0
  95. package/lib/host/settings.d.ts +9 -0
  96. package/lib/host/settings.d.ts.map +1 -0
  97. package/lib/host/settings.js +70 -0
  98. package/lib/host/settings.js.map +1 -0
  99. package/lib/host/status.d.ts +28 -0
  100. package/lib/host/status.d.ts.map +1 -0
  101. package/lib/host/status.js +60 -0
  102. package/lib/host/status.js.map +1 -0
  103. package/lib/host/tool.d.ts +59 -0
  104. package/lib/host/tool.d.ts.map +1 -0
  105. package/lib/host/tool.js +51 -0
  106. package/lib/host/tool.js.map +1 -0
  107. package/lib/host/types/config.d.ts +47 -0
  108. package/lib/host/types/config.d.ts.map +1 -0
  109. package/lib/host/types/config.js +2 -0
  110. package/lib/host/types/config.js.map +1 -0
  111. package/lib/host/types/executable.d.ts +30 -0
  112. package/lib/host/types/executable.d.ts.map +1 -0
  113. package/lib/host/types/executable.js +9 -0
  114. package/lib/host/types/executable.js.map +1 -0
  115. package/lib/host/types/index.d.ts +23 -0
  116. package/lib/host/types/index.d.ts.map +1 -0
  117. package/lib/host/types/index.js +2 -0
  118. package/lib/host/types/index.js.map +1 -0
  119. package/lib/host/types/plugin.d.ts +38 -0
  120. package/lib/host/types/plugin.d.ts.map +1 -0
  121. package/lib/host/types/plugin.js +10 -0
  122. package/lib/host/types/plugin.js.map +1 -0
  123. package/lib/host/types/settings.d.ts +127 -0
  124. package/lib/host/types/settings.d.ts.map +1 -0
  125. package/lib/host/types/settings.js +2 -0
  126. package/lib/host/types/settings.js.map +1 -0
  127. package/lib/host/types/stderr.d.ts +53 -0
  128. package/lib/host/types/stderr.d.ts.map +1 -0
  129. package/lib/host/types/stderr.js +9 -0
  130. package/lib/host/types/stderr.js.map +1 -0
  131. package/lib/host/types/windows.d.ts +39 -0
  132. package/lib/host/types/windows.d.ts.map +1 -0
  133. package/lib/host/types/windows.js +9 -0
  134. package/lib/host/types/windows.js.map +1 -0
  135. package/lib/host/values.d.ts +89 -0
  136. package/lib/host/values.d.ts.map +1 -0
  137. package/lib/host/values.js +212 -0
  138. package/lib/host/values.js.map +1 -0
  139. package/lib/host/windows/connect-flags.d.ts +16 -0
  140. package/lib/host/windows/connect-flags.d.ts.map +1 -0
  141. package/lib/host/windows/connect-flags.js +67 -0
  142. package/lib/host/windows/connect-flags.js.map +1 -0
  143. package/lib/host/windows/index.d.ts +31 -0
  144. package/lib/host/windows/index.d.ts.map +1 -0
  145. package/lib/host/windows/index.js +30 -0
  146. package/lib/host/windows/index.js.map +1 -0
  147. package/lib/host/windows/landing.d.ts +47 -0
  148. package/lib/host/windows/landing.d.ts.map +1 -0
  149. package/lib/host/windows/landing.js +174 -0
  150. package/lib/host/windows/landing.js.map +1 -0
  151. package/lib/host/windows/link.d.ts +11 -0
  152. package/lib/host/windows/link.d.ts.map +1 -0
  153. package/lib/host/windows/link.js +52 -0
  154. package/lib/host/windows/link.js.map +1 -0
  155. package/lib/host/windows/paths.d.ts +43 -0
  156. package/lib/host/windows/paths.d.ts.map +1 -0
  157. package/lib/host/windows/paths.js +121 -0
  158. package/lib/host/windows/paths.js.map +1 -0
  159. package/lib/host/windows/run.d.ts +74 -0
  160. package/lib/host/windows/run.d.ts.map +1 -0
  161. package/lib/host/windows/run.js +210 -0
  162. package/lib/host/windows/run.js.map +1 -0
  163. package/lib/host/windows/spawn.d.ts +22 -0
  164. package/lib/host/windows/spawn.d.ts.map +1 -0
  165. package/lib/host/windows/spawn.js +73 -0
  166. package/lib/host/windows/spawn.js.map +1 -0
  167. package/lib/index.d.ts +86 -0
  168. package/lib/index.d.ts.map +1 -0
  169. package/lib/index.js +72 -625
  170. package/lib/index.js.map +1 -0
  171. package/package.json +26 -4
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Chrome discovery — the low-level `--version` probe and the on-disk/identity
3
+ * helpers it rides on.
4
+ *
5
+ * The one place discovery reaches outside the process: `chromePath --version`
6
+ * run through a scrubbed parent env under a bounded timeout, the rule that says
7
+ * whether a path is worth spawning at all (a directory, socket or fifo never
8
+ * reaches a probe — a Chrome-less host must run one scan that launches nothing),
9
+ * and the identity key that folds two spellings of the same binary into one
10
+ * answer. The scan that drives them lives in ./scan.ts.
11
+ */
12
+ import { spawn } from "node:child_process";
13
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
14
+ import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
15
+ import { CHECK_TIMEOUT_MS } from "../constants.js";
16
+ import { firstLine } from "../status.js";
17
+ /**
18
+ * Spawn one `chromePath --version` probe (scrubbed parent env, bounded
19
+ * timeout). Resolves the first stdout line (the version); ENOENT rejects with
20
+ * "Chrome executable not found: <path>" carrying code "ENOENT"; a non-zero
21
+ * exit rejects with the first non-empty stderr/stdout line.
22
+ */
23
+ export function probeExecutable(target) {
24
+ return new Promise((resolve, reject) => {
25
+ let child;
26
+ try {
27
+ child = spawn(target, ["--version"], {
28
+ env: scrubbedParentEnv(),
29
+ stdio: ["ignore", "pipe", "pipe"],
30
+ timeout: CHECK_TIMEOUT_MS,
31
+ killSignal: "SIGKILL"
32
+ });
33
+ }
34
+ catch (error) {
35
+ reject(new Error(`could not run "${target} --version": ${String(error)}`));
36
+ return;
37
+ }
38
+ let stdout = "";
39
+ let stderr = "";
40
+ child.stdout?.on("data", (chunk) => (stdout += chunk));
41
+ child.stderr?.on("data", (chunk) => (stderr += chunk));
42
+ child.on("error", (error) => {
43
+ const code = error && typeof error.code === "string" ? error.code : "";
44
+ if (code === "ENOENT") {
45
+ const missing = new Error(`Chrome executable not found: ${target}`);
46
+ Object.defineProperty(missing, "code", { value: "ENOENT", writable: true, enumerable: false, configurable: true });
47
+ reject(missing);
48
+ return;
49
+ }
50
+ reject(new Error(`could not run "${target} --version": ${String(error)}`));
51
+ });
52
+ child.on("close", (code) => {
53
+ if (code === null) {
54
+ reject(new Error(`"${target} --version" timed out after ${String(CHECK_TIMEOUT_MS)}ms`));
55
+ return;
56
+ }
57
+ if (code !== 0) {
58
+ const detail = firstLine(stderr) !== "" ? firstLine(stderr) : firstLine(stdout) !== "" ? firstLine(stdout) : `exit code ${String(code)}`;
59
+ reject(new Error(`"${target} --version" failed: ${detail}`));
60
+ return;
61
+ }
62
+ resolve(firstLine(stdout));
63
+ });
64
+ });
65
+ }
66
+ /** True when a candidate path can be handed to the OS for a `--version` probe.
67
+ * A path that is absent (or not executable) never reaches the spawn: a
68
+ * Chrome-less host runs one scan without launching anything. Paths with no
69
+ * directory separator go straight to the spawn so the OS resolves them
70
+ * through `PATH`. */
71
+ export function presentOnDisk(candidate) {
72
+ if (!candidate.includes("/") && !candidate.includes("\\"))
73
+ return true;
74
+ try {
75
+ // A directory (or a socket, or a fifo) is not an executable, and
76
+ // spawning one to find out is a side effect we do not want.
77
+ if (!statSync(candidate).isFile())
78
+ return false;
79
+ accessSync(candidate, constants.X_OK);
80
+ return true;
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ /** Identity key used to fold two spellings of the same binary (`google-chrome`
87
+ * resolving onto `/usr/bin/google-chrome`) into one option. Falls back to the
88
+ * path itself when `realpath` cannot resolve it. */
89
+ export function identity(candidate) {
90
+ try {
91
+ return realpathSync(candidate);
92
+ }
93
+ catch {
94
+ return candidate;
95
+ }
96
+ }
97
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../../src/host/discovery/probe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type { ChromeExecutableStatus, ChromeMcpConfig } from "../types/index.js";
2
+ /**
3
+ * What one run probes: every candidate it has — the row-config candidate list,
4
+ * the paths the saved rows carry, and the current selection (see
5
+ * {@link chromeExecutableCandidates}) — probed in that order.
6
+ *
7
+ * A candidate that runs answers with its `--version` line; one that is present
8
+ * but not runnable (bad binary, a probe that timed out) keeps its place in the
9
+ * list carrying the failure, so a broken pin stays visible instead of vanishing
10
+ * into a red dot.
11
+ *
12
+ * - absent / not executable → dropped without a spawn,
13
+ * - `blockedForPlatform` answers non-empty → **no spawn**: the candidate keeps
14
+ * its place carrying that reason (the one case is a Windows `chrome.exe` on a
15
+ * WSL host, whose probe would start a browser instance it can never drive —
16
+ * see {@link WSL_WINDOWS_LAUNCH_ERROR}),
17
+ * - two spellings of the same binary collapse into the first one seen.
18
+ *
19
+ * An empty candidate list resolves `[]` (nothing probed: unknown platform, or a
20
+ * host patched down to nothing).
21
+ *
22
+ * @param candidates - candidate executable paths.
23
+ * @param blockedForPlatform - answers a reason for a candidate this host must
24
+ * never exec, `""` for one it may probe.
25
+ */
26
+ export declare function scanChromeExecutables(candidates: string[], blockedForPlatform?: (candidate: string) => string): Promise<ChromeExecutableStatus[]>;
27
+ /**
28
+ * Discover usable Google Chrome executables when nothing is fixed: probe every
29
+ * candidate and report the first runnable one. Kept for its `Promise<string>`
30
+ * contract (`src/index.ts` re-export, tests, diagnostics).
31
+ * @param candidatePaths - candidate executable paths (default: platform list).
32
+ * @param blockedForPlatform - answers a reason for a candidate this host must
33
+ * never exec, `""` for one it may probe.
34
+ * @returns the discovered executable version line.
35
+ */
36
+ export declare function discoverChrome(candidatePaths: string[], blockedForPlatform?: (candidate: string) => string): Promise<string>;
37
+ /**
38
+ * One check run — the public "did we find a runnable Chrome?" question:
39
+ *
40
+ * - `chromePath` non-empty → probe that executable (ENOENT → "Chrome
41
+ * executable not found: <path>"; non-zero → first stderr/stdout line;
42
+ * success → the version line).
43
+ * - `chromePath` empty → run the scan over the candidate list (row config
44
+ * `chromePaths`, default {@link defaultChromePaths}) and report the first
45
+ * runnable entry. Nothing usable → the known executable-missing error;
46
+ * only-present-but-broken entries → that entry's failure.
47
+ * - the extra flags declare a connect mode (`--browserUrl` / `--wsEndpoint`) →
48
+ * resolve "": there is no local executable to check.
49
+ * - `blockedForPlatform` answers non-empty for what is selected → resolve "" with
50
+ * **no spawn**. A Windows `chrome.exe` on a WSL host is the case: its
51
+ * `--version` probe answers (which is why the selection looks healthy) but
52
+ * answering means attaching to the Windows browser session, and that *starts*
53
+ * a browser instance no tool call could drive
54
+ * ({@link WSL_WINDOWS_LAUNCH_ERROR}).
55
+ *
56
+ * A run that probes **nothing** (no candidates, a connect mode, or a blocked
57
+ * path) resolves "" — callers must treat that as "no evidence", never as health.
58
+ *
59
+ * @param entry - validated {@link ChromeMcpConfig}.
60
+ * @param extraFlags - the flags list the bridge currently launches with; only
61
+ * inspected for connect-mode options.
62
+ * @param blockedForPlatform - answers a reason for a path this host must never
63
+ * exec, `""` for one it may probe.
64
+ * @returns the executable version line, or "" when nothing is probed.
65
+ */
66
+ export declare function checkChrome(entry: ChromeMcpConfig, extraFlags?: string[], blockedForPlatform?: (candidate: string) => string): Promise<string>;
67
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../../src/host/discovery/scan.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAKjF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,qBAAqB,CACvC,UAAU,EAAE,MAAM,EAAE,EACpB,kBAAkB,GAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAiB,GAC7D,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAyCnC;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAChC,cAAc,EAAE,MAAM,EAAE,EACxB,kBAAkB,GAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAiB,GAC7D,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CACvB,KAAK,EAAE,eAAe,EACtB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,kBAAkB,GAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAiB,GAC7D,OAAO,CAAC,MAAM,CAAC,CAYjB"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Chrome discovery — the scan and the check run that tie it together.
3
+ *
4
+ * What one executable run does: probe every candidate it has (the row-config
5
+ * list, the saved rows' ids, the selection), publish the per-path answer, and
6
+ * report the first runnable one. The candidate ordering lives in
7
+ * ./candidates.ts; the `--version` probe + presence + identity helpers in
8
+ * ./probe.ts.
9
+ *
10
+ * Upstream (`chrome-devtools-mcp`, via puppeteer) starts the browser only
11
+ * LAZILY, on the first tool call — so without a probe of our own, a
12
+ * Chrome-less host would show nothing until somebody called a tool. The scan
13
+ * therefore probes eagerly: there is no "auto" mode, the scan lists what exists,
14
+ * the card picks one of them, and what is picked stays picked.
15
+ *
16
+ * WSL NOTE: the executable must be a Linux binary. Launching the Windows
17
+ * chrome.exe through the WSL interop layer fails: chrome-devtools-mcp connects
18
+ * with `pipe: true` (inherited fd 3/4), and Windows-side processes cannot
19
+ * receive those pipes from WSL; the WebSocket fallback hard-codes 127.0.0.1 in
20
+ * the DevToolsActivePort file, unreachable across the WSL2 NAT bridge. Verified
21
+ * empirically 2025-09-19 (.probe/); do not "fix" discovery by pointing candidates
22
+ * or chromePath at a /mnt/c executable.
23
+ */
24
+ import { carriesConnectionMode } from "../flags.js";
25
+ import { firstLine } from "../status.js";
26
+ import { defaultChromePaths } from "./candidates.js";
27
+ import { identity, presentOnDisk, probeExecutable } from "./probe.js";
28
+ /**
29
+ * What one run probes: every candidate it has — the row-config candidate list,
30
+ * the paths the saved rows carry, and the current selection (see
31
+ * {@link chromeExecutableCandidates}) — probed in that order.
32
+ *
33
+ * A candidate that runs answers with its `--version` line; one that is present
34
+ * but not runnable (bad binary, a probe that timed out) keeps its place in the
35
+ * list carrying the failure, so a broken pin stays visible instead of vanishing
36
+ * into a red dot.
37
+ *
38
+ * - absent / not executable → dropped without a spawn,
39
+ * - `blockedForPlatform` answers non-empty → **no spawn**: the candidate keeps
40
+ * its place carrying that reason (the one case is a Windows `chrome.exe` on a
41
+ * WSL host, whose probe would start a browser instance it can never drive —
42
+ * see {@link WSL_WINDOWS_LAUNCH_ERROR}),
43
+ * - two spellings of the same binary collapse into the first one seen.
44
+ *
45
+ * An empty candidate list resolves `[]` (nothing probed: unknown platform, or a
46
+ * host patched down to nothing).
47
+ *
48
+ * @param candidates - candidate executable paths.
49
+ * @param blockedForPlatform - answers a reason for a candidate this host must
50
+ * never exec, `""` for one it may probe.
51
+ */
52
+ export async function scanChromeExecutables(candidates, blockedForPlatform = () => "") {
53
+ const list = Array.isArray(candidates) ? candidates : [];
54
+ const statuses = [];
55
+ const seen = new Set();
56
+ for (const candidate of list) {
57
+ const path = String(candidate ?? "").trim();
58
+ if (path === "")
59
+ continue;
60
+ const blocked = blockedForPlatform(path);
61
+ if (blocked !== "") {
62
+ // Never spawned: what a blocked candidate would do is start a
63
+ // browser this host could not drive. It stays on the list so the
64
+ // card can name it instead of dropping it into a red dot.
65
+ const key = `blocked\u0000${identity(path)}`;
66
+ if (seen.has(key))
67
+ continue;
68
+ seen.add(key);
69
+ statuses.push({ path, version: "", error: blocked });
70
+ continue;
71
+ }
72
+ if (!presentOnDisk(path))
73
+ continue;
74
+ try {
75
+ const version = await probeExecutable(path);
76
+ const key = `${identity(path)}\u0000${version}`;
77
+ if (seen.has(key))
78
+ continue;
79
+ seen.add(key);
80
+ statuses.push({ path, version, error: "" });
81
+ }
82
+ catch (error) {
83
+ const code = (error instanceof Error && typeof error.code === "string")
84
+ ? error.code
85
+ : "";
86
+ if (code === "ENOENT")
87
+ continue;
88
+ const key = identity(path);
89
+ if (seen.has(key))
90
+ continue;
91
+ seen.add(key);
92
+ statuses.push({
93
+ path,
94
+ version: "",
95
+ error: firstLine(error instanceof Error ? error.message : String(error))
96
+ });
97
+ }
98
+ }
99
+ return statuses;
100
+ }
101
+ /**
102
+ * Discover usable Google Chrome executables when nothing is fixed: probe every
103
+ * candidate and report the first runnable one. Kept for its `Promise<string>`
104
+ * contract (`src/index.ts` re-export, tests, diagnostics).
105
+ * @param candidatePaths - candidate executable paths (default: platform list).
106
+ * @param blockedForPlatform - answers a reason for a candidate this host must
107
+ * never exec, `""` for one it may probe.
108
+ * @returns the discovered executable version line.
109
+ */
110
+ export async function discoverChrome(candidatePaths, blockedForPlatform = () => "") {
111
+ const candidates = (Array.isArray(candidatePaths) ? candidatePaths : defaultChromePaths())
112
+ .map((candidate) => String(candidate ?? "").trim())
113
+ .filter((candidate) => candidate !== "");
114
+ if (candidates.length === 0)
115
+ return "";
116
+ const found = await scanChromeExecutables(candidates, blockedForPlatform);
117
+ for (const status of found) {
118
+ if (status.version !== "" && status.error === "")
119
+ return status.version;
120
+ }
121
+ const broken = found.find((status) => status.error !== "");
122
+ if (broken !== void 0)
123
+ throw new Error(broken.error);
124
+ throw new Error(`Chrome executable not found: no executable detected (checked: ${candidates.join(", ")})`);
125
+ }
126
+ /**
127
+ * One check run — the public "did we find a runnable Chrome?" question:
128
+ *
129
+ * - `chromePath` non-empty → probe that executable (ENOENT → "Chrome
130
+ * executable not found: <path>"; non-zero → first stderr/stdout line;
131
+ * success → the version line).
132
+ * - `chromePath` empty → run the scan over the candidate list (row config
133
+ * `chromePaths`, default {@link defaultChromePaths}) and report the first
134
+ * runnable entry. Nothing usable → the known executable-missing error;
135
+ * only-present-but-broken entries → that entry's failure.
136
+ * - the extra flags declare a connect mode (`--browserUrl` / `--wsEndpoint`) →
137
+ * resolve "": there is no local executable to check.
138
+ * - `blockedForPlatform` answers non-empty for what is selected → resolve "" with
139
+ * **no spawn**. A Windows `chrome.exe` on a WSL host is the case: its
140
+ * `--version` probe answers (which is why the selection looks healthy) but
141
+ * answering means attaching to the Windows browser session, and that *starts*
142
+ * a browser instance no tool call could drive
143
+ * ({@link WSL_WINDOWS_LAUNCH_ERROR}).
144
+ *
145
+ * A run that probes **nothing** (no candidates, a connect mode, or a blocked
146
+ * path) resolves "" — callers must treat that as "no evidence", never as health.
147
+ *
148
+ * @param entry - validated {@link ChromeMcpConfig}.
149
+ * @param extraFlags - the flags list the bridge currently launches with; only
150
+ * inspected for connect-mode options.
151
+ * @param blockedForPlatform - answers a reason for a path this host must never
152
+ * exec, `""` for one it may probe.
153
+ * @returns the executable version line, or "" when nothing is probed.
154
+ */
155
+ export function checkChrome(entry, extraFlags, blockedForPlatform = () => "") {
156
+ const target = typeof entry.chromePath === "string" ? entry.chromePath.trim() : "";
157
+ if (target !== "")
158
+ return blockedForPlatform(target) !== "" ? Promise.resolve("") : probeExecutable(target);
159
+ const flags = Array.isArray(extraFlags)
160
+ ? extraFlags
161
+ : Array.isArray(entry.extraFlags)
162
+ ? entry.extraFlags
163
+ : [];
164
+ if (flags.some((flag) => typeof flag === "string" && carriesConnectionMode(flag))) {
165
+ return Promise.resolve("");
166
+ }
167
+ return discoverChrome(entry.chromePaths, blockedForPlatform);
168
+ }
169
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../../../src/host/discovery/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Validate a user-authored extraFlags list: a plain array of flag strings
3
+ * carrying no control characters (argv is execve'd without a shell), and no
4
+ * executable-path flag — the executable is picked in the card's selector, so
5
+ * letting a flag pin one would put a second source of truth over the bridge
6
+ * argv. Throws with the offending entry so the settings layer can surface it.
7
+ * @param value - candidate flags array.
8
+ * @returns true when valid.
9
+ */
10
+ export declare function validateExtraFlags(value: unknown): true;
11
+ /** True when a flag selects the Chrome executable (upstream flags:
12
+ * `--executablePath` / `--executable-path` / `-e`, with or without `=`).
13
+ * No longer an accepted user flag (see {@link validateExtraFlags}): it stays
14
+ * here for the row-config fold ({@link extractExecutablePathFlag}) and the
15
+ * argv guard. */
16
+ export declare function carriesExecutablePath(flag: string): boolean;
17
+ /**
18
+ * Read the path a legacy `--executablePath` carries in the **row-config** flags
19
+ * list: the `=` spelling and the separated spelling both count, first entry
20
+ * wins, "" when nothing pins a path.
21
+ *
22
+ * The card's picker owns the executable now, so a row-config pin is not
23
+ * honored as a flag: it becomes the row-config source of the selection and gets
24
+ * stripped out of argv (see {@link stripExecutablePathFlags}).
25
+ */
26
+ export declare function extractExecutablePathFlag(flags: unknown): string;
27
+ /** Drop every entry that pins an executable from a flags list — paired with
28
+ * {@link extractExecutablePathFlag} so a legacy profile keeps its path as a
29
+ * selection source instead of reaching argv as a second source of truth.
30
+ * A `--flag <path>` spelling consumes the following entry; a flag-looking next
31
+ * entry keeps it (nothing was meant as the value). */
32
+ export declare function stripExecutablePathFlags(flags: unknown): string[];
33
+ /** True when one extraFlag declares a connect mode (no local executable). */
34
+ export declare function carriesConnectionMode(flag: string): boolean;
35
+ /** True when any entry of a flags list declares connect mode — the only thing
36
+ * that now spares the bridge from an executable probe. A flag that pins an
37
+ * executable is rejected on the way in instead (see
38
+ * {@link validateExtraFlags}). */
39
+ export declare function flagsSkipCheck(flags: unknown): boolean;
40
+ /** True when two flags lists are element-wise equal (flags are arrays). */
41
+ export declare function sameFlags(a: unknown, b: unknown): boolean;
42
+ //# sourceMappingURL=flags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../src/host/flags.ts"],"names":[],"mappings":"AAoBA;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAkBvD;AAED;;;;iBAIiB;AACjB,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAWhE;AAED;;;;sDAIsD;AACtD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAejE;AAED,6EAA6E;AAC7E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;;kCAGkC;AAClC,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEtD;AAED,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAGzD"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Extra-flags helpers shared by the bridge argv builder, the executable scan
3
+ * and the settings validation hook.
4
+ *
5
+ * Flags are execve'd as argv entries — never through a shell — so validation
6
+ * rejects control characters only, and the executable pin / connect-mode
7
+ * options are matched against their upstream spellings (`--executablePath`,
8
+ * `--executable-path`, `-e`, `--browserUrl`, `--wsEndpoint`).
9
+ *
10
+ * One rule has since hardened: pinning the executable by flag is a second
11
+ * source of truth for the same lever the executable picker owns, so a
12
+ * user-authored list may not carry it (`validateExtraFlags` rejects it), and
13
+ * the row config's copy gets folded into `chromePath` instead of reaching argv
14
+ * as a second opinion.
15
+ */
16
+ import { CONTROL_PATTERN } from "./constants.js";
17
+ /** One executable-path flag, upstream spellings, with or without `=`. */
18
+ const EXECUTABLE_PATH_FLAG = /^(?:--executablePath|--executable-path|-e)(?:=|(?=\s)|$)/iu;
19
+ /**
20
+ * Validate a user-authored extraFlags list: a plain array of flag strings
21
+ * carrying no control characters (argv is execve'd without a shell), and no
22
+ * executable-path flag — the executable is picked in the card's selector, so
23
+ * letting a flag pin one would put a second source of truth over the bridge
24
+ * argv. Throws with the offending entry so the settings layer can surface it.
25
+ * @param value - candidate flags array.
26
+ * @returns true when valid.
27
+ */
28
+ export function validateExtraFlags(value) {
29
+ if (!Array.isArray(value)) {
30
+ throw new Error("extraFlags must be an array of strings");
31
+ }
32
+ for (const flag of value) {
33
+ if (typeof flag !== "string" || flag === "") {
34
+ throw new Error("extraFlags entries must be non-empty strings");
35
+ }
36
+ if (CONTROL_PATTERN.test(flag)) {
37
+ throw new Error(`extraFlags entry "${flag}" contains control characters`);
38
+ }
39
+ if (carriesExecutablePath(flag)) {
40
+ throw new Error(`extraFlags entry "${flag}" selects the Chrome executable — pick it in the Chrome executable picker instead`);
41
+ }
42
+ }
43
+ return true;
44
+ }
45
+ /** True when a flag selects the Chrome executable (upstream flags:
46
+ * `--executablePath` / `--executable-path` / `-e`, with or without `=`).
47
+ * No longer an accepted user flag (see {@link validateExtraFlags}): it stays
48
+ * here for the row-config fold ({@link extractExecutablePathFlag}) and the
49
+ * argv guard. */
50
+ export function carriesExecutablePath(flag) {
51
+ return typeof flag === "string" && EXECUTABLE_PATH_FLAG.test(flag);
52
+ }
53
+ /**
54
+ * Read the path a legacy `--executablePath` carries in the **row-config** flags
55
+ * list: the `=` spelling and the separated spelling both count, first entry
56
+ * wins, "" when nothing pins a path.
57
+ *
58
+ * The card's picker owns the executable now, so a row-config pin is not
59
+ * honored as a flag: it becomes the row-config source of the selection and gets
60
+ * stripped out of argv (see {@link stripExecutablePathFlags}).
61
+ */
62
+ export function extractExecutablePathFlag(flags) {
63
+ if (!Array.isArray(flags))
64
+ return "";
65
+ for (let index = 0; index < flags.length; index++) {
66
+ const flag = flags[index];
67
+ if (typeof flag !== "string" || !carriesExecutablePath(flag))
68
+ continue;
69
+ const eq = flag.indexOf("=");
70
+ if (eq !== -1)
71
+ return flag.slice(eq + 1).trim();
72
+ const next = flags[index + 1];
73
+ return typeof next === "string" && next.trim() !== "" ? next.trim() : "";
74
+ }
75
+ return "";
76
+ }
77
+ /** Drop every entry that pins an executable from a flags list — paired with
78
+ * {@link extractExecutablePathFlag} so a legacy profile keeps its path as a
79
+ * selection source instead of reaching argv as a second source of truth.
80
+ * A `--flag <path>` spelling consumes the following entry; a flag-looking next
81
+ * entry keeps it (nothing was meant as the value). */
82
+ export function stripExecutablePathFlags(flags) {
83
+ if (!Array.isArray(flags))
84
+ return [];
85
+ const stripped = [];
86
+ let awaitingValue = false;
87
+ for (const flag of flags) {
88
+ if (typeof flag !== "string")
89
+ continue;
90
+ if (carriesExecutablePath(flag)) {
91
+ awaitingValue = !flag.includes("=");
92
+ continue;
93
+ }
94
+ if (awaitingValue && !flag.startsWith("-"))
95
+ continue;
96
+ awaitingValue = false;
97
+ stripped.push(flag);
98
+ }
99
+ return stripped;
100
+ }
101
+ /** True when one extraFlag declares a connect mode (no local executable). */
102
+ export function carriesConnectionMode(flag) {
103
+ return /^--(?:browserUrl|wsEndpoint)(?:=|(?=\s)|$)/iu.test(flag);
104
+ }
105
+ /** True when any entry of a flags list declares connect mode — the only thing
106
+ * that now spares the bridge from an executable probe. A flag that pins an
107
+ * executable is rejected on the way in instead (see
108
+ * {@link validateExtraFlags}). */
109
+ export function flagsSkipCheck(flags) {
110
+ return Array.isArray(flags) && flags.some((flag) => typeof flag === "string" && carriesConnectionMode(flag));
111
+ }
112
+ /** True when two flags lists are element-wise equal (flags are arrays). */
113
+ export function sameFlags(a, b) {
114
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
115
+ return false;
116
+ return a.every((flag, index) => flag === b[index]);
117
+ }
118
+ //# sourceMappingURL=flags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flags.js","sourceRoot":"","sources":["../../src/host/flags.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /** Everything {@link isWindowsSubsystemForLinux} consults; each field defaults
2
+ * to what the running process actually is. */
3
+ export type PlatformProbe = {
4
+ /** Defaults to `process.platform`. */
5
+ platform?: NodeJS.Platform;
6
+ /** Variable lookup; defaults to `process.env`. When a source is supplied its
7
+ * silence is the answer — the marker file is then not consulted, so a host
8
+ * that resolves its launch environment gets one deterministic answer
9
+ * instead of one that depends on the machine it booted on. */
10
+ env?: (name: string) => unknown;
11
+ /** File-presence lookup used only when `env` is not supplied (a bare call
12
+ * with no host to resolve against); defaults to the interop marker. */
13
+ exists?: (path: string) => boolean;
14
+ };
15
+ /**
16
+ * True when `path` names a Windows binary — one that lives on the interop mount
17
+ * (`/mnt/<drive>/…`), in a Windows spelling (`\\host\…`, `C:\…`), or simply
18
+ * carries the PE suffix. A Linux path is never Windows-side, and a candidate
19
+ * list built from {@link DEFAULT_CHROME_PATHS} never matches.
20
+ * @param path - one executable path as selected.
21
+ */
22
+ export declare function isWindowsExecutablePath(path: unknown): boolean;
23
+ /**
24
+ * True when the current process runs inside Windows Subsystem for Linux: a
25
+ * Linux kernel plus the WSL markers. A non-Linux platform (a plain Windows
26
+ * host) is never "under WSL", whatever its environment says.
27
+ * @param probe - what to consult (defaults to this process).
28
+ */
29
+ export declare function isWindowsSubsystemForLinux(probe?: PlatformProbe): boolean;
30
+ /**
31
+ * The interop trap the card warns about: we are under WSL **and** what is
32
+ * selected is a Windows executable. Both halves are required — a Linux binary
33
+ * selected on a Windows host is simply nothing to say.
34
+ * @param path - the effective executable path.
35
+ * @param probe - what to consult for the environment (defaults to this process).
36
+ */
37
+ export declare function isWindowsExecutableUnderWsl(path: unknown, probe?: PlatformProbe): boolean;
38
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/host/platform.ts"],"names":[],"mappings":"AAsBA;8CAC8C;AAC9C,MAAM,MAAM,aAAa,GAAG;IACxB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B;;;kEAG8D;IAC9D,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC;2EACuE;IACvE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACtC,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAK9D;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,GAAE,aAAkB,GAAG,OAAO,CAU7E;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,GAAE,aAAkB,GAAG,OAAO,CAE7F"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Host half — where the plugin is running: is this Linux host a Windows
3
+ * Subsystem for Linux instance, and does one executable path name a Windows
4
+ * binary rather than a Linux one.
5
+ *
6
+ * The two answers only ever combine into one warning. Launching a Windows
7
+ * `chrome.exe` from WSL cannot work (`.probe/`, AGENTS.md): the debug pipes
8
+ * `chrome-devtools-mcp` opens do not survive the interop boundary, and the
9
+ * debugging port it reads from `DevToolsActivePort` stays bound to the Windows
10
+ * loopback, unreachable across the WSL2 NAT bridge. It even *seems* to work: the
11
+ * `--version` probe answers successfully, so a broken selection hides behind a
12
+ * healthy pill until the first tool call. That is why the pair is surfaced on
13
+ * the card as its own notice rather than through the error dot: nothing is
14
+ * failing yet, the required configuration is simply different.
15
+ *
16
+ * Both probes stay pure and injectable (`platform`, `env`, `exists`) so the
17
+ * tests never depend on the machine they run on.
18
+ */
19
+ import { existsSync } from "node:fs";
20
+ import { WINDOWS_EXE_PATTERN, WINDOWS_PATH_PATTERN, WSL_ENV_VARS, WSL_INTEROP_MARKER } from "./constants.js";
21
+ /**
22
+ * True when `path` names a Windows binary — one that lives on the interop mount
23
+ * (`/mnt/<drive>/…`), in a Windows spelling (`\\host\…`, `C:\…`), or simply
24
+ * carries the PE suffix. A Linux path is never Windows-side, and a candidate
25
+ * list built from {@link DEFAULT_CHROME_PATHS} never matches.
26
+ * @param path - one executable path as selected.
27
+ */
28
+ export function isWindowsExecutablePath(path) {
29
+ if (typeof path !== "string")
30
+ return false;
31
+ const candidate = path.trim();
32
+ if (candidate === "")
33
+ return false;
34
+ return WINDOWS_PATH_PATTERN.test(candidate) || WINDOWS_EXE_PATTERN.test(candidate);
35
+ }
36
+ /**
37
+ * True when the current process runs inside Windows Subsystem for Linux: a
38
+ * Linux kernel plus the WSL markers. A non-Linux platform (a plain Windows
39
+ * host) is never "under WSL", whatever its environment says.
40
+ * @param probe - what to consult (defaults to this process).
41
+ */
42
+ export function isWindowsSubsystemForLinux(probe = {}) {
43
+ const platform = probe.platform ?? process.platform;
44
+ if (platform !== "linux")
45
+ return false;
46
+ if (probe.env !== void 0) {
47
+ return WSL_ENV_VARS.some((name) => {
48
+ const value = probe.env(name);
49
+ return typeof value === "string" && value.trim() !== "";
50
+ });
51
+ }
52
+ return (probe.exists ?? ((candidate) => existsSync(candidate)))(WSL_INTEROP_MARKER);
53
+ }
54
+ /**
55
+ * The interop trap the card warns about: we are under WSL **and** what is
56
+ * selected is a Windows executable. Both halves are required — a Linux binary
57
+ * selected on a Windows host is simply nothing to say.
58
+ * @param path - the effective executable path.
59
+ * @param probe - what to consult for the environment (defaults to this process).
60
+ */
61
+ export function isWindowsExecutableUnderWsl(path, probe = {}) {
62
+ return isWindowsSubsystemForLinux(probe) && isWindowsExecutablePath(path);
63
+ }
64
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/host/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAiB7G;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAa;IACjD,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,oBAAoB,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AACvF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,QAAuB,EAAE;IAChE,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC;IACpD,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAI,CAAC,IAAI,CAAC,CAAC;YAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QAC5D,CAAC,CAAC,CAAC;IACP,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,SAAiB,EAAE,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;AAChG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CAAC,IAAa,EAAE,QAAuB,EAAE;IAChF,OAAO,0BAA0B,CAAC,KAAK,CAAC,IAAI,uBAAuB,CAAC,IAAI,CAAC,CAAC;AAC9E,CAAC"}