unbound-cli 0.9.2 → 0.9.6
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/README.md +3 -1
- package/package.json +1 -1
- package/src/commands/discover.js +29 -6
- package/src/commands/setup.js +125 -69
- package/src/index.js +4 -2
- package/test/discover-exit-code.test.js +21 -0
- package/test/setup-args.test.js +85 -0
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ Automated setup (downloads scripts, sets env vars, configures tool):
|
|
|
72
72
|
| Command | Description |
|
|
73
73
|
|---------|-------------|
|
|
74
74
|
| `unbound setup cursor` | Download hooks, set env, restart Cursor |
|
|
75
|
+
| `unbound setup copilot` | Download hooks, set env, configure GitHub Copilot |
|
|
75
76
|
| `unbound setup claude-code` | Interactive mode selection (subscription or gateway) |
|
|
76
77
|
| `unbound setup claude-code --subscription` | Hooks only (keep your Claude subscription) |
|
|
77
78
|
| `unbound setup claude-code --gateway` | Use Unbound as the AI provider |
|
|
@@ -94,6 +95,7 @@ Remove configuration:
|
|
|
94
95
|
| Command | Description |
|
|
95
96
|
|---------|-------------|
|
|
96
97
|
| `unbound setup cursor --clear` | Remove Unbound config for Cursor |
|
|
98
|
+
| `unbound setup copilot --clear` | Remove Unbound config for GitHub Copilot |
|
|
97
99
|
| `unbound setup claude-code --clear` | Remove Unbound config for Claude Code |
|
|
98
100
|
| `unbound setup gemini-cli --clear` | Remove Unbound config for Gemini CLI |
|
|
99
101
|
| `unbound setup codex --clear` | Remove Unbound config for Codex |
|
|
@@ -108,7 +110,7 @@ Configure all users on a device via MDM. Requires root.
|
|
|
108
110
|
| `sudo unbound setup mdm --admin-api-key KEY cursor codex-subscription` | Set up specific tools |
|
|
109
111
|
| `sudo unbound setup mdm --admin-api-key KEY --clear cursor` | Remove config for specific tools |
|
|
110
112
|
|
|
111
|
-
Available tools: `cursor`, `claude-code-subscription`, `claude-code-gateway`, `gemini-cli`, `codex-subscription`, `codex-gateway`
|
|
113
|
+
Available tools: `cursor`, `copilot`, `claude-code-subscription`, `claude-code-gateway`, `gemini-cli`, `codex-subscription`, `codex-gateway`
|
|
112
114
|
|
|
113
115
|
`claude-code-subscription` and `claude-code-gateway` are mutually exclusive. `codex-subscription` and `codex-gateway` are mutually exclusive. When using `--all`, subscription mode is used by default for Claude Code and Codex.
|
|
114
116
|
|
package/package.json
CHANGED
package/src/commands/discover.js
CHANGED
|
@@ -9,6 +9,18 @@ const output = require('../output');
|
|
|
9
9
|
const DISCOVER_BASE_URL = 'https://raw.githubusercontent.com/websentry-ai/coding-discovery-tool/refs/heads/main';
|
|
10
10
|
const LAUNCH_AGENT_LABEL = 'ai.getunbound.discovery';
|
|
11
11
|
|
|
12
|
+
// install.sh exits with this code when the OS isn't supported for discovery
|
|
13
|
+
// (e.g. Linux). Treated as a skipped scan, not a failure.
|
|
14
|
+
const DISCOVERY_EXIT_UNSUPPORTED_OS = 3;
|
|
15
|
+
|
|
16
|
+
// Classifies a discovery subprocess exit code:
|
|
17
|
+
// 'success' (scan ran), 'unsupported' (skipped on this OS), or 'failure'.
|
|
18
|
+
function classifyDiscoveryExit(code) {
|
|
19
|
+
if (code === 0) return 'success';
|
|
20
|
+
if (code === DISCOVERY_EXIT_UNSUPPORTED_OS) return 'unsupported';
|
|
21
|
+
return 'failure';
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
// Native Windows (cmd/PowerShell) takes the install.ps1 path below. WSL reports
|
|
13
25
|
// as Linux via process.platform and keeps using the existing bash install.sh pipe.
|
|
14
26
|
function isWindowsNative() {
|
|
@@ -71,11 +83,15 @@ function runDiscoveryScript(scriptName, args) {
|
|
|
71
83
|
const child = spawn(cmd, { shell: true, stdio: 'inherit' });
|
|
72
84
|
|
|
73
85
|
child.on('close', (code) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} else {
|
|
86
|
+
const result = classifyDiscoveryExit(code);
|
|
87
|
+
if (result === 'failure') {
|
|
77
88
|
reject(new Error(`Discovery script failed with exit code ${code}`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (result === 'unsupported') {
|
|
92
|
+
output.warn('AI tool discovery is not supported on this operating system. Skipping the scan — the Unbound CLI works normally.');
|
|
78
93
|
}
|
|
94
|
+
resolve();
|
|
79
95
|
});
|
|
80
96
|
|
|
81
97
|
child.on('error', reject);
|
|
@@ -129,8 +145,15 @@ async function runDiscoveryScriptWindows(scriptName, args) {
|
|
|
129
145
|
{ stdio: 'inherit', shell: false, windowsHide: true }
|
|
130
146
|
);
|
|
131
147
|
child.on('close', (code) => {
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
const result = classifyDiscoveryExit(code);
|
|
149
|
+
if (result === 'failure') {
|
|
150
|
+
reject(new Error(`Discovery script failed with exit code ${code}`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (result === 'unsupported') {
|
|
154
|
+
output.warn('AI tool discovery is not supported on this operating system. Skipping the scan — the Unbound CLI works normally.');
|
|
155
|
+
}
|
|
156
|
+
resolve();
|
|
134
157
|
});
|
|
135
158
|
child.on('error', reject);
|
|
136
159
|
});
|
|
@@ -326,4 +349,4 @@ Examples:
|
|
|
326
349
|
});
|
|
327
350
|
}
|
|
328
351
|
|
|
329
|
-
module.exports = { register, runDiscoveryScan };
|
|
352
|
+
module.exports = { register, runDiscoveryScan, classifyDiscoveryExit };
|
package/src/commands/setup.js
CHANGED
|
@@ -18,6 +18,7 @@ function isWindowsNative() {
|
|
|
18
18
|
|
|
19
19
|
const SETUP_TOOLS = [
|
|
20
20
|
{ label: 'Cursor', value: 'cursor', script: 'cursor/setup.py' },
|
|
21
|
+
{ label: 'GitHub Copilot', value: 'copilot', script: 'copilot/hooks/setup.py' },
|
|
21
22
|
{ label: 'Claude Code \u2014 subscription (hooks)', value: 'claude-sub', script: 'claude-code/hooks/setup.py', group: 'claude-code' },
|
|
22
23
|
{ label: 'Claude Code \u2014 gateway (gateway)', value: 'claude-gw', script: 'claude-code/gateway/setup.py', group: 'claude-code' },
|
|
23
24
|
{ label: 'Gemini CLI', value: 'gemini', script: 'gemini-cli/gateway/setup.py' },
|
|
@@ -27,6 +28,7 @@ const SETUP_TOOLS = [
|
|
|
27
28
|
|
|
28
29
|
const MDM_TOOLS = {
|
|
29
30
|
'cursor': { label: 'Cursor', script: 'cursor/mdm/setup.py' },
|
|
31
|
+
'copilot': { label: 'GitHub Copilot', script: 'copilot/hooks/mdm/setup.py' },
|
|
30
32
|
'claude-code-subscription': { label: 'Claude Code (subscription)', script: 'claude-code/hooks/mdm/setup.py' },
|
|
31
33
|
'claude-code-gateway': { label: 'Claude Code (gateway)', script: 'claude-code/gateway/mdm/setup.py' },
|
|
32
34
|
'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/mdm/setup.py' },
|
|
@@ -34,16 +36,22 @@ const MDM_TOOLS = {
|
|
|
34
36
|
'codex-gateway': { label: 'Codex (gateway)', script: 'codex/gateway/mdm/setup.py' },
|
|
35
37
|
};
|
|
36
38
|
|
|
37
|
-
// Default tools for
|
|
38
|
-
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription'];
|
|
39
|
+
// Default MDM tools for `unbound onboard-mdm` (subscription mode for Claude Code/Codex since only one can be active)
|
|
40
|
+
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
39
41
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// Tools for `unbound setup mdm --all` — identical to MDM_ALL_TOOLS today; split kept for future flexibility.
|
|
43
|
+
const MDM_SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
44
|
+
|
|
45
|
+
// Default tools for `unbound onboard` (Cursor, Claude Code hooks, Codex hooks, Copilot hooks; no Gemini CLI).
|
|
46
|
+
const ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot'];
|
|
47
|
+
|
|
48
|
+
// Tools for `unbound setup --all` — identical to ALL_TOOLS today; split kept for future flexibility.
|
|
49
|
+
const SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot'];
|
|
43
50
|
|
|
44
51
|
// Tool name → script mapping for automated tools
|
|
45
52
|
const SETUP_TOOL_MAP = {
|
|
46
53
|
'cursor': { label: 'Cursor', script: 'cursor/setup.py' },
|
|
54
|
+
'copilot': { label: 'GitHub Copilot', script: 'copilot/hooks/setup.py' },
|
|
47
55
|
'claude-code-subscription': { label: 'Claude Code (subscription)', script: 'claude-code/hooks/setup.py' },
|
|
48
56
|
'claude-code-gateway': { label: 'Claude Code (gateway)', script: 'claude-code/gateway/setup.py' },
|
|
49
57
|
'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/setup.py' },
|
|
@@ -213,13 +221,15 @@ async function runPythonScriptWindows(scriptPath, args, { capture }) {
|
|
|
213
221
|
* have no browser-auth flow.
|
|
214
222
|
*/
|
|
215
223
|
function buildScriptArgs(apiKey, { backendUrl, frontendUrl, gatewayUrl, clear, mdm, backfill } = {}) {
|
|
216
|
-
|
|
224
|
+
// --clear runs no auth in the Python scripts, so the key may be absent. Omit
|
|
225
|
+
// the flag entirely rather than passing --api-key 'undefined'.
|
|
226
|
+
let args = apiKey ? `--api-key ${shellEscape(apiKey)}` : '';
|
|
217
227
|
if (backendUrl) args += ` --backend-url ${shellEscape(backendUrl)}`;
|
|
218
228
|
if (gatewayUrl) args += ` --gateway-url ${shellEscape(gatewayUrl)}`;
|
|
219
229
|
if (!mdm && frontendUrl) args += ` --domain ${shellEscape(frontendUrl)}`;
|
|
220
230
|
if (clear) args += ' --clear';
|
|
221
231
|
if (backfill) args += ' --backfill';
|
|
222
|
-
return args;
|
|
232
|
+
return args.trim();
|
|
223
233
|
}
|
|
224
234
|
|
|
225
235
|
// Backfill only applies to the hooks variants of Claude Code / Codex; gateway
|
|
@@ -233,14 +243,16 @@ function scriptSupportsBackfill(scriptPath) {
|
|
|
233
243
|
// Surfaces an early note when --backfill was requested for a tool that can't
|
|
234
244
|
// use it. The setup scripts no-op safely too, but earlier user signal is better UX.
|
|
235
245
|
function noteBackfillUnsupported(label, scriptPath) {
|
|
236
|
-
if (scriptPath
|
|
237
|
-
output.info(`${label} backfill is not supported (no historical transcript data on disk). Continuing without backfill for ${label}.`);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
246
|
+
if (scriptSupportsBackfill(scriptPath)) return;
|
|
240
247
|
if (scriptPath.includes('/gateway/')) {
|
|
241
248
|
output.info(`--backfill is not supported in gateway mode for ${label}. Continuing without backfill for ${label}.`);
|
|
242
249
|
return;
|
|
243
250
|
}
|
|
251
|
+
if (scriptPath.startsWith('cursor/')) {
|
|
252
|
+
output.info(`${label} backfill is not supported (no historical transcript data on disk). Continuing without backfill for ${label}.`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
output.info(`${label} does not support --backfill. Continuing without backfill for ${label}.`);
|
|
244
256
|
}
|
|
245
257
|
|
|
246
258
|
/**
|
|
@@ -336,17 +348,18 @@ function register(program) {
|
|
|
336
348
|
'Run with no arguments for interactive setup, or specify tools directly.'
|
|
337
349
|
)
|
|
338
350
|
.option('--api-key <key>', 'Authenticate with an API key (skips browser login)')
|
|
339
|
-
.option('--clear', 'Remove Unbound configuration for the specified tools')
|
|
351
|
+
.option('--clear', 'Remove Unbound configuration for the specified tools (no login or API key required)')
|
|
340
352
|
.option('--subscription', 'Use subscription mode for Claude Code / Codex (hooks only)')
|
|
341
353
|
.option('--gateway', 'Use gateway mode for Claude Code / Codex (Unbound as AI provider)')
|
|
342
|
-
.option('--all', 'Set up the default bundle: Cursor, Claude Code (hooks), Codex (hooks)')
|
|
343
|
-
.option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (subscription/hooks mode only; Cursor unsupported)')
|
|
354
|
+
.option('--all', 'Set up the default bundle: Cursor, Copilot, Claude Code (hooks), Codex (hooks)')
|
|
355
|
+
.option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (subscription/hooks mode only; Cursor and Copilot unsupported)')
|
|
344
356
|
.addOption(new Option('--backend-url <url>', 'Override backend URL for setup scripts (dev only)').hideHelp())
|
|
345
357
|
.addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
|
|
346
358
|
.addOption(new Option('--gateway-url <url>', 'Override gateway URL for setup scripts (dev only)').hideHelp())
|
|
347
359
|
.addHelpText('after', `
|
|
348
360
|
Available tools:
|
|
349
361
|
cursor Cursor IDE
|
|
362
|
+
copilot GitHub Copilot
|
|
350
363
|
claude-code Claude Code (use --subscription or --gateway)
|
|
351
364
|
gemini-cli Gemini CLI
|
|
352
365
|
codex Codex (use --subscription or --gateway)
|
|
@@ -364,11 +377,12 @@ For multi-tool setup, use explicit mode names:
|
|
|
364
377
|
Examples:
|
|
365
378
|
Single tool:
|
|
366
379
|
$ unbound setup cursor Set up Cursor
|
|
380
|
+
$ unbound setup copilot Set up GitHub Copilot
|
|
367
381
|
$ unbound setup claude-code --gateway Claude Code gateway mode
|
|
368
382
|
$ unbound setup claude-code --subscription Claude Code hooks only
|
|
369
383
|
$ unbound setup codex --gateway Codex gateway mode
|
|
370
384
|
|
|
371
|
-
Install the default bundle (Cursor + Claude Code hooks + Codex hooks):
|
|
385
|
+
Install the default bundle (Cursor + Copilot + Claude Code hooks + Codex hooks):
|
|
372
386
|
$ unbound setup --all Set up the default bundle
|
|
373
387
|
$ unbound setup --all --api-key <key> Login + set up the bundle
|
|
374
388
|
|
|
@@ -381,16 +395,19 @@ Examples:
|
|
|
381
395
|
$ unbound setup cursor claude-code-gateway --api-key <key>
|
|
382
396
|
Login + set up multiple tools
|
|
383
397
|
|
|
384
|
-
Remove configuration:
|
|
398
|
+
Remove configuration (no login or API key required):
|
|
385
399
|
$ unbound setup cursor --clear Remove Cursor config
|
|
386
|
-
$ unbound setup
|
|
400
|
+
$ unbound setup copilot --clear Remove GitHub Copilot config
|
|
401
|
+
$ unbound setup claude-code --clear Remove BOTH Claude Code modes (subscription + gateway)
|
|
402
|
+
$ unbound setup codex --clear Remove BOTH Codex modes (subscription + gateway)
|
|
387
403
|
|
|
388
404
|
Interactive:
|
|
389
405
|
$ unbound setup Select tools interactively
|
|
390
406
|
$ unbound setup --api-key <key> Login, then select interactively
|
|
391
407
|
|
|
392
|
-
|
|
393
|
-
automatically to authenticate
|
|
408
|
+
When setting up, if you are not logged in and --api-key is not provided, the
|
|
409
|
+
browser opens automatically to authenticate first. Clearing (--clear) never
|
|
410
|
+
requires authentication.
|
|
394
411
|
`)
|
|
395
412
|
.action(async (tools, opts) => {
|
|
396
413
|
try {
|
|
@@ -408,11 +425,15 @@ automatically to authenticate before proceeding.
|
|
|
408
425
|
const frontendUrl = written.frontend_url || config.getFrontendUrl();
|
|
409
426
|
const gatewayUrl = written.gateway_url || config.getGatewayUrl();
|
|
410
427
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
428
|
+
// Clearing config needs no credentials — the setup scripts remove
|
|
429
|
+
// files without calling the API — so don't force a login for --clear.
|
|
430
|
+
if (!opts.clear) {
|
|
431
|
+
await ensureLoggedIn({
|
|
432
|
+
apiKey: opts.apiKey,
|
|
433
|
+
baseUrl: written.base_url,
|
|
434
|
+
frontendUrl: written.frontend_url,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
416
437
|
const apiKey = config.getApiKey();
|
|
417
438
|
const urlOpts = { backendUrl, frontendUrl, gatewayUrl };
|
|
418
439
|
|
|
@@ -423,7 +444,7 @@ automatically to authenticate before proceeding.
|
|
|
423
444
|
process.exitCode = 1;
|
|
424
445
|
return;
|
|
425
446
|
}
|
|
426
|
-
tools = [...
|
|
447
|
+
tools = [...SETUP_ALL_TOOLS];
|
|
427
448
|
}
|
|
428
449
|
|
|
429
450
|
// No tools specified → interactive multi-select (existing flow)
|
|
@@ -480,30 +501,33 @@ automatically to authenticate before proceeding.
|
|
|
480
501
|
return;
|
|
481
502
|
}
|
|
482
503
|
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
504
|
+
// Mode mutual-exclusivity only applies when setting up — clearing both
|
|
505
|
+
// modes at once is valid (and is what bare claude-code/codex --clear does).
|
|
506
|
+
if (!opts.clear) {
|
|
507
|
+
if (tools.includes('claude-code-subscription') && tools.includes('claude-code-gateway')) {
|
|
508
|
+
output.error('Cannot set up both claude-code-subscription and claude-code-gateway. Choose one.');
|
|
509
|
+
process.exitCode = 1;
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (tools.includes('codex-subscription') && tools.includes('codex-gateway')) {
|
|
513
|
+
output.error('Cannot set up both codex-subscription and codex-gateway. Choose one.');
|
|
514
|
+
process.exitCode = 1;
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
494
517
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
518
|
+
// Validate no bare + suffixed name conflicts
|
|
519
|
+
if (tools.includes('claude-code') && (tools.includes('claude-code-subscription') || tools.includes('claude-code-gateway'))) {
|
|
520
|
+
output.error('Cannot combine claude-code with claude-code-subscription or claude-code-gateway.');
|
|
521
|
+
console.error(' Use --subscription or --gateway with claude-code, or use the explicit name directly.');
|
|
522
|
+
process.exitCode = 1;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (tools.includes('codex') && (tools.includes('codex-subscription') || tools.includes('codex-gateway'))) {
|
|
526
|
+
output.error('Cannot combine codex with codex-subscription or codex-gateway.');
|
|
527
|
+
console.error(' Use --subscription or --gateway with codex, or use the explicit name directly.');
|
|
528
|
+
process.exitCode = 1;
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
507
531
|
}
|
|
508
532
|
|
|
509
533
|
// Validate --subscription/--gateway only with tools that need them
|
|
@@ -610,7 +634,9 @@ automatically to authenticate before proceeding.
|
|
|
610
634
|
|
|
611
635
|
// --- MDM setup ---
|
|
612
636
|
|
|
613
|
-
|
|
637
|
+
// Bare claude-code/codex are accepted too: with --clear they remove both
|
|
638
|
+
// modes; for setup they default to subscription (MDM has no mode prompt).
|
|
639
|
+
const mdmToolNames = [...Object.keys(MDM_TOOLS), 'claude-code', 'codex'].join(', ');
|
|
614
640
|
|
|
615
641
|
setup
|
|
616
642
|
.command('mdm')
|
|
@@ -619,30 +645,38 @@ automatically to authenticate before proceeding.
|
|
|
619
645
|
'Used by organization admins to enroll devices via MDM.'
|
|
620
646
|
)
|
|
621
647
|
.argument('[tools...]', 'Tools to set up: ' + mdmToolNames)
|
|
622
|
-
.
|
|
623
|
-
.option('--clear', 'Remove Unbound configuration for the specified tools')
|
|
648
|
+
.option('--admin-api-key <key>', 'Admin API key for MDM enrollment (not required with --clear)')
|
|
649
|
+
.option('--clear', 'Remove Unbound configuration for the specified tools (no API key required)')
|
|
624
650
|
.option('--all', 'Set up all available tools')
|
|
625
|
-
.option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (subscription/hooks mode only; Cursor unsupported)')
|
|
651
|
+
.option('--backfill', 'Seed historical Claude Code / Codex sessions from local transcripts into Unbound analytics (subscription/hooks mode only; Cursor and Copilot unsupported)')
|
|
626
652
|
.addHelpText('after', `
|
|
627
653
|
Available tools:
|
|
628
654
|
cursor Cursor IDE
|
|
655
|
+
copilot GitHub Copilot
|
|
629
656
|
claude-code-subscription Claude Code with your own subscription (hooks only)
|
|
630
657
|
claude-code-gateway Claude Code with Unbound as AI provider
|
|
658
|
+
claude-code Both Claude Code modes (clears both; sets up subscription)
|
|
631
659
|
gemini-cli Gemini CLI
|
|
632
660
|
codex-subscription Codex with your own subscription (hooks only)
|
|
633
661
|
codex-gateway Codex with Unbound as AI provider
|
|
662
|
+
codex Both Codex modes (clears both; sets up subscription)
|
|
634
663
|
|
|
635
|
-
Note: claude-code-subscription and claude-code-gateway are mutually exclusive
|
|
636
|
-
codex
|
|
637
|
-
When using --all, subscription mode is used by default for Claude Code and Codex.
|
|
664
|
+
Note: claude-code-subscription and claude-code-gateway are mutually exclusive when
|
|
665
|
+
setting up; same for codex. Bare claude-code/codex set up subscription mode.
|
|
666
|
+
When using --all, subscription mode is used by default for Claude Code and Codex.
|
|
638
667
|
|
|
639
|
-
|
|
668
|
+
Setup examples (require --admin-api-key):
|
|
640
669
|
$ sudo unbound setup mdm --admin-api-key KEY cursor
|
|
641
670
|
$ sudo unbound setup mdm --admin-api-key KEY cursor claude-code-subscription codex-subscription
|
|
642
671
|
$ sudo unbound setup mdm --admin-api-key KEY --all
|
|
643
|
-
$ sudo unbound setup mdm --admin-api-key KEY --clear cursor codex-subscription
|
|
644
672
|
$ sudo unbound setup mdm --admin-api-key KEY claude-code-subscription --backfill
|
|
645
673
|
Install hooks AND backfill local history
|
|
674
|
+
|
|
675
|
+
Clear examples (no API key required):
|
|
676
|
+
$ sudo unbound setup mdm --clear cursor
|
|
677
|
+
$ sudo unbound setup mdm --clear claude-code Clears BOTH Claude Code modes
|
|
678
|
+
$ sudo unbound setup mdm --clear codex Clears BOTH Codex modes
|
|
679
|
+
$ sudo unbound setup mdm --clear --all Clears every tool
|
|
646
680
|
`)
|
|
647
681
|
.action(async (tools, opts, command) => {
|
|
648
682
|
try {
|
|
@@ -651,6 +685,13 @@ Examples:
|
|
|
651
685
|
// --backend-url, --frontend-url, --gateway-url are defined only on the parent `setup` command.
|
|
652
686
|
// Use optsWithGlobals() so they all work regardless of position relative to `mdm`.
|
|
653
687
|
const globalOpts = command.optsWithGlobals();
|
|
688
|
+
// Clearing removes config without calling the API, so a key is only
|
|
689
|
+
// required when actually enrolling tools.
|
|
690
|
+
if (!globalOpts.clear && !opts.adminApiKey) {
|
|
691
|
+
output.error('--admin-api-key is required to set up tools.');
|
|
692
|
+
process.exitCode = 1;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
654
695
|
// Persist URLs first so this MDM run wires tools at the new tenant
|
|
655
696
|
// and any subsequent non-MDM command on the same machine inherits.
|
|
656
697
|
// Prefer just-persisted values over env-var-aware getters so a stale
|
|
@@ -670,7 +711,9 @@ Examples:
|
|
|
670
711
|
|
|
671
712
|
let toolNames;
|
|
672
713
|
if (globalOpts.all) {
|
|
673
|
-
|
|
714
|
+
// --clear --all wipes every tool, including both Claude Code/Codex modes.
|
|
715
|
+
// Setup --all uses the subscription-default bundle (can't enroll both modes).
|
|
716
|
+
toolNames = globalOpts.clear ? Object.keys(MDM_TOOLS) : MDM_SETUP_ALL_TOOLS;
|
|
674
717
|
} else if (tools.length > 0) {
|
|
675
718
|
toolNames = tools;
|
|
676
719
|
} else {
|
|
@@ -680,6 +723,14 @@ Examples:
|
|
|
680
723
|
return;
|
|
681
724
|
}
|
|
682
725
|
|
|
726
|
+
// Expand bare claude-code/codex (MDM has no interactive mode prompt):
|
|
727
|
+
// --clear removes both modes; setup defaults to subscription, matching --all.
|
|
728
|
+
toolNames = [...new Set(toolNames.flatMap(name => {
|
|
729
|
+
const mode = MODE_TOOLS[name];
|
|
730
|
+
if (!mode) return [name];
|
|
731
|
+
return globalOpts.clear ? [mode.subscription, mode.gateway] : [mode.subscription];
|
|
732
|
+
}))];
|
|
733
|
+
|
|
683
734
|
const invalid = toolNames.filter(t => !MDM_TOOLS[t]);
|
|
684
735
|
if (invalid.length > 0) {
|
|
685
736
|
output.error(`Unknown tool(s): ${invalid.join(', ')}`);
|
|
@@ -688,16 +739,20 @@ Examples:
|
|
|
688
739
|
return;
|
|
689
740
|
}
|
|
690
741
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
742
|
+
// Mode mutual-exclusivity only applies when setting up — clearing both
|
|
743
|
+
// modes at once is valid (and is what bare claude-code/codex --clear does).
|
|
744
|
+
if (!globalOpts.clear) {
|
|
745
|
+
if (toolNames.includes('claude-code-subscription') && toolNames.includes('claude-code-gateway')) {
|
|
746
|
+
output.error('Cannot use both claude-code-subscription and claude-code-gateway. Choose one.');
|
|
747
|
+
process.exitCode = 1;
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
696
750
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
751
|
+
if (toolNames.includes('codex-subscription') && toolNames.includes('codex-gateway')) {
|
|
752
|
+
output.error('Cannot use both codex-subscription and codex-gateway. Choose one.');
|
|
753
|
+
process.exitCode = 1;
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
701
756
|
}
|
|
702
757
|
|
|
703
758
|
const resolvedTools = toolNames.map(name => ({ name, ...MDM_TOOLS[name] }));
|
|
@@ -735,7 +790,7 @@ Examples:
|
|
|
735
790
|
}
|
|
736
791
|
|
|
737
792
|
/**
|
|
738
|
-
* Runs the user-level default bundle (Cursor, Claude Code hooks, Codex hooks) with spinners.
|
|
793
|
+
* Runs the user-level default bundle (Cursor, Claude Code hooks, Codex hooks, Copilot hooks) with spinners.
|
|
739
794
|
* Assumes the caller has already ensured the user is logged in.
|
|
740
795
|
* Returns true on success, false on failure.
|
|
741
796
|
*/
|
|
@@ -760,7 +815,7 @@ async function runSetupAllBundle(apiKey, { backendUrl, frontendUrl, gatewayUrl,
|
|
|
760
815
|
}
|
|
761
816
|
|
|
762
817
|
/**
|
|
763
|
-
* Runs the MDM default bundle (Cursor, Claude Code hooks, Gemini CLI, Codex hooks) with spinners.
|
|
818
|
+
* Runs the MDM default bundle (Cursor, Claude Code hooks, Gemini CLI, Codex hooks, Copilot hooks) with spinners.
|
|
764
819
|
* Caller must ensure the process is running as root.
|
|
765
820
|
* Returns true on success, false on failure.
|
|
766
821
|
*/
|
|
@@ -787,4 +842,5 @@ module.exports = {
|
|
|
787
842
|
checkRoot,
|
|
788
843
|
ALL_TOOLS,
|
|
789
844
|
MDM_ALL_TOOLS,
|
|
845
|
+
buildScriptArgs,
|
|
790
846
|
};
|
package/src/index.js
CHANGED
|
@@ -50,8 +50,9 @@ ONBOARDING (one-step install + discover)
|
|
|
50
50
|
|
|
51
51
|
TOOL SETUP
|
|
52
52
|
$ unbound setup Select and install multiple tools interactively
|
|
53
|
-
$ unbound setup --all Set up the default bundle (Cursor + Claude Code hooks + Codex hooks)
|
|
53
|
+
$ unbound setup --all Set up the default bundle (Cursor + Copilot + Claude Code hooks + Codex hooks)
|
|
54
54
|
$ unbound setup cursor Set up Cursor
|
|
55
|
+
$ unbound setup copilot Set up GitHub Copilot
|
|
55
56
|
$ unbound setup claude-code Set up Claude Code (interactive mode selection)
|
|
56
57
|
$ unbound setup claude-code --gateway Use Unbound as AI provider
|
|
57
58
|
$ unbound setup claude-code --subscription Hooks only (keep your subscription)
|
|
@@ -74,13 +75,14 @@ TOOL SETUP
|
|
|
74
75
|
|
|
75
76
|
Remove configuration:
|
|
76
77
|
$ unbound setup cursor --clear Remove Unbound config for Cursor
|
|
78
|
+
$ unbound setup copilot --clear Remove Unbound config for GitHub Copilot
|
|
77
79
|
$ unbound setup claude-code --clear Remove Unbound config for Claude Code
|
|
78
80
|
$ unbound setup gemini-cli --clear Remove Unbound config for Gemini CLI
|
|
79
81
|
$ unbound setup codex --clear Remove Unbound config for Codex
|
|
80
82
|
|
|
81
83
|
MDM SETUP (admin, requires root)
|
|
82
84
|
$ sudo unbound setup mdm --admin-api-key KEY --all
|
|
83
|
-
$ sudo unbound setup mdm --admin-api-key KEY cursor codex-subscription
|
|
85
|
+
$ sudo unbound setup mdm --admin-api-key KEY cursor copilot codex-subscription
|
|
84
86
|
$ sudo unbound setup mdm --admin-api-key KEY claude-code-subscription codex-subscription gemini-cli
|
|
85
87
|
$ sudo unbound setup mdm --admin-api-key KEY --clear cursor codex-subscription
|
|
86
88
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { classifyDiscoveryExit } = require('../src/commands/discover');
|
|
4
|
+
|
|
5
|
+
test('classifyDiscoveryExit: 0 is a successful scan', () => {
|
|
6
|
+
assert.equal(classifyDiscoveryExit(0), 'success');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('classifyDiscoveryExit: 3 is an unsupported-OS skip, not a failure', () => {
|
|
10
|
+
assert.equal(classifyDiscoveryExit(3), 'unsupported');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('classifyDiscoveryExit: other non-zero codes are failures', () => {
|
|
14
|
+
assert.equal(classifyDiscoveryExit(1), 'failure');
|
|
15
|
+
assert.equal(classifyDiscoveryExit(2), 'failure');
|
|
16
|
+
assert.equal(classifyDiscoveryExit(127), 'failure');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('classifyDiscoveryExit: null (process killed by signal) is a failure', () => {
|
|
20
|
+
assert.equal(classifyDiscoveryExit(null), 'failure');
|
|
21
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { buildScriptArgs } = require('../src/commands/setup');
|
|
4
|
+
|
|
5
|
+
// shellEscape single-quotes every value, so a real key surfaces as
|
|
6
|
+
// --api-key '<key>' at the head of the argv tail.
|
|
7
|
+
test('buildScriptArgs: real apiKey leads with single-quoted --api-key', () => {
|
|
8
|
+
const args = buildScriptArgs('sk-abc123', {});
|
|
9
|
+
assert.ok(args.startsWith("--api-key 'sk-abc123'"), args);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// WEB-4498 core behavior: --clear runs no auth, so a missing key must NOT
|
|
13
|
+
// produce --api-key and must NOT leak the literal string "undefined".
|
|
14
|
+
test('buildScriptArgs: clear without apiKey omits --api-key and never emits undefined', () => {
|
|
15
|
+
const args = buildScriptArgs(undefined, { clear: true });
|
|
16
|
+
assert.ok(!args.includes('--api-key'), args);
|
|
17
|
+
assert.ok(!args.includes('undefined'), args);
|
|
18
|
+
assert.ok(args.includes('--clear'), args);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Empty-string key is falsy too — same omission rule applies.
|
|
22
|
+
test('buildScriptArgs: clear with empty-string apiKey still omits --api-key', () => {
|
|
23
|
+
const args = buildScriptArgs('', { clear: true });
|
|
24
|
+
assert.ok(!args.includes('--api-key'), args);
|
|
25
|
+
assert.ok(!args.includes('undefined'), args);
|
|
26
|
+
assert.ok(args.includes('--clear'), args);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// With no key, the first appended flag must not leave a leading space —
|
|
30
|
+
// the result is trimmed and carries the URL flags plus --clear.
|
|
31
|
+
test('buildScriptArgs: clear without key + URLs is trimmed and carries url flags', () => {
|
|
32
|
+
const args = buildScriptArgs(undefined, {
|
|
33
|
+
clear: true,
|
|
34
|
+
backendUrl: 'https://backend.acme.com',
|
|
35
|
+
gatewayUrl: 'https://gateway.acme.com',
|
|
36
|
+
});
|
|
37
|
+
assert.equal(args, args.trim());
|
|
38
|
+
assert.ok(!args.startsWith(' '), args);
|
|
39
|
+
assert.ok(args.includes('--backend-url'), args);
|
|
40
|
+
assert.ok(args.includes('--gateway-url'), args);
|
|
41
|
+
assert.ok(args.includes('--clear'), args);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('buildScriptArgs: clear:true appends --clear', () => {
|
|
45
|
+
const args = buildScriptArgs('sk-x', { clear: true });
|
|
46
|
+
assert.ok(args.includes('--clear'), args);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('buildScriptArgs: backfill:true appends --backfill', () => {
|
|
50
|
+
const args = buildScriptArgs('sk-x', { backfill: true });
|
|
51
|
+
assert.ok(args.includes('--backfill'), args);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// MDM scripts have no browser-auth flow, so --domain (frontend URL) is
|
|
55
|
+
// suppressed even when a frontendUrl is supplied.
|
|
56
|
+
test('buildScriptArgs: mdm:true suppresses --domain even with frontendUrl', () => {
|
|
57
|
+
const args = buildScriptArgs('sk-x', {
|
|
58
|
+
mdm: true,
|
|
59
|
+
frontendUrl: 'https://gateway.acme.com',
|
|
60
|
+
});
|
|
61
|
+
assert.ok(!args.includes('--domain'), args);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('buildScriptArgs: non-mdm includes --domain when frontendUrl passed', () => {
|
|
65
|
+
const args = buildScriptArgs('sk-x', {
|
|
66
|
+
mdm: false,
|
|
67
|
+
frontendUrl: 'https://gateway.acme.com',
|
|
68
|
+
});
|
|
69
|
+
assert.ok(args.includes("--domain 'https://gateway.acme.com'"), args);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// The result is always trimmed regardless of which flags are present.
|
|
73
|
+
test('buildScriptArgs: result is always trimmed', () => {
|
|
74
|
+
const cases = [
|
|
75
|
+
['sk-x', {}],
|
|
76
|
+
[undefined, { clear: true }],
|
|
77
|
+
['', { clear: true, backendUrl: 'https://b.acme.com' }],
|
|
78
|
+
['sk-x', { backfill: true, mdm: true, frontendUrl: 'https://f.acme.com' }],
|
|
79
|
+
[undefined, {}],
|
|
80
|
+
];
|
|
81
|
+
for (const [key, opts] of cases) {
|
|
82
|
+
const args = buildScriptArgs(key, opts);
|
|
83
|
+
assert.equal(args, args.trim(), JSON.stringify([key, opts]));
|
|
84
|
+
}
|
|
85
|
+
});
|