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.
@@ -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, checkRoot, ALL_TOOLS, MDM_ALL_TOOLS } = require('./setup');
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 user onboarding: install the default AI tools bundle and run device discovery. ' +
24
- 'Runs `setup --all` followed by `discover` in a single command.'
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>', 'User API key (or set UNBOUND_API_KEY env var, or reuse a stored `unbound login` key)')
27
- .option('--discovery-key <key>', 'Discovery API key for device scan (or set UNBOUND_DISCOVERY_KEY env var)')
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 for an end user:
36
- 1. Logs in with --api-key and stores credentials (or reuses a stored
37
- \`unbound login\` key when --api-key is omitted).
38
- 2. Installs the default tool bundle: ${ALL_TOOLS.join(', ')}.
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 user API key and discovery API key are separate keys obtained from
43
- different parts of the Unbound dashboard. Discovery uses its own key
44
- that is not stored in the CLI config.
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
- // A stored `unbound login` key is enough — only demand --api-key when not
61
- // already logged in. ensureLoggedIn() reuses the stored credential below.
62
- if (!apiKeyOpt && !config.isLoggedIn()) {
63
- output.error('--api-key is required (or set UNBOUND_API_KEY env var, or run `unbound login` first)');
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
- if (!discoveryKeyOpt) {
68
- output.error('--discovery-key is required (or set UNBOUND_DISCOVERY_KEY env var)');
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
- await ensureLoggedIn({
96
- apiKey: apiKeyOpt,
97
- baseUrl: written.base_url,
98
- frontendUrl: written.frontend_url,
99
- });
100
- const apiKey = config.getApiKey();
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
- console.log('');
103
- output.info('Step 1/2: Installing tool bundle');
104
- const { ok, skipped } = await runSetupAllBundle(apiKey, {
105
- backendUrl, frontendUrl, gatewayUrl, backfill: !!opts.backfill,
106
- });
107
- if (!ok) return;
108
- setupSucceeded = true;
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
- console.log('');
111
- output.info('Step 2/2: Running device discovery');
112
- console.log('');
113
- await runDiscoveryScan({ apiKey: discoveryKeyOpt, domain: discoveryDomain });
114
- discoverySucceeded = true;
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
- await setupScheduledRun({
121
- command: 'onboard',
122
- apiKey: apiKeyOpt || apiKey,
123
- discoveryKey: discoveryKeyOpt,
124
- domain: discoveryDomain,
125
- skipRunAtLoad: true,
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(skipped && skipped.length
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. Don't tell the user to re-run discovery they already
138
- // did. Tell them how to retry just the cron setup. discover schedule
139
- // would install a discover-only cron, dropping the tool-bundle
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
- console.error(` Re-run cron setup with: unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY> --set-cron${suffix}`);
145
- } else if (setupSucceeded && !discoverySucceeded) {
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 discovery only with: ${retryCmd}`);
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 };