@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.
- package/LICENSE +21 -21
- package/README.md +248 -247
- package/bin/.motherbase/config.json +5 -0
- package/bin/motherbase.js +10 -10
- package/package.json +1 -1
- package/src/api.js +39 -16
- package/src/commands/_util.js +106 -106
- package/src/commands/backups.js +16 -16
- package/src/commands/branches.js +113 -113
- package/src/commands/completion.js +90 -90
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db.js +103 -103
- package/src/commands/domains.js +124 -124
- package/src/commands/gen.js +178 -172
- package/src/commands/import.js +55 -55
- package/src/commands/index.js +85 -85
- package/src/commands/inspect.js +178 -178
- package/src/commands/keys.js +72 -72
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +100 -23
- package/src/commands/login.js +9 -3
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +61 -61
- package/src/commands/migrate.js +0 -5
- package/src/commands/migration.js +1 -1
- package/src/commands/network.js +83 -83
- package/src/commands/orgs.js +40 -40
- package/src/commands/postgres-config.js +113 -113
- package/src/commands/projects.js +131 -130
- package/src/commands/queues.js +133 -133
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -66
- package/src/commands/signing.js +130 -130
- package/src/commands/snippets.js +44 -44
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -261
- package/src/commands/test.js +9 -9
- package/src/commands/unlink.js +15 -15
- package/src/commands/vault.js +82 -82
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -150
- package/src/config.js +98 -98
- package/src/context.js +106 -106
- package/src/edge-templates.js +194 -194
- package/src/format.js +92 -92
- package/src/inspect/csv.js +22 -22
- package/src/inspect/queries.js +562 -562
- package/src/mcp/platform-tools.js +233 -233
- package/src/mcp/protocol.js +152 -152
- package/src/mcp/storage-tools.js +93 -93
- package/src/mcp/tools.js +478 -478
- package/src/migration/commands.js +2 -2
- package/src/migration/files.js +13 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -62
- package/src/ui.js +111 -111
package/src/commands/domains.js
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Custom domains and vanity subdomains (Supabase's `supabase domains` and
|
|
4
|
-
// `supabase vanity-subdomains`). Both are Console routes on the linked project.
|
|
5
|
-
|
|
6
|
-
const api = require('../api');
|
|
7
|
-
const { output, requireArg, consoleProjectPath } = require('./_util');
|
|
8
|
-
|
|
9
|
-
function renderHostname(d, ui) {
|
|
10
|
-
const custom = d.customHostname;
|
|
11
|
-
if (!custom) {
|
|
12
|
-
ui.info(`No custom domain. The API answers on ${d.projectDomain}.`);
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
ui.keyValue({ hostname: custom.hostname, status: custom.status, projectDomain: d.projectDomain });
|
|
16
|
-
if (custom.status !== 'active') {
|
|
17
|
-
ui.print('');
|
|
18
|
-
ui.print('Publish these DNS records, then run `motherbase domains reverify`:');
|
|
19
|
-
ui.table(custom.requiredRecords, [
|
|
20
|
-
{ key: 'type', label: 'TYPE' },
|
|
21
|
-
{ key: 'name', label: 'NAME' },
|
|
22
|
-
{ key: 'value', label: 'VALUE' },
|
|
23
|
-
]);
|
|
24
|
-
}
|
|
25
|
-
for (const check of custom.checks || []) {
|
|
26
|
-
ui[check.ok ? 'success' : 'warn'](`${check.record} ${check.name}: ${check.ok ? 'ok' : `found ${check.found.join(', ') || 'nothing'}`}`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const hostnamePath = (ctx, suffix = '') => consoleProjectPath(ctx, `/custom-hostname${suffix}`);
|
|
31
|
-
|
|
32
|
-
const domains = {
|
|
33
|
-
summary: 'Custom domain for the linked project\'s API',
|
|
34
|
-
usage: 'motherbase domains <create|get|reverify|activate|delete> ...',
|
|
35
|
-
subcommands: {
|
|
36
|
-
create: {
|
|
37
|
-
summary: 'Start using a custom hostname (prints the DNS records to publish)',
|
|
38
|
-
usage: 'motherbase domains create --custom-hostname <api.example.com>',
|
|
39
|
-
async run(ctx, _args, flags) {
|
|
40
|
-
const hostname = requireArg(flags['custom-hostname'], 'custom-hostname');
|
|
41
|
-
const data = await api.consoleRequest(ctx, 'POST', hostnamePath(ctx), { body: { hostname } });
|
|
42
|
-
output(ctx, data, renderHostname);
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
get: {
|
|
46
|
-
summary: 'Show the custom hostname and its verification state',
|
|
47
|
-
usage: 'motherbase domains get',
|
|
48
|
-
async run(ctx) {
|
|
49
|
-
output(ctx, await api.consoleRequest(ctx, 'GET', hostnamePath(ctx)), renderHostname);
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
reverify: {
|
|
53
|
-
summary: 'Check the DNS records again',
|
|
54
|
-
usage: 'motherbase domains reverify',
|
|
55
|
-
async run(ctx) {
|
|
56
|
-
output(ctx, await api.consoleRequest(ctx, 'POST', hostnamePath(ctx, '/reverify')), renderHostname);
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
activate: {
|
|
60
|
-
summary: 'Serve the API on the verified hostname (TLS is issued on first request)',
|
|
61
|
-
usage: 'motherbase domains activate',
|
|
62
|
-
async run(ctx) {
|
|
63
|
-
output(ctx, await api.consoleRequest(ctx, 'POST', hostnamePath(ctx, '/activate')), renderHostname);
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
delete: {
|
|
67
|
-
summary: 'Stop using the custom hostname',
|
|
68
|
-
usage: 'motherbase domains delete',
|
|
69
|
-
async run(ctx) {
|
|
70
|
-
const data = await api.consoleRequest(ctx, 'DELETE', hostnamePath(ctx));
|
|
71
|
-
output(ctx, data, (_d, ui) => ui.success('Custom hostname removed'));
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
function renderVanity(d, ui) {
|
|
78
|
-
if (d.vanitySubdomain) ui.keyValue(d.vanitySubdomain);
|
|
79
|
-
else ui.info(`No vanity subdomain${d.vanitySuffix ? ` (they live under ${d.vanitySuffix})` : ''}.`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const vanityPath = (ctx, suffix = '') => consoleProjectPath(ctx, `/vanity-subdomain${suffix}`);
|
|
83
|
-
|
|
84
|
-
const vanitySubdomains = {
|
|
85
|
-
summary: 'Vanity subdomain under the platform domain',
|
|
86
|
-
usage: 'motherbase vanity-subdomains <get|check-availability|activate|delete> ...',
|
|
87
|
-
subcommands: {
|
|
88
|
-
get: {
|
|
89
|
-
summary: 'Show the active vanity subdomain',
|
|
90
|
-
usage: 'motherbase vanity-subdomains get',
|
|
91
|
-
async run(ctx) {
|
|
92
|
-
output(ctx, await api.consoleRequest(ctx, 'GET', vanityPath(ctx)), renderVanity);
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
'check-availability': {
|
|
96
|
-
summary: 'Is a subdomain free?',
|
|
97
|
-
usage: 'motherbase vanity-subdomains check-availability --desired-subdomain <name>',
|
|
98
|
-
async run(ctx, _args, flags) {
|
|
99
|
-
const subdomain = requireArg(flags['desired-subdomain'], 'desired-subdomain');
|
|
100
|
-
const data = await api.consoleRequest(ctx, 'POST', vanityPath(ctx, '/check-availability'), { body: { subdomain } });
|
|
101
|
-
output(ctx, data, (d, ui) => (d.available ? ui.success(`${d.hostname} is available`) : ui.warn(`${d.hostname} is taken`)));
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
activate: {
|
|
105
|
-
summary: 'Serve the API on <name>.<platform domain> as well',
|
|
106
|
-
usage: 'motherbase vanity-subdomains activate --desired-subdomain <name>',
|
|
107
|
-
async run(ctx, _args, flags) {
|
|
108
|
-
const subdomain = requireArg(flags['desired-subdomain'], 'desired-subdomain');
|
|
109
|
-
const data = await api.consoleRequest(ctx, 'POST', vanityPath(ctx), { body: { subdomain } });
|
|
110
|
-
output(ctx, data, renderVanity);
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
delete: {
|
|
114
|
-
summary: 'Remove the vanity subdomain',
|
|
115
|
-
usage: 'motherbase vanity-subdomains delete',
|
|
116
|
-
async run(ctx) {
|
|
117
|
-
const data = await api.consoleRequest(ctx, 'DELETE', vanityPath(ctx));
|
|
118
|
-
output(ctx, data, (_d, ui) => ui.success('Vanity subdomain removed'));
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
module.exports = { domains, vanitySubdomains };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Custom domains and vanity subdomains (Supabase's `supabase domains` and
|
|
4
|
+
// `supabase vanity-subdomains`). Both are Console routes on the linked project.
|
|
5
|
+
|
|
6
|
+
const api = require('../api');
|
|
7
|
+
const { output, requireArg, consoleProjectPath } = require('./_util');
|
|
8
|
+
|
|
9
|
+
function renderHostname(d, ui) {
|
|
10
|
+
const custom = d.customHostname;
|
|
11
|
+
if (!custom) {
|
|
12
|
+
ui.info(`No custom domain. The API answers on ${d.projectDomain}.`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
ui.keyValue({ hostname: custom.hostname, status: custom.status, projectDomain: d.projectDomain });
|
|
16
|
+
if (custom.status !== 'active') {
|
|
17
|
+
ui.print('');
|
|
18
|
+
ui.print('Publish these DNS records, then run `motherbase domains reverify`:');
|
|
19
|
+
ui.table(custom.requiredRecords, [
|
|
20
|
+
{ key: 'type', label: 'TYPE' },
|
|
21
|
+
{ key: 'name', label: 'NAME' },
|
|
22
|
+
{ key: 'value', label: 'VALUE' },
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
for (const check of custom.checks || []) {
|
|
26
|
+
ui[check.ok ? 'success' : 'warn'](`${check.record} ${check.name}: ${check.ok ? 'ok' : `found ${check.found.join(', ') || 'nothing'}`}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const hostnamePath = (ctx, suffix = '') => consoleProjectPath(ctx, `/custom-hostname${suffix}`);
|
|
31
|
+
|
|
32
|
+
const domains = {
|
|
33
|
+
summary: 'Custom domain for the linked project\'s API',
|
|
34
|
+
usage: 'motherbase domains <create|get|reverify|activate|delete> ...',
|
|
35
|
+
subcommands: {
|
|
36
|
+
create: {
|
|
37
|
+
summary: 'Start using a custom hostname (prints the DNS records to publish)',
|
|
38
|
+
usage: 'motherbase domains create --custom-hostname <api.example.com>',
|
|
39
|
+
async run(ctx, _args, flags) {
|
|
40
|
+
const hostname = requireArg(flags['custom-hostname'], 'custom-hostname');
|
|
41
|
+
const data = await api.consoleRequest(ctx, 'POST', hostnamePath(ctx), { body: { hostname } });
|
|
42
|
+
output(ctx, data, renderHostname);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
get: {
|
|
46
|
+
summary: 'Show the custom hostname and its verification state',
|
|
47
|
+
usage: 'motherbase domains get',
|
|
48
|
+
async run(ctx) {
|
|
49
|
+
output(ctx, await api.consoleRequest(ctx, 'GET', hostnamePath(ctx)), renderHostname);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
reverify: {
|
|
53
|
+
summary: 'Check the DNS records again',
|
|
54
|
+
usage: 'motherbase domains reverify',
|
|
55
|
+
async run(ctx) {
|
|
56
|
+
output(ctx, await api.consoleRequest(ctx, 'POST', hostnamePath(ctx, '/reverify')), renderHostname);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
activate: {
|
|
60
|
+
summary: 'Serve the API on the verified hostname (TLS is issued on first request)',
|
|
61
|
+
usage: 'motherbase domains activate',
|
|
62
|
+
async run(ctx) {
|
|
63
|
+
output(ctx, await api.consoleRequest(ctx, 'POST', hostnamePath(ctx, '/activate')), renderHostname);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
delete: {
|
|
67
|
+
summary: 'Stop using the custom hostname',
|
|
68
|
+
usage: 'motherbase domains delete',
|
|
69
|
+
async run(ctx) {
|
|
70
|
+
const data = await api.consoleRequest(ctx, 'DELETE', hostnamePath(ctx));
|
|
71
|
+
output(ctx, data, (_d, ui) => ui.success('Custom hostname removed'));
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function renderVanity(d, ui) {
|
|
78
|
+
if (d.vanitySubdomain) ui.keyValue(d.vanitySubdomain);
|
|
79
|
+
else ui.info(`No vanity subdomain${d.vanitySuffix ? ` (they live under ${d.vanitySuffix})` : ''}.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const vanityPath = (ctx, suffix = '') => consoleProjectPath(ctx, `/vanity-subdomain${suffix}`);
|
|
83
|
+
|
|
84
|
+
const vanitySubdomains = {
|
|
85
|
+
summary: 'Vanity subdomain under the platform domain',
|
|
86
|
+
usage: 'motherbase vanity-subdomains <get|check-availability|activate|delete> ...',
|
|
87
|
+
subcommands: {
|
|
88
|
+
get: {
|
|
89
|
+
summary: 'Show the active vanity subdomain',
|
|
90
|
+
usage: 'motherbase vanity-subdomains get',
|
|
91
|
+
async run(ctx) {
|
|
92
|
+
output(ctx, await api.consoleRequest(ctx, 'GET', vanityPath(ctx)), renderVanity);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
'check-availability': {
|
|
96
|
+
summary: 'Is a subdomain free?',
|
|
97
|
+
usage: 'motherbase vanity-subdomains check-availability --desired-subdomain <name>',
|
|
98
|
+
async run(ctx, _args, flags) {
|
|
99
|
+
const subdomain = requireArg(flags['desired-subdomain'], 'desired-subdomain');
|
|
100
|
+
const data = await api.consoleRequest(ctx, 'POST', vanityPath(ctx, '/check-availability'), { body: { subdomain } });
|
|
101
|
+
output(ctx, data, (d, ui) => (d.available ? ui.success(`${d.hostname} is available`) : ui.warn(`${d.hostname} is taken`)));
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
activate: {
|
|
105
|
+
summary: 'Serve the API on <name>.<platform domain> as well',
|
|
106
|
+
usage: 'motherbase vanity-subdomains activate --desired-subdomain <name>',
|
|
107
|
+
async run(ctx, _args, flags) {
|
|
108
|
+
const subdomain = requireArg(flags['desired-subdomain'], 'desired-subdomain');
|
|
109
|
+
const data = await api.consoleRequest(ctx, 'POST', vanityPath(ctx), { body: { subdomain } });
|
|
110
|
+
output(ctx, data, renderVanity);
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
delete: {
|
|
114
|
+
summary: 'Remove the vanity subdomain',
|
|
115
|
+
usage: 'motherbase vanity-subdomains delete',
|
|
116
|
+
async run(ctx) {
|
|
117
|
+
const data = await api.consoleRequest(ctx, 'DELETE', vanityPath(ctx));
|
|
118
|
+
output(ctx, data, (_d, ui) => ui.success('Vanity subdomain removed'));
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
module.exports = { domains, vanitySubdomains };
|
package/src/commands/gen.js
CHANGED
|
@@ -1,172 +1,178 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Code generation from the live schema: TypeScript types and zod schemas.
|
|
4
|
-
//
|
|
5
|
-
// The server owns both emitters (apps/server/services/admin/typegen/).
|
|
6
|
-
// This command is a transport: it asks `GET /admin/schema/types` for the
|
|
7
|
-
// rendered file and writes it. Re-implementing the emitter here would guarantee
|
|
8
|
-
// drift between the CLI, the Studio download and the MCP tool within a release.
|
|
9
|
-
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const api = require('../api');
|
|
13
|
-
const config = require('../config');
|
|
14
|
-
const { output } = require('./_util');
|
|
15
|
-
const { outputFormat } = require('../format');
|
|
16
|
-
const { CliError } = require('../context');
|
|
17
|
-
|
|
18
|
-
const DEFAULT_OUTPUT = 'database.types.ts';
|
|
19
|
-
const DEFAULT_ZOD_OUTPUT = 'database.schemas.ts';
|
|
20
|
-
const DEFAULT_LOCAL_URL = 'http://localhost:5001';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* `--local` bypasses the Console proxy and talks straight to a project API on
|
|
24
|
-
* this machine, which is how someone iterating on a local stack works: there is
|
|
25
|
-
* no console account, no link file, just a server on a port. Everything else
|
|
26
|
-
* about the context (flags, cwd, json mode) is preserved.
|
|
27
|
-
*/
|
|
28
|
-
function resolveTarget(ctx, flags) {
|
|
29
|
-
if (!flags.local) return ctx;
|
|
30
|
-
const baseUrl = config.normalizeUrl(flags.url || ctx.env.MOTHERBASE_URL || DEFAULT_LOCAL_URL);
|
|
31
|
-
const token = flags.token || ctx.env.MOTHERBASE_ADMIN_TOKEN || ctx.token;
|
|
32
|
-
return Object.assign({}, ctx, {
|
|
33
|
-
mode: 'direct',
|
|
34
|
-
requireProject: () => ({ mode: 'direct', baseUrl, token }),
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Git checkouts on Windows may rewrite line endings. The generator always emits
|
|
39
|
-
// LF, so compare on normalised text: a CRLF working copy is not schema drift.
|
|
40
|
-
function normalize(text) {
|
|
41
|
-
return String(text).replace(/\r\n/g, '\n');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** First line where the two files diverge, 1-based, for a useful error. */
|
|
45
|
-
function firstDifference(a, b) {
|
|
46
|
-
const left = normalize(a).split('\n');
|
|
47
|
-
const right = normalize(b).split('\n');
|
|
48
|
-
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
49
|
-
if (left[i] !== right[i]) {
|
|
50
|
-
return { line: i + 1, expected: right[i], actual: left[i] };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Both targets are the same transport: ask the server for the rendered file,
|
|
58
|
-
* then write it, diff it or print it. `field` names which rendering to take
|
|
59
|
-
* from the one response, so a new target is a few lines rather than a copy of
|
|
60
|
-
* this whole flow.
|
|
61
|
-
*/
|
|
62
|
-
function emitter({ field, defaultOutput, label, command }) {
|
|
63
|
-
return async function run(ctx, args, flags) {
|
|
64
|
-
const schema = flags.schema || 'public';
|
|
65
|
-
const data = await api.adminRequest(resolveTarget(ctx, flags), 'GET', '/schema/types', {
|
|
66
|
-
query: { schema },
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const types = data && typeof data[field] === 'string' ? data[field] : null;
|
|
70
|
-
if (!types) {
|
|
71
|
-
throw new CliError(
|
|
72
|
-
`The server returned no generated ${label}. Check that the project serves ` +
|
|
73
|
-
`GET /admin/schema/types with a build new enough for \`${command}\`.`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Code generation from the live schema: TypeScript types and zod schemas.
|
|
4
|
+
//
|
|
5
|
+
// The server owns both emitters (apps/server/services/admin/typegen/).
|
|
6
|
+
// This command is a transport: it asks `GET /admin/schema/types` for the
|
|
7
|
+
// rendered file and writes it. Re-implementing the emitter here would guarantee
|
|
8
|
+
// drift between the CLI, the Studio download and the MCP tool within a release.
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const api = require('../api');
|
|
13
|
+
const config = require('../config');
|
|
14
|
+
const { output } = require('./_util');
|
|
15
|
+
const { outputFormat } = require('../format');
|
|
16
|
+
const { CliError } = require('../context');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_OUTPUT = 'database.types.ts';
|
|
19
|
+
const DEFAULT_ZOD_OUTPUT = 'database.schemas.ts';
|
|
20
|
+
const DEFAULT_LOCAL_URL = 'http://localhost:5001';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `--local` bypasses the Console proxy and talks straight to a project API on
|
|
24
|
+
* this machine, which is how someone iterating on a local stack works: there is
|
|
25
|
+
* no console account, no link file, just a server on a port. Everything else
|
|
26
|
+
* about the context (flags, cwd, json mode) is preserved.
|
|
27
|
+
*/
|
|
28
|
+
function resolveTarget(ctx, flags) {
|
|
29
|
+
if (!flags.local) return ctx;
|
|
30
|
+
const baseUrl = config.normalizeUrl(flags.url || ctx.env.MOTHERBASE_URL || DEFAULT_LOCAL_URL);
|
|
31
|
+
const token = flags.token || ctx.env.MOTHERBASE_ADMIN_TOKEN || ctx.token;
|
|
32
|
+
return Object.assign({}, ctx, {
|
|
33
|
+
mode: 'direct',
|
|
34
|
+
requireProject: () => ({ mode: 'direct', baseUrl, token }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Git checkouts on Windows may rewrite line endings. The generator always emits
|
|
39
|
+
// LF, so compare on normalised text: a CRLF working copy is not schema drift.
|
|
40
|
+
function normalize(text) {
|
|
41
|
+
return String(text).replace(/\r\n/g, '\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** First line where the two files diverge, 1-based, for a useful error. */
|
|
45
|
+
function firstDifference(a, b) {
|
|
46
|
+
const left = normalize(a).split('\n');
|
|
47
|
+
const right = normalize(b).split('\n');
|
|
48
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
49
|
+
if (left[i] !== right[i]) {
|
|
50
|
+
return { line: i + 1, expected: right[i], actual: left[i] };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Both targets are the same transport: ask the server for the rendered file,
|
|
58
|
+
* then write it, diff it or print it. `field` names which rendering to take
|
|
59
|
+
* from the one response, so a new target is a few lines rather than a copy of
|
|
60
|
+
* this whole flow.
|
|
61
|
+
*/
|
|
62
|
+
function emitter({ field, defaultOutput, label, command }) {
|
|
63
|
+
return async function run(ctx, args, flags) {
|
|
64
|
+
const schema = flags.schema || 'public';
|
|
65
|
+
const data = await api.adminRequest(resolveTarget(ctx, flags), 'GET', '/schema/types', {
|
|
66
|
+
query: { schema },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const types = data && typeof data[field] === 'string' ? data[field] : null;
|
|
70
|
+
if (!types) {
|
|
71
|
+
throw new CliError(
|
|
72
|
+
`The server returned no generated ${label}. Check that the project serves ` +
|
|
73
|
+
`GET /admin/schema/types with a build new enough for \`${command}\`.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A whole types file dumped into an interactive terminal is truncated by the
|
|
78
|
+
// scrollback and useless, so a TTY gets the default file instead. Pipes and
|
|
79
|
+
// redirects (`> database.types.ts`) still receive it on stdout.
|
|
80
|
+
const toTerminal = process.stdout.isTTY && outputFormat(ctx.flags) === 'pretty';
|
|
81
|
+
const outputPath = flags.output && flags.output !== true
|
|
82
|
+
? String(flags.output)
|
|
83
|
+
: toTerminal && !flags.check ? defaultOutput : null;
|
|
84
|
+
const DEFAULT_OUTPUT = defaultOutput;
|
|
85
|
+
|
|
86
|
+
// --check is the CI drift gate: compare the committed file with a fresh
|
|
87
|
+
// generation and fail loudly, without ever writing.
|
|
88
|
+
if (flags.check) {
|
|
89
|
+
const target = path.resolve(ctx.cwd, outputPath || DEFAULT_OUTPUT);
|
|
90
|
+
if (!fs.existsSync(target)) {
|
|
91
|
+
throw new CliError(`${target} does not exist. Run \`${command} --output ${outputPath || DEFAULT_OUTPUT}\`.`);
|
|
92
|
+
}
|
|
93
|
+
const onDisk = fs.readFileSync(target, 'utf8');
|
|
94
|
+
const diff = firstDifference(onDisk, types);
|
|
95
|
+
if (diff) {
|
|
96
|
+
throw new CliError(
|
|
97
|
+
`${target} is out of date with the ${schema} schema (first difference at line ${diff.line}).\n` +
|
|
98
|
+
` on disk: ${diff.actual === undefined ? '(end of file)' : diff.actual}\n` +
|
|
99
|
+
` generated: ${diff.expected === undefined ? '(end of file)' : diff.expected}\n` +
|
|
100
|
+
'Re-run without --check to update it.',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
output(ctx, { schema, output: target, upToDate: true }, (d, ui) =>
|
|
104
|
+
ui.success(`${target} is up to date`),
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// No --output: write the file to stdout so `... > database.types.ts` works.
|
|
110
|
+
// Nothing else may go to stdout on this path or it lands in the file.
|
|
111
|
+
if (!outputPath) {
|
|
112
|
+
if (outputFormat(ctx.flags) !== 'pretty') {
|
|
113
|
+
output(ctx, { schema, types }, () => {});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
process.stdout.write(types);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const target = path.resolve(ctx.cwd, outputPath);
|
|
121
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
122
|
+
fs.writeFileSync(target, types, 'utf8');
|
|
123
|
+
output(ctx, { schema, output: target, bytes: Buffer.byteLength(types) }, (d, ui) =>
|
|
124
|
+
ui.success(`Wrote ${target} (${schema} schema)`),
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const typescript = {
|
|
130
|
+
summary: 'Generate TypeScript types for the database schema',
|
|
131
|
+
usage:
|
|
132
|
+
'motherbase gen types typescript [--schema public] [--local] [--output database.types.ts] [--check]',
|
|
133
|
+
// Here `--output` names the file to write, not the `-o` output format.
|
|
134
|
+
outputIsPath: true,
|
|
135
|
+
run: emitter({
|
|
136
|
+
field: 'types',
|
|
137
|
+
defaultOutput: DEFAULT_OUTPUT,
|
|
138
|
+
label: 'types',
|
|
139
|
+
command: 'motherbase gen types typescript',
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const zod = {
|
|
144
|
+
summary: 'Generate zod schemas for the database schema',
|
|
145
|
+
usage:
|
|
146
|
+
'motherbase gen types zod [--schema public] [--local] [--output database.schemas.ts] [--check]',
|
|
147
|
+
outputIsPath: true,
|
|
148
|
+
run: emitter({
|
|
149
|
+
field: 'zod',
|
|
150
|
+
defaultOutput: DEFAULT_ZOD_OUTPUT,
|
|
151
|
+
label: 'schemas',
|
|
152
|
+
command: 'motherbase gen types zod',
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const LANGS = { typescript, zod };
|
|
157
|
+
|
|
158
|
+
// `gen types --lang typescript` is the Supabase spelling; `gen types typescript`
|
|
159
|
+
// reaches the same emitter through the subcommand.
|
|
160
|
+
const types = {
|
|
161
|
+
summary: 'Generate types from the database schema',
|
|
162
|
+
usage: 'motherbase gen types [--lang typescript|zod] [flags] | motherbase gen types <typescript|zod> [flags]',
|
|
163
|
+
subcommands: LANGS,
|
|
164
|
+
outputIsPath: true,
|
|
165
|
+
async run(ctx, args, flags) {
|
|
166
|
+
const lang = flags.lang || 'typescript';
|
|
167
|
+
if (!LANGS[lang]) throw new CliError(`--lang must be one of: ${Object.keys(LANGS).join(', ')}`);
|
|
168
|
+
return LANGS[lang].run(ctx, args, flags);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const signing = require('./signing');
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
summary: 'Generate code from the live schema, and signing keys, tokens and secrets',
|
|
176
|
+
usage: 'motherbase gen <types|signing-key|bearer-jwt|keys> ...',
|
|
177
|
+
subcommands: { types, 'signing-key': signing.signingKey, 'bearer-jwt': signing.bearerJwt, keys: signing.keys },
|
|
178
|
+
};
|