@hunt-labs/bytekit-cli 0.8.1 → 0.9.1
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 +75 -2
- package/README.md +3 -22
- package/dist/client.js +15 -1
- package/dist/commands/fetch.d.ts +0 -14
- package/dist/commands/fetch.js +2 -63
- package/dist/commands/scrape.js +1 -7
- package/dist/commands/screenshots.js +2 -8
- package/dist/lib/parse-flags.d.ts +1 -1
- package/dist/lib/parse-flags.js +1 -1
- package/dist/lib/url-lines.d.ts +6 -6
- package/dist/lib/url-lines.js +6 -6
- package/dist/output.d.ts +8 -0
- package/dist/output.js +155 -1
- package/dist/test-helpers/build-scratch.d.ts +23 -0
- package/dist/test-helpers/build-scratch.js +41 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,67 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.9.1] - 2026-08-27
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **A rejected request now names the field that was rejected.** When the API answers
|
|
18
|
+
`422 validation_error`, its envelope carries `details.fieldErrors` — the field the server
|
|
19
|
+
refused and what it will accept. The CLI dropped it twice over: its own transport parser never
|
|
20
|
+
read `error.details` off the envelope (the SDK has read it since `@hunt-labs/bytekit-sdk@0.6.0`),
|
|
21
|
+
and `handleError` rendered only the message, code and status. The result was
|
|
22
|
+
`Error: Invalid request body. (validation_error, status: 422)` and nothing else, in human and
|
|
23
|
+
`--json` modes alike, leaving `curl` as the only way to find out which field was wrong (#3538).
|
|
24
|
+
|
|
25
|
+
```console
|
|
26
|
+
$ bytekit scrape create --url https://example.com --country zzzz
|
|
27
|
+
Error: Invalid request body. (validation_error, status: 422)
|
|
28
|
+
Details:
|
|
29
|
+
fieldErrors.country: String must contain exactly 2 character(s)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The block is rendered on **stderr**, below the existing one-liner, and is **bounded** — at most
|
|
33
|
+
ten lines of at most 200 characters, with a `… and <n> more` line when there are further
|
|
34
|
+
entries — so an oversized or deeply nested `details` can never become a multi-KB dump in a
|
|
35
|
+
terminal. Nothing else changed: the `Error: ` prefix, the `(<code>, status: <n>)` suffix, exit
|
|
36
|
+
code `1` and the silence of stdout are all as before, and an error carrying no `details` (or a
|
|
37
|
+
`details` that is not a JSON object) prints exactly the single line it printed in `0.9.0`.
|
|
38
|
+
`--json` stdout stays byte-for-byte pipe-clean, because every byte of this goes to stderr.
|
|
39
|
+
|
|
40
|
+
The bound covers **depth** as well as size: the renderer descends at most eight array levels into
|
|
41
|
+
a `details` value and elides anything deeper as `…`. The line and width caps are applied after
|
|
42
|
+
that walk has finished, so on their own they could not stop a `details` payload nested thousands
|
|
43
|
+
of arrays deep — a body `JSON.parse` accepts, so a server can send one — from overflowing the
|
|
44
|
+
stack and printing a Node `RangeError` trace in place of the one-line error (#3633). Every
|
|
45
|
+
`details` shape the API sends renders byte-identically: nothing real is more than one array level
|
|
46
|
+
deep.
|
|
47
|
+
|
|
48
|
+
The same bound now covers the **object** arm of that walk. A `details` value that is a JSON object
|
|
49
|
+
was serialized with a bare `JSON.stringify`, which is itself natively recursive and overflowed the
|
|
50
|
+
stack on its own: flattening the key by one level bounds WHICH values are rendered, not how deep
|
|
51
|
+
each one is serialized, so a deeply object-nested payload still printed a `RangeError` trace
|
|
52
|
+
(#3647). Objects nested more than eight levels below the flattened key now elide as `…`, exactly
|
|
53
|
+
as over-deep arrays do. Everything shallower renders byte-identically, both live shapes included.
|
|
54
|
+
|
|
55
|
+
## [0.9.0] - 2026-08-20
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
- **BREAKING: `--cookies` and `--headers` are gone from `scrape create` and `screenshots create`**
|
|
60
|
+
(#3242). The API no longer accepts caller-supplied cookies or request headers on any capture
|
|
61
|
+
endpoint, so passing either flag is now an unknown-option error before any request is issued,
|
|
62
|
+
and a body carrying those keys is rejected by the server with `400 validation_error`. There is
|
|
63
|
+
no deprecation window, feature flag, or compatibility header.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# before
|
|
67
|
+
bytekit scrape create --url https://example.com --cookies '[{"name":"session","value":"abc"}]'
|
|
68
|
+
# after — the flag does not exist
|
|
69
|
+
bytekit scrape create --url https://example.com
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`--webhook-headers` on `monitors` is a different flag and is unaffected.
|
|
73
|
+
|
|
13
74
|
## [0.8.1] - 2026-08-13
|
|
14
75
|
|
|
15
76
|
### Fixed
|
|
@@ -75,6 +136,16 @@ today, see [README.md](./README.md), which remains the authoritative usage guide
|
|
|
75
136
|
npm when the entries above landed, so they ride this same pending release rather than stranding
|
|
76
137
|
it as a never-published version.
|
|
77
138
|
|
|
139
|
+
- **Corrected a false legacy-name claim carried since the rename (#2950).** The `0.8.0`-era note
|
|
140
|
+
below said the prior `rapidcrawl-cli` name "remains installable at its last version but is
|
|
141
|
+
deprecated." Verified 2026-08-13: `rapidcrawl-cli` returns npm `404` ("is not in this
|
|
142
|
+
registry"), and `npm deprecate rapidcrawl-cli@'*'` fails with the same `404` — there is no
|
|
143
|
+
published version left for a deprecation notice to attach to, so the claim never held. No
|
|
144
|
+
registry stub or deprecation was possible to publish (npm has nothing to act on); root
|
|
145
|
+
`CLAUDE.md` carried the same false claim and is corrected alongside this entry. Users of the old
|
|
146
|
+
name should install `@hunt-labs/bytekit-cli` directly. No command, flag, or runtime behavior
|
|
147
|
+
changes.
|
|
148
|
+
|
|
78
149
|
## [0.8.0] - 2026-08-11
|
|
79
150
|
|
|
80
151
|
### Added
|
|
@@ -511,8 +582,10 @@ Additive, lower-risk release — no breaking changes to existing commands.
|
|
|
511
582
|
### Changed
|
|
512
583
|
|
|
513
584
|
- Package renamed to `@hunt-labs/bytekit-cli` (the `@bytekit` npm scope was
|
|
514
|
-
unobtainable); the `bin` name stays `bytekit`. The prior `rapidcrawl-cli` name
|
|
515
|
-
remains installable at its last version but is deprecated
|
|
585
|
+
unobtainable); the `bin` name stays `bytekit`. ~~The prior `rapidcrawl-cli` name
|
|
586
|
+
remains installable at its last version but is deprecated.~~ **Corrected in
|
|
587
|
+
0.8.1 (see below): that claim does not hold — `rapidcrawl-cli` 404s on npm and
|
|
588
|
+
there is no published version left to deprecate.**
|
|
516
589
|
- Consumer call sites propagate the SDK's narrowed, spec-derived request types (#2426).
|
|
517
590
|
- Publish-ready packaging metadata: dropped `main`, unpinned the SDK dependency to
|
|
518
591
|
`workspace:^`, added `engines`, `keywords`, `homepage`, `bugs`, `README.md`,
|
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ exits `0`.
|
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
173
|
bytekit fetch create --url https://example.com
|
|
174
|
-
bytekit fetch get --url https://example.com
|
|
174
|
+
bytekit fetch get --url https://example.com
|
|
175
175
|
bytekit fetch get --url https://example.com/logo.png -o logo.png
|
|
176
176
|
bytekit fetch bulk create --file urls.txt --webhook-url https://your.app/hooks/bytekit
|
|
177
177
|
```
|
|
@@ -193,30 +193,11 @@ Note the acronym casing — `X-Fetch-ID` and `X-Fetch-URL`, not `X-Fetch-Id` /
|
|
|
193
193
|
`X-Fetch-Url`. (Versions up to 0.6.0 emitted the Title-Cased spellings; see the
|
|
194
194
|
CHANGELOG.)
|
|
195
195
|
|
|
196
|
-
Both verbs **require** `--url <url>`.
|
|
197
|
-
(`markdown` or `html`), `--country <cc>`, `--timeout-ms <n>`, `--cache-ttl <ttl>`
|
|
196
|
+
Both verbs **require** `--url <url>`. They accept `--country <cc>`, `--timeout-ms <n>`, `--cache-ttl <ttl>`
|
|
198
197
|
(`0`, `Nh`, or `Nd`), and `-o, --output <file>` — each of those omitted from the
|
|
199
198
|
request when absent.
|
|
200
199
|
|
|
201
|
-
`fetch create`
|
|
202
|
-
`fetch get` does not:
|
|
203
|
-
|
|
204
|
-
| Flag | Effect |
|
|
205
|
-
| ----------------------------- | ---------------------------------------------------------------------------- |
|
|
206
|
-
| `--markdown-mode <mode>` | Markdown extraction mode: `article`, `raw`, `llm`. |
|
|
207
|
-
| `--markdown-query <q>` | BM25 query for relevance-ranked markdown filtering. Omitted → disabled. |
|
|
208
|
-
| `--markdown-links <mode>` | Link rendering: `inline`, `references`, `none`, `text`. |
|
|
209
|
-
| `--markdown-images <mode>` | Image retention: `inline`, `references`, `none`, `text`. |
|
|
210
|
-
| `--with-links-summary` | Append a Links footer to the markdown. |
|
|
211
|
-
| `--with-images-summary` | Append an Images footer to the markdown. |
|
|
212
|
-
| `--markdown-compact` | Collapse excessive whitespace. |
|
|
213
|
-
| `--markdown-filter-images` | Filter low-signal images. |
|
|
214
|
-
| `--markdown-include-media` | Return rich link/image objects and a tables array. |
|
|
215
|
-
| `--markdown-include-warnings` | Include markdown-pipeline and tag-filter warnings. |
|
|
216
|
-
| `--markdown-include-stats` | Include a top-level stats object (chars, tokens, blocks). |
|
|
217
|
-
| `--custom <json>` | User payload echoed on the envelope, as a JSON object, e.g. `'{"job":"1"}'`. |
|
|
218
|
-
|
|
219
|
-
Each is omitted when absent, so a server default is never materialized.
|
|
200
|
+
`fetch create` and `fetch get` send only the raw-fetch options listed above.
|
|
220
201
|
|
|
221
202
|
`fetch bulk create` takes the shared bulk-job flags — see
|
|
222
203
|
[`bulk`](#bulk--bulk-screenshot-jobs).
|
package/dist/client.js
CHANGED
|
@@ -231,15 +231,29 @@ function drainBestEffort(body, abort) {
|
|
|
231
231
|
* envelope shape, so a flat 502 rendered `internal_error` instead of `search_provider_error`).
|
|
232
232
|
*
|
|
233
233
|
* The explicit `!== null` guard on the envelope check matters — `typeof null` is `'object'`.
|
|
234
|
+
*
|
|
235
|
+
* The envelope's structured `details` is mirrored too (#3538). The SDK grew that read in #2685 and
|
|
236
|
+
* this copy did not follow, so a `422 validation_error`'s `fieldErrors` — the one fact naming WHICH
|
|
237
|
+
* field the server rejected — was parsed away before `handleError()` could render it: the same
|
|
238
|
+
* drift class as #2586, in the same function. Only the envelope arm reads it, and only when it is a
|
|
239
|
+
* JSON object: the flat vendor-masked arm has no envelope to read from and must never grow a
|
|
240
|
+
* phantom `details`, and `ByteKitErrorDetails` promises something indexable, so an array or a
|
|
241
|
+
* scalar is dropped rather than surfaced. `error-parser-parity.test.ts` compares this field against
|
|
242
|
+
* the real SDK's parser on a shared corpus.
|
|
234
243
|
*/
|
|
235
244
|
async function toApiError(response) {
|
|
236
245
|
let code = 'internal_error';
|
|
237
246
|
let message = response.statusText;
|
|
247
|
+
let details;
|
|
238
248
|
try {
|
|
239
249
|
const json = (await response.json());
|
|
240
250
|
if (typeof json.error === 'object' && json.error !== null) {
|
|
241
251
|
code = json.error.code ?? code;
|
|
242
252
|
message = json.error.message ?? message;
|
|
253
|
+
const raw = json.error.details;
|
|
254
|
+
if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
|
|
255
|
+
details = raw;
|
|
256
|
+
}
|
|
243
257
|
}
|
|
244
258
|
else if (typeof json.error === 'string' && json.error.length > 0) {
|
|
245
259
|
// The string IS the error code. `message` falls back to it only when the response carried
|
|
@@ -251,7 +265,7 @@ async function toApiError(response) {
|
|
|
251
265
|
catch {
|
|
252
266
|
// leave defaults
|
|
253
267
|
}
|
|
254
|
-
return new ByteKitError(response.status, code, message);
|
|
268
|
+
return new ByteKitError(response.status, code, message, details);
|
|
255
269
|
}
|
|
256
270
|
/**
|
|
257
271
|
* Perform the authenticated round-trip with a single bounded retry on 429, returning whatever the
|
package/dist/commands/fetch.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type GlobalOptions } from '../client.js';
|
|
3
|
-
/** The `/v1/fetch` spec union for `format` — mirrors `FetchOpts['format']` in the SDK. */
|
|
4
|
-
type FetchFormat = 'markdown' | 'html';
|
|
5
|
-
/**
|
|
6
|
-
* Validate `--format` locally, before any HTTP request is issued.
|
|
7
|
-
*
|
|
8
|
-
* The SDK's `FetchOpts.format` is narrowed to the spec union `'markdown' | 'html'`
|
|
9
|
-
* (#2475), so an off-spec value like `pdf` is a compile-time error at the call site.
|
|
10
|
-
* Commander runs an option's argParser during parsing — ahead of the action callback —
|
|
11
|
-
* so rejecting here means the client is never built and no request is issued, and the
|
|
12
|
-
* caller gets an actionable message instead of a server 422 (same convention as
|
|
13
|
-
* `parseLimit` in search.ts).
|
|
14
|
-
*/
|
|
15
|
-
export declare function parseFormat(value: string): FetchFormat;
|
|
16
3
|
export declare function makeFetchCommand(getGlobal: () => GlobalOptions & {
|
|
17
4
|
json: boolean;
|
|
18
5
|
}): Command;
|
|
19
|
-
export {};
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
import { Command
|
|
1
|
+
import { Command } from 'commander';
|
|
2
2
|
import { buildClient } from '../client.js';
|
|
3
3
|
import { printResult, emitFetchResult, handleError } from '../output.js';
|
|
4
4
|
import { parseBulkLines } from '../lib/url-lines.js';
|
|
5
|
-
import { parseIntFlag,
|
|
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'];
|
|
11
|
-
/**
|
|
12
|
-
* Validate `--format` locally, before any HTTP request is issued.
|
|
13
|
-
*
|
|
14
|
-
* The SDK's `FetchOpts.format` is narrowed to the spec union `'markdown' | 'html'`
|
|
15
|
-
* (#2475), so an off-spec value like `pdf` is a compile-time error at the call site.
|
|
16
|
-
* Commander runs an option's argParser during parsing — ahead of the action callback —
|
|
17
|
-
* so rejecting here means the client is never built and no request is issued, and the
|
|
18
|
-
* caller gets an actionable message instead of a server 422 (same convention as
|
|
19
|
-
* `parseLimit` in search.ts).
|
|
20
|
-
*/
|
|
21
|
-
export function parseFormat(value) {
|
|
22
|
-
if (!FETCH_FORMATS.includes(value)) {
|
|
23
|
-
throw new InvalidArgumentError(`--format must be one of: ${FETCH_FORMATS.join(', ')}.`);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
5
|
+
import { parseIntFlag, parseCacheTtl, parseJsonFlag } from '../lib/parse-flags.js';
|
|
27
6
|
export function makeFetchCommand(getGlobal) {
|
|
28
7
|
const cmd = new Command('fetch').description('Fetch URL content operations');
|
|
29
8
|
const bulkCmd = new Command('bulk').description('Bulk fetch operations');
|
|
@@ -82,23 +61,9 @@ export function makeFetchCommand(getGlobal) {
|
|
|
82
61
|
.command('create')
|
|
83
62
|
.description('Fetch URL content (POST)')
|
|
84
63
|
.requiredOption('--url <url>', 'URL to fetch')
|
|
85
|
-
.option('--format <format>', 'Response format (markdown or html)', parseFormat)
|
|
86
64
|
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
87
65
|
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
88
66
|
.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))
|
|
89
|
-
// The markdown surface below is POST-only: FetchGetOpts excludes it, so adding any of it to
|
|
90
|
-
// `get` would be a compile error — the create/get asymmetry is correct by construction.
|
|
91
|
-
.option('--markdown-mode <mode>', `Markdown mode: ${MARKDOWN_MODES.join(', ')}`, (v) => parseEnum('--markdown-mode', MARKDOWN_MODES, v))
|
|
92
|
-
.option('--markdown-query <q>', 'BM25 query string for relevance-ranked markdown filtering (omitted → disabled)')
|
|
93
|
-
.option('--markdown-links <mode>', `Link rendering: ${MARKDOWN_RENDER_MODES.join(', ')} (omitted → server default)`, (v) => parseEnum('--markdown-links', MARKDOWN_RENDER_MODES, v))
|
|
94
|
-
.option('--markdown-images <mode>', `Image retention: ${MARKDOWN_RENDER_MODES.join(', ')} (omitted → server default)`, (v) => parseEnum('--markdown-images', MARKDOWN_RENDER_MODES, v))
|
|
95
|
-
.option('--with-links-summary', 'Append a Links footer to the markdown output (omitted → off)')
|
|
96
|
-
.option('--with-images-summary', 'Append an Images footer to the markdown output (omitted → off)')
|
|
97
|
-
.option('--markdown-compact', 'Collapse excessive whitespace in the markdown output (omitted → off)')
|
|
98
|
-
.option('--markdown-filter-images', 'Filter low-signal images from the markdown output (omitted → off)')
|
|
99
|
-
.option('--markdown-include-media', 'Return rich link/image objects and a tables array (omitted → off)')
|
|
100
|
-
.option('--markdown-include-warnings', 'Include markdown-pipeline and tag-filter warnings (omitted → off)')
|
|
101
|
-
.option('--markdown-include-stats', 'Include a top-level stats object (chars, tokens, blocks) (omitted → off)')
|
|
102
67
|
.option('--custom <json>', `User payload echoed on the envelope as a JSON object, e.g. '{"job":"1"}' (omitted → none)`, (v) => parseJsonFlag('--custom', v, 'object'))
|
|
103
68
|
.option('-o, --output <file>', 'Write the fetched body bytes to the given file path')
|
|
104
69
|
.action(async (opts) => {
|
|
@@ -108,36 +73,12 @@ export function makeFetchCommand(getGlobal) {
|
|
|
108
73
|
// exact same wire output they had as inline spreads; a boolean is written ONLY on
|
|
109
74
|
// `=== true`, so it never serializes `false` (#2480 E1).
|
|
110
75
|
const body = { url: opts.url };
|
|
111
|
-
if (opts.format !== undefined)
|
|
112
|
-
body['format'] = opts.format;
|
|
113
76
|
if (opts.country !== undefined)
|
|
114
77
|
body['country'] = opts.country;
|
|
115
78
|
if (opts.timeoutMs !== undefined)
|
|
116
79
|
body['timeout_ms'] = opts.timeoutMs;
|
|
117
80
|
if (opts.cacheTtl !== undefined)
|
|
118
81
|
body['cache_ttl'] = opts.cacheTtl;
|
|
119
|
-
if (opts.markdownMode !== undefined)
|
|
120
|
-
body['markdown_mode'] = opts.markdownMode;
|
|
121
|
-
if (opts.markdownQuery !== undefined)
|
|
122
|
-
body['markdown_query'] = opts.markdownQuery;
|
|
123
|
-
if (opts.markdownLinks !== undefined)
|
|
124
|
-
body['markdown_links'] = opts.markdownLinks;
|
|
125
|
-
if (opts.markdownImages !== undefined)
|
|
126
|
-
body['markdown_images'] = opts.markdownImages;
|
|
127
|
-
if (opts.withLinksSummary === true)
|
|
128
|
-
body['with_links_summary'] = true;
|
|
129
|
-
if (opts.withImagesSummary === true)
|
|
130
|
-
body['with_images_summary'] = true;
|
|
131
|
-
if (opts.markdownCompact === true)
|
|
132
|
-
body['markdown_compact'] = true;
|
|
133
|
-
if (opts.markdownFilterImages === true)
|
|
134
|
-
body['markdown_filter_images'] = true;
|
|
135
|
-
if (opts.markdownIncludeMedia === true)
|
|
136
|
-
body['markdown_include_media'] = true;
|
|
137
|
-
if (opts.markdownIncludeWarnings === true)
|
|
138
|
-
body['markdown_include_warnings'] = true;
|
|
139
|
-
if (opts.markdownIncludeStats === true)
|
|
140
|
-
body['markdown_include_stats'] = true;
|
|
141
82
|
if (opts.custom !== undefined)
|
|
142
83
|
body['custom'] = opts.custom;
|
|
143
84
|
try {
|
|
@@ -154,7 +95,6 @@ export function makeFetchCommand(getGlobal) {
|
|
|
154
95
|
.command('get')
|
|
155
96
|
.description('Fetch URL content (GET via query params)')
|
|
156
97
|
.requiredOption('--url <url>', 'URL to fetch')
|
|
157
|
-
.option('--format <format>', 'Response format (markdown or html)', parseFormat)
|
|
158
98
|
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
159
99
|
.option('--timeout-ms <n>', 'Per-request timeout in milliseconds', (v) => parseIntFlag('--timeout-ms', v))
|
|
160
100
|
.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))
|
|
@@ -166,7 +106,6 @@ export function makeFetchCommand(getGlobal) {
|
|
|
166
106
|
const client = buildClient(g);
|
|
167
107
|
const result = await client.fetch.get({
|
|
168
108
|
url: opts.url,
|
|
169
|
-
...(opts.format !== undefined ? { format: opts.format } : {}),
|
|
170
109
|
...(opts.country !== undefined ? { country: opts.country } : {}),
|
|
171
110
|
...(opts.timeoutMs !== undefined ? { timeout_ms: opts.timeoutMs } : {}),
|
|
172
111
|
...(opts.cacheTtl !== undefined ? { cache_ttl: opts.cacheTtl } : {}),
|
package/dist/commands/scrape.js
CHANGED
|
@@ -48,7 +48,7 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
48
48
|
.requiredOption('--file <path>', 'Path to a newline-delimited file. Each line is a plain URL, or a JSON object carrying "url" plus per-item config. A line whose only key is "url" behaves exactly as before (sent as a bare URL string); any line with additional keys switches the payload to the per-item shape.')
|
|
49
49
|
.requiredOption('--webhook-url <url>', 'HTTPS webhook URL notified when the job completes')
|
|
50
50
|
.option('--webhook-secret <secret>', 'HMAC secret used to sign the completion webhook (omitted → unsigned)')
|
|
51
|
-
.option('--defaults <json>', 'Defaults applied to every entry, as a JSON object (omitted → none). Unknown keys are rejected by /v1/bulk (422) and ignored by scrape bulk / fetch bulk; fetch bulk additionally rejects browser-only keys (
|
|
51
|
+
.option('--defaults <json>', 'Defaults applied to every entry, as a JSON object (omitted → none). Unknown keys are rejected by /v1/bulk (422) and ignored by scrape bulk / fetch bulk; fetch bulk additionally rejects browser-only keys (wait_until, wait_for_selector, delay_ms) with a 400. `cookies` and `headers` are no longer accepted by any capture endpoint and 400 (#3242).', (v) => parseJsonFlag('--defaults', v, 'object'))
|
|
52
52
|
.option('--metadata <json>', `Arbitrary metadata echoed on the job envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
53
53
|
// /v1/scrape/bulk is the ONE bulk endpoint with a top-level `custom`
|
|
54
54
|
// (validators/scrape-bulk.ts:88). Per-entry `custom` arrives through --file lines instead.
|
|
@@ -115,8 +115,6 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
115
115
|
.option('--markdown-mode <mode>', `Markdown extraction mode: ${MARKDOWN_MODES.join(', ')}`, (v) => parseEnum('--markdown-mode', MARKDOWN_MODES, v))
|
|
116
116
|
.option('--markdown-query <q>', 'BM25 query string for relevance-ranked markdown filtering (omitted → disabled)')
|
|
117
117
|
.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))
|
|
118
|
-
.option('--cookies <json>', `Cookies as a JSON array, e.g. '[{"name":"a","value":"b"}]' (omitted → none)`, (v) => parseJsonFlag('--cookies', v, 'array'))
|
|
119
|
-
.option('--headers <json>', `Extra request headers as a JSON object, e.g. '{"X-A":"b"}' (omitted → none)`, (v) => parseJsonFlag('--headers', v, 'object'))
|
|
120
118
|
.option('--delay-ms <n>', 'Post-load delay in milliseconds before capture', (v) => parseIntFlag('--delay-ms', v))
|
|
121
119
|
.option('--events <csv>', `Webhook events (comma-separated): ${SCRAPE_EVENTS.join(', ')} (omitted → server default)`, (v) => parseCsv(v).map((e) => parseEnum('--events', SCRAPE_EVENTS, e)))
|
|
122
120
|
.option('--token-encoding <enc>', `Tokenizer for --token-budget: ${TOKEN_ENCODINGS.join(', ')} (omitted → server default)`, (v) => parseEnum('--token-encoding', TOKEN_ENCODINGS, v))
|
|
@@ -164,10 +162,6 @@ export function makeScrapeCommand(getGlobal) {
|
|
|
164
162
|
body['markdown_query'] = opts.markdownQuery;
|
|
165
163
|
if (opts.cacheTtl !== undefined)
|
|
166
164
|
body['cache_ttl'] = opts.cacheTtl;
|
|
167
|
-
if (opts.cookies !== undefined)
|
|
168
|
-
body['cookies'] = opts.cookies;
|
|
169
|
-
if (opts.headers !== undefined)
|
|
170
|
-
body['headers'] = opts.headers;
|
|
171
165
|
if (opts.delayMs !== undefined)
|
|
172
166
|
body['delay_ms'] = opts.delayMs;
|
|
173
167
|
if (opts.events !== undefined)
|
|
@@ -346,8 +346,6 @@ export function makeScreenshotsCommand(getGlobal) {
|
|
|
346
346
|
.option('--include-html', 'Include the rendered HTML alongside the screenshot')
|
|
347
347
|
.option('--country <cc>', 'Two-letter country code to route the request through (e.g. us, de)')
|
|
348
348
|
.option('--language <lang>', 'Accept-Language override (e.g. en-US)')
|
|
349
|
-
.option('--headers <json>', `Extra request headers as a JSON object, e.g. '{"X-A":"b"}' (omitted → none)`, (v) => parseJsonFlag('--headers', v, 'object'))
|
|
350
|
-
.option('--cookies <json>', `Cookies as a JSON array of {name,value,domain}, e.g. '[{"name":"a","value":"b","domain":"example.com"}]' (omitted → none)`, (v) => parseJsonFlag('--cookies', v, 'array'))
|
|
351
349
|
.option('--metadata <json>', `Arbitrary metadata echoed on the screenshot envelope, as a JSON object, e.g. '{"run":"7"}' (omitted → none)`, (v) => parseJsonFlag('--metadata', v, 'object'))
|
|
352
350
|
.option('--async', 'Queue the screenshot and return a job envelope immediately (no wait)')
|
|
353
351
|
.option('-o, --output <file>', 'Download the captured image to this file path (use - for stdout). Not valid with --json.')
|
|
@@ -394,12 +392,8 @@ export function makeScreenshotsCommand(getGlobal) {
|
|
|
394
392
|
body['country'] = opts.country;
|
|
395
393
|
if (opts.language !== undefined)
|
|
396
394
|
body['language'] = opts.language;
|
|
397
|
-
// The server defaults
|
|
398
|
-
//
|
|
399
|
-
if (opts.headers !== undefined)
|
|
400
|
-
body['headers'] = opts.headers;
|
|
401
|
-
if (opts.cookies !== undefined)
|
|
402
|
-
body['cookies'] = opts.cookies;
|
|
395
|
+
// The server defaults metadata to {}; writing an empty container on a bare
|
|
396
|
+
// create would materialize that default (#2480 E1).
|
|
403
397
|
if (opts.metadata !== undefined)
|
|
404
398
|
body['metadata'] = opts.metadata;
|
|
405
399
|
try {
|
|
@@ -31,7 +31,7 @@ export declare function parseNumberFlag(flag: string, value: string): number;
|
|
|
31
31
|
export declare function parseCacheTtl(flag: string, value: string): string;
|
|
32
32
|
/**
|
|
33
33
|
* Parse a JSON-object- or JSON-array-valued flag (#2545). The ONLY `JSON.parse` call site for CLI
|
|
34
|
-
* flag values — every JSON-shaped flag (`--
|
|
34
|
+
* flag values — every JSON-shaped flag (`--custom`, `--metadata`, `--defaults`,
|
|
35
35
|
* `--webhook-headers`, `--options`, `--scrape-options`) routes through here, so command modules
|
|
36
36
|
* carry no raw `JSON.parse` (grep-gated). Rejects malformed JSON AND a value whose top-level shape
|
|
37
37
|
* does not match `expect`, with a copy-pasteable example in the message (E5).
|
package/dist/lib/parse-flags.js
CHANGED
|
@@ -57,7 +57,7 @@ export function parseCacheTtl(flag, value) {
|
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
59
|
* Parse a JSON-object- or JSON-array-valued flag (#2545). The ONLY `JSON.parse` call site for CLI
|
|
60
|
-
* flag values — every JSON-shaped flag (`--
|
|
60
|
+
* flag values — every JSON-shaped flag (`--custom`, `--metadata`, `--defaults`,
|
|
61
61
|
* `--webhook-headers`, `--options`, `--scrape-options`) routes through here, so command modules
|
|
62
62
|
* carry no raw `JSON.parse` (grep-gated). Rejects malformed JSON AND a value whose top-level shape
|
|
63
63
|
* does not match `expect`, with a copy-pasteable example in the message (E5).
|
package/dist/lib/url-lines.d.ts
CHANGED
|
@@ -30,12 +30,12 @@ export type BulkLine = string | Record<string, unknown>;
|
|
|
30
30
|
*
|
|
31
31
|
* The rejection message for a malformed line (#2596 cr-1 / E12) names the 1-based PHYSICAL line
|
|
32
32
|
* NUMBER (i.e. the line's own position in the file, counting blank lines) and the line's own KEY
|
|
33
|
-
* NAMES only — never the raw line or any value. A `--file` line is where per-item
|
|
34
|
-
* `
|
|
35
|
-
* leak session tokens and similar secrets to stderr, CI logs, and shell
|
|
36
|
-
* raw line is deliberately never echoed, the line number is the ONLY
|
|
37
|
-
* it must be computed BEFORE blank lines are filtered out, or it
|
|
38
|
-
* entry whenever the file contains blank lines above it (#2596 cr-2).
|
|
33
|
+
* NAMES only — never the raw line or any value. A `--file` line is where auth-bearing per-item
|
|
34
|
+
* `metadata` lives (#2596; per-item `cookies`/`headers` were removed in #3242), so echoing the
|
|
35
|
+
* raw line back would leak session tokens and similar secrets to stderr, CI logs, and shell
|
|
36
|
+
* transcripts. Because the raw line is deliberately never echoed, the line number is the ONLY
|
|
37
|
+
* locator left in the message — it must be computed BEFORE blank lines are filtered out, or it
|
|
38
|
+
* silently mis-locates the bad entry whenever the file contains blank lines above it (#2596 cr-2).
|
|
39
39
|
*/
|
|
40
40
|
export declare function parseBulkLines(file: string): BulkLine[];
|
|
41
41
|
/**
|
package/dist/lib/url-lines.js
CHANGED
|
@@ -24,12 +24,12 @@ import { InvalidArgumentError } from 'commander';
|
|
|
24
24
|
*
|
|
25
25
|
* The rejection message for a malformed line (#2596 cr-1 / E12) names the 1-based PHYSICAL line
|
|
26
26
|
* NUMBER (i.e. the line's own position in the file, counting blank lines) and the line's own KEY
|
|
27
|
-
* NAMES only — never the raw line or any value. A `--file` line is where per-item
|
|
28
|
-
* `
|
|
29
|
-
* leak session tokens and similar secrets to stderr, CI logs, and shell
|
|
30
|
-
* raw line is deliberately never echoed, the line number is the ONLY
|
|
31
|
-
* it must be computed BEFORE blank lines are filtered out, or it
|
|
32
|
-
* entry whenever the file contains blank lines above it (#2596 cr-2).
|
|
27
|
+
* NAMES only — never the raw line or any value. A `--file` line is where auth-bearing per-item
|
|
28
|
+
* `metadata` lives (#2596; per-item `cookies`/`headers` were removed in #3242), so echoing the
|
|
29
|
+
* raw line back would leak session tokens and similar secrets to stderr, CI logs, and shell
|
|
30
|
+
* transcripts. Because the raw line is deliberately never echoed, the line number is the ONLY
|
|
31
|
+
* locator left in the message — it must be computed BEFORE blank lines are filtered out, or it
|
|
32
|
+
* silently mis-locates the bad entry whenever the file contains blank lines above it (#2596 cr-2).
|
|
33
33
|
*/
|
|
34
34
|
export function parseBulkLines(file) {
|
|
35
35
|
return readFileSync(file, 'utf8')
|
package/dist/output.d.ts
CHANGED
|
@@ -204,6 +204,14 @@ export declare function printResult(data: unknown, jsonMode: boolean): void;
|
|
|
204
204
|
* error carrying an unrelated string `.code` (a Node errno error) gets NO code suffix. When a
|
|
205
205
|
* status is present but no code (a hand-built error), the suffix is `(status: <n>)` with no empty
|
|
206
206
|
* placeholder — never `(, status: <n>)`.
|
|
207
|
+
*
|
|
208
|
+
* When the error carries a structured `details` object — which a `422 validation_error` populates
|
|
209
|
+
* with zod's `fieldErrors`, naming WHICH field was rejected and what it accepts — a bounded
|
|
210
|
+
* `Details:` block follows on the SAME stderr write (#3538, see {@link detailBlock}). An error
|
|
211
|
+
* with no renderable `details` is byte-identical to before: one line, same text, same exit code.
|
|
212
|
+
*
|
|
213
|
+
* Everything goes to stderr, so `--json` stdout stays pipe-clean without this function needing to
|
|
214
|
+
* know which output mode is in effect.
|
|
207
215
|
*/
|
|
208
216
|
export declare function handleError(err: unknown): void;
|
|
209
217
|
export {};
|
package/dist/output.js
CHANGED
|
@@ -398,6 +398,151 @@ function errorCode(err) {
|
|
|
398
398
|
const raw = err.code;
|
|
399
399
|
return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
|
|
400
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Human-mode bound on the rendered `details` block (#3538 E1).
|
|
403
|
+
*
|
|
404
|
+
* `details` is typed `unknown` on the wire — the API spec declares no schema for it — so nothing
|
|
405
|
+
* stops it from being arbitrarily large or deeply nested. Ten lines of at most 200 characters caps
|
|
406
|
+
* the block at ~2 KB: wider and longer than every validation envelope the API is observed to send
|
|
407
|
+
* (a handful of fields, one sentence each, so nothing real is ever truncated), and small enough
|
|
408
|
+
* that a pathological payload lands as a readable block rather than a multi-KB JSON dump.
|
|
409
|
+
*/
|
|
410
|
+
const DETAIL_MAX_LINES = 10;
|
|
411
|
+
const DETAIL_MAX_LINE_CHARS = 200;
|
|
412
|
+
/**
|
|
413
|
+
* Bound on the descent inside {@link detailValueText}: the array walk (#3633) and the object
|
|
414
|
+
* serialization in {@link detailJson} (#3647), which are the function's two recursive arms.
|
|
415
|
+
*
|
|
416
|
+
* The two caps above bound the block that is PRINTED; they are applied in {@link detailBlock},
|
|
417
|
+
* after the walk that builds each line has already finished, so neither of them bounds the walk
|
|
418
|
+
* itself. A `details` payload nested a few thousand arrays deep survives `JSON.parse` on the CLI's
|
|
419
|
+
* Node runtime, so a server can deliver one, and the descent overflowed the stack — the CLI dying
|
|
420
|
+
* with a raw `RangeError` trace instead of its documented one-line error.
|
|
421
|
+
*
|
|
422
|
+
* Eight levels is far past anything with meaning to render: the live shapes bottom out at ONE
|
|
423
|
+
* array level below the key {@link detailEntries} flattens, and a field→messages list has nowhere
|
|
424
|
+
* deeper to go. Below the bound the descent stops and emits the ellipsis, so an over-deep value is
|
|
425
|
+
* elided in the same visible way an over-long one is — never dropped, which would render a
|
|
426
|
+
* legitimate list as silence.
|
|
427
|
+
*/
|
|
428
|
+
const DETAIL_MAX_DEPTH = 8;
|
|
429
|
+
/** A JSON object — the only `details` shape that has field→messages structure to render. */
|
|
430
|
+
function isDetailRecord(value) {
|
|
431
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* One detail value as a single line of text.
|
|
435
|
+
*
|
|
436
|
+
* Arrays are the zod `fieldErrors` shape — several messages about one field — and are joined with
|
|
437
|
+
* `; `. An object at this depth is already the SECOND level of nesting (see {@link detailEntries})
|
|
438
|
+
* and is emitted as compact JSON rather than flattened further: past that depth the key path is
|
|
439
|
+
* longer than the value it names.
|
|
440
|
+
*
|
|
441
|
+
* `depth` counts array levels already descended and is bounded by {@link DETAIL_MAX_DEPTH} (#3633);
|
|
442
|
+
* the object arm carries the SAME bound through {@link detailJson} (#3647), and every remaining arm
|
|
443
|
+
* terminates on its own.
|
|
444
|
+
*/
|
|
445
|
+
function detailValueText(value, depth = 0) {
|
|
446
|
+
if (typeof value === 'string')
|
|
447
|
+
return value;
|
|
448
|
+
if (Array.isArray(value)) {
|
|
449
|
+
if (depth >= DETAIL_MAX_DEPTH)
|
|
450
|
+
return '…';
|
|
451
|
+
return value
|
|
452
|
+
.map((item) => detailValueText(item, depth + 1))
|
|
453
|
+
.filter((t) => t !== '')
|
|
454
|
+
.join('; ');
|
|
455
|
+
}
|
|
456
|
+
if (isDetailRecord(value))
|
|
457
|
+
return detailJson(value);
|
|
458
|
+
return String(value);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Compact JSON for an object detail value, bounded to {@link DETAIL_MAX_DEPTH} nesting levels
|
|
462
|
+
* (#3647).
|
|
463
|
+
*
|
|
464
|
+
* #3633 bounded the array descent above; this arm used a bare `JSON.stringify`, which is itself
|
|
465
|
+
* natively recursive and overflowed the stack on its own. The one-level flatten in
|
|
466
|
+
* {@link detailEntries} bounds the WALK, not the SERIALIZATION: `{a: {b: X}}` yields the single
|
|
467
|
+
* entry `a.b` whose value `X` was handed straight to `JSON.stringify`, which then recursed to the
|
|
468
|
+
* bottom of `X` — the same raw `RangeError` in place of the documented one-line error, from the
|
|
469
|
+
* arm next door.
|
|
470
|
+
*
|
|
471
|
+
* The bound is the same eight levels and is applied by a replacer, so everything shallower than it
|
|
472
|
+
* serializes through the untouched native path and renders byte-identically: escaping, key order,
|
|
473
|
+
* dropped `undefined`s, `toJSON`. `ancestors` is the chain of holders the replacer is currently
|
|
474
|
+
* inside — `this` is the holder of each visited key, so popping back to it yields the depth — and
|
|
475
|
+
* only a value that would RECURSE is elided, which is what keeps a leaf sitting at the bound
|
|
476
|
+
* rendering exactly as the array arm's leaf at the same depth does.
|
|
477
|
+
*/
|
|
478
|
+
function detailJson(value) {
|
|
479
|
+
const ancestors = [];
|
|
480
|
+
return JSON.stringify(value, function (_key, entry) {
|
|
481
|
+
while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this)
|
|
482
|
+
ancestors.pop();
|
|
483
|
+
if (ancestors.length >= DETAIL_MAX_DEPTH && typeof entry === 'object' && entry !== null) {
|
|
484
|
+
return '…';
|
|
485
|
+
}
|
|
486
|
+
ancestors.push(entry);
|
|
487
|
+
return entry;
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The `key → value` pairs to render, flattened by exactly ONE level.
|
|
492
|
+
*
|
|
493
|
+
* Two `details` shapes are live (both pinned verbatim from staging by the SDK's parser suite):
|
|
494
|
+
* a 422 `validation_error` carries zod's `flatten()` output — `{formErrors, fieldErrors}` — where
|
|
495
|
+
* the field name lives one level down under `fieldErrors`; a 400 `invalid_url` carries a flat
|
|
496
|
+
* record (`{url, field, reason}`). One level of flattening renders both with a single rule and no
|
|
497
|
+
* per-shape special case: `fieldErrors.formats: …` and `url: …` respectively.
|
|
498
|
+
*/
|
|
499
|
+
function detailEntries(details) {
|
|
500
|
+
const entries = [];
|
|
501
|
+
for (const [key, value] of Object.entries(details)) {
|
|
502
|
+
if (isDetailRecord(value)) {
|
|
503
|
+
for (const [inner, innerValue] of Object.entries(value)) {
|
|
504
|
+
entries.push([`${key}.${inner}`, innerValue]);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
entries.push([key, value]);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return entries;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* The stderr block that renders a `ByteKitError.details` (#3538), or `''` when there is nothing to
|
|
515
|
+
* render.
|
|
516
|
+
*
|
|
517
|
+
* A `422 validation_error` envelope carries `details.fieldErrors` naming the offending field and
|
|
518
|
+
* its allowed values; the CLI printed only `message`/`code`/`status`, so the one fact the caller
|
|
519
|
+
* needed was dropped and reproducing the call with curl was the only recourse.
|
|
520
|
+
*
|
|
521
|
+
* Returns `''` — leaving the rendered line byte-identical to before — whenever `details` is
|
|
522
|
+
* absent, is not a JSON object (the untyped spec field permits a string, an array, `null`), or
|
|
523
|
+
* carries no non-empty value. `formErrors: []` on a passing form is the ordinary case of the last
|
|
524
|
+
* one: an empty bag is not a reason.
|
|
525
|
+
*/
|
|
526
|
+
function detailBlock(details) {
|
|
527
|
+
if (!isDetailRecord(details))
|
|
528
|
+
return '';
|
|
529
|
+
const lines = detailEntries(details).flatMap(([key, value]) => {
|
|
530
|
+
const text = detailValueText(value);
|
|
531
|
+
return text === '' ? [] : [` ${key}: ${text}`];
|
|
532
|
+
});
|
|
533
|
+
if (lines.length === 0)
|
|
534
|
+
return '';
|
|
535
|
+
const shown = lines.length > DETAIL_MAX_LINES
|
|
536
|
+
? [
|
|
537
|
+
...lines.slice(0, DETAIL_MAX_LINES - 1),
|
|
538
|
+
` … and ${lines.length - (DETAIL_MAX_LINES - 1)} more`,
|
|
539
|
+
]
|
|
540
|
+
: lines;
|
|
541
|
+
return [
|
|
542
|
+
'Details:',
|
|
543
|
+
...shown.map((line) => line.length > DETAIL_MAX_LINE_CHARS ? `${line.slice(0, DETAIL_MAX_LINE_CHARS - 1)}…` : line),
|
|
544
|
+
].join('\n');
|
|
545
|
+
}
|
|
401
546
|
/**
|
|
402
547
|
* Handle a CLI command error: print to stderr and signal exit 1.
|
|
403
548
|
*
|
|
@@ -427,6 +572,14 @@ function errorCode(err) {
|
|
|
427
572
|
* error carrying an unrelated string `.code` (a Node errno error) gets NO code suffix. When a
|
|
428
573
|
* status is present but no code (a hand-built error), the suffix is `(status: <n>)` with no empty
|
|
429
574
|
* placeholder — never `(, status: <n>)`.
|
|
575
|
+
*
|
|
576
|
+
* When the error carries a structured `details` object — which a `422 validation_error` populates
|
|
577
|
+
* with zod's `fieldErrors`, naming WHICH field was rejected and what it accepts — a bounded
|
|
578
|
+
* `Details:` block follows on the SAME stderr write (#3538, see {@link detailBlock}). An error
|
|
579
|
+
* with no renderable `details` is byte-identical to before: one line, same text, same exit code.
|
|
580
|
+
*
|
|
581
|
+
* Everything goes to stderr, so `--json` stdout stays pipe-clean without this function needing to
|
|
582
|
+
* know which output mode is in effect.
|
|
430
583
|
*/
|
|
431
584
|
export function handleError(err) {
|
|
432
585
|
if (err instanceof Error) {
|
|
@@ -440,7 +593,8 @@ export function handleError(err) {
|
|
|
440
593
|
else if (code !== undefined) {
|
|
441
594
|
suffix = ` (${code})`;
|
|
442
595
|
}
|
|
443
|
-
|
|
596
|
+
const details = detailBlock(err.details);
|
|
597
|
+
console.error(`Error: ${err.message}${suffix}${details === '' ? '' : `\n${details}`}`);
|
|
444
598
|
}
|
|
445
599
|
else {
|
|
446
600
|
console.error(`Error: ${String(err)}`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a scratch COPY of packages/cli (issue #3270), mirroring
|
|
3
|
+
* `packages/sdk-typescript/src/test-helpers/build-scratch.ts` (issue #2423 cr-2).
|
|
4
|
+
*
|
|
5
|
+
* `entrypoint.test.ts` and `queued-raw-flush.test.ts` both need a built `dist/index.js` and both
|
|
6
|
+
* used to rebuild the shared, workspace `packages/cli/dist` in place whenever it was missing —
|
|
7
|
+
* racing each other's build under vitest's default parallel file execution on a checkout with no
|
|
8
|
+
* prior `pnpm build` (one file's `existsSync` check passes while the other's build is still
|
|
9
|
+
* writing `dist/`).
|
|
10
|
+
*
|
|
11
|
+
* So instead: copy the tracked, on-disk source (never `dist/`, `tsconfig.tsbuildinfo`, or
|
|
12
|
+
* `node_modules` — those are the mutable/heavy bits) into a scratch tree that mirrors the real
|
|
13
|
+
* repo's relative layout (`<scratch>/tsconfig.base.json`, `<scratch>/packages/cli/**`), symlink in
|
|
14
|
+
* the real `node_modules` (read-only from the child process's point of view — nothing here writes
|
|
15
|
+
* back into it), and let the caller run the package's own `npm run build` / `npm pack` scripts
|
|
16
|
+
* against that copy. The real `packages/cli/dist` is never read and never written by either suite.
|
|
17
|
+
*
|
|
18
|
+
* @param pkgRoot packages/cli (the real, workspace-shared package root)
|
|
19
|
+
* @param repoRoot the monorepo root (for tsconfig.base.json, which the scratch tsconfig extends)
|
|
20
|
+
* @param scratchRoot a fresh tmp dir (e.g. from mkdtempSync) to build the scratch tree under
|
|
21
|
+
* @returns the scratch package dir (`<scratchRoot>/packages/cli`)
|
|
22
|
+
*/
|
|
23
|
+
export declare function createBuildScratch(pkgRoot: string, repoRoot: string, scratchRoot: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Test-only fixture, compiled like any other `src` file (not excluded from the tsc build) — same
|
|
2
|
+
// tradeoff sdk-typescript's own copy of this helper documents: a small compiled fixture lands in
|
|
3
|
+
// `dist/test-helpers/`, but excluding it breaks ESLint's typed-lint project-service resolution.
|
|
4
|
+
import { mkdirSync, cpSync, copyFileSync, symlinkSync } from 'node:fs';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Build a scratch COPY of packages/cli (issue #3270), mirroring
|
|
8
|
+
* `packages/sdk-typescript/src/test-helpers/build-scratch.ts` (issue #2423 cr-2).
|
|
9
|
+
*
|
|
10
|
+
* `entrypoint.test.ts` and `queued-raw-flush.test.ts` both need a built `dist/index.js` and both
|
|
11
|
+
* used to rebuild the shared, workspace `packages/cli/dist` in place whenever it was missing —
|
|
12
|
+
* racing each other's build under vitest's default parallel file execution on a checkout with no
|
|
13
|
+
* prior `pnpm build` (one file's `existsSync` check passes while the other's build is still
|
|
14
|
+
* writing `dist/`).
|
|
15
|
+
*
|
|
16
|
+
* So instead: copy the tracked, on-disk source (never `dist/`, `tsconfig.tsbuildinfo`, or
|
|
17
|
+
* `node_modules` — those are the mutable/heavy bits) into a scratch tree that mirrors the real
|
|
18
|
+
* repo's relative layout (`<scratch>/tsconfig.base.json`, `<scratch>/packages/cli/**`), symlink in
|
|
19
|
+
* the real `node_modules` (read-only from the child process's point of view — nothing here writes
|
|
20
|
+
* back into it), and let the caller run the package's own `npm run build` / `npm pack` scripts
|
|
21
|
+
* against that copy. The real `packages/cli/dist` is never read and never written by either suite.
|
|
22
|
+
*
|
|
23
|
+
* @param pkgRoot packages/cli (the real, workspace-shared package root)
|
|
24
|
+
* @param repoRoot the monorepo root (for tsconfig.base.json, which the scratch tsconfig extends)
|
|
25
|
+
* @param scratchRoot a fresh tmp dir (e.g. from mkdtempSync) to build the scratch tree under
|
|
26
|
+
* @returns the scratch package dir (`<scratchRoot>/packages/cli`)
|
|
27
|
+
*/
|
|
28
|
+
export function createBuildScratch(pkgRoot, repoRoot, scratchRoot) {
|
|
29
|
+
const pkgDest = join(scratchRoot, 'packages/cli');
|
|
30
|
+
mkdirSync(join(scratchRoot, 'packages'), { recursive: true });
|
|
31
|
+
copyFileSync(join(repoRoot, 'tsconfig.base.json'), join(scratchRoot, 'tsconfig.base.json'));
|
|
32
|
+
cpSync(pkgRoot, pkgDest, {
|
|
33
|
+
recursive: true,
|
|
34
|
+
filter: (src) => {
|
|
35
|
+
const name = basename(src);
|
|
36
|
+
return name !== 'node_modules' && name !== 'dist' && name !== 'tsconfig.tsbuildinfo';
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
symlinkSync(join(pkgRoot, 'node_modules'), join(pkgDest, 'node_modules'), 'dir');
|
|
40
|
+
return pkgDest;
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunt-labs/bytekit-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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.9.1"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc",
|