@mason-data/cli 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/src/api/error.js +4 -0
- package/dist/src/api/postgres/proxy.js +6 -160
- package/dist/src/api/postgres/server.js +22 -4
- package/dist/src/cli/command.js +169 -5
- package/dist/src/cli/commands.js +290 -157
- package/dist/src/cli/deploy.js +134 -28
- package/dist/src/cli/doctor.js +14 -11
- package/dist/src/cli/guides.js +20 -11
- package/dist/src/cli/hosted.js +1 -1
- package/dist/src/cli/init.js +110 -25
- package/dist/src/cli/login.js +20 -17
- package/dist/src/cli/machine.js +125 -40
- package/dist/src/cli/main.js +51 -8
- package/dist/src/cli/mcp.js +15 -12
- package/dist/src/cli/project.js +1 -1
- package/dist/src/cli/registry.js +80 -35
- package/dist/src/cli/replica-hosted.js +761 -102
- package/dist/src/cli/replica.js +43 -5
- package/dist/src/cli/replication-mode.js +5 -0
- package/dist/src/cli/request-filter.js +16 -6
- package/dist/src/cli/run-server.js +25 -2
- package/dist/src/cli/secret-input.js +193 -0
- package/dist/src/cli/serve.js +2 -2
- package/dist/src/cli/share.js +20 -15
- package/dist/src/cli/source-role.js +14 -15
- package/dist/src/cli/sources.js +20 -3
- package/dist/src/cli/style.js +14 -0
- package/dist/src/cli/sync.js +17 -8
- package/dist/src/cli/token-hosted.js +30 -24
- package/dist/src/cli/tunnel.js +1 -1
- package/dist/src/cli/ui-hosted.js +163 -0
- package/dist/src/cli/ui-ssh.js +3 -3
- package/dist/src/cli/ui.js +67 -26
- package/dist/src/cli/usage.js +19 -15
- package/dist/src/cli/user-hosted.js +78 -13
- package/dist/src/cli/user.js +168 -50
- package/dist/src/duck/users.js +105 -9
- package/dist/src/lib/cidr.js +177 -0
- package/dist/src/lib/table-sync.js +69 -19
- package/package.json +6 -6
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADDRESSES AND ADDRESS RANGES, READ BY ONE GRAMMAR: `MASON_PG_PROXY_FROM` (src/api/postgres/proxy.ts) and a `mason
|
|
3
|
+
* user`'s address ranges (src/duck/users.ts `from`) both read an entry here. An entry is an IPv4 or IPv6 address, alone
|
|
4
|
+
* (one address: /32 or /128) or with its prefix length, held to its family (`10.0.0.0/33` and `999.999.999.999/99` are
|
|
5
|
+
* refused), with no bits set past the prefix (`10.0.1.5/24` is refused with its network as the guess), and no host name
|
|
6
|
+
* (never looked up) nor zone index. An address is first held to `node:net`'s own reading (`isIPv4`, `isIPv6`), then
|
|
7
|
+
* read into its bytes here, so nothing either reading refuses is ever taken.
|
|
8
|
+
*
|
|
9
|
+
* EVERY ADDRESS IN ONE SPACE, as Mason means one: IPv6's 128 bits, an IPv4 address at its v4-mapped place
|
|
10
|
+
* (::ffff:a.b.c.d), since a v4-mapped peer or header source IS that IPv4 client (`shownAddress`). What an entry names and
|
|
11
|
+
* what a list covers are both read here, never from how an entry is spelt: `0.0.0.0/1,128.0.0.0/1` is every IPv4
|
|
12
|
+
* address, as `0.0.0.0/0` is, and `::/64` holds every v4-mapped address, so every IPv4 one too.
|
|
13
|
+
*/
|
|
14
|
+
import * as net from "node:net";
|
|
15
|
+
/** An address as Mason shows it: IPv4 dotted, IPv6 in RFC 5952's form, a v4-mapped IPv6 one as IPv4; null when it is no
|
|
16
|
+
* address (undefined, a zone index, a name). */
|
|
17
|
+
export function shownAddress(text) {
|
|
18
|
+
if (text === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
const v4 = ipv4Bytes(text);
|
|
21
|
+
if (v4 !== null)
|
|
22
|
+
return Array.from(v4).join(".");
|
|
23
|
+
const v6 = ipv6Bytes(text);
|
|
24
|
+
return v6 === null ? null : ipv6Text(v6);
|
|
25
|
+
}
|
|
26
|
+
/** An IPv4 address's 4 bytes, from dotted decimal with no leading zero; null for anything else. */
|
|
27
|
+
function ipv4Bytes(text) {
|
|
28
|
+
if (!net.isIPv4(text))
|
|
29
|
+
return null;
|
|
30
|
+
const parts = text.split(".");
|
|
31
|
+
if (parts.length !== 4 || !parts.every((p) => /^(?:0|[1-9]\d{0,2})$/.test(p) && Number(p) <= 255))
|
|
32
|
+
return null;
|
|
33
|
+
return Uint8Array.from(parts.map(Number));
|
|
34
|
+
}
|
|
35
|
+
/** An IPv6 address's 16 bytes (`::` once at most, an IPv4 tail); null for anything else, a zone index included. */
|
|
36
|
+
export function ipv6Bytes(text) {
|
|
37
|
+
// node:net takes a zone index (fe80::1%eth0): an address with one names no place here
|
|
38
|
+
if (!/^[0-9A-Fa-f:.]+$/.test(text) || !net.isIPv6(text))
|
|
39
|
+
return null;
|
|
40
|
+
const halves = text.split("::");
|
|
41
|
+
if (halves.length > 2 || (halves.length === 2 && halves[0].includes(".")))
|
|
42
|
+
return null;
|
|
43
|
+
const groups = (s) => {
|
|
44
|
+
if (s === "")
|
|
45
|
+
return [];
|
|
46
|
+
const out = [];
|
|
47
|
+
const parts = s.split(":");
|
|
48
|
+
for (const [i, p] of parts.entries()) {
|
|
49
|
+
if (i === parts.length - 1 && p.includes(".")) {
|
|
50
|
+
const v4 = ipv4Bytes(p);
|
|
51
|
+
if (v4 === null)
|
|
52
|
+
return null;
|
|
53
|
+
out.push((v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3]);
|
|
54
|
+
}
|
|
55
|
+
else if (/^[0-9A-Fa-f]{1,4}$/.test(p))
|
|
56
|
+
out.push(parseInt(p, 16));
|
|
57
|
+
else
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
};
|
|
62
|
+
const head = groups(halves[0]), tail = halves.length === 2 ? groups(halves[1]) : [];
|
|
63
|
+
if (head === null || tail === null)
|
|
64
|
+
return null;
|
|
65
|
+
const n = head.length + tail.length;
|
|
66
|
+
if (halves.length === 1 ? n !== 8 : n > 7)
|
|
67
|
+
return null;
|
|
68
|
+
const all = [...head, ...new Array(8 - n).fill(0), ...tail];
|
|
69
|
+
return Uint8Array.from(all.flatMap((g) => [g >> 8, g & 0xff]));
|
|
70
|
+
}
|
|
71
|
+
/** whether 16 bytes are an IPv4 address mapped into IPv6 (::ffff:a.b.c.d) */
|
|
72
|
+
const v4Mapped = (b) => b.subarray(0, 10).every((x) => x === 0) && b[10] === 0xff && b[11] === 0xff;
|
|
73
|
+
/** 16 bytes as Mason shows an IPv6 address: RFC 5952 (lower case, the longest run of two or more zero groups as `::`,
|
|
74
|
+
* the first of equal runs), and a v4-mapped one as its IPv4 address. */
|
|
75
|
+
export function ipv6Text(b) {
|
|
76
|
+
if (v4Mapped(b))
|
|
77
|
+
return Array.from(b.subarray(12, 16)).join(".");
|
|
78
|
+
const g = Array.from({ length: 8 }, (_, i) => (b[2 * i] << 8) | b[2 * i + 1]);
|
|
79
|
+
let at = -1, run = 0;
|
|
80
|
+
for (let i = 0; i < 8;) {
|
|
81
|
+
if (g[i] !== 0) {
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let j = i;
|
|
86
|
+
while (j < 8 && g[j] === 0)
|
|
87
|
+
j++;
|
|
88
|
+
if (j - i > run) {
|
|
89
|
+
at = i;
|
|
90
|
+
run = j - i;
|
|
91
|
+
}
|
|
92
|
+
i = j;
|
|
93
|
+
}
|
|
94
|
+
const hex = g.map((x) => x.toString(16));
|
|
95
|
+
return run < 2 ? hex.join(":") : `${hex.slice(0, at).join(":")}::${hex.slice(at + run).join(":")}`;
|
|
96
|
+
}
|
|
97
|
+
export const V4_START = 0xffffn << 32n;
|
|
98
|
+
export const V4_END = V4_START + (1n << 32n) - 1n;
|
|
99
|
+
export const ALL_END = (1n << 128n) - 1n;
|
|
100
|
+
const bigOf = (b) => b.reduce((n, x) => (n << 8n) | BigInt(x), 0n);
|
|
101
|
+
/** an address's place (`V4_START`), or null when it is no address */
|
|
102
|
+
function placeOf(address) {
|
|
103
|
+
const v4 = ipv4Bytes(address);
|
|
104
|
+
if (v4 !== null)
|
|
105
|
+
return V4_START | bigOf(v4);
|
|
106
|
+
const v6 = ipv6Bytes(address);
|
|
107
|
+
return v6 === null ? null : bigOf(v6);
|
|
108
|
+
}
|
|
109
|
+
/** the places an entry names */
|
|
110
|
+
export function rangeOf(c) {
|
|
111
|
+
const start = c.family === 4 ? V4_START | bigOf(c.bytes) : bigOf(c.bytes);
|
|
112
|
+
return { start, end: start + (1n << BigInt((c.family === 4 ? 32 : 128) - c.prefix)) - 1n };
|
|
113
|
+
}
|
|
114
|
+
/** Whether `address` (as `shownAddress` shows one, or in any spelling of the same address) is in one of `ranges`; never
|
|
115
|
+
* for what is no address. */
|
|
116
|
+
export function inRanges(ranges, address) {
|
|
117
|
+
const at = placeOf(address);
|
|
118
|
+
return at !== null && ranges.some((r) => r.start <= at && at <= r.end);
|
|
119
|
+
}
|
|
120
|
+
/** whether `ranges` together hold every place from `lo` to `hi` */
|
|
121
|
+
export function covers(ranges, lo, hi) {
|
|
122
|
+
let next = lo;
|
|
123
|
+
for (const r of [...ranges].sort((a, b) => (a.start < b.start ? -1 : a.start > b.start ? 1 : 0))) {
|
|
124
|
+
if (r.start > next)
|
|
125
|
+
return false;
|
|
126
|
+
if (r.end >= next)
|
|
127
|
+
next = r.end + 1n;
|
|
128
|
+
if (next > hi)
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return next > hi;
|
|
132
|
+
}
|
|
133
|
+
function prefixEqual(a, b, bits) {
|
|
134
|
+
for (let i = 0; i * 8 < bits; i++) {
|
|
135
|
+
const mask = bits - i * 8 >= 8 ? 0xff : (0xff << (8 - (bits - i * 8))) & 0xff;
|
|
136
|
+
if ((a[i] & mask) !== (b[i] & mask))
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
/** the network `bytes` / `prefix` names, its host bits cleared */
|
|
142
|
+
function network(bytes, prefix) {
|
|
143
|
+
return bytes.map((x, i) => i * 8 >= prefix ? 0 : prefix - i * 8 >= 8 ? x : x & ((0xff << (8 - (prefix - i * 8))) & 0xff));
|
|
144
|
+
}
|
|
145
|
+
const text = (family, bytes) => family === 4 ? Array.from(bytes).join(".") : ipv6Text(bytes);
|
|
146
|
+
/** An entry as Mason writes it back: the network in `shownAddress`'s form, its prefix only when it is not one address. */
|
|
147
|
+
export const cidrText = (c) => `${text(c.family, c.bytes)}${c.prefix === (c.family === 4 ? 32 : 128) ? "" : `/${c.prefix}`}`;
|
|
148
|
+
/**
|
|
149
|
+
* One entry read, or what is wrong with it, with a did-you-mean where one is a fair guess: a guess is offered only
|
|
150
|
+
* once it is read as an entry itself, so it is never one that would be refused in turn. `everyRefused`: a prefix of 0
|
|
151
|
+
* (every address of a family) is refused with these words (a PROXY header believed from anyone); else it is taken.
|
|
152
|
+
*/
|
|
153
|
+
export function cidrOf(entry, everyRefused = null) {
|
|
154
|
+
const quoted = JSON.stringify(entry);
|
|
155
|
+
const m = /^([^/]*)(?:\/(\d{1,3}))?$/.exec(entry);
|
|
156
|
+
const v4 = m === null ? null : ipv4Bytes(m[1]), v6 = m === null || v4 !== null ? null : ipv6Bytes(m[1]);
|
|
157
|
+
if (m === null || (v4 === null && v6 === null)) {
|
|
158
|
+
return { problem: `${quoted} is not an address or a CIDR (10.0.0.0/16, 2001:db8::/32, or one address such as 10.0.4.7; a host name is not looked up)` };
|
|
159
|
+
}
|
|
160
|
+
const family = v4 !== null ? 4 : 6, bytes = (v4 ?? v6), max = family === 4 ? 32 : 128;
|
|
161
|
+
const prefix = m[2] === undefined ? max : Number(m[2]);
|
|
162
|
+
if (prefix > max)
|
|
163
|
+
return { problem: `${quoted}: an IPv${family} prefix is at most /${max}` };
|
|
164
|
+
if (family === 6 && v4Mapped(bytes) && prefix >= 96) {
|
|
165
|
+
const p4 = prefix - 96, net4 = network(bytes.subarray(12, 16), p4);
|
|
166
|
+
const meant = `${text(4, net4)}${p4 === 32 ? "" : `/${p4}`}`, read = cidrOf(meant, everyRefused);
|
|
167
|
+
return { problem: "problem" in read
|
|
168
|
+
? `${quoted} is IPv4 written as IPv6, which Mason reads as ${meant}, and ${read.problem}`
|
|
169
|
+
: `${quoted} is IPv4 written as IPv6, which Mason reads as IPv4: did you mean ${meant}?` };
|
|
170
|
+
}
|
|
171
|
+
if (prefix === 0 && everyRefused !== null)
|
|
172
|
+
return { problem: `${quoted} names every address, ${everyRefused}` };
|
|
173
|
+
const net = network(bytes, prefix);
|
|
174
|
+
if (!prefixEqual(net, bytes, max))
|
|
175
|
+
return { problem: `${quoted} has bits set past its prefix: did you mean ${text(family, net)}/${prefix}?` };
|
|
176
|
+
return { family, bytes: net, prefix };
|
|
177
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* in the same words at both entrances:
|
|
6
6
|
*
|
|
7
7
|
* off nothing after the first copy until `mason sync <table>`, which polls it once, now (the default)
|
|
8
|
-
* poll <interval> the xmin poll every <interval>: <n>s, <n>m or <n>
|
|
8
|
+
* poll <interval> the xmin poll every <interval>: <n>s, <n>m, <n>h or <n>d, a whole number, at least 1s, at most 24h
|
|
9
9
|
* replication the database's change log, through the project's ONE slot and publication (src/duck/stream.ts):
|
|
10
10
|
* the publication carries the replicated tables only, and the polled ones are polled beside it
|
|
11
11
|
*
|
|
@@ -34,11 +34,11 @@ export const BIG_TABLE_ROWS = 1_000_000;
|
|
|
34
34
|
export const COSTLY_POLL_ROWS = 10_000_000;
|
|
35
35
|
export const COSTLY_POLL_SECONDS = 60;
|
|
36
36
|
const MODES = ["off", "poll", "replication"];
|
|
37
|
-
const UNIT_SECONDS = { s: 1, m: 60, h: 3600 };
|
|
37
|
+
const UNIT_SECONDS = { s: 1, m: 60, h: 3600, d: 86_400 };
|
|
38
38
|
/** the units a person may write for one of ours, said back as the one to write */
|
|
39
39
|
const UNIT_WORDS = {
|
|
40
40
|
s: "s", sec: "s", secs: "s", second: "s", seconds: "s", m: "m", min: "m", mins: "m", minute: "m", minutes: "m",
|
|
41
|
-
h: "h", hr: "h", hrs: "h", hour: "h", hours: "h",
|
|
41
|
+
h: "h", hr: "h", hrs: "h", hour: "h", hours: "h", d: "d", day: "d", days: "d",
|
|
42
42
|
};
|
|
43
43
|
const EXAMPLES = "poll 5s, poll 5m or poll 1h";
|
|
44
44
|
/** what a sync is, said where one is wrong */
|
|
@@ -52,23 +52,24 @@ export function intervalOf(seconds) {
|
|
|
52
52
|
const s = Math.max(MIN_POLL_SECONDS, Math.round(seconds));
|
|
53
53
|
return s % 3600 === 0 ? `${s / 3600}h` : s % 60 === 0 ? `${s / 60}m` : `${s}s`;
|
|
54
54
|
}
|
|
55
|
-
/** an interval (`<n>s|m|h`) read, or why it is not one, with the interval to write when there is one. ONE grammar with
|
|
56
|
-
* entrances, each worded as it is written: a `sync:`'s `poll <interval
|
|
57
|
-
* command's
|
|
55
|
+
/** an interval (`<n>s|m|h|d`) read, or why it is not one, with the interval to write when there is one. ONE grammar with
|
|
56
|
+
* two entrances, each worded as it is written: a `sync:`'s `poll <interval>` and a generated table's `target_lag:`. Its
|
|
57
|
+
* units are a command line's duration's too (`parseDuration`), which also takes a bare number of seconds: a file writes
|
|
58
|
+
* the unit, one spelling of each interval. */
|
|
58
59
|
function parseInterval(v, write = "poll ") {
|
|
59
60
|
const m = /^(\d+(?:\.\d+)?)\s*([a-z]+)$/i.exec(v);
|
|
60
61
|
const examples = write === "" ? "5s, 5m or 1h" : EXAMPLES;
|
|
61
|
-
const how = `an interval is <n>s, <n>m or <n>
|
|
62
|
+
const how = `an interval is <n>s, <n>m, <n>h or <n>d, a whole number, at least ${MIN_POLL_SECONDS}s (${examples})`;
|
|
63
|
+
if (/^\d+$/.test(v))
|
|
64
|
+
return { problem: `${v} has no unit: write ${write}${Number(v)}s for seconds (or m, h, d)` };
|
|
62
65
|
if (m === null)
|
|
63
66
|
return { problem: `${JSON.stringify(v)} is not an interval: ${how}` };
|
|
64
67
|
const n = Number(m[1]), word = m[2].toLowerCase();
|
|
65
68
|
if (word === "ms")
|
|
66
69
|
return { problem: `${v} is under the shortest interval, ${MIN_POLL_SECONDS}s${write === "" ? "" : " (a poll scans the table whole on your database)"}: write ${write}${MIN_POLL_SECONDS}s or longer` };
|
|
67
70
|
const unit = UNIT_WORDS[word];
|
|
68
|
-
if (unit === undefined)
|
|
69
|
-
|
|
70
|
-
return { problem: `${v}: ${word} is not a unit of an interval — ${days ? `days are hours${days}` : how}` };
|
|
71
|
-
}
|
|
71
|
+
if (unit === undefined)
|
|
72
|
+
return { problem: `${v}: ${word} is not a unit of an interval — ${how}` };
|
|
72
73
|
const seconds = n * UNIT_SECONDS[unit];
|
|
73
74
|
if (!Number.isInteger(n))
|
|
74
75
|
return { problem: `${v}: an interval is a whole number — write ${write}${intervalOf(seconds)}` };
|
|
@@ -89,10 +90,41 @@ export function parseLag(v) {
|
|
|
89
90
|
return { problem: `is ${v === null || v === undefined ? "nothing" : typeof v === "number" ? `the number ${String(v)}` : "not an interval"}: write how far behind the table may be, like 30s, 5m or 1h` };
|
|
90
91
|
return parseInterval(v.trim(), "");
|
|
91
92
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
/** How a duration on a command line is written: said where one is wrong, and `<duration>` in each usage. */
|
|
94
|
+
export const DURATION_FORMS = "a number of seconds, or <n>s, <n>m, <n>h or <n>d (90, 30s, 10m, 1h, 7d)";
|
|
95
|
+
/**
|
|
96
|
+
* A DURATION ON A COMMAND LINE, ONE GRAMMAR FOR EVERY FLAG THAT TAKES ONE (`--timeout` of mason wait, share, ui --ssh,
|
|
97
|
+
* deploy and replica create; mason usage's `--step`; the age `--since` / `--until` take, src/cli/request-filter.ts
|
|
98
|
+
* `timeOf`, which refuses a bare number as a time): a bare number is seconds, as wait, share and ui read it before; `<n>s|m|h|d`, and the words a sync's
|
|
99
|
+
* interval takes (`5 min`, `2 hours`, `7 days`), as a `sync:` interval and a `target_lag:` are written. A superset of
|
|
100
|
+
* every form each flag took before, so nothing that parsed stops parsing. The range is the flag's (`min`, `max` in
|
|
101
|
+
* seconds; over 0, unless `zero`: an age of now, `--since 0s`, and `--step 0s`, every sample, as each took it before);
|
|
102
|
+
* the caller names the flag.
|
|
103
|
+
*/
|
|
104
|
+
export function parseDuration(v, o = {}) {
|
|
105
|
+
const t = v.trim();
|
|
106
|
+
let seconds, every;
|
|
107
|
+
if (t !== "" && Number.isFinite(Number(t))) {
|
|
108
|
+
seconds = Number(t);
|
|
109
|
+
every = `${seconds}s`;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const m = /^(\d+(?:\.\d+)?)\s*([a-z]+)$/i.exec(t);
|
|
113
|
+
const unit = m === null ? undefined : UNIT_WORDS[m[2].toLowerCase()];
|
|
114
|
+
if (m === null || unit === undefined)
|
|
115
|
+
return { problem: `${JSON.stringify(v)} is not a duration: write ${DURATION_FORMS}` };
|
|
116
|
+
seconds = Number(m[1]) * UNIT_SECONDS[unit];
|
|
117
|
+
every = `${Number(m[1])}${unit}`;
|
|
118
|
+
}
|
|
119
|
+
if (o.zero === true && seconds === 0 && !Object.is(seconds, -0))
|
|
120
|
+
return { every, seconds: 0 };
|
|
121
|
+
if (!(seconds > 0))
|
|
122
|
+
return { problem: `${JSON.stringify(v)} is not a duration over 0: write ${DURATION_FORMS}` };
|
|
123
|
+
if (o.min !== undefined && seconds < o.min)
|
|
124
|
+
return { problem: `${every} is under the shortest, ${intervalOf(o.min)}` };
|
|
125
|
+
if (o.max !== undefined && seconds > o.max)
|
|
126
|
+
return { problem: `${every} is over the longest, ${intervalOf(o.max)}` };
|
|
127
|
+
return { every, seconds };
|
|
96
128
|
}
|
|
97
129
|
/**
|
|
98
130
|
* A `sync:` VALUE READ, or what is wrong with it in the words a person fixes it from (the key is the caller's to say):
|
|
@@ -186,9 +218,14 @@ function saidInTheFile(what) {
|
|
|
186
218
|
* every one given on ONE report, one coherent way out (a publication goes with replication). Null when none is given.
|
|
187
219
|
* Read from the raw command line, before the options are parsed, so a removed flag is answered as removed and never
|
|
188
220
|
* as an unknown option.
|
|
221
|
+
*
|
|
222
|
+
* `--port` IS NOW `--ui-port` (issue 729: it is the web page's and the commands' port, as MASON_UI_PORT is on a server,
|
|
223
|
+
* and read as the Postgres API's): answered on the same report with the line to run — `start`, built from the flags
|
|
224
|
+
* mason start takes alone, so nothing else of the line is repeated. `sync`: a sources-file flag was given.
|
|
189
225
|
*/
|
|
190
226
|
export function removedStartFlags(argv, top) {
|
|
191
|
-
let replication = false, poll = false, publication;
|
|
227
|
+
let replication = false, poll = false, publication, port = false;
|
|
228
|
+
const kept = [];
|
|
192
229
|
for (let i = 0; i < argv.length; i++) {
|
|
193
230
|
const a = argv[i];
|
|
194
231
|
if (a === "--")
|
|
@@ -203,10 +240,21 @@ export function removedStartFlags(argv, top) {
|
|
|
203
240
|
const v = argv[i + 1];
|
|
204
241
|
publication = v !== undefined && !v.startsWith("-") ? v : null;
|
|
205
242
|
}
|
|
243
|
+
else if (a === "--port" || a.startsWith("--port=")) {
|
|
244
|
+
const v = a === "--port" ? argv[i + 1] : a.slice("--port=".length);
|
|
245
|
+
if (a === "--port" && v !== undefined && !v.startsWith("-"))
|
|
246
|
+
i++;
|
|
247
|
+
port = true;
|
|
248
|
+
kept.push(`--ui-port ${v !== undefined && /^\d{1,5}$/.test(v) ? v : "<n>"}`);
|
|
249
|
+
}
|
|
250
|
+
else if (a === "--attach" || a === "--self-signed-tls" || a === "--json")
|
|
251
|
+
kept.push(a);
|
|
206
252
|
}
|
|
207
253
|
const given = [...(replication ? ["--replication"] : []), ...(poll ? ["--poll"] : []), ...(publication !== undefined ? ["--publication"] : [])];
|
|
254
|
+
const start = port ? ["mason start", ...kept].join(" ") : "mason start";
|
|
255
|
+
const portSaid = { message: "--port is now --ui-port", why: "it is the port of the web page and the mason commands (MASON_UI_PORT on a server), never the Postgres API's (MASON_PG_PORT)" };
|
|
208
256
|
if (given.length === 0)
|
|
209
|
-
return null;
|
|
257
|
+
return port ? { ...portSaid, fix: start, start, sync: false } : null;
|
|
210
258
|
const t = top();
|
|
211
259
|
const named = given.length === 1 ? `${given[0]} is gone` : `${given.slice(0, -1).join(", ")} and ${given.at(-1)} are gone`;
|
|
212
260
|
const pub = publication === undefined ? null : topEdit(t, "publication", [publication ?? "<name>"]);
|
|
@@ -230,8 +278,10 @@ export function removedStartFlags(argv, top) {
|
|
|
230
278
|
fix = `${edit} (${edit.startsWith("keep ") ? "every table is polled already; a table's entry may say its own"
|
|
231
279
|
: "any interval: poll 5s, poll 1h; or sync: off, the default, and mason sync <table> when you need it"})`;
|
|
232
280
|
}
|
|
233
|
-
|
|
234
|
-
|
|
281
|
+
const why = saidInTheFile({ sync: replication || poll, publication: publication !== undefined });
|
|
282
|
+
if (!port)
|
|
283
|
+
return { message: `${named}: the sources file says it now`, why, fix: `${fix}, then mason start`, start, sync: true };
|
|
284
|
+
return { message: `${named}: the sources file says it now; ${portSaid.message}`, why: `${why}; ${portSaid.why}`, fix: `${fix}, then ${start}`, start, sync: true };
|
|
235
285
|
}
|
|
236
286
|
/**
|
|
237
287
|
* `MASON_CHANGES` SET IS REFUSED, never run on a default (CLAUDE.md: a removed setting refuses the start, naming the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mason-data/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "mason start: point Mason at your Postgres and pick the tables to copy. Fast analytics for the database your app already has.",
|
|
5
5
|
"keywords": ["mason", "analytics", "postgres", "duckdb", "cli"],
|
|
6
6
|
"homepage": "https://github.com/embeddable-hq/mason-data#readme",
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"yaml": "^2.6.0"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"@mason-data/extensions-darwin-arm64": "0.
|
|
27
|
-
"@mason-data/extensions-darwin-x64": "0.
|
|
28
|
-
"@mason-data/extensions-linux-arm64": "0.
|
|
29
|
-
"@mason-data/extensions-linux-x64": "0.
|
|
30
|
-
"@mason-data/extensions-win32-x64": "0.
|
|
26
|
+
"@mason-data/extensions-darwin-arm64": "0.10.0",
|
|
27
|
+
"@mason-data/extensions-darwin-x64": "0.10.0",
|
|
28
|
+
"@mason-data/extensions-linux-arm64": "0.10.0",
|
|
29
|
+
"@mason-data/extensions-linux-x64": "0.10.0",
|
|
30
|
+
"@mason-data/extensions-win32-x64": "0.10.0"
|
|
31
31
|
}
|
|
32
32
|
}
|