@motherbase/cli 0.2.6 → 0.2.9
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 -21
- package/README.md +8 -1
- package/bin/.motherbase/config.json +5 -0
- package/bin/motherbase.js +10 -10
- package/package.json +4 -4
- package/src/api.js +39 -16
- package/src/commands/_util.js +106 -106
- package/src/commands/backups.js +16 -16
- package/src/commands/branches.js +113 -113
- package/src/commands/completion.js +90 -90
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db-catalog.js +39 -1
- package/src/commands/db.js +103 -103
- package/src/commands/domains.js +124 -124
- package/src/commands/gen.js +178 -172
- package/src/commands/import.js +55 -55
- package/src/commands/index.js +86 -85
- package/src/commands/inspect.js +178 -178
- package/src/commands/keys.js +72 -72
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +101 -24
- package/src/commands/login.js +9 -3
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +61 -61
- package/src/commands/migrate.js +0 -5
- package/src/commands/migration.js +1 -1
- package/src/commands/network.js +83 -83
- package/src/commands/orgs.js +40 -40
- package/src/commands/postgres-config.js +113 -113
- package/src/commands/projects.js +131 -130
- package/src/commands/queues.js +133 -133
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -66
- package/src/commands/signing.js +130 -130
- package/src/commands/snippets.js +44 -44
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -261
- package/src/commands/test.js +9 -9
- package/src/commands/unlink.js +15 -15
- package/src/commands/users.js +29 -1
- package/src/commands/vault.js +82 -82
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -150
- package/src/config.js +98 -98
- package/src/context.js +106 -106
- package/src/edge-templates.js +194 -194
- package/src/format.js +92 -92
- package/src/inspect/csv.js +22 -22
- package/src/inspect/queries.js +562 -562
- package/src/main.js +145 -66
- package/src/mcp/platform-tools.js +233 -233
- package/src/mcp/protocol.js +152 -152
- package/src/mcp/storage-tools.js +93 -93
- package/src/mcp/tools.js +478 -478
- package/src/migration/commands.js +2 -2
- package/src/migration/files.js +13 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -62
- package/src/ui.js +111 -111
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Registered from commands/migration.js, commands/db.js and commands/test.js.
|
|
10
10
|
// Files are read and written here (see ./files.js for the layout); the server
|
|
11
|
-
// side is /api/admin/db (apps/server controllers/admin-db.controller.ts).
|
|
11
|
+
// side is /api/admin/db (apps/server controllers/admin/admin-db.controller.ts).
|
|
12
12
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
@@ -81,7 +81,7 @@ function writeMigration(ctx, name, sql) {
|
|
|
81
81
|
const version = files.versionAt();
|
|
82
82
|
const file = path.join(ctx.cwd, files.MIGRATIONS_DIR, files.fileNameFor(version, name));
|
|
83
83
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
84
|
-
fs.writeFileSync(file, files.renderFile(sql));
|
|
84
|
+
fs.writeFileSync(file, files.renderFile(sql), { flag: 'wx' });
|
|
85
85
|
return { version, name, file: path.relative(ctx.cwd, file) };
|
|
86
86
|
}
|
|
87
87
|
|
package/src/migration/files.js
CHANGED
|
@@ -53,6 +53,16 @@ function byVersion(a, b) {
|
|
|
53
53
|
return a.version < b.version ? -1 : a.version > b.version ? 1 : 0;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function assertUniqueVersions(migrations) {
|
|
57
|
+
const seen = new Map();
|
|
58
|
+
for (const migration of migrations) {
|
|
59
|
+
if (seen.has(migration.version)) {
|
|
60
|
+
throw new CliError(`Duplicate migration version ${migration.version}: ${seen.get(migration.version)} and ${migration.name}. Give each migration a unique version before continuing.`);
|
|
61
|
+
}
|
|
62
|
+
seen.set(migration.version, migration.name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
// ── File contents ────────────────────────────────────────────────────────────
|
|
57
67
|
|
|
58
68
|
/** Split a file into up and down SQL at the `-- @down` marker. */
|
|
@@ -89,6 +99,7 @@ function readLocalMigrations(cwd) {
|
|
|
89
99
|
const file = path.join(dir, fileName);
|
|
90
100
|
migrations.push({ ...parsed, file, ...splitDown(fs.readFileSync(file, 'utf8')) });
|
|
91
101
|
}
|
|
102
|
+
assertUniqueVersions(migrations);
|
|
92
103
|
return { migrations: migrations.sort(byVersion), skipped };
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -160,6 +171,7 @@ function readTests(cwd, only = []) {
|
|
|
160
171
|
* as "on the remote", a rolled-back one is history, not schema.
|
|
161
172
|
*/
|
|
162
173
|
function reconcile(local, remote) {
|
|
174
|
+
assertUniqueVersions(local);
|
|
163
175
|
const rows = new Map();
|
|
164
176
|
for (const file of local) rows.set(file.version, { version: file.version, name: file.name, local: file, remote: null });
|
|
165
177
|
for (const entry of remote) {
|
|
@@ -207,6 +219,7 @@ function planPush(rows, { includeAll = false } = {}) {
|
|
|
207
219
|
* reverse. A partial down would roll back a fraction of the squash.
|
|
208
220
|
*/
|
|
209
221
|
function squash(migrations) {
|
|
222
|
+
assertUniqueVersions(migrations);
|
|
210
223
|
if (migrations.length < 2) throw new CliError('Nothing to squash: need at least two migrations.');
|
|
211
224
|
const ordered = [...migrations].sort(byVersion);
|
|
212
225
|
const last = ordered[ordered.length - 1];
|
package/src/prompt.js
CHANGED
|
@@ -1,91 +1,91 @@
|
|
|
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. Reads the TTY in raw mode so nothing echoes, masking each typed
|
|
18
|
-
// character with '*'. Handles backspace, Enter, Ctrl+C, Ctrl+D and swallows
|
|
19
|
-
// escape sequences (arrow keys etc.) so they never land in the value. When
|
|
20
|
-
// stdin is not a TTY (pipes, CI) we fall back to a plain, unmasked line read.
|
|
21
|
-
function password(query = 'Password: ') {
|
|
22
|
-
const stdin = process.stdin;
|
|
23
|
-
const stdout = process.stdout;
|
|
24
|
-
if (!stdin.isTTY) return question(query);
|
|
25
|
-
|
|
26
|
-
return new Promise(resolve => {
|
|
27
|
-
let value = '';
|
|
28
|
-
let inEscape = false;
|
|
29
|
-
stdout.write(query);
|
|
30
|
-
stdin.setRawMode(true);
|
|
31
|
-
stdin.resume();
|
|
32
|
-
stdin.setEncoding('utf8');
|
|
33
|
-
|
|
34
|
-
const done = result => {
|
|
35
|
-
stdin.removeListener('data', onData);
|
|
36
|
-
stdin.setRawMode(false);
|
|
37
|
-
stdin.pause();
|
|
38
|
-
stdout.write('\n');
|
|
39
|
-
resolve(result);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const onData = chunk => {
|
|
43
|
-
for (const ch of chunk) {
|
|
44
|
-
if (inEscape) {
|
|
45
|
-
// Terminators of a CSI/escape sequence (e.g. arrow keys): stop swallowing.
|
|
46
|
-
if (/[a-zA-Z~]/.test(ch)) inEscape = false;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
switch (ch) {
|
|
50
|
-
case '\r':
|
|
51
|
-
case '\n':
|
|
52
|
-
case '\u0004': // Ctrl-D
|
|
53
|
-
return done(value);
|
|
54
|
-
case '\u0003': // Ctrl-C
|
|
55
|
-
stdin.setRawMode(false);
|
|
56
|
-
stdin.pause();
|
|
57
|
-
stdout.write('\n');
|
|
58
|
-
process.exit(130);
|
|
59
|
-
return;
|
|
60
|
-
case '\u007f': // Backspace / Delete
|
|
61
|
-
case '\b':
|
|
62
|
-
if (value.length > 0) {
|
|
63
|
-
value = value.slice(0, -1);
|
|
64
|
-
stdout.write('\b \b');
|
|
65
|
-
}
|
|
66
|
-
break;
|
|
67
|
-
case '\u001b': // ESC: start of an escape sequence
|
|
68
|
-
inEscape = true;
|
|
69
|
-
break;
|
|
70
|
-
default:
|
|
71
|
-
// Ignore remaining control characters; mask printable ones.
|
|
72
|
-
if (ch >= ' ') {
|
|
73
|
-
value += ch;
|
|
74
|
-
stdout.write('*');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
stdin.on('data', onData);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function confirm(query, defaultYes = false) {
|
|
85
|
-
const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
|
|
86
|
-
const answer = (await question(query + suffix)).toLowerCase();
|
|
87
|
-
if (!answer) return defaultYes;
|
|
88
|
-
return answer === 'y' || answer === 'yes';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
module.exports = { question, password, confirm };
|
|
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. Reads the TTY in raw mode so nothing echoes, masking each typed
|
|
18
|
+
// character with '*'. Handles backspace, Enter, Ctrl+C, Ctrl+D and swallows
|
|
19
|
+
// escape sequences (arrow keys etc.) so they never land in the value. When
|
|
20
|
+
// stdin is not a TTY (pipes, CI) we fall back to a plain, unmasked line read.
|
|
21
|
+
function password(query = 'Password: ') {
|
|
22
|
+
const stdin = process.stdin;
|
|
23
|
+
const stdout = process.stdout;
|
|
24
|
+
if (!stdin.isTTY) return question(query);
|
|
25
|
+
|
|
26
|
+
return new Promise(resolve => {
|
|
27
|
+
let value = '';
|
|
28
|
+
let inEscape = false;
|
|
29
|
+
stdout.write(query);
|
|
30
|
+
stdin.setRawMode(true);
|
|
31
|
+
stdin.resume();
|
|
32
|
+
stdin.setEncoding('utf8');
|
|
33
|
+
|
|
34
|
+
const done = result => {
|
|
35
|
+
stdin.removeListener('data', onData);
|
|
36
|
+
stdin.setRawMode(false);
|
|
37
|
+
stdin.pause();
|
|
38
|
+
stdout.write('\n');
|
|
39
|
+
resolve(result);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const onData = chunk => {
|
|
43
|
+
for (const ch of chunk) {
|
|
44
|
+
if (inEscape) {
|
|
45
|
+
// Terminators of a CSI/escape sequence (e.g. arrow keys): stop swallowing.
|
|
46
|
+
if (/[a-zA-Z~]/.test(ch)) inEscape = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
switch (ch) {
|
|
50
|
+
case '\r':
|
|
51
|
+
case '\n':
|
|
52
|
+
case '\u0004': // Ctrl-D
|
|
53
|
+
return done(value);
|
|
54
|
+
case '\u0003': // Ctrl-C
|
|
55
|
+
stdin.setRawMode(false);
|
|
56
|
+
stdin.pause();
|
|
57
|
+
stdout.write('\n');
|
|
58
|
+
process.exit(130);
|
|
59
|
+
return;
|
|
60
|
+
case '\u007f': // Backspace / Delete
|
|
61
|
+
case '\b':
|
|
62
|
+
if (value.length > 0) {
|
|
63
|
+
value = value.slice(0, -1);
|
|
64
|
+
stdout.write('\b \b');
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case '\u001b': // ESC: start of an escape sequence
|
|
68
|
+
inEscape = true;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
// Ignore remaining control characters; mask printable ones.
|
|
72
|
+
if (ch >= ' ') {
|
|
73
|
+
value += ch;
|
|
74
|
+
stdout.write('*');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
stdin.on('data', onData);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function confirm(query, defaultYes = false) {
|
|
85
|
+
const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
|
|
86
|
+
const answer = (await question(query + suffix)).toLowerCase();
|
|
87
|
+
if (!answer) return defaultYes;
|
|
88
|
+
return answer === 'y' || answer === 'yes';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { question, password, confirm };
|
package/src/storage/remote.js
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Storage paths in the Supabase CLI's form: `ss:///<bucket>/<key>`. Anything
|
|
4
|
-
// else is a local path. Kept free of I/O so the path rules can be tested alone.
|
|
5
|
-
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const { CliError } = require('../context');
|
|
8
|
-
|
|
9
|
-
const SCHEME = 'ss:///';
|
|
10
|
-
|
|
11
|
-
const isRemote = value => typeof value === 'string' && value.startsWith('ss://');
|
|
12
|
-
|
|
13
|
-
/** `ss:///bucket/a/b` → `{ bucket: 'bucket', key: 'a/b' }`; `ss:///` → `{ bucket: '', key: '' }`. */
|
|
14
|
-
function parseRemote(value) {
|
|
15
|
-
if (!value.startsWith(SCHEME)) {
|
|
16
|
-
throw new CliError(`Storage paths look like ${SCHEME}<bucket>/<key>, got "${value}".`);
|
|
17
|
-
}
|
|
18
|
-
const [bucket = '', ...rest] = value.slice(SCHEME.length).split('/');
|
|
19
|
-
return { bucket, key: rest.join('/') };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function requireBucket(remote, value) {
|
|
23
|
-
if (!remote.bucket) throw new CliError(`"${value}" names no bucket.`);
|
|
24
|
-
return remote;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** A key as a URL path: every segment encoded, the slashes between them kept. */
|
|
28
|
-
const encodeKey = key => key.split('/').map(encodeURIComponent).join('/');
|
|
29
|
-
|
|
30
|
-
/** `a/b/` and `a/b` both mean the prefix `a/b/`; the empty prefix is the bucket root. */
|
|
31
|
-
const asPrefix = key => (key ? key.replace(/\/*$/, '/') : '');
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* One directory level of a listing, as `ls` shows it: the objects directly
|
|
35
|
-
* under `prefix`, and each deeper key collapsed into its first folder (with a
|
|
36
|
-
* trailing slash), deduplicated, in order.
|
|
37
|
-
*/
|
|
38
|
-
function childrenOf(keys, prefix) {
|
|
39
|
-
const seen = new Set();
|
|
40
|
-
for (const key of keys) {
|
|
41
|
-
if (!key.startsWith(prefix)) continue;
|
|
42
|
-
const rest = key.slice(prefix.length);
|
|
43
|
-
const slash = rest.indexOf('/');
|
|
44
|
-
seen.add(slash < 0 ? rest : rest.slice(0, slash + 1));
|
|
45
|
-
}
|
|
46
|
-
return [...seen].filter(Boolean);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Where a single file lands. A destination ending in `/` (or the bucket root)
|
|
51
|
-
* is a folder, so the source's basename is appended, as `cp` does locally.
|
|
52
|
-
*/
|
|
53
|
-
function destinationKey(dstKey, sourceName) {
|
|
54
|
-
return !dstKey || dstKey.endsWith('/') ? `${dstKey}${path.basename(sourceName)}` : dstKey;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The key under `toPrefix` that `key` maps to when moving/copying `fromPrefix`. */
|
|
58
|
-
function rebase(key, fromPrefix, toPrefix) {
|
|
59
|
-
return `${toPrefix}${key.slice(fromPrefix.length)}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
module.exports = { SCHEME, isRemote, parseRemote, requireBucket, encodeKey, asPrefix, childrenOf, destinationKey, rebase };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Storage paths in the Supabase CLI's form: `ss:///<bucket>/<key>`. Anything
|
|
4
|
+
// else is a local path. Kept free of I/O so the path rules can be tested alone.
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { CliError } = require('../context');
|
|
8
|
+
|
|
9
|
+
const SCHEME = 'ss:///';
|
|
10
|
+
|
|
11
|
+
const isRemote = value => typeof value === 'string' && value.startsWith('ss://');
|
|
12
|
+
|
|
13
|
+
/** `ss:///bucket/a/b` → `{ bucket: 'bucket', key: 'a/b' }`; `ss:///` → `{ bucket: '', key: '' }`. */
|
|
14
|
+
function parseRemote(value) {
|
|
15
|
+
if (!value.startsWith(SCHEME)) {
|
|
16
|
+
throw new CliError(`Storage paths look like ${SCHEME}<bucket>/<key>, got "${value}".`);
|
|
17
|
+
}
|
|
18
|
+
const [bucket = '', ...rest] = value.slice(SCHEME.length).split('/');
|
|
19
|
+
return { bucket, key: rest.join('/') };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireBucket(remote, value) {
|
|
23
|
+
if (!remote.bucket) throw new CliError(`"${value}" names no bucket.`);
|
|
24
|
+
return remote;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A key as a URL path: every segment encoded, the slashes between them kept. */
|
|
28
|
+
const encodeKey = key => key.split('/').map(encodeURIComponent).join('/');
|
|
29
|
+
|
|
30
|
+
/** `a/b/` and `a/b` both mean the prefix `a/b/`; the empty prefix is the bucket root. */
|
|
31
|
+
const asPrefix = key => (key ? key.replace(/\/*$/, '/') : '');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One directory level of a listing, as `ls` shows it: the objects directly
|
|
35
|
+
* under `prefix`, and each deeper key collapsed into its first folder (with a
|
|
36
|
+
* trailing slash), deduplicated, in order.
|
|
37
|
+
*/
|
|
38
|
+
function childrenOf(keys, prefix) {
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (const key of keys) {
|
|
41
|
+
if (!key.startsWith(prefix)) continue;
|
|
42
|
+
const rest = key.slice(prefix.length);
|
|
43
|
+
const slash = rest.indexOf('/');
|
|
44
|
+
seen.add(slash < 0 ? rest : rest.slice(0, slash + 1));
|
|
45
|
+
}
|
|
46
|
+
return [...seen].filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Where a single file lands. A destination ending in `/` (or the bucket root)
|
|
51
|
+
* is a folder, so the source's basename is appended, as `cp` does locally.
|
|
52
|
+
*/
|
|
53
|
+
function destinationKey(dstKey, sourceName) {
|
|
54
|
+
return !dstKey || dstKey.endsWith('/') ? `${dstKey}${path.basename(sourceName)}` : dstKey;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The key under `toPrefix` that `key` maps to when moving/copying `fromPrefix`. */
|
|
58
|
+
function rebase(key, fromPrefix, toPrefix) {
|
|
59
|
+
return `${toPrefix}${key.slice(fromPrefix.length)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { SCHEME, isRemote, parseRemote, requireBucket, encodeKey, asPrefix, childrenOf, destinationKey, rebase };
|
package/src/ui.js
CHANGED
|
@@ -1,111 +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}[0m` : 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[K');
|
|
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 };
|
|
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}[0m` : 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[K');
|
|
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 };
|