@mason-data/cli 0.8.0 → 0.10.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 +19 -8
- package/dist/src/api/error.js +4 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +166 -32
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/proxy.js +129 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +527 -103
- package/dist/src/api/postgres/translate.js +47 -2
- package/dist/src/cli/background.js +18 -19
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/command.js +337 -15
- package/dist/src/cli/commands.js +458 -169
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +2 -2
- package/dist/src/cli/deploy.js +680 -0
- package/dist/src/cli/dev.js +148 -27
- package/dist/src/cli/doctor.js +122 -36
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +116 -16
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +154 -33
- package/dist/src/cli/login.js +292 -0
- package/dist/src/cli/machine.js +125 -40
- package/dist/src/cli/main.js +59 -10
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +17 -14
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +1 -1
- package/dist/src/cli/pin.js +6 -2
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +130 -39
- package/dist/src/cli/project.js +100 -1
- package/dist/src/cli/readiness.js +42 -6
- package/dist/src/cli/registry.js +89 -36
- package/dist/src/cli/replica-hosted.js +1324 -0
- package/dist/src/cli/replica.js +114 -0
- package/dist/src/cli/replication-mode.js +5 -0
- package/dist/src/cli/request-filter.js +16 -6
- package/dist/src/cli/roles.js +2 -1
- package/dist/src/cli/run-server.js +179 -9
- package/dist/src/cli/secret-input.js +193 -0
- package/dist/src/cli/security.js +57 -10
- package/dist/src/cli/serve.js +65 -9
- package/dist/src/cli/share-run.js +51 -8
- package/dist/src/cli/share.js +23 -18
- package/dist/src/cli/source-edit.js +10 -4
- package/dist/src/cli/source-role.js +343 -0
- package/dist/src/cli/sources.js +50 -23
- package/dist/src/cli/style.js +16 -1
- package/dist/src/cli/supervise.js +15 -23
- package/dist/src/cli/sync.js +17 -8
- package/dist/src/cli/token-hosted.js +343 -0
- package/dist/src/cli/tunnel.js +1 -1
- package/dist/src/cli/ui-hosted.js +163 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +162 -17
- package/dist/src/cli/usage.js +19 -15
- package/dist/src/cli/user-hosted.js +364 -0
- package/dist/src/cli/user.js +275 -105
- package/dist/src/demo/seed.js +26 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +39 -8
- package/dist/src/duck/executor-worker.js +2 -2
- package/dist/src/duck/executor.js +6 -5
- package/dist/src/duck/explain.js +4 -0
- package/dist/src/duck/functions.js +448 -49
- package/dist/src/duck/gaps.js +34 -16
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +1 -1
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +4 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +9 -7
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +275 -30
- package/dist/src/duck/publication.js +14 -0
- package/dist/src/duck/requests.js +9 -5
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4567 -583
- package/dist/src/duck/row-filter.js +12 -6
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +294 -68
- package/dist/src/duck/stream.js +9 -5
- package/dist/src/duck/sync.js +602 -126
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +63 -7
- package/dist/src/duck/usage.js +6 -4
- package/dist/src/duck/users.js +110 -9
- package/dist/src/duck/verified.js +159 -2
- package/dist/src/duck/views.js +4 -3
- package/dist/src/lib/cidr.js +177 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +13 -4
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +387 -49
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +84 -19
- package/dist/src/web/app.js +9 -4
- package/dist/src/web/pages/status.js +178 -13
- package/dist/src/web/pages/syncs.js +1 -1
- package/dist/src/web/pages/tables.js +8 -3
- package/dist/src/web/pages/users.js +4 -3
- package/dist/src/web/shared.js +19 -3
- package/package.json +8 -7
package/dist/src/cli/command.js
CHANGED
|
@@ -7,24 +7,185 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - `mason <name> --help` and `mason help <name>` print its usage and exit 0;
|
|
9
9
|
* - the usage starts `usage: mason <name>`, and shows at least one example (an indented `mason <name> …` line);
|
|
10
|
+
* - it reads in the order a person needs it: the usage line, a short description, the examples, the flags (one
|
|
11
|
+
* entry each); then the long prose, and a long --json shape last (`The --json answer: …`, its entry saying so);
|
|
10
12
|
* - every option the parser takes is in the usage, by its `--name` (and `-x` when it has one);
|
|
11
13
|
* - `mason --help` lists it with its summary;
|
|
12
14
|
* - a wrong option prints what is wrong, then the usage, and exits 2.
|
|
13
15
|
*/
|
|
14
16
|
import { parseArgs } from "node:util";
|
|
15
17
|
import { err as styleErr, out as styleOut, PLAIN, wrap } from "./style.js";
|
|
18
|
+
import { closest } from "../lib/did-you-mean.js";
|
|
19
|
+
/**
|
|
20
|
+
* A LINE THAT HOLDS A HOSTED SERVICE'S TEXT IS BROKEN BY THE CLI, NEVER BY THE TERMINAL, AT EVERY WIDTH (PR 481's
|
|
21
|
+
* class rounds: a value sized to the terminal's width made the terminal start a visual line at column 0 with a forged
|
|
22
|
+
* `Next` heading, a step or a row; then the same under 40 columns, where no style wraps). src/cli/hosted.ts makes every
|
|
23
|
+
* value one line with no run of spaces (`serviceText`); the style a command gives its output when that output holds
|
|
24
|
+
* such a value (`serviceStyle`) makes every renderer here break every line it writes, at the terminal's own width
|
|
25
|
+
* (`Style.columns`, read when the output is drawn, whatever it is), so no line ever reaches the terminal's edge:
|
|
26
|
+
*
|
|
27
|
+
* - a line takes at most width - 1 columns (one column at width 1), a character outside ASCII counted as two;
|
|
28
|
+
* - it breaks at a space where one is in reach, else within the word; the headline, a row's label and a step's
|
|
29
|
+
* command are broken as any text is (`strictLines`, the last pass of every renderer: `strict`);
|
|
30
|
+
* - every continuation starts at `continuationAt(width)`: CONTINUATION (4) wherever the width leaves 4 columns after
|
|
31
|
+
* it, else 1 (3 to 8 columns), else 0 (1 or 2 columns, where one character is a line). The CLI's own lines start at
|
|
32
|
+
* 0 (a heading, a failure, a tip) or 2 (a row, a step), never at 1 or 4, so a continuation can never be taken for
|
|
33
|
+
* one; at 1 or 2 columns nothing the CLI starts a line with fits in a line, so none can be forged either;
|
|
34
|
+
* - a value's own column (a row's, a step's description) is kept while it leaves `MIN_ROOM` columns, else its lines
|
|
35
|
+
* continue at continuationAt(width), never one character a line under an indent wider than the terminal.
|
|
36
|
+
*
|
|
37
|
+
* So what a person sees begin a line is always the CLI's own. A pipe, a file and `--json` are unwrapped as ever (no
|
|
38
|
+
* terminal width): what an agent parses never changes.
|
|
39
|
+
*/
|
|
40
|
+
export const CONTINUATION = 4;
|
|
41
|
+
/** The fewest columns a value's own column must leave it before its lines continue there. */
|
|
42
|
+
const MIN_ROOM = 12;
|
|
43
|
+
export const serviceStyle = (s) => ({ ...s, untrusted: true });
|
|
44
|
+
/** The columns a word takes, counted so a line never takes more than the terminal has: a character outside ASCII as two. */
|
|
45
|
+
const columns = (w) => { let n = 0; for (const ch of w)
|
|
46
|
+
n += ch.codePointAt(0) <= 0x7e ? 1 : 2; return n; };
|
|
47
|
+
/** Where a continuation starts on a terminal `width` columns wide (above). */
|
|
48
|
+
export const continuationAt = (width) => lineLimit(width) - CONTINUATION >= 4 ? CONTINUATION : width >= 3 ? 1 : 0;
|
|
49
|
+
/** The most columns one line may take on a terminal `width` wide: one short of its edge, never none. */
|
|
50
|
+
const lineLimit = (width) => Math.max(1, width - 1);
|
|
51
|
+
/** The terminal width a style that holds a service's text breaks at; null when it is no terminal (or holds none). */
|
|
52
|
+
const strictWidth = (s) => s.untrusted ? s.columns ?? s.width : null;
|
|
53
|
+
/** `text` broken at `width` as `serviceStyle` asks (above), its first line after `firstUsed` columns of prefix: every
|
|
54
|
+
* line within the limit, a long word broken within, every line after the first at `indent` while that leaves MIN_ROOM,
|
|
55
|
+
* else at continuationAt(width). */
|
|
56
|
+
export function strictWrap(text, width, indent, firstUsed) {
|
|
57
|
+
const limit = lineLimit(width);
|
|
58
|
+
const cont = Math.max(indent, CONTINUATION) + MIN_ROOM <= limit ? Math.max(indent, CONTINUATION) : continuationAt(width);
|
|
59
|
+
const lines = [];
|
|
60
|
+
let line = "", taken = 0, used = firstUsed;
|
|
61
|
+
const room = () => Math.max(1, limit - used);
|
|
62
|
+
const breakLine = () => { lines.push(line); line = ""; taken = 0; used = cont; };
|
|
63
|
+
// a prefix that leaves no room: the text starts on the next line
|
|
64
|
+
if (limit - used < 1)
|
|
65
|
+
breakLine();
|
|
66
|
+
for (const word of text.split(" ")) {
|
|
67
|
+
if (word === "")
|
|
68
|
+
continue;
|
|
69
|
+
const wc = columns(word);
|
|
70
|
+
if (line && taken + 1 + wc <= room()) {
|
|
71
|
+
line += ` ${word}`;
|
|
72
|
+
taken += 1 + wc;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (line)
|
|
76
|
+
breakLine();
|
|
77
|
+
if (wc <= room()) {
|
|
78
|
+
line = word;
|
|
79
|
+
taken = wc;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const ch of word) {
|
|
83
|
+
const cc = columns(ch);
|
|
84
|
+
if (taken + cc > room() && line)
|
|
85
|
+
breakLine();
|
|
86
|
+
line += ch;
|
|
87
|
+
taken += cc;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
lines.push(line);
|
|
91
|
+
return lines.join(`\n${" ".repeat(cont)}`);
|
|
92
|
+
}
|
|
93
|
+
/** A colour code, which takes no column. */
|
|
94
|
+
const SGR = /^\x1b\[[0-9;]*m/;
|
|
95
|
+
/**
|
|
96
|
+
* THE LAST PASS OF EVERY RENDERER under serviceStyle: each line of `text` that would reach the terminal's edge broken
|
|
97
|
+
* (above) — at its last space in reach, else within a word — its continuations at continuationAt(width), its colour codes
|
|
98
|
+
* taking no column and never split. A line within the limit is left as it is.
|
|
99
|
+
*/
|
|
100
|
+
export function strictLines(text, width) {
|
|
101
|
+
const limit = lineLimit(width), cont = continuationAt(width);
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const whole of text.split("\n")) {
|
|
104
|
+
const tokens = [];
|
|
105
|
+
for (let i = 0; i < whole.length;) {
|
|
106
|
+
const sgr = SGR.exec(whole.slice(i));
|
|
107
|
+
if (sgr) {
|
|
108
|
+
tokens.push({ t: sgr[0], w: 0 });
|
|
109
|
+
i += sgr[0].length;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const ch = String.fromCodePoint(whole.codePointAt(i));
|
|
113
|
+
tokens.push({ t: ch, w: columns(ch) });
|
|
114
|
+
i += ch.length;
|
|
115
|
+
}
|
|
116
|
+
if (tokens.reduce((n, k) => n + k.w, 0) <= limit) {
|
|
117
|
+
out.push(whole);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// the line's own indent, kept while it leaves room for a word, else the continuation's
|
|
121
|
+
let lead = 0;
|
|
122
|
+
while (tokens[lead]?.t === " ")
|
|
123
|
+
lead++;
|
|
124
|
+
const indent = (n) => Array.from({ length: n }, () => ({ t: " ", w: 1 }));
|
|
125
|
+
let line = indent(lead + MIN_ROOM <= limit ? lead : Math.min(lead, cont));
|
|
126
|
+
let taken = line.length, start = line.length;
|
|
127
|
+
// each token placed in turn; past the limit the line is broken at its last space past the indent, and what followed
|
|
128
|
+
// the space is placed again from there (so it is broken again if it must), else broken here, within the word
|
|
129
|
+
let placed = []; // the source index of each of line's tokens past its indent
|
|
130
|
+
for (let i = lead; i < tokens.length; i++) {
|
|
131
|
+
const k = tokens[i];
|
|
132
|
+
if (taken + k.w > limit && taken > start) {
|
|
133
|
+
let at = -1;
|
|
134
|
+
for (let j = placed.length - 1; j > 0; j--)
|
|
135
|
+
if (tokens[placed[j]].t === " ") {
|
|
136
|
+
at = j;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
const kept = at >= 0 ? line.slice(0, start + at) : line;
|
|
140
|
+
while (kept.length > start && kept[kept.length - 1].t === " ")
|
|
141
|
+
kept.pop();
|
|
142
|
+
out.push(kept.map((x) => x.t).join(""));
|
|
143
|
+
if (at >= 0)
|
|
144
|
+
i = placed[at]; // the loop's i++ places what followed the space
|
|
145
|
+
else
|
|
146
|
+
i--; // this token begins the next line
|
|
147
|
+
line = indent(cont);
|
|
148
|
+
taken = cont;
|
|
149
|
+
start = cont;
|
|
150
|
+
placed = [];
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (k.t === " " && taken === start && start > 0)
|
|
154
|
+
continue; // no space at a continuation's start
|
|
155
|
+
line.push(k);
|
|
156
|
+
placed.push(i);
|
|
157
|
+
taken += k.w;
|
|
158
|
+
}
|
|
159
|
+
// a continuation with nothing after its indent (the line ended in spaces) is no line
|
|
160
|
+
if (taken > start || start === 0)
|
|
161
|
+
out.push(line.map((x) => x.t).join(""));
|
|
162
|
+
}
|
|
163
|
+
return out.join("\n");
|
|
164
|
+
}
|
|
165
|
+
/** A renderer's output, as serviceStyle asks it on a terminal (strictLines), else as it is. */
|
|
166
|
+
export const strict = (text, s) => { const w = strictWidth(s); return w === null ? text : strictLines(text, w); };
|
|
167
|
+
/** A renderer's wrap: the strict one for a style that holds a service's text on a terminal, else style.ts's. */
|
|
168
|
+
const fit = (text, s, indent, firstUsed = indent) => {
|
|
169
|
+
const w = strictWidth(s);
|
|
170
|
+
return w === null ? wrap(text, s.width, indent, firstUsed) : strictWrap(text, w, indent, firstUsed);
|
|
171
|
+
};
|
|
16
172
|
const HELP = { help: { type: "boolean", short: "h" } };
|
|
17
173
|
/**
|
|
18
174
|
* A command line read by its spec: the values and positionals; or the exit code when it answered itself — 0 after
|
|
19
|
-
* printing the usage for `--help
|
|
175
|
+
* printing the usage for `--help` (its spec as data for `--help --json`), 2 after saying what is wrong with the line,
|
|
176
|
+
* then the usage.
|
|
20
177
|
*/
|
|
21
178
|
export function parseCommand(spec, argv) {
|
|
179
|
+
if (specAsked(spec, argv)) {
|
|
180
|
+
process.stdout.write(`${specAnswer(spec)}\n`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
22
183
|
let parsed;
|
|
23
184
|
try {
|
|
24
185
|
parsed = parseArgs({ args: argv, options: { ...spec.options, ...HELP }, allowPositionals: true });
|
|
25
186
|
}
|
|
26
187
|
catch (e) {
|
|
27
|
-
return failed(spec.name, {
|
|
188
|
+
return failed(spec.name, { ...parseProblem(spec, e), code: "usage", usage: synopsis(spec), next: [[`mason ${spec.name} --help`, "its usage, with every option"]] }, { json: wantsJson(argv, spec) });
|
|
28
189
|
}
|
|
29
190
|
if (parsed.values.help) {
|
|
30
191
|
process.stdout.write(spec.usage);
|
|
@@ -32,6 +193,140 @@ export function parseCommand(spec, argv) {
|
|
|
32
193
|
}
|
|
33
194
|
return { values: parsed.values, positionals: parsed.positionals };
|
|
34
195
|
}
|
|
196
|
+
/** A LIST FLAG's value, one comma list however it was written (`multiple: true` in the spec): `--columns id,status`
|
|
197
|
+
* and `--columns id --columns status` mean the same, `id,status`; undefined when it was not given. */
|
|
198
|
+
export function listFlag(v) {
|
|
199
|
+
if (typeof v === "string")
|
|
200
|
+
return v;
|
|
201
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string").join(",") : undefined;
|
|
202
|
+
}
|
|
203
|
+
/** A command that takes no argument, given one (`mason stop myproject` stopped this folder's project, not that one):
|
|
204
|
+
* a wrong line, its usage's synopsis. Null when there is none. */
|
|
205
|
+
export function noArguments(spec, positionals, json) {
|
|
206
|
+
if (!positionals.length)
|
|
207
|
+
return null;
|
|
208
|
+
return failed(spec.name, { message: `it takes no argument (${positionals.join(" ")}): a project is its folder`, code: "usage",
|
|
209
|
+
fix: `run it in the project's folder: cd <folder> && mason ${spec.name}, or mason -C <folder> ${spec.name}`, usage: synopsis(spec),
|
|
210
|
+
next: [[`mason ${spec.name} --help`, "its usage, with every option"]] }, { json });
|
|
211
|
+
}
|
|
212
|
+
/** Whether a line asks for the command's spec as data: `--help` (or `-h`) and `--json` on a line that parses — every
|
|
213
|
+
* command, whether or not it takes --json itself (mason help, mason mcp): the spec is what an agent reads first. A line
|
|
214
|
+
* that does not parse is no such question: parseCommand's own parse says what is wrong with it. */
|
|
215
|
+
function specAsked(spec, argv) {
|
|
216
|
+
if (!argv.includes("--json") || !(argv.includes("--help") || argv.includes("-h")))
|
|
217
|
+
return false;
|
|
218
|
+
try {
|
|
219
|
+
const options = { ...spec.options, ...HELP, json: { type: "boolean" } };
|
|
220
|
+
const p = parseArgs({ args: [...argv], options, allowPositionals: true });
|
|
221
|
+
return p.values["help"] === true && p.values["json"] === true;
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
// not swallowed: the same line goes on to parseCommand's own parse, which says this error in the CLI's words
|
|
225
|
+
if (!(e instanceof TypeError))
|
|
226
|
+
throw e;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** The keys of a command's spec as data (`mason <name> --help --json`, `mason help <name> --json`). */
|
|
231
|
+
export const SPEC_ANSWER_KEYS = ["command", "next"];
|
|
232
|
+
/** What follows a command's spec as data. */
|
|
233
|
+
export const SPEC_NEXT = [["mason help --json", "every command's spec, the guides and the failure codes"]];
|
|
234
|
+
/** A command's spec as its --json answer: `{ command: <spec>, next }`. */
|
|
235
|
+
export const specAnswer = (spec) => jsonText({ ...spec, json: SPEC_ANSWER_KEYS }, { command: specJson(spec) }, SPEC_NEXT);
|
|
236
|
+
/**
|
|
237
|
+
* THE EXIT CODES, a closed set (`mason help agent` lists them, and each command's spec as data names those it can
|
|
238
|
+
* exit with): 124 only for a command TIMEOUT_COMMANDS names.
|
|
239
|
+
*/
|
|
240
|
+
export const EXIT_CODES = {
|
|
241
|
+
0: "done",
|
|
242
|
+
1: "failed, with the reason",
|
|
243
|
+
2: "the command line is wrong (without --json the usage follows)",
|
|
244
|
+
124: "ran out of time (nothing stopped)",
|
|
245
|
+
};
|
|
246
|
+
/** Whether a command can run out of time (exit 124): TIMEOUT_COMMANDS names it. */
|
|
247
|
+
const timesOut = (name) => new RegExp(`\\bmason ${name}\\b`).test(TIMEOUT_COMMANDS);
|
|
248
|
+
export function specJson(spec) {
|
|
249
|
+
const lines = spec.usage.split("\n");
|
|
250
|
+
const end = lines.findIndex((l) => l.trim() === "");
|
|
251
|
+
const forms = lines.slice(0, end < 0 ? lines.length : end).map((l) => l.replace(/^usage:\s*/, "").trim());
|
|
252
|
+
const options = Object.entries({ ...spec.options, ...HELP }).map(([name, d]) => {
|
|
253
|
+
// the placeholder the synopsis writes after it, else the first the usage does
|
|
254
|
+
const placeholder = new RegExp(`--${name}[ =]([^\\s\\[\\]]+)`);
|
|
255
|
+
const value = d.type === "string" ? (forms.map((f) => placeholder.exec(f)?.[1]).find((v) => v !== undefined) ?? placeholder.exec(spec.usage)?.[1] ?? null) : null;
|
|
256
|
+
return { name: `--${name}`, short: d.short ? `-${d.short}` : null, type: d.type, multiple: d.multiple === true, default: d.default ?? null, value };
|
|
257
|
+
});
|
|
258
|
+
const exitCodes = Object.entries(EXIT_CODES).filter(([code]) => code !== "124" || timesOut(spec.name)).map(([code, meaning]) => ({ code: Number(code), meaning }));
|
|
259
|
+
return { name: spec.name, summary: spec.summary, usage: spec.usage, forms: forms.map((f) => ({ synopsis: f, positionals: positionalsOf(f, spec.name) })), options,
|
|
260
|
+
json: spec.json ? [...spec.json] : null, needs: spec.needs ?? null, stdout: spec.stdout ?? null, exitCodes };
|
|
261
|
+
}
|
|
262
|
+
/** The words of a synopsis form that are no option: split at the spaces outside brackets, `mason <name>` and every
|
|
263
|
+
* option dropped — a bracketed group that starts with `-` (`[--json]`, `[-n <lines>]`), and a bare option with its
|
|
264
|
+
* value (`--ssh <user@server>`). */
|
|
265
|
+
function positionalsOf(form, name) {
|
|
266
|
+
const words = [];
|
|
267
|
+
let word = "", depth = 0;
|
|
268
|
+
for (const ch of form) {
|
|
269
|
+
if (ch === "[" || ch === "<")
|
|
270
|
+
depth++;
|
|
271
|
+
if (ch === "]" || ch === ">")
|
|
272
|
+
depth = Math.max(0, depth - 1);
|
|
273
|
+
if (ch === " " && depth === 0) {
|
|
274
|
+
if (word)
|
|
275
|
+
words.push(word);
|
|
276
|
+
word = "";
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
word += ch;
|
|
280
|
+
}
|
|
281
|
+
if (word)
|
|
282
|
+
words.push(word);
|
|
283
|
+
const rest = words[0] === "mason" && words[1] === name ? words.slice(2) : words;
|
|
284
|
+
const out = [];
|
|
285
|
+
for (let i = 0; i < rest.length; i++) {
|
|
286
|
+
const w = rest[i];
|
|
287
|
+
// an option is a dash and a letter (`[--json]`, `-n`); a lone `-` (`mason init -`, stdin) is a positional
|
|
288
|
+
if (/^\[--?[A-Za-z]/.test(w))
|
|
289
|
+
continue;
|
|
290
|
+
if (/^--?[A-Za-z]/.test(w)) {
|
|
291
|
+
if (rest[i + 1]?.startsWith("<"))
|
|
292
|
+
i++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
out.push(w);
|
|
296
|
+
}
|
|
297
|
+
// an alternative whose other side was an option (`<org>/<name> | --replica <org>/<name>`) leaves no `|` behind
|
|
298
|
+
return out.filter((w, i) => w !== "|" || (i > 0 && i < out.length - 1));
|
|
299
|
+
}
|
|
300
|
+
/** A command's synopsis: its usage's first lines, up to the first blank one (`usage: mason <name> …` and its other
|
|
301
|
+
* forms), then where the rest is. What a wrong line is answered with: the whole usage is for `--help` (94 lines for
|
|
302
|
+
* `mason status`, and the mistake scrolled away above it). */
|
|
303
|
+
export function synopsis(spec) {
|
|
304
|
+
const lines = spec.usage.split("\n");
|
|
305
|
+
const end = lines.findIndex((l) => l.trim() === "");
|
|
306
|
+
return `${lines.slice(0, end < 0 ? lines.length : end).join("\n")}\n\nmason ${spec.name} --help for more\n`;
|
|
307
|
+
}
|
|
308
|
+
/** What is wrong with a line the parser refused, in the CLI's words (never Node's, whose advice about `--` misleads):
|
|
309
|
+
* an option it does not take, with the closest it does (`didYouMean` over the spec's options and `--help`); one that
|
|
310
|
+
* needs a value and has none; one that takes none and was given one. */
|
|
311
|
+
function parseProblem(spec, e) {
|
|
312
|
+
const options = { ...spec.options, ...HELP };
|
|
313
|
+
const names = Object.keys(options).map((n) => `--${n}`);
|
|
314
|
+
const shorts = Object.entries(options).flatMap(([n, d]) => d.short ? [[`-${d.short}`, `--${n}`]] : []);
|
|
315
|
+
const said = /'(-[^'\s,]*)/.exec(e.message)?.[1];
|
|
316
|
+
const flag = said === undefined ? undefined : said.startsWith("--") ? said.replace(/=.*$/, "") : said;
|
|
317
|
+
if (e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" && flag !== undefined) {
|
|
318
|
+
const near = closest(flag, [...names, ...shorts.map(([s]) => s)]);
|
|
319
|
+
return { message: `there is no option ${flag}${near !== null ? ` — did you mean \`${near}\`?` : ""}`,
|
|
320
|
+
fix: `its options are ${names.join(", ")}` };
|
|
321
|
+
}
|
|
322
|
+
if (e.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE" && flag !== undefined) {
|
|
323
|
+
const long = shorts.find(([s]) => s === flag)?.[1] ?? flag;
|
|
324
|
+
const named = long === flag ? long : `${flag} (${long})`;
|
|
325
|
+
const takes = options[long.slice(2)]?.type === "string";
|
|
326
|
+
return takes ? { message: `${named} needs a value: ${flag} <value>` } : { message: `${named} takes no value: write ${flag} alone` };
|
|
327
|
+
}
|
|
328
|
+
return { message: e.message };
|
|
329
|
+
}
|
|
35
330
|
/** The column a step's `what` starts in, after the two-space indent: wide enough for `mason add orders --columns …`. */
|
|
36
331
|
const NEXT_WIDTH = 44;
|
|
37
332
|
/**
|
|
@@ -46,25 +341,30 @@ export function nextBlock(steps, s = styleOut()) {
|
|
|
46
341
|
if (!what)
|
|
47
342
|
return ` ${s.cyan(command)}`;
|
|
48
343
|
const used = 2 + Math.max(command.length, NEXT_WIDTH) + 2;
|
|
49
|
-
|
|
344
|
+
// a terminal too narrow for the column under serviceStyle: what it does on the next line, at the continuation
|
|
345
|
+
const w = strictWidth(s);
|
|
346
|
+
if (w !== null && 2 + NEXT_WIDTH + 2 + MIN_ROOM > lineLimit(w))
|
|
347
|
+
return ` ${s.cyan(command)}\n${" ".repeat(continuationAt(w))}${s.dim(fit(what, s, continuationAt(w)))}`;
|
|
348
|
+
return ` ${s.cyan(command.padEnd(NEXT_WIDTH))} ${s.dim(fit(what, s, 2 + NEXT_WIDTH + 2, used))}`;
|
|
50
349
|
};
|
|
51
|
-
return ["", s.bold("Next:"), ...steps.map(step)].join("\n");
|
|
350
|
+
return strict(["", s.bold("Next:"), ...steps.map(step)].join("\n"), s);
|
|
52
351
|
}
|
|
53
352
|
export function reportText(r, s = styleOut()) {
|
|
54
|
-
|
|
353
|
+
// a headline is a terminal's to wrap, unless it holds a service's text: then the CLI breaks it (above)
|
|
354
|
+
const lines = [s.bold(strictWidth(s) !== null ? fit(r.headline, s, CONTINUATION, 0) : r.headline)];
|
|
55
355
|
const w = Math.max(0, ...(r.details ?? []).map(([label]) => label.length));
|
|
56
356
|
for (const [label, value] of r.details ?? [])
|
|
57
|
-
lines.push(` ${s.dim(label.padEnd(w))} ${
|
|
58
|
-
return withEnd(lines.join("\n"), r, s);
|
|
357
|
+
lines.push(` ${s.dim(label.padEnd(w))} ${fit(value, s, 2 + w + 3)}`);
|
|
358
|
+
return strict(withEnd(lines.join("\n"), r, s), s);
|
|
59
359
|
}
|
|
60
360
|
/**
|
|
61
361
|
* The end of a command's output, to print on its own (out(endText(…))): its tips, then what to run next, each part after
|
|
62
362
|
* a blank line; "" when it has neither. withEnd puts it after text a command has not printed yet.
|
|
63
363
|
*/
|
|
64
364
|
export function endText(r, s = styleOut()) {
|
|
65
|
-
const tips = r.tips?.length ? `\n${r.tips.map((t) => s.dim(`Tip: ${
|
|
365
|
+
const tips = r.tips?.length ? `\n${r.tips.map((t) => s.dim(`Tip: ${fit(t, s, 5)}`)).join("\n")}` : "";
|
|
66
366
|
const next = r.next?.length ? nextBlock(r.next, s) : "";
|
|
67
|
-
return tips && next ? `${tips}\n${next}` : tips || next;
|
|
367
|
+
return strict(tips && next ? `${tips}\n${next}` : tips || next, s);
|
|
68
368
|
}
|
|
69
369
|
export function withEnd(text, r, s = styleOut()) {
|
|
70
370
|
const end = endText(r, s);
|
|
@@ -97,8 +397,11 @@ export function jsonText(spec, value, next = []) {
|
|
|
97
397
|
throw new Error(`mason ${spec.name}: its json keys must end with next (every --json answer carries what to run next)`);
|
|
98
398
|
return JSON.stringify({ ...value, next: nextJson(next) }, null, 2);
|
|
99
399
|
}
|
|
400
|
+
/** The commands that can run out of time, exit 124 (code `timeout`): the one list the codes and the agent's guide say. */
|
|
401
|
+
export const TIMEOUT_COMMANDS = "mason wait, mason deploy, mason replica create, mason replica delete, mason login (its code expired), mason share and mason ui --ssh (no address or answer in time)";
|
|
100
402
|
/**
|
|
101
|
-
* THE CODES A FAILURE CARRIES, a closed set a program can branch on (`mason help agent` lists them).
|
|
403
|
+
* THE CODES A FAILURE CARRIES, a closed set a program can branch on (`mason help agent` lists them): `Failure.code` is
|
|
404
|
+
* one of them, so a code outside the set does not compile.
|
|
102
405
|
*/
|
|
103
406
|
export const FAILURE_CODES = {
|
|
104
407
|
usage: "the command line is wrong: a flag, an argument, a value (exit 2; the usage follows for a person)",
|
|
@@ -108,12 +411,16 @@ export const FAILURE_CODES = {
|
|
|
108
411
|
bad_roles: "the project's roles file (mason/roles.yml) names something wrong: a table the sources file does not sync, a column the synced table lacks, a role with no table; the file, the key and the way out are said",
|
|
109
412
|
not_running: "Mason is not running for the project (mason start)",
|
|
110
413
|
unreachable: "Mason is running but does not answer (mason logs says why)",
|
|
414
|
+
not_answering: "Mason's process is alive and does not answer its commands, hung: mason stop ends it (its synced data stays)",
|
|
111
415
|
not_found: "a name nothing has: a table, a column, a user",
|
|
112
416
|
nothing_yet: "nothing to work on yet: no table added (the next steps say what adds one)",
|
|
113
417
|
refused: "Mason refused what was asked, with the reason",
|
|
114
418
|
failed: "the command tried and could not finish: the reason, and what to do",
|
|
115
|
-
timeout:
|
|
419
|
+
timeout: `ran out of time — ${TIMEOUT_COMMANDS}; nothing stopped (exit 124)`,
|
|
116
420
|
};
|
|
421
|
+
/** A code another process sent (the run's refusal, the server's Mason over ssh) as one of ours: its own when it is in
|
|
422
|
+
* the set, else `otherwise` — a code outside it never reaches a program that branches on the set. */
|
|
423
|
+
export const failureCode = (code, otherwise) => typeof code === "string" && Object.hasOwn(FAILURE_CODES, code) ? code : otherwise;
|
|
117
424
|
/** A failure thrown from deep in a command, for main.ts to print: the one way a failure leaves a command without a return code. */
|
|
118
425
|
export class CommandFailed extends Error {
|
|
119
426
|
failure;
|
|
@@ -127,17 +434,17 @@ export function failureText(cmd, f, s = styleErr()) {
|
|
|
127
434
|
const head = cmd ? `mason ${cmd}:` : "mason:";
|
|
128
435
|
// the checklist's marks: ✘ on a terminal (beside its ✔), ✗ in plain text (beside its ✓); a style that colours is one
|
|
129
436
|
const mark = s.red("x") === "x" ? "✗" : "✘";
|
|
130
|
-
const lines = [`${s.red(mark)} ${s.red(head)} ${s.bold(
|
|
437
|
+
const lines = [`${s.red(mark)} ${s.red(head)} ${s.bold(fit(f.message, s, 2, (strictWidth(s) === null ? mark.length : columns(mark)) + 1 + head.length + 1))}`];
|
|
131
438
|
const rows = [...(f.why ? [["why", f.why]] : []), ...(f.fix ? [["fix", f.fix]] : [])];
|
|
132
439
|
// a value of several lines (a list of failures, an entry to paste) keeps its lines, each under the first
|
|
133
440
|
for (const [label, value] of rows)
|
|
134
|
-
lines.push(` ${s.dim(label)} ${value.split("\n").map((l, i) => i ? `${" ".repeat(8)}${
|
|
441
|
+
lines.push(` ${s.dim(label)} ${value.split("\n").map((l, i) => i ? `${" ".repeat(8)}${fit(l, s, 8)}` : fit(l, s, 8)).join("\n")}`);
|
|
135
442
|
let text = lines.join("\n");
|
|
136
443
|
if (f.next?.length)
|
|
137
444
|
text += `\n${nextBlock(f.next, s)}`;
|
|
138
445
|
if (f.usage)
|
|
139
446
|
text += `\n\n${f.usage.replace(/\n$/, "")}`;
|
|
140
|
-
return text;
|
|
447
|
+
return strict(text, s);
|
|
141
448
|
}
|
|
142
449
|
/** A failure's `error` as --json carries it: on its own (failureJson), or beside a command's own keys when it did part of
|
|
143
450
|
* its work (mason add: the tables added; mason wait: the tables' state). */
|
|
@@ -154,7 +461,7 @@ export function failureJson(f) {
|
|
|
154
461
|
* data never has to read prose. tests/conventions.test.ts refuses a `mason <cmd>: …` written to stderr any other way.
|
|
155
462
|
*/
|
|
156
463
|
export function failed(cmd, f, o) {
|
|
157
|
-
const n = asFailure(f);
|
|
464
|
+
const n = usageWayOut(cmd, asFailure(f));
|
|
158
465
|
const io = o.io ?? STDIO;
|
|
159
466
|
if (o.json)
|
|
160
467
|
io.out(failureJson(n));
|
|
@@ -162,6 +469,21 @@ export function failed(cmd, f, o) {
|
|
|
162
469
|
io.err(failureText(cmd, n, o.style ?? failureStyle(io)));
|
|
163
470
|
return exitOf(n);
|
|
164
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* A WRONG COMMAND LINE ALWAYS SAYS WHERE ITS USAGE IS, whatever raised it (the parse, or a command's own check, many of
|
|
474
|
+
* which answered with `next: []`): its next steps end with `mason <cmd> --help` (`mason --help` for no command), and it
|
|
475
|
+
* carries a fix, its own, else the way to its usage. Decided here, where every failure is said, so a usage failure added
|
|
476
|
+
* later cannot leave it out.
|
|
477
|
+
*/
|
|
478
|
+
export function usageWayOut(cmd, f) {
|
|
479
|
+
if (f.code !== "usage")
|
|
480
|
+
return f;
|
|
481
|
+
const help = cmd ? `mason ${cmd} --help` : "mason --help";
|
|
482
|
+
// `mason help <cmd>` is the same page, so a failure that names it already says where the usage is
|
|
483
|
+
const said = (c) => c === help || (cmd !== "" && c === `mason help ${cmd}`);
|
|
484
|
+
const next = f.next?.some(([c]) => said(c)) ? f.next : [...(f.next ?? []), [help, cmd ? "its usage, with every option" : "every command"]];
|
|
485
|
+
return { ...f, next, fix: f.fix ?? (cmd ? `write the line as its usage says (mason ${cmd} --help)` : "name a command mason --help lists") };
|
|
486
|
+
}
|
|
165
487
|
/** The style a failure is drawn in: stderr's own when it goes to stderr, plain for an io a test or a caller supplied.
|
|
166
488
|
* By the STREAM, never by "was an io passed" (CLAUDE.md "Two readers, one interface"): mason init defaults its io to
|
|
167
489
|
* STDIO and passed it on, and every failure it drew was plain — no red head, no dim labels — on a terminal. */
|