@mason-data/cli 0.0.0-stage → 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.
Files changed (109) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +268 -2
  3. package/dist/src/api/error.js +18 -0
  4. package/dist/src/api/postgres/catalog-worker.js +92 -0
  5. package/dist/src/api/postgres/catalog.js +477 -0
  6. package/dist/src/api/postgres/encode.js +541 -0
  7. package/dist/src/api/postgres/explain.js +143 -0
  8. package/dist/src/api/postgres/listener.js +60 -0
  9. package/dist/src/api/postgres/parser-worker.js +58 -0
  10. package/dist/src/api/postgres/parser.js +121 -0
  11. package/dist/src/api/postgres/plain.js +33 -0
  12. package/dist/src/api/postgres/protocol.js +404 -0
  13. package/dist/src/api/postgres/schema.js +54 -0
  14. package/dist/src/api/postgres/server.js +1771 -0
  15. package/dist/src/api/postgres/translate.js +248 -0
  16. package/dist/src/api/request-log.js +50 -0
  17. package/dist/src/cdc/pg-wire.js +588 -0
  18. package/dist/src/cdc/pgoutput.js +107 -0
  19. package/dist/src/cdc/poll.js +67 -0
  20. package/dist/src/cdc/source.js +65 -0
  21. package/dist/src/cli/background.js +198 -0
  22. package/dist/src/cli/command.js +173 -0
  23. package/dist/src/cli/commands.js +1277 -0
  24. package/dist/src/cli/control.js +36 -0
  25. package/dist/src/cli/demo.js +149 -0
  26. package/dist/src/cli/dev.js +669 -0
  27. package/dist/src/cli/doctor.js +260 -0
  28. package/dist/src/cli/guides.js +158 -0
  29. package/dist/src/cli/init.js +366 -0
  30. package/dist/src/cli/machine.js +162 -0
  31. package/dist/src/cli/main.js +83 -0
  32. package/dist/src/cli/mcp.js +726 -0
  33. package/dist/src/cli/pg-tls.js +100 -0
  34. package/dist/src/cli/pg-url.js +52 -0
  35. package/dist/src/cli/pin.js +36 -0
  36. package/dist/src/cli/ports.js +118 -0
  37. package/dist/src/cli/preflight.js +347 -0
  38. package/dist/src/cli/project.js +129 -0
  39. package/dist/src/cli/readiness.js +76 -0
  40. package/dist/src/cli/registry.js +220 -0
  41. package/dist/src/cli/replication-checks.js +120 -0
  42. package/dist/src/cli/replication-mode.js +148 -0
  43. package/dist/src/cli/run-server.js +311 -0
  44. package/dist/src/cli/runs.js +120 -0
  45. package/dist/src/cli/security.js +86 -0
  46. package/dist/src/cli/self-signed-cert.js +64 -0
  47. package/dist/src/cli/serve.js +301 -0
  48. package/dist/src/cli/source-edit.js +116 -0
  49. package/dist/src/cli/sources.js +564 -0
  50. package/dist/src/cli/state.js +58 -0
  51. package/dist/src/cli/style.js +187 -0
  52. package/dist/src/cli/supervise.js +248 -0
  53. package/dist/src/cli/sync.js +94 -0
  54. package/dist/src/cli/ui.js +107 -0
  55. package/dist/src/cli/user.js +229 -0
  56. package/dist/src/cli/version.js +23 -0
  57. package/dist/src/demo/seed.js +237 -0
  58. package/dist/src/demo/source.js +655 -0
  59. package/dist/src/duck/analyze.js +113 -0
  60. package/dist/src/duck/cancel.js +49 -0
  61. package/dist/src/duck/distinct-key.js +228 -0
  62. package/dist/src/duck/executor-worker.js +268 -0
  63. package/dist/src/duck/executor.js +567 -0
  64. package/dist/src/duck/explain.js +238 -0
  65. package/dist/src/duck/functions.js +819 -0
  66. package/dist/src/duck/gaps.js +94 -0
  67. package/dist/src/duck/jsonb-order.js +175 -0
  68. package/dist/src/duck/jsonb.js +188 -0
  69. package/dist/src/duck/kept-route.js +299 -0
  70. package/dist/src/duck/kept.js +964 -0
  71. package/dist/src/duck/late.js +221 -0
  72. package/dist/src/duck/libc-order.js +190 -0
  73. package/dist/src/duck/pg-eval-worker.js +34 -0
  74. package/dist/src/duck/pg-eval.js +59 -0
  75. package/dist/src/duck/pgtree.js +378 -0
  76. package/dist/src/duck/plan.js +259 -0
  77. package/dist/src/duck/publication.js +207 -0
  78. package/dist/src/duck/requests.js +209 -0
  79. package/dist/src/duck/resolved.js +248 -0
  80. package/dist/src/duck/rewrite.js +5101 -0
  81. package/dist/src/duck/store.js +907 -0
  82. package/dist/src/duck/stream.js +518 -0
  83. package/dist/src/duck/sync.js +2929 -0
  84. package/dist/src/duck/to-char.js +482 -0
  85. package/dist/src/duck/token.js +31 -0
  86. package/dist/src/duck/types.js +427 -0
  87. package/dist/src/duck/users.js +231 -0
  88. package/dist/src/duck/verified.js +1001 -0
  89. package/dist/src/duck/views.js +150 -0
  90. package/dist/src/lib/database-url.js +74 -0
  91. package/dist/src/lib/did-you-mean.js +43 -0
  92. package/dist/src/lib/dotenv.js +72 -0
  93. package/dist/src/lib/exit-when-written.js +20 -0
  94. package/dist/src/lib/float-equal.js +69 -0
  95. package/dist/src/lib/http-json.js +22 -0
  96. package/dist/src/lib/log.js +161 -0
  97. package/dist/src/lib/pg-numeric.js +489 -0
  98. package/dist/src/lib/port.js +22 -0
  99. package/dist/src/lib/project-file.js +49 -0
  100. package/dist/src/lib/project-files.js +43 -0
  101. package/dist/src/lib/project-version.js +76 -0
  102. package/dist/src/lib/runnable.js +14 -0
  103. package/dist/src/lib/shell-word.js +4 -0
  104. package/dist/src/lib/timezone.js +237 -0
  105. package/dist/src/lib/zone-names.js +114 -0
  106. package/dist/src/web/app.css +76 -0
  107. package/dist/src/web/app.js +491 -0
  108. package/dist/src/web/index.html +44 -0
  109. package/package.json +22 -4
@@ -0,0 +1,36 @@
1
+ /** The run does not answer: a failure with the reason as `why` and mason logs as the way out (`failure` is what a command
2
+ * hands src/cli/command.ts failed). */
3
+ export class RunUnreachable extends Error {
4
+ failure;
5
+ constructor(message, why) {
6
+ super(message);
7
+ this.failure = { message, code: "unreachable", why, next: [["mason logs", "why it does not answer"], ["mason status", "whether it runs at all"]] };
8
+ }
9
+ }
10
+ /** One request to the run. A reply with an error status is returned (its `error` is the sentence to show). `timeoutMs`
11
+ * is for a request that does work on the database (mason sync scans a table whole): the others answer in moments. */
12
+ export async function callRun(run, method, route, body, timeoutMs = 30_000) {
13
+ let res;
14
+ try {
15
+ res = await fetch(run.control.url + route, {
16
+ method,
17
+ headers: { Authorization: `Bearer ${run.control.token}`, ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
18
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
19
+ signal: AbortSignal.timeout(timeoutMs),
20
+ });
21
+ }
22
+ catch (e) {
23
+ const why = e.cause?.code ?? e.message;
24
+ throw new RunUnreachable(`Mason for ${run.name} (pid ${run.pid}) does not answer at ${run.control.url}`, why);
25
+ }
26
+ const text = await res.text();
27
+ let parsed;
28
+ // the setup server answers JSON; anything else is an error to show as it came
29
+ try {
30
+ parsed = JSON.parse(text);
31
+ }
32
+ catch {
33
+ parsed = { error: text.trim() || `HTTP ${res.status}` };
34
+ }
35
+ return { status: res.status, body: parsed };
36
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * THE DEMO PROJECT, THE CLI'S SIDE (`mason init demo`; the database itself is src/demo/source.ts): a project whose
3
+ * database is one Mason hosts itself — a Postgres in this process, persisted in `.mason/demo/`, served on a loopback
4
+ * port over the Postgres wire protocol and seeded with a small shop schema — so a person tries Mason with nothing to
5
+ * install. The project stays an ordinary one: its sources file names `url: ${DATABASE_URL}` and three of the four
6
+ * tables (the fourth, `DEMO_TABLE_TO_ADD`, is the person's to add), `.env` the demo's URL, and `mason start` reads it
7
+ * over the wire as it reads any Postgres, so the demo runs the read path production runs (docs/DESIGN.md "The demo
8
+ * database").
9
+ *
10
+ * - THE PORT LIVES IN THE URL ALONE: `mason init demo` picks one free and writes it into `.env`; `mason start` reads
11
+ * it back from the resolved URL (`demoPort`), never from a second record that could disagree with it.
12
+ * - POLL ONLY: the demo database has no change log (no WAL sender), so `--replication` is refused before anything
13
+ * starts (`demoChangesRefusal`).
14
+ * - A WRITER: every MASON_DEMO_CHANGE_MS (`demoSettings`) the run makes one round of changes (`demoChanges`), so the
15
+ * copy is seen keeping current.
16
+ *
17
+ * The demo's module is imported when it is used (`demoModule`): a command that never hosts it never loads its database;
18
+ * its URL and names are src/demo/seed.ts's, which loads none.
19
+ */
20
+ import * as path from "node:path";
21
+ import { parseSource, isLocalHost } from "./preflight.js";
22
+ import { changesOf } from "./replication-mode.js";
23
+ import { heldText, portHolder } from "./ports.js";
24
+ import { DEMO_DIR, STATE_DIR } from "./project.js";
25
+ import { DEMO_DATABASE, DEMO_USER, demoUrl } from "../demo/seed.js";
26
+ /** What `mason init` takes in the URL's place to make a demo project. */
27
+ export const DEMO_ARG = "demo";
28
+ /** The port `mason init demo` prefers for the demo database (the next free one when it is held): beside Mason's 5433. */
29
+ export const DEMO_PORT = 5434;
30
+ /** The demo database's tables (the seed's own list), the ones `mason init demo` adds to the sources file and the one it
31
+ * leaves for the person to add, and its URL (src/demo/seed.ts: no PGlite is loaded by it, so `mason init demo` and
32
+ * `demoPort` never load the database). */
33
+ export { DEMO_ADDED_TABLES, DEMO_TABLES, DEMO_TABLE_TO_ADD, demoUrl } from "../demo/seed.js";
34
+ /** How often the run changes the demo database by default (MASON_DEMO_CHANGE_MS). */
35
+ export const DEMO_CHANGE_MS = 5000;
36
+ const DEMO_CHANGE_MIN_MS = 100;
37
+ /** The demo database's module (`DemoSource`, and PGlite with it), loaded when the run hosts it, never to build or read
38
+ * its URL (src/demo/seed.ts has those). */
39
+ export const demoModule = () => import("../demo/source.js");
40
+ /** What the run reads of the demo from its environment: how often it changes the database. Throws on a value that is no
41
+ * number of milliseconds, said with the way out (as src/duck/sync.ts copyParallelOf). */
42
+ export function demoSettings(env) {
43
+ const v = env.MASON_DEMO_CHANGE_MS;
44
+ if (v === undefined || v === "")
45
+ return { changeMs: DEMO_CHANGE_MS };
46
+ const n = Number(v);
47
+ if (!/^\d+$/.test(v.trim()) || n < DEMO_CHANGE_MIN_MS) {
48
+ throw new Error(`MASON_DEMO_CHANGE_MS=${JSON.stringify(v)} is not a number of milliseconds: how often the demo database changes (a new order, an older one's status moved on); ` +
49
+ `set it to ${DEMO_CHANGE_MIN_MS} or more, or unset it for ${DEMO_CHANGE_MS}`);
50
+ }
51
+ return { changeMs: n };
52
+ }
53
+ /**
54
+ * THE DEMO DATABASE'S PORT, READ FROM THE URL THE SOURCES FILE RESOLVED (`from` says where: `DATABASE_URL in .env`) — or
55
+ * why that URL is not the demo's, with the way out: a host off this machine (the demo database listens on 127.0.0.1
56
+ * alone), or any other change to `postgres://demo@127.0.0.1:<port>/demo`. The port is the one fact that may differ.
57
+ */
58
+ export function demoPort(url, from) {
59
+ const wayOut = "mason destroy and mason init demo again in a new folder, or put the URL back";
60
+ const parsed = parseSource(url);
61
+ if ("problem" in parsed)
62
+ return { problem: `${from} was changed: ${parsed.problem}. The demo database is postgres://${DEMO_USER}@127.0.0.1:<port>/${DEMO_DATABASE}; ${wayOut}` };
63
+ const s = parsed.source;
64
+ // loopback alone: the demo database listens on nothing else (0.0.0.0 or a container's host name is not it)
65
+ if (!isLocalHost(s.host) || s.host === "0.0.0.0" || s.host === "host.docker.internal") {
66
+ return { problem: `${from} names ${s.host}, which is not this machine's loopback: the demo database is hosted by mason start on 127.0.0.1 alone (${demoUrl(s.port)}); ${wayOut}` };
67
+ }
68
+ if (s.url !== demoUrl(s.port))
69
+ return { problem: `${from} was changed: the demo database is ${demoUrl(s.port)}; ${wayOut}` };
70
+ return { port: s.port };
71
+ }
72
+ /** The refusal of logical replication for a demo project (the flag, or a mode the config was edited to): before
73
+ * anything starts, with the way out; null for any other project, or a demo one polled. */
74
+ export function demoChangesRefusal(cfg, flag) {
75
+ if (cfg.demo !== true || changesOf(flag, cfg.changes) !== "replication")
76
+ return null;
77
+ return { message: "the demo database is read by polling only: --replication does not apply to it", code: "refused",
78
+ why: "the demo database has no change log for logical replication to read; mason start polls it with SELECTs, every few seconds",
79
+ fix: flag === "replication" ? "mason start, without --replication" : `remove "changes": "replication" from ${STATE_DIR}/config.json, then mason start`,
80
+ next: [["mason start", "the demo database and Mason, polled"]] };
81
+ }
82
+ /** The step's done line: where the demo database listens, and whether it was seeded now or kept from the last run. */
83
+ export function demoDoneLine(port, seeded) {
84
+ return `Demo database on 127.0.0.1:${port} (Postgres, hosted by this process in ${STATE_DIR}/${DEMO_DIR}; ${seeded ? "seeded with the shop schema" : "kept from the last run"})`;
85
+ }
86
+ /**
87
+ * THE DEMO DATABASE OPENED for the run in `home`/demo (`home` the project's `.mason/`), its events to `log`: the handle and
88
+ * the step's done line, or why it did not open — its own message (what failed), and when something holds the port, the
89
+ * way out ONCE, with its choices: mason stop for a run of this project's that may still hold it, stop what does, or move
90
+ * the demo database by changing the port in its URL, where `from` says the URL is (`DATABASE_URL in .env`: the port
91
+ * lives there alone). The port is free for every other reason, so the probe tells a held port apart without reading
92
+ * the message.
93
+ */
94
+ export async function openDemo(o) {
95
+ const open = o.open ?? (async (a) => (await demoModule()).DemoSource.open(a));
96
+ try {
97
+ const demo = await open({ dir: path.join(o.home, DEMO_DIR), port: o.port, log: o.log });
98
+ return { demo, done: demoDoneLine(demo.port, demo.seeded) };
99
+ }
100
+ catch (e) {
101
+ const problem = `the demo database did not start: ${e.message}`;
102
+ const held = await portHolder(o.port);
103
+ if (held === null)
104
+ return { problem };
105
+ const move = `move the demo database: change the port in ${o.from} (postgres://${DEMO_USER}@127.0.0.1:<another>/${DEMO_DATABASE}) and mason start again`;
106
+ return { problem: held.code === "EACCES"
107
+ ? `${problem} (${heldText(o.port, held)}): ${move}`
108
+ : `${problem}: mason stop ends a run that may still hold it, else stop what does (lsof -nP -iTCP:${o.port} -sTCP:LISTEN names it), or ${move}` };
109
+ }
110
+ }
111
+ /**
112
+ * THE WRITER: one round of changes every `ms`, each said to `log` as the demo says it; a round that fails is said and
113
+ * the next one tried, and a round is never begun while the last one runs. Returns what stops it (idempotent).
114
+ */
115
+ export function demoChanges(demo, ms, log) {
116
+ let busy = false;
117
+ const timer = setInterval(() => {
118
+ if (busy)
119
+ return;
120
+ busy = true;
121
+ demo.change().then((line) => log(line), (e) => log(`a change failed (the next one is tried in ${ms / 1000} s): ${e.message}`))
122
+ .finally(() => { busy = false; });
123
+ }, ms);
124
+ return () => clearInterval(timer);
125
+ }
126
+ /** The demo database closed once, however often it is asked: null when it closed, else what went wrong (said by the
127
+ * caller, never swallowed). */
128
+ export function closingOnce(demo) {
129
+ let closing = null;
130
+ return () => closing ??= demo.close().then(() => null, (e) => e.message);
131
+ }
132
+ /** What the crash path says of a demo database still closing when it stops waiting (`closedWithin`). */
133
+ export const DEMO_STILL_CLOSING = (ms) => `closing the demo database: still closing after ${ms / 1000} s; its data may need seeding again at the next start`;
134
+ /**
135
+ * A close waited for `ms` at most, and SAID when it is not done by then (`late`), never left silent: the crash path
136
+ * stops waiting so the supervisor can start the run again. The timer is cleared when the close wins, and is not
137
+ * unref'd, so a close that hangs is said before the process ends. A close that rejects rejects this.
138
+ */
139
+ export async function closedWithin(closing, ms, late) {
140
+ let timer;
141
+ const timedOut = new Promise((resolve) => { timer = setTimeout(() => resolve(true), ms); });
142
+ try {
143
+ if (await Promise.race([closing.then(() => false), timedOut]))
144
+ late();
145
+ }
146
+ finally {
147
+ clearTimeout(timer);
148
+ }
149
+ }