browser-cdp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Majestic Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # browser-cdp
2
+
3
+ Browser automation via Chrome DevTools Protocol. Control Chrome, Brave, Comet, or Edge using your real browser - same fingerprint, real cookies, no automation detection.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g browser-cdp
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Start browser with CDP enabled
15
+ browser-cdp start [browser] [--profile] [--port=PORT]
16
+
17
+ # Navigate to URL
18
+ browser-cdp nav <url> [--new]
19
+
20
+ # Execute JavaScript in page
21
+ browser-cdp eval '<code>'
22
+
23
+ # Take screenshot
24
+ browser-cdp screenshot
25
+
26
+ # Interactive element picker
27
+ browser-cdp pick '<message>'
28
+ ```
29
+
30
+ ## Environment Variables
31
+
32
+ | Variable | Description | Default |
33
+ |----------|-------------|---------|
34
+ | `DEBUG_PORT` | CDP debugging port | `9222` |
35
+ | `BROWSER` | Browser to use (chrome, brave, comet, edge) | `chrome` |
36
+ | `BROWSER_PATH` | Custom browser executable path | (auto-detect) |
37
+
38
+ ## Examples
39
+
40
+ ```bash
41
+ # Start Brave with your profile
42
+ browser-cdp start brave --profile
43
+
44
+ # Start Chrome on custom port
45
+ DEBUG_PORT=9333 browser-cdp start
46
+
47
+ # Navigate and search
48
+ browser-cdp nav https://google.com
49
+ browser-cdp eval 'document.querySelector("textarea").value = "hello"'
50
+
51
+ # Take screenshot
52
+ browser-cdp screenshot
53
+ # Returns: /tmp/screenshot-2024-01-01T12-00-00.png
54
+
55
+ # Pick elements interactively
56
+ browser-cdp pick "Select the login button"
57
+ ```
58
+
59
+ ## Pre-started Browser
60
+
61
+ If you already have a browser running with CDP enabled, the CLI will connect to it:
62
+
63
+ ```bash
64
+ # Add to ~/.zshrc
65
+ alias brave-debug='/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser --remote-debugging-port=9222'
66
+
67
+ # Start browser manually
68
+ brave-debug
69
+
70
+ # CLI detects and connects
71
+ browser-cdp start # "Browser already running on :9222"
72
+ browser-cdp nav https://example.com
73
+ ```
74
+
75
+ ## Supported Browsers
76
+
77
+ | Browser | Command | Profile Support |
78
+ |---------|---------|-----------------|
79
+ | Chrome | `chrome` (default) | Yes |
80
+ | Brave | `brave` | Yes |
81
+ | Comet | `comet` | No |
82
+ | Edge | `edge` | Yes |
83
+
84
+ ## Platform Support
85
+
86
+ Works on **macOS** and **Linux**. Browser paths auto-detected per platform.
87
+
88
+ | Platform | Chrome Path | Config Path |
89
+ |----------|-------------|-------------|
90
+ | macOS | `/Applications/Google Chrome.app/...` | `~/Library/Application Support/Google/Chrome/` |
91
+ | Linux | `/usr/bin/google-chrome` | `~/.config/google-chrome/` |
92
+
93
+ Use `BROWSER_PATH` env var to override if your browser is installed elsewhere.
94
+
95
+ ## Why Real Browser?
96
+
97
+ | Aspect | browser-cdp | Playwright Test Mode |
98
+ |--------|-------------|---------------------|
99
+ | Browser | Your installed Chrome/Brave/etc | Bundled Chromium |
100
+ | Profile | Can use real cookies/logins | Fresh test profile |
101
+ | Detection | Not detectable as automation | Automation flags present |
102
+ | Use case | Real-world testing, scraping | Isolated E2E tests |
103
+
104
+ ## License
105
+
106
+ MIT
package/cli.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ const command = process.argv[2];
4
+ const args = process.argv.slice(3);
5
+
6
+ const commands = {
7
+ start: "./start.js",
8
+ nav: "./nav.js",
9
+ eval: "./eval.js",
10
+ screenshot: "./screenshot.js",
11
+ pick: "./pick.js",
12
+ };
13
+
14
+ function printUsage() {
15
+ console.log("browser-cdp - Browser automation via Chrome DevTools Protocol");
16
+ console.log("");
17
+ console.log("Usage: browser-cdp <command> [options]");
18
+ console.log("");
19
+ console.log("Commands:");
20
+ console.log(" start [browser] Start browser with CDP enabled");
21
+ console.log(" nav <url> Navigate to URL");
22
+ console.log(" eval '<code>' Evaluate JavaScript in page");
23
+ console.log(" screenshot Take screenshot of current page");
24
+ console.log(" pick '<message>' Interactive element picker");
25
+ console.log("");
26
+ console.log("Environment:");
27
+ console.log(" DEBUG_PORT CDP port (default: 9222)");
28
+ console.log(" BROWSER Browser to use (chrome, brave, comet, edge)");
29
+ console.log(" BROWSER_PATH Custom browser executable path");
30
+ console.log("");
31
+ console.log("Examples:");
32
+ console.log(" browser-cdp start brave --profile");
33
+ console.log(" browser-cdp nav https://google.com");
34
+ console.log(" browser-cdp eval 'document.title'");
35
+ console.log(" browser-cdp screenshot");
36
+ process.exit(0);
37
+ }
38
+
39
+ if (!command || command === "--help" || command === "-h") {
40
+ printUsage();
41
+ }
42
+
43
+ if (!commands[command]) {
44
+ console.error(`Unknown command: ${command}`);
45
+ console.log(`Available: ${Object.keys(commands).join(", ")}`);
46
+ process.exit(1);
47
+ }
48
+
49
+ // Re-run with the specific command script
50
+ process.argv = [process.argv[0], commands[command], ...args];
51
+ await import(commands[command]);
package/eval.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chromium } from "playwright";
4
+
5
+ const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
6
+
7
+ const code = process.argv.slice(2).join(" ");
8
+ if (!code) {
9
+ console.log("Usage: eval.js 'code'");
10
+ console.log("\nExamples:");
11
+ console.log(' eval.js "document.title"');
12
+ console.log(" eval.js \"document.querySelectorAll('a').length\"");
13
+ process.exit(1);
14
+ }
15
+
16
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
17
+ const contexts = browser.contexts();
18
+ const context = contexts[0];
19
+
20
+ if (!context) {
21
+ console.error("No browser context found");
22
+ process.exit(1);
23
+ }
24
+
25
+ const pages = context.pages();
26
+ const page = pages[pages.length - 1];
27
+
28
+ if (!page) {
29
+ console.error("No active tab found");
30
+ process.exit(1);
31
+ }
32
+
33
+ let result;
34
+
35
+ try {
36
+ result = await page.evaluate((c) => {
37
+ const AsyncFunction = (async () => {}).constructor;
38
+ return new AsyncFunction(`return (${c})`)();
39
+ }, code);
40
+ } catch (e) {
41
+ console.log("Failed to evaluate expression");
42
+ console.log(` Expression: ${code}`);
43
+ console.log(e);
44
+ process.exit(1);
45
+ }
46
+
47
+ if (Array.isArray(result)) {
48
+ for (let i = 0; i < result.length; i++) {
49
+ if (i > 0) console.log("");
50
+ for (const [key, value] of Object.entries(result[i])) {
51
+ console.log(`${key}: ${value}`);
52
+ }
53
+ }
54
+ } else if (typeof result === "object" && result !== null) {
55
+ for (const [key, value] of Object.entries(result)) {
56
+ console.log(`${key}: ${value}`);
57
+ }
58
+ } else {
59
+ console.log(result);
60
+ }
61
+
62
+ await browser.close();
package/nav.js ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chromium } from "playwright";
4
+
5
+ const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
6
+
7
+ const url = process.argv[2];
8
+ const newTab = process.argv[3] === "--new";
9
+
10
+ if (!url) {
11
+ console.log("Usage: nav.js <url> [--new]");
12
+ console.log("\nExamples:");
13
+ console.log(" nav.js https://example.com # Navigate current tab");
14
+ console.log(" nav.js https://example.com --new # Open in new tab");
15
+ process.exit(1);
16
+ }
17
+
18
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
19
+ const contexts = browser.contexts();
20
+ const context = contexts[0] || await browser.newContext();
21
+
22
+ if (newTab) {
23
+ const page = await context.newPage();
24
+ await page.goto(url, { waitUntil: "domcontentloaded" });
25
+ console.log("Opened:", url);
26
+ } else {
27
+ const pages = context.pages();
28
+ const page = pages[pages.length - 1] || await context.newPage();
29
+ await page.goto(url, { waitUntil: "domcontentloaded" });
30
+ console.log("Navigated to:", url);
31
+ }
32
+
33
+ await browser.close();
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "browser-cdp",
3
+ "version": "0.1.0",
4
+ "description": "Browser automation via Chrome DevTools Protocol - control Chrome, Brave, Comet, Edge with real browser profiles",
5
+ "type": "module",
6
+ "bin": {
7
+ "browser-cdp": "./cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node cli.js"
11
+ },
12
+ "files": [
13
+ "cli.js",
14
+ "start.js",
15
+ "nav.js",
16
+ "eval.js",
17
+ "screenshot.js",
18
+ "pick.js",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/dpaluy/browser-cdp.git"
28
+ },
29
+ "homepage": "https://github.com/dpaluy/browser-cdp#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/dpaluy/browser-cdp/issues"
32
+ },
33
+ "dependencies": {
34
+ "playwright": "^1.49.0"
35
+ },
36
+ "keywords": [
37
+ "browser",
38
+ "automation",
39
+ "cdp",
40
+ "chrome",
41
+ "brave",
42
+ "playwright",
43
+ "devtools"
44
+ ],
45
+ "author": "Majestic Labs",
46
+ "license": "MIT"
47
+ }
package/pick.js ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chromium } from "playwright";
4
+
5
+ const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
6
+
7
+ const message = process.argv.slice(2).join(" ");
8
+ if (!message) {
9
+ console.log("Usage: pick.js 'message'");
10
+ console.log("\nExample:");
11
+ console.log(' pick.js "Click the submit button"');
12
+ process.exit(1);
13
+ }
14
+
15
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
16
+ const contexts = browser.contexts();
17
+ const context = contexts[0];
18
+
19
+ if (!context) {
20
+ console.error("No browser context found");
21
+ process.exit(1);
22
+ }
23
+
24
+ const pages = context.pages();
25
+ const page = pages[pages.length - 1];
26
+
27
+ if (!page) {
28
+ console.error("No active tab found");
29
+ process.exit(1);
30
+ }
31
+
32
+ // Inject pick() helper into current page
33
+ await page.evaluate(() => {
34
+ if (!window.pick) {
35
+ window.pick = async (message) => {
36
+ if (!message) {
37
+ throw new Error("pick() requires a message parameter");
38
+ }
39
+ return new Promise((resolve) => {
40
+ const selections = [];
41
+ const selectedElements = new Set();
42
+
43
+ const overlay = document.createElement("div");
44
+ overlay.style.cssText =
45
+ "position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none";
46
+
47
+ const highlight = document.createElement("div");
48
+ highlight.style.cssText =
49
+ "position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.1s";
50
+ overlay.appendChild(highlight);
51
+
52
+ const banner = document.createElement("div");
53
+ banner.style.cssText =
54
+ "position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647";
55
+
56
+ const updateBanner = () => {
57
+ banner.textContent = `${message} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`;
58
+ };
59
+ updateBanner();
60
+
61
+ document.body.append(banner, overlay);
62
+
63
+ const cleanup = () => {
64
+ document.removeEventListener("mousemove", onMove, true);
65
+ document.removeEventListener("click", onClick, true);
66
+ document.removeEventListener("keydown", onKey, true);
67
+ overlay.remove();
68
+ banner.remove();
69
+ selectedElements.forEach((el) => {
70
+ el.style.outline = "";
71
+ });
72
+ };
73
+
74
+ const onMove = (e) => {
75
+ const el = document.elementFromPoint(e.clientX, e.clientY);
76
+ if (!el || overlay.contains(el) || banner.contains(el)) return;
77
+ const r = el.getBoundingClientRect();
78
+ highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${r.top}px;left:${r.left}px;width:${r.width}px;height:${r.height}px`;
79
+ };
80
+
81
+ const buildElementInfo = (el) => {
82
+ const parents = [];
83
+ let current = el.parentElement;
84
+ while (current && current !== document.body) {
85
+ const parentInfo = current.tagName.toLowerCase();
86
+ const id = current.id ? `#${current.id}` : "";
87
+ const cls = current.className
88
+ ? `.${current.className.trim().split(/\s+/).join(".")}`
89
+ : "";
90
+ parents.push(parentInfo + id + cls);
91
+ current = current.parentElement;
92
+ }
93
+
94
+ return {
95
+ tag: el.tagName.toLowerCase(),
96
+ id: el.id || null,
97
+ class: el.className || null,
98
+ text: el.textContent?.trim().slice(0, 200) || null,
99
+ html: el.outerHTML.slice(0, 500),
100
+ parents: parents.join(" > "),
101
+ };
102
+ };
103
+
104
+ const onClick = (e) => {
105
+ if (banner.contains(e.target)) return;
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ const el = document.elementFromPoint(e.clientX, e.clientY);
109
+ if (!el || overlay.contains(el) || banner.contains(el)) return;
110
+
111
+ if (e.metaKey || e.ctrlKey) {
112
+ if (!selectedElements.has(el)) {
113
+ selectedElements.add(el);
114
+ el.style.outline = "3px solid #10b981";
115
+ selections.push(buildElementInfo(el));
116
+ updateBanner();
117
+ }
118
+ } else {
119
+ cleanup();
120
+ const info = buildElementInfo(el);
121
+ resolve(selections.length > 0 ? selections : info);
122
+ }
123
+ };
124
+
125
+ const onKey = (e) => {
126
+ if (e.key === "Escape") {
127
+ e.preventDefault();
128
+ cleanup();
129
+ resolve(null);
130
+ } else if (e.key === "Enter" && selections.length > 0) {
131
+ e.preventDefault();
132
+ cleanup();
133
+ resolve(selections);
134
+ }
135
+ };
136
+
137
+ document.addEventListener("mousemove", onMove, true);
138
+ document.addEventListener("click", onClick, true);
139
+ document.addEventListener("keydown", onKey, true);
140
+ });
141
+ };
142
+ }
143
+ });
144
+
145
+ const result = await page.evaluate((msg) => window.pick(msg), message);
146
+
147
+ if (Array.isArray(result)) {
148
+ for (let i = 0; i < result.length; i++) {
149
+ if (i > 0) console.log("");
150
+ for (const [key, value] of Object.entries(result[i])) {
151
+ console.log(`${key}: ${value}`);
152
+ }
153
+ }
154
+ } else if (typeof result === "object" && result !== null) {
155
+ for (const [key, value] of Object.entries(result)) {
156
+ console.log(`${key}: ${value}`);
157
+ }
158
+ } else {
159
+ console.log(result);
160
+ }
161
+
162
+ await browser.close();
package/screenshot.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { chromium } from "playwright";
6
+
7
+ const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
8
+
9
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
10
+ const contexts = browser.contexts();
11
+ const context = contexts[0];
12
+
13
+ if (!context) {
14
+ console.error("No browser context found");
15
+ process.exit(1);
16
+ }
17
+
18
+ const pages = context.pages();
19
+ const page = pages[pages.length - 1];
20
+
21
+ if (!page) {
22
+ console.error("No active tab found");
23
+ process.exit(1);
24
+ }
25
+
26
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
27
+ const filename = `screenshot-${timestamp}.png`;
28
+ const filepath = join(tmpdir(), filename);
29
+
30
+ await page.screenshot({ path: filepath });
31
+
32
+ console.log(filepath);
33
+
34
+ await browser.close();
package/start.js ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, execFileSync } from "node:child_process";
4
+ import { existsSync } from "node:fs";
5
+ import { platform } from "node:os";
6
+ import { chromium } from "playwright";
7
+
8
+ const isMac = platform() === "darwin";
9
+ const isLinux = platform() === "linux";
10
+
11
+ // Browser configurations per platform
12
+ const BROWSERS = {
13
+ chrome: {
14
+ name: "Google Chrome",
15
+ path: isMac
16
+ ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
17
+ : "/usr/bin/google-chrome",
18
+ process: "Google Chrome",
19
+ profileSource: isMac
20
+ ? `${process.env.HOME}/Library/Application Support/Google/Chrome/`
21
+ : `${process.env.HOME}/.config/google-chrome/`,
22
+ },
23
+ brave: {
24
+ name: "Brave",
25
+ path: isMac
26
+ ? "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
27
+ : "/usr/bin/brave-browser",
28
+ process: "Brave Browser",
29
+ profileSource: isMac
30
+ ? `${process.env.HOME}/Library/Application Support/BraveSoftware/Brave-Browser/`
31
+ : `${process.env.HOME}/.config/BraveSoftware/Brave-Browser/`,
32
+ },
33
+ comet: {
34
+ name: "Comet",
35
+ path: isMac ? "/Applications/Comet.app/Contents/MacOS/Comet" : "/usr/bin/comet",
36
+ process: "Comet",
37
+ profileSource: null,
38
+ },
39
+ edge: {
40
+ name: "Microsoft Edge",
41
+ path: isMac
42
+ ? "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
43
+ : "/usr/bin/microsoft-edge",
44
+ process: "Microsoft Edge",
45
+ profileSource: isMac
46
+ ? `${process.env.HOME}/Library/Application Support/Microsoft Edge/`
47
+ : `${process.env.HOME}/.config/microsoft-edge/`,
48
+ },
49
+ };
50
+
51
+ const DEFAULT_PORT = 9222;
52
+
53
+ function printUsage() {
54
+ console.log("Usage: start.js [browser] [--profile] [--port=PORT]");
55
+ console.log("\nBrowsers:");
56
+ console.log(" chrome - Google Chrome (default)");
57
+ console.log(" brave - Brave Browser");
58
+ console.log(" comet - Comet Browser");
59
+ console.log(" edge - Microsoft Edge");
60
+ console.log("\nOptions:");
61
+ console.log(" --profile Copy your browser profile (cookies, logins)");
62
+ console.log(" --port=N Use custom debugging port (default: 9222)");
63
+ console.log("\nEnvironment variables:");
64
+ console.log(" BROWSER Default browser (chrome, brave, comet, edge)");
65
+ console.log(" BROWSER_PATH Custom browser executable path");
66
+ console.log(" DEBUG_PORT Custom debugging port");
67
+ console.log("\nExamples:");
68
+ console.log(" start.js # Start Chrome with fresh profile");
69
+ console.log(" start.js brave --profile # Start Brave with your profile");
70
+ console.log(" start.js comet # Start Comet");
71
+ console.log(" start.js --port=9333 # Start Chrome on port 9333");
72
+ process.exit(1);
73
+ }
74
+
75
+ // Parse arguments
76
+ const args = process.argv.slice(2);
77
+ let browserName = process.env.BROWSER || "chrome";
78
+ let useProfile = false;
79
+ let port = parseInt(process.env.DEBUG_PORT) || DEFAULT_PORT;
80
+
81
+ for (const arg of args) {
82
+ if (arg === "--help" || arg === "-h") {
83
+ printUsage();
84
+ } else if (arg === "--profile") {
85
+ useProfile = true;
86
+ } else if (arg.startsWith("--port=")) {
87
+ port = parseInt(arg.split("=")[1]);
88
+ } else if (BROWSERS[arg]) {
89
+ browserName = arg;
90
+ } else if (!arg.startsWith("--")) {
91
+ console.error(`Unknown browser: ${arg}`);
92
+ console.log(`Available: ${Object.keys(BROWSERS).join(", ")}`);
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ // Resolve browser config
98
+ const browserPath = process.env.BROWSER_PATH || BROWSERS[browserName]?.path;
99
+ const browserConfig = BROWSERS[browserName] || {
100
+ name: "Custom",
101
+ path: browserPath,
102
+ process: null,
103
+ profileSource: null,
104
+ };
105
+
106
+ if (!browserPath) {
107
+ console.error("No browser path specified");
108
+ printUsage();
109
+ }
110
+
111
+ if (!existsSync(browserPath)) {
112
+ console.error(`Browser not found: ${browserPath}`);
113
+ process.exit(1);
114
+ }
115
+
116
+ // Check if port is available or has a browser with CDP
117
+ try {
118
+ const response = await fetch(`http://localhost:${port}/json/version`);
119
+ if (response.ok) {
120
+ // CDP endpoint responded - browser already running
121
+ console.log(`Browser already running on :${port}`);
122
+ process.exit(0);
123
+ } else {
124
+ // Port occupied by non-CDP process
125
+ console.error(`Error: Port ${port} is in use by another process (not a browser with CDP)`);
126
+ console.error(`Try a different port: ./start.js --port=9333`);
127
+ process.exit(1);
128
+ }
129
+ } catch (err) {
130
+ if (err.cause?.code === "ECONNREFUSED") {
131
+ // Port is free, proceed to start browser
132
+ } else {
133
+ // Port occupied but not responding to CDP
134
+ console.error(`Error: Port ${port} is in use by another process`);
135
+ console.error(`Try a different port: ./start.js --port=9333`);
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ // Setup profile directory
141
+ const cacheBase = isMac
142
+ ? `${process.env.HOME}/Library/Caches`
143
+ : `${process.env.HOME}/.cache`;
144
+ const profileDir = `${cacheBase}/browser-cdp/${browserName}`;
145
+ execFileSync("mkdir", ["-p", profileDir], { stdio: "ignore" });
146
+
147
+ if (useProfile && browserConfig.profileSource) {
148
+ console.log(`Syncing profile from ${browserConfig.profileSource}...`);
149
+ try {
150
+ execFileSync("rsync", ["-a", "--delete", browserConfig.profileSource, `${profileDir}/`], {
151
+ stdio: "pipe",
152
+ });
153
+ } catch {
154
+ console.warn("Warning: Could not sync profile, using fresh profile");
155
+ }
156
+ }
157
+
158
+ // Start browser
159
+ console.log(`Starting ${browserConfig.name} on port ${port}...`);
160
+
161
+ spawn(browserPath, [`--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`], {
162
+ detached: true,
163
+ stdio: "ignore",
164
+ }).unref();
165
+
166
+ // Wait for browser to be ready
167
+ let connected = false;
168
+ for (let i = 0; i < 30; i++) {
169
+ try {
170
+ const browser = await chromium.connectOverCDP(`http://localhost:${port}`);
171
+ await browser.close();
172
+ connected = true;
173
+ break;
174
+ } catch {
175
+ await new Promise((r) => setTimeout(r, 500));
176
+ }
177
+ }
178
+
179
+ if (!connected) {
180
+ console.error(`Failed to connect to ${browserConfig.name} on port ${port}`);
181
+ process.exit(1);
182
+ }
183
+
184
+ console.log(
185
+ `${browserConfig.name} started on :${port}${useProfile ? " with your profile" : ""}`
186
+ );