@cli4ai/chrome 1.0.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -15
- package/cli4ai.json +63 -31
- package/package.json +5 -4
- package/run.ts +327 -53
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Official @cli4ai package • https://cli4ai.com • Install cli4ai: `npm i -g cli4ai`
|
|
4
4
|
|
|
5
|
-
Chrome
|
|
5
|
+
Chrome browser automation via Puppeteer. Launches a managed browser instance with persistent profile - no need to quit your existing browser or enable remote debugging.
|
|
6
6
|
|
|
7
7
|
## Setup
|
|
8
8
|
|
|
@@ -11,28 +11,97 @@ npm i -g cli4ai
|
|
|
11
11
|
cli4ai add -g chrome
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Usage
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```bash
|
|
17
|
+
# Navigate to a URL (auto-launches browser if not running)
|
|
18
|
+
cli4ai run chrome navigate https://example.com
|
|
19
|
+
|
|
20
|
+
# Run in headless mode (no visible window)
|
|
21
|
+
cli4ai run chrome --headless navigate https://example.com
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
# Screenshot
|
|
24
|
+
cli4ai run chrome screenshot output.png --full-page
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
# Get page text
|
|
27
|
+
cli4ai run chrome text
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
### Browser Management
|
|
22
33
|
|
|
23
34
|
```bash
|
|
24
|
-
cli4ai run chrome
|
|
25
|
-
#
|
|
26
|
-
cli4ai run chrome
|
|
35
|
+
cli4ai run chrome launch # Explicitly launch browser
|
|
36
|
+
cli4ai run chrome --headless launch # Launch in headless mode
|
|
37
|
+
cli4ai run chrome close # Close managed browser
|
|
38
|
+
cli4ai run chrome status # Check if browser is running
|
|
39
|
+
cli4ai run chrome tabs # List open tabs
|
|
27
40
|
```
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
### Navigation
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
```bash
|
|
45
|
+
cli4ai run chrome navigate <url> # Open URL
|
|
46
|
+
cli4ai run chrome navigate <url> --new-tab # Open in new tab
|
|
47
|
+
cli4ai run chrome navigate <url> --wait load # Wait for: load, domcontentloaded, networkidle0, networkidle2
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Interaction
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cli4ai run chrome click <selector> # Click element
|
|
54
|
+
cli4ai run chrome type <selector> "text" # Type into input
|
|
55
|
+
cli4ai run chrome type <selector> "text" --clear # Clear first, then type
|
|
56
|
+
cli4ai run chrome wait <selector> # Wait for element
|
|
57
|
+
cli4ai run chrome wait <selector> --timeout 5000 # With timeout (ms)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Content Extraction
|
|
32
61
|
|
|
33
62
|
```bash
|
|
34
|
-
cli4ai run chrome
|
|
35
|
-
cli4ai run chrome
|
|
36
|
-
cli4ai run chrome
|
|
37
|
-
cli4ai run chrome
|
|
63
|
+
cli4ai run chrome html # Get full page HTML
|
|
64
|
+
cli4ai run chrome html <selector> # Get element HTML
|
|
65
|
+
cli4ai run chrome text # Get page text content
|
|
66
|
+
cli4ai run chrome text <selector> # Get element text
|
|
67
|
+
cli4ai run chrome cookies # Get all cookies
|
|
68
|
+
cli4ai run chrome cookies example.com # Filter by domain
|
|
69
|
+
cli4ai run chrome eval "document.title" # Run JavaScript
|
|
38
70
|
```
|
|
71
|
+
|
|
72
|
+
### Screenshots & PDF
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cli4ai run chrome screenshot # Save screenshot.png
|
|
76
|
+
cli4ai run chrome screenshot output.png # Custom filename
|
|
77
|
+
cli4ai run chrome screenshot --full-page # Full page capture
|
|
78
|
+
cli4ai run chrome screenshot --selector "#hero" # Element screenshot
|
|
79
|
+
cli4ai run chrome pdf # Save page.pdf (headless)
|
|
80
|
+
cli4ai run chrome pdf output.pdf # Custom filename
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Headless Mode
|
|
84
|
+
|
|
85
|
+
Add `--headless` before any command to run without a visible browser window:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cli4ai run chrome --headless navigate https://example.com
|
|
89
|
+
cli4ai run chrome --headless screenshot
|
|
90
|
+
cli4ai run chrome --headless text
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Headless mode is useful for:
|
|
94
|
+
- Automated scraping
|
|
95
|
+
- CI/CD pipelines
|
|
96
|
+
- Background tasks
|
|
97
|
+
|
|
98
|
+
## Profile Persistence
|
|
99
|
+
|
|
100
|
+
Browser data (cookies, localStorage, logins) persists in `~/.cli4ai/chrome/profile/`. This means:
|
|
101
|
+
- You stay logged into websites between sessions
|
|
102
|
+
- Browser extensions are preserved
|
|
103
|
+
- History and settings persist
|
|
104
|
+
|
|
105
|
+
## Related
|
|
106
|
+
|
|
107
|
+
Other browser-dependent tools like `@cli4ai/twitter` and `@cli4ai/linkedin` can use the managed browser.
|
package/cli4ai.json
CHANGED
|
@@ -1,63 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Chrome browser automation
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Chrome browser automation with managed browser instance",
|
|
5
5
|
"author": "cliforai",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "BUSL-1.1",
|
|
7
7
|
"entry": "run.ts",
|
|
8
8
|
"runtime": "node",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"chrome",
|
|
11
11
|
"browser",
|
|
12
12
|
"puppeteer",
|
|
13
|
-
"automation"
|
|
13
|
+
"automation",
|
|
14
|
+
"headless"
|
|
14
15
|
],
|
|
15
16
|
"commands": {
|
|
16
|
-
"
|
|
17
|
-
"description": "
|
|
17
|
+
"launch": {
|
|
18
|
+
"description": "Launch managed browser",
|
|
19
|
+
"args": []
|
|
20
|
+
},
|
|
21
|
+
"close": {
|
|
22
|
+
"description": "Close managed browser",
|
|
23
|
+
"args": []
|
|
24
|
+
},
|
|
25
|
+
"status": {
|
|
26
|
+
"description": "Check browser status",
|
|
27
|
+
"args": []
|
|
28
|
+
},
|
|
29
|
+
"navigate": {
|
|
30
|
+
"description": "Open URL in browser",
|
|
18
31
|
"args": [
|
|
19
|
-
{
|
|
20
|
-
"name": "port",
|
|
21
|
-
"description": "Default: 9222",
|
|
22
|
-
"required": false
|
|
23
|
-
}
|
|
32
|
+
{ "name": "url", "required": true }
|
|
24
33
|
]
|
|
25
34
|
},
|
|
26
|
-
"
|
|
27
|
-
"description": "
|
|
35
|
+
"click": {
|
|
36
|
+
"description": "Click an element",
|
|
28
37
|
"args": [
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
{ "name": "selector", "required": true }
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"type": {
|
|
42
|
+
"description": "Type text into an input",
|
|
43
|
+
"args": [
|
|
44
|
+
{ "name": "selector", "required": true },
|
|
45
|
+
{ "name": "text", "required": true }
|
|
33
46
|
]
|
|
34
47
|
},
|
|
35
48
|
"eval": {
|
|
36
|
-
"description": "Run JavaScript",
|
|
49
|
+
"description": "Run JavaScript in page context",
|
|
37
50
|
"args": [
|
|
38
|
-
{
|
|
39
|
-
"name": "script",
|
|
40
|
-
"required": true
|
|
41
|
-
}
|
|
51
|
+
{ "name": "script", "required": true }
|
|
42
52
|
]
|
|
43
53
|
},
|
|
44
54
|
"screenshot": {
|
|
45
|
-
"description": "Capture
|
|
55
|
+
"description": "Capture screenshot",
|
|
56
|
+
"args": [
|
|
57
|
+
{ "name": "output", "required": false }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"pdf": {
|
|
61
|
+
"description": "Save page as PDF",
|
|
46
62
|
"args": [
|
|
47
|
-
{
|
|
48
|
-
"name": "output",
|
|
49
|
-
"required": false
|
|
50
|
-
}
|
|
63
|
+
{ "name": "output", "required": false }
|
|
51
64
|
]
|
|
52
65
|
},
|
|
53
66
|
"cookies": {
|
|
54
67
|
"description": "Get cookies",
|
|
55
68
|
"args": [
|
|
56
|
-
{
|
|
57
|
-
"name": "domain",
|
|
58
|
-
"required": false
|
|
59
|
-
}
|
|
69
|
+
{ "name": "domain", "required": false }
|
|
60
70
|
]
|
|
71
|
+
},
|
|
72
|
+
"html": {
|
|
73
|
+
"description": "Get page HTML",
|
|
74
|
+
"args": [
|
|
75
|
+
{ "name": "selector", "required": false }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"text": {
|
|
79
|
+
"description": "Get text content",
|
|
80
|
+
"args": [
|
|
81
|
+
{ "name": "selector", "required": false }
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"wait": {
|
|
85
|
+
"description": "Wait for element",
|
|
86
|
+
"args": [
|
|
87
|
+
{ "name": "selector", "required": true }
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"tabs": {
|
|
91
|
+
"description": "List open tabs",
|
|
92
|
+
"args": []
|
|
61
93
|
}
|
|
62
94
|
},
|
|
63
95
|
"dependencies": {
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cli4ai/chrome",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Chrome browser automation
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Chrome browser automation with managed browser instance",
|
|
5
5
|
"author": "cliforai",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "BUSL-1.1",
|
|
7
7
|
"main": "run.ts",
|
|
8
8
|
"bin": {
|
|
9
9
|
"chrome": "./run.ts"
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"chrome",
|
|
17
17
|
"browser",
|
|
18
18
|
"puppeteer",
|
|
19
|
-
"automation"
|
|
19
|
+
"automation",
|
|
20
|
+
"headless"
|
|
20
21
|
],
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "git",
|
package/run.ts
CHANGED
|
@@ -1,93 +1,367 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
|
-
import puppeteer from 'puppeteer';
|
|
3
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import puppeteer, { Browser, Page } from 'puppeteer';
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join, resolve } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
5
6
|
import { cli, log, output, outputError, withErrorHandling } from '@cli4ai/lib/cli.ts';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// Persistent storage paths
|
|
9
|
+
const CLI4AI_DIR = join(homedir(), '.cli4ai');
|
|
10
|
+
const CHROME_DIR = join(CLI4AI_DIR, 'chrome');
|
|
11
|
+
const PROFILE_DIR = join(CHROME_DIR, 'profile');
|
|
12
|
+
const WS_FILE = join(CHROME_DIR, 'ws-endpoint');
|
|
13
|
+
const PID_FILE = join(CHROME_DIR, 'pid');
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
// Ensure directories exist
|
|
16
|
+
function ensureDirs() {
|
|
17
|
+
if (!existsSync(CLI4AI_DIR)) mkdirSync(CLI4AI_DIR, { recursive: true });
|
|
18
|
+
if (!existsSync(CHROME_DIR)) mkdirSync(CHROME_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if browser process is still running
|
|
22
|
+
function isBrowserRunning(): boolean {
|
|
23
|
+
if (!existsSync(PID_FILE)) return false;
|
|
24
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
11
25
|
try {
|
|
12
|
-
|
|
26
|
+
process.kill(pid, 0); // Check if process exists
|
|
27
|
+
return true;
|
|
13
28
|
} catch {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
// Process not running, clean up stale files
|
|
30
|
+
cleanup();
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Clean up state files
|
|
36
|
+
function cleanup() {
|
|
37
|
+
if (existsSync(WS_FILE)) unlinkSync(WS_FILE);
|
|
38
|
+
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Launch a new browser instance
|
|
42
|
+
async function launchBrowser(headless: boolean): Promise<Browser> {
|
|
43
|
+
ensureDirs();
|
|
44
|
+
|
|
45
|
+
const browser = await puppeteer.launch({
|
|
46
|
+
headless: headless ? 'shell' : false,
|
|
47
|
+
userDataDir: PROFILE_DIR,
|
|
48
|
+
args: [
|
|
49
|
+
'--no-first-run',
|
|
50
|
+
'--no-default-browser-check',
|
|
51
|
+
'--disable-infobars',
|
|
52
|
+
],
|
|
53
|
+
defaultViewport: null, // Use full window size
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Save connection info
|
|
57
|
+
const wsEndpoint = browser.wsEndpoint();
|
|
58
|
+
writeFileSync(WS_FILE, wsEndpoint);
|
|
59
|
+
|
|
60
|
+
// Get browser process PID
|
|
61
|
+
const browserProcess = browser.process();
|
|
62
|
+
if (browserProcess?.pid) {
|
|
63
|
+
writeFileSync(PID_FILE, String(browserProcess.pid));
|
|
17
64
|
}
|
|
18
65
|
|
|
19
|
-
|
|
66
|
+
return browser;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get or create browser instance
|
|
70
|
+
async function getBrowser(headless: boolean): Promise<Browser> {
|
|
71
|
+
// Try to connect to existing browser
|
|
72
|
+
if (isBrowserRunning() && existsSync(WS_FILE)) {
|
|
73
|
+
try {
|
|
74
|
+
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
75
|
+
return await puppeteer.connect({ browserWSEndpoint: ws });
|
|
76
|
+
} catch {
|
|
77
|
+
// Connection failed, clean up and launch new
|
|
78
|
+
cleanup();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Launch new browser
|
|
83
|
+
return launchBrowser(headless);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get a page from the browser
|
|
87
|
+
async function getPage(browser: Browser, newTab = false): Promise<Page> {
|
|
88
|
+
if (newTab) {
|
|
89
|
+
return browser.newPage();
|
|
90
|
+
}
|
|
20
91
|
const pages = await browser.pages();
|
|
21
|
-
|
|
22
|
-
return { page, browser };
|
|
92
|
+
return pages[pages.length - 1] || browser.newPage();
|
|
23
93
|
}
|
|
24
94
|
|
|
25
|
-
|
|
95
|
+
// Helper to run commands with browser
|
|
96
|
+
async function withBrowser<T>(
|
|
97
|
+
fn: (browser: Browser, page: Page) => Promise<T>,
|
|
98
|
+
options: { headless?: boolean; newTab?: boolean } = {}
|
|
99
|
+
): Promise<T> {
|
|
100
|
+
const browser = await getBrowser(options.headless ?? false);
|
|
101
|
+
const page = await getPage(browser, options.newTab);
|
|
102
|
+
try {
|
|
103
|
+
return await fn(browser, page);
|
|
104
|
+
} finally {
|
|
105
|
+
browser.disconnect(); // Disconnect but keep browser running
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const program = cli('chrome', '1.0.9', 'Chrome browser automation');
|
|
110
|
+
|
|
111
|
+
// Global headless option
|
|
112
|
+
program.option('--headless', 'Run in headless mode');
|
|
26
113
|
|
|
27
114
|
program
|
|
28
|
-
.command('
|
|
29
|
-
.description('
|
|
30
|
-
.action(withErrorHandling(async (
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
115
|
+
.command('launch')
|
|
116
|
+
.description('Launch managed browser')
|
|
117
|
+
.action(withErrorHandling(async () => {
|
|
118
|
+
const headless = program.opts().headless ?? false;
|
|
119
|
+
|
|
120
|
+
if (isBrowserRunning()) {
|
|
121
|
+
log('Browser already running');
|
|
122
|
+
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
123
|
+
output({ running: true, wsEndpoint: ws, profileDir: PROFILE_DIR });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const browser = await launchBrowser(headless);
|
|
128
|
+
log(`Browser launched ${headless ? '(headless)' : ''}`);
|
|
129
|
+
output({
|
|
130
|
+
launched: true,
|
|
131
|
+
headless,
|
|
132
|
+
wsEndpoint: browser.wsEndpoint(),
|
|
133
|
+
profileDir: PROFILE_DIR
|
|
134
|
+
});
|
|
36
135
|
browser.disconnect();
|
|
37
136
|
}));
|
|
38
137
|
|
|
138
|
+
program
|
|
139
|
+
.command('close')
|
|
140
|
+
.description('Close managed browser')
|
|
141
|
+
.action(withErrorHandling(async () => {
|
|
142
|
+
if (!isBrowserRunning()) {
|
|
143
|
+
log('No browser running');
|
|
144
|
+
output({ closed: false, reason: 'not_running' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const ws = readFileSync(WS_FILE, 'utf-8').trim();
|
|
150
|
+
const browser = await puppeteer.connect({ browserWSEndpoint: ws });
|
|
151
|
+
await browser.close();
|
|
152
|
+
cleanup();
|
|
153
|
+
log('Browser closed');
|
|
154
|
+
output({ closed: true });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
cleanup();
|
|
157
|
+
output({ closed: true, note: 'force_cleaned' });
|
|
158
|
+
}
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
program
|
|
162
|
+
.command('status')
|
|
163
|
+
.description('Check browser status')
|
|
164
|
+
.action(withErrorHandling(async () => {
|
|
165
|
+
const running = isBrowserRunning();
|
|
166
|
+
output({
|
|
167
|
+
running,
|
|
168
|
+
profileDir: PROFILE_DIR,
|
|
169
|
+
wsEndpoint: running && existsSync(WS_FILE) ? readFileSync(WS_FILE, 'utf-8').trim() : null,
|
|
170
|
+
});
|
|
171
|
+
}));
|
|
172
|
+
|
|
39
173
|
program
|
|
40
174
|
.command('navigate <url>')
|
|
41
|
-
.description('Open URL')
|
|
175
|
+
.description('Open URL in browser')
|
|
42
176
|
.option('--new-tab', 'Open in new tab')
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
browser
|
|
177
|
+
.option('--wait <event>', 'Wait for: load, domcontentloaded, networkidle0, networkidle2', 'networkidle2')
|
|
178
|
+
.action(withErrorHandling(async (url: string, options: { newTab?: boolean; wait?: string }) => {
|
|
179
|
+
const headless = program.opts().headless ?? false;
|
|
180
|
+
const waitUntil = options.wait as 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
|
|
181
|
+
|
|
182
|
+
await withBrowser(async (browser, page) => {
|
|
183
|
+
await page.goto(url, { waitUntil });
|
|
184
|
+
const title = await page.title();
|
|
185
|
+
log(`Navigated to: ${title}`);
|
|
186
|
+
output({ title, url: page.url() });
|
|
187
|
+
}, { headless, newTab: options.newTab });
|
|
188
|
+
}));
|
|
189
|
+
|
|
190
|
+
program
|
|
191
|
+
.command('click <selector>')
|
|
192
|
+
.description('Click an element')
|
|
193
|
+
.action(withErrorHandling(async (selector: string) => {
|
|
194
|
+
const headless = program.opts().headless ?? false;
|
|
195
|
+
|
|
196
|
+
await withBrowser(async (browser, page) => {
|
|
197
|
+
await page.waitForSelector(selector, { timeout: 5000 });
|
|
198
|
+
await page.click(selector);
|
|
199
|
+
log(`Clicked: ${selector}`);
|
|
200
|
+
output({ clicked: true, selector });
|
|
201
|
+
}, { headless });
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
program
|
|
205
|
+
.command('type <selector> <text>')
|
|
206
|
+
.description('Type text into an input')
|
|
207
|
+
.option('--clear', 'Clear field first')
|
|
208
|
+
.action(withErrorHandling(async (selector: string, text: string, options: { clear?: boolean }) => {
|
|
209
|
+
const headless = program.opts().headless ?? false;
|
|
210
|
+
|
|
211
|
+
await withBrowser(async (browser, page) => {
|
|
212
|
+
await page.waitForSelector(selector, { timeout: 5000 });
|
|
213
|
+
if (options.clear) {
|
|
214
|
+
await page.click(selector, { clickCount: 3 });
|
|
215
|
+
}
|
|
216
|
+
await page.type(selector, text);
|
|
217
|
+
log(`Typed into: ${selector}`);
|
|
218
|
+
output({ typed: true, selector, length: text.length });
|
|
219
|
+
}, { headless });
|
|
49
220
|
}));
|
|
50
221
|
|
|
51
222
|
program
|
|
52
223
|
.command('eval <script>')
|
|
53
|
-
.description('Run JavaScript
|
|
224
|
+
.description('Run JavaScript in page context')
|
|
54
225
|
.action(withErrorHandling(async (script: string) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
226
|
+
const headless = program.opts().headless ?? false;
|
|
227
|
+
log('Executing JavaScript in browser context');
|
|
228
|
+
|
|
229
|
+
await withBrowser(async (browser, page) => {
|
|
230
|
+
const result = await page.evaluate((code: string) => {
|
|
231
|
+
return eval(code);
|
|
232
|
+
}, script);
|
|
233
|
+
if (result !== undefined) output(result);
|
|
234
|
+
}, { headless });
|
|
63
235
|
}));
|
|
64
236
|
|
|
65
237
|
program
|
|
66
238
|
.command('screenshot [output]')
|
|
67
|
-
.description('Capture
|
|
239
|
+
.description('Capture screenshot')
|
|
68
240
|
.option('--full-page', 'Capture full page')
|
|
69
|
-
.
|
|
70
|
-
|
|
241
|
+
.option('--selector <sel>', 'Screenshot specific element')
|
|
242
|
+
.action(withErrorHandling(async (outputFile = 'screenshot.png', options: { fullPage?: boolean; selector?: string }) => {
|
|
243
|
+
const headless = program.opts().headless ?? false;
|
|
71
244
|
const outputPath = resolve(outputFile);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
245
|
+
|
|
246
|
+
await withBrowser(async (browser, page) => {
|
|
247
|
+
if (options.selector) {
|
|
248
|
+
const element = await page.$(options.selector);
|
|
249
|
+
if (!element) {
|
|
250
|
+
outputError('NOT_FOUND', `Element not found: ${options.selector}`);
|
|
251
|
+
}
|
|
252
|
+
await element!.screenshot({ path: outputPath });
|
|
253
|
+
} else {
|
|
254
|
+
await page.screenshot({ path: outputPath, fullPage: options.fullPage });
|
|
255
|
+
}
|
|
256
|
+
log(`Saved: ${outputPath}`);
|
|
257
|
+
output({ path: outputPath });
|
|
258
|
+
}, { headless });
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
program
|
|
262
|
+
.command('pdf [output]')
|
|
263
|
+
.description('Save page as PDF (headless only)')
|
|
264
|
+
.action(withErrorHandling(async (outputFile = 'page.pdf') => {
|
|
265
|
+
const outputPath = resolve(outputFile);
|
|
266
|
+
|
|
267
|
+
// PDF requires headless mode
|
|
268
|
+
await withBrowser(async (browser, page) => {
|
|
269
|
+
await page.pdf({ path: outputPath, format: 'A4' });
|
|
270
|
+
log(`Saved: ${outputPath}`);
|
|
271
|
+
output({ path: outputPath });
|
|
272
|
+
}, { headless: true });
|
|
76
273
|
}));
|
|
77
274
|
|
|
78
275
|
program
|
|
79
276
|
.command('cookies [domain]')
|
|
80
277
|
.description('Get cookies')
|
|
81
278
|
.action(withErrorHandling(async (domain?: string) => {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
279
|
+
const headless = program.opts().headless ?? false;
|
|
280
|
+
|
|
281
|
+
await withBrowser(async (browser, page) => {
|
|
282
|
+
const client = await page.createCDPSession();
|
|
283
|
+
const { cookies } = await client.send('Network.getAllCookies');
|
|
284
|
+
const filtered = domain
|
|
285
|
+
? cookies.filter((c: { domain: string }) => c.domain.includes(domain))
|
|
286
|
+
: cookies;
|
|
287
|
+
output(filtered);
|
|
288
|
+
log(`${filtered.length} cookies`);
|
|
289
|
+
}, { headless });
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
program
|
|
293
|
+
.command('html [selector]')
|
|
294
|
+
.description('Get page HTML or element HTML')
|
|
295
|
+
.action(withErrorHandling(async (selector?: string) => {
|
|
296
|
+
const headless = program.opts().headless ?? false;
|
|
297
|
+
|
|
298
|
+
await withBrowser(async (browser, page) => {
|
|
299
|
+
let html: string;
|
|
300
|
+
if (selector) {
|
|
301
|
+
const element = await page.$(selector);
|
|
302
|
+
if (!element) {
|
|
303
|
+
outputError('NOT_FOUND', `Element not found: ${selector}`);
|
|
304
|
+
}
|
|
305
|
+
html = await page.evaluate(el => el!.outerHTML, element);
|
|
306
|
+
} else {
|
|
307
|
+
html = await page.content();
|
|
308
|
+
}
|
|
309
|
+
output({ html, length: html.length });
|
|
310
|
+
}, { headless });
|
|
311
|
+
}));
|
|
312
|
+
|
|
313
|
+
program
|
|
314
|
+
.command('text [selector]')
|
|
315
|
+
.description('Get text content')
|
|
316
|
+
.action(withErrorHandling(async (selector?: string) => {
|
|
317
|
+
const headless = program.opts().headless ?? false;
|
|
318
|
+
|
|
319
|
+
await withBrowser(async (browser, page) => {
|
|
320
|
+
let text: string;
|
|
321
|
+
if (selector) {
|
|
322
|
+
const element = await page.$(selector);
|
|
323
|
+
if (!element) {
|
|
324
|
+
outputError('NOT_FOUND', `Element not found: ${selector}`);
|
|
325
|
+
}
|
|
326
|
+
text = await page.evaluate(el => el!.textContent || '', element);
|
|
327
|
+
} else {
|
|
328
|
+
text = await page.evaluate(() => document.body.innerText);
|
|
329
|
+
}
|
|
330
|
+
output({ text: text.trim(), length: text.trim().length });
|
|
331
|
+
}, { headless });
|
|
332
|
+
}));
|
|
333
|
+
|
|
334
|
+
program
|
|
335
|
+
.command('wait <selector>')
|
|
336
|
+
.description('Wait for element to appear')
|
|
337
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', '30000')
|
|
338
|
+
.action(withErrorHandling(async (selector: string, options: { timeout: string }) => {
|
|
339
|
+
const headless = program.opts().headless ?? false;
|
|
340
|
+
const timeout = parseInt(options.timeout, 10);
|
|
341
|
+
|
|
342
|
+
await withBrowser(async (browser, page) => {
|
|
343
|
+
await page.waitForSelector(selector, { timeout });
|
|
344
|
+
log(`Found: ${selector}`);
|
|
345
|
+
output({ found: true, selector });
|
|
346
|
+
}, { headless });
|
|
347
|
+
}));
|
|
348
|
+
|
|
349
|
+
program
|
|
350
|
+
.command('tabs')
|
|
351
|
+
.description('List open tabs')
|
|
352
|
+
.action(withErrorHandling(async () => {
|
|
353
|
+
const headless = program.opts().headless ?? false;
|
|
354
|
+
|
|
355
|
+
await withBrowser(async (browser, page) => {
|
|
356
|
+
const pages = await browser.pages();
|
|
357
|
+
const tabs = await Promise.all(pages.map(async (p, i) => ({
|
|
358
|
+
index: i,
|
|
359
|
+
url: p.url(),
|
|
360
|
+
title: await p.title(),
|
|
361
|
+
})));
|
|
362
|
+
output(tabs);
|
|
363
|
+
log(`${tabs.length} tabs`);
|
|
364
|
+
}, { headless });
|
|
91
365
|
}));
|
|
92
366
|
|
|
93
367
|
program.parse();
|