@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/LICENSE +21 -0
- package/README.md +2 -1
- package/bin/.motherbase/config.json +5 -0
- package/package.json +35 -26
- package/src/api.js +256 -241
- package/src/commands/_util.js +14 -1
- package/src/commands/db-catalog.js +326 -326
- package/src/commands/functions.js +304 -297
- package/src/commands/gen.js +7 -1
- package/src/commands/local.js +371 -273
- package/src/commands/login.js +45 -39
- package/src/commands/migrate.js +202 -179
- package/src/commands/migration.js +108 -108
- package/src/commands/projects.js +2 -1
- package/src/commands/storage.js +2 -2
- package/src/commands/users.js +142 -142
- package/src/context.js +6 -2
- package/src/docs.js +68 -0
- package/src/main.js +224 -194
- package/src/mcp/protocol.js +4 -1
- package/src/migration/commands.js +375 -375
- package/src/migration/files.js +258 -245
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 };
|
package/src/mcp/protocol.js
CHANGED
|
@@ -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.
|