@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
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
package/README.md CHANGED
@@ -1,3 +1,269 @@
1
- # Temporary Holding Version
1
+ # @mason-data/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The `mason` command points [Mason](https://masondata.ai) at the Postgres your app already uses, copies the tables you
4
+ add, keeps the copy current, and serves it to BI tools and your app over the Postgres protocol. Every command works the
5
+ same for you and for a coding agent (Claude Code, Codex, …): it prints for a person, or with `--json` as data, and its
6
+ exit code says how it went ([for coding agents](#for-coding-agents)).
7
+
8
+ There is nothing to clone or build: install this package and run `mason`. It needs Node 22.13 or later and nothing
9
+ else — no Docker, no database of its own; `mason doctor` checks this machine and says what is missing. On a terminal
10
+ its output is coloured and wrapped to the window, and a command that takes a moment shows a spinner; piped,
11
+ redirected, with `--json` or with `NO_COLOR` set, it is plain text.
12
+
13
+ **A Mason project is your app's folder.** `mason init` makes it one: its sources file in `mason/sources/` — the
14
+ database and the tables to copy — yours to commit; this machine's copy of your data and Mason's state in `.mason/`,
15
+ both rebuilt from that file. Every other command works
16
+ anywhere in the folder, as git does. `mason destroy` removes the copy and the state. ([Cleaning up](#cleaning-up))
17
+
18
+ ```sh
19
+ npm i -g @mason-data/cli
20
+ cd my-app
21
+ mason doctor # Node, memory, ports: what is missing, and what to do
22
+ mason init # this folder becomes a project, on the database in .env's DATABASE_URL (checked first)
23
+ mason tables # the tables: rows, keys, added; a table's columns with `mason tables orders`
24
+ mason add orders customers # add them to the sources file (--columns id,status: those only)
25
+ mason start # Mason in the background, copying them, then returns
26
+ mason wait # until every added table is copied and served
27
+ mason user add metabase # a user for a BI tool: its password, printed once, and the URL to paste
28
+ mason ui # the status page and the users, in your browser
29
+ ```
30
+
31
+ `mason init` takes the database's URL as an argument too (`mason init postgres://user:password@localhost:5432/your_app`);
32
+ without one it reads `DATABASE_URL` from the folder's `.env.local`, `.env` or the shell. The sources file it writes says
33
+ `url: ${DATABASE_URL}`, and the URL itself stays in `.env` (init writes the line there when you gave the URL as an
34
+ argument or it was only in the shell, and gitignores that file): change it there when the password changes. The
35
+ database is required.
36
+
37
+ **No database at hand?** `mason init demo && mason start`, in a new folder, makes a project on a demo database Mason
38
+ hosts itself — a small shop (`customers`, `products`, `orders`, `order_items`; the first three added, `order_items`
39
+ left for your first `mason add order_items` after `mason start`), kept in `.mason/demo/`, served on a port of this machine, with a new order every few seconds (`MASON_DEMO_CHANGE_MS`, default 5000) so
40
+ `mason status` shows the copy keeping up. Nothing to install. It is read by polling only (it has no change log), and
41
+ `mason destroy` removes it with the copy.
42
+
43
+ ## The project: its sources file, in your app
44
+
45
+ ```
46
+ my-app/
47
+ .env the database's URL (DATABASE_URL), as your app has it (gitignored)
48
+ .mason/ this machine's: Mason's key, the copy of your data, its state and logs (gitignored)
49
+ mason/sources/app.yml the database and the tables Mason copies — commit it
50
+ mason/mason.yml the Mason version the project is written for — commit it: production runs that version
51
+ ```
52
+
53
+ The sources file is named for the database (`app.yml` above) and lists what Mason copies, one entry per table:
54
+
55
+ ```yaml
56
+ kind: postgres
57
+ url: ${DATABASE_URL} # a variable from .env.local, .env or the shell; a literal URL is allowed for local dev
58
+ tables:
59
+ orders: # a bare name is in `public`; `billing.invoices` names its schema
60
+ columns: [id, customer_id, status, amount, created_at, payload]
61
+ customers: {} # every column
62
+ events:
63
+ exclude: [raw_body] # every column but these
64
+ sessions:
65
+ key: [tenant_id, id] # only for a table with no primary key: the unique columns to copy it by
66
+ ```
67
+
68
+ - An entry is `{}` (every column), `columns:` (those; the primary key is always copied), `exclude:` (every column but
69
+ those) — never both — and, for a table with no primary key, `key:`: the columns that tell its rows apart, which it is
70
+ copied by (polling only: logical replication cannot apply a change by them). A table with a primary key refuses `key:`.
71
+ - Column types are not in the file: Mason reads them from the database at every start.
72
+ - `url:` is `${NAME}`, read from `.env.local`, `.env` or the shell — or a literal URL, for a database on your machine.
73
+ - One source per project for now, and `postgres` is its only kind.
74
+ - `mason add` and `mason remove` edit one entry and keep every other byte of the file, your comments included, and
75
+ apply at once; an entry you edit by hand is applied at the next `mason start`. A mistake — a column the table does
76
+ not have, `columns` and `exclude` together, a variable that is not set — is refused, every one of the file's
77
+ mistakes on one report, each with its key and the way out (`did you mean …?`).
78
+
79
+ `mason init --folder <dir>` puts `sources/` elsewhere in the folder (`--folder .` for the folder itself). Keep it in
80
+ your app's repo, so a schema change and the tables that copy it travel together. A teammate who clones the repo runs
81
+ `mason init` once: the sources file is kept, and `.mason/` is per machine.
82
+
83
+ **A project an older Mason made** (its tables listed in `mason/models/`) is refused by `mason start`; nothing is
84
+ migrated. `mason init` in it writes the sources file alone — its `.mason/` and copy kept — then `mason add` each table
85
+ again and remove `mason/models/`.
86
+
87
+ ## Commands
88
+
89
+ | Command | What it does |
90
+ |---|---|
91
+ | `mason help [<command> \| <guide>]` | The commands by step, one command's usage, or one of the guides, offline: `mason help agent` (driving Mason from a coding agent: the commands in order, `--json` and its shapes, exit codes, failures) and `mason help production` (the road from a laptop to production: the image, its settings, Docker Compose, logical replication, upgrades). |
92
+ | `mason doctor` | Checks what Mason needs here — Node 22.13+, memory, Mason's ports — and in a project's folder its database; with Mason running for it, the run itself: whether every added table is copied, the statements refused or slow in the last day. Every failing line says what to do; it ends with the next command. |
93
+ | `mason init [<postgres url>]` | Makes this folder a project: checks it can read the database (nothing is written when it cannot; `--skip-check` writes anyway), writes `.mason/config.json` (Mason's key, never the URL) and `mason/sources/<database>.yml` (keeping a sources file already there), adds the URL to `.env` when it was not there, and adds `.mason/` and that `.env` to `.gitignore`. The URL from the argument, else `DATABASE_URL` in `.env.local`, `.env` or the shell. `--name` sets the project's name (default: the folder's) — the database name a tool connects to — `--folder` where its files go, `--remote` allows a database on another machine. Refuses a folder already in a project, but for one with no sources file (an older Mason's), whose sources file alone it writes. |
94
+ | `mason start` | Starts Mason for this folder's project in the background and returns once it is up. Run it again any time: it says so when Mason is already running. `--replication` reads the database's change log as production does (below); `--poll` goes back. `--self-signed-tls` serves the Postgres API over TLS; `--attach` stays in the foreground. |
95
+ | `mason tables [<table>…] [--all]` | The tables this user can read against the sources file, one line each: rows, primary key, whether added (and with which columns), and why a table cannot be added yet (no primary key: the `--key` to add it by). Name a table (or pass `--all` for every one) for its columns: each one's type, what it references, and whether `mason add` would leave it out as a secret. It reads the project's database itself, so Mason need not be running. |
96
+ | `mason add <table>… [--columns …] [--include …] [--key …]` | Copies tables into Mason, each one an entry of the sources file: every column (`orders: {}`), or `--columns id,status,created_at` for those (one table at a time; the key is written with them). Columns that look like secrets (`password`, `token`, `api_key`, `private_key`, …) are left out, named, and written as `exclude:`; `--include password_hash` adds one. `--key tenant_id,id` adds a table with no primary key, copied by those columns. Adding a table again with other columns changes its entry. Mason need not be running: the entry is checked against the database as the run checks it, and the copy begins at `mason start` (with Mason running, at once). |
97
+ | `mason remove <table>…` | Takes tables out of the sources file — each one's entry, every other byte as it was — and Mason drops their copies: at once when it is running, else at the next `mason start`. Mason need not be running. |
98
+ | `mason wait [--timeout <s>]` | Waits until every added table is copied and served, showing every added table on a line of its own: 100% for the finished ones, in the order they finished, and a bar for each still copying (rows, %, rate, time left) at the bottom. Exit 0 ready, 1 a copy failed (printed, with what to do), 124 timed out (default 600 s). |
99
+ | `mason user add\|list\|remove <name>` | The users of the Postgres API: a name and a password that does not expire, as a BI tool's datasource form asks for them or an app's `DATABASE_URL` carries them. `add` prints the password once with the URL; `remove` refuses the next connection at once and ends the sessions it holds at their next statement, without a restart. |
100
+ | `mason mcp [--url <url>]` | An MCP server on stdio for Claude Code, Claude Desktop and other agents (below): the tables Mason serves with each copy's rows and how current it is, a table's columns, a statement's rows with their Postgres types or Mason's refusal with its way out, and EXPLAIN — connected as a `mason user` (`MASON_URL`, else `--url`), beside `mason start` or a `mason serve` elsewhere. |
101
+ | `mason ui` | Opens the running Mason's web page in your browser: what `mason status` says, refreshed every 2 s, and the users of `mason user` (add one, its password shown once; remove one). It prints the URL with the run's token, which the page trades for a cookie on the first visit; `--no-open` prints it only, `--json` is `{ url, port }`. `mason start`'s alone: `mason serve` serves no page. |
102
+ | `mason status` | What Mason is doing: the database and how its changes are read, the project folder, the copy's file, the Postgres API and how it listens, each table's copy, the statements answered slowly in the last day and the ones refused or failed, each with its reason. In replication mode, where the stream is. |
103
+ | `mason sync <table>…` | Makes the copy of these tables current now: a table over the sync's row limit (1,000,000 rows) is copied and served but kept current only so, or by replication. |
104
+ | `mason logs [-n <lines>] [-f] [--start] [--requests [--failed]]` | Mason's log: every copy, poll and statement, with warnings and errors and their reasons; `-f` follows it, `--start` shows what `mason start` printed instead. `--requests` is the request log: every statement a tool sent, when, the outcome and SQLSTATE, how long, the rows, the tool and the user; `--failed` only the refused and failed ones. |
105
+ | `mason stop` | Stops Mason for this project. The copy and the state stay: the next `mason start` resumes in seconds. |
106
+ | `mason destroy` | Removes this project's copy of the data (its file) and Mason's state in `.mason/`, stopping Mason first if it runs — and, for a project started with `--replication`, its slot and publication on the database. Your files, `.mason/config.json` and the users stay: the next `mason start` copies the tables again. |
107
+ | `mason serve` | Mason for a server or a container: `mason start --attach` configured by the environment alone, its state in one folder (a volume), its log on stdout, `GET /healthz`, a clean stop on SIGTERM. `mason serve --help` lists every setting with its default. |
108
+
109
+ Every command documents itself: `mason --help` lists them, and `mason <command> --help` (or `mason help <command>`) says
110
+ how to use one, with examples. Every command but `help` and `mcp` (whose stdout is the MCP protocol's) takes `--json`. A command means the project of the folder it
111
+ runs in, or the nearest one above it.
112
+
113
+ ## What `mason start` does
114
+
115
+ 1. **It only reads the database** — the one the sources file names. Mason polls it with `SELECT` statements. It does not
116
+ need `wal_level = logical`, a replication slot, a publication or the `REPLICATION` attribute, and it creates none of
117
+ them. When you stop it, nothing is left behind on the database. A table over 1,000,000 rows is copied and served but
118
+ kept current only by `mason sync` or by replication, and `mason status` says so on its line. (`--replication` is the
119
+ exception, said below: it reads the change log, as production does, and makes its own slot.)
120
+ 2. **It copies each added table** into a DuckDB file in `.mason/`, a range of pages at a time, a few in parallel, one
121
+ Postgres backend each, smallest table first; a copy that stops continues with the ranges it did not commit. The table
122
+ is served the moment its copy lands; the copy runs beside the polls, never instead of them.
123
+ 3. **It serves the Postgres API** on 5433, or on the next port free on every address when something holds 5433 (often a
124
+ second Postgres; `mason start` says what held it) — and records it: every later start listens on the same port, so a
125
+ tool's saved connection keeps working, and refuses, naming what holds it, rather than move. `MASON_PG_PORT=55433` in
126
+ your shell names the port, or moves the project to it (said). It listens on 127.0.0.1 unless `MASON_PG_HOST` names
127
+ another address; off this machine it needs TLS (`--self-signed-tls`, or `MASON_PG_TLS_CERT` / `MASON_PG_TLS_KEY`).
128
+ 4. **It runs in the background**, copying what the sources file names: `mason add` and `mason remove` change it at once,
129
+ a hand edit (or an add or remove made while Mason was stopped) at the next `mason start`.
130
+
131
+ Mason's own settings (`MASON_POLL_MS`, `MASON_DUCKDB_MEMORY`, `MASON_COPY_PARALLEL`, …) set in your shell reach Mason.
132
+ A copy reads `MASON_COPY_PARALLEL` chunks at once (default 3), each sized to take about `MASON_COPY_CHUNK_SECONDS` to
133
+ read (default 12) at the rate it measures on your connection, so `mason wait` moves every few seconds even over a slow
134
+ link, and a copy that stops reads at most those chunks again.
135
+ A JSON value your queries keep reading (`payload ->> 'customer_id'`) is stored as a column of the copy and read from
136
+ there (`mason status` lists them); `MASON_STORE_JSON_PATHS=off` turns that off.
137
+
138
+ ## `mason start` options
139
+
140
+ | Option | What it does |
141
+ |---|---|
142
+ | `--replication` | Read the database's changes from its change log (logical replication), as production does, instead of polling. It needs `wal_level = logical` and a user that may replicate, and makes its own publication and slot (`mason_dev_<project>_<source>`, the source the sources file's name; past 63 bytes, a prefix and a hash) on the database. The project keeps the mode until `--poll`; a switch keeps the copies and catches each table up by one poll. `mason stop` leaves the slot (it holds WAL until the next start); `mason destroy` and `--poll` drop it. |
143
+ | `--publication <name>` | With `--replication`: a publication the database owner made (else `PG_PUBLICATION`), read as it is, verified to carry every added table and column, never created, altered or dropped. A read replica needs one. |
144
+ | `--poll` | Read them with `SELECT` again (the default). |
145
+ | `--self-signed-tls` | The Postgres API requires TLS, from a certificate made for this project in `.mason/` (served again by every later start with the flag); clients connect with `sslmode=require`. |
146
+ | `--port <n>` | The port the `mason` commands reach it on (default 4039, or the next free one). |
147
+ | `--attach` | Stay in the foreground: the terminal shows Mason's progress, and Ctrl-C stops it. |
148
+
149
+ A database on another machine needs `mason init --remote`, which records that you mean it: the copy leaves the
150
+ database's network, and each added table under the row limit is read whole every few seconds (`--replication` reads only
151
+ its changes). Point it at a read replica, never the primary your app writes to.
152
+
153
+ **When the database moves.** Mason records the Postgres server it copied from. The same database on a new server (a
154
+ restore, a move to another host) is read again at the next `mason start`: every row is compared with the copy, and only
155
+ what changed is written. A different database is refused: `mason destroy`, then `mason start`, copies it.
156
+
157
+ ## For coding agents
158
+
159
+ `mason help agent` is the page an agent reads first, offline: the commands in the order a project takes them, every
160
+ answer's shape, the exit codes and the failure codes. In short:
161
+
162
+ - Every command but `help` and `mcp` takes `--json` and answers with **one JSON object on stdout**, always with
163
+ `next: [{ command, what }]` — the commands to run next, the same a person reads under `Next:`. The other keys are the
164
+ command's own; `mason help <command>` names them on its `--json` line. Lists are wrapped (`{ tables: […], next }`).
165
+ - A failure is `{ "error": { "code", "message", "why", "fix" }, "next": […] }` on stdout, and the **exit code** says it:
166
+ 0 done, 1 failed with the reason, 2 the command line is wrong, 124 `mason wait` ran out of time. The codes are a closed
167
+ set (`usage`, `no_project`, `bad_config`, `bad_source`, `not_running`, `unreachable`, `not_found`, `nothing_yet`,
168
+ `refused`, `failed`, `timeout`). For a person the same failure is three short lines — what is wrong, `why`, `fix` — then `Next:`.
169
+ - Progress (a spinner, `mason wait`'s bars) goes to stderr, and only on a terminal; a pipe gets plain text, and nothing
170
+ under `--json`. The Postgres API's URL is in `mason status --json` (`postgres.url`); a user's password is printed by
171
+ `mason user add` once: never read `.mason/config.json`.
172
+
173
+ ```sh
174
+ mason doctor --json && mason init --json && mason start --json
175
+ mason tables --json # choose the tables
176
+ mason add orders customers --json && mason wait --json
177
+ mason user add app --json # the password and URL for the app, as data
178
+ ```
179
+
180
+ ### From an MCP client: `mason mcp`
181
+
182
+ A chat agent asks Mason with tools, over MCP, rather than through a shell. `mason mcp` is an MCP server on stdin and
183
+ stdout (newline-delimited JSON-RPC; stdout carries the protocol alone, what it says goes to stderr) that serves until
184
+ the client closes stdin; it takes no `--json`, and says a failure to start on stderr. It connects to the Postgres API as
185
+ a `mason user`, by the URL `mason user add` printed — `MASON_URL`, or `--url` — so it works beside `mason start` as against a `mason serve` elsewhere; TLS is as the URL's `sslmode`
186
+ says (a host off the machine: `require` when it names none). It never makes a user by itself: with no URL, it refuses
187
+ and says `mason user add claude`.
188
+
189
+ | Tool | Arguments | What it answers |
190
+ |---|---|---|
191
+ | `tables` | none | each table Mason serves, named as a statement writes it (`public.orders`, `public."Clients"`): `rows_about` (the copy's estimate), `kept` (how it is kept current, as `mason status` says it) and `kept_current_by` (`polling`, `logical replication`, `copying again`, or null when nothing keeps it current now) |
192
+ | `describe` | `table` (as `tables` names it, or a bare name in `public`) | its columns with their Postgres types and NOT NULL, its primary key, its foreign keys, comments, and the same rows and `kept` |
193
+ | `query` | `sql`, `max_rows` (200, at most 10,000), `timeout_ms` (30,000, at most 300,000) | `columns` (name, Postgres type), `rows` as Postgres's text, `truncated` when `max_rows` cut it; a refusal is a tool error with the SQLSTATE, message, DETAIL and HINT (the way out) |
194
+ | `explain` | `sql`, `analyze`, `timeout_ms` | Mason's EXPLAIN: each copy read with its rows and how current it is, the SQL it runs, the plan |
195
+
196
+ Its resources are `mason://help/agent` and `mason://help/mcp` (the two pages `mason help` prints). Every statement it
197
+ runs is in the request log under `application_name` `mason-mcp/<the client's name>` (`mason logs --requests`), a query
198
+ `max_rows` cut with the rows it sent; its catalog reads (the tables, a table's columns) are not, as no catalog read is.
199
+ A request the client cancels (`notifications/cancelled`) cancels its statement on the Postgres API.
200
+
201
+ Claude Code, in the project's folder with Mason running, the URL in the server's environment (the server's name
202
+ comes before `--env`, which takes every value after it):
203
+
204
+ ```sh
205
+ mason user add claude # the URL, printed once
206
+ claude mcp add mason --env MASON_URL="postgres://claude:<password>@127.0.0.1:5433/shop" -- mason mcp
207
+ # or on its command line, where ps shows it to this machine's users and claude mcp add prints it
208
+ claude mcp add mason -- mason mcp --url "postgres://claude:<password>@127.0.0.1:5433/shop"
209
+ ```
210
+
211
+ Claude Desktop, in `claude_desktop_config.json`. The app often starts a server without your shell's `PATH`, where
212
+ neither `mason` nor the `node` its first line asks for is found: name both by their absolute paths (`which node`,
213
+ `which mason`):
214
+
215
+ ```json
216
+ {
217
+ "mcpServers": {
218
+ "mason": {
219
+ "command": "/absolute/path/to/node",
220
+ "args": ["/absolute/path/to/mason", "mcp"],
221
+ "env": { "MASON_URL": "postgres://claude:<password>@127.0.0.1:5433/shop" }
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ ## Cleaning up
228
+
229
+ Three things exist once a project has run: your **files** (the sources file, in your app), the **copy of your data** (a
230
+ file in `.mason/`) and **Mason's state** (`.mason/`: its settings, logs and the request log). Each way of cleaning up
231
+ keeps a different set:
232
+
233
+ | You want | Run | Keeps |
234
+ |---|---|---|
235
+ | Pause | `mason stop` | the data, the state, your files: the next `mason start` resumes in seconds |
236
+ | Copy the data again, or free its disk | `mason destroy` | your files, `.mason/config.json` and the users: the next `mason start` copies everything again |
237
+
238
+ Restarting the machine is a `mason stop` that nobody ran: the next `mason start` resumes. In a demo project
239
+ (`mason init demo`) the demo database is state too: `mason destroy` removes it with the copy, and the next `mason start`
240
+ seeds it again.
241
+
242
+ ## Requirements
243
+
244
+ `mason doctor` checks all of this and says what is missing.
245
+
246
+ - Node 22.13 or later. The SQL parser (`libpg-query`), the catalog (`PGlite`) and the engine (DuckDB) ship as
247
+ WebAssembly or prebuilt binaries: no native build, no Python or compiler needed.
248
+ - Memory: the copy may use half the container's memory limit by default, else half the machine's RAM
249
+ (`MASON_DUCKDB_MEMORY=4GB` sets it; `mason doctor` says it). Sizing a server:
250
+ [deploy/mason-data/README.md#memory](https://github.com/embeddable-hq/mason-data/blob/main/deploy/mason-data/README.md#memory).
251
+ - Postgres 13 or later. Each table needs a primary key, or a `key:` in the sources file (polling only).
252
+
253
+ ## Security
254
+
255
+ `mason start`'s command server listens on 127.0.0.1 only; each request must carry the run's token, read from the
256
+ project's `.mason/config.json`, which a browser cannot send — or, for its web page (`mason ui`), the cookie the run's token is traded for on the first visit (HttpOnly, SameSite=Strict, its Host and every change's Origin checked). The Postgres API takes a user's password (`mason user`): a
257
+ MAC over its name keyed by the project's API key, so rotating the key removes every user at once. Off loopback the Postgres API is TLS or refused.
258
+
259
+ What a project keeps on this machine (the database URL, the API key, the run's token, the copy, its log and request
260
+ log) is in its `.mason/`, readable by your user only and gitignored by `mason init`.
261
+
262
+ ## In production
263
+
264
+ Only the project folder travels — `mason/`, with `mason.yml` naming the Mason version it is written for. Production runs
265
+ it as one container, `ghcr.io/embeddable-hq/mason-data`, with `mason serve`: the same run, configured by the environment,
266
+ its copy on one volume, reading Postgres's change log (`MASON_CHANGES=replication`) or polling it. `mason help
267
+ production` is the road, offline; `deploy/mason-data/` in the repo is the Compose file and its walkthrough.
268
+
269
+ Apache-2.0.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A refusal the Postgres listener answers as an ErrorResponse: a login or a users file that would not do
3
+ * (src/duck/users.ts), with the HTTP-style status the listener maps to a SQLSTATE (src/api/postgres/server.ts
4
+ * `sqlstateOf`) and a stable machine-readable code.
5
+ */
6
+ export class ApiError extends Error {
7
+ status;
8
+ /** The STABLE machine-readable name of the failure (`invalid_user`, `users_unreadable`, ...), carried in the
9
+ * ErrorResponse's detail so a reader branches on it instead of parsing `message`. */
10
+ code;
11
+ /** The user a refused `mason user` login asked for (src/duck/users.ts): its request-log row names it. */
12
+ userName;
13
+ constructor(status, message, code) {
14
+ super(message);
15
+ this.status = status;
16
+ this.code = code;
17
+ }
18
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * THE CATALOG'S WORKER: PGlite (Postgres as WebAssembly) runs HERE, on a worker thread, never on the listener's own
3
+ * (review of #480: a forwarded `SELECT pg_sleep(3600)` froze every other Postgres session, and PGlite ignores
4
+ * `statement_timeout`). The main thread (catalog.ts PgCatalog) posts one request at a
5
+ * time on the port and holds a deadline for it; past the deadline it TERMINATES this thread and starts another, so a
6
+ * statement that never ends costs one instance, not the process.
7
+ *
8
+ * Messages in: `{ id, op: "init", statements, optional? }` (start PGlite and run the DDL; the reply names the refused
9
+ * statement, or the optional ones refused),
10
+ * `{ id, op: "raw", bytes }` (frontend protocol bytes in, backend bytes out), `{ id, op: "query", sql }` (rows as
11
+ * objects, for the server's own lookups and the tests), `{ id, op: "close" }`. Every reply carries the id:
12
+ * `{ id, ok: true, bytes | rows }` or `{ id, ok: false, message }`.
13
+ */
14
+ import { parentPort } from "node:worker_threads";
15
+ import { PGlite } from "@electric-sql/pglite";
16
+ import { citext } from "@electric-sql/pglite/contrib/citext";
17
+ const port = parentPort;
18
+ if (port === null)
19
+ throw new Error("catalog-worker.ts runs on a worker thread only");
20
+ const errorMessage = (e) => (e instanceof Error ? e.message : String(e));
21
+ let db = null;
22
+ port.on("message", (m) => {
23
+ void (async () => {
24
+ try {
25
+ switch (m.op) {
26
+ case "init": {
27
+ // citext bundled: a copied citext column is declared as itself (catalog.ts `catalogDdl`)
28
+ const instance = new PGlite({ extensions: { citext } });
29
+ await instance.waitReady;
30
+ for (const statement of m.statements) {
31
+ try {
32
+ await instance.exec(statement);
33
+ }
34
+ catch (e) {
35
+ // the half-made instance is discarded (the start failed, said below with the statement); a close that fails is said too
36
+ await instance.close().catch((ce) => console.error(`catalog worker: PGlite did not close cleanly after a refused DDL: ${errorMessage(ce)}`));
37
+ throw new Error(`the catalog DDL was refused (${errorMessage(e)}) at:\n${statement}`, { cause: e });
38
+ }
39
+ }
40
+ // a statement the catalog may do without (a source's column default or view, catalog.ts `catalogDefaults`,
41
+ // `catalogViews`): each on its own (a multi-statement one is its own implicit transaction: all or none), in
42
+ // passes while one more stands — a view over a view stands once the other does — and the refused ones named in
43
+ // the reply for the log
44
+ let left = [...(m.optional ?? [])];
45
+ let refused = [];
46
+ for (let progress = true; progress && left.length > 0;) {
47
+ refused = [];
48
+ for (const statement of left) {
49
+ try {
50
+ await instance.exec(statement);
51
+ }
52
+ catch (e) {
53
+ refused.push({ statement, message: errorMessage(e) });
54
+ }
55
+ }
56
+ progress = refused.length < left.length;
57
+ left = refused.map((r) => r.statement);
58
+ }
59
+ db = instance;
60
+ port.postMessage({ id: m.id, ok: true, ...(refused.length > 0 ? { refused } : {}) });
61
+ return;
62
+ }
63
+ case "raw": {
64
+ if (db === null)
65
+ throw new Error("the catalog is not started");
66
+ const bytes = await db.execProtocolRaw(m.bytes);
67
+ const copy = new Uint8Array(bytes); // PGlite may reuse its buffer: a copy of our own is transferred
68
+ port.postMessage({ id: m.id, ok: true, bytes: copy }, [copy.buffer]);
69
+ return;
70
+ }
71
+ case "query": {
72
+ if (db === null)
73
+ throw new Error("the catalog is not started");
74
+ const result = await db.query(m.sql);
75
+ port.postMessage({ id: m.id, ok: true, rows: result.rows });
76
+ return;
77
+ }
78
+ case "close": {
79
+ const instance = db;
80
+ db = null;
81
+ if (instance !== null)
82
+ await instance.close();
83
+ port.postMessage({ id: m.id, ok: true });
84
+ return;
85
+ }
86
+ }
87
+ }
88
+ catch (e) {
89
+ port.postMessage({ id: m.id, ok: false, message: errorMessage(e) });
90
+ }
91
+ })();
92
+ });