@motherbase/cli 0.2.5 → 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 -0
- package/README.md +2 -1
- package/bin/.motherbase/config.json +5 -0
- package/package.json +35 -26
- package/src/api.js +256 -241
- package/src/commands/_util.js +14 -1
- package/src/commands/db-catalog.js +326 -326
- package/src/commands/functions.js +304 -297
- package/src/commands/gen.js +7 -1
- package/src/commands/local.js +371 -273
- package/src/commands/login.js +45 -39
- package/src/commands/migrate.js +202 -179
- package/src/commands/migration.js +108 -108
- package/src/commands/projects.js +2 -1
- package/src/commands/storage.js +2 -2
- package/src/commands/users.js +142 -142
- package/src/context.js +6 -2
- package/src/docs.js +68 -0
- package/src/main.js +224 -194
- package/src/mcp/protocol.js +4 -1
- package/src/migration/commands.js +375 -375
- package/src/migration/files.js +258 -245
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// `migration`: Supabase's name for the group. The server-side lifecycle
|
|
4
|
-
// (list/diff/get/create/apply/rollback) and the local `new` scaffold live
|
|
5
|
-
// together here; `migrations` is registered as an alias of this entry.
|
|
6
|
-
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const api = require('../api');
|
|
10
|
-
const ui = require('../ui');
|
|
11
|
-
const { output, requireArg } = require('./_util');
|
|
12
|
-
const { CliError } = require('../context');
|
|
13
|
-
|
|
14
|
-
function readSql(cwd, file) {
|
|
15
|
-
if (!file) return undefined;
|
|
16
|
-
return fs.readFileSync(path.resolve(cwd, file), 'utf8');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const subcommands = {
|
|
20
|
-
new: {
|
|
21
|
-
summary: 'Scaffold a local migration .sql file',
|
|
22
|
-
usage: 'motherbase migration new <name>',
|
|
23
|
-
async run(ctx, args) {
|
|
24
|
-
const name = requireArg(args[0], 'name');
|
|
25
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new CliError('name must be alphanumeric / dash / underscore');
|
|
26
|
-
const dir = path.join(ctx.cwd, 'motherbase', 'migrations');
|
|
27
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
-
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '');
|
|
29
|
-
const file = path.join(dir, `${stamp}_${name}.sql`);
|
|
30
|
-
fs.writeFileSync(file, `-- Migration: ${name}\n\n\n-- @down\n-- Optional: SQL that undoes the above, for \`migration down\`.\n
|
|
31
|
-
ui.success(`Created ${path.relative(ctx.cwd, file)}`);
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
list: {
|
|
35
|
-
summary: 'List schema migrations',
|
|
36
|
-
usage: 'motherbase migration list',
|
|
37
|
-
async run(ctx) {
|
|
38
|
-
const data = await api.adminRequest(ctx, 'GET', '/migrations');
|
|
39
|
-
const rows = (data && data.migrations) || [];
|
|
40
|
-
output(ctx, data, (_d, ui) =>
|
|
41
|
-
ui.table(rows, [
|
|
42
|
-
{ key: 'id', label: 'ID' },
|
|
43
|
-
{ key: 'name', label: 'NAME' },
|
|
44
|
-
{ key: 'status', label: 'STATUS' },
|
|
45
|
-
{ key: 'appliedAt', label: 'APPLIED' },
|
|
46
|
-
]),
|
|
47
|
-
);
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
diff: {
|
|
51
|
-
summary: 'Show a diff between live schema and migrations',
|
|
52
|
-
usage: 'motherbase migration diff',
|
|
53
|
-
async run(ctx) {
|
|
54
|
-
const data = await api.adminRequest(ctx, 'GET', '/migrations/diff');
|
|
55
|
-
output(ctx, data, (d, ui) => ui.json(d));
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
get: {
|
|
59
|
-
summary: 'Show a single migration',
|
|
60
|
-
usage: 'motherbase migration get <id>',
|
|
61
|
-
async run(ctx, args) {
|
|
62
|
-
const id = requireArg(args[0], 'id');
|
|
63
|
-
const data = await api.adminRequest(ctx, 'GET', `/migrations/${id}`);
|
|
64
|
-
output(ctx, data, (d, ui) => ui.keyValue((d && d.migration) || d || {}));
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
create: {
|
|
68
|
-
summary: 'Create a migration from up/down SQL files',
|
|
69
|
-
usage: 'motherbase migration create --name <name> --up <file.sql> [--down <file.sql>]',
|
|
70
|
-
async run(ctx, args, flags) {
|
|
71
|
-
const name = flags.name || args[0];
|
|
72
|
-
if (!name) throw new CliError('--name is required');
|
|
73
|
-
const upSql = readSql(ctx.cwd, flags.up);
|
|
74
|
-
const downSql = readSql(ctx.cwd, flags.down);
|
|
75
|
-
if (!upSql) throw new CliError('--up <file.sql> is required');
|
|
76
|
-
const data = await api.adminRequest(ctx, 'POST', '/migrations', { body: { name, upSql, downSql } });
|
|
77
|
-
output(ctx, data, (d, ui) => ui.success(`Created migration ${name}`));
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
apply: {
|
|
81
|
-
summary: 'Apply a migration',
|
|
82
|
-
usage: 'motherbase migration apply <id>',
|
|
83
|
-
async run(ctx, args) {
|
|
84
|
-
const id = requireArg(args[0], 'id');
|
|
85
|
-
const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/apply`);
|
|
86
|
-
output(ctx, data, (d, ui) => ui.success(`Applied ${id}`));
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
rollback: {
|
|
90
|
-
summary: 'Roll back a migration',
|
|
91
|
-
usage: 'motherbase migration rollback <id>',
|
|
92
|
-
async run(ctx, args) {
|
|
93
|
-
const id = requireArg(args[0], 'id');
|
|
94
|
-
const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/rollback`);
|
|
95
|
-
output(ctx, data, (d, ui) => ui.success(`Rolled back ${id}`));
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// The local-files workflow (list/up/down/repair/squash/fetch) lives with its
|
|
101
|
-
// helpers in ../migration.
|
|
102
|
-
Object.assign(subcommands, require('../migration/commands').migrationSubcommands);
|
|
103
|
-
|
|
104
|
-
module.exports = {
|
|
105
|
-
summary: 'Schema migrations: local files and the remote history',
|
|
106
|
-
usage: 'motherbase migration <new|list|up|down|repair|squash|fetch|diff|get|create|apply|rollback> ...',
|
|
107
|
-
subcommands,
|
|
108
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `migration`: Supabase's name for the group. The server-side lifecycle
|
|
4
|
+
// (list/diff/get/create/apply/rollback) and the local `new` scaffold live
|
|
5
|
+
// together here; `migrations` is registered as an alias of this entry.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const api = require('../api');
|
|
10
|
+
const ui = require('../ui');
|
|
11
|
+
const { output, requireArg } = require('./_util');
|
|
12
|
+
const { CliError } = require('../context');
|
|
13
|
+
|
|
14
|
+
function readSql(cwd, file) {
|
|
15
|
+
if (!file) return undefined;
|
|
16
|
+
return fs.readFileSync(path.resolve(cwd, file), 'utf8');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const subcommands = {
|
|
20
|
+
new: {
|
|
21
|
+
summary: 'Scaffold a local migration .sql file',
|
|
22
|
+
usage: 'motherbase migration new <name>',
|
|
23
|
+
async run(ctx, args) {
|
|
24
|
+
const name = requireArg(args[0], 'name');
|
|
25
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new CliError('name must be alphanumeric / dash / underscore');
|
|
26
|
+
const dir = path.join(ctx.cwd, 'motherbase', 'migrations');
|
|
27
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
28
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '');
|
|
29
|
+
const file = path.join(dir, `${stamp}_${name}.sql`);
|
|
30
|
+
fs.writeFileSync(file, `-- Migration: ${name}\n\n\n-- @down\n-- Optional: SQL that undoes the above, for \`migration down\`.\n`, { flag: 'wx' });
|
|
31
|
+
ui.success(`Created ${path.relative(ctx.cwd, file)}`);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
list: {
|
|
35
|
+
summary: 'List schema migrations',
|
|
36
|
+
usage: 'motherbase migration list',
|
|
37
|
+
async run(ctx) {
|
|
38
|
+
const data = await api.adminRequest(ctx, 'GET', '/migrations');
|
|
39
|
+
const rows = (data && data.migrations) || [];
|
|
40
|
+
output(ctx, data, (_d, ui) =>
|
|
41
|
+
ui.table(rows, [
|
|
42
|
+
{ key: 'id', label: 'ID' },
|
|
43
|
+
{ key: 'name', label: 'NAME' },
|
|
44
|
+
{ key: 'status', label: 'STATUS' },
|
|
45
|
+
{ key: 'appliedAt', label: 'APPLIED' },
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
diff: {
|
|
51
|
+
summary: 'Show a diff between live schema and migrations',
|
|
52
|
+
usage: 'motherbase migration diff',
|
|
53
|
+
async run(ctx) {
|
|
54
|
+
const data = await api.adminRequest(ctx, 'GET', '/migrations/diff');
|
|
55
|
+
output(ctx, data, (d, ui) => ui.json(d));
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
get: {
|
|
59
|
+
summary: 'Show a single migration',
|
|
60
|
+
usage: 'motherbase migration get <id>',
|
|
61
|
+
async run(ctx, args) {
|
|
62
|
+
const id = requireArg(args[0], 'id');
|
|
63
|
+
const data = await api.adminRequest(ctx, 'GET', `/migrations/${id}`);
|
|
64
|
+
output(ctx, data, (d, ui) => ui.keyValue((d && d.migration) || d || {}));
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
create: {
|
|
68
|
+
summary: 'Create a migration from up/down SQL files',
|
|
69
|
+
usage: 'motherbase migration create --name <name> --up <file.sql> [--down <file.sql>]',
|
|
70
|
+
async run(ctx, args, flags) {
|
|
71
|
+
const name = flags.name || args[0];
|
|
72
|
+
if (!name) throw new CliError('--name is required');
|
|
73
|
+
const upSql = readSql(ctx.cwd, flags.up);
|
|
74
|
+
const downSql = readSql(ctx.cwd, flags.down);
|
|
75
|
+
if (!upSql) throw new CliError('--up <file.sql> is required');
|
|
76
|
+
const data = await api.adminRequest(ctx, 'POST', '/migrations', { body: { name, upSql, downSql } });
|
|
77
|
+
output(ctx, data, (d, ui) => ui.success(`Created migration ${name}`));
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
apply: {
|
|
81
|
+
summary: 'Apply a migration',
|
|
82
|
+
usage: 'motherbase migration apply <id>',
|
|
83
|
+
async run(ctx, args) {
|
|
84
|
+
const id = requireArg(args[0], 'id');
|
|
85
|
+
const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/apply`);
|
|
86
|
+
output(ctx, data, (d, ui) => ui.success(`Applied ${id}`));
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
rollback: {
|
|
90
|
+
summary: 'Roll back a migration',
|
|
91
|
+
usage: 'motherbase migration rollback <id>',
|
|
92
|
+
async run(ctx, args) {
|
|
93
|
+
const id = requireArg(args[0], 'id');
|
|
94
|
+
const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/rollback`);
|
|
95
|
+
output(ctx, data, (d, ui) => ui.success(`Rolled back ${id}`));
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// The local-files workflow (list/up/down/repair/squash/fetch) lives with its
|
|
101
|
+
// helpers in ../migration.
|
|
102
|
+
Object.assign(subcommands, require('../migration/commands').migrationSubcommands);
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
summary: 'Schema migrations: local files and the remote history',
|
|
106
|
+
usage: 'motherbase migration <new|list|up|down|repair|squash|fetch|diff|get|create|apply|rollback> ...',
|
|
107
|
+
subcommands,
|
|
108
|
+
};
|
package/src/commands/projects.js
CHANGED
|
@@ -99,13 +99,14 @@ const subcommands = {
|
|
|
99
99
|
},
|
|
100
100
|
limits: {
|
|
101
101
|
summary: 'Get or set per-project resource limits',
|
|
102
|
-
usage: 'motherbase projects limits <id> [--api-cpus n --api-mem mb --db-cpus n --db-mem mb --minio-cpus n --minio-mem mb]',
|
|
102
|
+
usage: 'motherbase projects limits <id> [--api-cpus n --api-mem mb --db-cpus n --db-mem mb --minio-cpus n --minio-mem mb --edge-cpus n --edge-mem mb]',
|
|
103
103
|
async run(ctx, args, flags) {
|
|
104
104
|
const id = requireArg(args[0], 'id');
|
|
105
105
|
const map = {
|
|
106
106
|
'api-cpus': 'apiCpus', 'api-mem': 'apiMemMb',
|
|
107
107
|
'db-cpus': 'dbCpus', 'db-mem': 'dbMemMb',
|
|
108
108
|
'minio-cpus': 'minioCpus', 'minio-mem': 'minioMemMb',
|
|
109
|
+
'edge-cpus': 'edgeCpus', 'edge-mem': 'edgeMemMb',
|
|
109
110
|
};
|
|
110
111
|
const body = {};
|
|
111
112
|
for (const [flag, key] of Object.entries(map)) {
|
package/src/commands/storage.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const api = require('../api');
|
|
11
|
-
const { output, requireArg, splitRef } = require('./_util');
|
|
11
|
+
const { output, requireArg, splitRef, resolveInside } = require('./_util');
|
|
12
12
|
const { CliError } = require('../context');
|
|
13
13
|
const remote = require('../storage/remote');
|
|
14
14
|
|
|
@@ -108,7 +108,7 @@ async function download(ctx, srcArg, dstArg, recursive) {
|
|
|
108
108
|
const prefix = remote.asPrefix(src.key);
|
|
109
109
|
const keys = await sourceKeys(ctx, src, true);
|
|
110
110
|
return each(keys, 'download', key => {
|
|
111
|
-
const target =
|
|
111
|
+
const target = resolveInside(local, ...key.slice(prefix.length).split('/'));
|
|
112
112
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
113
113
|
return api.downloadFile(ctx, objectPath(src.bucket, key), target);
|
|
114
114
|
});
|
package/src/commands/users.js
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
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
|
-
audit: {
|
|
108
|
-
summary: 'Read the auth audit log, newest first',
|
|
109
|
-
usage: 'motherbase users audit [--action <a>] [--actor <email|id>] [--type <log type>] [--from <iso>] [--to <iso>] [--limit <n>]',
|
|
110
|
-
async run(ctx, args, flags) {
|
|
111
|
-
const query = {
|
|
112
|
-
action: flags.action,
|
|
113
|
-
actor: flags.actor,
|
|
114
|
-
log_type: flags.type,
|
|
115
|
-
from: flags.from,
|
|
116
|
-
to: flags.to,
|
|
117
|
-
per_page: flags.limit || 50,
|
|
118
|
-
};
|
|
119
|
-
const data = await api.adminRequest(ctx, 'GET', '/auth/audit-logs', { query });
|
|
120
|
-
const entries = ((data && data.entries) || []).map(e => ({
|
|
121
|
-
at: e.created_at,
|
|
122
|
-
action: e.payload.action,
|
|
123
|
-
actor: e.payload.actor_username || e.payload.actor_id,
|
|
124
|
-
ip: e.ip_address,
|
|
125
|
-
}));
|
|
126
|
-
output(ctx, data, (_d, ui) =>
|
|
127
|
-
ui.table(entries, [
|
|
128
|
-
{ key: 'at', label: 'AT' },
|
|
129
|
-
{ key: 'action', label: 'ACTION' },
|
|
130
|
-
{ key: 'actor', label: 'ACTOR' },
|
|
131
|
-
{ key: 'ip', label: 'IP' },
|
|
132
|
-
]),
|
|
133
|
-
);
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
module.exports = {
|
|
139
|
-
summary: 'Project auth users',
|
|
140
|
-
usage: 'motherbase users <list|get|create|update|delete|reset-password|promote|demote|revoke-sessions|audit> ...',
|
|
141
|
-
subcommands,
|
|
142
|
-
};
|
|
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 = typeof flags.password === 'string' ? { 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
|
+
audit: {
|
|
108
|
+
summary: 'Read the auth audit log, newest first',
|
|
109
|
+
usage: 'motherbase users audit [--action <a>] [--actor <email|id>] [--type <log type>] [--from <iso>] [--to <iso>] [--limit <n>]',
|
|
110
|
+
async run(ctx, args, flags) {
|
|
111
|
+
const query = {
|
|
112
|
+
action: flags.action,
|
|
113
|
+
actor: flags.actor,
|
|
114
|
+
log_type: flags.type,
|
|
115
|
+
from: flags.from,
|
|
116
|
+
to: flags.to,
|
|
117
|
+
per_page: flags.limit || 50,
|
|
118
|
+
};
|
|
119
|
+
const data = await api.adminRequest(ctx, 'GET', '/auth/audit-logs', { query });
|
|
120
|
+
const entries = ((data && data.entries) || []).map(e => ({
|
|
121
|
+
at: e.created_at,
|
|
122
|
+
action: e.payload.action,
|
|
123
|
+
actor: e.payload.actor_username || e.payload.actor_id,
|
|
124
|
+
ip: e.ip_address,
|
|
125
|
+
}));
|
|
126
|
+
output(ctx, data, (_d, ui) =>
|
|
127
|
+
ui.table(entries, [
|
|
128
|
+
{ key: 'at', label: 'AT' },
|
|
129
|
+
{ key: 'action', label: 'ACTION' },
|
|
130
|
+
{ key: 'actor', label: 'ACTOR' },
|
|
131
|
+
{ key: 'ip', label: 'IP' },
|
|
132
|
+
]),
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
summary: 'Project auth users',
|
|
140
|
+
usage: 'motherbase users <list|get|create|update|delete|reset-password|promote|demote|revoke-sessions|audit> ...',
|
|
141
|
+
subcommands,
|
|
142
|
+
};
|
package/src/context.js
CHANGED
|
@@ -36,9 +36,13 @@ function createContext(flags = {}, env = process.env, cwd = process.cwd()) {
|
|
|
36
36
|
);
|
|
37
37
|
const normConsoleUrl = consoleUrl ? config.normalizeUrl(consoleUrl) : undefined;
|
|
38
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
|
+
|
|
39
43
|
const token = firstDefined(
|
|
40
|
-
flags.token,
|
|
41
|
-
env.MOTHERBASE_TOKEN,
|
|
44
|
+
urlFromLinkOnly ? undefined : flags.token,
|
|
45
|
+
urlFromLinkOnly ? undefined : env.MOTHERBASE_TOKEN,
|
|
42
46
|
normConsoleUrl ? (config.consoleEntry(normConsoleUrl) || {}).token : undefined,
|
|
43
47
|
);
|
|
44
48
|
|
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 };
|