@motherbase/cli 0.2.4 → 0.2.5
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/README.md +247 -134
- package/package.json +2 -2
- package/src/api.js +14 -2
- package/src/commands/_util.js +49 -6
- package/src/commands/backups.js +16 -0
- package/src/commands/branches.js +113 -0
- package/src/commands/completion.js +90 -0
- package/src/commands/db-catalog.js +326 -0
- package/src/commands/db.js +16 -5
- package/src/commands/domains.js +124 -0
- package/src/commands/functions.js +297 -274
- package/src/commands/gen.js +172 -126
- package/src/commands/import.js +23 -3
- package/src/commands/index.js +27 -6
- package/src/commands/inspect.js +142 -5
- package/src/commands/keys.js +72 -72
- package/src/commands/local.js +273 -273
- package/src/commands/mcp.js +61 -43
- package/src/commands/migrate.js +179 -0
- package/src/commands/migration.js +81 -6
- 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 +8 -3
- package/src/commands/queues.js +133 -0
- package/src/commands/secrets.js +29 -8
- package/src/commands/signing.js +130 -0
- package/src/commands/snippets.js +44 -0
- package/src/commands/storage.js +192 -44
- package/src/commands/test.js +9 -0
- package/src/commands/users.js +30 -1
- package/src/commands/vault.js +82 -0
- package/src/commands/wrappers.js +150 -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 +41 -7
- package/src/mcp/platform-tools.js +233 -0
- package/src/mcp/protocol.js +149 -149
- package/src/mcp/storage-tools.js +93 -0
- package/src/mcp/tools.js +478 -166
- package/src/migration/commands.js +375 -0
- package/src/migration/files.js +245 -0
- package/src/prompt.js +1 -1
- package/src/storage/remote.js +62 -0
- package/src/commands/migrations.js +0 -85
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `motherbase migrate codemod <dir>`: port a Supabase codebase's imports, and
|
|
4
|
+
// report what this SDK does not implement.
|
|
5
|
+
//
|
|
6
|
+
// The rewrite is trivial and the report is the point. `@motherbase/client` is
|
|
7
|
+
// supabase-js shaped, so most files need one import line changed; what breaks a
|
|
8
|
+
// port is the handful of calls that do not exist here, and finding those at
|
|
9
|
+
// build time beats finding them when a user hits that screen.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately a text pass, not an AST pass. A codemod that needs a TypeScript
|
|
12
|
+
// parser needs typescript as a CLI dependency, and the patterns below are
|
|
13
|
+
// import specifiers and method names, which do not need one. The cost is that a
|
|
14
|
+
// match inside a comment or a string is still reported: over-reporting in a
|
|
15
|
+
// list a human reads is the acceptable direction to be wrong in.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { output } = require('./_util');
|
|
20
|
+
const { CliError } = require('../context');
|
|
21
|
+
|
|
22
|
+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
|
|
23
|
+
const SKIP_DIRECTORIES = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.turbo']);
|
|
24
|
+
|
|
25
|
+
/** Import specifiers that map one to one. */
|
|
26
|
+
const IMPORT_REWRITES = [
|
|
27
|
+
{ from: '@supabase/supabase-js', to: '@motherbase/client' },
|
|
28
|
+
{ from: '@supabase/ssr', to: '@motherbase/ssr' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Calls this SDK does not implement, and what to do instead.
|
|
33
|
+
*
|
|
34
|
+
* Kept short and kept true: every entry here is a row marked `Not supported` in
|
|
35
|
+
* packages/client/COMPATIBILITY.md. An entry that becomes supported and is left
|
|
36
|
+
* behind sends people to rewrite working code, so this list is part of the same
|
|
37
|
+
* change as implementing the feature.
|
|
38
|
+
*/
|
|
39
|
+
const UNSUPPORTED = [
|
|
40
|
+
{
|
|
41
|
+
pattern: /\.schema\(\s*['"`](?!public['"`])/g,
|
|
42
|
+
what: 'schema() on a schema other than public',
|
|
43
|
+
advice: 'The REST API exposes public only. Move the tables, or expose them through a view or an RPC.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
pattern: /exchangeCodeForSession\s*\(/g,
|
|
47
|
+
what: 'auth.exchangeCodeForSession()',
|
|
48
|
+
advice: 'This install runs the implicit flow. Read the session client side with auth.getSessionFromUrl().',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /signInAnonymously\s*\(/g,
|
|
52
|
+
what: 'auth.signInAnonymously()',
|
|
53
|
+
advice: 'Not implemented. Create a real user, or gate the feature behind sign-in.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pattern: /transform\s*:\s*\{/g,
|
|
57
|
+
what: 'storage image transform options',
|
|
58
|
+
advice: 'No image pipeline yet. Store the sizes you need at upload time.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
pattern: /\.delete\(\s*\)\s*(?![\s\S]{0,200}?\.(eq|neq|in|gt|gte|lt|lte|like|ilike|is|filter|match|or|not)\()/g,
|
|
62
|
+
what: 'delete() with no filter',
|
|
63
|
+
advice: 'Refused with 400 on purpose, so a missing filter cannot empty a table. Add a filter.',
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
function walk(dir, files = []) {
|
|
68
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
69
|
+
if (entry.name.startsWith('.') && entry.name !== '.') continue;
|
|
70
|
+
const full = path.join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
73
|
+
walk(full, files);
|
|
74
|
+
} else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
75
|
+
files.push(full);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Line number of an offset, 1-based, for a report a human can act on. */
|
|
82
|
+
function lineAt(text, index) {
|
|
83
|
+
let line = 1;
|
|
84
|
+
for (let i = 0; i < index && i < text.length; i++) if (text[i] === '\n') line++;
|
|
85
|
+
return line;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rewriteImports(text) {
|
|
89
|
+
let out = text;
|
|
90
|
+
let changed = 0;
|
|
91
|
+
for (const { from, to } of IMPORT_REWRITES) {
|
|
92
|
+
// Only inside a quoted specifier, so a mention in prose is left alone.
|
|
93
|
+
const pattern = new RegExp(`(['"\`])${from.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\1`, 'g');
|
|
94
|
+
out = out.replace(pattern, match => {
|
|
95
|
+
changed++;
|
|
96
|
+
return match.replace(from, to);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return { text: out, changed };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scanUnsupported(text, file) {
|
|
103
|
+
const findings = [];
|
|
104
|
+
for (const rule of UNSUPPORTED) {
|
|
105
|
+
rule.pattern.lastIndex = 0;
|
|
106
|
+
let match;
|
|
107
|
+
while ((match = rule.pattern.exec(text)) !== null) {
|
|
108
|
+
findings.push({ file, line: lineAt(text, match.index), what: rule.what, advice: rule.advice });
|
|
109
|
+
if (match.index === rule.pattern.lastIndex) rule.pattern.lastIndex++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return findings;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const codemod = {
|
|
116
|
+
summary: 'Rewrite Supabase imports and report what this SDK does not support',
|
|
117
|
+
usage: 'motherbase migrate codemod <dir> [--write]',
|
|
118
|
+
async run(ctx, args, flags) {
|
|
119
|
+
const target = path.resolve(ctx.cwd, args[0] || '.');
|
|
120
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
|
121
|
+
throw new CliError(`${target} is not a directory.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const files = walk(target);
|
|
125
|
+
const rewritten = [];
|
|
126
|
+
const findings = [];
|
|
127
|
+
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
130
|
+
const { text, changed } = rewriteImports(original);
|
|
131
|
+
if (changed) {
|
|
132
|
+
rewritten.push({ file: path.relative(ctx.cwd, file), imports: changed });
|
|
133
|
+
// Scan the rewritten text so a line number matches what the file will
|
|
134
|
+
// hold after --write, not what it held before.
|
|
135
|
+
if (flags.write) fs.writeFileSync(file, text, 'utf8');
|
|
136
|
+
}
|
|
137
|
+
findings.push(...scanUnsupported(text, path.relative(ctx.cwd, file)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const result = {
|
|
141
|
+
root: target,
|
|
142
|
+
scanned: files.length,
|
|
143
|
+
rewritten,
|
|
144
|
+
written: Boolean(flags.write),
|
|
145
|
+
findings,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
output(ctx, result, (data, ui) => {
|
|
149
|
+
const verb = data.written ? 'Rewrote' : 'Would rewrite';
|
|
150
|
+
ui.info(`Scanned ${data.scanned} files under ${data.root}`);
|
|
151
|
+
if (data.rewritten.length) {
|
|
152
|
+
ui.info(`${verb} imports in ${data.rewritten.length} file(s):`);
|
|
153
|
+
for (const entry of data.rewritten) ui.info(` ${entry.file} (${entry.imports})`);
|
|
154
|
+
} else {
|
|
155
|
+
ui.info('No Supabase imports found.');
|
|
156
|
+
}
|
|
157
|
+
if (!data.written && data.rewritten.length) ui.info('Re-run with --write to apply.');
|
|
158
|
+
|
|
159
|
+
if (!data.findings.length) {
|
|
160
|
+
ui.success('No unsupported calls found.');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
ui.info('');
|
|
164
|
+
ui.info(`${data.findings.length} call(s) this SDK does not support:`);
|
|
165
|
+
for (const finding of data.findings) {
|
|
166
|
+
ui.info(` ${finding.file}:${finding.line} ${finding.what}`);
|
|
167
|
+
ui.info(` ${finding.advice}`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
summary: 'Port an application from Supabase',
|
|
175
|
+
usage: 'motherbase migrate codemod <dir> [--write]',
|
|
176
|
+
subcommands: { codemod },
|
|
177
|
+
// Exported for the unit test: the scanner is the part worth pinning.
|
|
178
|
+
_internals: { rewriteImports, scanUnsupported },
|
|
179
|
+
};
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// `migration
|
|
4
|
-
// (list/apply/rollback)
|
|
3
|
+
// `migration`: Supabase's name for the group. The server-side lifecycle
|
|
4
|
+
// (list/diff/get/create/apply/rollback) and the local `new` scaffold live
|
|
5
|
+
// together here; `migrations` is registered as an alias of this entry.
|
|
5
6
|
|
|
6
7
|
const fs = require('fs');
|
|
7
8
|
const path = require('path');
|
|
9
|
+
const api = require('../api');
|
|
8
10
|
const ui = require('../ui');
|
|
9
|
-
const { requireArg } = require('./_util');
|
|
11
|
+
const { output, requireArg } = require('./_util');
|
|
10
12
|
const { CliError } = require('../context');
|
|
11
13
|
|
|
14
|
+
function readSql(cwd, file) {
|
|
15
|
+
if (!file) return undefined;
|
|
16
|
+
return fs.readFileSync(path.resolve(cwd, file), 'utf8');
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
const subcommands = {
|
|
13
20
|
new: {
|
|
14
21
|
summary: 'Scaffold a local migration .sql file',
|
|
@@ -20,14 +27,82 @@ const subcommands = {
|
|
|
20
27
|
fs.mkdirSync(dir, { recursive: true });
|
|
21
28
|
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '');
|
|
22
29
|
const file = path.join(dir, `${stamp}_${name}.sql`);
|
|
23
|
-
fs.writeFileSync(file, `-- Migration: ${name}\n
|
|
30
|
+
fs.writeFileSync(file, `-- Migration: ${name}\n\n\n-- @down\n-- Optional: SQL that undoes the above, for \`migration down\`.\n`);
|
|
24
31
|
ui.success(`Created ${path.relative(ctx.cwd, file)}`);
|
|
25
32
|
},
|
|
26
33
|
},
|
|
34
|
+
list: {
|
|
35
|
+
summary: 'List schema migrations',
|
|
36
|
+
usage: 'motherbase migration list',
|
|
37
|
+
async run(ctx) {
|
|
38
|
+
const data = await api.adminRequest(ctx, 'GET', '/migrations');
|
|
39
|
+
const rows = (data && data.migrations) || [];
|
|
40
|
+
output(ctx, data, (_d, ui) =>
|
|
41
|
+
ui.table(rows, [
|
|
42
|
+
{ key: 'id', label: 'ID' },
|
|
43
|
+
{ key: 'name', label: 'NAME' },
|
|
44
|
+
{ key: 'status', label: 'STATUS' },
|
|
45
|
+
{ key: 'appliedAt', label: 'APPLIED' },
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
diff: {
|
|
51
|
+
summary: 'Show a diff between live schema and migrations',
|
|
52
|
+
usage: 'motherbase migration diff',
|
|
53
|
+
async run(ctx) {
|
|
54
|
+
const data = await api.adminRequest(ctx, 'GET', '/migrations/diff');
|
|
55
|
+
output(ctx, data, (d, ui) => ui.json(d));
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
get: {
|
|
59
|
+
summary: 'Show a single migration',
|
|
60
|
+
usage: 'motherbase migration get <id>',
|
|
61
|
+
async run(ctx, args) {
|
|
62
|
+
const id = requireArg(args[0], 'id');
|
|
63
|
+
const data = await api.adminRequest(ctx, 'GET', `/migrations/${id}`);
|
|
64
|
+
output(ctx, data, (d, ui) => ui.keyValue((d && d.migration) || d || {}));
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
create: {
|
|
68
|
+
summary: 'Create a migration from up/down SQL files',
|
|
69
|
+
usage: 'motherbase migration create --name <name> --up <file.sql> [--down <file.sql>]',
|
|
70
|
+
async run(ctx, args, flags) {
|
|
71
|
+
const name = flags.name || args[0];
|
|
72
|
+
if (!name) throw new CliError('--name is required');
|
|
73
|
+
const upSql = readSql(ctx.cwd, flags.up);
|
|
74
|
+
const downSql = readSql(ctx.cwd, flags.down);
|
|
75
|
+
if (!upSql) throw new CliError('--up <file.sql> is required');
|
|
76
|
+
const data = await api.adminRequest(ctx, 'POST', '/migrations', { body: { name, upSql, downSql } });
|
|
77
|
+
output(ctx, data, (d, ui) => ui.success(`Created migration ${name}`));
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
apply: {
|
|
81
|
+
summary: 'Apply a migration',
|
|
82
|
+
usage: 'motherbase migration apply <id>',
|
|
83
|
+
async run(ctx, args) {
|
|
84
|
+
const id = requireArg(args[0], 'id');
|
|
85
|
+
const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/apply`);
|
|
86
|
+
output(ctx, data, (d, ui) => ui.success(`Applied ${id}`));
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
rollback: {
|
|
90
|
+
summary: 'Roll back a migration',
|
|
91
|
+
usage: 'motherbase migration rollback <id>',
|
|
92
|
+
async run(ctx, args) {
|
|
93
|
+
const id = requireArg(args[0], 'id');
|
|
94
|
+
const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/rollback`);
|
|
95
|
+
output(ctx, data, (d, ui) => ui.success(`Rolled back ${id}`));
|
|
96
|
+
},
|
|
97
|
+
},
|
|
27
98
|
};
|
|
28
99
|
|
|
100
|
+
// The local-files workflow (list/up/down/repair/squash/fetch) lives with its
|
|
101
|
+
// helpers in ../migration.
|
|
102
|
+
Object.assign(subcommands, require('../migration/commands').migrationSubcommands);
|
|
103
|
+
|
|
29
104
|
module.exports = {
|
|
30
|
-
summary: '
|
|
31
|
-
usage: 'motherbase migration new
|
|
105
|
+
summary: 'Schema migrations: local files and the remote history',
|
|
106
|
+
usage: 'motherbase migration <new|list|up|down|repair|squash|fetch|diff|get|create|apply|rollback> ...',
|
|
32
107
|
subcommands,
|
|
33
108
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Who may open a direct Postgres connection (Supabase's `network-restrictions`
|
|
4
|
+
// and `ssl-enforcement`). Both are project admin routes, so they work through
|
|
5
|
+
// the Console proxy and in direct mode alike.
|
|
6
|
+
|
|
7
|
+
const api = require('../api');
|
|
8
|
+
const { output } = require('./_util');
|
|
9
|
+
const { CliError } = require('../context');
|
|
10
|
+
|
|
11
|
+
function renderRestrictions(d, ui) {
|
|
12
|
+
ui.keyValue({
|
|
13
|
+
'IPv4 allowed': d.dbAllowedCidrs.join(', ') || '(none)',
|
|
14
|
+
'IPv6 allowed': d.dbAllowedCidrsV6.join(', ') || '(none)',
|
|
15
|
+
status: d.status,
|
|
16
|
+
exposure: d.exposure,
|
|
17
|
+
});
|
|
18
|
+
if (d.status !== 'applied') ui.warn('Stored only: this API does not manage its database access (not a Console stack).');
|
|
19
|
+
if (d.exposure !== 'public') ui.info('The database port is not public, so only the stack and SSH tunnels reach it today.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const networkRestrictions = {
|
|
23
|
+
summary: 'IP ranges allowed to connect to the database directly',
|
|
24
|
+
usage: 'motherbase network-restrictions <get|update> ...',
|
|
25
|
+
subcommands: {
|
|
26
|
+
get: {
|
|
27
|
+
summary: 'Show the allowed ranges',
|
|
28
|
+
usage: 'motherbase network-restrictions get',
|
|
29
|
+
async run(ctx) {
|
|
30
|
+
output(ctx, await api.adminRequest(ctx, 'GET', '/network-restrictions'), renderRestrictions);
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
update: {
|
|
34
|
+
summary: 'Replace the allowed ranges (repeat --db-allow-cidr; IPv4 and IPv6 are told apart)',
|
|
35
|
+
usage: 'motherbase network-restrictions update --db-allow-cidr <cidr> [--db-allow-cidr <cidr> ...]',
|
|
36
|
+
async run(ctx, _args, flags) {
|
|
37
|
+
const cidrs = [].concat(flags['db-allow-cidr'] ?? []).map(String);
|
|
38
|
+
if (!cidrs.length) {
|
|
39
|
+
throw new CliError('Pass at least one --db-allow-cidr (0.0.0.0/0 and ::/0 allow everyone).');
|
|
40
|
+
}
|
|
41
|
+
const body = {
|
|
42
|
+
dbAllowedCidrs: cidrs.filter(c => !c.includes(':')),
|
|
43
|
+
dbAllowedCidrsV6: cidrs.filter(c => c.includes(':')),
|
|
44
|
+
};
|
|
45
|
+
output(ctx, await api.adminRequest(ctx, 'PUT', '/network-restrictions', { body }), renderRestrictions);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function renderSsl(d, ui) {
|
|
52
|
+
ui.keyValue({ enforced: d.enforced, 'TLS active': d.sslActive, status: d.status });
|
|
53
|
+
if (d.enforced) ui.info('External clients must connect with sslmode=require.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sslEnforcement = {
|
|
57
|
+
summary: 'Require TLS for direct database connections',
|
|
58
|
+
usage: 'motherbase ssl-enforcement <get|update> ...',
|
|
59
|
+
subcommands: {
|
|
60
|
+
get: {
|
|
61
|
+
summary: 'Show whether TLS is enforced',
|
|
62
|
+
usage: 'motherbase ssl-enforcement get',
|
|
63
|
+
async run(ctx) {
|
|
64
|
+
output(ctx, await api.adminRequest(ctx, 'GET', '/ssl-enforcement'), renderSsl);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
update: {
|
|
68
|
+
summary: 'Turn enforcement on or off',
|
|
69
|
+
usage: 'motherbase ssl-enforcement update --enable-db-ssl-enforcement | --disable-db-ssl-enforcement',
|
|
70
|
+
async run(ctx, _args, flags) {
|
|
71
|
+
const enable = Boolean(flags['enable-db-ssl-enforcement']);
|
|
72
|
+
const disable = Boolean(flags['disable-db-ssl-enforcement']);
|
|
73
|
+
if (enable === disable) {
|
|
74
|
+
throw new CliError('Pass exactly one of --enable-db-ssl-enforcement or --disable-db-ssl-enforcement.');
|
|
75
|
+
}
|
|
76
|
+
const data = await api.adminRequest(ctx, 'PUT', '/ssl-enforcement', { body: { enforced: enable } });
|
|
77
|
+
output(ctx, data, renderSsl);
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
module.exports = { networkRestrictions, sslEnforcement };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Tenant organizations on the Platform Console. Projects sit under an org, as
|
|
4
|
+
// they do on Supabase.
|
|
5
|
+
|
|
6
|
+
const api = require('../api');
|
|
7
|
+
const { output, requireArg } = require('./_util');
|
|
8
|
+
|
|
9
|
+
const subcommands = {
|
|
10
|
+
list: {
|
|
11
|
+
summary: 'List the organizations you belong to',
|
|
12
|
+
usage: 'motherbase orgs list',
|
|
13
|
+
async run(ctx) {
|
|
14
|
+
const data = await api.consoleRequest(ctx, 'GET', '/orgs');
|
|
15
|
+
output(ctx, data, (rows, ui) =>
|
|
16
|
+
ui.table(Array.isArray(rows) ? rows : [], [
|
|
17
|
+
{ key: 'id', label: 'ID' },
|
|
18
|
+
{ key: 'name', label: 'NAME' },
|
|
19
|
+
{ key: 'role', label: 'ROLE' },
|
|
20
|
+
{ key: 'projectCount', label: 'PROJECTS' },
|
|
21
|
+
]),
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create: {
|
|
26
|
+
summary: 'Create an organization',
|
|
27
|
+
usage: 'motherbase orgs create <name>',
|
|
28
|
+
async run(ctx, args) {
|
|
29
|
+
const name = requireArg(args[0], 'name');
|
|
30
|
+
const data = await api.consoleRequest(ctx, 'POST', '/orgs', { body: { name } });
|
|
31
|
+
output(ctx, data, (d, ui) => ui.success(`Created org ${d.name} (${d.id})`));
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
summary: 'Manage organizations on the Platform Console',
|
|
38
|
+
usage: 'motherbase orgs <list|create> ...',
|
|
39
|
+
subcommands,
|
|
40
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `motherbase postgres-config get|update|delete`, Supabase's command of the
|
|
4
|
+
// same name. The one difference is where a value lands: the server picks the
|
|
5
|
+
// narrowest scope Postgres allows (a per-database override for
|
|
6
|
+
// `statement_timeout`, ALTER SYSTEM for `shared_buffers`), and `--scope`
|
|
7
|
+
// overrides that. Motherbase never restarts Postgres itself, so `--no-restart`
|
|
8
|
+
// is accepted for script compatibility and a restart-only change is reported.
|
|
9
|
+
|
|
10
|
+
const api = require('../api');
|
|
11
|
+
const { output, parseKeyValues } = require('./_util');
|
|
12
|
+
const { CliError } = require('../context');
|
|
13
|
+
|
|
14
|
+
const SCOPES = ['database', 'role', 'system'];
|
|
15
|
+
|
|
16
|
+
function scopeFlags(flags) {
|
|
17
|
+
if (flags.scope !== undefined && !SCOPES.includes(flags.scope)) {
|
|
18
|
+
throw new CliError(`--scope must be one of: ${SCOPES.join(', ')}`);
|
|
19
|
+
}
|
|
20
|
+
if (flags.scope === 'role' && !flags.role) throw new CliError('--scope role needs --role <name>');
|
|
21
|
+
return {
|
|
22
|
+
...(flags.scope ? { scope: flags.scope } : {}),
|
|
23
|
+
...(flags.role ? { role: String(flags.role) } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// What `get` prints: one row per setting that has an override, like Supabase,
|
|
28
|
+
// or every curated setting with --all.
|
|
29
|
+
function overrideRows(config, all) {
|
|
30
|
+
return config.settings.flatMap(setting => {
|
|
31
|
+
if (!setting.overrides.length) {
|
|
32
|
+
return all ? [{ name: setting.name, value: setting.value, scope: '', role: '', restart: setting.context === 'postmaster' }] : [];
|
|
33
|
+
}
|
|
34
|
+
return setting.overrides.map(o => ({
|
|
35
|
+
name: setting.name,
|
|
36
|
+
value: o.value,
|
|
37
|
+
scope: o.scope,
|
|
38
|
+
role: o.role || '',
|
|
39
|
+
restart: setting.context === 'postmaster',
|
|
40
|
+
}));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function reportChange(result, ui) {
|
|
45
|
+
for (const change of result.applied) {
|
|
46
|
+
ui.success(`${change.name}: ${change.scope}${change.role ? ` (${change.role})` : ''}`);
|
|
47
|
+
}
|
|
48
|
+
if (result.restartRequired.length) {
|
|
49
|
+
ui.warn(`Restart Postgres to apply: ${result.restartRequired.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const subcommands = {
|
|
54
|
+
get: {
|
|
55
|
+
summary: 'Show Postgres config overrides (--all for every managed setting)',
|
|
56
|
+
usage: 'motherbase postgres-config get [--all]',
|
|
57
|
+
async run(ctx, args, flags) {
|
|
58
|
+
const data = await api.adminRequest(ctx, 'GET', '/database/config');
|
|
59
|
+
output(ctx, data, (d, ui) => {
|
|
60
|
+
ui.table(overrideRows(d, Boolean(flags.all)), [
|
|
61
|
+
{ key: 'name', label: 'PARAMETER' },
|
|
62
|
+
{ key: 'value', label: 'VALUE' },
|
|
63
|
+
{ key: 'scope', label: 'SCOPE' },
|
|
64
|
+
{ key: 'role', label: 'ROLE' },
|
|
65
|
+
{ key: 'restart', label: 'NEEDS RESTART' },
|
|
66
|
+
]);
|
|
67
|
+
if (!d.systemOverridesReadable) ui.warn('System-level overrides could not be read with this connection.');
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
update: {
|
|
72
|
+
summary: 'Set Postgres config values',
|
|
73
|
+
usage: 'motherbase postgres-config update --config key=value [--config ...] [--scope database|role|system] [--role r] [--replace-existing-overrides]',
|
|
74
|
+
async run(ctx, args, flags) {
|
|
75
|
+
const values = parseKeyValues([].concat(flags.config || []).filter(Boolean), { coerce: false });
|
|
76
|
+
if (!Object.keys(values).length) throw new CliError('Provide at least one --config key=value.');
|
|
77
|
+
const target = scopeFlags(flags);
|
|
78
|
+
const changes = Object.entries(values).map(([name, value]) => ({ name, value, ...target }));
|
|
79
|
+
|
|
80
|
+
// Supabase's flag: anything overridden before and not named now goes back to its default.
|
|
81
|
+
if (flags['replace-existing-overrides']) {
|
|
82
|
+
const current = await api.adminRequest(ctx, 'GET', '/database/config');
|
|
83
|
+
for (const setting of current.settings) {
|
|
84
|
+
if (setting.name in values) continue;
|
|
85
|
+
for (const o of setting.overrides) {
|
|
86
|
+
changes.push({ name: setting.name, value: null, scope: o.scope, ...(o.role ? { role: o.role } : {}) });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await api.adminRequest(ctx, 'PATCH', '/database/config', { body: { changes } });
|
|
92
|
+
output(ctx, data, (d, ui) => reportChange(d, ui));
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
delete: {
|
|
96
|
+
summary: 'Remove Postgres config overrides, restoring the defaults',
|
|
97
|
+
usage: 'motherbase postgres-config delete --config key1,key2 [--scope database|role|system] [--role r]',
|
|
98
|
+
async run(ctx, args, flags) {
|
|
99
|
+
const names = [].concat(flags.config || []).flatMap(v => String(v).split(',')).map(s => s.trim()).filter(Boolean);
|
|
100
|
+
if (!names.length) throw new CliError('Provide the settings to reset with --config key1,key2.');
|
|
101
|
+
const target = scopeFlags(flags);
|
|
102
|
+
const changes = names.map(name => ({ name, value: null, ...target }));
|
|
103
|
+
const data = await api.adminRequest(ctx, 'PATCH', '/database/config', { body: { changes } });
|
|
104
|
+
output(ctx, data, (d, ui) => reportChange(d, ui));
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
summary: 'Postgres configuration (statement_timeout, work_mem, max_connections, ...)',
|
|
111
|
+
usage: 'motherbase postgres-config <get|update|delete> ...',
|
|
112
|
+
subcommands,
|
|
113
|
+
};
|
package/src/commands/projects.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const api = require('../api');
|
|
4
|
-
const
|
|
4
|
+
const keys = require('./keys');
|
|
5
|
+
const { output, alias, requireArg } = require('./_util');
|
|
5
6
|
const { CliError } = require('../context');
|
|
7
|
+
const { outputFormat } = require('../format');
|
|
6
8
|
|
|
7
9
|
const LIFECYCLE = ['start', 'stop', 'restart', 'update', 'rebuild'];
|
|
8
10
|
|
|
@@ -68,7 +70,7 @@ const subcommands = {
|
|
|
68
70
|
if (d && d.stdout) ui.print(d.stdout);
|
|
69
71
|
if (d && d.stderr) ui.printErr(d.stderr);
|
|
70
72
|
});
|
|
71
|
-
if (flags.f &&
|
|
73
|
+
if (flags.f && outputFormat(ctx.flags) === 'pretty') {
|
|
72
74
|
// The console logs endpoint returns a snapshot; approximate -f by polling.
|
|
73
75
|
const ui = require('../ui');
|
|
74
76
|
ui.warn('(-f) polling every 3s; Ctrl-C to stop');
|
|
@@ -118,8 +120,11 @@ const subcommands = {
|
|
|
118
120
|
|
|
119
121
|
for (const action of LIFECYCLE) subcommands[action] = lifecycleCmd(action);
|
|
120
122
|
|
|
123
|
+
// Supabase's `projects api-keys`; the keys themselves live under `keys`.
|
|
124
|
+
subcommands['api-keys'] = alias(keys.subcommands.list, 'motherbase projects api-keys [--project-ref <id>]');
|
|
125
|
+
|
|
121
126
|
module.exports = {
|
|
122
127
|
summary: 'Manage projects on the Platform Console',
|
|
123
|
-
usage: 'motherbase projects <list|create|start|stop|restart|update|rebuild|delete|logs|info|limits> ...',
|
|
128
|
+
usage: 'motherbase projects <list|create|start|stop|restart|update|rebuild|delete|logs|info|limits|api-keys> ...',
|
|
124
129
|
subcommands,
|
|
125
130
|
};
|