@hunt-labs/bytekit-cli 0.6.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 +95 -4
- package/README.md +51 -5
- package/dist/client.d.ts +15 -1
- package/dist/client.js +16 -3
- package/dist/commands/account.js +2 -1
- package/dist/commands/bulk.js +8 -4
- package/dist/commands/fetch.js +8 -4
- package/dist/commands/monitors.js +20 -6
- package/dist/commands/scrape.js +14 -4
- package/dist/commands/screenshots.js +141 -8
- package/dist/commands/search.js +2 -1
- package/dist/commands/sitemap.js +4 -2
- package/dist/commands/usage.js +6 -3
- package/dist/commands/webhook-deliveries.js +4 -2
- package/dist/lib/download-artifact.js +3 -2
- package/dist/lib/parse-flags.js +3 -1
- package/dist/output.d.ts +33 -2
- package/dist/output.js +112 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,74 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.7.0] - 2026-08-07
|
|
14
|
+
|
|
15
|
+
### Breaking
|
|
16
|
+
|
|
17
|
+
- **A not-ready `screenshots get -o` / `screenshots create -o` now exits `2` and prints the bare
|
|
18
|
+
`ss_` id, instead of exiting `1` with `not ready yet: screenshot is pending`.** The two poll
|
|
19
|
+
commands disagreed: `scrape get --raw` on a still-queued scrape already wrote the bare `sc_` id
|
|
20
|
+
to stdout and exited `2` — a distinct, scriptable "accepted but not ready" signal — while the
|
|
21
|
+
screenshots side reported the same state as a failure and threw the handle away, so a polling
|
|
22
|
+
script could branch on "queued" for one command and not the other. The screenshots branch now
|
|
23
|
+
mirrors scrape exactly: bare `ss_` id on stdout, a poll note on stderr, exit `2`, and (as before)
|
|
24
|
+
no file written. A script that treated any non-zero exit as "this screenshot failed" now sees `2`
|
|
25
|
+
for a job that is merely still running — branch on the code (`2` = poll again, `1` = failed)
|
|
26
|
+
rather than on non-zero. Three states deliberately do **not** move: a terminally `failed`
|
|
27
|
+
screenshot stays exit `1` with its error surfaced (it will never complete, so a "poll again"
|
|
28
|
+
signal would loop a script forever), an envelope carrying no `ss_` id stays exit `1` (there is no
|
|
29
|
+
handle to hand back), and `--json` keeps the envelope at exit `0`.
|
|
30
|
+
- **`--json` on a `fetch` command now emits `X-Fetch-ID` and `X-Fetch-URL`, not `X-Fetch-Id` and
|
|
31
|
+
`X-Fetch-Url`.** The metadata keys were derived by a generic segment-wise Title-Caser, which
|
|
32
|
+
lowercases every non-initial character of a segment — so the two acronym-bearing headers came out
|
|
33
|
+
in a spelling the API reference never uses, and a `jq '."X-Fetch-ID"'` filter written from the
|
|
34
|
+
docs returned `null`. All 16 `X-Fetch-*` keys now match the spec names byte-for-byte. The
|
|
35
|
+
**values** are unchanged, the non-`X-Fetch-*` headers stay filtered out, and every other key
|
|
36
|
+
(`X-Fetch-Waf-Vendor`, `X-Fetch-Duration-Ms`, `X-Fetch-Fast-Path`, …) is spelled exactly as
|
|
37
|
+
before. A consumer reading `.["X-Fetch-Id"]` must switch to `.["X-Fetch-ID"]`.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **A `screenshots get -o` / `screenshots create -o` poll only says "poll again" for a `pending` or
|
|
42
|
+
`processing` screenshot.** The not-ready predicate decided from the presence of an `image_url`,
|
|
43
|
+
an `id` and a nested error alone — `status` was read only to phrase the message — so a
|
|
44
|
+
`completed` screenshot whose `image_url` never materialized, a status outside the closed enum,
|
|
45
|
+
and the flat `error_code`/`error_message` failed shape all received exit `2` and "no image
|
|
46
|
+
artifact is available yet. Poll it with: …" for a job that would never complete. Those states now
|
|
47
|
+
exit `1`: a terminal failure prints `screenshot failed: …`, and any other artifact-less state
|
|
48
|
+
prints `no image artifact available: screenshot is <status>`. Genuine `pending` / `processing`
|
|
49
|
+
polls keep the bare `ss_` id, the poll note and exit `2` exactly as before — **including** a
|
|
50
|
+
poll whose envelope carries an `error_code` from an attempt that failed and is being retried,
|
|
51
|
+
since the status, not the error, decides whether polling can still help. `--json` still prints
|
|
52
|
+
the envelope at exit `0`.
|
|
53
|
+
- **A terminally failed screenshot reported through the spec's flat `error_code`/`error_message`
|
|
54
|
+
fields now reaches stderr.** Only the nested `{error:{code,message}}` envelope was read, so the
|
|
55
|
+
shape `ScreenshotResponse` actually declares surfaced neither code nor message. It now prints
|
|
56
|
+
`screenshot failed: <message> (<code>)` — the formatting `scrape get --raw` already uses for a
|
|
57
|
+
failed scrape. The nested envelope's own message is unchanged.
|
|
58
|
+
- **`scrape get --raw` on a terminally failed scrape now reports why it failed.** It printed
|
|
59
|
+
`--raw: response contains no formats` — technically true of a `ScrapeErrorEnvelope`, and the one
|
|
60
|
+
diagnostic that cannot explain anything — while the envelope's own `error.code` and
|
|
61
|
+
`error.message` never reached the caller. Both now go to stderr (exit `1`, unchanged). The
|
|
62
|
+
failure is reported ahead of the format diagnostics, so an envelope carrying both a failure and
|
|
63
|
+
an empty `formats: {}` still says why it failed rather than pointing at a `--format` choice that
|
|
64
|
+
cannot help.
|
|
65
|
+
- **An empty-formats success now names the warning that explains it.** A completed scrape whose
|
|
66
|
+
persisted artifact could not be rehydrated comes back with `formats: {}` and an
|
|
67
|
+
`artifact_unavailable` warning; `--raw` rendered that as `--raw requires --format when the
|
|
68
|
+
response contains multiple formats`, which is false on both counts. It now names the warning's
|
|
69
|
+
code and message, or says plainly that the formats map is empty when no warning explains it.
|
|
70
|
+
- **Error paths no longer risk losing their own diagnostic.** `handleError` and the missing-API-key
|
|
71
|
+
path each wrote to stderr and then immediately hard-`process.exit(1)`. Node's stderr is
|
|
72
|
+
asynchronous when it points at a pipe, and `process.exit` discards whatever is still queued — so
|
|
73
|
+
the one line a script greps was the line most at risk of being thrown away, which is exactly the
|
|
74
|
+
defect this package's flush policy exists to prevent. Both now set `process.exitCode` and let the
|
|
75
|
+
process end naturally once stderr has drained. The exit codes and the rendered messages are
|
|
76
|
+
byte-identical.
|
|
77
|
+
- `--headers`, `--custom` and `--metadata` said `must be valid JSON (a object)` on a malformed
|
|
78
|
+
value; the article now agrees — `an object` / `an array`.
|
|
79
|
+
- The `[0.6.0]` link reference, missing from the bottom of this file, is restored.
|
|
80
|
+
|
|
13
81
|
## [0.6.0] - 2026-07-29
|
|
14
82
|
|
|
15
83
|
### Breaking
|
|
@@ -65,7 +133,12 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
65
133
|
- `--metadata` on `monitors create`, and `--metadata` + `--url` on `monitors update`. `--type`
|
|
66
134
|
remains deliberately absent from `update` — it is immutable server-side.
|
|
67
135
|
|
|
68
|
-
## [0.5.1] - 2026-07-28
|
|
136
|
+
## [0.5.1] - 2026-07-28 [NEVER PUBLISHED]
|
|
137
|
+
|
|
138
|
+
> **Never published to npm.** This version was cut in-tree but no `@hunt-labs/bytekit-cli@0.5.1`
|
|
139
|
+
> tarball exists — `npm i @hunt-labs/bytekit-cli@0.5.1` fails. Everything below shipped to users in
|
|
140
|
+
> **0.6.0**, which is the first published release containing it. Reconciled by issue #2680; the
|
|
141
|
+
> `release-drift` CI gate now blocks a new phantom heading from being introduced.
|
|
69
142
|
|
|
70
143
|
### Fixed
|
|
71
144
|
|
|
@@ -188,7 +261,12 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
188
261
|
`sc_` id** on stdout with a note on stderr (it does not print the queued job
|
|
189
262
|
envelope). (#2544)
|
|
190
263
|
|
|
191
|
-
## [0.4.0] - 2026-07-23
|
|
264
|
+
## [0.4.0] - 2026-07-23 [NEVER PUBLISHED]
|
|
265
|
+
|
|
266
|
+
> **Never published to npm.** This version was cut in-tree but no `@hunt-labs/bytekit-cli@0.4.0`
|
|
267
|
+
> tarball exists — `npm i @hunt-labs/bytekit-cli@0.4.0` fails. Everything below shipped to users in
|
|
268
|
+
> **0.5.0**, which is the first published release containing it. Reconciled by issue #2680; the
|
|
269
|
+
> `release-drift` CI gate now blocks a new phantom heading from being introduced.
|
|
192
270
|
|
|
193
271
|
### Removed (**BREAKING**)
|
|
194
272
|
|
|
@@ -209,7 +287,13 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
209
287
|
**Replacement:** manage API keys from the dashboard at
|
|
210
288
|
[app.bytekit.com](https://app.bytekit.com). (#2536)
|
|
211
289
|
|
|
212
|
-
## [0.3.1] - 2026-07-22
|
|
290
|
+
## [0.3.1] - 2026-07-22 [NEVER PUBLISHED]
|
|
291
|
+
|
|
292
|
+
> **Never published to npm.** This version was cut in-tree but no `@hunt-labs/bytekit-cli@0.3.1`
|
|
293
|
+
> tarball exists — `npm i @hunt-labs/bytekit-cli@0.3.1` fails. Everything below shipped to users in
|
|
294
|
+
> **0.5.0**, which is the first published release containing it (0.4.0 was never published either —
|
|
295
|
+
> see its heading above). Reconciled by issue #2680; the `release-drift` CI gate now blocks a new
|
|
296
|
+
> phantom heading from being introduced.
|
|
213
297
|
|
|
214
298
|
### Fixed
|
|
215
299
|
|
|
@@ -292,7 +376,12 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
292
376
|
rebuilding the shared `dist`), which is now fixed. No consumer-facing changes
|
|
293
377
|
beyond 0.2.1.
|
|
294
378
|
|
|
295
|
-
## [0.2.1] - 2026-07-16
|
|
379
|
+
## [0.2.1] - 2026-07-16 [NEVER PUBLISHED]
|
|
380
|
+
|
|
381
|
+
> **Never published to npm.** This version was cut in-tree but no `@hunt-labs/bytekit-cli@0.2.1`
|
|
382
|
+
> tarball exists — `npm i @hunt-labs/bytekit-cli@0.2.1` fails. Everything below shipped to users in
|
|
383
|
+
> **0.2.2**, which is the first published release containing it. Reconciled by issue #2680; the
|
|
384
|
+
> `release-drift` CI gate now blocks a new phantom heading from being introduced.
|
|
296
385
|
|
|
297
386
|
### Changed
|
|
298
387
|
|
|
@@ -342,6 +431,8 @@ Additive, lower-risk release — no breaking changes to existing commands.
|
|
|
342
431
|
subcommands, published under the original `rapidcrawl-cli` name.
|
|
343
432
|
|
|
344
433
|
[Unreleased]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/cli
|
|
434
|
+
[0.7.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/cli
|
|
435
|
+
[0.6.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/cli
|
|
345
436
|
[0.5.1]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/cli
|
|
346
437
|
[0.5.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/cli
|
|
347
438
|
[0.4.0]: https://github.com/Hunt-Labs-Inc/ByteKit/tree/main/packages/cli
|
package/README.md
CHANGED
|
@@ -172,6 +172,17 @@ response metadata instead of the body. `-o <file>` writes the body bytes to a
|
|
|
172
172
|
file byte-for-byte (with `--json`, the metadata object goes to the file
|
|
173
173
|
instead). `fetch bulk` returns an ordinary JSON job envelope.
|
|
174
174
|
|
|
175
|
+
The `--json` metadata keys are the **spec header names exactly** as the API
|
|
176
|
+
reference declares them, so a filter written from the docs works unchanged:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
bytekit fetch get --url https://example.com --json | jq -r '."X-Fetch-ID"'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Note the acronym casing — `X-Fetch-ID` and `X-Fetch-URL`, not `X-Fetch-Id` /
|
|
183
|
+
`X-Fetch-Url`. (Versions up to 0.6.0 emitted the Title-Cased spellings; see the
|
|
184
|
+
CHANGELOG.)
|
|
185
|
+
|
|
175
186
|
Both verbs accept `--country <cc>`, `--timeout-ms <n>`, and `--cache-ttl <ttl>`
|
|
176
187
|
(`0`, `Nh`, or `Nd`), each omitted when absent. `--markdown-mode`
|
|
177
188
|
(`article`, `raw`, `llm`) is `fetch create` only.
|
|
@@ -303,11 +314,34 @@ omitted when absent. `retry <id>` requeues a failed or exhausted delivery.
|
|
|
303
314
|
|
|
304
315
|
## Exit codes
|
|
305
316
|
|
|
306
|
-
| Code | Meaning
|
|
307
|
-
| ---- |
|
|
308
|
-
| `0` | Success.
|
|
309
|
-
| `1` | Usage error, or an API/connection error.
|
|
310
|
-
| `2` | The job was accepted but is
|
|
317
|
+
| Code | Meaning |
|
|
318
|
+
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
319
|
+
| `0` | Success. |
|
|
320
|
+
| `1` | Usage error, or an API/connection error. |
|
|
321
|
+
| `2` | The job was accepted but is **not ready**, so there is no content body or artifact to emit. Applies to `scrape create --async` with `--raw` or a bare `-o` (which writes the bare `sc_` id to stdout) and in human mode; to `scrape get --raw`/`-o` on a still-queued job (bare `sc_` id to stdout); to `screenshots create --async` in human mode (including with `-o`, which deliberately writes no file); and to `screenshots create -o` / `screenshots get -o` on a pending or processing screenshot (bare `ss_` id to stdout, no file written). Not a failure: the job was accepted and billed. Poll it with `bytekit scrape get <id>` / `bytekit screenshots get <id>`. **`--json` keeps exit `0` in every case** — the envelope is a complete machine-readable payload. |
|
|
322
|
+
|
|
323
|
+
A **terminally failed** job is NOT code `2` — it will never complete, so it is
|
|
324
|
+
exit `1` with the reason on stderr. `scrape get --raw` on a failed scrape prints
|
|
325
|
+
that scrape's own `error.code` and message, and `screenshots get -o` on a failed
|
|
326
|
+
screenshot prints `screenshot failed: …`.
|
|
327
|
+
|
|
328
|
+
Only `pending` and `processing` earn code `2`. Any other screenshot state that
|
|
329
|
+
still has no artifact — a `completed` screenshot whose `image_url` never
|
|
330
|
+
materialized, or a status this CLI build does not know — is exit `1` with
|
|
331
|
+
`no image artifact available: screenshot is <status>` on stderr, because
|
|
332
|
+
polling it again cannot change the outcome.
|
|
333
|
+
|
|
334
|
+
The status is what decides, not the presence of an error. A `pending` or
|
|
335
|
+
`processing` screenshot that reports an `error_code` from an attempt that failed
|
|
336
|
+
and is being **retried** is still code `2` with its `ss_` id — the job has not
|
|
337
|
+
finished, and abandoning it there would throw away a capture the server is still
|
|
338
|
+
working on.
|
|
339
|
+
|
|
340
|
+
A **human-mode poll without `-o`/`--raw`** stays exit `0` whatever the job's
|
|
341
|
+
state: `bytekit scrape get <id>` and `bytekit screenshots get <id>` were asked
|
|
342
|
+
for the envelope, and the envelope is exactly what they printed — the status
|
|
343
|
+
field in it is the answer. Code `2` is reserved for the case where you asked for
|
|
344
|
+
a **body or artifact** that does not exist yet.
|
|
311
345
|
|
|
312
346
|
The stderr message for exit code `1` has two shapes, depending on where the
|
|
313
347
|
error originates:
|
|
@@ -334,6 +368,18 @@ case $? in
|
|
|
334
368
|
esac
|
|
335
369
|
```
|
|
336
370
|
|
|
371
|
+
The same branch works for a screenshot poll, because both commands now signal
|
|
372
|
+
"not ready" identically:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
ss_id=$(bytekit screenshots get "$ss_id" -o shot.png)
|
|
376
|
+
case $? in
|
|
377
|
+
0) echo "downloaded shot.png" ;;
|
|
378
|
+
2) echo "$ss_id not ready — poll again" ;;
|
|
379
|
+
*) exit 1 ;; # terminal failure, reason on stderr
|
|
380
|
+
esac
|
|
381
|
+
```
|
|
382
|
+
|
|
337
383
|
## Documentation
|
|
338
384
|
|
|
339
385
|
Full API reference: [bytekit.com/docs](https://bytekit.com/docs).
|
package/dist/client.d.ts
CHANGED
|
@@ -167,7 +167,21 @@ export declare function createRetryingRequestWithResponse(apiKey: string, baseUr
|
|
|
167
167
|
* Resolution order (explicit input beats environment):
|
|
168
168
|
* apiKey: --key flag → BYTEKIT_API_KEY.
|
|
169
169
|
* baseUrl: --base-url flag → BYTEKIT_BASE_URL → DEFAULT_BASE_URL.
|
|
170
|
-
*
|
|
170
|
+
*
|
|
171
|
+
* THROWS a plain `Error` when no key is available (#2686 item 4). It used to `console.error` and
|
|
172
|
+
* then hard-exit — an async stderr write followed immediately by the one call that discards
|
|
173
|
+
* the userspace stdio queue, which is exactly the defect the package's #2544 flush policy exists to
|
|
174
|
+
* prevent. Throwing stops just as hard (there is no client to return, E4) while routing the message
|
|
175
|
+
* through `handleError`, whose soft `process.exitCode` lets stderr drain first. The rendered stderr
|
|
176
|
+
* line is byte-identical: `handleError` supplies the `Error: ` prefix, and a plain Error carries
|
|
177
|
+
* neither a status nor a ByteKit code, so no parenthesised suffix is added.
|
|
178
|
+
*
|
|
179
|
+
* Because it throws, EVERY caller invokes it from inside its action's `try` block. `index.ts` drives
|
|
180
|
+
* commander with `.parse()`, not `.parseAsync()`, so nothing awaits an action's promise: a throw
|
|
181
|
+
* raised above the try would escape as an unhandled rejection and print a raw stack trace instead of
|
|
182
|
+
* the clean one-liner — the same constraint `commands/scrape.ts` already documents for
|
|
183
|
+
* `resolveOutputMode`. `error-exit-contract.test.ts` pins that placement structurally, across every
|
|
184
|
+
* call site at once.
|
|
171
185
|
*
|
|
172
186
|
* The returned client's `request`, `requestRaw`, AND `requestWithResponse` are all replaced by the
|
|
173
187
|
* retrying transports, so every command inherits the 429 retry — JSON commands via `request`, the
|
package/dist/client.js
CHANGED
|
@@ -492,7 +492,21 @@ function normalizeBaseUrl(url) {
|
|
|
492
492
|
* Resolution order (explicit input beats environment):
|
|
493
493
|
* apiKey: --key flag → BYTEKIT_API_KEY.
|
|
494
494
|
* baseUrl: --base-url flag → BYTEKIT_BASE_URL → DEFAULT_BASE_URL.
|
|
495
|
-
*
|
|
495
|
+
*
|
|
496
|
+
* THROWS a plain `Error` when no key is available (#2686 item 4). It used to `console.error` and
|
|
497
|
+
* then hard-exit — an async stderr write followed immediately by the one call that discards
|
|
498
|
+
* the userspace stdio queue, which is exactly the defect the package's #2544 flush policy exists to
|
|
499
|
+
* prevent. Throwing stops just as hard (there is no client to return, E4) while routing the message
|
|
500
|
+
* through `handleError`, whose soft `process.exitCode` lets stderr drain first. The rendered stderr
|
|
501
|
+
* line is byte-identical: `handleError` supplies the `Error: ` prefix, and a plain Error carries
|
|
502
|
+
* neither a status nor a ByteKit code, so no parenthesised suffix is added.
|
|
503
|
+
*
|
|
504
|
+
* Because it throws, EVERY caller invokes it from inside its action's `try` block. `index.ts` drives
|
|
505
|
+
* commander with `.parse()`, not `.parseAsync()`, so nothing awaits an action's promise: a throw
|
|
506
|
+
* raised above the try would escape as an unhandled rejection and print a raw stack trace instead of
|
|
507
|
+
* the clean one-liner — the same constraint `commands/scrape.ts` already documents for
|
|
508
|
+
* `resolveOutputMode`. `error-exit-contract.test.ts` pins that placement structurally, across every
|
|
509
|
+
* call site at once.
|
|
496
510
|
*
|
|
497
511
|
* The returned client's `request`, `requestRaw`, AND `requestWithResponse` are all replaced by the
|
|
498
512
|
* retrying transports, so every command inherits the 429 retry — JSON commands via `request`, the
|
|
@@ -511,8 +525,7 @@ function normalizeBaseUrl(url) {
|
|
|
511
525
|
export function buildClient(opts) {
|
|
512
526
|
const apiKey = opts.key ?? process.env['BYTEKIT_API_KEY'];
|
|
513
527
|
if (!apiKey) {
|
|
514
|
-
|
|
515
|
-
process.exit(1);
|
|
528
|
+
throw new Error('API key required. Pass --key or set BYTEKIT_API_KEY.');
|
|
516
529
|
}
|
|
517
530
|
const baseUrl = normalizeBaseUrl(opts.baseUrl ?? process.env['BYTEKIT_BASE_URL'] ?? DEFAULT_BASE_URL);
|
|
518
531
|
const client = new ByteKit({ apiKey, baseUrl });
|
package/dist/commands/account.js
CHANGED
|
@@ -19,8 +19,9 @@ export function makeAccountCommand(getGlobal) {
|
|
|
19
19
|
.description('Get the authenticated account')
|
|
20
20
|
.action(async () => {
|
|
21
21
|
const g = getGlobal();
|
|
22
|
-
const client = buildClient(g);
|
|
23
22
|
try {
|
|
23
|
+
// 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.
|
|
24
|
+
const client = buildClient(g);
|
|
24
25
|
const result = await client.account.get();
|
|
25
26
|
printResult(result, g.json);
|
|
26
27
|
}
|
package/dist/commands/bulk.js
CHANGED
|
@@ -11,8 +11,9 @@ export function makeBulkCommand(getGlobal) {
|
|
|
11
11
|
.description('List screenshots in a bulk job')
|
|
12
12
|
.action(async (id) => {
|
|
13
13
|
const g = getGlobal();
|
|
14
|
-
const client = buildClient(g);
|
|
15
14
|
try {
|
|
15
|
+
// 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.
|
|
16
|
+
const client = buildClient(g);
|
|
16
17
|
const result = await client.bulk.screenshots.list(id);
|
|
17
18
|
printResult(result, g.json);
|
|
18
19
|
}
|
|
@@ -33,8 +34,9 @@ export function makeBulkCommand(getGlobal) {
|
|
|
33
34
|
// property (validators/bulk.ts:135), so the flag would 422 every call (#2596 drift D2).
|
|
34
35
|
.action(async (opts) => {
|
|
35
36
|
const g = getGlobal();
|
|
36
|
-
const client = buildClient(g);
|
|
37
37
|
try {
|
|
38
|
+
// 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.
|
|
39
|
+
const client = buildClient(g);
|
|
38
40
|
const lines = parseBulkLines(opts.file);
|
|
39
41
|
// `urls` XOR `items` (validators/bulk.ts:136-155) — send exactly one. A file of only
|
|
40
42
|
// url-only lines keeps the pre-#2596 `{ urls, webhook_url }` body, key order included,
|
|
@@ -60,8 +62,9 @@ export function makeBulkCommand(getGlobal) {
|
|
|
60
62
|
.description('Get a bulk job status')
|
|
61
63
|
.action(async (id) => {
|
|
62
64
|
const g = getGlobal();
|
|
63
|
-
const client = buildClient(g);
|
|
64
65
|
try {
|
|
66
|
+
// 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.
|
|
67
|
+
const client = buildClient(g);
|
|
65
68
|
const result = await client.bulk.get(id);
|
|
66
69
|
printResult(result, g.json);
|
|
67
70
|
}
|
|
@@ -74,8 +77,9 @@ export function makeBulkCommand(getGlobal) {
|
|
|
74
77
|
.description('Cancel an in-flight bulk job')
|
|
75
78
|
.action(async (id) => {
|
|
76
79
|
const g = getGlobal();
|
|
77
|
-
const client = buildClient(g);
|
|
78
80
|
try {
|
|
81
|
+
// 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.
|
|
82
|
+
const client = buildClient(g);
|
|
79
83
|
const result = await client.bulk.cancel(id);
|
|
80
84
|
printResult(result, g.json);
|
|
81
85
|
}
|
package/dist/commands/fetch.js
CHANGED
|
@@ -41,8 +41,9 @@ export function makeFetchCommand(getGlobal) {
|
|
|
41
41
|
// FetchBulkItemOverrideSchema reject unknown keys), so this top-level behavior is unchanged.
|
|
42
42
|
.action(async (opts) => {
|
|
43
43
|
const g = getGlobal();
|
|
44
|
-
const client = buildClient(g);
|
|
45
44
|
try {
|
|
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);
|
|
46
47
|
// /v1/fetch/bulk has NO `items` property — its `urls` is a
|
|
47
48
|
// `string | FetchBulkItemOverride` union (validators/fetch-bulk.ts:57), so both line
|
|
48
49
|
// shapes ride there directly and no promotion step is needed (#2596 drift D3).
|
|
@@ -66,8 +67,9 @@ export function makeFetchCommand(getGlobal) {
|
|
|
66
67
|
.description('Get a bulk fetch job')
|
|
67
68
|
.action(async (id) => {
|
|
68
69
|
const g = getGlobal();
|
|
69
|
-
const client = buildClient(g);
|
|
70
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);
|
|
71
73
|
const result = await client.fetch.bulk.get(id);
|
|
72
74
|
printResult(result, g.json);
|
|
73
75
|
}
|
|
@@ -101,7 +103,6 @@ export function makeFetchCommand(getGlobal) {
|
|
|
101
103
|
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
102
104
|
.action(async (opts) => {
|
|
103
105
|
const g = getGlobal();
|
|
104
|
-
const client = buildClient(g);
|
|
105
106
|
// Omit-when-absent body, in the `body: Record<string, unknown>` form every other command
|
|
106
107
|
// file uses (scrape.ts:230-268 is the exemplar). The five pre-existing fields keep the
|
|
107
108
|
// exact same wire output they had as inline spreads; a boolean is written ONLY on
|
|
@@ -140,6 +141,8 @@ export function makeFetchCommand(getGlobal) {
|
|
|
140
141
|
if (opts.custom !== undefined)
|
|
141
142
|
body['custom'] = opts.custom;
|
|
142
143
|
try {
|
|
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);
|
|
143
146
|
const result = await client.fetch.create(body);
|
|
144
147
|
emitFetchResult(result, g.json, { file: opts.output });
|
|
145
148
|
}
|
|
@@ -158,8 +161,9 @@ export function makeFetchCommand(getGlobal) {
|
|
|
158
161
|
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
159
162
|
.action(async (opts) => {
|
|
160
163
|
const g = getGlobal();
|
|
161
|
-
const client = buildClient(g);
|
|
162
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);
|
|
163
167
|
const result = await client.fetch.get({
|
|
164
168
|
url: opts.url,
|
|
165
169
|
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
@@ -24,7 +24,6 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
24
24
|
.option('--cursor <cursor>', 'Pagination cursor from a previous page')
|
|
25
25
|
.action(async (id, opts) => {
|
|
26
26
|
const g = getGlobal();
|
|
27
|
-
const client = buildClient(g);
|
|
28
27
|
// Same omit-when-absent rule as `monitors list` (#2480): build the params object with
|
|
29
28
|
// ONLY the flags the caller passed, and call captures.list with a single argument (never
|
|
30
29
|
// captures.list(id, {}) / captures.list(id, undefined)) when no query flag is passed, so
|
|
@@ -35,6 +34,8 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
35
34
|
if (opts.cursor !== undefined)
|
|
36
35
|
params.cursor = opts.cursor;
|
|
37
36
|
try {
|
|
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);
|
|
38
39
|
const result = Object.keys(params).length > 0
|
|
39
40
|
? await client.monitors.captures.list(id, params)
|
|
40
41
|
: await client.monitors.captures.list(id);
|
|
@@ -49,6 +50,14 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
49
50
|
.command('create')
|
|
50
51
|
.description('Create a page-change monitor')
|
|
51
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.
|
|
52
61
|
.requiredOption('--interval-type <type>', 'Check interval: hourly, daily, weekly, or cron')
|
|
53
62
|
.requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified on change')
|
|
54
63
|
.option('--cron <expr>', 'Cron expression (required when --interval-type is cron, e.g. "0 * * * *")')
|
|
@@ -64,7 +73,6 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
64
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'))
|
|
65
74
|
.action(async (opts) => {
|
|
66
75
|
const g = getGlobal();
|
|
67
|
-
const client = buildClient(g);
|
|
68
76
|
// Omit-when-absent body (same rule as scrape.ts): each key is written ONLY when its flag is
|
|
69
77
|
// present, so a bare create never materializes a server default (#2480 E1). Typed via an
|
|
70
78
|
// assertion at the call site — the wire body is pinned by request-shapes.test.ts.
|
|
@@ -92,6 +100,8 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
92
100
|
if (opts.metadata !== undefined)
|
|
93
101
|
body['metadata'] = opts.metadata;
|
|
94
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);
|
|
95
105
|
const result = await client.monitors.create(body);
|
|
96
106
|
printResult(result, g.json);
|
|
97
107
|
}
|
|
@@ -110,7 +120,6 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
110
120
|
// with no runtime change.
|
|
111
121
|
.action(async (opts) => {
|
|
112
122
|
const g = getGlobal();
|
|
113
|
-
const client = buildClient(g);
|
|
114
123
|
// Build the params object with the same omit-when-absent rule as the body builders. When NO
|
|
115
124
|
// query flag is passed, call list() with ZERO arguments (never list({}) / list(undefined)) so
|
|
116
125
|
// the inverse-case request-shape assertion `monitors.list` args length 0 stays green (#2480).
|
|
@@ -122,6 +131,8 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
122
131
|
if (opts.status !== undefined)
|
|
123
132
|
params.status = opts.status;
|
|
124
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);
|
|
125
136
|
const result = Object.keys(params).length > 0
|
|
126
137
|
? await client.monitors.list(params)
|
|
127
138
|
: await client.monitors.list();
|
|
@@ -136,8 +147,9 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
136
147
|
.description('Get a monitor by ID')
|
|
137
148
|
.action(async (id) => {
|
|
138
149
|
const g = getGlobal();
|
|
139
|
-
const client = buildClient(g);
|
|
140
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);
|
|
141
153
|
const result = await client.monitors.get(id);
|
|
142
154
|
printResult(result, g.json);
|
|
143
155
|
}
|
|
@@ -164,7 +176,6 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
164
176
|
.option('--metadata <json>', `New metadata as a JSON object, e.g. '{"run":"7"}' (omitted → unchanged)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
165
177
|
.action(async (id, opts) => {
|
|
166
178
|
const g = getGlobal();
|
|
167
|
-
const client = buildClient(g);
|
|
168
179
|
const body = {};
|
|
169
180
|
// `url` IS settable on PATCH (openapi.yaml MonitorUpdateRequest) — unlike `type`, which
|
|
170
181
|
// stays immutable and deliberately has no flag (#1984).
|
|
@@ -193,6 +204,8 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
193
204
|
if (opts.metadata !== undefined)
|
|
194
205
|
body['metadata'] = opts.metadata;
|
|
195
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);
|
|
196
209
|
const result = await client.monitors.update(id, body);
|
|
197
210
|
printResult(result, g.json);
|
|
198
211
|
}
|
|
@@ -205,8 +218,9 @@ export function makeMonitorsCommand(getGlobal) {
|
|
|
205
218
|
.description('Delete a monitor')
|
|
206
219
|
.action(async (id) => {
|
|
207
220
|
const g = getGlobal();
|
|
208
|
-
const client = buildClient(g);
|
|
209
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);
|
|
210
224
|
const result = await client.monitors.delete(id);
|
|
211
225
|
printResult(result, g.json);
|
|
212
226
|
}
|
package/dist/commands/scrape.js
CHANGED
|
@@ -55,8 +55,9 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
55
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'))
|
|
56
56
|
.action(async (opts) => {
|
|
57
57
|
const g = getGlobal();
|
|
58
|
-
const client = buildClient(g);
|
|
59
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);
|
|
60
61
|
// readFileSync throws ENOENT on missing file — caught by handleError below
|
|
61
62
|
const lines = parseBulkLines(opts.file);
|
|
62
63
|
// `urls` XOR `items` (validators/scrape-bulk.ts:91-98) — send exactly one, keeping the
|
|
@@ -84,8 +85,9 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
84
85
|
.description('Get a bulk scrape job')
|
|
85
86
|
.action(async (id) => {
|
|
86
87
|
const g = getGlobal();
|
|
87
|
-
const client = buildClient(g);
|
|
88
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);
|
|
89
91
|
const result = await client.scrape.bulk.get(id);
|
|
90
92
|
printResult(result, g.json);
|
|
91
93
|
}
|
|
@@ -101,6 +103,12 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
101
103
|
// Repeatable AND CSV-aware: `--format raw_html --format markdown` and `--format raw_html,markdown`
|
|
102
104
|
// both send formats: ['raw_html','markdown']. Sent as the `formats` array; omitted → server
|
|
103
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.
|
|
104
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)
|
|
105
113
|
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
106
114
|
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
@@ -137,7 +145,6 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
137
145
|
.option('-o, --output <file>', 'Write the output to the given file path (implies --raw unless --json is set)')
|
|
138
146
|
.action(async (opts) => {
|
|
139
147
|
const g = getGlobal();
|
|
140
|
-
const client = buildClient(g);
|
|
141
148
|
const body = { url: opts.url };
|
|
142
149
|
// The v1 scrape validator expects `formats` (array). The --format collector already
|
|
143
150
|
// accumulates repeated/CSV occurrences into an array; write it verbatim, omitting the key
|
|
@@ -206,6 +213,8 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
206
213
|
if (opts.mobile === true)
|
|
207
214
|
body['mobile'] = true;
|
|
208
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);
|
|
209
218
|
// FIRST statement inside the try — deliberately NOT hoisted next to buildClient() where
|
|
210
219
|
// the other handlers put pre-request logic. index.ts drives commander with .parse(), not
|
|
211
220
|
// .parseAsync(), so nothing awaits this action's promise: a throw raised above the try
|
|
@@ -267,8 +276,9 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
267
276
|
.option('--format <format>', 'Which persisted format to emit — only takes effect with --raw/-o; ignored otherwise')
|
|
268
277
|
.action(async (id, opts) => {
|
|
269
278
|
const g = getGlobal();
|
|
270
|
-
const client = buildClient(g);
|
|
271
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);
|
|
272
282
|
// Mirror `scrape create`'s output path: resolveOutputMode FIRST inside the try (so a
|
|
273
283
|
// --json/--raw conflict rejects before the request), then emitResult. The request body is
|
|
274
284
|
// unchanged — scrape.get(id) takes no options — so the inverse-case arg-length assertion in
|
|
@@ -37,16 +37,108 @@ export function parseClip(value) {
|
|
|
37
37
|
* "queued" without parsing stderr.
|
|
38
38
|
*
|
|
39
39
|
* BREAKING (#2596): `screenshots create --async` previously exited 0. See the README exit-code
|
|
40
|
-
* table and the CHANGELOG `[0.6.0]` note (
|
|
41
|
-
*
|
|
40
|
+
* table and the CHANGELOG `[0.6.0]` note (`[Unreleased]` is kept deliberately empty per
|
|
41
|
+
* `packaging.test.ts:203`; see #2620).
|
|
42
|
+
*
|
|
43
|
+
* Also carried by a NOT-READY `screenshots get -o` poll (#2686 item 1) — same meaning, same value:
|
|
44
|
+
* accepted and billed, artifact not materialized yet.
|
|
42
45
|
*/
|
|
43
46
|
const QUEUED_EXIT_CODE = 2;
|
|
47
|
+
/**
|
|
48
|
+
* The ONLY statuses a screenshot can still reach `completed` from, and therefore the only ones
|
|
49
|
+
* worth telling a caller to poll (#2792). Every other value of `status` — the terminal ones, and
|
|
50
|
+
* anything the server may add later — is a state polling cannot improve.
|
|
51
|
+
*/
|
|
52
|
+
const POLLABLE_STATUSES = ['pending', 'processing'];
|
|
53
|
+
/**
|
|
54
|
+
* A still-pollable screenshot poll that can hand its `ss_` handle back, or undefined.
|
|
55
|
+
*
|
|
56
|
+
* "Not ready" is `status === 'pending' || status === 'processing'` and nothing else, plus a usable
|
|
57
|
+
* `ss_` handle and no materialized `image_url` (#2792). Before that gate existed the predicate
|
|
58
|
+
* decided from `error`/`image_url`/`id` alone and read `status` only to LABEL the message, so a
|
|
59
|
+
* `completed` envelope with a null `image_url`, a status outside the enum, and the FLAT
|
|
60
|
+
* `error_code`/`error_message` failed shape that `serializeScreenshot` emits all received "poll
|
|
61
|
+
* again" advice for a job that would never complete — the exact loop the exclusion exists to
|
|
62
|
+
* prevent.
|
|
63
|
+
*
|
|
64
|
+
* Two shapes are excluded deliberately rather than incidentally:
|
|
65
|
+
*
|
|
66
|
+
* - **No `status` at all (#2792 E1).** Not treated as pending. `serializeScreenshot`
|
|
67
|
+
* (`gateway/src/routes/screenshots.ts:365`) always writes `status`, so a statusless envelope is
|
|
68
|
+
* not something the server produces; and an envelope carrying no evidence that work is in
|
|
69
|
+
* progress is exactly what must not be advertised as pollable. It takes the error path, which
|
|
70
|
+
* names the state instead of promising an artifact.
|
|
71
|
+
* - **A status outside the enum (#2792 E3).** Same reasoning, stated as the closed-set gate: a new
|
|
72
|
+
* server status is unknown to this build, so it is reported, never polled and never fallen
|
|
73
|
+
* through into the download path.
|
|
74
|
+
*
|
|
75
|
+
* And one shape is deliberately NOT excluded: a pollable status carrying a flat `error_code`. That
|
|
76
|
+
* is the ordinary RETRY envelope, not a verdict — see {@link flatScreenshotFailure}. Excluding it
|
|
77
|
+
* would abandon a job the server is actively retrying (#2792 B1).
|
|
78
|
+
*
|
|
79
|
+
* An envelope with no string `id` is excluded too — there is no handle to emit, so the ordinary
|
|
80
|
+
* error path (which at least names the status) is the honest outcome.
|
|
81
|
+
*/
|
|
82
|
+
function notReadyScreenshot(result) {
|
|
83
|
+
if (typeof result !== 'object' || result === null)
|
|
84
|
+
return undefined;
|
|
85
|
+
const obj = result;
|
|
86
|
+
// The gate, not merely the label: `status` decides, and the closed list is the whole decision.
|
|
87
|
+
const status = POLLABLE_STATUSES.find((s) => s === obj.status);
|
|
88
|
+
if (status === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
// The NESTED error envelope stays disqualifying, exactly as before this ticket. A flat
|
|
91
|
+
// `error_code` is NOT consulted here: on a pollable status it is a retry error (#2792 B1), and
|
|
92
|
+
// the nested shape can never co-occur with a pollable status to disagree — it is built only by
|
|
93
|
+
// `buildFailedScreenshotEnvelope`, under `status === 'failed'` (`routes/screenshots.ts:267`).
|
|
94
|
+
if (obj.error !== undefined && obj.error !== null)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (typeof obj.image_url === 'string' && obj.image_url.length > 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (typeof obj.id !== 'string' || obj.id.length === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
return { id: obj.id, status };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The failure carried by the FLAT `error_code`/`error_message` fields, or undefined (#2792).
|
|
104
|
+
*
|
|
105
|
+
* `serializeScreenshot` (gateway `routes/screenshots.ts:365`) — and therefore the spec's own
|
|
106
|
+
* `ScreenshotResponse` — reports a failure as top-level `error_code`/`error_message`, while
|
|
107
|
+
* `buildFailedScreenshotEnvelope` (`:295`, #2027) reports it NESTED as `{error:{code,message}}`.
|
|
108
|
+
* Only the nested one was ever read, so a flat failure surfaced neither code nor message.
|
|
109
|
+
*
|
|
110
|
+
* These two are NOT interchangeable, which is why this helper reads only one of them:
|
|
111
|
+
*
|
|
112
|
+
* - The nested envelope is built only under `status === 'failed'` (`routes/screenshots.ts:267`), so
|
|
113
|
+
* it is always terminal.
|
|
114
|
+
* - The flat columns are ALSO written by the non-terminal retry path — `job-queue.ts:346-356` sets
|
|
115
|
+
* `error_code`/`error_message` and `status = 'pending'` in one UPDATE, and `claim()` (`:276-284`)
|
|
116
|
+
* carries them into the next `processing` attempt. On a pollable status they describe the attempt
|
|
117
|
+
* that just failed, not the job's outcome (#2792 B1).
|
|
118
|
+
*
|
|
119
|
+
* So the caller consults this only once the status is no longer pollable.
|
|
120
|
+
*/
|
|
121
|
+
function flatScreenshotFailure(obj) {
|
|
122
|
+
const detail = {};
|
|
123
|
+
if (typeof obj.error_code === 'string' && obj.error_code.length > 0)
|
|
124
|
+
detail.code = obj.error_code;
|
|
125
|
+
if (typeof obj.error_message === 'string' && obj.error_message.length > 0)
|
|
126
|
+
detail.message = obj.error_message;
|
|
127
|
+
return detail.code === undefined && detail.message === undefined ? undefined : detail;
|
|
128
|
+
}
|
|
44
129
|
/**
|
|
45
130
|
* Extract a non-empty string `image_url` from a screenshot envelope, or throw an actionable error.
|
|
46
131
|
*
|
|
47
|
-
* A pending/processing envelope has no `image_url` yet; a terminal-`failed` screenshot
|
|
48
|
-
*
|
|
49
|
-
*
|
|
132
|
+
* A pending/processing envelope has no `image_url` yet; a terminal-`failed` screenshot reports its
|
|
133
|
+
* failure either nested or flat. Either way the artifact was never materialized, so we throw rather
|
|
134
|
+
* than fetching `undefined` (#2480 E5).
|
|
135
|
+
*
|
|
136
|
+
* The NESTED shape's message is untouched by #2792 — the ticket's Specification pins it as an
|
|
137
|
+
* inverse case ("the nested-error failed shape keeps its current exit-1 message"), and it is now
|
|
138
|
+
* pinned by an exact-equality test rather than the substring assertions that would not have
|
|
139
|
+
* noticed. The FLAT shape, which previously reported nothing at all, names both of its fields as
|
|
140
|
+
* `<message> (<code>)` — `extractRawBody`'s format for a failed scrape, so the two poll commands
|
|
141
|
+
* read alike — degrading to whichever field exists.
|
|
50
142
|
*/
|
|
51
143
|
function extractImageUrl(result) {
|
|
52
144
|
if (typeof result === 'object' && result !== null) {
|
|
@@ -58,8 +150,28 @@ function extractImageUrl(result) {
|
|
|
58
150
|
return obj.image_url;
|
|
59
151
|
}
|
|
60
152
|
const status = typeof obj.status === 'string' ? obj.status : 'unknown';
|
|
61
|
-
|
|
153
|
+
// "not ready yet" is reserved for a genuinely pollable state (#2792). It is still reachable
|
|
154
|
+
// here for a pending/processing envelope carrying no `ss_` id — `notReadyScreenshot` rejects
|
|
155
|
+
// those for want of a handle, and this message stays their wording. Reached BEFORE the flat
|
|
156
|
+
// failure below, because on a pollable status those columns describe the attempt that just
|
|
157
|
+
// failed and is being retried, not the job's outcome (#2792 B1).
|
|
158
|
+
if (POLLABLE_STATUSES.includes(status)) {
|
|
159
|
+
throw new Error(`not ready yet: screenshot is ${status}`);
|
|
160
|
+
}
|
|
161
|
+
const flat = flatScreenshotFailure(obj);
|
|
162
|
+
if (flat !== undefined) {
|
|
163
|
+
const reason = flat.message !== undefined && flat.code !== undefined
|
|
164
|
+
? `${flat.message} (${flat.code})`
|
|
165
|
+
: (flat.message ?? flat.code ?? 'unknown error');
|
|
166
|
+
throw new Error(`screenshot failed: ${reason}`);
|
|
167
|
+
}
|
|
168
|
+
// Every other artifact-less state says so rather than implying the artifact soon will exist: a
|
|
169
|
+
// `completed` row with a null `image_url`, and any status this build does not know.
|
|
170
|
+
throw new Error(`no image artifact available: screenshot is ${status}`);
|
|
62
171
|
}
|
|
172
|
+
// The non-object arm keeps its original wording, pinned by #2480 cr-5
|
|
173
|
+
// (`screenshot-download.test.ts:287`): this is not a screenshot envelope at all, so neither the
|
|
174
|
+
// status gate above nor its vocabulary applies.
|
|
63
175
|
throw new Error('not ready yet: screenshot is unknown');
|
|
64
176
|
}
|
|
65
177
|
/**
|
|
@@ -89,6 +201,25 @@ async function emitScreenshot(result, opts) {
|
|
|
89
201
|
return;
|
|
90
202
|
}
|
|
91
203
|
if (opts.output !== undefined) {
|
|
204
|
+
// A not-ready poll has no artifact to download — but the job WAS accepted and billed, so
|
|
205
|
+
// losing the handle behind a generic exit 1 is the defect (#2686 item 1). Mirror
|
|
206
|
+
// `scrape get --raw` on a queued envelope EXACTLY: the bare id on STDOUT (so
|
|
207
|
+
// `ss=$(bytekit screenshots get … -o -)` still captures a usable handle), a human note on
|
|
208
|
+
// STDERR, and QUEUED_EXIT_CODE. The `-o` file is deliberately never written — there is no
|
|
209
|
+
// image yet, and a file holding a bare id would masquerade as an artifact.
|
|
210
|
+
//
|
|
211
|
+
// Soft `process.exitCode`, never `process.exit`: a hard exit runs before Node drains an async
|
|
212
|
+
// piped stdout and would discard the very id this branch exists to deliver (#2544). Execution
|
|
213
|
+
// therefore continues past the assignment, so this branch must `return` explicitly — falling
|
|
214
|
+
// through to `extractImageUrl` would throw `not ready yet` and clobber the signal with exit 1.
|
|
215
|
+
const notReady = notReadyScreenshot(result);
|
|
216
|
+
if (notReady !== undefined) {
|
|
217
|
+
console.error(`Note: screenshot ${notReady.id} is ${notReady.status} — no image artifact is available ` +
|
|
218
|
+
`yet. Poll it with: bytekit screenshots get ${notReady.id}`);
|
|
219
|
+
process.stdout.write(`${notReady.id}\n`);
|
|
220
|
+
process.exitCode = QUEUED_EXIT_CODE;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
92
223
|
await downloadArtifact(extractImageUrl(result), opts.output);
|
|
93
224
|
return;
|
|
94
225
|
}
|
|
@@ -129,7 +260,6 @@ export function makeScreenshotsCommand(getGlobal) {
|
|
|
129
260
|
.option('-o, --output <file>', 'Download the captured image to this file path (use - for stdout). Not valid with --json.')
|
|
130
261
|
.action(async (opts) => {
|
|
131
262
|
const g = getGlobal();
|
|
132
|
-
const client = buildClient(g);
|
|
133
263
|
const body = { url: opts.url };
|
|
134
264
|
// Each optional flag maps 1:1 to its snake_case ScreenshotRequest field, written ONLY when
|
|
135
265
|
// present — a boolean never serializes `false` (#2480 E1).
|
|
@@ -180,6 +310,8 @@ export function makeScreenshotsCommand(getGlobal) {
|
|
|
180
310
|
if (opts.metadata !== undefined)
|
|
181
311
|
body['metadata'] = opts.metadata;
|
|
182
312
|
try {
|
|
313
|
+
// 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.
|
|
314
|
+
const client = buildClient(g);
|
|
183
315
|
// Reject --json + -o BEFORE issuing the request: -o downloads BINARY artifact bytes, so
|
|
184
316
|
// it can never coexist with --json (which must stay a text envelope). Thrown here so no
|
|
185
317
|
// request — and therefore no download fetch — is ever issued (#2480 E2).
|
|
@@ -208,8 +340,9 @@ export function makeScreenshotsCommand(getGlobal) {
|
|
|
208
340
|
.option('-o, --output <file>', 'Download the captured image to this file path (use - for stdout). Not valid with --json.')
|
|
209
341
|
.action(async (id, opts) => {
|
|
210
342
|
const g = getGlobal();
|
|
211
|
-
const client = buildClient(g);
|
|
212
343
|
try {
|
|
344
|
+
// 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.
|
|
345
|
+
const client = buildClient(g);
|
|
213
346
|
if (g.json && opts.output !== undefined) {
|
|
214
347
|
throw new Error('-o cannot be used with --json');
|
|
215
348
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -49,7 +49,6 @@ export function makeSearchCommand(getGlobal) {
|
|
|
49
49
|
.option('--date-range <window>', 'Restrict results to a recency window: any (default), hour, day, week, month, or year')
|
|
50
50
|
.action(async (query, opts) => {
|
|
51
51
|
const g = getGlobal();
|
|
52
|
-
const client = buildClient(g);
|
|
53
52
|
// Build SearchOpts from the positional arg plus any provided flags.
|
|
54
53
|
// Omit absent optionals so we never send undefined/NaN over the wire.
|
|
55
54
|
// type/date_range are passed through verbatim; the API validates the
|
|
@@ -66,6 +65,8 @@ export function makeSearchCommand(getGlobal) {
|
|
|
66
65
|
if (opts.dateRange !== undefined)
|
|
67
66
|
body.date_range = opts.dateRange;
|
|
68
67
|
try {
|
|
68
|
+
// 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.
|
|
69
|
+
const client = buildClient(g);
|
|
69
70
|
const result = await client.search.query(body);
|
|
70
71
|
if (g.json) {
|
|
71
72
|
printResult(result, true);
|
package/dist/commands/sitemap.js
CHANGED
|
@@ -24,7 +24,6 @@ export function makeSitemapCommand(getGlobal) {
|
|
|
24
24
|
.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'))
|
|
25
25
|
.action(async (opts) => {
|
|
26
26
|
const g = getGlobal();
|
|
27
|
-
const client = buildClient(g);
|
|
28
27
|
// Omit-when-absent body; the remaining SitemapRequest fields keep their server defaults.
|
|
29
28
|
// The generated request type marks defaulted fields required, so the call needs an explicit
|
|
30
29
|
// assertion — type-level only; the wire body is pinned by request-shapes.test.ts.
|
|
@@ -50,6 +49,8 @@ export function makeSitemapCommand(getGlobal) {
|
|
|
50
49
|
if (opts.metadata !== undefined)
|
|
51
50
|
body['metadata'] = opts.metadata;
|
|
52
51
|
try {
|
|
52
|
+
// 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.
|
|
53
|
+
const client = buildClient(g);
|
|
53
54
|
const result = await client.sitemap.create(body);
|
|
54
55
|
printResult(result, g.json);
|
|
55
56
|
}
|
|
@@ -62,8 +63,9 @@ export function makeSitemapCommand(getGlobal) {
|
|
|
62
63
|
.description('Get a sitemap job by ID')
|
|
63
64
|
.action(async (id) => {
|
|
64
65
|
const g = getGlobal();
|
|
65
|
-
const client = buildClient(g);
|
|
66
66
|
try {
|
|
67
|
+
// 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.
|
|
68
|
+
const client = buildClient(g);
|
|
67
69
|
const result = await client.sitemap.get(id);
|
|
68
70
|
printResult(result, g.json);
|
|
69
71
|
}
|
package/dist/commands/usage.js
CHANGED
|
@@ -16,8 +16,9 @@ export function makeUsageCommand(getGlobal) {
|
|
|
16
16
|
.description('Get current billing-period usage summary')
|
|
17
17
|
.action(async () => {
|
|
18
18
|
const g = getGlobal();
|
|
19
|
-
const client = buildClient(g);
|
|
20
19
|
try {
|
|
20
|
+
// 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.
|
|
21
|
+
const client = buildClient(g);
|
|
21
22
|
const result = await client.usage.get();
|
|
22
23
|
printResult(result, g.json);
|
|
23
24
|
}
|
|
@@ -33,7 +34,6 @@ export function makeUsageCommand(getGlobal) {
|
|
|
33
34
|
.option('--api-key <ak_id>', 'Filter by API key prefixed ID (omitted → all keys)')
|
|
34
35
|
.action(async (opts) => {
|
|
35
36
|
const g = getGlobal();
|
|
36
|
-
const client = buildClient(g);
|
|
37
37
|
const params = {};
|
|
38
38
|
if (opts.from !== undefined)
|
|
39
39
|
params.from = opts.from;
|
|
@@ -42,6 +42,8 @@ export function makeUsageCommand(getGlobal) {
|
|
|
42
42
|
if (opts.apiKey !== undefined)
|
|
43
43
|
params.api_key = opts.apiKey;
|
|
44
44
|
try {
|
|
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);
|
|
45
47
|
const result = Object.keys(params).length > 0
|
|
46
48
|
? await client.usage.daily(params)
|
|
47
49
|
: await client.usage.daily();
|
|
@@ -59,7 +61,6 @@ export function makeUsageCommand(getGlobal) {
|
|
|
59
61
|
.option('--api-key <ak_id>', 'Filter by API key prefixed ID (omitted → all keys)')
|
|
60
62
|
.action(async (opts) => {
|
|
61
63
|
const g = getGlobal();
|
|
62
|
-
const client = buildClient(g);
|
|
63
64
|
const params = {};
|
|
64
65
|
if (opts.from !== undefined)
|
|
65
66
|
params.from = opts.from;
|
|
@@ -68,6 +69,8 @@ export function makeUsageCommand(getGlobal) {
|
|
|
68
69
|
if (opts.apiKey !== undefined)
|
|
69
70
|
params.api_key = opts.apiKey;
|
|
70
71
|
try {
|
|
72
|
+
// 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.
|
|
73
|
+
const client = buildClient(g);
|
|
71
74
|
const result = Object.keys(params).length > 0
|
|
72
75
|
? await client.usage.byEndpoint(params)
|
|
73
76
|
: await client.usage.byEndpoint();
|
|
@@ -21,7 +21,6 @@ export function makeWebhookDeliveriesCommand(getGlobal) {
|
|
|
21
21
|
.option('--since <iso>', 'Only deliveries created after this ISO date-time')
|
|
22
22
|
.action(async (opts) => {
|
|
23
23
|
const g = getGlobal();
|
|
24
|
-
const client = buildClient(g);
|
|
25
24
|
const params = {};
|
|
26
25
|
if (opts.limit !== undefined)
|
|
27
26
|
params.limit = opts.limit;
|
|
@@ -32,6 +31,8 @@ export function makeWebhookDeliveriesCommand(getGlobal) {
|
|
|
32
31
|
if (opts.since !== undefined)
|
|
33
32
|
params.since = opts.since;
|
|
34
33
|
try {
|
|
34
|
+
// 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.
|
|
35
|
+
const client = buildClient(g);
|
|
35
36
|
const result = Object.keys(params).length > 0
|
|
36
37
|
? await client.webhooks.list(params)
|
|
37
38
|
: await client.webhooks.list();
|
|
@@ -46,8 +47,9 @@ export function makeWebhookDeliveriesCommand(getGlobal) {
|
|
|
46
47
|
.description('Requeue a failed or exhausted webhook delivery')
|
|
47
48
|
.action(async (id) => {
|
|
48
49
|
const g = getGlobal();
|
|
49
|
-
const client = buildClient(g);
|
|
50
50
|
try {
|
|
51
|
+
// 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.
|
|
52
|
+
const client = buildClient(g);
|
|
51
53
|
const result = await client.webhooks.retry(id);
|
|
52
54
|
printResult(result, g.json);
|
|
53
55
|
}
|
|
@@ -82,8 +82,9 @@ export async function downloadArtifact(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS
|
|
|
82
82
|
// (mirroring client.ts's corrected 429-drain mechanism, cr-3), reasoning that a full
|
|
83
83
|
// `text()` read lets undici return the connection to the pool. That reasoning does not
|
|
84
84
|
// apply on THIS path: `commands/screenshots.ts` throws this error straight into
|
|
85
|
-
// `handleError`, which
|
|
86
|
-
//
|
|
85
|
+
// `handleError`, which is the last statement of the action's catch — the command then ends
|
|
86
|
+
// (signalling exit 1 via `process.exitCode` since #2686 item 4), so there is no next request
|
|
87
|
+
// in this process for a reclaimed socket to serve. A drain here was bounded only by the FULL
|
|
87
88
|
// per-attempt abort budget (or, under `timeoutMs <= 0`, by nothing at all) for zero benefit,
|
|
88
89
|
// so it is removed outright rather than merely capped.
|
|
89
90
|
throw new Error(`artifact download failed: HTTP ${res.status}`);
|
package/dist/lib/parse-flags.js
CHANGED
|
@@ -69,7 +69,9 @@ export function parseJsonFlag(flag, value, expect) {
|
|
|
69
69
|
parsed = JSON.parse(value);
|
|
70
70
|
}
|
|
71
71
|
catch {
|
|
72
|
-
|
|
72
|
+
// "an", not "a": both shape words (`object`, `array`) begin with a vowel, so a hardcoded
|
|
73
|
+
// article read `(a object)` / `(a array)` (#2686 item 5).
|
|
74
|
+
throw new InvalidArgumentError(`${flag} must be valid JSON (an ${expect}), e.g. '${example}'.`);
|
|
73
75
|
}
|
|
74
76
|
const matches = expect === 'array'
|
|
75
77
|
? Array.isArray(parsed)
|
package/dist/output.d.ts
CHANGED
|
@@ -72,6 +72,20 @@ export declare function resolveOutputMode(flags: OutputFlags): OutputMode;
|
|
|
72
72
|
*
|
|
73
73
|
* Errors — rather than guessing or stringifying — whenever the request is ambiguous or the body is
|
|
74
74
|
* not text, so `--raw` can never print `[object Object]` or a format the caller did not ask for.
|
|
75
|
+
*
|
|
76
|
+
* Three states have no body but are NOT "the response contains no formats" (#2686 item 2), and each
|
|
77
|
+
* gets its own diagnostic ahead of the generic ones, because the caller's next action differs:
|
|
78
|
+
*
|
|
79
|
+
* - a terminal-FAILED scrape (`ScrapeErrorEnvelope`) → the envelope's own `error.code` + message.
|
|
80
|
+
* This is the scrape twin of the screenshots extractor's error-envelope branch (#2027). It is
|
|
81
|
+
* checked FIRST so a failure envelope that also carries `formats: {}` still reports why it failed
|
|
82
|
+
* rather than a format-selection problem the caller cannot act on.
|
|
83
|
+
* - a completed scrape with an EMPTY `formats: {}` bag and a warning explaining it (the #2675
|
|
84
|
+
* `artifact_unavailable` shape) → the warning's code and message.
|
|
85
|
+
* - an empty bag with no warning → says the bag is empty. It is not "no formats" (the key IS
|
|
86
|
+
* there), and `--format` cannot help, so the multi-format diagnostic would be a false lead.
|
|
87
|
+
*
|
|
88
|
+
* A queued envelope never reaches here — {@link emitResult} diverts it to {@link emitQueuedRaw}.
|
|
75
89
|
*/
|
|
76
90
|
export declare function extractRawBody(response: unknown, requestedFormat?: string): string;
|
|
77
91
|
/**
|
|
@@ -133,6 +147,10 @@ export declare function emitResult(result: unknown, mode: OutputMode, target: Ou
|
|
|
133
147
|
/**
|
|
134
148
|
* Collect the `X-Fetch-*` response headers of a {@link FetchResult} into a Title-Cased object —
|
|
135
149
|
* the documented `--json` payload for a fetch command.
|
|
150
|
+
*
|
|
151
|
+
* Key names are the SPEC names: {@link FETCH_HEADER_CASING} first, {@link titleCaseHeader}
|
|
152
|
+
* otherwise. `fetch-metadata-keys.test.ts` pins the emitted set against the spec's own exhaustive
|
|
153
|
+
* header list. Values are passed through verbatim, and non-`X-Fetch-*` headers stay filtered out.
|
|
136
154
|
*/
|
|
137
155
|
export declare function collectFetchMetadata(headers: Headers): Record<string, string>;
|
|
138
156
|
/**
|
|
@@ -158,7 +176,20 @@ export declare function emitFetchResult(result: unknown, jsonMode: boolean, targ
|
|
|
158
176
|
*/
|
|
159
177
|
export declare function printResult(data: unknown, jsonMode: boolean): void;
|
|
160
178
|
/**
|
|
161
|
-
* Handle a CLI command error: print to stderr and exit 1.
|
|
179
|
+
* Handle a CLI command error: print to stderr and signal exit 1.
|
|
180
|
+
*
|
|
181
|
+
* The signal is a SOFT `process.exitCode`, never a hard `process.exit` (#2686 item 4, restoring the
|
|
182
|
+
* #2544 policy this site contradicted). Node documents `process.stderr` as ASYNCHRONOUS when it
|
|
183
|
+
* points at a pipe: a write that cannot be handed to the kernel in one go is queued in userspace
|
|
184
|
+
* and drained on a later event-loop turn, and `process.exit()` runs before any of those turns and
|
|
185
|
+
* DISCARDS the queue. The one line a script greps was therefore the line most at risk of being
|
|
186
|
+
* thrown away. Setting `process.exitCode` lets the process end naturally once stderr has drained,
|
|
187
|
+
* with the same observable exit code.
|
|
188
|
+
*
|
|
189
|
+
* Returns `void` (not `never`): the hard exit used to provide an implicit stop, so every caller
|
|
190
|
+
* must be the LAST statement of its `catch` block, or `return` immediately after (#2686 E4). Every
|
|
191
|
+
* call site in this package is the final statement of an action's `catch`, so the action returns
|
|
192
|
+
* and nothing further runs.
|
|
162
193
|
*
|
|
163
194
|
* The message always begins `Error: ` (a prefix scripts grep on) followed by the error message and
|
|
164
195
|
* an optional parenthesised suffix built from the two diagnostic axes the ByteKit errors carry:
|
|
@@ -174,5 +205,5 @@ export declare function printResult(data: unknown, jsonMode: boolean): void;
|
|
|
174
205
|
* status is present but no code (a hand-built error), the suffix is `(status: <n>)` with no empty
|
|
175
206
|
* placeholder — never `(, status: <n>)`.
|
|
176
207
|
*/
|
|
177
|
-
export declare function handleError(err: unknown):
|
|
208
|
+
export declare function handleError(err: unknown): void;
|
|
178
209
|
export {};
|
package/dist/output.js
CHANGED
|
@@ -58,13 +58,77 @@ export function resolveOutputMode(flags) {
|
|
|
58
58
|
return { kind: 'raw' };
|
|
59
59
|
return { kind: 'human' };
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The `error` of a spec `ScrapeErrorEnvelope` (`{status:'failed', id, error:{code, message, …}}`),
|
|
63
|
+
* or undefined when the response is not a failure envelope.
|
|
64
|
+
*
|
|
65
|
+
* Keyed on the presence of a well-formed `error` object rather than on `status === 'failed'`: the
|
|
66
|
+
* envelope is what a 200 GET returns for a terminally-failed job, and a producer that ships the
|
|
67
|
+
* error without the discriminator still owes the caller its reason.
|
|
68
|
+
*/
|
|
69
|
+
function scrapeErrorDetail(response) {
|
|
70
|
+
if (typeof response !== 'object' || response === null)
|
|
71
|
+
return undefined;
|
|
72
|
+
const err = response.error;
|
|
73
|
+
if (typeof err !== 'object' || err === null || Array.isArray(err))
|
|
74
|
+
return undefined;
|
|
75
|
+
const { code, message } = err;
|
|
76
|
+
const detail = {};
|
|
77
|
+
if (typeof code === 'string' && code.length > 0)
|
|
78
|
+
detail.code = code;
|
|
79
|
+
if (typeof message === 'string' && message.length > 0)
|
|
80
|
+
detail.message = message;
|
|
81
|
+
return detail;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The warning that best explains an empty `formats` bag — `artifact_unavailable` when present
|
|
85
|
+
* (the #2675 shape: a completed scrape whose persisted artifact could not be rehydrated),
|
|
86
|
+
* otherwise the first warning the envelope carries. Undefined when there are none.
|
|
87
|
+
*/
|
|
88
|
+
function emptyFormatsWarning(response) {
|
|
89
|
+
if (typeof response !== 'object' || response === null)
|
|
90
|
+
return undefined;
|
|
91
|
+
const raw = response.warnings;
|
|
92
|
+
if (!Array.isArray(raw))
|
|
93
|
+
return undefined;
|
|
94
|
+
const parsed = raw.flatMap((w) => {
|
|
95
|
+
if (typeof w !== 'object' || w === null)
|
|
96
|
+
return [];
|
|
97
|
+
const { code, message } = w;
|
|
98
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
99
|
+
return [];
|
|
100
|
+
return typeof message === 'string' && message.length > 0 ? [{ code, message }] : [{ code }];
|
|
101
|
+
});
|
|
102
|
+
return parsed.find((w) => w.code === 'artifact_unavailable') ?? parsed[0];
|
|
103
|
+
}
|
|
61
104
|
/**
|
|
62
105
|
* Narrow an SDK response (typed `unknown` today) down to the single format body to emit raw.
|
|
63
106
|
*
|
|
64
107
|
* Errors — rather than guessing or stringifying — whenever the request is ambiguous or the body is
|
|
65
108
|
* not text, so `--raw` can never print `[object Object]` or a format the caller did not ask for.
|
|
109
|
+
*
|
|
110
|
+
* Three states have no body but are NOT "the response contains no formats" (#2686 item 2), and each
|
|
111
|
+
* gets its own diagnostic ahead of the generic ones, because the caller's next action differs:
|
|
112
|
+
*
|
|
113
|
+
* - a terminal-FAILED scrape (`ScrapeErrorEnvelope`) → the envelope's own `error.code` + message.
|
|
114
|
+
* This is the scrape twin of the screenshots extractor's error-envelope branch (#2027). It is
|
|
115
|
+
* checked FIRST so a failure envelope that also carries `formats: {}` still reports why it failed
|
|
116
|
+
* rather than a format-selection problem the caller cannot act on.
|
|
117
|
+
* - a completed scrape with an EMPTY `formats: {}` bag and a warning explaining it (the #2675
|
|
118
|
+
* `artifact_unavailable` shape) → the warning's code and message.
|
|
119
|
+
* - an empty bag with no warning → says the bag is empty. It is not "no formats" (the key IS
|
|
120
|
+
* there), and `--format` cannot help, so the multi-format diagnostic would be a false lead.
|
|
121
|
+
*
|
|
122
|
+
* A queued envelope never reaches here — {@link emitResult} diverts it to {@link emitQueuedRaw}.
|
|
66
123
|
*/
|
|
67
124
|
export function extractRawBody(response, requestedFormat) {
|
|
125
|
+
const failure = scrapeErrorDetail(response);
|
|
126
|
+
if (failure !== undefined) {
|
|
127
|
+
const reason = failure.message !== undefined && failure.code !== undefined
|
|
128
|
+
? `${failure.message} (${failure.code})`
|
|
129
|
+
: (failure.message ?? failure.code ?? 'unknown error');
|
|
130
|
+
throw new Error(`--raw: scrape failed: ${reason}`);
|
|
131
|
+
}
|
|
68
132
|
const formats = typeof response === 'object' && response !== null
|
|
69
133
|
? response.formats
|
|
70
134
|
: undefined;
|
|
@@ -73,6 +137,13 @@ export function extractRawBody(response, requestedFormat) {
|
|
|
73
137
|
}
|
|
74
138
|
const bag = formats;
|
|
75
139
|
const keys = Object.keys(bag);
|
|
140
|
+
if (keys.length === 0) {
|
|
141
|
+
const warning = emptyFormatsWarning(response);
|
|
142
|
+
throw new Error(warning === undefined
|
|
143
|
+
? '--raw: the response carries an empty formats map — no body to emit'
|
|
144
|
+
: `--raw: the response carries an empty formats map — ${warning.code}` +
|
|
145
|
+
(warning.message !== undefined ? `: ${warning.message}` : ''));
|
|
146
|
+
}
|
|
76
147
|
let key;
|
|
77
148
|
if (requestedFormat !== undefined) {
|
|
78
149
|
key = requestedFormat;
|
|
@@ -197,15 +268,39 @@ function titleCaseHeader(name) {
|
|
|
197
268
|
.map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1).toLowerCase())
|
|
198
269
|
.join('-');
|
|
199
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Spec header names that segment-wise Title-Casing cannot produce (#2686 item 3).
|
|
273
|
+
*
|
|
274
|
+
* `titleCaseHeader` lowercases every non-initial character of a segment, so `x-fetch-id` came out
|
|
275
|
+
* `X-Fetch-Id` and `x-fetch-url` came out `X-Fetch-Url` — while `docs/api/openapi.yaml` declares
|
|
276
|
+
* `X-Fetch-ID` and `X-Fetch-URL` (the gateway's CORS expose list, `lib/cors-config.ts`, mirrors the
|
|
277
|
+
* same spellings). A `jq '."X-Fetch-ID"'` filter written from the API reference therefore got
|
|
278
|
+
* `null` off `bytekit fetch … --json`.
|
|
279
|
+
*
|
|
280
|
+
* The exception lives HERE, in a lookup keyed by the lowercase wire name, and deliberately NOT
|
|
281
|
+
* inside `titleCaseHeader` (#2686 E3). That helper is a generic contract — an acronym rule baked
|
|
282
|
+
* into it would silently re-case names for any future caller, and would have to guess at segments
|
|
283
|
+
* like `Waf` (which the spec really does spell `X-Fetch-Waf-Vendor`, not `WAF`). Every header
|
|
284
|
+
* outside this map keeps the generic Title-Cased spelling it has always had.
|
|
285
|
+
*/
|
|
286
|
+
const FETCH_HEADER_CASING = {
|
|
287
|
+
'x-fetch-id': 'X-Fetch-ID',
|
|
288
|
+
'x-fetch-url': 'X-Fetch-URL',
|
|
289
|
+
};
|
|
200
290
|
/**
|
|
201
291
|
* Collect the `X-Fetch-*` response headers of a {@link FetchResult} into a Title-Cased object —
|
|
202
292
|
* the documented `--json` payload for a fetch command.
|
|
293
|
+
*
|
|
294
|
+
* Key names are the SPEC names: {@link FETCH_HEADER_CASING} first, {@link titleCaseHeader}
|
|
295
|
+
* otherwise. `fetch-metadata-keys.test.ts` pins the emitted set against the spec's own exhaustive
|
|
296
|
+
* header list. Values are passed through verbatim, and non-`X-Fetch-*` headers stay filtered out.
|
|
203
297
|
*/
|
|
204
298
|
export function collectFetchMetadata(headers) {
|
|
205
299
|
const meta = {};
|
|
206
300
|
headers.forEach((value, key) => {
|
|
207
|
-
|
|
208
|
-
|
|
301
|
+
const lower = key.toLowerCase();
|
|
302
|
+
if (lower.startsWith('x-fetch-')) {
|
|
303
|
+
meta[FETCH_HEADER_CASING[lower] ?? titleCaseHeader(key)] = value;
|
|
209
304
|
}
|
|
210
305
|
});
|
|
211
306
|
return meta;
|
|
@@ -304,7 +399,20 @@ function errorCode(err) {
|
|
|
304
399
|
return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
|
|
305
400
|
}
|
|
306
401
|
/**
|
|
307
|
-
* Handle a CLI command error: print to stderr and exit 1.
|
|
402
|
+
* Handle a CLI command error: print to stderr and signal exit 1.
|
|
403
|
+
*
|
|
404
|
+
* The signal is a SOFT `process.exitCode`, never a hard `process.exit` (#2686 item 4, restoring the
|
|
405
|
+
* #2544 policy this site contradicted). Node documents `process.stderr` as ASYNCHRONOUS when it
|
|
406
|
+
* points at a pipe: a write that cannot be handed to the kernel in one go is queued in userspace
|
|
407
|
+
* and drained on a later event-loop turn, and `process.exit()` runs before any of those turns and
|
|
408
|
+
* DISCARDS the queue. The one line a script greps was therefore the line most at risk of being
|
|
409
|
+
* thrown away. Setting `process.exitCode` lets the process end naturally once stderr has drained,
|
|
410
|
+
* with the same observable exit code.
|
|
411
|
+
*
|
|
412
|
+
* Returns `void` (not `never`): the hard exit used to provide an implicit stop, so every caller
|
|
413
|
+
* must be the LAST statement of its `catch` block, or `return` immediately after (#2686 E4). Every
|
|
414
|
+
* call site in this package is the final statement of an action's `catch`, so the action returns
|
|
415
|
+
* and nothing further runs.
|
|
308
416
|
*
|
|
309
417
|
* The message always begins `Error: ` (a prefix scripts grep on) followed by the error message and
|
|
310
418
|
* an optional parenthesised suffix built from the two diagnostic axes the ByteKit errors carry:
|
|
@@ -337,5 +445,5 @@ export function handleError(err) {
|
|
|
337
445
|
else {
|
|
338
446
|
console.error(`Error: ${String(err)}`);
|
|
339
447
|
}
|
|
340
|
-
process.
|
|
448
|
+
process.exitCode = 1;
|
|
341
449
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunt-labs/bytekit-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "ByteKit CLI — wraps @hunt-labs/bytekit-sdk as shell subcommands",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"commander": "^12.0.0",
|
|
44
|
-
"@hunt-labs/bytekit-sdk": "^0.
|
|
44
|
+
"@hunt-labs/bytekit-sdk": "^0.6.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc",
|