@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.
Files changed (62) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +8 -1
  3. package/bin/.motherbase/config.json +5 -0
  4. package/bin/motherbase.js +10 -10
  5. package/package.json +4 -4
  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-catalog.js +39 -1
  14. package/src/commands/db.js +103 -103
  15. package/src/commands/domains.js +124 -124
  16. package/src/commands/gen.js +178 -172
  17. package/src/commands/import.js +55 -55
  18. package/src/commands/index.js +86 -85
  19. package/src/commands/inspect.js +178 -178
  20. package/src/commands/keys.js +72 -72
  21. package/src/commands/link.js +35 -35
  22. package/src/commands/local.js +101 -24
  23. package/src/commands/login.js +9 -3
  24. package/src/commands/logout.js +16 -16
  25. package/src/commands/mcp.js +61 -61
  26. package/src/commands/migrate.js +0 -5
  27. package/src/commands/migration.js +1 -1
  28. package/src/commands/network.js +83 -83
  29. package/src/commands/orgs.js +40 -40
  30. package/src/commands/postgres-config.js +113 -113
  31. package/src/commands/projects.js +131 -130
  32. package/src/commands/queues.js +133 -133
  33. package/src/commands/realtime.js +35 -35
  34. package/src/commands/rls.js +176 -176
  35. package/src/commands/secrets.js +66 -66
  36. package/src/commands/signing.js +130 -130
  37. package/src/commands/snippets.js +44 -44
  38. package/src/commands/status.js +42 -42
  39. package/src/commands/storage.js +261 -261
  40. package/src/commands/test.js +9 -9
  41. package/src/commands/unlink.js +15 -15
  42. package/src/commands/users.js +29 -1
  43. package/src/commands/vault.js +82 -82
  44. package/src/commands/webhooks.js +62 -62
  45. package/src/commands/whoami.js +20 -20
  46. package/src/commands/wrappers.js +150 -150
  47. package/src/config.js +98 -98
  48. package/src/context.js +106 -106
  49. package/src/edge-templates.js +194 -194
  50. package/src/format.js +92 -92
  51. package/src/inspect/csv.js +22 -22
  52. package/src/inspect/queries.js +562 -562
  53. package/src/main.js +145 -66
  54. package/src/mcp/platform-tools.js +233 -233
  55. package/src/mcp/protocol.js +152 -152
  56. package/src/mcp/storage-tools.js +93 -93
  57. package/src/mcp/tools.js +478 -478
  58. package/src/migration/commands.js +2 -2
  59. package/src/migration/files.js +13 -0
  60. package/src/prompt.js +91 -91
  61. package/src/storage/remote.js +62 -62
  62. package/src/ui.js +111 -111
@@ -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
- // 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.
25
- const DEFAULT_REPO = 'https://github.com/JulienGuinot/BoilerPlate-Backend.git';
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
- '# 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
  },
@@ -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 };