@motherbase/cli 0.2.6 → 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.
Files changed (59) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +248 -247
  3. package/bin/.motherbase/config.json +5 -0
  4. package/bin/motherbase.js +10 -10
  5. package/package.json +1 -1
  6. package/src/api.js +39 -16
  7. package/src/commands/_util.js +106 -106
  8. package/src/commands/backups.js +16 -16
  9. package/src/commands/branches.js +113 -113
  10. package/src/commands/completion.js +90 -90
  11. package/src/commands/config.js +47 -47
  12. package/src/commands/cron.js +65 -65
  13. package/src/commands/db.js +103 -103
  14. package/src/commands/domains.js +124 -124
  15. package/src/commands/gen.js +178 -172
  16. package/src/commands/import.js +55 -55
  17. package/src/commands/index.js +85 -85
  18. package/src/commands/inspect.js +178 -178
  19. package/src/commands/keys.js +72 -72
  20. package/src/commands/link.js +35 -35
  21. package/src/commands/local.js +100 -23
  22. package/src/commands/login.js +9 -3
  23. package/src/commands/logout.js +16 -16
  24. package/src/commands/mcp.js +61 -61
  25. package/src/commands/migrate.js +0 -5
  26. package/src/commands/migration.js +1 -1
  27. package/src/commands/network.js +83 -83
  28. package/src/commands/orgs.js +40 -40
  29. package/src/commands/postgres-config.js +113 -113
  30. package/src/commands/projects.js +131 -130
  31. package/src/commands/queues.js +133 -133
  32. package/src/commands/realtime.js +35 -35
  33. package/src/commands/rls.js +176 -176
  34. package/src/commands/secrets.js +66 -66
  35. package/src/commands/signing.js +130 -130
  36. package/src/commands/snippets.js +44 -44
  37. package/src/commands/status.js +42 -42
  38. package/src/commands/storage.js +261 -261
  39. package/src/commands/test.js +9 -9
  40. package/src/commands/unlink.js +15 -15
  41. package/src/commands/vault.js +82 -82
  42. package/src/commands/webhooks.js +62 -62
  43. package/src/commands/whoami.js +20 -20
  44. package/src/commands/wrappers.js +150 -150
  45. package/src/config.js +98 -98
  46. package/src/context.js +106 -106
  47. package/src/edge-templates.js +194 -194
  48. package/src/format.js +92 -92
  49. package/src/inspect/csv.js +22 -22
  50. package/src/inspect/queries.js +562 -562
  51. package/src/mcp/platform-tools.js +233 -233
  52. package/src/mcp/protocol.js +152 -152
  53. package/src/mcp/storage-tools.js +93 -93
  54. package/src/mcp/tools.js +478 -478
  55. package/src/migration/commands.js +2 -2
  56. package/src/migration/files.js +13 -0
  57. package/src/prompt.js +91 -91
  58. package/src/storage/remote.js +62 -62
  59. package/src/ui.js +111 -111
@@ -1,55 +1,55 @@
1
- 'use strict';
2
-
3
- const api = require('../api');
4
- const prompt = require('../prompt');
5
- const { output } = require('./_util');
6
- const { CliError } = require('../context');
7
-
8
- // Sections beyond the database need their own credential, because that is where
9
- // Supabase keeps them: object bytes behind the Storage API (service_role key),
10
- // edge functions and project secrets behind the Management API (a personal
11
- // access token). Both are optional; without one, that section reports what it
12
- // could not read instead of failing the import.
13
- module.exports = {
14
- summary: 'Import a Supabase project (schema, data, auth, storage, edge functions, secrets)',
15
- usage:
16
- 'motherbase import --from-supabase <connectionString> [--service-role-key <key>] [--management-token <sbp_…>] ' +
17
- '[--preview] [--no-data] [--no-auth] [--no-storage] [--no-edge-functions] [--no-secrets]',
18
- async run(ctx, args, flags) {
19
- const connectionString = flags['from-supabase'] || flags.fromSupabase;
20
- if (!connectionString) throw new CliError('--from-supabase <connectionString> is required');
21
-
22
- const include = {
23
- schema: true,
24
- data: !flags['no-data'],
25
- auth: !flags['no-auth'],
26
- storage: !flags['no-storage'],
27
- edgeFunctions: !flags['no-edge-functions'],
28
- secrets: !flags['no-secrets'],
29
- };
30
- const preview = Boolean(flags.preview);
31
-
32
- const body = { connectionString, include };
33
- const serviceRoleKey = flags['service-role-key'] || flags.serviceRoleKey;
34
- const managementToken = flags['management-token'] || flags.managementToken;
35
- if (serviceRoleKey) body.serviceRoleKey = serviceRoleKey;
36
- if (managementToken) body.managementToken = managementToken;
37
- if (flags['project-ref'] || flags.projectRef) body.projectRef = flags['project-ref'] || flags.projectRef;
38
-
39
- if (preview) {
40
- body.preview = true;
41
- } else {
42
- if (!flags.yes) {
43
- const ok = await prompt.confirm('This will import into the linked project. Continue?');
44
- if (!ok) throw new CliError('Aborted.');
45
- }
46
- body.confirm = 'IMPORT';
47
- }
48
-
49
- const data = await api.adminRequest(ctx, 'POST', '/import/supabase', { body });
50
- output(ctx, data, (d, ui) => {
51
- if (preview) { ui.info('Import preview (dry run):'); ui.json(d); }
52
- else { ui.success('Import complete'); ui.json(d); }
53
- });
54
- },
55
- };
1
+ 'use strict';
2
+
3
+ const api = require('../api');
4
+ const prompt = require('../prompt');
5
+ const { output } = require('./_util');
6
+ const { CliError } = require('../context');
7
+
8
+ // Sections beyond the database need their own credential, because that is where
9
+ // Supabase keeps them: object bytes behind the Storage API (service_role key),
10
+ // edge functions and project secrets behind the Management API (a personal
11
+ // access token). Both are optional; without one, that section reports what it
12
+ // could not read instead of failing the import.
13
+ module.exports = {
14
+ summary: 'Import a Supabase project (schema, data, auth, storage, edge functions, secrets)',
15
+ usage:
16
+ 'motherbase import --from-supabase <connectionString> [--service-role-key <key>] [--management-token <sbp_…>] ' +
17
+ '[--preview] [--no-data] [--no-auth] [--no-storage] [--no-edge-functions] [--no-secrets]',
18
+ async run(ctx, args, flags) {
19
+ const connectionString = flags['from-supabase'] || flags.fromSupabase;
20
+ if (!connectionString) throw new CliError('--from-supabase <connectionString> is required');
21
+
22
+ const include = {
23
+ schema: true,
24
+ data: !flags['no-data'],
25
+ auth: !flags['no-auth'],
26
+ storage: !flags['no-storage'],
27
+ edgeFunctions: !flags['no-edge-functions'],
28
+ secrets: !flags['no-secrets'],
29
+ };
30
+ const preview = Boolean(flags.preview);
31
+
32
+ const body = { connectionString, include };
33
+ const serviceRoleKey = flags['service-role-key'] || flags.serviceRoleKey;
34
+ const managementToken = flags['management-token'] || flags.managementToken;
35
+ if (serviceRoleKey) body.serviceRoleKey = serviceRoleKey;
36
+ if (managementToken) body.managementToken = managementToken;
37
+ if (flags['project-ref'] || flags.projectRef) body.projectRef = flags['project-ref'] || flags.projectRef;
38
+
39
+ if (preview) {
40
+ body.preview = true;
41
+ } else {
42
+ if (!flags.yes) {
43
+ const ok = await prompt.confirm('This will import into the linked project. Continue?');
44
+ if (!ok) throw new CliError('Aborted.');
45
+ }
46
+ body.confirm = 'IMPORT';
47
+ }
48
+
49
+ const data = await api.adminRequest(ctx, 'POST', '/import/supabase', { body });
50
+ output(ctx, data, (d, ui) => {
51
+ if (preview) { ui.info('Import preview (dry run):'); ui.json(d); }
52
+ else { ui.success('Import complete'); ui.json(d); }
53
+ });
54
+ },
55
+ };
@@ -1,85 +1,85 @@
1
- 'use strict';
2
-
3
- // Command registry: name -> { summary, usage, subcommands?, run? }.
4
- // `main.js` builds all help/dispatch from this single map.
5
-
6
- const local = require('./local');
7
- const { alias } = require('./_util');
8
- const migration = require('./migration');
9
-
10
- const registry = {
11
- // ── Connection ─────────────────────────────────────────────────────────────
12
- login: require('./login'),
13
- logout: require('./logout'),
14
- whoami: require('./whoami'),
15
- link: require('./link'),
16
- unlink: require('./unlink'),
17
- status: require('./status'),
18
-
19
- // ── Orgs & projects (Platform Console) ─────────────────────────────────────
20
- orgs: require('./orgs'),
21
- projects: require('./projects'),
22
- branches: require('./branches'),
23
- domains: require('./domains').domains,
24
- 'vanity-subdomains': require('./domains').vanitySubdomains,
25
-
26
- // ── Local self-host (docker compose) ───────────────────────────────────────
27
- init: local.init,
28
- start: local.start,
29
- stop: local.stop,
30
- deploy: local.deploy,
31
- logs: local.logs,
32
-
33
- // ── Database / migrations / import ─────────────────────────────────────────
34
- db: require('./db'),
35
- backups: require('./backups'),
36
- migration,
37
- // The name Motherbase used before it matched Supabase's `migration`.
38
- migrations: alias(migration, 'motherbase migrations <new|list|up|down|repair|squash|fetch|diff|get|create|apply|rollback> ... (alias of `migration`)'),
39
- import: require('./import'),
40
- migrate: require('./migrate'),
41
- test: require('./test'),
42
- 'postgres-config': require('./postgres-config'),
43
-
44
- // ── Resource domains ───────────────────────────────────────────────────────
45
- rls: require('./rls'),
46
- users: require('./users'),
47
- storage: require('./storage'),
48
- functions: require('./functions'),
49
- secrets: require('./secrets'),
50
- keys: require('./keys'),
51
- realtime: require('./realtime'),
52
- webhooks: require('./webhooks'),
53
- cron: require('./cron'),
54
- queues: require('./queues'),
55
- vault: require('./vault'),
56
- wrappers: require('./wrappers'),
57
- config: require('./config'),
58
- inspect: require('./inspect'),
59
- 'network-restrictions': require('./network').networkRestrictions,
60
- 'ssl-enforcement': require('./network').sslEnforcement,
61
- snippets: require('./snippets'),
62
-
63
- // ── Codegen ────────────────────────────────────────────────────────────────
64
- gen: require('./gen'),
65
-
66
- // ── AI agents ──────────────────────────────────────────────────────────────
67
- mcp: require('./mcp'),
68
-
69
- // ── Shell ──────────────────────────────────────────────────────────────────
70
- completion: require('./completion'),
71
- };
72
-
73
- // Grouping used only to render a tidy top-level help listing.
74
- const groups = [
75
- { title: 'Connection', names: ['login', 'logout', 'whoami', 'link', 'unlink', 'status'] },
76
- { title: 'Orgs & projects (Console)', names: ['orgs', 'projects', 'branches', 'domains', 'vanity-subdomains'] },
77
- { title: 'Local self-host', names: ['init', 'start', 'stop', 'deploy', 'logs'] },
78
- { title: 'Database', names: ['db', 'backups', 'migrations', 'migration', 'import', 'test', 'postgres-config'] },
79
- { title: 'Resources', names: ['rls', 'users', 'storage', 'functions', 'secrets', 'keys', 'realtime', 'webhooks', 'cron', 'queues', 'vault', 'wrappers', 'config', 'inspect', 'snippets', 'network-restrictions', 'ssl-enforcement'] },
80
- { title: 'Codegen', names: ['gen'] },
81
- { title: 'AI agents', names: ['mcp'] },
82
- { title: 'Shell', names: ['completion'] },
83
- ];
84
-
85
- module.exports = { registry, groups };
1
+ 'use strict';
2
+
3
+ // Command registry: name -> { summary, usage, subcommands?, run? }.
4
+ // `main.js` builds all help/dispatch from this single map.
5
+
6
+ const local = require('./local');
7
+ const { alias } = require('./_util');
8
+ const migration = require('./migration');
9
+
10
+ const registry = {
11
+ // ── Connection ─────────────────────────────────────────────────────────────
12
+ login: require('./login'),
13
+ logout: require('./logout'),
14
+ whoami: require('./whoami'),
15
+ link: require('./link'),
16
+ unlink: require('./unlink'),
17
+ status: require('./status'),
18
+
19
+ // ── Orgs & projects (Platform Console) ─────────────────────────────────────
20
+ orgs: require('./orgs'),
21
+ projects: require('./projects'),
22
+ branches: require('./branches'),
23
+ domains: require('./domains').domains,
24
+ 'vanity-subdomains': require('./domains').vanitySubdomains,
25
+
26
+ // ── Local self-host (docker compose) ───────────────────────────────────────
27
+ init: local.init,
28
+ start: local.start,
29
+ stop: local.stop,
30
+ deploy: local.deploy,
31
+ logs: local.logs,
32
+
33
+ // ── Database / migrations / import ─────────────────────────────────────────
34
+ db: require('./db'),
35
+ backups: require('./backups'),
36
+ migration,
37
+ // The name Motherbase used before it matched Supabase's `migration`.
38
+ migrations: alias(migration, 'motherbase migrations <new|list|up|down|repair|squash|fetch|diff|get|create|apply|rollback> ... (alias of `migration`)'),
39
+ import: require('./import'),
40
+ migrate: require('./migrate'),
41
+ test: require('./test'),
42
+ 'postgres-config': require('./postgres-config'),
43
+
44
+ // ── Resource domains ───────────────────────────────────────────────────────
45
+ rls: require('./rls'),
46
+ users: require('./users'),
47
+ storage: require('./storage'),
48
+ functions: require('./functions'),
49
+ secrets: require('./secrets'),
50
+ keys: require('./keys'),
51
+ realtime: require('./realtime'),
52
+ webhooks: require('./webhooks'),
53
+ cron: require('./cron'),
54
+ queues: require('./queues'),
55
+ vault: require('./vault'),
56
+ wrappers: require('./wrappers'),
57
+ config: require('./config'),
58
+ inspect: require('./inspect'),
59
+ 'network-restrictions': require('./network').networkRestrictions,
60
+ 'ssl-enforcement': require('./network').sslEnforcement,
61
+ snippets: require('./snippets'),
62
+
63
+ // ── Codegen ────────────────────────────────────────────────────────────────
64
+ gen: require('./gen'),
65
+
66
+ // ── AI agents ──────────────────────────────────────────────────────────────
67
+ mcp: require('./mcp'),
68
+
69
+ // ── Shell ──────────────────────────────────────────────────────────────────
70
+ completion: require('./completion'),
71
+ };
72
+
73
+ // Grouping used only to render a tidy top-level help listing.
74
+ const groups = [
75
+ { title: 'Connection', names: ['login', 'logout', 'whoami', 'link', 'unlink', 'status'] },
76
+ { title: 'Orgs & projects (Console)', names: ['orgs', 'projects', 'branches', 'domains', 'vanity-subdomains'] },
77
+ { title: 'Local self-host', names: ['init', 'start', 'stop', 'deploy', 'logs'] },
78
+ { title: 'Database', names: ['db', 'backups', 'migrations', 'migration', 'import', 'test', 'postgres-config'] },
79
+ { title: 'Resources', names: ['rls', 'users', 'storage', 'functions', 'secrets', 'keys', 'realtime', 'webhooks', 'cron', 'queues', 'vault', 'wrappers', 'config', 'inspect', 'snippets', 'network-restrictions', 'ssl-enforcement'] },
80
+ { title: 'Codegen', names: ['gen'] },
81
+ { title: 'AI agents', names: ['mcp'] },
82
+ { title: 'Shell', names: ['completion'] },
83
+ ];
84
+
85
+ module.exports = { registry, groups };
@@ -1,178 +1,178 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const api = require('../api');
6
- const { output } = require('./_util');
7
- const { CliError } = require('../context');
8
- const { QUERIES, DEPRECATED, REPORT, resolveQuery } = require('../inspect/queries');
9
- const { toCsv } = require('../inspect/csv');
10
-
11
- function simple(apiPath, summary, render) {
12
- return {
13
- summary,
14
- // usage is filled in from the subcommand key below: deriving it from the
15
- // summary printed `inspect security` for `shield` and `inspect request` for
16
- // `requests`, neither of which is a command.
17
- usage: '',
18
- async run(ctx) {
19
- const data = await api.adminRequest(ctx, 'GET', apiPath);
20
- output(ctx, data, render || ((d, ui) => ui.json(d)));
21
- },
22
- };
23
- }
24
-
25
- // Postgres' two ways of saying pg_stat_statements is not usable here: the
26
- // extension was never created in this database, or the library is not preloaded.
27
- const STAT_STATEMENTS_MISSING = /pg_stat_statements.*(does not exist|must be loaded)/i;
28
-
29
- class StatStatementsMissing extends CliError {
30
- constructor() {
31
- super(
32
- 'pg_stat_statements is not enabled in this database. Enable it from Studio (Database > Extensions) '
33
- + 'or run: motherbase db sql -e "create extension pg_stat_statements"',
34
- );
35
- }
36
- }
37
-
38
- /** Rows of one inspect query, run on the read-only SQL endpoint. */
39
- async function runQuery(ctx, query) {
40
- try {
41
- const data = await api.adminRequest(ctx, 'POST', '/sql/readonly', { body: { sql: query.sql } });
42
- return (data && data.rows) || [];
43
- } catch (err) {
44
- if (query.needsStatStatements && STAT_STATEMENTS_MISSING.test(err.message)) {
45
- throw new StatStatementsMissing();
46
- }
47
- throw err;
48
- }
49
- }
50
-
51
- // Statements span lines and `to_char` pads numbers; a table cell wants neither.
52
- // A statement is also cut short, or one query pushes every other column off
53
- // screen; -o json keeps it whole.
54
- const CELL_WIDTH = 60;
55
- function cell(value) {
56
- if (typeof value !== 'string') return value;
57
- const text = value.replace(/\s+/g, ' ').trim();
58
- return text.length > CELL_WIDTH ? `${text.slice(0, CELL_WIDTH - 1)}…` : text;
59
- }
60
-
61
- function dbCommand(name) {
62
- const { name: canonical, query } = resolveQuery(name);
63
- const replaced = canonical !== name;
64
- return {
65
- summary: replaced ? `Deprecated: use "${canonical}" instead` : query.summary,
66
- usage: `motherbase inspect db ${name}`,
67
- async run(ctx) {
68
- const rows = await runQuery(ctx, query);
69
- output(ctx, { query: canonical, rows }, (_d, ui) => {
70
- if (replaced) ui.warn(`"inspect db ${name}" is deprecated: showing "${canonical}".`);
71
- const display = rows.map(row => Object.fromEntries(Object.entries(row).map(([k, v]) => [k, cell(v)])));
72
- ui.table(display, query.columns);
73
- });
74
- },
75
- };
76
- }
77
-
78
- const db = {
79
- summary: 'Inspect the database: bloat, locks, slow queries, index and table stats',
80
- usage: `motherbase inspect db <${Object.keys(QUERIES).join('|')}>`,
81
- subcommands: Object.fromEntries(
82
- [...Object.keys(QUERIES), ...Object.keys(DEPRECATED)].map(name => [name, dbCommand(name)]),
83
- ),
84
- };
85
-
86
- // Local-time YYYY-MM-DD: the report's folder, one per day as upstream does.
87
- function dateFolder(now = new Date()) {
88
- const pad = n => String(n).padStart(2, '0');
89
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
90
- }
91
-
92
- const report = {
93
- summary: 'Write every inspect query to a CSV in a dated folder',
94
- usage: 'motherbase inspect report [--output-dir <dir>]',
95
- async run(ctx, _args, flags) {
96
- const dir = path.resolve(ctx.cwd, String(flags['output-dir'] || '.'), dateFolder());
97
- fs.mkdirSync(dir, { recursive: true });
98
-
99
- const files = [];
100
- const skipped = [];
101
- // One at a time: fourteen catalog scans at once is a load spike on the
102
- // database being diagnosed.
103
- for (const { file, name, query } of REPORT) {
104
- let rows;
105
- try {
106
- rows = await runQuery(ctx, query);
107
- } catch (err) {
108
- // A missing pg_stat_statements costs two files, not the whole report.
109
- if (!(err instanceof StatStatementsMissing)) throw err;
110
- skipped.push(name);
111
- continue;
112
- }
113
- const target = path.join(dir, `${file}.csv`);
114
- fs.writeFileSync(target, toCsv(query.columns, rows));
115
- files.push(target);
116
- }
117
-
118
- output(ctx, { outputDir: dir, files, skipped }, (d, ui) => {
119
- if (d.skipped.length) ui.warn(`Skipped ${d.skipped.join(', ')}: ${new StatStatementsMissing().message}`);
120
- ui.success(`Saved ${d.files.length} CSV files to ${path.relative(ctx.cwd, d.outputDir) || '.'}`);
121
- });
122
- },
123
- };
124
-
125
- const subcommands = {
126
- db,
127
- report,
128
- logs: {
129
- summary: 'Recent server logs',
130
- usage: 'motherbase inspect logs [--limit n] [--source api|rest|postgres|pooler|auth|storage|realtime|edge-function|cron|platform]',
131
- async run(ctx, args, flags) {
132
- // A Logs Explorer "source" is the server's `service` filter.
133
- const query = flags.limit || flags.source ? { limit: flags.limit, service: flags.source } : undefined;
134
- const data = await api.adminRequest(ctx, 'GET', '/logs', { query });
135
- const logs = (data && data.logs) || [];
136
- output(ctx, data, (_d, ui) =>
137
- logs.forEach(l => ui.print(typeof l === 'string' ? l : `${l.level || ''} ${l.source || ''} ${l.message || JSON.stringify(l)}`)),
138
- );
139
- },
140
- },
141
- 'index-advisor': {
142
- summary: 'Suggest indexes for a statement (hypopg + index_advisor)',
143
- usage: 'motherbase inspect index-advisor --sql "<select ...>" | motherbase inspect index-advisor --enable',
144
- async run(ctx, args, flags) {
145
- if (flags.enable) {
146
- const status = await api.adminRequest(ctx, 'POST', '/performance/index-advisor/enable');
147
- output(ctx, status, (_d, ui) => ui.success('Index advisor enabled'));
148
- return;
149
- }
150
- const query = flags.sql || flags.e;
151
- if (typeof query !== 'string') throw new CliError('Provide the statement with --sql "..." (or --enable to install the advisor).');
152
- const data = await api.adminRequest(ctx, 'POST', '/performance/index-advisor', { body: { query } });
153
- output(ctx, data, (d, ui) => {
154
- const statements = (d && d.indexStatements) || [];
155
- if (!statements.length) ui.print('No index would help this statement.');
156
- statements.forEach(s => ui.print(s));
157
- if (d && d.improvementPct != null) ui.print(`Estimated cost drop: ${d.improvementPct}%`);
158
- });
159
- },
160
- },
161
- stats: simple('/stats', 'Stats overview'),
162
- performance: simple('/performance', 'Performance metrics'),
163
- requests: simple('/metrics/requests', 'Request metrics'),
164
- advisor: simple('/advisor', 'Advisor findings'),
165
- shield: simple('/shield', 'Security shield report'),
166
- };
167
-
168
- for (const [name, def] of Object.entries(subcommands)) {
169
- if (!def.usage) def.usage = `motherbase inspect ${name}`;
170
- }
171
-
172
- module.exports = {
173
- summary: 'Diagnostics: database inspection, report, logs, stats, index-advisor, advisor, shield',
174
- usage: 'motherbase inspect <db|report|logs|stats|performance|index-advisor|requests|advisor|shield>',
175
- subcommands,
176
- dateFolder,
177
- cell,
178
- };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const api = require('../api');
6
+ const { output } = require('./_util');
7
+ const { CliError } = require('../context');
8
+ const { QUERIES, DEPRECATED, REPORT, resolveQuery } = require('../inspect/queries');
9
+ const { toCsv } = require('../inspect/csv');
10
+
11
+ function simple(apiPath, summary, render) {
12
+ return {
13
+ summary,
14
+ // usage is filled in from the subcommand key below: deriving it from the
15
+ // summary printed `inspect security` for `shield` and `inspect request` for
16
+ // `requests`, neither of which is a command.
17
+ usage: '',
18
+ async run(ctx) {
19
+ const data = await api.adminRequest(ctx, 'GET', apiPath);
20
+ output(ctx, data, render || ((d, ui) => ui.json(d)));
21
+ },
22
+ };
23
+ }
24
+
25
+ // Postgres' two ways of saying pg_stat_statements is not usable here: the
26
+ // extension was never created in this database, or the library is not preloaded.
27
+ const STAT_STATEMENTS_MISSING = /pg_stat_statements.*(does not exist|must be loaded)/i;
28
+
29
+ class StatStatementsMissing extends CliError {
30
+ constructor() {
31
+ super(
32
+ 'pg_stat_statements is not enabled in this database. Enable it from Studio (Database > Extensions) '
33
+ + 'or run: motherbase db sql -e "create extension pg_stat_statements"',
34
+ );
35
+ }
36
+ }
37
+
38
+ /** Rows of one inspect query, run on the read-only SQL endpoint. */
39
+ async function runQuery(ctx, query) {
40
+ try {
41
+ const data = await api.adminRequest(ctx, 'POST', '/sql/readonly', { body: { sql: query.sql } });
42
+ return (data && data.rows) || [];
43
+ } catch (err) {
44
+ if (query.needsStatStatements && STAT_STATEMENTS_MISSING.test(err.message)) {
45
+ throw new StatStatementsMissing();
46
+ }
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ // Statements span lines and `to_char` pads numbers; a table cell wants neither.
52
+ // A statement is also cut short, or one query pushes every other column off
53
+ // screen; -o json keeps it whole.
54
+ const CELL_WIDTH = 60;
55
+ function cell(value) {
56
+ if (typeof value !== 'string') return value;
57
+ const text = value.replace(/\s+/g, ' ').trim();
58
+ return text.length > CELL_WIDTH ? `${text.slice(0, CELL_WIDTH - 1)}…` : text;
59
+ }
60
+
61
+ function dbCommand(name) {
62
+ const { name: canonical, query } = resolveQuery(name);
63
+ const replaced = canonical !== name;
64
+ return {
65
+ summary: replaced ? `Deprecated: use "${canonical}" instead` : query.summary,
66
+ usage: `motherbase inspect db ${name}`,
67
+ async run(ctx) {
68
+ const rows = await runQuery(ctx, query);
69
+ output(ctx, { query: canonical, rows }, (_d, ui) => {
70
+ if (replaced) ui.warn(`"inspect db ${name}" is deprecated: showing "${canonical}".`);
71
+ const display = rows.map(row => Object.fromEntries(Object.entries(row).map(([k, v]) => [k, cell(v)])));
72
+ ui.table(display, query.columns);
73
+ });
74
+ },
75
+ };
76
+ }
77
+
78
+ const db = {
79
+ summary: 'Inspect the database: bloat, locks, slow queries, index and table stats',
80
+ usage: `motherbase inspect db <${Object.keys(QUERIES).join('|')}>`,
81
+ subcommands: Object.fromEntries(
82
+ [...Object.keys(QUERIES), ...Object.keys(DEPRECATED)].map(name => [name, dbCommand(name)]),
83
+ ),
84
+ };
85
+
86
+ // Local-time YYYY-MM-DD: the report's folder, one per day as upstream does.
87
+ function dateFolder(now = new Date()) {
88
+ const pad = n => String(n).padStart(2, '0');
89
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
90
+ }
91
+
92
+ const report = {
93
+ summary: 'Write every inspect query to a CSV in a dated folder',
94
+ usage: 'motherbase inspect report [--output-dir <dir>]',
95
+ async run(ctx, _args, flags) {
96
+ const dir = path.resolve(ctx.cwd, String(flags['output-dir'] || '.'), dateFolder());
97
+ fs.mkdirSync(dir, { recursive: true });
98
+
99
+ const files = [];
100
+ const skipped = [];
101
+ // One at a time: fourteen catalog scans at once is a load spike on the
102
+ // database being diagnosed.
103
+ for (const { file, name, query } of REPORT) {
104
+ let rows;
105
+ try {
106
+ rows = await runQuery(ctx, query);
107
+ } catch (err) {
108
+ // A missing pg_stat_statements costs two files, not the whole report.
109
+ if (!(err instanceof StatStatementsMissing)) throw err;
110
+ skipped.push(name);
111
+ continue;
112
+ }
113
+ const target = path.join(dir, `${file}.csv`);
114
+ fs.writeFileSync(target, toCsv(query.columns, rows));
115
+ files.push(target);
116
+ }
117
+
118
+ output(ctx, { outputDir: dir, files, skipped }, (d, ui) => {
119
+ if (d.skipped.length) ui.warn(`Skipped ${d.skipped.join(', ')}: ${new StatStatementsMissing().message}`);
120
+ ui.success(`Saved ${d.files.length} CSV files to ${path.relative(ctx.cwd, d.outputDir) || '.'}`);
121
+ });
122
+ },
123
+ };
124
+
125
+ const subcommands = {
126
+ db,
127
+ report,
128
+ logs: {
129
+ summary: 'Recent server logs',
130
+ usage: 'motherbase inspect logs [--limit n] [--source api|rest|postgres|pooler|auth|storage|realtime|edge-function|cron|platform]',
131
+ async run(ctx, args, flags) {
132
+ // A Logs Explorer "source" is the server's `service` filter.
133
+ const query = flags.limit || flags.source ? { limit: flags.limit, service: flags.source } : undefined;
134
+ const data = await api.adminRequest(ctx, 'GET', '/logs', { query });
135
+ const logs = (data && data.logs) || [];
136
+ output(ctx, data, (_d, ui) =>
137
+ logs.forEach(l => ui.print(typeof l === 'string' ? l : `${l.level || ''} ${l.source || ''} ${l.message || JSON.stringify(l)}`)),
138
+ );
139
+ },
140
+ },
141
+ 'index-advisor': {
142
+ summary: 'Suggest indexes for a statement (hypopg + index_advisor)',
143
+ usage: 'motherbase inspect index-advisor --sql "<select ...>" | motherbase inspect index-advisor --enable',
144
+ async run(ctx, args, flags) {
145
+ if (flags.enable) {
146
+ const status = await api.adminRequest(ctx, 'POST', '/performance/index-advisor/enable');
147
+ output(ctx, status, (_d, ui) => ui.success('Index advisor enabled'));
148
+ return;
149
+ }
150
+ const query = flags.sql || flags.e;
151
+ if (typeof query !== 'string') throw new CliError('Provide the statement with --sql "..." (or --enable to install the advisor).');
152
+ const data = await api.adminRequest(ctx, 'POST', '/performance/index-advisor', { body: { query } });
153
+ output(ctx, data, (d, ui) => {
154
+ const statements = (d && d.indexStatements) || [];
155
+ if (!statements.length) ui.print('No index would help this statement.');
156
+ statements.forEach(s => ui.print(s));
157
+ if (d && d.improvementPct != null) ui.print(`Estimated cost drop: ${d.improvementPct}%`);
158
+ });
159
+ },
160
+ },
161
+ stats: simple('/stats', 'Stats overview'),
162
+ performance: simple('/performance', 'Performance metrics'),
163
+ requests: simple('/metrics/requests', 'Request metrics'),
164
+ advisor: simple('/advisor', 'Advisor findings'),
165
+ shield: simple('/shield', 'Security shield report'),
166
+ };
167
+
168
+ for (const [name, def] of Object.entries(subcommands)) {
169
+ if (!def.usage) def.usage = `motherbase inspect ${name}`;
170
+ }
171
+
172
+ module.exports = {
173
+ summary: 'Diagnostics: database inspection, report, logs, stats, index-advisor, advisor, shield',
174
+ usage: 'motherbase inspect <db|report|logs|stats|performance|index-advisor|requests|advisor|shield>',
175
+ subcommands,
176
+ dateFolder,
177
+ cell,
178
+ };