@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/edge-templates.js
CHANGED
|
@@ -1,194 +1,194 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Starting points for `motherbase functions new --template <id>`. Pure data.
|
|
4
|
-
// The Studio's "new function" picker offers the same list
|
|
5
|
-
// (apps/studio/src/features/api-tools/lib/function-templates.ts): keep the ids
|
|
6
|
-
// and sources in step.
|
|
7
|
-
//
|
|
8
|
-
// Every source is a module-shaped function (`export default handler`) that runs
|
|
9
|
-
// on the Deno edge runtime. `%NAME%` is replaced with the function's name.
|
|
10
|
-
|
|
11
|
-
const templates = [
|
|
12
|
-
{
|
|
13
|
-
id: 'hello-world',
|
|
14
|
-
label: 'Hello world',
|
|
15
|
-
description: 'Echo the request body and the time.',
|
|
16
|
-
source: `// Edge function: %NAME%
|
|
17
|
-
//
|
|
18
|
-
// Runs on Deno with the whole Web platform: Request, Response, fetch, streams,
|
|
19
|
-
// crypto.subtle (for webhook signature checks) and friends. npm:, jsr: and URL
|
|
20
|
-
// imports load as they do on Deno; a bare package name is read as npm:
|
|
21
|
-
//
|
|
22
|
-
// import { z } from 'zod';
|
|
23
|
-
//
|
|
24
|
-
// \`env\` holds this project's edge secrets, and \`sendMail()\` sends through the
|
|
25
|
-
// project's own SMTP settings (the same transport auth uses):
|
|
26
|
-
//
|
|
27
|
-
// await sendMail({ to: 'sales@example.com', subject: 'New lead', text: '...' });
|
|
28
|
-
|
|
29
|
-
export default async (req: Request) => {
|
|
30
|
-
const body = req.method === 'GET' ? {} : await req.json().catch(() => ({}));
|
|
31
|
-
return Response.json({ hello: '%NAME%', received: body, at: new Date().toISOString() });
|
|
32
|
-
};
|
|
33
|
-
`,
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
id: 'stripe-webhook',
|
|
37
|
-
label: 'Stripe webhook',
|
|
38
|
-
description: 'Verify a Stripe signature, then handle the event.',
|
|
39
|
-
source: `// Edge function: %NAME%
|
|
40
|
-
// Secrets to set: STRIPE_WEBHOOK_SECRET (the endpoint's "whsec_..." signing secret).
|
|
41
|
-
|
|
42
|
-
const encoder = new TextEncoder();
|
|
43
|
-
|
|
44
|
-
async function sign(secret: string, payload: string): Promise<string> {
|
|
45
|
-
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
46
|
-
const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
|
|
47
|
-
return [...new Uint8Array(mac)].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export default async (req: Request) => {
|
|
51
|
-
// The raw body: a re-serialised one no longer matches the signature.
|
|
52
|
-
const payload = await req.text();
|
|
53
|
-
const header = req.headers.get('stripe-signature') ?? '';
|
|
54
|
-
const parts = Object.fromEntries(header.split(',').map(part => part.split('=') as [string, string]));
|
|
55
|
-
const expected = await sign(env.STRIPE_WEBHOOK_SECRET, \`\${parts.t}.\${payload}\`);
|
|
56
|
-
if (!parts.v1 || parts.v1 !== expected) return new Response('Invalid signature', { status: 400 });
|
|
57
|
-
|
|
58
|
-
const event = JSON.parse(payload);
|
|
59
|
-
switch (event.type) {
|
|
60
|
-
case 'checkout.session.completed':
|
|
61
|
-
console.log('checkout completed', event.data.object.id);
|
|
62
|
-
break;
|
|
63
|
-
default:
|
|
64
|
-
console.log('ignored event', event.type);
|
|
65
|
-
}
|
|
66
|
-
return Response.json({ received: true });
|
|
67
|
-
};
|
|
68
|
-
`,
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 'openai',
|
|
72
|
-
label: 'OpenAI',
|
|
73
|
-
description: 'Send a prompt to OpenAI and return the answer.',
|
|
74
|
-
source: `// Edge function: %NAME%
|
|
75
|
-
// Secrets to set: OPENAI_API_KEY.
|
|
76
|
-
|
|
77
|
-
export default async (req: Request) => {
|
|
78
|
-
const { prompt } = await req.json();
|
|
79
|
-
if (typeof prompt !== 'string' || !prompt) return Response.json({ error: 'prompt is required' }, { status: 400 });
|
|
80
|
-
|
|
81
|
-
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
82
|
-
method: 'POST',
|
|
83
|
-
headers: { 'content-type': 'application/json', authorization: \`Bearer \${env.OPENAI_API_KEY}\` },
|
|
84
|
-
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }] }),
|
|
85
|
-
});
|
|
86
|
-
if (!response.ok) return Response.json({ error: 'OpenAI request failed' }, { status: 502 });
|
|
87
|
-
|
|
88
|
-
const completion = await response.json();
|
|
89
|
-
return Response.json({ answer: completion.choices[0].message.content });
|
|
90
|
-
};
|
|
91
|
-
`,
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: 'resend-email',
|
|
95
|
-
label: 'Send email',
|
|
96
|
-
description: 'Send a transactional email with Resend, or with sendMail.',
|
|
97
|
-
source: `// Edge function: %NAME%
|
|
98
|
-
// Secrets to set: RESEND_API_KEY. Prefer this project's own SMTP? Replace the
|
|
99
|
-
// fetch below with: await sendMail({ to, subject, html });
|
|
100
|
-
|
|
101
|
-
export default async (req: Request) => {
|
|
102
|
-
const { to, subject, html } = await req.json();
|
|
103
|
-
if (!to || !subject || !html) return Response.json({ error: 'to, subject and html are required' }, { status: 400 });
|
|
104
|
-
|
|
105
|
-
const response = await fetch('https://api.resend.com/emails', {
|
|
106
|
-
method: 'POST',
|
|
107
|
-
headers: { 'content-type': 'application/json', authorization: \`Bearer \${env.RESEND_API_KEY}\` },
|
|
108
|
-
body: JSON.stringify({ from: 'Acme <onboarding@resend.dev>', to, subject, html }),
|
|
109
|
-
});
|
|
110
|
-
return Response.json(await response.json(), { status: response.ok ? 200 : 502 });
|
|
111
|
-
};
|
|
112
|
-
`,
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
id: 'image-transform',
|
|
116
|
-
label: 'Image transform',
|
|
117
|
-
description: 'Resize an image fetched from a URL.',
|
|
118
|
-
source: `// Edge function: %NAME%
|
|
119
|
-
// GET ?url=<image>&width=<px>. Uses ImageScript, a pure-JS image library.
|
|
120
|
-
|
|
121
|
-
import { Image } from 'npm:imagescript@1';
|
|
122
|
-
|
|
123
|
-
export default async (req: Request) => {
|
|
124
|
-
const params = new URL(req.url).searchParams;
|
|
125
|
-
const source = params.get('url');
|
|
126
|
-
const width = Math.min(Number(params.get('width') ?? 320), 2000);
|
|
127
|
-
if (!source || !Number.isFinite(width) || width < 1) return new Response('url and a positive width are required', { status: 400 });
|
|
128
|
-
|
|
129
|
-
const upstream = await fetch(source);
|
|
130
|
-
if (!upstream.ok) return new Response('Could not fetch the image', { status: 502 });
|
|
131
|
-
|
|
132
|
-
const image = await Image.decode(new Uint8Array(await upstream.arrayBuffer()));
|
|
133
|
-
const resized = image.resize(width, Image.RESIZE_AUTO);
|
|
134
|
-
return new Response(await resized.encode(), { headers: { 'content-type': 'image/png' } });
|
|
135
|
-
};
|
|
136
|
-
`,
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
id: 'websocket',
|
|
140
|
-
label: 'WebSocket',
|
|
141
|
-
description: 'Upgrade the request and echo every message.',
|
|
142
|
-
source: `// Edge function: %NAME%
|
|
143
|
-
|
|
144
|
-
export default (req: Request) => {
|
|
145
|
-
if (req.headers.get('upgrade') !== 'websocket') return new Response('Expected a WebSocket upgrade', { status: 426 });
|
|
146
|
-
|
|
147
|
-
const { socket, response } = Deno.upgradeWebSocket(req);
|
|
148
|
-
socket.onopen = () => console.log('client connected');
|
|
149
|
-
socket.onmessage = event => socket.send(\`echo: \${event.data}\`);
|
|
150
|
-
socket.onclose = () => console.log('client disconnected');
|
|
151
|
-
return response;
|
|
152
|
-
};
|
|
153
|
-
`,
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
id: 'storage-upload',
|
|
157
|
-
label: 'Storage upload',
|
|
158
|
-
description: 'Store a request body as a file in Storage.',
|
|
159
|
-
source: `// Edge function: %NAME%
|
|
160
|
-
// Secrets to set: SERVICE_TOKEN (a token allowed to write files). API_URL is
|
|
161
|
-
// optional and defaults to this project's own API.
|
|
162
|
-
|
|
163
|
-
export default async (req: Request) => {
|
|
164
|
-
const { path, text } = await req.json();
|
|
165
|
-
if (!path || typeof text !== 'string') return Response.json({ error: 'path and text are required' }, { status: 400 });
|
|
166
|
-
|
|
167
|
-
const form = new FormData();
|
|
168
|
-
form.append('file', new Blob([text], { type: 'text/plain' }), path);
|
|
169
|
-
form.append('path', path);
|
|
170
|
-
|
|
171
|
-
const response = await fetch(\`\${env.API_URL ?? 'http://api:5001'}/api/files/\`, {
|
|
172
|
-
method: 'POST',
|
|
173
|
-
headers: { authorization: \`Bearer \${env.SERVICE_TOKEN}\` },
|
|
174
|
-
body: form,
|
|
175
|
-
});
|
|
176
|
-
return Response.json(await response.json(), { status: response.ok ? 200 : 502 });
|
|
177
|
-
};
|
|
178
|
-
`,
|
|
179
|
-
},
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
const DEFAULT_TEMPLATE = 'hello-world';
|
|
183
|
-
|
|
184
|
-
/** The template with this id, or undefined. */
|
|
185
|
-
function findTemplate(id) {
|
|
186
|
-
return templates.find(template => template.id === id);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** The source of a template for a function of this name. */
|
|
190
|
-
function renderTemplate(template, name) {
|
|
191
|
-
return template.source.replace(/%NAME%/g, name);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
module.exports = { templates, DEFAULT_TEMPLATE, findTemplate, renderTemplate };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Starting points for `motherbase functions new --template <id>`. Pure data.
|
|
4
|
+
// The Studio's "new function" picker offers the same list
|
|
5
|
+
// (apps/studio/src/features/api-tools/lib/function-templates.ts): keep the ids
|
|
6
|
+
// and sources in step.
|
|
7
|
+
//
|
|
8
|
+
// Every source is a module-shaped function (`export default handler`) that runs
|
|
9
|
+
// on the Deno edge runtime. `%NAME%` is replaced with the function's name.
|
|
10
|
+
|
|
11
|
+
const templates = [
|
|
12
|
+
{
|
|
13
|
+
id: 'hello-world',
|
|
14
|
+
label: 'Hello world',
|
|
15
|
+
description: 'Echo the request body and the time.',
|
|
16
|
+
source: `// Edge function: %NAME%
|
|
17
|
+
//
|
|
18
|
+
// Runs on Deno with the whole Web platform: Request, Response, fetch, streams,
|
|
19
|
+
// crypto.subtle (for webhook signature checks) and friends. npm:, jsr: and URL
|
|
20
|
+
// imports load as they do on Deno; a bare package name is read as npm:
|
|
21
|
+
//
|
|
22
|
+
// import { z } from 'zod';
|
|
23
|
+
//
|
|
24
|
+
// \`env\` holds this project's edge secrets, and \`sendMail()\` sends through the
|
|
25
|
+
// project's own SMTP settings (the same transport auth uses):
|
|
26
|
+
//
|
|
27
|
+
// await sendMail({ to: 'sales@example.com', subject: 'New lead', text: '...' });
|
|
28
|
+
|
|
29
|
+
export default async (req: Request) => {
|
|
30
|
+
const body = req.method === 'GET' ? {} : await req.json().catch(() => ({}));
|
|
31
|
+
return Response.json({ hello: '%NAME%', received: body, at: new Date().toISOString() });
|
|
32
|
+
};
|
|
33
|
+
`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'stripe-webhook',
|
|
37
|
+
label: 'Stripe webhook',
|
|
38
|
+
description: 'Verify a Stripe signature, then handle the event.',
|
|
39
|
+
source: `// Edge function: %NAME%
|
|
40
|
+
// Secrets to set: STRIPE_WEBHOOK_SECRET (the endpoint's "whsec_..." signing secret).
|
|
41
|
+
|
|
42
|
+
const encoder = new TextEncoder();
|
|
43
|
+
|
|
44
|
+
async function sign(secret: string, payload: string): Promise<string> {
|
|
45
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
46
|
+
const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
|
|
47
|
+
return [...new Uint8Array(mac)].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default async (req: Request) => {
|
|
51
|
+
// The raw body: a re-serialised one no longer matches the signature.
|
|
52
|
+
const payload = await req.text();
|
|
53
|
+
const header = req.headers.get('stripe-signature') ?? '';
|
|
54
|
+
const parts = Object.fromEntries(header.split(',').map(part => part.split('=') as [string, string]));
|
|
55
|
+
const expected = await sign(env.STRIPE_WEBHOOK_SECRET, \`\${parts.t}.\${payload}\`);
|
|
56
|
+
if (!parts.v1 || parts.v1 !== expected) return new Response('Invalid signature', { status: 400 });
|
|
57
|
+
|
|
58
|
+
const event = JSON.parse(payload);
|
|
59
|
+
switch (event.type) {
|
|
60
|
+
case 'checkout.session.completed':
|
|
61
|
+
console.log('checkout completed', event.data.object.id);
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
console.log('ignored event', event.type);
|
|
65
|
+
}
|
|
66
|
+
return Response.json({ received: true });
|
|
67
|
+
};
|
|
68
|
+
`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'openai',
|
|
72
|
+
label: 'OpenAI',
|
|
73
|
+
description: 'Send a prompt to OpenAI and return the answer.',
|
|
74
|
+
source: `// Edge function: %NAME%
|
|
75
|
+
// Secrets to set: OPENAI_API_KEY.
|
|
76
|
+
|
|
77
|
+
export default async (req: Request) => {
|
|
78
|
+
const { prompt } = await req.json();
|
|
79
|
+
if (typeof prompt !== 'string' || !prompt) return Response.json({ error: 'prompt is required' }, { status: 400 });
|
|
80
|
+
|
|
81
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'content-type': 'application/json', authorization: \`Bearer \${env.OPENAI_API_KEY}\` },
|
|
84
|
+
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }] }),
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) return Response.json({ error: 'OpenAI request failed' }, { status: 502 });
|
|
87
|
+
|
|
88
|
+
const completion = await response.json();
|
|
89
|
+
return Response.json({ answer: completion.choices[0].message.content });
|
|
90
|
+
};
|
|
91
|
+
`,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'resend-email',
|
|
95
|
+
label: 'Send email',
|
|
96
|
+
description: 'Send a transactional email with Resend, or with sendMail.',
|
|
97
|
+
source: `// Edge function: %NAME%
|
|
98
|
+
// Secrets to set: RESEND_API_KEY. Prefer this project's own SMTP? Replace the
|
|
99
|
+
// fetch below with: await sendMail({ to, subject, html });
|
|
100
|
+
|
|
101
|
+
export default async (req: Request) => {
|
|
102
|
+
const { to, subject, html } = await req.json();
|
|
103
|
+
if (!to || !subject || !html) return Response.json({ error: 'to, subject and html are required' }, { status: 400 });
|
|
104
|
+
|
|
105
|
+
const response = await fetch('https://api.resend.com/emails', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'content-type': 'application/json', authorization: \`Bearer \${env.RESEND_API_KEY}\` },
|
|
108
|
+
body: JSON.stringify({ from: 'Acme <onboarding@resend.dev>', to, subject, html }),
|
|
109
|
+
});
|
|
110
|
+
return Response.json(await response.json(), { status: response.ok ? 200 : 502 });
|
|
111
|
+
};
|
|
112
|
+
`,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'image-transform',
|
|
116
|
+
label: 'Image transform',
|
|
117
|
+
description: 'Resize an image fetched from a URL.',
|
|
118
|
+
source: `// Edge function: %NAME%
|
|
119
|
+
// GET ?url=<image>&width=<px>. Uses ImageScript, a pure-JS image library.
|
|
120
|
+
|
|
121
|
+
import { Image } from 'npm:imagescript@1';
|
|
122
|
+
|
|
123
|
+
export default async (req: Request) => {
|
|
124
|
+
const params = new URL(req.url).searchParams;
|
|
125
|
+
const source = params.get('url');
|
|
126
|
+
const width = Math.min(Number(params.get('width') ?? 320), 2000);
|
|
127
|
+
if (!source || !Number.isFinite(width) || width < 1) return new Response('url and a positive width are required', { status: 400 });
|
|
128
|
+
|
|
129
|
+
const upstream = await fetch(source);
|
|
130
|
+
if (!upstream.ok) return new Response('Could not fetch the image', { status: 502 });
|
|
131
|
+
|
|
132
|
+
const image = await Image.decode(new Uint8Array(await upstream.arrayBuffer()));
|
|
133
|
+
const resized = image.resize(width, Image.RESIZE_AUTO);
|
|
134
|
+
return new Response(await resized.encode(), { headers: { 'content-type': 'image/png' } });
|
|
135
|
+
};
|
|
136
|
+
`,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'websocket',
|
|
140
|
+
label: 'WebSocket',
|
|
141
|
+
description: 'Upgrade the request and echo every message.',
|
|
142
|
+
source: `// Edge function: %NAME%
|
|
143
|
+
|
|
144
|
+
export default (req: Request) => {
|
|
145
|
+
if (req.headers.get('upgrade') !== 'websocket') return new Response('Expected a WebSocket upgrade', { status: 426 });
|
|
146
|
+
|
|
147
|
+
const { socket, response } = Deno.upgradeWebSocket(req);
|
|
148
|
+
socket.onopen = () => console.log('client connected');
|
|
149
|
+
socket.onmessage = event => socket.send(\`echo: \${event.data}\`);
|
|
150
|
+
socket.onclose = () => console.log('client disconnected');
|
|
151
|
+
return response;
|
|
152
|
+
};
|
|
153
|
+
`,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'storage-upload',
|
|
157
|
+
label: 'Storage upload',
|
|
158
|
+
description: 'Store a request body as a file in Storage.',
|
|
159
|
+
source: `// Edge function: %NAME%
|
|
160
|
+
// Secrets to set: SERVICE_TOKEN (a token allowed to write files). API_URL is
|
|
161
|
+
// optional and defaults to this project's own API.
|
|
162
|
+
|
|
163
|
+
export default async (req: Request) => {
|
|
164
|
+
const { path, text } = await req.json();
|
|
165
|
+
if (!path || typeof text !== 'string') return Response.json({ error: 'path and text are required' }, { status: 400 });
|
|
166
|
+
|
|
167
|
+
const form = new FormData();
|
|
168
|
+
form.append('file', new Blob([text], { type: 'text/plain' }), path);
|
|
169
|
+
form.append('path', path);
|
|
170
|
+
|
|
171
|
+
const response = await fetch(\`\${env.API_URL ?? 'http://api:5001'}/api/files/\`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { authorization: \`Bearer \${env.SERVICE_TOKEN}\` },
|
|
174
|
+
body: form,
|
|
175
|
+
});
|
|
176
|
+
return Response.json(await response.json(), { status: response.ok ? 200 : 502 });
|
|
177
|
+
};
|
|
178
|
+
`,
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const DEFAULT_TEMPLATE = 'hello-world';
|
|
183
|
+
|
|
184
|
+
/** The template with this id, or undefined. */
|
|
185
|
+
function findTemplate(id) {
|
|
186
|
+
return templates.find(template => template.id === id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The source of a template for a function of this name. */
|
|
190
|
+
function renderTemplate(template, name) {
|
|
191
|
+
return template.source.replace(/%NAME%/g, name);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { templates, DEFAULT_TEMPLATE, findTemplate, renderTemplate };
|
package/src/format.js
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Machine output formats for `-o/--output`, matching the Supabase CLI's set
|
|
4
|
-
// (minus toml). `pretty` is each command's own human renderer; the rest are
|
|
5
|
-
// rendered here from the command's data, so every command gets them for free.
|
|
6
|
-
|
|
7
|
-
const { CliError } = require('./context');
|
|
8
|
-
|
|
9
|
-
const FORMATS = ['pretty', 'json', 'yaml', 'env'];
|
|
10
|
-
|
|
11
|
-
// Resolved once in main (`-o json` also sets `json`), but commands and tests
|
|
12
|
-
// that only pass `json: true` still land on the right format.
|
|
13
|
-
function outputFormat(flags = {}) {
|
|
14
|
-
return flags.format || (flags.json ? 'json' : 'pretty');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// ── YAML ─────────────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
const YAML_RESERVED = /^(true|false|yes|no|on|off|null|~)$/i;
|
|
20
|
-
|
|
21
|
-
// A string stays plain only when YAML would read it back as that same string;
|
|
22
|
-
// anything else is double-quoted, and JSON's quoting is valid YAML.
|
|
23
|
-
function yamlScalar(value) {
|
|
24
|
-
if (value === null || value === undefined) return 'null';
|
|
25
|
-
if (typeof value !== 'string') return String(value);
|
|
26
|
-
const plain =
|
|
27
|
-
/^[A-Za-z0-9_./@-][A-Za-z0-9_ ./@:-]*$/.test(value) &&
|
|
28
|
-
!/(: |:$| $)/.test(value) &&
|
|
29
|
-
!YAML_RESERVED.test(value) &&
|
|
30
|
-
Number.isNaN(Number(value)) &&
|
|
31
|
-
!/^\d{4}-\d\d-\d\d/.test(value);
|
|
32
|
-
return plain ? value : JSON.stringify(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isNonEmptyCollection(value) {
|
|
36
|
-
return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function yamlLines(value) {
|
|
40
|
-
if (Array.isArray(value)) {
|
|
41
|
-
if (!value.length) return ['[]'];
|
|
42
|
-
return value.flatMap(item => {
|
|
43
|
-
const lines = yamlLines(item);
|
|
44
|
-
return [`- ${lines[0]}`, ...lines.slice(1).map(l => ` ${l}`)];
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
if (value !== null && typeof value === 'object') {
|
|
48
|
-
const keys = Object.keys(value).filter(k => value[k] !== undefined);
|
|
49
|
-
if (!keys.length) return ['{}'];
|
|
50
|
-
return keys.flatMap(key => {
|
|
51
|
-
const child = value[key];
|
|
52
|
-
const lines = yamlLines(child);
|
|
53
|
-
return isNonEmptyCollection(child)
|
|
54
|
-
? [`${yamlScalar(key)}:`, ...lines.map(l => ` ${l}`)]
|
|
55
|
-
: [`${yamlScalar(key)}: ${lines[0]}`];
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
return [yamlScalar(value)];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function toYaml(value) {
|
|
62
|
-
return yamlLines(value).join('\n') + '\n';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── env ──────────────────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
// `apiUrl` → `API_URL`, nested keys joined with `_`, as `supabase status -o env`.
|
|
68
|
-
function envKey(key) {
|
|
69
|
-
return String(key)
|
|
70
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
71
|
-
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
72
|
-
.toUpperCase();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function flattenEnv(value, prefix, out) {
|
|
76
|
-
for (const [key, child] of Object.entries(value)) {
|
|
77
|
-
if (child === undefined) continue;
|
|
78
|
-
const name = prefix ? `${prefix}_${envKey(key)}` : envKey(key);
|
|
79
|
-
if (child !== null && typeof child === 'object') flattenEnv(child, name, out);
|
|
80
|
-
else out.push(`${name}=${JSON.stringify(child === null ? '' : String(child))}`);
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function toEnv(value) {
|
|
86
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
87
|
-
throw new CliError('The env output format needs a single object; this command returns a list. Use -o json or -o yaml.');
|
|
88
|
-
}
|
|
89
|
-
return flattenEnv(value, '', []).join('\n') + '\n';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
module.exports = { FORMATS, outputFormat, toYaml, toEnv };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Machine output formats for `-o/--output`, matching the Supabase CLI's set
|
|
4
|
+
// (minus toml). `pretty` is each command's own human renderer; the rest are
|
|
5
|
+
// rendered here from the command's data, so every command gets them for free.
|
|
6
|
+
|
|
7
|
+
const { CliError } = require('./context');
|
|
8
|
+
|
|
9
|
+
const FORMATS = ['pretty', 'json', 'yaml', 'env'];
|
|
10
|
+
|
|
11
|
+
// Resolved once in main (`-o json` also sets `json`), but commands and tests
|
|
12
|
+
// that only pass `json: true` still land on the right format.
|
|
13
|
+
function outputFormat(flags = {}) {
|
|
14
|
+
return flags.format || (flags.json ? 'json' : 'pretty');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── YAML ─────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const YAML_RESERVED = /^(true|false|yes|no|on|off|null|~)$/i;
|
|
20
|
+
|
|
21
|
+
// A string stays plain only when YAML would read it back as that same string;
|
|
22
|
+
// anything else is double-quoted, and JSON's quoting is valid YAML.
|
|
23
|
+
function yamlScalar(value) {
|
|
24
|
+
if (value === null || value === undefined) return 'null';
|
|
25
|
+
if (typeof value !== 'string') return String(value);
|
|
26
|
+
const plain =
|
|
27
|
+
/^[A-Za-z0-9_./@-][A-Za-z0-9_ ./@:-]*$/.test(value) &&
|
|
28
|
+
!/(: |:$| $)/.test(value) &&
|
|
29
|
+
!YAML_RESERVED.test(value) &&
|
|
30
|
+
Number.isNaN(Number(value)) &&
|
|
31
|
+
!/^\d{4}-\d\d-\d\d/.test(value);
|
|
32
|
+
return plain ? value : JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNonEmptyCollection(value) {
|
|
36
|
+
return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function yamlLines(value) {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
if (!value.length) return ['[]'];
|
|
42
|
+
return value.flatMap(item => {
|
|
43
|
+
const lines = yamlLines(item);
|
|
44
|
+
return [`- ${lines[0]}`, ...lines.slice(1).map(l => ` ${l}`)];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (value !== null && typeof value === 'object') {
|
|
48
|
+
const keys = Object.keys(value).filter(k => value[k] !== undefined);
|
|
49
|
+
if (!keys.length) return ['{}'];
|
|
50
|
+
return keys.flatMap(key => {
|
|
51
|
+
const child = value[key];
|
|
52
|
+
const lines = yamlLines(child);
|
|
53
|
+
return isNonEmptyCollection(child)
|
|
54
|
+
? [`${yamlScalar(key)}:`, ...lines.map(l => ` ${l}`)]
|
|
55
|
+
: [`${yamlScalar(key)}: ${lines[0]}`];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return [yamlScalar(value)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toYaml(value) {
|
|
62
|
+
return yamlLines(value).join('\n') + '\n';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── env ──────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
// `apiUrl` → `API_URL`, nested keys joined with `_`, as `supabase status -o env`.
|
|
68
|
+
function envKey(key) {
|
|
69
|
+
return String(key)
|
|
70
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
71
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
72
|
+
.toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function flattenEnv(value, prefix, out) {
|
|
76
|
+
for (const [key, child] of Object.entries(value)) {
|
|
77
|
+
if (child === undefined) continue;
|
|
78
|
+
const name = prefix ? `${prefix}_${envKey(key)}` : envKey(key);
|
|
79
|
+
if (child !== null && typeof child === 'object') flattenEnv(child, name, out);
|
|
80
|
+
else out.push(`${name}=${JSON.stringify(child === null ? '' : String(child))}`);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toEnv(value) {
|
|
86
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
87
|
+
throw new CliError('The env output format needs a single object; this command returns a list. Use -o json or -o yaml.');
|
|
88
|
+
}
|
|
89
|
+
return flattenEnv(value, '', []).join('\n') + '\n';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { FORMATS, outputFormat, toYaml, toEnv };
|
package/src/inspect/csv.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// CSV for `inspect report`, written the way Postgres' `COPY ... TO STDOUT WITH
|
|
4
|
-
// CSV HEADER` writes it (which is how the Supabase CLI produces its report), so
|
|
5
|
-
// the files read the same whichever tool made them: NULL is an empty field, an
|
|
6
|
-
// empty string is `""`, booleans are `t`/`f`.
|
|
7
|
-
|
|
8
|
-
function field(value) {
|
|
9
|
-
if (value === null || value === undefined) return '';
|
|
10
|
-
if (typeof value === 'boolean') return value ? 't' : 'f';
|
|
11
|
-
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
12
|
-
return text === '' || /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** One CSV document: a header of column keys, then one line per row. */
|
|
16
|
-
function toCsv(columns, rows) {
|
|
17
|
-
const lines = [columns.map(c => field(c.key)).join(',')];
|
|
18
|
-
for (const row of rows) lines.push(columns.map(c => field(row[c.key])).join(','));
|
|
19
|
-
return lines.join('\n') + '\n';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
module.exports = { toCsv };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// CSV for `inspect report`, written the way Postgres' `COPY ... TO STDOUT WITH
|
|
4
|
+
// CSV HEADER` writes it (which is how the Supabase CLI produces its report), so
|
|
5
|
+
// the files read the same whichever tool made them: NULL is an empty field, an
|
|
6
|
+
// empty string is `""`, booleans are `t`/`f`.
|
|
7
|
+
|
|
8
|
+
function field(value) {
|
|
9
|
+
if (value === null || value === undefined) return '';
|
|
10
|
+
if (typeof value === 'boolean') return value ? 't' : 'f';
|
|
11
|
+
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
12
|
+
return text === '' || /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** One CSV document: a header of column keys, then one line per row. */
|
|
16
|
+
function toCsv(columns, rows) {
|
|
17
|
+
const lines = [columns.map(c => field(c.key)).join(',')];
|
|
18
|
+
for (const row of rows) lines.push(columns.map(c => field(row[c.key])).join(','));
|
|
19
|
+
return lines.join('\n') + '\n';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { toCsv };
|