@motherbase/cli 0.2.6 → 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 -21
- package/README.md +248 -247
- package/bin/.motherbase/config.json +5 -0
- package/bin/motherbase.js +10 -10
- package/package.json +1 -1
- package/src/api.js +39 -16
- package/src/commands/_util.js +106 -106
- package/src/commands/backups.js +16 -16
- package/src/commands/branches.js +113 -113
- package/src/commands/completion.js +90 -90
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db.js +103 -103
- package/src/commands/domains.js +124 -124
- package/src/commands/gen.js +178 -172
- package/src/commands/import.js +55 -55
- package/src/commands/index.js +85 -85
- package/src/commands/inspect.js +178 -178
- package/src/commands/keys.js +72 -72
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +100 -23
- package/src/commands/login.js +9 -3
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +61 -61
- package/src/commands/migrate.js +0 -5
- package/src/commands/migration.js +1 -1
- package/src/commands/network.js +83 -83
- package/src/commands/orgs.js +40 -40
- package/src/commands/postgres-config.js +113 -113
- package/src/commands/projects.js +131 -130
- package/src/commands/queues.js +133 -133
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -66
- package/src/commands/signing.js +130 -130
- package/src/commands/snippets.js +44 -44
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -261
- package/src/commands/test.js +9 -9
- package/src/commands/unlink.js +15 -15
- package/src/commands/vault.js +82 -82
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -150
- package/src/config.js +98 -98
- package/src/context.js +106 -106
- package/src/edge-templates.js +194 -194
- package/src/format.js +92 -92
- package/src/inspect/csv.js +22 -22
- package/src/inspect/queries.js +562 -562
- package/src/mcp/platform-tools.js +233 -233
- package/src/mcp/protocol.js +152 -152
- package/src/mcp/storage-tools.js +93 -93
- package/src/mcp/tools.js +478 -478
- package/src/migration/commands.js +2 -2
- package/src/migration/files.js +13 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -62
- package/src/ui.js +111 -111
package/src/api.js
CHANGED
|
@@ -107,35 +107,57 @@ function consoleRequest(ctx, method, apiPath, { body, query } = {}) {
|
|
|
107
107
|
return doFetch(url, { method, token, body });
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// Login is special: no token yet
|
|
111
|
-
// the
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
// Login is special: no token yet, so it talks to Better Auth's native endpoints
|
|
111
|
+
// directly. Returns the raw response next to the parsed body because the token
|
|
112
|
+
// lives in the `set-auth-token` header (bearer plugin) and the 2FA step needs
|
|
113
|
+
// the response cookies.
|
|
114
|
+
async function authPost(url, body, headers = {}) {
|
|
115
|
+
const finalHeaders = { 'content-type': 'application/json', ...headers };
|
|
116
|
+
const origin = originFor(url);
|
|
117
|
+
if (origin) finalHeaders['origin'] = origin;
|
|
115
118
|
let res;
|
|
116
119
|
try {
|
|
117
|
-
|
|
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
|
-
});
|
|
120
|
+
res = await fetch(url, { method: 'POST', headers: finalHeaders, body: JSON.stringify(body) });
|
|
125
121
|
} catch (err) {
|
|
126
122
|
throw new CliError(`Cannot reach ${url}: ${err.message}`);
|
|
127
123
|
}
|
|
128
124
|
trace('POST', url, res);
|
|
129
125
|
const text = await res.text();
|
|
130
|
-
let
|
|
131
|
-
if (text) { try {
|
|
126
|
+
let parsed;
|
|
127
|
+
if (text) { try { parsed = JSON.parse(text); } catch { parsed = undefined; } }
|
|
132
128
|
if (!res.ok) {
|
|
133
|
-
throw new CliError(errorMessage(res.status,
|
|
129
|
+
throw new CliError(errorMessage(res.status, parsed, text));
|
|
134
130
|
}
|
|
131
|
+
return { res, body: parsed };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The `two_factor` cookie (prefixed `__Secure-` on https) scopes the
|
|
135
|
+
// verify-totp call to the pending sign-in.
|
|
136
|
+
function twoFactorCookie(res) {
|
|
137
|
+
const setCookies = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [];
|
|
138
|
+
const pair = setCookies.map(c => c.split(';')[0]).find(c => /two_factor=./.test(c));
|
|
139
|
+
if (!pair) throw new CliError('two-factor sign-in started but the server sent no two_factor cookie');
|
|
140
|
+
return pair;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Returns `{ token, user }`, or `{ twoFactorCookie }` when the account has 2FA
|
|
144
|
+
// enrolled. In that case Better Auth answers `{ twoFactorRedirect: true }` and
|
|
145
|
+
// the `set-auth-token` header still carries the pre-2FA session it has just
|
|
146
|
+
// deleted, so the header must not be trusted (see the console login route).
|
|
147
|
+
async function loginRequest(consoleUrl, email, password) {
|
|
148
|
+
const { res, body } = await authPost(`${consoleUrl}/api/auth/sign-in/email`, { email, password });
|
|
149
|
+
if (body && body.twoFactorRedirect) return { twoFactorCookie: twoFactorCookie(res) };
|
|
135
150
|
const token = (body && body.token) || res.headers.get('set-auth-token') || undefined;
|
|
136
151
|
return { token, user: body && body.user };
|
|
137
152
|
}
|
|
138
153
|
|
|
154
|
+
// Second step of a 2FA sign-in: trade the TOTP code for the real session token.
|
|
155
|
+
async function verifyTotpRequest(consoleUrl, cookie, code) {
|
|
156
|
+
const { res, body } = await authPost(`${consoleUrl}/api/auth/two-factor/verify-totp`, { code }, { cookie });
|
|
157
|
+
const token = res.headers.get('set-auth-token') || (body && body.token) || undefined;
|
|
158
|
+
return { token, user: body && body.user };
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
// ── Admin / resource (via proxy or direct) ───────────────────────────────────
|
|
140
162
|
|
|
141
163
|
function adminBase(target) {
|
|
@@ -224,6 +246,7 @@ async function downloadFile(ctx, apiPath, destPath, { query } = {}) {
|
|
|
224
246
|
module.exports = {
|
|
225
247
|
consoleRequest,
|
|
226
248
|
loginRequest,
|
|
249
|
+
verifyTotpRequest,
|
|
227
250
|
adminRequest,
|
|
228
251
|
projectUrl,
|
|
229
252
|
uploadFile,
|
package/src/commands/_util.js
CHANGED
|
@@ -1,106 +1,106 @@
|
|
|
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 };
|
|
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 };
|
package/src/commands/backups.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Supabase's top-level `backups`. The implementations live under `db`, which
|
|
4
|
-
// keeps its own names for them.
|
|
5
|
-
|
|
6
|
-
const db = require('./db');
|
|
7
|
-
const { alias } = require('./_util');
|
|
8
|
-
|
|
9
|
-
module.exports = {
|
|
10
|
-
summary: 'List and restore database backups',
|
|
11
|
-
usage: 'motherbase backups <list|restore> ...',
|
|
12
|
-
subcommands: {
|
|
13
|
-
list: alias(db.subcommands.backups, 'motherbase backups list', 'List database backups'),
|
|
14
|
-
restore: alias(db.subcommands.restore, 'motherbase backups restore <file> | --key <remoteKey> --confirm'),
|
|
15
|
-
},
|
|
16
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Supabase's top-level `backups`. The implementations live under `db`, which
|
|
4
|
+
// keeps its own names for them.
|
|
5
|
+
|
|
6
|
+
const db = require('./db');
|
|
7
|
+
const { alias } = require('./_util');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
summary: 'List and restore database backups',
|
|
11
|
+
usage: 'motherbase backups <list|restore> ...',
|
|
12
|
+
subcommands: {
|
|
13
|
+
list: alias(db.subcommands.backups, 'motherbase backups list', 'List database backups'),
|
|
14
|
+
restore: alias(db.subcommands.restore, 'motherbase backups restore <file> | --key <remoteKey> --confirm'),
|
|
15
|
+
},
|
|
16
|
+
};
|
package/src/commands/branches.js
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Preview branches (Supabase's `supabase branches`). A branch is a project of
|
|
4
|
-
// its own on the Console, cut from the linked project; these commands address
|
|
5
|
-
// it by its name (e.g. the git branch) or its project id.
|
|
6
|
-
|
|
7
|
-
const api = require('../api');
|
|
8
|
-
const config = require('../config');
|
|
9
|
-
const { output, requireArg, consoleProjectPath } = require('./_util');
|
|
10
|
-
|
|
11
|
-
const COLUMNS = [
|
|
12
|
-
{ key: 'name', label: 'NAME' },
|
|
13
|
-
{ key: 'project', label: 'PROJECT' },
|
|
14
|
-
{ key: 'gitBranch', label: 'GIT BRANCH' },
|
|
15
|
-
{ key: 'status', label: 'STATUS' },
|
|
16
|
-
{ key: 'running', label: 'RUNNING' },
|
|
17
|
-
{ key: 'createdAt', label: 'CREATED' },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const branchPath = (ctx, name, action = '') =>
|
|
21
|
-
consoleProjectPath(ctx, `/branches/${encodeURIComponent(requireArg(name, 'name'))}${action}`);
|
|
22
|
-
|
|
23
|
-
function action(verb, summary, render) {
|
|
24
|
-
return {
|
|
25
|
-
summary,
|
|
26
|
-
usage: `motherbase branches ${verb} <name>`,
|
|
27
|
-
async run(ctx, args) {
|
|
28
|
-
const data = await api.consoleRequest(ctx, 'POST', branchPath(ctx, args[0], `/${verb}`));
|
|
29
|
-
output(ctx, data, (d, ui) => render(d, ui));
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const subcommands = {
|
|
35
|
-
create: {
|
|
36
|
-
summary: 'Create a preview branch of the linked project',
|
|
37
|
-
usage: 'motherbase branches create <name> [--git-branch <ref>] [--with-data]',
|
|
38
|
-
async run(ctx, args, flags) {
|
|
39
|
-
const body = { name: requireArg(args[0], 'name'), withData: Boolean(flags['with-data']) };
|
|
40
|
-
if (flags['git-branch']) body.gitBranch = String(flags['git-branch']);
|
|
41
|
-
const data = await api.consoleRequest(ctx, 'POST', consoleProjectPath(ctx, '/branches'), { body });
|
|
42
|
-
output(ctx, data, (d, ui) => {
|
|
43
|
-
ui.success(`Creating branch ${d.name} as project ${d.project} (${d.domain})`);
|
|
44
|
-
ui.info('This takes a few minutes. Follow it with `motherbase branches get ' + d.name + '`.');
|
|
45
|
-
});
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
list: {
|
|
49
|
-
summary: 'List the branches of the linked project, main first',
|
|
50
|
-
usage: 'motherbase branches list',
|
|
51
|
-
async run(ctx) {
|
|
52
|
-
const rows = await api.consoleRequest(ctx, 'GET', consoleProjectPath(ctx, '/branches'));
|
|
53
|
-
output(ctx, { branches: rows }, (d, ui) => ui.table(d.branches, COLUMNS));
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
get: {
|
|
57
|
-
summary: 'Show one branch, with the migrations it is ahead and behind',
|
|
58
|
-
usage: 'motherbase branches get <name>',
|
|
59
|
-
async run(ctx, args) {
|
|
60
|
-
const data = await api.consoleRequest(ctx, 'GET', branchPath(ctx, args[0]));
|
|
61
|
-
output(ctx, data, (d, ui) => {
|
|
62
|
-
const { migrations, ...rest } = d;
|
|
63
|
-
ui.keyValue(rest);
|
|
64
|
-
if (migrations) ui.keyValue({ ahead: migrations.ahead.join(', ') || '-', behind: migrations.behind.join(', ') || '-' });
|
|
65
|
-
});
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
update: {
|
|
69
|
-
summary: 'Change a branch\'s git branch',
|
|
70
|
-
usage: 'motherbase branches update <name> --git-branch <ref>',
|
|
71
|
-
async run(ctx, args, flags) {
|
|
72
|
-
const body = {};
|
|
73
|
-
if (flags['git-branch'] !== undefined) body.gitBranch = String(flags['git-branch']);
|
|
74
|
-
const data = await api.consoleRequest(ctx, 'PATCH', branchPath(ctx, args[0]), { body });
|
|
75
|
-
output(ctx, data, (d, ui) => ui.success(`Updated branch ${d.name}`));
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
delete: {
|
|
79
|
-
summary: 'Delete a branch and its whole stack',
|
|
80
|
-
usage: 'motherbase branches delete <name>',
|
|
81
|
-
async run(ctx, args) {
|
|
82
|
-
const data = await api.consoleRequest(ctx, 'DELETE', branchPath(ctx, args[0]));
|
|
83
|
-
output(ctx, data, (d, ui) => ui.success(`Deleted branch ${d.name}`));
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
pause: action('pause', 'Stop a branch\'s stack, keeping its data', (d, ui) => ui.success(`Paused ${d.name}`)),
|
|
87
|
-
unpause: action('unpause', 'Start a paused branch again', (d, ui) => ui.success(`Unpaused ${d.name}`)),
|
|
88
|
-
reset: action('reset', 'Recreate a branch from the parent as it is now (its changes are lost)',
|
|
89
|
-
(d, ui) => ui.success(`Resetting ${d.name}; it is being cut again from ${d.parent}`)),
|
|
90
|
-
merge: action('merge', 'Apply the branch\'s new migrations to the parent',
|
|
91
|
-
(d, ui) => ui.success(d.applied.length ? `Merged ${d.applied.length} migration(s): ${d.applied.join(', ')}` : 'Nothing to merge')),
|
|
92
|
-
rebase: action('rebase', 'Apply the parent\'s newer migrations to the branch',
|
|
93
|
-
(d, ui) => ui.success(d.applied.length ? `Rebased ${d.applied.length} migration(s): ${d.applied.join(', ')}` : 'Already up to date')),
|
|
94
|
-
switch: {
|
|
95
|
-
summary: 'Point this directory\'s link at a branch (or back at main)',
|
|
96
|
-
usage: 'motherbase branches switch <name|main>',
|
|
97
|
-
async run(ctx, args) {
|
|
98
|
-
const name = requireArg(args[0], 'name');
|
|
99
|
-
const rows = await api.consoleRequest(ctx, 'GET', consoleProjectPath(ctx, '/branches'));
|
|
100
|
-
const match = rows.find(b => b.name === name || b.project === name || (name === 'main' && b.isDefault));
|
|
101
|
-
if (!match) throw new Error(`No branch ${name}. See \`motherbase branches list\`.`);
|
|
102
|
-
const link = config.readLink(ctx.cwd) || {};
|
|
103
|
-
config.writeLink(ctx.cwd, { ...link, consoleUrl: ctx.consoleUrl, projectId: match.project, projectName: match.project });
|
|
104
|
-
output(ctx, { project: match.project, branch: match.name }, (d, ui) => ui.success(`Linked ${ctx.cwd} → ${d.project} (${d.branch})`));
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
module.exports = {
|
|
110
|
-
summary: 'Manage preview branches of the linked project',
|
|
111
|
-
usage: 'motherbase branches <create|list|get|update|delete|pause|unpause|reset|merge|rebase|switch> ...',
|
|
112
|
-
subcommands,
|
|
113
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Preview branches (Supabase's `supabase branches`). A branch is a project of
|
|
4
|
+
// its own on the Console, cut from the linked project; these commands address
|
|
5
|
+
// it by its name (e.g. the git branch) or its project id.
|
|
6
|
+
|
|
7
|
+
const api = require('../api');
|
|
8
|
+
const config = require('../config');
|
|
9
|
+
const { output, requireArg, consoleProjectPath } = require('./_util');
|
|
10
|
+
|
|
11
|
+
const COLUMNS = [
|
|
12
|
+
{ key: 'name', label: 'NAME' },
|
|
13
|
+
{ key: 'project', label: 'PROJECT' },
|
|
14
|
+
{ key: 'gitBranch', label: 'GIT BRANCH' },
|
|
15
|
+
{ key: 'status', label: 'STATUS' },
|
|
16
|
+
{ key: 'running', label: 'RUNNING' },
|
|
17
|
+
{ key: 'createdAt', label: 'CREATED' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const branchPath = (ctx, name, action = '') =>
|
|
21
|
+
consoleProjectPath(ctx, `/branches/${encodeURIComponent(requireArg(name, 'name'))}${action}`);
|
|
22
|
+
|
|
23
|
+
function action(verb, summary, render) {
|
|
24
|
+
return {
|
|
25
|
+
summary,
|
|
26
|
+
usage: `motherbase branches ${verb} <name>`,
|
|
27
|
+
async run(ctx, args) {
|
|
28
|
+
const data = await api.consoleRequest(ctx, 'POST', branchPath(ctx, args[0], `/${verb}`));
|
|
29
|
+
output(ctx, data, (d, ui) => render(d, ui));
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const subcommands = {
|
|
35
|
+
create: {
|
|
36
|
+
summary: 'Create a preview branch of the linked project',
|
|
37
|
+
usage: 'motherbase branches create <name> [--git-branch <ref>] [--with-data]',
|
|
38
|
+
async run(ctx, args, flags) {
|
|
39
|
+
const body = { name: requireArg(args[0], 'name'), withData: Boolean(flags['with-data']) };
|
|
40
|
+
if (flags['git-branch']) body.gitBranch = String(flags['git-branch']);
|
|
41
|
+
const data = await api.consoleRequest(ctx, 'POST', consoleProjectPath(ctx, '/branches'), { body });
|
|
42
|
+
output(ctx, data, (d, ui) => {
|
|
43
|
+
ui.success(`Creating branch ${d.name} as project ${d.project} (${d.domain})`);
|
|
44
|
+
ui.info('This takes a few minutes. Follow it with `motherbase branches get ' + d.name + '`.');
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
list: {
|
|
49
|
+
summary: 'List the branches of the linked project, main first',
|
|
50
|
+
usage: 'motherbase branches list',
|
|
51
|
+
async run(ctx) {
|
|
52
|
+
const rows = await api.consoleRequest(ctx, 'GET', consoleProjectPath(ctx, '/branches'));
|
|
53
|
+
output(ctx, { branches: rows }, (d, ui) => ui.table(d.branches, COLUMNS));
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
get: {
|
|
57
|
+
summary: 'Show one branch, with the migrations it is ahead and behind',
|
|
58
|
+
usage: 'motherbase branches get <name>',
|
|
59
|
+
async run(ctx, args) {
|
|
60
|
+
const data = await api.consoleRequest(ctx, 'GET', branchPath(ctx, args[0]));
|
|
61
|
+
output(ctx, data, (d, ui) => {
|
|
62
|
+
const { migrations, ...rest } = d;
|
|
63
|
+
ui.keyValue(rest);
|
|
64
|
+
if (migrations) ui.keyValue({ ahead: migrations.ahead.join(', ') || '-', behind: migrations.behind.join(', ') || '-' });
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
update: {
|
|
69
|
+
summary: 'Change a branch\'s git branch',
|
|
70
|
+
usage: 'motherbase branches update <name> --git-branch <ref>',
|
|
71
|
+
async run(ctx, args, flags) {
|
|
72
|
+
const body = {};
|
|
73
|
+
if (flags['git-branch'] !== undefined) body.gitBranch = String(flags['git-branch']);
|
|
74
|
+
const data = await api.consoleRequest(ctx, 'PATCH', branchPath(ctx, args[0]), { body });
|
|
75
|
+
output(ctx, data, (d, ui) => ui.success(`Updated branch ${d.name}`));
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
delete: {
|
|
79
|
+
summary: 'Delete a branch and its whole stack',
|
|
80
|
+
usage: 'motherbase branches delete <name>',
|
|
81
|
+
async run(ctx, args) {
|
|
82
|
+
const data = await api.consoleRequest(ctx, 'DELETE', branchPath(ctx, args[0]));
|
|
83
|
+
output(ctx, data, (d, ui) => ui.success(`Deleted branch ${d.name}`));
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
pause: action('pause', 'Stop a branch\'s stack, keeping its data', (d, ui) => ui.success(`Paused ${d.name}`)),
|
|
87
|
+
unpause: action('unpause', 'Start a paused branch again', (d, ui) => ui.success(`Unpaused ${d.name}`)),
|
|
88
|
+
reset: action('reset', 'Recreate a branch from the parent as it is now (its changes are lost)',
|
|
89
|
+
(d, ui) => ui.success(`Resetting ${d.name}; it is being cut again from ${d.parent}`)),
|
|
90
|
+
merge: action('merge', 'Apply the branch\'s new migrations to the parent',
|
|
91
|
+
(d, ui) => ui.success(d.applied.length ? `Merged ${d.applied.length} migration(s): ${d.applied.join(', ')}` : 'Nothing to merge')),
|
|
92
|
+
rebase: action('rebase', 'Apply the parent\'s newer migrations to the branch',
|
|
93
|
+
(d, ui) => ui.success(d.applied.length ? `Rebased ${d.applied.length} migration(s): ${d.applied.join(', ')}` : 'Already up to date')),
|
|
94
|
+
switch: {
|
|
95
|
+
summary: 'Point this directory\'s link at a branch (or back at main)',
|
|
96
|
+
usage: 'motherbase branches switch <name|main>',
|
|
97
|
+
async run(ctx, args) {
|
|
98
|
+
const name = requireArg(args[0], 'name');
|
|
99
|
+
const rows = await api.consoleRequest(ctx, 'GET', consoleProjectPath(ctx, '/branches'));
|
|
100
|
+
const match = rows.find(b => b.name === name || b.project === name || (name === 'main' && b.isDefault));
|
|
101
|
+
if (!match) throw new Error(`No branch ${name}. See \`motherbase branches list\`.`);
|
|
102
|
+
const link = config.readLink(ctx.cwd) || {};
|
|
103
|
+
config.writeLink(ctx.cwd, { ...link, consoleUrl: ctx.consoleUrl, projectId: match.project, projectName: match.project });
|
|
104
|
+
output(ctx, { project: match.project, branch: match.name }, (d, ui) => ui.success(`Linked ${ctx.cwd} → ${d.project} (${d.branch})`));
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
summary: 'Manage preview branches of the linked project',
|
|
111
|
+
usage: 'motherbase branches <create|list|get|update|delete|pause|unpause|reset|merge|rebase|switch> ...',
|
|
112
|
+
subcommands,
|
|
113
|
+
};
|