@motherbase/cli 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -18
- package/bin/motherbase.js +10 -10
- package/package.json +35 -26
- package/src/api.js +233 -229
- package/src/commands/_util.js +106 -50
- package/src/commands/backups.js +16 -0
- package/src/commands/branches.js +113 -0
- package/src/commands/completion.js +90 -0
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db-catalog.js +326 -0
- package/src/commands/db.js +103 -92
- package/src/commands/domains.js +124 -0
- package/src/commands/functions.js +63 -33
- package/src/commands/gen.js +63 -17
- package/src/commands/import.js +55 -35
- package/src/commands/index.js +85 -64
- package/src/commands/inspect.js +178 -41
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +23 -2
- package/src/commands/login.js +39 -39
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +27 -9
- package/src/commands/migrate.js +207 -0
- package/src/commands/migration.js +108 -33
- package/src/commands/network.js +83 -0
- package/src/commands/orgs.js +40 -0
- package/src/commands/postgres-config.js +113 -0
- package/src/commands/projects.js +130 -125
- package/src/commands/queues.js +133 -0
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -45
- package/src/commands/signing.js +130 -0
- package/src/commands/snippets.js +44 -0
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -113
- package/src/commands/test.js +9 -0
- package/src/commands/unlink.js +15 -15
- package/src/commands/users.js +142 -113
- package/src/commands/vault.js +82 -0
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -0
- package/src/config.js +98 -98
- package/src/context.js +106 -102
- package/src/docs.js +68 -0
- package/src/edge-templates.js +194 -0
- package/src/format.js +92 -0
- package/src/inspect/csv.js +22 -0
- package/src/inspect/queries.js +562 -0
- package/src/main.js +224 -160
- package/src/mcp/platform-tools.js +233 -0
- package/src/mcp/protocol.js +4 -1
- package/src/mcp/storage-tools.js +93 -0
- package/src/mcp/tools.js +318 -6
- package/src/migration/commands.js +375 -0
- package/src/migration/files.js +245 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -0
- package/src/ui.js +111 -111
- package/src/commands/migrations.js +0 -85
package/src/commands/inspect.js
CHANGED
|
@@ -1,41 +1,178 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const api = require('../api');
|
|
6
|
+
const { output } = require('./_util');
|
|
7
|
+
const { CliError } = require('../context');
|
|
8
|
+
const { QUERIES, DEPRECATED, REPORT, resolveQuery } = require('../inspect/queries');
|
|
9
|
+
const { toCsv } = require('../inspect/csv');
|
|
10
|
+
|
|
11
|
+
function simple(apiPath, summary, render) {
|
|
12
|
+
return {
|
|
13
|
+
summary,
|
|
14
|
+
// usage is filled in from the subcommand key below: deriving it from the
|
|
15
|
+
// summary printed `inspect security` for `shield` and `inspect request` for
|
|
16
|
+
// `requests`, neither of which is a command.
|
|
17
|
+
usage: '',
|
|
18
|
+
async run(ctx) {
|
|
19
|
+
const data = await api.adminRequest(ctx, 'GET', apiPath);
|
|
20
|
+
output(ctx, data, render || ((d, ui) => ui.json(d)));
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Postgres' two ways of saying pg_stat_statements is not usable here: the
|
|
26
|
+
// extension was never created in this database, or the library is not preloaded.
|
|
27
|
+
const STAT_STATEMENTS_MISSING = /pg_stat_statements.*(does not exist|must be loaded)/i;
|
|
28
|
+
|
|
29
|
+
class StatStatementsMissing extends CliError {
|
|
30
|
+
constructor() {
|
|
31
|
+
super(
|
|
32
|
+
'pg_stat_statements is not enabled in this database. Enable it from Studio (Database > Extensions) '
|
|
33
|
+
+ 'or run: motherbase db sql -e "create extension pg_stat_statements"',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Rows of one inspect query, run on the read-only SQL endpoint. */
|
|
39
|
+
async function runQuery(ctx, query) {
|
|
40
|
+
try {
|
|
41
|
+
const data = await api.adminRequest(ctx, 'POST', '/sql/readonly', { body: { sql: query.sql } });
|
|
42
|
+
return (data && data.rows) || [];
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (query.needsStatStatements && STAT_STATEMENTS_MISSING.test(err.message)) {
|
|
45
|
+
throw new StatStatementsMissing();
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Statements span lines and `to_char` pads numbers; a table cell wants neither.
|
|
52
|
+
// A statement is also cut short, or one query pushes every other column off
|
|
53
|
+
// screen; -o json keeps it whole.
|
|
54
|
+
const CELL_WIDTH = 60;
|
|
55
|
+
function cell(value) {
|
|
56
|
+
if (typeof value !== 'string') return value;
|
|
57
|
+
const text = value.replace(/\s+/g, ' ').trim();
|
|
58
|
+
return text.length > CELL_WIDTH ? `${text.slice(0, CELL_WIDTH - 1)}…` : text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function dbCommand(name) {
|
|
62
|
+
const { name: canonical, query } = resolveQuery(name);
|
|
63
|
+
const replaced = canonical !== name;
|
|
64
|
+
return {
|
|
65
|
+
summary: replaced ? `Deprecated: use "${canonical}" instead` : query.summary,
|
|
66
|
+
usage: `motherbase inspect db ${name}`,
|
|
67
|
+
async run(ctx) {
|
|
68
|
+
const rows = await runQuery(ctx, query);
|
|
69
|
+
output(ctx, { query: canonical, rows }, (_d, ui) => {
|
|
70
|
+
if (replaced) ui.warn(`"inspect db ${name}" is deprecated: showing "${canonical}".`);
|
|
71
|
+
const display = rows.map(row => Object.fromEntries(Object.entries(row).map(([k, v]) => [k, cell(v)])));
|
|
72
|
+
ui.table(display, query.columns);
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const db = {
|
|
79
|
+
summary: 'Inspect the database: bloat, locks, slow queries, index and table stats',
|
|
80
|
+
usage: `motherbase inspect db <${Object.keys(QUERIES).join('|')}>`,
|
|
81
|
+
subcommands: Object.fromEntries(
|
|
82
|
+
[...Object.keys(QUERIES), ...Object.keys(DEPRECATED)].map(name => [name, dbCommand(name)]),
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Local-time YYYY-MM-DD: the report's folder, one per day as upstream does.
|
|
87
|
+
function dateFolder(now = new Date()) {
|
|
88
|
+
const pad = n => String(n).padStart(2, '0');
|
|
89
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const report = {
|
|
93
|
+
summary: 'Write every inspect query to a CSV in a dated folder',
|
|
94
|
+
usage: 'motherbase inspect report [--output-dir <dir>]',
|
|
95
|
+
async run(ctx, _args, flags) {
|
|
96
|
+
const dir = path.resolve(ctx.cwd, String(flags['output-dir'] || '.'), dateFolder());
|
|
97
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const files = [];
|
|
100
|
+
const skipped = [];
|
|
101
|
+
// One at a time: fourteen catalog scans at once is a load spike on the
|
|
102
|
+
// database being diagnosed.
|
|
103
|
+
for (const { file, name, query } of REPORT) {
|
|
104
|
+
let rows;
|
|
105
|
+
try {
|
|
106
|
+
rows = await runQuery(ctx, query);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// A missing pg_stat_statements costs two files, not the whole report.
|
|
109
|
+
if (!(err instanceof StatStatementsMissing)) throw err;
|
|
110
|
+
skipped.push(name);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const target = path.join(dir, `${file}.csv`);
|
|
114
|
+
fs.writeFileSync(target, toCsv(query.columns, rows));
|
|
115
|
+
files.push(target);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
output(ctx, { outputDir: dir, files, skipped }, (d, ui) => {
|
|
119
|
+
if (d.skipped.length) ui.warn(`Skipped ${d.skipped.join(', ')}: ${new StatStatementsMissing().message}`);
|
|
120
|
+
ui.success(`Saved ${d.files.length} CSV files to ${path.relative(ctx.cwd, d.outputDir) || '.'}`);
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const subcommands = {
|
|
126
|
+
db,
|
|
127
|
+
report,
|
|
128
|
+
logs: {
|
|
129
|
+
summary: 'Recent server logs',
|
|
130
|
+
usage: 'motherbase inspect logs [--limit n] [--source api|rest|postgres|pooler|auth|storage|realtime|edge-function|cron|platform]',
|
|
131
|
+
async run(ctx, args, flags) {
|
|
132
|
+
// A Logs Explorer "source" is the server's `service` filter.
|
|
133
|
+
const query = flags.limit || flags.source ? { limit: flags.limit, service: flags.source } : undefined;
|
|
134
|
+
const data = await api.adminRequest(ctx, 'GET', '/logs', { query });
|
|
135
|
+
const logs = (data && data.logs) || [];
|
|
136
|
+
output(ctx, data, (_d, ui) =>
|
|
137
|
+
logs.forEach(l => ui.print(typeof l === 'string' ? l : `${l.level || ''} ${l.source || ''} ${l.message || JSON.stringify(l)}`)),
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
'index-advisor': {
|
|
142
|
+
summary: 'Suggest indexes for a statement (hypopg + index_advisor)',
|
|
143
|
+
usage: 'motherbase inspect index-advisor --sql "<select ...>" | motherbase inspect index-advisor --enable',
|
|
144
|
+
async run(ctx, args, flags) {
|
|
145
|
+
if (flags.enable) {
|
|
146
|
+
const status = await api.adminRequest(ctx, 'POST', '/performance/index-advisor/enable');
|
|
147
|
+
output(ctx, status, (_d, ui) => ui.success('Index advisor enabled'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const query = flags.sql || flags.e;
|
|
151
|
+
if (typeof query !== 'string') throw new CliError('Provide the statement with --sql "..." (or --enable to install the advisor).');
|
|
152
|
+
const data = await api.adminRequest(ctx, 'POST', '/performance/index-advisor', { body: { query } });
|
|
153
|
+
output(ctx, data, (d, ui) => {
|
|
154
|
+
const statements = (d && d.indexStatements) || [];
|
|
155
|
+
if (!statements.length) ui.print('No index would help this statement.');
|
|
156
|
+
statements.forEach(s => ui.print(s));
|
|
157
|
+
if (d && d.improvementPct != null) ui.print(`Estimated cost drop: ${d.improvementPct}%`);
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
stats: simple('/stats', 'Stats overview'),
|
|
162
|
+
performance: simple('/performance', 'Performance metrics'),
|
|
163
|
+
requests: simple('/metrics/requests', 'Request metrics'),
|
|
164
|
+
advisor: simple('/advisor', 'Advisor findings'),
|
|
165
|
+
shield: simple('/shield', 'Security shield report'),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
for (const [name, def] of Object.entries(subcommands)) {
|
|
169
|
+
if (!def.usage) def.usage = `motherbase inspect ${name}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
summary: 'Diagnostics: database inspection, report, logs, stats, index-advisor, advisor, shield',
|
|
174
|
+
usage: 'motherbase inspect <db|report|logs|stats|performance|index-advisor|requests|advisor|shield>',
|
|
175
|
+
subcommands,
|
|
176
|
+
dateFolder,
|
|
177
|
+
cell,
|
|
178
|
+
};
|
package/src/commands/link.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const api = require('../api');
|
|
4
|
-
const config = require('../config');
|
|
5
|
-
const prompt = require('../prompt');
|
|
6
|
-
const ui = require('../ui');
|
|
7
|
-
const { CliError } = require('../context');
|
|
8
|
-
|
|
9
|
-
module.exports = {
|
|
10
|
-
summary: 'Link the current directory to a project (writes ./.motherbase/config.json)',
|
|
11
|
-
usage: 'motherbase link [projectNameOrId]',
|
|
12
|
-
async run(ctx, args, flags) {
|
|
13
|
-
const { consoleUrl } = ctx.requireConsole();
|
|
14
|
-
const projects = await api.consoleRequest(ctx, 'GET', '/projects');
|
|
15
|
-
const list = Array.isArray(projects) ? projects : [];
|
|
16
|
-
if (list.length === 0) throw new CliError('No projects available on this console.');
|
|
17
|
-
|
|
18
|
-
let picked = args[0];
|
|
19
|
-
if (!picked) {
|
|
20
|
-
if (flags.yes) throw new CliError('Specify a project name/id when using --yes.');
|
|
21
|
-
ui.print(ui.color.bold('Available projects:'));
|
|
22
|
-
list.forEach((p, i) => ui.print(` ${i + 1}. ${p.name || p.id}`));
|
|
23
|
-
const answer = await prompt.question('Pick a project (number or name): ');
|
|
24
|
-
const asNum = Number(answer);
|
|
25
|
-
picked = Number.isInteger(asNum) && list[asNum - 1] ? (list[asNum - 1].name || list[asNum - 1].id) : answer;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const match = list.find(p => p.name === picked || p.id === picked);
|
|
29
|
-
if (!match) throw new CliError(`Project "${picked}" not found on ${consoleUrl}.`);
|
|
30
|
-
|
|
31
|
-
const projectId = match.name || match.id;
|
|
32
|
-
config.writeLink(ctx.cwd, { consoleUrl, projectId, projectName: match.name || projectId });
|
|
33
|
-
ui.success(`Linked ${ctx.cwd} → ${projectId} (${consoleUrl})`);
|
|
34
|
-
},
|
|
35
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const prompt = require('../prompt');
|
|
6
|
+
const ui = require('../ui');
|
|
7
|
+
const { CliError } = require('../context');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
summary: 'Link the current directory to a project (writes ./.motherbase/config.json)',
|
|
11
|
+
usage: 'motherbase link [projectNameOrId]',
|
|
12
|
+
async run(ctx, args, flags) {
|
|
13
|
+
const { consoleUrl } = ctx.requireConsole();
|
|
14
|
+
const projects = await api.consoleRequest(ctx, 'GET', '/projects');
|
|
15
|
+
const list = Array.isArray(projects) ? projects : [];
|
|
16
|
+
if (list.length === 0) throw new CliError('No projects available on this console.');
|
|
17
|
+
|
|
18
|
+
let picked = args[0];
|
|
19
|
+
if (!picked) {
|
|
20
|
+
if (flags.yes) throw new CliError('Specify a project name/id when using --yes.');
|
|
21
|
+
ui.print(ui.color.bold('Available projects:'));
|
|
22
|
+
list.forEach((p, i) => ui.print(` ${i + 1}. ${p.name || p.id}`));
|
|
23
|
+
const answer = await prompt.question('Pick a project (number or name): ');
|
|
24
|
+
const asNum = Number(answer);
|
|
25
|
+
picked = Number.isInteger(asNum) && list[asNum - 1] ? (list[asNum - 1].name || list[asNum - 1].id) : answer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const match = list.find(p => p.name === picked || p.id === picked);
|
|
29
|
+
if (!match) throw new CliError(`Project "${picked}" not found on ${consoleUrl}.`);
|
|
30
|
+
|
|
31
|
+
const projectId = match.name || match.id;
|
|
32
|
+
config.writeLink(ctx.cwd, { consoleUrl, projectId, projectName: match.name || projectId });
|
|
33
|
+
ui.success(`Linked ${ctx.cwd} → ${projectId} (${consoleUrl})`);
|
|
34
|
+
},
|
|
35
|
+
};
|
package/src/commands/local.js
CHANGED
|
@@ -138,7 +138,7 @@ services:
|
|
|
138
138
|
|
|
139
139
|
# One-shot: a private bucket and a separate anonymously-downloadable one.
|
|
140
140
|
createbuckets:
|
|
141
|
-
image: minio/mc:RELEASE.2025-08-13T08-35-41Z
|
|
141
|
+
image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z
|
|
142
142
|
depends_on:
|
|
143
143
|
minio:
|
|
144
144
|
condition: service_healthy
|
|
@@ -183,10 +183,31 @@ volumes:
|
|
|
183
183
|
`;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// Values that end up unquoted in `.env` and in the compose `context:` line. A
|
|
187
|
+
// newline would inject a second variable or key; the character sets below are
|
|
188
|
+
// what these fields legitimately contain.
|
|
189
|
+
const INIT_FLAG_PATTERNS = {
|
|
190
|
+
'admin-email': /^[^\s"'`$#\\]+$/,
|
|
191
|
+
'admin-password': /^[^\s"'`$#\\]+$/,
|
|
192
|
+
repo: /^[\w.:/@~+-]+$/,
|
|
193
|
+
ref: /^[\w./-]+$/,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function validateInitFlags(flags) {
|
|
197
|
+
for (const [name, pattern] of Object.entries(INIT_FLAG_PATTERNS)) {
|
|
198
|
+
const value = flags[name];
|
|
199
|
+
if (value === undefined) continue;
|
|
200
|
+
if (typeof value !== 'string' || !pattern.test(value)) {
|
|
201
|
+
throw new CliError(`Invalid --${name}: contains whitespace, quotes or other unsafe characters`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
186
206
|
const init = {
|
|
187
207
|
summary: 'Scaffold a runnable local stack (compose file, .env, generated secrets)',
|
|
188
208
|
usage: 'motherbase init [dir] [--port 5001] [--repo URL] [--ref BRANCH] [--force]',
|
|
189
209
|
async run(ctx, args, flags = {}) {
|
|
210
|
+
validateInitFlags(flags);
|
|
190
211
|
const dir = args[0] || '.';
|
|
191
212
|
const target = path.resolve(ctx.cwd, dir);
|
|
192
213
|
fs.mkdirSync(target, { recursive: true });
|
|
@@ -270,4 +291,4 @@ const logs = {
|
|
|
270
291
|
},
|
|
271
292
|
};
|
|
272
293
|
|
|
273
|
-
module.exports = { init, start, stop, deploy, logs, renderEnv, renderCompose };
|
|
294
|
+
module.exports = { init, start, stop, deploy, logs, renderEnv, renderCompose, validateInitFlags };
|
package/src/commands/login.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const api = require('../api');
|
|
4
|
-
const config = require('../config');
|
|
5
|
-
const prompt = require('../prompt');
|
|
6
|
-
const ui = require('../ui');
|
|
7
|
-
const { CliError } = require('../context');
|
|
8
|
-
|
|
9
|
-
module.exports = {
|
|
10
|
-
summary: 'Log in to a Motherbase Console (email/password → session token)',
|
|
11
|
-
usage: 'motherbase login [--url <consoleUrl>] [--email <email>]',
|
|
12
|
-
async run(ctx, args, flags) {
|
|
13
|
-
const consoleUrl = config.normalizeUrl(
|
|
14
|
-
flags.url || ctx.env.MOTHERBASE_CONSOLE_URL || ctx.consoleUrl || '',
|
|
15
|
-
);
|
|
16
|
-
if (!consoleUrl) {
|
|
17
|
-
throw new CliError('Console URL required. Use --url <consoleUrl> or set MOTHERBASE_CONSOLE_URL.');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Non-interactive: accept a token directly (CI).
|
|
21
|
-
if (flags.token) {
|
|
22
|
-
const email = flags.email || 'token';
|
|
23
|
-
config.saveConsoleToken(consoleUrl, flags.token, email);
|
|
24
|
-
ui.success(`Stored token for ${consoleUrl}`);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const email = flags.email || (await prompt.question('Email: '));
|
|
29
|
-
if (!email) throw new CliError('email is required');
|
|
30
|
-
const password = flags.password || (await prompt.password('Password: '));
|
|
31
|
-
if (!password) throw new CliError('password is required');
|
|
32
|
-
|
|
33
|
-
const data = await api.loginRequest(consoleUrl, email, password);
|
|
34
|
-
const token = data && data.token;
|
|
35
|
-
if (!token) throw new CliError('login succeeded but no token was returned');
|
|
36
|
-
config.saveConsoleToken(consoleUrl, token, email);
|
|
37
|
-
ui.success(`Logged in to ${consoleUrl} as ${email}`);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const prompt = require('../prompt');
|
|
6
|
+
const ui = require('../ui');
|
|
7
|
+
const { CliError } = require('../context');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
summary: 'Log in to a Motherbase Console (email/password → session token)',
|
|
11
|
+
usage: 'motherbase login [--url <consoleUrl>] [--email <email>]',
|
|
12
|
+
async run(ctx, args, flags) {
|
|
13
|
+
const consoleUrl = config.normalizeUrl(
|
|
14
|
+
flags.url || ctx.env.MOTHERBASE_CONSOLE_URL || ctx.consoleUrl || '',
|
|
15
|
+
);
|
|
16
|
+
if (!consoleUrl) {
|
|
17
|
+
throw new CliError('Console URL required. Use --url <consoleUrl> or set MOTHERBASE_CONSOLE_URL.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Non-interactive: accept a token directly (CI).
|
|
21
|
+
if (flags.token) {
|
|
22
|
+
const email = flags.email || 'token';
|
|
23
|
+
config.saveConsoleToken(consoleUrl, flags.token, email);
|
|
24
|
+
ui.success(`Stored token for ${consoleUrl}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const email = flags.email || (await prompt.question('Email: '));
|
|
29
|
+
if (!email) throw new CliError('email is required');
|
|
30
|
+
const password = (typeof flags.password === 'string' && flags.password) || (await prompt.password('Password: '));
|
|
31
|
+
if (!password) throw new CliError('password is required');
|
|
32
|
+
|
|
33
|
+
const data = await api.loginRequest(consoleUrl, email, password);
|
|
34
|
+
const token = data && data.token;
|
|
35
|
+
if (!token) throw new CliError('login succeeded but no token was returned');
|
|
36
|
+
config.saveConsoleToken(consoleUrl, token, email);
|
|
37
|
+
ui.success(`Logged in to ${consoleUrl} as ${email}`);
|
|
38
|
+
},
|
|
39
|
+
};
|
package/src/commands/logout.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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 stored token for the current (or --url) console',
|
|
9
|
-
usage: 'motherbase logout [--url <consoleUrl>]',
|
|
10
|
-
async run(ctx) {
|
|
11
|
-
const url = config.normalizeUrl(ctx.flags.url || ctx.consoleUrl || '');
|
|
12
|
-
if (!url) throw new CliError('No console to log out of.');
|
|
13
|
-
config.clearConsoleToken(url);
|
|
14
|
-
ui.success(`Logged out of ${url}`);
|
|
15
|
-
},
|
|
16
|
-
};
|
|
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 stored token for the current (or --url) console',
|
|
9
|
+
usage: 'motherbase logout [--url <consoleUrl>]',
|
|
10
|
+
async run(ctx) {
|
|
11
|
+
const url = config.normalizeUrl(ctx.flags.url || ctx.consoleUrl || '');
|
|
12
|
+
if (!url) throw new CliError('No console to log out of.');
|
|
13
|
+
config.clearConsoleToken(url);
|
|
14
|
+
ui.success(`Logged out of ${url}`);
|
|
15
|
+
},
|
|
16
|
+
};
|
package/src/commands/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// `motherbase mcp
|
|
4
|
-
// Context Protocol.
|
|
3
|
+
// `motherbase mcp`: expose the linked project, and the console's organizations
|
|
4
|
+
// and projects, to an AI agent over the Model Context Protocol.
|
|
5
5
|
//
|
|
6
6
|
// The transport is stdio, which is how an MCP client launches a server: it spawns
|
|
7
7
|
// this process and talks JSON-RPC over its stdin/stdout. Nothing is printed to
|
|
@@ -9,26 +9,44 @@
|
|
|
9
9
|
|
|
10
10
|
const { serve, toolServer } = require('../mcp/protocol');
|
|
11
11
|
const { buildTools } = require('../mcp/tools');
|
|
12
|
+
const { CliError } = require('../context');
|
|
12
13
|
|
|
13
14
|
const pkg = require('../../package.json');
|
|
14
15
|
|
|
16
|
+
/** Whether a `require*()` resolves, without surfacing its error. */
|
|
17
|
+
function reaches(resolve) {
|
|
18
|
+
try {
|
|
19
|
+
resolve();
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
const mcp = {
|
|
16
|
-
summary: 'Serve the linked project to an AI agent over MCP (stdio)',
|
|
27
|
+
summary: 'Serve the linked project and console to an AI agent over MCP (stdio)',
|
|
17
28
|
usage: 'motherbase mcp [--write]',
|
|
18
29
|
async run(ctx, _args, flags = {}) {
|
|
19
|
-
// Resolve
|
|
20
|
-
// on the first tool call is an opaque one inside someone's agent.
|
|
21
|
-
ctx.requireProject();
|
|
30
|
+
// Resolve what this session reaches up front. Failing here is a clear CLI
|
|
31
|
+
// error; failing on the first tool call is an opaque one inside someone's agent.
|
|
32
|
+
const scope = { project: reaches(() => ctx.requireProject()), console: reaches(() => ctx.requireConsole()) };
|
|
33
|
+
if (!scope.project && !scope.console) {
|
|
34
|
+
throw new CliError(
|
|
35
|
+
'Nothing to serve. Run `motherbase login` for the organization and project tools, and `motherbase link` '
|
|
36
|
+
+ '(or set MOTHERBASE_URL + MOTHERBASE_ADMIN_TOKEN) for the database tools.',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
22
39
|
|
|
23
40
|
const write = Boolean(flags.write);
|
|
24
|
-
const tools = buildTools(ctx, { write });
|
|
41
|
+
const tools = buildTools(ctx, { write, ...scope });
|
|
25
42
|
|
|
26
43
|
process.stderr.write(
|
|
27
44
|
`motherbase mcp: ${tools.length} tools, ${write ? 'read and write' : 'read only'}. `
|
|
28
|
-
+ `Project: ${ctx.projectName || ctx.projectId || 'direct'}
|
|
45
|
+
+ `Project: ${scope.project ? ctx.projectName || ctx.projectId || 'direct' : 'none linked'}. `
|
|
46
|
+
+ `Console: ${scope.console ? ctx.consoleUrl : 'not logged in'}\n`,
|
|
29
47
|
);
|
|
30
48
|
if (!write) {
|
|
31
|
-
process.stderr.write(' Pass --write to expose apply_migration
|
|
49
|
+
process.stderr.write(' Pass --write to expose apply_migration, raw SQL writes, function deploys and project lifecycle.\n');
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
await serve({
|