@mason-data/cli 0.6.2 → 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.
- package/README.md +71 -12
- package/dist/src/api/postgres/catalog.js +36 -1
- package/dist/src/api/postgres/server.js +133 -32
- package/dist/src/cdc/pg-wire.js +144 -38
- package/dist/src/cli/background.js +29 -5
- package/dist/src/cli/command.js +4 -3
- package/dist/src/cli/commands.js +259 -127
- package/dist/src/cli/demo.js +149 -0
- package/dist/src/cli/dev.js +113 -20
- package/dist/src/cli/doctor.js +6 -3
- package/dist/src/cli/guides.js +32 -8
- package/dist/src/cli/init.js +58 -18
- package/dist/src/cli/machine.js +8 -3
- package/dist/src/cli/main.js +2 -2
- package/dist/src/cli/mcp.js +726 -0
- package/dist/src/cli/preflight.js +16 -7
- package/dist/src/cli/project.js +5 -3
- package/dist/src/cli/registry.js +8 -5
- package/dist/src/cli/run-server.js +29 -106
- package/dist/src/cli/serve.js +6 -1
- package/dist/src/cli/source-edit.js +116 -0
- package/dist/src/cli/sources.js +6 -4
- package/dist/src/demo/seed.js +237 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/executor-worker.js +7 -2
- package/dist/src/duck/executor.js +67 -26
- package/dist/src/duck/explain.js +128 -9
- package/dist/src/duck/functions.js +60 -5
- package/dist/src/duck/gaps.js +9 -4
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +10 -8
- package/dist/src/duck/kept-route.js +82 -2
- package/dist/src/duck/kept.js +206 -39
- package/dist/src/duck/pgtree.js +40 -0
- package/dist/src/duck/plan.js +21 -8
- package/dist/src/duck/rewrite.js +1168 -68
- package/dist/src/duck/store.js +132 -56
- package/dist/src/duck/sync.js +90 -25
- package/dist/src/duck/verified.js +6 -0
- package/dist/src/lib/timezone.js +3 -2
- package/dist/src/web/app.css +11 -0
- package/dist/src/web/app.js +167 -12
- package/dist/src/web/index.html +6 -1
- package/package.json +1 -1
package/dist/src/web/app.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* THE WEB PAGE OF `mason start` (`mason ui`; docs/WEB-UI.md, and .claude/rules/web-ui.md for whoever builds it out):
|
|
3
|
-
* one page,
|
|
3
|
+
* one page, three sections, rendered from the run's command server's JSON (src/cli/run-server.ts) in the words the
|
|
4
4
|
* commands print —
|
|
5
5
|
*
|
|
6
6
|
* STATUS GET /api/state, /api/slow, /api/refused: what `mason status` prints, polled every 2 s while the tab is seen
|
|
7
|
+
* TABLES GET /api/state `tables`, POST /api/pick, POST /api/remove: what `mason tables`, `mason add` and `mason remove` do
|
|
7
8
|
* USERS GET /api/users, POST /api/users, DELETE /api/users/<name>: what `mason user list|add|remove` does
|
|
8
9
|
*
|
|
9
10
|
* Plain JavaScript, an ES module, no framework and no build step: the server serves this file as it is, under a
|
|
@@ -82,14 +83,47 @@ export function tableLine(t) {
|
|
|
82
83
|
return { state: "done", fraction: 1, what: `${n(t.copy.rowsCopied ?? 0)} rows · copied${t.copy.retired ? ` · ${t.copy.retired}` : ""}` };
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/** A table's facts as `mason tables` says them on its line (src/cli/commands.ts `tableFacts`): its size, its key, and
|
|
87
|
+
* added or why it cannot be added as it is (null: neither). */
|
|
88
|
+
export function tableFacts(t) {
|
|
89
|
+
return { size: t.rows === null ? "rows unknown" : `~${n(t.rows)} rows`, key: t.key.length ? `key ${t.key.join(", ")}` : "no key",
|
|
90
|
+
state: t.added ? "added" : t.reason ? `cannot be added as it is: ${t.reason}` : null };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** What `mason add` prints for a table added, from `/api/pick`'s answer (src/cli/commands.ts `addedLines`): its line,
|
|
94
|
+
* then its notes — the key added to the columns asked, the secret-looking columns left out. */
|
|
95
|
+
export function addedLines(b, file) {
|
|
96
|
+
const copies = b.columns === null ? (b.exclude.length ? `every column but ${b.exclude.join(", ")}` : "every column") : b.columns.join(", ");
|
|
97
|
+
const s = b.skippedSecrets;
|
|
98
|
+
return { line: `Added ${b.table} to ${file}: ${copies}${b.key ? `, copied by ${b.key.join(", ")}` : ""}${b.already ? " (already so)" : ""}`, notes: [
|
|
99
|
+
...(b.addedKey.length ? [`key added: ${b.addedKey.join(", ")} (a copy always has its key)`] : []),
|
|
100
|
+
...(s.length ? [`left out ${s.join(", ")}: ${s.length === 1 ? "it looks" : "they look"} like ${s.length === 1 ? "a secret" : "secrets"}, ` +
|
|
101
|
+
`which anyone with a user's password could read (--include ${s[0]} adds it)`] : []),
|
|
102
|
+
] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** What `mason remove` prints for the tables removed (src/cli/commands.ts `removedReport`). */
|
|
106
|
+
export function removedReport(removed, file) {
|
|
107
|
+
return { headline: `Removed ${removed.join(", ")}`, details: [["from", file]],
|
|
108
|
+
tips: [`Mason stops copying ${removed.length === 1 ? "it" : "them"} and drops ${removed.length === 1 ? "its copy" : "their copies"}`] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `POST /api/pick`'s body for a table added: the columns ticked as `--columns`, the key's as `--key`, each left out when
|
|
113
|
+
* none is ticked (none: every column, as `mason add <table>` copies; no key: its primary key). What the lists may hold
|
|
114
|
+
* is the server's to judge (src/cli/run-server.ts `pick`), never the page's.
|
|
115
|
+
*/
|
|
116
|
+
export const pickBody = (table, columns = [], key = []) => ({ table, picked: true, ...(columns.length ? { columns } : {}), ...(key.length ? { key } : {}) });
|
|
117
|
+
|
|
85
118
|
/** `mason status`'s rows for the database (src/cli/commands.ts `sourceRows`): its sources file, its URL as the run masked it, how its changes arrive. */
|
|
86
119
|
export function sourceRows(s) {
|
|
87
120
|
return [
|
|
88
121
|
...(s.source.file ? [["Source", `${s.source.file} (the tables it names are added)`]] : []),
|
|
89
|
-
["Database",
|
|
122
|
+
["Database", s.source.demo ? `the demo database Mason hosts (${s.source.url} · Postgres ${s.source.version})`
|
|
123
|
+
: `${s.source.url} · Postgres ${s.source.version}${s.source.standby ? " · a read replica" : ""}`],
|
|
90
124
|
["Changes", s.source.replication ? `logical replication: slot ${s.source.replication.slot}, publication ${s.source.replication.publication} ` +
|
|
91
125
|
`(${s.source.replication.owned ? "Mason's own" : "named: read as it is, never altered"}) · mason start --poll goes back to polling`
|
|
92
|
-
: `polling with SELECTs (xmin), every ${s.engine.pollSeconds} s · mason start --replication reads the database's change log instead`],
|
|
126
|
+
: `polling with SELECTs (xmin), every ${s.engine.pollSeconds} s · ${s.source.demo ? "the demo database is read by polling only" : "mason start --replication reads the database's change log instead"}`],
|
|
93
127
|
];
|
|
94
128
|
}
|
|
95
129
|
|
|
@@ -150,6 +184,71 @@ export function renderStatus(s, requests = {}) {
|
|
|
150
184
|
return parts.join("");
|
|
151
185
|
}
|
|
152
186
|
|
|
187
|
+
/** A table just added (`/api/pick`'s answer, `mason add --json`'s `added` entry): the line and notes `mason add` prints. */
|
|
188
|
+
export function renderTableAdded(b) {
|
|
189
|
+
const l = addedLines(b, b.file);
|
|
190
|
+
return `<div class="done" role="status"><p class="headline">${esc(l.line)}</p>${l.notes.map((x) => `<p class="note">${esc(x)}</p>`).join("")}` +
|
|
191
|
+
`<button type="button" data-dismiss-outcome>Dismiss</button></div>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Tables just removed (`/api/remove`'s answer, `mason remove --json`'s shape): what `mason remove` prints. */
|
|
195
|
+
export function renderTablesRemoved(b) {
|
|
196
|
+
const r = removedReport(b.removed, b.file);
|
|
197
|
+
return `<div class="done" role="status"><p class="headline">${esc(r.headline)}</p>${rowsHtml(r.details)}${r.tips.map((x) => `<p class="tip">Tip: ${esc(x)}</p>`).join("")}` +
|
|
198
|
+
`<button type="button" data-dismiss-outcome>Dismiss</button></div>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const tick = (name, c, on) => `<label class="tick"><input type="checkbox" name="${name}" value="${esc(c.name)}"${on ? " checked" : ""}> ${code(c.name)} <span class="dim">${esc(c.type)}</span></label>`;
|
|
202
|
+
|
|
203
|
+
/** The form that adds a table with the columns ticked (`--columns`) and, for one with no primary key, its key (`--key`).
|
|
204
|
+
* The ticks live in the document while the person ticks (the tables part is held still meanwhile: `createApp`);
|
|
205
|
+
* `ticked`, what a refused submit sent, writes them back when the refusal is said under the form. */
|
|
206
|
+
function pickForm(t, ticked = null) {
|
|
207
|
+
const name = entryName(t.table);
|
|
208
|
+
const boxes = (field) => t.columns.map((c) => tick(field, c, ticked?.[field]?.includes(c.name) ?? false)).join("");
|
|
209
|
+
return `<form class="pick" data-pick-form="${esc(t.table)}" autocomplete="off">` +
|
|
210
|
+
(t.key.length ? "" : `<fieldset><legend>Its key: the columns that tell its rows apart, as ${code(`mason add ${name} --key`)}</legend>${boxes("key")}</fieldset>`) +
|
|
211
|
+
`<fieldset><legend>Its columns, as ${code(`mason add ${name} --columns`)} (none ticked: as ${code(`mason add ${name}`)}, every column)</legend>${boxes("columns")}</fieldset>` +
|
|
212
|
+
`<button type="submit">Add ${esc(name)}</button> <button type="button" data-cancel-choose>Cancel</button></form>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* THE DATABASE'S TABLES from `/api/state` `tables` (`mason tables --json`'s list): a line each — its size, its key, and
|
|
217
|
+
* added or why it cannot be added as it is, as `mason tables` prints them — with what can be done to it: Add (every
|
|
218
|
+
* column, as `mason add <table>`) and Columns… (the form) for a table not added, Name its key… for one with no primary
|
|
219
|
+
* key, Remove (asking first, `ui.removing`) for an added one. `ui.choosing` is the table whose form is open (`ui.ticked`
|
|
220
|
+
* its boxes ticked: { columns, key }); `ui.outcome` what the last add or remove answered ({ table, added } |
|
|
221
|
+
* { table, removed } | { table, failure }), under its table.
|
|
222
|
+
*/
|
|
223
|
+
export function renderTables(s, ui = {}) {
|
|
224
|
+
const { removing = null, choosing = null, outcome = null, ticked = null } = ui;
|
|
225
|
+
if (!s.tables.length) return `<p class="dim">${esc(s.source.database)} has no table this user can read.</p>`;
|
|
226
|
+
const under = (html) => `<tr class="under"><td colspan="5">${html}</td></tr>`;
|
|
227
|
+
const line = (t) => {
|
|
228
|
+
const f = tableFacts(t);
|
|
229
|
+
const name = entryName(t.table);
|
|
230
|
+
const act = t.added
|
|
231
|
+
? removing === t.table
|
|
232
|
+
? `<span class="confirm">Remove ${esc(name)}? ${esc(removedReport([t.table], "").tips[0])}.</span> ` +
|
|
233
|
+
`<button type="button" class="danger" data-confirm-remove-table="${esc(t.table)}">Remove ${esc(name)}</button> <button type="button" data-cancel-remove-table>Cancel</button>`
|
|
234
|
+
: `<button type="button" data-remove-table="${esc(t.table)}">Remove</button>`
|
|
235
|
+
: t.reason === null
|
|
236
|
+
? `<button type="button" data-add-table="${esc(t.table)}">Add</button> <button type="button" data-choose="${esc(t.table)}">Columns…</button>`
|
|
237
|
+
: t.key.length ? "" : `<button type="button" data-choose="${esc(t.table)}">Name its key…</button>`;
|
|
238
|
+
const said = outcome?.table !== t.table ? ""
|
|
239
|
+
: under("failure" in outcome ? renderFailure(outcome.failure) : "added" in outcome ? renderTableAdded(outcome.added) : renderTablesRemoved(outcome.removed));
|
|
240
|
+
return `<tr class="${t.added ? "c-added" : t.reason ? "c-cannot" : "c-not"}"><th scope="row">${code(t.table)}</th>` +
|
|
241
|
+
`<td class="num">${esc(f.size)}</td><td>${esc(f.key)}</td><td class="state">${esc(f.state ?? "")}</td><td class="act">${act}</td></tr>` +
|
|
242
|
+
(choosing === t.table && !t.added ? under(pickForm(t, ticked)) : "") + said;
|
|
243
|
+
};
|
|
244
|
+
const added = s.tables.filter((t) => t.added).length;
|
|
245
|
+
// a refusal for a table the list does not have (dropped since the run read the catalog): said above the list
|
|
246
|
+
const stray = outcome !== null && "failure" in outcome && !s.tables.some((t) => t.table === outcome.table) ? renderFailure(outcome.failure) : "";
|
|
247
|
+
return `${stray}<p class="count">${n(added)} of ${n(s.tables.length)} added</p>` +
|
|
248
|
+
`<table class="catalog"><thead><tr><th scope="col">Table</th><th scope="col">Rows</th><th scope="col">Key</th><th scope="col"></th><th scope="col"></th></tr></thead>` +
|
|
249
|
+
`<tbody>${s.tables.map(line).join("")}</tbody></table>`;
|
|
250
|
+
}
|
|
251
|
+
|
|
153
252
|
/** The heading's line: the database the run copies, and its sources file's name when that is another. */
|
|
154
253
|
export function renderWhere(s) {
|
|
155
254
|
return `${esc(s.source.database)}${s.source.name && s.source.name !== s.source.database ? ` · source ${esc(s.source.name)}` : ""}`;
|
|
@@ -214,11 +313,20 @@ export const updatedText = (at) => `Updated ${at.toLocaleTimeString("en-GB")}`;
|
|
|
214
313
|
|
|
215
314
|
/**
|
|
216
315
|
* THE PAGE'S STATE AND ACTIONS, over `fetch` (the browser's, or a test's): `poll` reads the status and the users,
|
|
217
|
-
* `addUser` / `askRemove` / `cancelRemove` / `confirmRemove` / `dismissAdded` are the users' buttons
|
|
218
|
-
*
|
|
316
|
+
* `addUser` / `askRemove` / `cancelRemove` / `confirmRemove` / `dismissAdded` are the users' buttons, `addTable` /
|
|
317
|
+
* `choose` / `cancelChoose` / `askRemoveTable` / `cancelRemoveTable` / `confirmRemoveTable` / `dismissOutcome` the
|
|
318
|
+
* tables'; `view()` is the HTML of each part of the page, from what it knows now. Nothing here touches a document.
|
|
219
319
|
*/
|
|
220
320
|
export function createApp(fetchFn, now = () => new Date()) {
|
|
221
|
-
const m = { state: null, requests: {}, users: null, usersFailure: null, pollFailure: null, actionFailure: null, added: null, confirming: null, updatedAt: null
|
|
321
|
+
const m = { state: null, requests: {}, users: null, usersFailure: null, pollFailure: null, actionFailure: null, added: null, confirming: null, updatedAt: null,
|
|
322
|
+
removing: null, choosing: null, outcome: null, ticked: null, held: null };
|
|
323
|
+
// WHILE A FORM IS OPEN OR A REMOVE ASKS, THE TABLES ARE RENDERED FROM THE STATE THEY WERE OPENED OVER (`held`): the
|
|
324
|
+
// run reads the catalog again every few seconds, and a live database's row estimates move, so a part written from
|
|
325
|
+
// each poll's state would be rewritten under the person — the form's ticks gone, the confirm's button replaced under
|
|
326
|
+
// the click. So the list is FROZEN for as long as a form stays open or a Remove asks — however long that is, with no
|
|
327
|
+
// refresh in between (its rows and its added marks may be minutes old; the Status part above is written from each
|
|
328
|
+
// poll as ever) — and is written from the latest state again once they Add, Cancel or confirm.
|
|
329
|
+
const hold = () => { m.held = m.choosing !== null || m.removing !== null ? m.held ?? m.state : null; if (m.choosing === null) m.ticked = null; };
|
|
222
330
|
/** One request: its JSON, or a Failure (`failure`) when it was refused or nothing answered. */
|
|
223
331
|
const ask = async (path, init) => {
|
|
224
332
|
let res;
|
|
@@ -236,15 +344,44 @@ export function createApp(fetchFn, now = () => new Date()) {
|
|
|
236
344
|
if (m.confirming !== null && !m.users.users.some((x) => x.name === m.confirming)) m.confirming = null;
|
|
237
345
|
};
|
|
238
346
|
const json = (method, body) => ({ method, headers: { "Content-Type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
347
|
+
const poll = async () => {
|
|
348
|
+
const [st, slow, refused] = await Promise.all([ask("/api/state"), ask("/api/slow"), ask("/api/refused")]);
|
|
349
|
+
if ("failure" in st) { m.pollFailure = st.failure; return; }
|
|
350
|
+
m.state = st.body; m.pollFailure = null; m.updatedAt = now();
|
|
351
|
+
m.requests = { slow: "failure" in slow ? slow : slow.body, refused: "failure" in refused ? refused : refused.body };
|
|
352
|
+
// a table removed or added elsewhere (mason remove, mason add) takes its confirm or its form with it
|
|
353
|
+
const added = (table) => m.state.tables.some((t) => t.table === table && t.added);
|
|
354
|
+
if (m.removing !== null && !added(m.removing)) m.removing = null;
|
|
355
|
+
if (m.choosing !== null && added(m.choosing)) m.choosing = null;
|
|
356
|
+
hold();
|
|
357
|
+
await readUsers();
|
|
358
|
+
};
|
|
239
359
|
return {
|
|
240
360
|
model: m,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
361
|
+
poll,
|
|
362
|
+
/** Add a table (`mason add`): the answer under its line, then the state read again — the Status shows its copy starting. */
|
|
363
|
+
async addTable(table, columns = [], key = []) {
|
|
364
|
+
const r = await ask("/api/pick", json("POST", pickBody(table, columns, key)));
|
|
365
|
+
// refused: said under the form, which is written again with the boxes the person had ticked
|
|
366
|
+
if ("failure" in r) { m.outcome = { table, failure: r.failure }; if (m.choosing === table) m.ticked = { columns, key }; return false; }
|
|
367
|
+
m.outcome = { table, added: r.body }; m.choosing = null; hold();
|
|
368
|
+
await poll();
|
|
369
|
+
return true;
|
|
370
|
+
},
|
|
371
|
+
choose(table) { m.choosing = table; m.removing = null; m.ticked = null; hold(); },
|
|
372
|
+
cancelChoose() { m.choosing = null; hold(); },
|
|
373
|
+
askRemoveTable(table) { m.removing = table; m.choosing = null; hold(); },
|
|
374
|
+
cancelRemoveTable() { m.removing = null; hold(); },
|
|
375
|
+
/** Remove a table (`mason remove`), once asked: the answer under its line, then the state read again. */
|
|
376
|
+
async confirmRemoveTable(table) {
|
|
377
|
+
const r = await ask("/api/remove", json("POST", { tables: [table] }));
|
|
378
|
+
m.removing = null; hold();
|
|
379
|
+
if ("failure" in r) { m.outcome = { table, failure: r.failure }; return false; }
|
|
380
|
+
m.outcome = { table, removed: r.body };
|
|
381
|
+
await poll();
|
|
382
|
+
return true;
|
|
247
383
|
},
|
|
384
|
+
dismissOutcome() { m.outcome = null; },
|
|
248
385
|
async addUser(name) {
|
|
249
386
|
m.actionFailure = null;
|
|
250
387
|
const r = await ask("/api/users", json("POST", { name }));
|
|
@@ -270,6 +407,7 @@ export function createApp(fetchFn, now = () => new Date()) {
|
|
|
270
407
|
updated: m.updatedAt ? esc(updatedText(m.updatedAt)) : "",
|
|
271
408
|
poll: m.pollFailure ? renderFailure(m.pollFailure) : "",
|
|
272
409
|
status: m.state ? renderStatus(m.state, m.requests) : m.pollFailure ? "" : `<p class="dim">Asking Mason…</p>`,
|
|
410
|
+
tables: m.state ? renderTables(m.held ?? m.state, { removing: m.removing, choosing: m.choosing, outcome: m.outcome, ticked: m.ticked }) : "",
|
|
273
411
|
users: m.usersFailure ? renderFailure(m.usersFailure) : m.users ? renderUsers(m.users, m.confirming) : "",
|
|
274
412
|
added: m.added ? renderAdded(m.added) : "",
|
|
275
413
|
action: m.actionFailure ? renderFailure(m.actionFailure) : "",
|
|
@@ -309,6 +447,16 @@ export function start(doc, win) {
|
|
|
309
447
|
if (await app.addUser(input.value.trim())) input.value = "";
|
|
310
448
|
paint();
|
|
311
449
|
});
|
|
450
|
+
// a table's form is written into the tables part: its submit is caught where it bubbles to, the ticked boxes sent as
|
|
451
|
+
// they are (FormData's getAll: the values of the boxes ticked, in the page's order)
|
|
452
|
+
doc.addEventListener("submit", async (e) => {
|
|
453
|
+
const f = e.target;
|
|
454
|
+
if (f.dataset?.pickForm === undefined) return;
|
|
455
|
+
e.preventDefault();
|
|
456
|
+
const ticked = new win.FormData(f);
|
|
457
|
+
await app.addTable(f.dataset.pickForm, ticked.getAll("columns"), ticked.getAll("key"));
|
|
458
|
+
paint();
|
|
459
|
+
});
|
|
312
460
|
doc.addEventListener("click", async (e) => {
|
|
313
461
|
const b = e.target.closest?.("button");
|
|
314
462
|
if (!b) return;
|
|
@@ -316,6 +464,13 @@ export function start(doc, win) {
|
|
|
316
464
|
else if (b.dataset.cancelRemove !== undefined) app.cancelRemove();
|
|
317
465
|
else if (b.dataset.confirmRemove !== undefined) await app.confirmRemove(b.dataset.confirmRemove);
|
|
318
466
|
else if (b.dataset.dismissAdded !== undefined) app.dismissAdded();
|
|
467
|
+
else if (b.dataset.addTable !== undefined) await app.addTable(b.dataset.addTable);
|
|
468
|
+
else if (b.dataset.choose !== undefined) app.choose(b.dataset.choose);
|
|
469
|
+
else if (b.dataset.cancelChoose !== undefined) app.cancelChoose();
|
|
470
|
+
else if (b.dataset.removeTable !== undefined) app.askRemoveTable(b.dataset.removeTable);
|
|
471
|
+
else if (b.dataset.cancelRemoveTable !== undefined) app.cancelRemoveTable();
|
|
472
|
+
else if (b.dataset.confirmRemoveTable !== undefined) await app.confirmRemoveTable(b.dataset.confirmRemoveTable);
|
|
473
|
+
else if (b.dataset.dismissOutcome !== undefined) app.dismissOutcome();
|
|
319
474
|
else if (b.dataset.copy !== undefined) {
|
|
320
475
|
const text = doc.getElementById(b.dataset.copy)?.textContent ?? "";
|
|
321
476
|
try { await win.navigator.clipboard.writeText(text); b.textContent = "Copied"; }
|
package/dist/src/web/index.html
CHANGED
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
<h2 id="status-h">Status</h2>
|
|
22
22
|
<div id="status"><p class="dim">Asking Mason…</p></div>
|
|
23
23
|
</section>
|
|
24
|
+
<section aria-labelledby="tables-h">
|
|
25
|
+
<h2 id="tables-h">Tables</h2>
|
|
26
|
+
<p class="lead">The database's tables, as <code>mason tables</code> lists them. Add one and Mason copies it: it is added to the sources file, as <code>mason add</code> does; Remove takes it out and drops its copy, as <code>mason remove</code> does.</p>
|
|
27
|
+
<div id="tables"></div>
|
|
28
|
+
</section>
|
|
24
29
|
<section aria-labelledby="users-h">
|
|
25
30
|
<h2 id="users-h">Users</h2>
|
|
26
31
|
<p class="lead">A user of the Postgres API: a name and a password that does not expire, for a BI tool or an app. The same users as <code>mason user</code>.</p>
|
|
@@ -34,6 +39,6 @@
|
|
|
34
39
|
<div id="users"></div>
|
|
35
40
|
</section>
|
|
36
41
|
</main>
|
|
37
|
-
<noscript><p>This page needs JavaScript. <code>mason status</code> and <code>mason user list</code> say the same in a terminal.</p></noscript>
|
|
42
|
+
<noscript><p>This page needs JavaScript. <code>mason status</code>, <code>mason tables</code> and <code>mason user list</code> say the same in a terminal.</p></noscript>
|
|
38
43
|
</body>
|
|
39
44
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mason-data/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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://masondata.ai",
|