unbound-cli 1.1.8 → 1.3.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 +10 -10
- package/README.md +8 -8
- package/package.json +1 -1
- package/src/commands/chat.js +2 -2
- package/src/commands/doctor.js +198 -0
- package/src/commands/onboard.js +112 -161
- package/src/commands/setup.js +106 -180
- package/src/commands/status.js +42 -2
- package/src/config.js +1 -1
- package/src/index.js +12 -12
- package/src/toolHealth.js +288 -0
- package/test/command-merge.test.js +44 -0
- package/test/doctor-exit.test.js +31 -0
- package/test/onboard-scope.test.js +114 -0
- package/test/tool-health.test.js +210 -0
- package/src/commands/whoami.js +0 -65
package/src/commands/onboard.js
CHANGED
|
@@ -2,7 +2,7 @@ const { Option } = require('commander');
|
|
|
2
2
|
const config = require('../config');
|
|
3
3
|
const output = require('../output');
|
|
4
4
|
const { ensureLoggedIn } = require('../auth');
|
|
5
|
-
const { runSetupAllBundle, runMdmSetupAllBundle,
|
|
5
|
+
const { runSetupAllBundle, runMdmSetupAllBundle, hasRootPrivileges, ALL_TOOLS, MDM_ALL_TOOLS } = require('./setup');
|
|
6
6
|
const { runDiscoveryScan } = require('./discover');
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -20,52 +20,55 @@ function register(program) {
|
|
|
20
20
|
const onboard = program
|
|
21
21
|
.command('onboard')
|
|
22
22
|
.description(
|
|
23
|
-
'One-step
|
|
24
|
-
'
|
|
23
|
+
'One-step onboarding: install the AI tools bundle and run device discovery. ' +
|
|
24
|
+
'Scope is auto-detected from your privileges — run with sudo to enroll every ' +
|
|
25
|
+
'user on the device (MDM/org scope), or without sudo to set up just the current user.'
|
|
25
26
|
)
|
|
26
|
-
.option('--api-key <key>', '
|
|
27
|
-
.
|
|
27
|
+
.option('--api-key <key>', 'API key (user key, or admin key when run with sudo). Falls back to UNBOUND_API_KEY or a stored `unbound login` key)')
|
|
28
|
+
.addOption(new Option('--admin-api-key <key>', 'Alias for --api-key (back-compat)').hideHelp())
|
|
29
|
+
.option('--discovery-key <key>', 'Discovery API key for device scan (or set UNBOUND_DISCOVERY_KEY)')
|
|
28
30
|
.option('--domain <url>', 'Backend URL for discovery (defaults to configured backend)')
|
|
29
|
-
.option('--set-cron', 'Set up a daily background job to keep governance up to date')
|
|
30
|
-
.option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (Cursor skipped automatically)')
|
|
31
|
+
.option('--set-cron', 'Set up a daily background job to keep governance up to date (under sudo, schedules a discovery scan only — not a full tool re-install)')
|
|
32
|
+
.option('--backfill', 'Seed historical Claude Code / Codex / Copilot sessions from local transcripts into Unbound analytics (Cursor skipped automatically)')
|
|
31
33
|
.addOption(new Option('--backend-url <url>', 'Override backend URL for setup scripts (dev only)').hideHelp())
|
|
32
34
|
.addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
|
|
33
35
|
.addOption(new Option('--gateway-url <url>', 'Override gateway URL for setup scripts (dev only)').hideHelp())
|
|
34
36
|
.addHelpText('after', `
|
|
35
|
-
Runs the full onboarding flow
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
Runs the full onboarding flow, auto-detecting scope from your privileges:
|
|
38
|
+
With sudo, installs the MDM tool bundle (${MDM_ALL_TOOLS.join(', ')}) and
|
|
39
|
+
scans all users on the device. Without sudo, installs the user bundle
|
|
40
|
+
(${ALL_TOOLS.join(', ')}) and scans the current user.
|
|
41
|
+
|
|
42
|
+
1. Logs in / resolves the API key (or reuses a stored \`unbound login\` key
|
|
43
|
+
when --api-key is omitted).
|
|
44
|
+
2. Installs the tool bundle for the detected scope.
|
|
39
45
|
3. Runs device discovery with --discovery-key. With --set-cron, sets up a
|
|
40
46
|
recurring daily scheduled scan (cross-platform) instead of a one-time scan.
|
|
41
47
|
|
|
42
|
-
The
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Run with sudo to let the discovery step scan all users on the device.
|
|
47
|
-
Without sudo, discovery only scans the current user.
|
|
48
|
-
|
|
49
|
-
For admin device enrollment via MDM, use \`unbound onboard-mdm\` instead.
|
|
48
|
+
The API key and discovery API key are separate keys obtained from different
|
|
49
|
+
parts of the Unbound dashboard. Discovery uses its own key that is not stored
|
|
50
|
+
in the CLI config.
|
|
50
51
|
|
|
51
52
|
Examples:
|
|
52
53
|
$ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
54
|
+
$ sudo unbound onboard --api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
53
55
|
$ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY> --backfill
|
|
54
56
|
$ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY> --set-cron
|
|
55
|
-
$ sudo unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
56
57
|
`)
|
|
57
58
|
.action(async (opts) => {
|
|
58
|
-
const apiKeyOpt = opts.apiKey || process.env.UNBOUND_API_KEY;
|
|
59
|
+
const apiKeyOpt = opts.apiKey || opts.adminApiKey || process.env.UNBOUND_API_KEY;
|
|
59
60
|
const discoveryKeyOpt = opts.discoveryKey || process.env.UNBOUND_DISCOVERY_KEY;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!
|
|
63
|
-
output.error('--
|
|
61
|
+
const isMdm = hasRootPrivileges();
|
|
62
|
+
|
|
63
|
+
if (!discoveryKeyOpt) {
|
|
64
|
+
output.error('--discovery-key is required (or set UNBOUND_DISCOVERY_KEY env var)');
|
|
64
65
|
process.exitCode = 1;
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
// User scope needs a key or an existing login; MDM scope resolves the
|
|
69
|
+
// admin key below (it may come from a stored `unbound login` credential).
|
|
70
|
+
if (!isMdm && !apiKeyOpt && !config.isLoggedIn()) {
|
|
71
|
+
output.error('--api-key is required (or set UNBOUND_API_KEY env var, or run `unbound login` first)');
|
|
69
72
|
process.exitCode = 1;
|
|
70
73
|
return;
|
|
71
74
|
}
|
|
@@ -73,84 +76,120 @@ Examples:
|
|
|
73
76
|
let setupSucceeded = false;
|
|
74
77
|
let discoverySucceeded = false;
|
|
75
78
|
let discoveryDomain;
|
|
79
|
+
let skippedTools;
|
|
76
80
|
try {
|
|
77
81
|
// Persist URLs first, then login, then setup — order matters so the
|
|
78
82
|
// login validates against the new backend and setup wires tools at the
|
|
79
83
|
// new gateway. setUrls is atomic; a malformed URL throws before any
|
|
80
|
-
// disk write so the three URLs never end up out of sync.
|
|
84
|
+
// disk write so the three URLs never end up out of sync. Prefer the
|
|
85
|
+
// values we JUST persisted over the env-var-aware getters — a stale
|
|
86
|
+
// UNBOUND_*_URL from a prior shell session could otherwise silently
|
|
87
|
+
// shadow the user's explicit --*-url flag.
|
|
81
88
|
const written = config.setUrls({
|
|
82
89
|
backend: opts.backendUrl,
|
|
83
90
|
frontend: opts.frontendUrl,
|
|
84
91
|
gateway: opts.gatewayUrl,
|
|
85
92
|
});
|
|
86
|
-
// Prefer the values we JUST persisted over the env-var-aware getters —
|
|
87
|
-
// a stale UNBOUND_*_URL from a prior shell session could otherwise
|
|
88
|
-
// silently shadow the user's explicit --*-url flag and route login or
|
|
89
|
-
// setup at the wrong tenant.
|
|
90
93
|
const backendUrl = written.base_url || config.getBaseUrl();
|
|
91
94
|
const frontendUrl = written.frontend_url || config.getFrontendUrl();
|
|
92
95
|
const gatewayUrl = written.gateway_url || config.getGatewayUrl();
|
|
93
96
|
discoveryDomain = opts.domain || backendUrl;
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
if (isMdm) {
|
|
99
|
+
// Reuse the key stored by `unbound login` when no key was passed.
|
|
100
|
+
const adminApiKey = apiKeyOpt || config.getApiKey();
|
|
101
|
+
if (!adminApiKey) {
|
|
102
|
+
output.error('--api-key is required (or run `unbound login` first).');
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
console.log('');
|
|
108
|
+
output.info('Step 1/2: Installing MDM tool bundle (all users)');
|
|
109
|
+
const { ok, skipped } = await runMdmSetupAllBundle(adminApiKey, {
|
|
110
|
+
backendUrl, frontendUrl, gatewayUrl, backfill: !!opts.backfill,
|
|
111
|
+
});
|
|
112
|
+
if (!ok) return;
|
|
113
|
+
setupSucceeded = true;
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
console.log('');
|
|
116
|
+
output.info('Step 2/2: Running device discovery');
|
|
117
|
+
console.log('');
|
|
118
|
+
await runDiscoveryScan({ apiKey: discoveryKeyOpt, domain: discoveryDomain });
|
|
119
|
+
discoverySucceeded = true;
|
|
120
|
+
skippedTools = skipped;
|
|
121
|
+
} else {
|
|
122
|
+
await ensureLoggedIn({
|
|
123
|
+
apiKey: apiKeyOpt,
|
|
124
|
+
baseUrl: written.base_url,
|
|
125
|
+
frontendUrl: written.frontend_url,
|
|
126
|
+
});
|
|
127
|
+
const apiKey = config.getApiKey();
|
|
128
|
+
|
|
129
|
+
console.log('');
|
|
130
|
+
output.info('Step 1/2: Installing tool bundle');
|
|
131
|
+
const { ok, skipped } = await runSetupAllBundle(apiKey, {
|
|
132
|
+
backendUrl, frontendUrl, gatewayUrl, backfill: !!opts.backfill,
|
|
133
|
+
});
|
|
134
|
+
if (!ok) return;
|
|
135
|
+
setupSucceeded = true;
|
|
136
|
+
|
|
137
|
+
console.log('');
|
|
138
|
+
output.info('Step 2/2: Running device discovery');
|
|
139
|
+
console.log('');
|
|
140
|
+
await runDiscoveryScan({ apiKey: discoveryKeyOpt, domain: discoveryDomain });
|
|
141
|
+
discoverySucceeded = true;
|
|
142
|
+
skippedTools = skipped;
|
|
143
|
+
}
|
|
115
144
|
|
|
116
145
|
if (opts.setCron) {
|
|
117
146
|
console.log('');
|
|
118
|
-
output.info('Setting up daily scheduled run');
|
|
119
147
|
const { setupScheduledRun } = require('../scheduled');
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
if (isMdm) {
|
|
149
|
+
// Under sudo, the recurring job refreshes the device/tool inventory
|
|
150
|
+
// only — it does NOT re-push the full MDM tool bundle daily, and it
|
|
151
|
+
// stores the lower-privilege discovery key, not the admin key.
|
|
152
|
+
output.info('Setting up daily scheduled discovery scan');
|
|
153
|
+
await setupScheduledRun({
|
|
154
|
+
command: 'discover',
|
|
155
|
+
apiKey: discoveryKeyOpt,
|
|
156
|
+
domain: discoveryDomain,
|
|
157
|
+
skipRunAtLoad: true,
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
output.info('Setting up daily scheduled run');
|
|
161
|
+
await setupScheduledRun({
|
|
162
|
+
command: 'onboard',
|
|
163
|
+
apiKey: apiKeyOpt || config.getApiKey(),
|
|
164
|
+
discoveryKey: discoveryKeyOpt,
|
|
165
|
+
domain: discoveryDomain,
|
|
166
|
+
skipRunAtLoad: true,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
127
169
|
}
|
|
128
170
|
|
|
129
171
|
console.log('');
|
|
130
|
-
output.success(
|
|
172
|
+
output.success(skippedTools && skippedTools.length
|
|
131
173
|
? 'Onboarding complete — tools managed by MDM were skipped (see above)'
|
|
132
174
|
: 'Onboarding complete');
|
|
133
175
|
} catch (err) {
|
|
134
176
|
if (!err.displayed) output.error(err.message);
|
|
177
|
+
const suffix = domainHintSuffix(discoveryDomain);
|
|
178
|
+
const sudo = isMdm ? 'sudo ' : '';
|
|
135
179
|
if (discoverySucceeded && opts.setCron) {
|
|
136
180
|
// Both setup and discovery completed; only the optional --set-cron
|
|
137
|
-
// step failed.
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// reinstall the user originally asked for, so the correct retry is
|
|
141
|
-
// re-running onboard with both keys.
|
|
142
|
-
const suffix = domainHintSuffix(discoveryDomain);
|
|
181
|
+
// step failed. Retry just the cron: under sudo the scheduled job is a
|
|
182
|
+
// discovery-only scan, so point at `discover --set-cron`; in user
|
|
183
|
+
// scope it re-runs the full onboard (the bundle reinstall is intended).
|
|
143
184
|
console.error(' Setup and discovery completed successfully — only scheduled-run setup failed.');
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const suffix = domainHintSuffix(discoveryDomain);
|
|
147
|
-
const retryCmd = `unbound discover --api-key <DISCOVERY_KEY>${suffix}`;
|
|
148
|
-
console.error(' Tool setup completed successfully — only discovery failed.');
|
|
149
|
-
if (opts.setCron) {
|
|
150
|
-
console.error(` Re-run with: unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY> --set-cron${suffix}`);
|
|
185
|
+
if (isMdm) {
|
|
186
|
+
console.error(` Re-run just the scheduled scan with: sudo unbound discover --api-key <DISCOVERY_KEY> --set-cron${suffix}`);
|
|
151
187
|
} else {
|
|
152
|
-
console.error(` Re-run
|
|
188
|
+
console.error(` Re-run cron setup with: unbound onboard --api-key <KEY> --discovery-key <DISCOVERY_KEY> --set-cron${suffix}`);
|
|
153
189
|
}
|
|
190
|
+
} else if (setupSucceeded && !discoverySucceeded) {
|
|
191
|
+
console.error(' Tool setup completed successfully — only discovery failed.');
|
|
192
|
+
console.error(` Re-run discovery only with: ${sudo}unbound discover --api-key <DISCOVERY_KEY>${suffix}`);
|
|
154
193
|
}
|
|
155
194
|
process.exitCode = 1;
|
|
156
195
|
}
|
|
@@ -178,94 +217,6 @@ Examples:
|
|
|
178
217
|
process.exitCode = 1;
|
|
179
218
|
}
|
|
180
219
|
});
|
|
181
|
-
|
|
182
|
-
// --- MDM onboard (separate top-level command, mirrors `unbound onboard`) ---
|
|
183
|
-
|
|
184
|
-
program
|
|
185
|
-
.command('onboard-mdm')
|
|
186
|
-
.description(
|
|
187
|
-
'One-step MDM onboarding: install the default MDM tool bundle and run device discovery. ' +
|
|
188
|
-
'Requires root. Used by organization admins to enroll devices via MDM.'
|
|
189
|
-
)
|
|
190
|
-
.option('--admin-api-key <key>', 'Admin API key for MDM enrollment (falls back to your stored `unbound login` key)')
|
|
191
|
-
.requiredOption('--discovery-key <key>', 'Discovery API key (for device scan)')
|
|
192
|
-
.option('--domain <url>', 'Backend URL for discovery (defaults to configured backend)')
|
|
193
|
-
.option('--backfill', 'Seed historical Claude Code / Codex / Copilot sessions from local transcripts into Unbound analytics (Cursor skipped automatically)')
|
|
194
|
-
.addOption(new Option('--backend-url <url>', 'Override backend URL for setup scripts (dev only)').hideHelp())
|
|
195
|
-
.addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
|
|
196
|
-
.addOption(new Option('--gateway-url <url>', 'Override gateway URL for setup scripts (dev only)').hideHelp())
|
|
197
|
-
.addHelpText('after', `
|
|
198
|
-
Runs the full MDM onboarding flow for device enrollment:
|
|
199
|
-
1. Installs the MDM tool bundle: ${MDM_ALL_TOOLS.join(', ')}.
|
|
200
|
-
2. Runs device discovery against all users on the device.
|
|
201
|
-
|
|
202
|
-
Both steps require root. The admin API key and discovery API key are
|
|
203
|
-
separate keys obtained from different parts of the Unbound admin dashboard.
|
|
204
|
-
--admin-api-key may be omitted to reuse the key stored by a prior
|
|
205
|
-
\`unbound login\` (run sudo with HOME preserved so the stored key is found).
|
|
206
|
-
|
|
207
|
-
For end-user onboarding (non-MDM), use \`unbound onboard\` instead.
|
|
208
|
-
|
|
209
|
-
Examples:
|
|
210
|
-
$ sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
211
|
-
$ sudo unbound onboard-mdm --discovery-key <DISCOVERY_KEY> Reuse the stored \`unbound login\` key
|
|
212
|
-
$ sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY> --backfill
|
|
213
|
-
`)
|
|
214
|
-
.action(async (opts) => {
|
|
215
|
-
let setupSucceeded = false;
|
|
216
|
-
let discoveryDomain;
|
|
217
|
-
try {
|
|
218
|
-
// Persist URLs first, then login, then setup — order matters so this
|
|
219
|
-
// MDM run wires tools at the new tenant. Prefer just-written values
|
|
220
|
-
// over env-var-aware getters so a stale UNBOUND_*_URL can't shadow the
|
|
221
|
-
// user's explicit --*-url flag.
|
|
222
|
-
const written = config.setUrls({
|
|
223
|
-
backend: opts.backendUrl,
|
|
224
|
-
frontend: opts.frontendUrl,
|
|
225
|
-
gateway: opts.gatewayUrl,
|
|
226
|
-
});
|
|
227
|
-
const backendUrl = written.base_url || config.getBaseUrl();
|
|
228
|
-
const frontendUrl = written.frontend_url || config.getFrontendUrl();
|
|
229
|
-
const gatewayUrl = written.gateway_url || config.getGatewayUrl();
|
|
230
|
-
discoveryDomain = opts.domain || backendUrl;
|
|
231
|
-
|
|
232
|
-
checkRoot('onboard-mdm');
|
|
233
|
-
|
|
234
|
-
// Reuse the key stored by `unbound login` when --admin-api-key is omitted.
|
|
235
|
-
const adminApiKey = opts.adminApiKey || config.getApiKey();
|
|
236
|
-
if (!adminApiKey) {
|
|
237
|
-
output.error('--admin-api-key is required (or run `unbound login` first).');
|
|
238
|
-
process.exitCode = 1;
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
console.log('');
|
|
243
|
-
output.info('Step 1/2: Installing MDM tool bundle');
|
|
244
|
-
const { ok, skipped } = await runMdmSetupAllBundle(adminApiKey, {
|
|
245
|
-
backendUrl, frontendUrl, gatewayUrl, backfill: !!opts.backfill,
|
|
246
|
-
});
|
|
247
|
-
if (!ok) return;
|
|
248
|
-
setupSucceeded = true;
|
|
249
|
-
|
|
250
|
-
console.log('');
|
|
251
|
-
output.info('Step 2/2: Running device discovery');
|
|
252
|
-
console.log('');
|
|
253
|
-
await runDiscoveryScan({ apiKey: opts.discoveryKey, domain: discoveryDomain });
|
|
254
|
-
|
|
255
|
-
console.log('');
|
|
256
|
-
output.success(skipped && skipped.length
|
|
257
|
-
? 'MDM onboarding complete — tools managed by MDM were skipped (see above)'
|
|
258
|
-
: 'MDM onboarding complete');
|
|
259
|
-
} catch (err) {
|
|
260
|
-
if (!err.displayed) output.error(err.message);
|
|
261
|
-
if (setupSucceeded) {
|
|
262
|
-
const suffix = domainHintSuffix(discoveryDomain);
|
|
263
|
-
console.error(' MDM tool setup completed successfully — only discovery failed.');
|
|
264
|
-
console.error(` Re-run discovery only with: sudo unbound discover --api-key <DISCOVERY_KEY>${suffix}`);
|
|
265
|
-
}
|
|
266
|
-
process.exitCode = 1;
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
220
|
}
|
|
270
221
|
|
|
271
222
|
module.exports = { register };
|