@crustjs/openapi 0.1.0 → 0.1.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/README.md +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +219 -58
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Call a CLI's HTTP API and browse its OpenAPI spec from an api command
|
|
4
4
|
|
|
5
|
-
`apiCommand({ spec, baseUrl, auth })` adds `<cli> api`, a `gh api`-style escape hatch: `--list` and `--spec` read the bundled OpenAPI document offline, and `api <path>` sends an authenticated request. It also provides the `apiClient` Context, so your own commands share the same base URL and auth.
|
|
5
|
+
`apiCommand({ spec, baseUrl, auth, paginate })` adds `<cli> api`, a `gh api`-style escape hatch: `--list` and `--spec` read the bundled OpenAPI document offline, and `api <path>` sends an authenticated request and prints the response body, byte for byte to a pipe or file. With the `json()` Extension, `--json` prints each mode's data as JSON on stdout. It also provides the `apiClient` Context, so your own commands share the same base URL and auth.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CommandDefinition, CommandShape, ContextFactory, ContextInstance, ContextSetup, ExtensionFactory } from "@crustjs/core";
|
|
2
|
+
import { JsonValue } from "@crustjs/utils/json";
|
|
2
3
|
interface ApiClientOptions {
|
|
3
4
|
/** URL request paths resolve against, e.g. `https://api.example.com/v1`. */
|
|
4
5
|
readonly baseUrl: string;
|
|
@@ -63,7 +64,10 @@ interface ApiCommandOptions extends ApiClientOptions {
|
|
|
63
64
|
*/
|
|
64
65
|
readonly paginate?: (response: Response) => ApiRequest | undefined | Promise<ApiRequest | undefined>;
|
|
65
66
|
}
|
|
66
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* A non-2xx response to `api <path>`; its body was already printed (to stderr under `--json`).
|
|
69
|
+
* The `json()` Extension prints its `status` as the error's `details`.
|
|
70
|
+
*/
|
|
67
71
|
export declare class ApiResponseError extends Error {
|
|
68
72
|
override name: string;
|
|
69
73
|
/** The HTTP status code. */
|
|
@@ -113,14 +117,16 @@ declare const apiFlags: {
|
|
|
113
117
|
};
|
|
114
118
|
};
|
|
115
119
|
/** The contributed `api` command, spelled out because the exported factory needs a declared type. */
|
|
116
|
-
type ApiCommandDefinition = CommandDefinition<"api", readonly [], CommandShape<readonly [typeof pathArg], typeof apiFlags, {},
|
|
120
|
+
type ApiCommandDefinition = CommandDefinition<"api", readonly [], CommandShape<readonly [typeof pathArg], typeof apiFlags, {}, JsonValue | undefined, {}, readonly JsonValue[] | undefined>, {
|
|
117
121
|
api: ApiClient;
|
|
118
122
|
}>;
|
|
119
123
|
/**
|
|
120
124
|
* Provides the {@link apiClient} Context and contributes `api`: `api --list` and
|
|
121
125
|
* `api --spec <operationId|path>` read the spec (calling its loader, if any); `api <path>` sends
|
|
122
|
-
* a request through the client, prints the response body to stdout
|
|
123
|
-
* {@link ApiResponseError} on a non-2xx
|
|
126
|
+
* a request through the client, prints the response body to stdout (its exact bytes to a pipe or
|
|
127
|
+
* file; as text to a terminal, failing instead for a binary body), and fails with {@link ApiResponseError} on a non-2xx
|
|
128
|
+
* status. Under the `json()` Extension's `--json`, `api` returns its data as the command result instead: the operations, the
|
|
129
|
+
* definition, or the parsed response body; `--paginate` yields each page body as an event.
|
|
124
130
|
*/
|
|
125
131
|
export declare const apiCommand: ExtensionFactory<[options: ApiCommandOptions], {
|
|
126
132
|
api: ApiClient;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CrustError, defineCommand, defineContext, defineExtension, defineExtensionId } from "@crustjs/core";
|
|
2
|
+
import { jsonErrorDetails } from "@crustjs/utils/json";
|
|
2
3
|
// src/client.ts
|
|
3
4
|
/** `path` joined below `baseUrl`, keeping the base's own path; absolute URLs pass through. */
|
|
4
5
|
function resolveUrl(baseUrl, path) {
|
|
@@ -137,7 +138,10 @@ const apiClient = /* @__PURE__ */ defineContext("api").setup(async (input, optio
|
|
|
137
138
|
});
|
|
138
139
|
// src/api-command.ts
|
|
139
140
|
const API = /* @__PURE__ */ defineExtensionId("crust:openapi");
|
|
140
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* A non-2xx response to `api <path>`; its body was already printed (to stderr under `--json`).
|
|
143
|
+
* The `json()` Extension prints its `status` as the error's `details`.
|
|
144
|
+
*/
|
|
141
145
|
var ApiResponseError = class extends Error {
|
|
142
146
|
name = "ApiResponseError";
|
|
143
147
|
/** The HTTP status code. */
|
|
@@ -145,6 +149,7 @@ var ApiResponseError = class extends Error {
|
|
|
145
149
|
constructor(status, message) {
|
|
146
150
|
super(message);
|
|
147
151
|
this.status = status;
|
|
152
|
+
Object.defineProperty(this, jsonErrorDetails, { value: { status } });
|
|
148
153
|
}
|
|
149
154
|
};
|
|
150
155
|
const METHODS = [
|
|
@@ -201,7 +206,8 @@ function operations(spec) {
|
|
|
201
206
|
return typeof operation === "object" && operation !== null ? [{
|
|
202
207
|
method,
|
|
203
208
|
path,
|
|
204
|
-
operation
|
|
209
|
+
operation,
|
|
210
|
+
shared: resolved.parameters
|
|
205
211
|
}] : [];
|
|
206
212
|
});
|
|
207
213
|
});
|
|
@@ -225,44 +231,116 @@ async function loadSpec(spec, signal) {
|
|
|
225
231
|
}
|
|
226
232
|
return loaded;
|
|
227
233
|
}
|
|
228
|
-
/**
|
|
234
|
+
/** Each operation's method, path, and the `operationId` and `summary` the spec has. */
|
|
229
235
|
function listOperations(spec) {
|
|
230
236
|
return operations(spec).map(({ method, path, operation }) => {
|
|
231
237
|
const { operationId, summary } = operation;
|
|
232
|
-
return
|
|
233
|
-
method.toUpperCase(),
|
|
238
|
+
return {
|
|
239
|
+
method: method.toUpperCase(),
|
|
234
240
|
path,
|
|
235
|
-
operationId,
|
|
236
|
-
summary
|
|
237
|
-
|
|
241
|
+
...typeof operationId === "string" && operationId !== "" ? { operationId } : {},
|
|
242
|
+
...typeof summary === "string" && summary !== "" ? { summary } : {}
|
|
243
|
+
};
|
|
238
244
|
});
|
|
239
245
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
function isLocalRef(ref) {
|
|
247
|
+
return typeof ref === "string" && ref.startsWith("#");
|
|
248
|
+
}
|
|
249
|
+
/** `value` with its local `$ref` chain followed, stopping at a cycle or a dangling `$ref`. */
|
|
250
|
+
function deref(spec, value) {
|
|
251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
252
|
+
for (;;) {
|
|
253
|
+
const ref = typeof value === "object" && value !== null ? value.$ref : void 0;
|
|
254
|
+
if (!isLocalRef(ref) || seen.has(ref)) return value;
|
|
255
|
+
seen.add(ref);
|
|
256
|
+
const target = refTarget(spec, ref);
|
|
257
|
+
if (target === void 0) return value;
|
|
258
|
+
value = target;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** The path item's `parameters`, minus those the operation redefines (same `name` and `in`), then the operation's. */
|
|
262
|
+
function mergeParameters(spec, shared, own) {
|
|
263
|
+
const list = (parameters) => Array.isArray(parameters) ? parameters : [];
|
|
264
|
+
const key = (parameter) => {
|
|
265
|
+
const { name, in: location } = deref(spec, parameter) ?? {};
|
|
266
|
+
return typeof name === "string" && typeof location === "string" ? JSON.stringify([location, name]) : void 0;
|
|
267
|
+
};
|
|
268
|
+
const redefined = new Set(list(own).map(key));
|
|
269
|
+
return [...list(shared).filter((parameter) => {
|
|
270
|
+
const id = key(parameter);
|
|
271
|
+
return id === void 0 || !redefined.has(id);
|
|
272
|
+
}), ...list(own)];
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* `value` with local `$ref`s (`#/...`) followed, each target written once: a target used once is
|
|
276
|
+
* inlined, keeping sibling keys; one used more often, or recursively, keeps its `$ref`s and is
|
|
277
|
+
* written once under `$refs`, keyed by the `$ref` text. Inlining every use instead grows
|
|
278
|
+
* exponentially with shared schemas, as in large public specs.
|
|
279
|
+
*/
|
|
280
|
+
function resolveRefs(value, spec) {
|
|
281
|
+
const targets = /* @__PURE__ */ new Map();
|
|
282
|
+
const target = (ref) => {
|
|
283
|
+
if (!targets.has(ref)) targets.set(ref, refTarget(spec, ref));
|
|
284
|
+
return targets.get(ref);
|
|
285
|
+
};
|
|
286
|
+
const uses = /* @__PURE__ */ new Map();
|
|
287
|
+
const count = (node) => {
|
|
288
|
+
if (typeof node !== "object" || node === null) return;
|
|
289
|
+
for (const item of Object.values(node)) count(item);
|
|
290
|
+
const { $ref } = node;
|
|
291
|
+
if (!isLocalRef($ref) || target($ref) === void 0) return;
|
|
292
|
+
const seen = uses.get($ref) ?? 0;
|
|
293
|
+
uses.set($ref, seen + 1);
|
|
294
|
+
if (seen === 0) count(target($ref));
|
|
295
|
+
};
|
|
296
|
+
count(value);
|
|
297
|
+
const refs = {};
|
|
298
|
+
const write = (node) => {
|
|
299
|
+
if (Array.isArray(node)) return node.map(write);
|
|
300
|
+
if (typeof node !== "object" || node === null) return node;
|
|
301
|
+
const { $ref, ...siblings } = node;
|
|
302
|
+
const used = isLocalRef($ref) ? uses.get($ref) ?? 0 : 0;
|
|
303
|
+
if (used === 1) {
|
|
304
|
+
const resolved = write(target($ref));
|
|
249
305
|
return typeof resolved === "object" && resolved !== null && !Array.isArray(resolved) ? {
|
|
250
306
|
...resolved,
|
|
251
|
-
...
|
|
307
|
+
...write(siblings)
|
|
252
308
|
} : resolved;
|
|
253
309
|
}
|
|
254
|
-
|
|
255
|
-
|
|
310
|
+
if (used > 1 && !Object.hasOwn(refs, $ref)) {
|
|
311
|
+
refs[$ref] = void 0;
|
|
312
|
+
refs[$ref] = write(target($ref));
|
|
313
|
+
}
|
|
314
|
+
return Object.fromEntries(Object.entries(node).map(([key, item]) => [key, key === "$ref" ? item : write(item)]));
|
|
315
|
+
};
|
|
316
|
+
const written = write(value);
|
|
317
|
+
return Object.keys(refs).length > 0 ? {
|
|
318
|
+
...written,
|
|
319
|
+
$refs: refs
|
|
320
|
+
} : written;
|
|
256
321
|
}
|
|
257
322
|
/**
|
|
258
|
-
* The path item for a spec path, or the operation with that `operationId
|
|
259
|
-
* local `$ref`s resolved.
|
|
323
|
+
* The path item for a spec path, or the operation with that `operationId` with its method, path,
|
|
324
|
+
* and the path item's shared `parameters`; local `$ref`s resolved by {@link resolveRefs}.
|
|
260
325
|
*/
|
|
261
326
|
function specFor(spec, target) {
|
|
262
327
|
const paths = spec.paths ?? {};
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
328
|
+
if (target.startsWith("/") && Object.hasOwn(paths, target)) return resolveRefs(paths[target], spec);
|
|
329
|
+
const matches = operations(spec).filter(({ operation }) => operation.operationId === target);
|
|
330
|
+
const [match] = matches;
|
|
331
|
+
if (match === void 0) throw new Error(`No operation or path "${target}" in the OpenAPI spec`);
|
|
332
|
+
if (matches.length > 1) {
|
|
333
|
+
const candidates = matches.map(({ method, path }) => `${method.toUpperCase()} ${path}`);
|
|
334
|
+
throw new Error(`The operationId "${target}" is ambiguous in the OpenAPI spec (${candidates.join(", ")}): pass a path instead`);
|
|
335
|
+
}
|
|
336
|
+
const { method, path, operation, shared } = match;
|
|
337
|
+
const parameters = mergeParameters(spec, shared, operation.parameters);
|
|
338
|
+
return resolveRefs({
|
|
339
|
+
method: method.toUpperCase(),
|
|
340
|
+
path,
|
|
341
|
+
...operation,
|
|
342
|
+
...parameters.length > 0 ? { parameters } : {}
|
|
343
|
+
}, spec);
|
|
266
344
|
}
|
|
267
345
|
const JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
268
346
|
/** `key=value`; `true`, `false`, `null`, and JSON numbers become JSON values, like `gh api -F`. */
|
|
@@ -281,6 +359,22 @@ function parseField(field) {
|
|
|
281
359
|
if (!Number.isFinite(number)) throw new CrustError("PARSE", `Invalid -F value for "${field.slice(0, at)}": the number is out of range.`, { flag: "field" });
|
|
282
360
|
return [field.slice(0, at), number];
|
|
283
361
|
}
|
|
362
|
+
/** RFC 9110 `token`, the grammar of a method. */
|
|
363
|
+
const METHOD_TOKEN = /^[!#$%&'*+.^_`|~\w-]+$/;
|
|
364
|
+
/** The method to send for `-X`, upper-cased, failing before any request for one fetch() would not send. */
|
|
365
|
+
function parseMethod(method) {
|
|
366
|
+
if (!METHOD_TOKEN.test(method)) throw new CrustError("PARSE", "Invalid -X value: expected an HTTP method such as GET or PATCH, without spaces.", { flag: "method" });
|
|
367
|
+
const normalized = method.toUpperCase();
|
|
368
|
+
let sent;
|
|
369
|
+
try {
|
|
370
|
+
sent = new Request("http://localhost/", { method: normalized }).method;
|
|
371
|
+
} catch {}
|
|
372
|
+
if (sent !== normalized) {
|
|
373
|
+
const problem = sent === void 0 ? "cannot send it" : `would send it as ${sent}`;
|
|
374
|
+
throw new CrustError("PARSE", `-X ${normalized}: this runtime's fetch() ${problem}.`, { flag: "method" });
|
|
375
|
+
}
|
|
376
|
+
return normalized;
|
|
377
|
+
}
|
|
284
378
|
function parseHeader(header) {
|
|
285
379
|
const at = header.indexOf(":");
|
|
286
380
|
if (at <= 0) throw new CrustError("PARSE", "Invalid -H value: expected \"Name: value\".", { flag: "header" });
|
|
@@ -349,16 +443,23 @@ const apiFlags = {
|
|
|
349
443
|
/**
|
|
350
444
|
* Provides the {@link apiClient} Context and contributes `api`: `api --list` and
|
|
351
445
|
* `api --spec <operationId|path>` read the spec (calling its loader, if any); `api <path>` sends
|
|
352
|
-
* a request through the client, prints the response body to stdout
|
|
353
|
-
* {@link ApiResponseError} on a non-2xx
|
|
446
|
+
* a request through the client, prints the response body to stdout (its exact bytes to a pipe or
|
|
447
|
+
* file; as text to a terminal, failing instead for a binary body), and fails with {@link ApiResponseError} on a non-2xx
|
|
448
|
+
* status. Under the `json()` Extension's `--json`, `api` returns its data as the command result instead: the operations, the
|
|
449
|
+
* definition, or the parsed response body; `--paginate` yields each page body as an event.
|
|
354
450
|
*/
|
|
355
|
-
const apiCommand = /* @__PURE__ */ defineExtension(API).factory((extension, { spec, paginate, ...client }) => extension.provide(apiClient(client)).add(defineCommand("api", { description: "Call the HTTP API, or browse its OpenAPI spec" }, (command) => command.use(apiClient).args(pathArg).flags(apiFlags).action(async ({ args, flags, ctx, signal, stdout }) => {
|
|
451
|
+
const apiCommand = /* @__PURE__ */ defineExtension(API).factory((extension, { spec, paginate, ...client }) => extension.provide(apiClient(client)).add(defineCommand("api", { description: "Call the HTTP API, or browse its OpenAPI spec" }, (command) => command.use(apiClient).args(pathArg).flags(apiFlags).action(async ({ args, flags, ctx, signal, stdout, stderr }) => {
|
|
452
|
+
const json = flags.json === true;
|
|
356
453
|
if (flags.list) {
|
|
357
|
-
|
|
454
|
+
const listed = listOperations(await loadSpec(spec, signal));
|
|
455
|
+
if (json) return listed;
|
|
456
|
+
for (const entry of listed) stdout(Object.values(entry).join(" "));
|
|
358
457
|
return;
|
|
359
458
|
}
|
|
360
459
|
if (flags.spec !== void 0) {
|
|
361
|
-
|
|
460
|
+
const found = specFor(await loadSpec(spec, signal), flags.spec);
|
|
461
|
+
if (json) return found;
|
|
462
|
+
stdout(JSON.stringify(found));
|
|
362
463
|
return;
|
|
363
464
|
}
|
|
364
465
|
if (args.path === void 0) throw new CrustError("VALIDATION", "Missing required argument \"<path>\" (or pass --list or --spec)");
|
|
@@ -372,7 +473,7 @@ const apiCommand = /* @__PURE__ */ defineExtension(API).factory((extension, { sp
|
|
|
372
473
|
}
|
|
373
474
|
const fields = (flags.field ?? []).map(parseField);
|
|
374
475
|
const body = flags.body === void 0 ? void 0 : await readBody(flags.body, signal);
|
|
375
|
-
const method = (flags.method ?? (fields.length > 0 || body !== void 0 ? "POST" : "GET"))
|
|
476
|
+
const method = parseMethod(flags.method ?? (fields.length > 0 || body !== void 0 ? "POST" : "GET"));
|
|
376
477
|
const headers = new Headers((flags.header ?? []).map(parseHeader));
|
|
377
478
|
let path = args.path;
|
|
378
479
|
let requestBody = body;
|
|
@@ -385,36 +486,96 @@ const apiCommand = /* @__PURE__ */ defineExtension(API).factory((extension, { sp
|
|
|
385
486
|
}
|
|
386
487
|
if (requestBody !== void 0 && !headers.has("content-type")) headers.set("content-type", "application/json");
|
|
387
488
|
const api = await ctx.api;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
let response = await api.fetch(request.path, request.init);
|
|
398
|
-
for (let pageNumber = 2;; pageNumber++) {
|
|
399
|
-
const page = flags.paginate ? response.clone() : void 0;
|
|
400
|
-
const text = await response.text();
|
|
401
|
-
if (flags.paginate) stdout(compactJson(text));
|
|
402
|
-
else if (text !== "") stdout(text.replace(/\r?\n$/, ""));
|
|
403
|
-
if (!response.ok) throw new ApiResponseError(response.status, `${request.init?.method ?? "GET"} ${request.path}: HTTP ${response.status} ${response.statusText}`.trimEnd());
|
|
404
|
-
const next = page && paginate ? await paginate(page) : void 0;
|
|
405
|
-
if (next === void 0) return;
|
|
406
|
-
const nextInit = next.init ?? request.init;
|
|
407
|
-
const nextUrl = resolveUrl(client.baseUrl, next.path).href;
|
|
408
|
-
const nextMethod = nextInit?.method ?? "GET";
|
|
409
|
-
if (nextInit === request.init && nextMethod === sentMethod && nextUrl === resolveUrl(client.baseUrl, request.path).href) throw new Error(`--paginate: page ${pageNumber} repeats the previous request (${nextMethod} ${nextUrl})`);
|
|
410
|
-
request = {
|
|
411
|
-
path: next.path,
|
|
412
|
-
init: nextInit
|
|
489
|
+
/** The response and body of each page: just one without --paginate. Fails after a non-2xx page. */
|
|
490
|
+
async function* pages() {
|
|
491
|
+
let request = {
|
|
492
|
+
path,
|
|
493
|
+
init: {
|
|
494
|
+
method,
|
|
495
|
+
headers,
|
|
496
|
+
body: requestBody
|
|
497
|
+
}
|
|
413
498
|
};
|
|
414
|
-
|
|
415
|
-
|
|
499
|
+
let shown = args.path;
|
|
500
|
+
let sentMethod = method;
|
|
501
|
+
let response = await api.fetch(request.path, request.init);
|
|
502
|
+
for (let pageNumber = 2;; pageNumber++) {
|
|
503
|
+
const page = flags.paginate ? response.clone() : void 0;
|
|
504
|
+
yield {
|
|
505
|
+
response,
|
|
506
|
+
bytes: new Uint8Array(await response.arrayBuffer())
|
|
507
|
+
};
|
|
508
|
+
if (!response.ok) throw new ApiResponseError(response.status, `${sentMethod} ${withoutQuery(shown)}: HTTP ${response.status} ${response.statusText}`.trimEnd());
|
|
509
|
+
const next = page && paginate ? await paginate(page) : void 0;
|
|
510
|
+
if (next === void 0) return;
|
|
511
|
+
const nextInit = next.init ?? request.init;
|
|
512
|
+
const nextUrl = resolveUrl(client.baseUrl, next.path).href;
|
|
513
|
+
const nextMethod = nextInit?.method ?? "GET";
|
|
514
|
+
if (nextInit === request.init && nextMethod === sentMethod && nextUrl === resolveUrl(client.baseUrl, request.path).href) throw new Error(`--paginate: page ${pageNumber} repeats the previous request (${nextMethod} ${withoutQuery(nextUrl)})`);
|
|
515
|
+
request = {
|
|
516
|
+
path: next.path,
|
|
517
|
+
init: nextInit
|
|
518
|
+
};
|
|
519
|
+
shown = next.path;
|
|
520
|
+
sentMethod = nextMethod;
|
|
521
|
+
response = await fetchNextPage(api, request.path, request.init);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/** Prints each page's body; under --json, yields a successful page's body as JSON data instead. */
|
|
525
|
+
async function* bodies() {
|
|
526
|
+
const { getAmbientTerminalIO, writeProcessOutput } = await import("@crustjs/utils/terminal");
|
|
527
|
+
let refusal;
|
|
528
|
+
if (flags.paginate) refusal = "--paginate prints only text and JSON bodies";
|
|
529
|
+
else if (json) refusal = "--json prints only text and JSON bodies, so run without it to write the bytes to stdout";
|
|
530
|
+
else if (getAmbientTerminalIO() !== void 0) refusal = "its bytes go only to the process's stdout, not to captured output";
|
|
531
|
+
else if (process.getBuiltinModule("node:tty").isatty(1)) refusal = "redirect or pipe stdout to save it instead of printing it to the terminal";
|
|
532
|
+
for await (const { response, bytes } of pages()) {
|
|
533
|
+
if (refusal === void 0) {
|
|
534
|
+
writeProcessOutput(1, bytes);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const text = bodyText(bytes);
|
|
538
|
+
if (text === void 0) {
|
|
539
|
+
if (response.ok) {
|
|
540
|
+
const type = response.headers.get("content-type") ?? "no Content-Type";
|
|
541
|
+
throw new Error(`api: ${flags.paginate ? "a --paginate page" : "the response"} body is binary (${type}); ${refusal}`);
|
|
542
|
+
}
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (!json) {
|
|
546
|
+
if (flags.paginate) stdout(compactJson(text));
|
|
547
|
+
else if (text !== "") stdout(text.replace(/\r?\n$/, ""));
|
|
548
|
+
} else if (!response.ok) {
|
|
549
|
+
if (text !== "") stderr(text.replace(/\r?\n$/, ""));
|
|
550
|
+
} else if (flags.paginate || text !== "") yield bodyValue(text);
|
|
551
|
+
}
|
|
552
|
+
return void 0;
|
|
416
553
|
}
|
|
554
|
+
if (flags.paginate && json) return bodies();
|
|
555
|
+
let result;
|
|
556
|
+
for await (const value of bodies()) result = value;
|
|
557
|
+
return result;
|
|
417
558
|
}))));
|
|
559
|
+
/** `path` up to its query or fragment, which may hold a credential such as an `-F` field. */
|
|
560
|
+
function withoutQuery(path) {
|
|
561
|
+
return path.replace(/[?#][\s\S]*$/, "");
|
|
562
|
+
}
|
|
563
|
+
/** The body as text, or `undefined` when it is not valid UTF-8. */
|
|
564
|
+
function bodyText(bytes) {
|
|
565
|
+
try {
|
|
566
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
567
|
+
} catch {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/** A body as JSON data: parsed when it is JSON (numbers as doubles), the text itself otherwise. */
|
|
572
|
+
function bodyValue(text) {
|
|
573
|
+
try {
|
|
574
|
+
return JSON.parse(text, (_key, value) => Object.is(value, -0) ? 0 : value);
|
|
575
|
+
} catch {
|
|
576
|
+
return text;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
418
579
|
/** A page body as one NDJSON line: JSON compacted, any other text (even empty) as a JSON string. */
|
|
419
580
|
function compactJson(text) {
|
|
420
581
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/openapi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Call a CLI's HTTP API and browse its OpenAPI spec from an api command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -36,17 +36,18 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@crustjs/utils": "^0.3.
|
|
39
|
+
"@crustjs/utils": "^0.3.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@crustjs/config": "0.0.0",
|
|
43
|
-
"@crustjs/core": "0.6.
|
|
44
|
-
"@crustjs/
|
|
43
|
+
"@crustjs/core": "0.6.1",
|
|
44
|
+
"@crustjs/extensions": "0.6.1",
|
|
45
|
+
"@crustjs/testing": "0.1.8",
|
|
45
46
|
"vite-plus": "1.4.0"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
|
-
"@crustjs/core": "^0.6.
|
|
49
|
-
"typescript": "
|
|
49
|
+
"@crustjs/core": "^0.6.1",
|
|
50
|
+
"typescript": ">=5.9.0"
|
|
50
51
|
},
|
|
51
52
|
"peerDependenciesMeta": {
|
|
52
53
|
"typescript": {
|