@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/cli/init.js
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* of it): nothing more when it came from there, the line added when it came from the argument or the shell — and adds
|
|
12
12
|
* `.mason/` and that .env to the folder's .gitignore. It refuses a folder that is in a project already (here or above)
|
|
13
13
|
* — but for a project with no source file (one an older Mason made), whose source file it writes, nothing else changed.
|
|
14
|
+
*
|
|
15
|
+
* `mason init demo` (src/cli/demo.ts) writes the same files for a database Mason hosts itself: the demo's URL, on a port
|
|
16
|
+
* free now, in `.env` (the one place the port is kept), three of the demo's four tables already added (the fourth,
|
|
17
|
+
* `DEMO_TABLE_TO_ADD`, left for the person's first `mason add`), and `demo: true` in the config; there is nothing to
|
|
18
|
+
* connect to yet — `mason start` makes the database in `.mason/demo/`.
|
|
14
19
|
*/
|
|
15
20
|
import * as fs from "node:fs";
|
|
16
21
|
import * as path from "node:path";
|
|
@@ -22,9 +27,11 @@ import { cliPinProblem, ensurePin } from "./pin.js";
|
|
|
22
27
|
import { version } from "./version.js";
|
|
23
28
|
import { parseVersion, PIN_FILE } from "../lib/project-version.js";
|
|
24
29
|
import { appDatabaseUrl, appEnvValue, appendEnvLine, ENV_FILE } from "../lib/dotenv.js";
|
|
25
|
-
import { NAME_RE, STATE_DIR, brokenProblem, findProject, ignoresStateDir, nameFromFolder, newConfig, writeConfig } from "./project.js";
|
|
30
|
+
import { DEMO_DIR, NAME_RE, STATE_DIR, brokenProblem, findProject, ignoresStateDir, nameFromFolder, newConfig, writeConfig } from "./project.js";
|
|
31
|
+
import { DEMO_ADDED_TABLES, DEMO_ARG, DEMO_PORT, DEMO_TABLE_TO_ADD, demoUrl } from "./demo.js";
|
|
32
|
+
import { freePort } from "./ports.js";
|
|
26
33
|
import { newSourceText, OLD_TABLES_DIR, parseSourceText, shownPath, sourceFileNames, sourceNameFor, sourcesDir } from "./sources.js";
|
|
27
|
-
export const INIT_USAGE = `usage: mason init [<postgres url>] [--name <name>] [--folder <dir>] [--remote] [--skip-check] [--json]
|
|
34
|
+
export const INIT_USAGE = `usage: mason init [<postgres url> | demo] [--name <name>] [--folder <dir>] [--remote] [--skip-check] [--json]
|
|
28
35
|
|
|
29
36
|
Makes this folder a Mason project — run it in your app's folder. It checks it can read the database (and writes
|
|
30
37
|
nothing when it cannot), then creates:
|
|
@@ -34,27 +41,31 @@ export const INIT_USAGE = `usage: mason init [<postgres url>] [--name <name>] [-
|
|
|
34
41
|
The source file says url: \${DATABASE_URL}: the URL itself is in .env (written there when you give it here or it is
|
|
35
42
|
only in the shell), never in a committed file. .mason/ and that .env go into .gitignore. Every other mason command
|
|
36
43
|
then works anywhere in this folder. In a project with no source file (an older Mason's), it writes that file alone.
|
|
44
|
+
mason init demo, in a new folder, makes a project on a demo database Mason hosts itself (a shop: customers, products,
|
|
45
|
+
orders, order_items): mason start makes it in .mason/demo/ and changes it every few seconds. Three of its tables are
|
|
46
|
+
added; the fourth is yours to add after mason start (mason add ${DEMO_TABLE_TO_ADD}), to watch its copy begin.
|
|
37
47
|
|
|
38
48
|
mason init DATABASE_URL from .env.local, .env or the shell
|
|
39
49
|
mason init postgres://me:secret@localhost:5432/shop
|
|
50
|
+
mason init demo a demo database Mason hosts itself: nothing to install
|
|
40
51
|
mason init --folder db/mason --name shop-admin the project's files elsewhere; another name
|
|
41
52
|
mason init postgres://me:secret@db.example.com/shop --remote
|
|
42
53
|
|
|
43
54
|
--name <name> the project's name: the database name a tool connects to on the Postgres API (default: this folder's)
|
|
44
55
|
--folder <dir> where sources/ goes, relative to this folder (default mason; . for this folder itself)
|
|
45
|
-
--remote go ahead with a database on another machine (read the warnings mason init prints)
|
|
46
|
-
--skip-check write the project without connecting (the database is not up yet); mason start checks it
|
|
47
|
-
--json as data: { name, root, folder, config,
|
|
48
|
-
file writes it), env (the .env line
|
|
56
|
+
--remote go ahead with a database on another machine (read the warnings mason init prints); not with demo
|
|
57
|
+
--skip-check write the project without connecting (the database is not up yet); mason start checks it; not with demo
|
|
58
|
+
--json as data: { name, root, folder, config, demo (true for the demo database's project, else false),
|
|
59
|
+
source: { name (the source's: its file's), file, url (as the file writes it), env (the .env line
|
|
49
60
|
written, its value masked, or null) }, database: { url (password masked), from, version, tables },
|
|
50
61
|
warnings, next }
|
|
51
62
|
`;
|
|
52
63
|
export const INIT = {
|
|
53
64
|
name: "init",
|
|
54
|
-
summary: "make this folder a Mason project on your Postgres: mason init (DATABASE_URL from .env)
|
|
65
|
+
summary: "make this folder a Mason project on your Postgres: mason init (DATABASE_URL from .env), mason init postgres://… or demo",
|
|
55
66
|
usage: INIT_USAGE,
|
|
56
67
|
options: { name: { type: "string" }, folder: { type: "string" }, remote: { type: "boolean" }, "skip-check": { type: "boolean" }, json: { type: "boolean" } },
|
|
57
|
-
json: ["name", "root", "folder", "config", "source", "database", "warnings", "next"],
|
|
68
|
+
json: ["name", "root", "folder", "config", "demo", "source", "database", "warnings", "next"],
|
|
58
69
|
};
|
|
59
70
|
/** Why `name` cannot be this project's, or null: not a name the Postgres API's database and the slot take. */
|
|
60
71
|
export function nameProblem(name) {
|
|
@@ -137,6 +148,14 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
137
148
|
const usage = (message, fix) => failed("init", { message, code: "usage", fix }, { json, io });
|
|
138
149
|
if (f.positionals.length > 1)
|
|
139
150
|
return failed("init", { message: "one postgres:// URL at most", code: "usage", usage: INIT_USAGE }, { json, io });
|
|
151
|
+
// THE DEMO (src/cli/demo.ts): a database Mason hosts, made by mason start; there is nothing to check, nor elsewhere
|
|
152
|
+
const demo = f.positionals[0] === DEMO_ARG;
|
|
153
|
+
if (demo) {
|
|
154
|
+
for (const flag of ["remote", "skip-check"]) {
|
|
155
|
+
if (f.values[flag] === true)
|
|
156
|
+
return failed("init", { message: `--${flag} means nothing for the demo database: it is on this machine, made and hosted by mason start`, code: "usage", fix: "mason init demo, without it", usage: INIT_USAGE }, { json, io });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
140
159
|
let root = path.resolve(cwd);
|
|
141
160
|
const existing = findProject(root);
|
|
142
161
|
// a project whose config cannot be read is still this folder's: a second init over it would hide the first's data
|
|
@@ -155,6 +174,10 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
155
174
|
return failed("init", { message: `${existing.root === root ? "this folder is" : `this folder is inside ${existing.root}, which is`} already the Mason project ${existing.config.name}`, code: "refused",
|
|
156
175
|
fix: `to point it at another database, ${where}`, next: [["mason status", "what it is doing"]] }, { json, io });
|
|
157
176
|
}
|
|
177
|
+
if (sourceless && demo) {
|
|
178
|
+
return failed("init", { message: `this folder is the Mason project ${sourceless.config.name} already: mason init demo makes a project of its own`, code: "refused",
|
|
179
|
+
fix: "run it in a new folder (mkdir mason-demo && cd mason-demo && mason init demo)", next: [["mason init", "this project's source file, on your database"]] }, { json, io });
|
|
180
|
+
}
|
|
158
181
|
if (sourceless && (f.values.name !== undefined || f.values.folder !== undefined)) {
|
|
159
182
|
return usage(`this folder is the Mason project ${sourceless.config.name} already, whose source file is all mason init writes here: it takes no --name or --folder`, "mason init [<postgres url>], in the project's folder");
|
|
160
183
|
}
|
|
@@ -170,16 +193,18 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
170
193
|
const named = nameProblem(name);
|
|
171
194
|
if (named)
|
|
172
195
|
return usage(named);
|
|
173
|
-
|
|
196
|
+
// the demo's URL names a port free now, and is the one place the port is kept: mason start reads it from there
|
|
197
|
+
let url = demo ? demoUrl(await freePort(DEMO_PORT)) : f.positionals[0] ?? null;
|
|
174
198
|
let from = "";
|
|
175
199
|
let envFile = null; // the folder's file the URL was read from (.env.local or .env): ignored with .mason/
|
|
176
200
|
if (url === null) {
|
|
177
201
|
const found = appDatabaseUrl(root);
|
|
178
202
|
if (!found) {
|
|
179
|
-
// the
|
|
203
|
+
// the fixes ARE the next commands, so a coding agent reads them as such too
|
|
180
204
|
return failed("init", { message: "which database? none is named, and this folder has no DATABASE_URL", code: "usage",
|
|
181
205
|
fix: "give its URL on the command line, or put it in this folder's .env as DATABASE_URL=postgres://… (your app's, usually)",
|
|
182
|
-
next: [["mason init postgres://user:password@localhost:5432/your_app", "its URL, given"], ["mason init", "again, once .env has DATABASE_URL"]
|
|
206
|
+
next: [["mason init postgres://user:password@localhost:5432/your_app", "its URL, given"], ["mason init", "again, once .env has DATABASE_URL"],
|
|
207
|
+
["mason init demo", "a demo database Mason makes itself: nothing to install"]] }, { json, io });
|
|
183
208
|
}
|
|
184
209
|
url = found.url;
|
|
185
210
|
from = ` (DATABASE_URL in ${found.from})`;
|
|
@@ -191,6 +216,11 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
191
216
|
const remote = f.values.remote === true || sourceless?.config.remote === true;
|
|
192
217
|
// THE SOURCE FILE: one already here (a clone of the project) is kept; else written, its URL a variable of .env
|
|
193
218
|
const kept = sourceFileNames(folder)[0] ?? null;
|
|
219
|
+
if (demo && kept !== null) {
|
|
220
|
+
// a clone's sources file names the team's database: the demo never takes its place
|
|
221
|
+
return failed("init", { message: `${path.relative(root, path.join(sourcesDir(folder), kept))} is already here: mason init demo makes a project of its own`, code: "refused",
|
|
222
|
+
fix: "run it in a new folder (mkdir mason-demo && cd mason-demo && mason init demo), or mason init here for the database that file names" }, { json, io });
|
|
223
|
+
}
|
|
194
224
|
const keptText = kept === null ? null : fs.readFileSync(path.join(sourcesDir(folder), kept), "utf8");
|
|
195
225
|
const keptFile = kept === null ? null : parseSourceText(keptText, kept.replace(/\.ya?ml$/, ""), path.join(sourcesDir(folder), kept));
|
|
196
226
|
const keptVar = keptFile !== null && !("problems" in keptFile) && "env" in keptFile.url ? keptFile.url.env : null;
|
|
@@ -225,7 +255,11 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
225
255
|
wayOut: "run the same mason init again with --remote: that says yes, copy from that machine",
|
|
226
256
|
next: [["mason init --remote <url>", "the same command, confirmed"]] }), { json, io });
|
|
227
257
|
try {
|
|
228
|
-
if (
|
|
258
|
+
if (demo) {
|
|
259
|
+
// nothing exists yet: mason start makes the database (src/demo/source.ts) at its first start
|
|
260
|
+
await steps.step("Choosing the demo database's port", () => null, () => `The demo database will be ${db}: mason start makes it in ${STATE_DIR}/${DEMO_DIR}/ and seeds it (nothing to connect to yet)`);
|
|
261
|
+
}
|
|
262
|
+
else if (f.values["skip-check"] === true) {
|
|
229
263
|
if (parsed.source.remote && !remote)
|
|
230
264
|
return confirmRemote();
|
|
231
265
|
await steps.step(`Not checking ${db}${from}`, () => null, () => `Not checking ${db}${from} (--skip-check): mason start checks it`);
|
|
@@ -257,7 +291,7 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
257
291
|
await steps.step(`Keeping ${STATE_DIR}/config.json`, () => undefined, () => `Kept ${STATE_DIR}/config.json: the project ${name}, its key and its copy`);
|
|
258
292
|
else {
|
|
259
293
|
await steps.step(`Writing ${STATE_DIR}/config.json`, () => {
|
|
260
|
-
writeConfig(path.join(root, STATE_DIR), newConfig(name, path.relative(root, folder) || ".", remote));
|
|
294
|
+
writeConfig(path.join(root, STATE_DIR), newConfig(name, path.relative(root, folder) || ".", remote, { demo }));
|
|
261
295
|
}, () => `Wrote ${STATE_DIR}/config.json: Mason's key (this machine's)`);
|
|
262
296
|
}
|
|
263
297
|
if (kept !== null) {
|
|
@@ -268,8 +302,8 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
268
302
|
else {
|
|
269
303
|
await steps.step(`Writing ${rel(sourceFile)}`, () => {
|
|
270
304
|
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
|
|
271
|
-
fs.writeFileSync(sourceFile, newSourceText(`\${${where.variable}}
|
|
272
|
-
}, () => `Wrote ${rel(sourceFile)}: the database (url: \${${where.variable}}) and the tables to copy, none yet (mason add) — commit it`);
|
|
305
|
+
fs.writeFileSync(sourceFile, newSourceText(`\${${where.variable}}`, demo ? DEMO_ADDED_TABLES : []));
|
|
306
|
+
}, () => `Wrote ${rel(sourceFile)}: the database (url: \${${where.variable}}) and the tables to copy, ${demo ? `the demo's ${DEMO_ADDED_TABLES.length} added — mason add ${DEMO_TABLE_TO_ADD} adds the fourth` : "none yet (mason add)"} — commit it`);
|
|
273
307
|
}
|
|
274
308
|
if (where.line !== null) {
|
|
275
309
|
const envLine = where.line;
|
|
@@ -307,13 +341,18 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
307
341
|
return failed("init", { message: first ?? e.message, code: e.exit === 2 ? "usage" : "failed", exit: e.exit, fix: rest.length ? rest.map((l) => l.replace(/^\s*- /, "")).join("\n") : undefined,
|
|
308
342
|
why: parsed.source.remote ? undefined : `the database's URL is ${db}${from}` }, { json, io });
|
|
309
343
|
}
|
|
310
|
-
const next = [
|
|
344
|
+
const next = demo ? [
|
|
345
|
+
["mason start", "the demo database and Mason, in the background: its tables copied, then kept current"],
|
|
346
|
+
[`mason add ${DEMO_TABLE_TO_ADD}`, "the demo's fourth table, once Mason is up: watch its copy begin"],
|
|
347
|
+
["mason status", "the copy keeping up with the demo's changes"],
|
|
348
|
+
["mason user add <name>", "a user for psql or a BI tool: its password and URL, printed once"],
|
|
349
|
+
] : [
|
|
311
350
|
["mason start", "Mason in the background, for this folder"],
|
|
312
351
|
["mason tables", "the database's tables and their columns"],
|
|
313
352
|
["mason add <table>", "copy a table into Mason"],
|
|
314
353
|
];
|
|
315
354
|
if (json) {
|
|
316
|
-
say(jsonText(INIT, { name, root, folder: path.relative(root, folder) || ".", config: path.join(root, STATE_DIR, "config.json"),
|
|
355
|
+
say(jsonText(INIT, { name, root, folder: path.relative(root, folder) || ".", config: path.join(root, STATE_DIR, "config.json"), demo,
|
|
317
356
|
source: { name: path.basename(sourceFile).replace(/\.ya?ml$/, ""), file: sourceFile, url: keptFile !== null && !("problems" in keptFile) ? ("env" in keptFile.url ? `\${${keptFile.url.env}}` : maskedUrl(keptFile.url.literal)) : `\${${where.variable}}`,
|
|
318
357
|
env: where.line === null ? null : maskedLine(where.line) },
|
|
319
358
|
database, warnings: notes }, next));
|
|
@@ -321,6 +360,7 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
321
360
|
}
|
|
322
361
|
for (const n of notes)
|
|
323
362
|
say(` ! ${n}`);
|
|
324
|
-
say(endText({ tips: [
|
|
363
|
+
say(endText({ tips: [demo ? `The demo database runs inside mason start, read by polling: a new order every few seconds; ${DEMO_TABLE_TO_ADD} is left for you to add; mason destroy removes it with the copy`
|
|
364
|
+
: "Mason only reads your database: no setting, slot or publication is needed or made"], next }, st));
|
|
325
365
|
return 0;
|
|
326
366
|
}
|
package/dist/src/cli/machine.js
CHANGED
|
@@ -26,7 +26,8 @@ export const DESTROY_USAGE = `usage: mason destroy [--json]
|
|
|
26
26
|
|
|
27
27
|
mason stop keeps the copy and the state, and the next mason start resumes in seconds; mason destroy frees the disk, and
|
|
28
28
|
the next mason start begins again. In replication mode the slot (and Mason's own publication) are dropped from the
|
|
29
|
-
database too: they hold its WAL for as long as they exist.
|
|
29
|
+
database too: they hold its WAL for as long as they exist. A demo project's database (mason init demo, in .mason/demo/)
|
|
30
|
+
goes with the copy: the next mason start makes and seeds it again.
|
|
30
31
|
|
|
31
32
|
--json as data: { removed: [{ name, removed: [what], error }], next }
|
|
32
33
|
|
|
@@ -101,7 +102,9 @@ export async function removeProject(p, o = {}) {
|
|
|
101
102
|
}
|
|
102
103
|
// the users (src/duck/users.ts) are logins a BI tool keeps, like the API key their passwords are made with, and the
|
|
103
104
|
// self-signed certificate is the fingerprint a tool pinned (the review of the cut: a new one at the next
|
|
104
|
-
// start, and config.selfSignedTls stale): the next start copies again, and every tool connects as before
|
|
105
|
+
// start, and config.selfSignedTls stale): the next start copies again, and every tool connects as before. A demo
|
|
106
|
+
// project's database (`.mason/demo/`, src/cli/demo.ts) is state like the copy: it goes, and config.demo stays, so the
|
|
107
|
+
// next start makes and seeds it again
|
|
105
108
|
const kept = new Set(["config.json", USERS_FILE, SELF_SIGNED_FILES.cert, SELF_SIGNED_FILES.key]);
|
|
106
109
|
const gone = fs.existsSync(p.dir) ? fs.readdirSync(p.dir).filter((x) => !kept.has(x)) : [];
|
|
107
110
|
for (const x of gone)
|
|
@@ -151,7 +154,9 @@ export async function destroy(argv, deps = {}) {
|
|
|
151
154
|
return failed("destroy", { message: `${name}: ${result.error}`, code: "failed" }, { json: false, io });
|
|
152
155
|
out(withEnd(reportText({
|
|
153
156
|
headline: result.removed.length ? `Removed ${name}` : `Nothing was left of ${name} to remove`,
|
|
154
|
-
details: [...(result.removed.length ? [["removed", result.removed.join(", ")]] : []), ["kept", `your files in ${t.project.folder}, .mason/config.json and the users`
|
|
157
|
+
details: [...(result.removed.length ? [["removed", result.removed.join(", ")]] : []), ["kept", `your files in ${t.project.folder}, .mason/config.json and the users` +
|
|
158
|
+
// the demo database is state like the copy (src/cli/demo.ts): it goes, and config.demo stays to make it again
|
|
159
|
+
(t.project.config.demo === true ? ` — not the demo database, which went with the copy: the next mason start seeds it again` : "")]],
|
|
155
160
|
}), { next }));
|
|
156
161
|
return 0;
|
|
157
162
|
}
|
package/dist/src/cli/main.js
CHANGED
|
@@ -32,7 +32,7 @@ async function main(argv) {
|
|
|
32
32
|
return 0;
|
|
33
33
|
}
|
|
34
34
|
const cmd = COMMANDS.find((c) => c.name === command);
|
|
35
|
-
const json = wantsJson(args);
|
|
35
|
+
const json = wantsJson(args, cmd);
|
|
36
36
|
// the commands' old names: said, never run as different ones
|
|
37
37
|
const RENAMED = {
|
|
38
38
|
dev: { message: "mason dev is now mason start", next: [["mason init <postgres url>", "once, in your app's folder"], ["mason start", "Mason in the background"]] },
|
|
@@ -76,7 +76,7 @@ main(process.argv.slice(2)).then(exitWhenWritten, (e) => {
|
|
|
76
76
|
// MASON_DEBUG=1 prints where it happened, for the bug report it deserves. The same shape as every failure, JSON under --json.
|
|
77
77
|
const message = e instanceof Error ? e.message : String(e);
|
|
78
78
|
const [command, ...args] = process.argv.slice(2);
|
|
79
|
-
const code = failed(command ?? "", { message, code: "failed", fix: process.env.MASON_DEBUG ? undefined : "this is a bug in mason: MASON_DEBUG=1 prints where it happened, for the report" }, { json: wantsJson(args) });
|
|
79
|
+
const code = failed(command ?? "", { message, code: "failed", fix: process.env.MASON_DEBUG ? undefined : "this is a bug in mason: MASON_DEBUG=1 prints where it happened, for the report" }, { json: wantsJson(args, COMMANDS.find((c) => c.name === command)) });
|
|
80
80
|
if (process.env.MASON_DEBUG)
|
|
81
81
|
process.stderr.write(`${e.stack ?? ""}\n`);
|
|
82
82
|
exitWhenWritten(code);
|