unbound-cli 0.1.6 → 0.1.8
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 +7 -1
- package/src/auth.js +11 -1
- package/src/commands/login.js +16 -9
- package/src/commands/logout.js +9 -5
- package/src/commands/status.js +18 -14
- package/src/commands/whoami.js +11 -8
- package/src/config.js +1 -1
- package/src/index.js +79 -50
- package/src/output.js +31 -1
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -3,7 +3,8 @@ const http = require('http');
|
|
|
3
3
|
const { URL } = require('url');
|
|
4
4
|
const config = require('./config');
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const { version } = require('../package.json');
|
|
7
|
+
const USER_AGENT = `UnboundCLI/${version}`;
|
|
7
8
|
|
|
8
9
|
const STATUS_MESSAGES = {
|
|
9
10
|
400: 'Bad request. Check your input and try again.',
|
|
@@ -79,6 +80,11 @@ function request(method, path, { body, query, apiKey } = {}) {
|
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
req.setTimeout(30000, () => {
|
|
84
|
+
req.destroy();
|
|
85
|
+
reject(new Error(`Request to ${url.host} timed out after 30s. Please try again.`));
|
|
86
|
+
});
|
|
87
|
+
|
|
82
88
|
req.on('error', (err) => {
|
|
83
89
|
if (err.code === 'ECONNREFUSED') {
|
|
84
90
|
reject(new Error(`Could not connect to ${url.host}. Check that the server is running.`));
|
package/src/auth.js
CHANGED
|
@@ -74,19 +74,29 @@ async function loginWithBrowser(frontendUrl) {
|
|
|
74
74
|
output.info(`If the browser does not open, visit:\n${authUrl}`);
|
|
75
75
|
output.info('Waiting for authentication...');
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
try {
|
|
78
|
+
await open(authUrl);
|
|
79
|
+
} catch {
|
|
80
|
+
// Browser failed to open — URL is already printed above
|
|
81
|
+
}
|
|
78
82
|
|
|
79
83
|
const timeout = setTimeout(() => {
|
|
80
84
|
callbackReject(new Error('Authentication timed out after 120 seconds'));
|
|
81
85
|
server.close();
|
|
82
86
|
}, 120_000);
|
|
83
87
|
|
|
88
|
+
const reminder = setInterval(() => {
|
|
89
|
+
output.info('Still waiting for browser authentication...');
|
|
90
|
+
}, 30_000);
|
|
91
|
+
|
|
84
92
|
try {
|
|
85
93
|
const result = await callbackPromise;
|
|
86
94
|
clearTimeout(timeout);
|
|
95
|
+
clearInterval(reminder);
|
|
87
96
|
return result;
|
|
88
97
|
} catch (err) {
|
|
89
98
|
clearTimeout(timeout);
|
|
99
|
+
clearInterval(reminder);
|
|
90
100
|
throw err;
|
|
91
101
|
}
|
|
92
102
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { Option } = require('commander');
|
|
1
2
|
const config = require('../config');
|
|
2
3
|
const output = require('../output');
|
|
3
4
|
const api = require('../api');
|
|
@@ -8,7 +9,7 @@ function register(program) {
|
|
|
8
9
|
.command('login')
|
|
9
10
|
.description('Authenticate with Unbound. Opens a browser for interactive login, or use --api-key for CI/CD environments.')
|
|
10
11
|
.option('--api-key <key>', 'Authenticate with an API key directly (non-interactive)')
|
|
11
|
-
.
|
|
12
|
+
.addOption(new Option('--base-url <url>', 'Set a custom API base URL').hideHelp())
|
|
12
13
|
.option('--domain <domain>', 'Use a custom domain for login (e.g. custom.example.com)')
|
|
13
14
|
.addHelpText('after', `
|
|
14
15
|
Authentication methods:
|
|
@@ -23,13 +24,11 @@ Authentication methods:
|
|
|
23
24
|
|
|
24
25
|
Options:
|
|
25
26
|
--domain sets a custom domain for organizations with self-hosted frontends.
|
|
26
|
-
--base-url sets the backend API URL before authenticating (persisted).
|
|
27
27
|
|
|
28
28
|
Examples:
|
|
29
29
|
$ unbound login # Login via default gateway
|
|
30
30
|
$ unbound login --domain custom.example.com # Login via custom domain
|
|
31
31
|
$ unbound login --api-key sk-abc123 # Non-interactive login
|
|
32
|
-
$ unbound login --base-url http://localhost:8000 # Use local backend
|
|
33
32
|
`)
|
|
34
33
|
.action(async (opts) => {
|
|
35
34
|
try {
|
|
@@ -41,23 +40,31 @@ Examples:
|
|
|
41
40
|
|
|
42
41
|
if (opts.apiKey) {
|
|
43
42
|
apiKey = opts.apiKey;
|
|
43
|
+
// Validate the key before storing
|
|
44
|
+
const spin = output.spinner('Validating API key...');
|
|
45
|
+
try {
|
|
46
|
+
await api.get('/api/v1/users/privileges/', { apiKey });
|
|
47
|
+
spin.stop();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
spin.fail('Invalid API key. Check your key and try again.');
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
config.setApiKey(apiKey);
|
|
44
54
|
} else {
|
|
45
|
-
//
|
|
55
|
+
// Browser flow — key is already saved by auth.loginWithBrowser()
|
|
46
56
|
let frontendUrl;
|
|
47
57
|
if (opts.domain) {
|
|
48
58
|
frontendUrl = opts.domain.startsWith('http') ? opts.domain : `https://${opts.domain}`;
|
|
49
59
|
} else {
|
|
50
60
|
frontendUrl = config.getFrontendUrl();
|
|
51
61
|
}
|
|
52
|
-
|
|
53
62
|
await loginWithBrowser(frontendUrl);
|
|
54
63
|
apiKey = config.getApiKey();
|
|
64
|
+
// Validate the stored key
|
|
65
|
+
await api.get('/api/v1/users/privileges/');
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
// Store the API key and validate through the backend
|
|
58
|
-
config.setApiKey(apiKey);
|
|
59
|
-
await api.get('/api/v1/users/privileges/');
|
|
60
|
-
|
|
61
68
|
const cfg = config.readConfig();
|
|
62
69
|
const parts = [];
|
|
63
70
|
if (cfg.email) parts.push(`as ${cfg.email}`);
|
package/src/commands/logout.js
CHANGED
|
@@ -4,20 +4,24 @@ const output = require('../output');
|
|
|
4
4
|
function register(program) {
|
|
5
5
|
program
|
|
6
6
|
.command('logout')
|
|
7
|
-
.description('Log out of Unbound. Removes stored credentials
|
|
7
|
+
.description('Log out of Unbound. Removes stored credentials from ~/.unbound/config.json while preserving custom URL settings.')
|
|
8
8
|
.addHelpText('after', `
|
|
9
9
|
What this does:
|
|
10
|
-
Clears
|
|
10
|
+
Clears stored credentials (API key, email, organization) from
|
|
11
11
|
~/.unbound/config.json. Custom URL settings (base_url, frontend_url)
|
|
12
|
-
are
|
|
12
|
+
are preserved so you don't need to reconfigure them.
|
|
13
13
|
|
|
14
14
|
Examples:
|
|
15
15
|
$ unbound logout
|
|
16
16
|
`)
|
|
17
17
|
.action(() => {
|
|
18
18
|
try {
|
|
19
|
-
config.
|
|
20
|
-
|
|
19
|
+
const cfg = config.readConfig();
|
|
20
|
+
delete cfg.api_key;
|
|
21
|
+
delete cfg.email;
|
|
22
|
+
delete cfg.org_name;
|
|
23
|
+
config.writeConfig(cfg);
|
|
24
|
+
output.success('Logged out successfully. Custom URL settings preserved.');
|
|
21
25
|
} catch (err) {
|
|
22
26
|
output.error(err.message);
|
|
23
27
|
process.exitCode = 1;
|
package/src/commands/status.js
CHANGED
|
@@ -8,13 +8,12 @@ function register(program) {
|
|
|
8
8
|
.description('Show the current CLI status including config location, login state, and API connectivity. Useful for debugging connection issues.')
|
|
9
9
|
.addHelpText('after', `
|
|
10
10
|
Output fields:
|
|
11
|
-
Config file
|
|
12
|
-
Logged in
|
|
13
|
-
Email
|
|
14
|
-
Organization
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
API status - Connectivity check result (Connected / Error)
|
|
11
|
+
Config file - Path to the config file (~/.unbound/config.json)
|
|
12
|
+
Logged in - Whether credentials are stored (Yes/No)
|
|
13
|
+
Email - The authenticated user's email (if logged in)
|
|
14
|
+
Organization - The organization name (if logged in)
|
|
15
|
+
Unbound Gateway - The Unbound gateway URL (if logged in)
|
|
16
|
+
API status - Connectivity check result (Connected / Error)
|
|
18
17
|
|
|
19
18
|
Examples:
|
|
20
19
|
$ unbound status
|
|
@@ -34,28 +33,33 @@ Examples:
|
|
|
34
33
|
if (loggedIn) {
|
|
35
34
|
pairs.push(['Email', cfg.email || '-']);
|
|
36
35
|
pairs.push(['Organization', cfg.org_name || '-']);
|
|
37
|
-
pairs.push(['
|
|
38
|
-
pairs.push(['Frontend URL', config.getFrontendUrl()]);
|
|
36
|
+
pairs.push(['Unbound Gateway', config.getFrontendUrl()]);
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
// Check API connectivity
|
|
42
40
|
let connectivity = 'Not checked (not logged in)';
|
|
43
41
|
if (loggedIn) {
|
|
42
|
+
const spin = output.spinner('Checking API connectivity...');
|
|
44
43
|
try {
|
|
45
44
|
await api.get('/api/v1/users/privileges/');
|
|
45
|
+
spin.stop();
|
|
46
46
|
connectivity = 'Connected';
|
|
47
47
|
} catch (err) {
|
|
48
|
+
spin.stop();
|
|
48
49
|
connectivity = `Error: ${err.message}`;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
pairs.push(['API status', connectivity]);
|
|
52
53
|
|
|
53
54
|
if (opts.json) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
output.json({
|
|
56
|
+
config_file: config.CONFIG_FILE,
|
|
57
|
+
logged_in: loggedIn,
|
|
58
|
+
email: loggedIn ? (cfg.email || null) : null,
|
|
59
|
+
organization: loggedIn ? (cfg.org_name || null) : null,
|
|
60
|
+
gateway_url: loggedIn ? config.getFrontendUrl() : null,
|
|
61
|
+
api_status: connectivity,
|
|
62
|
+
});
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
65
|
|
package/src/commands/whoami.js
CHANGED
|
@@ -25,15 +25,18 @@ Examples:
|
|
|
25
25
|
`)
|
|
26
26
|
.option('--json', 'Output raw JSON')
|
|
27
27
|
.action(async (opts) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
28
|
+
if (!config.isLoggedIn()) {
|
|
29
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
const cfg = config.readConfig();
|
|
35
|
+
const spin = output.spinner('Fetching user info...');
|
|
36
|
+
try {
|
|
36
37
|
const privileges = await api.get('/api/v1/users/privileges/');
|
|
38
|
+
spin.stop();
|
|
39
|
+
|
|
37
40
|
const role = roleFromPrivileges(privileges);
|
|
38
41
|
|
|
39
42
|
if (opts.json) {
|
|
@@ -47,7 +50,7 @@ Examples:
|
|
|
47
50
|
['Role', role],
|
|
48
51
|
]);
|
|
49
52
|
} catch (err) {
|
|
50
|
-
|
|
53
|
+
spin.fail(err.message);
|
|
51
54
|
process.exitCode = 1;
|
|
52
55
|
}
|
|
53
56
|
});
|
package/src/config.js
CHANGED
|
@@ -19,7 +19,7 @@ function readConfig() {
|
|
|
19
19
|
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
20
20
|
}
|
|
21
21
|
} catch {
|
|
22
|
-
|
|
22
|
+
console.error(`\x1b[33mWarning: Config file at ${CONFIG_FILE} is corrupted. Using defaults.\x1b[0m`);
|
|
23
23
|
}
|
|
24
24
|
return {};
|
|
25
25
|
}
|
package/src/index.js
CHANGED
|
@@ -9,31 +9,71 @@ const program = new Command();
|
|
|
9
9
|
|
|
10
10
|
program
|
|
11
11
|
.name('unbound')
|
|
12
|
-
.description('Unbound CLI - Manage your AI Gateway from the command line.\n\nUnbound is an AI gateway that provides centralized policy management,\ncost controls, and security guardrails for AI coding tools like\nClaude Code, Cursor, Gemini CLI, and more.')
|
|
12
|
+
.description('Unbound CLI - Manage your AI Gateway from the command line.\n\nUnbound is an AI gateway that provides centralized policy management,\ncost controls, and security guardrails for AI coding tools like\nClaude Code, Cursor, Gemini CLI, Codex, and more.')
|
|
13
13
|
.version(version)
|
|
14
14
|
.addHelpText('after', `
|
|
15
|
-
|
|
16
|
-
$ unbound login
|
|
17
|
-
$ unbound login --api-key <key>
|
|
18
|
-
$ unbound
|
|
19
|
-
$ unbound
|
|
20
|
-
$ unbound
|
|
21
|
-
$ unbound
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
$ unbound setup
|
|
26
|
-
$ unbound
|
|
27
|
-
$ unbound
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
AUTHENTICATION
|
|
16
|
+
$ unbound login Sign in via browser
|
|
17
|
+
$ unbound login --api-key <key> Sign in with an API key (for CI/CD)
|
|
18
|
+
$ unbound login --domain custom.co Sign in via custom domain
|
|
19
|
+
$ unbound logout Remove stored credentials
|
|
20
|
+
$ unbound whoami Show current user and organization
|
|
21
|
+
$ unbound status Show CLI status and API connectivity
|
|
22
|
+
|
|
23
|
+
TOOL SETUP
|
|
24
|
+
Automated setup (installs hooks, sets env vars, configures tool):
|
|
25
|
+
$ unbound setup cursor Set up Cursor
|
|
26
|
+
$ unbound setup claude-code Set up Claude Code (interactive mode selection)
|
|
27
|
+
$ unbound setup claude-code --gateway Use Unbound as AI provider
|
|
28
|
+
$ unbound setup claude-code --subscription Hooks only (keep your subscription)
|
|
29
|
+
$ unbound setup gemini-cli Set up Gemini CLI
|
|
30
|
+
$ unbound setup codex Set up Codex
|
|
31
|
+
|
|
32
|
+
Instruction-only (shows config values to set manually):
|
|
33
|
+
$ unbound setup roo-code Show Roo Code config values
|
|
34
|
+
$ unbound setup cline Show Cline config values
|
|
35
|
+
$ unbound setup kilo-code Show Kilo Code config values
|
|
36
|
+
$ unbound setup custom-access Show API key and base URL
|
|
37
|
+
|
|
38
|
+
Remove configuration:
|
|
39
|
+
$ unbound setup cursor --clear Remove Unbound config for Cursor
|
|
40
|
+
$ unbound setup claude-code --clear Remove Unbound config for Claude Code
|
|
41
|
+
$ unbound setup gemini-cli --clear Remove Unbound config for Gemini CLI
|
|
42
|
+
$ unbound setup codex --clear Remove Unbound config for Codex
|
|
43
|
+
|
|
44
|
+
POLICY MANAGEMENT
|
|
45
|
+
$ unbound policy list List all policies
|
|
46
|
+
$ unbound policy list --type SECURITY Filter by type (SECURITY, MODEL, COST)
|
|
47
|
+
$ unbound policy get <id> View policy details
|
|
48
|
+
$ unbound policy create --name <n> --type <t> Create a new policy
|
|
49
|
+
$ unbound policy update <id> --name <n> Update a policy
|
|
50
|
+
$ unbound policy delete <id> Delete a policy
|
|
51
|
+
$ unbound policy effective <id> View effective policies for a user/group
|
|
52
|
+
$ unbound policy form-data Get reference data for policy creation
|
|
53
|
+
|
|
54
|
+
USER & GROUP MANAGEMENT
|
|
55
|
+
$ unbound users list List organization members
|
|
56
|
+
$ unbound users effective-policies <id> View effective policies for a user
|
|
57
|
+
$ unbound user-groups list List user groups
|
|
58
|
+
$ unbound user-groups get <id> View group details and members
|
|
59
|
+
$ unbound user-groups create --name <n> Create a user group
|
|
60
|
+
$ unbound user-groups update <id> Update a user group
|
|
61
|
+
$ unbound user-groups delete <id> Delete a user group
|
|
62
|
+
|
|
63
|
+
TOOL CONNECTIONS
|
|
64
|
+
$ unbound tools list List connected AI tools
|
|
65
|
+
$ unbound tools connect <type> Connect a new tool
|
|
66
|
+
$ unbound tools approved List approved tool types
|
|
67
|
+
|
|
68
|
+
CONFIGURATION
|
|
69
|
+
$ unbound config show Show all settings
|
|
70
|
+
|
|
71
|
+
FILES
|
|
72
|
+
~/.unbound/config.json Credentials and CLI settings
|
|
73
|
+
|
|
74
|
+
LEARN MORE
|
|
75
|
+
Use "unbound <command> --help" for more information about a command.
|
|
76
|
+
Use "unbound <command> <subcommand> --help" for subcommand details.
|
|
37
77
|
`);
|
|
38
78
|
|
|
39
79
|
// Register all command modules
|
|
@@ -50,31 +90,27 @@ require('./commands/setup').register(program);
|
|
|
50
90
|
// config command for managing CLI settings
|
|
51
91
|
const configCmd = program
|
|
52
92
|
.command('config')
|
|
53
|
-
.description('Manage CLI configuration
|
|
93
|
+
.description('Manage CLI configuration settings.');
|
|
54
94
|
|
|
95
|
+
// Internal/dev commands — hidden from help but still functional
|
|
55
96
|
configCmd
|
|
56
|
-
.command('set-url <url>')
|
|
57
|
-
.description('Set the API base URL
|
|
58
|
-
.addHelpText('after', `
|
|
59
|
-
Examples:
|
|
60
|
-
$ unbound config set-url http://localhost:8000 # Local development
|
|
61
|
-
$ unbound config set-url https://backend.getunbound.ai # Production (default)
|
|
62
|
-
`)
|
|
97
|
+
.command('set-url <url>', { hidden: true })
|
|
98
|
+
.description('Set the API base URL (internal)')
|
|
63
99
|
.action((url) => {
|
|
64
100
|
config.setBaseUrl(url);
|
|
65
101
|
output.success(`API base URL set to ${url}`);
|
|
66
102
|
});
|
|
67
103
|
|
|
68
104
|
configCmd
|
|
69
|
-
.command('get-url')
|
|
70
|
-
.description('Show the current API base URL
|
|
105
|
+
.command('get-url', { hidden: true })
|
|
106
|
+
.description('Show the current API base URL (internal)')
|
|
71
107
|
.action(() => {
|
|
72
108
|
console.log(config.getBaseUrl());
|
|
73
109
|
});
|
|
74
110
|
|
|
75
111
|
configCmd
|
|
76
|
-
.command('reset-url')
|
|
77
|
-
.description('Reset the API base URL to
|
|
112
|
+
.command('reset-url', { hidden: true })
|
|
113
|
+
.description('Reset the API base URL to default (internal)')
|
|
78
114
|
.action(() => {
|
|
79
115
|
const cfg = config.readConfig();
|
|
80
116
|
delete cfg.base_url;
|
|
@@ -83,28 +119,23 @@ configCmd
|
|
|
83
119
|
});
|
|
84
120
|
|
|
85
121
|
configCmd
|
|
86
|
-
.command('set-frontend-url <url>')
|
|
87
|
-
.description('Set the frontend URL
|
|
88
|
-
.addHelpText('after', `
|
|
89
|
-
Examples:
|
|
90
|
-
$ unbound config set-frontend-url http://localhost:3000 # Local development
|
|
91
|
-
$ unbound config set-frontend-url https://gateway.getunbound.ai # Production (default)
|
|
92
|
-
`)
|
|
122
|
+
.command('set-frontend-url <url>', { hidden: true })
|
|
123
|
+
.description('Set the frontend URL (internal)')
|
|
93
124
|
.action((url) => {
|
|
94
125
|
config.setFrontendUrl(url);
|
|
95
126
|
output.success(`Frontend URL set to ${url}`);
|
|
96
127
|
});
|
|
97
128
|
|
|
98
129
|
configCmd
|
|
99
|
-
.command('get-frontend-url')
|
|
100
|
-
.description('Show the current frontend URL
|
|
130
|
+
.command('get-frontend-url', { hidden: true })
|
|
131
|
+
.description('Show the current frontend URL (internal)')
|
|
101
132
|
.action(() => {
|
|
102
133
|
console.log(config.getFrontendUrl());
|
|
103
134
|
});
|
|
104
135
|
|
|
105
136
|
configCmd
|
|
106
|
-
.command('reset-frontend-url')
|
|
107
|
-
.description('Reset the frontend URL to
|
|
137
|
+
.command('reset-frontend-url', { hidden: true })
|
|
138
|
+
.description('Reset the frontend URL to default (internal)')
|
|
108
139
|
.action(() => {
|
|
109
140
|
const cfg = config.readConfig();
|
|
110
141
|
delete cfg.frontend_url;
|
|
@@ -122,8 +153,7 @@ configCmd
|
|
|
122
153
|
if (opts.json) {
|
|
123
154
|
output.json({
|
|
124
155
|
config_file: config.CONFIG_FILE,
|
|
125
|
-
|
|
126
|
-
frontend_url: config.getFrontendUrl(),
|
|
156
|
+
gateway_url: config.getFrontendUrl(),
|
|
127
157
|
logged_in: config.isLoggedIn(),
|
|
128
158
|
email: cfg.email || null,
|
|
129
159
|
organization: cfg.org_name || null,
|
|
@@ -133,8 +163,7 @@ configCmd
|
|
|
133
163
|
|
|
134
164
|
output.keyValue([
|
|
135
165
|
['Config file', config.CONFIG_FILE],
|
|
136
|
-
['
|
|
137
|
-
['Frontend URL', config.getFrontendUrl()],
|
|
166
|
+
['Unbound Gateway', config.getFrontendUrl()],
|
|
138
167
|
['Logged in', config.isLoggedIn() ? 'Yes' : 'No'],
|
|
139
168
|
['Email', cfg.email || '-'],
|
|
140
169
|
['Organization', cfg.org_name || '-'],
|
package/src/output.js
CHANGED
|
@@ -77,4 +77,34 @@ function info(msg) {
|
|
|
77
77
|
console.error(c.cyan(`\u2139 ${msg}`));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
function spinner(message) {
|
|
81
|
+
if (!process.stderr.isTTY) {
|
|
82
|
+
console.error(message);
|
|
83
|
+
return { stop: () => {}, succeed: (msg) => success(msg), fail: (msg) => error(msg) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const frames = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
|
|
87
|
+
let i = 0;
|
|
88
|
+
const interval = setInterval(() => {
|
|
89
|
+
process.stderr.write(`\r${c.cyan(frames[i++ % frames.length])} ${message}`);
|
|
90
|
+
}, 80);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
stop() {
|
|
94
|
+
clearInterval(interval);
|
|
95
|
+
process.stderr.write('\r\x1b[K');
|
|
96
|
+
},
|
|
97
|
+
succeed(msg) {
|
|
98
|
+
clearInterval(interval);
|
|
99
|
+
process.stderr.write('\r\x1b[K');
|
|
100
|
+
success(msg);
|
|
101
|
+
},
|
|
102
|
+
fail(msg) {
|
|
103
|
+
clearInterval(interval);
|
|
104
|
+
process.stderr.write('\r\x1b[K');
|
|
105
|
+
error(msg);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { table, json, keyValue, success, error, warn, info, spinner };
|