browser-cdp 0.5.1 → 0.6.3
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 +113 -0
- package/cli.js +18 -10
- package/package.json +7 -9
- package/src/close.js +31 -0
- package/{console.js → src/console.js} +8 -24
- package/src/cookies.js +88 -0
- package/src/dom.js +26 -0
- package/{eval.js → src/eval.js} +8 -28
- package/{insights.js → src/insights.js} +2 -8
- package/src/lighthouse.js +152 -0
- package/{nav.js → src/nav.js} +8 -22
- package/src/network.js +171 -0
- package/src/pdf.js +78 -0
- package/{pick.js → src/pick.js} +2 -8
- package/{screenshot.js → src/screenshot.js} +2 -8
- package/{start.js → src/start.js} +3 -74
- package/src/utils.js +118 -0
package/README.md
CHANGED
|
@@ -28,12 +28,23 @@ browser-cdp eval '<code>'
|
|
|
28
28
|
# Take screenshot
|
|
29
29
|
browser-cdp screenshot
|
|
30
30
|
|
|
31
|
+
# Export page as PDF
|
|
32
|
+
browser-cdp pdf [--path=FILE] [--format=A4|Letter|Legal|Tabloid] [--landscape]
|
|
33
|
+
|
|
31
34
|
# Interactive element picker
|
|
32
35
|
browser-cdp pick '<message>'
|
|
33
36
|
|
|
34
37
|
# Stream browser console output (network errors, exceptions, logs)
|
|
35
38
|
browser-cdp console [--duration=SECONDS]
|
|
36
39
|
|
|
40
|
+
# Stream network requests/responses
|
|
41
|
+
browser-cdp network [--filter=PATTERN] [--json] [--errors] [--duration=SECONDS]
|
|
42
|
+
|
|
43
|
+
# Manage cookies (export/import/clear)
|
|
44
|
+
browser-cdp cookies export [--path=FILE]
|
|
45
|
+
browser-cdp cookies import <file>
|
|
46
|
+
browser-cdp cookies clear
|
|
47
|
+
|
|
37
48
|
# Show page performance metrics
|
|
38
49
|
browser-cdp insights [--json]
|
|
39
50
|
|
|
@@ -69,6 +80,13 @@ browser-cdp eval 'document.querySelector("textarea").value = "hello"'
|
|
|
69
80
|
browser-cdp screenshot
|
|
70
81
|
# Returns: /tmp/screenshot-2024-01-01T12-00-00.png
|
|
71
82
|
|
|
83
|
+
# Export page as PDF
|
|
84
|
+
browser-cdp pdf
|
|
85
|
+
# Returns: /tmp/pdf-2024-01-01T12-00-00.pdf
|
|
86
|
+
|
|
87
|
+
# Export to specific file in A4 landscape
|
|
88
|
+
browser-cdp pdf --path report.pdf --format A4 --landscape
|
|
89
|
+
|
|
72
90
|
# Pick elements interactively
|
|
73
91
|
browser-cdp pick "Select the login button"
|
|
74
92
|
|
|
@@ -79,6 +97,32 @@ browser-cdp console
|
|
|
79
97
|
# Stream console for 10 seconds
|
|
80
98
|
browser-cdp console --duration=10
|
|
81
99
|
|
|
100
|
+
# Stream network traffic
|
|
101
|
+
browser-cdp network
|
|
102
|
+
# Then navigate to see requests
|
|
103
|
+
|
|
104
|
+
# Filter to API calls only
|
|
105
|
+
browser-cdp network --filter=api
|
|
106
|
+
|
|
107
|
+
# Only show failed requests (4xx/5xx)
|
|
108
|
+
browser-cdp network --errors
|
|
109
|
+
|
|
110
|
+
# JSON output for parsing
|
|
111
|
+
browser-cdp network --json --duration=5 | jq '.url'
|
|
112
|
+
|
|
113
|
+
# Export cookies to JSON file
|
|
114
|
+
browser-cdp cookies export
|
|
115
|
+
# Returns: cookies.json with all cookies
|
|
116
|
+
|
|
117
|
+
# Export to custom file
|
|
118
|
+
browser-cdp cookies export --path session.json
|
|
119
|
+
|
|
120
|
+
# Import cookies from file
|
|
121
|
+
browser-cdp cookies import session.json
|
|
122
|
+
|
|
123
|
+
# Clear all cookies
|
|
124
|
+
browser-cdp cookies clear
|
|
125
|
+
|
|
82
126
|
# Get page performance insights
|
|
83
127
|
browser-cdp insights
|
|
84
128
|
# Returns: TTFB, First Paint, FCP, DOM loaded, resources, memory
|
|
@@ -93,6 +137,56 @@ browser-cdp lighthouse
|
|
|
93
137
|
browser-cdp close
|
|
94
138
|
```
|
|
95
139
|
|
|
140
|
+
## Cookies Command
|
|
141
|
+
|
|
142
|
+
The `cookies` command provides session persistence for authenticated workflows:
|
|
143
|
+
|
|
144
|
+
### Export Cookies
|
|
145
|
+
|
|
146
|
+
Save your browser cookies to a JSON file for later use:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
browser-cdp cookies export # Saves to cookies.json
|
|
150
|
+
browser-cdp cookies export --path auth.json # Save to specific file
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Output format:
|
|
154
|
+
```json
|
|
155
|
+
[
|
|
156
|
+
{
|
|
157
|
+
"name": "session_id",
|
|
158
|
+
"value": "abc123...",
|
|
159
|
+
"domain": "example.com",
|
|
160
|
+
"path": "/",
|
|
161
|
+
"httpOnly": true,
|
|
162
|
+
"secure": true,
|
|
163
|
+
"sameSite": "Strict",
|
|
164
|
+
"expires": 1735689600
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Import Cookies
|
|
170
|
+
|
|
171
|
+
Load previously exported cookies into the browser:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
browser-cdp cookies import auth.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Useful for:
|
|
178
|
+
- Resuming authenticated sessions across browser restarts
|
|
179
|
+
- Sharing sessions across team members
|
|
180
|
+
- Preserving login state for automation workflows
|
|
181
|
+
|
|
182
|
+
### Clear Cookies
|
|
183
|
+
|
|
184
|
+
Delete all cookies from the browser:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
browser-cdp cookies clear
|
|
188
|
+
```
|
|
189
|
+
|
|
96
190
|
## Pre-started Browser
|
|
97
191
|
|
|
98
192
|
If you already have a browser running with CDP enabled, the CLI will connect to it:
|
|
@@ -137,6 +231,25 @@ Use `BROWSER_PATH` env var to override if your browser is installed elsewhere.
|
|
|
137
231
|
| Detection | Not detectable as automation | Automation flags present |
|
|
138
232
|
| Use case | Real-world testing, scraping | Isolated E2E tests |
|
|
139
233
|
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Install dependencies
|
|
238
|
+
bun install
|
|
239
|
+
|
|
240
|
+
# Run all tests
|
|
241
|
+
bun run test
|
|
242
|
+
|
|
243
|
+
# Run unit tests only (fast, no browser needed)
|
|
244
|
+
bun run test:unit
|
|
245
|
+
|
|
246
|
+
# Run integration tests (requires browser)
|
|
247
|
+
bun run test:integration
|
|
248
|
+
|
|
249
|
+
# Watch mode
|
|
250
|
+
bun run test:watch
|
|
251
|
+
```
|
|
252
|
+
|
|
140
253
|
## See Also
|
|
141
254
|
|
|
142
255
|
- [dev-browser](https://github.com/SawyerHood/dev-browser) - Browser automation plugin for Claude Code with LLM-optimized DOM snapshots
|
package/cli.js
CHANGED
|
@@ -11,16 +11,19 @@ const command = process.argv[2];
|
|
|
11
11
|
const args = process.argv.slice(3);
|
|
12
12
|
|
|
13
13
|
const commands = {
|
|
14
|
-
start: "./start.js",
|
|
15
|
-
close: "./close.js",
|
|
16
|
-
nav: "./nav.js",
|
|
17
|
-
eval: "./eval.js",
|
|
18
|
-
dom: "./dom.js",
|
|
19
|
-
screenshot: "./screenshot.js",
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
network: "./src/network.js",
|
|
24
|
+
cookies: "./src/cookies.js",
|
|
25
|
+
insights: "./src/insights.js",
|
|
26
|
+
lighthouse: "./src/lighthouse.js",
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
function printUsage() {
|
|
@@ -35,8 +38,11 @@ function printUsage() {
|
|
|
35
38
|
console.log(" eval '<code>' Evaluate JS in page (--console to capture logs)");
|
|
36
39
|
console.log(" dom Capture full page DOM/HTML");
|
|
37
40
|
console.log(" screenshot Take screenshot of current page");
|
|
41
|
+
console.log(" pdf Export current page as PDF");
|
|
38
42
|
console.log(" pick '<message>' Interactive element picker");
|
|
39
43
|
console.log(" console Stream browser console output");
|
|
44
|
+
console.log(" network Stream network requests/responses");
|
|
45
|
+
console.log(" cookies Export/import/clear browser cookies");
|
|
40
46
|
console.log(" insights Show page performance metrics");
|
|
41
47
|
console.log(" lighthouse Run Lighthouse audit");
|
|
42
48
|
console.log("");
|
|
@@ -51,6 +57,8 @@ function printUsage() {
|
|
|
51
57
|
console.log(" browser-cdp eval 'document.title'");
|
|
52
58
|
console.log(" browser-cdp dom > page.html");
|
|
53
59
|
console.log(" browser-cdp console --duration=10");
|
|
60
|
+
console.log(" browser-cdp network --filter=api");
|
|
61
|
+
console.log(" browser-cdp cookies export --path session.json");
|
|
54
62
|
console.log(" browser-cdp insights --json");
|
|
55
63
|
process.exit(0);
|
|
56
64
|
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-cdp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.3",
|
|
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
|
],
|
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
|
+
}
|
|
@@ -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, formatTime, levelColors, resetColor } from "./utils.js";
|
|
6
5
|
|
|
7
6
|
const args = process.argv.slice(2);
|
|
8
7
|
const duration = args.find((a) => a.startsWith("--duration="));
|
|
@@ -33,12 +32,7 @@ if (!context) {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
const pages = context.pages();
|
|
36
|
-
|
|
37
|
-
const realPages = pages.filter(p => {
|
|
38
|
-
const url = p.url();
|
|
39
|
-
return url.startsWith("http://") || url.startsWith("https://");
|
|
40
|
-
});
|
|
41
|
-
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
35
|
+
const page = getActivePage(pages);
|
|
42
36
|
|
|
43
37
|
if (!page) {
|
|
44
38
|
console.error("No active tab found");
|
|
@@ -47,16 +41,6 @@ if (!page) {
|
|
|
47
41
|
|
|
48
42
|
console.error(`Connected to: ${page.url()}`);
|
|
49
43
|
|
|
50
|
-
const formatTime = () => new Date().toISOString().split("T")[1].slice(0, 12);
|
|
51
|
-
|
|
52
|
-
const levelColors = {
|
|
53
|
-
verbose: "\x1b[90m", // gray
|
|
54
|
-
info: "\x1b[36m", // cyan
|
|
55
|
-
warning: "\x1b[33m", // yellow
|
|
56
|
-
error: "\x1b[31m", // red
|
|
57
|
-
};
|
|
58
|
-
const reset = "\x1b[0m";
|
|
59
|
-
|
|
60
44
|
// Use CDP directly for Log domain (captures network errors, etc.)
|
|
61
45
|
const cdp = await page.context().newCDPSession(page);
|
|
62
46
|
await cdp.send("Log.enable");
|
|
@@ -64,9 +48,9 @@ await cdp.send("Log.enable");
|
|
|
64
48
|
cdp.on("Log.entryAdded", ({ entry }) => {
|
|
65
49
|
const color = levelColors[entry.level] || levelColors.info;
|
|
66
50
|
const source = entry.source ? `[${entry.source}]` : "";
|
|
67
|
-
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${
|
|
51
|
+
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${resetColor}`);
|
|
68
52
|
if (entry.url) {
|
|
69
|
-
console.log(`${color} URL: ${entry.url}${
|
|
53
|
+
console.log(`${color} URL: ${entry.url}${resetColor}`);
|
|
70
54
|
}
|
|
71
55
|
});
|
|
72
56
|
|
|
@@ -74,14 +58,14 @@ cdp.on("Log.entryAdded", ({ entry }) => {
|
|
|
74
58
|
await cdp.send("Runtime.enable");
|
|
75
59
|
cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
76
60
|
const text = exceptionDetails.exception?.description || exceptionDetails.text;
|
|
77
|
-
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${
|
|
61
|
+
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${resetColor}`);
|
|
78
62
|
});
|
|
79
63
|
|
|
80
64
|
// Capture network failures (ERR_BLOCKED_BY_CLIENT, etc.)
|
|
81
65
|
await cdp.send("Network.enable");
|
|
82
66
|
cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
|
|
83
67
|
const reason = blockedReason ? ` (${blockedReason})` : "";
|
|
84
|
-
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${
|
|
68
|
+
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${resetColor}`);
|
|
85
69
|
});
|
|
86
70
|
|
|
87
71
|
// Keep Playwright listeners for console.log() calls
|
|
@@ -89,11 +73,11 @@ page.on("console", (msg) => {
|
|
|
89
73
|
const type = msg.type();
|
|
90
74
|
const color = levelColors[type] || levelColors.info;
|
|
91
75
|
const text = msg.text();
|
|
92
|
-
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${text}${
|
|
76
|
+
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${text}${resetColor}`);
|
|
93
77
|
});
|
|
94
78
|
|
|
95
79
|
page.on("pageerror", (error) => {
|
|
96
|
-
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${
|
|
80
|
+
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${resetColor}`);
|
|
97
81
|
});
|
|
98
82
|
|
|
99
83
|
if (shouldReload) {
|
package/src/cookies.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
import { DEFAULT_PORT, getActivePage } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const subcommand = args[0];
|
|
9
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
10
|
+
|
|
11
|
+
function printUsage() {
|
|
12
|
+
console.log("Usage: cookies <subcommand> [options]");
|
|
13
|
+
console.log("");
|
|
14
|
+
console.log("Subcommands:");
|
|
15
|
+
console.log(" export Export cookies to JSON file");
|
|
16
|
+
console.log(" import <file> Import cookies from JSON file");
|
|
17
|
+
console.log(" clear Clear all cookies from browser");
|
|
18
|
+
console.log("");
|
|
19
|
+
console.log("Options:");
|
|
20
|
+
console.log(" --path <file> Output file path for export (default: cookies.json)");
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log("Examples:");
|
|
23
|
+
console.log(" cookies export");
|
|
24
|
+
console.log(" cookies export --path session.json");
|
|
25
|
+
console.log(" cookies import session.json");
|
|
26
|
+
console.log(" cookies clear");
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!subcommand || showHelp) {
|
|
31
|
+
printUsage();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!["export", "import", "clear"].includes(subcommand)) {
|
|
35
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
36
|
+
console.log("Available: export, import, clear");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
41
|
+
const contexts = browser.contexts();
|
|
42
|
+
const context = contexts[0];
|
|
43
|
+
|
|
44
|
+
if (!context) {
|
|
45
|
+
console.error("No browser context found");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const pages = context.pages();
|
|
50
|
+
const page = getActivePage(pages);
|
|
51
|
+
|
|
52
|
+
if (!page) {
|
|
53
|
+
console.error("No active tab found");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cdp = await page.context().newCDPSession(page);
|
|
58
|
+
|
|
59
|
+
if (subcommand === "export") {
|
|
60
|
+
const pathIdx = args.findIndex((a) => a === "--path");
|
|
61
|
+
const outputFile = pathIdx !== -1 ? args[pathIdx + 1] : "cookies.json";
|
|
62
|
+
|
|
63
|
+
const { cookies } = await cdp.send("Network.getCookies");
|
|
64
|
+
writeFileSync(outputFile, JSON.stringify(cookies, null, 2));
|
|
65
|
+
console.log(`Exported ${cookies.length} cookie(s) to ${outputFile}`);
|
|
66
|
+
} else if (subcommand === "import") {
|
|
67
|
+
const importFile = args[1];
|
|
68
|
+
|
|
69
|
+
if (!importFile) {
|
|
70
|
+
console.error("Error: import requires a file path");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cookies = JSON.parse(readFileSync(importFile, "utf8"));
|
|
75
|
+
|
|
76
|
+
if (!Array.isArray(cookies)) {
|
|
77
|
+
console.error("Error: Cookie file must contain a JSON array");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await cdp.send("Network.setCookies", { cookies });
|
|
82
|
+
console.log(`Imported ${cookies.length} cookie(s) from ${importFile}`);
|
|
83
|
+
} else if (subcommand === "clear") {
|
|
84
|
+
await cdp.send("Network.clearBrowserCookies");
|
|
85
|
+
console.log("Cleared all cookies");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await browser.close();
|
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/{eval.js → src/eval.js}
RENAMED
|
@@ -1,16 +1,13 @@
|
|
|
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, formatTime, levelColors, resetColor } from "./utils.js";
|
|
6
5
|
|
|
7
6
|
const args = process.argv.slice(2);
|
|
8
7
|
const showHelp = args.includes("--help") || args.includes("-h");
|
|
9
8
|
const captureConsole = args.includes("--console");
|
|
10
9
|
const durationArg = args.find((a) => a.startsWith("--duration="));
|
|
11
10
|
const durationMs = durationArg ? parseInt(durationArg.split("=")[1]) * 1000 : 3000;
|
|
12
|
-
|
|
13
|
-
// Get code (everything that's not a flag)
|
|
14
11
|
const code = args.filter((a) => !a.startsWith("--")).join(" ");
|
|
15
12
|
|
|
16
13
|
if (showHelp || !code) {
|
|
@@ -35,29 +32,14 @@ if (!context) {
|
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
const pages = context.pages();
|
|
38
|
-
|
|
39
|
-
const realPages = pages.filter(p => {
|
|
40
|
-
const url = p.url();
|
|
41
|
-
return url.startsWith("http://") || url.startsWith("https://");
|
|
42
|
-
});
|
|
43
|
-
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
35
|
+
const page = getActivePage(pages);
|
|
44
36
|
|
|
45
37
|
if (!page) {
|
|
46
38
|
console.error("No active tab found");
|
|
47
39
|
process.exit(1);
|
|
48
40
|
}
|
|
49
41
|
|
|
50
|
-
// Set up console capture BEFORE evaluation
|
|
51
42
|
if (captureConsole) {
|
|
52
|
-
const formatTime = () => new Date().toISOString().split("T")[1].slice(0, 12);
|
|
53
|
-
const levelColors = {
|
|
54
|
-
verbose: "\x1b[90m",
|
|
55
|
-
info: "\x1b[36m",
|
|
56
|
-
warning: "\x1b[33m",
|
|
57
|
-
error: "\x1b[31m",
|
|
58
|
-
};
|
|
59
|
-
const reset = "\x1b[0m";
|
|
60
|
-
|
|
61
43
|
const cdp = await page.context().newCDPSession(page);
|
|
62
44
|
await cdp.send("Log.enable");
|
|
63
45
|
await cdp.send("Runtime.enable");
|
|
@@ -66,30 +48,30 @@ if (captureConsole) {
|
|
|
66
48
|
cdp.on("Log.entryAdded", ({ entry }) => {
|
|
67
49
|
const color = levelColors[entry.level] || levelColors.info;
|
|
68
50
|
const source = entry.source ? `[${entry.source}]` : "";
|
|
69
|
-
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${
|
|
51
|
+
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${resetColor}`);
|
|
70
52
|
if (entry.url) {
|
|
71
|
-
console.log(`${color} URL: ${entry.url}${
|
|
53
|
+
console.log(`${color} URL: ${entry.url}${resetColor}`);
|
|
72
54
|
}
|
|
73
55
|
});
|
|
74
56
|
|
|
75
57
|
cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
76
58
|
const text = exceptionDetails.exception?.description || exceptionDetails.text;
|
|
77
|
-
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${
|
|
59
|
+
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${resetColor}`);
|
|
78
60
|
});
|
|
79
61
|
|
|
80
62
|
cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
|
|
81
63
|
const reason = blockedReason ? ` (${blockedReason})` : "";
|
|
82
|
-
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${
|
|
64
|
+
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${resetColor}`);
|
|
83
65
|
});
|
|
84
66
|
|
|
85
67
|
page.on("console", (msg) => {
|
|
86
68
|
const type = msg.type();
|
|
87
69
|
const color = levelColors[type] || levelColors.info;
|
|
88
|
-
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${msg.text()}${
|
|
70
|
+
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${msg.text()}${resetColor}`);
|
|
89
71
|
});
|
|
90
72
|
|
|
91
73
|
page.on("pageerror", (error) => {
|
|
92
|
-
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${
|
|
74
|
+
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${resetColor}`);
|
|
93
75
|
});
|
|
94
76
|
}
|
|
95
77
|
|
|
@@ -107,7 +89,6 @@ try {
|
|
|
107
89
|
process.exit(1);
|
|
108
90
|
}
|
|
109
91
|
|
|
110
|
-
// Print result
|
|
111
92
|
if (Array.isArray(result)) {
|
|
112
93
|
for (let i = 0; i < result.length; i++) {
|
|
113
94
|
if (i > 0) console.log("");
|
|
@@ -123,7 +104,6 @@ if (Array.isArray(result)) {
|
|
|
123
104
|
console.log(result);
|
|
124
105
|
}
|
|
125
106
|
|
|
126
|
-
// Wait for async console output
|
|
127
107
|
if (captureConsole) {
|
|
128
108
|
console.error(`\nListening for ${durationMs / 1000}s...`);
|
|
129
109
|
await new Promise((r) => setTimeout(r, durationMs));
|
|
@@ -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,12 +30,7 @@ if (!context) {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
const pages = context.pages();
|
|
34
|
-
|
|
35
|
-
const realPages = pages.filter(p => {
|
|
36
|
-
const url = p.url();
|
|
37
|
-
return url.startsWith("http://") || url.startsWith("https://");
|
|
38
|
-
});
|
|
39
|
-
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
33
|
+
const page = getActivePage(pages);
|
|
40
34
|
|
|
41
35
|
if (!page) {
|
|
42
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/{nav.js → src/nav.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, normalizeUrl, getActivePage, formatTime, levelColors, resetColor } from "./utils.js";
|
|
6
5
|
|
|
7
6
|
const args = process.argv.slice(2);
|
|
8
7
|
const showHelp = args.includes("--help") || args.includes("-h");
|
|
@@ -27,10 +26,7 @@ if (showHelp || !url) {
|
|
|
27
26
|
process.exit(showHelp ? 0 : 1);
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
if (!url.match(/^https?:\/\//i)) {
|
|
32
|
-
url = "https://" + url;
|
|
33
|
-
}
|
|
29
|
+
url = normalizeUrl(url);
|
|
34
30
|
|
|
35
31
|
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
36
32
|
const contexts = browser.contexts();
|
|
@@ -48,17 +44,7 @@ if (newTab) {
|
|
|
48
44
|
page = realPages[realPages.length - 1] || pages[pages.length - 1] || await context.newPage();
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
// Set up console capture BEFORE navigation
|
|
52
47
|
if (captureConsole) {
|
|
53
|
-
const formatTime = () => new Date().toISOString().split("T")[1].slice(0, 12);
|
|
54
|
-
const levelColors = {
|
|
55
|
-
verbose: "\x1b[90m",
|
|
56
|
-
info: "\x1b[36m",
|
|
57
|
-
warning: "\x1b[33m",
|
|
58
|
-
error: "\x1b[31m",
|
|
59
|
-
};
|
|
60
|
-
const reset = "\x1b[0m";
|
|
61
|
-
|
|
62
48
|
const cdp = await page.context().newCDPSession(page);
|
|
63
49
|
await cdp.send("Log.enable");
|
|
64
50
|
await cdp.send("Runtime.enable");
|
|
@@ -67,30 +53,30 @@ if (captureConsole) {
|
|
|
67
53
|
cdp.on("Log.entryAdded", ({ entry }) => {
|
|
68
54
|
const color = levelColors[entry.level] || levelColors.info;
|
|
69
55
|
const source = entry.source ? `[${entry.source}]` : "";
|
|
70
|
-
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${
|
|
56
|
+
console.log(`${color}[${formatTime()}] [${entry.level.toUpperCase()}]${source} ${entry.text}${resetColor}`);
|
|
71
57
|
if (entry.url) {
|
|
72
|
-
console.log(`${color} URL: ${entry.url}${
|
|
58
|
+
console.log(`${color} URL: ${entry.url}${resetColor}`);
|
|
73
59
|
}
|
|
74
60
|
});
|
|
75
61
|
|
|
76
62
|
cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
77
63
|
const text = exceptionDetails.exception?.description || exceptionDetails.text;
|
|
78
|
-
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${
|
|
64
|
+
console.log(`\x1b[31m[${formatTime()}] [EXCEPTION] ${text}${resetColor}`);
|
|
79
65
|
});
|
|
80
66
|
|
|
81
67
|
cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason }) => {
|
|
82
68
|
const reason = blockedReason ? ` (${blockedReason})` : "";
|
|
83
|
-
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${
|
|
69
|
+
console.log(`\x1b[31m[${formatTime()}] [NETWORK ERROR] ${errorText}${reason}${resetColor}`);
|
|
84
70
|
});
|
|
85
71
|
|
|
86
72
|
page.on("console", (msg) => {
|
|
87
73
|
const type = msg.type();
|
|
88
74
|
const color = levelColors[type] || levelColors.info;
|
|
89
|
-
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${msg.text()}${
|
|
75
|
+
console.log(`${color}[${formatTime()}] [CONSOLE.${type.toUpperCase()}] ${msg.text()}${resetColor}`);
|
|
90
76
|
});
|
|
91
77
|
|
|
92
78
|
page.on("pageerror", (error) => {
|
|
93
|
-
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${
|
|
79
|
+
console.log(`\x1b[31m[${formatTime()}] [PAGE ERROR] ${error.message}${resetColor}`);
|
|
94
80
|
});
|
|
95
81
|
|
|
96
82
|
console.error(`Navigating to ${url} (capturing console for ${durationMs / 1000}s)...`);
|
package/src/network.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chromium } from "playwright";
|
|
4
|
+
import { DEFAULT_PORT, getActivePage, formatTime, resetColor } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const filterPattern = args.find((a) => a.startsWith("--filter="))?.split("=")[1];
|
|
8
|
+
const jsonOutput = args.includes("--json");
|
|
9
|
+
const errorsOnly = args.includes("--errors");
|
|
10
|
+
const duration = args.find((a) => a.startsWith("--duration="));
|
|
11
|
+
const durationMs = duration ? parseInt(duration.split("=")[1]) * 1000 : null;
|
|
12
|
+
const showHelp = args.includes("--help") || args.includes("-h");
|
|
13
|
+
|
|
14
|
+
if (showHelp) {
|
|
15
|
+
console.log("Usage: network.js [options]");
|
|
16
|
+
console.log("\nStream network requests and responses in real-time.");
|
|
17
|
+
console.log("\nOptions:");
|
|
18
|
+
console.log(" --filter=PATTERN Filter URLs by regex pattern");
|
|
19
|
+
console.log(" --json Output as JSON for piping");
|
|
20
|
+
console.log(" --errors Only show failed requests (4xx/5xx)");
|
|
21
|
+
console.log(" --duration=N Stop after N seconds (default: run until Ctrl+C)");
|
|
22
|
+
console.log("\nExamples:");
|
|
23
|
+
console.log(" network.js # Stream all network traffic");
|
|
24
|
+
console.log(" network.js --filter=api # Only show URLs containing 'api'");
|
|
25
|
+
console.log(" network.js --errors # Only show failed requests");
|
|
26
|
+
console.log(" network.js --json # JSON output for jq/parsing");
|
|
27
|
+
console.log(" network.js --duration=10 # Capture for 10 seconds");
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const browser = await chromium.connectOverCDP(`http://localhost:${DEFAULT_PORT}`);
|
|
32
|
+
const contexts = browser.contexts();
|
|
33
|
+
const context = contexts[0];
|
|
34
|
+
|
|
35
|
+
if (!context) {
|
|
36
|
+
console.error("No browser context found");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pages = context.pages();
|
|
41
|
+
const page = getActivePage(pages);
|
|
42
|
+
|
|
43
|
+
if (!page) {
|
|
44
|
+
console.error("No active tab found");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.error(`Connected to: ${page.url()}`);
|
|
49
|
+
|
|
50
|
+
const cdp = await page.context().newCDPSession(page);
|
|
51
|
+
await cdp.send("Network.enable");
|
|
52
|
+
|
|
53
|
+
const requests = new Map();
|
|
54
|
+
|
|
55
|
+
const statusColors = {
|
|
56
|
+
success: "\x1b[32m", // green for 2xx
|
|
57
|
+
redirect: "\x1b[33m", // yellow for 3xx
|
|
58
|
+
error: "\x1b[31m", // red for 4xx/5xx
|
|
59
|
+
request: "\x1b[36m", // cyan for requests
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function matchesFilter(url) {
|
|
63
|
+
if (!filterPattern) return true;
|
|
64
|
+
try {
|
|
65
|
+
return new RegExp(filterPattern).test(url);
|
|
66
|
+
} catch {
|
|
67
|
+
return url.includes(filterPattern);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cdp.on("Network.requestWillBeSent", ({ requestId, request, timestamp }) => {
|
|
72
|
+
requests.set(requestId, { method: request.method, url: request.url, timestamp });
|
|
73
|
+
|
|
74
|
+
if (!matchesFilter(request.url)) return;
|
|
75
|
+
if (errorsOnly) return;
|
|
76
|
+
|
|
77
|
+
if (jsonOutput) {
|
|
78
|
+
console.log(
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
type: "request",
|
|
81
|
+
timestamp: new Date(timestamp * 1000).toISOString(),
|
|
82
|
+
method: request.method,
|
|
83
|
+
url: request.url,
|
|
84
|
+
requestId,
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
console.log(`${statusColors.request}[${formatTime()}] → ${request.method} ${request.url}${resetColor}`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
cdp.on("Network.responseReceived", ({ requestId, response, timestamp, type }) => {
|
|
93
|
+
const req = requests.get(requestId);
|
|
94
|
+
|
|
95
|
+
if (!matchesFilter(response.url)) return;
|
|
96
|
+
if (errorsOnly && response.status < 400) return;
|
|
97
|
+
|
|
98
|
+
if (jsonOutput) {
|
|
99
|
+
console.log(
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
type: "response",
|
|
102
|
+
timestamp: new Date(timestamp * 1000).toISOString(),
|
|
103
|
+
status: response.status,
|
|
104
|
+
statusText: response.statusText,
|
|
105
|
+
url: response.url,
|
|
106
|
+
mimeType: response.mimeType,
|
|
107
|
+
resourceType: type,
|
|
108
|
+
requestId,
|
|
109
|
+
method: req?.method,
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
let color = statusColors.success;
|
|
114
|
+
if (response.status >= 400) {
|
|
115
|
+
color = statusColors.error;
|
|
116
|
+
} else if (response.status >= 300) {
|
|
117
|
+
color = statusColors.redirect;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const method = req ? `${req.method} ` : "";
|
|
121
|
+
console.log(`${color}[${formatTime()}] ← ${response.status} ${method}${response.url}${resetColor}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
cdp.on("Network.loadingFailed", ({ requestId, errorText, blockedReason, canceled }) => {
|
|
126
|
+
const req = requests.get(requestId);
|
|
127
|
+
|
|
128
|
+
if (req && !matchesFilter(req.url)) return;
|
|
129
|
+
|
|
130
|
+
if (jsonOutput) {
|
|
131
|
+
console.log(
|
|
132
|
+
JSON.stringify({
|
|
133
|
+
type: "failed",
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
url: req?.url,
|
|
136
|
+
method: req?.method,
|
|
137
|
+
error: errorText,
|
|
138
|
+
blockedReason,
|
|
139
|
+
canceled,
|
|
140
|
+
requestId,
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
const reason = blockedReason ? ` (${blockedReason})` : "";
|
|
145
|
+
const canceledStr = canceled ? " [CANCELED]" : "";
|
|
146
|
+
console.log(
|
|
147
|
+
`${statusColors.error}[${formatTime()}] ✗ ${errorText}${reason}${canceledStr} - ${req?.url || "unknown"}${resetColor}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (requestId) {
|
|
152
|
+
requests.delete(requestId);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.error(`Listening for network activity... (Ctrl+C to stop)`);
|
|
157
|
+
|
|
158
|
+
if (durationMs) {
|
|
159
|
+
await new Promise((r) => setTimeout(r, durationMs));
|
|
160
|
+
await browser.close();
|
|
161
|
+
} else {
|
|
162
|
+
// Keep running until interrupted
|
|
163
|
+
process.on("SIGINT", async () => {
|
|
164
|
+
console.error("\nStopping...");
|
|
165
|
+
await browser.close();
|
|
166
|
+
process.exit(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Keep the process alive
|
|
170
|
+
await new Promise(() => {});
|
|
171
|
+
}
|
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,12 +21,7 @@ if (!context) {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
const pages = context.pages();
|
|
25
|
-
|
|
26
|
-
const realPages = pages.filter(p => {
|
|
27
|
-
const url = p.url();
|
|
28
|
-
return url.startsWith("http://") || url.startsWith("https://");
|
|
29
|
-
});
|
|
30
|
-
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
24
|
+
const page = getActivePage(pages);
|
|
31
25
|
|
|
32
26
|
if (!page) {
|
|
33
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,12 +15,7 @@ if (!context) {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
const pages = context.pages();
|
|
19
|
-
|
|
20
|
-
const realPages = pages.filter(p => {
|
|
21
|
-
const url = p.url();
|
|
22
|
-
return url.startsWith("http://") || url.startsWith("https://");
|
|
23
|
-
});
|
|
24
|
-
const page = realPages[realPages.length - 1] || pages[pages.length - 1];
|
|
18
|
+
const page = getActivePage(pages);
|
|
25
19
|
|
|
26
20
|
if (!page) {
|
|
27
21
|
console.error("No active tab found");
|
|
@@ -1,75 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn, execFileSync } from "node:child_process";
|
|
4
|
-
import { existsSync
|
|
5
|
-
import { platform } from "node:os";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
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;
|
|
46
|
-
|
|
47
|
-
// Resolve profile name to directory (supports both "Profile 1" and "Suppli" style names)
|
|
48
|
-
function resolveProfileDir(profileSource, profileName) {
|
|
49
|
-
// If it looks like a directory name already, use it
|
|
50
|
-
if (profileName === "Default" || profileName.startsWith("Profile ")) {
|
|
51
|
-
return profileName;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Try to find profile by name in Local State
|
|
55
|
-
if (profileSource) {
|
|
56
|
-
try {
|
|
57
|
-
const localStatePath = `${profileSource}Local State`;
|
|
58
|
-
const localState = JSON.parse(readFileSync(localStatePath, "utf8"));
|
|
59
|
-
const profiles = localState.profile?.info_cache || {};
|
|
60
|
-
|
|
61
|
-
for (const [dir, info] of Object.entries(profiles)) {
|
|
62
|
-
if (info.name?.toLowerCase() === profileName.toLowerCase()) {
|
|
63
|
-
return dir;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
// Fall through to return original name
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return profileName;
|
|
72
|
-
}
|
|
6
|
+
import { BROWSERS, DEFAULT_PORT, isMac, resolveProfileDir } from "./utils.js";
|
|
73
7
|
|
|
74
8
|
function printUsage() {
|
|
75
9
|
console.log("Usage: start.js [browser] [--profile=NAME] [--isolated] [--port=PORT]");
|
|
@@ -94,12 +28,11 @@ function printUsage() {
|
|
|
94
28
|
process.exit(1);
|
|
95
29
|
}
|
|
96
30
|
|
|
97
|
-
// Parse arguments
|
|
98
31
|
const args = process.argv.slice(2);
|
|
99
32
|
let browserName = process.env.BROWSER || "chrome";
|
|
100
33
|
let isolated = false;
|
|
101
34
|
let profile = null;
|
|
102
|
-
let port =
|
|
35
|
+
let port = DEFAULT_PORT;
|
|
103
36
|
|
|
104
37
|
for (const arg of args) {
|
|
105
38
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -119,7 +52,6 @@ for (const arg of args) {
|
|
|
119
52
|
}
|
|
120
53
|
}
|
|
121
54
|
|
|
122
|
-
// Resolve browser config
|
|
123
55
|
const browserPath = process.env.BROWSER_PATH || BROWSERS[browserName]?.path;
|
|
124
56
|
const browserConfig = BROWSERS[browserName] || {
|
|
125
57
|
name: "Custom",
|
|
@@ -186,7 +118,6 @@ if (!isolated && browserConfig.process) {
|
|
|
186
118
|
}
|
|
187
119
|
}
|
|
188
120
|
|
|
189
|
-
// Build browser arguments
|
|
190
121
|
const browserArgs = [
|
|
191
122
|
`--remote-debugging-port=${port}`,
|
|
192
123
|
// Required for Lighthouse/CDP debugger access (prevents bfcache blocking)
|
|
@@ -206,7 +137,6 @@ if (isolated) {
|
|
|
206
137
|
browserArgs.push(`--profile-directory=${profileDir}`);
|
|
207
138
|
}
|
|
208
139
|
|
|
209
|
-
// Start browser
|
|
210
140
|
const profileInfo = isolated ? " (isolated)" : profile ? ` (${profile})` : "";
|
|
211
141
|
console.log(`Starting ${browserConfig.name} on port ${port}${profileInfo}...`);
|
|
212
142
|
|
|
@@ -215,7 +145,6 @@ spawn(browserPath, browserArgs, {
|
|
|
215
145
|
stdio: "ignore",
|
|
216
146
|
}).unref();
|
|
217
147
|
|
|
218
|
-
// Wait for browser to be ready
|
|
219
148
|
let connected = false;
|
|
220
149
|
for (let i = 0; i < 30; i++) {
|
|
221
150
|
try {
|
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
|
+
}
|