@motherbase/cli 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/bin/motherbase.js +11 -0
- package/package.json +24 -0
- package/src/api.js +200 -0
- package/src/commands/_util.js +50 -0
- package/src/commands/config.js +47 -0
- package/src/commands/cron.js +65 -0
- package/src/commands/db.js +92 -0
- package/src/commands/functions.js +112 -0
- package/src/commands/import.js +35 -0
- package/src/commands/index.js +55 -0
- package/src/commands/inspect.js +41 -0
- package/src/commands/link.js +35 -0
- package/src/commands/local.js +64 -0
- package/src/commands/login.js +39 -0
- package/src/commands/logout.js +16 -0
- package/src/commands/migration.js +33 -0
- package/src/commands/migrations.js +85 -0
- package/src/commands/projects.js +125 -0
- package/src/commands/realtime.js +35 -0
- package/src/commands/rls.js +85 -0
- package/src/commands/secrets.js +45 -0
- package/src/commands/status.js +42 -0
- package/src/commands/storage.js +113 -0
- package/src/commands/unlink.js +15 -0
- package/src/commands/users.js +113 -0
- package/src/commands/webhooks.js +62 -0
- package/src/commands/whoami.js +20 -0
- package/src/config.js +98 -0
- package/src/context.js +102 -0
- package/src/main.js +155 -0
- package/src/prompt.js +44 -0
- package/src/ui.js +111 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const api = require('../api');
|
|
6
|
+
const ui = require('../ui');
|
|
7
|
+
const { output, requireArg } = require('./_util');
|
|
8
|
+
const { CliError } = require('../context');
|
|
9
|
+
|
|
10
|
+
const subcommands = {
|
|
11
|
+
list: {
|
|
12
|
+
summary: 'List edge functions',
|
|
13
|
+
usage: 'motherbase functions list',
|
|
14
|
+
async run(ctx) {
|
|
15
|
+
const data = await api.adminRequest(ctx, 'GET', '/edge-functions');
|
|
16
|
+
const rows = (data && data.functions) || [];
|
|
17
|
+
output(ctx, data, (_d, u) =>
|
|
18
|
+
u.table(rows, [
|
|
19
|
+
{ key: 'id', label: 'ID' },
|
|
20
|
+
{ key: 'name', label: 'NAME' },
|
|
21
|
+
{ key: 'route', label: 'ROUTE' },
|
|
22
|
+
{ key: 'enabled', label: 'ENABLED' },
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
get: {
|
|
28
|
+
summary: 'Get an edge function by id',
|
|
29
|
+
usage: 'motherbase functions get <id>',
|
|
30
|
+
async run(ctx, args) {
|
|
31
|
+
const id = requireArg(args[0], 'id');
|
|
32
|
+
const data = await api.adminRequest(ctx, 'GET', `/edge-functions/${id}`);
|
|
33
|
+
output(ctx, data, (d, u) => u.keyValue((d && d.function) || d || {}));
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
new: {
|
|
37
|
+
summary: 'Scaffold a local edge function file',
|
|
38
|
+
usage: 'motherbase functions new <name>',
|
|
39
|
+
async run(ctx, args) {
|
|
40
|
+
const name = requireArg(args[0], 'name');
|
|
41
|
+
const dir = path.join(ctx.cwd, 'motherbase', 'functions');
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
43
|
+
const file = path.join(dir, `${name}.js`);
|
|
44
|
+
fs.writeFileSync(file, `// Edge function: ${name}\n// The resolved value of this expression becomes the response body.\nasync () => {\n return { hello: '${name}', at: new Date().toISOString() };\n}\n`);
|
|
45
|
+
ui.success(`Created ${path.relative(ctx.cwd, file)}`);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
deploy: {
|
|
49
|
+
summary: 'Create or update an edge function (upsert by route)',
|
|
50
|
+
usage: 'motherbase functions deploy --name <n> --route <r> --file <f.js>',
|
|
51
|
+
async run(ctx, args, flags) {
|
|
52
|
+
const route = flags.route || args[0];
|
|
53
|
+
const file = flags.file || args[1];
|
|
54
|
+
const name = flags.name || route;
|
|
55
|
+
if (!route) throw new CliError('--route is required');
|
|
56
|
+
if (!file) throw new CliError('--file <f.js> is required');
|
|
57
|
+
const code = fs.readFileSync(path.resolve(ctx.cwd, file), 'utf8');
|
|
58
|
+
|
|
59
|
+
// Upsert: PATCH the existing function when a matching route is present.
|
|
60
|
+
const listing = await api.adminRequest(ctx, 'GET', '/edge-functions');
|
|
61
|
+
const existing = ((listing && listing.functions) || []).find(f => f.route === route);
|
|
62
|
+
let data;
|
|
63
|
+
if (existing) {
|
|
64
|
+
data = await api.adminRequest(ctx, 'PATCH', `/edge-functions/${existing.id}`, { body: { name, route, code } });
|
|
65
|
+
} else {
|
|
66
|
+
data = await api.adminRequest(ctx, 'POST', '/edge-functions', { body: { name, route, code, enabled: true } });
|
|
67
|
+
}
|
|
68
|
+
output(ctx, data, (d, u) => u.success(`Deployed function ${name} (${route})`));
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
delete: {
|
|
72
|
+
summary: 'Delete an edge function by id',
|
|
73
|
+
usage: 'motherbase functions delete <id>',
|
|
74
|
+
async run(ctx, args) {
|
|
75
|
+
const id = requireArg(args[0], 'id');
|
|
76
|
+
const data = await api.adminRequest(ctx, 'DELETE', `/edge-functions/${id}`);
|
|
77
|
+
output(ctx, data, (d, u) => u.success(`Deleted function ${id}`));
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
invoke: {
|
|
81
|
+
summary: 'Invoke an edge function by route',
|
|
82
|
+
usage: 'motherbase functions invoke <route> [--body <json>]',
|
|
83
|
+
async run(ctx, args, flags) {
|
|
84
|
+
const route = requireArg(args[0], 'route');
|
|
85
|
+
let body = {};
|
|
86
|
+
if (flags.body) {
|
|
87
|
+
try { body = JSON.parse(flags.body); } catch { throw new CliError('--body must be valid JSON'); }
|
|
88
|
+
}
|
|
89
|
+
const data = await api.edgeInvokeRequest(ctx, route, body);
|
|
90
|
+
output(ctx, data, (d, u) => u.json(d));
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
logs: {
|
|
94
|
+
summary: 'Show logs for an edge function',
|
|
95
|
+
usage: 'motherbase functions logs <id>',
|
|
96
|
+
async run(ctx, args) {
|
|
97
|
+
const id = requireArg(args[0], 'id');
|
|
98
|
+
const data = await api.adminRequest(ctx, 'GET', `/edge-functions/${id}/logs`);
|
|
99
|
+
output(ctx, data, (d, u) => {
|
|
100
|
+
const logs = (d && d.logs) || d || [];
|
|
101
|
+
if (Array.isArray(logs)) logs.forEach(l => u.print(typeof l === 'string' ? l : JSON.stringify(l)));
|
|
102
|
+
else u.json(d);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
summary: 'Edge functions (list/deploy/invoke/logs)',
|
|
110
|
+
usage: 'motherbase functions <list|get|new|deploy|delete|invoke|logs> ...',
|
|
111
|
+
subcommands,
|
|
112
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const prompt = require('../prompt');
|
|
5
|
+
const { output } = require('./_util');
|
|
6
|
+
const { CliError } = require('../context');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
summary: 'Import a Supabase project (schema + data + auth) into the linked project',
|
|
10
|
+
usage: 'motherbase import --from-supabase <connectionString> [--preview] [--no-data] [--no-auth]',
|
|
11
|
+
async run(ctx, args, flags) {
|
|
12
|
+
const connectionString = flags['from-supabase'] || flags.fromSupabase;
|
|
13
|
+
if (!connectionString) throw new CliError('--from-supabase <connectionString> is required');
|
|
14
|
+
|
|
15
|
+
const include = { schema: true, data: !flags['no-data'], auth: !flags['no-auth'] };
|
|
16
|
+
const preview = Boolean(flags.preview);
|
|
17
|
+
|
|
18
|
+
const body = { connectionString, include };
|
|
19
|
+
if (preview) {
|
|
20
|
+
body.preview = true;
|
|
21
|
+
} else {
|
|
22
|
+
if (!flags.yes) {
|
|
23
|
+
const ok = await prompt.confirm('This will import into the linked project. Continue?');
|
|
24
|
+
if (!ok) throw new CliError('Aborted.');
|
|
25
|
+
}
|
|
26
|
+
body.confirm = 'IMPORT';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = await api.adminRequest(ctx, 'POST', '/import/supabase', { body });
|
|
30
|
+
output(ctx, data, (d, ui) => {
|
|
31
|
+
if (preview) { ui.info('Import preview (dry run):'); ui.json(d); }
|
|
32
|
+
else { ui.success('Import complete'); ui.json(d); }
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Command registry: name -> { summary, usage, subcommands?, run? }.
|
|
4
|
+
// `main.js` builds all help/dispatch from this single map.
|
|
5
|
+
|
|
6
|
+
const local = require('./local');
|
|
7
|
+
|
|
8
|
+
const registry = {
|
|
9
|
+
// ── Connection ─────────────────────────────────────────────────────────────
|
|
10
|
+
login: require('./login'),
|
|
11
|
+
logout: require('./logout'),
|
|
12
|
+
whoami: require('./whoami'),
|
|
13
|
+
link: require('./link'),
|
|
14
|
+
unlink: require('./unlink'),
|
|
15
|
+
status: require('./status'),
|
|
16
|
+
|
|
17
|
+
// ── Projects (Platform Console) ────────────────────────────────────────────
|
|
18
|
+
projects: require('./projects'),
|
|
19
|
+
|
|
20
|
+
// ── Local self-host (docker compose) ───────────────────────────────────────
|
|
21
|
+
init: local.init,
|
|
22
|
+
start: local.start,
|
|
23
|
+
stop: local.stop,
|
|
24
|
+
deploy: local.deploy,
|
|
25
|
+
logs: local.logs,
|
|
26
|
+
|
|
27
|
+
// ── Database / migrations / import ─────────────────────────────────────────
|
|
28
|
+
db: require('./db'),
|
|
29
|
+
migrations: require('./migrations'),
|
|
30
|
+
migration: require('./migration'),
|
|
31
|
+
import: require('./import'),
|
|
32
|
+
|
|
33
|
+
// ── Resource domains ───────────────────────────────────────────────────────
|
|
34
|
+
rls: require('./rls'),
|
|
35
|
+
users: require('./users'),
|
|
36
|
+
storage: require('./storage'),
|
|
37
|
+
functions: require('./functions'),
|
|
38
|
+
secrets: require('./secrets'),
|
|
39
|
+
realtime: require('./realtime'),
|
|
40
|
+
webhooks: require('./webhooks'),
|
|
41
|
+
cron: require('./cron'),
|
|
42
|
+
config: require('./config'),
|
|
43
|
+
inspect: require('./inspect'),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Grouping used only to render a tidy top-level help listing.
|
|
47
|
+
const groups = [
|
|
48
|
+
{ title: 'Connection', names: ['login', 'logout', 'whoami', 'link', 'unlink', 'status'] },
|
|
49
|
+
{ title: 'Projects (Console)', names: ['projects'] },
|
|
50
|
+
{ title: 'Local self-host', names: ['init', 'start', 'stop', 'deploy', 'logs'] },
|
|
51
|
+
{ title: 'Database', names: ['db', 'migrations', 'migration', 'import'] },
|
|
52
|
+
{ title: 'Resources', names: ['rls', 'users', 'storage', 'functions', 'secrets', 'realtime', 'webhooks', 'cron', 'config', 'inspect'] },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
module.exports = { registry, groups };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output } = require('./_util');
|
|
5
|
+
|
|
6
|
+
function simple(apiPath, summary, render) {
|
|
7
|
+
return {
|
|
8
|
+
summary,
|
|
9
|
+
usage: `motherbase inspect ${summary.split(' ')[0].toLowerCase()}`,
|
|
10
|
+
async run(ctx) {
|
|
11
|
+
const data = await api.adminRequest(ctx, 'GET', apiPath);
|
|
12
|
+
output(ctx, data, render || ((d, ui) => ui.json(d)));
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const subcommands = {
|
|
18
|
+
logs: {
|
|
19
|
+
summary: 'Recent server logs',
|
|
20
|
+
usage: 'motherbase inspect logs [--limit n]',
|
|
21
|
+
async run(ctx, args, flags) {
|
|
22
|
+
const query = flags.limit ? { limit: flags.limit } : undefined;
|
|
23
|
+
const data = await api.adminRequest(ctx, 'GET', '/logs', { query });
|
|
24
|
+
const logs = (data && data.logs) || [];
|
|
25
|
+
output(ctx, data, (_d, ui) =>
|
|
26
|
+
logs.forEach(l => ui.print(typeof l === 'string' ? l : `${l.level || ''} ${l.source || ''} ${l.message || JSON.stringify(l)}`)),
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
stats: simple('/stats', 'Stats overview'),
|
|
31
|
+
performance: simple('/performance', 'Performance metrics'),
|
|
32
|
+
requests: simple('/metrics/requests', 'Request metrics'),
|
|
33
|
+
advisor: simple('/advisor', 'Advisor findings'),
|
|
34
|
+
shield: simple('/shield', 'Security shield report'),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
summary: 'Diagnostics: logs, stats, performance, advisor, shield',
|
|
39
|
+
usage: 'motherbase inspect <logs|stats|performance|requests|advisor|shield>',
|
|
40
|
+
subcommands,
|
|
41
|
+
};
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Local self-host workflow: thin wrappers over `docker compose`, plus `init`
|
|
4
|
+
// scaffolding. No HTTP, no context resolution required. Ported and extended from
|
|
5
|
+
// the original single-file CLI.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const ui = require('../ui');
|
|
11
|
+
const { CliError } = require('../context');
|
|
12
|
+
|
|
13
|
+
function dockerCompose(composeArgs) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn('docker', ['compose', ...composeArgs], { stdio: 'inherit', windowsHide: true });
|
|
16
|
+
child.on('error', err => reject(new CliError(`failed to run docker: ${err.message}`)));
|
|
17
|
+
child.on('close', code => (code === 0 ? resolve() : reject(new CliError(`docker compose exited with code ${code}`))));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const init = {
|
|
22
|
+
summary: 'Scaffold a local project directory with a .env',
|
|
23
|
+
usage: 'motherbase init [dir]',
|
|
24
|
+
async run(ctx, args) {
|
|
25
|
+
const dir = args[0] || '.';
|
|
26
|
+
const target = path.resolve(ctx.cwd, dir);
|
|
27
|
+
fs.mkdirSync(target, { recursive: true });
|
|
28
|
+
const envPath = path.join(target, '.env');
|
|
29
|
+
if (!fs.existsSync(envPath)) {
|
|
30
|
+
const example = path.join(ctx.cwd, '.env.example');
|
|
31
|
+
const template = fs.existsSync(example)
|
|
32
|
+
? fs.readFileSync(example, 'utf8')
|
|
33
|
+
: 'PORT=5001\nDATABASE_URL=postgresql://postgres:postgres@localhost:5432/app?schema=public\nJWT_SECRET=change-me\n';
|
|
34
|
+
fs.writeFileSync(envPath, template);
|
|
35
|
+
}
|
|
36
|
+
ui.success(`Initialized Motherbase project at ${target}`);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const start = {
|
|
41
|
+
summary: 'Start the local docker compose stack',
|
|
42
|
+
usage: 'motherbase start',
|
|
43
|
+
async run() { await dockerCompose(['up', '-d']); },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const stop = {
|
|
47
|
+
summary: 'Stop the local docker compose stack',
|
|
48
|
+
usage: 'motherbase stop',
|
|
49
|
+
async run() { await dockerCompose(['down']); },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const deploy = {
|
|
53
|
+
summary: 'Rebuild and start the local docker compose stack',
|
|
54
|
+
usage: 'motherbase deploy',
|
|
55
|
+
async run() { await dockerCompose(['up', '-d', '--build']); },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const logs = {
|
|
59
|
+
summary: 'Follow logs from the local docker compose stack',
|
|
60
|
+
usage: 'motherbase logs [service...]',
|
|
61
|
+
async run(ctx, args) { await dockerCompose(['logs', '-f', ...args]); },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = { init, start, stop, deploy, logs };
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
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.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const ui = require('../ui');
|
|
9
|
+
const { requireArg } = require('./_util');
|
|
10
|
+
const { CliError } = require('../context');
|
|
11
|
+
|
|
12
|
+
const subcommands = {
|
|
13
|
+
new: {
|
|
14
|
+
summary: 'Scaffold a local migration .sql file',
|
|
15
|
+
usage: 'motherbase migration new <name>',
|
|
16
|
+
async run(ctx, args) {
|
|
17
|
+
const name = requireArg(args[0], 'name');
|
|
18
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new CliError('name must be alphanumeric / dash / underscore');
|
|
19
|
+
const dir = path.join(ctx.cwd, 'motherbase', 'migrations');
|
|
20
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '');
|
|
22
|
+
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`);
|
|
24
|
+
ui.success(`Created ${path.relative(ctx.cwd, file)}`);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
summary: 'Scaffold local migration files',
|
|
31
|
+
usage: 'motherbase migration new <name>',
|
|
32
|
+
subcommands,
|
|
33
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const api = require('../api');
|
|
6
|
+
const { output, requireArg } = require('./_util');
|
|
7
|
+
const { CliError } = require('../context');
|
|
8
|
+
|
|
9
|
+
function readSql(cwd, file) {
|
|
10
|
+
if (!file) return undefined;
|
|
11
|
+
return fs.readFileSync(path.resolve(cwd, file), 'utf8');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const subcommands = {
|
|
15
|
+
list: {
|
|
16
|
+
summary: 'List schema migrations',
|
|
17
|
+
usage: 'motherbase migrations list',
|
|
18
|
+
async run(ctx) {
|
|
19
|
+
const data = await api.adminRequest(ctx, 'GET', '/migrations');
|
|
20
|
+
const rows = (data && data.migrations) || [];
|
|
21
|
+
output(ctx, data, (_d, ui) =>
|
|
22
|
+
ui.table(rows, [
|
|
23
|
+
{ key: 'id', label: 'ID' },
|
|
24
|
+
{ key: 'name', label: 'NAME' },
|
|
25
|
+
{ key: 'status', label: 'STATUS' },
|
|
26
|
+
{ key: 'appliedAt', label: 'APPLIED' },
|
|
27
|
+
]),
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
diff: {
|
|
32
|
+
summary: 'Show a diff between live schema and migrations',
|
|
33
|
+
usage: 'motherbase migrations diff',
|
|
34
|
+
async run(ctx) {
|
|
35
|
+
const data = await api.adminRequest(ctx, 'GET', '/migrations/diff');
|
|
36
|
+
output(ctx, data, (d, ui) => ui.json(d));
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
get: {
|
|
40
|
+
summary: 'Show a single migration',
|
|
41
|
+
usage: 'motherbase migrations get <id>',
|
|
42
|
+
async run(ctx, args) {
|
|
43
|
+
const id = requireArg(args[0], 'id');
|
|
44
|
+
const data = await api.adminRequest(ctx, 'GET', `/migrations/${id}`);
|
|
45
|
+
output(ctx, data, (d, ui) => ui.keyValue((d && d.migration) || d || {}));
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
create: {
|
|
49
|
+
summary: 'Create a migration from up/down SQL files',
|
|
50
|
+
usage: 'motherbase migrations create --name <name> --up <file.sql> [--down <file.sql>]',
|
|
51
|
+
async run(ctx, args, flags) {
|
|
52
|
+
const name = flags.name || args[0];
|
|
53
|
+
if (!name) throw new CliError('--name is required');
|
|
54
|
+
const upSql = readSql(ctx.cwd, flags.up);
|
|
55
|
+
const downSql = readSql(ctx.cwd, flags.down);
|
|
56
|
+
if (!upSql) throw new CliError('--up <file.sql> is required');
|
|
57
|
+
const data = await api.adminRequest(ctx, 'POST', '/migrations', { body: { name, upSql, downSql } });
|
|
58
|
+
output(ctx, data, (d, ui) => ui.success(`Created migration ${name}`));
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
apply: {
|
|
62
|
+
summary: 'Apply a migration',
|
|
63
|
+
usage: 'motherbase migrations apply <id>',
|
|
64
|
+
async run(ctx, args) {
|
|
65
|
+
const id = requireArg(args[0], 'id');
|
|
66
|
+
const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/apply`);
|
|
67
|
+
output(ctx, data, (d, ui) => ui.success(`Applied ${id}`));
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
rollback: {
|
|
71
|
+
summary: 'Roll back a migration',
|
|
72
|
+
usage: 'motherbase migrations rollback <id>',
|
|
73
|
+
async run(ctx, args) {
|
|
74
|
+
const id = requireArg(args[0], 'id');
|
|
75
|
+
const data = await api.adminRequest(ctx, 'POST', `/migrations/${id}/rollback`);
|
|
76
|
+
output(ctx, data, (d, ui) => ui.success(`Rolled back ${id}`));
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
summary: 'Schema migrations (list/diff/create/apply/rollback)',
|
|
83
|
+
usage: 'motherbase migrations <list|diff|get|create|apply|rollback> ...',
|
|
84
|
+
subcommands,
|
|
85
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('../api');
|
|
4
|
+
const { output, requireArg } = require('./_util');
|
|
5
|
+
const { CliError } = require('../context');
|
|
6
|
+
|
|
7
|
+
const LIFECYCLE = ['start', 'stop', 'restart', 'update', 'rebuild'];
|
|
8
|
+
|
|
9
|
+
function lifecycleCmd(action) {
|
|
10
|
+
return {
|
|
11
|
+
summary: `${action} a project`,
|
|
12
|
+
usage: `motherbase projects ${action} <id>`,
|
|
13
|
+
async run(ctx, args) {
|
|
14
|
+
const id = requireArg(args[0], 'id');
|
|
15
|
+
const data = await api.consoleRequest(ctx, 'POST', `/projects/${id}/${action}`);
|
|
16
|
+
output(ctx, data, (d, ui) => ui.success(`${action} → ${id}`));
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const subcommands = {
|
|
22
|
+
list: {
|
|
23
|
+
summary: 'List all projects on the console',
|
|
24
|
+
usage: 'motherbase projects list',
|
|
25
|
+
async run(ctx) {
|
|
26
|
+
const data = await api.consoleRequest(ctx, 'GET', '/projects');
|
|
27
|
+
output(ctx, data, (rows, ui) =>
|
|
28
|
+
ui.table(Array.isArray(rows) ? rows : [], [
|
|
29
|
+
{ key: 'name', label: 'NAME' },
|
|
30
|
+
{ key: 'status', label: 'STATUS' },
|
|
31
|
+
{ key: 'domain', label: 'DOMAIN' },
|
|
32
|
+
]),
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
create: {
|
|
37
|
+
summary: 'Create a new project',
|
|
38
|
+
usage: 'motherbase projects create <name> [--domain <domain>]',
|
|
39
|
+
async run(ctx, args, flags) {
|
|
40
|
+
const name = requireArg(args[0], 'name');
|
|
41
|
+
const body = { name };
|
|
42
|
+
if (flags.domain) body.domain = flags.domain;
|
|
43
|
+
const data = await api.consoleRequest(ctx, 'POST', '/projects', { body });
|
|
44
|
+
output(ctx, data, (d, ui) => ui.success(`Created project ${name}`));
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
delete: {
|
|
48
|
+
summary: 'Delete a project (requires --confirm <name>)',
|
|
49
|
+
usage: 'motherbase projects delete <id> --confirm <name>',
|
|
50
|
+
async run(ctx, args, flags) {
|
|
51
|
+
const id = requireArg(args[0], 'id');
|
|
52
|
+
const confirm = flags.confirm;
|
|
53
|
+
if (confirm !== id) {
|
|
54
|
+
throw new CliError(`Refusing to delete. Pass --confirm ${id} to proceed.`);
|
|
55
|
+
}
|
|
56
|
+
const data = await api.consoleRequest(ctx, 'DELETE', `/projects/${id}`, { body: { confirm } });
|
|
57
|
+
output(ctx, data, (d, ui) => ui.success(`Deleted project ${id}`));
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
logs: {
|
|
61
|
+
summary: 'Show recent logs for a project',
|
|
62
|
+
usage: 'motherbase projects logs <id> [-f]',
|
|
63
|
+
async run(ctx, args, flags) {
|
|
64
|
+
const id = requireArg(args[0], 'id');
|
|
65
|
+
const fetchOnce = () => api.consoleRequest(ctx, 'GET', `/projects/${id}/logs`);
|
|
66
|
+
const data = await fetchOnce();
|
|
67
|
+
output(ctx, data, (d, ui) => {
|
|
68
|
+
if (d && d.stdout) ui.print(d.stdout);
|
|
69
|
+
if (d && d.stderr) ui.printErr(d.stderr);
|
|
70
|
+
});
|
|
71
|
+
if (flags.f && !ctx.flags.json) {
|
|
72
|
+
// The console logs endpoint returns a snapshot; approximate -f by polling.
|
|
73
|
+
const ui = require('../ui');
|
|
74
|
+
ui.warn('(-f) polling every 3s; Ctrl-C to stop');
|
|
75
|
+
setInterval(async () => {
|
|
76
|
+
try {
|
|
77
|
+
const next = await fetchOnce();
|
|
78
|
+
if (next && next.stdout) process.stdout.write(next.stdout);
|
|
79
|
+
} catch (e) { ui.warn(e.message); }
|
|
80
|
+
}, 3000);
|
|
81
|
+
await new Promise(() => {});
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
info: {
|
|
86
|
+
summary: 'Show project details and DNS checks',
|
|
87
|
+
usage: 'motherbase projects info <id>',
|
|
88
|
+
async run(ctx, args) {
|
|
89
|
+
const id = requireArg(args[0], 'id');
|
|
90
|
+
const list = await api.consoleRequest(ctx, 'GET', '/projects');
|
|
91
|
+
const project = (Array.isArray(list) ? list : []).find(p => p.name === id || p.id === id) || { name: id };
|
|
92
|
+
let dns = null;
|
|
93
|
+
try { dns = await api.consoleRequest(ctx, 'GET', `/projects/${id}/dns`); } catch { /* optional */ }
|
|
94
|
+
const data = { ...project, dns: dns && dns.checks };
|
|
95
|
+
output(ctx, data, (d, ui) => ui.keyValue(d));
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
limits: {
|
|
99
|
+
summary: 'Get or set per-project resource limits',
|
|
100
|
+
usage: 'motherbase projects limits <id> [--api-cpus n --api-mem mb --db-cpus n --db-mem mb --minio-cpus n --minio-mem mb]',
|
|
101
|
+
async run(ctx, args, flags) {
|
|
102
|
+
const id = requireArg(args[0], 'id');
|
|
103
|
+
const map = {
|
|
104
|
+
'api-cpus': 'apiCpus', 'api-mem': 'apiMemMb',
|
|
105
|
+
'db-cpus': 'dbCpus', 'db-mem': 'dbMemMb',
|
|
106
|
+
'minio-cpus': 'minioCpus', 'minio-mem': 'minioMemMb',
|
|
107
|
+
};
|
|
108
|
+
const body = {};
|
|
109
|
+
for (const [flag, key] of Object.entries(map)) {
|
|
110
|
+
if (flags[flag] !== undefined) body[key] = Number(flags[flag]);
|
|
111
|
+
}
|
|
112
|
+
const method = Object.keys(body).length ? 'PATCH' : 'GET';
|
|
113
|
+
const data = await api.consoleRequest(ctx, method, `/projects/${id}/limits`, method === 'PATCH' ? { body } : {});
|
|
114
|
+
output(ctx, data, (d, ui) => ui.keyValue(d || {}));
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
for (const action of LIFECYCLE) subcommands[action] = lifecycleCmd(action);
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
summary: 'Manage projects on the Platform Console',
|
|
123
|
+
usage: 'motherbase projects <list|create|start|stop|restart|update|rebuild|delete|logs|info|limits> ...',
|
|
124
|
+
subcommands,
|
|
125
|
+
};
|