@motherbase/cli 0.2.5 → 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 +247 -247
  3. package/bin/motherbase.js +10 -10
  4. package/package.json +35 -26
  5. package/src/api.js +233 -241
  6. package/src/commands/_util.js +106 -93
  7. package/src/commands/backups.js +16 -16
  8. package/src/commands/branches.js +113 -113
  9. package/src/commands/completion.js +90 -90
  10. package/src/commands/config.js +47 -47
  11. package/src/commands/cron.js +65 -65
  12. package/src/commands/db-catalog.js +326 -326
  13. package/src/commands/db.js +103 -103
  14. package/src/commands/domains.js +124 -124
  15. package/src/commands/functions.js +304 -297
  16. package/src/commands/gen.js +172 -172
  17. package/src/commands/import.js +55 -55
  18. package/src/commands/index.js +85 -85
  19. package/src/commands/inspect.js +178 -178
  20. package/src/commands/keys.js +72 -72
  21. package/src/commands/link.js +35 -35
  22. package/src/commands/local.js +294 -273
  23. package/src/commands/login.js +39 -39
  24. package/src/commands/logout.js +16 -16
  25. package/src/commands/mcp.js +61 -61
  26. package/src/commands/migrate.js +207 -179
  27. package/src/commands/migration.js +108 -108
  28. package/src/commands/network.js +83 -83
  29. package/src/commands/orgs.js +40 -40
  30. package/src/commands/postgres-config.js +113 -113
  31. package/src/commands/projects.js +130 -130
  32. package/src/commands/queues.js +133 -133
  33. package/src/commands/realtime.js +35 -35
  34. package/src/commands/rls.js +176 -176
  35. package/src/commands/secrets.js +66 -66
  36. package/src/commands/signing.js +130 -130
  37. package/src/commands/snippets.js +44 -44
  38. package/src/commands/status.js +42 -42
  39. package/src/commands/storage.js +261 -261
  40. package/src/commands/test.js +9 -9
  41. package/src/commands/unlink.js +15 -15
  42. package/src/commands/users.js +142 -142
  43. package/src/commands/vault.js +82 -82
  44. package/src/commands/webhooks.js +62 -62
  45. package/src/commands/whoami.js +20 -20
  46. package/src/commands/wrappers.js +150 -150
  47. package/src/config.js +98 -98
  48. package/src/context.js +106 -102
  49. package/src/docs.js +68 -0
  50. package/src/edge-templates.js +194 -194
  51. package/src/format.js +92 -92
  52. package/src/inspect/csv.js +22 -22
  53. package/src/inspect/queries.js +562 -562
  54. package/src/main.js +224 -194
  55. package/src/mcp/platform-tools.js +233 -233
  56. package/src/mcp/protocol.js +152 -149
  57. package/src/mcp/storage-tools.js +93 -93
  58. package/src/mcp/tools.js +478 -478
  59. package/src/migration/commands.js +375 -375
  60. package/src/migration/files.js +245 -245
  61. package/src/prompt.js +91 -91
  62. package/src/storage/remote.js +62 -62
  63. package/src/ui.js +111 -111
package/src/api.js CHANGED
@@ -1,241 +1,233 @@
1
- 'use strict';
2
-
3
- // The one and only HTTP layer. Every command routes through here so auth headers,
4
- // URL construction and error mapping live in a single place.
5
- //
6
- // URL shapes:
7
- // console request -> ${consoleUrl}/api/platform${path}
8
- // admin (console mode) -> ${consoleUrl}/api/projects/${projectId}/admin${path}
9
- // admin (direct mode) -> ${directUrl}/api/admin${path}
10
- // edge invoke (console)-> ${consoleUrl}/api/projects/${projectId}/edge/${route}
11
- // edge invoke (direct) -> ${directUrl}/api/edge/${route}
12
- //
13
- // Auth: the console session token is sent as `Authorization: Bearer <token>`.
14
- // Success envelopes differ across surfaces; both wrap payload under `data`, so we
15
- // unwrap `body.data` when present. Non-2xx becomes a thrown CliError with the
16
- // status and a short message/body snippet.
17
-
18
- const fs = require('fs');
19
- const path = require('path');
20
- const { Readable } = require('stream');
21
- const { CliError } = require('./context');
22
- const ui = require('./ui');
23
-
24
- function buildQuery(query) {
25
- if (!query) return '';
26
- const params = new URLSearchParams();
27
- for (const [k, v] of Object.entries(query)) {
28
- if (v === undefined || v === null) continue;
29
- params.append(k, String(v));
30
- }
31
- const s = params.toString();
32
- return s ? `?${s}` : '';
33
- }
34
-
35
- function unwrap(body) {
36
- if (body && typeof body === 'object' && 'data' in body) return body.data;
37
- return body;
38
- }
39
-
40
- // Non-browser clients send no Origin header. Better Auth (and reverse proxies
41
- // that inject cookies) then treat the request as CSRF-suspect and reject it with
42
- // "Missing or null Origin". Sending the target's own origin (which the server
43
- // keeps in its trustedOrigins (baseURL)) satisfies that check for every route.
44
- function originFor(url) {
45
- try { return new URL(url).origin; } catch { return undefined; }
46
- }
47
-
48
- function errorMessage(status, body, text) {
49
- const snippet = (body && (body.message || body.error)) || (text ? text.slice(0, 300) : '');
50
- if (status === 401) {
51
- return `Unauthorized (401)${snippet ? `: ${snippet}` : ''}. Run \`motherbase login\` (or check --token).`;
52
- }
53
- if (status === 403) {
54
- return `Forbidden (403)${snippet ? `: ${snippet}` : ''}.`;
55
- }
56
- if (status === 404) {
57
- return `Not found (404)${snippet ? `: ${snippet}` : ''}.`;
58
- }
59
- return `Request failed (${status})${snippet ? `: ${snippet}` : ''}`;
60
- }
61
-
62
- // `--debug` (or MOTHERBASE_DEBUG=1): one stderr line per request. Never the
63
- // headers or body, which carry the token and passwords.
64
- function trace(method, url, res) {
65
- if (!process.env.MOTHERBASE_DEBUG) return;
66
- ui.printErr(ui.color.dim(`[debug] ${method} ${url} → ${res.status}`));
67
- }
68
-
69
- async function parseResponse(res) {
70
- const text = await res.text();
71
- let body;
72
- if (text) {
73
- try { body = JSON.parse(text); } catch { body = undefined; }
74
- }
75
- if (!res.ok) {
76
- throw new CliError(errorMessage(res.status, body, text));
77
- }
78
- return body === undefined ? text : unwrap(body);
79
- }
80
-
81
- async function doFetch(url, { method = 'GET', token, body, headers = {} } = {}) {
82
- const finalHeaders = { ...headers };
83
- if (token) finalHeaders['authorization'] = `Bearer ${token}`;
84
- if (!finalHeaders['origin']) {
85
- const origin = originFor(url);
86
- if (origin) finalHeaders['origin'] = origin;
87
- }
88
- let payload;
89
- if (body !== undefined && body !== null) {
90
- finalHeaders['content-type'] = 'application/json';
91
- payload = JSON.stringify(body);
92
- }
93
- let res;
94
- try {
95
- res = await fetch(url, { method, headers: finalHeaders, body: payload });
96
- } catch (err) {
97
- throw new CliError(`Cannot reach ${url}: ${err.message}`);
98
- }
99
- trace(method, url, res);
100
- return parseResponse(res);
101
- }
102
-
103
- // ── Console (account / multi-project) ────────────────────────────────────────
104
-
105
- function consoleRequest(ctx, method, apiPath, { body, query } = {}) {
106
- const { consoleUrl, token } = ctx.requireConsole();
107
- const url = `${consoleUrl}/api/platform${apiPath}${buildQuery(query)}`;
108
- return doFetch(url, { method, token, body });
109
- }
110
-
111
- // Login is special: no token yet. Uses Better Auth's native sign-in endpoint;
112
- // the session token comes back in the body and/or the `set-auth-token` header
113
- // (bearer plugin). Returns `{ token, user }` so the caller can persist it.
114
- async function loginRequest(consoleUrl, email, password) {
115
- const url = `${consoleUrl}/api/auth/sign-in/email`;
116
- let res;
117
- try {
118
- const headers = { 'content-type': 'application/json' };
119
- const origin = originFor(url);
120
- if (origin) headers['origin'] = origin;
121
- res = await fetch(url, {
122
- method: 'POST',
123
- headers,
124
- body: JSON.stringify({ email, password }),
125
- });
126
- } catch (err) {
127
- throw new CliError(`Cannot reach ${url}: ${err.message}`);
128
- }
129
- trace('POST', url, res);
130
- const text = await res.text();
131
- let body;
132
- if (text) { try { body = JSON.parse(text); } catch { body = undefined; } }
133
- if (!res.ok) {
134
- throw new CliError(errorMessage(res.status, body, text));
135
- }
136
- const token = (body && body.token) || res.headers.get('set-auth-token') || undefined;
137
- return { token, user: body && body.user };
138
- }
139
-
140
- // ── Admin / resource (via proxy or direct) ───────────────────────────────────
141
-
142
- function adminBase(target) {
143
- if (target.mode === 'direct') {
144
- return { base: `${target.baseUrl}/api/admin`, token: target.token };
145
- }
146
- return { base: `${target.consoleUrl}/api/projects/${target.projectId}/admin`, token: target.token };
147
- }
148
-
149
- function adminRequest(ctx, method, apiPath, { body, query } = {}) {
150
- const target = ctx.requireProject();
151
- const { base, token } = adminBase(target);
152
- const url = `${base}${apiPath}${buildQuery(query)}`;
153
- return doFetch(url, { method, token, body });
154
- }
155
-
156
- // The public URL of a project path (`/edge/<route>`, `/rest/v1/<table>`), for
157
- // printing. Mirrors how the proxy addresses a project in each mode.
158
- function projectUrl(ctx, projectPath) {
159
- const target = ctx.requireProject();
160
- return target.mode === 'direct'
161
- ? `${target.baseUrl}/api${projectPath}`
162
- : `${target.consoleUrl}/api/projects/${target.projectId}${projectPath}`;
163
- }
164
-
165
- // Edge invoke goes through the un-authenticated edge route, not /admin.
166
- function edgeInvokeRequest(ctx, route, body) {
167
- const url = projectUrl(ctx, `/edge/${encodeURIComponent(route)}`);
168
- const target = ctx.requireProject();
169
- return doFetch(url, { method: 'POST', token: target.token, body: body || {} });
170
- }
171
-
172
- // ── Multipart upload (native FormData/Blob; zero-dep on Node >=18) ────────────
173
-
174
- async function uploadFile(ctx, apiPath, filePath, fields = {}) {
175
- const target = ctx.requireProject();
176
- const { base, token } = adminBase(target);
177
- const url = `${base}${apiPath}`;
178
- const buf = fs.readFileSync(filePath);
179
- const form = new FormData();
180
- form.append('file', new Blob([buf]), path.basename(filePath));
181
- for (const [k, v] of Object.entries(fields)) {
182
- if (v !== undefined && v !== null) form.append(k, String(v));
183
- }
184
- const uploadHeaders = token ? { authorization: `Bearer ${token}` } : {};
185
- const uploadOrigin = originFor(url);
186
- if (uploadOrigin) uploadHeaders['origin'] = uploadOrigin;
187
- let res;
188
- try {
189
- res = await fetch(url, {
190
- method: 'POST',
191
- headers: uploadHeaders,
192
- body: form,
193
- });
194
- } catch (err) {
195
- throw new CliError(`Cannot reach ${url}: ${err.message}`);
196
- }
197
- trace('POST', url, res);
198
- return parseResponse(res);
199
- }
200
-
201
- // ── Streamed download to a local file ────────────────────────────────────────
202
-
203
- async function downloadFile(ctx, apiPath, destPath, { query } = {}) {
204
- const target = ctx.requireProject();
205
- const { base, token } = adminBase(target);
206
- const url = `${base}${apiPath}${buildQuery(query)}`;
207
- const dlHeaders = token ? { authorization: `Bearer ${token}` } : {};
208
- const dlOrigin = originFor(url);
209
- if (dlOrigin) dlHeaders['origin'] = dlOrigin;
210
- let res;
211
- try {
212
- res = await fetch(url, { headers: dlHeaders });
213
- } catch (err) {
214
- throw new CliError(`Cannot reach ${url}: ${err.message}`);
215
- }
216
- trace('GET', url, res);
217
- if (!res.ok) {
218
- const text = await res.text().catch(() => '');
219
- let body; try { body = JSON.parse(text); } catch { /* noop */ }
220
- throw new CliError(errorMessage(res.status, body, text));
221
- }
222
- await new Promise((resolve, reject) => {
223
- const out = fs.createWriteStream(destPath);
224
- out.on('error', reject);
225
- out.on('finish', resolve);
226
- Readable.fromWeb(res.body).pipe(out);
227
- });
228
- return { path: destPath, contentType: res.headers.get('content-type') || undefined };
229
- }
230
-
231
- module.exports = {
232
- consoleRequest,
233
- loginRequest,
234
- adminRequest,
235
- edgeInvokeRequest,
236
- projectUrl,
237
- uploadFile,
238
- downloadFile,
239
- buildQuery,
240
- unwrap,
241
- };
1
+ 'use strict';
2
+
3
+ // The one and only HTTP layer. Every command routes through here so auth headers,
4
+ // URL construction and error mapping live in a single place.
5
+ //
6
+ // URL shapes:
7
+ // console request -> ${consoleUrl}/api/platform${path}
8
+ // admin (console mode) -> ${consoleUrl}/api/projects/${projectId}/admin${path}
9
+ // admin (direct mode) -> ${directUrl}/api/admin${path}
10
+ //
11
+ // Auth: the console session token is sent as `Authorization: Bearer <token>`.
12
+ // Success envelopes differ across surfaces; both wrap payload under `data`, so we
13
+ // unwrap `body.data` when present. Non-2xx becomes a thrown CliError with the
14
+ // status and a short message/body snippet.
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { Readable } = require('stream');
19
+ const { pipeline } = require('stream/promises');
20
+ const { CliError } = require('./context');
21
+ const ui = require('./ui');
22
+
23
+ function buildQuery(query) {
24
+ if (!query) return '';
25
+ const params = new URLSearchParams();
26
+ for (const [k, v] of Object.entries(query)) {
27
+ if (v === undefined || v === null) continue;
28
+ params.append(k, String(v));
29
+ }
30
+ const s = params.toString();
31
+ return s ? `?${s}` : '';
32
+ }
33
+
34
+ function unwrap(body) {
35
+ if (body && typeof body === 'object' && 'data' in body) return body.data;
36
+ return body;
37
+ }
38
+
39
+ // Non-browser clients send no Origin header. Better Auth (and reverse proxies
40
+ // that inject cookies) then treat the request as CSRF-suspect and reject it with
41
+ // "Missing or null Origin". Sending the target's own origin (which the server
42
+ // keeps in its trustedOrigins (baseURL)) satisfies that check for every route.
43
+ function originFor(url) {
44
+ try { return new URL(url).origin; } catch { return undefined; }
45
+ }
46
+
47
+ function errorMessage(status, body, text) {
48
+ const snippet = (body && (body.message || body.error)) || (text ? text.slice(0, 300) : '');
49
+ if (status === 401) {
50
+ return `Unauthorized (401)${snippet ? `: ${snippet}` : ''}. Run \`motherbase login\` (or check --token).`;
51
+ }
52
+ if (status === 403) {
53
+ return `Forbidden (403)${snippet ? `: ${snippet}` : ''}.`;
54
+ }
55
+ if (status === 404) {
56
+ return `Not found (404)${snippet ? `: ${snippet}` : ''}.`;
57
+ }
58
+ return `Request failed (${status})${snippet ? `: ${snippet}` : ''}`;
59
+ }
60
+
61
+ // `--debug` (or MOTHERBASE_DEBUG=1): one stderr line per request. Never the
62
+ // headers or body, which carry the token and passwords.
63
+ function trace(method, url, res) {
64
+ if (!process.env.MOTHERBASE_DEBUG) return;
65
+ ui.printErr(ui.color.dim(`[debug] ${method} ${url} → ${res.status}`));
66
+ }
67
+
68
+ async function parseResponse(res) {
69
+ const text = await res.text();
70
+ let body;
71
+ if (text) {
72
+ try { body = JSON.parse(text); } catch { body = undefined; }
73
+ }
74
+ if (!res.ok) {
75
+ throw new CliError(errorMessage(res.status, body, text));
76
+ }
77
+ return body === undefined ? text : unwrap(body);
78
+ }
79
+
80
+ async function doFetch(url, { method = 'GET', token, body, headers = {} } = {}) {
81
+ const finalHeaders = { ...headers };
82
+ if (token) finalHeaders['authorization'] = `Bearer ${token}`;
83
+ if (!finalHeaders['origin']) {
84
+ const origin = originFor(url);
85
+ if (origin) finalHeaders['origin'] = origin;
86
+ }
87
+ let payload;
88
+ if (body !== undefined && body !== null) {
89
+ finalHeaders['content-type'] = 'application/json';
90
+ payload = JSON.stringify(body);
91
+ }
92
+ let res;
93
+ try {
94
+ res = await fetch(url, { method, headers: finalHeaders, body: payload });
95
+ } catch (err) {
96
+ throw new CliError(`Cannot reach ${url}: ${err.message}`);
97
+ }
98
+ trace(method, url, res);
99
+ return parseResponse(res);
100
+ }
101
+
102
+ // ── Console (account / multi-project) ────────────────────────────────────────
103
+
104
+ function consoleRequest(ctx, method, apiPath, { body, query } = {}) {
105
+ const { consoleUrl, token } = ctx.requireConsole();
106
+ const url = `${consoleUrl}/api/platform${apiPath}${buildQuery(query)}`;
107
+ return doFetch(url, { method, token, body });
108
+ }
109
+
110
+ // Login is special: no token yet. Uses Better Auth's native sign-in endpoint;
111
+ // the session token comes back in the body and/or the `set-auth-token` header
112
+ // (bearer plugin). Returns `{ token, user }` so the caller can persist it.
113
+ async function loginRequest(consoleUrl, email, password) {
114
+ const url = `${consoleUrl}/api/auth/sign-in/email`;
115
+ let res;
116
+ try {
117
+ const headers = { 'content-type': 'application/json' };
118
+ const origin = originFor(url);
119
+ if (origin) headers['origin'] = origin;
120
+ res = await fetch(url, {
121
+ method: 'POST',
122
+ headers,
123
+ body: JSON.stringify({ email, password }),
124
+ });
125
+ } catch (err) {
126
+ throw new CliError(`Cannot reach ${url}: ${err.message}`);
127
+ }
128
+ trace('POST', url, res);
129
+ const text = await res.text();
130
+ let body;
131
+ if (text) { try { body = JSON.parse(text); } catch { body = undefined; } }
132
+ if (!res.ok) {
133
+ throw new CliError(errorMessage(res.status, body, text));
134
+ }
135
+ const token = (body && body.token) || res.headers.get('set-auth-token') || undefined;
136
+ return { token, user: body && body.user };
137
+ }
138
+
139
+ // ── Admin / resource (via proxy or direct) ───────────────────────────────────
140
+
141
+ function adminBase(target) {
142
+ if (target.mode === 'direct') {
143
+ return { base: `${target.baseUrl}/api/admin`, token: target.token };
144
+ }
145
+ return { base: `${target.consoleUrl}/api/projects/${target.projectId}/admin`, token: target.token };
146
+ }
147
+
148
+ function adminRequest(ctx, method, apiPath, { body, query } = {}) {
149
+ const target = ctx.requireProject();
150
+ const { base, token } = adminBase(target);
151
+ const url = `${base}${apiPath}${buildQuery(query)}`;
152
+ return doFetch(url, { method, token, body });
153
+ }
154
+
155
+ // The public URL of a project path (`/edge/<route>`, `/rest/v1/<table>`), for
156
+ // printing. Mirrors how the proxy addresses a project in each mode.
157
+ function projectUrl(ctx, projectPath) {
158
+ const target = ctx.requireProject();
159
+ return target.mode === 'direct'
160
+ ? `${target.baseUrl}/api${projectPath}`
161
+ : `${target.consoleUrl}/api/projects/${target.projectId}${projectPath}`;
162
+ }
163
+
164
+ // ── Multipart upload (native FormData/Blob; zero-dep on Node >=18) ────────────
165
+
166
+ async function uploadFile(ctx, apiPath, filePath, fields = {}) {
167
+ const target = ctx.requireProject();
168
+ const { base, token } = adminBase(target);
169
+ const url = `${base}${apiPath}`;
170
+ const buf = fs.readFileSync(filePath);
171
+ const form = new FormData();
172
+ form.append('file', new Blob([buf]), path.basename(filePath));
173
+ for (const [k, v] of Object.entries(fields)) {
174
+ if (v !== undefined && v !== null) form.append(k, String(v));
175
+ }
176
+ const uploadHeaders = token ? { authorization: `Bearer ${token}` } : {};
177
+ const uploadOrigin = originFor(url);
178
+ if (uploadOrigin) uploadHeaders['origin'] = uploadOrigin;
179
+ let res;
180
+ try {
181
+ res = await fetch(url, {
182
+ method: 'POST',
183
+ headers: uploadHeaders,
184
+ body: form,
185
+ });
186
+ } catch (err) {
187
+ throw new CliError(`Cannot reach ${url}: ${err.message}`);
188
+ }
189
+ trace('POST', url, res);
190
+ return parseResponse(res);
191
+ }
192
+
193
+ // ── Streamed download to a local file ────────────────────────────────────────
194
+
195
+ async function downloadFile(ctx, apiPath, destPath, { query } = {}) {
196
+ const target = ctx.requireProject();
197
+ const { base, token } = adminBase(target);
198
+ const url = `${base}${apiPath}${buildQuery(query)}`;
199
+ const dlHeaders = token ? { authorization: `Bearer ${token}` } : {};
200
+ const dlOrigin = originFor(url);
201
+ if (dlOrigin) dlHeaders['origin'] = dlOrigin;
202
+ let res;
203
+ try {
204
+ res = await fetch(url, { headers: dlHeaders });
205
+ } catch (err) {
206
+ throw new CliError(`Cannot reach ${url}: ${err.message}`);
207
+ }
208
+ trace('GET', url, res);
209
+ if (!res.ok) {
210
+ const text = await res.text().catch(() => '');
211
+ let body; try { body = JSON.parse(text); } catch { /* noop */ }
212
+ throw new CliError(errorMessage(res.status, body, text));
213
+ }
214
+ try {
215
+ await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(destPath));
216
+ } catch (err) {
217
+ // A dropped connection leaves a truncated file that looks like a download.
218
+ fs.rmSync(destPath, { force: true });
219
+ throw new CliError(`Download failed: ${err.message}`);
220
+ }
221
+ return { path: destPath, contentType: res.headers.get('content-type') || undefined };
222
+ }
223
+
224
+ module.exports = {
225
+ consoleRequest,
226
+ loginRequest,
227
+ adminRequest,
228
+ projectUrl,
229
+ uploadFile,
230
+ downloadFile,
231
+ buildQuery,
232
+ unwrap,
233
+ };