@hunt-labs/bytekit-cli 0.2.0 → 0.3.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 +123 -0
- package/README.md +56 -19
- package/dist/client.d.ts +18 -5
- package/dist/client.js +140 -12
- package/dist/commands/account.js +49 -0
- package/dist/commands/fetch.d.ts +14 -0
- package/dist/commands/fetch.js +23 -6
- package/dist/commands/monitors.js +26 -3
- package/dist/commands/scrape.js +88 -0
- package/dist/commands/screenshots.d.ts +12 -0
- package/dist/commands/screenshots.js +105 -6
- package/dist/commands/search.js +5 -1
- package/dist/commands/sitemap.js +8 -1
- package/dist/index.js +2 -4
- package/dist/lib/download-artifact.d.ts +13 -0
- package/dist/lib/download-artifact.js +25 -0
- package/dist/lib/fetch-body.d.ts +18 -0
- package/dist/lib/fetch-body.js +6 -0
- package/dist/lib/parse-flags.d.ts +16 -0
- package/dist/lib/parse-flags.js +32 -0
- package/dist/output.d.ts +59 -2
- package/dist/output.js +110 -8
- package/package.json +6 -9
- package/dist/commands/recordings.d.ts +0 -5
- package/dist/commands/recordings.js +0 -36
package/dist/output.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { isFetchBodyResult } from './lib/fetch-body.js';
|
|
2
3
|
/** Text formats whose bodies are strings and can therefore be emitted raw. */
|
|
3
4
|
const TEXT_FORMATS = 'markdown, html, raw_html';
|
|
4
5
|
/**
|
|
@@ -71,18 +72,56 @@ export function extractRawBody(response, requestedFormat) {
|
|
|
71
72
|
}
|
|
72
73
|
return value;
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Exit code for `--raw`/`-o` against a 202 queued (async) scrape.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately NOT 0 (a full raw-body success) and NOT 1 (a usage/API error): the job WAS accepted
|
|
79
|
+
* and billed, there is simply no content body to emit yet. A distinct code lets a caller branch on
|
|
80
|
+
* "queued" without parsing stderr.
|
|
81
|
+
*/
|
|
82
|
+
export const QUEUED_RAW_EXIT_CODE = 2;
|
|
83
|
+
/** Is `result` a 202 queued scrape envelope carrying a job id? */
|
|
84
|
+
function isQueuedEnvelope(result) {
|
|
85
|
+
if (typeof result !== 'object' || result === null)
|
|
86
|
+
return false;
|
|
87
|
+
const obj = result;
|
|
88
|
+
return obj.status === 'queued' && typeof obj.id === 'string' && obj.id.length > 0;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Handle `--raw`/`-o` on a 202 queued envelope.
|
|
92
|
+
*
|
|
93
|
+
* The user asked for a content body that does not exist yet — but the job was accepted, so losing
|
|
94
|
+
* the handle (the old behaviour: throw `--raw: response contains no formats`, exit 1) is the real
|
|
95
|
+
* defect. Instead: emit the `sc_` id to STDOUT so `$(bytekit scrape create … --raw)` still captures
|
|
96
|
+
* a usable handle, print a human note to STDERR explaining it is queued and how to poll, and exit
|
|
97
|
+
* with {@link QUEUED_RAW_EXIT_CODE} — distinguishable from both success and error.
|
|
98
|
+
*/
|
|
99
|
+
export function emitQueuedRaw(env) {
|
|
100
|
+
console.error(`Note: scrape ${env.id} was queued (async) — no content body is available yet. ` +
|
|
101
|
+
`Poll it with: bytekit scrape get ${env.id}`);
|
|
102
|
+
process.stdout.write(`${env.id}\n`);
|
|
103
|
+
process.exit(QUEUED_RAW_EXIT_CODE);
|
|
104
|
+
}
|
|
74
105
|
/**
|
|
75
106
|
* Emit a raw body to the target.
|
|
76
107
|
*
|
|
77
|
-
* Uses `process.stdout.write`, never `console.log`: `console.log` appends a newline,
|
|
78
|
-
* make the stdout bytes and the `--output` file bytes differ.
|
|
108
|
+
* Uses `process.stdout.write`, never `console.log`: `console.log` unconditionally appends a newline,
|
|
109
|
+
* which would make the stdout bytes and the `--output` file bytes differ.
|
|
110
|
+
*
|
|
111
|
+
* Trailing-newline rule (#2452 item 6): a body has no guaranteed trailing newline (a scraped markdown
|
|
112
|
+
* page rarely ends in one), so a naked `--raw` glued the shell prompt onto the last line. When stdout
|
|
113
|
+
* is an interactive TTY we append a single newline (idempotent — never doubling an existing one) so the
|
|
114
|
+
* prompt starts cleanly. Off a TTY (a pipe or `>`/`>>` redirect) we write the body verbatim, preserving
|
|
115
|
+
* the byte-exact invariant that `--raw > file` matches `--output file`. A FILE destination is never
|
|
116
|
+
* touched by this rule at all.
|
|
79
117
|
*/
|
|
80
118
|
export function emitRaw(body, target) {
|
|
81
119
|
if (target.file !== undefined) {
|
|
82
120
|
writeFileSync(target.file, body);
|
|
83
121
|
return;
|
|
84
122
|
}
|
|
85
|
-
process.stdout.
|
|
123
|
+
const appendNewline = process.stdout.isTTY === true && !body.endsWith('\n');
|
|
124
|
+
process.stdout.write(appendNewline ? `${body}\n` : body);
|
|
86
125
|
}
|
|
87
126
|
/**
|
|
88
127
|
* The single output dispatch a command calls — so no command needs `node:fs` and every rendering
|
|
@@ -93,6 +132,14 @@ export function emitRaw(body, target) {
|
|
|
93
132
|
*/
|
|
94
133
|
export function emitResult(result, mode, target, requestedFormat) {
|
|
95
134
|
if (mode.kind === 'raw') {
|
|
135
|
+
// A 202 queued scrape has no format body to print — surface its `sc_` handle instead of
|
|
136
|
+
// throwing away the accepted (and billed) job. Scoped to raw mode only: json/human rendering
|
|
137
|
+
// of a queued envelope stays unchanged (falls through to printResult below). A `-o <file>`
|
|
138
|
+
// target is deliberately ignored here (id always on STDOUT, never written to the file): there
|
|
139
|
+
// is no content body yet, and a file holding a bare `sc_` id would masquerade as content.
|
|
140
|
+
if (isQueuedEnvelope(result)) {
|
|
141
|
+
emitQueuedRaw(result);
|
|
142
|
+
}
|
|
96
143
|
emitRaw(extractRawBody(result, requestedFormat), target);
|
|
97
144
|
return;
|
|
98
145
|
}
|
|
@@ -107,6 +154,28 @@ export function emitResult(result, mode, target, requestedFormat) {
|
|
|
107
154
|
// human — unreachable with a file destination (resolveOutputMode never yields it alongside one).
|
|
108
155
|
printResult(result, false);
|
|
109
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Render a `/v1/fetch` content result.
|
|
159
|
+
*
|
|
160
|
+
* The transport hands back a {@link FetchBodyResult} whose body IS the fetched content and whose
|
|
161
|
+
* metadata are the `X-Fetch-*` headers. Default: write the raw body bytes to stdout verbatim
|
|
162
|
+
* (`process.stdout.write`, never `console.log` — no coercion, no appended newline, binary-safe).
|
|
163
|
+
* With `--json`: print the `X-Fetch-*` metadata as JSON instead of the body.
|
|
164
|
+
*
|
|
165
|
+
* Falls back to {@link printResult} for any result that is not a fetch body (defensive — e.g. a
|
|
166
|
+
* bulk-fetch envelope routed here by mistake), so no path can silently emit nothing.
|
|
167
|
+
*/
|
|
168
|
+
export function emitFetchResult(result, jsonMode) {
|
|
169
|
+
if (isFetchBodyResult(result)) {
|
|
170
|
+
if (jsonMode) {
|
|
171
|
+
console.log(JSON.stringify(result.metadata, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
process.stdout.write(result.body);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
printResult(result, jsonMode);
|
|
178
|
+
}
|
|
110
179
|
/**
|
|
111
180
|
* Print a result to stdout.
|
|
112
181
|
*
|
|
@@ -131,19 +200,52 @@ export function printResult(data, jsonMode) {
|
|
|
131
200
|
}
|
|
132
201
|
console.log(JSON.stringify(data, null, 2));
|
|
133
202
|
}
|
|
203
|
+
/** The SDK error classes whose `code` is a ByteKit diagnostic worth surfacing as a suffix. */
|
|
204
|
+
const BYTEKIT_ERROR_NAMES = new Set(['ByteKitError', 'ByteKitConnectionError']);
|
|
205
|
+
/**
|
|
206
|
+
* A non-empty API/connection error `code`, or undefined when absent.
|
|
207
|
+
*
|
|
208
|
+
* Scoped by `err.name` to the ByteKit taxonomy: plenty of OTHER errors also carry a string
|
|
209
|
+
* `.code` — Node fs/system errors (ENOENT from `parseUrlLines`'s readFileSync, EACCES on a `-o`
|
|
210
|
+
* write) — whose message already embeds the code, so suffixing them would render the redundant
|
|
211
|
+
* `Error: ENOENT: … (ENOENT)`. Only `ByteKitError` / `ByteKitConnectionError` codes qualify.
|
|
212
|
+
*/
|
|
213
|
+
function errorCode(err) {
|
|
214
|
+
if (!BYTEKIT_ERROR_NAMES.has(err.name))
|
|
215
|
+
return undefined;
|
|
216
|
+
const raw = err.code;
|
|
217
|
+
return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
|
|
218
|
+
}
|
|
134
219
|
/**
|
|
135
220
|
* Handle a CLI command error: print to stderr and exit 1.
|
|
221
|
+
*
|
|
222
|
+
* The message always begins `Error: ` (a prefix scripts grep on) followed by the error message and
|
|
223
|
+
* an optional parenthesised suffix built from the two diagnostic axes the ByteKit errors carry:
|
|
224
|
+
*
|
|
225
|
+
* - a `ByteKitError` from the transport carries both a numeric `status` and an API `code`
|
|
226
|
+
* (`rate_limited`, `invalid_url`, …) → `Error: <message> (<code>, status: <n>)`;
|
|
227
|
+
* - a `ByteKitConnectionError` (client-side timeout / abort / socket failure) carries a `code`
|
|
228
|
+
* (`timeout` | `aborted` | `connection`) but NO status → `Error: <message> (<code>)`;
|
|
229
|
+
* - a plain error with neither → `Error: <message>`.
|
|
230
|
+
*
|
|
231
|
+
* The `code` axis is read only off the two ByteKit error classes (see {@link errorCode}) — a plain
|
|
232
|
+
* error carrying an unrelated string `.code` (a Node errno error) gets NO code suffix. When a
|
|
233
|
+
* status is present but no code (a hand-built error), the suffix is `(status: <n>)` with no empty
|
|
234
|
+
* placeholder — never `(, status: <n>)`.
|
|
136
235
|
*/
|
|
137
236
|
export function handleError(err) {
|
|
138
237
|
if (err instanceof Error) {
|
|
139
|
-
const
|
|
238
|
+
const status = err.status;
|
|
239
|
+
const hasStatus = typeof status === 'number';
|
|
240
|
+
const code = errorCode(err);
|
|
241
|
+
let suffix = '';
|
|
140
242
|
if (hasStatus) {
|
|
141
|
-
|
|
142
|
-
console.error(`Error: ${apiErr.message} (status: ${apiErr.status})`);
|
|
243
|
+
suffix = code !== undefined ? ` (${code}, status: ${status})` : ` (status: ${status})`;
|
|
143
244
|
}
|
|
144
|
-
else {
|
|
145
|
-
|
|
245
|
+
else if (code !== undefined) {
|
|
246
|
+
suffix = ` (${code})`;
|
|
146
247
|
}
|
|
248
|
+
console.error(`Error: ${err.message}${suffix}`);
|
|
147
249
|
}
|
|
148
250
|
else {
|
|
149
251
|
console.error(`Error: ${String(err)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunt-labs/bytekit-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "ByteKit CLI — wraps @hunt-labs/bytekit-sdk as shell subcommands",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,19 +32,16 @@
|
|
|
32
32
|
],
|
|
33
33
|
"homepage": "https://bytekit.com/docs",
|
|
34
34
|
"bugs": {
|
|
35
|
-
"email": "support@bytekit.com"
|
|
35
|
+
"email": "support@bytekit.com",
|
|
36
|
+
"url": "https://bytekit.com/contact"
|
|
36
37
|
},
|
|
37
38
|
"files": [
|
|
38
|
-
"dist"
|
|
39
|
+
"dist",
|
|
40
|
+
"CHANGELOG.md"
|
|
39
41
|
],
|
|
40
42
|
"dependencies": {
|
|
41
43
|
"commander": "^12.0.0",
|
|
42
|
-
"@hunt-labs/bytekit-sdk": "^0.
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@types/node": "^22.0.0",
|
|
46
|
-
"typescript": "^5.0.0",
|
|
47
|
-
"vitest": "^1.0.0"
|
|
44
|
+
"@hunt-labs/bytekit-sdk": "^0.3.0"
|
|
48
45
|
},
|
|
49
46
|
"scripts": {
|
|
50
47
|
"build": "tsc",
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { buildClient } from '../client.js';
|
|
3
|
-
import { printResult, handleError } from '../output.js';
|
|
4
|
-
export function makeRecordingsCommand(getGlobal) {
|
|
5
|
-
const cmd = new Command('recordings').description('Recording operations');
|
|
6
|
-
cmd
|
|
7
|
-
.command('create')
|
|
8
|
-
.description('Start a page recording')
|
|
9
|
-
.requiredOption('--url <url>', 'URL to record')
|
|
10
|
-
.action(async (opts) => {
|
|
11
|
-
const g = getGlobal();
|
|
12
|
-
const client = buildClient(g);
|
|
13
|
-
try {
|
|
14
|
-
const result = await client.recordings.create({ url: opts.url });
|
|
15
|
-
printResult(result, g.json);
|
|
16
|
-
}
|
|
17
|
-
catch (err) {
|
|
18
|
-
handleError(err);
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
cmd
|
|
22
|
-
.command('get <id>')
|
|
23
|
-
.description('Get a recording by ID')
|
|
24
|
-
.action(async (id) => {
|
|
25
|
-
const g = getGlobal();
|
|
26
|
-
const client = buildClient(g);
|
|
27
|
-
try {
|
|
28
|
-
const result = await client.recordings.get(id);
|
|
29
|
-
printResult(result, g.json);
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
handleError(err);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
return cmd;
|
|
36
|
-
}
|