@motherbase/cli 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/bin/motherbase.js +11 -0
- package/package.json +24 -0
- package/src/api.js +200 -0
- package/src/commands/_util.js +50 -0
- package/src/commands/config.js +47 -0
- package/src/commands/cron.js +65 -0
- package/src/commands/db.js +92 -0
- package/src/commands/functions.js +112 -0
- package/src/commands/import.js +35 -0
- package/src/commands/index.js +55 -0
- package/src/commands/inspect.js +41 -0
- package/src/commands/link.js +35 -0
- package/src/commands/local.js +64 -0
- package/src/commands/login.js +39 -0
- package/src/commands/logout.js +16 -0
- package/src/commands/migration.js +33 -0
- package/src/commands/migrations.js +85 -0
- package/src/commands/projects.js +125 -0
- package/src/commands/realtime.js +35 -0
- package/src/commands/rls.js +85 -0
- package/src/commands/secrets.js +45 -0
- package/src/commands/status.js +42 -0
- package/src/commands/storage.js +113 -0
- package/src/commands/unlink.js +15 -0
- package/src/commands/users.js +113 -0
- package/src/commands/webhooks.js +62 -0
- package/src/commands/whoami.js +20 -0
- package/src/config.js +98 -0
- package/src/context.js +102 -0
- package/src/main.js +155 -0
- package/src/prompt.js +44 -0
- package/src/ui.js +111 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output } = require('./_util');
|
|
5
|
+
|
|
6
|
+
const subcommands = {
|
|
7
|
+
config: {
|
|
8
|
+
summary: 'Show realtime config, or set enabled models with --models a,b,c',
|
|
9
|
+
usage: 'motherbase realtime config [--models a,b,c]',
|
|
10
|
+
async run(ctx, args, flags) {
|
|
11
|
+
if (flags.models !== undefined) {
|
|
12
|
+
const models = String(flags.models).split(',').map(s => s.trim()).filter(Boolean);
|
|
13
|
+
const data = await api.adminRequest(ctx, 'PATCH', '/realtime/config', { body: { models } });
|
|
14
|
+
output(ctx, data, (d, ui) => ui.success(`Enabled models: ${(d && d.models ? d.models : models).join(', ') || '(none)'}`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const data = await api.adminRequest(ctx, 'GET', '/realtime/config');
|
|
18
|
+
output(ctx, data, (d, ui) => ui.keyValue(d || {}));
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
channels: {
|
|
22
|
+
summary: 'List active realtime channels',
|
|
23
|
+
usage: 'motherbase realtime channels',
|
|
24
|
+
async run(ctx) {
|
|
25
|
+
const data = await api.adminRequest(ctx, 'GET', '/realtime/channels');
|
|
26
|
+
output(ctx, data, (d, ui) => ui.json((d && d.channels) || d));
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
summary: 'Realtime configuration and channels',
|
|
33
|
+
usage: 'motherbase realtime <config|channels> ...',
|
|
34
|
+
subcommands,
|
|
35
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output, requireArg } = require('./_util');
|
|
5
|
+
const { CliError } = require('../context');
|
|
6
|
+
|
|
7
|
+
const policy = {
|
|
8
|
+
summary: 'Manage RLS policies',
|
|
9
|
+
usage: 'motherbase rls policy create --table <t> --name <n> --command <SELECT|INSERT|UPDATE|DELETE|ALL> [--using <expr>] [--check <expr>] [--roles a,b] | motherbase rls policy delete <table> <name>',
|
|
10
|
+
subcommands: {
|
|
11
|
+
create: {
|
|
12
|
+
summary: 'Create an RLS policy',
|
|
13
|
+
usage: 'motherbase rls policy create --table <t> --name <n> --command <cmd> [--using <expr>] [--check <expr>] [--roles a,b]',
|
|
14
|
+
async run(ctx, args, flags) {
|
|
15
|
+
const body = {
|
|
16
|
+
table: flags.table,
|
|
17
|
+
name: flags.name,
|
|
18
|
+
command: (flags.command || 'ALL').toUpperCase(),
|
|
19
|
+
};
|
|
20
|
+
if (!body.table || !body.name) throw new CliError('--table and --name are required');
|
|
21
|
+
if (flags.using) body.using = flags.using;
|
|
22
|
+
if (flags.check) body.check = flags.check;
|
|
23
|
+
if (flags.roles) body.roles = String(flags.roles).split(',').map(s => s.trim()).filter(Boolean);
|
|
24
|
+
const data = await api.adminRequest(ctx, 'POST', '/rls/policies', { body });
|
|
25
|
+
output(ctx, data, (d, ui) => ui.success(`Created policy ${body.name} on ${body.table}`));
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
delete: {
|
|
29
|
+
summary: 'Delete an RLS policy',
|
|
30
|
+
usage: 'motherbase rls policy delete <table> <name>',
|
|
31
|
+
async run(ctx, args) {
|
|
32
|
+
const table = requireArg(args[0], 'table');
|
|
33
|
+
const name = requireArg(args[1], 'name');
|
|
34
|
+
const data = await api.adminRequest(ctx, 'DELETE', '/rls/policies', { body: { table, name } });
|
|
35
|
+
output(ctx, data, (d, ui) => ui.success(`Dropped policy ${name} on ${table}`));
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const subcommands = {
|
|
42
|
+
list: {
|
|
43
|
+
summary: 'List tables with RLS status and policies',
|
|
44
|
+
usage: 'motherbase rls list',
|
|
45
|
+
async run(ctx) {
|
|
46
|
+
const data = await api.adminRequest(ctx, 'GET', '/rls/tables');
|
|
47
|
+
output(ctx, data, (rows, ui) =>
|
|
48
|
+
ui.table((Array.isArray(rows) ? rows : []).map(r => ({
|
|
49
|
+
table: r.table,
|
|
50
|
+
rls: r.rlsEnabled ? 'on' : 'off',
|
|
51
|
+
policies: Array.isArray(r.policies) ? r.policies.length : 0,
|
|
52
|
+
})), [
|
|
53
|
+
{ key: 'table', label: 'TABLE' },
|
|
54
|
+
{ key: 'rls', label: 'RLS' },
|
|
55
|
+
{ key: 'policies', label: 'POLICIES' },
|
|
56
|
+
]),
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
enable: {
|
|
61
|
+
summary: 'Enable RLS on a table',
|
|
62
|
+
usage: 'motherbase rls enable <table>',
|
|
63
|
+
async run(ctx, args) {
|
|
64
|
+
const table = requireArg(args[0], 'table');
|
|
65
|
+
const data = await api.adminRequest(ctx, 'POST', `/rls/tables/${table}/rls`, { body: { enabled: true } });
|
|
66
|
+
output(ctx, data, (d, ui) => ui.success(`RLS enabled on ${table}`));
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
disable: {
|
|
70
|
+
summary: 'Disable RLS on a table',
|
|
71
|
+
usage: 'motherbase rls disable <table>',
|
|
72
|
+
async run(ctx, args) {
|
|
73
|
+
const table = requireArg(args[0], 'table');
|
|
74
|
+
const data = await api.adminRequest(ctx, 'POST', `/rls/tables/${table}/rls`, { body: { enabled: false } });
|
|
75
|
+
output(ctx, data, (d, ui) => ui.success(`RLS disabled on ${table}`));
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
policy,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
summary: 'Row-level security (tables + policies)',
|
|
83
|
+
usage: 'motherbase rls <list|enable|disable|policy> ...',
|
|
84
|
+
subcommands,
|
|
85
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output, requireArg } = require('./_util');
|
|
5
|
+
|
|
6
|
+
const subcommands = {
|
|
7
|
+
list: {
|
|
8
|
+
summary: 'List edge secrets (names only)',
|
|
9
|
+
usage: 'motherbase secrets list',
|
|
10
|
+
async run(ctx) {
|
|
11
|
+
const data = await api.adminRequest(ctx, 'GET', '/edge-secrets');
|
|
12
|
+
const secrets = (data && data.secrets) || data || [];
|
|
13
|
+
output(ctx, data, (_d, ui) =>
|
|
14
|
+
ui.table((Array.isArray(secrets) ? secrets : []).map(s => (typeof s === 'string' ? { name: s } : s)), [
|
|
15
|
+
{ key: 'name', label: 'NAME' },
|
|
16
|
+
]),
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
set: {
|
|
21
|
+
summary: 'Set a secret',
|
|
22
|
+
usage: 'motherbase secrets set <NAME> <value>',
|
|
23
|
+
async run(ctx, args) {
|
|
24
|
+
const name = requireArg(args[0], 'NAME');
|
|
25
|
+
const value = requireArg(args[1], 'value');
|
|
26
|
+
const data = await api.adminRequest(ctx, 'PUT', '/edge-secrets', { body: { name, value } });
|
|
27
|
+
output(ctx, data, (d, ui) => ui.success(`Set secret ${name}`));
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
unset: {
|
|
31
|
+
summary: 'Remove a secret',
|
|
32
|
+
usage: 'motherbase secrets unset <NAME>',
|
|
33
|
+
async run(ctx, args) {
|
|
34
|
+
const name = requireArg(args[0], 'NAME');
|
|
35
|
+
const data = await api.adminRequest(ctx, 'DELETE', `/edge-secrets/${encodeURIComponent(name)}`);
|
|
36
|
+
output(ctx, data, (d, ui) => ui.success(`Removed secret ${name}`));
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
summary: 'Edge function secrets',
|
|
43
|
+
usage: 'motherbase secrets <list|set|unset> ...',
|
|
44
|
+
subcommands,
|
|
45
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output } = require('./_util');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
summary: 'Show connection status: console, link, and a project health probe',
|
|
8
|
+
usage: 'motherbase status',
|
|
9
|
+
async run(ctx) {
|
|
10
|
+
const data = {
|
|
11
|
+
console: ctx.consoleUrl || null,
|
|
12
|
+
loggedIn: Boolean(ctx.token),
|
|
13
|
+
mode: ctx.mode,
|
|
14
|
+
project: ctx.projectId || null,
|
|
15
|
+
reachable: null,
|
|
16
|
+
api: null,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (ctx.projectId || ctx.mode === 'direct') {
|
|
20
|
+
try {
|
|
21
|
+
const info = await api.adminRequest(ctx, 'GET', '/connect-info');
|
|
22
|
+
data.reachable = true;
|
|
23
|
+
data.api = info && info.apiUrl;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
data.reachable = false;
|
|
26
|
+
data.error = err.message;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
output(ctx, data, (d, ui) => {
|
|
31
|
+
ui.keyValue({
|
|
32
|
+
Console: d.console || '(none)',
|
|
33
|
+
'Logged in': d.loggedIn ? 'yes' : 'no',
|
|
34
|
+
Mode: d.mode,
|
|
35
|
+
Project: d.project || '(not linked)',
|
|
36
|
+
Reachable: d.reachable === null ? 'n/a' : d.reachable ? 'yes' : 'no',
|
|
37
|
+
'API URL': d.api || '',
|
|
38
|
+
});
|
|
39
|
+
if (d.error) ui.warn(d.error);
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const api = require('../api');
|
|
6
|
+
const { output, requireArg, splitRef } = require('./_util');
|
|
7
|
+
|
|
8
|
+
const buckets = {
|
|
9
|
+
summary: 'Manage storage buckets',
|
|
10
|
+
usage: 'motherbase storage buckets <list|create|delete> ...',
|
|
11
|
+
subcommands: {
|
|
12
|
+
list: {
|
|
13
|
+
summary: 'List buckets',
|
|
14
|
+
usage: 'motherbase storage buckets list',
|
|
15
|
+
async run(ctx) {
|
|
16
|
+
const data = await api.adminRequest(ctx, 'GET', '/storage/buckets');
|
|
17
|
+
const rows = Array.isArray(data) ? data : (data && data.buckets) || [];
|
|
18
|
+
output(ctx, data, (_d, ui) =>
|
|
19
|
+
ui.table(rows, [
|
|
20
|
+
{ key: 'slug', label: 'SLUG' },
|
|
21
|
+
{ key: 'name', label: 'NAME' },
|
|
22
|
+
{ key: 'visibility', label: 'VISIBILITY' },
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
create: {
|
|
28
|
+
summary: 'Create a bucket',
|
|
29
|
+
usage: 'motherbase storage buckets create <slug> [--name n] [--visibility public|private]',
|
|
30
|
+
async run(ctx, args, flags) {
|
|
31
|
+
const slug = requireArg(args[0], 'slug');
|
|
32
|
+
const body = { slug, name: flags.name || slug };
|
|
33
|
+
if (flags.visibility) body.visibility = flags.visibility;
|
|
34
|
+
const data = await api.adminRequest(ctx, 'POST', '/storage/buckets', { body });
|
|
35
|
+
output(ctx, data, (d, ui) => ui.success(`Created bucket ${slug}`));
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
delete: {
|
|
39
|
+
summary: 'Delete a bucket',
|
|
40
|
+
usage: 'motherbase storage buckets delete <slug> [--force]',
|
|
41
|
+
async run(ctx, args, flags) {
|
|
42
|
+
const slug = requireArg(args[0], 'slug');
|
|
43
|
+
const query = flags.force ? { force: 'true' } : undefined;
|
|
44
|
+
const data = await api.adminRequest(ctx, 'DELETE', `/storage/buckets/${slug}`, { query });
|
|
45
|
+
output(ctx, data, (d, ui) => ui.success(`Deleted bucket ${slug}`));
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const subcommands = {
|
|
52
|
+
buckets,
|
|
53
|
+
ls: {
|
|
54
|
+
summary: 'List files (optionally within a bucket)',
|
|
55
|
+
usage: 'motherbase storage ls [bucket]',
|
|
56
|
+
async run(ctx, args) {
|
|
57
|
+
const bucket = args[0];
|
|
58
|
+
const apiPath = bucket ? `/storage/buckets/${bucket}/files` : '/storage/files';
|
|
59
|
+
const data = await api.adminRequest(ctx, 'GET', apiPath);
|
|
60
|
+
const rows = (data && (data.files || data.items)) || (Array.isArray(data) ? data : []);
|
|
61
|
+
output(ctx, data, (_d, ui) =>
|
|
62
|
+
ui.table(rows, [
|
|
63
|
+
{ key: 'id', label: 'ID' },
|
|
64
|
+
{ key: 'filename', label: 'FILENAME' },
|
|
65
|
+
{ key: 'size', label: 'SIZE' },
|
|
66
|
+
{ key: 'visibility', label: 'VISIBILITY' },
|
|
67
|
+
]),
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
cp: {
|
|
72
|
+
summary: 'Upload (local -> bucket) or download (fileId -> local)',
|
|
73
|
+
usage: 'motherbase storage cp <src> <dst> e.g. cp ./a.png mybucket: | cp <fileId> ./a.png',
|
|
74
|
+
async run(ctx, args, flags) {
|
|
75
|
+
const src = requireArg(args[0], 'src');
|
|
76
|
+
const dst = requireArg(args[1], 'dst');
|
|
77
|
+
const srcLocal = fs.existsSync(path.resolve(ctx.cwd, src));
|
|
78
|
+
|
|
79
|
+
if (srcLocal) {
|
|
80
|
+
// Upload: dst is "bucket" or "bucket:path" (path is advisory; server keys by id).
|
|
81
|
+
const { bucket } = splitRef(dst);
|
|
82
|
+
const fields = {};
|
|
83
|
+
if (bucket) fields.bucket = bucket;
|
|
84
|
+
if (flags.visibility) fields.visibility = flags.visibility;
|
|
85
|
+
const data = await api.uploadFile(ctx, '/storage/files', path.resolve(ctx.cwd, src), fields);
|
|
86
|
+
output(ctx, data, (d, ui) => ui.success(`Uploaded ${src}${d && d.id ? ` → ${d.id}` : ''}`));
|
|
87
|
+
} else {
|
|
88
|
+
// Download: src is a fileId (optionally "bucket:id"), dst is a local path.
|
|
89
|
+
const { rest } = splitRef(src);
|
|
90
|
+
const fileId = rest || src;
|
|
91
|
+
const destPath = path.resolve(ctx.cwd, dst);
|
|
92
|
+
const res = await api.downloadFile(ctx, `/storage/files/${fileId}/raw`, destPath);
|
|
93
|
+
if (ctx.flags.json) require('../ui').json(res);
|
|
94
|
+
else require('../ui').success(`Downloaded ${fileId} → ${dst}`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
rm: {
|
|
99
|
+
summary: 'Delete a file by id',
|
|
100
|
+
usage: 'motherbase storage rm <id>',
|
|
101
|
+
async run(ctx, args) {
|
|
102
|
+
const id = requireArg(args[0], 'id');
|
|
103
|
+
const data = await api.adminRequest(ctx, 'DELETE', `/storage/files/${id}`);
|
|
104
|
+
output(ctx, data, (d, ui) => ui.success(`Deleted file ${id}`));
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
summary: 'Storage buckets and files',
|
|
111
|
+
usage: 'motherbase storage <buckets|ls|cp|rm> ...',
|
|
112
|
+
subcommands,
|
|
113
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const ui = require('../ui');
|
|
5
|
+
const { CliError } = require('../context');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
summary: 'Remove the project link from the current directory',
|
|
9
|
+
usage: 'motherbase unlink',
|
|
10
|
+
async run(ctx) {
|
|
11
|
+
const removed = config.removeLink(ctx.cwd);
|
|
12
|
+
if (!removed) throw new CliError('No project link found in this directory.');
|
|
13
|
+
ui.success('Unlinked project.');
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output, requireArg } = require('./_util');
|
|
5
|
+
const { CliError } = require('../context');
|
|
6
|
+
|
|
7
|
+
// Collect known user fields from flags into a body.
|
|
8
|
+
function userBody(flags) {
|
|
9
|
+
const body = {};
|
|
10
|
+
for (const key of ['email', 'password', 'name', 'role']) {
|
|
11
|
+
if (flags[key] !== undefined) body[key] = flags[key];
|
|
12
|
+
}
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const subcommands = {
|
|
17
|
+
list: {
|
|
18
|
+
summary: 'List project users',
|
|
19
|
+
usage: 'motherbase users list',
|
|
20
|
+
async run(ctx) {
|
|
21
|
+
const data = await api.adminRequest(ctx, 'GET', '/users');
|
|
22
|
+
const users = (data && data.users) || data || [];
|
|
23
|
+
output(ctx, data, (_d, ui) =>
|
|
24
|
+
ui.table(Array.isArray(users) ? users : [], [
|
|
25
|
+
{ key: 'id', label: 'ID' },
|
|
26
|
+
{ key: 'email', label: 'EMAIL' },
|
|
27
|
+
{ key: 'role', label: 'ROLE' },
|
|
28
|
+
]),
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
get: {
|
|
33
|
+
summary: 'Get a user by id',
|
|
34
|
+
usage: 'motherbase users get <id>',
|
|
35
|
+
async run(ctx, args) {
|
|
36
|
+
const id = requireArg(args[0], 'id');
|
|
37
|
+
const data = await api.adminRequest(ctx, 'GET', `/users/${id}`);
|
|
38
|
+
output(ctx, data, (d, ui) => ui.keyValue(d || {}));
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
create: {
|
|
42
|
+
summary: 'Create a user',
|
|
43
|
+
usage: 'motherbase users create --email <e> --password <p> [--name <n>] [--role <r>]',
|
|
44
|
+
async run(ctx, args, flags) {
|
|
45
|
+
const body = userBody(flags);
|
|
46
|
+
if (!body.email || !body.password) throw new CliError('--email and --password are required');
|
|
47
|
+
const data = await api.adminRequest(ctx, 'POST', '/users', { body });
|
|
48
|
+
output(ctx, data, (d, ui) => ui.success(`Created user ${body.email}`));
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
update: {
|
|
52
|
+
summary: 'Update a user',
|
|
53
|
+
usage: 'motherbase users update <id> [--email ...] [--name ...] [--role ...]',
|
|
54
|
+
async run(ctx, args, flags) {
|
|
55
|
+
const id = requireArg(args[0], 'id');
|
|
56
|
+
const body = userBody(flags);
|
|
57
|
+
const data = await api.adminRequest(ctx, 'PATCH', `/users/${id}`, { body });
|
|
58
|
+
output(ctx, data, (d, ui) => ui.success(`Updated user ${id}`));
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
delete: {
|
|
62
|
+
summary: 'Delete a user',
|
|
63
|
+
usage: 'motherbase users delete <id>',
|
|
64
|
+
async run(ctx, args) {
|
|
65
|
+
const id = requireArg(args[0], 'id');
|
|
66
|
+
const data = await api.adminRequest(ctx, 'DELETE', `/users/${id}`);
|
|
67
|
+
output(ctx, data, (d, ui) => ui.success(`Deleted user ${id}`));
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
'reset-password': {
|
|
71
|
+
summary: 'Reset a user password',
|
|
72
|
+
usage: 'motherbase users reset-password <id> [--password <p>]',
|
|
73
|
+
async run(ctx, args, flags) {
|
|
74
|
+
const id = requireArg(args[0], 'id');
|
|
75
|
+
const body = flags.password ? { password: flags.password } : {};
|
|
76
|
+
const data = await api.adminRequest(ctx, 'POST', `/users/${id}/reset-password`, { body });
|
|
77
|
+
output(ctx, data, (d, ui) => ui.success(`Password reset for ${id}`));
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
promote: {
|
|
81
|
+
summary: 'Promote a user to ADMIN',
|
|
82
|
+
usage: 'motherbase users promote <id>',
|
|
83
|
+
async run(ctx, args) {
|
|
84
|
+
const id = requireArg(args[0], 'id');
|
|
85
|
+
const data = await api.adminRequest(ctx, 'POST', `/users/${id}/promote`);
|
|
86
|
+
output(ctx, data, (d, ui) => ui.success(`Promoted ${id}`));
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
demote: {
|
|
90
|
+
summary: 'Demote a user to USER',
|
|
91
|
+
usage: 'motherbase users demote <id>',
|
|
92
|
+
async run(ctx, args) {
|
|
93
|
+
const id = requireArg(args[0], 'id');
|
|
94
|
+
const data = await api.adminRequest(ctx, 'POST', `/users/${id}/demote`);
|
|
95
|
+
output(ctx, data, (d, ui) => ui.success(`Demoted ${id}`));
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
'revoke-sessions': {
|
|
99
|
+
summary: 'Revoke all sessions for a user',
|
|
100
|
+
usage: 'motherbase users revoke-sessions <id>',
|
|
101
|
+
async run(ctx, args) {
|
|
102
|
+
const id = requireArg(args[0], 'id');
|
|
103
|
+
const data = await api.adminRequest(ctx, 'POST', `/users/${id}/revoke-sessions`);
|
|
104
|
+
output(ctx, data, (d, ui) => ui.success(`Sessions revoked for ${id}`));
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
summary: 'Project auth users',
|
|
111
|
+
usage: 'motherbase users <list|get|create|update|delete|reset-password|promote|demote|revoke-sessions> ...',
|
|
112
|
+
subcommands,
|
|
113
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output, requireArg } = require('./_util');
|
|
5
|
+
const { CliError } = require('../context');
|
|
6
|
+
|
|
7
|
+
const subcommands = {
|
|
8
|
+
list: {
|
|
9
|
+
summary: 'List webhooks',
|
|
10
|
+
usage: 'motherbase webhooks list',
|
|
11
|
+
async run(ctx) {
|
|
12
|
+
const data = await api.adminRequest(ctx, 'GET', '/webhooks');
|
|
13
|
+
const rows = (data && data.webhooks) || data || [];
|
|
14
|
+
output(ctx, data, (_d, ui) =>
|
|
15
|
+
ui.table(Array.isArray(rows) ? rows : [], [
|
|
16
|
+
{ key: 'id', label: 'ID' },
|
|
17
|
+
{ key: 'name', label: 'NAME' },
|
|
18
|
+
{ key: 'url', label: 'URL' },
|
|
19
|
+
{ key: 'events', label: 'EVENTS' },
|
|
20
|
+
]),
|
|
21
|
+
);
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create: {
|
|
25
|
+
summary: 'Create a webhook',
|
|
26
|
+
usage: 'motherbase webhooks create --name <n> --url <u> --events a,b',
|
|
27
|
+
async run(ctx, args, flags) {
|
|
28
|
+
if (!flags.name || !flags.url || !flags.events) throw new CliError('--name, --url and --events are required');
|
|
29
|
+
const body = {
|
|
30
|
+
name: flags.name,
|
|
31
|
+
url: flags.url,
|
|
32
|
+
events: String(flags.events).split(',').map(s => s.trim()).filter(Boolean),
|
|
33
|
+
};
|
|
34
|
+
const data = await api.adminRequest(ctx, 'POST', '/webhooks', { body });
|
|
35
|
+
output(ctx, data, (d, ui) => ui.success(`Created webhook ${flags.name}`));
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
delete: {
|
|
39
|
+
summary: 'Delete a webhook',
|
|
40
|
+
usage: 'motherbase webhooks delete <id>',
|
|
41
|
+
async run(ctx, args) {
|
|
42
|
+
const id = requireArg(args[0], 'id');
|
|
43
|
+
const data = await api.adminRequest(ctx, 'DELETE', `/webhooks/${id}`);
|
|
44
|
+
output(ctx, data, (d, ui) => ui.success(`Deleted webhook ${id}`));
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
test: {
|
|
48
|
+
summary: 'Send a test event to a webhook',
|
|
49
|
+
usage: 'motherbase webhooks test <id>',
|
|
50
|
+
async run(ctx, args) {
|
|
51
|
+
const id = requireArg(args[0], 'id');
|
|
52
|
+
const data = await api.adminRequest(ctx, 'POST', `/webhooks/${id}/test`);
|
|
53
|
+
output(ctx, data, (d, ui) => ui.json(d));
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
summary: 'Outgoing webhooks',
|
|
60
|
+
usage: 'motherbase webhooks <list|create|delete|test> ...',
|
|
61
|
+
subcommands,
|
|
62
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output } = require('./_util');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
summary: 'Show the current console, account and platform info',
|
|
8
|
+
usage: 'motherbase whoami',
|
|
9
|
+
async run(ctx) {
|
|
10
|
+
const { consoleUrl } = ctx.requireConsole();
|
|
11
|
+
const info = await api.consoleRequest(ctx, 'GET', '/info');
|
|
12
|
+
const data = {
|
|
13
|
+
console: consoleUrl,
|
|
14
|
+
email: ctx.accountEmail || null,
|
|
15
|
+
mode: info && info.mode,
|
|
16
|
+
domainSuffix: info && info.domainSuffix,
|
|
17
|
+
};
|
|
18
|
+
output(ctx, data, (d, ui) => ui.keyValue(d));
|
|
19
|
+
},
|
|
20
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +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
|
+
};
|