arn-browser 0.1.30 → 0.1.32

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/bin/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  /**
4
4
  * @file CLI for arn-browser
5
- * @description Provides `npx arn-browser install` to download browser binaries.
5
+ * @description Provides `npx arn-browser install` to download Camoufox and scan OS browsers.
6
6
  *
7
7
  * Usage:
8
- * npx arn-browser install - Install all browser binaries (Brave, Camoufox, Chromium, Firefox)
8
+ * npx arn-browser install - Install Camoufox, clean stale folders, scan OS browsers
9
9
  * npx arn-browser help - Show help
10
10
  */
11
11
 
@@ -19,8 +19,12 @@ function showHelp() {
19
19
  npx arn-browser <command>
20
20
 
21
21
  Commands:
22
- install Download and install browser binaries (Brave, Camoufox, Chromium, Firefox)
22
+ install Install Camoufox, clean up old browser folders, and scan for OS-installed browsers
23
23
  help Show this help message
24
+
25
+ Note:
26
+ Chrome, Chromium, Firefox, and Brave are detected from your OS installation.
27
+ Only Camoufox needs to be downloaded via this installer.
24
28
  `);
25
29
  }
26
30
 
package/bin/install.js CHANGED
@@ -1,44 +1,24 @@
1
1
  /**
2
2
  * @file install.js
3
3
  * @description Cross-platform browser installer for arn-browser.
4
- * Downloads and extracts Brave, Camoufox, and optionally Chromium to ~/.cache/
4
+ * Downloads and extracts Camoufox to ~/.cache/camoufox
5
+ * Cleans up stale browser folders from previous versions.
6
+ * Scans for OS-installed browsers and reports status.
5
7
  *
6
8
  * Works on Linux, macOS, and Windows.
7
9
  */
8
10
 
9
- // ==========================================================================
10
- // INSTALL TOGGLES — set to false to skip a browser
11
- // ==========================================================================
12
- const INSTALL_BRAVE = true;
13
- const INSTALL_CAMOUFOX = true;
14
- const INSTALL_CHROMIUM = true;
15
- const INSTALL_FIREFOX = true;
16
-
17
11
  import fs from "fs";
18
12
  import path from "path";
19
13
  import os from "os";
20
14
  import { execSync } from "child_process";
21
15
  import { createWriteStream } from "fs";
22
16
 
17
+ import { checkBrowserAvailability } from "../src/utility/playwright/findBrowserPath.js";
18
+
23
19
  // ==========================================================================
24
- // CONFIGURATION URLS
20
+ // CONFIGURATION
25
21
  // ==========================================================================
26
- // OS identifiers used in our dictionaries
27
- // process.platform: 'win32', 'darwin', 'linux'
28
- // os.arch(): 'x64', 'arm64'
29
-
30
- const BRAVE_URLS = {
31
- linux: {
32
- x64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-browser-1.88.134-linux-amd64.zip",
33
- arm64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-browser-1.88.134-linux-arm64.zip",
34
- },
35
- darwin: {
36
- arm64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/Brave-Browser-arm64.dmg",
37
- },
38
- win32: {
39
- x64: "https://github.com/brave/brave-browser/releases/download/v1.88.134/brave-v1.88.134-win32-x64.zip",
40
- }
41
- };
42
22
 
43
23
  const CAM_URLS = {
44
24
  linux: {
@@ -53,42 +33,6 @@ const CAM_URLS = {
53
33
  }
54
34
  };
55
35
 
56
- // Chromium x64 uses Chrome for Testing (CFT) URLs with browserVersion.
57
- // Chromium arm64 uses legacy revision-based URLs.
58
- // CDN: cdn.playwright.dev
59
- const CHROMIUM_REVISION = "1214";
60
- const CHROMIUM_BROWSER_VERSION = "146.0.7680.31";
61
-
62
- const CHROMIUM_URLS = {
63
- linux: {
64
- x64: `https://cdn.playwright.dev/builds/cft/${CHROMIUM_BROWSER_VERSION}/linux64/chrome-linux64.zip`,
65
-
66
- arm64: `https://cdn.playwright.dev/dbazure/download/playwright/builds/chromium/${CHROMIUM_REVISION}/chromium-linux-arm64.zip`,
67
- },
68
- darwin: {
69
- arm64: `https://cdn.playwright.dev/builds/cft/${CHROMIUM_BROWSER_VERSION}/mac-arm64/chrome-mac-arm64.zip`,
70
- },
71
- win32: {
72
- x64: `https://cdn.playwright.dev/builds/cft/${CHROMIUM_BROWSER_VERSION}/win64/chrome-win64.zip`,
73
- }
74
- };
75
-
76
- // Firefox uses revision-based URLs on the new CDN.
77
- const FIREFOX_REVISION = "1509";
78
-
79
- const FIREFOX_URLS = {
80
- linux: {
81
- x64: `https://cdn.playwright.dev/dbazure/download/playwright/builds/firefox/${FIREFOX_REVISION}/firefox-ubuntu-22.04.zip`,
82
- arm64: `https://cdn.playwright.dev/dbazure/download/playwright/builds/firefox/${FIREFOX_REVISION}/firefox-ubuntu-22.04-arm64.zip`,
83
- },
84
- darwin: {
85
- arm64: `https://cdn.playwright.dev/dbazure/download/playwright/builds/firefox/${FIREFOX_REVISION}/firefox-mac-arm64.zip`,
86
- },
87
- win32: {
88
- x64: `https://cdn.playwright.dev/dbazure/download/playwright/builds/firefox/${FIREFOX_REVISION}/firefox-win64.zip`,
89
- }
90
- };
91
-
92
36
  const CAM_VERSION = { version: "135.0.1", release: "beta.24" };
93
37
 
94
38
  // ==========================================================================
@@ -96,25 +40,26 @@ const CAM_VERSION = { version: "135.0.1", release: "beta.24" };
96
40
  // ==========================================================================
97
41
 
98
42
  const ARN_BROWSERS_DIR = path.join(os.homedir(), ".arn-browser", "browsers");
99
- const BRAVE_DIR = path.join(ARN_BROWSERS_DIR, "brave");
100
- const CHROMIUM_DIR = path.join(ARN_BROWSERS_DIR, "chromium");
101
- const FIREFOX_DIR = path.join(ARN_BROWSERS_DIR, "firefox");
102
43
  const CAM_DIR = path.join(os.homedir(), ".cache", "camoufox");
103
44
  const TEMP_DIR = path.join(os.tmpdir(), "arn-browser-install");
104
45
 
46
+ // Stale folders from previous versions that downloaded browsers
47
+ const STALE_BROWSER_DIRS = [
48
+ path.join(ARN_BROWSERS_DIR, "brave"),
49
+ path.join(ARN_BROWSERS_DIR, "chromium"),
50
+ path.join(ARN_BROWSERS_DIR, "firefox"),
51
+ ];
52
+
105
53
  // ==========================================================================
106
54
  // HELPERS
107
55
  // ==========================================================================
108
56
 
109
57
  function getArch() {
110
- const arch = os.arch(); // "x64", "arm64", etc.
58
+ const arch = os.arch();
111
59
  if (arch === "x64" || arch === "arm64") return arch;
112
60
  throw new Error(`❌ Unsupported architecture: ${arch}`);
113
61
  }
114
62
 
115
- /**
116
- * Download a file with redirect following (GitHub releases use 302 redirects).
117
- */
118
63
  function formatBytes(bytes) {
119
64
  if (!Number.isFinite(bytes) || bytes < 0) return "0 B";
120
65
  const units = ["B", "KB", "MB", "GB", "TB"];
@@ -181,9 +126,6 @@ async function downloadFile(url, destPath, label) {
181
126
  process.stdout.write("\n");
182
127
  }
183
128
 
184
- /**
185
- * Cross-platform unzip.
186
- */
187
129
  function unzipFile(zipPath, destDir) {
188
130
  const isWindows = process.platform === "win32";
189
131
 
@@ -194,38 +136,12 @@ function unzipFile(zipPath, destDir) {
194
136
  }
195
137
  }
196
138
 
197
- /**
198
- * If the extracted zip contains a single top-level directory,
199
- * move its contents up to destDir (flatten).
200
- */
201
- function flattenExtract(destDir) {
202
- const entries = fs.readdirSync(destDir, { withFileTypes: true });
203
- if (entries.length === 1 && entries[0].isDirectory()) {
204
- const subDir = path.join(destDir, entries[0].name);
205
- // Rename subfolder to a temp name to avoid name collisions
206
- // (e.g. firefox/firefox where the dir and executable share a name)
207
- const tmpDir = subDir + "__tmp";
208
- fs.renameSync(subDir, tmpDir);
209
- const subEntries = fs.readdirSync(tmpDir);
210
- for (const item of subEntries) {
211
- fs.renameSync(path.join(tmpDir, item), path.join(destDir, item));
212
- }
213
- fs.rmdirSync(tmpDir);
214
- }
215
- }
216
-
217
- /**
218
- * Recursively delete a directory.
219
- */
220
139
  function rmDir(dirPath) {
221
140
  if (fs.existsSync(dirPath)) {
222
141
  fs.rmSync(dirPath, { recursive: true, force: true });
223
142
  }
224
143
  }
225
144
 
226
- /**
227
- * Set executable permission (no-op on Windows).
228
- */
229
145
  function setExecutable(dirPath, filename) {
230
146
  if (process.platform === "win32") return;
231
147
 
@@ -235,9 +151,6 @@ function setExecutable(dirPath, filename) {
235
151
  }
236
152
  }
237
153
 
238
- /**
239
- * Recursively find files by name.
240
- */
241
154
  function findFiles(dir, name) {
242
155
  const results = [];
243
156
  if (!fs.existsSync(dir)) return results;
@@ -254,48 +167,15 @@ function findFiles(dir, name) {
254
167
  return results;
255
168
  }
256
169
 
257
- /**
258
- * Helper to get the correct URL for the current platform and architecture.
259
- */
260
170
  function getDownloadUrl(urlMap, osName, arch) {
261
171
  if (!urlMap[osName]) return null;
262
172
  return urlMap[osName][arch] || null;
263
173
  }
264
174
 
265
-
266
175
  // ==========================================================================
267
- // BROWSER INSTALLERS
176
+ // CAMOUFOX INSTALLER
268
177
  // ==========================================================================
269
178
 
270
- async function installBrave(osName, arch) {
271
- const url = getDownloadUrl(BRAVE_URLS, osName, arch);
272
- if (!url) {
273
- console.log(`⚠️ Brave not available for ${osName} ${arch}`);
274
- return;
275
- }
276
-
277
- // Clean previous install
278
- rmDir(BRAVE_DIR);
279
-
280
- const ext = url.endsWith(".dmg") ? ".dmg" : ".zip";
281
- const dlPath = path.join(TEMP_DIR, `brave${ext}`);
282
-
283
- console.log("⬇️ Downloading Brave...");
284
- await downloadFile(url, dlPath, "Brave");
285
-
286
- fs.mkdirSync(BRAVE_DIR, { recursive: true });
287
-
288
- console.log(`📦 Extracting Brave to ${BRAVE_DIR}...`);
289
- if (ext === ".zip") {
290
- unzipFile(dlPath, BRAVE_DIR);
291
- flattenExtract(BRAVE_DIR);
292
- } else if (ext === ".dmg") {
293
- console.log(`⚠️ Brave downloaded as DMG to ${dlPath}. Automatic extraction of DMG is not fully supported in this script. Recommend manual installation.`);
294
- }
295
-
296
- console.log("✅ Brave installed!");
297
- }
298
-
299
179
  async function installCamoufox(osName, arch) {
300
180
  const url = getDownloadUrl(CAM_URLS, osName, arch);
301
181
  if (!url) {
@@ -329,62 +209,53 @@ async function installCamoufox(osName, arch) {
329
209
  console.log("✅ Camoufox installed!");
330
210
  }
331
211
 
332
- async function installChromium(osName, arch) {
333
- const url = getDownloadUrl(CHROMIUM_URLS, osName, arch);
334
- if (!url) {
335
- console.log(`⚠️ Chromium not available for ${osName} ${arch}`);
336
- return;
337
- }
338
-
339
- // Clean previous install
340
- rmDir(CHROMIUM_DIR);
341
-
342
- const ext = url.endsWith(".dmg") ? ".dmg" : ".zip";
343
- const dlPath = path.join(TEMP_DIR, `chromium${ext}`);
344
-
345
- console.log("⬇️ Downloading Chromium...");
346
- await downloadFile(url, dlPath, "Chromium");
347
-
348
- fs.mkdirSync(CHROMIUM_DIR, { recursive: true });
212
+ // ==========================================================================
213
+ // STALE FOLDER CLEANUP
214
+ // ==========================================================================
349
215
 
350
- console.log(`📦 Extracting Chromium to ${CHROMIUM_DIR}...`);
351
- if (ext === ".zip") {
352
- unzipFile(dlPath, CHROMIUM_DIR);
353
- flattenExtract(CHROMIUM_DIR);
354
- } else {
355
- console.log(`⚠️ Chromium downloaded as DMG to ${dlPath}. Manual extraction required.`);
216
+ function cleanupStaleBrowserFolders() {
217
+ let cleaned = false;
218
+ for (const dir of STALE_BROWSER_DIRS) {
219
+ if (fs.existsSync(dir)) {
220
+ console.log(`🗑️ Removing stale browser folder: ${dir}`);
221
+ rmDir(dir);
222
+ cleaned = true;
223
+ }
356
224
  }
357
225
 
358
- console.log("✅ Chromium installed!");
359
- }
360
-
361
- async function installFirefox(osName, arch) {
362
- const url = getDownloadUrl(FIREFOX_URLS, osName, arch);
363
- if (!url) {
364
- console.log(`⚠️ Firefox not available for ${osName} ${arch}`);
365
- return;
226
+ // Also clean up the parent browsers dir if it's now empty
227
+ if (fs.existsSync(ARN_BROWSERS_DIR)) {
228
+ try {
229
+ const remaining = fs.readdirSync(ARN_BROWSERS_DIR);
230
+ if (remaining.length === 0) {
231
+ fs.rmdirSync(ARN_BROWSERS_DIR);
232
+ }
233
+ } catch {
234
+ // Non-critical
235
+ }
366
236
  }
367
237
 
368
- // Clean previous install
369
- rmDir(FIREFOX_DIR);
370
-
371
- const ext = url.endsWith(".dmg") ? ".dmg" : ".zip";
372
- const dlPath = path.join(TEMP_DIR, `firefox${ext}`);
238
+ if (!cleaned) {
239
+ console.log(" No stale browser folders found.");
240
+ }
241
+ }
373
242
 
374
- console.log("⬇️ Downloading Firefox...");
375
- await downloadFile(url, dlPath, "Firefox");
243
+ // ==========================================================================
244
+ // OS BROWSER SCAN
245
+ // ==========================================================================
376
246
 
377
- fs.mkdirSync(FIREFOX_DIR, { recursive: true });
247
+ function scanOsBrowsers() {
248
+ console.log("\n🔍 Scanning for OS-installed browsers...\n");
378
249
 
379
- console.log(`📦 Extracting Firefox to ${FIREFOX_DIR}...`);
380
- if (ext === ".zip") {
381
- unzipFile(dlPath, FIREFOX_DIR);
382
- flattenExtract(FIREFOX_DIR);
383
- } else {
384
- console.log(`⚠️ Firefox downloaded as DMG to ${dlPath}. Manual extraction required.`);
250
+ const browsers = ["chrome", "chromium", "firefox", "brave"];
251
+ for (const name of browsers) {
252
+ const result = checkBrowserAvailability(name);
253
+ if (result.found) {
254
+ console.log(` ✅ ${name.padEnd(10)} ${result.path}`);
255
+ } else {
256
+ console.log(` ❌ ${name.padEnd(10)} → Not found`);
257
+ }
385
258
  }
386
-
387
- console.log("✅ Firefox installed!");
388
259
  }
389
260
 
390
261
  // ==========================================================================
@@ -398,23 +269,26 @@ export async function installBrowsers() {
398
269
  console.log(`\n🚀 arn-browser: Installing browser binaries`);
399
270
  console.log(` Platform: ${osName} | Architecture: ${arch}\n`);
400
271
 
401
- // Prepare temp directory
272
+ // 1. Clean up stale browser folders from previous versions
273
+ console.log("🧹 Cleaning up old downloaded browser folders...");
274
+ cleanupStaleBrowserFolders();
275
+
276
+ // 2. Prepare temp directory
402
277
  fs.mkdirSync(TEMP_DIR, { recursive: true });
403
278
 
404
279
  try {
405
- if (INSTALL_BRAVE) await installBrave(osName, arch);
406
- if (INSTALL_CAMOUFOX) await installCamoufox(osName, arch);
407
- if (INSTALL_CHROMIUM) await installChromium(osName, arch);
408
- if (INSTALL_FIREFOX) await installFirefox(osName, arch);
280
+ // 3. Install Camoufox
281
+ await installCamoufox(osName, arch);
409
282
  } finally {
410
283
  // Cleanup temp directory
411
284
  rmDir(TEMP_DIR);
412
285
  }
413
286
 
287
+ // 4. Scan for OS-installed browsers
288
+ scanOsBrowsers();
289
+
414
290
  console.log(`\n🎉 Done!`);
415
- console.log(` Brave: ${BRAVE_DIR}`);
416
291
  console.log(` Camoufox: ${CAM_DIR}`);
417
- console.log(` Chromium: ${CHROMIUM_DIR}`);
418
- console.log(` Firefox: ${FIREFOX_DIR}`);
419
- console.log(` Version: ${CAM_DIR}/version.json\n`);
292
+ console.log(` Version: ${CAM_DIR}/version.json`);
293
+ console.log(`\n Other browsers (Chrome, Chromium, Firefox, Brave) use your OS installation.\n`);
420
294
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arn-browser",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "A lightweight, browser autmation helper.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -16,6 +16,7 @@
16
16
  "@ghostery/adblocker": "^2.13.0",
17
17
  "arn-knexjs": "^0.0.3",
18
18
  "camoufox-js": "^0.9.3",
19
+ "devtools-detector": "^2.0.25",
19
20
  "dotenv": "^17.2.3",
20
21
  "fingerprint-generator": "^2.1.78",
21
22
  "fingerprint-injector": "^2.1.78",
@@ -0,0 +1,211 @@
1
+ /**
2
+ * @file findBrowserPath.js
3
+ * @description Cross-platform utility to find OS-installed browser executables.
4
+ * Supports: chrome, chromium, firefox, brave
5
+ * Works on: Linux (x64/arm64), macOS (arm64), Windows (x64)
6
+ */
7
+
8
+ import { execSync } from "child_process";
9
+ import fs from "fs";
10
+ import path from "path";
11
+
12
+ // ==========================================================================
13
+ // KNOWN BINARY PATHS PER PLATFORM
14
+ // ==========================================================================
15
+
16
+ const BROWSER_PATHS = {
17
+ chrome: {
18
+ linux: [
19
+ "google-chrome-stable",
20
+ "google-chrome",
21
+ "/opt/google/chrome/google-chrome",
22
+ "/usr/bin/google-chrome-stable",
23
+ "/usr/bin/google-chrome",
24
+ ],
25
+ darwin: [
26
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
27
+ ],
28
+ win32: [
29
+ path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
30
+ path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe"),
31
+ path.join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
32
+ ],
33
+ },
34
+ chromium: {
35
+ linux: [
36
+ "chromium-browser",
37
+ "chromium",
38
+ "/usr/bin/chromium-browser",
39
+ "/usr/bin/chromium",
40
+ "/snap/bin/chromium",
41
+ ],
42
+ darwin: [
43
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
44
+ ],
45
+ win32: [
46
+ path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Chromium", "Application", "chrome.exe"),
47
+ path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Chromium", "Application", "chrome.exe"),
48
+ path.join(process.env.LOCALAPPDATA || "", "Chromium", "Application", "chrome.exe"),
49
+ ],
50
+ },
51
+ firefox: {
52
+ linux: [
53
+ "firefox",
54
+ "/usr/bin/firefox",
55
+ "/snap/bin/firefox",
56
+ "/usr/lib/firefox/firefox",
57
+ ],
58
+ darwin: [
59
+ "/Applications/Firefox.app/Contents/MacOS/firefox",
60
+ ],
61
+ win32: [
62
+ path.join(process.env.PROGRAMFILES || "C:\\Program Files", "Mozilla Firefox", "firefox.exe"),
63
+ path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "Mozilla Firefox", "firefox.exe"),
64
+ ],
65
+ },
66
+ brave: {
67
+ linux: [
68
+ "brave-browser",
69
+ "brave-browser-stable",
70
+ "/opt/brave.com/brave/brave-browser",
71
+ "/usr/bin/brave-browser",
72
+ "/usr/bin/brave-browser-stable",
73
+ "/snap/bin/brave",
74
+ ],
75
+ darwin: [
76
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
77
+ ],
78
+ win32: [
79
+ path.join(process.env.PROGRAMFILES || "C:\\Program Files", "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
80
+ path.join(process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)", "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
81
+ path.join(process.env.LOCALAPPDATA || "", "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
82
+ ],
83
+ },
84
+ };
85
+
86
+ // Friendly display names for error messages
87
+ const BROWSER_DISPLAY_NAMES = {
88
+ chrome: "Google Chrome",
89
+ chromium: "Chromium",
90
+ firefox: "Mozilla Firefox",
91
+ brave: "Brave Browser",
92
+ };
93
+
94
+ // Install hints per platform
95
+ const INSTALL_HINTS = {
96
+ chrome: {
97
+ linux: "sudo apt install google-chrome-stable OR download from https://www.google.com/chrome/",
98
+ darwin: "Download from https://www.google.com/chrome/",
99
+ win32: "Download from https://www.google.com/chrome/",
100
+ },
101
+ chromium: {
102
+ linux: "sudo apt install chromium-browser OR sudo snap install chromium",
103
+ darwin: "brew install --cask chromium OR download from https://www.chromium.org/",
104
+ win32: "Download from https://www.chromium.org/getting-involved/download-chromium/",
105
+ },
106
+ firefox: {
107
+ linux: "sudo apt install firefox OR sudo snap install firefox",
108
+ darwin: "brew install --cask firefox OR download from https://www.mozilla.org/firefox/",
109
+ win32: "Download from https://www.mozilla.org/firefox/",
110
+ },
111
+ brave: {
112
+ linux: "See https://brave.com/linux/ OR sudo apt install brave-browser",
113
+ darwin: "brew install --cask brave-browser OR download from https://brave.com/",
114
+ win32: "Download from https://brave.com/",
115
+ },
116
+ };
117
+
118
+ // ==========================================================================
119
+ // DETECTION LOGIC
120
+ // ==========================================================================
121
+
122
+ /**
123
+ * Try to resolve a command name using `which` (Unix) or `where` (Windows).
124
+ * Returns the absolute path if found, null otherwise.
125
+ */
126
+ function resolveFromPath(command) {
127
+ const isWindows = process.platform === "win32";
128
+ try {
129
+ const cmd = isWindows ? `where "${command}"` : `which "${command}"`;
130
+ const result = execSync(cmd, { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" }).trim();
131
+ // `where` on Windows may return multiple lines — take the first
132
+ const firstLine = result.split("\n")[0].trim();
133
+ if (firstLine && fs.existsSync(firstLine)) {
134
+ return firstLine;
135
+ }
136
+ } catch {
137
+ // Command not found in PATH
138
+ }
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Find the OS-installed executable path for a given browser.
144
+ *
145
+ * @param {string} browserName - One of: "chrome", "chromium", "firefox", "brave"
146
+ * @returns {string} Absolute path to the browser executable
147
+ * @throws {Error} If the browser is not found on the system
148
+ */
149
+ export function findBrowserPath(browserName) {
150
+ const name = browserName.toLowerCase();
151
+ const candidates = BROWSER_PATHS[name];
152
+
153
+ if (!candidates) {
154
+ throw new Error(
155
+ `❌ [findBrowserPath] Unknown browser: "${browserName}"\n` +
156
+ ` Supported: chrome, chromium, firefox, brave`
157
+ );
158
+ }
159
+
160
+ const platform = process.platform; // 'linux', 'darwin', 'win32'
161
+ const paths = candidates[platform];
162
+
163
+ if (!paths || paths.length === 0) {
164
+ throw new Error(
165
+ `❌ [findBrowserPath] ${BROWSER_DISPLAY_NAMES[name]} is not supported on platform: ${platform}`
166
+ );
167
+ }
168
+
169
+ // Strategy 1: Try PATH lookup first (works for short names like "google-chrome", "firefox")
170
+ for (const candidate of paths) {
171
+ // If it's not an absolute path, try resolving from PATH
172
+ if (!path.isAbsolute(candidate)) {
173
+ const resolved = resolveFromPath(candidate);
174
+ if (resolved) return resolved;
175
+ }
176
+ }
177
+
178
+ // Strategy 2: Check known absolute paths
179
+ for (const candidate of paths) {
180
+ if (path.isAbsolute(candidate) && fs.existsSync(candidate)) {
181
+ return candidate;
182
+ }
183
+ }
184
+
185
+ // Not found — throw descriptive error
186
+ const displayName = BROWSER_DISPLAY_NAMES[name];
187
+ const hint = INSTALL_HINTS[name]?.[platform] || `Install ${displayName} from its official website.`;
188
+
189
+ throw new Error(
190
+ `❌ [findBrowserPath] ${displayName} not found on this system.\n` +
191
+ ` Platform: ${platform}\n` +
192
+ ` Searched: ${paths.filter(p => path.isAbsolute(p)).join(", ") || "(PATH lookup only)"}\n\n` +
193
+ ` To install:\n` +
194
+ ` ${hint}`
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Check if a browser is available without throwing.
200
+ *
201
+ * @param {string} browserName - One of: "chrome", "chromium", "firefox", "brave"
202
+ * @returns {{ found: boolean, path: string|null, error: string|null }}
203
+ */
204
+ export function checkBrowserAvailability(browserName) {
205
+ try {
206
+ const p = findBrowserPath(browserName);
207
+ return { found: true, path: p, error: null };
208
+ } catch (err) {
209
+ return { found: false, path: null, error: err.message };
210
+ }
211
+ }