unbound-cli 0.3.1 → 0.4.0
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/LOCAL_DEV.md +11 -0
- package/README.md +15 -0
- package/package.json +1 -1
- package/src/commands/discover.js +32 -10
- package/src/commands/onboard.js +134 -0
- package/src/commands/setup.js +53 -3
- package/src/index.js +6 -0
package/LOCAL_DEV.md
CHANGED
|
@@ -90,6 +90,17 @@ node src/index.js tools approved
|
|
|
90
90
|
# Setup
|
|
91
91
|
node src/index.js setup cursor
|
|
92
92
|
node src/index.js setup claude-code
|
|
93
|
+
|
|
94
|
+
# Setup the default bundle (Cursor + Claude Code hooks + Codex hooks)
|
|
95
|
+
node src/index.js setup --all
|
|
96
|
+
node src/index.js setup --all --api-key <key>
|
|
97
|
+
|
|
98
|
+
# Onboarding (one command: login + setup --all + discover)
|
|
99
|
+
node src/index.js onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
100
|
+
sudo node src/index.js onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
101
|
+
|
|
102
|
+
# MDM onboarding (admin device enrollment, requires root)
|
|
103
|
+
sudo node src/index.js onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
93
104
|
```
|
|
94
105
|
|
|
95
106
|
## Unlink when done
|
package/README.md
CHANGED
|
@@ -30,6 +30,20 @@ unbound setup
|
|
|
30
30
|
unbound setup cursor
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Onboarding (one command)
|
|
34
|
+
|
|
35
|
+
For new users, the fastest path from install to a fully-configured device is the `onboard` command. It logs you in, installs the default tool bundle (Cursor, Claude Code hooks, Codex hooks), and runs device discovery — all in one step.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# End user
|
|
39
|
+
npm install -g unbound-cli
|
|
40
|
+
unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
41
|
+
|
|
42
|
+
# Admin enrolling a device via MDM (requires root)
|
|
43
|
+
sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The user API key and discovery API key are separate — discovery uses its own key that the CLI does not store. Run with `sudo` to let the discovery step scan all users on the device; without it, only the current user is scanned.
|
|
33
47
|
|
|
34
48
|
## Commands
|
|
35
49
|
|
|
@@ -51,6 +65,7 @@ Interactive batch setup:
|
|
|
51
65
|
| Command | Description |
|
|
52
66
|
|---------|-------------|
|
|
53
67
|
| `unbound setup` | Select and install multiple tools interactively |
|
|
68
|
+
| `unbound setup --all` | Install the default bundle: Cursor + Claude Code hooks + Codex hooks |
|
|
54
69
|
|
|
55
70
|
Automated setup (downloads scripts, sets env vars, configures tool):
|
|
56
71
|
|
package/package.json
CHANGED
package/src/commands/discover.js
CHANGED
|
@@ -13,6 +13,15 @@ function isRoot() {
|
|
|
13
13
|
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Escapes a string for safe embedding in a shell command (single-quote wrap).
|
|
18
|
+
* Mirrors the helper in setup.js. Required because runDiscoveryScript uses
|
|
19
|
+
* spawn(cmd, { shell: true }) which routes the full command through bash.
|
|
20
|
+
*/
|
|
21
|
+
function shellEscape(str) {
|
|
22
|
+
return "'" + String(str).replace(/'/g, "'\\''") + "'";
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Downloads a bash script from the discovery repo and executes it with arguments.
|
|
18
27
|
* Uses stdio: 'inherit' so the script's output is shown live.
|
|
@@ -36,6 +45,26 @@ function runDiscoveryScript(scriptName, args) {
|
|
|
36
45
|
});
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Runs the main discovery scan (install.sh) with the given discovery key and domain.
|
|
50
|
+
* Emits a warning if not running as root (scan will be limited to the current user).
|
|
51
|
+
* Throws on failure. Used by both the `discover` command and the `onboard` command.
|
|
52
|
+
*/
|
|
53
|
+
async function runDiscoveryScan({ apiKey, domain = DEFAULT_DOMAIN }) {
|
|
54
|
+
if (!apiKey) {
|
|
55
|
+
throw new Error('Discovery API key is required.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!isRoot()) {
|
|
59
|
+
output.warn('Running without root. Only scanning current user\'s tools.');
|
|
60
|
+
output.info('Run with sudo to scan all users on this device.');
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const args = `--api-key ${shellEscape(apiKey)} --domain ${shellEscape(domain)}`;
|
|
65
|
+
await runDiscoveryScript('install.sh', args);
|
|
66
|
+
}
|
|
67
|
+
|
|
39
68
|
function register(program) {
|
|
40
69
|
const discover = program
|
|
41
70
|
.command('discover')
|
|
@@ -72,14 +101,7 @@ Examples:
|
|
|
72
101
|
return;
|
|
73
102
|
}
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
output.warn('Running without root. Only scanning current user\'s tools.');
|
|
77
|
-
output.info('Run with sudo to scan all users on this device.');
|
|
78
|
-
console.log('');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const args = `--api-key "${opts.apiKey}" --domain "${opts.domain}"`;
|
|
82
|
-
await runDiscoveryScript('install.sh', args);
|
|
104
|
+
await runDiscoveryScan({ apiKey: opts.apiKey, domain: opts.domain });
|
|
83
105
|
|
|
84
106
|
console.log('');
|
|
85
107
|
output.success('Discovery complete');
|
|
@@ -125,7 +147,7 @@ Examples:
|
|
|
125
147
|
return;
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
const args = `--api-key
|
|
150
|
+
const args = `--api-key ${shellEscape(opts.apiKey)} --domain ${shellEscape(opts.domain)}`;
|
|
129
151
|
await runDiscoveryScript('setup-scheduled-scan.sh', args);
|
|
130
152
|
} catch (err) {
|
|
131
153
|
output.error(err.message);
|
|
@@ -207,4 +229,4 @@ Examples:
|
|
|
207
229
|
});
|
|
208
230
|
}
|
|
209
231
|
|
|
210
|
-
module.exports = { register };
|
|
232
|
+
module.exports = { register, runDiscoveryScan };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { Option } = require('commander');
|
|
2
|
+
const config = require('../config');
|
|
3
|
+
const output = require('../output');
|
|
4
|
+
const { ensureLoggedIn } = require('../auth');
|
|
5
|
+
const { runSetupAllBundle, runMdmSetupAllBundle, checkRoot, ALL_TOOLS, MDM_ALL_TOOLS } = require('./setup');
|
|
6
|
+
const { runDiscoveryScan } = require('./discover');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DOMAIN = 'https://backend.getunbound.ai';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds the recovery-command suffix for partial-failure hints.
|
|
12
|
+
* Includes `--domain <value>` only when the user supplied a non-default domain,
|
|
13
|
+
* so users running against a custom backend get a copy-pastable command.
|
|
14
|
+
*/
|
|
15
|
+
function domainHintSuffix(domain) {
|
|
16
|
+
if (!domain || domain === DEFAULT_DOMAIN) return '';
|
|
17
|
+
return ` --domain ${domain}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function register(program) {
|
|
21
|
+
program
|
|
22
|
+
.command('onboard')
|
|
23
|
+
.description(
|
|
24
|
+
'One-step user onboarding: install the default AI tools bundle and run device discovery. ' +
|
|
25
|
+
'Runs `setup --all` followed by `discover` in a single command.'
|
|
26
|
+
)
|
|
27
|
+
.requiredOption('--api-key <key>', 'User API key (for tool setup and login)')
|
|
28
|
+
.requiredOption('--discovery-key <key>', 'Discovery API key (for device scan)')
|
|
29
|
+
.option('--domain <url>', 'Backend URL for discovery', DEFAULT_DOMAIN)
|
|
30
|
+
.addHelpText('after', `
|
|
31
|
+
Runs the full onboarding flow for an end user:
|
|
32
|
+
1. Logs in with --api-key and stores credentials.
|
|
33
|
+
2. Installs the default tool bundle: ${ALL_TOOLS.join(', ')}.
|
|
34
|
+
3. Runs device discovery with --discovery-key.
|
|
35
|
+
|
|
36
|
+
The user API key and discovery API key are separate keys obtained from
|
|
37
|
+
different parts of the Unbound dashboard. Discovery uses its own key
|
|
38
|
+
that is not stored in the CLI config.
|
|
39
|
+
|
|
40
|
+
Run with sudo to let the discovery step scan all users on the device.
|
|
41
|
+
Without sudo, discovery only scans the current user.
|
|
42
|
+
|
|
43
|
+
For admin device enrollment via MDM, use \`unbound onboard-mdm\` instead.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
$ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
47
|
+
$ sudo unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
48
|
+
`)
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
let setupSucceeded = false;
|
|
51
|
+
try {
|
|
52
|
+
await ensureLoggedIn({ apiKey: opts.apiKey });
|
|
53
|
+
const apiKey = config.getApiKey();
|
|
54
|
+
|
|
55
|
+
console.log('');
|
|
56
|
+
output.info('Step 1/2: Installing tool bundle');
|
|
57
|
+
const ok = await runSetupAllBundle(apiKey);
|
|
58
|
+
if (!ok) return;
|
|
59
|
+
setupSucceeded = true;
|
|
60
|
+
|
|
61
|
+
console.log('');
|
|
62
|
+
output.info('Step 2/2: Running device discovery');
|
|
63
|
+
console.log('');
|
|
64
|
+
await runDiscoveryScan({ apiKey: opts.discoveryKey, domain: opts.domain });
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
output.success('Onboarding complete');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (!err.displayed) output.error(err.message);
|
|
70
|
+
if (setupSucceeded) {
|
|
71
|
+
const suffix = domainHintSuffix(opts.domain);
|
|
72
|
+
console.error(' Tool setup completed successfully — only discovery failed.');
|
|
73
|
+
console.error(` Re-run discovery only with: unbound discover --api-key <DISCOVERY_KEY>${suffix}`);
|
|
74
|
+
}
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// --- MDM onboard (separate top-level command, mirrors `unbound onboard`) ---
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command('onboard-mdm')
|
|
83
|
+
.description(
|
|
84
|
+
'One-step MDM onboarding: install the default MDM tool bundle and run device discovery. ' +
|
|
85
|
+
'Requires root. Used by organization admins to enroll devices via MDM.'
|
|
86
|
+
)
|
|
87
|
+
.requiredOption('--admin-api-key <key>', 'Admin API key for MDM enrollment')
|
|
88
|
+
.requiredOption('--discovery-key <key>', 'Discovery API key (for device scan)')
|
|
89
|
+
.option('--domain <url>', 'Backend URL for discovery', DEFAULT_DOMAIN)
|
|
90
|
+
.addOption(new Option('--url <url>', 'Override backend URL for setup scripts').hideHelp())
|
|
91
|
+
.addHelpText('after', `
|
|
92
|
+
Runs the full MDM onboarding flow for device enrollment:
|
|
93
|
+
1. Installs the MDM tool bundle: ${MDM_ALL_TOOLS.join(', ')}.
|
|
94
|
+
2. Runs device discovery against all users on the device.
|
|
95
|
+
|
|
96
|
+
Both steps require root. The admin API key and discovery API key are
|
|
97
|
+
separate keys obtained from different parts of the Unbound admin dashboard.
|
|
98
|
+
|
|
99
|
+
For end-user onboarding (non-MDM), use \`unbound onboard\` instead.
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
$ sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
103
|
+
`)
|
|
104
|
+
.action(async (opts) => {
|
|
105
|
+
let setupSucceeded = false;
|
|
106
|
+
try {
|
|
107
|
+
checkRoot('onboard-mdm');
|
|
108
|
+
|
|
109
|
+
console.log('');
|
|
110
|
+
output.info('Step 1/2: Installing MDM tool bundle');
|
|
111
|
+
const ok = await runMdmSetupAllBundle(opts.adminApiKey, { url: opts.url });
|
|
112
|
+
if (!ok) return;
|
|
113
|
+
setupSucceeded = true;
|
|
114
|
+
|
|
115
|
+
console.log('');
|
|
116
|
+
output.info('Step 2/2: Running device discovery');
|
|
117
|
+
console.log('');
|
|
118
|
+
await runDiscoveryScan({ apiKey: opts.discoveryKey, domain: opts.domain });
|
|
119
|
+
|
|
120
|
+
console.log('');
|
|
121
|
+
output.success('MDM onboarding complete');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (!err.displayed) output.error(err.message);
|
|
124
|
+
if (setupSucceeded) {
|
|
125
|
+
const suffix = domainHintSuffix(opts.domain);
|
|
126
|
+
console.error(' MDM tool setup completed successfully — only discovery failed.');
|
|
127
|
+
console.error(` Re-run discovery only with: sudo unbound discover --api-key <DISCOVERY_KEY>${suffix}`);
|
|
128
|
+
}
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { register };
|
package/src/commands/setup.js
CHANGED
|
@@ -27,6 +27,10 @@ const MDM_TOOLS = {
|
|
|
27
27
|
// Default tools for --all (uses subscription mode for Claude Code and Codex since only one can be active)
|
|
28
28
|
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription'];
|
|
29
29
|
|
|
30
|
+
// Default tools for user-level `unbound setup --all`.
|
|
31
|
+
// Includes Cursor, Claude Code hooks, and Codex hooks (no Gemini CLI).
|
|
32
|
+
const ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription'];
|
|
33
|
+
|
|
30
34
|
// Tool name → script mapping for automated tools
|
|
31
35
|
const SETUP_TOOL_MAP = {
|
|
32
36
|
'cursor': { label: 'Cursor', script: 'cursor/setup.py' },
|
|
@@ -130,11 +134,12 @@ function runScriptPiped(scriptPath, args) {
|
|
|
130
134
|
/**
|
|
131
135
|
* Checks that the process is running as root (macOS/Linux).
|
|
132
136
|
* Windows admin check is handled by the Python MDM scripts themselves.
|
|
137
|
+
* Pass a customized hint for the calling command (defaults to "setup mdm").
|
|
133
138
|
*/
|
|
134
|
-
function checkRoot() {
|
|
139
|
+
function checkRoot(commandHint = 'setup mdm') {
|
|
135
140
|
if (process.platform === 'win32') return;
|
|
136
141
|
if (typeof process.getuid !== 'function' || process.getuid() !== 0) {
|
|
137
|
-
throw new Error(
|
|
142
|
+
throw new Error(`MDM setup requires root. Run with: sudo unbound ${commandHint} ...`);
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
|
|
@@ -171,6 +176,7 @@ function register(program) {
|
|
|
171
176
|
.option('--clear', 'Remove Unbound configuration for the specified tools')
|
|
172
177
|
.option('--subscription', 'Use subscription mode for Claude Code / Codex (hooks only)')
|
|
173
178
|
.option('--gateway', 'Use gateway mode for Claude Code / Codex (Unbound as AI provider)')
|
|
179
|
+
.option('--all', 'Set up the default bundle: Cursor, Claude Code (hooks), Codex (hooks)')
|
|
174
180
|
.addHelpText('after', `
|
|
175
181
|
Available tools:
|
|
176
182
|
cursor Cursor IDE
|
|
@@ -195,6 +201,10 @@ Examples:
|
|
|
195
201
|
$ unbound setup claude-code --subscription Claude Code hooks only
|
|
196
202
|
$ unbound setup codex --gateway Codex gateway mode
|
|
197
203
|
|
|
204
|
+
Install the default bundle (Cursor + Claude Code hooks + Codex hooks):
|
|
205
|
+
$ unbound setup --all Set up the default bundle
|
|
206
|
+
$ unbound setup --all --api-key <key> Login + set up the bundle
|
|
207
|
+
|
|
198
208
|
One-step login and setup:
|
|
199
209
|
$ unbound setup cursor --api-key <key> Login + set up Cursor
|
|
200
210
|
$ unbound setup cursor claude-code-gateway --api-key <key>
|
|
@@ -217,6 +227,16 @@ automatically to authenticate before proceeding.
|
|
|
217
227
|
const apiKey = config.getApiKey();
|
|
218
228
|
const frontendUrl = config.getFrontendUrl();
|
|
219
229
|
|
|
230
|
+
// --all expands to the default bundle. Cannot be combined with explicit tool names.
|
|
231
|
+
if (opts.all) {
|
|
232
|
+
if (tools.length > 0) {
|
|
233
|
+
output.error('Cannot combine --all with explicit tool names.');
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
tools = [...ALL_TOOLS];
|
|
238
|
+
}
|
|
239
|
+
|
|
220
240
|
// No tools specified → interactive multi-select (existing flow)
|
|
221
241
|
if (tools.length === 0) {
|
|
222
242
|
const selected = await output.multiSelect(
|
|
@@ -477,4 +497,34 @@ Examples:
|
|
|
477
497
|
});
|
|
478
498
|
}
|
|
479
499
|
|
|
480
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Runs the user-level default bundle (Cursor, Claude Code hooks, Codex hooks) with spinners.
|
|
502
|
+
* Assumes the caller has already ensured the user is logged in.
|
|
503
|
+
* Returns true on success, false on failure.
|
|
504
|
+
*/
|
|
505
|
+
async function runSetupAllBundle(apiKey) {
|
|
506
|
+
const resolvedTools = ALL_TOOLS.map(name => ({ name, ...SETUP_TOOL_MAP[name] }));
|
|
507
|
+
const args = `--api-key ${shellEscape(apiKey)}`;
|
|
508
|
+
return runBatch(resolvedTools, (tool) => runScriptPiped(tool.script, args));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Runs the MDM default bundle (Cursor, Claude Code hooks, Gemini CLI, Codex hooks) with spinners.
|
|
513
|
+
* Caller must ensure the process is running as root.
|
|
514
|
+
* Returns true on success, false on failure.
|
|
515
|
+
*/
|
|
516
|
+
async function runMdmSetupAllBundle(adminApiKey, { url } = {}) {
|
|
517
|
+
const resolvedTools = MDM_ALL_TOOLS.map(name => ({ name, ...MDM_TOOLS[name] }));
|
|
518
|
+
let args = `--api-key ${shellEscape(adminApiKey)}`;
|
|
519
|
+
if (url) args += ` --url ${shellEscape(url)}`;
|
|
520
|
+
return runBatch(resolvedTools, (tool) => runScriptPiped(tool.script, args));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
module.exports = {
|
|
524
|
+
register,
|
|
525
|
+
runSetupAllBundle,
|
|
526
|
+
runMdmSetupAllBundle,
|
|
527
|
+
checkRoot,
|
|
528
|
+
ALL_TOOLS,
|
|
529
|
+
MDM_ALL_TOOLS,
|
|
530
|
+
};
|
package/src/index.js
CHANGED
|
@@ -23,8 +23,13 @@ AUTHENTICATION
|
|
|
23
23
|
$ unbound whoami Show current user and organization
|
|
24
24
|
$ unbound status Show CLI status and API connectivity
|
|
25
25
|
|
|
26
|
+
ONBOARDING (one-step install + discover)
|
|
27
|
+
$ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
28
|
+
$ sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
29
|
+
|
|
26
30
|
TOOL SETUP
|
|
27
31
|
$ unbound setup Select and install multiple tools interactively
|
|
32
|
+
$ unbound setup --all Set up the default bundle (Cursor + Claude Code hooks + Codex hooks)
|
|
28
33
|
$ unbound setup cursor Set up Cursor
|
|
29
34
|
$ unbound setup claude-code Set up Claude Code (interactive mode selection)
|
|
30
35
|
$ unbound setup claude-code --gateway Use Unbound as AI provider
|
|
@@ -113,6 +118,7 @@ require('./commands/user-groups').register(program);
|
|
|
113
118
|
require('./commands/tools').register(program);
|
|
114
119
|
require('./commands/setup').register(program);
|
|
115
120
|
require('./commands/discover').register(program);
|
|
121
|
+
require('./commands/onboard').register(program);
|
|
116
122
|
|
|
117
123
|
// config command for managing CLI settings
|
|
118
124
|
const configCmd = program
|