unbound-cli 0.1.3 → 0.1.4
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/package.json +1 -1
- package/src/api.js +25 -2
- package/src/auth.js +5 -5
- package/src/commands/login.js +26 -16
- package/src/commands/policy.js +10 -26
- package/src/commands/status.js +12 -1
- package/src/commands/tools.js +21 -15
- package/src/commands/user-groups.js +2 -22
- package/src/commands/whoami.js +8 -1
- package/src/index.js +15 -1
- package/src/output.js +23 -8
- package/src/utils.js +23 -0
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -5,9 +5,22 @@ const config = require('./config');
|
|
|
5
5
|
|
|
6
6
|
const USER_AGENT = 'UnboundCLI/0.1.0';
|
|
7
7
|
|
|
8
|
+
const STATUS_MESSAGES = {
|
|
9
|
+
400: 'Bad request. Check your input and try again.',
|
|
10
|
+
401: 'Authentication failed. Your API key may be invalid or expired. Run `unbound login` to re-authenticate.',
|
|
11
|
+
403: 'Permission denied. You may not have the required role for this action.',
|
|
12
|
+
404: 'Not found. Check the ID and try again.',
|
|
13
|
+
409: 'Conflict. The resource may already exist.',
|
|
14
|
+
422: 'Invalid input. Check your parameters and try again.',
|
|
15
|
+
429: 'Too many requests. Please wait and try again.',
|
|
16
|
+
500: 'Something went wrong on the server. Please try again later.',
|
|
17
|
+
502: 'The server is temporarily unavailable. Please try again later.',
|
|
18
|
+
503: 'The server is temporarily unavailable. Please try again later.',
|
|
19
|
+
};
|
|
20
|
+
|
|
8
21
|
class ApiError extends Error {
|
|
9
22
|
constructor(statusCode, body) {
|
|
10
|
-
const message = body?.error || body?.message || `HTTP ${statusCode}`;
|
|
23
|
+
const message = body?.error || body?.message || STATUS_MESSAGES[statusCode] || `Unexpected error (HTTP ${statusCode})`;
|
|
11
24
|
super(message);
|
|
12
25
|
this.name = 'ApiError';
|
|
13
26
|
this.statusCode = statusCode;
|
|
@@ -66,7 +79,17 @@ function request(method, path, { body, query, apiKey } = {}) {
|
|
|
66
79
|
});
|
|
67
80
|
});
|
|
68
81
|
|
|
69
|
-
req.on('error',
|
|
82
|
+
req.on('error', (err) => {
|
|
83
|
+
if (err.code === 'ECONNREFUSED') {
|
|
84
|
+
reject(new Error(`Could not connect to ${url.host}. Check that the server is running.`));
|
|
85
|
+
} else if (err.code === 'ENOTFOUND') {
|
|
86
|
+
reject(new Error(`Could not resolve ${url.host}. Check your internet connection and the API URL.`));
|
|
87
|
+
} else if (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT') {
|
|
88
|
+
reject(new Error(`Connection to ${url.host} timed out. Please try again.`));
|
|
89
|
+
} else {
|
|
90
|
+
reject(new Error(`Network error: ${err.message}`));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
70
93
|
if (payload) req.write(payload);
|
|
71
94
|
req.end();
|
|
72
95
|
});
|
package/src/auth.js
CHANGED
|
@@ -70,9 +70,9 @@ async function loginWithBrowser(frontendUrl) {
|
|
|
70
70
|
|
|
71
71
|
const open = (await import('open')).default;
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
output.info('Opening browser for authentication...');
|
|
74
|
+
output.info(`If the browser does not open, visit:\n${authUrl}`);
|
|
75
|
+
output.info('Waiting for authentication...');
|
|
76
76
|
|
|
77
77
|
await open(authUrl);
|
|
78
78
|
|
|
@@ -100,14 +100,14 @@ async function ensureLoggedIn() {
|
|
|
100
100
|
return true;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
output.warn('Not logged in. Opening browser to authenticate
|
|
103
|
+
output.warn('Not logged in. Opening browser to authenticate...');
|
|
104
104
|
const frontendUrl = config.getFrontendUrl();
|
|
105
105
|
const result = await loginWithBrowser(frontendUrl);
|
|
106
106
|
|
|
107
107
|
const parts = [];
|
|
108
108
|
if (result.email) parts.push(`as ${result.email}`);
|
|
109
109
|
if (result.orgName) parts.push(`to ${result.orgName}`);
|
|
110
|
-
output.success(`Logged in successfully${parts.length ? ' ' + parts.join(' ') : ''}
|
|
110
|
+
output.success(`Logged in successfully${parts.length ? ' ' + parts.join(' ') : ''}.`);
|
|
111
111
|
|
|
112
112
|
return true;
|
|
113
113
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const config = require('../config');
|
|
2
2
|
const output = require('../output');
|
|
3
|
+
const api = require('../api');
|
|
3
4
|
const { loginWithBrowser } = require('../auth');
|
|
4
5
|
|
|
5
6
|
function register(program) {
|
|
@@ -8,7 +9,7 @@ function register(program) {
|
|
|
8
9
|
.description('Authenticate with Unbound. Opens a browser for interactive login, or use --api-key for CI/CD environments.')
|
|
9
10
|
.option('--api-key <key>', 'Authenticate with an API key directly (non-interactive)')
|
|
10
11
|
.option('--base-url <url>', 'Set a custom API base URL before logging in')
|
|
11
|
-
.option('--
|
|
12
|
+
.option('--domain <domain>', 'Use a custom domain for login (e.g. custom.example.com)')
|
|
12
13
|
.addHelpText('after', `
|
|
13
14
|
Authentication methods:
|
|
14
15
|
Browser login (default):
|
|
@@ -21,14 +22,14 @@ Authentication methods:
|
|
|
21
22
|
API key directly without browser interaction.
|
|
22
23
|
|
|
23
24
|
Options:
|
|
25
|
+
--domain sets a custom domain for organizations with self-hosted frontends.
|
|
24
26
|
--base-url sets the backend API URL before authenticating (persisted).
|
|
25
|
-
--frontend-url sets the frontend URL for browser login (persisted).
|
|
26
27
|
|
|
27
28
|
Examples:
|
|
28
|
-
$ unbound login
|
|
29
|
-
$ unbound login --
|
|
30
|
-
$ unbound login --
|
|
31
|
-
$ unbound login --base-url
|
|
29
|
+
$ unbound login # Login via default gateway
|
|
30
|
+
$ unbound login --domain custom.example.com # Login via custom domain
|
|
31
|
+
$ unbound login --api-key sk-abc123 # Non-interactive login
|
|
32
|
+
$ unbound login --base-url http://localhost:8000 # Use local backend
|
|
32
33
|
`)
|
|
33
34
|
.action(async (opts) => {
|
|
34
35
|
try {
|
|
@@ -36,22 +37,31 @@ Examples:
|
|
|
36
37
|
config.setBaseUrl(opts.baseUrl);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
config.setFrontendUrl(opts.frontendUrl);
|
|
41
|
-
}
|
|
40
|
+
let apiKey;
|
|
42
41
|
|
|
43
42
|
if (opts.apiKey) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
apiKey = opts.apiKey;
|
|
44
|
+
} else {
|
|
45
|
+
// Determine frontend URL: --domain flag or default
|
|
46
|
+
let frontendUrl;
|
|
47
|
+
if (opts.domain) {
|
|
48
|
+
frontendUrl = opts.domain.startsWith('http') ? opts.domain : `https://${opts.domain}`;
|
|
49
|
+
} else {
|
|
50
|
+
frontendUrl = config.getFrontendUrl();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await loginWithBrowser(frontendUrl);
|
|
54
|
+
apiKey = config.getApiKey();
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
// Store the API key and validate through the backend
|
|
58
|
+
config.setApiKey(apiKey);
|
|
59
|
+
await api.get('/api/v1/users/privileges/');
|
|
51
60
|
|
|
61
|
+
const cfg = config.readConfig();
|
|
52
62
|
const parts = [];
|
|
53
|
-
if (
|
|
54
|
-
if (
|
|
63
|
+
if (cfg.email) parts.push(`as ${cfg.email}`);
|
|
64
|
+
if (cfg.org_name) parts.push(`to ${cfg.org_name}`);
|
|
55
65
|
output.success(`Logged in successfully${parts.length ? ' ' + parts.join(' ') : ''}.`);
|
|
56
66
|
} catch (err) {
|
|
57
67
|
output.error(err.message);
|
package/src/commands/policy.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
const readline = require('readline');
|
|
2
1
|
const config = require('../config');
|
|
3
2
|
const api = require('../api');
|
|
4
3
|
const output = require('../output');
|
|
5
|
-
|
|
6
|
-
function formatDate(dateStr) {
|
|
7
|
-
if (!dateStr) return '-';
|
|
8
|
-
return new Date(dateStr).toLocaleDateString();
|
|
9
|
-
}
|
|
4
|
+
const { formatDate, confirm, parseCommaSeparated } = require('../utils');
|
|
10
5
|
|
|
11
6
|
function formatScope(groups) {
|
|
12
7
|
if (!groups || groups.length === 0) return '-';
|
|
@@ -52,21 +47,6 @@ function displayEffectivePolicies(data, opts) {
|
|
|
52
47
|
]);
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
function confirm(message) {
|
|
56
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
rl.question(`${message} (y/N) `, (answer) => {
|
|
59
|
-
rl.close();
|
|
60
|
-
resolve(answer.toLowerCase() === 'y');
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseCommaSeparated(value) {
|
|
66
|
-
if (!value) return undefined;
|
|
67
|
-
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
50
|
function register(program) {
|
|
71
51
|
const policy = program
|
|
72
52
|
.command('policy')
|
|
@@ -335,22 +315,25 @@ Examples:
|
|
|
335
315
|
|
|
336
316
|
// Display each section of the form data
|
|
337
317
|
if (data.user_groups) {
|
|
338
|
-
console.log('
|
|
318
|
+
console.log('');
|
|
319
|
+
output.info('User Groups');
|
|
339
320
|
output.table(data.user_groups, [
|
|
340
321
|
{ key: 'id', header: 'ID' },
|
|
341
322
|
{ key: 'name', header: 'Name' },
|
|
342
323
|
]);
|
|
343
324
|
}
|
|
344
325
|
|
|
345
|
-
if (data.tool_types) {
|
|
346
|
-
console.log('
|
|
326
|
+
if (data.tool_types && data.tool_types.length > 0) {
|
|
327
|
+
console.log('');
|
|
328
|
+
output.info('Tool Types');
|
|
347
329
|
for (const type of data.tool_types) {
|
|
348
330
|
console.log(` ${type}`);
|
|
349
331
|
}
|
|
350
332
|
}
|
|
351
333
|
|
|
352
334
|
if (data.guardrails) {
|
|
353
|
-
console.log('
|
|
335
|
+
console.log('');
|
|
336
|
+
output.info('Guardrails');
|
|
354
337
|
output.table(data.guardrails, [
|
|
355
338
|
{ key: 'id', header: 'ID' },
|
|
356
339
|
{ key: 'name', header: 'Name' },
|
|
@@ -359,7 +342,8 @@ Examples:
|
|
|
359
342
|
}
|
|
360
343
|
|
|
361
344
|
if (data.models) {
|
|
362
|
-
console.log('
|
|
345
|
+
console.log('');
|
|
346
|
+
output.info('Models');
|
|
363
347
|
output.table(data.models, [
|
|
364
348
|
{ key: 'id', header: 'ID' },
|
|
365
349
|
{ key: 'name', header: 'Name' },
|
package/src/commands/status.js
CHANGED
|
@@ -18,8 +18,10 @@ Output fields:
|
|
|
18
18
|
|
|
19
19
|
Examples:
|
|
20
20
|
$ unbound status
|
|
21
|
+
$ unbound status --json
|
|
21
22
|
`)
|
|
22
|
-
.
|
|
23
|
+
.option('--json', 'Output raw JSON')
|
|
24
|
+
.action(async (opts) => {
|
|
23
25
|
try {
|
|
24
26
|
const loggedIn = config.isLoggedIn();
|
|
25
27
|
const cfg = config.readConfig();
|
|
@@ -48,6 +50,15 @@ Examples:
|
|
|
48
50
|
}
|
|
49
51
|
pairs.push(['API status', connectivity]);
|
|
50
52
|
|
|
53
|
+
if (opts.json) {
|
|
54
|
+
const obj = {};
|
|
55
|
+
for (const [key, value] of pairs) {
|
|
56
|
+
obj[key.toLowerCase().replace(/\s+/g, '_')] = value;
|
|
57
|
+
}
|
|
58
|
+
output.json(obj);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
output.keyValue(pairs);
|
|
52
63
|
} catch (err) {
|
|
53
64
|
output.error(err.message);
|
package/src/commands/tools.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
const config = require('../config');
|
|
2
2
|
const api = require('../api');
|
|
3
3
|
const output = require('../output');
|
|
4
|
-
|
|
5
|
-
function formatDate(dateStr) {
|
|
6
|
-
if (!dateStr) return '-';
|
|
7
|
-
return new Date(dateStr).toLocaleDateString();
|
|
8
|
-
}
|
|
4
|
+
const { formatDate } = require('../utils');
|
|
9
5
|
|
|
10
6
|
const SUPPORTED_TOOL_TYPES = [
|
|
11
7
|
'CLAUDE_CODE',
|
|
@@ -92,8 +88,10 @@ Examples:
|
|
|
92
88
|
$ unbound tools connect CLAUDE_CODE
|
|
93
89
|
$ unbound tools connect CURSOR
|
|
94
90
|
$ unbound tools connect GEMINI_CLI
|
|
91
|
+
$ unbound tools connect CLAUDE_CODE --json
|
|
95
92
|
`)
|
|
96
|
-
.
|
|
93
|
+
.option('--json', 'Output raw JSON')
|
|
94
|
+
.action(async (toolType, opts) => {
|
|
97
95
|
try {
|
|
98
96
|
if (!config.isLoggedIn()) {
|
|
99
97
|
output.error('Not logged in. Run `unbound login` first.');
|
|
@@ -110,6 +108,11 @@ Examples:
|
|
|
110
108
|
|
|
111
109
|
const data = await api.post('/api/v1/agents/connect/', { body: { tool_type: normalized } });
|
|
112
110
|
|
|
111
|
+
if (opts.json) {
|
|
112
|
+
output.json(data);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
113
116
|
output.success(`Connected ${normalized}.`);
|
|
114
117
|
output.keyValue([
|
|
115
118
|
['Tool Type', data.tool_type],
|
|
@@ -153,18 +156,21 @@ Examples:
|
|
|
153
156
|
return;
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
159
|
+
output.keyValue([
|
|
160
|
+
['Restriction Enabled', data.restriction_enabled !== undefined ? String(data.restriction_enabled) : '-'],
|
|
161
|
+
]);
|
|
160
162
|
|
|
161
163
|
if (data.approved_tools && data.approved_tools.length > 0) {
|
|
162
|
-
console.log('
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
console.log('');
|
|
165
|
+
output.table(data.approved_tools.map((tool) => {
|
|
166
|
+
const name = typeof tool === 'string' ? tool : tool.tool_type || tool.name || JSON.stringify(tool);
|
|
167
|
+
return { name };
|
|
168
|
+
}), [
|
|
169
|
+
{ key: 'name', header: 'Tool Type' },
|
|
170
|
+
]);
|
|
166
171
|
} else {
|
|
167
|
-
console.log('
|
|
172
|
+
console.log('');
|
|
173
|
+
output.info('No approved tools configured.');
|
|
168
174
|
}
|
|
169
175
|
} catch (err) {
|
|
170
176
|
output.error(err.message);
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
const readline = require('readline');
|
|
2
1
|
const config = require('../config');
|
|
3
2
|
const api = require('../api');
|
|
4
3
|
const output = require('../output');
|
|
5
|
-
|
|
6
|
-
function formatDate(dateStr) {
|
|
7
|
-
if (!dateStr) return '-';
|
|
8
|
-
return new Date(dateStr).toLocaleDateString();
|
|
9
|
-
}
|
|
4
|
+
const { formatDate, confirm, parseCommaSeparated } = require('../utils');
|
|
10
5
|
|
|
11
6
|
function displayGroup(group) {
|
|
12
7
|
output.keyValue([
|
|
@@ -19,7 +14,7 @@ function displayGroup(group) {
|
|
|
19
14
|
]);
|
|
20
15
|
|
|
21
16
|
if (!group.all_org_users && group.members && group.members.length > 0) {
|
|
22
|
-
console.log('
|
|
17
|
+
console.log('');
|
|
23
18
|
output.table(group.members, [
|
|
24
19
|
{ key: 'id', header: 'ID' },
|
|
25
20
|
{ key: 'email', header: 'Email' },
|
|
@@ -29,21 +24,6 @@ function displayGroup(group) {
|
|
|
29
24
|
}
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
function confirm(message) {
|
|
33
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
-
return new Promise((resolve) => {
|
|
35
|
-
rl.question(`${message} (y/N) `, (answer) => {
|
|
36
|
-
rl.close();
|
|
37
|
-
resolve(answer.toLowerCase() === 'y');
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function parseCommaSeparated(value) {
|
|
43
|
-
if (!value) return undefined;
|
|
44
|
-
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
27
|
function register(program) {
|
|
48
28
|
const userGroups = program
|
|
49
29
|
.command('user-groups')
|
package/src/commands/whoami.js
CHANGED
|
@@ -21,8 +21,10 @@ Output fields:
|
|
|
21
21
|
|
|
22
22
|
Examples:
|
|
23
23
|
$ unbound whoami
|
|
24
|
+
$ unbound whoami --json
|
|
24
25
|
`)
|
|
25
|
-
.
|
|
26
|
+
.option('--json', 'Output raw JSON')
|
|
27
|
+
.action(async (opts) => {
|
|
26
28
|
try {
|
|
27
29
|
if (!config.isLoggedIn()) {
|
|
28
30
|
output.error('Not logged in. Run `unbound login` first.');
|
|
@@ -34,6 +36,11 @@ Examples:
|
|
|
34
36
|
const privileges = await api.get('/api/v1/users/privileges/');
|
|
35
37
|
const role = roleFromPrivileges(privileges);
|
|
36
38
|
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
output.json({ email: cfg.email || null, organization: cfg.org_name || null, role });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
output.keyValue([
|
|
38
45
|
['Email', cfg.email || '-'],
|
|
39
46
|
['Organization', cfg.org_name || '-'],
|
package/src/index.js
CHANGED
|
@@ -114,8 +114,22 @@ configCmd
|
|
|
114
114
|
configCmd
|
|
115
115
|
.command('show')
|
|
116
116
|
.description('Show all current configuration values.')
|
|
117
|
-
.
|
|
117
|
+
.option('--json', 'Output raw JSON')
|
|
118
|
+
.action((opts) => {
|
|
118
119
|
const cfg = config.readConfig();
|
|
120
|
+
|
|
121
|
+
if (opts.json) {
|
|
122
|
+
output.json({
|
|
123
|
+
config_file: config.CONFIG_FILE,
|
|
124
|
+
api_base_url: config.getBaseUrl(),
|
|
125
|
+
frontend_url: config.getFrontendUrl(),
|
|
126
|
+
logged_in: config.isLoggedIn(),
|
|
127
|
+
email: cfg.email || null,
|
|
128
|
+
organization: cfg.org_name || null,
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
output.keyValue([
|
|
120
134
|
['Config file', config.CONFIG_FILE],
|
|
121
135
|
['API base URL', config.getBaseUrl()],
|
package/src/output.js
CHANGED
|
@@ -3,9 +3,19 @@
|
|
|
3
3
|
* Provides table, JSON, and key-value output for terminal display.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
7
|
+
const c = {
|
|
8
|
+
green: (s) => useColor ? `\x1b[32m${s}\x1b[0m` : s,
|
|
9
|
+
red: (s) => useColor ? `\x1b[31m${s}\x1b[0m` : s,
|
|
10
|
+
yellow: (s) => useColor ? `\x1b[33m${s}\x1b[0m` : s,
|
|
11
|
+
cyan: (s) => useColor ? `\x1b[36m${s}\x1b[0m` : s,
|
|
12
|
+
dim: (s) => useColor ? `\x1b[2m${s}\x1b[0m` : s,
|
|
13
|
+
bold: (s) => useColor ? `\x1b[1m${s}\x1b[0m` : s,
|
|
14
|
+
};
|
|
15
|
+
|
|
6
16
|
function table(rows, columns) {
|
|
7
17
|
if (!rows || rows.length === 0) {
|
|
8
|
-
console.log('No results found.');
|
|
18
|
+
console.log(c.dim('No results found.'));
|
|
9
19
|
return;
|
|
10
20
|
}
|
|
11
21
|
|
|
@@ -22,8 +32,8 @@ function table(rows, columns) {
|
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
// Header
|
|
25
|
-
const header = columns.map((col) => col.header.padEnd(widths[col.key])).join(' ');
|
|
26
|
-
const separator = columns.map((col) => '
|
|
35
|
+
const header = columns.map((col) => c.bold(c.dim(col.header.padEnd(widths[col.key])))).join(' ');
|
|
36
|
+
const separator = columns.map((col) => '\u2500'.repeat(widths[col.key])).join(' ');
|
|
27
37
|
console.log(header);
|
|
28
38
|
console.log(separator);
|
|
29
39
|
|
|
@@ -46,20 +56,25 @@ function json(data) {
|
|
|
46
56
|
function keyValue(pairs) {
|
|
47
57
|
const maxKey = Math.max(...pairs.map(([k]) => k.length));
|
|
48
58
|
for (const [key, value] of pairs) {
|
|
49
|
-
console.log(`${key.padEnd(maxKey)} ${value ?? '-'}`);
|
|
59
|
+
console.log(`${c.dim(key.padEnd(maxKey))} ${value ?? '-'}`);
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
function success(msg) {
|
|
54
|
-
|
|
64
|
+
const check = process.stdout.isTTY ? '\u2714' : '\u221A';
|
|
65
|
+
console.log(c.green(`${check} ${msg}`));
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
function error(msg) {
|
|
58
|
-
console.error(`Error: ${msg}`);
|
|
69
|
+
console.error(c.red(c.bold(`Error: ${msg}`)));
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
function warn(msg) {
|
|
62
|
-
console.error(`Warning: ${msg}`);
|
|
73
|
+
console.error(c.yellow(`Warning: ${msg}`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function info(msg) {
|
|
77
|
+
console.error(c.cyan(`\u2139 ${msg}`));
|
|
63
78
|
}
|
|
64
79
|
|
|
65
|
-
module.exports = { table, json, keyValue, success, error, warn };
|
|
80
|
+
module.exports = { table, json, keyValue, success, error, warn, info };
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
|
|
3
|
+
function formatDate(dateStr) {
|
|
4
|
+
if (!dateStr) return '-';
|
|
5
|
+
return new Date(dateStr).toLocaleDateString();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function confirm(message) {
|
|
9
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
12
|
+
rl.close();
|
|
13
|
+
resolve(answer.toLowerCase() === 'y');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseCommaSeparated(value) {
|
|
19
|
+
if (!value) return undefined;
|
|
20
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = { formatDate, confirm, parseCommaSeparated };
|