@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.
Files changed (59) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +248 -247
  3. package/bin/.motherbase/config.json +5 -0
  4. package/bin/motherbase.js +10 -10
  5. package/package.json +1 -1
  6. package/src/api.js +39 -16
  7. package/src/commands/_util.js +106 -106
  8. package/src/commands/backups.js +16 -16
  9. package/src/commands/branches.js +113 -113
  10. package/src/commands/completion.js +90 -90
  11. package/src/commands/config.js +47 -47
  12. package/src/commands/cron.js +65 -65
  13. package/src/commands/db.js +103 -103
  14. package/src/commands/domains.js +124 -124
  15. package/src/commands/gen.js +178 -172
  16. package/src/commands/import.js +55 -55
  17. package/src/commands/index.js +85 -85
  18. package/src/commands/inspect.js +178 -178
  19. package/src/commands/keys.js +72 -72
  20. package/src/commands/link.js +35 -35
  21. package/src/commands/local.js +100 -23
  22. package/src/commands/login.js +9 -3
  23. package/src/commands/logout.js +16 -16
  24. package/src/commands/mcp.js +61 -61
  25. package/src/commands/migrate.js +0 -5
  26. package/src/commands/migration.js +1 -1
  27. package/src/commands/network.js +83 -83
  28. package/src/commands/orgs.js +40 -40
  29. package/src/commands/postgres-config.js +113 -113
  30. package/src/commands/projects.js +131 -130
  31. package/src/commands/queues.js +133 -133
  32. package/src/commands/realtime.js +35 -35
  33. package/src/commands/rls.js +176 -176
  34. package/src/commands/secrets.js +66 -66
  35. package/src/commands/signing.js +130 -130
  36. package/src/commands/snippets.js +44 -44
  37. package/src/commands/status.js +42 -42
  38. package/src/commands/storage.js +261 -261
  39. package/src/commands/test.js +9 -9
  40. package/src/commands/unlink.js +15 -15
  41. package/src/commands/vault.js +82 -82
  42. package/src/commands/webhooks.js +62 -62
  43. package/src/commands/whoami.js +20 -20
  44. package/src/commands/wrappers.js +150 -150
  45. package/src/config.js +98 -98
  46. package/src/context.js +106 -106
  47. package/src/edge-templates.js +194 -194
  48. package/src/format.js +92 -92
  49. package/src/inspect/csv.js +22 -22
  50. package/src/inspect/queries.js +562 -562
  51. package/src/mcp/platform-tools.js +233 -233
  52. package/src/mcp/protocol.js +152 -152
  53. package/src/mcp/storage-tools.js +93 -93
  54. package/src/mcp/tools.js +478 -478
  55. package/src/migration/commands.js +2 -2
  56. package/src/migration/files.js +13 -0
  57. package/src/prompt.js +91 -91
  58. package/src/storage/remote.js +62 -62
  59. 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. 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`;
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
- 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
- });
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 body;
131
- if (text) { try { body = JSON.parse(text); } catch { body = undefined; } }
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, body, text));
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,
@@ -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 };
@@ -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
+ };
@@ -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
+ };