@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.
- package/LICENSE +21 -0
- package/README.md +131 -18
- package/bin/motherbase.js +10 -10
- package/package.json +35 -26
- package/src/api.js +233 -229
- package/src/commands/_util.js +106 -50
- package/src/commands/backups.js +16 -0
- package/src/commands/branches.js +113 -0
- package/src/commands/completion.js +90 -0
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db-catalog.js +326 -0
- package/src/commands/db.js +103 -92
- package/src/commands/domains.js +124 -0
- package/src/commands/functions.js +63 -33
- package/src/commands/gen.js +63 -17
- package/src/commands/import.js +55 -35
- package/src/commands/index.js +85 -64
- package/src/commands/inspect.js +178 -41
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +23 -2
- package/src/commands/login.js +39 -39
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +27 -9
- package/src/commands/migrate.js +207 -0
- package/src/commands/migration.js +108 -33
- package/src/commands/network.js +83 -0
- package/src/commands/orgs.js +40 -0
- package/src/commands/postgres-config.js +113 -0
- package/src/commands/projects.js +130 -125
- package/src/commands/queues.js +133 -0
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -45
- package/src/commands/signing.js +130 -0
- package/src/commands/snippets.js +44 -0
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -113
- package/src/commands/test.js +9 -0
- package/src/commands/unlink.js +15 -15
- package/src/commands/users.js +142 -113
- package/src/commands/vault.js +82 -0
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -0
- package/src/config.js +98 -98
- package/src/context.js +106 -102
- package/src/docs.js +68 -0
- package/src/edge-templates.js +194 -0
- package/src/format.js +92 -0
- package/src/inspect/csv.js +22 -0
- package/src/inspect/queries.js +562 -0
- package/src/main.js +224 -160
- package/src/mcp/platform-tools.js +233 -0
- package/src/mcp/protocol.js +4 -1
- package/src/mcp/storage-tools.js +93 -0
- package/src/mcp/tools.js +318 -6
- package/src/migration/commands.js +375 -0
- package/src/migration/files.js +245 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -0
- package/src/ui.js +111 -111
- package/src/commands/migrations.js +0 -85
package/src/api.js
CHANGED
|
@@ -1,229 +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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const {
|
|
21
|
-
const
|
|
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
|
|
42
|
-
// keeps in its trustedOrigins (baseURL)
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
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
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const url = `${consoleUrl}/api/
|
|
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
|
-
return {
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
function
|
|
149
|
-
const target = ctx.requireProject();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const target = ctx.requireProject();
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
};
|
package/src/commands/_util.js
CHANGED
|
@@ -1,50 +1,106 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Small shared helpers for command modules.
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Small shared helpers for command modules.
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { CliError } = require('../context');
|
|
7
|
+
const { outputFormat, toYaml, toEnv } = require('../format');
|
|
8
|
+
|
|
9
|
+
// Parse `key=value` tokens into an object. Values that look like numbers/bools are
|
|
10
|
+
// coerced unless `raw` is set. Supports dotted keys via `--json`-style callers.
|
|
11
|
+
function parseKeyValues(tokens, { coerce = true } = {}) {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const tok of tokens) {
|
|
14
|
+
const idx = tok.indexOf('=');
|
|
15
|
+
if (idx <= 0) throw new CliError(`expected key=value, got "${tok}"`);
|
|
16
|
+
const key = tok.slice(0, idx).trim();
|
|
17
|
+
let value = tok.slice(idx + 1);
|
|
18
|
+
if (coerce) {
|
|
19
|
+
if (value === 'true') value = true;
|
|
20
|
+
else if (value === 'false') value = false;
|
|
21
|
+
else if (value !== '' && !Number.isNaN(Number(value)) && /^-?\d+(\.\d+)?$/.test(value)) value = Number(value);
|
|
22
|
+
}
|
|
23
|
+
out[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Render a command's result in the selected `-o/--output` format; `pretty` is
|
|
29
|
+
// the command's own human renderer.
|
|
30
|
+
function output(ctx, data, humanFn) {
|
|
31
|
+
const ui = require('../ui');
|
|
32
|
+
switch (outputFormat(ctx.flags)) {
|
|
33
|
+
case 'json': ui.json(data); return;
|
|
34
|
+
case 'yaml': process.stdout.write(toYaml(data)); return;
|
|
35
|
+
case 'env': process.stdout.write(toEnv(data)); return;
|
|
36
|
+
default: humanFn(data, ui);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The same command under a second (Supabase) name: one implementation, with
|
|
41
|
+
// help that shows the name it was reached by.
|
|
42
|
+
function alias(entry, usage, summary = entry.summary) {
|
|
43
|
+
return { ...entry, usage, summary };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Parse a dotenv file: `KEY=value`, `export KEY=value`, `#` comments, and
|
|
47
|
+
// single- or double-quoted values (double quotes expand \n).
|
|
48
|
+
function parseDotenv(text) {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const raw of String(text).split(/\r?\n/)) {
|
|
51
|
+
const line = raw.trim().replace(/^export\s+/, '');
|
|
52
|
+
if (!line || line.startsWith('#')) continue;
|
|
53
|
+
const idx = line.indexOf('=');
|
|
54
|
+
if (idx <= 0) throw new CliError(`invalid env line: "${raw}"`);
|
|
55
|
+
const key = line.slice(0, idx).trim();
|
|
56
|
+
let value = line.slice(idx + 1).trim();
|
|
57
|
+
const quote = value[0];
|
|
58
|
+
if ((quote === '"' || quote === "'") && value.endsWith(quote) && value.length > 1) {
|
|
59
|
+
value = value.slice(1, -1);
|
|
60
|
+
if (quote === '"') value = value.replace(/\\n/g, '\n');
|
|
61
|
+
} else {
|
|
62
|
+
value = value.replace(/\s+#.*$/, '');
|
|
63
|
+
}
|
|
64
|
+
out[key] = value;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requireArg(value, name) {
|
|
70
|
+
if (value === undefined || value === null || value === '') {
|
|
71
|
+
throw new CliError(`missing required argument: <${name}>`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Split "bucket:path" into { bucket, rest }.
|
|
77
|
+
function splitRef(ref) {
|
|
78
|
+
const idx = ref.indexOf(':');
|
|
79
|
+
if (idx < 0) return { bucket: undefined, rest: ref };
|
|
80
|
+
return { bucket: ref.slice(0, idx), rest: ref.slice(idx + 1) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// `/projects/<linked project>` on the Console, for the per-project platform
|
|
84
|
+
// surfaces (branches, domains) that live on the Console rather than the
|
|
85
|
+
// project API. They need a Console, so direct mode cannot reach them.
|
|
86
|
+
function consoleProjectPath(ctx, suffix = '') {
|
|
87
|
+
ctx.requireConsole();
|
|
88
|
+
if (!ctx.projectId) {
|
|
89
|
+
throw new CliError('No project linked. Run `motherbase link` or pass --project-ref <id>.');
|
|
90
|
+
}
|
|
91
|
+
return `/projects/${encodeURIComponent(ctx.projectId)}${suffix}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Join `parts` under `root`, refusing a result that escapes it. Names that come
|
|
95
|
+
// from the server (object keys, function routes) must not write outside the
|
|
96
|
+
// directory the user picked: `../../.bashrc` is a valid object key.
|
|
97
|
+
function resolveInside(root, ...parts) {
|
|
98
|
+
const target = path.resolve(root, ...parts);
|
|
99
|
+
const rel = path.relative(root, target);
|
|
100
|
+
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
101
|
+
throw new CliError(`Refusing to write outside ${root}: ${parts.join('/')}`);
|
|
102
|
+
}
|
|
103
|
+
return target;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { parseKeyValues, output, alias, parseDotenv, requireArg, splitRef, consoleProjectPath, resolveInside };
|