@motherbase/cli 0.2.4 → 0.2.6

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -18
  3. package/bin/motherbase.js +10 -10
  4. package/package.json +35 -26
  5. package/src/api.js +233 -229
  6. package/src/commands/_util.js +106 -50
  7. package/src/commands/backups.js +16 -0
  8. package/src/commands/branches.js +113 -0
  9. package/src/commands/completion.js +90 -0
  10. package/src/commands/config.js +47 -47
  11. package/src/commands/cron.js +65 -65
  12. package/src/commands/db-catalog.js +326 -0
  13. package/src/commands/db.js +103 -92
  14. package/src/commands/domains.js +124 -0
  15. package/src/commands/functions.js +63 -33
  16. package/src/commands/gen.js +63 -17
  17. package/src/commands/import.js +55 -35
  18. package/src/commands/index.js +85 -64
  19. package/src/commands/inspect.js +178 -41
  20. package/src/commands/link.js +35 -35
  21. package/src/commands/local.js +23 -2
  22. package/src/commands/login.js +39 -39
  23. package/src/commands/logout.js +16 -16
  24. package/src/commands/mcp.js +27 -9
  25. package/src/commands/migrate.js +207 -0
  26. package/src/commands/migration.js +108 -33
  27. package/src/commands/network.js +83 -0
  28. package/src/commands/orgs.js +40 -0
  29. package/src/commands/postgres-config.js +113 -0
  30. package/src/commands/projects.js +130 -125
  31. package/src/commands/queues.js +133 -0
  32. package/src/commands/realtime.js +35 -35
  33. package/src/commands/rls.js +176 -176
  34. package/src/commands/secrets.js +66 -45
  35. package/src/commands/signing.js +130 -0
  36. package/src/commands/snippets.js +44 -0
  37. package/src/commands/status.js +42 -42
  38. package/src/commands/storage.js +261 -113
  39. package/src/commands/test.js +9 -0
  40. package/src/commands/unlink.js +15 -15
  41. package/src/commands/users.js +142 -113
  42. package/src/commands/vault.js +82 -0
  43. package/src/commands/webhooks.js +62 -62
  44. package/src/commands/whoami.js +20 -20
  45. package/src/commands/wrappers.js +150 -0
  46. package/src/config.js +98 -98
  47. package/src/context.js +106 -102
  48. package/src/docs.js +68 -0
  49. package/src/edge-templates.js +194 -0
  50. package/src/format.js +92 -0
  51. package/src/inspect/csv.js +22 -0
  52. package/src/inspect/queries.js +562 -0
  53. package/src/main.js +224 -160
  54. package/src/mcp/platform-tools.js +233 -0
  55. package/src/mcp/protocol.js +4 -1
  56. package/src/mcp/storage-tools.js +93 -0
  57. package/src/mcp/tools.js +318 -6
  58. package/src/migration/commands.js +375 -0
  59. package/src/migration/files.js +245 -0
  60. package/src/prompt.js +91 -91
  61. package/src/storage/remote.js +62 -0
  62. package/src/ui.js +111 -111
  63. package/src/commands/migrations.js +0 -85
package/src/prompt.js CHANGED
@@ -1,91 +1,91 @@
1
- 'use strict';
2
-
3
- // readline-based prompting, including hidden password entry. Built-ins only.
4
-
5
- const readline = require('readline');
6
-
7
- function question(query) {
8
- return new Promise(resolve => {
9
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
- rl.question(query, answer => {
11
- rl.close();
12
- resolve(answer.trim());
13
- });
14
- });
15
- }
16
-
17
- // Hidden input. Reads the TTY in raw mode so nothing echoes, masking each typed
18
- // character with '*'. Handles backspace, Enter, Ctrl+C, Ctrl+D and swallows
19
- // escape sequences (arrow keys etc.) so they never land in the value. When
20
- // stdin is not a TTY (pipes, CI) we fall back to a plain, unmasked line read.
21
- function password(query = 'Password: ') {
22
- const stdin = process.stdin;
23
- const stdout = process.stdout;
24
- if (!stdin.isTTY) return question(query);
25
-
26
- return new Promise(resolve => {
27
- let value = '';
28
- let inEscape = false;
29
- stdout.write(query);
30
- stdin.setRawMode(true);
31
- stdin.resume();
32
- stdin.setEncoding('utf8');
33
-
34
- const done = result => {
35
- stdin.removeListener('data', onData);
36
- stdin.setRawMode(false);
37
- stdin.pause();
38
- stdout.write('\n');
39
- resolve(result);
40
- };
41
-
42
- const onData = chunk => {
43
- for (const ch of chunk) {
44
- if (inEscape) {
45
- // Terminators of a CSI/escape sequence (e.g. arrow keys): stop swallowing.
46
- if (/[a-zA-Z~]/.test(ch)) inEscape = false;
47
- continue;
48
- }
49
- switch (ch) {
50
- case '\r':
51
- case '\n':
52
- case '\u0004': // Ctrl-D
53
- return done(value);
54
- case '\u0003': // Ctrl-C
55
- stdin.setRawMode(false);
56
- stdin.pause();
57
- stdout.write('\n');
58
- process.exit(130);
59
- return;
60
- case '\u007f': // Backspace / Delete
61
- case '\b':
62
- if (value.length > 0) {
63
- value = value.slice(0, -1);
64
- stdout.write('\b \b');
65
- }
66
- break;
67
- case '\u001b': // ESC — start of an escape sequence
68
- inEscape = true;
69
- break;
70
- default:
71
- // Ignore remaining control characters; mask printable ones.
72
- if (ch >= ' ') {
73
- value += ch;
74
- stdout.write('*');
75
- }
76
- }
77
- }
78
- };
79
-
80
- stdin.on('data', onData);
81
- });
82
- }
83
-
84
- async function confirm(query, defaultYes = false) {
85
- const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
86
- const answer = (await question(query + suffix)).toLowerCase();
87
- if (!answer) return defaultYes;
88
- return answer === 'y' || answer === 'yes';
89
- }
90
-
91
- module.exports = { question, password, confirm };
1
+ 'use strict';
2
+
3
+ // readline-based prompting, including hidden password entry. Built-ins only.
4
+
5
+ const readline = require('readline');
6
+
7
+ function question(query) {
8
+ return new Promise(resolve => {
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
+ rl.question(query, answer => {
11
+ rl.close();
12
+ resolve(answer.trim());
13
+ });
14
+ });
15
+ }
16
+
17
+ // Hidden input. Reads the TTY in raw mode so nothing echoes, masking each typed
18
+ // character with '*'. Handles backspace, Enter, Ctrl+C, Ctrl+D and swallows
19
+ // escape sequences (arrow keys etc.) so they never land in the value. When
20
+ // stdin is not a TTY (pipes, CI) we fall back to a plain, unmasked line read.
21
+ function password(query = 'Password: ') {
22
+ const stdin = process.stdin;
23
+ const stdout = process.stdout;
24
+ if (!stdin.isTTY) return question(query);
25
+
26
+ return new Promise(resolve => {
27
+ let value = '';
28
+ let inEscape = false;
29
+ stdout.write(query);
30
+ stdin.setRawMode(true);
31
+ stdin.resume();
32
+ stdin.setEncoding('utf8');
33
+
34
+ const done = result => {
35
+ stdin.removeListener('data', onData);
36
+ stdin.setRawMode(false);
37
+ stdin.pause();
38
+ stdout.write('\n');
39
+ resolve(result);
40
+ };
41
+
42
+ const onData = chunk => {
43
+ for (const ch of chunk) {
44
+ if (inEscape) {
45
+ // Terminators of a CSI/escape sequence (e.g. arrow keys): stop swallowing.
46
+ if (/[a-zA-Z~]/.test(ch)) inEscape = false;
47
+ continue;
48
+ }
49
+ switch (ch) {
50
+ case '\r':
51
+ case '\n':
52
+ case '\u0004': // Ctrl-D
53
+ return done(value);
54
+ case '\u0003': // Ctrl-C
55
+ stdin.setRawMode(false);
56
+ stdin.pause();
57
+ stdout.write('\n');
58
+ process.exit(130);
59
+ return;
60
+ case '\u007f': // Backspace / Delete
61
+ case '\b':
62
+ if (value.length > 0) {
63
+ value = value.slice(0, -1);
64
+ stdout.write('\b \b');
65
+ }
66
+ break;
67
+ case '\u001b': // ESC: start of an escape sequence
68
+ inEscape = true;
69
+ break;
70
+ default:
71
+ // Ignore remaining control characters; mask printable ones.
72
+ if (ch >= ' ') {
73
+ value += ch;
74
+ stdout.write('*');
75
+ }
76
+ }
77
+ }
78
+ };
79
+
80
+ stdin.on('data', onData);
81
+ });
82
+ }
83
+
84
+ async function confirm(query, defaultYes = false) {
85
+ const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
86
+ const answer = (await question(query + suffix)).toLowerCase();
87
+ if (!answer) return defaultYes;
88
+ return answer === 'y' || answer === 'yes';
89
+ }
90
+
91
+ module.exports = { question, password, confirm };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ // Storage paths in the Supabase CLI's form: `ss:///<bucket>/<key>`. Anything
4
+ // else is a local path. Kept free of I/O so the path rules can be tested alone.
5
+
6
+ const path = require('path');
7
+ const { CliError } = require('../context');
8
+
9
+ const SCHEME = 'ss:///';
10
+
11
+ const isRemote = value => typeof value === 'string' && value.startsWith('ss://');
12
+
13
+ /** `ss:///bucket/a/b` → `{ bucket: 'bucket', key: 'a/b' }`; `ss:///` → `{ bucket: '', key: '' }`. */
14
+ function parseRemote(value) {
15
+ if (!value.startsWith(SCHEME)) {
16
+ throw new CliError(`Storage paths look like ${SCHEME}<bucket>/<key>, got "${value}".`);
17
+ }
18
+ const [bucket = '', ...rest] = value.slice(SCHEME.length).split('/');
19
+ return { bucket, key: rest.join('/') };
20
+ }
21
+
22
+ function requireBucket(remote, value) {
23
+ if (!remote.bucket) throw new CliError(`"${value}" names no bucket.`);
24
+ return remote;
25
+ }
26
+
27
+ /** A key as a URL path: every segment encoded, the slashes between them kept. */
28
+ const encodeKey = key => key.split('/').map(encodeURIComponent).join('/');
29
+
30
+ /** `a/b/` and `a/b` both mean the prefix `a/b/`; the empty prefix is the bucket root. */
31
+ const asPrefix = key => (key ? key.replace(/\/*$/, '/') : '');
32
+
33
+ /**
34
+ * One directory level of a listing, as `ls` shows it: the objects directly
35
+ * under `prefix`, and each deeper key collapsed into its first folder (with a
36
+ * trailing slash), deduplicated, in order.
37
+ */
38
+ function childrenOf(keys, prefix) {
39
+ const seen = new Set();
40
+ for (const key of keys) {
41
+ if (!key.startsWith(prefix)) continue;
42
+ const rest = key.slice(prefix.length);
43
+ const slash = rest.indexOf('/');
44
+ seen.add(slash < 0 ? rest : rest.slice(0, slash + 1));
45
+ }
46
+ return [...seen].filter(Boolean);
47
+ }
48
+
49
+ /**
50
+ * Where a single file lands. A destination ending in `/` (or the bucket root)
51
+ * is a folder, so the source's basename is appended, as `cp` does locally.
52
+ */
53
+ function destinationKey(dstKey, sourceName) {
54
+ return !dstKey || dstKey.endsWith('/') ? `${dstKey}${path.basename(sourceName)}` : dstKey;
55
+ }
56
+
57
+ /** The key under `toPrefix` that `key` maps to when moving/copying `fromPrefix`. */
58
+ function rebase(key, fromPrefix, toPrefix) {
59
+ return `${toPrefix}${key.slice(fromPrefix.length)}`;
60
+ }
61
+
62
+ module.exports = { SCHEME, isRemote, parseRemote, requireBucket, encodeKey, asPrefix, childrenOf, destinationKey, rebase };
package/src/ui.js CHANGED
@@ -1,111 +1,111 @@
1
- 'use strict';
2
-
3
- // Minimal, dependency-free presentation helpers: ANSI colour (auto-off when not a
4
- // TTY or NO_COLOR is set), a table printer, a key/value printer, a spinner, and a
5
- // --json passthrough.
6
-
7
- const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
8
-
9
- function wrap(code) {
10
- return s => (useColor ? `[${code}m${s}` : String(s));
11
- }
12
-
13
- const color = {
14
- bold: wrap('1'),
15
- dim: wrap('2'),
16
- red: wrap('31'),
17
- green: wrap('32'),
18
- yellow: wrap('33'),
19
- cyan: wrap('36'),
20
- gray: wrap('90'),
21
- };
22
-
23
- function print(msg = '') {
24
- process.stdout.write(msg + '\n');
25
- }
26
-
27
- function printErr(msg = '') {
28
- process.stderr.write(msg + '\n');
29
- }
30
-
31
- function success(msg) {
32
- print(color.green('✓ ') + msg);
33
- }
34
-
35
- function info(msg) {
36
- print(color.cyan('• ') + msg);
37
- }
38
-
39
- function warn(msg) {
40
- printErr(color.yellow('! ') + msg);
41
- }
42
-
43
- function json(value) {
44
- print(JSON.stringify(value, null, 2));
45
- }
46
-
47
- // Render an array of flat objects as an aligned table using the given columns
48
- // (array of keys or { key, label } specs). Falls back to a friendly note if empty.
49
- function table(rows, columns) {
50
- if (!Array.isArray(rows) || rows.length === 0) {
51
- print(color.dim('(no rows)'));
52
- return;
53
- }
54
- const cols = (columns || Object.keys(rows[0])).map(c =>
55
- typeof c === 'string' ? { key: c, label: c } : c,
56
- );
57
- const widths = cols.map(c => c.label.length);
58
- const cells = rows.map(row =>
59
- cols.map((c, i) => {
60
- const v = row[c.key];
61
- const s = v === undefined || v === null ? '' : String(v);
62
- if (s.length > widths[i]) widths[i] = s.length;
63
- return s;
64
- }),
65
- );
66
- const header = cols.map((c, i) => color.bold(c.label.padEnd(widths[i]))).join(' ');
67
- print(header);
68
- print(color.dim(cols.map((_, i) => '-'.repeat(widths[i])).join(' ')));
69
- for (const rowCells of cells) {
70
- print(rowCells.map((s, i) => s.padEnd(widths[i])).join(' '));
71
- }
72
- }
73
-
74
- // Print a { key: value } object as aligned "key : value" lines.
75
- function keyValue(obj) {
76
- if (!obj || typeof obj !== 'object') {
77
- print(String(obj));
78
- return;
79
- }
80
- const keys = Object.keys(obj);
81
- const w = keys.reduce((m, k) => Math.max(m, k.length), 0);
82
- for (const k of keys) {
83
- let v = obj[k];
84
- if (v && typeof v === 'object') v = JSON.stringify(v);
85
- print(`${color.gray(k.padEnd(w))} ${v === undefined || v === null ? '' : v}`);
86
- }
87
- }
88
-
89
- // A tiny spinner. No-op (single status line) when stderr is not a TTY.
90
- function spinner(text) {
91
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
92
- if (!process.stderr.isTTY) {
93
- printErr(color.dim(text + ' ...'));
94
- return { stop() {}, succeed() {}, fail() {} };
95
- }
96
- let i = 0;
97
- const timer = setInterval(() => {
98
- process.stderr.write(`\r${color.cyan(frames[i++ % frames.length])} ${text}`);
99
- }, 80);
100
- const clear = () => {
101
- clearInterval(timer);
102
- process.stderr.write('\r');
103
- };
104
- return {
105
- stop: clear,
106
- succeed(msg) { clear(); if (msg) printErr(color.green('✓ ') + msg); },
107
- fail(msg) { clear(); if (msg) printErr(color.red('✗ ') + msg); },
108
- };
109
- }
110
-
111
- module.exports = { color, print, printErr, success, info, warn, json, table, keyValue, spinner };
1
+ 'use strict';
2
+
3
+ // Minimal, dependency-free presentation helpers: ANSI colour (auto-off when not a
4
+ // TTY or NO_COLOR is set), a table printer, a key/value printer, a spinner, and a
5
+ // --json passthrough.
6
+
7
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
8
+
9
+ function wrap(code) {
10
+ return s => (useColor ? `[${code}m${s}` : String(s));
11
+ }
12
+
13
+ const color = {
14
+ bold: wrap('1'),
15
+ dim: wrap('2'),
16
+ red: wrap('31'),
17
+ green: wrap('32'),
18
+ yellow: wrap('33'),
19
+ cyan: wrap('36'),
20
+ gray: wrap('90'),
21
+ };
22
+
23
+ function print(msg = '') {
24
+ process.stdout.write(msg + '\n');
25
+ }
26
+
27
+ function printErr(msg = '') {
28
+ process.stderr.write(msg + '\n');
29
+ }
30
+
31
+ function success(msg) {
32
+ print(color.green('✓ ') + msg);
33
+ }
34
+
35
+ function info(msg) {
36
+ print(color.cyan('• ') + msg);
37
+ }
38
+
39
+ function warn(msg) {
40
+ printErr(color.yellow('! ') + msg);
41
+ }
42
+
43
+ function json(value) {
44
+ print(JSON.stringify(value, null, 2));
45
+ }
46
+
47
+ // Render an array of flat objects as an aligned table using the given columns
48
+ // (array of keys or { key, label } specs). Falls back to a friendly note if empty.
49
+ function table(rows, columns) {
50
+ if (!Array.isArray(rows) || rows.length === 0) {
51
+ print(color.dim('(no rows)'));
52
+ return;
53
+ }
54
+ const cols = (columns || Object.keys(rows[0])).map(c =>
55
+ typeof c === 'string' ? { key: c, label: c } : c,
56
+ );
57
+ const widths = cols.map(c => c.label.length);
58
+ const cells = rows.map(row =>
59
+ cols.map((c, i) => {
60
+ const v = row[c.key];
61
+ const s = v === undefined || v === null ? '' : String(v);
62
+ if (s.length > widths[i]) widths[i] = s.length;
63
+ return s;
64
+ }),
65
+ );
66
+ const header = cols.map((c, i) => color.bold(c.label.padEnd(widths[i]))).join(' ');
67
+ print(header);
68
+ print(color.dim(cols.map((_, i) => '-'.repeat(widths[i])).join(' ')));
69
+ for (const rowCells of cells) {
70
+ print(rowCells.map((s, i) => s.padEnd(widths[i])).join(' '));
71
+ }
72
+ }
73
+
74
+ // Print a { key: value } object as aligned "key : value" lines.
75
+ function keyValue(obj) {
76
+ if (!obj || typeof obj !== 'object') {
77
+ print(String(obj));
78
+ return;
79
+ }
80
+ const keys = Object.keys(obj);
81
+ const w = keys.reduce((m, k) => Math.max(m, k.length), 0);
82
+ for (const k of keys) {
83
+ let v = obj[k];
84
+ if (v && typeof v === 'object') v = JSON.stringify(v);
85
+ print(`${color.gray(k.padEnd(w))} ${v === undefined || v === null ? '' : v}`);
86
+ }
87
+ }
88
+
89
+ // A tiny spinner. No-op (single status line) when stderr is not a TTY.
90
+ function spinner(text) {
91
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
92
+ if (!process.stderr.isTTY) {
93
+ printErr(color.dim(text + ' ...'));
94
+ return { stop() {}, succeed() {}, fail() {} };
95
+ }
96
+ let i = 0;
97
+ const timer = setInterval(() => {
98
+ process.stderr.write(`\r${color.cyan(frames[i++ % frames.length])} ${text}`);
99
+ }, 80);
100
+ const clear = () => {
101
+ clearInterval(timer);
102
+ process.stderr.write('\r');
103
+ };
104
+ return {
105
+ stop: clear,
106
+ succeed(msg) { clear(); if (msg) printErr(color.green('✓ ') + msg); },
107
+ fail(msg) { clear(); if (msg) printErr(color.red('✗ ') + msg); },
108
+ };
109
+ }
110
+
111
+ module.exports = { color, print, printErr, success, info, warn, json, table, keyValue, spinner };
@@ -1,85 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const api = require('../api');
6
- const { output, requireArg } = require('./_util');
7
- const { CliError } = require('../context');
8
-
9
- function readSql(cwd, file) {
10
- if (!file) return undefined;
11
- return fs.readFileSync(path.resolve(cwd, file), 'utf8');
12
- }
13
-
14
- const subcommands = {
15
- list: {
16
- summary: 'List schema migrations',
17
- usage: 'motherbase migrations list',
18
- async run(ctx) {
19
- const data = await api.adminRequest(ctx, 'GET', '/migrations');
20
- const rows = (data && data.migrations) || [];
21
- output(ctx, data, (_d, ui) =>
22
- ui.table(rows, [
23
- { key: 'id', label: 'ID' },
24
- { key: 'name', label: 'NAME' },
25
- { key: 'status', label: 'STATUS' },
26
- { key: 'appliedAt', label: 'APPLIED' },
27
- ]),
28
- );
29
- },
30
- },
31
- diff: {
32
- summary: 'Show a diff between live schema and migrations',
33
- usage: 'motherbase migrations diff',
34
- async run(ctx) {
35
- const data = await api.adminRequest(ctx, 'GET', '/migrations/diff');
36
- output(ctx, data, (d, ui) => ui.json(d));
37
- },
38
- },
39
- get: {
40
- summary: 'Show a single migration',
41
- usage: 'motherbase migrations get <id>',
42
- async run(ctx, args) {
43
- const id = requireArg(args[0], 'id');
44
- const data = await api.adminRequest(ctx, 'GET', `/migrations/${id}`);
45
- output(ctx, data, (d, ui) => ui.keyValue((d && d.migration) || d || {}));
46
- },
47
- },
48
- create: {
49
- summary: 'Create a migration from up/down SQL files',
50
- usage: 'motherbase migrations create --name <name> --up <file.sql> [--down <file.sql>]',
51
- async run(ctx, args, flags) {
52
- const name = flags.name || args[0];
53
- if (!name) throw new CliError('--name is required');
54
- const upSql = readSql(ctx.cwd, flags.up);
55
- const downSql = readSql(ctx.cwd, flags.down);
56
- if (!upSql) throw new CliError('--up <file.sql> is required');
57
- const data = await api.adminRequest(ctx, 'POST', '/migrations', { body: { name, upSql, downSql } });
58
- output(ctx, data, (d, ui) => ui.success(`Created migration ${name}`));
59
- },
60
- },
61
- apply: {
62
- summary: 'Apply a migration',
63
- usage: 'motherbase migrations apply <id>',
64
- async run(ctx, args) {
65
- const id = requireArg(args[0], 'id');
66
- const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/apply`);
67
- output(ctx, data, (d, ui) => ui.success(`Applied ${id}`));
68
- },
69
- },
70
- rollback: {
71
- summary: 'Roll back a migration',
72
- usage: 'motherbase migrations rollback <id>',
73
- async run(ctx, args) {
74
- const id = requireArg(args[0], 'id');
75
- const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/rollback`);
76
- output(ctx, data, (d, ui) => ui.success(`Rolled back ${id}`));
77
- },
78
- },
79
- };
80
-
81
- module.exports = {
82
- summary: 'Schema migrations (list/diff/create/apply/rollback)',
83
- usage: 'motherbase migrations <list|diff|get|create|apply|rollback> ...',
84
- subcommands,
85
- };