@motherbase/cli 0.2.5 → 0.2.8

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/src/main.js CHANGED
@@ -1,194 +1,224 @@
1
- 'use strict';
2
-
3
- const path = require('path');
4
- const { registry, groups } = require('./commands');
5
- const { createContext, CliError } = require('./context');
6
- const { FORMATS } = require('./format');
7
- const ui = require('./ui');
8
-
9
- // Flags that never consume a following value.
10
- const PURE_BOOLEAN = new Set([
11
- 'json', 'yes', 'help', 'version', 'force', 'preview', 'app', 'no-data', 'no-auth', 'f', 'debug',
12
- // Codegen: `--check` and `--local` never take a value, so a trailing
13
- // `--check database.types.ts` must not silently swallow the path.
14
- 'check', 'local',
15
- // `rls preset <table> <preset> --replace` must not swallow a following word.
16
- 'replace',
17
- // `storage cp --recursive ./dir ss:///bucket` must not swallow the source.
18
- 'recursive',
19
- // Migration workflow switches: `db push --dry-run` must not eat a positional.
20
- 'dry-run', 'include-seed', 'include-all', 'schema-only', 'data-only', 'use-copy', 'confirm', 'no-seed', 'linked',
21
- // `postgres-config` and `db publications` switches, never followed by a value.
22
- 'replace-existing-overrides', 'no-restart', 'all-tables',
23
- // Branching, keys and database access: trailing switches that must not
24
- // swallow the next word.
25
- 'with-data', 'append', 'enable-db-ssl-enforcement', 'disable-db-ssl-enforcement',
26
- ]);
27
- // Short flag aliases → canonical (with whether they take a value).
28
- const SHORT = {
29
- y: { name: 'yes', boolean: true },
30
- h: { name: 'help', boolean: true },
31
- v: { name: 'version', boolean: true },
32
- p: { name: 'project', boolean: false },
33
- u: { name: 'url', boolean: false },
34
- e: { name: 'e', boolean: false },
35
- f: { name: 'f', boolean: true },
36
- o: { name: 'output', boolean: false },
37
- r: { name: 'recursive', boolean: true },
38
- };
39
-
40
- // Tokenize argv into positionals + flags. Repeated flags collapse into arrays.
41
- function tokenize(argv) {
42
- const positionals = [];
43
- const flags = {};
44
- const setFlag = (key, value) => {
45
- if (key in flags) {
46
- flags[key] = [].concat(flags[key], value);
47
- } else {
48
- flags[key] = value;
49
- }
50
- };
51
-
52
- for (let i = 0; i < argv.length; i++) {
53
- const tok = argv[i];
54
- if (tok === '--') { positionals.push(...argv.slice(i + 1)); break; }
55
-
56
- if (tok.startsWith('--')) {
57
- const body = tok.slice(2);
58
- const eq = body.indexOf('=');
59
- if (eq >= 0) { setFlag(body.slice(0, eq), body.slice(eq + 1)); continue; }
60
- const next = argv[i + 1];
61
- if (!PURE_BOOLEAN.has(body) && next !== undefined && !next.startsWith('-')) {
62
- setFlag(body, next); i++;
63
- } else {
64
- setFlag(body, true);
65
- }
66
- } else if (tok.startsWith('-') && tok.length > 1 && tok !== '-') {
67
- const spec = SHORT[tok.slice(1)];
68
- if (!spec) { setFlag(tok.slice(1), true); continue; }
69
- const next = argv[i + 1];
70
- if (!spec.boolean && next !== undefined && !next.startsWith('-')) { setFlag(spec.name, next); i++; }
71
- else setFlag(spec.name, true);
72
- } else {
73
- positionals.push(tok);
74
- }
75
- }
76
- return { positionals, flags };
77
- }
78
-
79
- // Fold the Supabase spellings of global flags into the names commands read:
80
- // `--project-ref` is `--project`, and `-o/--output <format>` becomes `format`
81
- // (with `json` kept in step, since commands test it for machine output).
82
- // `--output` stays a file path on commands that declare `outputIsPath`.
83
- function normalizeFlags(flags, command = {}) {
84
- const out = { ...flags };
85
- if (out.project === undefined && out['project-ref'] !== undefined) out.project = out['project-ref'];
86
- if (out.output !== undefined && FORMATS.includes(out.output)) {
87
- out.format = out.output;
88
- delete out.output;
89
- } else if (out.output !== undefined && !command.outputIsPath) {
90
- throw new CliError(`--output must be one of: ${FORMATS.join(', ')}`);
91
- }
92
- out.format = out.format || (out.json ? 'json' : 'pretty');
93
- out.json = out.format === 'json';
94
- return out;
95
- }
96
-
97
- function version() {
98
- try {
99
- return require(path.join(__dirname, '..', 'package.json')).version;
100
- } catch { return '0.0.0'; }
101
- }
102
-
103
- function pad(s, n) { return String(s).padEnd(n); }
104
-
105
- function topLevelHelp() {
106
- ui.print(ui.color.bold('Motherbase CLI') + ` v${version()}`);
107
- ui.print('');
108
- ui.print('Usage: motherbase <command> [subcommand] [args] [flags]');
109
- ui.print('');
110
- const width = Object.keys(registry).reduce((m, k) => Math.max(m, k.length), 0) + 2;
111
- for (const group of groups) {
112
- ui.print(ui.color.bold(group.title));
113
- for (const name of group.names) {
114
- const entry = registry[name];
115
- if (!entry) continue;
116
- ui.print(' ' + ui.color.cyan(pad(name, width)) + ui.color.dim(entry.summary || ''));
117
- }
118
- ui.print('');
119
- }
120
- ui.print('Global flags: -o/--output pretty|json|yaml|env --json --yes/-y --project/-p/--project-ref <id> --url/-u <consoleUrl>');
121
- ui.print(' --token <t> --workdir <dir> --debug -h/--help -v/--version');
122
- ui.print('Run `motherbase <command> -h` for command-specific help.');
123
- }
124
-
125
- function commandHelp(name, entry, trail) {
126
- const full = ['motherbase', ...trail].join(' ');
127
- ui.print(ui.color.bold(full) + (entry.summary ? `, ${entry.summary}` : ''));
128
- ui.print('');
129
- if (entry.usage) { ui.print('Usage: ' + entry.usage); ui.print(''); }
130
- if (entry.subcommands) {
131
- ui.print(ui.color.bold('Subcommands'));
132
- const subs = Object.keys(entry.subcommands);
133
- const width = subs.reduce((m, k) => Math.max(m, k.length), 0) + 2;
134
- for (const sub of subs) {
135
- const s = entry.subcommands[sub];
136
- ui.print(' ' + ui.color.cyan(pad(sub, width)) + ui.color.dim(s.summary || ''));
137
- }
138
- }
139
- }
140
-
141
- async function main(argv) {
142
- const { positionals, flags } = tokenize(argv);
143
-
144
- // Only bare: `migration squash --version <v>` is Supabase's own flag.
145
- if (flags.version && positionals.length === 0) { ui.print(version()); return 0; }
146
-
147
- if (positionals.length === 0) { topLevelHelp(); return 0; }
148
-
149
- const name = positionals[0];
150
- const entry = registry[name];
151
- if (!entry) {
152
- ui.printErr(ui.color.red(`Unknown command: ${name}`));
153
- ui.printErr('Run `motherbase --help` to see available commands.');
154
- return 1;
155
- }
156
-
157
- // Descend into subcommands.
158
- let current = entry;
159
- const trail = [name];
160
- const rest = positionals.slice(1);
161
- while (current.subcommands && rest.length && current.subcommands[rest[0]]) {
162
- current = current.subcommands[rest[0]];
163
- trail.push(rest.shift());
164
- }
165
-
166
- // Help requested, or a group with no runnable action here.
167
- if (flags.help || (!current.run && !current.subcommands)) {
168
- commandHelp(name, current, trail);
169
- return 0;
170
- }
171
- if (!current.run) {
172
- // Group node: either no subcommand given or an unknown one.
173
- if (rest.length) ui.printErr(ui.color.red(`Unknown subcommand: ${trail.join(' ')} ${rest[0]}`));
174
- commandHelp(name, current, trail);
175
- return rest.length ? 1 : 0;
176
- }
177
-
178
- try {
179
- const resolved = normalizeFlags(flags, current);
180
- if (resolved.debug) process.env.MOTHERBASE_DEBUG = '1';
181
- const cwd = resolved.workdir ? path.resolve(String(resolved.workdir)) : process.cwd();
182
- await current.run(createContext(resolved, process.env, cwd), rest, resolved);
183
- return 0;
184
- } catch (err) {
185
- if (err instanceof CliError || err.isCliError) {
186
- ui.printErr(ui.color.red(err.message));
187
- } else {
188
- ui.printErr(ui.color.red(`Error: ${err.message}`));
189
- }
190
- return 1;
191
- }
192
- }
193
-
194
- module.exports = { main, tokenize, normalizeFlags, version };
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { registry, groups } = require('./commands');
5
+ const { createContext, CliError } = require('./context');
6
+ const { FORMATS } = require('./format');
7
+ const ui = require('./ui');
8
+
9
+ // Flags that never consume a following value.
10
+ const PURE_BOOLEAN = new Set([
11
+ 'json', 'yes', 'help', 'version', 'force', 'preview', 'app', 'no-data', 'no-auth', 'f', 'debug',
12
+ // Codegen: `--check` and `--local` never take a value, so a trailing
13
+ // `--check database.types.ts` must not silently swallow the path.
14
+ 'check', 'local',
15
+ // `rls preset <table> <preset> --replace` must not swallow a following word.
16
+ 'replace',
17
+ // `storage cp --recursive ./dir ss:///bucket` must not swallow the source.
18
+ 'recursive',
19
+ // Migration workflow switches: `db push --dry-run` must not eat a positional.
20
+ 'dry-run', 'include-seed', 'include-all', 'schema-only', 'data-only', 'use-copy', 'confirm', 'no-seed', 'linked',
21
+ // `postgres-config` and `db publications` switches, never followed by a value.
22
+ 'replace-existing-overrides', 'no-restart', 'all-tables',
23
+ // Branching, keys and database access: trailing switches that must not
24
+ // swallow the next word.
25
+ 'with-data', 'append', 'enable-db-ssl-enforcement', 'disable-db-ssl-enforcement',
26
+ ]);
27
+ // Short flag aliases → canonical (with whether they take a value).
28
+ const SHORT = {
29
+ y: { name: 'yes', boolean: true },
30
+ h: { name: 'help', boolean: true },
31
+ v: { name: 'version', boolean: true },
32
+ p: { name: 'project', boolean: false },
33
+ u: { name: 'url', boolean: false },
34
+ e: { name: 'e', boolean: false },
35
+ f: { name: 'f', boolean: true },
36
+ o: { name: 'output', boolean: false },
37
+ r: { name: 'recursive', boolean: true },
38
+ };
39
+
40
+ /**
41
+ * Is this token the next flag rather than a value that happens to start with
42
+ * `-`? `--anything` is: long flags are free-form here, so the parser accepts
43
+ * any name. A single-dash token only is when `SHORT` declares it, which is why
44
+ * `-s3cret!` and `-5` are not.
45
+ *
46
+ * PURE_BOOLEAN cannot answer this on its own. It lists the flags that must not
47
+ * swallow a following *positional* (`db push --dry-run` before a path), and a
48
+ * switch with no positional after it was never added to it -- `--superuser`,
49
+ * `--write`, `--no-storage`. Treating every switch missing from that list as a
50
+ * flag that needs a value is what turned `db roles create app --login
51
+ * --superuser` into an error.
52
+ */
53
+ function isFlagToken(token) {
54
+ if (token.startsWith('--')) return token.length > 2;
55
+ return Object.prototype.hasOwnProperty.call(SHORT, token.slice(1));
56
+ }
57
+
58
+ /**
59
+ * A value flag followed by a dash-leading token that is *not* a flag is
60
+ * ambiguous: the token could be the value or a typo. This used to resolve
61
+ * silently in the worst direction -- the flag became a bare `true` and the
62
+ * value was re-parsed as short flags, so `--password -s3cret!` sent no password
63
+ * and set `-s`, `-3`, `-c`... The `--flag=value` form is unambiguous, so say so
64
+ * instead of guessing (PKG-16).
65
+ */
66
+ function dashValue(flag, value) {
67
+ return new CliError(`${flag} needs a value, and "${value}" starts with "-". Write ${flag}=${value} instead.`);
68
+ }
69
+
70
+ // Tokenize argv into positionals + flags. Repeated flags collapse into arrays.
71
+ function tokenize(argv) {
72
+ const positionals = [];
73
+ const flags = {};
74
+ const setFlag = (key, value) => {
75
+ if (key in flags) {
76
+ flags[key] = [].concat(flags[key], value);
77
+ } else {
78
+ flags[key] = value;
79
+ }
80
+ };
81
+
82
+ for (let i = 0; i < argv.length; i++) {
83
+ const tok = argv[i];
84
+ if (tok === '--') { positionals.push(...argv.slice(i + 1)); break; }
85
+
86
+ if (tok.startsWith('--')) {
87
+ const body = tok.slice(2);
88
+ const eq = body.indexOf('=');
89
+ if (eq >= 0) { setFlag(body.slice(0, eq), body.slice(eq + 1)); continue; }
90
+ const next = argv[i + 1];
91
+ if (PURE_BOOLEAN.has(body)) { setFlag(body, true); continue; }
92
+ if (next === undefined || isFlagToken(next)) { setFlag(body, true); continue; }
93
+ if (next.startsWith('-')) throw dashValue(`--${body}`, next);
94
+ setFlag(body, next); i++;
95
+ } else if (tok.startsWith('-') && tok.length > 1 && tok !== '-') {
96
+ const spec = SHORT[tok.slice(1)];
97
+ if (!spec) { setFlag(tok.slice(1), true); continue; }
98
+ const next = argv[i + 1];
99
+ if (spec.boolean || next === undefined || isFlagToken(next)) { setFlag(spec.name, true); continue; }
100
+ if (next.startsWith('-')) throw dashValue(`--${spec.name}`, next);
101
+ setFlag(spec.name, next); i++;
102
+ } else {
103
+ positionals.push(tok);
104
+ }
105
+ }
106
+ return { positionals, flags };
107
+ }
108
+
109
+ // Fold the Supabase spellings of global flags into the names commands read:
110
+ // `--project-ref` is `--project`, and `-o/--output <format>` becomes `format`
111
+ // (with `json` kept in step, since commands test it for machine output).
112
+ // `--output` stays a file path on commands that declare `outputIsPath`.
113
+ function normalizeFlags(flags, command = {}) {
114
+ const out = { ...flags };
115
+ if (out.project === undefined && out['project-ref'] !== undefined) out.project = out['project-ref'];
116
+ if (out.output !== undefined && FORMATS.includes(out.output)) {
117
+ out.format = out.output;
118
+ delete out.output;
119
+ } else if (out.output !== undefined && !command.outputIsPath) {
120
+ throw new CliError(`--output must be one of: ${FORMATS.join(', ')}`);
121
+ }
122
+ out.format = out.format || (out.json ? 'json' : 'pretty');
123
+ out.json = out.format === 'json';
124
+ return out;
125
+ }
126
+
127
+ function version() {
128
+ try {
129
+ return require(path.join(__dirname, '..', 'package.json')).version;
130
+ } catch { return '0.0.0'; }
131
+ }
132
+
133
+ function pad(s, n) { return String(s).padEnd(n); }
134
+
135
+ function topLevelHelp() {
136
+ ui.print(ui.color.bold('Motherbase CLI') + ` v${version()}`);
137
+ ui.print('');
138
+ ui.print('Usage: motherbase <command> [subcommand] [args] [flags]');
139
+ ui.print('');
140
+ const width = Object.keys(registry).reduce((m, k) => Math.max(m, k.length), 0) + 2;
141
+ for (const group of groups) {
142
+ ui.print(ui.color.bold(group.title));
143
+ for (const name of group.names) {
144
+ const entry = registry[name];
145
+ if (!entry) continue;
146
+ ui.print(' ' + ui.color.cyan(pad(name, width)) + ui.color.dim(entry.summary || ''));
147
+ }
148
+ ui.print('');
149
+ }
150
+ ui.print('Global flags: -o/--output pretty|json|yaml|env --json --yes/-y --project/-p/--project-ref <id> --url/-u <consoleUrl>');
151
+ ui.print(' --token <t> --workdir <dir> --debug -h/--help -v/--version');
152
+ ui.print('Run `motherbase <command> -h` for command-specific help.');
153
+ }
154
+
155
+ function commandHelp(name, entry, trail) {
156
+ const full = ['motherbase', ...trail].join(' ');
157
+ ui.print(ui.color.bold(full) + (entry.summary ? `, ${entry.summary}` : ''));
158
+ ui.print('');
159
+ if (entry.usage) { ui.print('Usage: ' + entry.usage); ui.print(''); }
160
+ if (entry.subcommands) {
161
+ ui.print(ui.color.bold('Subcommands'));
162
+ const subs = Object.keys(entry.subcommands);
163
+ const width = subs.reduce((m, k) => Math.max(m, k.length), 0) + 2;
164
+ for (const sub of subs) {
165
+ const s = entry.subcommands[sub];
166
+ ui.print(' ' + ui.color.cyan(pad(sub, width)) + ui.color.dim(s.summary || ''));
167
+ }
168
+ }
169
+ }
170
+
171
+ async function main(argv) {
172
+ const { positionals, flags } = tokenize(argv);
173
+
174
+ // Only bare: `migration squash --version <v>` is Supabase's own flag.
175
+ if (flags.version && positionals.length === 0) { ui.print(version()); return 0; }
176
+
177
+ if (positionals.length === 0) { topLevelHelp(); return 0; }
178
+
179
+ const name = positionals[0];
180
+ const entry = registry[name];
181
+ if (!entry) {
182
+ ui.printErr(ui.color.red(`Unknown command: ${name}`));
183
+ ui.printErr('Run `motherbase --help` to see available commands.');
184
+ return 1;
185
+ }
186
+
187
+ // Descend into subcommands.
188
+ let current = entry;
189
+ const trail = [name];
190
+ const rest = positionals.slice(1);
191
+ while (current.subcommands && rest.length && current.subcommands[rest[0]]) {
192
+ current = current.subcommands[rest[0]];
193
+ trail.push(rest.shift());
194
+ }
195
+
196
+ // Help requested, or a group with no runnable action here.
197
+ if (flags.help || (!current.run && !current.subcommands)) {
198
+ commandHelp(name, current, trail);
199
+ return 0;
200
+ }
201
+ if (!current.run) {
202
+ // Group node: either no subcommand given or an unknown one.
203
+ if (rest.length) ui.printErr(ui.color.red(`Unknown subcommand: ${trail.join(' ')} ${rest[0]}`));
204
+ commandHelp(name, current, trail);
205
+ return rest.length ? 1 : 0;
206
+ }
207
+
208
+ try {
209
+ const resolved = normalizeFlags(flags, current);
210
+ if (resolved.debug) process.env.MOTHERBASE_DEBUG = '1';
211
+ const cwd = resolved.workdir ? path.resolve(String(resolved.workdir)) : process.cwd();
212
+ await current.run(createContext(resolved, process.env, cwd), rest, resolved);
213
+ return 0;
214
+ } catch (err) {
215
+ if (err instanceof CliError || err.isCliError) {
216
+ ui.printErr(ui.color.red(err.message));
217
+ } else {
218
+ ui.printErr(ui.color.red(`Error: ${err.message}`));
219
+ }
220
+ return 1;
221
+ }
222
+ }
223
+
224
+ module.exports = { main, tokenize, normalizeFlags, version };
@@ -49,8 +49,11 @@ function serve({ input, output, handlers, onError }) {
49
49
  };
50
50
 
51
51
  const dispatch = async message => {
52
+ // Valid JSON is not necessarily a request: `null` or `42` would throw on
53
+ // destructuring and, un-awaited, take the process down.
54
+ if (message === null || typeof message !== 'object') return;
52
55
  const { id, method, params } = message;
53
- const handler = handlers[method];
56
+ const handler = Object.hasOwn(handlers, method) ? handlers[method] : undefined;
54
57
  if (!handler) {
55
58
  // Unknown notifications are ignored rather than answered: the spec sends
56
59
  // several the server is not required to know about.