@motherbase/cli 0.2.5 → 0.2.6

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -247
  3. package/bin/motherbase.js +10 -10
  4. package/package.json +35 -26
  5. package/src/api.js +233 -241
  6. package/src/commands/_util.js +106 -93
  7. package/src/commands/backups.js +16 -16
  8. package/src/commands/branches.js +113 -113
  9. package/src/commands/completion.js +90 -90
  10. package/src/commands/config.js +47 -47
  11. package/src/commands/cron.js +65 -65
  12. package/src/commands/db-catalog.js +326 -326
  13. package/src/commands/db.js +103 -103
  14. package/src/commands/domains.js +124 -124
  15. package/src/commands/functions.js +304 -297
  16. package/src/commands/gen.js +172 -172
  17. package/src/commands/import.js +55 -55
  18. package/src/commands/index.js +85 -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 +294 -273
  23. package/src/commands/login.js +39 -39
  24. package/src/commands/logout.js +16 -16
  25. package/src/commands/mcp.js +61 -61
  26. package/src/commands/migrate.js +207 -179
  27. package/src/commands/migration.js +108 -108
  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 +130 -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 +142 -142
  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 -102
  49. package/src/docs.js +68 -0
  50. package/src/edge-templates.js +194 -194
  51. package/src/format.js +92 -92
  52. package/src/inspect/csv.js +22 -22
  53. package/src/inspect/queries.js +562 -562
  54. package/src/main.js +224 -194
  55. package/src/mcp/platform-tools.js +233 -233
  56. package/src/mcp/protocol.js +152 -149
  57. package/src/mcp/storage-tools.js +93 -93
  58. package/src/mcp/tools.js +478 -478
  59. package/src/migration/commands.js +375 -375
  60. package/src/migration/files.js +245 -245
  61. package/src/prompt.js +91 -91
  62. package/src/storage/remote.js +62 -62
  63. package/src/ui.js +111 -111
@@ -1,273 +1,294 @@
1
- 'use strict';
2
-
3
- // Local self-host workflow: thin wrappers over `docker compose`, plus `init`
4
- // scaffolding.
5
- //
6
- // `init` used to write a `.env` and nothing else, which left a directory that
7
- // `motherbase start` could not start: `docker compose up` in a folder with no
8
- // compose file is a raw Docker error, and that was the first thing a new user
9
- // saw. It now writes a self-contained stack that runs.
10
-
11
- const crypto = require('crypto');
12
- const fs = require('fs');
13
- const path = require('path');
14
- const { spawn } = require('child_process');
15
- const ui = require('../ui');
16
- const { CliError } = require('../context');
17
-
18
- const COMPOSE_FILE = 'docker-compose.yml';
19
- const ENV_FILE = '.env';
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';
26
- const DEFAULT_REF = 'master';
27
-
28
- function dockerCompose(composeArgs) {
29
- return new Promise((resolve, reject) => {
30
- const child = spawn('docker', ['compose', ...composeArgs], { stdio: 'inherit', windowsHide: true });
31
- child.on('error', err => reject(new CliError(`failed to run docker: ${err.message}`)));
32
- child.on('close', code => (code === 0 ? resolve() : reject(new CliError(`docker compose exited with code ${code}`))));
33
- });
34
- }
35
-
36
- /**
37
- * Refuse to run compose from a directory that has no stack, and say what to do.
38
- * Without this the user gets Docker's own "no configuration file provided",
39
- * which does not mention `motherbase init`.
40
- */
41
- function assertStack(ctx) {
42
- const file = path.join(ctx.cwd, COMPOSE_FILE);
43
- if (!fs.existsSync(file)) {
44
- throw new CliError(
45
- `no ${COMPOSE_FILE} here. Run \`motherbase init\` first, or cd into a directory that has one.`,
46
- );
47
- }
48
- }
49
-
50
- const hex = n => crypto.randomBytes(n).toString('hex');
51
- const b64 = n => crypto.randomBytes(n).toString('base64');
52
-
53
- function renderEnv(context) {
54
- return [
55
- '# Generated by `motherbase init`. Secrets below are unique to this install.',
56
- '# Keep this file out of version control.',
57
- '',
58
- `PORT=${context.port}`,
59
- `BASE_HOST=localhost:${context.port}`,
60
- 'NODE_ENV=production',
61
- '',
62
- '# Secrets. Regenerating any of these invalidates what it protects:',
63
- '# JWT_SECRET signs sessions, CONFIG_ENCRYPTION_KEY encrypts Studio-managed',
64
- '# secrets (SMTP, OAuth credentials) at rest.',
65
- `JWT_SECRET=${hex(32)}`,
66
- `CONFIG_ENCRYPTION_KEY=${b64(32)}`,
67
- `POSTGRES_PASSWORD=${hex(16)}`,
68
- `S3_ACCESS_KEY_ID=${hex(8)}`,
69
- `S3_SECRET_ACCESS_KEY=${hex(20)}`,
70
- '',
71
- 'S3_BUCKET=uploads',
72
- 'S3_PUBLIC_BUCKET=public',
73
- 'S3_REGION=us-east-1',
74
- '',
75
- '# Host ports. Change these if something already listens on them.',
76
- `POSTGRES_PORT=${context.postgresPort}`,
77
- 'MINIO_PORT=59000',
78
- 'MINIO_CONSOLE_PORT=59001',
79
- '',
80
- '# First operator account, created on first boot.',
81
- `SEED_ADMIN_EMAIL=${context.adminEmail}`,
82
- `SEED_ADMIN_PASSWORD=${context.adminPassword}`,
83
- '',
84
- ].join('\n');
85
- }
86
-
87
- function renderCompose(context) {
88
- const buildContext = `${context.repo}#${context.ref}`;
89
- return `# Generated by \`motherbase init\`.
90
- #
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.
97
- services:
98
- 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']
103
- restart: unless-stopped
104
- environment:
105
- POSTGRES_USER: postgres
106
- POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
107
- POSTGRES_DB: app
108
- ports:
109
- # Loopback only: reachable by psql, Prisma, Drizzle or a BI client on this
110
- # machine, and by nothing on the network.
111
- - '127.0.0.1:\${POSTGRES_PORT:-55432}:5432'
112
- volumes:
113
- - db_data:/var/lib/postgresql/data
114
- healthcheck:
115
- test: ['CMD-SHELL', 'pg_isready -U postgres']
116
- interval: 5s
117
- timeout: 5s
118
- retries: 10
119
-
120
- minio:
121
- image: alpine/minio:RELEASE.2025-10-15T17-29-55Z
122
- restart: unless-stopped
123
- user: "0:0"
124
- command: server /data --console-address ":9001"
125
- environment:
126
- MINIO_ROOT_USER: \${S3_ACCESS_KEY_ID}
127
- MINIO_ROOT_PASSWORD: \${S3_SECRET_ACCESS_KEY}
128
- ports:
129
- - '127.0.0.1:\${MINIO_PORT:-59000}:9000'
130
- - '127.0.0.1:\${MINIO_CONSOLE_PORT:-59001}:9001'
131
- volumes:
132
- - minio_data:/data
133
- healthcheck:
134
- test: ['CMD-SHELL', 'wget -qO- http://localhost:9000/minio/health/live >/dev/null']
135
- interval: 5s
136
- timeout: 5s
137
- retries: 10
138
-
139
- # One-shot: a private bucket and a separate anonymously-downloadable one.
140
- createbuckets:
141
- image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z
142
- depends_on:
143
- minio:
144
- condition: service_healthy
145
- entrypoint: >
146
- /bin/sh -c "
147
- mc alias set local http://minio:9000 \${S3_ACCESS_KEY_ID} \${S3_SECRET_ACCESS_KEY};
148
- mc mb --ignore-existing local/\${S3_BUCKET:-uploads};
149
- mc anonymous set none local/\${S3_BUCKET:-uploads};
150
- mc mb --ignore-existing local/\${S3_PUBLIC_BUCKET:-public};
151
- mc anonymous set download local/\${S3_PUBLIC_BUCKET:-public};
152
- echo 'buckets ready';
153
- "
154
-
155
- api:
156
- build:
157
- context: ${buildContext}
158
- restart: unless-stopped
159
- depends_on:
160
- db:
161
- condition: service_healthy
162
- minio:
163
- condition: service_healthy
164
- createbuckets:
165
- condition: service_completed_successfully
166
- env_file:
167
- - ${ENV_FILE}
168
- # Container-internal wiring, overriding the localhost values in .env so the
169
- # same file works for both host tooling and the containers.
170
- environment:
171
- NODE_ENV: production
172
- PORT: 5001
173
- DATABASE_URL: postgresql://postgres:\${POSTGRES_PASSWORD}@db:5432/app?schema=public
174
- STORAGE_DRIVER: s3
175
- S3_ENDPOINT: http://minio:9000
176
- DB_PORT: \${POSTGRES_PORT:-55432}
177
- ports:
178
- - '\${PORT:-5001}:5001'
179
-
180
- volumes:
181
- db_data:
182
- minio_data:
183
- `;
184
- }
185
-
186
- const init = {
187
- summary: 'Scaffold a runnable local stack (compose file, .env, generated secrets)',
188
- usage: 'motherbase init [dir] [--port 5001] [--repo URL] [--ref BRANCH] [--force]',
189
- async run(ctx, args, flags = {}) {
190
- const dir = args[0] || '.';
191
- const target = path.resolve(ctx.cwd, dir);
192
- fs.mkdirSync(target, { recursive: true });
193
-
194
- const context = {
195
- port: Number(flags.port) || 5001,
196
- postgresPort: Number(flags['postgres-port']) || 55432,
197
- repo: flags.repo || DEFAULT_REPO,
198
- ref: flags.ref || DEFAULT_REF,
199
- adminEmail: flags['admin-email'] || 'admin@example.com',
200
- // Generated, never a placeholder: a scaffold that ships `change-me` is a
201
- // scaffold that reaches production with `change-me`.
202
- adminPassword: flags['admin-password'] || crypto.randomBytes(12).toString('base64url'),
203
- };
204
-
205
- const written = [];
206
- const skipped = [];
207
- for (const [name, render] of [[ENV_FILE, renderEnv], [COMPOSE_FILE, renderCompose]]) {
208
- const file = path.join(target, name);
209
- // Never silently overwrite: .env holds the only copy of secrets that
210
- // existing data was encrypted with.
211
- if (fs.existsSync(file) && !flags.force) {
212
- skipped.push(name);
213
- continue;
214
- }
215
- fs.writeFileSync(file, render(context), name === ENV_FILE ? { mode: 0o600 } : undefined);
216
- written.push(name);
217
- }
218
-
219
- ui.success(`Initialized Motherbase project at ${target}`);
220
- if (written.length) ui.info(`wrote ${written.join(', ')}`);
221
- if (skipped.length) {
222
- ui.warn(`kept existing ${skipped.join(', ')} (use --force to overwrite, which discards their secrets)`);
223
- }
224
- if (written.includes(ENV_FILE)) {
225
- ui.print('');
226
- ui.print(` Studio: http://localhost:${context.port}`);
227
- ui.print(` Sign in: ${context.adminEmail} / ${context.adminPassword}`);
228
- ui.print('');
229
- ui.print(' Next:');
230
- if (dir !== '.') ui.print(` cd ${dir}`);
231
- ui.print(' motherbase start # first run builds the images, give it a few minutes');
232
- ui.print('');
233
- }
234
- },
235
- };
236
-
237
- const start = {
238
- summary: 'Start the local docker compose stack',
239
- usage: 'motherbase start',
240
- async run(ctx) {
241
- assertStack(ctx);
242
- await dockerCompose(['up', '-d']);
243
- },
244
- };
245
-
246
- const stop = {
247
- summary: 'Stop the local docker compose stack',
248
- usage: 'motherbase stop',
249
- async run(ctx) {
250
- assertStack(ctx);
251
- await dockerCompose(['down']);
252
- },
253
- };
254
-
255
- const deploy = {
256
- summary: 'Rebuild and start the local docker compose stack',
257
- usage: 'motherbase deploy',
258
- async run(ctx) {
259
- assertStack(ctx);
260
- await dockerCompose(['up', '-d', '--build']);
261
- },
262
- };
263
-
264
- const logs = {
265
- summary: 'Follow logs from the local docker compose stack',
266
- usage: 'motherbase logs [service...]',
267
- async run(ctx, args) {
268
- assertStack(ctx);
269
- await dockerCompose(['logs', '-f', ...args]);
270
- },
271
- };
272
-
273
- module.exports = { init, start, stop, deploy, logs, renderEnv, renderCompose };
1
+ 'use strict';
2
+
3
+ // Local self-host workflow: thin wrappers over `docker compose`, plus `init`
4
+ // scaffolding.
5
+ //
6
+ // `init` used to write a `.env` and nothing else, which left a directory that
7
+ // `motherbase start` could not start: `docker compose up` in a folder with no
8
+ // compose file is a raw Docker error, and that was the first thing a new user
9
+ // saw. It now writes a self-contained stack that runs.
10
+
11
+ const crypto = require('crypto');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { spawn } = require('child_process');
15
+ const ui = require('../ui');
16
+ const { CliError } = require('../context');
17
+
18
+ const COMPOSE_FILE = 'docker-compose.yml';
19
+ const ENV_FILE = '.env';
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';
26
+ const DEFAULT_REF = 'master';
27
+
28
+ function dockerCompose(composeArgs) {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn('docker', ['compose', ...composeArgs], { stdio: 'inherit', windowsHide: true });
31
+ child.on('error', err => reject(new CliError(`failed to run docker: ${err.message}`)));
32
+ child.on('close', code => (code === 0 ? resolve() : reject(new CliError(`docker compose exited with code ${code}`))));
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Refuse to run compose from a directory that has no stack, and say what to do.
38
+ * Without this the user gets Docker's own "no configuration file provided",
39
+ * which does not mention `motherbase init`.
40
+ */
41
+ function assertStack(ctx) {
42
+ const file = path.join(ctx.cwd, COMPOSE_FILE);
43
+ if (!fs.existsSync(file)) {
44
+ throw new CliError(
45
+ `no ${COMPOSE_FILE} here. Run \`motherbase init\` first, or cd into a directory that has one.`,
46
+ );
47
+ }
48
+ }
49
+
50
+ const hex = n => crypto.randomBytes(n).toString('hex');
51
+ const b64 = n => crypto.randomBytes(n).toString('base64');
52
+
53
+ function renderEnv(context) {
54
+ return [
55
+ '# Generated by `motherbase init`. Secrets below are unique to this install.',
56
+ '# Keep this file out of version control.',
57
+ '',
58
+ `PORT=${context.port}`,
59
+ `BASE_HOST=localhost:${context.port}`,
60
+ 'NODE_ENV=production',
61
+ '',
62
+ '# Secrets. Regenerating any of these invalidates what it protects:',
63
+ '# JWT_SECRET signs sessions, CONFIG_ENCRYPTION_KEY encrypts Studio-managed',
64
+ '# secrets (SMTP, OAuth credentials) at rest.',
65
+ `JWT_SECRET=${hex(32)}`,
66
+ `CONFIG_ENCRYPTION_KEY=${b64(32)}`,
67
+ `POSTGRES_PASSWORD=${hex(16)}`,
68
+ `S3_ACCESS_KEY_ID=${hex(8)}`,
69
+ `S3_SECRET_ACCESS_KEY=${hex(20)}`,
70
+ '',
71
+ 'S3_BUCKET=uploads',
72
+ 'S3_PUBLIC_BUCKET=public',
73
+ 'S3_REGION=us-east-1',
74
+ '',
75
+ '# Host ports. Change these if something already listens on them.',
76
+ `POSTGRES_PORT=${context.postgresPort}`,
77
+ 'MINIO_PORT=59000',
78
+ 'MINIO_CONSOLE_PORT=59001',
79
+ '',
80
+ '# First operator account, created on first boot.',
81
+ `SEED_ADMIN_EMAIL=${context.adminEmail}`,
82
+ `SEED_ADMIN_PASSWORD=${context.adminPassword}`,
83
+ '',
84
+ ].join('\n');
85
+ }
86
+
87
+ function renderCompose(context) {
88
+ const buildContext = `${context.repo}#${context.ref}`;
89
+ return `# Generated by \`motherbase init\`.
90
+ #
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.
97
+ services:
98
+ 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']
103
+ restart: unless-stopped
104
+ environment:
105
+ POSTGRES_USER: postgres
106
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
107
+ POSTGRES_DB: app
108
+ ports:
109
+ # Loopback only: reachable by psql, Prisma, Drizzle or a BI client on this
110
+ # machine, and by nothing on the network.
111
+ - '127.0.0.1:\${POSTGRES_PORT:-55432}:5432'
112
+ volumes:
113
+ - db_data:/var/lib/postgresql/data
114
+ healthcheck:
115
+ test: ['CMD-SHELL', 'pg_isready -U postgres']
116
+ interval: 5s
117
+ timeout: 5s
118
+ retries: 10
119
+
120
+ minio:
121
+ image: alpine/minio:RELEASE.2025-10-15T17-29-55Z
122
+ restart: unless-stopped
123
+ user: "0:0"
124
+ command: server /data --console-address ":9001"
125
+ environment:
126
+ MINIO_ROOT_USER: \${S3_ACCESS_KEY_ID}
127
+ MINIO_ROOT_PASSWORD: \${S3_SECRET_ACCESS_KEY}
128
+ ports:
129
+ - '127.0.0.1:\${MINIO_PORT:-59000}:9000'
130
+ - '127.0.0.1:\${MINIO_CONSOLE_PORT:-59001}:9001'
131
+ volumes:
132
+ - minio_data:/data
133
+ healthcheck:
134
+ test: ['CMD-SHELL', 'wget -qO- http://localhost:9000/minio/health/live >/dev/null']
135
+ interval: 5s
136
+ timeout: 5s
137
+ retries: 10
138
+
139
+ # One-shot: a private bucket and a separate anonymously-downloadable one.
140
+ createbuckets:
141
+ image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z
142
+ depends_on:
143
+ minio:
144
+ condition: service_healthy
145
+ entrypoint: >
146
+ /bin/sh -c "
147
+ mc alias set local http://minio:9000 \${S3_ACCESS_KEY_ID} \${S3_SECRET_ACCESS_KEY};
148
+ mc mb --ignore-existing local/\${S3_BUCKET:-uploads};
149
+ mc anonymous set none local/\${S3_BUCKET:-uploads};
150
+ mc mb --ignore-existing local/\${S3_PUBLIC_BUCKET:-public};
151
+ mc anonymous set download local/\${S3_PUBLIC_BUCKET:-public};
152
+ echo 'buckets ready';
153
+ "
154
+
155
+ api:
156
+ build:
157
+ context: ${buildContext}
158
+ restart: unless-stopped
159
+ depends_on:
160
+ db:
161
+ condition: service_healthy
162
+ minio:
163
+ condition: service_healthy
164
+ createbuckets:
165
+ condition: service_completed_successfully
166
+ env_file:
167
+ - ${ENV_FILE}
168
+ # Container-internal wiring, overriding the localhost values in .env so the
169
+ # same file works for both host tooling and the containers.
170
+ environment:
171
+ NODE_ENV: production
172
+ PORT: 5001
173
+ DATABASE_URL: postgresql://postgres:\${POSTGRES_PASSWORD}@db:5432/app?schema=public
174
+ STORAGE_DRIVER: s3
175
+ S3_ENDPOINT: http://minio:9000
176
+ DB_PORT: \${POSTGRES_PORT:-55432}
177
+ ports:
178
+ - '\${PORT:-5001}:5001'
179
+
180
+ volumes:
181
+ db_data:
182
+ minio_data:
183
+ `;
184
+ }
185
+
186
+ // Values that end up unquoted in `.env` and in the compose `context:` line. A
187
+ // newline would inject a second variable or key; the character sets below are
188
+ // what these fields legitimately contain.
189
+ const INIT_FLAG_PATTERNS = {
190
+ 'admin-email': /^[^\s"'`$#\\]+$/,
191
+ 'admin-password': /^[^\s"'`$#\\]+$/,
192
+ repo: /^[\w.:/@~+-]+$/,
193
+ ref: /^[\w./-]+$/,
194
+ };
195
+
196
+ function validateInitFlags(flags) {
197
+ for (const [name, pattern] of Object.entries(INIT_FLAG_PATTERNS)) {
198
+ const value = flags[name];
199
+ if (value === undefined) continue;
200
+ if (typeof value !== 'string' || !pattern.test(value)) {
201
+ throw new CliError(`Invalid --${name}: contains whitespace, quotes or other unsafe characters`);
202
+ }
203
+ }
204
+ }
205
+
206
+ const init = {
207
+ summary: 'Scaffold a runnable local stack (compose file, .env, generated secrets)',
208
+ usage: 'motherbase init [dir] [--port 5001] [--repo URL] [--ref BRANCH] [--force]',
209
+ async run(ctx, args, flags = {}) {
210
+ validateInitFlags(flags);
211
+ const dir = args[0] || '.';
212
+ const target = path.resolve(ctx.cwd, dir);
213
+ fs.mkdirSync(target, { recursive: true });
214
+
215
+ const context = {
216
+ port: Number(flags.port) || 5001,
217
+ postgresPort: Number(flags['postgres-port']) || 55432,
218
+ repo: flags.repo || DEFAULT_REPO,
219
+ ref: flags.ref || DEFAULT_REF,
220
+ adminEmail: flags['admin-email'] || 'admin@example.com',
221
+ // Generated, never a placeholder: a scaffold that ships `change-me` is a
222
+ // scaffold that reaches production with `change-me`.
223
+ adminPassword: flags['admin-password'] || crypto.randomBytes(12).toString('base64url'),
224
+ };
225
+
226
+ const written = [];
227
+ const skipped = [];
228
+ for (const [name, render] of [[ENV_FILE, renderEnv], [COMPOSE_FILE, renderCompose]]) {
229
+ const file = path.join(target, name);
230
+ // Never silently overwrite: .env holds the only copy of secrets that
231
+ // existing data was encrypted with.
232
+ if (fs.existsSync(file) && !flags.force) {
233
+ skipped.push(name);
234
+ continue;
235
+ }
236
+ fs.writeFileSync(file, render(context), name === ENV_FILE ? { mode: 0o600 } : undefined);
237
+ written.push(name);
238
+ }
239
+
240
+ ui.success(`Initialized Motherbase project at ${target}`);
241
+ if (written.length) ui.info(`wrote ${written.join(', ')}`);
242
+ if (skipped.length) {
243
+ ui.warn(`kept existing ${skipped.join(', ')} (use --force to overwrite, which discards their secrets)`);
244
+ }
245
+ if (written.includes(ENV_FILE)) {
246
+ ui.print('');
247
+ ui.print(` Studio: http://localhost:${context.port}`);
248
+ ui.print(` Sign in: ${context.adminEmail} / ${context.adminPassword}`);
249
+ ui.print('');
250
+ ui.print(' Next:');
251
+ if (dir !== '.') ui.print(` cd ${dir}`);
252
+ ui.print(' motherbase start # first run builds the images, give it a few minutes');
253
+ ui.print('');
254
+ }
255
+ },
256
+ };
257
+
258
+ const start = {
259
+ summary: 'Start the local docker compose stack',
260
+ usage: 'motherbase start',
261
+ async run(ctx) {
262
+ assertStack(ctx);
263
+ await dockerCompose(['up', '-d']);
264
+ },
265
+ };
266
+
267
+ const stop = {
268
+ summary: 'Stop the local docker compose stack',
269
+ usage: 'motherbase stop',
270
+ async run(ctx) {
271
+ assertStack(ctx);
272
+ await dockerCompose(['down']);
273
+ },
274
+ };
275
+
276
+ const deploy = {
277
+ summary: 'Rebuild and start the local docker compose stack',
278
+ usage: 'motherbase deploy',
279
+ async run(ctx) {
280
+ assertStack(ctx);
281
+ await dockerCompose(['up', '-d', '--build']);
282
+ },
283
+ };
284
+
285
+ const logs = {
286
+ summary: 'Follow logs from the local docker compose stack',
287
+ usage: 'motherbase logs [service...]',
288
+ async run(ctx, args) {
289
+ assertStack(ctx);
290
+ await dockerCompose(['logs', '-f', ...args]);
291
+ },
292
+ };
293
+
294
+ module.exports = { init, start, stop, deploy, logs, renderEnv, renderCompose, validateInitFlags };
@@ -1,39 +1,39 @@
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: 'Log in to a Motherbase Console (email/password → session token)',
11
- usage: 'motherbase login [--url <consoleUrl>] [--email <email>]',
12
- async run(ctx, args, flags) {
13
- const consoleUrl = config.normalizeUrl(
14
- flags.url || ctx.env.MOTHERBASE_CONSOLE_URL || ctx.consoleUrl || '',
15
- );
16
- if (!consoleUrl) {
17
- throw new CliError('Console URL required. Use --url <consoleUrl> or set MOTHERBASE_CONSOLE_URL.');
18
- }
19
-
20
- // Non-interactive: accept a token directly (CI).
21
- if (flags.token) {
22
- const email = flags.email || 'token';
23
- config.saveConsoleToken(consoleUrl, flags.token, email);
24
- ui.success(`Stored token for ${consoleUrl}`);
25
- return;
26
- }
27
-
28
- const email = flags.email || (await prompt.question('Email: '));
29
- if (!email) throw new CliError('email is required');
30
- const password = flags.password || (await prompt.password('Password: '));
31
- if (!password) throw new CliError('password is required');
32
-
33
- const data = await api.loginRequest(consoleUrl, email, password);
34
- const token = data && data.token;
35
- if (!token) throw new CliError('login succeeded but no token was returned');
36
- config.saveConsoleToken(consoleUrl, token, email);
37
- ui.success(`Logged in to ${consoleUrl} as ${email}`);
38
- },
39
- };
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: 'Log in to a Motherbase Console (email/password → session token)',
11
+ usage: 'motherbase login [--url <consoleUrl>] [--email <email>]',
12
+ async run(ctx, args, flags) {
13
+ const consoleUrl = config.normalizeUrl(
14
+ flags.url || ctx.env.MOTHERBASE_CONSOLE_URL || ctx.consoleUrl || '',
15
+ );
16
+ if (!consoleUrl) {
17
+ throw new CliError('Console URL required. Use --url <consoleUrl> or set MOTHERBASE_CONSOLE_URL.');
18
+ }
19
+
20
+ // Non-interactive: accept a token directly (CI).
21
+ if (flags.token) {
22
+ const email = flags.email || 'token';
23
+ config.saveConsoleToken(consoleUrl, flags.token, email);
24
+ ui.success(`Stored token for ${consoleUrl}`);
25
+ return;
26
+ }
27
+
28
+ const email = flags.email || (await prompt.question('Email: '));
29
+ if (!email) throw new CliError('email is required');
30
+ const password = (typeof flags.password === 'string' && flags.password) || (await prompt.password('Password: '));
31
+ if (!password) throw new CliError('password is required');
32
+
33
+ const data = await api.loginRequest(consoleUrl, email, password);
34
+ const token = data && data.token;
35
+ if (!token) throw new CliError('login succeeded but no token was returned');
36
+ config.saveConsoleToken(consoleUrl, token, email);
37
+ ui.success(`Logged in to ${consoleUrl} as ${email}`);
38
+ },
39
+ };