stealth-cli 0.5.0 → 0.6.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 +248 -145
- package/bin/stealth.js +6 -1
- package/package.json +1 -1
- package/src/browser.js +45 -54
- package/src/commands/batch.js +7 -5
- package/src/commands/browse.js +9 -7
- package/src/commands/crawl.js +14 -12
- package/src/commands/extract.js +30 -17
- package/src/commands/fingerprint.js +6 -5
- package/src/commands/interactive.js +4 -2
- package/src/commands/monitor.js +7 -5
- package/src/commands/pdf.js +7 -5
- package/src/commands/screenshot.js +10 -8
- package/src/commands/search.js +10 -19
- package/src/commands/serve.js +43 -24
- package/src/daemon.js +4 -37
- package/src/errors.js +21 -13
- package/src/mcp-server.js +27 -20
- package/src/profiles.js +5 -4
- package/src/proxy-pool.js +8 -10
- package/src/session.js +3 -3
- package/src/utils/browser-factory.js +86 -0
- package/src/utils/resolve-opts.js +83 -0
package/src/browser.js
CHANGED
|
@@ -6,26 +6,16 @@
|
|
|
6
6
|
* 2. Daemon mode — reuses background browser (faster)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { launchOptions } from 'camoufox-js';
|
|
10
|
-
import { firefox } from 'playwright-core';
|
|
11
|
-
import os from 'os';
|
|
12
9
|
import { withRetry, navigateWithRetry } from './retry.js';
|
|
13
|
-
import {
|
|
10
|
+
import { postNavigationBehavior } from './humanize.js';
|
|
14
11
|
import { isDaemonRunning } from './daemon.js';
|
|
15
|
-
import { daemonNavigate,
|
|
12
|
+
import { daemonNavigate, daemonRequest } from './client.js';
|
|
16
13
|
import { loadProfile, touchProfile, saveCookiesToProfile, loadCookiesFromProfile } from './profiles.js';
|
|
17
14
|
import { restoreSession, captureSession } from './session.js';
|
|
18
|
-
import { getNextProxy
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*/
|
|
23
|
-
function getHostOS() {
|
|
24
|
-
const platform = os.platform();
|
|
25
|
-
if (platform === 'darwin') return 'macos';
|
|
26
|
-
if (platform === 'win32') return 'windows';
|
|
27
|
-
return 'linux';
|
|
28
|
-
}
|
|
15
|
+
import { getNextProxy } from './proxy-pool.js';
|
|
16
|
+
import { getHostOS, createBrowser, extractPageText } from './utils/browser-factory.js';
|
|
17
|
+
import { log } from './output.js';
|
|
18
|
+
import { BrowserLaunchError, NavigationError } from './errors.js';
|
|
29
19
|
|
|
30
20
|
/**
|
|
31
21
|
* Build proxy configuration
|
|
@@ -93,7 +83,9 @@ export async function launchBrowser(opts = {}) {
|
|
|
93
83
|
proxyStr = profileData.proxy;
|
|
94
84
|
}
|
|
95
85
|
touchProfile(profileName);
|
|
96
|
-
} catch {
|
|
86
|
+
} catch (err) {
|
|
87
|
+
log.warn(`Profile "${profileName}" failed to load: ${err.message}`);
|
|
88
|
+
}
|
|
97
89
|
}
|
|
98
90
|
|
|
99
91
|
// --- Proxy pool rotation ---
|
|
@@ -115,16 +107,16 @@ export async function launchBrowser(opts = {}) {
|
|
|
115
107
|
const hostOS = profileData?.fingerprint?.os || getHostOS();
|
|
116
108
|
const proxy = buildProxy(proxyStr);
|
|
117
109
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
|
|
110
|
+
let browser;
|
|
111
|
+
try {
|
|
112
|
+
browser = await createBrowser({
|
|
113
|
+
headless,
|
|
114
|
+
os: hostOS,
|
|
115
|
+
proxy: proxy || undefined,
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new BrowserLaunchError(err.message, { cause: err });
|
|
119
|
+
}
|
|
128
120
|
|
|
129
121
|
const contextOptions = {
|
|
130
122
|
viewport,
|
|
@@ -157,7 +149,9 @@ export async function launchBrowser(opts = {}) {
|
|
|
157
149
|
if (sessionInfo?.lastUrl && sessionInfo.lastUrl !== 'about:blank') {
|
|
158
150
|
try {
|
|
159
151
|
await page.goto(sessionInfo.lastUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
160
|
-
} catch {
|
|
152
|
+
} catch (err) {
|
|
153
|
+
log.warn(`Session URL restore failed (${sessionInfo.lastUrl}): ${err.message}`);
|
|
154
|
+
}
|
|
161
155
|
}
|
|
162
156
|
|
|
163
157
|
return {
|
|
@@ -205,26 +199,31 @@ export async function closeBrowser(handle) {
|
|
|
205
199
|
export async function navigate(handle, url, opts = {}) {
|
|
206
200
|
const { timeout = 30000, waitUntil = 'domcontentloaded', humanize = false, retries = 2 } = opts;
|
|
207
201
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
202
|
+
try {
|
|
203
|
+
if (handle.isDaemon) {
|
|
204
|
+
const result = await withRetry(
|
|
205
|
+
async () => {
|
|
206
|
+
const res = await daemonNavigate(url, { timeout, waitUntil });
|
|
207
|
+
if (!res?.ok) throw new Error(res?.error || 'Daemon navigation failed');
|
|
208
|
+
return res.url;
|
|
209
|
+
},
|
|
210
|
+
{ maxRetries: retries, label: `navigate(daemon)` },
|
|
211
|
+
);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
219
214
|
|
|
220
|
-
|
|
215
|
+
const finalUrl = await navigateWithRetry(handle.page, url, { timeout, waitUntil, maxRetries: retries });
|
|
221
216
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
217
|
+
// Human behavior after navigation
|
|
218
|
+
if (humanize) {
|
|
219
|
+
await postNavigationBehavior(handle.page);
|
|
220
|
+
}
|
|
226
221
|
|
|
227
|
-
|
|
222
|
+
return finalUrl;
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (err instanceof NavigationError) throw err;
|
|
225
|
+
throw new NavigationError(url, err);
|
|
226
|
+
}
|
|
228
227
|
}
|
|
229
228
|
|
|
230
229
|
/**
|
|
@@ -273,15 +272,7 @@ export async function getTextContent(handle) {
|
|
|
273
272
|
return res?.text || '';
|
|
274
273
|
}
|
|
275
274
|
|
|
276
|
-
return handle.page.evaluate(
|
|
277
|
-
const body = document.body;
|
|
278
|
-
if (!body) return '';
|
|
279
|
-
|
|
280
|
-
const clone = body.cloneNode(true);
|
|
281
|
-
clone.querySelectorAll('script, style, noscript').forEach((el) => el.remove());
|
|
282
|
-
|
|
283
|
-
return clone.innerText || clone.textContent || '';
|
|
284
|
-
});
|
|
275
|
+
return handle.page.evaluate(extractPageText);
|
|
285
276
|
}
|
|
286
277
|
|
|
287
278
|
/**
|
package/src/commands/batch.js
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
import { navigateWithRetry } from '../retry.js';
|
|
12
12
|
import { randomDelay } from '../humanize.js';
|
|
13
13
|
import { formatOutput, log } from '../output.js';
|
|
14
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
15
|
+
import { handleError } from '../errors.js';
|
|
14
16
|
|
|
15
17
|
export function registerBatch(program) {
|
|
16
18
|
program
|
|
@@ -30,6 +32,7 @@ export function registerBatch(program) {
|
|
|
30
32
|
.option('--retries <n>', 'Max retries per URL', '2')
|
|
31
33
|
.option('--skip-errors', 'Continue on errors instead of stopping')
|
|
32
34
|
.action(async (file, opts) => {
|
|
35
|
+
opts = resolveOpts(opts);
|
|
33
36
|
// Read URLs from file
|
|
34
37
|
if (!fs.existsSync(file)) {
|
|
35
38
|
log.error(`File not found: ${file}`);
|
|
@@ -81,10 +84,10 @@ export function registerBatch(program) {
|
|
|
81
84
|
|
|
82
85
|
try {
|
|
83
86
|
if (handle.isDaemon) {
|
|
84
|
-
await navigate(handle, url, { retries:
|
|
87
|
+
await navigate(handle, url, { retries: opts.retries });
|
|
85
88
|
} else {
|
|
86
89
|
await navigateWithRetry(handle.page, url, {
|
|
87
|
-
maxRetries:
|
|
90
|
+
maxRetries: opts.retries,
|
|
88
91
|
});
|
|
89
92
|
await waitForReady(handle.page, { timeout: 3000 });
|
|
90
93
|
}
|
|
@@ -159,7 +162,7 @@ export function registerBatch(program) {
|
|
|
159
162
|
|
|
160
163
|
// Delay between URLs
|
|
161
164
|
if (i < urls.length - 1) {
|
|
162
|
-
const delay =
|
|
165
|
+
const delay = opts.delay;
|
|
163
166
|
await randomDelay(delay * 0.8, delay * 1.2);
|
|
164
167
|
}
|
|
165
168
|
}
|
|
@@ -170,9 +173,8 @@ export function registerBatch(program) {
|
|
|
170
173
|
log.success(`Batch complete: ${success} succeeded, ${failed} failed, ${urls.length} total`);
|
|
171
174
|
} catch (err) {
|
|
172
175
|
spinner.stop();
|
|
173
|
-
log.error(`Batch failed: ${err.message}`);
|
|
174
176
|
log.dim(` Completed: ${success}/${urls.length}`);
|
|
175
|
-
|
|
177
|
+
handleError(err, { log });
|
|
176
178
|
} finally {
|
|
177
179
|
if (handle) await closeBrowser(handle);
|
|
178
180
|
}
|
package/src/commands/browse.js
CHANGED
|
@@ -8,25 +8,28 @@ import {
|
|
|
8
8
|
getTextContent, getTitle, getUrl, evaluate, waitForReady,
|
|
9
9
|
} from '../browser.js';
|
|
10
10
|
import { formatOutput, log } from '../output.js';
|
|
11
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
12
|
+
import { handleError } from '../errors.js';
|
|
11
13
|
|
|
12
14
|
export function registerBrowse(program) {
|
|
13
15
|
program
|
|
14
16
|
.command('browse')
|
|
15
17
|
.description('Visit a URL and print page content')
|
|
16
18
|
.argument('<url>', 'URL to visit')
|
|
17
|
-
.option('-f, --format <format>', 'Output format: text, json, markdown, snapshot'
|
|
19
|
+
.option('-f, --format <format>', 'Output format: text, json, markdown, snapshot')
|
|
18
20
|
.option('-w, --wait <ms>', 'Wait time after page load (ms)', '2000')
|
|
19
21
|
.option('--proxy <proxy>', 'Proxy server (http://user:pass@host:port)')
|
|
20
22
|
.option('--cookies <file>', 'Load cookies from Netscape-format file')
|
|
21
23
|
.option('--no-headless', 'Show browser window')
|
|
22
|
-
.option('--locale <locale>', 'Browser locale'
|
|
24
|
+
.option('--locale <locale>', 'Browser locale')
|
|
23
25
|
.option('--user-agent', 'Print the browser user-agent')
|
|
24
26
|
.option('--humanize', 'Enable human behavior simulation')
|
|
25
|
-
.option('--retries <n>', 'Max retries on failure'
|
|
27
|
+
.option('--retries <n>', 'Max retries on failure')
|
|
26
28
|
.option('--profile <name>', 'Use a browser profile')
|
|
27
29
|
.option('--session <name>', 'Use/restore a named session')
|
|
28
30
|
.option('--proxy-rotate', 'Rotate proxy from pool')
|
|
29
31
|
.action(async (url, opts) => {
|
|
32
|
+
opts = resolveOpts(opts);
|
|
30
33
|
const spinner = ora('Launching stealth browser...').start();
|
|
31
34
|
let handle;
|
|
32
35
|
|
|
@@ -50,11 +53,11 @@ export function registerBrowse(program) {
|
|
|
50
53
|
spinner.text = `Navigating to ${url}...`;
|
|
51
54
|
await navigate(handle, url, {
|
|
52
55
|
humanize: opts.humanize,
|
|
53
|
-
retries:
|
|
56
|
+
retries: opts.retries,
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
if (!handle.isDaemon) {
|
|
57
|
-
await waitForReady(handle.page, { timeout:
|
|
60
|
+
await waitForReady(handle.page, { timeout: opts.wait });
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
spinner.stop();
|
|
@@ -92,8 +95,7 @@ export function registerBrowse(program) {
|
|
|
92
95
|
log.success(`Done: ${currentUrl}`);
|
|
93
96
|
} catch (err) {
|
|
94
97
|
spinner.stop();
|
|
95
|
-
|
|
96
|
-
process.exit(1);
|
|
98
|
+
handleError(err, { log });
|
|
97
99
|
} finally {
|
|
98
100
|
if (handle) await closeBrowser(handle);
|
|
99
101
|
}
|
package/src/commands/crawl.js
CHANGED
|
@@ -7,6 +7,8 @@ import { launchBrowser, closeBrowser, getTextContent, evaluate, waitForReady } f
|
|
|
7
7
|
import { navigateWithRetry } from '../retry.js';
|
|
8
8
|
import { randomDelay, humanScroll } from '../humanize.js';
|
|
9
9
|
import { formatOutput, log } from '../output.js';
|
|
10
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
11
|
+
import { handleError } from '../errors.js';
|
|
10
12
|
|
|
11
13
|
export function registerCrawl(program) {
|
|
12
14
|
program
|
|
@@ -25,17 +27,18 @@ export function registerCrawl(program) {
|
|
|
25
27
|
.option('--include <pattern>', 'Only crawl URLs matching this pattern (regex)')
|
|
26
28
|
.option('--exclude <pattern>', 'Skip URLs matching this pattern (regex)')
|
|
27
29
|
.option('--humanize', 'Enable human behavior simulation')
|
|
28
|
-
.option('--retries <n>', 'Max retries per page'
|
|
30
|
+
.option('--retries <n>', 'Max retries per page')
|
|
29
31
|
.option('--profile <name>', 'Use a browser profile')
|
|
30
32
|
.option('--proxy-rotate', 'Rotate proxy from pool')
|
|
31
33
|
.action(async (startUrl, opts) => {
|
|
34
|
+
opts = resolveOpts(opts);
|
|
32
35
|
const spinner = ora('Launching stealth browser...').start();
|
|
33
36
|
let handle;
|
|
34
37
|
|
|
35
|
-
const maxDepth =
|
|
36
|
-
const maxPages =
|
|
37
|
-
const delay =
|
|
38
|
-
const maxRetries =
|
|
38
|
+
const maxDepth = opts.depth;
|
|
39
|
+
const maxPages = opts.limit;
|
|
40
|
+
const delay = opts.delay;
|
|
41
|
+
const maxRetries = opts.retries;
|
|
39
42
|
const includeRegex = opts.include ? new RegExp(opts.include) : null;
|
|
40
43
|
const excludeRegex = opts.exclude ? new RegExp(opts.exclude) : null;
|
|
41
44
|
|
|
@@ -60,7 +63,7 @@ export function registerCrawl(program) {
|
|
|
60
63
|
const startOrigin = new URL(startUrl).origin;
|
|
61
64
|
const visited = new Set();
|
|
62
65
|
const queue = [{ url: startUrl, depth: 0 }];
|
|
63
|
-
|
|
66
|
+
let resultCount = 0;
|
|
64
67
|
let outputStream;
|
|
65
68
|
|
|
66
69
|
if (opts.output) {
|
|
@@ -78,10 +81,10 @@ export function registerCrawl(program) {
|
|
|
78
81
|
} else {
|
|
79
82
|
console.log(line);
|
|
80
83
|
}
|
|
81
|
-
|
|
84
|
+
resultCount++;
|
|
82
85
|
};
|
|
83
86
|
|
|
84
|
-
while (queue.length > 0 &&
|
|
87
|
+
while (queue.length > 0 && resultCount < maxPages) {
|
|
85
88
|
const { url, depth } = queue.shift();
|
|
86
89
|
|
|
87
90
|
if (visited.has(url)) continue;
|
|
@@ -90,7 +93,7 @@ export function registerCrawl(program) {
|
|
|
90
93
|
if (includeRegex && !includeRegex.test(url)) continue;
|
|
91
94
|
if (excludeRegex && excludeRegex.test(url)) continue;
|
|
92
95
|
|
|
93
|
-
spinner.text = `[${
|
|
96
|
+
spinner.text = `[${resultCount + 1}/${maxPages}] Crawling: ${url.slice(0, 60)}...`;
|
|
94
97
|
|
|
95
98
|
try {
|
|
96
99
|
// Navigate with retry
|
|
@@ -154,14 +157,13 @@ export function registerCrawl(program) {
|
|
|
154
157
|
if (outputStream) outputStream.end();
|
|
155
158
|
|
|
156
159
|
spinner.stop();
|
|
157
|
-
log.success(`Crawl complete: ${
|
|
160
|
+
log.success(`Crawl complete: ${resultCount} pages crawled`);
|
|
158
161
|
log.dim(` Start: ${startUrl}`);
|
|
159
162
|
log.dim(` Depth: ${maxDepth}, Visited: ${visited.size}`);
|
|
160
163
|
if (opts.output) log.dim(` Output: ${opts.output}`);
|
|
161
164
|
} catch (err) {
|
|
162
165
|
spinner.stop();
|
|
163
|
-
|
|
164
|
-
process.exit(1);
|
|
166
|
+
handleError(err, { log });
|
|
165
167
|
} finally {
|
|
166
168
|
if (handle) await closeBrowser(handle);
|
|
167
169
|
}
|
package/src/commands/extract.js
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
getUrl, evaluate, waitForReady,
|
|
9
9
|
} from '../browser.js';
|
|
10
10
|
import { formatOutput, log } from '../output.js';
|
|
11
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
12
|
+
import { handleError, ExtractionError } from '../errors.js';
|
|
11
13
|
|
|
12
14
|
export function registerExtract(program) {
|
|
13
15
|
program
|
|
@@ -27,11 +29,12 @@ export function registerExtract(program) {
|
|
|
27
29
|
.option('--cookies <file>', 'Load cookies from Netscape-format file')
|
|
28
30
|
.option('--no-headless', 'Show browser window')
|
|
29
31
|
.option('--humanize', 'Enable human behavior simulation')
|
|
30
|
-
.option('--retries <n>', 'Max retries on failure'
|
|
32
|
+
.option('--retries <n>', 'Max retries on failure')
|
|
31
33
|
.option('--profile <name>', 'Use a browser profile')
|
|
32
34
|
.option('--session <name>', 'Use/restore a named session')
|
|
33
35
|
.option('--proxy-rotate', 'Rotate proxy from pool')
|
|
34
36
|
.action(async (url, opts) => {
|
|
37
|
+
opts = resolveOpts(opts);
|
|
35
38
|
const spinner = ora('Launching stealth browser...').start();
|
|
36
39
|
let handle;
|
|
37
40
|
|
|
@@ -52,11 +55,11 @@ export function registerExtract(program) {
|
|
|
52
55
|
spinner.text = `Navigating to ${url}...`;
|
|
53
56
|
await navigate(handle, url, {
|
|
54
57
|
humanize: opts.humanize,
|
|
55
|
-
retries:
|
|
58
|
+
retries: opts.retries,
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
if (!handle.isDaemon) {
|
|
59
|
-
await waitForReady(handle.page, { timeout:
|
|
62
|
+
await waitForReady(handle.page, { timeout: opts.wait });
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
spinner.text = 'Extracting data...';
|
|
@@ -115,18 +118,29 @@ export function registerExtract(program) {
|
|
|
115
118
|
return headings;
|
|
116
119
|
})()`);
|
|
117
120
|
} else {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
121
|
+
// Use parameter passing (not string interpolation) to prevent injection
|
|
122
|
+
result = await handle.page.evaluate(
|
|
123
|
+
({ selector, attr, all }) => {
|
|
124
|
+
const elements = all
|
|
125
|
+
? Array.from(document.querySelectorAll(selector))
|
|
126
|
+
: [document.querySelector(selector)].filter(Boolean);
|
|
127
|
+
return elements.map(el => {
|
|
128
|
+
if (attr) return el.getAttribute(attr);
|
|
129
|
+
return el.textContent?.trim() || '';
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
{ selector: opts.selector, attr: opts.attr || null, all: !!opts.all },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Warn when custom selector matched nothing
|
|
137
|
+
if (opts.selector && opts.selector !== 'body' &&
|
|
138
|
+
(result === null || result === undefined ||
|
|
139
|
+
(Array.isArray(result) && result.length === 0))) {
|
|
140
|
+
throw new ExtractionError(
|
|
141
|
+
`No elements found matching "${opts.selector}"`,
|
|
142
|
+
{ hint: `Check the CSS selector. Try: stealth browse <url> -f snapshot` },
|
|
143
|
+
);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
const title = await getTitle(handle);
|
|
@@ -144,8 +158,7 @@ export function registerExtract(program) {
|
|
|
144
158
|
log.success(`Extracted from: ${currentUrl}`);
|
|
145
159
|
} catch (err) {
|
|
146
160
|
spinner.stop();
|
|
147
|
-
|
|
148
|
-
process.exit(1);
|
|
161
|
+
handleError(err, { log });
|
|
149
162
|
} finally {
|
|
150
163
|
if (handle) await closeBrowser(handle);
|
|
151
164
|
}
|
|
@@ -6,6 +6,8 @@ import ora from 'ora';
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { launchBrowser, closeBrowser, navigate, evaluate, waitForReady } from '../browser.js';
|
|
8
8
|
import { log } from '../output.js';
|
|
9
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
10
|
+
import { handleError } from '../errors.js';
|
|
9
11
|
|
|
10
12
|
export function registerFingerprint(program) {
|
|
11
13
|
program
|
|
@@ -18,7 +20,8 @@ export function registerFingerprint(program) {
|
|
|
18
20
|
.option('--compare <n>', 'Launch N times and compare fingerprints for uniqueness', '1')
|
|
19
21
|
.option('--no-headless', 'Show browser window')
|
|
20
22
|
.action(async (opts) => {
|
|
21
|
-
|
|
23
|
+
opts = resolveOpts(opts);
|
|
24
|
+
const compareCount = opts.compare;
|
|
22
25
|
|
|
23
26
|
if (compareCount > 1) {
|
|
24
27
|
await compareFingerprints(compareCount, opts);
|
|
@@ -135,8 +138,7 @@ async function showFingerprint(opts) {
|
|
|
135
138
|
console.log();
|
|
136
139
|
} catch (err) {
|
|
137
140
|
spinner.stop();
|
|
138
|
-
|
|
139
|
-
process.exit(1);
|
|
141
|
+
handleError(err, { log });
|
|
140
142
|
} finally {
|
|
141
143
|
if (handle) await closeBrowser(handle);
|
|
142
144
|
}
|
|
@@ -239,8 +241,7 @@ async function runDetectionTests(opts) {
|
|
|
239
241
|
}
|
|
240
242
|
} catch (err) {
|
|
241
243
|
spinner.stop();
|
|
242
|
-
|
|
243
|
-
process.exit(1);
|
|
244
|
+
handleError(err, { log });
|
|
244
245
|
} finally {
|
|
245
246
|
if (handle) await closeBrowser(handle);
|
|
246
247
|
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import { expandMacro, getSupportedEngines } from '../macros.js';
|
|
13
13
|
import { humanClick, humanType, humanScroll, randomDelay } from '../humanize.js';
|
|
14
14
|
import { log } from '../output.js';
|
|
15
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
16
|
+
import { handleError } from '../errors.js';
|
|
15
17
|
|
|
16
18
|
const HELP_TEXT = `
|
|
17
19
|
${chalk.bold('Navigation:')}
|
|
@@ -53,6 +55,7 @@ export function registerInteractive(program) {
|
|
|
53
55
|
.option('--no-headless', 'Show browser window')
|
|
54
56
|
.option('--url <url>', 'Initial URL to open')
|
|
55
57
|
.action(async (opts) => {
|
|
58
|
+
opts = resolveOpts(opts);
|
|
56
59
|
const spinner = ora('Launching stealth browser...').start();
|
|
57
60
|
let handle;
|
|
58
61
|
|
|
@@ -276,9 +279,8 @@ export function registerInteractive(program) {
|
|
|
276
279
|
});
|
|
277
280
|
} catch (err) {
|
|
278
281
|
spinner.stop();
|
|
279
|
-
log.error(`Failed to start: ${err.message}`);
|
|
280
282
|
if (handle) await closeBrowser(handle);
|
|
281
|
-
|
|
283
|
+
handleError(err, { log });
|
|
282
284
|
}
|
|
283
285
|
});
|
|
284
286
|
}
|
package/src/commands/monitor.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import { launchBrowser, closeBrowser, navigate,
|
|
7
|
+
import { launchBrowser, closeBrowser, navigate, waitForReady } from '../browser.js';
|
|
8
8
|
import { randomDelay } from '../humanize.js';
|
|
9
9
|
import { log } from '../output.js';
|
|
10
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
11
|
+
import { handleError } from '../errors.js';
|
|
10
12
|
|
|
11
13
|
export function registerMonitor(program) {
|
|
12
14
|
program
|
|
@@ -25,8 +27,9 @@ export function registerMonitor(program) {
|
|
|
25
27
|
.option('--proxy-rotate', 'Rotate proxy from pool')
|
|
26
28
|
.option('--no-headless', 'Show browser window')
|
|
27
29
|
.action(async (url, opts) => {
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
+
opts = resolveOpts(opts);
|
|
31
|
+
const interval = opts.interval * 1000;
|
|
32
|
+
const maxChecks = opts.count;
|
|
30
33
|
const selector = opts.selector || 'body';
|
|
31
34
|
|
|
32
35
|
log.info(`Monitoring ${url}`);
|
|
@@ -151,8 +154,7 @@ export function registerMonitor(program) {
|
|
|
151
154
|
console.log();
|
|
152
155
|
log.success(`Monitoring complete: ${checkCount} checks, ${changeCount} changes`);
|
|
153
156
|
} catch (err) {
|
|
154
|
-
|
|
155
|
-
process.exit(1);
|
|
157
|
+
handleError(err, { log });
|
|
156
158
|
} finally {
|
|
157
159
|
if (handle) await closeBrowser(handle);
|
|
158
160
|
}
|
package/src/commands/pdf.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { launchBrowser, closeBrowser, navigate, waitForReady } from '../browser.js';
|
|
7
7
|
import { log } from '../output.js';
|
|
8
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
9
|
+
import { handleError } from '../errors.js';
|
|
8
10
|
|
|
9
11
|
export function registerPdf(program) {
|
|
10
12
|
program
|
|
@@ -25,8 +27,9 @@ export function registerPdf(program) {
|
|
|
25
27
|
.option('--cookies <file>', 'Load cookies from Netscape-format file')
|
|
26
28
|
.option('--profile <name>', 'Use a browser profile')
|
|
27
29
|
.option('--no-headless', 'Show browser window')
|
|
28
|
-
.option('--retries <n>', 'Max retries on failure'
|
|
30
|
+
.option('--retries <n>', 'Max retries on failure')
|
|
29
31
|
.action(async (url, opts) => {
|
|
32
|
+
opts = resolveOpts(opts);
|
|
30
33
|
const spinner = ora('Launching stealth browser...').start();
|
|
31
34
|
let handle;
|
|
32
35
|
|
|
@@ -50,8 +53,8 @@ export function registerPdf(program) {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
spinner.text = `Navigating to ${url}...`;
|
|
53
|
-
await navigate(handle, url, { retries:
|
|
54
|
-
await waitForReady(handle.page, { timeout:
|
|
56
|
+
await navigate(handle, url, { retries: opts.retries });
|
|
57
|
+
await waitForReady(handle.page, { timeout: opts.wait });
|
|
55
58
|
|
|
56
59
|
spinner.text = 'Generating PDF...';
|
|
57
60
|
|
|
@@ -100,8 +103,7 @@ export function registerPdf(program) {
|
|
|
100
103
|
log.dim(` Format: ${opts.format}${opts.landscape ? ' landscape' : ''}`);
|
|
101
104
|
} catch (err) {
|
|
102
105
|
spinner.stop();
|
|
103
|
-
|
|
104
|
-
process.exit(1);
|
|
106
|
+
handleError(err, { log });
|
|
105
107
|
} finally {
|
|
106
108
|
if (handle) await closeBrowser(handle);
|
|
107
109
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { launchBrowser, closeBrowser, navigate, takeScreenshot, getUrl, waitForReady } from '../browser.js';
|
|
7
7
|
import { log } from '../output.js';
|
|
8
|
+
import { resolveOpts } from '../utils/resolve-opts.js';
|
|
9
|
+
import { handleError } from '../errors.js';
|
|
8
10
|
|
|
9
11
|
export function registerScreenshot(program) {
|
|
10
12
|
program
|
|
@@ -21,11 +23,12 @@ export function registerScreenshot(program) {
|
|
|
21
23
|
.option('--no-headless', 'Show browser window')
|
|
22
24
|
.option('--quality <n>', 'JPEG quality (1-100), only for .jpg output')
|
|
23
25
|
.option('--humanize', 'Enable human behavior simulation')
|
|
24
|
-
.option('--retries <n>', 'Max retries on failure'
|
|
26
|
+
.option('--retries <n>', 'Max retries on failure')
|
|
25
27
|
.option('--profile <name>', 'Use a browser profile')
|
|
26
28
|
.option('--session <name>', 'Use/restore a named session')
|
|
27
29
|
.option('--proxy-rotate', 'Rotate proxy from pool')
|
|
28
30
|
.action(async (url, opts) => {
|
|
31
|
+
opts = resolveOpts(opts);
|
|
29
32
|
const spinner = ora('Launching stealth browser...').start();
|
|
30
33
|
let handle;
|
|
31
34
|
|
|
@@ -37,8 +40,8 @@ export function registerScreenshot(program) {
|
|
|
37
40
|
profile: opts.profile,
|
|
38
41
|
session: opts.session,
|
|
39
42
|
viewport: {
|
|
40
|
-
width:
|
|
41
|
-
height:
|
|
43
|
+
width: opts.width,
|
|
44
|
+
height: opts.height,
|
|
42
45
|
},
|
|
43
46
|
});
|
|
44
47
|
|
|
@@ -51,11 +54,11 @@ export function registerScreenshot(program) {
|
|
|
51
54
|
spinner.text = `Navigating to ${url}...`;
|
|
52
55
|
await navigate(handle, url, {
|
|
53
56
|
humanize: opts.humanize,
|
|
54
|
-
retries:
|
|
57
|
+
retries: opts.retries,
|
|
55
58
|
});
|
|
56
59
|
|
|
57
60
|
if (!handle.isDaemon) {
|
|
58
|
-
await waitForReady(handle.page, { timeout:
|
|
61
|
+
await waitForReady(handle.page, { timeout: opts.wait });
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
spinner.text = 'Taking screenshot...';
|
|
@@ -72,7 +75,7 @@ export function registerScreenshot(program) {
|
|
|
72
75
|
|
|
73
76
|
if (opts.output.endsWith('.jpg') || opts.output.endsWith('.jpeg')) {
|
|
74
77
|
screenshotOpts.type = 'jpeg';
|
|
75
|
-
if (opts.quality) screenshotOpts.quality =
|
|
78
|
+
if (opts.quality) screenshotOpts.quality = opts.quality;
|
|
76
79
|
} else {
|
|
77
80
|
screenshotOpts.type = 'png';
|
|
78
81
|
}
|
|
@@ -87,8 +90,7 @@ export function registerScreenshot(program) {
|
|
|
87
90
|
log.dim(` Size: ${opts.width}x${opts.height}${opts.full ? ' (full page)' : ''}`);
|
|
88
91
|
} catch (err) {
|
|
89
92
|
spinner.stop();
|
|
90
|
-
|
|
91
|
-
process.exit(1);
|
|
93
|
+
handleError(err, { log });
|
|
92
94
|
} finally {
|
|
93
95
|
if (handle) await closeBrowser(handle);
|
|
94
96
|
}
|