@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
@@ -1,42 +1,42 @@
1
- 'use strict';
2
-
3
- const api = require('../api');
4
- const { output } = require('./_util');
5
-
6
- module.exports = {
7
- summary: 'Show connection status: console, link, and a project health probe',
8
- usage: 'motherbase status',
9
- async run(ctx) {
10
- const data = {
11
- console: ctx.consoleUrl || null,
12
- loggedIn: Boolean(ctx.token),
13
- mode: ctx.mode,
14
- project: ctx.projectId || null,
15
- reachable: null,
16
- api: null,
17
- };
18
-
19
- if (ctx.projectId || ctx.mode === 'direct') {
20
- try {
21
- const info = await api.adminRequest(ctx, 'GET', '/connect-info');
22
- data.reachable = true;
23
- data.api = info && info.apiUrl;
24
- } catch (err) {
25
- data.reachable = false;
26
- data.error = err.message;
27
- }
28
- }
29
-
30
- output(ctx, data, (d, ui) => {
31
- ui.keyValue({
32
- Console: d.console || '(none)',
33
- 'Logged in': d.loggedIn ? 'yes' : 'no',
34
- Mode: d.mode,
35
- Project: d.project || '(not linked)',
36
- Reachable: d.reachable === null ? 'n/a' : d.reachable ? 'yes' : 'no',
37
- 'API URL': d.api || '',
38
- });
39
- if (d.error) ui.warn(d.error);
40
- });
41
- },
42
- };
1
+ 'use strict';
2
+
3
+ const api = require('../api');
4
+ const { output } = require('./_util');
5
+
6
+ module.exports = {
7
+ summary: 'Show connection status: console, link, and a project health probe',
8
+ usage: 'motherbase status',
9
+ async run(ctx) {
10
+ const data = {
11
+ console: ctx.consoleUrl || null,
12
+ loggedIn: Boolean(ctx.token),
13
+ mode: ctx.mode,
14
+ project: ctx.projectId || null,
15
+ reachable: null,
16
+ api: null,
17
+ };
18
+
19
+ if (ctx.projectId || ctx.mode === 'direct') {
20
+ try {
21
+ const info = await api.adminRequest(ctx, 'GET', '/connect-info');
22
+ data.reachable = true;
23
+ data.api = info && info.apiUrl;
24
+ } catch (err) {
25
+ data.reachable = false;
26
+ data.error = err.message;
27
+ }
28
+ }
29
+
30
+ output(ctx, data, (d, ui) => {
31
+ ui.keyValue({
32
+ Console: d.console || '(none)',
33
+ 'Logged in': d.loggedIn ? 'yes' : 'no',
34
+ Mode: d.mode,
35
+ Project: d.project || '(not linked)',
36
+ Reachable: d.reachable === null ? 'n/a' : d.reachable ? 'yes' : 'no',
37
+ 'API URL': d.api || '',
38
+ });
39
+ if (d.error) ui.warn(d.error);
40
+ });
41
+ },
42
+ };
@@ -1,113 +1,261 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const api = require('../api');
6
- const { output, requireArg, splitRef } = require('./_util');
7
-
8
- const buckets = {
9
- summary: 'Manage storage buckets',
10
- usage: 'motherbase storage buckets <list|create|delete> ...',
11
- subcommands: {
12
- list: {
13
- summary: 'List buckets',
14
- usage: 'motherbase storage buckets list',
15
- async run(ctx) {
16
- const data = await api.adminRequest(ctx, 'GET', '/storage/buckets');
17
- const rows = Array.isArray(data) ? data : (data && data.buckets) || [];
18
- output(ctx, data, (_d, ui) =>
19
- ui.table(rows, [
20
- { key: 'slug', label: 'SLUG' },
21
- { key: 'name', label: 'NAME' },
22
- { key: 'visibility', label: 'VISIBILITY' },
23
- ]),
24
- );
25
- },
26
- },
27
- create: {
28
- summary: 'Create a bucket',
29
- usage: 'motherbase storage buckets create <slug> [--name n] [--visibility public|private]',
30
- async run(ctx, args, flags) {
31
- const slug = requireArg(args[0], 'slug');
32
- const body = { slug, name: flags.name || slug };
33
- if (flags.visibility) body.visibility = flags.visibility;
34
- const data = await api.adminRequest(ctx, 'POST', '/storage/buckets', { body });
35
- output(ctx, data, (d, ui) => ui.success(`Created bucket ${slug}`));
36
- },
37
- },
38
- delete: {
39
- summary: 'Delete a bucket',
40
- usage: 'motherbase storage buckets delete <slug> [--force]',
41
- async run(ctx, args, flags) {
42
- const slug = requireArg(args[0], 'slug');
43
- const query = flags.force ? { force: 'true' } : undefined;
44
- const data = await api.adminRequest(ctx, 'DELETE', `/storage/buckets/${slug}`, { query });
45
- output(ctx, data, (d, ui) => ui.success(`Deleted bucket ${slug}`));
46
- },
47
- },
48
- },
49
- };
50
-
51
- const subcommands = {
52
- buckets,
53
- ls: {
54
- summary: 'List files (optionally within a bucket)',
55
- usage: 'motherbase storage ls [bucket]',
56
- async run(ctx, args) {
57
- const bucket = args[0];
58
- const apiPath = bucket ? `/storage/buckets/${bucket}/files` : '/storage/files';
59
- const data = await api.adminRequest(ctx, 'GET', apiPath);
60
- const rows = (data && (data.files || data.items)) || (Array.isArray(data) ? data : []);
61
- output(ctx, data, (_d, ui) =>
62
- ui.table(rows, [
63
- { key: 'id', label: 'ID' },
64
- { key: 'filename', label: 'FILENAME' },
65
- { key: 'size', label: 'SIZE' },
66
- { key: 'visibility', label: 'VISIBILITY' },
67
- ]),
68
- );
69
- },
70
- },
71
- cp: {
72
- summary: 'Upload (local -> bucket) or download (fileId -> local)',
73
- usage: 'motherbase storage cp <src> <dst> e.g. cp ./a.png mybucket: | cp <fileId> ./a.png',
74
- async run(ctx, args, flags) {
75
- const src = requireArg(args[0], 'src');
76
- const dst = requireArg(args[1], 'dst');
77
- const srcLocal = fs.existsSync(path.resolve(ctx.cwd, src));
78
-
79
- if (srcLocal) {
80
- // Upload: dst is "bucket" or "bucket:path" (path is advisory; server keys by id).
81
- const { bucket } = splitRef(dst);
82
- const fields = {};
83
- if (bucket) fields.bucket = bucket;
84
- if (flags.visibility) fields.visibility = flags.visibility;
85
- const data = await api.uploadFile(ctx, '/storage/files', path.resolve(ctx.cwd, src), fields);
86
- output(ctx, data, (d, ui) => ui.success(`Uploaded ${src}${d && d.id ? ` → ${d.id}` : ''}`));
87
- } else {
88
- // Download: src is a fileId (optionally "bucket:id"), dst is a local path.
89
- const { rest } = splitRef(src);
90
- const fileId = rest || src;
91
- const destPath = path.resolve(ctx.cwd, dst);
92
- const res = await api.downloadFile(ctx, `/storage/files/${fileId}/raw`, destPath);
93
- if (ctx.flags.json) require('../ui').json(res);
94
- else require('../ui').success(`Downloaded ${fileId} → ${dst}`);
95
- }
96
- },
97
- },
98
- rm: {
99
- summary: 'Delete a file by id',
100
- usage: 'motherbase storage rm <id>',
101
- async run(ctx, args) {
102
- const id = requireArg(args[0], 'id');
103
- const data = await api.adminRequest(ctx, 'DELETE', `/storage/files/${id}`);
104
- output(ctx, data, (d, ui) => ui.success(`Deleted file ${id}`));
105
- },
106
- },
107
- };
108
-
109
- module.exports = {
110
- summary: 'Storage buckets and files',
111
- usage: 'motherbase storage <buckets|ls|cp|rm> ...',
112
- subcommands,
113
- };
1
+ 'use strict';
2
+
3
+ // Storage in the Supabase CLI's shape: objects are addressed `ss:///bucket/key`
4
+ // and `ls|cp|mv|rm` take `-r` for a whole prefix. The pre-Supabase forms still
5
+ // work: `cp ./a.png bucket:` uploads, `cp <fileId> ./a.png` downloads and
6
+ // `rm <fileId>` deletes by id.
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const api = require('../api');
11
+ const { output, requireArg, splitRef, resolveInside } = require('./_util');
12
+ const { CliError } = require('../context');
13
+ const remote = require('../storage/remote');
14
+
15
+ const PAGE = 1000;
16
+
17
+ const objectPath = (bucket, key) => `/storage/buckets/${encodeURIComponent(bucket)}/objects/${remote.encodeKey(key)}`;
18
+
19
+ /** Every key under a prefix, paged through the server's 1000-row limit. */
20
+ async function listKeys(ctx, bucket, prefix) {
21
+ const keys = [];
22
+ for (let offset = 0; ; offset += PAGE) {
23
+ const data = await api.adminRequest(ctx, 'GET', `/storage/buckets/${encodeURIComponent(bucket)}/objects`, {
24
+ query: { prefix: prefix || undefined, limit: PAGE, offset },
25
+ });
26
+ const page = (data && data.objects) || [];
27
+ keys.push(...page.map(o => o.name));
28
+ if (page.length < PAGE) return keys;
29
+ }
30
+ }
31
+
32
+ /** Files under a local directory, as paths relative to it with `/` separators. */
33
+ function walk(dir, base = dir) {
34
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
35
+ const full = path.join(dir, entry.name);
36
+ if (entry.isDirectory()) return walk(full, base);
37
+ return entry.isFile() ? [path.relative(base, full).split(path.sep).join('/')] : [];
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Run one request per object, in order. The first failure stops the batch and
43
+ * says how far it got, so a partial copy or delete is never reported as done.
44
+ */
45
+ async function each(items, label, fn) {
46
+ const done = [];
47
+ for (const item of items) {
48
+ try {
49
+ await fn(item);
50
+ } catch (err) {
51
+ throw new CliError(`${label} failed on ${item} after ${done.length} of ${items.length}: ${err.message}`);
52
+ }
53
+ done.push(item);
54
+ }
55
+ return done;
56
+ }
57
+
58
+ /** The keys an operation on `key` touches: the one object, or everything under it with -r. */
59
+ async function sourceKeys(ctx, { bucket, key }, recursive) {
60
+ if (!recursive) {
61
+ if (!key) throw new CliError('Name an object, or pass -r to act on a whole prefix.');
62
+ return [key];
63
+ }
64
+ const keys = await listKeys(ctx, bucket, remote.asPrefix(key));
65
+ if (!keys.length) throw new CliError(`No objects under ss:///${bucket}/${remote.asPrefix(key)}`);
66
+ return keys;
67
+ }
68
+
69
+ /** Remote-to-remote copy or move: same bucket, since both routes are bucket-scoped. */
70
+ async function transfer(ctx, action, srcArg, dstArg, recursive) {
71
+ const src = remote.requireBucket(remote.parseRemote(srcArg), srcArg);
72
+ const dst = remote.requireBucket(remote.parseRemote(dstArg), dstArg);
73
+ if (src.bucket !== dst.bucket) {
74
+ throw new CliError(`${action} works within one bucket: ${src.bucket} and ${dst.bucket} differ.`);
75
+ }
76
+ const keys = await sourceKeys(ctx, src, recursive);
77
+ const target = key => (recursive
78
+ ? remote.rebase(key, remote.asPrefix(src.key), remote.asPrefix(dst.key))
79
+ : remote.destinationKey(dst.key, key));
80
+ // cp overwrites as it does locally; mv refuses to, as Supabase's does.
81
+ const upsert = action === 'copy';
82
+ return each(keys, action, key =>
83
+ api.adminRequest(ctx, 'POST', `/storage/buckets/${encodeURIComponent(src.bucket)}/${action}`, {
84
+ body: { from: key, to: target(key), upsert },
85
+ }),
86
+ );
87
+ }
88
+
89
+ async function upload(ctx, srcArg, dstArg, recursive) {
90
+ const dst = remote.requireBucket(remote.parseRemote(dstArg), dstArg);
91
+ const local = path.resolve(ctx.cwd, srcArg);
92
+ const upsert = 'true';
93
+ if (fs.statSync(local).isDirectory()) {
94
+ if (!recursive) throw new CliError(`${srcArg} is a directory: pass -r to upload it.`);
95
+ const prefix = remote.asPrefix(dst.key);
96
+ return each(walk(local), 'upload', rel =>
97
+ api.uploadFile(ctx, '/storage/files', path.join(local, rel), { bucket: dst.bucket, path: prefix + rel, upsert }),
98
+ );
99
+ }
100
+ const key = remote.destinationKey(dst.key, local);
101
+ return each([key], 'upload', () => api.uploadFile(ctx, '/storage/files', local, { bucket: dst.bucket, path: key, upsert }));
102
+ }
103
+
104
+ async function download(ctx, srcArg, dstArg, recursive) {
105
+ const src = remote.requireBucket(remote.parseRemote(srcArg), srcArg);
106
+ const local = path.resolve(ctx.cwd, dstArg);
107
+ if (recursive) {
108
+ const prefix = remote.asPrefix(src.key);
109
+ const keys = await sourceKeys(ctx, src, true);
110
+ return each(keys, 'download', key => {
111
+ const target = resolveInside(local, ...key.slice(prefix.length).split('/'));
112
+ fs.mkdirSync(path.dirname(target), { recursive: true });
113
+ return api.downloadFile(ctx, objectPath(src.bucket, key), target);
114
+ });
115
+ }
116
+ const [key] = await sourceKeys(ctx, src, false);
117
+ const intoDir = dstArg.endsWith('/') || dstArg.endsWith(path.sep) || (fs.existsSync(local) && fs.statSync(local).isDirectory());
118
+ const target = intoDir ? path.join(local, path.basename(key)) : local;
119
+ fs.mkdirSync(path.dirname(target), { recursive: true });
120
+ return each([key], 'download', () => api.downloadFile(ctx, objectPath(src.bucket, key), target));
121
+ }
122
+
123
+ // The id-addressed forms that predate `ss://` paths.
124
+ async function legacyCopy(ctx, src, dst, flags) {
125
+ if (fs.existsSync(path.resolve(ctx.cwd, src))) {
126
+ const { bucket } = splitRef(dst);
127
+ const fields = { bucket, visibility: flags.visibility };
128
+ const data = await api.uploadFile(ctx, '/storage/files', path.resolve(ctx.cwd, src), fields);
129
+ return output(ctx, data, (d, ui) => ui.success(`Uploaded ${src}${d && d.id ? ` → ${d.id}` : ''}`));
130
+ }
131
+ const { rest } = splitRef(src);
132
+ const fileId = rest || src;
133
+ const res = await api.downloadFile(ctx, `/storage/files/${fileId}/raw`, path.resolve(ctx.cwd, dst));
134
+ return output(ctx, res, (d, ui) => ui.success(`Downloaded ${fileId} → ${dst}`));
135
+ }
136
+
137
+ const reportDone = (verb, count) => (_d, ui) => ui.success(`${verb} ${count} object${count === 1 ? '' : 's'}`);
138
+
139
+ const buckets = {
140
+ summary: 'Manage storage buckets',
141
+ usage: 'motherbase storage buckets <list|create|delete> ...',
142
+ subcommands: {
143
+ list: {
144
+ summary: 'List buckets',
145
+ usage: 'motherbase storage buckets list',
146
+ async run(ctx) {
147
+ const data = await api.adminRequest(ctx, 'GET', '/storage/buckets');
148
+ const rows = Array.isArray(data) ? data : (data && data.buckets) || [];
149
+ output(ctx, data, (_d, ui) =>
150
+ ui.table(rows, [
151
+ { key: 'slug', label: 'SLUG' },
152
+ { key: 'name', label: 'NAME' },
153
+ { key: 'visibility', label: 'VISIBILITY' },
154
+ ]),
155
+ );
156
+ },
157
+ },
158
+ create: {
159
+ summary: 'Create a bucket',
160
+ usage:
161
+ 'motherbase storage buckets create <slug> [--name n] [--visibility public|private]'
162
+ + ' [--file-size-limit 20MB] [--allowed-mime-types image/*,application/pdf]',
163
+ async run(ctx, args, flags) {
164
+ const slug = requireArg(args[0], 'slug');
165
+ const body = { slug, name: flags.name || slug };
166
+ if (flags.visibility) body.visibility = flags.visibility;
167
+ // Passed through as typed: the server reads "20MB" as well as a byte count.
168
+ if (flags['file-size-limit']) body.fileSizeLimit = flags['file-size-limit'];
169
+ if (flags['allowed-mime-types']) body.allowedMimeTypes = String(flags['allowed-mime-types']).split(',');
170
+ const data = await api.adminRequest(ctx, 'POST', '/storage/buckets', { body });
171
+ output(ctx, data, (d, ui) => ui.success(`Created bucket ${slug}`));
172
+ },
173
+ },
174
+ delete: {
175
+ summary: 'Delete a bucket',
176
+ usage: 'motherbase storage buckets delete <slug> [--force]',
177
+ async run(ctx, args, flags) {
178
+ const slug = requireArg(args[0], 'slug');
179
+ const query = flags.force ? { force: 'true' } : undefined;
180
+ const data = await api.adminRequest(ctx, 'DELETE', `/storage/buckets/${slug}`, { query });
181
+ output(ctx, data, (d, ui) => ui.success(`Deleted bucket ${slug}`));
182
+ },
183
+ },
184
+ },
185
+ };
186
+
187
+ const subcommands = {
188
+ buckets,
189
+ ls: {
190
+ summary: 'List buckets, or the objects under a path',
191
+ usage: 'motherbase storage ls [ss:///bucket/prefix] [-r]',
192
+ async run(ctx, args, flags) {
193
+ const arg = args[0] || remote.SCHEME;
194
+ // A bare bucket name is the pre-`ss://` form of the same request.
195
+ const { bucket, key } = remote.parseRemote(remote.isRemote(arg) ? arg : `${remote.SCHEME}${arg}`);
196
+ if (!bucket) {
197
+ const data = await api.adminRequest(ctx, 'GET', '/storage/buckets');
198
+ const slugs = (Array.isArray(data) ? data : []).map(b => `${b.slug}/`);
199
+ return output(ctx, { entries: slugs }, (d, ui) => d.entries.forEach(e => ui.print(e)));
200
+ }
201
+ const prefix = remote.asPrefix(key);
202
+ const keys = await listKeys(ctx, bucket, prefix);
203
+ const entries = flags.recursive ? keys : remote.childrenOf(keys, prefix);
204
+ output(ctx, { bucket, prefix, entries }, (d, ui) => d.entries.forEach(e => ui.print(e)));
205
+ },
206
+ },
207
+ cp: {
208
+ summary: 'Copy between local paths and ss:///bucket/key (upload, download or in-bucket copy)',
209
+ usage: 'motherbase storage cp <src> <dst> [-r] e.g. cp ./a.png ss:///media/ | cp -r ss:///media/img ./img',
210
+ async run(ctx, args, flags) {
211
+ const src = requireArg(args[0], 'src');
212
+ const dst = requireArg(args[1], 'dst');
213
+ const recursive = Boolean(flags.recursive);
214
+ let done;
215
+ if (remote.isRemote(src) && remote.isRemote(dst)) done = await transfer(ctx, 'copy', src, dst, recursive);
216
+ else if (remote.isRemote(dst)) done = await upload(ctx, src, dst, recursive);
217
+ else if (remote.isRemote(src)) done = await download(ctx, src, dst, recursive);
218
+ else return legacyCopy(ctx, src, dst, flags);
219
+ output(ctx, { copied: done }, reportDone('Copied', done.length));
220
+ },
221
+ },
222
+ mv: {
223
+ summary: 'Move objects within a bucket',
224
+ usage: 'motherbase storage mv <ss:///bucket/src> <ss:///bucket/dst> [-r]',
225
+ async run(ctx, args, flags) {
226
+ const src = requireArg(args[0], 'src');
227
+ const dst = requireArg(args[1], 'dst');
228
+ if (!remote.isRemote(src) || !remote.isRemote(dst)) {
229
+ throw new CliError('mv moves objects: both paths must be ss:///bucket/key.');
230
+ }
231
+ const done = await transfer(ctx, 'move', src, dst, Boolean(flags.recursive));
232
+ output(ctx, { moved: done }, reportDone('Moved', done.length));
233
+ },
234
+ },
235
+ rm: {
236
+ summary: 'Delete objects by path (or a file by id)',
237
+ usage: 'motherbase storage rm <ss:///bucket/key | fileId>... [-r]',
238
+ async run(ctx, args, flags) {
239
+ requireArg(args[0], 'path');
240
+ const removed = [];
241
+ for (const arg of args) {
242
+ if (!remote.isRemote(arg)) {
243
+ await api.adminRequest(ctx, 'DELETE', `/storage/files/${encodeURIComponent(arg)}`);
244
+ removed.push(arg);
245
+ continue;
246
+ }
247
+ const target = remote.requireBucket(remote.parseRemote(arg), arg);
248
+ const keys = await sourceKeys(ctx, target, Boolean(flags.recursive));
249
+ const done = await each(keys, 'rm', key => api.adminRequest(ctx, 'DELETE', objectPath(target.bucket, key)));
250
+ removed.push(...done.map(key => `${target.bucket}/${key}`));
251
+ }
252
+ output(ctx, { removed }, reportDone('Deleted', removed.length));
253
+ },
254
+ },
255
+ };
256
+
257
+ module.exports = {
258
+ summary: 'Storage buckets and objects (ss:///bucket/key paths)',
259
+ usage: 'motherbase storage <buckets|ls|cp|mv|rm> ...',
260
+ subcommands,
261
+ };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ // Supabase's `test db`: pgTAP files from motherbase/tests, run on the linked project.
4
+
5
+ module.exports = {
6
+ summary: 'Run database tests (pgTAP)',
7
+ usage: 'motherbase test db [file.sql ...]',
8
+ subcommands: require('../migration/commands').testSubcommands,
9
+ };
@@ -1,15 +1,15 @@
1
- 'use strict';
2
-
3
- const config = require('../config');
4
- const ui = require('../ui');
5
- const { CliError } = require('../context');
6
-
7
- module.exports = {
8
- summary: 'Remove the project link from the current directory',
9
- usage: 'motherbase unlink',
10
- async run(ctx) {
11
- const removed = config.removeLink(ctx.cwd);
12
- if (!removed) throw new CliError('No project link found in this directory.');
13
- ui.success('Unlinked project.');
14
- },
15
- };
1
+ 'use strict';
2
+
3
+ const config = require('../config');
4
+ const ui = require('../ui');
5
+ const { CliError } = require('../context');
6
+
7
+ module.exports = {
8
+ summary: 'Remove the project link from the current directory',
9
+ usage: 'motherbase unlink',
10
+ async run(ctx) {
11
+ const removed = config.removeLink(ctx.cwd);
12
+ if (!removed) throw new CliError('No project link found in this directory.');
13
+ ui.success('Unlinked project.');
14
+ },
15
+ };