@mason-data/cli 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -12
- package/dist/src/api/postgres/catalog.js +36 -1
- package/dist/src/api/postgres/server.js +133 -32
- package/dist/src/cdc/pg-wire.js +144 -38
- package/dist/src/cli/background.js +29 -5
- package/dist/src/cli/command.js +4 -3
- package/dist/src/cli/commands.js +259 -127
- package/dist/src/cli/demo.js +149 -0
- package/dist/src/cli/dev.js +113 -20
- package/dist/src/cli/doctor.js +6 -3
- package/dist/src/cli/guides.js +32 -8
- package/dist/src/cli/init.js +58 -18
- package/dist/src/cli/machine.js +8 -3
- package/dist/src/cli/main.js +2 -2
- package/dist/src/cli/mcp.js +726 -0
- package/dist/src/cli/preflight.js +16 -7
- package/dist/src/cli/project.js +5 -3
- package/dist/src/cli/registry.js +8 -5
- package/dist/src/cli/run-server.js +29 -106
- package/dist/src/cli/serve.js +6 -1
- package/dist/src/cli/source-edit.js +116 -0
- package/dist/src/cli/sources.js +6 -4
- package/dist/src/demo/seed.js +237 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/executor-worker.js +7 -2
- package/dist/src/duck/executor.js +67 -26
- package/dist/src/duck/explain.js +128 -9
- package/dist/src/duck/functions.js +60 -5
- package/dist/src/duck/gaps.js +9 -4
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +10 -8
- package/dist/src/duck/kept-route.js +82 -2
- package/dist/src/duck/kept.js +206 -39
- package/dist/src/duck/pgtree.js +40 -0
- package/dist/src/duck/plan.js +21 -8
- package/dist/src/duck/rewrite.js +1168 -68
- package/dist/src/duck/store.js +132 -56
- package/dist/src/duck/sync.js +90 -25
- package/dist/src/duck/verified.js +6 -0
- package/dist/src/lib/timezone.js +3 -2
- package/dist/src/web/app.css +11 -0
- package/dist/src/web/app.js +167 -12
- package/dist/src/web/index.html +6 -1
- package/package.json +1 -1
package/dist/src/cdc/pg-wire.js
CHANGED
|
@@ -18,10 +18,13 @@ import * as tls from "node:tls";
|
|
|
18
18
|
export class PgError extends Error {
|
|
19
19
|
code;
|
|
20
20
|
detail;
|
|
21
|
+
/** the ErrorResponse's own fields, by their one-letter codes (M the message alone, H the hint, P the position) */
|
|
22
|
+
fields;
|
|
21
23
|
constructor(fields) {
|
|
22
24
|
super(`${fields.S ?? "ERROR"}: ${fields.M ?? "unknown error"}${fields.D ? ` (${fields.D})` : ""}`);
|
|
23
25
|
this.code = fields.C ?? "";
|
|
24
26
|
this.detail = fields.D;
|
|
27
|
+
this.fields = fields;
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
/** Microseconds between the Unix epoch and Postgres's (2000-01-01). */
|
|
@@ -72,45 +75,39 @@ export class PgConnection {
|
|
|
72
75
|
}
|
|
73
76
|
/** TCP connect, the SSL negotiation and the startup exchange; `hold` is told the live socket, for a timeout to destroy. */
|
|
74
77
|
async handshake(hold) {
|
|
75
|
-
const
|
|
76
|
-
const mode = o.sslmode ?? "prefer";
|
|
77
|
-
const raw = await new Promise((resolve, reject) => {
|
|
78
|
-
const s = net.connect({ host: o.host, port: o.port });
|
|
79
|
-
hold(s);
|
|
80
|
-
s.once("connect", () => resolve(s));
|
|
81
|
-
s.once("error", (e) => reject(new Error(`connecting to ${o.host}:${o.port}: ${socketErrorText(e)}`)));
|
|
82
|
-
});
|
|
83
|
-
let sock = raw;
|
|
84
|
-
if (mode !== "disable" && mode !== "allow") {
|
|
85
|
-
// SSLRequest: length 8, code 80877103; the server answers one byte, S or N
|
|
86
|
-
raw.write(Buffer.from([0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f]));
|
|
87
|
-
const answer = await new Promise((resolve, reject) => {
|
|
88
|
-
raw.once("data", (d) => resolve(String.fromCharCode(d[0])));
|
|
89
|
-
raw.once("error", reject);
|
|
90
|
-
});
|
|
91
|
-
if (answer === "S") {
|
|
92
|
-
const verify = mode === "verify-ca" || mode === "verify-full";
|
|
93
|
-
sock = await new Promise((resolve, reject) => {
|
|
94
|
-
const t = tls.connect({
|
|
95
|
-
socket: raw, rejectUnauthorized: verify,
|
|
96
|
-
...(mode === "verify-full" ? { servername: o.host } : { checkServerIdentity: () => undefined }),
|
|
97
|
-
});
|
|
98
|
-
hold(t);
|
|
99
|
-
t.once("secureConnect", () => resolve(t));
|
|
100
|
-
t.once("error", reject);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
else if (mode !== "prefer") {
|
|
104
|
-
raw.destroy();
|
|
105
|
-
throw new Error(`sslmode=${mode}, but ${o.host} does not accept TLS`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
78
|
+
const sock = await transport(this.opts, hold);
|
|
108
79
|
this.socket = sock;
|
|
109
80
|
sock.on("data", (d) => this.onData(d));
|
|
110
81
|
sock.on("error", (e) => this.fail(e));
|
|
111
82
|
sock.on("close", () => this.fail(new Error("connection closed by the server")));
|
|
112
83
|
await this.startup();
|
|
113
84
|
}
|
|
85
|
+
/** the server's key for this session (BackendKeyData), for a CancelRequest */
|
|
86
|
+
key = null;
|
|
87
|
+
/**
|
|
88
|
+
* ASK THE SERVER TO CANCEL THIS SESSION'S RUNNING STATEMENT (Postgres's CancelRequest, on a connection of its own,
|
|
89
|
+
* TLS as this one's sslmode): the statement ends with 57014 on this connection, which stays usable. Resolves once
|
|
90
|
+
* the request is written.
|
|
91
|
+
*/
|
|
92
|
+
async cancel() {
|
|
93
|
+
const key = this.key;
|
|
94
|
+
if (key === null)
|
|
95
|
+
throw new Error("the server sent no key for this session: it cannot be cancelled");
|
|
96
|
+
let held = null;
|
|
97
|
+
const timer = setTimeout(() => { held?.destroy(); }, this.opts.connectTimeoutMs ?? 15_000);
|
|
98
|
+
try {
|
|
99
|
+
const s = await transport(this.opts, (x) => { held = x; });
|
|
100
|
+
const b = Buffer.alloc(16);
|
|
101
|
+
b.writeInt32BE(16, 0);
|
|
102
|
+
b.writeInt32BE(80877102, 4);
|
|
103
|
+
b.writeInt32BE(key.pid, 8);
|
|
104
|
+
b.writeInt32BE(key.secret, 12);
|
|
105
|
+
await new Promise((resolve, reject) => { s.once("error", reject); s.end(b, () => resolve()); });
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
114
111
|
fail(e) {
|
|
115
112
|
if (this.closedWith)
|
|
116
113
|
return;
|
|
@@ -167,6 +164,16 @@ export class PgConnection {
|
|
|
167
164
|
if (k !== undefined)
|
|
168
165
|
this.parameters[k] = v ?? "";
|
|
169
166
|
}
|
|
167
|
+
/** the next message of a statement's reply; when the server closed the connection after an ErrorResponse it sent
|
|
168
|
+
* (a FATAL: a user removed, a shutdown) and before ReadyForQuery, that error — never the bare "connection closed" */
|
|
169
|
+
async reply(error) {
|
|
170
|
+
try {
|
|
171
|
+
return await this.next();
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
throw error ?? e;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
170
177
|
next() {
|
|
171
178
|
const m = this.queue.shift();
|
|
172
179
|
if (m) {
|
|
@@ -195,7 +202,7 @@ export class PgConnection {
|
|
|
195
202
|
const params = [
|
|
196
203
|
["user", o.user], ["database", o.database], ["client_encoding", "UTF8"],
|
|
197
204
|
// Text values in the forms the reader parses: ISO dates in UTC, floats round-trippable
|
|
198
|
-
["DateStyle", "ISO, YMD"], ["TimeZone", "UTC"], ["extra_float_digits", "3"], ["IntervalStyle", "iso_8601"],
|
|
205
|
+
...(o.textForms ?? [["DateStyle", "ISO, YMD"], ["TimeZone", "UTC"], ["extra_float_digits", "3"], ["IntervalStyle", "iso_8601"]]).map(([k, v]) => [k, v]),
|
|
199
206
|
["application_name", o.applicationName ?? "mason"],
|
|
200
207
|
];
|
|
201
208
|
if (o.replication)
|
|
@@ -216,8 +223,10 @@ export class PgConnection {
|
|
|
216
223
|
await this.auth(m.body);
|
|
217
224
|
continue;
|
|
218
225
|
}
|
|
219
|
-
if (m.type === "K")
|
|
220
|
-
|
|
226
|
+
if (m.type === "K") {
|
|
227
|
+
this.key = { pid: m.body.readInt32BE(0), secret: m.body.readInt32BE(4) };
|
|
228
|
+
continue;
|
|
229
|
+
} // BackendKeyData
|
|
221
230
|
if (m.type === "Z")
|
|
222
231
|
return;
|
|
223
232
|
throw new Error(`unexpected message ${m.type} during startup`);
|
|
@@ -288,7 +297,7 @@ export class PgConnection {
|
|
|
288
297
|
let result = { fields: [], rows: [], command: "" };
|
|
289
298
|
let error = null;
|
|
290
299
|
for (;;) {
|
|
291
|
-
const m = await this.
|
|
300
|
+
const m = await this.reply(error);
|
|
292
301
|
switch (m.type) {
|
|
293
302
|
case "T":
|
|
294
303
|
result = { fields: rowDescription(m.body), rows: [], command: "" };
|
|
@@ -321,7 +330,7 @@ export class PgConnection {
|
|
|
321
330
|
this.send("Q", cstr(sql));
|
|
322
331
|
let error = null;
|
|
323
332
|
for (;;) {
|
|
324
|
-
const m = await this.
|
|
333
|
+
const m = await this.reply(error);
|
|
325
334
|
switch (m.type) {
|
|
326
335
|
case "D":
|
|
327
336
|
if (error === null)
|
|
@@ -338,6 +347,52 @@ export class PgConnection {
|
|
|
338
347
|
}
|
|
339
348
|
}
|
|
340
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* ONE STATEMENT THROUGH THE EXTENDED PROTOCOL (`mason mcp`'s query): Parse, Bind, Describe and Execute of the unnamed
|
|
352
|
+
* portal, then Sync — so the server takes exactly one statement (a second is Postgres's 42601), and `maxRows` is the
|
|
353
|
+
* Execute's row count: the server stops at it and says so (PortalSuspended), so a query an agent leaves unbounded is
|
|
354
|
+
* never sent whole. 0 is every row. Every value is text.
|
|
355
|
+
*/
|
|
356
|
+
async extended(sql, maxRows) {
|
|
357
|
+
const int16 = (n) => { const b = Buffer.alloc(2); b.writeInt16BE(n); return b; };
|
|
358
|
+
this.send("P", Buffer.concat([cstr(""), cstr(sql), int16(0)]));
|
|
359
|
+
// no parameters, every result column in text
|
|
360
|
+
this.send("B", Buffer.concat([cstr(""), cstr(""), int16(0), int16(0), int16(0)]));
|
|
361
|
+
this.send("D", Buffer.concat([Buffer.from("P"), cstr("")]));
|
|
362
|
+
this.send("E", Buffer.concat([cstr(""), int32(maxRows)]));
|
|
363
|
+
this.send("S", Buffer.alloc(0));
|
|
364
|
+
const result = { fields: [], rows: [], suspended: false, command: "", empty: false };
|
|
365
|
+
let error = null;
|
|
366
|
+
for (;;) {
|
|
367
|
+
const m = await this.reply(error);
|
|
368
|
+
switch (m.type) {
|
|
369
|
+
case "T":
|
|
370
|
+
result.fields = rowFields(m.body);
|
|
371
|
+
break;
|
|
372
|
+
case "D":
|
|
373
|
+
if (error === null)
|
|
374
|
+
result.rows.push(dataRow(m.body));
|
|
375
|
+
break;
|
|
376
|
+
case "s":
|
|
377
|
+
result.suspended = true;
|
|
378
|
+
break;
|
|
379
|
+
case "C":
|
|
380
|
+
result.command = cstrings(m.body)[0] ?? "";
|
|
381
|
+
break;
|
|
382
|
+
case "E":
|
|
383
|
+
error = new PgError(errorFields(m.body));
|
|
384
|
+
break;
|
|
385
|
+
case "I":
|
|
386
|
+
result.empty = true;
|
|
387
|
+
break;
|
|
388
|
+
case "Z":
|
|
389
|
+
if (error)
|
|
390
|
+
throw error;
|
|
391
|
+
return result;
|
|
392
|
+
default: break; // ParseComplete, BindComplete, NoData
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
341
396
|
// ------------------------------------------------------------------ replication
|
|
342
397
|
/** START_REPLICATION, then an async stream of XLogData and keepalives until the connection ends. */
|
|
343
398
|
async *replicate(command) {
|
|
@@ -392,6 +447,42 @@ export class PgConnection {
|
|
|
392
447
|
}
|
|
393
448
|
}
|
|
394
449
|
// ------------------------------------------------------------------ helpers
|
|
450
|
+
/** TCP connect and the SSL negotiation (libpq's sslmode): the socket a startup, or a CancelRequest, is written to. */
|
|
451
|
+
async function transport(o, hold) {
|
|
452
|
+
const mode = o.sslmode ?? "prefer";
|
|
453
|
+
const raw = await new Promise((resolve, reject) => {
|
|
454
|
+
const s = net.connect({ host: o.host, port: o.port });
|
|
455
|
+
hold(s);
|
|
456
|
+
s.once("connect", () => resolve(s));
|
|
457
|
+
s.once("error", (e) => reject(new Error(`connecting to ${o.host}:${o.port}: ${socketErrorText(e)}`)));
|
|
458
|
+
});
|
|
459
|
+
let sock = raw;
|
|
460
|
+
if (mode !== "disable" && mode !== "allow") {
|
|
461
|
+
// SSLRequest: length 8, code 80877103; the server answers one byte, S or N
|
|
462
|
+
raw.write(Buffer.from([0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f]));
|
|
463
|
+
const answer = await new Promise((resolve, reject) => {
|
|
464
|
+
raw.once("data", (d) => resolve(String.fromCharCode(d[0])));
|
|
465
|
+
raw.once("error", reject);
|
|
466
|
+
});
|
|
467
|
+
if (answer === "S") {
|
|
468
|
+
const verify = mode === "verify-ca" || mode === "verify-full";
|
|
469
|
+
sock = await new Promise((resolve, reject) => {
|
|
470
|
+
const t = tls.connect({
|
|
471
|
+
socket: raw, rejectUnauthorized: verify,
|
|
472
|
+
...(mode === "verify-full" ? { servername: o.host } : { checkServerIdentity: () => undefined }),
|
|
473
|
+
});
|
|
474
|
+
hold(t);
|
|
475
|
+
t.once("secureConnect", () => resolve(t));
|
|
476
|
+
t.once("error", reject);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
else if (mode !== "prefer") {
|
|
480
|
+
raw.destroy();
|
|
481
|
+
throw new Error(`sslmode=${mode}, but ${o.host} does not accept TLS`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return sock;
|
|
485
|
+
}
|
|
395
486
|
/** `16/B374D848` -> its 64-bit position; throws on anything else. */
|
|
396
487
|
export function parseLsn(text) {
|
|
397
488
|
const m = /^([0-9A-Fa-f]{1,8})\/([0-9A-Fa-f]{1,8})$/.exec(text.trim());
|
|
@@ -452,6 +543,21 @@ function rowDescription(b) {
|
|
|
452
543
|
}
|
|
453
544
|
return names;
|
|
454
545
|
}
|
|
546
|
+
/** A RowDescription's columns with their types (rowDescription keeps the names alone). */
|
|
547
|
+
function rowFields(b) {
|
|
548
|
+
const n = b.readInt16BE(0);
|
|
549
|
+
const out = [];
|
|
550
|
+
let i = 2;
|
|
551
|
+
for (let k = 0; k < n; k++) {
|
|
552
|
+
const end = b.indexOf(0, i);
|
|
553
|
+
const name = b.toString("utf8", i, end);
|
|
554
|
+
i = end + 1;
|
|
555
|
+
// table oid (4), column number (2), type oid (4), type size (2), type modifier (4), format (2)
|
|
556
|
+
out.push({ name, typeOid: b.readUInt32BE(i + 6), typmod: b.readInt32BE(i + 12) });
|
|
557
|
+
i += 18;
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
455
561
|
/** onData pauses the socket above this many queued bytes, and next() resumes it below a quarter of it. */
|
|
456
562
|
const QUEUE_PAUSE_BYTES = 16 * 1024 * 1024;
|
|
457
563
|
function dataRow(b) {
|
|
@@ -19,8 +19,9 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
import { runnable } from "../lib/runnable.js";
|
|
20
20
|
import { READY, STEP_MARK } from "./dev.js";
|
|
21
21
|
import { liveRun } from "./runs.js";
|
|
22
|
-
import { theProject } from "./project.js";
|
|
22
|
+
import { projectSource, theProject } from "./project.js";
|
|
23
23
|
import { runListenerSettings } from "./pg-tls.js";
|
|
24
|
+
import { demoChangesRefusal, demoPort } from "./demo.js";
|
|
24
25
|
import { endText, failed, jsonText, nextBlock } from "./command.js";
|
|
25
26
|
import { DEV } from "./registry.js";
|
|
26
27
|
import { out as styleOut, stepRenderer } from "./style.js";
|
|
@@ -38,6 +39,18 @@ export const NEXT_STEPS = [
|
|
|
38
39
|
["mason logs", "its log: every copy, poll and statement"],
|
|
39
40
|
["mason stop", "stop Mason; the next mason start resumes where it stopped"],
|
|
40
41
|
];
|
|
42
|
+
/** The ports a demo project's database takes (its URL's port, read as the run reads it), for the API's choice to avoid;
|
|
43
|
+
* none for any other project. A sources file or a URL that cannot be read gives none HERE: the run refuses it in its
|
|
44
|
+
* own words (its source step, its demo step), shown by this start as every other refusal of the run's is. */
|
|
45
|
+
function demoPortsOf(project) {
|
|
46
|
+
if (project.config.demo !== true)
|
|
47
|
+
return new Set();
|
|
48
|
+
const src = projectSource(project, process.env);
|
|
49
|
+
if ("message" in src)
|
|
50
|
+
return new Set();
|
|
51
|
+
const at = demoPort(src.resolved, src.from);
|
|
52
|
+
return new Set("port" in at ? [at.port] : []);
|
|
53
|
+
}
|
|
41
54
|
/** `args` are `mason start`'s own, passed on to the run with --attach. */
|
|
42
55
|
export async function devInBackground(args, o) {
|
|
43
56
|
const t = theProject(process.cwd());
|
|
@@ -55,9 +68,14 @@ export async function devInBackground(args, o) {
|
|
|
55
68
|
say(nextBlock([["mason status", "what it is doing"], ["mason stop", "stop it"]]));
|
|
56
69
|
return 0;
|
|
57
70
|
}
|
|
71
|
+
// a demo project with --replication (src/cli/demo.ts): refused here too, where it is read, before a run is spawned
|
|
72
|
+
const demoRefused = demoChangesRefusal(t.project.config, o.changes ?? null);
|
|
73
|
+
if (demoRefused)
|
|
74
|
+
return failed("start", demoRefused, { json: o.json });
|
|
58
75
|
// a MASON_PG_PORT that is no port, or is held, is refused HERE, where the person (or --json) reads it, before a run is
|
|
59
|
-
// spawned; the run checks again for itself (choosePgPort, src/cli/ports.ts), since a port may be taken in between
|
|
60
|
-
|
|
76
|
+
// spawned; the run checks again for itself (choosePgPort, src/cli/ports.ts), since a port may be taken in between.
|
|
77
|
+
// A demo project's database port (its URL's, src/cli/demo.ts demoPort) is avoided as the run avoids it
|
|
78
|
+
const pg = await choosePgPort(process.env.MASON_PG_PORT, demoPortsOf(t.project), t.project.config.pgPort ?? null);
|
|
61
79
|
if ("problem" in pg)
|
|
62
80
|
return failed("start", pg, { json: o.json });
|
|
63
81
|
// and so is a listener off loopback without TLS, by the run's own check (src/cli/pg-tls.ts): read, never written
|
|
@@ -140,8 +158,8 @@ export async function devInBackground(args, o) {
|
|
|
140
158
|
steps.fail(l.slice(STEP_MARK.fail.length));
|
|
141
159
|
continue;
|
|
142
160
|
}
|
|
143
|
-
// the run's line (src/cli/dev.ts: "Postgres <url>")
|
|
144
|
-
const pgLine = /^\s+Postgres\s+(
|
|
161
|
+
// the run's line (src/cli/dev.ts: "Postgres <url>", the URL in its shown form, `withPlaceholders`: postgres://<user>:<password>@…)
|
|
162
|
+
const pgLine = /^\s+Postgres\s+(postgres:\/\/<user>:<password>@\S+)/.exec(l);
|
|
145
163
|
if (pgLine) {
|
|
146
164
|
postgres = postgresLink(withoutPlaceholders(pgLine[1]));
|
|
147
165
|
steps.print(` ${st.dim("Postgres API")} ${pgLine[1]} ${st.dim("(mason user add <name> makes a user)")}`);
|
|
@@ -151,6 +169,12 @@ export async function devInBackground(args, o) {
|
|
|
151
169
|
steps.print(` ${st.dim("Web UI".padEnd("Postgres API".length))} ${st.cyan("mason ui")} ${st.dim("(the status page and the users, in your browser)")}`);
|
|
152
170
|
continue;
|
|
153
171
|
}
|
|
172
|
+
// a demo project's line (src/cli/dev.ts: "Demo a new order lands every <n> s: …"), in the same column
|
|
173
|
+
const demoLine = /^\s+Demo\s{2}(.+)$/.exec(l);
|
|
174
|
+
if (demoLine) {
|
|
175
|
+
steps.print(` ${st.dim("Demo".padEnd("Postgres API".length))} ${demoLine[1]}`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
154
178
|
if (l.trim())
|
|
155
179
|
since.push(l.trim().replace(/^mason start: /, ""));
|
|
156
180
|
steps.print(l);
|
package/dist/src/cli/command.js
CHANGED
|
@@ -24,7 +24,7 @@ export function parseCommand(spec, argv) {
|
|
|
24
24
|
parsed = parseArgs({ args: argv, options: { ...spec.options, ...HELP }, allowPositionals: true });
|
|
25
25
|
}
|
|
26
26
|
catch (e) {
|
|
27
|
-
return failed(spec.name, { message: e.message, code: "usage", usage: spec.usage, next: [[`mason ${spec.name} --help`, "its usage, with every option"]] }, { json: wantsJson(argv) });
|
|
27
|
+
return failed(spec.name, { message: e.message, code: "usage", usage: spec.usage, next: [[`mason ${spec.name} --help`, "its usage, with every option"]] }, { json: wantsJson(argv, spec) });
|
|
28
28
|
}
|
|
29
29
|
if (parsed.values.help) {
|
|
30
30
|
process.stdout.write(spec.usage);
|
|
@@ -76,8 +76,9 @@ export function nextJson(steps = []) {
|
|
|
76
76
|
return steps.map(([command, what]) => what ? { command, what } : { command });
|
|
77
77
|
}
|
|
78
78
|
/** Whether a command line asked for data: `--json` anywhere on it (read before the line is parsed, so a wrong line
|
|
79
|
-
* is answered in JSON too).
|
|
80
|
-
|
|
79
|
+
* is answered in JSON too) — never for a command whose stdout is a protocol's (`CommandSpec.stdout`), whose every
|
|
80
|
+
* failure is said on stderr. */
|
|
81
|
+
export const wantsJson = (argv, spec) => spec?.stdout !== "protocol" && argv.includes("--json");
|
|
81
82
|
/**
|
|
82
83
|
* A COMMAND'S --json ANSWER (an agent wants clear, unambiguous context and a machine-readable format, and a person
|
|
83
84
|
* and an agent both want the next steps): one JSON object on stdout, always with `next` — the same steps the human
|