@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.
Files changed (59) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +248 -247
  3. package/bin/.motherbase/config.json +5 -0
  4. package/bin/motherbase.js +10 -10
  5. package/package.json +1 -1
  6. package/src/api.js +39 -16
  7. package/src/commands/_util.js +106 -106
  8. package/src/commands/backups.js +16 -16
  9. package/src/commands/branches.js +113 -113
  10. package/src/commands/completion.js +90 -90
  11. package/src/commands/config.js +47 -47
  12. package/src/commands/cron.js +65 -65
  13. package/src/commands/db.js +103 -103
  14. package/src/commands/domains.js +124 -124
  15. package/src/commands/gen.js +178 -172
  16. package/src/commands/import.js +55 -55
  17. package/src/commands/index.js +85 -85
  18. package/src/commands/inspect.js +178 -178
  19. package/src/commands/keys.js +72 -72
  20. package/src/commands/link.js +35 -35
  21. package/src/commands/local.js +100 -23
  22. package/src/commands/login.js +9 -3
  23. package/src/commands/logout.js +16 -16
  24. package/src/commands/mcp.js +61 -61
  25. package/src/commands/migrate.js +0 -5
  26. package/src/commands/migration.js +1 -1
  27. package/src/commands/network.js +83 -83
  28. package/src/commands/orgs.js +40 -40
  29. package/src/commands/postgres-config.js +113 -113
  30. package/src/commands/projects.js +131 -130
  31. package/src/commands/queues.js +133 -133
  32. package/src/commands/realtime.js +35 -35
  33. package/src/commands/rls.js +176 -176
  34. package/src/commands/secrets.js +66 -66
  35. package/src/commands/signing.js +130 -130
  36. package/src/commands/snippets.js +44 -44
  37. package/src/commands/status.js +42 -42
  38. package/src/commands/storage.js +261 -261
  39. package/src/commands/test.js +9 -9
  40. package/src/commands/unlink.js +15 -15
  41. package/src/commands/vault.js +82 -82
  42. package/src/commands/webhooks.js +62 -62
  43. package/src/commands/whoami.js +20 -20
  44. package/src/commands/wrappers.js +150 -150
  45. package/src/config.js +98 -98
  46. package/src/context.js +106 -106
  47. package/src/edge-templates.js +194 -194
  48. package/src/format.js +92 -92
  49. package/src/inspect/csv.js +22 -22
  50. package/src/inspect/queries.js +562 -562
  51. package/src/mcp/platform-tools.js +233 -233
  52. package/src/mcp/protocol.js +152 -152
  53. package/src/mcp/storage-tools.js +93 -93
  54. package/src/mcp/tools.js +478 -478
  55. package/src/migration/commands.js +2 -2
  56. package/src/migration/files.js +13 -0
  57. package/src/prompt.js +91 -91
  58. package/src/storage/remote.js +62 -62
  59. package/src/ui.js +111 -111
@@ -1,82 +1,82 @@
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 Vault secrets (names and metadata, never values)',
10
- usage: 'motherbase vault list',
11
- async run(ctx) {
12
- const data = await api.adminRequest(ctx, 'GET', '/vault');
13
- const secrets = (data && data.secrets) || [];
14
- output(ctx, data, (_d, ui) =>
15
- ui.table(secrets, [
16
- { key: 'id', label: 'ID' },
17
- { key: 'name', label: 'NAME' },
18
- { key: 'description', label: 'DESCRIPTION' },
19
- { key: 'updatedAt', label: 'UPDATED' },
20
- ]),
21
- );
22
- },
23
- },
24
- enable: {
25
- summary: 'Install the Vault for this project',
26
- usage: 'motherbase vault enable',
27
- async run(ctx) {
28
- const data = await api.adminRequest(ctx, 'POST', '/vault/enable');
29
- output(ctx, data, (_d, ui) => ui.success('Vault enabled'));
30
- },
31
- },
32
- create: {
33
- summary: 'Store an encrypted secret',
34
- usage: 'motherbase vault create <name> --secret <value> [--description <text>]',
35
- async run(ctx, args, flags) {
36
- const name = requireArg(args[0], 'name');
37
- if (typeof flags.secret !== 'string') throw new CliError('--secret is required');
38
- const body = { name, secret: flags.secret };
39
- if (typeof flags.description === 'string') body.description = flags.description;
40
- const data = await api.adminRequest(ctx, 'POST', '/vault/secrets', { body });
41
- output(ctx, data, (_d, ui) => ui.success(`Created secret ${name}`));
42
- },
43
- },
44
- update: {
45
- summary: 'Rename a secret, change its value or its description',
46
- usage: 'motherbase vault update <id> [--name <n>] [--secret <value>] [--description <text>]',
47
- async run(ctx, args, flags) {
48
- const id = requireArg(args[0], 'id');
49
- const body = {};
50
- for (const key of ['name', 'secret', 'description']) {
51
- if (typeof flags[key] === 'string') body[key] = flags[key];
52
- }
53
- if (!Object.keys(body).length) throw new CliError('nothing to update: pass --name, --secret or --description');
54
- const data = await api.adminRequest(ctx, 'PATCH', `/vault/secrets/${encodeURIComponent(id)}`, { body });
55
- output(ctx, data, (_d, ui) => ui.success(`Updated secret ${id}`));
56
- },
57
- },
58
- reveal: {
59
- summary: 'Print the decrypted value of a secret',
60
- usage: 'motherbase vault reveal <id>',
61
- async run(ctx, args) {
62
- const id = requireArg(args[0], 'id');
63
- const data = await api.adminRequest(ctx, 'GET', `/vault/secrets/${encodeURIComponent(id)}/reveal`);
64
- output(ctx, data, (d, ui) => ui.print(String((d && d.secret) ?? '')));
65
- },
66
- },
67
- delete: {
68
- summary: 'Delete a secret',
69
- usage: 'motherbase vault delete <id>',
70
- async run(ctx, args) {
71
- const id = requireArg(args[0], 'id');
72
- const data = await api.adminRequest(ctx, 'DELETE', `/vault/secrets/${encodeURIComponent(id)}`);
73
- output(ctx, data, (_d, ui) => ui.success(`Deleted secret ${id}`));
74
- },
75
- },
76
- };
77
-
78
- module.exports = {
79
- summary: 'Encrypted secrets (Vault)',
80
- usage: 'motherbase vault <list|enable|create|update|reveal|delete> ...',
81
- subcommands,
82
- };
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 Vault secrets (names and metadata, never values)',
10
+ usage: 'motherbase vault list',
11
+ async run(ctx) {
12
+ const data = await api.adminRequest(ctx, 'GET', '/vault');
13
+ const secrets = (data && data.secrets) || [];
14
+ output(ctx, data, (_d, ui) =>
15
+ ui.table(secrets, [
16
+ { key: 'id', label: 'ID' },
17
+ { key: 'name', label: 'NAME' },
18
+ { key: 'description', label: 'DESCRIPTION' },
19
+ { key: 'updatedAt', label: 'UPDATED' },
20
+ ]),
21
+ );
22
+ },
23
+ },
24
+ enable: {
25
+ summary: 'Install the Vault for this project',
26
+ usage: 'motherbase vault enable',
27
+ async run(ctx) {
28
+ const data = await api.adminRequest(ctx, 'POST', '/vault/enable');
29
+ output(ctx, data, (_d, ui) => ui.success('Vault enabled'));
30
+ },
31
+ },
32
+ create: {
33
+ summary: 'Store an encrypted secret',
34
+ usage: 'motherbase vault create <name> --secret <value> [--description <text>]',
35
+ async run(ctx, args, flags) {
36
+ const name = requireArg(args[0], 'name');
37
+ if (typeof flags.secret !== 'string') throw new CliError('--secret is required');
38
+ const body = { name, secret: flags.secret };
39
+ if (typeof flags.description === 'string') body.description = flags.description;
40
+ const data = await api.adminRequest(ctx, 'POST', '/vault/secrets', { body });
41
+ output(ctx, data, (_d, ui) => ui.success(`Created secret ${name}`));
42
+ },
43
+ },
44
+ update: {
45
+ summary: 'Rename a secret, change its value or its description',
46
+ usage: 'motherbase vault update <id> [--name <n>] [--secret <value>] [--description <text>]',
47
+ async run(ctx, args, flags) {
48
+ const id = requireArg(args[0], 'id');
49
+ const body = {};
50
+ for (const key of ['name', 'secret', 'description']) {
51
+ if (typeof flags[key] === 'string') body[key] = flags[key];
52
+ }
53
+ if (!Object.keys(body).length) throw new CliError('nothing to update: pass --name, --secret or --description');
54
+ const data = await api.adminRequest(ctx, 'PATCH', `/vault/secrets/${encodeURIComponent(id)}`, { body });
55
+ output(ctx, data, (_d, ui) => ui.success(`Updated secret ${id}`));
56
+ },
57
+ },
58
+ reveal: {
59
+ summary: 'Print the decrypted value of a secret',
60
+ usage: 'motherbase vault reveal <id>',
61
+ async run(ctx, args) {
62
+ const id = requireArg(args[0], 'id');
63
+ const data = await api.adminRequest(ctx, 'GET', `/vault/secrets/${encodeURIComponent(id)}/reveal`);
64
+ output(ctx, data, (d, ui) => ui.print(String((d && d.secret) ?? '')));
65
+ },
66
+ },
67
+ delete: {
68
+ summary: 'Delete a secret',
69
+ usage: 'motherbase vault delete <id>',
70
+ async run(ctx, args) {
71
+ const id = requireArg(args[0], 'id');
72
+ const data = await api.adminRequest(ctx, 'DELETE', `/vault/secrets/${encodeURIComponent(id)}`);
73
+ output(ctx, data, (_d, ui) => ui.success(`Deleted secret ${id}`));
74
+ },
75
+ },
76
+ };
77
+
78
+ module.exports = {
79
+ summary: 'Encrypted secrets (Vault)',
80
+ usage: 'motherbase vault <list|enable|create|update|reveal|delete> ...',
81
+ subcommands,
82
+ };
@@ -1,62 +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
- };
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
+ };
@@ -1,20 +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
- };
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
+ };
@@ -1,150 +1,150 @@
1
- 'use strict';
2
-
3
- const api = require('../api');
4
- const { output, requireArg, parseKeyValues } = require('./_util');
5
- const { CliError } = require('../context');
6
-
7
- const enc = encodeURIComponent;
8
-
9
- // `--option host=db.example.com --option port=5432`, repeatable.
10
- function options(flags) {
11
- return parseKeyValues([].concat(flags.option || []), { coerce: false });
12
- }
13
-
14
- // `schema.name` -> [schema, name]
15
- function qualified(ref) {
16
- const [schema, name, ...rest] = requireArg(ref, 'schema.table').split('.');
17
- if (!schema || !name || rest.length) throw new CliError('expected <schema>.<table>');
18
- return [schema, name];
19
- }
20
-
21
- // `--column id:integer --column label:text`, repeatable.
22
- function columns(flags) {
23
- const specs = [].concat(flags.column || []);
24
- if (!specs.length) throw new CliError('at least one --column <name>:<type> is required');
25
- return specs.map(spec => {
26
- const idx = String(spec).indexOf(':');
27
- if (idx <= 0) throw new CliError(`expected <name>:<type>, got "${spec}"`);
28
- return { name: spec.slice(0, idx), type: spec.slice(idx + 1) };
29
- });
30
- }
31
-
32
- const subcommands = {
33
- list: {
34
- summary: 'List wrappers, foreign servers and foreign tables',
35
- usage: 'motherbase wrappers list',
36
- async run(ctx) {
37
- const data = await api.adminRequest(ctx, 'GET', '/wrappers');
38
- output(ctx, data, (d, ui) => {
39
- ui.table((d && d.wrappers) || [], [
40
- { key: 'name', label: 'WRAPPER' },
41
- { key: 'available', label: 'AVAILABLE' },
42
- { key: 'installed', label: 'INSTALLED' },
43
- ]);
44
- ui.table((d && d.servers) || [], [
45
- { key: 'name', label: 'SERVER' },
46
- { key: 'wrapper', label: 'WRAPPER' },
47
- { key: 'remoteUser', label: 'REMOTE USER' },
48
- { key: 'foreignTables', label: 'TABLES' },
49
- ]);
50
- ui.table((d && d.tables) || [], [
51
- { key: 'schema', label: 'SCHEMA' },
52
- { key: 'name', label: 'TABLE' },
53
- { key: 'server', label: 'SERVER' },
54
- { key: 'columns', label: 'COLUMNS' },
55
- ]);
56
- });
57
- },
58
- },
59
- enable: {
60
- summary: 'Install a wrapper extension',
61
- usage: 'motherbase wrappers enable <postgres_fdw|file_fdw>',
62
- async run(ctx, args) {
63
- const wrapper = requireArg(args[0], 'wrapper');
64
- const data = await api.adminRequest(ctx, 'POST', `/wrappers/${enc(wrapper)}/enable`);
65
- output(ctx, data, (_d, ui) => ui.success(`Enabled ${wrapper}`));
66
- },
67
- },
68
- 'create-server': {
69
- summary: 'Create a foreign server; the password goes to the Vault',
70
- usage:
71
- 'motherbase wrappers create-server <name> --wrapper <postgres_fdw|file_fdw> [--option k=v ...] [--user <u> --password <p>]',
72
- async run(ctx, args, flags) {
73
- const name = requireArg(args[0], 'name');
74
- const body = { name, wrapper: requireArg(flags.wrapper, '--wrapper'), options: options(flags) };
75
- if (typeof flags.user === 'string') body.user = flags.user;
76
- if (typeof flags.password === 'string') body.password = flags.password;
77
- const data = await api.adminRequest(ctx, 'POST', '/wrappers/servers', { body });
78
- output(ctx, data, (_d, ui) => ui.success(`Created foreign server ${name}`));
79
- },
80
- },
81
- 'drop-server': {
82
- summary: 'Drop a foreign server',
83
- usage: 'motherbase wrappers drop-server <name>',
84
- async run(ctx, args) {
85
- const name = requireArg(args[0], 'name');
86
- const data = await api.adminRequest(ctx, 'DELETE', `/wrappers/servers/${enc(name)}`);
87
- output(ctx, data, (_d, ui) => ui.success(`Dropped foreign server ${name}`));
88
- },
89
- },
90
- credentials: {
91
- summary: 'Replace the remote credentials of a foreign server',
92
- usage: 'motherbase wrappers credentials <server> --user <u> --password <p>',
93
- async run(ctx, args, flags) {
94
- const name = requireArg(args[0], 'server');
95
- if (typeof flags.user !== 'string' || typeof flags.password !== 'string') {
96
- throw new CliError('--user and --password are required');
97
- }
98
- const body = { user: flags.user, password: flags.password };
99
- const data = await api.adminRequest(ctx, 'PUT', `/wrappers/servers/${enc(name)}/credentials`, { body });
100
- output(ctx, data, (_d, ui) => ui.success(`Updated credentials of ${name}`));
101
- },
102
- },
103
- import: {
104
- summary: 'Import a remote schema as foreign tables',
105
- usage:
106
- 'motherbase wrappers import <server> --remote-schema <s> --local-schema <s> [--tables a,b]',
107
- async run(ctx, args, flags) {
108
- const name = requireArg(args[0], 'server');
109
- const body = {
110
- remoteSchema: requireArg(flags['remote-schema'], '--remote-schema'),
111
- localSchema: requireArg(flags['local-schema'], '--local-schema'),
112
- };
113
- if (typeof flags.tables === 'string') body.tables = flags.tables.split(',').map(t => t.trim()).filter(Boolean);
114
- const data = await api.adminRequest(ctx, 'POST', `/wrappers/servers/${enc(name)}/import`, { body });
115
- output(ctx, data, (_d, ui) => ui.success(`Imported ${body.remoteSchema} into ${body.localSchema}`));
116
- },
117
- },
118
- 'create-table': {
119
- summary: 'Create one foreign table',
120
- usage:
121
- 'motherbase wrappers create-table <schema>.<table> --server <s> --column <name>:<type> ... [--option k=v ...]',
122
- async run(ctx, args, flags) {
123
- const [schema, name] = qualified(args[0]);
124
- const body = {
125
- server: requireArg(flags.server, '--server'),
126
- schema,
127
- name,
128
- columns: columns(flags),
129
- options: options(flags),
130
- };
131
- const data = await api.adminRequest(ctx, 'POST', '/wrappers/tables', { body });
132
- output(ctx, data, (_d, ui) => ui.success(`Created foreign table ${schema}.${name}`));
133
- },
134
- },
135
- 'drop-table': {
136
- summary: 'Drop a foreign table',
137
- usage: 'motherbase wrappers drop-table <schema>.<table>',
138
- async run(ctx, args) {
139
- const [schema, name] = qualified(args[0]);
140
- const data = await api.adminRequest(ctx, 'DELETE', `/wrappers/tables/${enc(schema)}/${enc(name)}`);
141
- output(ctx, data, (_d, ui) => ui.success(`Dropped foreign table ${schema}.${name}`));
142
- },
143
- },
144
- };
145
-
146
- module.exports = {
147
- summary: 'Foreign data wrappers (postgres_fdw, file_fdw)',
148
- usage: 'motherbase wrappers <list|enable|create-server|drop-server|credentials|import|create-table|drop-table> ...',
149
- subcommands,
150
- };
1
+ 'use strict';
2
+
3
+ const api = require('../api');
4
+ const { output, requireArg, parseKeyValues } = require('./_util');
5
+ const { CliError } = require('../context');
6
+
7
+ const enc = encodeURIComponent;
8
+
9
+ // `--option host=db.example.com --option port=5432`, repeatable.
10
+ function options(flags) {
11
+ return parseKeyValues([].concat(flags.option || []), { coerce: false });
12
+ }
13
+
14
+ // `schema.name` -> [schema, name]
15
+ function qualified(ref) {
16
+ const [schema, name, ...rest] = requireArg(ref, 'schema.table').split('.');
17
+ if (!schema || !name || rest.length) throw new CliError('expected <schema>.<table>');
18
+ return [schema, name];
19
+ }
20
+
21
+ // `--column id:integer --column label:text`, repeatable.
22
+ function columns(flags) {
23
+ const specs = [].concat(flags.column || []);
24
+ if (!specs.length) throw new CliError('at least one --column <name>:<type> is required');
25
+ return specs.map(spec => {
26
+ const idx = String(spec).indexOf(':');
27
+ if (idx <= 0) throw new CliError(`expected <name>:<type>, got "${spec}"`);
28
+ return { name: spec.slice(0, idx), type: spec.slice(idx + 1) };
29
+ });
30
+ }
31
+
32
+ const subcommands = {
33
+ list: {
34
+ summary: 'List wrappers, foreign servers and foreign tables',
35
+ usage: 'motherbase wrappers list',
36
+ async run(ctx) {
37
+ const data = await api.adminRequest(ctx, 'GET', '/wrappers');
38
+ output(ctx, data, (d, ui) => {
39
+ ui.table((d && d.wrappers) || [], [
40
+ { key: 'name', label: 'WRAPPER' },
41
+ { key: 'available', label: 'AVAILABLE' },
42
+ { key: 'installed', label: 'INSTALLED' },
43
+ ]);
44
+ ui.table((d && d.servers) || [], [
45
+ { key: 'name', label: 'SERVER' },
46
+ { key: 'wrapper', label: 'WRAPPER' },
47
+ { key: 'remoteUser', label: 'REMOTE USER' },
48
+ { key: 'foreignTables', label: 'TABLES' },
49
+ ]);
50
+ ui.table((d && d.tables) || [], [
51
+ { key: 'schema', label: 'SCHEMA' },
52
+ { key: 'name', label: 'TABLE' },
53
+ { key: 'server', label: 'SERVER' },
54
+ { key: 'columns', label: 'COLUMNS' },
55
+ ]);
56
+ });
57
+ },
58
+ },
59
+ enable: {
60
+ summary: 'Install a wrapper extension',
61
+ usage: 'motherbase wrappers enable <postgres_fdw|file_fdw>',
62
+ async run(ctx, args) {
63
+ const wrapper = requireArg(args[0], 'wrapper');
64
+ const data = await api.adminRequest(ctx, 'POST', `/wrappers/${enc(wrapper)}/enable`);
65
+ output(ctx, data, (_d, ui) => ui.success(`Enabled ${wrapper}`));
66
+ },
67
+ },
68
+ 'create-server': {
69
+ summary: 'Create a foreign server; the password goes to the Vault',
70
+ usage:
71
+ 'motherbase wrappers create-server <name> --wrapper <postgres_fdw|file_fdw> [--option k=v ...] [--user <u> --password <p>]',
72
+ async run(ctx, args, flags) {
73
+ const name = requireArg(args[0], 'name');
74
+ const body = { name, wrapper: requireArg(flags.wrapper, '--wrapper'), options: options(flags) };
75
+ if (typeof flags.user === 'string') body.user = flags.user;
76
+ if (typeof flags.password === 'string') body.password = flags.password;
77
+ const data = await api.adminRequest(ctx, 'POST', '/wrappers/servers', { body });
78
+ output(ctx, data, (_d, ui) => ui.success(`Created foreign server ${name}`));
79
+ },
80
+ },
81
+ 'drop-server': {
82
+ summary: 'Drop a foreign server',
83
+ usage: 'motherbase wrappers drop-server <name>',
84
+ async run(ctx, args) {
85
+ const name = requireArg(args[0], 'name');
86
+ const data = await api.adminRequest(ctx, 'DELETE', `/wrappers/servers/${enc(name)}`);
87
+ output(ctx, data, (_d, ui) => ui.success(`Dropped foreign server ${name}`));
88
+ },
89
+ },
90
+ credentials: {
91
+ summary: 'Replace the remote credentials of a foreign server',
92
+ usage: 'motherbase wrappers credentials <server> --user <u> --password <p>',
93
+ async run(ctx, args, flags) {
94
+ const name = requireArg(args[0], 'server');
95
+ if (typeof flags.user !== 'string' || typeof flags.password !== 'string') {
96
+ throw new CliError('--user and --password are required');
97
+ }
98
+ const body = { user: flags.user, password: flags.password };
99
+ const data = await api.adminRequest(ctx, 'PUT', `/wrappers/servers/${enc(name)}/credentials`, { body });
100
+ output(ctx, data, (_d, ui) => ui.success(`Updated credentials of ${name}`));
101
+ },
102
+ },
103
+ import: {
104
+ summary: 'Import a remote schema as foreign tables',
105
+ usage:
106
+ 'motherbase wrappers import <server> --remote-schema <s> --local-schema <s> [--tables a,b]',
107
+ async run(ctx, args, flags) {
108
+ const name = requireArg(args[0], 'server');
109
+ const body = {
110
+ remoteSchema: requireArg(flags['remote-schema'], '--remote-schema'),
111
+ localSchema: requireArg(flags['local-schema'], '--local-schema'),
112
+ };
113
+ if (typeof flags.tables === 'string') body.tables = flags.tables.split(',').map(t => t.trim()).filter(Boolean);
114
+ const data = await api.adminRequest(ctx, 'POST', `/wrappers/servers/${enc(name)}/import`, { body });
115
+ output(ctx, data, (_d, ui) => ui.success(`Imported ${body.remoteSchema} into ${body.localSchema}`));
116
+ },
117
+ },
118
+ 'create-table': {
119
+ summary: 'Create one foreign table',
120
+ usage:
121
+ 'motherbase wrappers create-table <schema>.<table> --server <s> --column <name>:<type> ... [--option k=v ...]',
122
+ async run(ctx, args, flags) {
123
+ const [schema, name] = qualified(args[0]);
124
+ const body = {
125
+ server: requireArg(flags.server, '--server'),
126
+ schema,
127
+ name,
128
+ columns: columns(flags),
129
+ options: options(flags),
130
+ };
131
+ const data = await api.adminRequest(ctx, 'POST', '/wrappers/tables', { body });
132
+ output(ctx, data, (_d, ui) => ui.success(`Created foreign table ${schema}.${name}`));
133
+ },
134
+ },
135
+ 'drop-table': {
136
+ summary: 'Drop a foreign table',
137
+ usage: 'motherbase wrappers drop-table <schema>.<table>',
138
+ async run(ctx, args) {
139
+ const [schema, name] = qualified(args[0]);
140
+ const data = await api.adminRequest(ctx, 'DELETE', `/wrappers/tables/${enc(schema)}/${enc(name)}`);
141
+ output(ctx, data, (_d, ui) => ui.success(`Dropped foreign table ${schema}.${name}`));
142
+ },
143
+ },
144
+ };
145
+
146
+ module.exports = {
147
+ summary: 'Foreign data wrappers (postgres_fdw, file_fdw)',
148
+ usage: 'motherbase wrappers <list|enable|create-server|drop-server|credentials|import|create-table|drop-table> ...',
149
+ subcommands,
150
+ };