@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/src/context.js ADDED
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ // Resolve the runtime context: which console, which token, which project, and
4
+ // whether we talk through the Console proxy ('console' mode) or straight at a
5
+ // single project API ('direct' mode). Precedence everywhere: flags > env > config.
6
+ //
7
+ // Resolution is LAZY: building a context never throws, so commands that need no
8
+ // project (login, local docker, init) work with zero configuration. The
9
+ // require*() helpers throw a clear, actionable error only when a value is
10
+ // actually needed and missing.
11
+
12
+ const config = require('./config');
13
+
14
+ class CliError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = 'CliError';
18
+ this.isCliError = true;
19
+ }
20
+ }
21
+
22
+ function firstDefined(...vals) {
23
+ for (const v of vals) if (v !== undefined && v !== null && v !== '') return v;
24
+ return undefined;
25
+ }
26
+
27
+ function createContext(flags = {}, env = process.env, cwd = process.cwd()) {
28
+ const link = config.readLink(cwd) || {};
29
+ const global = config.readGlobal();
30
+
31
+ const consoleUrl = firstDefined(
32
+ flags.url,
33
+ env.MOTHERBASE_CONSOLE_URL,
34
+ link.consoleUrl,
35
+ global.current,
36
+ );
37
+ const normConsoleUrl = consoleUrl ? config.normalizeUrl(consoleUrl) : undefined;
38
+
39
+ const token = firstDefined(
40
+ flags.token,
41
+ env.MOTHERBASE_TOKEN,
42
+ normConsoleUrl ? (config.consoleEntry(normConsoleUrl) || {}).token : undefined,
43
+ );
44
+
45
+ const projectId = firstDefined(flags.project, env.MOTHERBASE_PROJECT, link.projectId);
46
+ const projectName = firstDefined(link.projectName, projectId);
47
+
48
+ // Direct project mode: an admin JWT pointed straight at one project API. Only
49
+ // used as a fallback when there is no console link/flags to route through.
50
+ const directUrl = env.MOTHERBASE_URL ? config.normalizeUrl(env.MOTHERBASE_URL) : undefined;
51
+ const directToken = env.MOTHERBASE_ADMIN_TOKEN;
52
+ const hasConsole = Boolean(normConsoleUrl && token);
53
+ const hasProjectLink = Boolean(projectId);
54
+ const preferDirect = Boolean(directUrl && directToken) && !(hasConsole && hasProjectLink);
55
+ const mode = preferDirect ? 'direct' : 'console';
56
+
57
+ return {
58
+ flags,
59
+ env,
60
+ cwd,
61
+ mode,
62
+ consoleUrl: normConsoleUrl,
63
+ token,
64
+ projectId,
65
+ projectName,
66
+ directUrl,
67
+ directToken,
68
+ linkedConsoleUrl: link.consoleUrl,
69
+ accountEmail: normConsoleUrl ? (config.consoleEntry(normConsoleUrl) || {}).email : undefined,
70
+
71
+ // Console (account / multi-project) requests need URL + session token.
72
+ requireConsole() {
73
+ if (!this.consoleUrl) {
74
+ throw new CliError('No console URL. Run `motherbase login --url <consoleUrl>` or set MOTHERBASE_CONSOLE_URL.');
75
+ }
76
+ if (!this.token) {
77
+ throw new CliError('Not logged in. Run `motherbase login` or set MOTHERBASE_TOKEN.');
78
+ }
79
+ return { consoleUrl: this.consoleUrl, token: this.token };
80
+ },
81
+
82
+ // Admin/resource requests: resolve either the console proxy path or a direct
83
+ // project API, whichever the context selected.
84
+ requireProject() {
85
+ if (this.mode === 'direct') {
86
+ return { mode: 'direct', baseUrl: this.directUrl, token: this.directToken };
87
+ }
88
+ this.requireConsole();
89
+ if (!this.projectId) {
90
+ throw new CliError('No project linked. Run `motherbase link` or set MOTHERBASE_PROJECT (or use direct mode via MOTHERBASE_URL + MOTHERBASE_ADMIN_TOKEN).');
91
+ }
92
+ return {
93
+ mode: 'console',
94
+ consoleUrl: this.consoleUrl,
95
+ token: this.token,
96
+ projectId: this.projectId,
97
+ };
98
+ },
99
+ };
100
+ }
101
+
102
+ module.exports = { createContext, CliError };
package/src/main.js ADDED
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { registry, groups } = require('./commands');
5
+ const { createContext, CliError } = require('./context');
6
+ const ui = require('./ui');
7
+
8
+ // Flags that never consume a following value.
9
+ const PURE_BOOLEAN = new Set([
10
+ 'json', 'yes', 'help', 'version', 'force', 'preview', 'app', 'no-data', 'no-auth', 'f',
11
+ ]);
12
+ // Short flag aliases → canonical (with whether they take a value).
13
+ const SHORT = {
14
+ y: { name: 'yes', boolean: true },
15
+ h: { name: 'help', boolean: true },
16
+ v: { name: 'version', boolean: true },
17
+ p: { name: 'project', boolean: false },
18
+ u: { name: 'url', boolean: false },
19
+ e: { name: 'e', boolean: false },
20
+ f: { name: 'f', boolean: true },
21
+ };
22
+
23
+ // Tokenize argv into positionals + flags. Repeated flags collapse into arrays.
24
+ function tokenize(argv) {
25
+ const positionals = [];
26
+ const flags = {};
27
+ const setFlag = (key, value) => {
28
+ if (key in flags) {
29
+ flags[key] = [].concat(flags[key], value);
30
+ } else {
31
+ flags[key] = value;
32
+ }
33
+ };
34
+
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const tok = argv[i];
37
+ if (tok === '--') { positionals.push(...argv.slice(i + 1)); break; }
38
+
39
+ if (tok.startsWith('--')) {
40
+ const body = tok.slice(2);
41
+ const eq = body.indexOf('=');
42
+ if (eq >= 0) { setFlag(body.slice(0, eq), body.slice(eq + 1)); continue; }
43
+ const next = argv[i + 1];
44
+ if (!PURE_BOOLEAN.has(body) && next !== undefined && !next.startsWith('-')) {
45
+ setFlag(body, next); i++;
46
+ } else {
47
+ setFlag(body, true);
48
+ }
49
+ } else if (tok.startsWith('-') && tok.length > 1 && tok !== '-') {
50
+ const spec = SHORT[tok.slice(1)];
51
+ if (!spec) { setFlag(tok.slice(1), true); continue; }
52
+ const next = argv[i + 1];
53
+ if (!spec.boolean && next !== undefined && !next.startsWith('-')) { setFlag(spec.name, next); i++; }
54
+ else setFlag(spec.name, true);
55
+ } else {
56
+ positionals.push(tok);
57
+ }
58
+ }
59
+ return { positionals, flags };
60
+ }
61
+
62
+ function version() {
63
+ try {
64
+ return require(path.join(__dirname, '..', 'package.json')).version;
65
+ } catch { return '0.0.0'; }
66
+ }
67
+
68
+ function pad(s, n) { return String(s).padEnd(n); }
69
+
70
+ function topLevelHelp() {
71
+ ui.print(ui.color.bold('Motherbase CLI') + ` v${version()}`);
72
+ ui.print('');
73
+ ui.print('Usage: motherbase <command> [subcommand] [args] [flags]');
74
+ ui.print('');
75
+ const width = Object.keys(registry).reduce((m, k) => Math.max(m, k.length), 0) + 2;
76
+ for (const group of groups) {
77
+ ui.print(ui.color.bold(group.title));
78
+ for (const name of group.names) {
79
+ const entry = registry[name];
80
+ if (!entry) continue;
81
+ ui.print(' ' + ui.color.cyan(pad(name, width)) + ui.color.dim(entry.summary || ''));
82
+ }
83
+ ui.print('');
84
+ }
85
+ ui.print('Global flags: --json --yes/-y --project/-p <id> --url/-u <consoleUrl> --token <t> -h/--help -v/--version');
86
+ ui.print('Run `motherbase <command> -h` for command-specific help.');
87
+ }
88
+
89
+ function commandHelp(name, entry, trail) {
90
+ const full = ['motherbase', ...trail].join(' ');
91
+ ui.print(ui.color.bold(full) + (entry.summary ? ` — ${entry.summary}` : ''));
92
+ ui.print('');
93
+ if (entry.usage) { ui.print('Usage: ' + entry.usage); ui.print(''); }
94
+ if (entry.subcommands) {
95
+ ui.print(ui.color.bold('Subcommands'));
96
+ const subs = Object.keys(entry.subcommands);
97
+ const width = subs.reduce((m, k) => Math.max(m, k.length), 0) + 2;
98
+ for (const sub of subs) {
99
+ const s = entry.subcommands[sub];
100
+ ui.print(' ' + ui.color.cyan(pad(sub, width)) + ui.color.dim(s.summary || ''));
101
+ }
102
+ }
103
+ }
104
+
105
+ async function main(argv) {
106
+ const { positionals, flags } = tokenize(argv);
107
+
108
+ if (flags.version) { ui.print(version()); return 0; }
109
+
110
+ if (positionals.length === 0) { topLevelHelp(); return 0; }
111
+
112
+ const name = positionals[0];
113
+ const entry = registry[name];
114
+ if (!entry) {
115
+ ui.printErr(ui.color.red(`Unknown command: ${name}`));
116
+ ui.printErr('Run `motherbase --help` to see available commands.');
117
+ return 1;
118
+ }
119
+
120
+ // Descend into subcommands.
121
+ let current = entry;
122
+ const trail = [name];
123
+ const rest = positionals.slice(1);
124
+ while (current.subcommands && rest.length && current.subcommands[rest[0]]) {
125
+ current = current.subcommands[rest[0]];
126
+ trail.push(rest.shift());
127
+ }
128
+
129
+ // Help requested, or a group with no runnable action here.
130
+ if (flags.help || (!current.run && !current.subcommands)) {
131
+ commandHelp(name, current, trail);
132
+ return 0;
133
+ }
134
+ if (!current.run) {
135
+ // Group node: either no subcommand given or an unknown one.
136
+ if (rest.length) ui.printErr(ui.color.red(`Unknown subcommand: ${trail.join(' ')} ${rest[0]}`));
137
+ commandHelp(name, current, trail);
138
+ return rest.length ? 1 : 0;
139
+ }
140
+
141
+ const ctx = createContext(flags);
142
+ try {
143
+ await current.run(ctx, rest, flags);
144
+ return 0;
145
+ } catch (err) {
146
+ if (err instanceof CliError || err.isCliError) {
147
+ ui.printErr(ui.color.red(err.message));
148
+ } else {
149
+ ui.printErr(ui.color.red(`Error: ${err.message}`));
150
+ }
151
+ return 1;
152
+ }
153
+ }
154
+
155
+ module.exports = { main, tokenize, version };
package/src/prompt.js ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ // readline-based prompting, including hidden password entry. Built-ins only.
4
+
5
+ const readline = require('readline');
6
+
7
+ function question(query) {
8
+ return new Promise(resolve => {
9
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
10
+ rl.question(query, answer => {
11
+ rl.close();
12
+ resolve(answer.trim());
13
+ });
14
+ });
15
+ }
16
+
17
+ // Hidden input: we mute the output stream by intercepting writes so the typed
18
+ // characters (and the password) never echo.
19
+ function password(query = 'Password: ') {
20
+ return new Promise(resolve => {
21
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
22
+ const onData = () => {
23
+ // Overwrite whatever readline just echoed with the prompt only.
24
+ process.stdout.write(`\r${query}`);
25
+ };
26
+ process.stdout.write(query);
27
+ process.stdin.on('data', onData);
28
+ rl.question('', answer => {
29
+ process.stdin.removeListener('data', onData);
30
+ rl.close();
31
+ process.stdout.write('\n');
32
+ resolve(answer);
33
+ });
34
+ });
35
+ }
36
+
37
+ async function confirm(query, defaultYes = false) {
38
+ const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
39
+ const answer = (await question(query + suffix)).toLowerCase();
40
+ if (!answer) return defaultYes;
41
+ return answer === 'y' || answer === 'yes';
42
+ }
43
+
44
+ module.exports = { question, password, confirm };
package/src/ui.js ADDED
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ // Minimal, dependency-free presentation helpers: ANSI colour (auto-off when not a
4
+ // TTY or NO_COLOR is set), a table printer, a key/value printer, a spinner, and a
5
+ // --json passthrough.
6
+
7
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
8
+
9
+ function wrap(code) {
10
+ return s => (useColor ? `[${code}m${s}` : String(s));
11
+ }
12
+
13
+ const color = {
14
+ bold: wrap('1'),
15
+ dim: wrap('2'),
16
+ red: wrap('31'),
17
+ green: wrap('32'),
18
+ yellow: wrap('33'),
19
+ cyan: wrap('36'),
20
+ gray: wrap('90'),
21
+ };
22
+
23
+ function print(msg = '') {
24
+ process.stdout.write(msg + '\n');
25
+ }
26
+
27
+ function printErr(msg = '') {
28
+ process.stderr.write(msg + '\n');
29
+ }
30
+
31
+ function success(msg) {
32
+ print(color.green('✓ ') + msg);
33
+ }
34
+
35
+ function info(msg) {
36
+ print(color.cyan('• ') + msg);
37
+ }
38
+
39
+ function warn(msg) {
40
+ printErr(color.yellow('! ') + msg);
41
+ }
42
+
43
+ function json(value) {
44
+ print(JSON.stringify(value, null, 2));
45
+ }
46
+
47
+ // Render an array of flat objects as an aligned table using the given columns
48
+ // (array of keys or { key, label } specs). Falls back to a friendly note if empty.
49
+ function table(rows, columns) {
50
+ if (!Array.isArray(rows) || rows.length === 0) {
51
+ print(color.dim('(no rows)'));
52
+ return;
53
+ }
54
+ const cols = (columns || Object.keys(rows[0])).map(c =>
55
+ typeof c === 'string' ? { key: c, label: c } : c,
56
+ );
57
+ const widths = cols.map(c => c.label.length);
58
+ const cells = rows.map(row =>
59
+ cols.map((c, i) => {
60
+ const v = row[c.key];
61
+ const s = v === undefined || v === null ? '' : String(v);
62
+ if (s.length > widths[i]) widths[i] = s.length;
63
+ return s;
64
+ }),
65
+ );
66
+ const header = cols.map((c, i) => color.bold(c.label.padEnd(widths[i]))).join(' ');
67
+ print(header);
68
+ print(color.dim(cols.map((_, i) => '-'.repeat(widths[i])).join(' ')));
69
+ for (const rowCells of cells) {
70
+ print(rowCells.map((s, i) => s.padEnd(widths[i])).join(' '));
71
+ }
72
+ }
73
+
74
+ // Print a { key: value } object as aligned "key : value" lines.
75
+ function keyValue(obj) {
76
+ if (!obj || typeof obj !== 'object') {
77
+ print(String(obj));
78
+ return;
79
+ }
80
+ const keys = Object.keys(obj);
81
+ const w = keys.reduce((m, k) => Math.max(m, k.length), 0);
82
+ for (const k of keys) {
83
+ let v = obj[k];
84
+ if (v && typeof v === 'object') v = JSON.stringify(v);
85
+ print(`${color.gray(k.padEnd(w))} ${v === undefined || v === null ? '' : v}`);
86
+ }
87
+ }
88
+
89
+ // A tiny spinner. No-op (single status line) when stderr is not a TTY.
90
+ function spinner(text) {
91
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
92
+ if (!process.stderr.isTTY) {
93
+ printErr(color.dim(text + ' ...'));
94
+ return { stop() {}, succeed() {}, fail() {} };
95
+ }
96
+ let i = 0;
97
+ const timer = setInterval(() => {
98
+ process.stderr.write(`\r${color.cyan(frames[i++ % frames.length])} ${text}`);
99
+ }, 80);
100
+ const clear = () => {
101
+ clearInterval(timer);
102
+ process.stderr.write('\r');
103
+ };
104
+ return {
105
+ stop: clear,
106
+ succeed(msg) { clear(); if (msg) printErr(color.green('✓ ') + msg); },
107
+ fail(msg) { clear(); if (msg) printErr(color.red('✗ ') + msg); },
108
+ };
109
+ }
110
+
111
+ module.exports = { color, print, printErr, success, info, warn, json, table, keyValue, spinner };