@hunt-labs/bytekit-cli 0.3.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/CHANGELOG.md +230 -0
- package/README.md +187 -29
- package/dist/client.d.ts +128 -11
- package/dist/client.js +369 -95
- package/dist/commands/account.d.ts +11 -0
- package/dist/commands/account.js +10 -127
- package/dist/commands/bulk.js +36 -4
- package/dist/commands/fetch.js +91 -10
- package/dist/commands/monitors.js +92 -11
- package/dist/commands/scrape.js +131 -20
- package/dist/commands/screenshots.js +86 -1
- package/dist/commands/sitemap.js +38 -6
- package/dist/commands/usage.d.ts +13 -0
- package/dist/commands/usage.js +81 -0
- package/dist/commands/webhook-deliveries.d.ts +11 -0
- package/dist/commands/webhook-deliveries.js +59 -0
- package/dist/index.js +4 -0
- package/dist/lib/download-artifact.d.ts +30 -1
- package/dist/lib/download-artifact.js +89 -5
- package/dist/lib/parse-flags.d.ts +30 -0
- package/dist/lib/parse-flags.js +58 -0
- package/dist/lib/url-lines.d.ts +46 -8
- package/dist/lib/url-lines.js +58 -19
- package/dist/output.d.ts +47 -9
- package/dist/output.js +103 -16
- package/package.json +3 -3
- package/dist/lib/fetch-body.d.ts +0 -18
- package/dist/lib/fetch-body.js +0 -6
package/dist/commands/account.js
CHANGED
|
@@ -1,136 +1,19 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
3
|
import { printResult, handleError } from '../output.js';
|
|
4
|
-
/** Visible lead-in of the masked form: the `sk_live_`/`sk_test_` prefix (or first 8 chars). */
|
|
5
|
-
const MASK_PREFIX_CHARS = 8;
|
|
6
|
-
/** Visible tail of the masked form, shown only when enough of the middle stays hidden. */
|
|
7
|
-
const MASK_SUFFIX_CHARS = 4;
|
|
8
|
-
/** Minimum number of characters the `…` must actually hide before a suffix may be shown. */
|
|
9
|
-
const MIN_MASKED_CHARS = 8;
|
|
10
4
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
5
|
+
* The `account` command group.
|
|
6
|
+
*
|
|
7
|
+
* Key management is DELIBERATELY absent (#2536). Those operations are mounted behind
|
|
8
|
+
* `clerkSession(db)` in the gateway and are annotated `x-internal: true` / `x-sdk-scope: internal`
|
|
9
|
+
* in the OpenAPI spec, so a bearer `sk_` key — the CLI's only credential — can never satisfy them.
|
|
10
|
+
* Keys are managed from the dashboard instead. Widening the gateway to accept bearer auth was
|
|
11
|
+
* rejected on security grounds: the reveal operation returns the live plaintext of any key on the
|
|
12
|
+
* account and there is no per-key scope model, so any leaked key would become a full
|
|
13
|
+
* account-credential factory. `internal-scope-guard.test.ts` mechanically enforces this boundary.
|
|
17
14
|
*/
|
|
18
|
-
function maskKey(key) {
|
|
19
|
-
const prefixMatch = /^(sk_(?:live|test)_)/.exec(key);
|
|
20
|
-
if (key.length < MASK_PREFIX_CHARS + MASK_SUFFIX_CHARS + MIN_MASKED_CHARS) {
|
|
21
|
-
const prefix = prefixMatch ? prefixMatch[1] : key.slice(0, MASK_PREFIX_CHARS);
|
|
22
|
-
return `${prefix}…`;
|
|
23
|
-
}
|
|
24
|
-
return `${key.slice(0, MASK_PREFIX_CHARS)}…${key.slice(-MASK_SUFFIX_CHARS)}`;
|
|
25
|
-
}
|
|
26
15
|
export function makeAccountCommand(getGlobal) {
|
|
27
|
-
const cmd = new Command('account').description('Account
|
|
28
|
-
const apiKeysCmd = new Command('api-keys').description('API key management');
|
|
29
|
-
apiKeysCmd
|
|
30
|
-
.command('create')
|
|
31
|
-
.description('Create a new API key')
|
|
32
|
-
.requiredOption('--name <name>', 'Display name for the key')
|
|
33
|
-
.action(async (opts) => {
|
|
34
|
-
const g = getGlobal();
|
|
35
|
-
const client = buildClient(g);
|
|
36
|
-
try {
|
|
37
|
-
// The API's field is `label`, not `name` — sending `{ name }` created an
|
|
38
|
-
// UNLABELLED key, because the gateway ignored the unknown field (#2426:
|
|
39
|
-
// the SDK's newly derived request type is what surfaced this).
|
|
40
|
-
const result = await client.account.apiKeys.create({ label: opts.name });
|
|
41
|
-
printResult(result, g.json);
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
handleError(err);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
apiKeysCmd
|
|
48
|
-
.command('list')
|
|
49
|
-
.description('List all API keys')
|
|
50
|
-
.action(async () => {
|
|
51
|
-
const g = getGlobal();
|
|
52
|
-
const client = buildClient(g);
|
|
53
|
-
try {
|
|
54
|
-
const result = await client.account.apiKeys.list();
|
|
55
|
-
printResult(result, g.json);
|
|
56
|
-
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
handleError(err);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
apiKeysCmd
|
|
62
|
-
.command('get <id>')
|
|
63
|
-
.description('Get an API key by ID')
|
|
64
|
-
.action(async (id) => {
|
|
65
|
-
const g = getGlobal();
|
|
66
|
-
const client = buildClient(g);
|
|
67
|
-
try {
|
|
68
|
-
const result = await client.account.apiKeys.get(id);
|
|
69
|
-
printResult(result, g.json);
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
handleError(err);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
apiKeysCmd
|
|
76
|
-
.command('update <id>')
|
|
77
|
-
.description('Update an API key')
|
|
78
|
-
.requiredOption('--name <name>', 'New display name')
|
|
79
|
-
.action(async (id, opts) => {
|
|
80
|
-
const g = getGlobal();
|
|
81
|
-
const client = buildClient(g);
|
|
82
|
-
try {
|
|
83
|
-
// As above: the API's field is `label`, so `--name` never actually
|
|
84
|
-
// renamed the key.
|
|
85
|
-
const result = await client.account.apiKeys.update(id, { label: opts.name });
|
|
86
|
-
printResult(result, g.json);
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
handleError(err);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
apiKeysCmd
|
|
93
|
-
.command('reveal <id>')
|
|
94
|
-
.description('Reveal an API key value (masked by default; use --show for plaintext)')
|
|
95
|
-
.option('--show', 'Print the full plaintext key instead of the masked form')
|
|
96
|
-
.action(async (id, opts) => {
|
|
97
|
-
const g = getGlobal();
|
|
98
|
-
const client = buildClient(g);
|
|
99
|
-
try {
|
|
100
|
-
const result = await client.account.apiKeys.reveal(id);
|
|
101
|
-
// Masking decision (#2480 E10): `reveal` prints a LIVE secret to a terminal that may be
|
|
102
|
-
// logged or screen-shared, so plaintext is opt-in. Default (human) mode masks the key
|
|
103
|
-
// (prefix + last-4, e.g. `sk_live_…2345`); `--show` prints the full plaintext; `--json`
|
|
104
|
-
// emits the full envelope verbatim for machine consumption.
|
|
105
|
-
if (g.json) {
|
|
106
|
-
printResult(result, true);
|
|
107
|
-
}
|
|
108
|
-
else if (opts.show === true) {
|
|
109
|
-
console.log(result.key);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
console.log(maskKey(result.key));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
handleError(err);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
apiKeysCmd
|
|
120
|
-
.command('revoke <id>')
|
|
121
|
-
.description('Revoke an API key (idempotent)')
|
|
122
|
-
.action(async (id) => {
|
|
123
|
-
const g = getGlobal();
|
|
124
|
-
const client = buildClient(g);
|
|
125
|
-
try {
|
|
126
|
-
const result = await client.account.apiKeys.revoke(id);
|
|
127
|
-
printResult(result, g.json);
|
|
128
|
-
}
|
|
129
|
-
catch (err) {
|
|
130
|
-
handleError(err);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
cmd.addCommand(apiKeysCmd);
|
|
16
|
+
const cmd = new Command('account').description('Account operations');
|
|
134
17
|
cmd
|
|
135
18
|
.command('get')
|
|
136
19
|
.description('Get the authenticated account')
|
package/dist/commands/bulk.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
3
|
import { printResult, handleError } from '../output.js';
|
|
4
|
-
import {
|
|
4
|
+
import { parseBulkLines, toBulkItems } from '../lib/url-lines.js';
|
|
5
|
+
import { parseJsonFlag } from '../lib/parse-flags.js';
|
|
5
6
|
export function makeBulkCommand(getGlobal) {
|
|
6
7
|
const cmd = new Command('bulk').description('Bulk screenshot operations');
|
|
7
8
|
const screenshotsCmd = new Command('screenshots').description('Bulk screenshot sub-resources');
|
|
@@ -23,14 +24,31 @@ export function makeBulkCommand(getGlobal) {
|
|
|
23
24
|
cmd
|
|
24
25
|
.command('create')
|
|
25
26
|
.description('Create a bulk screenshots job')
|
|
26
|
-
.requiredOption('--file <path>', 'Path to newline-delimited file
|
|
27
|
+
.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.')
|
|
27
28
|
.requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified when the job completes')
|
|
29
|
+
.option('--webhook-secret <secret>', 'HMAC secret used to sign the completion webhook (omitted → unsigned)')
|
|
30
|
+
.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'))
|
|
31
|
+
.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'))
|
|
32
|
+
// Deliberately NO --custom: /v1/bulk is `.strict()` at the top level with no `custom`
|
|
33
|
+
// property (validators/bulk.ts:135), so the flag would 422 every call (#2596 drift D2).
|
|
28
34
|
.action(async (opts) => {
|
|
29
35
|
const g = getGlobal();
|
|
30
36
|
const client = buildClient(g);
|
|
31
37
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
38
|
+
const lines = parseBulkLines(opts.file);
|
|
39
|
+
// `urls` XOR `items` (validators/bulk.ts:136-155) — send exactly one. A file of only
|
|
40
|
+
// url-only lines keeps the pre-#2596 `{ urls, webhook_url }` body, key order included,
|
|
41
|
+
// so existing callers are byte-identical on the wire (E2).
|
|
42
|
+
const body = lines.every((l) => typeof l === 'string')
|
|
43
|
+
? { urls: lines, webhook_url: opts.webhookUrl }
|
|
44
|
+
: { items: toBulkItems(lines), webhook_url: opts.webhookUrl };
|
|
45
|
+
if (opts.webhookSecret !== undefined)
|
|
46
|
+
body['webhook_secret'] = opts.webhookSecret;
|
|
47
|
+
if (opts.defaults !== undefined)
|
|
48
|
+
body['defaults'] = opts.defaults;
|
|
49
|
+
if (opts.metadata !== undefined)
|
|
50
|
+
body['metadata'] = opts.metadata;
|
|
51
|
+
const result = await client.bulk.create(body);
|
|
34
52
|
printResult(result, g.json);
|
|
35
53
|
}
|
|
36
54
|
catch (err) {
|
|
@@ -51,5 +69,19 @@ export function makeBulkCommand(getGlobal) {
|
|
|
51
69
|
handleError(err);
|
|
52
70
|
}
|
|
53
71
|
});
|
|
72
|
+
cmd
|
|
73
|
+
.command('cancel <id>')
|
|
74
|
+
.description('Cancel an in-flight bulk job')
|
|
75
|
+
.action(async (id) => {
|
|
76
|
+
const g = getGlobal();
|
|
77
|
+
const client = buildClient(g);
|
|
78
|
+
try {
|
|
79
|
+
const result = await client.bulk.cancel(id);
|
|
80
|
+
printResult(result, g.json);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
handleError(err);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
54
86
|
return cmd;
|
|
55
87
|
}
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,8 +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 {
|
|
4
|
+
import { parseBulkLines } from '../lib/url-lines.js';
|
|
5
|
+
import { parseIntFlag, parseEnum, parseCacheTtl, parseJsonFlag } from '../lib/parse-flags.js';
|
|
5
6
|
const FETCH_FORMATS = ['markdown', 'html'];
|
|
7
|
+
/** The `FetchRequest.markdown_mode` spec enum (create/POST only — excluded from the GET query). */
|
|
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'];
|
|
6
11
|
/**
|
|
7
12
|
* Validate `--format` locally, before any HTTP request is issued.
|
|
8
13
|
*
|
|
@@ -25,14 +30,31 @@ export function makeFetchCommand(getGlobal) {
|
|
|
25
30
|
bulkCmd
|
|
26
31
|
.command('create')
|
|
27
32
|
.description('Create a bulk fetch job')
|
|
28
|
-
.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.')
|
|
29
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.
|
|
30
42
|
.action(async (opts) => {
|
|
31
43
|
const g = getGlobal();
|
|
32
44
|
const client = buildClient(g);
|
|
33
45
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
// /v1/fetch/bulk has NO `items` property — its `urls` is a
|
|
47
|
+
// `string | FetchBulkItemOverride` union (validators/fetch-bulk.ts:57), so both line
|
|
48
|
+
// shapes ride there directly and no promotion step is needed (#2596 drift D3).
|
|
49
|
+
const urls = parseBulkLines(opts.file);
|
|
50
|
+
const body = { urls, webhook_url: opts.webhookUrl };
|
|
51
|
+
if (opts.webhookSecret !== undefined)
|
|
52
|
+
body['webhook_secret'] = opts.webhookSecret;
|
|
53
|
+
if (opts.defaults !== undefined)
|
|
54
|
+
body['defaults'] = opts.defaults;
|
|
55
|
+
if (opts.metadata !== undefined)
|
|
56
|
+
body['metadata'] = opts.metadata;
|
|
57
|
+
const result = await client.fetch.bulk.create(body);
|
|
36
58
|
printResult(result, g.json);
|
|
37
59
|
}
|
|
38
60
|
catch (err) {
|
|
@@ -59,15 +81,67 @@ export function makeFetchCommand(getGlobal) {
|
|
|
59
81
|
.description('Fetch URL content (POST)')
|
|
60
82
|
.requiredOption('--url <url>', 'URL to fetch')
|
|
61
83
|
.option('--format <format>', 'Response format (markdown or html)', parseFormat)
|
|
84
|
+
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
85
|
+
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
86
|
+
.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))
|
|
87
|
+
// The markdown surface below is POST-only: FetchGetOpts excludes it, so adding any of it to
|
|
88
|
+
// `get` would be a compile error — the create/get asymmetry is correct by construction.
|
|
89
|
+
.option('--markdown-mode <mode>', `Markdown mode: ${MARKDOWN_MODES.join(', ')}`, (v) => parseEnum('--markdown-mode', MARKDOWN_MODES, v))
|
|
90
|
+
.option('--markdown-query <q>', 'BM25 query string for relevance-ranked markdown filtering (omitted → disabled)')
|
|
91
|
+
.option('--markdown-links <mode>', `Link rendering: ${MARKDOWN_RENDER_MODES.join(', ')} (omitted → server default)`, (v) => parseEnum('--markdown-links', MARKDOWN_RENDER_MODES, v))
|
|
92
|
+
.option('--markdown-images <mode>', `Image retention: ${MARKDOWN_RENDER_MODES.join(', ')} (omitted → server default)`, (v) => parseEnum('--markdown-images', MARKDOWN_RENDER_MODES, v))
|
|
93
|
+
.option('--with-links-summary', 'Append a Links footer to the markdown output (omitted → off)')
|
|
94
|
+
.option('--with-images-summary', 'Append an Images footer to the markdown output (omitted → off)')
|
|
95
|
+
.option('--markdown-compact', 'Collapse excessive whitespace in the markdown output (omitted → off)')
|
|
96
|
+
.option('--markdown-filter-images', 'Filter low-signal images from the markdown output (omitted → off)')
|
|
97
|
+
.option('--markdown-include-media', 'Return rich link/image objects and a tables array (omitted → off)')
|
|
98
|
+
.option('--markdown-include-warnings', 'Include markdown-pipeline and tag-filter warnings (omitted → off)')
|
|
99
|
+
.option('--markdown-include-stats', 'Include a top-level stats object (chars, tokens, blocks) (omitted → off)')
|
|
100
|
+
.option('--custom <json>', `User payload echoed on the envelope as a JSON object, e.g. '{"job":"1"}' (omitted → none)`, (v) => parseJsonFlag('--custom', v, 'object'))
|
|
101
|
+
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
62
102
|
.action(async (opts) => {
|
|
63
103
|
const g = getGlobal();
|
|
64
104
|
const client = buildClient(g);
|
|
105
|
+
// Omit-when-absent body, in the `body: Record<string, unknown>` form every other command
|
|
106
|
+
// file uses (scrape.ts:230-268 is the exemplar). The five pre-existing fields keep the
|
|
107
|
+
// exact same wire output they had as inline spreads; a boolean is written ONLY on
|
|
108
|
+
// `=== true`, so it never serializes `false` (#2480 E1).
|
|
109
|
+
const body = { url: opts.url };
|
|
110
|
+
if (opts.format !== undefined)
|
|
111
|
+
body['format'] = opts.format;
|
|
112
|
+
if (opts.country !== undefined)
|
|
113
|
+
body['country'] = opts.country;
|
|
114
|
+
if (opts.timeoutMs !== undefined)
|
|
115
|
+
body['timeout_ms'] = opts.timeoutMs;
|
|
116
|
+
if (opts.cacheTtl !== undefined)
|
|
117
|
+
body['cache_ttl'] = opts.cacheTtl;
|
|
118
|
+
if (opts.markdownMode !== undefined)
|
|
119
|
+
body['markdown_mode'] = opts.markdownMode;
|
|
120
|
+
if (opts.markdownQuery !== undefined)
|
|
121
|
+
body['markdown_query'] = opts.markdownQuery;
|
|
122
|
+
if (opts.markdownLinks !== undefined)
|
|
123
|
+
body['markdown_links'] = opts.markdownLinks;
|
|
124
|
+
if (opts.markdownImages !== undefined)
|
|
125
|
+
body['markdown_images'] = opts.markdownImages;
|
|
126
|
+
if (opts.withLinksSummary === true)
|
|
127
|
+
body['with_links_summary'] = true;
|
|
128
|
+
if (opts.withImagesSummary === true)
|
|
129
|
+
body['with_images_summary'] = true;
|
|
130
|
+
if (opts.markdownCompact === true)
|
|
131
|
+
body['markdown_compact'] = true;
|
|
132
|
+
if (opts.markdownFilterImages === true)
|
|
133
|
+
body['markdown_filter_images'] = true;
|
|
134
|
+
if (opts.markdownIncludeMedia === true)
|
|
135
|
+
body['markdown_include_media'] = true;
|
|
136
|
+
if (opts.markdownIncludeWarnings === true)
|
|
137
|
+
body['markdown_include_warnings'] = true;
|
|
138
|
+
if (opts.markdownIncludeStats === true)
|
|
139
|
+
body['markdown_include_stats'] = true;
|
|
140
|
+
if (opts.custom !== undefined)
|
|
141
|
+
body['custom'] = opts.custom;
|
|
65
142
|
try {
|
|
66
|
-
const result = await client.fetch.create(
|
|
67
|
-
|
|
68
|
-
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
69
|
-
});
|
|
70
|
-
emitFetchResult(result, g.json);
|
|
143
|
+
const result = await client.fetch.create(body);
|
|
144
|
+
emitFetchResult(result, g.json, { file: opts.output });
|
|
71
145
|
}
|
|
72
146
|
catch (err) {
|
|
73
147
|
handleError(err);
|
|
@@ -78,6 +152,10 @@ export function makeFetchCommand(getGlobal) {
|
|
|
78
152
|
.description('Fetch URL content (GET via query params)')
|
|
79
153
|
.requiredOption('--url <url>', 'URL to fetch')
|
|
80
154
|
.option('--format <format>', 'Response format (markdown or html)', parseFormat)
|
|
155
|
+
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
156
|
+
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
157
|
+
.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))
|
|
158
|
+
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
81
159
|
.action(async (opts) => {
|
|
82
160
|
const g = getGlobal();
|
|
83
161
|
const client = buildClient(g);
|
|
@@ -85,8 +163,11 @@ export function makeFetchCommand(getGlobal) {
|
|
|
85
163
|
const result = await client.fetch.get({
|
|
86
164
|
url: opts.url,
|
|
87
165
|
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
166
|
+
...(opts.country !== undefined ? { country: opts.country } : {}),
|
|
167
|
+
...(opts.timeoutMs !== undefined ? { timeout_ms: opts.timeoutMs } : {}),
|
|
168
|
+
...(opts.cacheTtl !== undefined ? { cache_ttl: opts.cacheTtl } : {}),
|
|
88
169
|
});
|
|
89
|
-
emitFetchResult(result, g.json);
|
|
170
|
+
emitFetchResult(result, g.json, { file: opts.output });
|
|
90
171
|
}
|
|
91
172
|
catch (err) {
|
|
92
173
|
handleError(err);
|
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
3
|
import { printResult, handleError } from '../output.js';
|
|
4
|
-
import { parseEnum, parseIntFlag } from '../lib/parse-flags.js';
|
|
5
|
-
/** The `listMonitors` `status` query enum. */
|
|
4
|
+
import { parseEnum, parseIntFlag, parseNumberFlag, parseJsonFlag } from '../lib/parse-flags.js';
|
|
5
|
+
/** The `listMonitors` `status` query enum (5-value list FILTER). */
|
|
6
6
|
const MONITOR_STATUSES = ['active', 'processing', 'paused', 'cancelled', 'suspended'];
|
|
7
|
+
/**
|
|
8
|
+
* The `MonitorUpdateRequest.status` enum — DISTINCT from {@link MONITOR_STATUSES}. Only `active`
|
|
9
|
+
* (resume) and `paused` (pause) are valid update targets (openapi.yaml MonitorUpdateRequest.status);
|
|
10
|
+
* the other three list-filter states are server-managed lifecycle states, not settable via PATCH.
|
|
11
|
+
*/
|
|
12
|
+
const MONITOR_UPDATE_STATUSES = ['active', 'paused'];
|
|
13
|
+
/** The `notify_on` enum shared by MonitorCreateRequest / MonitorUpdateRequest. */
|
|
14
|
+
const NOTIFY_ONS = ['change', 'every'];
|
|
15
|
+
/** The create-only `type` enum (PATCH rejects `type` with 422 — immutable, issue #1984). */
|
|
16
|
+
const MONITOR_TYPES = ['screenshot', 'scrape'];
|
|
7
17
|
export function makeMonitorsCommand(getGlobal) {
|
|
8
18
|
const cmd = new Command('monitors').description('Page-change monitor operations');
|
|
9
19
|
const capturesCmd = new Command('captures').description('Monitor capture sub-resources');
|
|
10
20
|
capturesCmd
|
|
11
21
|
.command('list <id>')
|
|
12
22
|
.description('List captures for a monitor')
|
|
13
|
-
.
|
|
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) => {
|
|
14
26
|
const g = getGlobal();
|
|
15
27
|
const client = buildClient(g);
|
|
28
|
+
// Same omit-when-absent rule as `monitors list` (#2480): build the params object with
|
|
29
|
+
// ONLY the flags the caller passed, and call captures.list with a single argument (never
|
|
30
|
+
// captures.list(id, {}) / captures.list(id, undefined)) when no query flag is passed, so
|
|
31
|
+
// the inverse-case request-shape assertion stays byte-identical to today (#2594 DoD-4).
|
|
32
|
+
const params = {};
|
|
33
|
+
if (opts.limit !== undefined)
|
|
34
|
+
params.limit = opts.limit;
|
|
35
|
+
if (opts.cursor !== undefined)
|
|
36
|
+
params.cursor = opts.cursor;
|
|
16
37
|
try {
|
|
17
|
-
const result =
|
|
38
|
+
const result = Object.keys(params).length > 0
|
|
39
|
+
? await client.monitors.captures.list(id, params)
|
|
40
|
+
: await client.monitors.captures.list(id);
|
|
18
41
|
printResult(result, g.json);
|
|
19
42
|
}
|
|
20
43
|
catch (err) {
|
|
@@ -29,21 +52,45 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
29
52
|
.requiredOption('--interval-type <type>', 'Check interval: hourly, daily, weekly, or cron')
|
|
30
53
|
.requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified on change')
|
|
31
54
|
.option('--cron <expr>', 'Cron expression (required when --interval-type is cron, e.g. "0 * * * *")')
|
|
55
|
+
// `--type` is CREATE-ONLY (a monitor's type is immutable per #1984); the PATCH builder below
|
|
56
|
+
// deliberately omits it.
|
|
57
|
+
.option('--type <type>', `Monitor type: ${MONITOR_TYPES.join(', ')} (omitted → server default screenshot)`, (v) => parseEnum('--type', MONITOR_TYPES, v))
|
|
58
|
+
.option('--change-threshold <pct>', 'Change percentage that triggers a notification (float; omitted → server default 5)', (v) => parseNumberFlag('--change-threshold', v))
|
|
59
|
+
.option('--notify-on <event>', `When to notify: ${NOTIFY_ONS.join(', ')} (omitted → server default change)`, (v) => parseEnum('--notify-on', NOTIFY_ONS, v))
|
|
60
|
+
.option('--webhook-secret <secret>', 'HMAC secret for webhook signature verification')
|
|
61
|
+
.option('--webhook-headers <json>', `Extra webhook headers as a JSON object, e.g. '{"X-Sig":"abc"}' (omitted → none)`, (v) => parseJsonFlag('--webhook-headers', v, 'object'))
|
|
62
|
+
.option('--options <json>', `Screenshot-monitor options as a JSON object (omitted → server defaults)`, (v) => parseJsonFlag('--options', v, 'object'))
|
|
63
|
+
.option('--scrape-options <json>', `Scrape-monitor options as a JSON object (type=scrape only; omitted → server defaults)`, (v) => parseJsonFlag('--scrape-options', v, 'object'))
|
|
64
|
+
.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'))
|
|
32
65
|
.action(async (opts) => {
|
|
33
66
|
const g = getGlobal();
|
|
34
67
|
const client = buildClient(g);
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// the
|
|
38
|
-
// only; the wire body is unchanged (pinned by __tests__/request-shapes.test.ts).
|
|
68
|
+
// Omit-when-absent body (same rule as scrape.ts): each key is written ONLY when its flag is
|
|
69
|
+
// present, so a bare create never materializes a server default (#2480 E1). Typed via an
|
|
70
|
+
// assertion at the call site — the wire body is pinned by request-shapes.test.ts.
|
|
39
71
|
const body = {
|
|
40
72
|
url: opts.url,
|
|
41
|
-
// Commander delivers a plain string; the server validates the enum (422 on a bad value).
|
|
42
73
|
interval_type: opts.intervalType,
|
|
43
74
|
webhook_url: opts.webhookUrl,
|
|
44
75
|
};
|
|
45
76
|
if (opts.cron !== undefined)
|
|
46
|
-
body
|
|
77
|
+
body['cron_expression'] = opts.cron;
|
|
78
|
+
if (opts.type !== undefined)
|
|
79
|
+
body['type'] = opts.type;
|
|
80
|
+
if (opts.changeThreshold !== undefined)
|
|
81
|
+
body['change_threshold'] = opts.changeThreshold;
|
|
82
|
+
if (opts.notifyOn !== undefined)
|
|
83
|
+
body['notify_on'] = opts.notifyOn;
|
|
84
|
+
if (opts.webhookSecret !== undefined)
|
|
85
|
+
body['webhook_secret'] = opts.webhookSecret;
|
|
86
|
+
if (opts.webhookHeaders !== undefined)
|
|
87
|
+
body['webhook_headers'] = opts.webhookHeaders;
|
|
88
|
+
if (opts.options !== undefined)
|
|
89
|
+
body['options'] = opts.options;
|
|
90
|
+
if (opts.scrapeOptions !== undefined)
|
|
91
|
+
body['scrape_options'] = opts.scrapeOptions;
|
|
92
|
+
if (opts.metadata !== undefined)
|
|
93
|
+
body['metadata'] = opts.metadata;
|
|
47
94
|
try {
|
|
48
95
|
const result = await client.monitors.create(body);
|
|
49
96
|
printResult(result, g.json);
|
|
@@ -58,6 +105,9 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
58
105
|
.option('--limit <n>', 'Maximum number of monitors to return', (v) => parseIntFlag('--limit', v))
|
|
59
106
|
.option('--cursor <cursor>', 'Pagination cursor from a previous page')
|
|
60
107
|
.option('--status <status>', `Filter by status: ${MONITOR_STATUSES.join(', ')} (default active)`, (v) => parseEnum('--status', MONITOR_STATUSES, v))
|
|
108
|
+
// `status` is the SDK's spec-derived `MonitorStatus` union, not a bare string (#2547) —
|
|
109
|
+
// `parseEnum` above already returns exactly that union, so this is a type-only narrowing
|
|
110
|
+
// with no runtime change.
|
|
61
111
|
.action(async (opts) => {
|
|
62
112
|
const g = getGlobal();
|
|
63
113
|
const client = buildClient(g);
|
|
@@ -97,20 +147,51 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
97
147
|
});
|
|
98
148
|
cmd
|
|
99
149
|
.command('update <id>')
|
|
100
|
-
.description('Update a monitor')
|
|
150
|
+
.description('Update a monitor (including pause/resume via --status)')
|
|
151
|
+
.option('--url <url>', 'New URL to monitor (omitted → unchanged)')
|
|
101
152
|
.option('--interval-type <type>', 'New check interval: hourly, daily, weekly, or cron')
|
|
102
153
|
.option('--cron <expr>', 'New cron expression (when interval-type is cron, e.g. "0 * * * *")')
|
|
103
154
|
.option('--webhook-url <url>', 'New HTTPS webhook URL')
|
|
155
|
+
// `--status` is the 2-value MonitorUpdateRequest.status enum — NOT the 5-value list filter.
|
|
156
|
+
// Deliberately NO `--type`: PATCH /v1/monitors/{id} rejects `type` with 422 (immutable, #1984).
|
|
157
|
+
.option('--status <status>', `Pause or resume the monitor: ${MONITOR_UPDATE_STATUSES.join(', ')}`, (v) => parseEnum('--status', MONITOR_UPDATE_STATUSES, v))
|
|
158
|
+
.option('--change-threshold <pct>', 'New change percentage that triggers a notification (float)', (v) => parseNumberFlag('--change-threshold', v))
|
|
159
|
+
.option('--notify-on <event>', `When to notify: ${NOTIFY_ONS.join(', ')}`, (v) => parseEnum('--notify-on', NOTIFY_ONS, v))
|
|
160
|
+
.option('--webhook-secret <secret>', 'New HMAC secret for webhook signature verification')
|
|
161
|
+
.option('--webhook-headers <json>', `New webhook headers as a JSON object, e.g. '{"X-Sig":"abc"}'`, (v) => parseJsonFlag('--webhook-headers', v, 'object'))
|
|
162
|
+
.option('--options <json>', `New screenshot-monitor options as a JSON object`, (v) => parseJsonFlag('--options', v, 'object'))
|
|
163
|
+
.option('--scrape-options <json>', `New scrape-monitor options as a JSON object`, (v) => parseJsonFlag('--scrape-options', v, 'object'))
|
|
164
|
+
.option('--metadata <json>', `New metadata as a JSON object, e.g. '{"run":"7"}' (omitted → unchanged)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
104
165
|
.action(async (id, opts) => {
|
|
105
166
|
const g = getGlobal();
|
|
106
167
|
const client = buildClient(g);
|
|
107
168
|
const body = {};
|
|
169
|
+
// `url` IS settable on PATCH (openapi.yaml MonitorUpdateRequest) — unlike `type`, which
|
|
170
|
+
// stays immutable and deliberately has no flag (#1984).
|
|
171
|
+
if (opts.url !== undefined)
|
|
172
|
+
body['url'] = opts.url;
|
|
108
173
|
if (opts.intervalType !== undefined)
|
|
109
174
|
body['interval_type'] = opts.intervalType;
|
|
110
175
|
if (opts.cron !== undefined)
|
|
111
176
|
body['cron_expression'] = opts.cron;
|
|
112
177
|
if (opts.webhookUrl !== undefined)
|
|
113
178
|
body['webhook_url'] = opts.webhookUrl;
|
|
179
|
+
if (opts.status !== undefined)
|
|
180
|
+
body['status'] = opts.status;
|
|
181
|
+
if (opts.changeThreshold !== undefined)
|
|
182
|
+
body['change_threshold'] = opts.changeThreshold;
|
|
183
|
+
if (opts.notifyOn !== undefined)
|
|
184
|
+
body['notify_on'] = opts.notifyOn;
|
|
185
|
+
if (opts.webhookSecret !== undefined)
|
|
186
|
+
body['webhook_secret'] = opts.webhookSecret;
|
|
187
|
+
if (opts.webhookHeaders !== undefined)
|
|
188
|
+
body['webhook_headers'] = opts.webhookHeaders;
|
|
189
|
+
if (opts.options !== undefined)
|
|
190
|
+
body['options'] = opts.options;
|
|
191
|
+
if (opts.scrapeOptions !== undefined)
|
|
192
|
+
body['scrape_options'] = opts.scrapeOptions;
|
|
193
|
+
if (opts.metadata !== undefined)
|
|
194
|
+
body['metadata'] = opts.metadata;
|
|
114
195
|
try {
|
|
115
196
|
const result = await client.monitors.update(id, body);
|
|
116
197
|
printResult(result, g.json);
|