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 +59 -5
- package/cli.js +18 -9
- package/package.json +8 -9
- package/src/close.js +31 -0
- package/src/console.js +103 -0
- package/src/dom.js +26 -0
- package/src/eval.js +112 -0
- package/{insights.js → src/insights.js} +2 -3
- package/src/lighthouse.js +152 -0
- package/src/nav.js +95 -0
- package/src/pdf.js +78 -0
- package/{pick.js → src/pick.js} +2 -3
- package/{screenshot.js → src/screenshot.js} +2 -3
- package/{start.js → src/start.js} +26 -56
- package/src/utils.js +118 -0
- package/console.js +0 -77
- package/eval.js +0 -62
- package/nav.js +0 -38
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# browser-cdp
|
|
2
2
|
|
|
3
|
+
[](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
|
|
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
|
-
#
|
|
72
|
-
browser-cdp
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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("
|
|
31
|
-
console.log("
|
|
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
|
+
"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
|
-
"
|
|
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
|
|
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();
|
package/{pick.js → src/pick.js}
RENAMED
|
@@ -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
|
|
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
|
|
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(" --
|
|
55
|
-
console.log(" --
|
|
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
|
|
62
|
-
console.log(" start.js brave
|
|
63
|
-
console.log(" start.js
|
|
64
|
-
console.log(" start.js --
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
170
|
-
console.log(`Starting ${browserConfig.name} on port ${port}${
|
|
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}${
|
|
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();
|