@cli4ai/chrome 1.1.1 → 1.1.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/cli4ai.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "chrome",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Chrome browser automation with managed browser instance",
5
5
  "author": "cliforai",
6
6
  "license": "BUSL-1.1",
7
- "entry": "run.ts",
7
+ "entry": "dist/run.js",
8
8
  "runtime": "node",
9
9
  "keywords": [
10
10
  "chrome",
@@ -29,62 +29,95 @@
29
29
  "navigate": {
30
30
  "description": "Open URL in browser",
31
31
  "args": [
32
- { "name": "url", "required": true }
32
+ {
33
+ "name": "url",
34
+ "required": true
35
+ }
33
36
  ]
34
37
  },
35
38
  "click": {
36
39
  "description": "Click an element",
37
40
  "args": [
38
- { "name": "selector", "required": true }
41
+ {
42
+ "name": "selector",
43
+ "required": true
44
+ }
39
45
  ]
40
46
  },
41
47
  "type": {
42
48
  "description": "Type text into an input",
43
49
  "args": [
44
- { "name": "selector", "required": true },
45
- { "name": "text", "required": true }
50
+ {
51
+ "name": "selector",
52
+ "required": true
53
+ },
54
+ {
55
+ "name": "text",
56
+ "required": true
57
+ }
46
58
  ]
47
59
  },
48
60
  "eval": {
49
61
  "description": "Run JavaScript in page context",
50
62
  "args": [
51
- { "name": "script", "required": true }
63
+ {
64
+ "name": "script",
65
+ "required": true
66
+ }
52
67
  ]
53
68
  },
54
69
  "screenshot": {
55
70
  "description": "Capture screenshot",
56
71
  "args": [
57
- { "name": "output", "required": false }
72
+ {
73
+ "name": "output",
74
+ "required": false
75
+ }
58
76
  ]
59
77
  },
60
78
  "pdf": {
61
79
  "description": "Save page as PDF",
62
80
  "args": [
63
- { "name": "output", "required": false }
81
+ {
82
+ "name": "output",
83
+ "required": false
84
+ }
64
85
  ]
65
86
  },
66
87
  "cookies": {
67
88
  "description": "Get cookies",
68
89
  "args": [
69
- { "name": "domain", "required": false }
90
+ {
91
+ "name": "domain",
92
+ "required": false
93
+ }
70
94
  ]
71
95
  },
72
96
  "html": {
73
97
  "description": "Get page HTML",
74
98
  "args": [
75
- { "name": "selector", "required": false }
99
+ {
100
+ "name": "selector",
101
+ "required": false
102
+ }
76
103
  ]
77
104
  },
78
105
  "text": {
79
106
  "description": "Get text content",
80
107
  "args": [
81
- { "name": "selector", "required": false }
108
+ {
109
+ "name": "selector",
110
+ "required": false
111
+ }
82
112
  ]
83
113
  },
84
114
  "wait": {
85
115
  "description": "Wait for element",
86
116
  "args": [
87
- { "name": "selector", "required": true }
117
+ {
118
+ "name": "selector",
119
+ "required": true
120
+ }
88
121
  ]
89
122
  },
90
123
  "tabs": {
package/dist/run.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/run.js ADDED
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+ import puppeteer from 'puppeteer';
3
+ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
4
+ import { join, resolve } from 'path';
5
+ import { homedir } from 'os';
6
+ import { cli, log, output, outputError, withErrorHandling } from '@cli4ai/lib';
7
+ // Persistent storage paths
8
+ const CLI4AI_DIR = join(homedir(), '.cli4ai');
9
+ const CHROME_DIR = join(CLI4AI_DIR, 'chrome');
10
+ const PROFILE_DIR = join(CHROME_DIR, 'profile');
11
+ const WS_FILE = join(CHROME_DIR, 'ws-endpoint');
12
+ const PID_FILE = join(CHROME_DIR, 'pid');
13
+ // Ensure directories exist
14
+ function ensureDirs() {
15
+ if (!existsSync(CLI4AI_DIR))
16
+ mkdirSync(CLI4AI_DIR, { recursive: true });
17
+ if (!existsSync(CHROME_DIR))
18
+ mkdirSync(CHROME_DIR, { recursive: true });
19
+ }
20
+ // Check if browser process is still running
21
+ function isBrowserRunning() {
22
+ if (!existsSync(PID_FILE))
23
+ return false;
24
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
25
+ try {
26
+ process.kill(pid, 0); // Check if process exists
27
+ return true;
28
+ }
29
+ catch {
30
+ // Process not running, clean up stale files
31
+ cleanup();
32
+ return false;
33
+ }
34
+ }
35
+ // Clean up state files
36
+ function cleanup() {
37
+ if (existsSync(WS_FILE))
38
+ unlinkSync(WS_FILE);
39
+ if (existsSync(PID_FILE))
40
+ unlinkSync(PID_FILE);
41
+ }
42
+ // Launch a new browser instance
43
+ async function launchBrowser(headless) {
44
+ ensureDirs();
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
+ '--disable-blink-features=AutomationControlled',
53
+ '--disable-features=IsolateOrigins,site-per-process',
54
+ ],
55
+ defaultViewport: null, // Use full window size
56
+ ignoreDefaultArgs: ['--enable-automation'],
57
+ });
58
+ // Remove automation detection signals
59
+ const userAgent = process.platform === 'win32'
60
+ ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'
61
+ : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36';
62
+ const pages = await browser.pages();
63
+ for (const page of pages) {
64
+ await page.setUserAgent(userAgent);
65
+ await page.evaluateOnNewDocument(() => {
66
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
67
+ });
68
+ }
69
+ // Save connection info
70
+ const wsEndpoint = browser.wsEndpoint();
71
+ writeFileSync(WS_FILE, wsEndpoint);
72
+ // Get browser process PID
73
+ const browserProcess = browser.process();
74
+ if (browserProcess?.pid) {
75
+ writeFileSync(PID_FILE, String(browserProcess.pid));
76
+ }
77
+ return browser;
78
+ }
79
+ // Get or create browser instance
80
+ async function getBrowser(headless) {
81
+ // Try to connect to existing browser
82
+ if (isBrowserRunning() && existsSync(WS_FILE)) {
83
+ try {
84
+ const ws = readFileSync(WS_FILE, 'utf-8').trim();
85
+ return await puppeteer.connect({ browserWSEndpoint: ws });
86
+ }
87
+ catch {
88
+ // Connection failed, clean up and launch new
89
+ cleanup();
90
+ }
91
+ }
92
+ // Launch new browser
93
+ return launchBrowser(headless);
94
+ }
95
+ // Get a page from the browser
96
+ async function getPage(browser, newTab = false) {
97
+ if (newTab) {
98
+ return browser.newPage();
99
+ }
100
+ const pages = await browser.pages();
101
+ return pages[pages.length - 1] || browser.newPage();
102
+ }
103
+ // Helper to run commands with browser
104
+ async function withBrowser(fn, options = {}) {
105
+ const browser = await getBrowser(options.headless ?? false);
106
+ const page = await getPage(browser, options.newTab);
107
+ try {
108
+ return await fn(browser, page);
109
+ }
110
+ finally {
111
+ browser.disconnect(); // Disconnect but keep browser running
112
+ }
113
+ }
114
+ const program = cli('chrome', '1.0.9', 'Chrome browser automation');
115
+ // Global headless option
116
+ program.option('--headless', 'Run in headless mode');
117
+ program
118
+ .command('launch')
119
+ .description('Launch managed browser')
120
+ .action(withErrorHandling(async () => {
121
+ const headless = program.opts().headless ?? false;
122
+ if (isBrowserRunning()) {
123
+ log('Browser already running');
124
+ const ws = readFileSync(WS_FILE, 'utf-8').trim();
125
+ output({ running: true, wsEndpoint: ws, profileDir: PROFILE_DIR });
126
+ return;
127
+ }
128
+ const browser = await launchBrowser(headless);
129
+ log(`Browser launched ${headless ? '(headless)' : ''}`);
130
+ output({
131
+ launched: true,
132
+ headless,
133
+ wsEndpoint: browser.wsEndpoint(),
134
+ profileDir: PROFILE_DIR
135
+ });
136
+ browser.disconnect();
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
+ try {
148
+ const ws = readFileSync(WS_FILE, 'utf-8').trim();
149
+ const browser = await puppeteer.connect({ browserWSEndpoint: ws });
150
+ await browser.close();
151
+ cleanup();
152
+ log('Browser closed');
153
+ output({ closed: true });
154
+ }
155
+ catch (err) {
156
+ cleanup();
157
+ output({ closed: true, note: 'force_cleaned' });
158
+ }
159
+ }));
160
+ program
161
+ .command('status')
162
+ .description('Check browser status')
163
+ .action(withErrorHandling(async () => {
164
+ const running = isBrowserRunning();
165
+ output({
166
+ running,
167
+ profileDir: PROFILE_DIR,
168
+ wsEndpoint: running && existsSync(WS_FILE) ? readFileSync(WS_FILE, 'utf-8').trim() : null,
169
+ });
170
+ }));
171
+ program
172
+ .command('navigate <url>')
173
+ .description('Open URL in browser')
174
+ .option('--new-tab', 'Open in new tab')
175
+ .option('--wait <event>', 'Wait for: load, domcontentloaded, networkidle0, networkidle2', 'networkidle2')
176
+ .action(withErrorHandling(async (url, options) => {
177
+ const headless = program.opts().headless ?? false;
178
+ const waitUntil = options.wait;
179
+ await withBrowser(async (browser, page) => {
180
+ await page.goto(url, { waitUntil });
181
+ const title = await page.title();
182
+ log(`Navigated to: ${title}`);
183
+ output({ title, url: page.url() });
184
+ }, { headless, newTab: options.newTab });
185
+ }));
186
+ program
187
+ .command('click <selector>')
188
+ .description('Click an element')
189
+ .action(withErrorHandling(async (selector) => {
190
+ const headless = program.opts().headless ?? false;
191
+ await withBrowser(async (browser, page) => {
192
+ await page.waitForSelector(selector, { timeout: 5000 });
193
+ await page.click(selector);
194
+ log(`Clicked: ${selector}`);
195
+ output({ clicked: true, selector });
196
+ }, { headless });
197
+ }));
198
+ program
199
+ .command('type <selector> <text>')
200
+ .description('Type text into an input')
201
+ .option('--clear', 'Clear field first')
202
+ .action(withErrorHandling(async (selector, text, options) => {
203
+ const headless = program.opts().headless ?? false;
204
+ await withBrowser(async (browser, page) => {
205
+ await page.waitForSelector(selector, { timeout: 5000 });
206
+ if (options.clear) {
207
+ await page.click(selector, { clickCount: 3 });
208
+ }
209
+ await page.type(selector, text);
210
+ log(`Typed into: ${selector}`);
211
+ output({ typed: true, selector, length: text.length });
212
+ }, { headless });
213
+ }));
214
+ program
215
+ .command('eval <script>')
216
+ .description('Run JavaScript in page context')
217
+ .action(withErrorHandling(async (script) => {
218
+ const headless = program.opts().headless ?? false;
219
+ log('Executing JavaScript in browser context');
220
+ await withBrowser(async (browser, page) => {
221
+ const result = await page.evaluate((code) => {
222
+ return eval(code);
223
+ }, script);
224
+ if (result !== undefined)
225
+ output(result);
226
+ }, { headless });
227
+ }));
228
+ program
229
+ .command('screenshot [output]')
230
+ .description('Capture screenshot')
231
+ .option('--full-page', 'Capture full page')
232
+ .option('--selector <sel>', 'Screenshot specific element')
233
+ .action(withErrorHandling(async (outputFile = 'screenshot.png', options) => {
234
+ const headless = program.opts().headless ?? false;
235
+ const outputPath = resolve(outputFile);
236
+ await withBrowser(async (browser, page) => {
237
+ if (options.selector) {
238
+ const element = await page.$(options.selector);
239
+ if (!element) {
240
+ outputError('NOT_FOUND', `Element not found: ${options.selector}`);
241
+ }
242
+ await element.screenshot({ path: outputPath });
243
+ }
244
+ else {
245
+ await page.screenshot({ path: outputPath, fullPage: options.fullPage });
246
+ }
247
+ log(`Saved: ${outputPath}`);
248
+ output({ path: outputPath });
249
+ }, { headless });
250
+ }));
251
+ program
252
+ .command('pdf [output]')
253
+ .description('Save page as PDF (headless only)')
254
+ .action(withErrorHandling(async (outputFile = 'page.pdf') => {
255
+ const outputPath = resolve(outputFile);
256
+ // PDF requires headless mode
257
+ await withBrowser(async (browser, page) => {
258
+ await page.pdf({ path: outputPath, format: 'A4' });
259
+ log(`Saved: ${outputPath}`);
260
+ output({ path: outputPath });
261
+ }, { headless: true });
262
+ }));
263
+ program
264
+ .command('cookies [domain]')
265
+ .description('Get cookies')
266
+ .action(withErrorHandling(async (domain) => {
267
+ const headless = program.opts().headless ?? false;
268
+ await withBrowser(async (browser, page) => {
269
+ const client = await page.createCDPSession();
270
+ const { cookies } = await client.send('Network.getAllCookies');
271
+ const filtered = domain
272
+ ? cookies.filter((c) => c.domain.includes(domain))
273
+ : cookies;
274
+ output(filtered);
275
+ log(`${filtered.length} cookies`);
276
+ }, { headless });
277
+ }));
278
+ program
279
+ .command('html [selector]')
280
+ .description('Get page HTML or element HTML')
281
+ .action(withErrorHandling(async (selector) => {
282
+ const headless = program.opts().headless ?? false;
283
+ await withBrowser(async (browser, page) => {
284
+ let html;
285
+ if (selector) {
286
+ const element = await page.$(selector);
287
+ if (!element) {
288
+ outputError('NOT_FOUND', `Element not found: ${selector}`);
289
+ }
290
+ html = await page.evaluate(el => el.outerHTML, element);
291
+ }
292
+ else {
293
+ html = await page.content();
294
+ }
295
+ output({ html, length: html.length });
296
+ }, { headless });
297
+ }));
298
+ program
299
+ .command('text [selector]')
300
+ .description('Get text content')
301
+ .action(withErrorHandling(async (selector) => {
302
+ const headless = program.opts().headless ?? false;
303
+ await withBrowser(async (browser, page) => {
304
+ let text;
305
+ if (selector) {
306
+ const element = await page.$(selector);
307
+ if (!element) {
308
+ outputError('NOT_FOUND', `Element not found: ${selector}`);
309
+ }
310
+ text = await page.evaluate(el => el.textContent || '', element);
311
+ }
312
+ else {
313
+ text = await page.evaluate(() => document.body.innerText);
314
+ }
315
+ output({ text: text.trim(), length: text.trim().length });
316
+ }, { headless });
317
+ }));
318
+ program
319
+ .command('wait <selector>')
320
+ .description('Wait for element to appear')
321
+ .option('--timeout <ms>', 'Timeout in milliseconds', '30000')
322
+ .action(withErrorHandling(async (selector, options) => {
323
+ const headless = program.opts().headless ?? false;
324
+ const timeout = parseInt(options.timeout, 10);
325
+ await withBrowser(async (browser, page) => {
326
+ await page.waitForSelector(selector, { timeout });
327
+ log(`Found: ${selector}`);
328
+ output({ found: true, selector });
329
+ }, { headless });
330
+ }));
331
+ program
332
+ .command('tabs')
333
+ .description('List open tabs')
334
+ .action(withErrorHandling(async () => {
335
+ const headless = program.opts().headless ?? false;
336
+ await withBrowser(async (browser, page) => {
337
+ const pages = await browser.pages();
338
+ const tabs = await Promise.all(pages.map(async (p, i) => ({
339
+ index: i,
340
+ url: p.url(),
341
+ title: await p.title(),
342
+ })));
343
+ output(tabs);
344
+ log(`${tabs.length} tabs`);
345
+ }, { headless });
346
+ }));
347
+ program.parse();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cli4ai/chrome",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Chrome browser automation with managed browser instance",
5
5
  "author": "cliforai",
6
6
  "license": "BUSL-1.1",
7
- "main": "run.ts",
7
+ "main": "dist/run.js",
8
8
  "bin": {
9
- "chrome": "./run.ts"
9
+ "chrome": "./dist/run.js"
10
10
  },
11
11
  "type": "module",
12
12
  "keywords": [
@@ -34,12 +34,20 @@
34
34
  "commander": "^14.0.0"
35
35
  },
36
36
  "files": [
37
- "run.ts",
37
+ "dist",
38
38
  "cli4ai.json",
39
39
  "README.md",
40
40
  "LICENSE"
41
41
  ],
42
42
  "publishConfig": {
43
43
  "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "prepublishOnly": "npm run build"
48
+ },
49
+ "devDependencies": {
50
+ "typescript": "^5.0.0",
51
+ "@types/node": "^22.0.0"
44
52
  }
45
53
  }
package/run.ts DELETED
@@ -1,383 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- import puppeteer, { Browser, Page } from 'puppeteer';
3
- import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
4
- import { join, resolve } from 'path';
5
- import { homedir } from 'os';
6
- import { cli, log, output, outputError, withErrorHandling } from '@cli4ai/lib/cli.ts';
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');
14
-
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);
25
- try {
26
- process.kill(pid, 0); // Check if process exists
27
- return true;
28
- } catch {
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
- '--disable-blink-features=AutomationControlled',
53
- '--disable-features=IsolateOrigins,site-per-process',
54
- ],
55
- defaultViewport: null, // Use full window size
56
- ignoreDefaultArgs: ['--enable-automation'],
57
- });
58
-
59
- // Remove automation detection signals
60
- const userAgent = process.platform === 'win32'
61
- ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'
62
- : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36';
63
-
64
- const pages = await browser.pages();
65
- for (const page of pages) {
66
- await page.setUserAgent(userAgent);
67
- await page.evaluateOnNewDocument(() => {
68
- Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
69
- });
70
- }
71
-
72
- // Save connection info
73
- const wsEndpoint = browser.wsEndpoint();
74
- writeFileSync(WS_FILE, wsEndpoint);
75
-
76
- // Get browser process PID
77
- const browserProcess = browser.process();
78
- if (browserProcess?.pid) {
79
- writeFileSync(PID_FILE, String(browserProcess.pid));
80
- }
81
-
82
- return browser;
83
- }
84
-
85
- // Get or create browser instance
86
- async function getBrowser(headless: boolean): Promise<Browser> {
87
- // Try to connect to existing browser
88
- if (isBrowserRunning() && existsSync(WS_FILE)) {
89
- try {
90
- const ws = readFileSync(WS_FILE, 'utf-8').trim();
91
- return await puppeteer.connect({ browserWSEndpoint: ws });
92
- } catch {
93
- // Connection failed, clean up and launch new
94
- cleanup();
95
- }
96
- }
97
-
98
- // Launch new browser
99
- return launchBrowser(headless);
100
- }
101
-
102
- // Get a page from the browser
103
- async function getPage(browser: Browser, newTab = false): Promise<Page> {
104
- if (newTab) {
105
- return browser.newPage();
106
- }
107
- const pages = await browser.pages();
108
- return pages[pages.length - 1] || browser.newPage();
109
- }
110
-
111
- // Helper to run commands with browser
112
- async function withBrowser<T>(
113
- fn: (browser: Browser, page: Page) => Promise<T>,
114
- options: { headless?: boolean; newTab?: boolean } = {}
115
- ): Promise<T> {
116
- const browser = await getBrowser(options.headless ?? false);
117
- const page = await getPage(browser, options.newTab);
118
- try {
119
- return await fn(browser, page);
120
- } finally {
121
- browser.disconnect(); // Disconnect but keep browser running
122
- }
123
- }
124
-
125
- const program = cli('chrome', '1.0.9', 'Chrome browser automation');
126
-
127
- // Global headless option
128
- program.option('--headless', 'Run in headless mode');
129
-
130
- program
131
- .command('launch')
132
- .description('Launch managed browser')
133
- .action(withErrorHandling(async () => {
134
- const headless = program.opts().headless ?? false;
135
-
136
- if (isBrowserRunning()) {
137
- log('Browser already running');
138
- const ws = readFileSync(WS_FILE, 'utf-8').trim();
139
- output({ running: true, wsEndpoint: ws, profileDir: PROFILE_DIR });
140
- return;
141
- }
142
-
143
- const browser = await launchBrowser(headless);
144
- log(`Browser launched ${headless ? '(headless)' : ''}`);
145
- output({
146
- launched: true,
147
- headless,
148
- wsEndpoint: browser.wsEndpoint(),
149
- profileDir: PROFILE_DIR
150
- });
151
- browser.disconnect();
152
- }));
153
-
154
- program
155
- .command('close')
156
- .description('Close managed browser')
157
- .action(withErrorHandling(async () => {
158
- if (!isBrowserRunning()) {
159
- log('No browser running');
160
- output({ closed: false, reason: 'not_running' });
161
- return;
162
- }
163
-
164
- try {
165
- const ws = readFileSync(WS_FILE, 'utf-8').trim();
166
- const browser = await puppeteer.connect({ browserWSEndpoint: ws });
167
- await browser.close();
168
- cleanup();
169
- log('Browser closed');
170
- output({ closed: true });
171
- } catch (err) {
172
- cleanup();
173
- output({ closed: true, note: 'force_cleaned' });
174
- }
175
- }));
176
-
177
- program
178
- .command('status')
179
- .description('Check browser status')
180
- .action(withErrorHandling(async () => {
181
- const running = isBrowserRunning();
182
- output({
183
- running,
184
- profileDir: PROFILE_DIR,
185
- wsEndpoint: running && existsSync(WS_FILE) ? readFileSync(WS_FILE, 'utf-8').trim() : null,
186
- });
187
- }));
188
-
189
- program
190
- .command('navigate <url>')
191
- .description('Open URL in browser')
192
- .option('--new-tab', 'Open in new tab')
193
- .option('--wait <event>', 'Wait for: load, domcontentloaded, networkidle0, networkidle2', 'networkidle2')
194
- .action(withErrorHandling(async (url: string, options: { newTab?: boolean; wait?: string }) => {
195
- const headless = program.opts().headless ?? false;
196
- const waitUntil = options.wait as 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
197
-
198
- await withBrowser(async (browser, page) => {
199
- await page.goto(url, { waitUntil });
200
- const title = await page.title();
201
- log(`Navigated to: ${title}`);
202
- output({ title, url: page.url() });
203
- }, { headless, newTab: options.newTab });
204
- }));
205
-
206
- program
207
- .command('click <selector>')
208
- .description('Click an element')
209
- .action(withErrorHandling(async (selector: string) => {
210
- const headless = program.opts().headless ?? false;
211
-
212
- await withBrowser(async (browser, page) => {
213
- await page.waitForSelector(selector, { timeout: 5000 });
214
- await page.click(selector);
215
- log(`Clicked: ${selector}`);
216
- output({ clicked: true, selector });
217
- }, { headless });
218
- }));
219
-
220
- program
221
- .command('type <selector> <text>')
222
- .description('Type text into an input')
223
- .option('--clear', 'Clear field first')
224
- .action(withErrorHandling(async (selector: string, text: string, options: { clear?: boolean }) => {
225
- const headless = program.opts().headless ?? false;
226
-
227
- await withBrowser(async (browser, page) => {
228
- await page.waitForSelector(selector, { timeout: 5000 });
229
- if (options.clear) {
230
- await page.click(selector, { clickCount: 3 });
231
- }
232
- await page.type(selector, text);
233
- log(`Typed into: ${selector}`);
234
- output({ typed: true, selector, length: text.length });
235
- }, { headless });
236
- }));
237
-
238
- program
239
- .command('eval <script>')
240
- .description('Run JavaScript in page context')
241
- .action(withErrorHandling(async (script: string) => {
242
- const headless = program.opts().headless ?? false;
243
- log('Executing JavaScript in browser context');
244
-
245
- await withBrowser(async (browser, page) => {
246
- const result = await page.evaluate((code: string) => {
247
- return eval(code);
248
- }, script);
249
- if (result !== undefined) output(result);
250
- }, { headless });
251
- }));
252
-
253
- program
254
- .command('screenshot [output]')
255
- .description('Capture screenshot')
256
- .option('--full-page', 'Capture full page')
257
- .option('--selector <sel>', 'Screenshot specific element')
258
- .action(withErrorHandling(async (outputFile = 'screenshot.png', options: { fullPage?: boolean; selector?: string }) => {
259
- const headless = program.opts().headless ?? false;
260
- const outputPath = resolve(outputFile);
261
-
262
- await withBrowser(async (browser, page) => {
263
- if (options.selector) {
264
- const element = await page.$(options.selector);
265
- if (!element) {
266
- outputError('NOT_FOUND', `Element not found: ${options.selector}`);
267
- }
268
- await element!.screenshot({ path: outputPath });
269
- } else {
270
- await page.screenshot({ path: outputPath, fullPage: options.fullPage });
271
- }
272
- log(`Saved: ${outputPath}`);
273
- output({ path: outputPath });
274
- }, { headless });
275
- }));
276
-
277
- program
278
- .command('pdf [output]')
279
- .description('Save page as PDF (headless only)')
280
- .action(withErrorHandling(async (outputFile = 'page.pdf') => {
281
- const outputPath = resolve(outputFile);
282
-
283
- // PDF requires headless mode
284
- await withBrowser(async (browser, page) => {
285
- await page.pdf({ path: outputPath, format: 'A4' });
286
- log(`Saved: ${outputPath}`);
287
- output({ path: outputPath });
288
- }, { headless: true });
289
- }));
290
-
291
- program
292
- .command('cookies [domain]')
293
- .description('Get cookies')
294
- .action(withErrorHandling(async (domain?: string) => {
295
- const headless = program.opts().headless ?? false;
296
-
297
- await withBrowser(async (browser, page) => {
298
- const client = await page.createCDPSession();
299
- const { cookies } = await client.send('Network.getAllCookies');
300
- const filtered = domain
301
- ? cookies.filter((c: { domain: string }) => c.domain.includes(domain))
302
- : cookies;
303
- output(filtered);
304
- log(`${filtered.length} cookies`);
305
- }, { headless });
306
- }));
307
-
308
- program
309
- .command('html [selector]')
310
- .description('Get page HTML or element HTML')
311
- .action(withErrorHandling(async (selector?: string) => {
312
- const headless = program.opts().headless ?? false;
313
-
314
- await withBrowser(async (browser, page) => {
315
- let html: string;
316
- if (selector) {
317
- const element = await page.$(selector);
318
- if (!element) {
319
- outputError('NOT_FOUND', `Element not found: ${selector}`);
320
- }
321
- html = await page.evaluate(el => el!.outerHTML, element);
322
- } else {
323
- html = await page.content();
324
- }
325
- output({ html, length: html.length });
326
- }, { headless });
327
- }));
328
-
329
- program
330
- .command('text [selector]')
331
- .description('Get text content')
332
- .action(withErrorHandling(async (selector?: string) => {
333
- const headless = program.opts().headless ?? false;
334
-
335
- await withBrowser(async (browser, page) => {
336
- let text: string;
337
- if (selector) {
338
- const element = await page.$(selector);
339
- if (!element) {
340
- outputError('NOT_FOUND', `Element not found: ${selector}`);
341
- }
342
- text = await page.evaluate(el => el!.textContent || '', element);
343
- } else {
344
- text = await page.evaluate(() => document.body.innerText);
345
- }
346
- output({ text: text.trim(), length: text.trim().length });
347
- }, { headless });
348
- }));
349
-
350
- program
351
- .command('wait <selector>')
352
- .description('Wait for element to appear')
353
- .option('--timeout <ms>', 'Timeout in milliseconds', '30000')
354
- .action(withErrorHandling(async (selector: string, options: { timeout: string }) => {
355
- const headless = program.opts().headless ?? false;
356
- const timeout = parseInt(options.timeout, 10);
357
-
358
- await withBrowser(async (browser, page) => {
359
- await page.waitForSelector(selector, { timeout });
360
- log(`Found: ${selector}`);
361
- output({ found: true, selector });
362
- }, { headless });
363
- }));
364
-
365
- program
366
- .command('tabs')
367
- .description('List open tabs')
368
- .action(withErrorHandling(async () => {
369
- const headless = program.opts().headless ?? false;
370
-
371
- await withBrowser(async (browser, page) => {
372
- const pages = await browser.pages();
373
- const tabs = await Promise.all(pages.map(async (p, i) => ({
374
- index: i,
375
- url: p.url(),
376
- title: await p.title(),
377
- })));
378
- output(tabs);
379
- log(`${tabs.length} tabs`);
380
- }, { headless });
381
- }));
382
-
383
- program.parse();