@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/ui.js
CHANGED
|
@@ -2,23 +2,40 @@
|
|
|
2
2
|
* `mason ui` — THE WEB PAGE OF THE RUNNING MASON, IN THE BROWSER (src/web/, docs/WEB-UI.md): the Overview (what `mason
|
|
3
3
|
* status` says), the Tables (`mason tables`, `add`, `remove`, `sync`), the Users (`mason user`) and Observability (`mason
|
|
4
4
|
* logs`, `mason usage`), refreshed while the tab is seen. It prints the page's URL WITH the run's token (src/cli/security.ts: the first request trades it for an HttpOnly cookie and is redirected to a URL without
|
|
5
|
-
* it) and opens the browser
|
|
5
|
+
* it) and opens the browser when its stdout is a terminal (as `mason login` does: a pipe, a file or an agent's shell has
|
|
6
|
+
* no one at a browser, so it gets the address alone). The reader is the person at this terminal, whom `mason user add` already shows a
|
|
6
7
|
* password once; the token is never written anywhere else — not in `mason start`'s output (its log is a file), not in
|
|
7
8
|
* `mason status`, which prints the URL without it.
|
|
8
9
|
*
|
|
9
10
|
* Before it prints anything it ASKS the page (`GET /` with the run's token as the commands send it), so a run that serves
|
|
10
|
-
* none (
|
|
11
|
+
* none (one from before the page) is said, never a URL that answers 404.
|
|
12
|
+
*
|
|
13
|
+
* `--ssh <user@server>` (src/cli/ui-ssh.ts, docs/WEB-UI.md "On a server") opens a server's page from this machine, through
|
|
14
|
+
* the machine's own ssh, and stays in the foreground until Ctrl-C, as `mason share` does; it needs no project here. Its
|
|
15
|
+
* URL carries a ticket for one use, never the run's token, which stays on the server. `--bridge` is what it runs there.
|
|
16
|
+
*
|
|
17
|
+
* `--replica <org>/<name>` (else `MASON_REPLICA`; src/cli/ui-hosted.ts, docs/HOSTED-API.md "A replica's page") opens a
|
|
18
|
+
* replica's page on a hosted Mason service: one request, whose one-use address is opened as this command opens a run's.
|
|
11
19
|
*/
|
|
12
20
|
import { spawn } from "node:child_process";
|
|
13
21
|
import { failed, jsonText, parseCommand, reportText } from "./command.js";
|
|
14
22
|
import { reach } from "./state.js";
|
|
23
|
+
import { bridge, openSshTunnel, targetProblem } from "./ui-ssh.js";
|
|
24
|
+
import { version } from "./version.js";
|
|
15
25
|
import { RunUnreachable } from "./control.js";
|
|
26
|
+
import { parseDuration } from "../lib/table-sync.js";
|
|
27
|
+
import { replicaOf } from "./replica.js";
|
|
28
|
+
import { hostedUi } from "./ui-hosted.js";
|
|
16
29
|
const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
30
|
+
const say = (m) => { process.stderr.write(`${m}\n`); };
|
|
31
|
+
const DEFAULT_TIMEOUT_S = 60;
|
|
17
32
|
export const UI = {
|
|
18
33
|
needs: "run",
|
|
19
34
|
name: "ui",
|
|
20
35
|
summary: "the status, tables, users and statements, in your browser",
|
|
21
36
|
usage: `usage: mason ui [--no-open] [--json]
|
|
37
|
+
mason ui --ssh <user@server> [--mason <command>] [--timeout <duration>] [--no-open] [--json]
|
|
38
|
+
mason ui --replica <org>/<name> [--no-open] [--json]
|
|
22
39
|
|
|
23
40
|
Opens the running Mason's web page in your browser, refreshed while the tab is open: the Overview (what mason status
|
|
24
41
|
says: whether the synced data is ready, the database, the sync, the statements and the recent errors); the Tables (add,
|
|
@@ -26,20 +43,51 @@ export const UI = {
|
|
|
26
43
|
the URL to paste, or remove one); and Observability: the statements running and that ran (mason logs --requests,
|
|
27
44
|
cancel one), usage over time (mason usage), and the sync's events (mason logs --events).
|
|
28
45
|
|
|
29
|
-
The URL carries this run's token: the page trades it for a cookie at once and drops it from the address
|
|
30
|
-
bar. It is printed for you alone, like a user's password: paste it nowhere else. Every mason start makes a new
|
|
31
|
-
token, so after a restart run mason ui again. The page is mason start's: mason serve serves none (off this
|
|
32
|
-
machine it would need TLS and a login).
|
|
33
|
-
|
|
34
46
|
mason ui
|
|
35
47
|
mason ui --no-open print the URL, open no browser
|
|
36
48
|
mason ui --json
|
|
49
|
+
mason ui --ssh deploy@mason.example.com
|
|
50
|
+
mason ui --ssh deploy@mason.example.com --mason "docker exec -i mason-mason-1 mason"
|
|
51
|
+
mason ui --ssh deploy@mason.example.com --mason "cd /var/lib/mason && mason" --timeout 120 --no-open
|
|
52
|
+
mason ui --replica acme/shop a replica's page on a hosted Mason service
|
|
53
|
+
mason ui --bridge --local-port 51000 what --ssh runs on the server; its stdout is a stream, not for reading
|
|
54
|
+
|
|
55
|
+
--no-open print the URL only (as when its output is not a terminal: a browser opens only for one)
|
|
56
|
+
--ssh <target> the server, as you give it to ssh: user@server, or a Host of your ssh config
|
|
57
|
+
--mason <command> how to run mason on the server (default mason); a shell's text, kept to ssh's command line
|
|
58
|
+
--timeout <duration> how long ssh and the server have to answer (default ${DEFAULT_TIMEOUT_S}s; a bare number is seconds;
|
|
59
|
+
2m): a passphrase or a touch counts; past it, exit 124
|
|
60
|
+
--bridge the server's end of --ssh, on stdin and stdout
|
|
61
|
+
--local-port <n> the port the browser names, for --bridge: the laptop's
|
|
62
|
+
--replica <org>/<name> a replica on a hosted Mason service, as the service lists it
|
|
63
|
+
--json as data: its shape is at the end
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
The URL carries this run's token: the page trades it for a cookie at once and drops it from the address
|
|
66
|
+
bar. It is printed for you alone, like a user's password: paste it nowhere else. Every mason start makes a new
|
|
67
|
+
token, so after a restart run mason ui again.
|
|
68
|
+
|
|
69
|
+
--ssh opens a server's page (mason serve's) from this machine, with nothing public: it runs your ssh, so your
|
|
70
|
+
~/.ssh/config, keys and SSO apply, starts mason there, and forwards a free local port to the page, which listens on the
|
|
71
|
+
server's loopback alone. It stays in the foreground: Ctrl-C closes the tunnel. It needs no project here. Its URL
|
|
72
|
+
carries a ticket for one use, never the run's token, which stays on the server. The server's Mason must be this
|
|
73
|
+
version or close to it, and must be a command ssh can run: --mason says how, for a container or a folder.
|
|
74
|
+
|
|
75
|
+
--replica opens the page of a replica on a hosted Mason service (else MASON_REPLICA, unless --ssh or --bridge names
|
|
76
|
+
the target), for the organisation's owner alone: the service answers an address that opens it once, for a few
|
|
77
|
+
minutes. The page is read-only: the service refuses changes through it, so add or remove users with mason user …
|
|
78
|
+
--replica. It needs no project here, and goes with --no-open and --json alone. Opened in your browser, the address is
|
|
79
|
+
not printed: run it again for another tab. A replica not ready yet is said, with when to try again.
|
|
80
|
+
|
|
81
|
+
The --json answer: { url (with this run's token; with --ssh a ticket for one use; null when Ctrl-C came
|
|
82
|
+
before the server answered, as port is, exit 0; with --replica the one-use address), port, server,
|
|
83
|
+
project, version (with --ssh: the server's Mason, for what its answer may lack), replica
|
|
84
|
+
(with --replica: "<org>/<name>"), expiresAt (with --replica: until when the address opens it)
|
|
85
|
+
}, next; it never opens a
|
|
86
|
+
browser. With --ssh it then holds the tunnel: the end is on stderr, exit 0 for Ctrl-C or SIGTERM, 1 otherwise
|
|
40
87
|
`,
|
|
41
|
-
options: { "no-open": { type: "boolean", default: false }, json: { type: "boolean", default: false } },
|
|
42
|
-
|
|
88
|
+
options: { "no-open": { type: "boolean", default: false }, json: { type: "boolean", default: false }, ssh: { type: "string" }, mason: { type: "string" },
|
|
89
|
+
timeout: { type: "string" }, bridge: { type: "boolean", default: false }, "local-port": { type: "string" }, replica: { type: "string" } },
|
|
90
|
+
json: ["url", "port", "server", "project", "version", "replica", "expiresAt", "next"],
|
|
43
91
|
};
|
|
44
92
|
const NEXT = [["mason status", "the same, in this terminal"], ["mason user list", "the users"]];
|
|
45
93
|
/** The command that opens a URL in the person's browser, by platform (macOS `open`, Windows `start`, else `xdg-open`). */
|
|
@@ -51,8 +99,11 @@ export function openCommand(platform, url) {
|
|
|
51
99
|
return ["cmd", ["/c", "start", "", url]];
|
|
52
100
|
return ["xdg-open", [url]];
|
|
53
101
|
}
|
|
102
|
+
/** An address Mason hands the browser by itself (a sign-in page, a replica's page): a plain https (or this machine's
|
|
103
|
+
* http) URL, and none of the characters a shell or `cmd /c start` reads (the service's URL is not ours). */
|
|
104
|
+
export const OPENABLE = /^https?:\/\/[A-Za-z0-9.:[\]-]+(?:\/[A-Za-z0-9._~/?=+-]*)?$/;
|
|
54
105
|
/** Open `url` in the browser: null once the opener started, else why it could not (the URL is printed either way). */
|
|
55
|
-
function openBrowser(url) {
|
|
106
|
+
export function openBrowser(url) {
|
|
56
107
|
const [command, args] = openCommand(process.platform, url);
|
|
57
108
|
return new Promise((resolve) => {
|
|
58
109
|
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
@@ -60,6 +111,9 @@ function openBrowser(url) {
|
|
|
60
111
|
child.once("spawn", () => { child.unref(); resolve(null); });
|
|
61
112
|
});
|
|
62
113
|
}
|
|
114
|
+
/** Whether to open a browser: asked for (no --no-open) and stdout a terminal, a person in front of it (never a pipe,
|
|
115
|
+
* a file or a coding agent's shell, which get the address alone). */
|
|
116
|
+
const opens = (noOpen) => !noOpen && process.stdout.isTTY === true;
|
|
63
117
|
/** Whether the run serves the page: its `GET /` answered, with the token the commands send. */
|
|
64
118
|
async function servesPage(run) {
|
|
65
119
|
let res;
|
|
@@ -72,21 +126,111 @@ async function servesPage(run) {
|
|
|
72
126
|
await res.arrayBuffer();
|
|
73
127
|
return res.status === 200 && /^text\/html/.test(res.headers.get("content-type") ?? "");
|
|
74
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* THE PAGE OF A SERVER (the header): the tunnel, the URL once, and held until Ctrl-C or until it drops. As `mason share`:
|
|
131
|
+
* what `--json` prints is one object, once, and everything after it is on stderr, so stdout stays one document. The
|
|
132
|
+
* signals are heard before ssh starts: a Ctrl-C at a passphrase ends both.
|
|
133
|
+
*/
|
|
134
|
+
async function overSsh(target, mason, timeoutMs, open, json) {
|
|
135
|
+
let signalled = null;
|
|
136
|
+
const signal = new Promise((resolve) => {
|
|
137
|
+
const heard = (s) => { signalled = s; resolve(s); };
|
|
138
|
+
process.once("SIGINT", heard);
|
|
139
|
+
process.once("SIGTERM", heard);
|
|
140
|
+
});
|
|
141
|
+
const t = await openSshTunnel({ target, mason, timeoutMs, interactive: !json && process.stdin.isTTY === true, stopped: signal });
|
|
142
|
+
const next = [[`mason ui --ssh ${target}`, "again, once this one is closed: a new port and a new ticket"]];
|
|
143
|
+
if ("message" in t) {
|
|
144
|
+
if (signalled === null)
|
|
145
|
+
return failed("ui", t, { json });
|
|
146
|
+
// stopped on purpose, exit 0: under --json still the one object every answer is, its url null as nothing opened
|
|
147
|
+
if (json)
|
|
148
|
+
out(jsonText(UI, { url: null, port: null, server: target }, next));
|
|
149
|
+
else
|
|
150
|
+
say(`stopped before ${target} answered: nothing was left open`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
if (json)
|
|
154
|
+
out(jsonText(UI, { url: t.url, port: t.port, server: target, project: t.project, version: t.serverVersion }, next));
|
|
155
|
+
else {
|
|
156
|
+
const couldNot = open ? await openBrowser(t.url) : null;
|
|
157
|
+
out(reportText({
|
|
158
|
+
headline: open && couldNot === null ? `${t.project} on ${target}, opened in your browser` : `${t.project} on ${target}`,
|
|
159
|
+
details: [["url", t.url], ["server", target], ["project", t.project], ["version", t.serverVersion]],
|
|
160
|
+
tips: [
|
|
161
|
+
...(couldNot !== null ? [`Could not open a browser (${couldNot}): open the URL yourself.`] : []),
|
|
162
|
+
...(t.serverVersion !== version() ? [`This machine's Mason is ${version()}: the page is the server's.`] : []),
|
|
163
|
+
"The URL carries a ticket for one use, and works for 5 minutes. Ctrl-C closes the tunnel.",
|
|
164
|
+
],
|
|
165
|
+
next,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
const how = await Promise.race([signal.then((s) => ({ signal: s })), t.ended.then((why) => ({ why }))]);
|
|
169
|
+
if ("signal" in how) {
|
|
170
|
+
await t.stop();
|
|
171
|
+
say(`the tunnel to ${target} is closed`);
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
return failed("ui", { message: `the tunnel to ${target} dropped`, code: "failed", why: how.why, fix: "is the server up, and this machine online? The page in your browser stops answering until a new tunnel opens one", next }, { json: false });
|
|
175
|
+
}
|
|
75
176
|
export async function ui(argv) {
|
|
76
177
|
const f = parseCommand(UI, argv);
|
|
77
178
|
if (typeof f === "number")
|
|
78
179
|
return f;
|
|
79
180
|
const json = f.values.json === true;
|
|
181
|
+
const v = f.values;
|
|
182
|
+
const wrong = (message) => failed("ui", { message, code: "usage", usage: UI.usage }, { json });
|
|
80
183
|
if (f.positionals.length)
|
|
81
|
-
return
|
|
184
|
+
return wrong(`mason ui takes no argument: ${f.positionals.join(" ")}`);
|
|
185
|
+
if (v.replica !== undefined) {
|
|
186
|
+
const beside = [["--ssh", v.ssh !== undefined], ["--bridge", v.bridge], ["--mason", v.mason !== undefined], ["--timeout", v.timeout !== undefined],
|
|
187
|
+
["--local-port", v["local-port"] !== undefined]].filter(([, given]) => given).map(([name]) => name);
|
|
188
|
+
if (beside.length)
|
|
189
|
+
return wrong(`--replica opens a hosted replica's page: it goes with --no-open and --json alone, not ${beside.join(", ")}`);
|
|
190
|
+
}
|
|
191
|
+
if (v.bridge) {
|
|
192
|
+
if (v.ssh !== undefined || v.mason !== undefined || v.timeout !== undefined || json)
|
|
193
|
+
return wrong("--bridge is mason ui --ssh's own end on the server: it goes with --local-port alone");
|
|
194
|
+
const port = Number(v["local-port"]);
|
|
195
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
196
|
+
return wrong(`--bridge needs --local-port, a port number, not ${JSON.stringify(v["local-port"] ?? "")}`);
|
|
197
|
+
return bridge(port);
|
|
198
|
+
}
|
|
199
|
+
if (v["local-port"] !== undefined)
|
|
200
|
+
return wrong("--local-port goes with --bridge");
|
|
201
|
+
if (v.ssh === undefined && (v.mason !== undefined || v.timeout !== undefined))
|
|
202
|
+
return wrong(`${v.mason !== undefined ? "--mason" : "--timeout"} goes with --ssh <user@server>`);
|
|
203
|
+
if (v.ssh !== undefined) {
|
|
204
|
+
const problem = targetProblem(v.ssh);
|
|
205
|
+
if (problem !== null)
|
|
206
|
+
return wrong(`--ssh: ${problem}`);
|
|
207
|
+
if (v.mason !== undefined && (v.mason.trim() === "" || /[\r\n]/.test(v.mason)))
|
|
208
|
+
return wrong("--mason is one line of shell that runs mason on the server");
|
|
209
|
+
const read = parseDuration(v.timeout ?? String(DEFAULT_TIMEOUT_S));
|
|
210
|
+
if ("problem" in read)
|
|
211
|
+
return wrong(`--timeout ${read.problem}`);
|
|
212
|
+
const seconds = read.seconds;
|
|
213
|
+
return overSsh(v.ssh, v.mason ?? "mason", seconds * 1000, opens(v["no-open"]), json);
|
|
214
|
+
}
|
|
215
|
+
// a replica on a hosted service (--replica, else MASON_REPLICA): --ssh and --bridge name their own target, above
|
|
216
|
+
const replica = replicaOf(v.replica, UI.usage);
|
|
217
|
+
if (replica !== null && "message" in replica)
|
|
218
|
+
return failed("ui", replica, { json });
|
|
219
|
+
if (replica !== null)
|
|
220
|
+
return hostedUi(replica, { open: opens(v["no-open"]), json });
|
|
82
221
|
const w = await reach(UI, json);
|
|
83
222
|
if (w === null || w.state !== "up")
|
|
84
223
|
return 1;
|
|
85
224
|
const run = w.run;
|
|
225
|
+
if (w.project.config.served === true) {
|
|
226
|
+
return failed("ui", { message: `Mason for ${run.name} runs on this server: its page is opened from your own machine`, code: "refused",
|
|
227
|
+
why: "the page listens on this machine's loopback alone, and a browser on a server has no one in front of it",
|
|
228
|
+
fix: "on your machine, mason ui --ssh <user@server> (mason ui --help: for a container, --mason)", next: [["mason status", "what it is doing, in this terminal"]] }, { json });
|
|
229
|
+
}
|
|
86
230
|
if (!await servesPage(run)) {
|
|
87
231
|
return failed("ui", { message: `Mason for ${run.name} serves no web page`, code: "refused",
|
|
88
|
-
why: "
|
|
89
|
-
fix: "
|
|
232
|
+
why: "this run was started by a Mason from before the page",
|
|
233
|
+
fix: "mason stop && mason start runs it with the page",
|
|
90
234
|
next: [["mason status", "what it is doing, in this terminal"]] }, { json });
|
|
91
235
|
}
|
|
92
236
|
const url = `${run.control.url}/?token=${encodeURIComponent(run.control.token)}`;
|
|
@@ -95,8 +239,9 @@ export async function ui(argv) {
|
|
|
95
239
|
out(jsonText(UI, { url, port }, NEXT));
|
|
96
240
|
return 0;
|
|
97
241
|
}
|
|
98
|
-
const
|
|
99
|
-
const
|
|
242
|
+
const open = opens(v["no-open"]);
|
|
243
|
+
const couldNot = open ? await openBrowser(url) : null;
|
|
244
|
+
const opened = open && couldNot === null;
|
|
100
245
|
out(reportText({
|
|
101
246
|
headline: opened ? `Mason's web page, opened in your browser` : `Mason's web page`,
|
|
102
247
|
details: [["url", url], ["project", run.name]],
|
package/dist/src/cli/usage.js
CHANGED
|
@@ -12,9 +12,10 @@ import { fileSize, guarded, runFailure } from "./commands.js";
|
|
|
12
12
|
import { timeOf } from "./request-filter.js";
|
|
13
13
|
import { duration, msText } from "./readiness.js";
|
|
14
14
|
import { SIZE_EVERY_MS } from "../duck/usage.js";
|
|
15
|
+
import { parseDuration } from "../lib/table-sync.js";
|
|
15
16
|
const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
16
17
|
const n = (x) => x.toLocaleString("en-US");
|
|
17
|
-
export const USAGE_USAGE = `usage: mason usage [--since <when>] [--until <when>] [--step <
|
|
18
|
+
export const USAGE_USAGE = `usage: mason usage [--since <when>] [--until <when>] [--step <duration>] [--json]
|
|
18
19
|
|
|
19
20
|
What Mason was and did over time, from a sample it takes every minute and keeps MASON_HISTORY_DAYS days
|
|
20
21
|
(default 7): a line each span — the statements of the Postgres API (how many, how many were refused, failed or
|
|
@@ -29,15 +30,18 @@ export const USAGE_USAGE = `usage: mason usage [--since <when>] [--until <when>]
|
|
|
29
30
|
|
|
30
31
|
--since <when> from when: an age (90s, 30m, 6h, 7d) or an ISO 8601 date or time with its zone; default 1h
|
|
31
32
|
--until <when> until when, the same way; default now
|
|
32
|
-
--step <
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
--step <duration> a line per span (1m to the days kept; default 5m; a bare number is seconds), or 0 for every
|
|
34
|
+
sample as it was taken
|
|
35
|
+
--json as data: its shape is at the end
|
|
36
|
+
|
|
37
|
+
The --json answer: { since, until, stepSeconds (null: every sample), sampleSeconds, keptDays, samples: [{ at
|
|
38
|
+
(with a step, the span's start), seconds, copyBytes, copyRows, tablesAdded, tablesCopied,
|
|
39
|
+
tablesFailed, oldestSyncSeconds, statements, statementsRefused, statementsFailed, statementsCancelled,
|
|
40
|
+
rowsSent, bytesSent, durationP50Ms, durationP95Ms, streamLagBytes, streamLagSeconds, rssBytes,
|
|
41
|
+
heapUsedBytes, copyMemoryBytes, copyMemoryLimitBytes, slotWalHeldBytes (replication: the WAL the
|
|
42
|
+
slot holds on the database), diskFreeBytes (free where the synced data is; with a step, the lowest),
|
|
43
|
+
sessionsOpen (the Postgres API's sessions logged in) }],
|
|
44
|
+
copySizes: [{ table, bytes, at }], next }
|
|
41
45
|
`;
|
|
42
46
|
export const USAGE = {
|
|
43
47
|
needs: "run",
|
|
@@ -47,13 +51,13 @@ export const USAGE = {
|
|
|
47
51
|
options: { since: { type: "string" }, until: { type: "string" }, step: { type: "string" }, json: { type: "boolean" } },
|
|
48
52
|
json: ["since", "until", "stepSeconds", "sampleSeconds", "keptDays", "samples", "copySizes", "next"],
|
|
49
53
|
};
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
/** A span as the options write it (the command line's one duration grammar, src/lib/table-sync.ts `parseDuration`:
|
|
55
|
+
* 90s, 30m, 6h, 7d, a bare number of seconds; 0 for every sample), in whole seconds, or null when it is not one. */
|
|
52
56
|
export function spanSeconds(text) {
|
|
53
57
|
if (text.trim() === "0")
|
|
54
58
|
return 0;
|
|
55
|
-
const
|
|
56
|
-
return
|
|
59
|
+
const d = parseDuration(text, { zero: true });
|
|
60
|
+
return "problem" in d || !Number.isInteger(d.seconds) ? null : d.seconds;
|
|
57
61
|
}
|
|
58
62
|
/** One sample on one line: the time, the statements, those refused, failed or cancelled, p95, rows sent, the copy, its rows, the stalest copy, the lag, the memory. */
|
|
59
63
|
export function sampleLine(s, withLag) {
|
|
@@ -87,7 +91,7 @@ export async function usage(argv, now = Date.now()) {
|
|
|
87
91
|
return failed("usage", { ...t, usage: USAGE_USAGE }, { json });
|
|
88
92
|
const step = spanSeconds(v.step ?? "5m");
|
|
89
93
|
if (step === null)
|
|
90
|
-
return failed("usage", { message: `--step is a
|
|
94
|
+
return failed("usage", { message: `--step is a duration (300, 1m, 5m, 1h, 1d: a bare number is seconds) or 0 for every sample, not ${JSON.stringify(v.step)}`, code: "usage", usage: USAGE_USAGE }, { json });
|
|
91
95
|
const w = await reach(USAGE, json);
|
|
92
96
|
if (w === null || !("run" in w))
|
|
93
97
|
return 1;
|