arn-browser 0.1.31 → 0.1.33
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 +7 -3
- package/bin/install.js +65 -191
- package/package.json +4 -3
- package/src/utility/playwright/findBrowserPath.js +211 -0
- package/src/utility/playwright/pwLaunch.d.ts +18 -1
- package/src/utility/playwright/pwLaunch.js +51 -73
- package/src/utility/puppeteer/deleteDirectory.js +104 -0
- package/src/utility/puppeteer/findBrowserPath.js +211 -0
- package/src/utility/puppeteer/ppLaunch.d.ts +18 -1
- package/src/utility/puppeteer/ppLaunch.js +62 -40
- /package/src/utility/{deleteDirectory.js → playwright/deleteDirectory.js} +0 -0
|
@@ -18,7 +18,8 @@ import { FingerprintGenerator } from "fingerprint-generator";
|
|
|
18
18
|
|
|
19
19
|
// Internal Utilities
|
|
20
20
|
import { getMultiloginToken } from "../mlx_token.js";
|
|
21
|
-
import { deleteDirectoryWithRetries } from "
|
|
21
|
+
import { deleteDirectoryWithRetries } from "./deleteDirectory.js";
|
|
22
|
+
import { findBrowserPath } from "./findBrowserPath.js";
|
|
22
23
|
|
|
23
24
|
// Human Cursor - for human-like mouse movements
|
|
24
25
|
import { createCursor } from "./human-cursor/index.js";
|
|
@@ -70,7 +71,8 @@ function resolveProfilePath(nameOrPath, browserName) {
|
|
|
70
71
|
|
|
71
72
|
let prefix = browserName.toLowerCase();
|
|
72
73
|
if (prefix.includes("brave")) prefix = "brave";
|
|
73
|
-
else if (prefix.includes("
|
|
74
|
+
else if (prefix.includes("chromium")) prefix = "chromium";
|
|
75
|
+
else if (prefix.includes("chrome")) prefix = "chrome";
|
|
74
76
|
else if (prefix.includes("firefox")) prefix = "firefox";
|
|
75
77
|
else if (prefix.includes("camoufox")) prefix = "camoufox";
|
|
76
78
|
|
|
@@ -78,70 +80,8 @@ function resolveProfilePath(nameOrPath, browserName) {
|
|
|
78
80
|
return path.join(PERSISTENT_DIR, folderName);
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* Uses ~/.arn-browser/browsers/{browser}/{executable}
|
|
84
|
-
*/
|
|
85
|
-
function getBinaryPath(browserName) {
|
|
86
|
-
const isWindows = process.platform === "win32";
|
|
87
|
-
const browsersDir = path.join(ARN_BROWSER_DIR, "browsers");
|
|
88
|
-
|
|
89
|
-
const binaryMap = {
|
|
90
|
-
brave: isWindows
|
|
91
|
-
? path.join(browsersDir, "brave", "brave.exe")
|
|
92
|
-
: path.join(browsersDir, "brave", "brave"),
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const binaryPath = binaryMap[browserName];
|
|
96
|
-
if (!binaryPath) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`░░░░░ [LaunchBrowser] Unknown browser for getBinaryPath: ${browserName}`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!fs.existsSync(binaryPath)) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`░░░░░ [LaunchBrowser] ${browserName} binary not found at: ${binaryPath}\n` +
|
|
105
|
-
` Run "node bin/cli.js install" to install browsers.`
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return binaryPath;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Gets the executable path for Playwright-managed browsers (Chromium, Firefox).
|
|
114
|
-
* Uses ~/.arn-browser/browsers/{browser}/{executable}
|
|
115
|
-
*/
|
|
116
|
-
function getPlaywrightExePath(browserName) {
|
|
117
|
-
const isWindows = process.platform === "win32";
|
|
118
|
-
const browsersDir = path.join(ARN_BROWSER_DIR, "browsers");
|
|
119
|
-
|
|
120
|
-
const binaryMap = {
|
|
121
|
-
chromium: isWindows
|
|
122
|
-
? path.join(browsersDir, "chromium", "chrome.exe")
|
|
123
|
-
: path.join(browsersDir, "chromium", "chrome"),
|
|
124
|
-
firefox: isWindows
|
|
125
|
-
? path.join(browsersDir, "firefox", "firefox.exe")
|
|
126
|
-
: path.join(browsersDir, "firefox", "firefox"),
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const binaryPath = binaryMap[browserName];
|
|
130
|
-
if (!binaryPath) {
|
|
131
|
-
throw new Error(
|
|
132
|
-
`░░░░░ [LaunchBrowser] Unknown browser: ${browserName}`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!fs.existsSync(binaryPath)) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
`░░░░░ [LaunchBrowser] ${browserName} binary not found at: ${binaryPath}\n` +
|
|
139
|
-
` Run "node bin/cli.js install" to install browsers.`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return binaryPath;
|
|
144
|
-
}
|
|
83
|
+
// Browser path resolution now uses OS-installed browsers via findBrowserPath.
|
|
84
|
+
// See src/utility/findBrowserPath.js for detection logic.
|
|
145
85
|
|
|
146
86
|
const PROFILE_META_FILE = "_profile_meta.json";
|
|
147
87
|
|
|
@@ -175,6 +115,31 @@ function writeProfileMeta(dirPath, type, cleanupMinutes) {
|
|
|
175
115
|
}
|
|
176
116
|
}
|
|
177
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Writes per-host notification permissions into the Chrome Preferences object.
|
|
120
|
+
* Accepts plain hostnames (e.g. "web.whatsapp.com") and converts them
|
|
121
|
+
* to Chrome's internal format: "https://host:443,*" with setting=1 (Allow).
|
|
122
|
+
* If hosts array is empty, does nothing (Chrome default "Ask" behavior).
|
|
123
|
+
*/
|
|
124
|
+
function writeNotificationPermissions(prefs, hosts) {
|
|
125
|
+
if (!hosts || hosts.length === 0) return;
|
|
126
|
+
|
|
127
|
+
if (!prefs.profile) prefs.profile = {};
|
|
128
|
+
if (!prefs.profile.content_settings) prefs.profile.content_settings = {};
|
|
129
|
+
if (!prefs.profile.content_settings.exceptions) prefs.profile.content_settings.exceptions = {};
|
|
130
|
+
if (!prefs.profile.content_settings.exceptions.notifications) prefs.profile.content_settings.exceptions.notifications = {};
|
|
131
|
+
|
|
132
|
+
const now = String(Date.now());
|
|
133
|
+
for (const host of hosts) {
|
|
134
|
+
const origin = host.startsWith("http") ? host : `https://${host}:443`;
|
|
135
|
+
const key = `${origin},*`;
|
|
136
|
+
prefs.profile.content_settings.exceptions.notifications[key] = {
|
|
137
|
+
last_modified: now,
|
|
138
|
+
setting: 1, // 1 = Allow
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
178
143
|
/**
|
|
179
144
|
* Scans both persistent and temp profile directories.
|
|
180
145
|
* Deletes profiles whose `_profile_meta.json` indicates they are expired.
|
|
@@ -339,6 +304,10 @@ export async function pwLaunch({
|
|
|
339
304
|
camoufox_options = {}, // { geoip, humanize, ... }
|
|
340
305
|
multilogin_options = {}, // { profileId, os_type, canvas_noise, ... }
|
|
341
306
|
|
|
307
|
+
// Notifications — per-host allow list
|
|
308
|
+
// Pass plain hostnames: ["web.whatsapp.com", "messages.google.com"]
|
|
309
|
+
notification_hosts = [],
|
|
310
|
+
|
|
342
311
|
// Logging
|
|
343
312
|
launch_logs = false,
|
|
344
313
|
cleanup_logs = false,
|
|
@@ -385,6 +354,8 @@ export async function pwLaunch({
|
|
|
385
354
|
humanize_options: effectiveHumanizeOptions,
|
|
386
355
|
spoof_fingerprint,
|
|
387
356
|
cleanupMinutes: effectiveCleanupMinutes,
|
|
357
|
+
browserType: which_browser, // "chrome" or "chromium"
|
|
358
|
+
notification_hosts,
|
|
388
359
|
});
|
|
389
360
|
break;
|
|
390
361
|
case "firefox":
|
|
@@ -407,6 +378,7 @@ export async function pwLaunch({
|
|
|
407
378
|
humanize_options: effectiveHumanizeOptions,
|
|
408
379
|
spoof_fingerprint,
|
|
409
380
|
cleanupMinutes: effectiveCleanupMinutes,
|
|
381
|
+
notification_hosts,
|
|
410
382
|
});
|
|
411
383
|
break;
|
|
412
384
|
case "camoufox":
|
|
@@ -442,7 +414,7 @@ export async function pwLaunch({
|
|
|
442
414
|
// ==========================================================================
|
|
443
415
|
// 4. ENGINE: CHROMIUM
|
|
444
416
|
// ==========================================================================
|
|
445
|
-
async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint, cleanupMinutes }) {
|
|
417
|
+
async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, humanize_options, spoof_fingerprint, cleanupMinutes, browserType = "chromium", notification_hosts = [] }) {
|
|
446
418
|
const isPersistent = !!profilePath;
|
|
447
419
|
|
|
448
420
|
// 1. Determine Path (Temp needs it for fingerprint storage, Persistent needs it for data)
|
|
@@ -471,6 +443,8 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
471
443
|
// --- Silence & Networking ---
|
|
472
444
|
"--disable-background-networking",
|
|
473
445
|
"--disable-background-timer-throttling",
|
|
446
|
+
"--disable-backgrounding-occluded-windows",
|
|
447
|
+
"--disable-renderer-backgrounding",
|
|
474
448
|
"--disable-breakpad",
|
|
475
449
|
"--disable-crash-reporter",
|
|
476
450
|
"--disable-component-update",
|
|
@@ -504,7 +478,7 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
504
478
|
// Launch standard browser (not persistent context)
|
|
505
479
|
const browser = await chromium.launch({
|
|
506
480
|
headless: false,
|
|
507
|
-
executablePath:
|
|
481
|
+
executablePath: findBrowserPath(browserType),
|
|
508
482
|
proxy: proxyObj,
|
|
509
483
|
args: args,
|
|
510
484
|
ignoreDefaultArgs: ignoreDefaultArgs,
|
|
@@ -558,6 +532,7 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
558
532
|
|
|
559
533
|
if (!prefs.profile) prefs.profile = {};
|
|
560
534
|
prefs.profile.exit_type = "Normal";
|
|
535
|
+
writeNotificationPermissions(prefs, notification_hosts);
|
|
561
536
|
|
|
562
537
|
if (!prefs.session) prefs.session = {};
|
|
563
538
|
prefs.session.restore_on_startup = 4;
|
|
@@ -571,7 +546,7 @@ async function chromiumLauncher({ profilePath, proxy, timezoneId, CapSolver, hum
|
|
|
571
546
|
// Logic: Native Persistent Launch. No fingerprint-injector.
|
|
572
547
|
const context = await chromium.launchPersistentContext(activePath, {
|
|
573
548
|
headless: false,
|
|
574
|
-
executablePath:
|
|
549
|
+
executablePath: findBrowserPath(browserType),
|
|
575
550
|
proxy: proxyObj,
|
|
576
551
|
args: args,
|
|
577
552
|
timezoneId: tz,
|
|
@@ -625,7 +600,7 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
625
600
|
|
|
626
601
|
const browser = await firefox.launch({
|
|
627
602
|
headless: false,
|
|
628
|
-
executablePath:
|
|
603
|
+
executablePath: findBrowserPath("firefox"),
|
|
629
604
|
proxy: proxyObj,
|
|
630
605
|
ignoreDefaultArgs: ["--enable-automation"],
|
|
631
606
|
firefoxUserPrefs: firefoxUserPrefs,
|
|
@@ -667,7 +642,7 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
667
642
|
|
|
668
643
|
const context = await firefox.launchPersistentContext(activePath, {
|
|
669
644
|
headless: false,
|
|
670
|
-
executablePath:
|
|
645
|
+
executablePath: findBrowserPath("firefox"),
|
|
671
646
|
proxy: proxyObj,
|
|
672
647
|
timezoneId: tz,
|
|
673
648
|
ignoreDefaultArgs: ["--enable-automation"],
|
|
@@ -691,7 +666,7 @@ async function firefoxLauncher({ profilePath, proxy, timezoneId, humanize_option
|
|
|
691
666
|
// ==========================================================================
|
|
692
667
|
// 6. ENGINE: BRAVE
|
|
693
668
|
// ==========================================================================
|
|
694
|
-
async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humanize_options, spoof_fingerprint, cleanupMinutes }) {
|
|
669
|
+
async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humanize_options, spoof_fingerprint, cleanupMinutes, notification_hosts = [] }) {
|
|
695
670
|
const isPersistent = !!profilePath;
|
|
696
671
|
const activePath = isPersistent ? profilePath : path.join(TEMP_DIR, crypto.randomUUID());
|
|
697
672
|
|
|
@@ -744,7 +719,7 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
744
719
|
}
|
|
745
720
|
}
|
|
746
721
|
|
|
747
|
-
const braveBin =
|
|
722
|
+
const braveBin = findBrowserPath("brave");
|
|
748
723
|
|
|
749
724
|
// ======================================================
|
|
750
725
|
// Disable Brave Sidebar via Preferences
|
|
@@ -766,6 +741,7 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
766
741
|
// Prevent tab restore (saves proxy bandwidth)
|
|
767
742
|
if (!prefs.profile) prefs.profile = {};
|
|
768
743
|
prefs.profile.exit_type = "Normal";
|
|
744
|
+
writeNotificationPermissions(prefs, notification_hosts);
|
|
769
745
|
|
|
770
746
|
if (!prefs.session) prefs.session = {};
|
|
771
747
|
prefs.session.restore_on_startup = 4;
|
|
@@ -819,6 +795,8 @@ async function braveLauncher({ profilePath, proxy, CapSolver, timezoneId, humani
|
|
|
819
795
|
// --- Silence & Networking ---
|
|
820
796
|
"--disable-background-networking",
|
|
821
797
|
"--disable-background-timer-throttling",
|
|
798
|
+
"--disable-backgrounding-occluded-windows",
|
|
799
|
+
"--disable-renderer-backgrounding",
|
|
822
800
|
"--disable-breakpad",
|
|
823
801
|
"--disable-crash-reporter",
|
|
824
802
|
"--disable-component-update",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deletes a directory with built-in retry logic.
|
|
7
|
+
* Useful for browser profiles where files might still be locked by the OS process.
|
|
8
|
+
* * @param {string} targetPath - The absolute path to delete.
|
|
9
|
+
* @param {number} maxRetries - How many times to retry (default: 5).
|
|
10
|
+
* @param {number} retryDelayMs - Time to wait between retries (default: 2000ms).
|
|
11
|
+
*/
|
|
12
|
+
export async function deleteDirectoryWithRetries(targetPath, maxRetries = 5, retryDelayMs = 2000) {
|
|
13
|
+
if (!fs.existsSync(targetPath)) {
|
|
14
|
+
return true; // Already gone
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let attempt = 0;
|
|
18
|
+
|
|
19
|
+
while (attempt < maxRetries) {
|
|
20
|
+
try {
|
|
21
|
+
// Force: true allows deleting files even if read-only
|
|
22
|
+
// Recursive: true deletes inner folders
|
|
23
|
+
await fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
24
|
+
|
|
25
|
+
// Double check if it's actually gone
|
|
26
|
+
if (!fs.existsSync(targetPath)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const isLastAttempt = attempt === maxRetries - 1;
|
|
31
|
+
|
|
32
|
+
// If it's the last attempt, log error
|
|
33
|
+
if (isLastAttempt) {
|
|
34
|
+
console.error(`░░░░░ Failed to delete directory after ${maxRetries} attempts: ${targetPath}`);
|
|
35
|
+
console.error(` Error: ${error.message}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Log warning and wait
|
|
40
|
+
console.warn(
|
|
41
|
+
`░░░░░ Delete failed (Attempt ${attempt + 1}/${maxRetries}). File might be locked. Retrying in ${retryDelayMs / 1000
|
|
42
|
+
}s...`
|
|
43
|
+
);
|
|
44
|
+
await sleep(retryDelayMs);
|
|
45
|
+
}
|
|
46
|
+
attempt++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deletes specific contents inside a directory but keeps the parent folder.
|
|
54
|
+
* (Optional utility if you need to clear cache without removing the profile folder itself)
|
|
55
|
+
*/
|
|
56
|
+
export async function deleteAllItemsInDirectory(directoryPath) {
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.existsSync(directoryPath)) return;
|
|
59
|
+
|
|
60
|
+
const files = await fs.promises.readdir(directoryPath);
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const curPath = path.join(directoryPath, file);
|
|
63
|
+
await deleteDirectoryWithRetries(curPath);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`Error emptying directory ${directoryPath}:`, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Legacy utility: Scan a directory and delete folders older than X minutes.
|
|
72
|
+
* (Note: launchBrowser.js now has its own internal version of this, but keeping this
|
|
73
|
+
* here is useful if you want to run cleanup scripts independently).
|
|
74
|
+
*/
|
|
75
|
+
export function deleteOldDirectories({ directoryPath, ageLimitMinutes }) {
|
|
76
|
+
if (!fs.existsSync(directoryPath)) return;
|
|
77
|
+
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const limit = ageLimitMinutes * 60 * 1000;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const files = fs.readdirSync(directoryPath);
|
|
83
|
+
|
|
84
|
+
files.forEach((file) => {
|
|
85
|
+
const curPath = path.join(directoryPath, file);
|
|
86
|
+
try {
|
|
87
|
+
const stats = fs.statSync(curPath);
|
|
88
|
+
if (stats.isDirectory()) {
|
|
89
|
+
const age = now - stats.mtimeMs;
|
|
90
|
+
if (age > limit) {
|
|
91
|
+
console.log(`Cleaning up old directory: ${file}`);
|
|
92
|
+
// We use the sync version of retry logic or just fire-and-forget async here
|
|
93
|
+
// For simplicity in a sync loop, we often just do a force removal:
|
|
94
|
+
fs.rmSync(curPath, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// Ignore access errors on specific files
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error("Error during old directory cleanup:", err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -180,7 +180,24 @@ export interface PpLaunchOptions {
|
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
// ========================================================================
|
|
183
|
-
// 8.
|
|
183
|
+
// 8. NOTIFICATIONS
|
|
184
|
+
// ========================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* List of hostnames to auto-grant notification permissions for.
|
|
188
|
+
* Pass plain hostnames — they are auto-converted to `https://host:443` internally.
|
|
189
|
+
*
|
|
190
|
+
* If empty (default), Chrome's default "Ask" behavior is used.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* notification_hosts: ["web.whatsapp.com", "messages.google.com"]
|
|
194
|
+
*
|
|
195
|
+
* Default: []
|
|
196
|
+
*/
|
|
197
|
+
notification_hosts?: string[];
|
|
198
|
+
|
|
199
|
+
// ========================================================================
|
|
200
|
+
// 9. LOGGING
|
|
184
201
|
// ========================================================================
|
|
185
202
|
|
|
186
203
|
/**
|