browser-cdp 0.3.0 → 0.6.1

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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # browser-cdp
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/browser-cdp.svg)](https://www.npmjs.com/package/browser-cdp)
4
+
3
5
  Browser automation via Chrome DevTools Protocol. Control Chrome, Brave, or Edge using your real browser - same fingerprint, real cookies, no automation detection.
4
6
 
5
7
  ## Install
@@ -12,7 +14,10 @@ npm install -g browser-cdp
12
14
 
13
15
  ```bash
14
16
  # Start browser with CDP enabled
15
- browser-cdp start [browser] [--isolated] [--port=PORT]
17
+ browser-cdp start [browser] [--profile=NAME] [--isolated] [--port=PORT]
18
+
19
+ # Close the browser
20
+ browser-cdp close
16
21
 
17
22
  # Navigate to URL
18
23
  browser-cdp nav <url> [--new]
@@ -23,14 +28,20 @@ browser-cdp eval '<code>'
23
28
  # Take screenshot
24
29
  browser-cdp screenshot
25
30
 
31
+ # Export page as PDF
32
+ browser-cdp pdf [--path=FILE] [--format=A4|Letter|Legal|Tabloid] [--landscape]
33
+
26
34
  # Interactive element picker
27
35
  browser-cdp pick '<message>'
28
36
 
29
- # Stream browser console output
37
+ # Stream browser console output (network errors, exceptions, logs)
30
38
  browser-cdp console [--duration=SECONDS]
31
39
 
32
40
  # Show page performance metrics
33
41
  browser-cdp insights [--json]
42
+
43
+ # Run Lighthouse audit (Chrome only)
44
+ browser-cdp lighthouse [--json] [--category=NAME]
34
45
  ```
35
46
 
36
47
  ## Environment Variables
@@ -47,6 +58,9 @@ browser-cdp insights [--json]
47
58
  # Start Brave with real profile
48
59
  browser-cdp start brave
49
60
 
61
+ # Start Brave with specific profile (by name)
62
+ browser-cdp start brave --profile=Work
63
+
50
64
  # Start Chrome on custom port
51
65
  DEBUG_PORT=9333 browser-cdp start
52
66
 
@@ -58,18 +72,35 @@ browser-cdp eval 'document.querySelector("textarea").value = "hello"'
58
72
  browser-cdp screenshot
59
73
  # Returns: /tmp/screenshot-2024-01-01T12-00-00.png
60
74
 
75
+ # Export page as PDF
76
+ browser-cdp pdf
77
+ # Returns: /tmp/pdf-2024-01-01T12-00-00.pdf
78
+
79
+ # Export to specific file in A4 landscape
80
+ browser-cdp pdf --path report.pdf --format A4 --landscape
81
+
61
82
  # Pick elements interactively
62
83
  browser-cdp pick "Select the login button"
63
84
 
64
- # Stream console output for 10 seconds
85
+ # Stream console output (captures network errors, exceptions, console.log)
86
+ browser-cdp console
87
+ # Then refresh the page to see errors
88
+
89
+ # Stream console for 10 seconds
65
90
  browser-cdp console --duration=10
66
91
 
67
92
  # Get page performance insights
68
93
  browser-cdp insights
69
94
  # Returns: TTFB, First Paint, FCP, DOM loaded, resources, memory
70
95
 
71
- # Get insights as JSON
72
- browser-cdp insights --json
96
+ # Run Lighthouse audit (Chrome only - Brave blocks CDP debugger)
97
+ browser-cdp start chrome --isolated
98
+ browser-cdp nav https://example.com
99
+ browser-cdp lighthouse
100
+ # Returns: Performance, Accessibility, Best Practices, SEO scores
101
+
102
+ # Close browser when done
103
+ browser-cdp close
73
104
  ```
74
105
 
75
106
  ## Pre-started Browser
@@ -116,6 +147,29 @@ Use `BROWSER_PATH` env var to override if your browser is installed elsewhere.
116
147
  | Detection | Not detectable as automation | Automation flags present |
117
148
  | Use case | Real-world testing, scraping | Isolated E2E tests |
118
149
 
150
+ ## Development
151
+
152
+ ```bash
153
+ # Install dependencies
154
+ bun install
155
+
156
+ # Run all tests
157
+ bun run test
158
+
159
+ # Run unit tests only (fast, no browser needed)
160
+ bun run test:unit
161
+
162
+ # Run integration tests (requires browser)
163
+ bun run test:integration
164
+
165
+ # Watch mode
166
+ bun run test:watch
167
+ ```
168
+
169
+ ## See Also
170
+
171
+ - [dev-browser](https://github.com/SawyerHood/dev-browser) - Browser automation plugin for Claude Code with LLM-optimized DOM snapshots
172
+
119
173
  ## License
120
174
 
121
175
  MIT
package/cli.js CHANGED
@@ -11,13 +11,17 @@ const command = process.argv[2];
11
11
  const args = process.argv.slice(3);
12
12
 
13
13
  const commands = {
14
- start: "./start.js",
15
- nav: "./nav.js",
16
- eval: "./eval.js",
17
- screenshot: "./screenshot.js",
18
- pick: "./pick.js",
19
- console: "./console.js",
20
- insights: "./insights.js",
14
+ start: "./src/start.js",
15
+ close: "./src/close.js",
16
+ nav: "./src/nav.js",
17
+ eval: "./src/eval.js",
18
+ dom: "./src/dom.js",
19
+ screenshot: "./src/screenshot.js",
20
+ pdf: "./src/pdf.js",
21
+ pick: "./src/pick.js",
22
+ console: "./src/console.js",
23
+ insights: "./src/insights.js",
24
+ lighthouse: "./src/lighthouse.js",
21
25
  };
22
26
 
23
27
  function printUsage() {
@@ -27,12 +31,16 @@ function printUsage() {
27
31
  console.log("");
28
32
  console.log("Commands:");
29
33
  console.log(" start [browser] Start browser with CDP (uses real profile)");
30
- console.log(" nav <url> Navigate to URL");
31
- console.log(" eval '<code>' Evaluate JavaScript in page");
34
+ console.log(" close Close the browser");
35
+ console.log(" nav <url> Navigate to URL (--console to capture logs)");
36
+ console.log(" eval '<code>' Evaluate JS in page (--console to capture logs)");
37
+ console.log(" dom Capture full page DOM/HTML");
32
38
  console.log(" screenshot Take screenshot of current page");
39
+ console.log(" pdf Export current page as PDF");
33
40
  console.log(" pick '<message>' Interactive element picker");
34
41
  console.log(" console Stream browser console output");
35
42
  console.log(" insights Show page performance metrics");
43
+ console.log(" lighthouse Run Lighthouse audit");
36
44
  console.log("");
37
45
  console.log("Environment:");
38
46
  console.log(" DEBUG_PORT CDP port (default: 9222)");
@@ -43,6 +51,7 @@ function printUsage() {
43
51
  console.log(" browser-cdp start brave");
44
52
  console.log(" browser-cdp nav https://google.com");
45
53
  console.log(" browser-cdp eval 'document.title'");
54
+ console.log(" browser-cdp dom > page.html");
46
55
  console.log(" browser-cdp console --duration=10");
47
56
  console.log(" browser-cdp insights --json");
48
57
  process.exit(0);
package/package.json CHANGED
@@ -1,23 +1,21 @@
1
1
  {
2
2
  "name": "browser-cdp",
3
- "version": "0.3.0",
3
+ "version": "0.6.1",
4
4
  "description": "Browser automation via Chrome DevTools Protocol - control Chrome, Brave, Edge with real browser profiles",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "browser-cdp": "./cli.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node cli.js"
10
+ "start": "node cli.js",
11
+ "test": "node --test 'tests/*.test.js'",
12
+ "test:unit": "node --test tests/utils.test.js",
13
+ "test:integration": "node --test tests/commands.test.js",
14
+ "test:watch": "node --test --watch tests/"
11
15
  },
12
16
  "files": [
13
17
  "cli.js",
14
- "start.js",
15
- "nav.js",
16
- "eval.js",
17
- "screenshot.js",
18
- "pick.js",
19
- "console.js",
20
- "insights.js",
18
+ "src/",
21
19
  "README.md",
22
20
  "LICENSE"
23
21
  ],
@@ -33,6 +31,7 @@
33
31
  "url": "https://github.com/dpaluy/browser-cdp/issues"
34
32
  },
35
33
  "dependencies": {
34
+ "lighthouse": "^13.0.1",
36
35
  "playwright": "^1.49.0"
37
36
  },
38
37
  "keywords": [
package/src/close.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ import WebSocket from "ws";
4
+ import { DEFAULT_PORT } from "./utils.js";
5
+
6
+ try {
7
+ // Use CDP Browser.close to actually quit the browser
8
+ const res = await fetch(`http://localhost:${DEFAULT_PORT}/json/version`);
9
+ const { webSocketDebuggerUrl } = await res.json();
10
+
11
+ const socket = new WebSocket(webSocketDebuggerUrl);
12
+
13
+ await new Promise((resolve, reject) => {
14
+ socket.on("open", () => {
15
+ socket.send(JSON.stringify({ id: 1, method: "Browser.close" }));
16
+ });
17
+ socket.on("message", () => {
18
+ resolve();
19
+ });
20
+ socket.on("error", reject);
21
+ setTimeout(() => resolve(), 2000); // Browser closes before responding
22
+ });
23
+
24
+ console.log("Browser closed");
25
+ } catch (err) {
26
+ if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
27
+ console.log("No browser running on port", DEFAULT_PORT);
28
+ } else {
29
+ throw err;
30
+ }
31
+ }
package/src/console.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chromium } from "playwright";
4
+ import { DEFAULT_PORT, getActivePage, formatTime, levelColors, resetColor } from "./utils.js";
5
+
6
+ const args = process.argv.slice(2);
7
+ const duration = args.find((a) => a.startsWith("--duration="));
8
+ const durationMs = duration ? parseInt(duration.split("=")[1]) * 1000 : null;
9
+ const shouldReload = args.includes("--reload") || args.includes("-r");
10
+ const showHelp = args.includes("--help") || args.includes("-h");
11
+
12
+ if (showHelp) {
13
+ console.log("Usage: console.js [options]");
14
+ console.log("\nCapture browser console output in real-time.");
15
+ console.log("\nOptions:");
16
+ console.log(" --duration=N Stop after N seconds (default: run until Ctrl+C)");
17
+ console.log(" --reload, -r Reload the page before capturing");
18
+ console.log("\nExamples:");
19
+ console.log(" console.js # Stream console logs until Ctrl+C");
20
+ console.log(" console.js --duration=5 # Capture for 5 seconds");
21
+ console.log(" console.js --reload # Reload page and capture logs");
22
+ process.exit(0);
23
+ }
24
+
25
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
26
+ const contexts = browser.contexts();
27
+ const context = contexts[0];
28
+
29
+ if (!context) {
30
+ console.error("No browser context found");
31
+ process.exit(1);
32
+ }
33
+
34
+ const pages = context.pages();
35
+ const page = getActivePage(pages);
36
+
37
+ if (!page) {
38
+ console.error("No active tab found");
39
+ process.exit(1);
40
+ }
41
+
42
+ console.error(`Connected to: ${page.url()}`);
43
+
44
+ // Use CDP directly for Log domain (captures network errors, etc.)
45
+ const cdp = await page.context().newCDPSession(page);
46
+ await cdp.send("Log.enable");
47
+
48
+ cdp.on("Log.entryAdded", ({ entry }) => {
49
+ const color = levelColors[entry.level] || levelColors.info;
50
+ const source = entry.source ? `[${entry.source}]` : "";
51
+ console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${resetColor}`);
52
+ if (entry.url) {
53
+ console.log(`${color} URL: ${entry.url}${resetColor}`);
54
+ }
55
+ });
56
+
57
+ // Also capture runtime exceptions
58
+ await cdp.send("Runtime.enable");
59
+ cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
60
+ const text = exceptionDetails.exception?.description || exceptionDetails.text;
61
+ console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${resetColor}`);
62
+ });
63
+
64
+ // Capture network failures (ERR_BLOCKED_BY_CLIENT, etc.)
65
+ await cdp.send("Network.enable");
66
+ cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
67
+ const reason = blockedReason ? ` (${blockedReason})` : "";
68
+ console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${resetColor}`);
69
+ });
70
+
71
+ // Keep Playwright listeners for console.log() calls
72
+ page.on("console", (msg) => {
73
+ const type = msg.type();
74
+ const color = levelColors[type] || levelColors.info;
75
+ const text = msg.text();
76
+ console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${text}${resetColor}`);
77
+ });
78
+
79
+ page.on("pageerror", (error) => {
80
+ console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${resetColor}`);
81
+ });
82
+
83
+ if (shouldReload) {
84
+ console.error("Reloading page...");
85
+ await page.reload();
86
+ }
87
+
88
+ console.error(`Listening for console output... (Ctrl+C to stop)`);
89
+
90
+ if (durationMs) {
91
+ await new Promise((r) => setTimeout(r, durationMs));
92
+ await browser.close();
93
+ } else {
94
+ // Keep running until interrupted
95
+ process.on("SIGINT", async () => {
96
+ console.error("\nStopping...");
97
+ await browser.close();
98
+ process.exit(0);
99
+ });
100
+
101
+ // Keep the process alive
102
+ await new Promise(() => {});
103
+ }
package/src/dom.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chromium } from "playwright";
4
+ import { DEFAULT_PORT, getActivePage } from "./utils.js";
5
+
6
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
7
+ const contexts = browser.contexts();
8
+ const context = contexts[0];
9
+
10
+ if (!context) {
11
+ console.error("No browser context found");
12
+ process.exit(1);
13
+ }
14
+
15
+ const pages = context.pages();
16
+ const page = getActivePage(pages);
17
+
18
+ if (!page) {
19
+ console.error("No active tab found");
20
+ process.exit(1);
21
+ }
22
+
23
+ const html = await page.content();
24
+ console.log(html);
25
+
26
+ await browser.close();
package/src/eval.js ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chromium } from "playwright";
4
+ import { DEFAULT_PORT, getActivePage, formatTime, levelColors, resetColor } from "./utils.js";
5
+
6
+ const args = process.argv.slice(2);
7
+ const showHelp = args.includes("--help") || args.includes("-h");
8
+ const captureConsole = args.includes("--console");
9
+ const durationArg = args.find((a) => a.startsWith("--duration="));
10
+ const durationMs = durationArg ? parseInt(durationArg.split("=")[1]) * 1000 : 3000;
11
+ const code = args.filter((a) => !a.startsWith("--")).join(" ");
12
+
13
+ if (showHelp || !code) {
14
+ console.log("Usage: eval.js '<code>' [options]");
15
+ console.log("\nOptions:");
16
+ console.log(" --console Capture console output during evaluation");
17
+ console.log(" --duration=N With --console, capture for N seconds (default: 3)");
18
+ console.log("\nExamples:");
19
+ console.log(' eval.js "document.title"');
20
+ console.log(" eval.js \"document.querySelectorAll('a').length\"");
21
+ console.log(" eval.js \"fetch('/api/data')\" --console");
22
+ process.exit(showHelp ? 0 : 1);
23
+ }
24
+
25
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
26
+ const contexts = browser.contexts();
27
+ const context = contexts[0];
28
+
29
+ if (!context) {
30
+ console.error("No browser context found");
31
+ process.exit(1);
32
+ }
33
+
34
+ const pages = context.pages();
35
+ const page = getActivePage(pages);
36
+
37
+ if (!page) {
38
+ console.error("No active tab found");
39
+ process.exit(1);
40
+ }
41
+
42
+ if (captureConsole) {
43
+ const cdp = await page.context().newCDPSession(page);
44
+ await cdp.send("Log.enable");
45
+ await cdp.send("Runtime.enable");
46
+ await cdp.send("Network.enable");
47
+
48
+ cdp.on("Log.entryAdded", ({ entry }) => {
49
+ const color = levelColors[entry.level] || levelColors.info;
50
+ const source = entry.source ? `[${entry.source}]` : "";
51
+ console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${resetColor}`);
52
+ if (entry.url) {
53
+ console.log(`${color} URL: ${entry.url}${resetColor}`);
54
+ }
55
+ });
56
+
57
+ cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
58
+ const text = exceptionDetails.exception?.description || exceptionDetails.text;
59
+ console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${resetColor}`);
60
+ });
61
+
62
+ cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
63
+ const reason = blockedReason ? ` (${blockedReason})` : "";
64
+ console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${resetColor}`);
65
+ });
66
+
67
+ page.on("console", (msg) => {
68
+ const type = msg.type();
69
+ const color = levelColors[type] || levelColors.info;
70
+ console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${msg.text()}${resetColor}`);
71
+ });
72
+
73
+ page.on("pageerror", (error) => {
74
+ console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${resetColor}`);
75
+ });
76
+ }
77
+
78
+ let result;
79
+
80
+ try {
81
+ result = await page.evaluate((c) => {
82
+ const AsyncFunction = (async () => {}).constructor;
83
+ return new AsyncFunction(`return (${c})`)();
84
+ }, code);
85
+ } catch (e) {
86
+ console.log("Failed to evaluate expression");
87
+ console.log(` Expression: ${code}`);
88
+ console.log(e);
89
+ process.exit(1);
90
+ }
91
+
92
+ if (Array.isArray(result)) {
93
+ for (let i = 0; i < result.length; i++) {
94
+ if (i > 0) console.log("");
95
+ for (const [key, value] of Object.entries(result[i])) {
96
+ console.log(`${key}: ${value}`);
97
+ }
98
+ }
99
+ } else if (typeof result === "object" && result !== null) {
100
+ for (const [key, value] of Object.entries(result)) {
101
+ console.log(`${key}: ${value}`);
102
+ }
103
+ } else {
104
+ console.log(result);
105
+ }
106
+
107
+ if (captureConsole) {
108
+ console.error(`\nListening for ${durationMs / 1000}s...`);
109
+ await new Promise((r) => setTimeout(r, durationMs));
110
+ }
111
+
112
+ await browser.close();
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { chromium } from "playwright";
4
-
5
- const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
4
+ import { DEFAULT_PORT, getActivePage } from "./utils.js";
6
5
 
7
6
  const args = process.argv.slice(2);
8
7
  const showHelp = args.includes("--help") || args.includes("-h");
@@ -31,7 +30,7 @@ if (!context) {
31
30
  }
32
31
 
33
32
  const pages = context.pages();
34
- const page = pages[pages.length - 1];
33
+ const page = getActivePage(pages);
35
34
 
36
35
  if (!page) {
37
36
  console.error("No active tab found");
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+
3
+ import lighthouse from "lighthouse";
4
+ import { chromium } from "playwright";
5
+ import { DEFAULT_PORT } from "./utils.js";
6
+
7
+ const args = process.argv.slice(2);
8
+ const showHelp = args.includes("--help") || args.includes("-h");
9
+ const jsonOutput = args.includes("--json");
10
+ const category = args.find((a) => a.startsWith("--category="))?.split("=")[1];
11
+
12
+ if (showHelp) {
13
+ console.log("Usage: lighthouse.js [--json] [--category=NAME]");
14
+ console.log("\nRun Lighthouse audit on the current page.");
15
+ console.log("\nOptions:");
16
+ console.log(" --json Output full JSON report");
17
+ console.log(" --category=NAME Run specific category only");
18
+ console.log(" (performance, accessibility, best-practices, seo)");
19
+ console.log("\nExamples:");
20
+ console.log(" lighthouse.js # Full audit");
21
+ console.log(" lighthouse.js --category=performance # Performance only");
22
+ console.log(" lighthouse.js --json # JSON output");
23
+ process.exit(0);
24
+ }
25
+
26
+ // Get current page URL and check for DevTools
27
+ const targetsRes = await fetch(`http://localhost:${DEFAULT_PORT}/json`);
28
+ const targets = await targetsRes.json();
29
+
30
+ const httpPages = targets.filter(t =>
31
+ t.type === "page" && (t.url.startsWith("http://") || t.url.startsWith("https://"))
32
+ );
33
+ const devtoolsPages = targets.filter(t =>
34
+ t.type === "page" && t.url.startsWith("devtools://")
35
+ );
36
+
37
+ if (httpPages.length === 0) {
38
+ console.error("No HTTP page found to audit");
39
+ process.exit(1);
40
+ }
41
+
42
+ const targetPage = httpPages[0];
43
+ const url = targetPage.url;
44
+
45
+ // Check if DevTools is open (it will block Lighthouse)
46
+ if (devtoolsPages.length > 0) {
47
+ console.error("⚠️ DevTools is open - please close it first (Cmd+Option+I)");
48
+ console.error(" Lighthouse needs exclusive debugger access to run audits.");
49
+ process.exit(1);
50
+ }
51
+
52
+ if (!url.startsWith("http")) {
53
+ console.error(`Cannot audit non-HTTP URL: ${url}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ console.error(`Running Lighthouse audit on ${url}...`);
58
+
59
+ // Navigate existing same-origin pages away to prevent conflicts
60
+ // Lighthouse creates a new tab, and existing same-origin tabs block debugger access
61
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
62
+ const pages = browser.contexts()[0]?.pages() || [];
63
+ for (const page of pages) {
64
+ const pageUrl = page.url();
65
+ if (pageUrl.startsWith("http") && new URL(pageUrl).origin === new URL(url).origin) {
66
+ console.error("(Navigating existing tab away to avoid conflicts)");
67
+ await page.goto("about:blank");
68
+ }
69
+ }
70
+ await browser.close();
71
+
72
+ let result;
73
+ try {
74
+ result = await lighthouse(
75
+ url,
76
+ {
77
+ port: DEFAULT_PORT,
78
+ output: "json",
79
+ logLevel: "error",
80
+ }
81
+ );
82
+ } catch (err) {
83
+ if (err.message?.includes("Script execution is prohibited")) {
84
+ console.error("\n❌ Lighthouse failed - browser blocks debugger access");
85
+ console.error("\nBrave browser blocks CDP Debugger.enable. Use Chrome instead:");
86
+ console.error(" 1. Close Brave");
87
+ console.error(" 2. browser-cdp start chrome --isolated");
88
+ console.error(" 3. browser-cdp nav <url>");
89
+ console.error(" 4. browser-cdp lighthouse");
90
+ process.exit(1);
91
+ }
92
+ throw err;
93
+ }
94
+
95
+ if (!result) {
96
+ console.error("Lighthouse audit failed");
97
+ process.exit(1);
98
+ }
99
+
100
+ const { lhr } = result;
101
+
102
+ if (jsonOutput) {
103
+ console.log(JSON.stringify(lhr, null, 2));
104
+ } else {
105
+ console.log(`\nLighthouse Report: ${lhr.finalDisplayedUrl}\n`);
106
+
107
+ // Scores
108
+ console.log("Scores:");
109
+ for (const [key, cat] of Object.entries(lhr.categories)) {
110
+ const score = Math.round((cat.score || 0) * 100);
111
+ const bar = getScoreBar(score);
112
+ console.log(` ${cat.title.padEnd(20)} ${bar} ${score}`);
113
+ }
114
+
115
+ // Top opportunities (performance)
116
+ if (lhr.categories.performance && lhr.audits) {
117
+ const opportunities = Object.values(lhr.audits)
118
+ .filter((a) => a.details?.type === "opportunity" && a.score !== null && a.score < 1)
119
+ .sort((a, b) => (a.score || 0) - (b.score || 0))
120
+ .slice(0, 5);
121
+
122
+ if (opportunities.length > 0) {
123
+ console.log("\nTop Opportunities:");
124
+ for (const opp of opportunities) {
125
+ const savings = opp.details?.overallSavingsMs
126
+ ? ` (${Math.round(opp.details.overallSavingsMs)}ms)`
127
+ : "";
128
+ console.log(` - ${opp.title}${savings}`);
129
+ }
130
+ }
131
+ }
132
+
133
+ // Failed audits summary
134
+ const failed = Object.values(lhr.audits)
135
+ .filter((a) => a.score === 0)
136
+ .slice(0, 5);
137
+
138
+ if (failed.length > 0) {
139
+ console.log("\nFailed Audits:");
140
+ for (const audit of failed) {
141
+ console.log(` ✗ ${audit.title}`);
142
+ }
143
+ }
144
+ }
145
+
146
+ function getScoreBar(score) {
147
+ const color = score >= 90 ? "\x1b[32m" : score >= 50 ? "\x1b[33m" : "\x1b[31m";
148
+ const reset = "\x1b[0m";
149
+ const filled = Math.round(score / 10);
150
+ const empty = 10 - filled;
151
+ return `${color}${"█".repeat(filled)}${"░".repeat(empty)}${reset}`;
152
+ }
package/src/nav.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chromium } from "playwright";
4
+ import { DEFAULT_PORT, normalizeUrl, getActivePage, formatTime, levelColors, resetColor } from "./utils.js";
5
+
6
+ const args = process.argv.slice(2);
7
+ const showHelp = args.includes("--help") || args.includes("-h");
8
+ const newTab = args.includes("--new");
9
+ const captureConsole = args.includes("--console");
10
+ const durationArg = args.find((a) => a.startsWith("--duration="));
11
+ const durationMs = durationArg ? parseInt(durationArg.split("=")[1]) * 1000 : 5000;
12
+
13
+ // Get URL (first arg that doesn't start with --)
14
+ let url = args.find((a) => !a.startsWith("--"));
15
+
16
+ if (showHelp || !url) {
17
+ console.log("Usage: nav.js <url> [options]");
18
+ console.log("\nOptions:");
19
+ console.log(" --new Open in new tab");
20
+ console.log(" --console Capture console output during navigation");
21
+ console.log(" --duration=N With --console, capture for N seconds (default: 5)");
22
+ console.log("\nExamples:");
23
+ console.log(" nav.js example.com # Navigate current tab");
24
+ console.log(" nav.js example.com --new # Open in new tab");
25
+ console.log(" nav.js example.com --console # Navigate and capture console");
26
+ process.exit(showHelp ? 0 : 1);
27
+ }
28
+
29
+ url = normalizeUrl(url);
30
+
31
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
32
+ const contexts = browser.contexts();
33
+ const context = contexts[0] || await browser.newContext();
34
+
35
+ let page;
36
+ if (newTab) {
37
+ page = await context.newPage();
38
+ } else {
39
+ const pages = context.pages();
40
+ const realPages = pages.filter(p => {
41
+ const u = p.url();
42
+ return u.startsWith("http://") || u.startsWith("https://") || u === "about:blank";
43
+ });
44
+ page = realPages[realPages.length - 1] || pages[pages.length - 1] || await context.newPage();
45
+ }
46
+
47
+ if (captureConsole) {
48
+ const cdp = await page.context().newCDPSession(page);
49
+ await cdp.send("Log.enable");
50
+ await cdp.send("Runtime.enable");
51
+ await cdp.send("Network.enable");
52
+
53
+ cdp.on("Log.entryAdded", ({ entry }) => {
54
+ const color = levelColors[entry.level] || levelColors.info;
55
+ const source = entry.source ? `[${entry.source}]` : "";
56
+ console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${resetColor}`);
57
+ if (entry.url) {
58
+ console.log(`${color} URL: ${entry.url}${resetColor}`);
59
+ }
60
+ });
61
+
62
+ cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
63
+ const text = exceptionDetails.exception?.description || exceptionDetails.text;
64
+ console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${resetColor}`);
65
+ });
66
+
67
+ cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
68
+ const reason = blockedReason ? ` (${blockedReason})` : "";
69
+ console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${resetColor}`);
70
+ });
71
+
72
+ page.on("console", (msg) => {
73
+ const type = msg.type();
74
+ const color = levelColors[type] || levelColors.info;
75
+ console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${msg.text()}${resetColor}`);
76
+ });
77
+
78
+ page.on("pageerror", (error) => {
79
+ console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${resetColor}`);
80
+ });
81
+
82
+ console.error(`Navigating to ${url} (capturing console for ${durationMs / 1000}s)...`);
83
+ }
84
+
85
+ await page.goto(url, { waitUntil: "domcontentloaded" });
86
+
87
+ if (captureConsole) {
88
+ console.error(`Loaded: ${url}`);
89
+ await new Promise((r) => setTimeout(r, durationMs));
90
+ console.error("Done.");
91
+ } else {
92
+ console.log(newTab ? "Opened:" : "Navigated to:", url);
93
+ }
94
+
95
+ await browser.close();
package/src/pdf.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { chromium } from "playwright";
7
+ import { DEFAULT_PORT, getActivePage } from "./utils.js";
8
+
9
+ const args = process.argv.slice(2);
10
+
11
+ if (args.includes("--help") || args.includes("-h")) {
12
+ console.log("Usage: pdf [options]");
13
+ console.log("");
14
+ console.log("Options:");
15
+ console.log(" --path <file> Output path (default: temp dir with timestamp)");
16
+ console.log(" --format <format> Paper size: A4, Letter, Legal, Tabloid (default: Letter)");
17
+ console.log(" --landscape Landscape orientation (default: portrait)");
18
+ console.log("");
19
+ console.log("Examples:");
20
+ console.log(" pdf");
21
+ console.log(" pdf --path output.pdf");
22
+ console.log(" pdf --format A4 --landscape");
23
+ process.exit(0);
24
+ }
25
+
26
+ const PAPER_FORMATS = {
27
+ A4: { width: 8.27, height: 11.69 },
28
+ LETTER: { width: 8.5, height: 11 },
29
+ LEGAL: { width: 8.5, height: 14 },
30
+ TABLOID: { width: 11, height: 17 },
31
+ };
32
+
33
+ const pathIdx = args.findIndex((a) => a === "--path");
34
+ const formatIdx = args.findIndex((a) => a === "--format");
35
+ const landscape = args.includes("--landscape");
36
+
37
+ const customPath = pathIdx !== -1 ? args[pathIdx + 1] : null;
38
+ const format = formatIdx !== -1 ? args[formatIdx + 1]?.toUpperCase() : "LETTER";
39
+ const paperSize = PAPER_FORMATS[format] || PAPER_FORMATS.LETTER;
40
+
41
+ const outputPath =
42
+ customPath ||
43
+ join(tmpdir(), `pdf-${new Date().toISOString().replace(/[:.]/g, "-")}.pdf`);
44
+
45
+ const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
46
+ const contexts = browser.contexts();
47
+ const context = contexts[0];
48
+
49
+ if (!context) {
50
+ console.error("No browser context found");
51
+ process.exit(1);
52
+ }
53
+
54
+ const pages = context.pages();
55
+ const page = getActivePage(pages);
56
+
57
+ if (!page) {
58
+ console.error("No active tab found");
59
+ process.exit(1);
60
+ }
61
+
62
+ const cdp = await context.newCDPSession(page);
63
+ const { data } = await cdp.send("Page.printToPDF", {
64
+ printBackground: true,
65
+ paperWidth: paperSize.width,
66
+ paperHeight: paperSize.height,
67
+ landscape,
68
+ marginTop: 0.4,
69
+ marginBottom: 0.4,
70
+ marginLeft: 0.4,
71
+ marginRight: 0.4,
72
+ });
73
+
74
+ writeFileSync(outputPath, Buffer.from(data, "base64"));
75
+
76
+ console.log(outputPath);
77
+
78
+ await browser.close();
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { chromium } from "playwright";
4
-
5
- const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
4
+ import { DEFAULT_PORT, getActivePage } from "./utils.js";
6
5
 
7
6
  const message = process.argv.slice(2).join(" ");
8
7
  if (!message) {
@@ -22,7 +21,7 @@ if (!context) {
22
21
  }
23
22
 
24
23
  const pages = context.pages();
25
- const page = pages[pages.length - 1];
24
+ const page = getActivePage(pages);
26
25
 
27
26
  if (!page) {
28
27
  console.error("No active tab found");
@@ -3,8 +3,7 @@
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { chromium } from "playwright";
6
-
7
- const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
6
+ import { DEFAULT_PORT, getActivePage } from "./utils.js";
8
7
 
9
8
  const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
10
9
  const contexts = browser.contexts();
@@ -16,7 +15,7 @@ if (!context) {
16
15
  }
17
16
 
18
17
  const pages = context.pages();
19
- const page = pages[pages.length - 1];
18
+ const page = getActivePage(pages);
20
19
 
21
20
  if (!page) {
22
21
  console.error("No active tab found");
@@ -2,80 +2,45 @@
2
2
 
3
3
  import { spawn, execFileSync } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
- import { platform } from "node:os";
6
5
  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
- edge: {
34
- name: "Microsoft Edge",
35
- path: isMac
36
- ? "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
37
- : "/usr/bin/microsoft-edge",
38
- process: "Microsoft Edge",
39
- profileSource: isMac
40
- ? `${process.env.HOME}/Library/Application Support/Microsoft Edge/`
41
- : `${process.env.HOME}/.config/microsoft-edge/`,
42
- },
43
- };
44
-
45
- const DEFAULT_PORT = 9222;
6
+ import { BROWSERS, DEFAULT_PORT, isMac, resolveProfileDir } from "./utils.js";
46
7
 
47
8
  function printUsage() {
48
- console.log("Usage: start.js [browser] [--isolated] [--port=PORT]");
9
+ console.log("Usage: start.js [browser] [--profile=NAME] [--isolated] [--port=PORT]");
49
10
  console.log("\nBrowsers:");
50
11
  console.log(" chrome - Google Chrome (default)");
51
12
  console.log(" brave - Brave Browser");
52
13
  console.log(" edge - Microsoft Edge");
53
14
  console.log("\nOptions:");
54
- console.log(" --isolated Use isolated profile (default: real profile)");
55
- console.log(" --port=N Use custom debugging port (default: 9222)");
15
+ console.log(" --profile=NAME Use specific profile by name or directory");
16
+ console.log(" --isolated Use isolated profile (default: real profile)");
17
+ console.log(" --port=N Use custom debugging port (default: 9222)");
56
18
  console.log("\nEnvironment variables:");
57
19
  console.log(" BROWSER Default browser (chrome, brave, edge)");
58
20
  console.log(" BROWSER_PATH Custom browser executable path");
59
21
  console.log(" DEBUG_PORT Custom debugging port");
60
22
  console.log("\nExamples:");
61
- console.log(" start.js # Start Chrome with real profile");
62
- console.log(" start.js brave # Start Brave with real profile");
63
- console.log(" start.js edge --isolated # Start Edge with isolated profile");
64
- console.log(" start.js --port=9333 # Start Chrome on port 9333");
23
+ console.log(" start.js # Start Chrome with default profile");
24
+ console.log(" start.js brave # Start Brave with default profile");
25
+ console.log(" start.js brave --profile=Work # Start Brave with 'Work' profile");
26
+ console.log(" start.js edge --isolated # Start Edge with isolated profile");
27
+ console.log(" start.js --port=9333 # Start Chrome on port 9333");
65
28
  process.exit(1);
66
29
  }
67
30
 
68
- // Parse arguments
69
31
  const args = process.argv.slice(2);
70
32
  let browserName = process.env.BROWSER || "chrome";
71
33
  let isolated = false;
72
- let port = parseInt(process.env.DEBUG_PORT) || DEFAULT_PORT;
34
+ let profile = null;
35
+ let port = DEFAULT_PORT;
73
36
 
74
37
  for (const arg of args) {
75
38
  if (arg === "--help" || arg === "-h") {
76
39
  printUsage();
77
40
  } else if (arg === "--isolated") {
78
41
  isolated = true;
42
+ } else if (arg.startsWith("--profile=")) {
43
+ profile = arg.split("=")[1];
79
44
  } else if (arg.startsWith("--port=")) {
80
45
  port = parseInt(arg.split("=")[1]);
81
46
  } else if (BROWSERS[arg]) {
@@ -87,7 +52,6 @@ for (const arg of args) {
87
52
  }
88
53
  }
89
54
 
90
- // Resolve browser config
91
55
  const browserPath = process.env.BROWSER_PATH || BROWSERS[browserName]?.path;
92
56
  const browserConfig = BROWSERS[browserName] || {
93
57
  name: "Custom",
@@ -154,8 +118,11 @@ if (!isolated && browserConfig.process) {
154
118
  }
155
119
  }
156
120
 
157
- // Build browser arguments
158
- const browserArgs = [`--remote-debugging-port=${port}`];
121
+ const browserArgs = [
122
+ `--remote-debugging-port=${port}`,
123
+ // Required for Lighthouse/CDP debugger access (prevents bfcache blocking)
124
+ "--disable-features=ProcessPerSiteUpToMainFrameThreshold",
125
+ ];
159
126
 
160
127
  if (isolated) {
161
128
  const cacheBase = isMac
@@ -164,17 +131,20 @@ if (isolated) {
164
131
  const profileDir = `${cacheBase}/browser-cdp/${browserName}`;
165
132
  execFileSync("mkdir", ["-p", profileDir], { stdio: "ignore" });
166
133
  browserArgs.push(`--user-data-dir=${profileDir}`);
134
+ } else if (profile) {
135
+ // Resolve profile name to directory if needed
136
+ const profileDir = resolveProfileDir(browserConfig.profileSource, profile);
137
+ browserArgs.push(`--profile-directory=${profileDir}`);
167
138
  }
168
139
 
169
- // Start browser
170
- console.log(`Starting ${browserConfig.name} on port ${port}${isolated ? " (isolated)" : ""}...`);
140
+ const profileInfo = isolated ? " (isolated)" : profile ? ` (${profile})` : "";
141
+ console.log(`Starting ${browserConfig.name} on port ${port}${profileInfo}...`);
171
142
 
172
143
  spawn(browserPath, browserArgs, {
173
144
  detached: true,
174
145
  stdio: "ignore",
175
146
  }).unref();
176
147
 
177
- // Wait for browser to be ready
178
148
  let connected = false;
179
149
  for (let i = 0; i < 30; i++) {
180
150
  try {
@@ -192,4 +162,4 @@ if (!connected) {
192
162
  process.exit(1);
193
163
  }
194
164
 
195
- console.log(`${browserConfig.name} started on :${port}${isolated ? " (isolated)" : ""}`);
165
+ console.log(`${browserConfig.name} started on :${port}${profileInfo}`);
package/src/utils.js ADDED
@@ -0,0 +1,118 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { platform } from "node:os";
3
+
4
+ export const isMac = platform() === "darwin";
5
+ export const isLinux = platform() === "linux";
6
+
7
+ export const DEFAULT_PORT = parseInt(process.env.DEBUG_PORT) || 9222;
8
+
9
+ // Browser configurations per platform
10
+ export const BROWSERS = {
11
+ chrome: {
12
+ name: "Google Chrome",
13
+ path: isMac
14
+ ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
15
+ : "/usr/bin/google-chrome",
16
+ process: "Google Chrome",
17
+ profileSource: isMac
18
+ ? `${process.env.HOME}/Library/Application Support/Google/Chrome/`
19
+ : `${process.env.HOME}/.config/google-chrome/`,
20
+ },
21
+ brave: {
22
+ name: "Brave",
23
+ path: isMac
24
+ ? "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
25
+ : "/usr/bin/brave-browser",
26
+ process: "Brave Browser",
27
+ profileSource: isMac
28
+ ? `${process.env.HOME}/Library/Application Support/BraveSoftware/Brave-Browser/`
29
+ : `${process.env.HOME}/.config/BraveSoftware/Brave-Browser/`,
30
+ },
31
+ edge: {
32
+ name: "Microsoft Edge",
33
+ path: isMac
34
+ ? "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
35
+ : "/usr/bin/microsoft-edge",
36
+ process: "Microsoft Edge",
37
+ profileSource: isMac
38
+ ? `${process.env.HOME}/Library/Application Support/Microsoft Edge/`
39
+ : `${process.env.HOME}/.config/microsoft-edge/`,
40
+ },
41
+ };
42
+
43
+ export function resolveProfileDir(profileSource, profileName) {
44
+ // If it looks like a directory name already, use it
45
+ if (profileName === "Default" || profileName.startsWith("Profile ")) {
46
+ return profileName;
47
+ }
48
+
49
+ // Try to find profile by name in Local State
50
+ if (profileSource) {
51
+ try {
52
+ const localStatePath = `${profileSource}Local State`;
53
+ const localState = JSON.parse(readFileSync(localStatePath, "utf8"));
54
+ const profiles = localState.profile?.info_cache || {};
55
+
56
+ for (const [dir, info] of Object.entries(profiles)) {
57
+ if (info.name?.toLowerCase() === profileName.toLowerCase()) {
58
+ return dir;
59
+ }
60
+ }
61
+ } catch {
62
+ // Fall through to return original name
63
+ }
64
+ }
65
+
66
+ return profileName;
67
+ }
68
+
69
+ export function parseProfileName(localState, profileName) {
70
+ const profiles = localState.profile?.info_cache || {};
71
+
72
+ for (const [dir, info] of Object.entries(profiles)) {
73
+ if (info.name?.toLowerCase() === profileName.toLowerCase()) {
74
+ return dir;
75
+ }
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ export function normalizeUrl(url) {
82
+ if (!url.match(/^https?:\/\//i)) {
83
+ return "https://" + url;
84
+ }
85
+ return url;
86
+ }
87
+
88
+ export function filterRealPages(pages) {
89
+ return pages.filter((p) => {
90
+ const url = p.url();
91
+ return url.startsWith("http://") || url.startsWith("https://");
92
+ });
93
+ }
94
+
95
+ export function getActivePage(pages) {
96
+ const realPages = filterRealPages(pages);
97
+ return realPages[realPages.length - 1] || pages[pages.length - 1] || null;
98
+ }
99
+
100
+ export function formatTime() {
101
+ return new Date().toISOString().split("T")[1].slice(0, 12);
102
+ }
103
+
104
+ // Console output colors
105
+ export const levelColors = {
106
+ verbose: "\x1b[90m", // gray
107
+ info: "\x1b[36m", // cyan
108
+ warning: "\x1b[33m", // yellow
109
+ error: "\x1b[31m", // red
110
+ };
111
+
112
+ export const resetColor = "\x1b[0m";
113
+
114
+ export function formatLogEntry(level, source, text) {
115
+ const color = levelColors[level] || levelColors.info;
116
+ const sourceTag = source ? `[${source}]` : "";
117
+ return `${color}[${formatTime()}] [${level.toUpperCase()}]${sourceTag} ${text}${resetColor}`;
118
+ }
package/console.js DELETED
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { chromium } from "playwright";
4
-
5
- const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
6
-
7
- const args = process.argv.slice(2);
8
- const duration = args.find((a) => a.startsWith("--duration="));
9
- const durationMs = duration ? parseInt(duration.split("=")[1]) * 1000 : null;
10
- const showHelp = args.includes("--help") || args.includes("-h");
11
-
12
- if (showHelp) {
13
- console.log("Usage: console.js [--duration=SECONDS]");
14
- console.log("\nCapture browser console output in real-time.");
15
- console.log("\nOptions:");
16
- console.log(" --duration=N Stop after N seconds (default: run until Ctrl+C)");
17
- console.log("\nExamples:");
18
- console.log(" console.js # Stream console logs until Ctrl+C");
19
- console.log(" console.js --duration=5 # Capture for 5 seconds");
20
- process.exit(0);
21
- }
22
-
23
- const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
24
- const contexts = browser.contexts();
25
- const context = contexts[0];
26
-
27
- if (!context) {
28
- console.error("No browser context found");
29
- process.exit(1);
30
- }
31
-
32
- const pages = context.pages();
33
- const page = pages[pages.length - 1];
34
-
35
- if (!page) {
36
- console.error("No active tab found");
37
- process.exit(1);
38
- }
39
-
40
- const formatTime = () => new Date().toISOString().split("T")[1].slice(0, 12);
41
-
42
- const typeColors = {
43
- log: "\x1b[0m", // default
44
- info: "\x1b[36m", // cyan
45
- warn: "\x1b[33m", // yellow
46
- error: "\x1b[31m", // red
47
- debug: "\x1b[90m", // gray
48
- };
49
- const reset = "\x1b[0m";
50
-
51
- page.on("console", (msg) => {
52
- const type = msg.type();
53
- const color = typeColors[type] || typeColors.log;
54
- const text = msg.text();
55
- console.log(`${color}[${formatTime()}] [${type.toUpperCase()}] ${text}${reset}`);
56
- });
57
-
58
- page.on("pageerror", (error) => {
59
- console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${reset}`);
60
- });
61
-
62
- console.error(`Listening for console output... (Ctrl+C to stop)`);
63
-
64
- if (durationMs) {
65
- await new Promise((r) => setTimeout(r, durationMs));
66
- await browser.close();
67
- } else {
68
- // Keep running until interrupted
69
- process.on("SIGINT", async () => {
70
- console.error("\nStopping...");
71
- await browser.close();
72
- process.exit(0);
73
- });
74
-
75
- // Keep the process alive
76
- await new Promise(() => {});
77
- }
package/eval.js DELETED
@@ -1,62 +0,0 @@
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 DELETED
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { chromium } from "playwright";
4
-
5
- const DEFAULT_PORT = process.env.DEBUG_PORT || 9222;
6
-
7
- let url = process.argv[2];
8
- const newTab = process.argv[3] === "--new";
9
-
10
- // Add protocol if missing
11
- if (url && !url.match(/^https?:\/\//i)) {
12
- url = "https://" + url;
13
- }
14
-
15
- if (!url) {
16
- console.log("Usage: nav.js <url> [--new]");
17
- console.log("\nExamples:");
18
- console.log(" nav.js example.com # Navigate current tab");
19
- console.log(" nav.js example.com --new # Open in new tab");
20
- process.exit(1);
21
- }
22
-
23
- const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
24
- const contexts = browser.contexts();
25
- const context = contexts[0] || await browser.newContext();
26
-
27
- if (newTab) {
28
- const page = await context.newPage();
29
- await page.goto(url, { waitUntil: "domcontentloaded" });
30
- console.log("Opened:", url);
31
- } else {
32
- const pages = context.pages();
33
- const page = pages[pages.length - 1] || await context.newPage();
34
- await page.goto(url, { waitUntil: "domcontentloaded" });
35
- console.log("Navigated to:", url);
36
- }
37
-
38
- await browser.close();