@hunt-labs/bytekit-cli 0.5.0 → 0.7.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/CHANGELOG.md +203 -3
- package/README.md +74 -6
- package/dist/client.d.ts +126 -6
- package/dist/client.js +248 -30
- package/dist/commands/account.js +2 -1
- package/dist/commands/bulk.js +30 -8
- package/dist/commands/fetch.js +83 -20
- package/dist/commands/monitors.js +46 -8
- package/dist/commands/scrape.js +38 -9
- package/dist/commands/screenshots.js +184 -7
- package/dist/commands/search.js +2 -1
- package/dist/commands/sitemap.js +28 -3
- package/dist/commands/usage.js +6 -3
- package/dist/commands/webhook-deliveries.js +4 -2
- package/dist/lib/download-artifact.d.ts +30 -1
- package/dist/lib/download-artifact.js +90 -5
- package/dist/lib/parse-flags.js +3 -1
- package/dist/lib/url-lines.d.ts +46 -8
- package/dist/lib/url-lines.js +58 -19
- package/dist/output.d.ts +33 -2
- package/dist/output.js +113 -5
- package/package.json +3 -3
package/dist/commands/fetch.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Command, InvalidArgumentError } from 'commander';
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
3
|
import { printResult, emitFetchResult, handleError } from '../output.js';
|
|
4
|
-
import {
|
|
5
|
-
import { parseIntFlag, parseEnum, parseCacheTtl } from '../lib/parse-flags.js';
|
|
4
|
+
import { parseBulkLines } from '../lib/url-lines.js';
|
|
5
|
+
import { parseIntFlag, parseEnum, parseCacheTtl, parseJsonFlag } from '../lib/parse-flags.js';
|
|
6
6
|
const FETCH_FORMATS = ['markdown', 'html'];
|
|
7
7
|
/** The `FetchRequest.markdown_mode` spec enum (create/POST only — excluded from the GET query). */
|
|
8
8
|
const MARKDOWN_MODES = ['article', 'raw', 'llm'];
|
|
9
|
+
/** The `markdown_links` / `markdown_images` render-mode enum (shared with `scrape create`). */
|
|
10
|
+
const MARKDOWN_RENDER_MODES = ['inline', 'references', 'none', 'text'];
|
|
9
11
|
/**
|
|
10
12
|
* Validate `--format` locally, before any HTTP request is issued.
|
|
11
13
|
*
|
|
@@ -28,14 +30,32 @@ export function makeFetchCommand(getGlobal) {
|
|
|
28
30
|
bulkCmd
|
|
29
31
|
.command('create')
|
|
30
32
|
.description('Create a bulk fetch job')
|
|
31
|
-
.requiredOption('--file <path>', 'Path to newline-delimited file
|
|
33
|
+
.requiredOption('--file <path>', 'Path to a newline-delimited file. Each line is a plain URL, or a JSON object carrying "url" plus per-item config. A line whose only key is "url" behaves exactly as before (sent as a bare URL string); any line with additional keys switches the payload to the per-item shape.')
|
|
32
34
|
.requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified when the job completes')
|
|
35
|
+
.option('--webhook-secret <secret>', 'HMAC secret used to sign the completion webhook (omitted → unsigned)')
|
|
36
|
+
.option('--defaults <json>', 'Defaults applied to every entry, as a JSON object (omitted → none). Unknown keys are rejected by /v1/bulk (422) and ignored by scrape bulk / fetch bulk; fetch bulk additionally rejects browser-only keys (cookies, wait_until, wait_for_selector, delay_ms) with a 400.', (v) => parseJsonFlag('--defaults', v, 'object'))
|
|
37
|
+
.option('--metadata <json>', `Arbitrary metadata echoed on the job envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
38
|
+
// Deliberately NO --custom: /v1/fetch/bulk has no top-level `custom` property
|
|
39
|
+
// (validators/fetch-bulk.ts:52-62), so the flag would be silently stripped (#2596 drift D2).
|
|
40
|
+
// FetchBulkBodySchema itself stays non-strict (issue #2648 only makes the per-item
|
|
41
|
+
// FetchBulkItemOverrideSchema reject unknown keys), so this top-level behavior is unchanged.
|
|
33
42
|
.action(async (opts) => {
|
|
34
43
|
const g = getGlobal();
|
|
35
|
-
const client = buildClient(g);
|
|
36
44
|
try {
|
|
37
|
-
|
|
38
|
-
const
|
|
45
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
46
|
+
const client = buildClient(g);
|
|
47
|
+
// /v1/fetch/bulk has NO `items` property — its `urls` is a
|
|
48
|
+
// `string | FetchBulkItemOverride` union (validators/fetch-bulk.ts:57), so both line
|
|
49
|
+
// shapes ride there directly and no promotion step is needed (#2596 drift D3).
|
|
50
|
+
const urls = parseBulkLines(opts.file);
|
|
51
|
+
const body = { urls, webhook_url: opts.webhookUrl };
|
|
52
|
+
if (opts.webhookSecret !== undefined)
|
|
53
|
+
body['webhook_secret'] = opts.webhookSecret;
|
|
54
|
+
if (opts.defaults !== undefined)
|
|
55
|
+
body['defaults'] = opts.defaults;
|
|
56
|
+
if (opts.metadata !== undefined)
|
|
57
|
+
body['metadata'] = opts.metadata;
|
|
58
|
+
const result = await client.fetch.bulk.create(body);
|
|
39
59
|
printResult(result, g.json);
|
|
40
60
|
}
|
|
41
61
|
catch (err) {
|
|
@@ -47,8 +67,9 @@ export function makeFetchCommand(getGlobal) {
|
|
|
47
67
|
.description('Get a bulk fetch job')
|
|
48
68
|
.action(async (id) => {
|
|
49
69
|
const g = getGlobal();
|
|
50
|
-
const client = buildClient(g);
|
|
51
70
|
try {
|
|
71
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
72
|
+
const client = buildClient(g);
|
|
52
73
|
const result = await client.fetch.bulk.get(id);
|
|
53
74
|
printResult(result, g.json);
|
|
54
75
|
}
|
|
@@ -65,23 +86,64 @@ export function makeFetchCommand(getGlobal) {
|
|
|
65
86
|
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
66
87
|
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
67
88
|
.option('--cache-ttl <ttl>', 'Cache freshness: 0, Nh (hours), or Nd (days), e.g. 48h, 2d, 0 (omitted → server default)', (v) => parseCacheTtl('--cache-ttl', v))
|
|
68
|
-
//
|
|
69
|
-
// a compile error — the create/get asymmetry is correct by construction.
|
|
89
|
+
// The markdown surface below is POST-only: FetchGetOpts excludes it, so adding any of it to
|
|
90
|
+
// `get` would be a compile error — the create/get asymmetry is correct by construction.
|
|
70
91
|
.option('--markdown-mode <mode>', `Markdown mode: ${MARKDOWN_MODES.join(', ')}`, (v) => parseEnum('--markdown-mode', MARKDOWN_MODES, v))
|
|
92
|
+
.option('--markdown-query <q>', 'BM25 query string for relevance-ranked markdown filtering (omitted → disabled)')
|
|
93
|
+
.option('--markdown-links <mode>', `Link rendering: ${MARKDOWN_RENDER_MODES.join(', ')} (omitted → server default)`, (v) => parseEnum('--markdown-links', MARKDOWN_RENDER_MODES, v))
|
|
94
|
+
.option('--markdown-images <mode>', `Image retention: ${MARKDOWN_RENDER_MODES.join(', ')} (omitted → server default)`, (v) => parseEnum('--markdown-images', MARKDOWN_RENDER_MODES, v))
|
|
95
|
+
.option('--with-links-summary', 'Append a Links footer to the markdown output (omitted → off)')
|
|
96
|
+
.option('--with-images-summary', 'Append an Images footer to the markdown output (omitted → off)')
|
|
97
|
+
.option('--markdown-compact', 'Collapse excessive whitespace in the markdown output (omitted → off)')
|
|
98
|
+
.option('--markdown-filter-images', 'Filter low-signal images from the markdown output (omitted → off)')
|
|
99
|
+
.option('--markdown-include-media', 'Return rich link/image objects and a tables array (omitted → off)')
|
|
100
|
+
.option('--markdown-include-warnings', 'Include markdown-pipeline and tag-filter warnings (omitted → off)')
|
|
101
|
+
.option('--markdown-include-stats', 'Include a top-level stats object (chars, tokens, blocks) (omitted → off)')
|
|
102
|
+
.option('--custom <json>', `User payload echoed on the envelope as a JSON object, e.g. '{"job":"1"}' (omitted → none)`, (v) => parseJsonFlag('--custom', v, 'object'))
|
|
71
103
|
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
72
104
|
.action(async (opts) => {
|
|
73
105
|
const g = getGlobal();
|
|
74
|
-
|
|
106
|
+
// Omit-when-absent body, in the `body: Record<string, unknown>` form every other command
|
|
107
|
+
// file uses (scrape.ts:230-268 is the exemplar). The five pre-existing fields keep the
|
|
108
|
+
// exact same wire output they had as inline spreads; a boolean is written ONLY on
|
|
109
|
+
// `=== true`, so it never serializes `false` (#2480 E1).
|
|
110
|
+
const body = { url: opts.url };
|
|
111
|
+
if (opts.format !== undefined)
|
|
112
|
+
body['format'] = opts.format;
|
|
113
|
+
if (opts.country !== undefined)
|
|
114
|
+
body['country'] = opts.country;
|
|
115
|
+
if (opts.timeoutMs !== undefined)
|
|
116
|
+
body['timeout_ms'] = opts.timeoutMs;
|
|
117
|
+
if (opts.cacheTtl !== undefined)
|
|
118
|
+
body['cache_ttl'] = opts.cacheTtl;
|
|
119
|
+
if (opts.markdownMode !== undefined)
|
|
120
|
+
body['markdown_mode'] = opts.markdownMode;
|
|
121
|
+
if (opts.markdownQuery !== undefined)
|
|
122
|
+
body['markdown_query'] = opts.markdownQuery;
|
|
123
|
+
if (opts.markdownLinks !== undefined)
|
|
124
|
+
body['markdown_links'] = opts.markdownLinks;
|
|
125
|
+
if (opts.markdownImages !== undefined)
|
|
126
|
+
body['markdown_images'] = opts.markdownImages;
|
|
127
|
+
if (opts.withLinksSummary === true)
|
|
128
|
+
body['with_links_summary'] = true;
|
|
129
|
+
if (opts.withImagesSummary === true)
|
|
130
|
+
body['with_images_summary'] = true;
|
|
131
|
+
if (opts.markdownCompact === true)
|
|
132
|
+
body['markdown_compact'] = true;
|
|
133
|
+
if (opts.markdownFilterImages === true)
|
|
134
|
+
body['markdown_filter_images'] = true;
|
|
135
|
+
if (opts.markdownIncludeMedia === true)
|
|
136
|
+
body['markdown_include_media'] = true;
|
|
137
|
+
if (opts.markdownIncludeWarnings === true)
|
|
138
|
+
body['markdown_include_warnings'] = true;
|
|
139
|
+
if (opts.markdownIncludeStats === true)
|
|
140
|
+
body['markdown_include_stats'] = true;
|
|
141
|
+
if (opts.custom !== undefined)
|
|
142
|
+
body['custom'] = opts.custom;
|
|
75
143
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
80
|
-
...(opts.country !== undefined ? { country: opts.country } : {}),
|
|
81
|
-
...(opts.timeoutMs !== undefined ? { timeout_ms: opts.timeoutMs } : {}),
|
|
82
|
-
...(opts.cacheTtl !== undefined ? { cache_ttl: opts.cacheTtl } : {}),
|
|
83
|
-
...(opts.markdownMode !== undefined ? { markdown_mode: opts.markdownMode } : {}),
|
|
84
|
-
});
|
|
144
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
145
|
+
const client = buildClient(g);
|
|
146
|
+
const result = await client.fetch.create(body);
|
|
85
147
|
emitFetchResult(result, g.json, { file: opts.output });
|
|
86
148
|
}
|
|
87
149
|
catch (err) {
|
|
@@ -99,8 +161,9 @@ export function makeFetchCommand(getGlobal) {
|
|
|
99
161
|
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
100
162
|
.action(async (opts) => {
|
|
101
163
|
const g = getGlobal();
|
|
102
|
-
const client = buildClient(g);
|
|
103
164
|
try {
|
|
165
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
166
|
+
const client = buildClient(g);
|
|
104
167
|
const result = await client.fetch.get({
|
|
105
168
|
url: opts.url,
|
|
106
169
|
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
@@ -20,11 +20,25 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
20
20
|
capturesCmd
|
|
21
21
|
.command('list <id>')
|
|
22
22
|
.description('List captures for a monitor')
|
|
23
|
-
.
|
|
23
|
+
.option('--limit <n>', 'Maximum number of captures to return', (v) => parseIntFlag('--limit', v))
|
|
24
|
+
.option('--cursor <cursor>', 'Pagination cursor from a previous page')
|
|
25
|
+
.action(async (id, opts) => {
|
|
24
26
|
const g = getGlobal();
|
|
25
|
-
|
|
27
|
+
// Same omit-when-absent rule as `monitors list` (#2480): build the params object with
|
|
28
|
+
// ONLY the flags the caller passed, and call captures.list with a single argument (never
|
|
29
|
+
// captures.list(id, {}) / captures.list(id, undefined)) when no query flag is passed, so
|
|
30
|
+
// the inverse-case request-shape assertion stays byte-identical to today (#2594 DoD-4).
|
|
31
|
+
const params = {};
|
|
32
|
+
if (opts.limit !== undefined)
|
|
33
|
+
params.limit = opts.limit;
|
|
34
|
+
if (opts.cursor !== undefined)
|
|
35
|
+
params.cursor = opts.cursor;
|
|
26
36
|
try {
|
|
27
|
-
|
|
37
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
38
|
+
const client = buildClient(g);
|
|
39
|
+
const result = Object.keys(params).length > 0
|
|
40
|
+
? await client.monitors.captures.list(id, params)
|
|
41
|
+
: await client.monitors.captures.list(id);
|
|
28
42
|
printResult(result, g.json);
|
|
29
43
|
}
|
|
30
44
|
catch (err) {
|
|
@@ -36,6 +50,14 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
36
50
|
.command('create')
|
|
37
51
|
.description('Create a page-change monitor')
|
|
38
52
|
.requiredOption('--url <url>', 'URL to monitor')
|
|
53
|
+
// `--interval-type` is passed through VERBATIM and validated by the API, not locally — a
|
|
54
|
+
// deliberate choice recorded for #2686 item 5's open question, matching what `search`'s
|
|
55
|
+
// `--type`/`--date-range` already do. A local enum list is a second source of truth that
|
|
56
|
+
// drifts from the spec silently: the CLI would start rejecting a value the API has begun
|
|
57
|
+
// accepting, and the error text would come from a stale copy rather than the server. The
|
|
58
|
+
// siblings that DO validate locally (`--type`, `--markdown-mode`, `--device`, …) are the
|
|
59
|
+
// enums this package chose to pin; nothing here should be read as those being wrong, only as
|
|
60
|
+
// the pass-through being intentional rather than an oversight.
|
|
39
61
|
.requiredOption('--interval-type <type>', 'Check interval: hourly, daily, weekly, or cron')
|
|
40
62
|
.requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified on change')
|
|
41
63
|
.option('--cron <expr>', 'Cron expression (required when --interval-type is cron, e.g. "0 * * * *")')
|
|
@@ -48,9 +70,9 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
48
70
|
.option('--webhook-headers <json>', `Extra webhook headers as a JSON object, e.g. '{"X-Sig":"abc"}' (omitted → none)`, (v) => parseJsonFlag('--webhook-headers', v, 'object'))
|
|
49
71
|
.option('--options <json>', `Screenshot-monitor options as a JSON object (omitted → server defaults)`, (v) => parseJsonFlag('--options', v, 'object'))
|
|
50
72
|
.option('--scrape-options <json>', `Scrape-monitor options as a JSON object (type=scrape only; omitted → server defaults)`, (v) => parseJsonFlag('--scrape-options', v, 'object'))
|
|
73
|
+
.option('--metadata <json>', `Arbitrary metadata echoed on the monitor envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
51
74
|
.action(async (opts) => {
|
|
52
75
|
const g = getGlobal();
|
|
53
|
-
const client = buildClient(g);
|
|
54
76
|
// Omit-when-absent body (same rule as scrape.ts): each key is written ONLY when its flag is
|
|
55
77
|
// present, so a bare create never materializes a server default (#2480 E1). Typed via an
|
|
56
78
|
// assertion at the call site — the wire body is pinned by request-shapes.test.ts.
|
|
@@ -75,7 +97,11 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
75
97
|
body['options'] = opts.options;
|
|
76
98
|
if (opts.scrapeOptions !== undefined)
|
|
77
99
|
body['scrape_options'] = opts.scrapeOptions;
|
|
100
|
+
if (opts.metadata !== undefined)
|
|
101
|
+
body['metadata'] = opts.metadata;
|
|
78
102
|
try {
|
|
103
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
104
|
+
const client = buildClient(g);
|
|
79
105
|
const result = await client.monitors.create(body);
|
|
80
106
|
printResult(result, g.json);
|
|
81
107
|
}
|
|
@@ -94,7 +120,6 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
94
120
|
// with no runtime change.
|
|
95
121
|
.action(async (opts) => {
|
|
96
122
|
const g = getGlobal();
|
|
97
|
-
const client = buildClient(g);
|
|
98
123
|
// Build the params object with the same omit-when-absent rule as the body builders. When NO
|
|
99
124
|
// query flag is passed, call list() with ZERO arguments (never list({}) / list(undefined)) so
|
|
100
125
|
// the inverse-case request-shape assertion `monitors.list` args length 0 stays green (#2480).
|
|
@@ -106,6 +131,8 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
106
131
|
if (opts.status !== undefined)
|
|
107
132
|
params.status = opts.status;
|
|
108
133
|
try {
|
|
134
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
135
|
+
const client = buildClient(g);
|
|
109
136
|
const result = Object.keys(params).length > 0
|
|
110
137
|
? await client.monitors.list(params)
|
|
111
138
|
: await client.monitors.list();
|
|
@@ -120,8 +147,9 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
120
147
|
.description('Get a monitor by ID')
|
|
121
148
|
.action(async (id) => {
|
|
122
149
|
const g = getGlobal();
|
|
123
|
-
const client = buildClient(g);
|
|
124
150
|
try {
|
|
151
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
152
|
+
const client = buildClient(g);
|
|
125
153
|
const result = await client.monitors.get(id);
|
|
126
154
|
printResult(result, g.json);
|
|
127
155
|
}
|
|
@@ -132,6 +160,7 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
132
160
|
cmd
|
|
133
161
|
.command('update <id>')
|
|
134
162
|
.description('Update a monitor (including pause/resume via --status)')
|
|
163
|
+
.option('--url <url>', 'New URL to monitor (omitted → unchanged)')
|
|
135
164
|
.option('--interval-type <type>', 'New check interval: hourly, daily, weekly, or cron')
|
|
136
165
|
.option('--cron <expr>', 'New cron expression (when interval-type is cron, e.g. "0 * * * *")')
|
|
137
166
|
.option('--webhook-url <url>', 'New HTTPS webhook URL')
|
|
@@ -144,10 +173,14 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
144
173
|
.option('--webhook-headers <json>', `New webhook headers as a JSON object, e.g. '{"X-Sig":"abc"}'`, (v) => parseJsonFlag('--webhook-headers', v, 'object'))
|
|
145
174
|
.option('--options <json>', `New screenshot-monitor options as a JSON object`, (v) => parseJsonFlag('--options', v, 'object'))
|
|
146
175
|
.option('--scrape-options <json>', `New scrape-monitor options as a JSON object`, (v) => parseJsonFlag('--scrape-options', v, 'object'))
|
|
176
|
+
.option('--metadata <json>', `New metadata as a JSON object, e.g. '{"run":"7"}' (omitted → unchanged)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
147
177
|
.action(async (id, opts) => {
|
|
148
178
|
const g = getGlobal();
|
|
149
|
-
const client = buildClient(g);
|
|
150
179
|
const body = {};
|
|
180
|
+
// `url` IS settable on PATCH (openapi.yaml MonitorUpdateRequest) — unlike `type`, which
|
|
181
|
+
// stays immutable and deliberately has no flag (#1984).
|
|
182
|
+
if (opts.url !== undefined)
|
|
183
|
+
body['url'] = opts.url;
|
|
151
184
|
if (opts.intervalType !== undefined)
|
|
152
185
|
body['interval_type'] = opts.intervalType;
|
|
153
186
|
if (opts.cron !== undefined)
|
|
@@ -168,7 +201,11 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
168
201
|
body['options'] = opts.options;
|
|
169
202
|
if (opts.scrapeOptions !== undefined)
|
|
170
203
|
body['scrape_options'] = opts.scrapeOptions;
|
|
204
|
+
if (opts.metadata !== undefined)
|
|
205
|
+
body['metadata'] = opts.metadata;
|
|
171
206
|
try {
|
|
207
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
208
|
+
const client = buildClient(g);
|
|
172
209
|
const result = await client.monitors.update(id, body);
|
|
173
210
|
printResult(result, g.json);
|
|
174
211
|
}
|
|
@@ -181,8 +218,9 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
181
218
|
.description('Delete a monitor')
|
|
182
219
|
.action(async (id) => {
|
|
183
220
|
const g = getGlobal();
|
|
184
|
-
const client = buildClient(g);
|
|
185
221
|
try {
|
|
222
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
223
|
+
const client = buildClient(g);
|
|
186
224
|
const result = await client.monitors.delete(id);
|
|
187
225
|
printResult(result, g.json);
|
|
188
226
|
}
|
package/dist/commands/scrape.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
3
|
import { printResult, handleError, resolveOutputMode, emitResult } from '../output.js';
|
|
4
|
-
import {
|
|
4
|
+
import { parseBulkLines, toBulkItems } from '../lib/url-lines.js';
|
|
5
5
|
import { parseIntFlag, parseEnum, parseCsv, parseCacheTtl, parseJsonFlag, collectFormats, } from '../lib/parse-flags.js';
|
|
6
6
|
/** The `ScrapeRequest.markdown_mode` spec enum. */
|
|
7
7
|
const MARKDOWN_MODES = ['article', 'raw', 'llm'];
|
|
@@ -45,15 +45,35 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
45
45
|
bulkCmd
|
|
46
46
|
.command('create')
|
|
47
47
|
.description('Create a bulk scrape job')
|
|
48
|
-
.requiredOption('--file <path>', 'Path to newline-delimited file
|
|
48
|
+
.requiredOption('--file <path>', 'Path to a newline-delimited file. Each line is a plain URL, or a JSON object carrying "url" plus per-item config. A line whose only key is "url" behaves exactly as before (sent as a bare URL string); any line with additional keys switches the payload to the per-item shape.')
|
|
49
49
|
.requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified when the job completes')
|
|
50
|
+
.option('--webhook-secret <secret>', 'HMAC secret used to sign the completion webhook (omitted → unsigned)')
|
|
51
|
+
.option('--defaults <json>', 'Defaults applied to every entry, as a JSON object (omitted → none). Unknown keys are rejected by /v1/bulk (422) and ignored by scrape bulk / fetch bulk; fetch bulk additionally rejects browser-only keys (cookies, wait_until, wait_for_selector, delay_ms) with a 400.', (v) => parseJsonFlag('--defaults', v, 'object'))
|
|
52
|
+
.option('--metadata <json>', `Arbitrary metadata echoed on the job envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
53
|
+
// /v1/scrape/bulk is the ONE bulk endpoint with a top-level `custom`
|
|
54
|
+
// (validators/scrape-bulk.ts:88). Per-entry `custom` arrives through --file lines instead.
|
|
55
|
+
.option('--custom <json>', `User payload echoed on every scrape entry, as a JSON object, e.g. '{"job":"1"}' (omitted → none; a per-entry value in --file wins)`, (v) => parseJsonFlag('--custom', v, 'object'))
|
|
50
56
|
.action(async (opts) => {
|
|
51
57
|
const g = getGlobal();
|
|
52
|
-
const client = buildClient(g);
|
|
53
58
|
try {
|
|
59
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
60
|
+
const client = buildClient(g);
|
|
54
61
|
// readFileSync throws ENOENT on missing file — caught by handleError below
|
|
55
|
-
const
|
|
56
|
-
|
|
62
|
+
const lines = parseBulkLines(opts.file);
|
|
63
|
+
// `urls` XOR `items` (validators/scrape-bulk.ts:91-98) — send exactly one, keeping the
|
|
64
|
+
// pre-#2596 `{ urls, webhook_url }` body byte-identical for url-only files (E2).
|
|
65
|
+
const body = lines.every((l) => typeof l === 'string')
|
|
66
|
+
? { urls: lines, webhook_url: opts.webhookUrl }
|
|
67
|
+
: { items: toBulkItems(lines), webhook_url: opts.webhookUrl };
|
|
68
|
+
if (opts.webhookSecret !== undefined)
|
|
69
|
+
body['webhook_secret'] = opts.webhookSecret;
|
|
70
|
+
if (opts.defaults !== undefined)
|
|
71
|
+
body['defaults'] = opts.defaults;
|
|
72
|
+
if (opts.metadata !== undefined)
|
|
73
|
+
body['metadata'] = opts.metadata;
|
|
74
|
+
if (opts.custom !== undefined)
|
|
75
|
+
body['custom'] = opts.custom;
|
|
76
|
+
const result = await client.scrape.bulk.create(body);
|
|
57
77
|
printResult(result, g.json);
|
|
58
78
|
}
|
|
59
79
|
catch (err) {
|
|
@@ -65,8 +85,9 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
65
85
|
.description('Get a bulk scrape job')
|
|
66
86
|
.action(async (id) => {
|
|
67
87
|
const g = getGlobal();
|
|
68
|
-
const client = buildClient(g);
|
|
69
88
|
try {
|
|
89
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
90
|
+
const client = buildClient(g);
|
|
70
91
|
const result = await client.scrape.bulk.get(id);
|
|
71
92
|
printResult(result, g.json);
|
|
72
93
|
}
|
|
@@ -82,6 +103,12 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
82
103
|
// Repeatable AND CSV-aware: `--format raw_html --format markdown` and `--format raw_html,markdown`
|
|
83
104
|
// both send formats: ['raw_html','markdown']. Sent as the `formats` array; omitted → server
|
|
84
105
|
// default. `--raw`/`-o` require EXACTLY ONE format (validated pre-request; see the action).
|
|
106
|
+
//
|
|
107
|
+
// The format VALUES travel to the server unvalidated — deliberate, and recorded here for
|
|
108
|
+
// #2686 item 5's open question (same rationale as `monitors --interval-type` and `search`'s
|
|
109
|
+
// `--type`/`--date-range`). The persisted-format vocabulary grows with the spec, and a local
|
|
110
|
+
// enum list is a second source of truth that drifts silently: the CLI would reject a format
|
|
111
|
+
// the API has begun serving, and the error would come from a stale copy rather than the API.
|
|
85
112
|
.option('--format <format>', 'Output format; repeat or comma-separate for multiple (e.g. --format raw_html --format markdown, or --format raw_html,markdown). Sent as the formats array; omitted → server default.', collectFormats)
|
|
86
113
|
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
87
114
|
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
@@ -118,7 +145,6 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
118
145
|
.option('-o, --output <file>', 'Write the output to the given file path (implies --raw unless --json is set)')
|
|
119
146
|
.action(async (opts) => {
|
|
120
147
|
const g = getGlobal();
|
|
121
|
-
const client = buildClient(g);
|
|
122
148
|
const body = { url: opts.url };
|
|
123
149
|
// The v1 scrape validator expects `formats` (array). The --format collector already
|
|
124
150
|
// accumulates repeated/CSV occurrences into an array; write it verbatim, omitting the key
|
|
@@ -187,6 +213,8 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
187
213
|
if (opts.mobile === true)
|
|
188
214
|
body['mobile'] = true;
|
|
189
215
|
try {
|
|
216
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
217
|
+
const client = buildClient(g);
|
|
190
218
|
// FIRST statement inside the try — deliberately NOT hoisted next to buildClient() where
|
|
191
219
|
// the other handlers put pre-request logic. index.ts drives commander with .parse(), not
|
|
192
220
|
// .parseAsync(), so nothing awaits this action's promise: a throw raised above the try
|
|
@@ -245,11 +273,12 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
245
273
|
.description('Get a scrape job by ID (use --raw/-o/--format to retrieve a completed body)')
|
|
246
274
|
.option('--raw', 'Print the requested format body raw, with no JSON wrapper')
|
|
247
275
|
.option('-o, --output <file>', 'Write the output to the given file path (implies --raw unless --json is set)')
|
|
248
|
-
.option('--format <format>', 'Which persisted format to emit
|
|
276
|
+
.option('--format <format>', 'Which persisted format to emit — only takes effect with --raw/-o; ignored otherwise')
|
|
249
277
|
.action(async (id, opts) => {
|
|
250
278
|
const g = getGlobal();
|
|
251
|
-
const client = buildClient(g);
|
|
252
279
|
try {
|
|
280
|
+
// Inside the try: buildClient() throws on a missing key (#2686 item 4) and the throw must reach handleError(). See its doc comment in client.ts.
|
|
281
|
+
const client = buildClient(g);
|
|
253
282
|
// Mirror `scrape create`'s output path: resolveOutputMode FIRST inside the try (so a
|
|
254
283
|
// --json/--raw conflict rejects before the request), then emitResult. The request body is
|
|
255
284
|
// unchanged — scrape.get(id) takes no options — so the inverse-case arg-length assertion in
|