@motherbase/cli 0.2.6 → 0.2.9
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 +8 -1
- package/bin/.motherbase/config.json +5 -0
- package/bin/motherbase.js +10 -10
- package/package.json +4 -4
- 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-catalog.js +39 -1
- 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 +86 -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 +101 -24
- 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/users.js +29 -1
- 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/main.js +145 -66
- 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/commands/local.js
CHANGED
|
@@ -18,11 +18,16 @@ const { CliError } = require('../context');
|
|
|
18
18
|
const COMPOSE_FILE = 'docker-compose.yml';
|
|
19
19
|
const ENV_FILE = '.env';
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
21
|
+
// By default the stack runs the images CI publishes after every green build of
|
|
22
|
+
// the default branch, so `start` pulls instead of compiling. Building from a git
|
|
23
|
+
// context used to be the default and could not work on a clean machine: the API
|
|
24
|
+
// image derives from a base image (motherbase-deps) that only the repository's
|
|
25
|
+
// own build script produces.
|
|
26
|
+
const DEFAULT_REGISTRY = 'ghcr.io/julienguinot';
|
|
27
|
+
const DEFAULT_TAG = 'latest';
|
|
28
|
+
// Build-from-source mode, kept for --repo / --ref. It needs motherbase-deps:1.0.0
|
|
29
|
+
// built locally first (see the compose header it writes).
|
|
30
|
+
const DEFAULT_REPO = 'https://github.com/JulienGuinot/motherbase.git';
|
|
26
31
|
const DEFAULT_REF = 'master';
|
|
27
32
|
|
|
28
33
|
function dockerCompose(composeArgs) {
|
|
@@ -59,6 +64,14 @@ function renderEnv(context) {
|
|
|
59
64
|
`BASE_HOST=localhost:${context.port}`,
|
|
60
65
|
'NODE_ENV=production',
|
|
61
66
|
'',
|
|
67
|
+
'# Origins allowed to call this API from a browser. Required: the API refuses',
|
|
68
|
+
'# to boot in production without an explicit list. Comma-separated.',
|
|
69
|
+
`CORS_ORIGINS=${context.corsOrigins}`,
|
|
70
|
+
'',
|
|
71
|
+
'# Raw SQL over the admin API (`motherbase db sql`). This stack has no Console',
|
|
72
|
+
'# in front of it, so the admin API is the only way in.',
|
|
73
|
+
'CONSOLE_SQL_ENABLED=true',
|
|
74
|
+
'',
|
|
62
75
|
'# Secrets. Regenerating any of these invalidates what it protects:',
|
|
63
76
|
'# JWT_SECRET signs sessions, CONFIG_ENCRYPTION_KEY encrypts Studio-managed',
|
|
64
77
|
'# secrets (SMTP, OAuth credentials) at rest.',
|
|
@@ -67,6 +80,7 @@ function renderEnv(context) {
|
|
|
67
80
|
`POSTGRES_PASSWORD=${hex(16)}`,
|
|
68
81
|
`S3_ACCESS_KEY_ID=${hex(8)}`,
|
|
69
82
|
`S3_SECRET_ACCESS_KEY=${hex(20)}`,
|
|
83
|
+
`EDGE_RUNTIME_TOKEN=${hex(24)}`,
|
|
70
84
|
'',
|
|
71
85
|
'S3_BUCKET=uploads',
|
|
72
86
|
'S3_PUBLIC_BUCKET=public',
|
|
@@ -77,29 +91,44 @@ function renderEnv(context) {
|
|
|
77
91
|
'MINIO_PORT=59000',
|
|
78
92
|
'MINIO_CONSOLE_PORT=59001',
|
|
79
93
|
'',
|
|
80
|
-
'#
|
|
94
|
+
'# Operator account. Created (or reset to these values) by running the seed',
|
|
95
|
+
'# once: docker compose exec api node apps/server/dist/prisma/seed.js',
|
|
81
96
|
`SEED_ADMIN_EMAIL=${context.adminEmail}`,
|
|
82
97
|
`SEED_ADMIN_PASSWORD=${context.adminPassword}`,
|
|
83
98
|
'',
|
|
99
|
+
...(context.mode === 'image'
|
|
100
|
+
? [
|
|
101
|
+
'# Published images this stack runs. Pin MOTHERBASE_TAG to a sha-<commit> tag',
|
|
102
|
+
'# to stop `docker compose pull` from moving it.',
|
|
103
|
+
`MOTHERBASE_REGISTRY=${context.registry}`,
|
|
104
|
+
`MOTHERBASE_TAG=${context.tag}`,
|
|
105
|
+
'',
|
|
106
|
+
]
|
|
107
|
+
: []),
|
|
84
108
|
].join('\n');
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
function renderCompose(context) {
|
|
88
112
|
const buildContext = `${context.repo}#${context.ref}`;
|
|
113
|
+
const fromImages = context.mode === 'image';
|
|
114
|
+
const image = name => `\${MOTHERBASE_REGISTRY:-${DEFAULT_REGISTRY}}/motherbase-${name}:\${MOTHERBASE_TAG:-${DEFAULT_TAG}}`;
|
|
115
|
+
const source = (dockerfile, indent = ' ') => fromImages
|
|
116
|
+
? ''
|
|
117
|
+
: `${indent}build:\n${indent} context: ${buildContext}\n${dockerfile ? `${indent} dockerfile: ${dockerfile}\n` : ''}`;
|
|
118
|
+
const header = fromImages
|
|
119
|
+
? `# Runs the images published for Motherbase (MOTHERBASE_REGISTRY / MOTHERBASE_TAG
|
|
120
|
+
# in .env), so this directory needs Docker and nothing else. \`docker compose pull\`
|
|
121
|
+
# then \`motherbase start\` updates it.`
|
|
122
|
+
: `# Builds from ${buildContext}. The API image derives from motherbase-deps:1.0.0,
|
|
123
|
+
# which Compose cannot build: in a checkout of that repository, run
|
|
124
|
+
# docker build -t motherbase-deps:1.0.0 -f deploy/deps.Dockerfile .
|
|
125
|
+
# once first. Point every \`context:\` value at that checkout to build locally.`;
|
|
89
126
|
return `# Generated by \`motherbase init\`.
|
|
90
127
|
#
|
|
91
|
-
|
|
92
|
-
# nothing else: no checkout, no registry login. The first \`up\` builds them, which
|
|
93
|
-
# takes a few minutes; after that the layers are cached.
|
|
94
|
-
#
|
|
95
|
-
# Working from a checkout instead? Replace both \`context:\` values with the path
|
|
96
|
-
# to it and the build becomes local and incremental.
|
|
128
|
+
${header}
|
|
97
129
|
services:
|
|
98
130
|
db:
|
|
99
|
-
|
|
100
|
-
context: ${buildContext}
|
|
101
|
-
dockerfile: deploy/postgres.Dockerfile
|
|
102
|
-
command: ['postgres', '-c', 'shared_preload_libraries=pg_cron,pg_stat_statements', '-c', 'cron.database_name=app']
|
|
131
|
+
${fromImages ? ` image: ${image('postgres')}\n` : source('deploy/postgres.Dockerfile')} command: ['postgres', '-c', 'shared_preload_libraries=pg_cron,pg_stat_statements', '-c', 'cron.database_name=app']
|
|
103
132
|
restart: unless-stopped
|
|
104
133
|
environment:
|
|
105
134
|
POSTGRES_USER: postgres
|
|
@@ -138,7 +167,11 @@ services:
|
|
|
138
167
|
|
|
139
168
|
# One-shot: a private bucket and a separate anonymously-downloadable one.
|
|
140
169
|
createbuckets:
|
|
141
|
-
|
|
170
|
+
# Not quay.io/minio/mc: that path now requires a registry login.
|
|
171
|
+
image: bitnamilegacy/minio-client:2025.5.21
|
|
172
|
+
# mc keeps its config under $HOME; the image's HOME (/) is not writable.
|
|
173
|
+
environment:
|
|
174
|
+
HOME: /tmp
|
|
142
175
|
depends_on:
|
|
143
176
|
minio:
|
|
144
177
|
condition: service_healthy
|
|
@@ -152,10 +185,27 @@ services:
|
|
|
152
185
|
echo 'buckets ready';
|
|
153
186
|
"
|
|
154
187
|
|
|
188
|
+
# Runs edge functions. Only the API shares its network, so function code has
|
|
189
|
+
# no route to Postgres or MinIO.
|
|
190
|
+
edge-runtime:
|
|
191
|
+
${fromImages ? ` image: ${image('edge-runtime')}\n` : source('deploy/edge-runtime.Dockerfile')} restart: unless-stopped
|
|
192
|
+
environment:
|
|
193
|
+
EDGE_RUNTIME_PORT: 9000
|
|
194
|
+
DENO_DIR: /tmp/deno-dir
|
|
195
|
+
EDGE_RUNTIME_TOKEN: \${EDGE_RUNTIME_TOKEN:?EDGE_RUNTIME_TOKEN is required}
|
|
196
|
+
EDGE_ALLOWED_INTERNAL_ORIGINS: http://api:5001
|
|
197
|
+
networks:
|
|
198
|
+
- edge
|
|
199
|
+
read_only: true
|
|
200
|
+
tmpfs:
|
|
201
|
+
- /tmp
|
|
202
|
+
cap_drop:
|
|
203
|
+
- ALL
|
|
204
|
+
security_opt:
|
|
205
|
+
- no-new-privileges:true
|
|
206
|
+
|
|
155
207
|
api:
|
|
156
|
-
|
|
157
|
-
context: ${buildContext}
|
|
158
|
-
restart: unless-stopped
|
|
208
|
+
${fromImages ? ` image: ${image('api')}\n` : source(null)} restart: unless-stopped
|
|
159
209
|
depends_on:
|
|
160
210
|
db:
|
|
161
211
|
condition: service_healthy
|
|
@@ -163,6 +213,8 @@ services:
|
|
|
163
213
|
condition: service_healthy
|
|
164
214
|
createbuckets:
|
|
165
215
|
condition: service_completed_successfully
|
|
216
|
+
edge-runtime:
|
|
217
|
+
condition: service_started
|
|
166
218
|
env_file:
|
|
167
219
|
- ${ENV_FILE}
|
|
168
220
|
# Container-internal wiring, overriding the localhost values in .env so the
|
|
@@ -173,13 +225,24 @@ services:
|
|
|
173
225
|
DATABASE_URL: postgresql://postgres:\${POSTGRES_PASSWORD}@db:5432/app?schema=public
|
|
174
226
|
STORAGE_DRIVER: s3
|
|
175
227
|
S3_ENDPOINT: http://minio:9000
|
|
228
|
+
# Signed URLs are fetched from this machine, where MinIO is published on
|
|
229
|
+
# MINIO_PORT, not at its container address.
|
|
230
|
+
S3_PRESIGN_ENDPOINT: http://localhost:\${MINIO_PORT:-59000}
|
|
176
231
|
DB_PORT: \${POSTGRES_PORT:-55432}
|
|
232
|
+
EDGE_RUNTIME_URL: http://edge-runtime:9000
|
|
233
|
+
EDGE_RUNTIME_CALLBACK_URL: http://api:5001
|
|
177
234
|
ports:
|
|
178
235
|
- '\${PORT:-5001}:5001'
|
|
236
|
+
networks:
|
|
237
|
+
- default
|
|
238
|
+
- edge
|
|
179
239
|
|
|
180
240
|
volumes:
|
|
181
241
|
db_data:
|
|
182
242
|
minio_data:
|
|
243
|
+
|
|
244
|
+
networks:
|
|
245
|
+
edge:
|
|
183
246
|
`;
|
|
184
247
|
}
|
|
185
248
|
|
|
@@ -189,8 +252,11 @@ volumes:
|
|
|
189
252
|
const INIT_FLAG_PATTERNS = {
|
|
190
253
|
'admin-email': /^[^\s"'`$#\\]+$/,
|
|
191
254
|
'admin-password': /^[^\s"'`$#\\]+$/,
|
|
255
|
+
'cors-origins': /^[\w.:/,*-]+$/,
|
|
192
256
|
repo: /^[\w.:/@~+-]+$/,
|
|
193
257
|
ref: /^[\w./-]+$/,
|
|
258
|
+
registry: /^[\w.:/-]+$/,
|
|
259
|
+
tag: /^[\w.-]+$/,
|
|
194
260
|
};
|
|
195
261
|
|
|
196
262
|
function validateInitFlags(flags) {
|
|
@@ -205,7 +271,7 @@ function validateInitFlags(flags) {
|
|
|
205
271
|
|
|
206
272
|
const init = {
|
|
207
273
|
summary: 'Scaffold a runnable local stack (compose file, .env, generated secrets)',
|
|
208
|
-
usage: 'motherbase init [dir] [--port 5001] [--
|
|
274
|
+
usage: 'motherbase init [dir] [--port 5001] [--cors-origins http://localhost:3000] [--tag latest] [--repo URL --ref BRANCH] [--force]',
|
|
209
275
|
async run(ctx, args, flags = {}) {
|
|
210
276
|
validateInitFlags(flags);
|
|
211
277
|
const dir = args[0] || '.';
|
|
@@ -215,6 +281,11 @@ const init = {
|
|
|
215
281
|
const context = {
|
|
216
282
|
port: Number(flags.port) || 5001,
|
|
217
283
|
postgresPort: Number(flags['postgres-port']) || 55432,
|
|
284
|
+
corsOrigins: flags['cors-origins'] || 'http://localhost:3000',
|
|
285
|
+
// --repo / --ref ask for a source build; anything else runs published images.
|
|
286
|
+
mode: flags.repo || flags.ref ? 'source' : 'image',
|
|
287
|
+
registry: flags.registry || DEFAULT_REGISTRY,
|
|
288
|
+
tag: flags.tag || DEFAULT_TAG,
|
|
218
289
|
repo: flags.repo || DEFAULT_REPO,
|
|
219
290
|
ref: flags.ref || DEFAULT_REF,
|
|
220
291
|
adminEmail: flags['admin-email'] || 'admin@example.com',
|
|
@@ -243,13 +314,19 @@ const init = {
|
|
|
243
314
|
ui.warn(`kept existing ${skipped.join(', ')} (use --force to overwrite, which discards their secrets)`);
|
|
244
315
|
}
|
|
245
316
|
if (written.includes(ENV_FILE)) {
|
|
317
|
+
// No Studio here: Studio is served by the Platform Console, which this
|
|
318
|
+
// stack does not include. Saying otherwise sent people to a URL that
|
|
319
|
+
// answers JSON.
|
|
246
320
|
ui.print('');
|
|
247
|
-
ui.print(`
|
|
248
|
-
ui.print(`
|
|
321
|
+
ui.print(` API: http://localhost:${context.port} (no Studio: that comes with the Platform Console)`);
|
|
322
|
+
ui.print(` Operator: ${context.adminEmail} / ${context.adminPassword}`);
|
|
249
323
|
ui.print('');
|
|
250
324
|
ui.print(' Next:');
|
|
251
325
|
if (dir !== '.') ui.print(` cd ${dir}`);
|
|
252
|
-
ui.print(
|
|
326
|
+
ui.print(context.mode === 'image'
|
|
327
|
+
? ' motherbase start # pulls the images on first run'
|
|
328
|
+
: ' motherbase start # builds the images on first run (see the compose header)');
|
|
329
|
+
ui.print(' docker compose exec api node apps/server/dist/prisma/seed.js # once, creates the operator');
|
|
253
330
|
ui.print('');
|
|
254
331
|
}
|
|
255
332
|
},
|
package/src/commands/login.js
CHANGED
|
@@ -8,7 +8,7 @@ const { CliError } = require('../context');
|
|
|
8
8
|
|
|
9
9
|
module.exports = {
|
|
10
10
|
summary: 'Log in to a Motherbase Console (email/password → session token)',
|
|
11
|
-
usage: 'motherbase login [--url <consoleUrl>] [--email <email>]',
|
|
11
|
+
usage: 'motherbase login [--url <consoleUrl>] [--email <email>] [--code <totp>]',
|
|
12
12
|
async run(ctx, args, flags) {
|
|
13
13
|
const consoleUrl = config.normalizeUrl(
|
|
14
14
|
flags.url || ctx.env.MOTHERBASE_CONSOLE_URL || ctx.consoleUrl || '',
|
|
@@ -25,13 +25,19 @@ module.exports = {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
|
|
28
29
|
const email = flags.email || (await prompt.question('Email: '));
|
|
29
30
|
if (!email) throw new CliError('email is required');
|
|
30
31
|
const password = (typeof flags.password === 'string' && flags.password) || (await prompt.password('Password: '));
|
|
31
32
|
if (!password) throw new CliError('password is required');
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
let data = await api.loginRequest(consoleUrl, email, password);
|
|
35
|
+
if (data.twoFactorCookie) {
|
|
36
|
+
const code = (typeof flags.code === 'string' && flags.code) || (await prompt.question('Authenticator code: '));
|
|
37
|
+
if (!code) throw new CliError('two-factor code is required');
|
|
38
|
+
data = await api.verifyTotpRequest(consoleUrl, data.twoFactorCookie, code);
|
|
39
|
+
}
|
|
40
|
+
const token = data.token;
|
|
35
41
|
if (!token) throw new CliError('login succeeded but no token was returned');
|
|
36
42
|
config.saveConsoleToken(consoleUrl, token, email);
|
|
37
43
|
ui.success(`Logged in to ${consoleUrl} as ${email}`);
|
package/src/commands/logout.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const config = require('../config');
|
|
4
|
-
const ui = require('../ui');
|
|
5
|
-
const { CliError } = require('../context');
|
|
6
|
-
|
|
7
|
-
module.exports = {
|
|
8
|
-
summary: 'Remove the stored token for the current (or --url) console',
|
|
9
|
-
usage: 'motherbase logout [--url <consoleUrl>]',
|
|
10
|
-
async run(ctx) {
|
|
11
|
-
const url = config.normalizeUrl(ctx.flags.url || ctx.consoleUrl || '');
|
|
12
|
-
if (!url) throw new CliError('No console to log out of.');
|
|
13
|
-
config.clearConsoleToken(url);
|
|
14
|
-
ui.success(`Logged out of ${url}`);
|
|
15
|
-
},
|
|
16
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const ui = require('../ui');
|
|
5
|
+
const { CliError } = require('../context');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
summary: 'Remove the stored token for the current (or --url) console',
|
|
9
|
+
usage: 'motherbase logout [--url <consoleUrl>]',
|
|
10
|
+
async run(ctx) {
|
|
11
|
+
const url = config.normalizeUrl(ctx.flags.url || ctx.consoleUrl || '');
|
|
12
|
+
if (!url) throw new CliError('No console to log out of.');
|
|
13
|
+
config.clearConsoleToken(url);
|
|
14
|
+
ui.success(`Logged out of ${url}`);
|
|
15
|
+
},
|
|
16
|
+
};
|
package/src/commands/mcp.js
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// `motherbase mcp`: expose the linked project, and the console's organizations
|
|
4
|
-
// and projects, to an AI agent over the Model Context Protocol.
|
|
5
|
-
//
|
|
6
|
-
// The transport is stdio, which is how an MCP client launches a server: it spawns
|
|
7
|
-
// this process and talks JSON-RPC over its stdin/stdout. Nothing is printed to
|
|
8
|
-
// stdout except protocol messages, so diagnostics go to stderr.
|
|
9
|
-
|
|
10
|
-
const { serve, toolServer } = require('../mcp/protocol');
|
|
11
|
-
const { buildTools } = require('../mcp/tools');
|
|
12
|
-
const { CliError } = require('../context');
|
|
13
|
-
|
|
14
|
-
const pkg = require('../../package.json');
|
|
15
|
-
|
|
16
|
-
/** Whether a `require*()` resolves, without surfacing its error. */
|
|
17
|
-
function reaches(resolve) {
|
|
18
|
-
try {
|
|
19
|
-
resolve();
|
|
20
|
-
return true;
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const mcp = {
|
|
27
|
-
summary: 'Serve the linked project and console to an AI agent over MCP (stdio)',
|
|
28
|
-
usage: 'motherbase mcp [--write]',
|
|
29
|
-
async run(ctx, _args, flags = {}) {
|
|
30
|
-
// Resolve what this session reaches up front. Failing here is a clear CLI
|
|
31
|
-
// error; failing on the first tool call is an opaque one inside someone's agent.
|
|
32
|
-
const scope = { project: reaches(() => ctx.requireProject()), console: reaches(() => ctx.requireConsole()) };
|
|
33
|
-
if (!scope.project && !scope.console) {
|
|
34
|
-
throw new CliError(
|
|
35
|
-
'Nothing to serve. Run `motherbase login` for the organization and project tools, and `motherbase link` '
|
|
36
|
-
+ '(or set MOTHERBASE_URL + MOTHERBASE_ADMIN_TOKEN) for the database tools.',
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const write = Boolean(flags.write);
|
|
41
|
-
const tools = buildTools(ctx, { write, ...scope });
|
|
42
|
-
|
|
43
|
-
process.stderr.write(
|
|
44
|
-
`motherbase mcp: ${tools.length} tools, ${write ? 'read and write' : 'read only'}. `
|
|
45
|
-
+ `Project: ${scope.project ? ctx.projectName || ctx.projectId || 'direct' : 'none linked'}. `
|
|
46
|
-
+ `Console: ${scope.console ? ctx.consoleUrl : 'not logged in'}\n`,
|
|
47
|
-
);
|
|
48
|
-
if (!write) {
|
|
49
|
-
process.stderr.write(' Pass --write to expose apply_migration, raw SQL writes, function deploys and project lifecycle.\n');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
await serve({
|
|
53
|
-
input: process.stdin,
|
|
54
|
-
output: process.stdout,
|
|
55
|
-
handlers: toolServer({ name: 'motherbase', version: pkg.version, tools }),
|
|
56
|
-
onError: err => process.stderr.write(`motherbase mcp: ${err && err.message ? err.message : err}\n`),
|
|
57
|
-
});
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
module.exports = mcp;
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `motherbase mcp`: expose the linked project, and the console's organizations
|
|
4
|
+
// and projects, to an AI agent over the Model Context Protocol.
|
|
5
|
+
//
|
|
6
|
+
// The transport is stdio, which is how an MCP client launches a server: it spawns
|
|
7
|
+
// this process and talks JSON-RPC over its stdin/stdout. Nothing is printed to
|
|
8
|
+
// stdout except protocol messages, so diagnostics go to stderr.
|
|
9
|
+
|
|
10
|
+
const { serve, toolServer } = require('../mcp/protocol');
|
|
11
|
+
const { buildTools } = require('../mcp/tools');
|
|
12
|
+
const { CliError } = require('../context');
|
|
13
|
+
|
|
14
|
+
const pkg = require('../../package.json');
|
|
15
|
+
|
|
16
|
+
/** Whether a `require*()` resolves, without surfacing its error. */
|
|
17
|
+
function reaches(resolve) {
|
|
18
|
+
try {
|
|
19
|
+
resolve();
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const mcp = {
|
|
27
|
+
summary: 'Serve the linked project and console to an AI agent over MCP (stdio)',
|
|
28
|
+
usage: 'motherbase mcp [--write]',
|
|
29
|
+
async run(ctx, _args, flags = {}) {
|
|
30
|
+
// Resolve what this session reaches up front. Failing here is a clear CLI
|
|
31
|
+
// error; failing on the first tool call is an opaque one inside someone's agent.
|
|
32
|
+
const scope = { project: reaches(() => ctx.requireProject()), console: reaches(() => ctx.requireConsole()) };
|
|
33
|
+
if (!scope.project && !scope.console) {
|
|
34
|
+
throw new CliError(
|
|
35
|
+
'Nothing to serve. Run `motherbase login` for the organization and project tools, and `motherbase link` '
|
|
36
|
+
+ '(or set MOTHERBASE_URL + MOTHERBASE_ADMIN_TOKEN) for the database tools.',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const write = Boolean(flags.write);
|
|
41
|
+
const tools = buildTools(ctx, { write, ...scope });
|
|
42
|
+
|
|
43
|
+
process.stderr.write(
|
|
44
|
+
`motherbase mcp: ${tools.length} tools, ${write ? 'read and write' : 'read only'}. `
|
|
45
|
+
+ `Project: ${scope.project ? ctx.projectName || ctx.projectId || 'direct' : 'none linked'}. `
|
|
46
|
+
+ `Console: ${scope.console ? ctx.consoleUrl : 'not logged in'}\n`,
|
|
47
|
+
);
|
|
48
|
+
if (!write) {
|
|
49
|
+
process.stderr.write(' Pass --write to expose apply_migration, raw SQL writes, function deploys and project lifecycle.\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await serve({
|
|
53
|
+
input: process.stdin,
|
|
54
|
+
output: process.stdout,
|
|
55
|
+
handlers: toolServer({ name: 'motherbase', version: pkg.version, tools }),
|
|
56
|
+
onError: err => process.stderr.write(`motherbase mcp: ${err && err.message ? err.message : err}\n`),
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
module.exports = mcp;
|
package/src/commands/migrate.js
CHANGED
|
@@ -80,11 +80,6 @@ const UNSUPPORTED = [
|
|
|
80
80
|
what: 'auth.mfa.*',
|
|
81
81
|
advice: 'Use auth.twoFactor: enable({ password }), verifyTotp({ code }), disable({ password }).',
|
|
82
82
|
},
|
|
83
|
-
{
|
|
84
|
-
pattern: /transform\s*:\s*\{/g,
|
|
85
|
-
what: 'storage image transform options',
|
|
86
|
-
advice: 'No image pipeline yet. Store the sizes you need at upload time.',
|
|
87
|
-
},
|
|
88
83
|
{
|
|
89
84
|
pattern: /\.delete\(\s*\)\s*(?![\s\S]{0,200}?\.(eq|neq|in|gt|gte|lt|lte|like|ilike|is|filter|match|or|not)\()/g,
|
|
90
85
|
what: 'delete() with no filter',
|
|
@@ -27,7 +27,7 @@ const subcommands = {
|
|
|
27
27
|
fs.mkdirSync(dir, { recursive: true });
|
|
28
28
|
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '');
|
|
29
29
|
const file = path.join(dir, `${stamp}_${name}.sql`);
|
|
30
|
-
fs.writeFileSync(file, `-- Migration: ${name}\n\n\n-- @down\n-- Optional: SQL that undoes the above, for \`migration down\`.\n
|
|
30
|
+
fs.writeFileSync(file, `-- Migration: ${name}\n\n\n-- @down\n-- Optional: SQL that undoes the above, for \`migration down\`.\n`, { flag: 'wx' });
|
|
31
31
|
ui.success(`Created ${path.relative(ctx.cwd, file)}`);
|
|
32
32
|
},
|
|
33
33
|
},
|
package/src/commands/network.js
CHANGED
|
@@ -1,83 +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 };
|
|
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 };
|