@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 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
- With no `--output` the file goes to stdout and nothing else does, so the
155
- redirect above produces a compiling file. `--local` skips the Console proxy and
156
- talks to a project API on this machine (`MOTHERBASE_URL`, default
157
- `http://localhost:5001`, with `MOTHERBASE_ADMIN_TOKEN`).
158
-
159
- Gate CI on schema drift with `--check`, which never writes and exits non-zero
160
- when the committed file differs from a fresh generation:
161
-
162
- ```bash
163
- motherbase gen types typescript --check --output database.types.ts
164
- ```
165
-
166
- ## Storage paths
167
-
168
- Objects are addressed as the Supabase CLI does, `ss:///<bucket>/<key>`, and
169
- `-r` applies a command to everything under a prefix:
170
-
171
- ```bash
172
- motherbase storage ls # buckets
173
- motherbase storage ls ss:///media/img # one level; -r for every key
174
- motherbase storage cp ./logo.png ss:///media/img/ # upload (overwrites)
175
- motherbase storage cp -r ./site ss:///web/v2 # upload a directory tree
176
- motherbase storage cp -r ss:///web/v2 ./site-backup # download a prefix
177
- motherbase storage mv -r ss:///web/v2 ss:///web/live # move within a bucket
178
- motherbase storage rm -r ss:///web/old
179
- ```
180
-
181
- Bulk commands send one request per object and stop at the first failure,
182
- saying how many had already gone through. `mv` and in-bucket `cp` work within
183
- one bucket. The older forms still work: `cp ./a.png bucket:` uploads with a
184
- generated key, `cp <fileId> ./a.png` downloads and `rm <fileId>` deletes by id.
185
-
186
- ## Database inspection
187
-
188
- `inspect db` runs the Supabase CLI's diagnostic queries against the linked
189
- project, on its read-only SQL endpoint:
190
-
191
- ```bash
192
- motherbase inspect db outliers # slowest queries by total time
193
- motherbase inspect db table-stats -o json
194
- motherbase inspect report --output-dir reports # one CSV per query in reports/<YYYY-MM-DD>/
195
- ```
196
-
197
- The queries are `bloat`, `blocking`, `calls`, `db-stats`, `index-stats`,
198
- `locks`, `long-running-queries`, `outliers`, `replication-slots`, `role-stats`,
199
- `table-stats`, `traffic-profile` and `vacuum-stats`. The names Supabase has
200
- deprecated (`cache-hit`, `index-usage`, `table-sizes`, `unused-indexes`, …)
201
- still work and run the query that replaced them. `calls` and `outliers` read
202
- `pg_stat_statements`: it is preloaded by the Motherbase Postgres image but has
203
- to be created once per database (Studio > Database > Extensions, or
204
- `motherbase db sql -e "create extension pg_stat_statements"`); `inspect report`
205
- skips those two files until it is.
206
-
207
- ## MCP server
208
-
209
- `motherbase mcp` serves this project to an AI agent over stdio:
210
-
211
- ```json
212
- { "mcpServers": { "motherbase": { "command": "npx", "args": ["@motherbase/cli", "mcp"] } } }
213
- ```
214
-
215
- It exposes what the session can reach. With a linked project (or direct mode):
216
- `list_tables`, `describe_table`, `list_policies`, `simulate_rls`,
217
- `get_advisors`, `get_logs`, `generate_types`, `execute_sql` (read-only),
218
- `get_project_url`, `get_publishable_keys`, `list_extensions`,
219
- `list_migrations`, `list_edge_functions`, `get_edge_function`,
220
- `list_storage_buckets` and `get_storage_config`. With a console login:
221
- `list_organizations`, `get_organization`, `list_projects` and `get_project`.
222
-
223
- Everything that changes something needs `--write`: `apply_migration`,
224
- `execute_sql_write`, `deploy_edge_function`, `create_project`,
225
- `pause_project` and `restore_project`. The names and arguments follow
226
- Supabase's MCP server.
227
-
228
- ## Supabase migration
229
-
230
- Preview a Supabase import without writing to Motherbase:
231
-
232
- ```bash
233
- motherbase import --from-supabase "$SUPABASE_DB_URL" --preview --json
234
- ```
235
-
236
- Run the import:
237
-
238
- ```bash
239
- motherbase import --from-supabase "$SUPABASE_DB_URL" --yes
240
- ```
241
-
242
- 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.
243
-
244
- ## Tests
245
-
246
- ```bash
247
- npm test # node --test, mocked fetch, no network/DB
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.8",
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/Boilerplate-backend.git",
28
+ "url": "git+https://github.com/JulienGuinot/motherbase.git",
29
29
  "directory": "packages/cli"
30
30
  },
31
- "homepage": "https://github.com/JulienGuinot/Boilerplate-backend#readme",
31
+ "homepage": "https://github.com/JulienGuinot/motherbase#readme",
32
32
  "bugs": {
33
- "url": "https://github.com/JulienGuinot/Boilerplate-backend/issues"
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
- module.exports = { indexes, types, roles, 'column-privileges': columnPrivileges, publications };
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};
@@ -68,6 +68,7 @@ const registry = {
68
68
 
69
69
  // ── Shell ──────────────────────────────────────────────────────────────────
70
70
  completion: require('./completion'),
71
+
71
72
  };
72
73
 
73
74
  // Grouping used only to render a tidy top-level help listing.
@@ -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/BoilerPlate-Backend.git';
30
+ const DEFAULT_REPO = 'https://github.com/JulienGuinot/motherbase.git';
31
31
  const DEFAULT_REF = 'master';
32
32
 
33
33
  function dockerCompose(composeArgs) {
@@ -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
- 'use strict';
1
+ "use strict";
2
2
 
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');
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
- 'json', 'yes', 'help', 'version', 'force', 'preview', 'app', 'no-data', 'no-auth', 'f', 'debug',
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
- 'check', 'local',
24
+ "check",
25
+ "local",
15
26
  // `rls preset <table> <preset> --replace` must not swallow a following word.
16
- 'replace',
27
+ "replace",
17
28
  // `storage cp --recursive ./dir ss:///bucket` must not swallow the source.
18
- 'recursive',
29
+ "recursive",
19
30
  // Migration workflow switches: `db push --dry-run` must not eat a positional.
20
- 'dry-run', 'include-seed', 'include-all', 'schema-only', 'data-only', 'use-copy', 'confirm', 'no-seed', 'linked',
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
- 'replace-existing-overrides', 'no-restart', 'all-tables',
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
- 'with-data', 'append', 'enable-db-ssl-enforcement', 'disable-db-ssl-enforcement',
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: 'yes', boolean: true },
30
- h: { name: 'help', boolean: true },
31
- v: { name: 'version', boolean: true },
32
- p: { name: 'project', boolean: false },
33
- u: { name: 'url', boolean: false },
34
- e: { name: 'e', boolean: false },
35
- f: { name: 'f', boolean: true },
36
- o: { name: 'output', boolean: false },
37
- r: { name: 'recursive', boolean: true },
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('--')) return token.length > 2;
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(`${flag} needs a value, and "${value}" starts with "-". Write ${flag}=${value} instead.`);
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 === '--') { positionals.push(...argv.slice(i + 1)); break; }
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) { setFlag(body.slice(0, eq), body.slice(eq + 1)); continue; }
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)) { setFlag(body, true); continue; }
92
- if (next === undefined || isFlagToken(next)) { setFlag(body, true); continue; }
93
- if (next.startsWith('-')) throw dashValue(`--${body}`, next);
94
- setFlag(body, next); i++;
95
- } else if (tok.startsWith('-') && tok.length > 1 && tok !== '-') {
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) { setFlag(tok.slice(1), true); continue; }
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)) { setFlag(spec.name, true); continue; }
100
- if (next.startsWith('-')) throw dashValue(`--${spec.name}`, next);
101
- setFlag(spec.name, next); i++;
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['project-ref'] !== undefined) out.project = out['project-ref'];
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 ? 'json' : 'pretty');
123
- out.json = out.format === 'json';
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, '..', 'package.json')).version;
130
- } catch { return '0.0.0'; }
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) { return String(s).padEnd(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('Motherbase CLI') + ` v${version()}`);
137
- ui.print('');
138
- ui.print('Usage: motherbase <command> [subcommand] [args] [flags]');
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(' ' + ui.color.cyan(pad(name, width)) + ui.color.dim(entry.summary || ''));
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('Global flags: -o/--output pretty|json|yaml|env --json --yes/-y --project/-p/--project-ref <id> --url/-u <consoleUrl>');
151
- ui.print(' --token <t> --workdir <dir> --debug -h/--help -v/--version');
152
- ui.print('Run `motherbase <command> -h` for command-specific help.');
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 = ['motherbase', ...trail].join(' ');
157
- ui.print(ui.color.bold(full) + (entry.summary ? `, ${entry.summary}` : ''));
158
- ui.print('');
159
- if (entry.usage) { ui.print('Usage: ' + entry.usage); ui.print(''); }
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('Subcommands'));
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(' ' + ui.color.cyan(pad(sub, width)) + ui.color.dim(s.summary || ''));
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) { ui.print(version()); return 0; }
239
+ if (flags.version && positionals.length === 0) {
240
+ ui.print(version());
241
+ return 0;
242
+ }
176
243
 
177
- if (positionals.length === 0) { topLevelHelp(); return 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('Run `motherbase --help` to see available commands.');
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) ui.printErr(ui.color.red(`Unknown subcommand: ${trail.join(' ')} ${rest[0]}`));
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 = '1';
211
- const cwd = resolved.workdir ? path.resolve(String(resolved.workdir)) : process.cwd();
212
- await current.run(createContext(resolved, process.env, cwd), rest, resolved);
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) {