@motherbase/cli 0.2.5 → 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.
- package/LICENSE +21 -0
- package/README.md +2 -1
- package/bin/.motherbase/config.json +5 -0
- package/package.json +35 -26
- package/src/api.js +256 -241
- package/src/commands/_util.js +14 -1
- package/src/commands/db-catalog.js +326 -326
- package/src/commands/functions.js +304 -297
- package/src/commands/gen.js +7 -1
- package/src/commands/local.js +371 -273
- package/src/commands/login.js +45 -39
- package/src/commands/migrate.js +202 -179
- package/src/commands/migration.js +108 -108
- package/src/commands/projects.js +2 -1
- package/src/commands/storage.js +2 -2
- package/src/commands/users.js +142 -142
- package/src/context.js +6 -2
- package/src/docs.js +68 -0
- package/src/main.js +224 -194
- package/src/mcp/protocol.js +4 -1
- package/src/migration/commands.js +375 -375
- package/src/migration/files.js +258 -245
package/src/commands/local.js
CHANGED
|
@@ -1,273 +1,371 @@
|
|
|
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
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
'
|
|
61
|
-
'',
|
|
62
|
-
'
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
'',
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'',
|
|
75
|
-
'#
|
|
76
|
-
|
|
77
|
-
'
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
// 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/BoilerPlate-Backend.git';
|
|
31
|
+
const DEFAULT_REF = 'master';
|
|
32
|
+
|
|
33
|
+
function dockerCompose(composeArgs) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const child = spawn('docker', ['compose', ...composeArgs], { stdio: 'inherit', windowsHide: true });
|
|
36
|
+
child.on('error', err => reject(new CliError(`failed to run docker: ${err.message}`)));
|
|
37
|
+
child.on('close', code => (code === 0 ? resolve() : reject(new CliError(`docker compose exited with code ${code}`))));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Refuse to run compose from a directory that has no stack, and say what to do.
|
|
43
|
+
* Without this the user gets Docker's own "no configuration file provided",
|
|
44
|
+
* which does not mention `motherbase init`.
|
|
45
|
+
*/
|
|
46
|
+
function assertStack(ctx) {
|
|
47
|
+
const file = path.join(ctx.cwd, COMPOSE_FILE);
|
|
48
|
+
if (!fs.existsSync(file)) {
|
|
49
|
+
throw new CliError(
|
|
50
|
+
`no ${COMPOSE_FILE} here. Run \`motherbase init\` first, or cd into a directory that has one.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const hex = n => crypto.randomBytes(n).toString('hex');
|
|
56
|
+
const b64 = n => crypto.randomBytes(n).toString('base64');
|
|
57
|
+
|
|
58
|
+
function renderEnv(context) {
|
|
59
|
+
return [
|
|
60
|
+
'# Generated by `motherbase init`. Secrets below are unique to this install.',
|
|
61
|
+
'# Keep this file out of version control.',
|
|
62
|
+
'',
|
|
63
|
+
`PORT=${context.port}`,
|
|
64
|
+
`BASE_HOST=localhost:${context.port}`,
|
|
65
|
+
'NODE_ENV=production',
|
|
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
|
+
'',
|
|
75
|
+
'# Secrets. Regenerating any of these invalidates what it protects:',
|
|
76
|
+
'# JWT_SECRET signs sessions, CONFIG_ENCRYPTION_KEY encrypts Studio-managed',
|
|
77
|
+
'# secrets (SMTP, OAuth credentials) at rest.',
|
|
78
|
+
`JWT_SECRET=${hex(32)}`,
|
|
79
|
+
`CONFIG_ENCRYPTION_KEY=${b64(32)}`,
|
|
80
|
+
`POSTGRES_PASSWORD=${hex(16)}`,
|
|
81
|
+
`S3_ACCESS_KEY_ID=${hex(8)}`,
|
|
82
|
+
`S3_SECRET_ACCESS_KEY=${hex(20)}`,
|
|
83
|
+
`EDGE_RUNTIME_TOKEN=${hex(24)}`,
|
|
84
|
+
'',
|
|
85
|
+
'S3_BUCKET=uploads',
|
|
86
|
+
'S3_PUBLIC_BUCKET=public',
|
|
87
|
+
'S3_REGION=us-east-1',
|
|
88
|
+
'',
|
|
89
|
+
'# Host ports. Change these if something already listens on them.',
|
|
90
|
+
`POSTGRES_PORT=${context.postgresPort}`,
|
|
91
|
+
'MINIO_PORT=59000',
|
|
92
|
+
'MINIO_CONSOLE_PORT=59001',
|
|
93
|
+
'',
|
|
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',
|
|
96
|
+
`SEED_ADMIN_EMAIL=${context.adminEmail}`,
|
|
97
|
+
`SEED_ADMIN_PASSWORD=${context.adminPassword}`,
|
|
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
|
+
: []),
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderCompose(context) {
|
|
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.`;
|
|
126
|
+
return `# Generated by \`motherbase init\`.
|
|
127
|
+
#
|
|
128
|
+
${header}
|
|
129
|
+
services:
|
|
130
|
+
db:
|
|
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']
|
|
132
|
+
restart: unless-stopped
|
|
133
|
+
environment:
|
|
134
|
+
POSTGRES_USER: postgres
|
|
135
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
|
|
136
|
+
POSTGRES_DB: app
|
|
137
|
+
ports:
|
|
138
|
+
# Loopback only: reachable by psql, Prisma, Drizzle or a BI client on this
|
|
139
|
+
# machine, and by nothing on the network.
|
|
140
|
+
- '127.0.0.1:\${POSTGRES_PORT:-55432}:5432'
|
|
141
|
+
volumes:
|
|
142
|
+
- db_data:/var/lib/postgresql/data
|
|
143
|
+
healthcheck:
|
|
144
|
+
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
145
|
+
interval: 5s
|
|
146
|
+
timeout: 5s
|
|
147
|
+
retries: 10
|
|
148
|
+
|
|
149
|
+
minio:
|
|
150
|
+
image: alpine/minio:RELEASE.2025-10-15T17-29-55Z
|
|
151
|
+
restart: unless-stopped
|
|
152
|
+
user: "0:0"
|
|
153
|
+
command: server /data --console-address ":9001"
|
|
154
|
+
environment:
|
|
155
|
+
MINIO_ROOT_USER: \${S3_ACCESS_KEY_ID}
|
|
156
|
+
MINIO_ROOT_PASSWORD: \${S3_SECRET_ACCESS_KEY}
|
|
157
|
+
ports:
|
|
158
|
+
- '127.0.0.1:\${MINIO_PORT:-59000}:9000'
|
|
159
|
+
- '127.0.0.1:\${MINIO_CONSOLE_PORT:-59001}:9001'
|
|
160
|
+
volumes:
|
|
161
|
+
- minio_data:/data
|
|
162
|
+
healthcheck:
|
|
163
|
+
test: ['CMD-SHELL', 'wget -qO- http://localhost:9000/minio/health/live >/dev/null']
|
|
164
|
+
interval: 5s
|
|
165
|
+
timeout: 5s
|
|
166
|
+
retries: 10
|
|
167
|
+
|
|
168
|
+
# One-shot: a private bucket and a separate anonymously-downloadable one.
|
|
169
|
+
createbuckets:
|
|
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
|
|
175
|
+
depends_on:
|
|
176
|
+
minio:
|
|
177
|
+
condition: service_healthy
|
|
178
|
+
entrypoint: >
|
|
179
|
+
/bin/sh -c "
|
|
180
|
+
mc alias set local http://minio:9000 \${S3_ACCESS_KEY_ID} \${S3_SECRET_ACCESS_KEY};
|
|
181
|
+
mc mb --ignore-existing local/\${S3_BUCKET:-uploads};
|
|
182
|
+
mc anonymous set none local/\${S3_BUCKET:-uploads};
|
|
183
|
+
mc mb --ignore-existing local/\${S3_PUBLIC_BUCKET:-public};
|
|
184
|
+
mc anonymous set download local/\${S3_PUBLIC_BUCKET:-public};
|
|
185
|
+
echo 'buckets ready';
|
|
186
|
+
"
|
|
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
|
+
|
|
207
|
+
api:
|
|
208
|
+
${fromImages ? ` image: ${image('api')}\n` : source(null)} restart: unless-stopped
|
|
209
|
+
depends_on:
|
|
210
|
+
db:
|
|
211
|
+
condition: service_healthy
|
|
212
|
+
minio:
|
|
213
|
+
condition: service_healthy
|
|
214
|
+
createbuckets:
|
|
215
|
+
condition: service_completed_successfully
|
|
216
|
+
edge-runtime:
|
|
217
|
+
condition: service_started
|
|
218
|
+
env_file:
|
|
219
|
+
- ${ENV_FILE}
|
|
220
|
+
# Container-internal wiring, overriding the localhost values in .env so the
|
|
221
|
+
# same file works for both host tooling and the containers.
|
|
222
|
+
environment:
|
|
223
|
+
NODE_ENV: production
|
|
224
|
+
PORT: 5001
|
|
225
|
+
DATABASE_URL: postgresql://postgres:\${POSTGRES_PASSWORD}@db:5432/app?schema=public
|
|
226
|
+
STORAGE_DRIVER: s3
|
|
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}
|
|
231
|
+
DB_PORT: \${POSTGRES_PORT:-55432}
|
|
232
|
+
EDGE_RUNTIME_URL: http://edge-runtime:9000
|
|
233
|
+
EDGE_RUNTIME_CALLBACK_URL: http://api:5001
|
|
234
|
+
ports:
|
|
235
|
+
- '\${PORT:-5001}:5001'
|
|
236
|
+
networks:
|
|
237
|
+
- default
|
|
238
|
+
- edge
|
|
239
|
+
|
|
240
|
+
volumes:
|
|
241
|
+
db_data:
|
|
242
|
+
minio_data:
|
|
243
|
+
|
|
244
|
+
networks:
|
|
245
|
+
edge:
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Values that end up unquoted in `.env` and in the compose `context:` line. A
|
|
250
|
+
// newline would inject a second variable or key; the character sets below are
|
|
251
|
+
// what these fields legitimately contain.
|
|
252
|
+
const INIT_FLAG_PATTERNS = {
|
|
253
|
+
'admin-email': /^[^\s"'`$#\\]+$/,
|
|
254
|
+
'admin-password': /^[^\s"'`$#\\]+$/,
|
|
255
|
+
'cors-origins': /^[\w.:/,*-]+$/,
|
|
256
|
+
repo: /^[\w.:/@~+-]+$/,
|
|
257
|
+
ref: /^[\w./-]+$/,
|
|
258
|
+
registry: /^[\w.:/-]+$/,
|
|
259
|
+
tag: /^[\w.-]+$/,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
function validateInitFlags(flags) {
|
|
263
|
+
for (const [name, pattern] of Object.entries(INIT_FLAG_PATTERNS)) {
|
|
264
|
+
const value = flags[name];
|
|
265
|
+
if (value === undefined) continue;
|
|
266
|
+
if (typeof value !== 'string' || !pattern.test(value)) {
|
|
267
|
+
throw new CliError(`Invalid --${name}: contains whitespace, quotes or other unsafe characters`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const init = {
|
|
273
|
+
summary: 'Scaffold a runnable local stack (compose file, .env, generated secrets)',
|
|
274
|
+
usage: 'motherbase init [dir] [--port 5001] [--cors-origins http://localhost:3000] [--tag latest] [--repo URL --ref BRANCH] [--force]',
|
|
275
|
+
async run(ctx, args, flags = {}) {
|
|
276
|
+
validateInitFlags(flags);
|
|
277
|
+
const dir = args[0] || '.';
|
|
278
|
+
const target = path.resolve(ctx.cwd, dir);
|
|
279
|
+
fs.mkdirSync(target, { recursive: true });
|
|
280
|
+
|
|
281
|
+
const context = {
|
|
282
|
+
port: Number(flags.port) || 5001,
|
|
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,
|
|
289
|
+
repo: flags.repo || DEFAULT_REPO,
|
|
290
|
+
ref: flags.ref || DEFAULT_REF,
|
|
291
|
+
adminEmail: flags['admin-email'] || 'admin@example.com',
|
|
292
|
+
// Generated, never a placeholder: a scaffold that ships `change-me` is a
|
|
293
|
+
// scaffold that reaches production with `change-me`.
|
|
294
|
+
adminPassword: flags['admin-password'] || crypto.randomBytes(12).toString('base64url'),
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const written = [];
|
|
298
|
+
const skipped = [];
|
|
299
|
+
for (const [name, render] of [[ENV_FILE, renderEnv], [COMPOSE_FILE, renderCompose]]) {
|
|
300
|
+
const file = path.join(target, name);
|
|
301
|
+
// Never silently overwrite: .env holds the only copy of secrets that
|
|
302
|
+
// existing data was encrypted with.
|
|
303
|
+
if (fs.existsSync(file) && !flags.force) {
|
|
304
|
+
skipped.push(name);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
fs.writeFileSync(file, render(context), name === ENV_FILE ? { mode: 0o600 } : undefined);
|
|
308
|
+
written.push(name);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
ui.success(`Initialized Motherbase project at ${target}`);
|
|
312
|
+
if (written.length) ui.info(`wrote ${written.join(', ')}`);
|
|
313
|
+
if (skipped.length) {
|
|
314
|
+
ui.warn(`kept existing ${skipped.join(', ')} (use --force to overwrite, which discards their secrets)`);
|
|
315
|
+
}
|
|
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.
|
|
320
|
+
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}`);
|
|
323
|
+
ui.print('');
|
|
324
|
+
ui.print(' Next:');
|
|
325
|
+
if (dir !== '.') ui.print(` cd ${dir}`);
|
|
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');
|
|
330
|
+
ui.print('');
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const start = {
|
|
336
|
+
summary: 'Start the local docker compose stack',
|
|
337
|
+
usage: 'motherbase start',
|
|
338
|
+
async run(ctx) {
|
|
339
|
+
assertStack(ctx);
|
|
340
|
+
await dockerCompose(['up', '-d']);
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const stop = {
|
|
345
|
+
summary: 'Stop the local docker compose stack',
|
|
346
|
+
usage: 'motherbase stop',
|
|
347
|
+
async run(ctx) {
|
|
348
|
+
assertStack(ctx);
|
|
349
|
+
await dockerCompose(['down']);
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const deploy = {
|
|
354
|
+
summary: 'Rebuild and start the local docker compose stack',
|
|
355
|
+
usage: 'motherbase deploy',
|
|
356
|
+
async run(ctx) {
|
|
357
|
+
assertStack(ctx);
|
|
358
|
+
await dockerCompose(['up', '-d', '--build']);
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const logs = {
|
|
363
|
+
summary: 'Follow logs from the local docker compose stack',
|
|
364
|
+
usage: 'motherbase logs [service...]',
|
|
365
|
+
async run(ctx, args) {
|
|
366
|
+
assertStack(ctx);
|
|
367
|
+
await dockerCompose(['logs', '-f', ...args]);
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
module.exports = { init, start, stop, deploy, logs, renderEnv, renderCompose, validateInitFlags };
|