@datumos/datum-cli 0.0.0 → 0.589.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/LICENSE +201 -0
- package/NOTICE +8 -0
- package/README.md +37 -0
- package/cli/a2aCommand.js +225 -0
- package/cli/artifactsPublish.js +356 -0
- package/cli/authCommands.js +168 -0
- package/cli/bundleSignature.js +111 -0
- package/cli/commandTree.js +243 -0
- package/cli/credentialProcess.js +54 -0
- package/cli/credentials.js +93 -0
- package/cli/datum.js +814 -0
- package/cli/entraLogin.js +254 -0
- package/cli/help.js +509 -0
- package/cli/invoke.js +427 -0
- package/cli/mcpServe.js +361 -0
- package/cli/outputsDownload.js +126 -0
- package/cli/previewCommand.js +333 -0
- package/cli/satellitePairToken.js +48 -0
- package/cli/secureOrigin.js +37 -0
- package/cli/selfUpdate.js +779 -0
- package/cli/spaceManifestCommand.js +495 -0
- package/cli/spaceManifestDir.js +481 -0
- package/cli/spaceRepoTemplate.js +463 -0
- package/cli/updateCommand.js +184 -0
- package/npm-shrinkwrap.json +1231 -0
- package/openapi/spec.js +51199 -0
- package/package.json +25 -3
- package/spaces/reservedHostLabels.js +66 -0
package/cli/invoke.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a matched CLI command + parsed arguments into an HTTP request and
|
|
3
|
+
* executes it. Pure request-building is separated from I/O so tests cover
|
|
4
|
+
* the full URL/header/body derivation without a server.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { insecureOriginReason } from "./secureOrigin.js";
|
|
11
|
+
/**
|
|
12
|
+
* Neither of the two caps below is about bandwidth. They exist because this
|
|
13
|
+
* CLI is driven by agents as often as by people, and everything a command
|
|
14
|
+
* PRINTS becomes part of the model's context — which is re-sent on every
|
|
15
|
+
* later turn, so a megabyte printed once is paid for many times over
|
|
16
|
+
* (#646). The request bytes themselves are free; only what reaches stdout
|
|
17
|
+
* is not.
|
|
18
|
+
*/
|
|
19
|
+
/** Above this, `--curl` names the body instead of rendering it. */
|
|
20
|
+
export const MAX_CURL_BODY_BYTES = 2 * 1024;
|
|
21
|
+
/** Above this, a pretty-printed JSON response is written to a file. */
|
|
22
|
+
export const MAX_PRINTED_RESPONSE_BYTES = 256 * 1024;
|
|
23
|
+
/** Variant-pinning header understood by the Ring Accelerator. */
|
|
24
|
+
export const X_OA_VARIANT_HEADER = "x-oa-variant";
|
|
25
|
+
export class UsageError extends Error {
|
|
26
|
+
}
|
|
27
|
+
function coerce(flag, raw) {
|
|
28
|
+
switch (flag.type) {
|
|
29
|
+
case "boolean": {
|
|
30
|
+
const v = raw[raw.length - 1];
|
|
31
|
+
if (v === "" || v === undefined || v === "true")
|
|
32
|
+
return true;
|
|
33
|
+
if (v === "false")
|
|
34
|
+
return false;
|
|
35
|
+
throw new UsageError(`--${flag.flag} takes true/false, got "${v}"`);
|
|
36
|
+
}
|
|
37
|
+
case "number":
|
|
38
|
+
case "integer": {
|
|
39
|
+
const v = Number(raw[raw.length - 1]);
|
|
40
|
+
if (Number.isNaN(v)) {
|
|
41
|
+
throw new UsageError(`--${flag.flag} expects a number`);
|
|
42
|
+
}
|
|
43
|
+
return v;
|
|
44
|
+
}
|
|
45
|
+
case "array":
|
|
46
|
+
// Arrays of OBJECTS (e.g. provision's repos: [{owner, repo}]) are
|
|
47
|
+
// expressible per item: a value that looks like JSON is parsed —
|
|
48
|
+
// half-typed JSON errors loudly instead of shipping a string the
|
|
49
|
+
// server will 400 on. Plain values stay strings.
|
|
50
|
+
return raw.map((v) => {
|
|
51
|
+
const t = v.trim();
|
|
52
|
+
if (!t.startsWith("{") && !t.startsWith("["))
|
|
53
|
+
return v;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(t);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new UsageError(`--${flag.flag}: "${v.slice(0, 40)}…" looks like JSON but does not parse`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
case "object": {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(raw[raw.length - 1]);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new UsageError(`--${flag.flag} expects JSON`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
default:
|
|
70
|
+
return raw[raw.length - 1];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Resolves --data: inline JSON, @file, or "-" (stdin already read). */
|
|
74
|
+
export function resolveBodyArg(body, readStdin = () => readFileSync(0, "utf8")) {
|
|
75
|
+
const text = body === "-"
|
|
76
|
+
? readStdin()
|
|
77
|
+
: body.startsWith("@")
|
|
78
|
+
? readFileSync(body.slice(1), "utf8")
|
|
79
|
+
: body;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new UsageError("--data must be valid JSON (inline, @file or - for stdin)");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Builds the request. Enum values are validated here (the server would 400
|
|
89
|
+
* anyway, but a local message with the allowed values is kinder).
|
|
90
|
+
*/
|
|
91
|
+
export function buildRequest(command, args, config) {
|
|
92
|
+
if (args.positionals.length !== command.positionals.length) {
|
|
93
|
+
const expected = command.positionals.map((p) => `<${p.name}>`).join(" ");
|
|
94
|
+
throw new UsageError(`expected ${command.positionals.length} argument${command.positionals.length === 1 ? "" : "s"}` +
|
|
95
|
+
`${expected ? ` (${expected})` : ""}, got ${args.positionals.length} — see: datum ${command.name} --help`);
|
|
96
|
+
}
|
|
97
|
+
// Path params, in spec order.
|
|
98
|
+
let path = command.path;
|
|
99
|
+
command.positionals.forEach((p, i) => {
|
|
100
|
+
path = path.replace(`{${p.name}}`, encodeURIComponent(args.positionals[i]));
|
|
101
|
+
});
|
|
102
|
+
const known = new Map();
|
|
103
|
+
for (const f of [...command.queryFlags, ...command.bodyFlags]) {
|
|
104
|
+
known.set(f.flag, f);
|
|
105
|
+
}
|
|
106
|
+
for (const flag of args.flags.keys()) {
|
|
107
|
+
if (!known.has(flag)) {
|
|
108
|
+
const hint = known.size > 0
|
|
109
|
+
? ` Known flags: ${[...known.keys()].map((f) => `--${f}`).join(", ")}.`
|
|
110
|
+
: command.hasBody
|
|
111
|
+
? " This command takes its input via --data."
|
|
112
|
+
: "";
|
|
113
|
+
throw new UsageError(`unknown flag --${flag}.${hint}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const query = new URLSearchParams();
|
|
117
|
+
const bodyFromFlags = {};
|
|
118
|
+
for (const [flag, raw] of args.flags) {
|
|
119
|
+
const spec = known.get(flag);
|
|
120
|
+
const value = coerce(spec, raw);
|
|
121
|
+
if (spec.enum && !spec.enum.includes(String(value))) {
|
|
122
|
+
throw new UsageError(`--${flag} must be one of: ${spec.enum.join(", ")}`);
|
|
123
|
+
}
|
|
124
|
+
if (spec.source === "query") {
|
|
125
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
126
|
+
query.append(spec.name, String(v));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
bodyFromFlags[spec.name] = value;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const f of [...command.queryFlags, ...command.bodyFlags]) {
|
|
134
|
+
if (f.required && !args.flags.has(f.flag)) {
|
|
135
|
+
// --data may carry required body fields; only query flags are
|
|
136
|
+
// strictly checkable client-side.
|
|
137
|
+
if (f.source === "query" || !args.body) {
|
|
138
|
+
throw new UsageError(`missing required flag --${f.flag} — see: datum ${command.name} --help`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
let body;
|
|
143
|
+
let bodySource;
|
|
144
|
+
if (args.body !== undefined || Object.keys(bodyFromFlags).length > 0) {
|
|
145
|
+
if (!command.hasBody) {
|
|
146
|
+
throw new UsageError(`${command.method} ${command.path} takes no request body`);
|
|
147
|
+
}
|
|
148
|
+
const base = args.body !== undefined ? resolveBodyArg(args.body) : {};
|
|
149
|
+
// The source is only quotable when nothing was merged into it.
|
|
150
|
+
const merged = Object.keys(bodyFromFlags).length > 0;
|
|
151
|
+
bodySource =
|
|
152
|
+
!merged && args.body !== undefined
|
|
153
|
+
? args.body === "-"
|
|
154
|
+
? { kind: "stdin" }
|
|
155
|
+
: args.body.startsWith("@")
|
|
156
|
+
? { kind: "file", path: args.body.slice(1) }
|
|
157
|
+
: { kind: "inline" }
|
|
158
|
+
: { kind: "inline" };
|
|
159
|
+
if (base !== null && typeof base === "object" && !Array.isArray(base)) {
|
|
160
|
+
body = JSON.stringify({ ...base, ...bodyFromFlags });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Non-object bodies pass through untouched (flags can't merge).
|
|
164
|
+
body = JSON.stringify(base);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (command.hasBody && command.bodyRequired) {
|
|
168
|
+
body = JSON.stringify({});
|
|
169
|
+
bodySource = { kind: "inline" };
|
|
170
|
+
}
|
|
171
|
+
const surface = apiSurface(config);
|
|
172
|
+
const qs = query.size > 0 ? `?${query.toString()}` : "";
|
|
173
|
+
const headers = { ...surface.headers };
|
|
174
|
+
if (body !== undefined)
|
|
175
|
+
headers["content-type"] = "application/json";
|
|
176
|
+
return {
|
|
177
|
+
url: `${surface.url(path)}${qs}`,
|
|
178
|
+
method: command.method,
|
|
179
|
+
headers,
|
|
180
|
+
...(body !== undefined ? { body } : {}),
|
|
181
|
+
...(bodySource !== undefined ? { bodySource } : {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Where an API call goes and what credential it carries — the ONE place
|
|
186
|
+
* that decides the surface, so the hand-written commands
|
|
187
|
+
* (`artifacts publish`) address the API exactly like a derived one.
|
|
188
|
+
*
|
|
189
|
+
* The credential decides: an API key rides /public/api (a key is mandatory
|
|
190
|
+
* there — docs/public-api.md); a person token from `datum login` rides
|
|
191
|
+
* /api, where bearerIdentity verifies it and the role gate applies the
|
|
192
|
+
* person's (operator-capped) role. Same handlers either way.
|
|
193
|
+
*/
|
|
194
|
+
export function apiSurface(config) {
|
|
195
|
+
if (!config.apiKey && !config.bearerToken && !config.allowMissingKey) {
|
|
196
|
+
throw new UsageError("no credentials — run `datum login` (sign in with your Microsoft account), " +
|
|
197
|
+
"or pass --api-key / set DATUM_API_KEY (panel: Settings → API keys)");
|
|
198
|
+
}
|
|
199
|
+
const insecure = insecureOriginReason(config.baseUrl);
|
|
200
|
+
if (insecure)
|
|
201
|
+
throw new UsageError(insecure);
|
|
202
|
+
const origin = config.baseUrl.replace(/\/+$/, "");
|
|
203
|
+
const useKey = Boolean(config.apiKey) || !config.bearerToken;
|
|
204
|
+
const headers = {
|
|
205
|
+
authorization: `Bearer ${useKey ? (config.apiKey ?? "") : config.bearerToken}`,
|
|
206
|
+
};
|
|
207
|
+
if (config.variant) {
|
|
208
|
+
let host;
|
|
209
|
+
try {
|
|
210
|
+
host = new URL(origin).hostname;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
throw new UsageError(`--base-url is not a valid URL: ${config.baseUrl}`);
|
|
214
|
+
}
|
|
215
|
+
headers[X_OA_VARIANT_HEADER] = `${host}::${config.variant}`;
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
url: (path) => `${origin}${useKey ? "/public" : ""}${path}`,
|
|
219
|
+
headers,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** Single-quotes a value for a POSIX shell, escaping embedded quotes. */
|
|
223
|
+
function shellQuote(value) {
|
|
224
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
225
|
+
}
|
|
226
|
+
/** Human-readable byte count for the notes below. */
|
|
227
|
+
function humanBytes(n) {
|
|
228
|
+
if (n < 1024)
|
|
229
|
+
return `${n} B`;
|
|
230
|
+
if (n < 1024 * 1024)
|
|
231
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
232
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* How `-d` is rendered. A body that fits is shown as it is — a short body
|
|
236
|
+
* in the preview is exactly what `--curl` is reached for. A large one is
|
|
237
|
+
* NAMED: `@path` when the body is that file unchanged (runnable as
|
|
238
|
+
* printed), `@-` for stdin, and a placeholder plus a note when it was
|
|
239
|
+
* typed inline, because expanding it would put the whole payload on stdout
|
|
240
|
+
* and that is the cost this flag existed to avoid.
|
|
241
|
+
*/
|
|
242
|
+
export function curlBodyArg(req, full) {
|
|
243
|
+
const body = req.body ?? "";
|
|
244
|
+
const size = Buffer.byteLength(body);
|
|
245
|
+
if (full || size <= MAX_CURL_BODY_BYTES)
|
|
246
|
+
return { arg: shellQuote(body) };
|
|
247
|
+
const source = req.bodySource ?? { kind: "inline" };
|
|
248
|
+
// Quoted like any other argument: a path with a space or a quote in it
|
|
249
|
+
// would otherwise split into two arguments (or worse), and the whole
|
|
250
|
+
// point of naming the body is that the line stays runnable as printed.
|
|
251
|
+
if (source.kind === "file")
|
|
252
|
+
return { arg: shellQuote(`@${source.path}`) };
|
|
253
|
+
if (source.kind === "stdin")
|
|
254
|
+
return { arg: shellQuote("@-") };
|
|
255
|
+
return {
|
|
256
|
+
arg: shellQuote("@body.json"),
|
|
257
|
+
note: `# body not rendered: ${humanBytes(size)} of JSON. Write it to ` +
|
|
258
|
+
"body.json, or re-run with --full to print it here.",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** The --curl rendering (secrets stay in env-style placeholders). */
|
|
262
|
+
export function toCurl(req, opts = {}) {
|
|
263
|
+
const parts = [`curl -X ${req.method}`];
|
|
264
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
265
|
+
// Secrets never render: the key placeholder on the public surface, a
|
|
266
|
+
// token placeholder on the internal one (`datum login` credentials).
|
|
267
|
+
const shown = k === "authorization"
|
|
268
|
+
? req.url.includes("/public/api/")
|
|
269
|
+
? "authorization: Bearer $DATUM_API_KEY"
|
|
270
|
+
: "authorization: Bearer $DATUM_TOKEN"
|
|
271
|
+
: `${k}: ${v}`;
|
|
272
|
+
parts.push(`-H '${shown}'`);
|
|
273
|
+
}
|
|
274
|
+
let note;
|
|
275
|
+
if (req.body !== undefined) {
|
|
276
|
+
const rendered = curlBodyArg(req, opts.full === true);
|
|
277
|
+
parts.push(`-d ${rendered.arg}`);
|
|
278
|
+
note = rendered.note;
|
|
279
|
+
}
|
|
280
|
+
parts.push(`'${req.url}'`);
|
|
281
|
+
const command = parts.join(" \\\n ");
|
|
282
|
+
return note ? `${command}\n${note}` : command;
|
|
283
|
+
}
|
|
284
|
+
/** Best short description of a non-JSON (usually HTML) response body. */
|
|
285
|
+
function describeTextBody(text) {
|
|
286
|
+
const title = /<title[^>]*>([^<]+)<\/title>/i.exec(text)?.[1]?.trim();
|
|
287
|
+
if (title)
|
|
288
|
+
return `page titled "${title}"`;
|
|
289
|
+
const line = text
|
|
290
|
+
.replaceAll(/<[^>]+>/g, " ")
|
|
291
|
+
.split("\n")
|
|
292
|
+
.map((l) => l.trim())
|
|
293
|
+
.find(Boolean);
|
|
294
|
+
return line ? line.slice(0, 120) : undefined;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Client-side projection for --fields: keeps only the named top-level
|
|
298
|
+
* keys of an object (or of every element of an array). Solves the "list
|
|
299
|
+
* responses are huge" class of problems (e.g. embedded avatar data URLs)
|
|
300
|
+
* without any server change.
|
|
301
|
+
*/
|
|
302
|
+
export function projectFields(value, fields) {
|
|
303
|
+
const pick = (item) => item !== null && typeof item === "object" && !Array.isArray(item)
|
|
304
|
+
? Object.fromEntries(fields
|
|
305
|
+
.filter((f) => f in item)
|
|
306
|
+
.map((f) => [f, item[f]]))
|
|
307
|
+
: item;
|
|
308
|
+
return Array.isArray(value) ? value.map(pick) : pick(value);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Where an oversized response is parked, and what the reader is told
|
|
312
|
+
* instead. Separated so a test can point it somewhere writable.
|
|
313
|
+
*/
|
|
314
|
+
export async function spillResponse(rendered, dir = tmpdir()) {
|
|
315
|
+
// A response can carry tenant or personal data, so it never lands on a
|
|
316
|
+
// guessable path in a shared directory: `mkdtemp` creates a fresh 0700
|
|
317
|
+
// directory (nothing to pre-plant a symlink on, no collision between
|
|
318
|
+
// parallel invocations), and the file inside is created exclusively with
|
|
319
|
+
// 0600 rather than whatever the umask would have allowed.
|
|
320
|
+
const home = await mkdtemp(join(dir, "datum-response-"));
|
|
321
|
+
const file = join(home, "response.json");
|
|
322
|
+
await writeFile(file, `${rendered}\n`, {
|
|
323
|
+
encoding: "utf8",
|
|
324
|
+
mode: 0o600,
|
|
325
|
+
flag: "wx",
|
|
326
|
+
});
|
|
327
|
+
return file;
|
|
328
|
+
}
|
|
329
|
+
/** The notice that replaces a spilled body on stdout. */
|
|
330
|
+
export function spillNotice(bytes, file, parsed, opts = {}) {
|
|
331
|
+
const items = Array.isArray(parsed) ? parsed.length : undefined;
|
|
332
|
+
const shape = items === undefined ? "" : ` (${items} item${items === 1 ? "" : "s"})`;
|
|
333
|
+
const hint = opts.hint ?? "narrow it with --fields a,b, or re-run with --full";
|
|
334
|
+
return (`datum: ${opts.what ?? "response"} is ${humanBytes(bytes)}${shape} — ` +
|
|
335
|
+
`written to ${file}\n` +
|
|
336
|
+
"datum: printing it here would cost more than reading it does; " +
|
|
337
|
+
`${hint} to print it anyway\n`);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Executes the request and writes the response to `out`. JSON is
|
|
341
|
+
* pretty-printed (unless raw); event streams and other text stream through
|
|
342
|
+
* line by line; anything else pipes raw bytes (logo downloads etc.).
|
|
343
|
+
*
|
|
344
|
+
* The API never redirects and never answers with HTML — both mean the call
|
|
345
|
+
* hit an SSO proxy or a wrong host, so instead of dumping a login page to
|
|
346
|
+
* stdout they fail with a hint (--raw restores blind piping for the body).
|
|
347
|
+
*
|
|
348
|
+
* A pretty-printed JSON answer above MAX_PRINTED_RESPONSE_BYTES is written
|
|
349
|
+
* to a file and replaced by a two-line notice (#646): that branch exists to
|
|
350
|
+
* be READ, and an answer nobody can read is worth less than the context it
|
|
351
|
+
* costs. The raw and binary branches below are deliberately NOT capped —
|
|
352
|
+
* their contract is byte-exact passthrough, and `datum … > logo.png` must
|
|
353
|
+
* keep producing a logo, not a notice.
|
|
354
|
+
*/
|
|
355
|
+
export async function executeRequest(req, opts) {
|
|
356
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
357
|
+
const res = await doFetch(req.url, {
|
|
358
|
+
method: req.method,
|
|
359
|
+
headers: req.headers,
|
|
360
|
+
redirect: "manual",
|
|
361
|
+
...(req.body !== undefined ? { body: req.body } : {}),
|
|
362
|
+
});
|
|
363
|
+
const type = res.headers.get("content-type") ?? "";
|
|
364
|
+
if (res.status >= 300 && res.status < 400) {
|
|
365
|
+
const location = res.headers.get("location") ?? "(no location header)";
|
|
366
|
+
return {
|
|
367
|
+
status: res.status,
|
|
368
|
+
ok: false,
|
|
369
|
+
detail: `the server redirected to ${location} — the API never redirects; ` +
|
|
370
|
+
"this is usually an SSO/login page or a wrong --base-url",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (!opts.raw && type.includes("application/json")) {
|
|
374
|
+
const text = await res.text();
|
|
375
|
+
try {
|
|
376
|
+
let parsed = JSON.parse(text);
|
|
377
|
+
// Projection runs BEFORE the size check on purpose: --fields is the
|
|
378
|
+
// cheap way under the cap, and it should keep working as one.
|
|
379
|
+
if (opts.fields?.length)
|
|
380
|
+
parsed = projectFields(parsed, opts.fields);
|
|
381
|
+
const rendered = JSON.stringify(parsed, null, 2);
|
|
382
|
+
const bytes = Buffer.byteLength(rendered);
|
|
383
|
+
if (!opts.full && bytes > MAX_PRINTED_RESPONSE_BYTES) {
|
|
384
|
+
const file = await spillResponse(rendered, opts.spillDir);
|
|
385
|
+
opts.out.write(spillNotice(bytes, file, parsed));
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
opts.out.write(`${rendered}\n`);
|
|
389
|
+
}
|
|
390
|
+
const error = parsed !== null && typeof parsed === "object"
|
|
391
|
+
? parsed
|
|
392
|
+
: {};
|
|
393
|
+
const detail = error.error ?? error.message;
|
|
394
|
+
return {
|
|
395
|
+
status: res.status,
|
|
396
|
+
ok: res.ok,
|
|
397
|
+
...(typeof detail === "string" && !res.ok ? { detail } : {}),
|
|
398
|
+
...(typeof error.code === "string" && !res.ok ? { code: error.code } : {}),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
opts.out.write(`${text}\n`);
|
|
403
|
+
return { status: res.status, ok: res.ok };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!opts.raw && (type.includes("text/html") || !res.ok)) {
|
|
407
|
+
// HTML (even a 200) or a non-JSON error: describe it, don't dump it.
|
|
408
|
+
const text = res.body ? await res.text() : "";
|
|
409
|
+
const described = describeTextBody(text);
|
|
410
|
+
const detail = type.includes("text/html")
|
|
411
|
+
? `the response is a web page${described ? ` (${described})` : ""}, ` +
|
|
412
|
+
"not API JSON — check --base-url points at the API origin"
|
|
413
|
+
: described;
|
|
414
|
+
return {
|
|
415
|
+
status: res.status,
|
|
416
|
+
ok: false,
|
|
417
|
+
...(detail ? { detail } : {}),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (res.body) {
|
|
421
|
+
for await (const chunk of res.body) {
|
|
422
|
+
opts.out.write(Buffer.from(chunk));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { status: res.status, ok: res.ok };
|
|
426
|
+
}
|
|
427
|
+
//# sourceMappingURL=invoke.js.map
|