@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.
Files changed (118) hide show
  1. package/README.md +19 -8
  2. package/dist/src/api/error.js +4 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +166 -32
  5. package/dist/src/api/postgres/listener.js +11 -3
  6. package/dist/src/api/postgres/proxy.js +129 -0
  7. package/dist/src/api/postgres/schema.js +6 -0
  8. package/dist/src/api/postgres/server.js +527 -103
  9. package/dist/src/api/postgres/translate.js +47 -2
  10. package/dist/src/cli/background.js +18 -19
  11. package/dist/src/cli/bridge-stdin.js +101 -0
  12. package/dist/src/cli/command.js +337 -15
  13. package/dist/src/cli/commands.js +458 -169
  14. package/dist/src/cli/credentials.js +182 -0
  15. package/dist/src/cli/demo.js +2 -2
  16. package/dist/src/cli/deploy.js +680 -0
  17. package/dist/src/cli/dev.js +148 -27
  18. package/dist/src/cli/doctor.js +122 -36
  19. package/dist/src/cli/generated-files.js +143 -0
  20. package/dist/src/cli/guides.js +116 -16
  21. package/dist/src/cli/hosted.js +850 -0
  22. package/dist/src/cli/init.js +154 -33
  23. package/dist/src/cli/login.js +292 -0
  24. package/dist/src/cli/machine.js +125 -40
  25. package/dist/src/cli/main.js +59 -10
  26. package/dist/src/cli/mason.cjs +17 -0
  27. package/dist/src/cli/mcp.js +17 -14
  28. package/dist/src/cli/output-gate.js +176 -0
  29. package/dist/src/cli/page-share.js +96 -0
  30. package/dist/src/cli/pg-tls.js +1 -1
  31. package/dist/src/cli/pin.js +6 -2
  32. package/dist/src/cli/pool-size.cjs +85 -0
  33. package/dist/src/cli/ports.js +2 -0
  34. package/dist/src/cli/preflight.js +130 -39
  35. package/dist/src/cli/project.js +100 -1
  36. package/dist/src/cli/readiness.js +42 -6
  37. package/dist/src/cli/registry.js +89 -36
  38. package/dist/src/cli/replica-hosted.js +1324 -0
  39. package/dist/src/cli/replica.js +114 -0
  40. package/dist/src/cli/replication-mode.js +5 -0
  41. package/dist/src/cli/request-filter.js +16 -6
  42. package/dist/src/cli/roles.js +2 -1
  43. package/dist/src/cli/run-server.js +179 -9
  44. package/dist/src/cli/secret-input.js +193 -0
  45. package/dist/src/cli/security.js +57 -10
  46. package/dist/src/cli/serve.js +65 -9
  47. package/dist/src/cli/share-run.js +51 -8
  48. package/dist/src/cli/share.js +23 -18
  49. package/dist/src/cli/source-edit.js +10 -4
  50. package/dist/src/cli/source-role.js +343 -0
  51. package/dist/src/cli/sources.js +50 -23
  52. package/dist/src/cli/style.js +16 -1
  53. package/dist/src/cli/supervise.js +15 -23
  54. package/dist/src/cli/sync.js +17 -8
  55. package/dist/src/cli/token-hosted.js +343 -0
  56. package/dist/src/cli/tunnel.js +1 -1
  57. package/dist/src/cli/ui-hosted.js +163 -0
  58. package/dist/src/cli/ui-ssh.js +460 -0
  59. package/dist/src/cli/ui.js +162 -17
  60. package/dist/src/cli/usage.js +19 -15
  61. package/dist/src/cli/user-hosted.js +364 -0
  62. package/dist/src/cli/user.js +275 -105
  63. package/dist/src/demo/seed.js +26 -0
  64. package/dist/src/duck/analyze.js +36 -20
  65. package/dist/src/duck/collation.js +266 -0
  66. package/dist/src/duck/constant-key.js +94 -0
  67. package/dist/src/duck/distinct-key.js +283 -26
  68. package/dist/src/duck/events.js +39 -8
  69. package/dist/src/duck/executor-worker.js +2 -2
  70. package/dist/src/duck/executor.js +6 -5
  71. package/dist/src/duck/explain.js +4 -0
  72. package/dist/src/duck/functions.js +448 -49
  73. package/dist/src/duck/gaps.js +34 -16
  74. package/dist/src/duck/generated-plan.js +1070 -0
  75. package/dist/src/duck/generated.js +906 -0
  76. package/dist/src/duck/grants.js +1 -1
  77. package/dist/src/duck/held-types.js +64 -0
  78. package/dist/src/duck/history.js +4 -0
  79. package/dist/src/duck/jsonb-order.js +9 -12
  80. package/dist/src/duck/kept-route.js +6 -5
  81. package/dist/src/duck/kept.js +9 -7
  82. package/dist/src/duck/late.js +1469 -40
  83. package/dist/src/duck/libc-bound.js +341 -0
  84. package/dist/src/duck/libc-order.js +892 -126
  85. package/dist/src/duck/pg-eval-worker.js +7 -1
  86. package/dist/src/duck/pg-eval.js +6 -4
  87. package/dist/src/duck/pgtree.js +774 -23
  88. package/dist/src/duck/plan.js +275 -30
  89. package/dist/src/duck/publication.js +14 -0
  90. package/dist/src/duck/requests.js +9 -5
  91. package/dist/src/duck/resolved.js +117 -2
  92. package/dist/src/duck/rewrite.js +4567 -583
  93. package/dist/src/duck/row-filter.js +12 -6
  94. package/dist/src/duck/statement.js +19 -0
  95. package/dist/src/duck/store.js +294 -68
  96. package/dist/src/duck/stream.js +9 -5
  97. package/dist/src/duck/sync.js +602 -126
  98. package/dist/src/duck/token.js +4 -1
  99. package/dist/src/duck/types.js +63 -7
  100. package/dist/src/duck/usage.js +6 -4
  101. package/dist/src/duck/users.js +110 -9
  102. package/dist/src/duck/verified.js +159 -2
  103. package/dist/src/duck/views.js +4 -3
  104. package/dist/src/lib/cidr.js +177 -0
  105. package/dist/src/lib/database-url.js +55 -10
  106. package/dist/src/lib/instant.js +50 -0
  107. package/dist/src/lib/log-buffer.js +13 -4
  108. package/dist/src/lib/log-read.js +196 -0
  109. package/dist/src/lib/pg-numeric.js +387 -49
  110. package/dist/src/lib/secret-mask.js +142 -0
  111. package/dist/src/lib/table-sync.js +84 -19
  112. package/dist/src/web/app.js +9 -4
  113. package/dist/src/web/pages/status.js +178 -13
  114. package/dist/src/web/pages/syncs.js +1 -1
  115. package/dist/src/web/pages/tables.js +8 -3
  116. package/dist/src/web/pages/users.js +4 -3
  117. package/dist/src/web/shared.js +19 -3
  118. package/package.json +8 -7
@@ -24,7 +24,7 @@ export const syncsHref = (table) => `#observability/syncs?table=${encodeURICompo
24
24
  export const columnsPath = (table) => `/api/tables/${encodeURIComponent(table)}/columns`;
25
25
 
26
26
  /** A table's sync in one word (`/api/state` `tables[].sync.state`, src/cli/readiness.ts `syncStateOf`). */
27
- const SYNC_WORDS = { synced: "Synced", syncing: "Syncing", failed: "Failed", waiting: "Waiting" };
27
+ const SYNC_WORDS = { synced: "Synced", syncing: "Syncing", behind: "Behind", failed: "Failed", waiting: "Waiting" };
28
28
 
29
29
  /** Whether a table's name has the search box's text (any case; nothing typed: every table). */
30
30
  const nameMatches = (table, filter) => !filter || table.toLowerCase().includes(filter.trim().toLowerCase());
@@ -199,6 +199,8 @@ export function renderTables(s, ui = {}) {
199
199
  const { removing = null, choosing = null, columns = null, outcome = null, ticked = null, filter = "", show = "all", sizes, syncing = [], adding = [], shares = new Map(), before = new Map(), synced = null, open = () => false, limit = TABLE_ROWS } = ui;
200
200
  const now = ui.now ?? Date.now();
201
201
  if (!s.tables.length) return empty(`${s.source.database} has no table this user can read.`);
202
+ // why no table can be added or removed from here (a server's sources file this run cannot write), or null
203
+ const readOnly = s.sourcesReadOnly ?? null;
202
204
  const stream = s.stream ? new Map(streamRows(s.stream, now).tables) : null;
203
205
  // SEVEN NARROW COLUMNS OF FIXED WIDTHS, NOTHING LONG IN A CELL, AND NOTHING A CLICK INSERTS: a table's state is its word
204
206
  // over its copy's bar (a line kept for it when it has none); its last sync an age over how long it took; whatever is
@@ -227,7 +229,9 @@ export function renderTables(s, ui = {}) {
227
229
  ? `<a class="btn small" href="${esc(syncsHref(t.table))}">View events</a> ` +
228
230
  (syncing.includes(t.table) ? button("Syncing…", {}, "small", { room: ROOMS.sync, disabled: true }) : button(syncedNow ? "Synced ✓" : "Sync now", { syncTable: t.table }, "small", { room: ROOMS.sync })) + " " +
229
231
  // while it asks, Remove stays where it was, held down: the question beside it answers it
230
- (asking ? button("Remove", {}, "danger small", { disabled: true, expanded: true }) : button("Remove", { removeTable: t.table }, "danger small"))
232
+ (readOnly ? "" : asking ? button("Remove", {}, "danger small", { disabled: true, expanded: true }) : button("Remove", { removeTable: t.table }, "danger small"))
233
+ // a sources file this run cannot write (`s.sourcesReadOnly`, a server's): no control that could only fail
234
+ : readOnly ? ""
231
235
  : t.reason === null
232
236
  ? `${button("Add", { addTable: t.table }, "primary small")} ${choosing === t.table ? button("Columns…", {}, "small", { disabled: true, expanded: true }) : button("Columns…", { choose: t.table }, "small")}`
233
237
  : t.key.length ? "" : choosing === t.table ? button("Name its key…", {}, "small", { disabled: true, expanded: true }) : button("Name its key…", { choose: t.table }, "small");
@@ -284,7 +288,8 @@ export function renderTables(s, ui = {}) {
284
288
  const matching = filter.trim() ? `<p class="count dim">${n(shown.length)} with “${esc(filter.trim())}” in the name</p>` : "";
285
289
  const sizesFailed = added && sizes && "failure" in sizes ? `<p class="count">The synced tables' own sizes: could not read them (${esc(firstLine(sizes.failure.message))}; ${code("mason usage")} has them)</p>` : "";
286
290
  const rowsHtml_ = written.map(line).join("");
287
- return stray + (added ? "" : empty("No table added yet", `Add one from the list, as ${code("mason add <table>")} does.`)) + matching +
291
+ return stray + (readOnly === null ? "" : `<p class="count">Tables are not added or removed from this page: ${esc(readOnly)}.</p>`) +
292
+ (added ? "" : empty("No table added yet", readOnly === null ? `Add one from the list, as ${code("mason add <table>")} does.` : `The sources file names the tables to sync.`)) + matching +
288
293
  panel(`<table class="catalog"><thead><tr>${head.map(([h, c]) => `<th scope="col"${c ? ` class="${c}"` : ""}>${h}</th>`).join("")}</tr></thead>` +
289
294
  `<tbody>${rowsHtml_}</tbody></table>`) + pops.join("") + moreRows("tables", Math.min(limit, shown.length), shown.length, TABLE_ROWS) +
290
295
  (!shown.length ? `<p class="dim">No ${show === "added" ? "added " : show === "failed" ? "failed " : show === "not-added" ? "table that is not added, " : ""}table${filter.trim() ? `'s name has “${esc(filter.trim())}”` : ""}.</p>` : "") + sizesFailed +
@@ -3,7 +3,7 @@
3
3
  * POST /api/users/<name>/disable and /enable, DELETE /api/users/<name>. An added user's password lives in this page's memory until the person dismisses it, never in
4
4
  * storage, a URL or a log line.
5
5
  */
6
- import { actId, badge, button, code, copyable, copyClick, duration, empty, esc, json, panel, pop, renderFailure, ROOMS, status, utcMinute } from "../shared.js";
6
+ import { actId, badge, button, code, copyable, copyClick, duration, empty, esc, json, panel, pop, renderFailure, ROOMS, serverHostNote, status, utcMinute } from "../shared.js";
7
7
 
8
8
  /** The users as the page reads them: with each one's last use (`mason user list` asks the same), then every 30 s. */
9
9
  const USERS = "/api/users?lastUsed=1";
@@ -97,12 +97,13 @@ export function renderUsers(list, confirming = null, now = Date.now(), refused =
97
97
  * state), and a user's password is copied only from the card shown once when it is added (`renderAdded`). "" when the
98
98
  * run did not say.
99
99
  */
100
- export function renderUsersConnect(pg) {
100
+ export function renderUsersConnect(pg, served = false) {
101
101
  if (!pg || typeof pg.shown !== "string") return "";
102
102
  const rows = [["Host", pg.host, "users-connect-host"], ["Port", String(pg.port), "users-connect-port"], ["Database", pg.database, "users-connect-database"],
103
103
  ["SSL mode", pg.sslmode ?? "disable", "users-connect-sslmode"], ["URL", pg.shown, "users-connect-url"], ["psql", pg.psql, "users-connect-psql"]];
104
104
  return `<div class="connect"><p class="connect-title">Connect</p>` +
105
105
  `<dl class="facts">${rows.map(([k, v, id]) => `<dt>${esc(k)}</dt><dd>${copyable(id, v)}</dd>`).join("")}</dl>` +
106
+ (served ? `<p class="tip">${serverHostNote(pg)}</p>` : "") +
106
107
  `<p class="tip">A user's name and password go in place of ${code("<user>")} and ${code("<password>")}. The password is shown once, when the user is added: lost, remove the user and add it again.</p></div>`;
107
108
  }
108
109
 
@@ -192,7 +193,7 @@ export const page = {
192
193
  return {
193
194
  // the roles file's state is the run's (/api/state roles): a file that cannot be read refuses every user held to it
194
195
  users: u ? renderUsers(u, m.confirming, ctx.now().getTime(), m.refused, ctx.read(STATE)?.body?.roles ?? null) : "",
195
- "users-connect": renderUsersConnect(ctx.read(STATE)?.body?.postgres),
196
+ "users-connect": renderUsersConnect(ctx.read(STATE)?.body?.postgres, ctx.read(STATE)?.body?.served === true),
196
197
  added: m.added ? renderAdded(m.added) : "",
197
198
  action: m.actionFailure ? renderFailure(m.actionFailure) : "",
198
199
  };
@@ -292,6 +292,18 @@ export function failureOf(status, text) {
292
292
  next: [{ command: "mason status", what: "whether Mason runs, and why it stopped" }, { command: "mason start", what: "start it" }, { command: "mason ui", what: "this page, for the Mason that is running" }] };
293
293
  }
294
294
 
295
+ /**
296
+ * A FAILURE AS IT IS ON A SERVER'S PAGE (`/api/state` `served`: read through `mason ui --ssh`): the two the page words
297
+ * itself name the commands of a run on this machine, and here what stopped answering is the tunnel, opened from the
298
+ * person's own machine. Every other failure is the run's, and stands.
299
+ */
300
+ export function overTunnel(f) {
301
+ if (f.code !== "unreachable" && f.message !== "this page's cookie is not the running Mason's") return f;
302
+ return { code: f.code, message: "the tunnel to this server has closed", why: f.why,
303
+ fix: "mason ui --ssh <user@server>, on your own machine, opens this page again: each tunnel has its own address",
304
+ next: [{ command: "mason ui --ssh <user@server>", what: "this page, through a new tunnel" }] };
305
+ }
306
+
295
307
  /** A part of a page written from one read: its body rendered, its Failure said where it shows when `says` (not the
296
308
  * page's first read's: that is the banner), or that it is being asked. */
297
309
  export function readPart(ctx, path, render, says) {
@@ -328,7 +340,7 @@ export function statusKind(word) {
328
340
  const w = String(word ?? "").toLowerCase();
329
341
  if (/^(ready|synced|done|copied|completed|ok|streamed|running|live|added)\b/.test(w)) return "ok";
330
342
  if (/^(failed|error|refused|lost|not running|stopped)\b/.test(w)) return "bad";
331
- if (/^(under way|syncing|copying|waiting|catching up|not streamed|not kept current|unsynced|cancelled|warning|left out|pending)\b/.test(w)) return "warn";
343
+ if (/^(under way|syncing|behind|copying|waiting|catching up|not streamed|not kept current|unsynced|cancelled|warning|left out|pending)\b/.test(w)) return "warn";
332
344
  return "unknown";
333
345
  }
334
346
 
@@ -471,16 +483,20 @@ export const USERS_HASH = "#users";
471
483
  * place. `first` (nothing has asked Mason anything yet) leads with what the URL is for. Nothing from a run that does
472
484
  * not say it.
473
485
  */
474
- export function renderConnect(pg, { first = false, table = null } = {}) {
486
+ export function renderConnect(pg, { first = false, table = null, served = false, share = "" } = {}) {
475
487
  if (!pg || typeof pg.shown !== "string") return "";
476
488
  // a first statement to type that the request log lists (one that reads a copied table: UNLISTED_STATEMENTS)
477
489
  const tryIt = first && table ? `<dt>Then try</dt><dd>${copyable("connect-try", `select count(*) from ${entryName(table)};`)} <span class="dim">it shows on Queries</span></dd>` : "";
478
490
  return `<div class="connect${first ? " first" : ""}"><p class="connect-title">${first ? "Connect your first tool" : "Connect"}</p>` +
479
491
  (first ? `<p class="lead">Mason answers SQL over the Postgres protocol, as a read replica of your database would: point psql, a BI tool or your app's analytics code at this URL.</p>` : "") +
480
492
  `<dl class="facts"><dt>URL</dt><dd>${copyable("connect-url", pg.shown)}</dd><dt>psql</dt><dd>${copyable("connect-psql", pg.psql)}</dd>${tryIt}</dl>` +
481
- `<p class="tip"><a class="btn${first ? " primary" : ""}" href="${USERS_HASH}">Create a user</a> <span>Its name and password go in place of ${code("<user>:<password>")}: the Users page shows the whole URL once, as ${code("mason user add <name>")} prints it.</span></p></div>`;
493
+ (served ? `<p class="tip">${serverHostNote(pg)}</p>` : "") +
494
+ `<p class="tip"><a class="btn${first ? " primary" : ""}" href="${USERS_HASH}">Create a user</a> <span>Its name and password go in place of ${code("<user>:<password>")}: the Users page shows the whole URL once, as ${code("mason user add <name>")} prints it.</span></p>${share}</div>`;
482
495
  }
483
496
 
497
+ /** On a server's page the Postgres URL's host is the server's own view of itself: said beside it, as a tool elsewhere needs the server's address. */
498
+ export const serverHostNote = (pg) => `This is the address on the server: from elsewhere a tool connects to the server's own address, in place of ${code(pg.host ?? "localhost")}.`;
499
+
484
500
  /** Nothing to show, quietly: a title and a line under it (`hintHtml`: HTML, a command in code()). */
485
501
  export const empty = (title, hintHtml = "") => `<div class="empty"><p class="empty-title">${esc(title)}</p>${hintHtml ? `<p>${hintHtml}</p>` : ""}</div>`;
486
502
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@mason-data/cli",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "mason start: point Mason at your Postgres and pick the tables to copy. Fast analytics for the database your app already has.",
5
5
  "keywords": ["mason", "analytics", "postgres", "duckdb", "cli"],
6
6
  "homepage": "https://github.com/embeddable-hq/mason-data#readme",
7
7
  "repository": { "type": "git", "url": "git+https://github.com/embeddable-hq/mason-data.git", "directory": "cli" },
8
8
  "license": "Apache-2.0",
9
9
  "type": "module",
10
- "bin": { "mason": "dist/src/cli/main.js" },
10
+ "bin": { "mason": "dist/src/cli/mason.cjs" },
11
11
  "files": ["dist", "README.md", "LICENSE"],
12
12
  "engines": { "node": ">=22.13" },
13
13
  "scripts": {
@@ -16,16 +16,17 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@duckdb/node-api": "2.0.0-r.1",
19
+ "@duckdb/node-bindings": "2.0.0-r.1",
19
20
  "@electric-sql/pglite": "^0.5.10",
20
21
  "libpg-query": "^18.1.5",
21
22
  "pgsql-deparser": "18.3.8",
22
23
  "yaml": "^2.6.0"
23
24
  },
24
25
  "optionalDependencies": {
25
- "@mason-data/extensions-darwin-arm64": "0.8.0",
26
- "@mason-data/extensions-darwin-x64": "0.8.0",
27
- "@mason-data/extensions-linux-arm64": "0.8.0",
28
- "@mason-data/extensions-linux-x64": "0.8.0",
29
- "@mason-data/extensions-win32-x64": "0.8.0"
26
+ "@mason-data/extensions-darwin-arm64": "0.10.0",
27
+ "@mason-data/extensions-darwin-x64": "0.10.0",
28
+ "@mason-data/extensions-linux-arm64": "0.10.0",
29
+ "@mason-data/extensions-linux-x64": "0.10.0",
30
+ "@mason-data/extensions-win32-x64": "0.10.0"
30
31
  }
31
32
  }