@motherbase/cli 0.2.4 → 0.2.6
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 -0
- package/README.md +131 -18
- package/bin/motherbase.js +10 -10
- package/package.json +35 -26
- package/src/api.js +233 -229
- package/src/commands/_util.js +106 -50
- package/src/commands/backups.js +16 -0
- package/src/commands/branches.js +113 -0
- package/src/commands/completion.js +90 -0
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db-catalog.js +326 -0
- package/src/commands/db.js +103 -92
- package/src/commands/domains.js +124 -0
- package/src/commands/functions.js +63 -33
- package/src/commands/gen.js +63 -17
- package/src/commands/import.js +55 -35
- package/src/commands/index.js +85 -64
- package/src/commands/inspect.js +178 -41
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +23 -2
- package/src/commands/login.js +39 -39
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +27 -9
- package/src/commands/migrate.js +207 -0
- package/src/commands/migration.js +108 -33
- package/src/commands/network.js +83 -0
- package/src/commands/orgs.js +40 -0
- package/src/commands/postgres-config.js +113 -0
- package/src/commands/projects.js +130 -125
- package/src/commands/queues.js +133 -0
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -45
- package/src/commands/signing.js +130 -0
- package/src/commands/snippets.js +44 -0
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -113
- package/src/commands/test.js +9 -0
- package/src/commands/unlink.js +15 -15
- package/src/commands/users.js +142 -113
- package/src/commands/vault.js +82 -0
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -0
- package/src/config.js +98 -98
- package/src/context.js +106 -102
- package/src/docs.js +68 -0
- package/src/edge-templates.js +194 -0
- package/src/format.js +92 -0
- package/src/inspect/csv.js +22 -0
- package/src/inspect/queries.js +562 -0
- package/src/main.js +224 -160
- package/src/mcp/platform-tools.js +233 -0
- package/src/mcp/protocol.js +4 -1
- package/src/mcp/storage-tools.js +93 -0
- package/src/mcp/tools.js +318 -6
- package/src/migration/commands.js +375 -0
- package/src/migration/files.js +245 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -0
- package/src/ui.js +111 -111
- package/src/commands/migrations.js +0 -85
package/src/config.js
CHANGED
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Config persistence. Two files:
|
|
4
|
-
// Global creds : ~/.motherbase/config.json (contains tokens; written 0o600)
|
|
5
|
-
// { "consoles": { "<url>": { "token": "...", "email": "..." } }, "current": "<url>" }
|
|
6
|
-
// Project link : ./.motherbase/config.json (versionable, non-secret)
|
|
7
|
-
// { "consoleUrl": "<url>", "projectId": "abc", "projectName": "my-app" }
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const os = require('os');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
|
|
13
|
-
const GLOBAL_DIR = path.join(os.homedir(), '.motherbase');
|
|
14
|
-
const GLOBAL_FILE = path.join(GLOBAL_DIR, 'config.json');
|
|
15
|
-
const LINK_DIRNAME = '.motherbase';
|
|
16
|
-
const LINK_BASENAME = 'config.json';
|
|
17
|
-
|
|
18
|
-
function normalizeUrl(url) {
|
|
19
|
-
return String(url || '').replace(/\/+$/, '');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function readJson(file) {
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
25
|
-
} catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ── Global credentials ───────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
function readGlobal() {
|
|
33
|
-
return readJson(GLOBAL_FILE) || { consoles: {}, current: null };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function writeGlobal(cfg) {
|
|
37
|
-
fs.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
38
|
-
fs.writeFileSync(GLOBAL_FILE, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
|
|
39
|
-
// Best-effort tighten perms even if the file already existed with looser mode.
|
|
40
|
-
try { fs.chmodSync(GLOBAL_FILE, 0o600); } catch { /* windows / noop */ }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function saveConsoleToken(url, token, email) {
|
|
44
|
-
const u = normalizeUrl(url);
|
|
45
|
-
const cfg = readGlobal();
|
|
46
|
-
cfg.consoles = cfg.consoles || {};
|
|
47
|
-
cfg.consoles[u] = { ...(cfg.consoles[u] || {}), token, email };
|
|
48
|
-
cfg.current = u;
|
|
49
|
-
writeGlobal(cfg);
|
|
50
|
-
return u;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function clearConsoleToken(url) {
|
|
54
|
-
const cfg = readGlobal();
|
|
55
|
-
const u = normalizeUrl(url);
|
|
56
|
-
if (cfg.consoles && cfg.consoles[u]) delete cfg.consoles[u];
|
|
57
|
-
if (cfg.current === u) cfg.current = null;
|
|
58
|
-
writeGlobal(cfg);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function consoleEntry(url) {
|
|
62
|
-
const cfg = readGlobal();
|
|
63
|
-
return (cfg.consoles && cfg.consoles[normalizeUrl(url)]) || null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Per-project link ─────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
function linkPath(cwd) {
|
|
69
|
-
return path.join(cwd || process.cwd(), LINK_DIRNAME, LINK_BASENAME);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function readLink(cwd) {
|
|
73
|
-
return readJson(linkPath(cwd));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function writeLink(cwd, data) {
|
|
77
|
-
const dir = path.join(cwd || process.cwd(), LINK_DIRNAME);
|
|
78
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
79
|
-
fs.writeFileSync(linkPath(cwd), JSON.stringify(data, null, 2) + '\n');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function removeLink(cwd) {
|
|
83
|
-
try { fs.unlinkSync(linkPath(cwd)); return true; } catch { return false; }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
module.exports = {
|
|
87
|
-
GLOBAL_FILE,
|
|
88
|
-
normalizeUrl,
|
|
89
|
-
readGlobal,
|
|
90
|
-
writeGlobal,
|
|
91
|
-
saveConsoleToken,
|
|
92
|
-
clearConsoleToken,
|
|
93
|
-
consoleEntry,
|
|
94
|
-
readLink,
|
|
95
|
-
writeLink,
|
|
96
|
-
removeLink,
|
|
97
|
-
linkPath,
|
|
98
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Config persistence. Two files:
|
|
4
|
+
// Global creds : ~/.motherbase/config.json (contains tokens; written 0o600)
|
|
5
|
+
// { "consoles": { "<url>": { "token": "...", "email": "..." } }, "current": "<url>" }
|
|
6
|
+
// Project link : ./.motherbase/config.json (versionable, non-secret)
|
|
7
|
+
// { "consoleUrl": "<url>", "projectId": "abc", "projectName": "my-app" }
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const GLOBAL_DIR = path.join(os.homedir(), '.motherbase');
|
|
14
|
+
const GLOBAL_FILE = path.join(GLOBAL_DIR, 'config.json');
|
|
15
|
+
const LINK_DIRNAME = '.motherbase';
|
|
16
|
+
const LINK_BASENAME = 'config.json';
|
|
17
|
+
|
|
18
|
+
function normalizeUrl(url) {
|
|
19
|
+
return String(url || '').replace(/\/+$/, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readJson(file) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Global credentials ───────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function readGlobal() {
|
|
33
|
+
return readJson(GLOBAL_FILE) || { consoles: {}, current: null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeGlobal(cfg) {
|
|
37
|
+
fs.mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
38
|
+
fs.writeFileSync(GLOBAL_FILE, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
|
|
39
|
+
// Best-effort tighten perms even if the file already existed with looser mode.
|
|
40
|
+
try { fs.chmodSync(GLOBAL_FILE, 0o600); } catch { /* windows / noop */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function saveConsoleToken(url, token, email) {
|
|
44
|
+
const u = normalizeUrl(url);
|
|
45
|
+
const cfg = readGlobal();
|
|
46
|
+
cfg.consoles = cfg.consoles || {};
|
|
47
|
+
cfg.consoles[u] = { ...(cfg.consoles[u] || {}), token, email };
|
|
48
|
+
cfg.current = u;
|
|
49
|
+
writeGlobal(cfg);
|
|
50
|
+
return u;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearConsoleToken(url) {
|
|
54
|
+
const cfg = readGlobal();
|
|
55
|
+
const u = normalizeUrl(url);
|
|
56
|
+
if (cfg.consoles && cfg.consoles[u]) delete cfg.consoles[u];
|
|
57
|
+
if (cfg.current === u) cfg.current = null;
|
|
58
|
+
writeGlobal(cfg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function consoleEntry(url) {
|
|
62
|
+
const cfg = readGlobal();
|
|
63
|
+
return (cfg.consoles && cfg.consoles[normalizeUrl(url)]) || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Per-project link ─────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function linkPath(cwd) {
|
|
69
|
+
return path.join(cwd || process.cwd(), LINK_DIRNAME, LINK_BASENAME);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readLink(cwd) {
|
|
73
|
+
return readJson(linkPath(cwd));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeLink(cwd, data) {
|
|
77
|
+
const dir = path.join(cwd || process.cwd(), LINK_DIRNAME);
|
|
78
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
79
|
+
fs.writeFileSync(linkPath(cwd), JSON.stringify(data, null, 2) + '\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeLink(cwd) {
|
|
83
|
+
try { fs.unlinkSync(linkPath(cwd)); return true; } catch { return false; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
GLOBAL_FILE,
|
|
88
|
+
normalizeUrl,
|
|
89
|
+
readGlobal,
|
|
90
|
+
writeGlobal,
|
|
91
|
+
saveConsoleToken,
|
|
92
|
+
clearConsoleToken,
|
|
93
|
+
consoleEntry,
|
|
94
|
+
readLink,
|
|
95
|
+
writeLink,
|
|
96
|
+
removeLink,
|
|
97
|
+
linkPath,
|
|
98
|
+
};
|
package/src/context.js
CHANGED
|
@@ -1,102 +1,106 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Resolve the runtime context: which console, which token, which project, and
|
|
4
|
-
// whether we talk through the Console proxy ('console' mode) or straight at a
|
|
5
|
-
// single project API ('direct' mode). Precedence everywhere: flags > env > config.
|
|
6
|
-
//
|
|
7
|
-
// Resolution is LAZY: building a context never throws, so commands that need no
|
|
8
|
-
// project (login, local docker, init) work with zero configuration. The
|
|
9
|
-
// require*() helpers throw a clear, actionable error only when a value is
|
|
10
|
-
// actually needed and missing.
|
|
11
|
-
|
|
12
|
-
const config = require('./config');
|
|
13
|
-
|
|
14
|
-
class CliError extends Error {
|
|
15
|
-
constructor(message) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = 'CliError';
|
|
18
|
-
this.isCliError = true;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function firstDefined(...vals) {
|
|
23
|
-
for (const v of vals) if (v !== undefined && v !== null && v !== '') return v;
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function createContext(flags = {}, env = process.env, cwd = process.cwd()) {
|
|
28
|
-
const link = config.readLink(cwd) || {};
|
|
29
|
-
const global = config.readGlobal();
|
|
30
|
-
|
|
31
|
-
const consoleUrl = firstDefined(
|
|
32
|
-
flags.url,
|
|
33
|
-
env.MOTHERBASE_CONSOLE_URL,
|
|
34
|
-
link.consoleUrl,
|
|
35
|
-
global.current,
|
|
36
|
-
);
|
|
37
|
-
const normConsoleUrl = consoleUrl ? config.normalizeUrl(consoleUrl) : undefined;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Resolve the runtime context: which console, which token, which project, and
|
|
4
|
+
// whether we talk through the Console proxy ('console' mode) or straight at a
|
|
5
|
+
// single project API ('direct' mode). Precedence everywhere: flags > env > config.
|
|
6
|
+
//
|
|
7
|
+
// Resolution is LAZY: building a context never throws, so commands that need no
|
|
8
|
+
// project (login, local docker, init) work with zero configuration. The
|
|
9
|
+
// require*() helpers throw a clear, actionable error only when a value is
|
|
10
|
+
// actually needed and missing.
|
|
11
|
+
|
|
12
|
+
const config = require('./config');
|
|
13
|
+
|
|
14
|
+
class CliError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'CliError';
|
|
18
|
+
this.isCliError = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function firstDefined(...vals) {
|
|
23
|
+
for (const v of vals) if (v !== undefined && v !== null && v !== '') return v;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createContext(flags = {}, env = process.env, cwd = process.cwd()) {
|
|
28
|
+
const link = config.readLink(cwd) || {};
|
|
29
|
+
const global = config.readGlobal();
|
|
30
|
+
|
|
31
|
+
const consoleUrl = firstDefined(
|
|
32
|
+
flags.url,
|
|
33
|
+
env.MOTHERBASE_CONSOLE_URL,
|
|
34
|
+
link.consoleUrl,
|
|
35
|
+
global.current,
|
|
36
|
+
);
|
|
37
|
+
const normConsoleUrl = consoleUrl ? config.normalizeUrl(consoleUrl) : undefined;
|
|
38
|
+
|
|
39
|
+
// The link file is committed to the repo, so a URL that came only from it is
|
|
40
|
+
// not trusted with a token the user supplied for some other console.
|
|
41
|
+
const urlFromLinkOnly = !firstDefined(flags.url, env.MOTHERBASE_CONSOLE_URL) && Boolean(link.consoleUrl);
|
|
42
|
+
|
|
43
|
+
const token = firstDefined(
|
|
44
|
+
urlFromLinkOnly ? undefined : flags.token,
|
|
45
|
+
urlFromLinkOnly ? undefined : env.MOTHERBASE_TOKEN,
|
|
46
|
+
normConsoleUrl ? (config.consoleEntry(normConsoleUrl) || {}).token : undefined,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const projectId = firstDefined(flags.project, env.MOTHERBASE_PROJECT, link.projectId);
|
|
50
|
+
const projectName = firstDefined(link.projectName, projectId);
|
|
51
|
+
|
|
52
|
+
// Direct project mode: an admin JWT pointed straight at one project API. Only
|
|
53
|
+
// used as a fallback when there is no console link/flags to route through.
|
|
54
|
+
const directUrl = env.MOTHERBASE_URL ? config.normalizeUrl(env.MOTHERBASE_URL) : undefined;
|
|
55
|
+
const directToken = env.MOTHERBASE_ADMIN_TOKEN;
|
|
56
|
+
const hasConsole = Boolean(normConsoleUrl && token);
|
|
57
|
+
const hasProjectLink = Boolean(projectId);
|
|
58
|
+
const preferDirect = Boolean(directUrl && directToken) && !(hasConsole && hasProjectLink);
|
|
59
|
+
const mode = preferDirect ? 'direct' : 'console';
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
flags,
|
|
63
|
+
env,
|
|
64
|
+
cwd,
|
|
65
|
+
mode,
|
|
66
|
+
consoleUrl: normConsoleUrl,
|
|
67
|
+
token,
|
|
68
|
+
projectId,
|
|
69
|
+
projectName,
|
|
70
|
+
directUrl,
|
|
71
|
+
directToken,
|
|
72
|
+
linkedConsoleUrl: link.consoleUrl,
|
|
73
|
+
accountEmail: normConsoleUrl ? (config.consoleEntry(normConsoleUrl) || {}).email : undefined,
|
|
74
|
+
|
|
75
|
+
// Console (account / multi-project) requests need URL + session token.
|
|
76
|
+
requireConsole() {
|
|
77
|
+
if (!this.consoleUrl) {
|
|
78
|
+
throw new CliError('No console URL. Run `motherbase login --url <consoleUrl>` or set MOTHERBASE_CONSOLE_URL.');
|
|
79
|
+
}
|
|
80
|
+
if (!this.token) {
|
|
81
|
+
throw new CliError('Not logged in. Run `motherbase login` or set MOTHERBASE_TOKEN.');
|
|
82
|
+
}
|
|
83
|
+
return { consoleUrl: this.consoleUrl, token: this.token };
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Admin/resource requests: resolve either the console proxy path or a direct
|
|
87
|
+
// project API, whichever the context selected.
|
|
88
|
+
requireProject() {
|
|
89
|
+
if (this.mode === 'direct') {
|
|
90
|
+
return { mode: 'direct', baseUrl: this.directUrl, token: this.directToken };
|
|
91
|
+
}
|
|
92
|
+
this.requireConsole();
|
|
93
|
+
if (!this.projectId) {
|
|
94
|
+
throw new CliError('No project linked. Run `motherbase link` or set MOTHERBASE_PROJECT (or use direct mode via MOTHERBASE_URL + MOTHERBASE_ADMIN_TOKEN).');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
mode: 'console',
|
|
98
|
+
consoleUrl: this.consoleUrl,
|
|
99
|
+
token: this.token,
|
|
100
|
+
projectId: this.projectId,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { createContext, CliError };
|
package/src/docs.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Renders the command registry as the Markdown table published in the docs.
|
|
4
|
+
//
|
|
5
|
+
// `main.js` already builds every help screen from `registry` + `groups`, so the
|
|
6
|
+
// terminal could never disagree with the code. The published CLI reference
|
|
7
|
+
// could, and did: thirteen whole groups (`orgs`, `branches`, `domains`,
|
|
8
|
+
// `vanity-subdomains`, `backups`, `test`, `postgres-config`, `queues`, `vault`,
|
|
9
|
+
// `wrappers`, `network-restrictions`, `ssl-enforcement`, `snippets`) existed in
|
|
10
|
+
// `motherbase --help` and not in the reference, so the documented CLI was a
|
|
11
|
+
// strict subset of the shipped one.
|
|
12
|
+
//
|
|
13
|
+
// Writing it from the same two objects the help screens use is what stops that
|
|
14
|
+
// happening again. `scripts/generate-cli-docs.js` writes the file;
|
|
15
|
+
// `test/docs.test.js` fails when the committed copy no longer matches.
|
|
16
|
+
|
|
17
|
+
const { registry, groups } = require('./commands');
|
|
18
|
+
|
|
19
|
+
/** Escape the pipe so a usage string containing one cannot break the table. */
|
|
20
|
+
const cell = value => String(value ?? '').replace(/\|/g, '\\|');
|
|
21
|
+
|
|
22
|
+
/** `motherbase db sql -e <sql>` → `` `motherbase db sql` `` plus its arguments. */
|
|
23
|
+
function usageCell(usage) {
|
|
24
|
+
return usage ? `\`${cell(usage)}\`` : '—';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Commands in group order, then whatever the registry holds that no group
|
|
29
|
+
* names. A command with no group still renders: adding one to the registry and
|
|
30
|
+
* forgetting the group list must not make it vanish from the docs.
|
|
31
|
+
*/
|
|
32
|
+
function sections() {
|
|
33
|
+
const grouped = new Set(groups.flatMap(group => group.names));
|
|
34
|
+
const ungrouped = Object.keys(registry).filter(name => !grouped.has(name));
|
|
35
|
+
const all = groups.map(group => ({ title: group.title, names: group.names }));
|
|
36
|
+
if (ungrouped.length) all.push({ title: 'Ungrouped', names: ungrouped });
|
|
37
|
+
return all;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderCommandDocs() {
|
|
41
|
+
const lines = [
|
|
42
|
+
'<!-- GENERATED by `npm run docs:cli` from packages/cli/src/commands. Do not edit by hand. -->',
|
|
43
|
+
'',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
for (const section of sections()) {
|
|
47
|
+
const entries = section.names.map(name => [name, registry[name]]).filter(([, entry]) => entry);
|
|
48
|
+
if (!entries.length) continue;
|
|
49
|
+
|
|
50
|
+
lines.push(`### ${section.title}`, '');
|
|
51
|
+
lines.push('| Command | Usage | What it does |');
|
|
52
|
+
lines.push('|---|---|---|');
|
|
53
|
+
|
|
54
|
+
for (const [name, entry] of entries) {
|
|
55
|
+
lines.push(`| \`${cell(name)}\` | ${usageCell(entry.usage)} | ${cell(entry.summary)} |`);
|
|
56
|
+
for (const [sub, subEntry] of Object.entries(entry.subcommands ?? {})) {
|
|
57
|
+
lines.push(
|
|
58
|
+
`| \`${cell(name)} ${cell(sub)}\` | ${usageCell(subEntry.usage)} | ${cell(subEntry.summary)} |`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
lines.push('');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { renderCommandDocs };
|
|
@@ -0,0 +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 };
|