@motherbase/cli 0.2.3 → 0.2.5

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 (47) hide show
  1. package/README.md +247 -134
  2. package/package.json +4 -3
  3. package/src/api.js +14 -2
  4. package/src/commands/_util.js +49 -6
  5. package/src/commands/backups.js +16 -0
  6. package/src/commands/branches.js +113 -0
  7. package/src/commands/completion.js +90 -0
  8. package/src/commands/db-catalog.js +326 -0
  9. package/src/commands/db.js +16 -5
  10. package/src/commands/domains.js +124 -0
  11. package/src/commands/functions.js +297 -274
  12. package/src/commands/gen.js +172 -126
  13. package/src/commands/import.js +23 -3
  14. package/src/commands/index.js +32 -6
  15. package/src/commands/inspect.js +142 -5
  16. package/src/commands/keys.js +72 -0
  17. package/src/commands/local.js +226 -17
  18. package/src/commands/mcp.js +61 -0
  19. package/src/commands/migrate.js +179 -0
  20. package/src/commands/migration.js +81 -6
  21. package/src/commands/network.js +83 -0
  22. package/src/commands/orgs.js +40 -0
  23. package/src/commands/postgres-config.js +113 -0
  24. package/src/commands/projects.js +8 -3
  25. package/src/commands/queues.js +133 -0
  26. package/src/commands/secrets.js +29 -8
  27. package/src/commands/signing.js +130 -0
  28. package/src/commands/snippets.js +44 -0
  29. package/src/commands/storage.js +192 -44
  30. package/src/commands/test.js +9 -0
  31. package/src/commands/users.js +30 -1
  32. package/src/commands/vault.js +82 -0
  33. package/src/commands/wrappers.js +150 -0
  34. package/src/edge-templates.js +194 -0
  35. package/src/format.js +92 -0
  36. package/src/inspect/csv.js +22 -0
  37. package/src/inspect/queries.js +562 -0
  38. package/src/main.js +41 -7
  39. package/src/mcp/platform-tools.js +233 -0
  40. package/src/mcp/protocol.js +149 -0
  41. package/src/mcp/storage-tools.js +93 -0
  42. package/src/mcp/tools.js +478 -0
  43. package/src/migration/commands.js +375 -0
  44. package/src/migration/files.js +245 -0
  45. package/src/prompt.js +1 -1
  46. package/src/storage/remote.js +62 -0
  47. package/src/commands/migrations.js +0 -85
@@ -1,14 +1,21 @@
1
1
  'use strict';
2
2
 
3
- // `migration new <name>` — local scaffold only. The real migration lifecycle
4
- // (list/apply/rollback) lives under the `migrations` group and hits the server.
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.
5
6
 
6
7
  const fs = require('fs');
7
8
  const path = require('path');
9
+ const api = require('../api');
8
10
  const ui = require('../ui');
9
- const { requireArg } = require('./_util');
11
+ const { output, requireArg } = require('./_util');
10
12
  const { CliError } = require('../context');
11
13
 
14
+ function readSql(cwd, file) {
15
+ if (!file) return undefined;
16
+ return fs.readFileSync(path.resolve(cwd, file), 'utf8');
17
+ }
18
+
12
19
  const subcommands = {
13
20
  new: {
14
21
  summary: 'Scaffold a local migration .sql file',
@@ -20,14 +27,82 @@ const subcommands = {
20
27
  fs.mkdirSync(dir, { recursive: true });
21
28
  const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '');
22
29
  const file = path.join(dir, `${stamp}_${name}.sql`);
23
- fs.writeFileSync(file, `-- Migration: ${name}\n-- Up\n\n\n-- Down (optional; keep in a separate file for \`migrations create --down\`)\n`);
30
+ fs.writeFileSync(file, `-- Migration: ${name}\n\n\n-- @down\n-- Optional: SQL that undoes the above, for \`migration down\`.\n`);
24
31
  ui.success(`Created ${path.relative(ctx.cwd, file)}`);
25
32
  },
26
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
+ },
27
98
  };
28
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
+
29
104
  module.exports = {
30
- summary: 'Scaffold local migration files',
31
- usage: 'motherbase migration new <name>',
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> ...',
32
107
  subcommands,
33
108
  };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ // Who may open a direct Postgres connection (Supabase's `network-restrictions`
4
+ // and `ssl-enforcement`). Both are project admin routes, so they work through
5
+ // the Console proxy and in direct mode alike.
6
+
7
+ const api = require('../api');
8
+ const { output } = require('./_util');
9
+ const { CliError } = require('../context');
10
+
11
+ function renderRestrictions(d, ui) {
12
+ ui.keyValue({
13
+ 'IPv4 allowed': d.dbAllowedCidrs.join(', ') || '(none)',
14
+ 'IPv6 allowed': d.dbAllowedCidrsV6.join(', ') || '(none)',
15
+ status: d.status,
16
+ exposure: d.exposure,
17
+ });
18
+ if (d.status !== 'applied') ui.warn('Stored only: this API does not manage its database access (not a Console stack).');
19
+ if (d.exposure !== 'public') ui.info('The database port is not public, so only the stack and SSH tunnels reach it today.');
20
+ }
21
+
22
+ const networkRestrictions = {
23
+ summary: 'IP ranges allowed to connect to the database directly',
24
+ usage: 'motherbase network-restrictions <get|update> ...',
25
+ subcommands: {
26
+ get: {
27
+ summary: 'Show the allowed ranges',
28
+ usage: 'motherbase network-restrictions get',
29
+ async run(ctx) {
30
+ output(ctx, await api.adminRequest(ctx, 'GET', '/network-restrictions'), renderRestrictions);
31
+ },
32
+ },
33
+ update: {
34
+ summary: 'Replace the allowed ranges (repeat --db-allow-cidr; IPv4 and IPv6 are told apart)',
35
+ usage: 'motherbase network-restrictions update --db-allow-cidr <cidr> [--db-allow-cidr <cidr> ...]',
36
+ async run(ctx, _args, flags) {
37
+ const cidrs = [].concat(flags['db-allow-cidr'] ?? []).map(String);
38
+ if (!cidrs.length) {
39
+ throw new CliError('Pass at least one --db-allow-cidr (0.0.0.0/0 and ::/0 allow everyone).');
40
+ }
41
+ const body = {
42
+ dbAllowedCidrs: cidrs.filter(c => !c.includes(':')),
43
+ dbAllowedCidrsV6: cidrs.filter(c => c.includes(':')),
44
+ };
45
+ output(ctx, await api.adminRequest(ctx, 'PUT', '/network-restrictions', { body }), renderRestrictions);
46
+ },
47
+ },
48
+ },
49
+ };
50
+
51
+ function renderSsl(d, ui) {
52
+ ui.keyValue({ enforced: d.enforced, 'TLS active': d.sslActive, status: d.status });
53
+ if (d.enforced) ui.info('External clients must connect with sslmode=require.');
54
+ }
55
+
56
+ const sslEnforcement = {
57
+ summary: 'Require TLS for direct database connections',
58
+ usage: 'motherbase ssl-enforcement <get|update> ...',
59
+ subcommands: {
60
+ get: {
61
+ summary: 'Show whether TLS is enforced',
62
+ usage: 'motherbase ssl-enforcement get',
63
+ async run(ctx) {
64
+ output(ctx, await api.adminRequest(ctx, 'GET', '/ssl-enforcement'), renderSsl);
65
+ },
66
+ },
67
+ update: {
68
+ summary: 'Turn enforcement on or off',
69
+ usage: 'motherbase ssl-enforcement update --enable-db-ssl-enforcement | --disable-db-ssl-enforcement',
70
+ async run(ctx, _args, flags) {
71
+ const enable = Boolean(flags['enable-db-ssl-enforcement']);
72
+ const disable = Boolean(flags['disable-db-ssl-enforcement']);
73
+ if (enable === disable) {
74
+ throw new CliError('Pass exactly one of --enable-db-ssl-enforcement or --disable-db-ssl-enforcement.');
75
+ }
76
+ const data = await api.adminRequest(ctx, 'PUT', '/ssl-enforcement', { body: { enforced: enable } });
77
+ output(ctx, data, renderSsl);
78
+ },
79
+ },
80
+ },
81
+ };
82
+
83
+ module.exports = { networkRestrictions, sslEnforcement };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ // Tenant organizations on the Platform Console. Projects sit under an org, as
4
+ // they do on Supabase.
5
+
6
+ const api = require('../api');
7
+ const { output, requireArg } = require('./_util');
8
+
9
+ const subcommands = {
10
+ list: {
11
+ summary: 'List the organizations you belong to',
12
+ usage: 'motherbase orgs list',
13
+ async run(ctx) {
14
+ const data = await api.consoleRequest(ctx, 'GET', '/orgs');
15
+ output(ctx, data, (rows, ui) =>
16
+ ui.table(Array.isArray(rows) ? rows : [], [
17
+ { key: 'id', label: 'ID' },
18
+ { key: 'name', label: 'NAME' },
19
+ { key: 'role', label: 'ROLE' },
20
+ { key: 'projectCount', label: 'PROJECTS' },
21
+ ]),
22
+ );
23
+ },
24
+ },
25
+ create: {
26
+ summary: 'Create an organization',
27
+ usage: 'motherbase orgs create <name>',
28
+ async run(ctx, args) {
29
+ const name = requireArg(args[0], 'name');
30
+ const data = await api.consoleRequest(ctx, 'POST', '/orgs', { body: { name } });
31
+ output(ctx, data, (d, ui) => ui.success(`Created org ${d.name} (${d.id})`));
32
+ },
33
+ },
34
+ };
35
+
36
+ module.exports = {
37
+ summary: 'Manage organizations on the Platform Console',
38
+ usage: 'motherbase orgs <list|create> ...',
39
+ subcommands,
40
+ };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ // `motherbase postgres-config get|update|delete`, Supabase's command of the
4
+ // same name. The one difference is where a value lands: the server picks the
5
+ // narrowest scope Postgres allows (a per-database override for
6
+ // `statement_timeout`, ALTER SYSTEM for `shared_buffers`), and `--scope`
7
+ // overrides that. Motherbase never restarts Postgres itself, so `--no-restart`
8
+ // is accepted for script compatibility and a restart-only change is reported.
9
+
10
+ const api = require('../api');
11
+ const { output, parseKeyValues } = require('./_util');
12
+ const { CliError } = require('../context');
13
+
14
+ const SCOPES = ['database', 'role', 'system'];
15
+
16
+ function scopeFlags(flags) {
17
+ if (flags.scope !== undefined && !SCOPES.includes(flags.scope)) {
18
+ throw new CliError(`--scope must be one of: ${SCOPES.join(', ')}`);
19
+ }
20
+ if (flags.scope === 'role' && !flags.role) throw new CliError('--scope role needs --role <name>');
21
+ return {
22
+ ...(flags.scope ? { scope: flags.scope } : {}),
23
+ ...(flags.role ? { role: String(flags.role) } : {}),
24
+ };
25
+ }
26
+
27
+ // What `get` prints: one row per setting that has an override, like Supabase,
28
+ // or every curated setting with --all.
29
+ function overrideRows(config, all) {
30
+ return config.settings.flatMap(setting => {
31
+ if (!setting.overrides.length) {
32
+ return all ? [{ name: setting.name, value: setting.value, scope: '', role: '', restart: setting.context === 'postmaster' }] : [];
33
+ }
34
+ return setting.overrides.map(o => ({
35
+ name: setting.name,
36
+ value: o.value,
37
+ scope: o.scope,
38
+ role: o.role || '',
39
+ restart: setting.context === 'postmaster',
40
+ }));
41
+ });
42
+ }
43
+
44
+ function reportChange(result, ui) {
45
+ for (const change of result.applied) {
46
+ ui.success(`${change.name}: ${change.scope}${change.role ? ` (${change.role})` : ''}`);
47
+ }
48
+ if (result.restartRequired.length) {
49
+ ui.warn(`Restart Postgres to apply: ${result.restartRequired.join(', ')}`);
50
+ }
51
+ }
52
+
53
+ const subcommands = {
54
+ get: {
55
+ summary: 'Show Postgres config overrides (--all for every managed setting)',
56
+ usage: 'motherbase postgres-config get [--all]',
57
+ async run(ctx, args, flags) {
58
+ const data = await api.adminRequest(ctx, 'GET', '/database/config');
59
+ output(ctx, data, (d, ui) => {
60
+ ui.table(overrideRows(d, Boolean(flags.all)), [
61
+ { key: 'name', label: 'PARAMETER' },
62
+ { key: 'value', label: 'VALUE' },
63
+ { key: 'scope', label: 'SCOPE' },
64
+ { key: 'role', label: 'ROLE' },
65
+ { key: 'restart', label: 'NEEDS RESTART' },
66
+ ]);
67
+ if (!d.systemOverridesReadable) ui.warn('System-level overrides could not be read with this connection.');
68
+ });
69
+ },
70
+ },
71
+ update: {
72
+ summary: 'Set Postgres config values',
73
+ usage: 'motherbase postgres-config update --config key=value [--config ...] [--scope database|role|system] [--role r] [--replace-existing-overrides]',
74
+ async run(ctx, args, flags) {
75
+ const values = parseKeyValues([].concat(flags.config || []).filter(Boolean), { coerce: false });
76
+ if (!Object.keys(values).length) throw new CliError('Provide at least one --config key=value.');
77
+ const target = scopeFlags(flags);
78
+ const changes = Object.entries(values).map(([name, value]) => ({ name, value, ...target }));
79
+
80
+ // Supabase's flag: anything overridden before and not named now goes back to its default.
81
+ if (flags['replace-existing-overrides']) {
82
+ const current = await api.adminRequest(ctx, 'GET', '/database/config');
83
+ for (const setting of current.settings) {
84
+ if (setting.name in values) continue;
85
+ for (const o of setting.overrides) {
86
+ changes.push({ name: setting.name, value: null, scope: o.scope, ...(o.role ? { role: o.role } : {}) });
87
+ }
88
+ }
89
+ }
90
+
91
+ const data = await api.adminRequest(ctx, 'PATCH', '/database/config', { body: { changes } });
92
+ output(ctx, data, (d, ui) => reportChange(d, ui));
93
+ },
94
+ },
95
+ delete: {
96
+ summary: 'Remove Postgres config overrides, restoring the defaults',
97
+ usage: 'motherbase postgres-config delete --config key1,key2 [--scope database|role|system] [--role r]',
98
+ async run(ctx, args, flags) {
99
+ const names = [].concat(flags.config || []).flatMap(v => String(v).split(',')).map(s => s.trim()).filter(Boolean);
100
+ if (!names.length) throw new CliError('Provide the settings to reset with --config key1,key2.');
101
+ const target = scopeFlags(flags);
102
+ const changes = names.map(name => ({ name, value: null, ...target }));
103
+ const data = await api.adminRequest(ctx, 'PATCH', '/database/config', { body: { changes } });
104
+ output(ctx, data, (d, ui) => reportChange(d, ui));
105
+ },
106
+ },
107
+ };
108
+
109
+ module.exports = {
110
+ summary: 'Postgres configuration (statement_timeout, work_mem, max_connections, ...)',
111
+ usage: 'motherbase postgres-config <get|update|delete> ...',
112
+ subcommands,
113
+ };
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const api = require('../api');
4
- const { output, requireArg } = require('./_util');
4
+ const keys = require('./keys');
5
+ const { output, alias, requireArg } = require('./_util');
5
6
  const { CliError } = require('../context');
7
+ const { outputFormat } = require('../format');
6
8
 
7
9
  const LIFECYCLE = ['start', 'stop', 'restart', 'update', 'rebuild'];
8
10
 
@@ -68,7 +70,7 @@ const subcommands = {
68
70
  if (d && d.stdout) ui.print(d.stdout);
69
71
  if (d && d.stderr) ui.printErr(d.stderr);
70
72
  });
71
- if (flags.f && !ctx.flags.json) {
73
+ if (flags.f && outputFormat(ctx.flags) === 'pretty') {
72
74
  // The console logs endpoint returns a snapshot; approximate -f by polling.
73
75
  const ui = require('../ui');
74
76
  ui.warn('(-f) polling every 3s; Ctrl-C to stop');
@@ -118,8 +120,11 @@ const subcommands = {
118
120
 
119
121
  for (const action of LIFECYCLE) subcommands[action] = lifecycleCmd(action);
120
122
 
123
+ // Supabase's `projects api-keys`; the keys themselves live under `keys`.
124
+ subcommands['api-keys'] = alias(keys.subcommands.list, 'motherbase projects api-keys [--project-ref <id>]');
125
+
121
126
  module.exports = {
122
127
  summary: 'Manage projects on the Platform Console',
123
- usage: 'motherbase projects <list|create|start|stop|restart|update|rebuild|delete|logs|info|limits> ...',
128
+ usage: 'motherbase projects <list|create|start|stop|restart|update|rebuild|delete|logs|info|limits|api-keys> ...',
124
129
  subcommands,
125
130
  };
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const api = require('../api');
4
+ const { output, requireArg } = require('./_util');
5
+ const { CliError } = require('../context');
6
+
7
+ const enc = encodeURIComponent;
8
+
9
+ function int(flag, name) {
10
+ if (flag === undefined) return undefined;
11
+ const n = Number(flag);
12
+ if (!Number.isInteger(n) || n < 0) throw new CliError(`--${name} must be a non-negative integer`);
13
+ return n;
14
+ }
15
+
16
+ function parseMessage(raw) {
17
+ try {
18
+ return JSON.parse(requireArg(raw, 'json'));
19
+ } catch (err) {
20
+ if (err instanceof CliError) throw err;
21
+ throw new CliError('the message must be valid JSON, e.g. \'{"task":"resize"}\'');
22
+ }
23
+ }
24
+
25
+ const subcommands = {
26
+ list: {
27
+ summary: 'List queues with their depth',
28
+ usage: 'motherbase queues list',
29
+ async run(ctx) {
30
+ const data = await api.adminRequest(ctx, 'GET', '/queues');
31
+ const queues = (data && data.queues) || [];
32
+ output(ctx, data, (_d, ui) =>
33
+ ui.table(queues, [
34
+ { key: 'name', label: 'NAME' },
35
+ { key: 'type', label: 'TYPE' },
36
+ { key: 'length', label: 'MESSAGES' },
37
+ { key: 'visibleLength', label: 'VISIBLE' },
38
+ { key: 'totalMessages', label: 'TOTAL' },
39
+ ]),
40
+ );
41
+ },
42
+ },
43
+ enable: {
44
+ summary: 'Install pgmq for this project',
45
+ usage: 'motherbase queues enable',
46
+ async run(ctx) {
47
+ const data = await api.adminRequest(ctx, 'POST', '/queues/enable');
48
+ output(ctx, data, (_d, ui) => ui.success('Queues enabled'));
49
+ },
50
+ },
51
+ expose: {
52
+ summary: 'Expose queues to the client SDK through pgmq_public (or hide them)',
53
+ usage: 'motherbase queues expose <true|false>',
54
+ async run(ctx, args) {
55
+ const value = requireArg(args[0], 'true|false');
56
+ if (value !== 'true' && value !== 'false') throw new CliError('expected true or false');
57
+ const data = await api.adminRequest(ctx, 'PUT', '/queues/expose', { body: { enabled: value === 'true' } });
58
+ output(ctx, data, (_d, ui) => ui.success(`Queues ${value === 'true' ? 'exposed' : 'hidden'}`));
59
+ },
60
+ },
61
+ create: {
62
+ summary: 'Create a queue',
63
+ usage: 'motherbase queues create <name> [--type basic|unlogged|partitioned] [--rls true|false]',
64
+ async run(ctx, args, flags) {
65
+ const name = requireArg(args[0], 'name');
66
+ const body = { name };
67
+ if (flags.type) body.type = flags.type;
68
+ if (flags.rls !== undefined) body.rls = flags.rls === true || flags.rls === 'true';
69
+ const data = await api.adminRequest(ctx, 'POST', '/queues', { body });
70
+ output(ctx, data, (_d, ui) => ui.success(`Created queue ${name}`));
71
+ },
72
+ },
73
+ drop: {
74
+ summary: 'Drop a queue and its messages',
75
+ usage: 'motherbase queues drop <name>',
76
+ async run(ctx, args) {
77
+ const name = requireArg(args[0], 'name');
78
+ const data = await api.adminRequest(ctx, 'DELETE', `/queues/${enc(name)}`);
79
+ output(ctx, data, (_d, ui) => ui.success(`Dropped queue ${name}`));
80
+ },
81
+ },
82
+ send: {
83
+ summary: 'Send a JSON message',
84
+ usage: "motherbase queues send <name> '<json>' [--delay seconds]",
85
+ async run(ctx, args, flags) {
86
+ const name = requireArg(args[0], 'name');
87
+ const body = { message: parseMessage(args[1]) };
88
+ const delay = int(flags.delay, 'delay');
89
+ if (delay !== undefined) body.delay = delay;
90
+ const data = await api.adminRequest(ctx, 'POST', `/queues/${enc(name)}/messages`, { body });
91
+ output(ctx, data, (d, ui) => ui.success(`Sent message ${((d && d.msgIds) || []).join(', ')}`));
92
+ },
93
+ },
94
+ read: {
95
+ summary: 'Read messages, hiding them from other readers for the visibility timeout',
96
+ usage: 'motherbase queues read <name> [--qty 1] [--vt 30]',
97
+ async run(ctx, args, flags) {
98
+ const name = requireArg(args[0], 'name');
99
+ const body = {};
100
+ const qty = int(flags.qty, 'qty');
101
+ const vt = int(flags.vt, 'vt');
102
+ if (qty !== undefined) body.qty = qty;
103
+ if (vt !== undefined) body.vt = vt;
104
+ const data = await api.adminRequest(ctx, 'POST', `/queues/${enc(name)}/read`, { body });
105
+ output(ctx, data, (d, ui) => ui.json((d && d.messages) || []));
106
+ },
107
+ },
108
+ archive: {
109
+ summary: 'Move a message to the archive',
110
+ usage: 'motherbase queues archive <name> <msg-id>',
111
+ async run(ctx, args) {
112
+ const name = requireArg(args[0], 'name');
113
+ const id = requireArg(args[1], 'msg-id');
114
+ const data = await api.adminRequest(ctx, 'POST', `/queues/${enc(name)}/messages/${enc(id)}/archive`);
115
+ output(ctx, data, (_d, ui) => ui.success(`Archived message ${id}`));
116
+ },
117
+ },
118
+ purge: {
119
+ summary: 'Delete every message in a queue',
120
+ usage: 'motherbase queues purge <name>',
121
+ async run(ctx, args) {
122
+ const name = requireArg(args[0], 'name');
123
+ const data = await api.adminRequest(ctx, 'POST', `/queues/${enc(name)}/purge`);
124
+ output(ctx, data, (d, ui) => ui.success(`Purged ${(d && d.purged) ?? 0} messages from ${name}`));
125
+ },
126
+ },
127
+ };
128
+
129
+ module.exports = {
130
+ summary: 'Message queues (pgmq)',
131
+ usage: 'motherbase queues <list|enable|expose|create|drop|send|read|archive|purge> ...',
132
+ subcommands,
133
+ };
@@ -1,7 +1,24 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
4
+ const path = require('path');
3
5
  const api = require('../api');
4
- const { output, requireArg } = require('./_util');
6
+ const { output, requireArg, parseKeyValues, parseDotenv } = require('./_util');
7
+ const { CliError } = require('../context');
8
+
9
+ // `NAME=VALUE ...` plus `--env-file`, as the Supabase CLI takes them. The older
10
+ // `set NAME value` form keeps working. Positional pairs win over the file.
11
+ // Node itself pre-scans argv for `--env-file` and exits (code 9) when the path
12
+ // does not exist relative to the shell's cwd, before this code runs; with
13
+ // `--workdir`, pass an absolute path.
14
+ function collectSecrets(cwd, args, flags) {
15
+ const fromFile = flags['env-file']
16
+ ? parseDotenv(fs.readFileSync(path.resolve(cwd, String(flags['env-file'])), 'utf8'))
17
+ : {};
18
+ const legacy = args.length === 2 && !args[0].includes('=');
19
+ const fromArgs = legacy ? { [args[0]]: args[1] } : parseKeyValues(args, { coerce: false });
20
+ return { ...fromFile, ...fromArgs };
21
+ }
5
22
 
6
23
  const subcommands = {
7
24
  list: {
@@ -18,13 +35,17 @@ const subcommands = {
18
35
  },
19
36
  },
20
37
  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}`));
38
+ summary: 'Set one or more secrets',
39
+ usage: 'motherbase secrets set NAME=VALUE [NAME=VALUE ...] [--env-file .env]',
40
+ async run(ctx, args, flags) {
41
+ const secrets = collectSecrets(ctx.cwd, args, flags);
42
+ const names = Object.keys(secrets);
43
+ if (!names.length) throw new CliError('Nothing to set. Pass NAME=VALUE pairs or --env-file <path>.');
44
+ // The server sets one secret per request.
45
+ for (const name of names) {
46
+ await api.adminRequest(ctx, 'PUT', '/edge-secrets', { body: { name, value: secrets[name] } });
47
+ }
48
+ output(ctx, { set: names }, (d, ui) => ui.success(`Set ${names.length === 1 ? 'secret' : 'secrets'} ${names.join(', ')}`));
28
49
  },
29
50
  },
30
51
  unset: {