@motherbase/cli 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -248
- package/package.json +4 -4
- package/src/commands/db-catalog.js +39 -1
- package/src/commands/index.js +1 -0
- package/src/commands/local.js +1 -1
- package/src/commands/users.js +29 -1
- package/src/main.js +145 -66
package/README.md
CHANGED
|
@@ -1,248 +1,254 @@
|
|
|
1
|
-
# Motherbase CLI
|
|
2
|
-
|
|
3
|
-
A Supabase-CLI-like operator CLI for **Motherbase**. It drives both the
|
|
4
|
-
**Platform Console** (account + multi-project lifecycle) and a **linked project's
|
|
5
|
-
admin API** through the Console proxy, or a single project API directly.
|
|
6
|
-
|
|
7
|
-
Zero runtime dependencies. Node ≥ 18 (uses built-in `fetch`, `readline`, `fs`,
|
|
8
|
-
`child_process` only).
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
node bin/motherbase.js --help # auto-generated help
|
|
12
|
-
npm link # or: npm i -g @motherbase/cli
|
|
13
|
-
motherbase login --url https://console.example.com
|
|
14
|
-
motherbase link # pick a project → writes ./.motherbase/config.json
|
|
15
|
-
motherbase db tables
|
|
16
|
-
motherbase functions deploy --name hello --route hello --file ./hello.js
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Connection model
|
|
20
|
-
|
|
21
|
-
1. **Console / account**, `motherbase login [--url <consoleUrl>]` performs a real
|
|
22
|
-
Console login (`POST /api/platform/login`) and stores the session token.
|
|
23
|
-
2. **Linked project**, `motherbase link [project]` records which project this
|
|
24
|
-
directory targets. All resource commands then route through the Console proxy
|
|
25
|
-
(`/api/projects/:id/admin/*`).
|
|
26
|
-
3. **Direct mode (CI / single stack)**. Set `MOTHERBASE_URL` +
|
|
27
|
-
`MOTHERBASE_ADMIN_TOKEN` to talk straight to one project's `/api/admin/*`.
|
|
28
|
-
|
|
29
|
-
### Configuration files
|
|
30
|
-
|
|
31
|
-
**Global credentials**: `~/.motherbase/config.json` (contains tokens, written `0o600`):
|
|
32
|
-
|
|
33
|
-
```json
|
|
34
|
-
{
|
|
35
|
-
"consoles": {
|
|
36
|
-
"https://console.example.com": { "token": "…", "email": "you@example.com" }
|
|
37
|
-
},
|
|
38
|
-
"current": "https://console.example.com"
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Project link**: `./.motherbase/config.json` (versionable, non-secret):
|
|
43
|
-
|
|
44
|
-
```json
|
|
45
|
-
{ "consoleUrl": "https://console.example.com", "projectId": "my-app", "projectName": "my-app" }
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Resolution precedence
|
|
49
|
-
|
|
50
|
-
`flags > env > config`, per value:
|
|
51
|
-
|
|
52
|
-
| Value | Flag | Env | Config |
|
|
53
|
-
| ----------- | ----------------- | ------------------------- | ------ |
|
|
54
|
-
| console URL | `--url/-u` | `MOTHERBASE_CONSOLE_URL` | link / global `current` |
|
|
55
|
-
| token | `--token` | `MOTHERBASE_TOKEN` | global console entry |
|
|
56
|
-
| project | `--project/-p` | `MOTHERBASE_PROJECT` | link |
|
|
57
|
-
| direct API | (| `MOTHERBASE_URL` + `MOTHERBASE_ADMIN_TOKEN` |) |
|
|
58
|
-
|
|
59
|
-
### Auth headers
|
|
60
|
-
|
|
61
|
-
The console session token is sent as `Authorization: Bearer <token>` on every
|
|
62
|
-
request. Console requests hit `/api/platform/*`; admin requests hit the proxy at
|
|
63
|
-
`/api/projects/:id/admin/*` (which validates the session, then injects its own
|
|
64
|
-
`X-Platform-Token` to the project). In direct mode the admin token is the Bearer.
|
|
65
|
-
|
|
66
|
-
## Commands
|
|
67
|
-
|
|
68
|
-
Global flags: `-o/--output pretty|json|yaml|env` (`--json` is `-o json`),
|
|
69
|
-
`--yes/-y`, `--project/-p/--project-ref <id>`, `--url/-u <consoleUrl>`,
|
|
70
|
-
`--token <t>`, `--workdir <dir>`, `--debug` (one stderr line per HTTP request),
|
|
71
|
-
`-h/--help`, `-v/--version`.
|
|
72
|
-
|
|
73
|
-
Commands follow the Supabase CLI's names. Where Motherbase already had its own
|
|
74
|
-
name, both work: `projects api-keys` / `keys list`, `backups list|restore` /
|
|
75
|
-
`db backups|restore`, `functions download` / `functions pull`, `db lint` /
|
|
76
|
-
`rls lint`, `gen types --lang <l>` / `gen types <l>`.
|
|
77
|
-
|
|
78
|
-
- **Connection**: `login`, `logout`, `whoami`, `link`, `unlink`, `status`
|
|
79
|
-
- **Orgs & projects (Console)**: `orgs list|create`, `projects list|create|start|stop|restart|update|rebuild|delete|logs|info|limits|api-keys`
|
|
80
|
-
- **Branches (Console)**: `branches create <name> [--with-data] [--git-branch <ref>]`, `branches list|get|update|delete|pause|unpause|reset|merge|rebase|switch <name>`
|
|
81
|
-
- **Domains (Console)**: `domains create --custom-hostname <host>`, `domains get|reverify|activate|delete`, `vanity-subdomains get|check-availability|activate --desired-subdomain <name>|delete`
|
|
82
|
-
- **Database access**: `network-restrictions get|update --db-allow-cidr <cidr> ...`, `ssl-enforcement get|update --enable-db-ssl-enforcement|--disable-db-ssl-enforcement`
|
|
83
|
-
- **Local self-host** (docker compose): `init`, `start`, `stop`, `deploy`, `logs`
|
|
84
|
-
- **Database**: `db push|pull|diff|reset|dump|seed`, `db tables|sql|backup|restore|backups|lint`, `backups list|restore`, `test db`
|
|
85
|
-
- **Database objects**: `db indexes list|create|drop`, `db types list|create|add-value|rename-value|drop`, `db roles list|create|alter|drop`, `db column-privileges list|grant|revoke`, `db publications list|create|alter|drop`
|
|
86
|
-
- **Postgres config**: `postgres-config get [--all]`, `postgres-config update --config k=v [--scope database|role|system] [--replace-existing-overrides]`, `postgres-config delete --config k1,k2`
|
|
87
|
-
- **Migrations**: `migration new|list|up|down|repair|squash|fetch|diff|get|create|apply|rollback` (`migrations` is an alias)
|
|
88
|
-
- **Import**: `import --from-supabase <conn> [--preview] [--no-data] [--no-auth]`
|
|
89
|
-
- **RLS**: `rls list|enable|disable`, `rls policy create|delete`
|
|
90
|
-
- **Users**: `users list|get|create|update|delete|reset-password|promote|demote|revoke-sessions|audit`
|
|
91
|
-
- **Storage**: `storage buckets list|create|delete`, `storage ls|cp|mv|rm [-r]` on `ss:///bucket/key` paths
|
|
92
|
-
- **Functions**: `functions list|get|new|deploy|pull|download|serve|delete|invoke|logs`
|
|
93
|
-
- **Secrets**: `secrets list|unset`, `secrets set NAME=VALUE ... [--env-file .env]`
|
|
94
|
-
- **Realtime**: `realtime config|channels`
|
|
95
|
-
- **Webhooks**: `webhooks list|create|delete|test`
|
|
96
|
-
- **Cron**: `cron list|create|delete|toggle`
|
|
97
|
-
- **Queues**: `queues list|enable|expose|create|drop|send|read|archive|purge`
|
|
98
|
-
- **Vault**: `vault list|enable|create|update|reveal|delete`
|
|
99
|
-
- **Wrappers**: `wrappers list|enable|create-server|drop-server|credentials|import|create-table|drop-table`
|
|
100
|
-
- **Config**: `config list|get|set`
|
|
101
|
-
- **Inspect**: `inspect db <query>`, `inspect report`, `inspect logs|stats|performance|index-advisor|requests|advisor|shield`
|
|
102
|
-
- **Codegen**: `gen types [--lang typescript|zod] [--schema public] [--local] [--output <file>] [--check]`
|
|
103
|
-
- **AI agents**: `mcp [--write]`
|
|
104
|
-
- **Keys**: `gen signing-key [--algorithm ES256|RS256|EdDSA] [--append]`, `gen bearer-jwt --sub <user> [--role] [--valid-for 1h]`, `gen keys [-o env]`
|
|
105
|
-
- **Shell**: `completion bash|zsh|fish` (e.g. `source <(motherbase completion bash)`)
|
|
106
|
-
|
|
107
|
-
Run `motherbase <group> -h` for per-group usage.
|
|
108
|
-
|
|
109
|
-
## Migrations and seeds
|
|
110
|
-
|
|
111
|
-
Local files, laid out like a Supabase project's `supabase/` directory:
|
|
112
|
-
|
|
113
|
-
```
|
|
114
|
-
motherbase/
|
|
115
|
-
migrations/<version>_<name>.sql `migration new <name>` stamps the version (UTC)
|
|
116
|
-
seed.sql or db.seed.sql_paths in motherbase/config.json
|
|
117
|
-
tests/*.sql pgTAP, for `motherbase test db`
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
A migration file may end with a `-- @down` line followed by the SQL that undoes
|
|
121
|
-
it; `migration down` needs one. The linked project keeps the history, keyed on
|
|
122
|
-
`version`, and the Studio Migrations page shows the same rows (origin `cli`,
|
|
123
|
-
`studio` or `git`).
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
motherbase migration list # LOCAL | REMOTE side by side
|
|
127
|
-
motherbase db push --dry-run # then without, add --include-seed for the seeds
|
|
128
|
-
motherbase db pull # remote changes -> a new file, recorded as applied
|
|
129
|
-
motherbase db diff -f add_index # the same diff, written as a new migration
|
|
130
|
-
motherbase migration repair 20260101120000 --status reverted
|
|
131
|
-
motherbase db reset --confirm # backup, empty app schemas, replay migrations + seed
|
|
132
|
-
motherbase db dump -f schema.sql --schema-only
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Each pushed file runs in its own transaction; a push stops at the first file
|
|
136
|
-
that fails, and nothing of that file is recorded. `db diff` replays the local
|
|
137
|
-
migrations into a throwaway database on the same server and compares tables,
|
|
138
|
-
columns and indexes: review what it writes. With no local migration yet, `db
|
|
139
|
-
pull` writes the remote schema itself, from `pg_dump`. `db dump` used to take a
|
|
140
|
-
server-side backup; that is `db backup` now.
|
|
141
|
-
|
|
142
|
-
## Type generation
|
|
143
|
-
|
|
144
|
-
Generate TypeScript definitions for the live schema, in the shape supabase-js
|
|
145
|
-
emits, so `createClient<Database>(url, key)` types every row, rejects unknown
|
|
146
|
-
columns and types `.rpc()` calls:
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
motherbase gen types typescript # in a terminal: writes ./database.types.ts
|
|
150
|
-
motherbase gen types typescript > database.types.ts # piped: prints the file
|
|
151
|
-
motherbase gen types typescript --schema shop --output src/shop.types.ts
|
|
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
|
-
motherbase storage
|
|
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
|
-
```bash
|
|
239
|
-
motherbase import --from-supabase "$SUPABASE_DB_URL" --
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
|
|
1
|
+
# Motherbase CLI
|
|
2
|
+
|
|
3
|
+
A Supabase-CLI-like operator CLI for **Motherbase**. It drives both the
|
|
4
|
+
**Platform Console** (account + multi-project lifecycle) and a **linked project's
|
|
5
|
+
admin API** through the Console proxy, or a single project API directly.
|
|
6
|
+
|
|
7
|
+
Zero runtime dependencies. Node ≥ 18 (uses built-in `fetch`, `readline`, `fs`,
|
|
8
|
+
`child_process` only).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
node bin/motherbase.js --help # auto-generated help
|
|
12
|
+
npm link # or: npm i -g @motherbase/cli
|
|
13
|
+
motherbase login --url https://console.example.com
|
|
14
|
+
motherbase link # pick a project → writes ./.motherbase/config.json
|
|
15
|
+
motherbase db tables
|
|
16
|
+
motherbase functions deploy --name hello --route hello --file ./hello.js
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Connection model
|
|
20
|
+
|
|
21
|
+
1. **Console / account**, `motherbase login [--url <consoleUrl>]` performs a real
|
|
22
|
+
Console login (`POST /api/platform/login`) and stores the session token.
|
|
23
|
+
2. **Linked project**, `motherbase link [project]` records which project this
|
|
24
|
+
directory targets. All resource commands then route through the Console proxy
|
|
25
|
+
(`/api/projects/:id/admin/*`).
|
|
26
|
+
3. **Direct mode (CI / single stack)**. Set `MOTHERBASE_URL` +
|
|
27
|
+
`MOTHERBASE_ADMIN_TOKEN` to talk straight to one project's `/api/admin/*`.
|
|
28
|
+
|
|
29
|
+
### Configuration files
|
|
30
|
+
|
|
31
|
+
**Global credentials**: `~/.motherbase/config.json` (contains tokens, written `0o600`):
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"consoles": {
|
|
36
|
+
"https://console.example.com": { "token": "…", "email": "you@example.com" }
|
|
37
|
+
},
|
|
38
|
+
"current": "https://console.example.com"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Project link**: `./.motherbase/config.json` (versionable, non-secret):
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{ "consoleUrl": "https://console.example.com", "projectId": "my-app", "projectName": "my-app" }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Resolution precedence
|
|
49
|
+
|
|
50
|
+
`flags > env > config`, per value:
|
|
51
|
+
|
|
52
|
+
| Value | Flag | Env | Config |
|
|
53
|
+
| ----------- | ----------------- | ------------------------- | ------ |
|
|
54
|
+
| console URL | `--url/-u` | `MOTHERBASE_CONSOLE_URL` | link / global `current` |
|
|
55
|
+
| token | `--token` | `MOTHERBASE_TOKEN` | global console entry |
|
|
56
|
+
| project | `--project/-p` | `MOTHERBASE_PROJECT` | link |
|
|
57
|
+
| direct API | (| `MOTHERBASE_URL` + `MOTHERBASE_ADMIN_TOKEN` |) |
|
|
58
|
+
|
|
59
|
+
### Auth headers
|
|
60
|
+
|
|
61
|
+
The console session token is sent as `Authorization: Bearer <token>` on every
|
|
62
|
+
request. Console requests hit `/api/platform/*`; admin requests hit the proxy at
|
|
63
|
+
`/api/projects/:id/admin/*` (which validates the session, then injects its own
|
|
64
|
+
`X-Platform-Token` to the project). In direct mode the admin token is the Bearer.
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
Global flags: `-o/--output pretty|json|yaml|env` (`--json` is `-o json`),
|
|
69
|
+
`--yes/-y`, `--project/-p/--project-ref <id>`, `--url/-u <consoleUrl>`,
|
|
70
|
+
`--token <t>`, `--workdir <dir>`, `--debug` (one stderr line per HTTP request),
|
|
71
|
+
`-h/--help`, `-v/--version`.
|
|
72
|
+
|
|
73
|
+
Commands follow the Supabase CLI's names. Where Motherbase already had its own
|
|
74
|
+
name, both work: `projects api-keys` / `keys list`, `backups list|restore` /
|
|
75
|
+
`db backups|restore`, `functions download` / `functions pull`, `db lint` /
|
|
76
|
+
`rls lint`, `gen types --lang <l>` / `gen types <l>`.
|
|
77
|
+
|
|
78
|
+
- **Connection**: `login`, `logout`, `whoami`, `link`, `unlink`, `status`
|
|
79
|
+
- **Orgs & projects (Console)**: `orgs list|create`, `projects list|create|start|stop|restart|update|rebuild|delete|logs|info|limits|api-keys`
|
|
80
|
+
- **Branches (Console)**: `branches create <name> [--with-data] [--git-branch <ref>]`, `branches list|get|update|delete|pause|unpause|reset|merge|rebase|switch <name>`
|
|
81
|
+
- **Domains (Console)**: `domains create --custom-hostname <host>`, `domains get|reverify|activate|delete`, `vanity-subdomains get|check-availability|activate --desired-subdomain <name>|delete`
|
|
82
|
+
- **Database access**: `network-restrictions get|update --db-allow-cidr <cidr> ...`, `ssl-enforcement get|update --enable-db-ssl-enforcement|--disable-db-ssl-enforcement`
|
|
83
|
+
- **Local self-host** (docker compose): `init`, `start`, `stop`, `deploy`, `logs`
|
|
84
|
+
- **Database**: `db push|pull|diff|reset|dump|seed`, `db tables|sql|backup|restore|backups|lint`, `backups list|restore`, `test db`
|
|
85
|
+
- **Database objects**: `db indexes list|create|drop`, `db types list|create|add-value|rename-value|drop`, `db roles list|create|alter|drop`, `db column-privileges list|grant|revoke`, `db publications list|create|alter|drop`
|
|
86
|
+
- **Postgres config**: `postgres-config get [--all]`, `postgres-config update --config k=v [--scope database|role|system] [--replace-existing-overrides]`, `postgres-config delete --config k1,k2`
|
|
87
|
+
- **Migrations**: `migration new|list|up|down|repair|squash|fetch|diff|get|create|apply|rollback` (`migrations` is an alias)
|
|
88
|
+
- **Import**: `import --from-supabase <conn> [--preview] [--no-data] [--no-auth]`
|
|
89
|
+
- **RLS**: `rls list|enable|disable`, `rls policy create|delete`
|
|
90
|
+
- **Users**: `users list|get|create|update|delete|reset-password|promote|demote|revoke-sessions|audit`
|
|
91
|
+
- **Storage**: `storage buckets list|create|delete`, `storage ls|cp|mv|rm [-r]` on `ss:///bucket/key` paths
|
|
92
|
+
- **Functions**: `functions list|get|new|deploy|pull|download|serve|delete|invoke|logs`
|
|
93
|
+
- **Secrets**: `secrets list|unset`, `secrets set NAME=VALUE ... [--env-file .env]`
|
|
94
|
+
- **Realtime**: `realtime config|channels`
|
|
95
|
+
- **Webhooks**: `webhooks list|create|delete|test`
|
|
96
|
+
- **Cron**: `cron list|create|delete|toggle`
|
|
97
|
+
- **Queues**: `queues list|enable|expose|create|drop|send|read|archive|purge`
|
|
98
|
+
- **Vault**: `vault list|enable|create|update|reveal|delete`
|
|
99
|
+
- **Wrappers**: `wrappers list|enable|create-server|drop-server|credentials|import|create-table|drop-table`
|
|
100
|
+
- **Config**: `config list|get|set`
|
|
101
|
+
- **Inspect**: `inspect db <query>`, `inspect report`, `inspect logs|stats|performance|index-advisor|requests|advisor|shield`
|
|
102
|
+
- **Codegen**: `gen types [--lang typescript|zod] [--schema public] [--local] [--output <file>] [--check]`
|
|
103
|
+
- **AI agents**: `mcp [--write]`
|
|
104
|
+
- **Keys**: `gen signing-key [--algorithm ES256|RS256|EdDSA] [--append]`, `gen bearer-jwt --sub <user> [--role] [--valid-for 1h]`, `gen keys [-o env]`
|
|
105
|
+
- **Shell**: `completion bash|zsh|fish` (e.g. `source <(motherbase completion bash)`)
|
|
106
|
+
|
|
107
|
+
Run `motherbase <group> -h` for per-group usage.
|
|
108
|
+
|
|
109
|
+
## Migrations and seeds
|
|
110
|
+
|
|
111
|
+
Local files, laid out like a Supabase project's `supabase/` directory:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
motherbase/
|
|
115
|
+
migrations/<version>_<name>.sql `migration new <name>` stamps the version (UTC)
|
|
116
|
+
seed.sql or db.seed.sql_paths in motherbase/config.json
|
|
117
|
+
tests/*.sql pgTAP, for `motherbase test db`
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A migration file may end with a `-- @down` line followed by the SQL that undoes
|
|
121
|
+
it; `migration down` needs one. The linked project keeps the history, keyed on
|
|
122
|
+
`version`, and the Studio Migrations page shows the same rows (origin `cli`,
|
|
123
|
+
`studio` or `git`).
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
motherbase migration list # LOCAL | REMOTE side by side
|
|
127
|
+
motherbase db push --dry-run # then without, add --include-seed for the seeds
|
|
128
|
+
motherbase db pull # remote changes -> a new file, recorded as applied
|
|
129
|
+
motherbase db diff -f add_index # the same diff, written as a new migration
|
|
130
|
+
motherbase migration repair 20260101120000 --status reverted
|
|
131
|
+
motherbase db reset --confirm # backup, empty app schemas, replay migrations + seed
|
|
132
|
+
motherbase db dump -f schema.sql --schema-only
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Each pushed file runs in its own transaction; a push stops at the first file
|
|
136
|
+
that fails, and nothing of that file is recorded. `db diff` replays the local
|
|
137
|
+
migrations into a throwaway database on the same server and compares tables,
|
|
138
|
+
columns and indexes: review what it writes. With no local migration yet, `db
|
|
139
|
+
pull` writes the remote schema itself, from `pg_dump`. `db dump` used to take a
|
|
140
|
+
server-side backup; that is `db backup` now.
|
|
141
|
+
|
|
142
|
+
## Type generation
|
|
143
|
+
|
|
144
|
+
Generate TypeScript definitions for the live schema, in the shape supabase-js
|
|
145
|
+
emits, so `createClient<Database>(url, key)` types every row, rejects unknown
|
|
146
|
+
columns and types `.rpc()` calls:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
motherbase gen types typescript # in a terminal: writes ./database.types.ts
|
|
150
|
+
motherbase gen types typescript > database.types.ts # piped: prints the file
|
|
151
|
+
motherbase gen types typescript --schema shop --output src/shop.types.ts
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The same `Database` also names the project's edge functions and buckets, under
|
|
155
|
+
`__InternalMotherbase`, so `client.functions.invoke('…')` and
|
|
156
|
+
`client.storage.from('…')` autocomplete with no extra generic. A name created
|
|
157
|
+
after the file was generated still compiles. Since they are part of the file,
|
|
158
|
+
adding a function or a bucket is drift for `--check` below.
|
|
159
|
+
|
|
160
|
+
With no `--output` the file goes to stdout and nothing else does, so the
|
|
161
|
+
redirect above produces a compiling file. `--local` skips the Console proxy and
|
|
162
|
+
talks to a project API on this machine (`MOTHERBASE_URL`, default
|
|
163
|
+
`http://localhost:5001`, with `MOTHERBASE_ADMIN_TOKEN`).
|
|
164
|
+
|
|
165
|
+
Gate CI on schema drift with `--check`, which never writes and exits non-zero
|
|
166
|
+
when the committed file differs from a fresh generation:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
motherbase gen types typescript --check --output database.types.ts
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Storage paths
|
|
173
|
+
|
|
174
|
+
Objects are addressed as the Supabase CLI does, `ss:///<bucket>/<key>`, and
|
|
175
|
+
`-r` applies a command to everything under a prefix:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
motherbase storage ls # buckets
|
|
179
|
+
motherbase storage ls ss:///media/img # one level; -r for every key
|
|
180
|
+
motherbase storage cp ./logo.png ss:///media/img/ # upload (overwrites)
|
|
181
|
+
motherbase storage cp -r ./site ss:///web/v2 # upload a directory tree
|
|
182
|
+
motherbase storage cp -r ss:///web/v2 ./site-backup # download a prefix
|
|
183
|
+
motherbase storage mv -r ss:///web/v2 ss:///web/live # move within a bucket
|
|
184
|
+
motherbase storage rm -r ss:///web/old
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Bulk commands send one request per object and stop at the first failure,
|
|
188
|
+
saying how many had already gone through. `mv` and in-bucket `cp` work within
|
|
189
|
+
one bucket. The older forms still work: `cp ./a.png bucket:` uploads with a
|
|
190
|
+
generated key, `cp <fileId> ./a.png` downloads and `rm <fileId>` deletes by id.
|
|
191
|
+
|
|
192
|
+
## Database inspection
|
|
193
|
+
|
|
194
|
+
`inspect db` runs the Supabase CLI's diagnostic queries against the linked
|
|
195
|
+
project, on its read-only SQL endpoint:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
motherbase inspect db outliers # slowest queries by total time
|
|
199
|
+
motherbase inspect db table-stats -o json
|
|
200
|
+
motherbase inspect report --output-dir reports # one CSV per query in reports/<YYYY-MM-DD>/
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The queries are `bloat`, `blocking`, `calls`, `db-stats`, `index-stats`,
|
|
204
|
+
`locks`, `long-running-queries`, `outliers`, `replication-slots`, `role-stats`,
|
|
205
|
+
`table-stats`, `traffic-profile` and `vacuum-stats`. The names Supabase has
|
|
206
|
+
deprecated (`cache-hit`, `index-usage`, `table-sizes`, `unused-indexes`, …)
|
|
207
|
+
still work and run the query that replaced them. `calls` and `outliers` read
|
|
208
|
+
`pg_stat_statements`: it is preloaded by the Motherbase Postgres image but has
|
|
209
|
+
to be created once per database (Studio > Database > Extensions, or
|
|
210
|
+
`motherbase db sql -e "create extension pg_stat_statements"`); `inspect report`
|
|
211
|
+
skips those two files until it is.
|
|
212
|
+
|
|
213
|
+
## MCP server
|
|
214
|
+
|
|
215
|
+
`motherbase mcp` serves this project to an AI agent over stdio:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{ "mcpServers": { "motherbase": { "command": "npx", "args": ["@motherbase/cli", "mcp"] } } }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
It exposes what the session can reach. With a linked project (or direct mode):
|
|
222
|
+
`list_tables`, `describe_table`, `list_policies`, `simulate_rls`,
|
|
223
|
+
`get_advisors`, `get_logs`, `generate_types`, `execute_sql` (read-only),
|
|
224
|
+
`get_project_url`, `get_publishable_keys`, `list_extensions`,
|
|
225
|
+
`list_migrations`, `list_edge_functions`, `get_edge_function`,
|
|
226
|
+
`list_storage_buckets` and `get_storage_config`. With a console login:
|
|
227
|
+
`list_organizations`, `get_organization`, `list_projects` and `get_project`.
|
|
228
|
+
|
|
229
|
+
Everything that changes something needs `--write`: `apply_migration`,
|
|
230
|
+
`execute_sql_write`, `deploy_edge_function`, `create_project`,
|
|
231
|
+
`pause_project` and `restore_project`. The names and arguments follow
|
|
232
|
+
Supabase's MCP server.
|
|
233
|
+
|
|
234
|
+
## Supabase migration
|
|
235
|
+
|
|
236
|
+
Preview a Supabase import without writing to Motherbase:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
motherbase import --from-supabase "$SUPABASE_DB_URL" --preview --json
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Run the import:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
motherbase import --from-supabase "$SUPABASE_DB_URL" --yes
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The importer moves the portable parts of a Supabase project: `public` schema, table data, RLS policies, and email/password users from `auth.users`. Storage objects are not copied. Use `--no-data` or `--no-auth` for partial imports.
|
|
249
|
+
|
|
250
|
+
## Tests
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm test # node --test, mocked fetch, no network/DB
|
|
254
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motherbase/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Motherbase operator CLI: drives the Platform Console and linked projects.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/JulienGuinot/
|
|
28
|
+
"url": "git+https://github.com/JulienGuinot/motherbase.git",
|
|
29
29
|
"directory": "packages/cli"
|
|
30
30
|
},
|
|
31
|
-
"homepage": "https://github.com/JulienGuinot/
|
|
31
|
+
"homepage": "https://github.com/JulienGuinot/motherbase#readme",
|
|
32
32
|
"bugs": {
|
|
33
|
-
"url": "https://github.com/JulienGuinot/
|
|
33
|
+
"url": "https://github.com/JulienGuinot/motherbase/issues"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -323,4 +323,42 @@ const columnPrivileges = {
|
|
|
323
323
|
},
|
|
324
324
|
};
|
|
325
325
|
|
|
326
|
-
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
const cron = {
|
|
331
|
+
summary : "Manage cron jobs",
|
|
332
|
+
usage : "motherbase db cron <list|get|update|run|create>",
|
|
333
|
+
|
|
334
|
+
subcommands : {
|
|
335
|
+
list : {
|
|
336
|
+
summary : "List cron jobs",
|
|
337
|
+
usage :"motherbase db cron list",
|
|
338
|
+
async run(ctx) {
|
|
339
|
+
const data = await api.adminRequest(ctx, "GET", "/cron/pg/jobs")
|
|
340
|
+
|
|
341
|
+
console.log(data.jobs)
|
|
342
|
+
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
get : {
|
|
348
|
+
summary: "Get a cron job by id",
|
|
349
|
+
usage : "motherbase db cron get <id>",
|
|
350
|
+
async run(ctx, args) {
|
|
351
|
+
const jobId = requireArg(args[0], 'id');
|
|
352
|
+
const data = await api.adminRequest(ctx, "GET",`/cron/pg/jobs/${jobId}`)
|
|
353
|
+
|
|
354
|
+
console.log(data.jobs)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
module.exports = { indexes, types, roles, 'column-privileges': columnPrivileges, publications, cron};
|
package/src/commands/index.js
CHANGED
package/src/commands/local.js
CHANGED
|
@@ -27,7 +27,7 @@ const DEFAULT_REGISTRY = 'ghcr.io/julienguinot';
|
|
|
27
27
|
const DEFAULT_TAG = 'latest';
|
|
28
28
|
// Build-from-source mode, kept for --repo / --ref. It needs motherbase-deps:1.0.0
|
|
29
29
|
// built locally first (see the compose header it writes).
|
|
30
|
-
const DEFAULT_REPO = 'https://github.com/JulienGuinot/
|
|
30
|
+
const DEFAULT_REPO = 'https://github.com/JulienGuinot/motherbase.git';
|
|
31
31
|
const DEFAULT_REF = 'master';
|
|
32
32
|
|
|
33
33
|
function dockerCompose(composeArgs) {
|
package/src/commands/users.js
CHANGED
|
@@ -104,6 +104,34 @@ const subcommands = {
|
|
|
104
104
|
output(ctx, data, (d, ui) => ui.success(`Sessions revoked for ${id}`));
|
|
105
105
|
},
|
|
106
106
|
},
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
deactivate: {
|
|
110
|
+
summary: 'Deactivate a user account and revoke its sessions',
|
|
111
|
+
usage: 'motherbase users deactivate <id>',
|
|
112
|
+
async run(ctx, args) {
|
|
113
|
+
const id = requireArg(args[0], 'id');
|
|
114
|
+
const data = await api.adminRequest(ctx, 'PATCH', `/users/${id}`, { body: { status: 'deleted' } });
|
|
115
|
+
output(ctx, data, (_d, ui) => ui.success(`Deactivated user ${id}`));
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
impersonate: {
|
|
120
|
+
summary: 'Grants a token to impersonate a user by id',
|
|
121
|
+
usage: 'motherbase users impersonate <id>',
|
|
122
|
+
async run(ctx, args) {
|
|
123
|
+
const id = requireArg(args[0], 'id');
|
|
124
|
+
const data = await api.adminRequest(ctx, 'POST', `/users/${id}/impersonate`);
|
|
125
|
+
const session = { accessToken: data.token, user: data.user };
|
|
126
|
+
const cookie = `motherbase.session=${encodeURIComponent(JSON.stringify(session))}; path=/; expires=${new Date(data.expiresAt).toUTCString()}; SameSite=Lax`;
|
|
127
|
+
const command = `document.cookie=${JSON.stringify(cookie)}; location.reload()`;
|
|
128
|
+
|
|
129
|
+
output(ctx, data, (_d, ui) =>
|
|
130
|
+
ui.success(`Paste this in your browser console to impersonate ${data.user.username ?? data.user.email}: ${command}`),
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
|
|
107
135
|
audit: {
|
|
108
136
|
summary: 'Read the auth audit log, newest first',
|
|
109
137
|
usage: 'motherbase users audit [--action <a>] [--actor <email|id>] [--type <log type>] [--from <iso>] [--to <iso>] [--limit <n>]',
|
|
@@ -137,6 +165,6 @@ const subcommands = {
|
|
|
137
165
|
|
|
138
166
|
module.exports = {
|
|
139
167
|
summary: 'Project auth users',
|
|
140
|
-
usage: 'motherbase users <list|get|create|update|delete|reset-password|promote|demote|revoke-sessions|audit> ...',
|
|
168
|
+
usage: 'motherbase users <list|get|create|update|delete|reset-password|promote|demote|revoke-sessions|deactivate|impersonate|audit> ...',
|
|
141
169
|
subcommands,
|
|
142
170
|
};
|
package/src/main.js
CHANGED
|
@@ -1,40 +1,64 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const path = require(
|
|
4
|
-
const { registry, groups } = require(
|
|
5
|
-
const { createContext, CliError } = require(
|
|
6
|
-
const { FORMATS } = require(
|
|
7
|
-
const ui = require(
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { registry, groups } = require("./commands");
|
|
5
|
+
const { createContext, CliError } = require("./context");
|
|
6
|
+
const { FORMATS } = require("./format");
|
|
7
|
+
const ui = require("./ui");
|
|
8
8
|
|
|
9
9
|
// Flags that never consume a following value.
|
|
10
10
|
const PURE_BOOLEAN = new Set([
|
|
11
|
-
|
|
11
|
+
"json",
|
|
12
|
+
"yes",
|
|
13
|
+
"help",
|
|
14
|
+
"version",
|
|
15
|
+
"force",
|
|
16
|
+
"preview",
|
|
17
|
+
"app",
|
|
18
|
+
"no-data",
|
|
19
|
+
"no-auth",
|
|
20
|
+
"f",
|
|
21
|
+
"debug",
|
|
12
22
|
// Codegen: `--check` and `--local` never take a value, so a trailing
|
|
13
23
|
// `--check database.types.ts` must not silently swallow the path.
|
|
14
|
-
|
|
24
|
+
"check",
|
|
25
|
+
"local",
|
|
15
26
|
// `rls preset <table> <preset> --replace` must not swallow a following word.
|
|
16
|
-
|
|
27
|
+
"replace",
|
|
17
28
|
// `storage cp --recursive ./dir ss:///bucket` must not swallow the source.
|
|
18
|
-
|
|
29
|
+
"recursive",
|
|
19
30
|
// Migration workflow switches: `db push --dry-run` must not eat a positional.
|
|
20
|
-
|
|
31
|
+
"dry-run",
|
|
32
|
+
"include-seed",
|
|
33
|
+
"include-all",
|
|
34
|
+
"schema-only",
|
|
35
|
+
"data-only",
|
|
36
|
+
"use-copy",
|
|
37
|
+
"confirm",
|
|
38
|
+
"no-seed",
|
|
39
|
+
"linked",
|
|
21
40
|
// `postgres-config` and `db publications` switches, never followed by a value.
|
|
22
|
-
|
|
41
|
+
"replace-existing-overrides",
|
|
42
|
+
"no-restart",
|
|
43
|
+
"all-tables",
|
|
23
44
|
// Branching, keys and database access: trailing switches that must not
|
|
24
45
|
// swallow the next word.
|
|
25
|
-
|
|
46
|
+
"with-data",
|
|
47
|
+
"append",
|
|
48
|
+
"enable-db-ssl-enforcement",
|
|
49
|
+
"disable-db-ssl-enforcement",
|
|
26
50
|
]);
|
|
27
51
|
// Short flag aliases → canonical (with whether they take a value).
|
|
28
52
|
const SHORT = {
|
|
29
|
-
y: { name:
|
|
30
|
-
h: { name:
|
|
31
|
-
v: { name:
|
|
32
|
-
p: { name:
|
|
33
|
-
u: { name:
|
|
34
|
-
e: { name:
|
|
35
|
-
f: { name:
|
|
36
|
-
o: { name:
|
|
37
|
-
r: { name:
|
|
53
|
+
y: { name: "yes", boolean: true },
|
|
54
|
+
h: { name: "help", boolean: true },
|
|
55
|
+
v: { name: "version", boolean: true },
|
|
56
|
+
p: { name: "project", boolean: false },
|
|
57
|
+
u: { name: "url", boolean: false },
|
|
58
|
+
e: { name: "e", boolean: false },
|
|
59
|
+
f: { name: "f", boolean: true },
|
|
60
|
+
o: { name: "output", boolean: false },
|
|
61
|
+
r: { name: "recursive", boolean: true },
|
|
38
62
|
};
|
|
39
63
|
|
|
40
64
|
/**
|
|
@@ -51,7 +75,7 @@ const SHORT = {
|
|
|
51
75
|
* --superuser` into an error.
|
|
52
76
|
*/
|
|
53
77
|
function isFlagToken(token) {
|
|
54
|
-
if (token.startsWith(
|
|
78
|
+
if (token.startsWith("--")) return token.length > 2;
|
|
55
79
|
return Object.prototype.hasOwnProperty.call(SHORT, token.slice(1));
|
|
56
80
|
}
|
|
57
81
|
|
|
@@ -64,7 +88,9 @@ function isFlagToken(token) {
|
|
|
64
88
|
* instead of guessing (PKG-16).
|
|
65
89
|
*/
|
|
66
90
|
function dashValue(flag, value) {
|
|
67
|
-
return new CliError(
|
|
91
|
+
return new CliError(
|
|
92
|
+
`${flag} needs a value, and "${value}" starts with "-". Write ${flag}=${value} instead.`,
|
|
93
|
+
);
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
// Tokenize argv into positionals + flags. Repeated flags collapse into arrays.
|
|
@@ -81,24 +107,44 @@ function tokenize(argv) {
|
|
|
81
107
|
|
|
82
108
|
for (let i = 0; i < argv.length; i++) {
|
|
83
109
|
const tok = argv[i];
|
|
84
|
-
if (tok ===
|
|
110
|
+
if (tok === "--") {
|
|
111
|
+
positionals.push(...argv.slice(i + 1));
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
85
114
|
|
|
86
|
-
if (tok.startsWith(
|
|
115
|
+
if (tok.startsWith("--")) {
|
|
87
116
|
const body = tok.slice(2);
|
|
88
|
-
const eq = body.indexOf(
|
|
89
|
-
if (eq >= 0) {
|
|
117
|
+
const eq = body.indexOf("=");
|
|
118
|
+
if (eq >= 0) {
|
|
119
|
+
setFlag(body.slice(0, eq), body.slice(eq + 1));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
90
122
|
const next = argv[i + 1];
|
|
91
|
-
if (PURE_BOOLEAN.has(body)) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
if (PURE_BOOLEAN.has(body)) {
|
|
124
|
+
setFlag(body, true);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (next === undefined || isFlagToken(next)) {
|
|
128
|
+
setFlag(body, true);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (next.startsWith("-")) throw dashValue(`--${body}`, next);
|
|
132
|
+
setFlag(body, next);
|
|
133
|
+
i++;
|
|
134
|
+
} else if (tok.startsWith("-") && tok.length > 1 && tok !== "-") {
|
|
96
135
|
const spec = SHORT[tok.slice(1)];
|
|
97
|
-
if (!spec) {
|
|
136
|
+
if (!spec) {
|
|
137
|
+
setFlag(tok.slice(1), true);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
98
140
|
const next = argv[i + 1];
|
|
99
|
-
if (spec.boolean || next === undefined || isFlagToken(next)) {
|
|
100
|
-
|
|
101
|
-
|
|
141
|
+
if (spec.boolean || next === undefined || isFlagToken(next)) {
|
|
142
|
+
setFlag(spec.name, true);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (next.startsWith("-")) throw dashValue(`--${spec.name}`, next);
|
|
146
|
+
setFlag(spec.name, next);
|
|
147
|
+
i++;
|
|
102
148
|
} else {
|
|
103
149
|
positionals.push(tok);
|
|
104
150
|
}
|
|
@@ -112,58 +158,76 @@ function tokenize(argv) {
|
|
|
112
158
|
// `--output` stays a file path on commands that declare `outputIsPath`.
|
|
113
159
|
function normalizeFlags(flags, command = {}) {
|
|
114
160
|
const out = { ...flags };
|
|
115
|
-
if (out.project === undefined && out[
|
|
161
|
+
if (out.project === undefined && out["project-ref"] !== undefined)
|
|
162
|
+
out.project = out["project-ref"];
|
|
116
163
|
if (out.output !== undefined && FORMATS.includes(out.output)) {
|
|
117
164
|
out.format = out.output;
|
|
118
165
|
delete out.output;
|
|
119
166
|
} else if (out.output !== undefined && !command.outputIsPath) {
|
|
120
|
-
throw new CliError(`--output must be one of: ${FORMATS.join(
|
|
167
|
+
throw new CliError(`--output must be one of: ${FORMATS.join(", ")}`);
|
|
121
168
|
}
|
|
122
|
-
out.format = out.format || (out.json ?
|
|
123
|
-
out.json = out.format ===
|
|
169
|
+
out.format = out.format || (out.json ? "json" : "pretty");
|
|
170
|
+
out.json = out.format === "json";
|
|
124
171
|
return out;
|
|
125
172
|
}
|
|
126
173
|
|
|
127
174
|
function version() {
|
|
128
175
|
try {
|
|
129
|
-
return require(path.join(__dirname,
|
|
130
|
-
} catch {
|
|
176
|
+
return require(path.join(__dirname, "..", "package.json")).version;
|
|
177
|
+
} catch {
|
|
178
|
+
return "0.0.0";
|
|
179
|
+
}
|
|
131
180
|
}
|
|
132
181
|
|
|
133
|
-
function pad(s, n) {
|
|
182
|
+
function pad(s, n) {
|
|
183
|
+
return String(s).padEnd(n);
|
|
184
|
+
}
|
|
134
185
|
|
|
135
186
|
function topLevelHelp() {
|
|
136
|
-
ui.print(ui.color.bold(
|
|
137
|
-
ui.print(
|
|
138
|
-
ui.print(
|
|
139
|
-
ui.print(
|
|
187
|
+
ui.print(ui.color.bold("Motherbase CLI") + ` v${version()}`);
|
|
188
|
+
ui.print("");
|
|
189
|
+
ui.print("Usage: motherbase <command> [subcommand] [args] [flags]");
|
|
190
|
+
ui.print("");
|
|
140
191
|
const width = Object.keys(registry).reduce((m, k) => Math.max(m, k.length), 0) + 2;
|
|
141
192
|
for (const group of groups) {
|
|
142
193
|
ui.print(ui.color.bold(group.title));
|
|
143
194
|
for (const name of group.names) {
|
|
144
195
|
const entry = registry[name];
|
|
145
196
|
if (!entry) continue;
|
|
146
|
-
ui.print(
|
|
197
|
+
ui.print(
|
|
198
|
+
" " +
|
|
199
|
+
ui.color.cyan(pad(name, width)) +
|
|
200
|
+
ui.color.dim(entry.summary || ""),
|
|
201
|
+
);
|
|
147
202
|
}
|
|
148
|
-
ui.print(
|
|
203
|
+
ui.print("");
|
|
149
204
|
}
|
|
150
|
-
ui.print(
|
|
151
|
-
|
|
152
|
-
|
|
205
|
+
ui.print(
|
|
206
|
+
"Global flags: -o/--output pretty|json|yaml|env --json --yes/-y --project/-p/--project-ref <id> --url/-u <consoleUrl>",
|
|
207
|
+
);
|
|
208
|
+
ui.print(
|
|
209
|
+
" --token <t> --workdir <dir> --debug -h/--help -v/--version",
|
|
210
|
+
);
|
|
211
|
+
ui.print("Run `motherbase <command> -h` for command-specific help.");
|
|
153
212
|
}
|
|
154
213
|
|
|
155
214
|
function commandHelp(name, entry, trail) {
|
|
156
|
-
const full = [
|
|
157
|
-
ui.print(ui.color.bold(full) + (entry.summary ? `, ${entry.summary}` :
|
|
158
|
-
ui.print(
|
|
159
|
-
if (entry.usage) {
|
|
215
|
+
const full = ["motherbase", ...trail].join(" ");
|
|
216
|
+
ui.print(ui.color.bold(full) + (entry.summary ? `, ${entry.summary}` : ""));
|
|
217
|
+
ui.print("");
|
|
218
|
+
if (entry.usage) {
|
|
219
|
+
ui.print("Usage: " + entry.usage);
|
|
220
|
+
ui.print("");
|
|
221
|
+
}
|
|
160
222
|
if (entry.subcommands) {
|
|
161
|
-
ui.print(ui.color.bold(
|
|
223
|
+
ui.print(ui.color.bold("Subcommands"));
|
|
162
224
|
const subs = Object.keys(entry.subcommands);
|
|
163
225
|
const width = subs.reduce((m, k) => Math.max(m, k.length), 0) + 2;
|
|
164
226
|
for (const sub of subs) {
|
|
165
227
|
const s = entry.subcommands[sub];
|
|
166
|
-
ui.print(
|
|
228
|
+
ui.print(
|
|
229
|
+
" " + ui.color.cyan(pad(sub, width)) + ui.color.dim(s.summary || ""),
|
|
230
|
+
);
|
|
167
231
|
}
|
|
168
232
|
}
|
|
169
233
|
}
|
|
@@ -172,15 +236,21 @@ async function main(argv) {
|
|
|
172
236
|
const { positionals, flags } = tokenize(argv);
|
|
173
237
|
|
|
174
238
|
// Only bare: `migration squash --version <v>` is Supabase's own flag.
|
|
175
|
-
if (flags.version && positionals.length === 0) {
|
|
239
|
+
if (flags.version && positionals.length === 0) {
|
|
240
|
+
ui.print(version());
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
176
243
|
|
|
177
|
-
if (positionals.length === 0) {
|
|
244
|
+
if (positionals.length === 0) {
|
|
245
|
+
topLevelHelp();
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
178
248
|
|
|
179
249
|
const name = positionals[0];
|
|
180
250
|
const entry = registry[name];
|
|
181
251
|
if (!entry) {
|
|
182
252
|
ui.printErr(ui.color.red(`Unknown command: ${name}`));
|
|
183
|
-
ui.printErr(
|
|
253
|
+
ui.printErr("Run `motherbase --help` to see available commands.");
|
|
184
254
|
return 1;
|
|
185
255
|
}
|
|
186
256
|
|
|
@@ -200,16 +270,25 @@ async function main(argv) {
|
|
|
200
270
|
}
|
|
201
271
|
if (!current.run) {
|
|
202
272
|
// Group node: either no subcommand given or an unknown one.
|
|
203
|
-
if (rest.length)
|
|
273
|
+
if (rest.length)
|
|
274
|
+
ui.printErr(
|
|
275
|
+
ui.color.red(`Unknown subcommand: ${trail.join(" ")} ${rest[0]}`),
|
|
276
|
+
);
|
|
204
277
|
commandHelp(name, current, trail);
|
|
205
278
|
return rest.length ? 1 : 0;
|
|
206
279
|
}
|
|
207
280
|
|
|
208
281
|
try {
|
|
209
282
|
const resolved = normalizeFlags(flags, current);
|
|
210
|
-
if (resolved.debug) process.env.MOTHERBASE_DEBUG =
|
|
211
|
-
const cwd = resolved.workdir
|
|
212
|
-
|
|
283
|
+
if (resolved.debug) process.env.MOTHERBASE_DEBUG = "1";
|
|
284
|
+
const cwd = resolved.workdir
|
|
285
|
+
? path.resolve(String(resolved.workdir))
|
|
286
|
+
: process.cwd();
|
|
287
|
+
await current.run(
|
|
288
|
+
createContext(resolved, process.env, cwd),
|
|
289
|
+
rest,
|
|
290
|
+
resolved,
|
|
291
|
+
);
|
|
213
292
|
return 0;
|
|
214
293
|
} catch (err) {
|
|
215
294
|
if (err instanceof CliError || err.isCliError) {
|