@mason-data/cli 0.7.0 → 0.9.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 +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- 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 +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- 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 +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -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 +15 -13
- 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 +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
package/dist/src/cli/init.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `mason init [<postgres url>]` (a project directory is initiated as other CLIs do it, by `mason init`):
|
|
3
|
-
* makes the folder it is run in a Mason project. The database's URL comes from the argument, else
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* makes the folder it is run in a Mason project. The database's URL comes from the argument, else `MASON_DATABASE_URL`
|
|
4
|
+
* (Mason's own, for a database other than the app's), else `DATABASE_URL` (the variable web frameworks use), each from
|
|
5
|
+
* the folder's `.env.local`, then `.env`, then the shell (`appDatabaseUrl`) — or, when a sources file is here already (a
|
|
6
|
+
* clone), the variable its `url: ${…}` names, read the same way, so the database checked is the one the run copies from.
|
|
7
|
+
* The database is checked as `mason start` checks it (checkSource) and nothing is written when it fails, unless
|
|
8
|
+
* --skip-check says the database is not up yet.
|
|
7
9
|
*
|
|
8
10
|
* It writes `.mason/config.json` (0600: the project's name and Mason's key), the project's source file
|
|
9
|
-
* (`mason/sources/<database>.yml`, src/cli/sources.ts: `kind: postgres`, `url: ${DATABASE_URL}
|
|
10
|
-
* URL itself in `.env`, never in a committed file (the sources file is the
|
|
11
|
-
* of it): nothing more when it came from there, the line added when it came
|
|
12
|
-
* `.mason/` and that .env to the folder's .gitignore. It refuses a folder that
|
|
13
|
-
* — but for a project with no source file (one an older Mason made), whose
|
|
11
|
+
* (`mason/sources/<database>.yml`, src/cli/sources.ts: `kind: postgres`, `url: ${DATABASE_URL}` — or the variable the
|
|
12
|
+
* URL was read from, `tables: {}`, and `database: <name>` only under `--database`) — the URL itself in `.env`, never in a committed file (the sources file is the
|
|
13
|
+
* developer's to commit, so the URL stays out of it): nothing more when it came from there, the line added when it came
|
|
14
|
+
* from the argument or the shell — and adds `.mason/` and that .env to the folder's .gitignore. It refuses a folder that
|
|
15
|
+
* is in a project already (here or above) — but for a project with no source file (one an older Mason made), whose
|
|
16
|
+
* source file it writes, nothing else changed.
|
|
14
17
|
*
|
|
15
18
|
* `mason init demo` (src/cli/demo.ts) writes the same files for a database Mason hosts itself: the demo's URL, on a port
|
|
16
19
|
* free now, in `.env` (the one place the port is kept), three of the demo's four tables already added (the fourth,
|
|
@@ -27,47 +30,63 @@ import { cliPinProblem, ensurePin } from "./pin.js";
|
|
|
27
30
|
import { version } from "./version.js";
|
|
28
31
|
import { parseVersion, PIN_FILE } from "../lib/project-version.js";
|
|
29
32
|
import { appDatabaseUrl, appEnvValue, appendEnvLine, ENV_FILE } from "../lib/dotenv.js";
|
|
30
|
-
import { DEMO_DIR, NAME_RE, STATE_DIR, brokenProblem, findProject, ignoresStateDir, nameFromFolder, newConfig, writeConfig } from "./project.js";
|
|
31
|
-
import { DEMO_ADDED_TABLES, DEMO_ARG, DEMO_PORT, DEMO_TABLE_TO_ADD, demoUrl } from "./demo.js";
|
|
33
|
+
import { DEMO_DIR, NAME_RE, STATE_DIR, brokenProblem, findProject, folderPlace, ignoresStateDir, nameFromFolder, newConfig, projectWriteRefusal, writeConfig } from "./project.js";
|
|
34
|
+
import { DEMO_ADDED_TABLES, DEMO_ARG, DEMO_GENERATED, DEMO_PORT, DEMO_SYNC, DEMO_TABLE_TO_ADD, demoUrl } from "./demo.js";
|
|
35
|
+
import { GENERATED_DIR, generatedDir } from "./generated-files.js";
|
|
32
36
|
import { freePort } from "./ports.js";
|
|
33
|
-
import {
|
|
34
|
-
|
|
37
|
+
import { databaseNameProblem, DEFAULT_DATABASE } from "../lib/database-name.js";
|
|
38
|
+
import { DEFAULT_SYNC, syncText } from "../lib/table-sync.js";
|
|
39
|
+
import { newRolesText, rolesFile } from "./roles.js";
|
|
40
|
+
import { newSourceText, OLD_TABLES_DIR, parseSourceText, shownPath, sourceFileNames, sourceNameFor, sourceOf, sourcesDir } from "./sources.js";
|
|
41
|
+
export const INIT_USAGE = `usage: mason init [<postgres url> | demo] [--name <name>] [--database <name>] [--folder <dir>] [--remote] [--skip-check] [--json]
|
|
35
42
|
|
|
36
43
|
Makes this folder a Mason project — run it in your app's folder. It checks it can read the database (and writes
|
|
37
44
|
nothing when it cannot), then creates:
|
|
38
|
-
.mason/ this machine's: Mason's key and, once Mason runs, its
|
|
39
|
-
mason/sources/<db>.yml the database and the tables to
|
|
45
|
+
.mason/ this machine's: Mason's key and, once Mason runs, its synced data and state (gitignored)
|
|
46
|
+
mason/sources/<db>.yml the database and the tables to sync (mason add adds them) — commit it
|
|
47
|
+
mason/roles.yml who may read what: one role, everything (tables: all), the default of a user added
|
|
48
|
+
without --role, so every user reads every synced table until you narrow it — commit it
|
|
40
49
|
mason/mason.yml the Mason version the project is written for — commit it: production runs that version
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
With no URL it reads MASON_DATABASE_URL, else your app's DATABASE_URL, each from .env.local, then .env, then the
|
|
51
|
+
shell. The source file names the variable read, url: \${DATABASE_URL} — \${MASON_DATABASE_URL} when that is the one
|
|
52
|
+
read, or when a URL given here is another database than your app's DATABASE_URL: the URL itself is in .env (written
|
|
53
|
+
there when you give it here or it is only in the shell), never in a committed file. .mason/ and that .env go into
|
|
54
|
+
.gitignore. Every other mason command then works anywhere in this folder. A source file already here (a clone of the
|
|
55
|
+
project) is kept, and a bare mason init reads the variable it names; a roles file already here is kept. In a project
|
|
56
|
+
with no source file (an older Mason's), it writes that file alone, and the roles file when there is none.
|
|
44
57
|
mason init demo, in a new folder, makes a project on a demo database Mason hosts itself (a shop: customers, products,
|
|
45
58
|
orders, order_items): mason start makes it in .mason/demo/ and changes it every few seconds. Three of its tables are
|
|
46
|
-
added; the fourth is yours to add after mason start (mason add ${DEMO_TABLE_TO_ADD}), to watch its
|
|
59
|
+
added; the fourth is yours to add after mason start (mason add ${DEMO_TABLE_TO_ADD}), to watch its first sync begin.
|
|
60
|
+
It also writes mason/${GENERATED_DIR}/${DEMO_GENERATED.file}, a generated table over the synced orders (orders and
|
|
61
|
+
revenue per day and status, kept current: mason wait ${DEMO_GENERATED.table} once Mason is up; mason help generated); one
|
|
62
|
+
already there is kept.
|
|
47
63
|
|
|
48
|
-
mason init
|
|
64
|
+
mason init MASON_DATABASE_URL, else DATABASE_URL (.env or the shell)
|
|
49
65
|
mason init postgres://me:secret@localhost:5432/shop
|
|
50
66
|
mason init demo a demo database Mason hosts itself: nothing to install
|
|
51
67
|
mason init --folder db/mason --name shop-admin the project's files elsewhere; another name
|
|
52
68
|
mason init postgres://me:secret@db.example.com/shop --remote
|
|
53
69
|
|
|
54
|
-
--name <name> the project's name
|
|
70
|
+
--name <name> the project's name, which its replication slot is named for (default: this folder's)
|
|
71
|
+
--database <name> the database name a tool connects with, written as database: in the source file (default: none
|
|
72
|
+
written, so tools connect with ${DEFAULT_DATABASE}): your database's own, and a tool keeps the name it has
|
|
55
73
|
--folder <dir> where sources/ goes, relative to this folder (default mason; . for this folder itself)
|
|
56
74
|
--remote go ahead with a database on another machine (read the warnings mason init prints); not with demo
|
|
57
75
|
--skip-check write the project without connecting (the database is not up yet); mason start checks it; not with demo
|
|
58
76
|
--json as data: { name, root, folder, config, demo (true for the demo database's project, else false),
|
|
59
|
-
source: { name (the source's: its file's), file, url (as the file writes it),
|
|
60
|
-
written, its value masked, or null) },
|
|
61
|
-
|
|
77
|
+
source: { name (the source's: its file's), file, url (as the file writes it), database (the name
|
|
78
|
+
a tool connects with, or null when the file has a mistake), env (the .env line written, its value masked, or null) }, roles: { file, written (false: one was here, and kept) },
|
|
79
|
+
generated: [{ table, file, written (false: one was here, and kept) }] (the demo's generated table; [] for
|
|
80
|
+
any other project), database: { url (password masked), from, version, tables }, warnings, next }
|
|
62
81
|
`;
|
|
63
82
|
export const INIT = {
|
|
64
83
|
name: "init",
|
|
65
|
-
summary: "make this folder a Mason project on your Postgres: mason init (
|
|
84
|
+
summary: "make this folder a Mason project on your Postgres: mason init (its URL from .env), mason init postgres://… or demo",
|
|
66
85
|
usage: INIT_USAGE,
|
|
67
|
-
options: { name: { type: "string" }, folder: { type: "string" }, remote: { type: "boolean" }, "skip-check": { type: "boolean" }, json: { type: "boolean" } },
|
|
68
|
-
json: ["name", "root", "folder", "config", "demo", "source", "database", "warnings", "next"],
|
|
86
|
+
options: { name: { type: "string" }, database: { type: "string" }, folder: { type: "string" }, remote: { type: "boolean" }, "skip-check": { type: "boolean" }, json: { type: "boolean" } },
|
|
87
|
+
json: ["name", "root", "folder", "config", "demo", "source", "roles", "generated", "database", "warnings", "next"],
|
|
69
88
|
};
|
|
70
|
-
/** Why `name` cannot be this project's, or null: not a name
|
|
89
|
+
/** Why `name` cannot be this project's, or null: not a name its replication slot takes. */
|
|
71
90
|
export function nameProblem(name) {
|
|
72
91
|
if (!NAME_RE.test(name))
|
|
73
92
|
return `${JSON.stringify(name)} is not a project name: lower-case letters, digits, - and _, starting with a letter or digit (--name sets it)`;
|
|
@@ -85,7 +104,7 @@ export function withIgnored(gitignore, files = []) {
|
|
|
85
104
|
let tail = "";
|
|
86
105
|
if (!ignoresStateDir(text)) {
|
|
87
106
|
added.push(`${STATE_DIR}/`);
|
|
88
|
-
tail += `# Mason: this machine's key,
|
|
107
|
+
tail += `# Mason: this machine's key, synced data and state (mason init)\n${STATE_DIR}/\n`;
|
|
89
108
|
}
|
|
90
109
|
for (const f of files)
|
|
91
110
|
if (!ignoresFile(text, f)) {
|
|
@@ -123,8 +142,13 @@ export function committedInGit(root, file) {
|
|
|
123
142
|
* when the folder's .env.local or .env gives it already. DATABASE_URL when the folder's files give it no other value
|
|
124
143
|
* (one only in the shell is written down: the next shell may not have it); else MASON_DATABASE_URL, so the app's own
|
|
125
144
|
* DATABASE_URL is never changed under it. A problem when both name other databases.
|
|
145
|
+
* A URL READ FROM A VARIABLE (`found`, a bare mason init's `appDatabaseUrl`) is kept in that variable: the one read is
|
|
146
|
+
* the one named, and nothing is written when a folder's file gave it — so a MASON_DATABASE_URL is never written again as
|
|
147
|
+
* DATABASE_URL; one only in the shell is written down under its own name.
|
|
126
148
|
*/
|
|
127
|
-
export function urlVariable(root, url) {
|
|
149
|
+
export function urlVariable(root, url, found = null) {
|
|
150
|
+
if (found !== null)
|
|
151
|
+
return { variable: found.variable, line: found.file === null ? `${found.variable}=${url}` : null };
|
|
128
152
|
for (const variable of ["DATABASE_URL", "MASON_DATABASE_URL"]) {
|
|
129
153
|
const inFiles = appEnvValue(root, variable, {});
|
|
130
154
|
if (inFiles === null)
|
|
@@ -132,10 +156,10 @@ export function urlVariable(root, url) {
|
|
|
132
156
|
if (inFiles.value === url)
|
|
133
157
|
return { variable, line: null };
|
|
134
158
|
}
|
|
135
|
-
return { problem: "this folder's .env names DATABASE_URL and MASON_DATABASE_URL for other databases: set one of them to the database Mason
|
|
159
|
+
return { problem: "this folder's .env names DATABASE_URL and MASON_DATABASE_URL for other databases: set one of them to the database Mason syncs from, then mason init again" };
|
|
136
160
|
}
|
|
137
161
|
/** `.env` with `line` added (made when there is none). */
|
|
138
|
-
const addEnvLine = (root, line) => appendEnvLine(root, line, "the database Mason
|
|
162
|
+
const addEnvLine = (root, line) => appendEnvLine(root, line, "the database Mason syncs from (mason/sources/ names it; never commit this file)");
|
|
139
163
|
/** A `.env` line as it is said: its value's password masked. */
|
|
140
164
|
const maskedLine = (line) => { const eq = line.indexOf("="); return `${line.slice(0, eq)}=${maskedUrl(line.slice(eq + 1))}`; };
|
|
141
165
|
export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
@@ -186,47 +210,87 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
186
210
|
const name = sourceless?.config.name ?? f.values.name ?? nameFromFolder(root);
|
|
187
211
|
const folderArg = f.values.folder ?? "mason";
|
|
188
212
|
const folder = sourceless?.folder ?? path.resolve(root, folderArg);
|
|
189
|
-
|
|
190
|
-
|
|
213
|
+
// the one authority on where the files go (src/cli/project.ts folderPlace): outside this folder, or a place it cannot
|
|
214
|
+
// resolve (a loop, a file as a folder, unreadable, too long), which can never become a project's folder, is refused
|
|
215
|
+
const place = folderPlace(root, folder);
|
|
216
|
+
const shownArg = folderArg.length > 200 ? `${folderArg.slice(0, 200)}…` : folderArg;
|
|
217
|
+
if (place.place === "outside")
|
|
218
|
+
return usage(`--folder ${shownArg} is outside this folder`, "the project's files go in this folder or below it");
|
|
219
|
+
if (place.place === "unknown") {
|
|
220
|
+
return failed("init", { message: `--folder ${shownArg} cannot be resolved to a folder`, code: "usage", why: place.why,
|
|
221
|
+
fix: "name a folder of its own inside this one with --folder (the default, mason, is one), or fix the path" }, { json, io });
|
|
222
|
+
}
|
|
191
223
|
if (fs.existsSync(folder) && !fs.statSync(folder).isDirectory())
|
|
192
224
|
return usage(`${folder} is a file, not a folder`, "--folder <dir> chooses another");
|
|
193
225
|
const named = nameProblem(name);
|
|
194
226
|
if (named)
|
|
195
227
|
return usage(named);
|
|
228
|
+
const connectName = f.values.database ?? null;
|
|
229
|
+
const badDatabase = connectName === null ? null : databaseNameProblem(connectName);
|
|
230
|
+
if (badDatabase !== null)
|
|
231
|
+
return usage(`--database: ${badDatabase}`);
|
|
232
|
+
// THE PROJECT'S FILES DEPLOYED READ-ONLY (src/cli/project.ts projectWriteRefusal, the one authority): init writes them,
|
|
233
|
+
// so it writes none — refused before the database is asked, as every writer of a project file is
|
|
234
|
+
const readOnly = projectWriteRefusal(sourceless?.root ?? null, deps.env ?? process.env);
|
|
235
|
+
if (readOnly !== null)
|
|
236
|
+
return failed("init", readOnly, { json, io });
|
|
237
|
+
// THE SOURCE FILE: one already here (a clone of the project) is kept; else written, its URL a variable of .env
|
|
238
|
+
const kept = sourceFileNames(folder)[0] ?? null;
|
|
239
|
+
if (demo && kept !== null) {
|
|
240
|
+
// a clone's sources file names the team's database: the demo never takes its place
|
|
241
|
+
return failed("init", { message: `${path.relative(root, path.join(sourcesDir(folder), kept))} is already here: mason init demo makes a project of its own`, code: "refused",
|
|
242
|
+
fix: "run it in a new folder (mkdir mason-demo && cd mason-demo && mason init demo), or mason init here for the database that file names" }, { json, io });
|
|
243
|
+
}
|
|
244
|
+
const keptPath = kept === null ? null : path.join(sourcesDir(folder), kept);
|
|
245
|
+
if (connectName !== null && keptPath !== null)
|
|
246
|
+
return usage(`--database writes a new source file, and ${path.relative(root, keptPath)} is here already`, `write database: ${connectName} in it`);
|
|
247
|
+
const keptFile = kept === null || keptPath === null ? null : parseSourceText(fs.readFileSync(keptPath, "utf8"), kept.replace(/\.ya?ml$/, ""), keptPath);
|
|
248
|
+
const keptVar = keptFile !== null && !("problems" in keptFile) && "env" in keptFile.url ? keptFile.url.env : null;
|
|
196
249
|
// the demo's URL names a port free now, and is the one place the port is kept: mason start reads it from there
|
|
197
250
|
let url = demo ? demoUrl(await freePort(DEMO_PORT)) : f.positionals[0] ?? null;
|
|
198
251
|
let from = "";
|
|
199
252
|
let envFile = null; // the folder's file the URL was read from (.env.local or .env): ignored with .mason/
|
|
253
|
+
let envVariable = null; // the variable envFile holds the URL in, set with it: the warning names it
|
|
254
|
+
// the variable a bare mason init read the URL from: the one the sources file names (urlVariable)
|
|
255
|
+
let found = null;
|
|
200
256
|
if (url === null) {
|
|
201
|
-
|
|
257
|
+
// THE KEPT FILE DECIDES WHAT IS READ: the variable its url: names, as the run reads it (.env.local, .env, the shell),
|
|
258
|
+
// so the database init checks is the one mason start copies from; MASON_DATABASE_URL, then DATABASE_URL, only when
|
|
259
|
+
// no kept file names one (a clone naming DATABASE_URL was checked against a MASON_DATABASE_URL in the shell)
|
|
260
|
+
if (keptVar !== null && keptPath !== null) {
|
|
261
|
+
const v = appEnvValue(root, keptVar, deps.env ?? process.env);
|
|
262
|
+
const file = shownPath(root, keptPath);
|
|
263
|
+
if (v === null) {
|
|
264
|
+
return failed("init", { message: `which database? ${file} names url: \${${keptVar}}, and this folder has no ${keptVar}`, code: "usage",
|
|
265
|
+
why: `mason start reads the URL from ${keptVar}, so mason init checks that one`,
|
|
266
|
+
fix: `put it in this folder's .env as ${keptVar}=postgres://… (the variable ${file} names), or give its URL on the command line: mason init writes it there`,
|
|
267
|
+
next: [["mason init postgres://user:password@localhost:5432/your_app", `its URL, given: written to .env as ${keptVar}`], ["mason init", `again, once .env has ${keptVar}`]] }, { json, io });
|
|
268
|
+
}
|
|
269
|
+
found = { url: v.value, variable: keptVar, from: `${keptVar} in ${v.from}`, file: v.file };
|
|
270
|
+
}
|
|
271
|
+
else
|
|
272
|
+
found = appDatabaseUrl(root, deps.env ?? process.env);
|
|
202
273
|
if (!found) {
|
|
203
274
|
// the fixes ARE the next commands, so a coding agent reads them as such too
|
|
204
|
-
return failed("init", { message: "which database? none is named, and this folder has no DATABASE_URL", code: "usage",
|
|
205
|
-
fix: "give its URL on the command line, or put it in this folder's .env as DATABASE_URL=postgres://… (your app's, usually)",
|
|
206
|
-
next: [["mason init postgres://user:password@localhost:5432/your_app", "its URL, given"], ["mason init", "again, once .env has DATABASE_URL"],
|
|
275
|
+
return failed("init", { message: "which database? none is named, and this folder has no MASON_DATABASE_URL or DATABASE_URL", code: "usage",
|
|
276
|
+
fix: "give its URL on the command line, or put it in this folder's .env as DATABASE_URL=postgres://… (your app's, usually) or as MASON_DATABASE_URL=postgres://… (read first: a database other than your app's)",
|
|
277
|
+
next: [["mason init postgres://user:password@localhost:5432/your_app", "its URL, given"], ["mason init", "again, once .env has MASON_DATABASE_URL or DATABASE_URL"],
|
|
207
278
|
["mason init demo", "a demo database Mason makes itself: nothing to install"]] }, { json, io });
|
|
208
279
|
}
|
|
209
280
|
url = found.url;
|
|
210
|
-
from = ` (
|
|
281
|
+
from = ` (${found.from})`;
|
|
211
282
|
envFile = found.file;
|
|
283
|
+
envVariable = found.variable;
|
|
212
284
|
}
|
|
213
|
-
|
|
285
|
+
// a problem names where the URL is (DATABASE_URL in .env, the URL given), never the value (src/cli/preflight.ts shownUrl)
|
|
286
|
+
const urlName = found !== null ? found.from : demo ? "the demo database's URL" : "the URL given";
|
|
287
|
+
const parsed = parseSource(url, urlName);
|
|
214
288
|
if ("problem" in parsed)
|
|
215
289
|
return usage(parsed.problem);
|
|
216
290
|
const remote = f.values.remote === true || sourceless?.config.remote === true;
|
|
217
|
-
// THE SOURCE FILE: one already here (a clone of the project) is kept; else written, its URL a variable of .env
|
|
218
|
-
const kept = sourceFileNames(folder)[0] ?? null;
|
|
219
|
-
if (demo && kept !== null) {
|
|
220
|
-
// a clone's sources file names the team's database: the demo never takes its place
|
|
221
|
-
return failed("init", { message: `${path.relative(root, path.join(sourcesDir(folder), kept))} is already here: mason init demo makes a project of its own`, code: "refused",
|
|
222
|
-
fix: "run it in a new folder (mkdir mason-demo && cd mason-demo && mason init demo), or mason init here for the database that file names" }, { json, io });
|
|
223
|
-
}
|
|
224
|
-
const keptText = kept === null ? null : fs.readFileSync(path.join(sourcesDir(folder), kept), "utf8");
|
|
225
|
-
const keptFile = kept === null ? null : parseSourceText(keptText, kept.replace(/\.ya?ml$/, ""), path.join(sourcesDir(folder), kept));
|
|
226
|
-
const keptVar = keptFile !== null && !("problems" in keptFile) && "env" in keptFile.url ? keptFile.url.env : null;
|
|
227
291
|
// a kept file's variable gets this URL in .env when the folder's files give it none; one they give another value is said
|
|
228
292
|
const keptValue = keptVar === null ? null : appEnvValue(root, keptVar, {});
|
|
229
|
-
const where = kept === null ? urlVariable(root, url)
|
|
293
|
+
const where = kept === null ? urlVariable(root, url, found)
|
|
230
294
|
: { variable: keptVar ?? "", line: keptVar !== null && keptValue === null ? `${keptVar}=${url}` : null };
|
|
231
295
|
if ("problem" in where)
|
|
232
296
|
return failed("init", { message: where.problem, code: "refused" }, { json, io });
|
|
@@ -241,10 +305,15 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
241
305
|
line(st.bold(`Making ${name} a Mason project in ${root}`));
|
|
242
306
|
const notes = [];
|
|
243
307
|
if (keptValue !== null && keptValue.value !== url)
|
|
244
|
-
notes.push(`${kept} reads ${keptVar} from ${keptValue.from}, which is ${maskedUrl(keptValue.value)}: mason start
|
|
308
|
+
notes.push(`${kept} reads ${keptVar} from ${keptValue.from}, which is ${maskedUrl(keptValue.value)}: mason start syncs from that one, not ${db}`);
|
|
245
309
|
let database = { url: db, from: from ? from.slice(2, -1) : null, version: null, tables: null };
|
|
246
310
|
const rel = (p) => path.relative(root, p) || ".";
|
|
247
311
|
const sourceFile = kept !== null ? path.join(sourcesDir(folder), kept) : path.join(sourcesDir(folder), `${sourceNameFor(parsed.source.database)}.yml`);
|
|
312
|
+
const rolesPath = rolesFile(folder);
|
|
313
|
+
const rolesKept = fs.existsSync(rolesPath);
|
|
314
|
+
// THE DEMO'S GENERATED TABLE (src/demo/seed.ts DEMO_GENERATED): written beside the sources file, kept when one is here
|
|
315
|
+
const generatedPath = demo ? path.join(generatedDir(folder), DEMO_GENERATED.file) : null;
|
|
316
|
+
const generatedKept = generatedPath !== null && fs.existsSync(generatedPath);
|
|
248
317
|
// the folder an older Mason listed the tables in: read by nothing (no backwards compatibility before v1.0), said
|
|
249
318
|
const oldTables = path.join(folder, OLD_TABLES_DIR);
|
|
250
319
|
if (fs.existsSync(oldTables))
|
|
@@ -252,7 +321,7 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
252
321
|
// A DATABASE ON ANOTHER MACHINE, NOT CONFIRMED: not a failed step (the check passed, or was skipped) but a stop before
|
|
253
322
|
// anything is written, said as one failure (remoteConfirmation) — the same words whether the check ran or not
|
|
254
323
|
const confirmRemote = () => failed("init", remoteConfirmation(parsed.source.host, { cmd: "mason init",
|
|
255
|
-
wayOut: "run the same mason init again with --remote: that says yes,
|
|
324
|
+
wayOut: "run the same mason init again with --remote: that says yes, sync from that machine",
|
|
256
325
|
next: [["mason init --remote <url>", "the same command, confirmed"]] }), { json, io });
|
|
257
326
|
try {
|
|
258
327
|
if (demo) {
|
|
@@ -269,7 +338,7 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
269
338
|
const said = [];
|
|
270
339
|
let needsRemote = false;
|
|
271
340
|
const checked = await steps.step(`Connecting to ${db}${from}`, async () => {
|
|
272
|
-
const c = await checkSource(url, { remote, cmd: "mason init", say: (m) => said.push(m) });
|
|
341
|
+
const c = await checkSource(url, { remote, cmd: "mason init", say: (m) => said.push(m), name: urlName });
|
|
273
342
|
if ("exit" in c)
|
|
274
343
|
throw new StepFailed(said.filter((m) => !/: connecting to /.test(m)).join("\n"), c.exit);
|
|
275
344
|
if ("needsRemote" in c) {
|
|
@@ -288,33 +357,57 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
288
357
|
database = { ...database, version: checked.facts.versionText, tables: checked.facts.tables.length };
|
|
289
358
|
}
|
|
290
359
|
if (sourceless)
|
|
291
|
-
await steps.step(`Keeping ${STATE_DIR}/config.json`, () => undefined, () => `Kept ${STATE_DIR}/config.json: the project ${name}, its key and its
|
|
360
|
+
await steps.step(`Keeping ${STATE_DIR}/config.json`, () => undefined, () => `Kept ${STATE_DIR}/config.json: the project ${name}, its key and its synced data`);
|
|
292
361
|
else {
|
|
293
362
|
await steps.step(`Writing ${STATE_DIR}/config.json`, () => {
|
|
294
363
|
writeConfig(path.join(root, STATE_DIR), newConfig(name, path.relative(root, folder) || ".", remote, { demo }));
|
|
295
364
|
}, () => `Wrote ${STATE_DIR}/config.json: Mason's key (this machine's)`);
|
|
296
365
|
}
|
|
297
366
|
if (kept !== null) {
|
|
298
|
-
await steps.step(`Keeping ${rel(sourceFile)}`, () => undefined, () => `Kept ${rel(sourceFile)}: the database and the tables this project
|
|
367
|
+
await steps.step(`Keeping ${rel(sourceFile)}`, () => undefined, () => `Kept ${rel(sourceFile)}: the database and the tables this project syncs`);
|
|
299
368
|
if (keptFile !== null && "problems" in keptFile)
|
|
300
369
|
notes.push(`${rel(sourceFile)} has ${keptFile.problems.length === 1 ? "a mistake" : "mistakes"}, which mason start refuses: ${keptFile.problems.join("; ")}`);
|
|
301
370
|
}
|
|
302
371
|
else {
|
|
303
372
|
await steps.step(`Writing ${rel(sourceFile)}`, () => {
|
|
304
373
|
fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
|
|
305
|
-
fs.writeFileSync(sourceFile, newSourceText(`\${${where.variable}}`, demo ? DEMO_ADDED_TABLES : []));
|
|
306
|
-
}, () => `Wrote ${rel(sourceFile)}: the database (url: \${${where.variable}})
|
|
374
|
+
fs.writeFileSync(sourceFile, newSourceText(`\${${where.variable}}`, demo ? DEMO_ADDED_TABLES : [], demo ? DEMO_SYNC : DEFAULT_SYNC, connectName));
|
|
375
|
+
}, () => `Wrote ${rel(sourceFile)}: the database (url: \${${where.variable}}), ${connectName === null ? "" : `database: ${connectName}, `}sync: ${syncText(demo ? DEMO_SYNC : DEFAULT_SYNC)} ` +
|
|
376
|
+
`(${demo ? "every table polled every 5 s" : "each table read once, then again only by mason sync <table>"}) and the tables to sync, ` +
|
|
377
|
+
`${demo ? `the demo's ${DEMO_ADDED_TABLES.length} added — mason add ${DEMO_TABLE_TO_ADD} adds the fourth` : "none yet (mason add)"} — commit it`);
|
|
378
|
+
}
|
|
379
|
+
if (generatedPath !== null) {
|
|
380
|
+
const g = rel(generatedPath);
|
|
381
|
+
if (generatedKept)
|
|
382
|
+
await steps.step(`Keeping ${g}`, () => undefined, () => `Kept ${g}: the demo's generated table, as it is`);
|
|
383
|
+
else {
|
|
384
|
+
await steps.step(`Writing ${g}`, () => {
|
|
385
|
+
fs.mkdirSync(path.dirname(generatedPath), { recursive: true });
|
|
386
|
+
fs.writeFileSync(generatedPath, DEMO_GENERATED.text);
|
|
387
|
+
}, () => `Wrote ${g}: a generated table, orders and revenue per day, kept current from the synced orders (mason help generated) — commit it`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// THE ROLES FILE (src/cli/roles.ts newRolesText): today's access said in a committed file — every user reads every
|
|
391
|
+
// copied table, as the default role everything — kept when one is here (a clone of the project)
|
|
392
|
+
if (rolesKept)
|
|
393
|
+
await steps.step(`Keeping ${rel(rolesPath)}`, () => undefined, () => `Kept ${rel(rolesPath)}: the roles and the default for a user added without --role`);
|
|
394
|
+
else {
|
|
395
|
+
await steps.step(`Writing ${rel(rolesPath)}`, () => {
|
|
396
|
+
fs.mkdirSync(path.dirname(rolesPath), { recursive: true });
|
|
397
|
+
fs.writeFileSync(rolesPath, newRolesText());
|
|
398
|
+
}, () => `Wrote ${rel(rolesPath)}: every user reads every synced table (default: everything, tables: all) — narrow it with a second role — commit it`);
|
|
307
399
|
}
|
|
308
400
|
if (where.line !== null) {
|
|
309
401
|
const envLine = where.line;
|
|
310
402
|
await steps.step(`Writing ${where.variable} to .env`, () => addEnvLine(root, envLine), () => `Wrote ${maskedLine(envLine)} to .env: the URL the source file names, kept out of git`);
|
|
311
403
|
envFile = ENV_FILE;
|
|
404
|
+
envVariable = where.variable;
|
|
312
405
|
}
|
|
313
406
|
// THE VERSION PIN (src/cli/pin.ts): what production's image is held to. A checkout's mason (`dev`) has none to write
|
|
314
407
|
const cli = deps.version ?? version();
|
|
315
408
|
const pinFile = rel(path.join(folder, PIN_FILE));
|
|
316
409
|
if (parseVersion(cli) !== null) {
|
|
317
|
-
await steps.step(`Writing ${pinFile}`, () => ensurePin(folder, cli), (wrote) => wrote ? `Wrote ${pinFile}: version ${cli}, the Mason this project is written for — commit it` : `Kept ${pinFile}`);
|
|
410
|
+
await steps.step(`Writing ${pinFile}`, () => ensurePin(folder, cli, root, deps.env ?? process.env), (wrote) => wrote ? `Wrote ${pinFile}: version ${cli}, the Mason this project is written for — commit it` : `Kept ${pinFile}`);
|
|
318
411
|
const pinned = cliPinProblem(folder, cli);
|
|
319
412
|
if (pinned)
|
|
320
413
|
notes.push(pinned);
|
|
@@ -329,7 +422,7 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
329
422
|
return after === null ? "already" : before === null ? "made" : after.added.join(" and ");
|
|
330
423
|
}, (r) => r === "already" ? `.gitignore already ignores ${ignored}` : r === "made" ? `Made .gitignore, ignoring ${ignored}` : `Added ${r} to .gitignore`);
|
|
331
424
|
if (envFile && committedInGit(root, envFile)) {
|
|
332
|
-
notes.push(`${envFile} holds the database URL (
|
|
425
|
+
notes.push(`${envFile} holds the database URL (${envVariable}) and is committed in git: git rm --cached ${envFile} takes it out of the next commit (the file stays); then change the password`);
|
|
333
426
|
}
|
|
334
427
|
}
|
|
335
428
|
catch (e) {
|
|
@@ -342,25 +435,29 @@ export async function init(argv, cwd = process.cwd(), deps = {}) {
|
|
|
342
435
|
why: parsed.source.remote ? undefined : `the database's URL is ${db}${from}` }, { json, io });
|
|
343
436
|
}
|
|
344
437
|
const next = demo ? [
|
|
345
|
-
["mason start", "the demo database and Mason, in the background: its tables
|
|
346
|
-
[`mason add ${DEMO_TABLE_TO_ADD}`, "the demo's fourth table, once Mason is up: watch its
|
|
347
|
-
[
|
|
438
|
+
["mason start", "the demo database and Mason, in the background: its tables synced, then kept current"],
|
|
439
|
+
[`mason add ${DEMO_TABLE_TO_ADD}`, "the demo's fourth table, once Mason is up: watch its first sync begin"],
|
|
440
|
+
[`mason wait ${DEMO_GENERATED.table}`, "the demo's generated table, once Mason is up: orders and revenue per day, kept current"],
|
|
441
|
+
["mason status", "the synced data keeping up with the demo's changes"],
|
|
348
442
|
["mason user add <name>", "a user for psql or a BI tool: its password and URL, printed once"],
|
|
349
443
|
] : [
|
|
350
444
|
["mason start", "Mason in the background, for this folder"],
|
|
351
445
|
["mason tables", "the database's tables and their columns"],
|
|
352
|
-
["mason add <table>", "
|
|
446
|
+
["mason add <table>", "sync a table into Mason"],
|
|
353
447
|
];
|
|
354
448
|
if (json) {
|
|
355
449
|
say(jsonText(INIT, { name, root, folder: path.relative(root, folder) || ".", config: path.join(root, STATE_DIR, "config.json"), demo,
|
|
356
450
|
source: { name: path.basename(sourceFile).replace(/\.ya?ml$/, ""), file: sourceFile, url: keptFile !== null && !("problems" in keptFile) ? ("env" in keptFile.url ? `\${${keptFile.url.env}}` : maskedUrl(keptFile.url.literal)) : `\${${where.variable}}`,
|
|
451
|
+
database: keptFile === null ? connectName ?? DEFAULT_DATABASE : "problems" in keptFile ? null : keptFile.database ?? DEFAULT_DATABASE,
|
|
357
452
|
env: where.line === null ? null : maskedLine(where.line) },
|
|
453
|
+
roles: { file: rolesPath, written: !rolesKept },
|
|
454
|
+
generated: generatedPath === null ? [] : [{ table: sourceOf(DEMO_GENERATED.table), file: generatedPath, written: !generatedKept }],
|
|
358
455
|
database, warnings: notes }, next));
|
|
359
456
|
return 0;
|
|
360
457
|
}
|
|
361
458
|
for (const n of notes)
|
|
362
459
|
say(` ! ${n}`);
|
|
363
|
-
say(endText({ tips: [demo ? `The demo database runs inside mason start, read by polling: a new order every few seconds; ${DEMO_TABLE_TO_ADD} is left for you to add; mason destroy removes
|
|
460
|
+
say(endText({ tips: [demo ? `The demo database runs inside mason start, read by polling: a new order every few seconds; ${DEMO_TABLE_TO_ADD} is left for you to add; ${DEMO_GENERATED.table} is a generated table over the synced orders; mason destroy removes the demo database with the synced data`
|
|
364
461
|
: "Mason only reads your database: no setting, slot or publication is needed or made"], next }, st));
|
|
365
462
|
return 0;
|
|
366
463
|
}
|