@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
@@ -1,72 +1,72 @@
1
- 'use strict';
2
-
3
- // Project API keys. `secrets` next door is a different thing: those are values
4
- // an edge function reads, these are what a client presents to reach the project.
5
-
6
- const api = require('../api');
7
- const { output, requireArg } = require('./_util');
8
-
9
- const TYPES = ['publishable', 'secret'];
10
-
11
- const subcommands = {
12
- list: {
13
- summary: 'List project API keys',
14
- usage: 'motherbase keys list',
15
- async run(ctx) {
16
- const data = await api.adminRequest(ctx, 'GET', '/api-keys');
17
- const keys = (data && data.keys) || [];
18
- output(ctx, data, (_d, ui) =>
19
- ui.table(
20
- keys.map(k => ({
21
- name: k.name,
22
- type: k.type,
23
- // A secret key has no stored clear value; the prefix is all there is.
24
- key: k.token || `${k.prefix}…`,
25
- status: k.revokedAt ? 'revoked' : 'active',
26
- })),
27
- [
28
- { key: 'name', label: 'NAME' },
29
- { key: 'type', label: 'TYPE' },
30
- { key: 'key', label: 'KEY' },
31
- { key: 'status', label: 'STATUS' },
32
- ],
33
- ),
34
- );
35
- },
36
- },
37
- create: {
38
- summary: 'Create a key. A secret key is printed once and never again',
39
- usage: 'motherbase keys create <NAME> [--type publishable|secret]',
40
- async run(ctx, args, flags) {
41
- const name = requireArg(args[0], 'NAME');
42
- const type = flags.type || 'secret';
43
- if (!TYPES.includes(type)) {
44
- throw new Error(`--type must be one of: ${TYPES.join(', ')}`);
45
- }
46
- const data = await api.adminRequest(ctx, 'POST', '/api-keys', { body: { name, type } });
47
- output(ctx, data, (d, ui) => {
48
- ui.success(`Created ${type} key "${name}"`);
49
- ui.print(d.clearToken);
50
- if (type === 'secret') {
51
- ui.warn('Copy it now. It is stored hashed and cannot be shown again.');
52
- ui.warn('It bypasses row level security: keep it server side.');
53
- }
54
- });
55
- },
56
- },
57
- revoke: {
58
- summary: 'Revoke a key by id',
59
- usage: 'motherbase keys revoke <ID>',
60
- async run(ctx, args) {
61
- const id = requireArg(args[0], 'ID');
62
- const data = await api.adminRequest(ctx, 'DELETE', `/api-keys/${encodeURIComponent(id)}`);
63
- output(ctx, data, (d, ui) => ui.success(`Revoked key ${d.name || id}`));
64
- },
65
- },
66
- };
67
-
68
- module.exports = {
69
- summary: 'Project API keys (publishable and secret)',
70
- usage: 'motherbase keys <list|create|revoke> ...',
71
- subcommands,
72
- };
1
+ 'use strict';
2
+
3
+ // Project API keys. `secrets` next door is a different thing: those are values
4
+ // an edge function reads, these are what a client presents to reach the project.
5
+
6
+ const api = require('../api');
7
+ const { output, requireArg } = require('./_util');
8
+
9
+ const TYPES = ['publishable', 'secret'];
10
+
11
+ const subcommands = {
12
+ list: {
13
+ summary: 'List project API keys',
14
+ usage: 'motherbase keys list',
15
+ async run(ctx) {
16
+ const data = await api.adminRequest(ctx, 'GET', '/api-keys');
17
+ const keys = (data && data.keys) || [];
18
+ output(ctx, data, (_d, ui) =>
19
+ ui.table(
20
+ keys.map(k => ({
21
+ name: k.name,
22
+ type: k.type,
23
+ // A secret key has no stored clear value; the prefix is all there is.
24
+ key: k.token || `${k.prefix}…`,
25
+ status: k.revokedAt ? 'revoked' : 'active',
26
+ })),
27
+ [
28
+ { key: 'name', label: 'NAME' },
29
+ { key: 'type', label: 'TYPE' },
30
+ { key: 'key', label: 'KEY' },
31
+ { key: 'status', label: 'STATUS' },
32
+ ],
33
+ ),
34
+ );
35
+ },
36
+ },
37
+ create: {
38
+ summary: 'Create a key. A secret key is printed once and never again',
39
+ usage: 'motherbase keys create <NAME> [--type publishable|secret]',
40
+ async run(ctx, args, flags) {
41
+ const name = requireArg(args[0], 'NAME');
42
+ const type = flags.type || 'secret';
43
+ if (!TYPES.includes(type)) {
44
+ throw new Error(`--type must be one of: ${TYPES.join(', ')}`);
45
+ }
46
+ const data = await api.adminRequest(ctx, 'POST', '/api-keys', { body: { name, type } });
47
+ output(ctx, data, (d, ui) => {
48
+ ui.success(`Created ${type} key "${name}"`);
49
+ ui.print(d.clearToken);
50
+ if (type === 'secret') {
51
+ ui.warn('Copy it now. It is stored hashed and cannot be shown again.');
52
+ ui.warn('It bypasses row level security: keep it server side.');
53
+ }
54
+ });
55
+ },
56
+ },
57
+ revoke: {
58
+ summary: 'Revoke a key by id',
59
+ usage: 'motherbase keys revoke <ID>',
60
+ async run(ctx, args) {
61
+ const id = requireArg(args[0], 'ID');
62
+ const data = await api.adminRequest(ctx, 'DELETE', `/api-keys/${encodeURIComponent(id)}`);
63
+ output(ctx, data, (d, ui) => ui.success(`Revoked key ${d.name || id}`));
64
+ },
65
+ },
66
+ };
67
+
68
+ module.exports = {
69
+ summary: 'Project API keys (publishable and secret)',
70
+ usage: 'motherbase keys <list|create|revoke> ...',
71
+ subcommands,
72
+ };
@@ -1,35 +1,35 @@
1
- 'use strict';
2
-
3
- const api = require('../api');
4
- const config = require('../config');
5
- const prompt = require('../prompt');
6
- const ui = require('../ui');
7
- const { CliError } = require('../context');
8
-
9
- module.exports = {
10
- summary: 'Link the current directory to a project (writes ./.motherbase/config.json)',
11
- usage: 'motherbase link [projectNameOrId]',
12
- async run(ctx, args, flags) {
13
- const { consoleUrl } = ctx.requireConsole();
14
- const projects = await api.consoleRequest(ctx, 'GET', '/projects');
15
- const list = Array.isArray(projects) ? projects : [];
16
- if (list.length === 0) throw new CliError('No projects available on this console.');
17
-
18
- let picked = args[0];
19
- if (!picked) {
20
- if (flags.yes) throw new CliError('Specify a project name/id when using --yes.');
21
- ui.print(ui.color.bold('Available projects:'));
22
- list.forEach((p, i) => ui.print(` ${i + 1}. ${p.name || p.id}`));
23
- const answer = await prompt.question('Pick a project (number or name): ');
24
- const asNum = Number(answer);
25
- picked = Number.isInteger(asNum) && list[asNum - 1] ? (list[asNum - 1].name || list[asNum - 1].id) : answer;
26
- }
27
-
28
- const match = list.find(p => p.name === picked || p.id === picked);
29
- if (!match) throw new CliError(`Project "${picked}" not found on ${consoleUrl}.`);
30
-
31
- const projectId = match.name || match.id;
32
- config.writeLink(ctx.cwd, { consoleUrl, projectId, projectName: match.name || projectId });
33
- ui.success(`Linked ${ctx.cwd} → ${projectId} (${consoleUrl})`);
34
- },
35
- };
1
+ 'use strict';
2
+
3
+ const api = require('../api');
4
+ const config = require('../config');
5
+ const prompt = require('../prompt');
6
+ const ui = require('../ui');
7
+ const { CliError } = require('../context');
8
+
9
+ module.exports = {
10
+ summary: 'Link the current directory to a project (writes ./.motherbase/config.json)',
11
+ usage: 'motherbase link [projectNameOrId]',
12
+ async run(ctx, args, flags) {
13
+ const { consoleUrl } = ctx.requireConsole();
14
+ const projects = await api.consoleRequest(ctx, 'GET', '/projects');
15
+ const list = Array.isArray(projects) ? projects : [];
16
+ if (list.length === 0) throw new CliError('No projects available on this console.');
17
+
18
+ let picked = args[0];
19
+ if (!picked) {
20
+ if (flags.yes) throw new CliError('Specify a project name/id when using --yes.');
21
+ ui.print(ui.color.bold('Available projects:'));
22
+ list.forEach((p, i) => ui.print(` ${i + 1}. ${p.name || p.id}`));
23
+ const answer = await prompt.question('Pick a project (number or name): ');
24
+ const asNum = Number(answer);
25
+ picked = Number.isInteger(asNum) && list[asNum - 1] ? (list[asNum - 1].name || list[asNum - 1].id) : answer;
26
+ }
27
+
28
+ const match = list.find(p => p.name === picked || p.id === picked);
29
+ if (!match) throw new CliError(`Project "${picked}" not found on ${consoleUrl}.`);
30
+
31
+ const projectId = match.name || match.id;
32
+ config.writeLink(ctx.cwd, { consoleUrl, projectId, projectName: match.name || projectId });
33
+ ui.success(`Linked ${ctx.cwd} → ${projectId} (${consoleUrl})`);
34
+ },
35
+ };
@@ -18,10 +18,15 @@ const { CliError } = require('../context');
18
18
  const COMPOSE_FILE = 'docker-compose.yml';
19
19
  const ENV_FILE = '.env';
20
20
 
21
- // Where the images are built from when the caller has no checkout. Docker
22
- // Compose accepts a git URL as a build context, so a scaffolded directory needs
23
- // nothing but Docker: no clone, no registry, no published image. The first `up`
24
- // pays for the build; every one after that is cached.
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).
25
30
  const DEFAULT_REPO = 'https://github.com/JulienGuinot/BoilerPlate-Backend.git';
26
31
  const DEFAULT_REF = 'master';
27
32
 
@@ -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
- '# First operator account, created on first boot.',
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
- # Images are built from the public repository, so this directory needs Docker and
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
- build:
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
- image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z
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
- build:
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] [--repo URL] [--ref BRANCH] [--force]',
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(` Studio: http://localhost:${context.port}`);
248
- ui.print(` Sign in: ${context.adminEmail} / ${context.adminPassword}`);
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(' motherbase start # first run builds the images, give it a few minutes');
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
  },
@@ -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
- const data = await api.loginRequest(consoleUrl, email, password);
34
- const token = data && data.token;
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}`);
@@ -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
+ };
@@ -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;
@@ -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
  },