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/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 { randomDelay, postNavigationBehavior } from './humanize.js';
10
+ import { postNavigationBehavior } from './humanize.js';
14
11
  import { isDaemonRunning } from './daemon.js';
15
- import { daemonNavigate, daemonText, daemonScreenshot, daemonRequest } from './client.js';
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, getRandomProxy, reportProxy } from './proxy-pool.js';
19
-
20
- /**
21
- * Detect host OS for fingerprint matching
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
- const options = await launchOptions({
119
- headless,
120
- os: hostOS,
121
- humanize: true,
122
- enable_cache: true,
123
- proxy: proxy || undefined,
124
- geoip: !!proxy,
125
- });
126
-
127
- const browser = await firefox.launch(options);
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
- if (handle.isDaemon) {
209
- const result = await withRetry(
210
- async () => {
211
- const res = await daemonNavigate(url, { timeout, waitUntil });
212
- if (!res?.ok) throw new Error(res?.error || 'Daemon navigation failed');
213
- return res.url;
214
- },
215
- { maxRetries: retries, label: `navigate(daemon)` },
216
- );
217
- return result;
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
- const finalUrl = await navigateWithRetry(handle.page, url, { timeout, waitUntil, maxRetries: retries });
215
+ const finalUrl = await navigateWithRetry(handle.page, url, { timeout, waitUntil, maxRetries: retries });
221
216
 
222
- // Human behavior after navigation
223
- if (humanize) {
224
- await postNavigationBehavior(handle.page);
225
- }
217
+ // Human behavior after navigation
218
+ if (humanize) {
219
+ await postNavigationBehavior(handle.page);
220
+ }
226
221
 
227
- return finalUrl;
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
  /**
@@ -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: parseInt(opts.retries) });
87
+ await navigate(handle, url, { retries: opts.retries });
85
88
  } else {
86
89
  await navigateWithRetry(handle.page, url, {
87
- maxRetries: parseInt(opts.retries),
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 = parseInt(opts.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
- process.exit(1);
177
+ handleError(err, { log });
176
178
  } finally {
177
179
  if (handle) await closeBrowser(handle);
178
180
  }
@@ -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', 'text')
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', 'en-US')
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', '2')
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: parseInt(opts.retries),
56
+ retries: opts.retries,
54
57
  });
55
58
 
56
59
  if (!handle.isDaemon) {
57
- await waitForReady(handle.page, { timeout: parseInt(opts.wait) });
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
- log.error(`Browse failed: ${err.message}`);
96
- process.exit(1);
98
+ handleError(err, { log });
97
99
  } finally {
98
100
  if (handle) await closeBrowser(handle);
99
101
  }
@@ -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', '2')
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 = parseInt(opts.depth);
36
- const maxPages = parseInt(opts.limit);
37
- const delay = parseInt(opts.delay);
38
- const maxRetries = parseInt(opts.retries);
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
- const results = [];
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
- results.push(result);
84
+ resultCount++;
82
85
  };
83
86
 
84
- while (queue.length > 0 && results.length < maxPages) {
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 = `[${results.length + 1}/${maxPages}] Crawling: ${url.slice(0, 60)}...`;
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: ${results.length} pages crawled`);
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
- log.error(`Crawl failed: ${err.message}`);
164
- process.exit(1);
166
+ handleError(err, { log });
165
167
  } finally {
166
168
  if (handle) await closeBrowser(handle);
167
169
  }
@@ -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', '2')
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: parseInt(opts.retries),
58
+ retries: opts.retries,
56
59
  });
57
60
 
58
61
  if (!handle.isDaemon) {
59
- await waitForReady(handle.page, { timeout: parseInt(opts.wait) });
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
- const selector = opts.selector;
119
- const attr = opts.attr;
120
- const all = opts.all;
121
- result = await evalFn(`(() => {
122
- const elements = ${all}
123
- ? Array.from(document.querySelectorAll('${selector}'))
124
- : [document.querySelector('${selector}')].filter(Boolean);
125
- return elements.map(el => {
126
- if ('${attr || ''}') return el.getAttribute('${attr || ''}');
127
- return el.textContent?.trim() || '';
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
- log.error(`Extract failed: ${err.message}`);
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
- const compareCount = parseInt(opts.compare);
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
- log.error(`Fingerprint check failed: ${err.message}`);
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
- log.error(`Detection tests failed: ${err.message}`);
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
- process.exit(1);
283
+ handleError(err, { log });
282
284
  }
283
285
  });
284
286
  }
@@ -4,9 +4,11 @@
4
4
 
5
5
  import ora from 'ora';
6
6
  import chalk from 'chalk';
7
- import { launchBrowser, closeBrowser, navigate, evaluate, waitForReady } from '../browser.js';
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
- const interval = parseInt(opts.interval) * 1000;
29
- const maxChecks = parseInt(opts.count);
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
- log.error(`Monitor failed: ${err.message}`);
155
- process.exit(1);
157
+ handleError(err, { log });
156
158
  } finally {
157
159
  if (handle) await closeBrowser(handle);
158
160
  }
@@ -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', '2')
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: parseInt(opts.retries) });
54
- await waitForReady(handle.page, { timeout: parseInt(opts.wait) });
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
- log.error(`PDF failed: ${err.message}`);
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', '2')
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: parseInt(opts.width),
41
- height: parseInt(opts.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: parseInt(opts.retries),
57
+ retries: opts.retries,
55
58
  });
56
59
 
57
60
  if (!handle.isDaemon) {
58
- await waitForReady(handle.page, { timeout: parseInt(opts.wait) });
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 = parseInt(opts.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
- log.error(`Screenshot failed: ${err.message}`);
91
- process.exit(1);
93
+ handleError(err, { log });
92
94
  } finally {
93
95
  if (handle) await closeBrowser(handle);
94
96
  }