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/setup.js
CHANGED
|
@@ -45,10 +45,10 @@ const MDM_TOOLS = {
|
|
|
45
45
|
'codex-gateway': { label: 'Codex (gateway)', script: 'codex/gateway/mdm/setup.py' },
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
// Default MDM tools for `unbound onboard
|
|
48
|
+
// Default MDM tools for `sudo unbound onboard` (subscription mode for Claude Code/Codex since only one can be active)
|
|
49
49
|
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
50
50
|
|
|
51
|
-
// Tools for `unbound setup
|
|
51
|
+
// Tools for `sudo unbound setup --all` — identical to MDM_ALL_TOOLS today; split kept for future flexibility.
|
|
52
52
|
const MDM_SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
53
53
|
|
|
54
54
|
// Default tools for `unbound onboard` (Cursor, Claude Code hooks, Codex hooks, Copilot hooks; no Gemini CLI).
|
|
@@ -73,7 +73,7 @@ const SETUP_TOOL_MAP = {
|
|
|
73
73
|
* bundle (one mode per tool, no Gemini CLI), but clearing must remove EVERY
|
|
74
74
|
* tool Unbound can configure — both modes plus Gemini CLI — so a prior
|
|
75
75
|
* gateway-mode or Gemini setup isn't silently left behind. Mirrors the
|
|
76
|
-
*
|
|
76
|
+
* MDM `--all` split.
|
|
77
77
|
*/
|
|
78
78
|
function resolveSetupAllTools(clear) {
|
|
79
79
|
return clear ? Object.keys(SETUP_TOOL_MAP) : [...SETUP_ALL_TOOLS];
|
|
@@ -338,18 +338,6 @@ function runScriptPiped(scriptPath, args) {
|
|
|
338
338
|
});
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
/**
|
|
342
|
-
* Checks that the process is running as root (macOS/Linux).
|
|
343
|
-
* Windows admin check is handled by the Python MDM scripts themselves.
|
|
344
|
-
* Pass a customized hint for the calling command (defaults to "setup mdm").
|
|
345
|
-
*/
|
|
346
|
-
function checkRoot(commandHint = 'setup mdm') {
|
|
347
|
-
if (process.platform === 'win32') return;
|
|
348
|
-
if (typeof process.getuid !== 'function' || process.getuid() !== 0) {
|
|
349
|
-
throw new Error(`MDM setup requires root. Run with: sudo unbound ${commandHint} ...`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
341
|
/**
|
|
354
342
|
* Returns true when the process has the privileges needed to touch system-level
|
|
355
343
|
* (MDM) configuration. On Windows, `net session` succeeds only when elevated, so
|
|
@@ -455,9 +443,12 @@ function register(program) {
|
|
|
455
443
|
.argument('[tools...]', 'Tools to set up')
|
|
456
444
|
.description(
|
|
457
445
|
'Configure AI coding tools to use Unbound as their API gateway. ' +
|
|
458
|
-
'Run with no arguments for interactive setup, or specify tools directly.'
|
|
446
|
+
'Run with no arguments for interactive setup, or specify tools directly. ' +
|
|
447
|
+
'Run with sudo to configure every user on the device (MDM/org scope); ' +
|
|
448
|
+
'without sudo, only the current user.'
|
|
459
449
|
)
|
|
460
450
|
.option('--api-key <key>', 'Authenticate with an API key (skips browser login)')
|
|
451
|
+
.addOption(new Option('--admin-api-key <key>', 'Alias for --api-key for MDM enrollment (back-compat)').hideHelp())
|
|
461
452
|
.option('--clear', 'Remove Unbound configuration for the specified tools (no login or API key required)')
|
|
462
453
|
.option('--subscription', 'Use subscription mode for Claude Code / Codex (hooks only)')
|
|
463
454
|
.option('--gateway', 'Use gateway mode for Claude Code / Codex (Unbound as AI provider)')
|
|
@@ -467,6 +458,10 @@ function register(program) {
|
|
|
467
458
|
.addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
|
|
468
459
|
.addOption(new Option('--gateway-url <url>', 'Override gateway URL for setup scripts (dev only)').hideHelp())
|
|
469
460
|
.addHelpText('after', `
|
|
461
|
+
Scope is chosen automatically from your privileges:
|
|
462
|
+
Run with sudo to configure every user on the device (MDM/org scope);
|
|
463
|
+
without sudo, only the current user is configured.
|
|
464
|
+
|
|
470
465
|
Available tools:
|
|
471
466
|
cursor Cursor IDE
|
|
472
467
|
copilot GitHub Copilot
|
|
@@ -496,6 +491,11 @@ Examples:
|
|
|
496
491
|
$ unbound setup --all Set up the default bundle
|
|
497
492
|
$ unbound setup --all --api-key <key> Login + set up the bundle
|
|
498
493
|
|
|
494
|
+
Configure all users on the device (MDM/org scope — run with sudo):
|
|
495
|
+
$ sudo unbound setup --all Configure all users (MDM)
|
|
496
|
+
$ sudo unbound setup cursor codex-subscription Configure specific tools for all users
|
|
497
|
+
$ sudo unbound setup --clear --all Remove config for all users
|
|
498
|
+
|
|
499
499
|
Seed historical sessions (Claude Code / Codex subscription mode + Copilot):
|
|
500
500
|
$ unbound setup claude-code --subscription --backfill Install hooks AND backfill local history
|
|
501
501
|
$ unbound setup codex --subscription --backfill Install hooks AND backfill local history
|
|
@@ -545,6 +545,95 @@ must update the MDM configuration.
|
|
|
545
545
|
const frontendUrl = written.frontend_url || config.getFrontendUrl();
|
|
546
546
|
const gatewayUrl = written.gateway_url || config.getGatewayUrl();
|
|
547
547
|
|
|
548
|
+
// Scope is auto-detected from privileges: with sudo/root we configure
|
|
549
|
+
// every user on the device (MDM scope); without it, just the current
|
|
550
|
+
// user. URLs were already persisted above and apply to both scopes.
|
|
551
|
+
const isMdm = hasRootPrivileges();
|
|
552
|
+
if (isMdm) {
|
|
553
|
+
const mdmToolNames = [...Object.keys(MDM_TOOLS), 'claude-code', 'codex'].join(', ');
|
|
554
|
+
const adminApiKey = opts.adminApiKey || opts.apiKey || config.getApiKey();
|
|
555
|
+
if (!opts.clear && !adminApiKey) {
|
|
556
|
+
output.error('--api-key is required to set up tools (or run `unbound login` first).');
|
|
557
|
+
process.exitCode = 1;
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (opts.all && tools.length > 0) {
|
|
561
|
+
output.error('Cannot combine --all with specific tool names. Use one or the other.');
|
|
562
|
+
process.exitCode = 1;
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
let toolNames;
|
|
566
|
+
if (opts.all) {
|
|
567
|
+
// --clear --all wipes every tool (both modes); setup --all uses the
|
|
568
|
+
// subscription-default bundle (can't enroll both modes at once).
|
|
569
|
+
toolNames = opts.clear ? Object.keys(MDM_TOOLS) : MDM_SETUP_ALL_TOOLS;
|
|
570
|
+
} else if (tools.length > 0) {
|
|
571
|
+
toolNames = tools;
|
|
572
|
+
} else {
|
|
573
|
+
output.error('Specify tools to set up, or use --all.');
|
|
574
|
+
console.error(' Available: ' + mdmToolNames);
|
|
575
|
+
process.exitCode = 1;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Bare claude-code/codex have no interactive mode prompt under MDM:
|
|
579
|
+
// --clear removes both modes; setup honors --gateway/--subscription
|
|
580
|
+
// (matching user scope) and defaults to subscription.
|
|
581
|
+
if (!opts.clear && opts.subscription && opts.gateway) {
|
|
582
|
+
output.error('Cannot use both --subscription and --gateway. Choose one.');
|
|
583
|
+
process.exitCode = 1;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
toolNames = [...new Set(toolNames.flatMap(name => {
|
|
587
|
+
const mode = MODE_TOOLS[name];
|
|
588
|
+
if (!mode) return [name];
|
|
589
|
+
if (opts.clear) return [mode.subscription, mode.gateway];
|
|
590
|
+
return opts.gateway ? [mode.gateway] : [mode.subscription];
|
|
591
|
+
}))];
|
|
592
|
+
const invalid = toolNames.filter(t => !MDM_TOOLS[t]);
|
|
593
|
+
if (invalid.length > 0) {
|
|
594
|
+
output.error(`Unknown tool(s): ${invalid.join(', ')}`);
|
|
595
|
+
console.error(' Available: ' + mdmToolNames);
|
|
596
|
+
process.exitCode = 1;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (!opts.clear) {
|
|
600
|
+
if (toolNames.includes('claude-code-subscription') && toolNames.includes('claude-code-gateway')) {
|
|
601
|
+
output.error('Cannot use both claude-code-subscription and claude-code-gateway. Choose one.');
|
|
602
|
+
process.exitCode = 1;
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (toolNames.includes('codex-subscription') && toolNames.includes('codex-gateway')) {
|
|
606
|
+
output.error('Cannot use both codex-subscription and codex-gateway. Choose one.');
|
|
607
|
+
process.exitCode = 1;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const resolvedTools = toolNames.map(name => ({ name, ...MDM_TOOLS[name] }));
|
|
612
|
+
console.log('');
|
|
613
|
+
if (opts.backfill) {
|
|
614
|
+
for (const tool of resolvedTools) {
|
|
615
|
+
if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const { ok } = await runBatch(
|
|
619
|
+
resolvedTools,
|
|
620
|
+
(tool) => {
|
|
621
|
+
const toolArgs = buildScriptArgs(adminApiKey, {
|
|
622
|
+
backendUrl,
|
|
623
|
+
frontendUrl,
|
|
624
|
+
gatewayUrl,
|
|
625
|
+
clear: opts.clear,
|
|
626
|
+
mdm: true,
|
|
627
|
+
backfill: opts.backfill && scriptSupportsBackfill(tool.script),
|
|
628
|
+
});
|
|
629
|
+
return runScriptPiped(tool.script, toolArgs);
|
|
630
|
+
},
|
|
631
|
+
{ clear: opts.clear, summary: opts.clear ? 'All tools cleared' : 'All tools configured' }
|
|
632
|
+
);
|
|
633
|
+
if (!ok) return;
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
548
637
|
// Clearing config needs no credentials — the setup scripts remove
|
|
549
638
|
// files without calling the API — so don't force a login for --clear.
|
|
550
639
|
if (!opts.clear) {
|
|
@@ -747,169 +836,6 @@ must update the MDM configuration.
|
|
|
747
836
|
}
|
|
748
837
|
});
|
|
749
838
|
|
|
750
|
-
// --- MDM setup ---
|
|
751
|
-
|
|
752
|
-
// Bare claude-code/codex are accepted too: with --clear they remove both
|
|
753
|
-
// modes; for setup they default to subscription (MDM has no mode prompt).
|
|
754
|
-
const mdmToolNames = [...Object.keys(MDM_TOOLS), 'claude-code', 'codex'].join(', ');
|
|
755
|
-
|
|
756
|
-
setup
|
|
757
|
-
.command('mdm')
|
|
758
|
-
.description(
|
|
759
|
-
'MDM setup: configure all users on this device. Requires root. ' +
|
|
760
|
-
'Used by organization admins to enroll devices via MDM.'
|
|
761
|
-
)
|
|
762
|
-
.argument('[tools...]', 'Tools to set up: ' + mdmToolNames)
|
|
763
|
-
.option('--admin-api-key <key>', 'Admin API key for MDM enrollment (falls back to your stored `unbound login` key; not required with --clear)')
|
|
764
|
-
.option('--clear', 'Remove Unbound configuration for the specified tools (no API key required)')
|
|
765
|
-
.option('--all', 'Set up all available tools')
|
|
766
|
-
.option('--backfill', 'Seed historical Claude Code / Codex / Copilot sessions from local transcripts into Unbound analytics (subscription/hooks mode only; Cursor unsupported)')
|
|
767
|
-
.addHelpText('after', `
|
|
768
|
-
Available tools:
|
|
769
|
-
cursor Cursor IDE
|
|
770
|
-
copilot GitHub Copilot
|
|
771
|
-
claude-code-subscription Claude Code with your own subscription (hooks only)
|
|
772
|
-
claude-code-gateway Claude Code with Unbound as AI provider
|
|
773
|
-
claude-code Both Claude Code modes (clears both; sets up subscription)
|
|
774
|
-
gemini-cli Gemini CLI
|
|
775
|
-
codex-subscription Codex with your own subscription (hooks only)
|
|
776
|
-
codex-gateway Codex with Unbound as AI provider
|
|
777
|
-
codex Both Codex modes (clears both; sets up subscription)
|
|
778
|
-
|
|
779
|
-
Note: claude-code-subscription and claude-code-gateway are mutually exclusive when
|
|
780
|
-
setting up; same for codex. Bare claude-code/codex set up subscription mode.
|
|
781
|
-
When using --all, subscription mode is used by default for Claude Code and Codex.
|
|
782
|
-
|
|
783
|
-
Setup examples (need an admin key — pass --admin-api-key, or omit it to reuse the
|
|
784
|
-
key stored by a prior \`unbound login\`):
|
|
785
|
-
$ sudo unbound setup mdm --admin-api-key KEY cursor
|
|
786
|
-
$ sudo unbound setup mdm --admin-api-key KEY cursor claude-code-subscription codex-subscription
|
|
787
|
-
$ sudo unbound setup mdm --admin-api-key KEY --all
|
|
788
|
-
$ sudo unbound setup mdm --all Reuse the stored \`unbound login\` key
|
|
789
|
-
$ sudo unbound setup mdm --admin-api-key KEY claude-code-subscription --backfill
|
|
790
|
-
Install hooks AND backfill local history
|
|
791
|
-
$ sudo unbound setup mdm --admin-api-key KEY copilot --backfill
|
|
792
|
-
Install Copilot hooks AND backfill local history
|
|
793
|
-
|
|
794
|
-
Clear examples (no API key required):
|
|
795
|
-
$ sudo unbound setup mdm --clear cursor
|
|
796
|
-
$ sudo unbound setup mdm --clear claude-code Clears BOTH Claude Code modes
|
|
797
|
-
$ sudo unbound setup mdm --clear codex Clears BOTH Codex modes
|
|
798
|
-
$ sudo unbound setup mdm --clear --all Clears every tool
|
|
799
|
-
`)
|
|
800
|
-
.action(async (tools, opts, command) => {
|
|
801
|
-
try {
|
|
802
|
-
checkRoot();
|
|
803
|
-
// --all and --clear are defined on both this command and the parent `setup` command;
|
|
804
|
-
// --backend-url, --frontend-url, --gateway-url are defined only on the parent `setup` command.
|
|
805
|
-
// Use optsWithGlobals() so they all work regardless of position relative to `mdm`.
|
|
806
|
-
const globalOpts = command.optsWithGlobals();
|
|
807
|
-
// Clearing removes config without calling the API, so a key is only
|
|
808
|
-
// required when actually enrolling tools. Fall back to the API key
|
|
809
|
-
// stored by `unbound login` so admins who are already logged in don't
|
|
810
|
-
// have to pass --admin-api-key again.
|
|
811
|
-
const adminApiKey = opts.adminApiKey || config.getApiKey();
|
|
812
|
-
if (!globalOpts.clear && !adminApiKey) {
|
|
813
|
-
output.error('--admin-api-key is required to set up tools (or run `unbound login` first).');
|
|
814
|
-
process.exitCode = 1;
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
// Persist URLs first so this MDM run wires tools at the new tenant
|
|
818
|
-
// and any subsequent non-MDM command on the same machine inherits.
|
|
819
|
-
// Prefer just-persisted values over env-var-aware getters so a stale
|
|
820
|
-
// UNBOUND_*_URL can't shadow the explicit --*-url flag.
|
|
821
|
-
const written = config.setUrls({
|
|
822
|
-
backend: globalOpts.backendUrl,
|
|
823
|
-
frontend: globalOpts.frontendUrl,
|
|
824
|
-
gateway: globalOpts.gatewayUrl,
|
|
825
|
-
});
|
|
826
|
-
const backendUrl = written.base_url || config.getBaseUrl();
|
|
827
|
-
const frontendUrl = written.frontend_url || config.getFrontendUrl();
|
|
828
|
-
const gatewayUrl = written.gateway_url || config.getGatewayUrl();
|
|
829
|
-
|
|
830
|
-
if (globalOpts.all && tools.length > 0) {
|
|
831
|
-
output.error('Cannot combine --all with specific tool names. Use one or the other.');
|
|
832
|
-
process.exitCode = 1;
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
let toolNames;
|
|
837
|
-
if (globalOpts.all) {
|
|
838
|
-
// --clear --all wipes every tool, including both Claude Code/Codex modes.
|
|
839
|
-
// Setup --all uses the subscription-default bundle (can't enroll both modes).
|
|
840
|
-
toolNames = globalOpts.clear ? Object.keys(MDM_TOOLS) : MDM_SETUP_ALL_TOOLS;
|
|
841
|
-
} else if (tools.length > 0) {
|
|
842
|
-
toolNames = tools;
|
|
843
|
-
} else {
|
|
844
|
-
output.error('Specify tools to set up, or use --all.');
|
|
845
|
-
console.error(' Available: ' + mdmToolNames);
|
|
846
|
-
process.exitCode = 1;
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// Expand bare claude-code/codex (MDM has no interactive mode prompt):
|
|
851
|
-
// --clear removes both modes; setup defaults to subscription, matching --all.
|
|
852
|
-
toolNames = [...new Set(toolNames.flatMap(name => {
|
|
853
|
-
const mode = MODE_TOOLS[name];
|
|
854
|
-
if (!mode) return [name];
|
|
855
|
-
return globalOpts.clear ? [mode.subscription, mode.gateway] : [mode.subscription];
|
|
856
|
-
}))];
|
|
857
|
-
|
|
858
|
-
const invalid = toolNames.filter(t => !MDM_TOOLS[t]);
|
|
859
|
-
if (invalid.length > 0) {
|
|
860
|
-
output.error(`Unknown tool(s): ${invalid.join(', ')}`);
|
|
861
|
-
console.error(' Available: ' + mdmToolNames);
|
|
862
|
-
process.exitCode = 1;
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
// Mode mutual-exclusivity only applies when setting up — clearing both
|
|
867
|
-
// modes at once is valid (and is what bare claude-code/codex --clear does).
|
|
868
|
-
if (!globalOpts.clear) {
|
|
869
|
-
if (toolNames.includes('claude-code-subscription') && toolNames.includes('claude-code-gateway')) {
|
|
870
|
-
output.error('Cannot use both claude-code-subscription and claude-code-gateway. Choose one.');
|
|
871
|
-
process.exitCode = 1;
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
if (toolNames.includes('codex-subscription') && toolNames.includes('codex-gateway')) {
|
|
876
|
-
output.error('Cannot use both codex-subscription and codex-gateway. Choose one.');
|
|
877
|
-
process.exitCode = 1;
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const resolvedTools = toolNames.map(name => ({ name, ...MDM_TOOLS[name] }));
|
|
883
|
-
console.log('');
|
|
884
|
-
|
|
885
|
-
if (globalOpts.backfill) {
|
|
886
|
-
for (const tool of resolvedTools) {
|
|
887
|
-
if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
const { ok } = await runBatch(
|
|
892
|
-
resolvedTools,
|
|
893
|
-
(tool) => {
|
|
894
|
-
const toolArgs = buildScriptArgs(adminApiKey, {
|
|
895
|
-
backendUrl,
|
|
896
|
-
frontendUrl,
|
|
897
|
-
gatewayUrl,
|
|
898
|
-
clear: globalOpts.clear,
|
|
899
|
-
mdm: true,
|
|
900
|
-
backfill: globalOpts.backfill && scriptSupportsBackfill(tool.script),
|
|
901
|
-
});
|
|
902
|
-
return runScriptPiped(tool.script, toolArgs);
|
|
903
|
-
},
|
|
904
|
-
{ clear: globalOpts.clear, summary: globalOpts.clear ? 'All tools cleared' : 'All tools configured' }
|
|
905
|
-
);
|
|
906
|
-
if (!ok) return;
|
|
907
|
-
} catch (err) {
|
|
908
|
-
output.error(err.message);
|
|
909
|
-
process.exitCode = 1;
|
|
910
|
-
}
|
|
911
|
-
});
|
|
912
|
-
|
|
913
839
|
// --- Full uninstall ---
|
|
914
840
|
|
|
915
841
|
program
|
|
@@ -1054,7 +980,7 @@ module.exports = {
|
|
|
1054
980
|
register,
|
|
1055
981
|
runSetupAllBundle,
|
|
1056
982
|
runMdmSetupAllBundle,
|
|
1057
|
-
|
|
983
|
+
hasRootPrivileges,
|
|
1058
984
|
ALL_TOOLS,
|
|
1059
985
|
MDM_ALL_TOOLS,
|
|
1060
986
|
buildScriptArgs,
|
package/src/commands/status.js
CHANGED
|
@@ -2,21 +2,36 @@ const config = require('../config');
|
|
|
2
2
|
const api = require('../api');
|
|
3
3
|
const output = require('../output');
|
|
4
4
|
const { getDeviceSerial } = require('../device-serial');
|
|
5
|
+
const { detectTools } = require('../toolHealth');
|
|
6
|
+
|
|
7
|
+
function roleFromPrivileges(p) {
|
|
8
|
+
if (!p) return null;
|
|
9
|
+
if (p.is_admin) return 'Admin';
|
|
10
|
+
if (p.is_manager) return 'Manager';
|
|
11
|
+
if (p.is_member) return 'Member';
|
|
12
|
+
return 'Unknown';
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
|
|
6
16
|
function register(program) {
|
|
7
17
|
program
|
|
8
18
|
.command('status')
|
|
9
|
-
.description('Show the current CLI status
|
|
19
|
+
.description('Show the current CLI status: config location, login state, role, connected tools, and API connectivity. Useful for debugging connection issues.')
|
|
10
20
|
.addHelpText('after', `
|
|
11
21
|
Output fields:
|
|
12
22
|
Config file - Path to the config file (~/.unbound/config.json)
|
|
13
23
|
Logged in - Whether credentials are stored (Yes/No)
|
|
14
24
|
Email - The authenticated user's email (if logged in)
|
|
15
25
|
Organization - The organization name (if logged in)
|
|
26
|
+
Role - Admin / Manager / Member (if logged in)
|
|
16
27
|
Backend URL - REST API host (configurable for tenant deployments)
|
|
17
28
|
Frontend URL - Browser login host
|
|
18
29
|
Gateway URL - AI gateway host (used by tool setup)
|
|
19
30
|
API status - Connectivity check result (Connected / Error)
|
|
31
|
+
Connected tools - AI tools wired through Unbound on this device, with mode
|
|
32
|
+
|
|
33
|
+
For a deep per-tool health check (config, hook script, env wiring), run
|
|
34
|
+
\`unbound doctor\`.
|
|
20
35
|
|
|
21
36
|
Examples:
|
|
22
37
|
$ unbound status
|
|
@@ -32,8 +47,8 @@ Examples:
|
|
|
32
47
|
['Logged in', loggedIn ? 'Yes' : 'No'],
|
|
33
48
|
];
|
|
34
49
|
|
|
35
|
-
// Check API connectivity
|
|
36
50
|
let connectivity = 'Not checked (not logged in)';
|
|
51
|
+
let role = null;
|
|
37
52
|
if (loggedIn) {
|
|
38
53
|
const spin = output.spinner('Checking API connectivity...');
|
|
39
54
|
try {
|
|
@@ -42,6 +57,7 @@ Examples:
|
|
|
42
57
|
query: { device_serial: deviceSerial },
|
|
43
58
|
});
|
|
44
59
|
config.backfillUserInfo(privileges);
|
|
60
|
+
role = roleFromPrivileges(privileges);
|
|
45
61
|
spin.stop();
|
|
46
62
|
connectivity = 'Connected';
|
|
47
63
|
} catch (err) {
|
|
@@ -52,12 +68,17 @@ Examples:
|
|
|
52
68
|
const cfg = config.readConfig();
|
|
53
69
|
pairs.push(['Email', cfg.email || '-']);
|
|
54
70
|
pairs.push(['Organization', cfg.org_name || '-']);
|
|
71
|
+
pairs.push(['Role', role || '-']);
|
|
55
72
|
}
|
|
56
73
|
pairs.push(['Backend URL', config.getBaseUrl()]);
|
|
57
74
|
pairs.push(['Frontend URL', config.getFrontendUrl()]);
|
|
58
75
|
pairs.push(['Gateway URL', config.getGatewayUrl()]);
|
|
59
76
|
pairs.push(['API status', connectivity]);
|
|
60
77
|
|
|
78
|
+
// Locally detected AI tools wired through Unbound, with their mode.
|
|
79
|
+
const connected = detectTools({ gatewayUrl: config.getGatewayUrl(), apiKey: config.getApiKey() })
|
|
80
|
+
.filter((t) => t.status !== 'not-installed');
|
|
81
|
+
|
|
61
82
|
if (opts.json) {
|
|
62
83
|
const cfg = loggedIn ? config.readConfig() : {};
|
|
63
84
|
output.json({
|
|
@@ -65,15 +86,34 @@ Examples:
|
|
|
65
86
|
logged_in: loggedIn,
|
|
66
87
|
email: loggedIn ? (cfg.email || null) : null,
|
|
67
88
|
organization: loggedIn ? (cfg.org_name || null) : null,
|
|
89
|
+
role: role,
|
|
68
90
|
backend_url: config.getBaseUrl(),
|
|
69
91
|
frontend_url: config.getFrontendUrl(),
|
|
70
92
|
gateway_url: config.getGatewayUrl(),
|
|
71
93
|
api_status: connectivity,
|
|
94
|
+
connected_tools: connected.map((t) => ({ tool: t.key, label: t.label, mode: t.mode, status: t.status })),
|
|
72
95
|
});
|
|
73
96
|
return;
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
output.keyValue(pairs);
|
|
100
|
+
|
|
101
|
+
console.log('');
|
|
102
|
+
output.info('Connected tools');
|
|
103
|
+
const C = output.colors;
|
|
104
|
+
if (connected.length === 0) {
|
|
105
|
+
console.log(` ${C.dim('None set up yet.')} Run ${C.bold('unbound setup')} to wire a tool.`);
|
|
106
|
+
} else {
|
|
107
|
+
for (const t of connected) {
|
|
108
|
+
const mode = t.mode ? C.dim(` (${t.mode})`) : '';
|
|
109
|
+
let mark = C.green('✓');
|
|
110
|
+
let note = '';
|
|
111
|
+
if (t.status === 'tampered') { mark = C.red('✗'); note = C.dim(' — run `unbound doctor`'); }
|
|
112
|
+
else if (t.status === 'managed-by-mdm') { note = C.dim(' (managed by MDM)'); }
|
|
113
|
+
console.log(` ${mark} ${t.label}${mode}${note}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log('');
|
|
77
117
|
} catch (err) {
|
|
78
118
|
output.error(err.message);
|
|
79
119
|
process.exitCode = 1;
|
package/src/config.js
CHANGED
|
@@ -141,7 +141,7 @@ function isLoggedIn() {
|
|
|
141
141
|
* Refreshes cached user identity (email, org_name) from a backend response.
|
|
142
142
|
* Always overwrites when the response carries a non-empty value so that
|
|
143
143
|
* switching tenants under the same API key (or rotating the key to a new org)
|
|
144
|
-
* shows the correct organization in `
|
|
144
|
+
* shows the correct organization in `status` instead of stale data.
|
|
145
145
|
* Defensive: leaves an existing cached value untouched if the response field
|
|
146
146
|
* is missing or empty, so a partial API response can't blank the local config.
|
|
147
147
|
*/
|
package/src/index.js
CHANGED
|
@@ -29,8 +29,8 @@ AUTHENTICATION
|
|
|
29
29
|
$ unbound login --api-key <key> Sign in with an API key (for CI/CD)
|
|
30
30
|
$ unbound login --domain custom.co Sign in via custom domain
|
|
31
31
|
$ unbound logout Remove stored credentials
|
|
32
|
-
$ unbound
|
|
33
|
-
$ unbound
|
|
32
|
+
$ unbound status Show CLI status, role, and connected tools
|
|
33
|
+
$ unbound doctor Diagnose per-tool health and API key
|
|
34
34
|
|
|
35
35
|
Tenant deployments — pass URL flags on login; they persist to ~/.unbound/config.json:
|
|
36
36
|
$ unbound login --api-key <YOUR_API_KEY> \\
|
|
@@ -44,9 +44,9 @@ AUTHENTICATION
|
|
|
44
44
|
Or set URLs separately (any time):
|
|
45
45
|
$ unbound config urls <gateway-url> <frontend-url> <backend-url>
|
|
46
46
|
|
|
47
|
-
ONBOARDING (one-step install + discover)
|
|
48
|
-
$ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
49
|
-
$ sudo unbound onboard
|
|
47
|
+
ONBOARDING (one-step install + discover; scope auto-detected from sudo)
|
|
48
|
+
$ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY> Current user
|
|
49
|
+
$ sudo unbound onboard --api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY> All users (MDM)
|
|
50
50
|
|
|
51
51
|
TOOL SETUP
|
|
52
52
|
$ unbound setup Select and install multiple tools interactively
|
|
@@ -85,11 +85,11 @@ TOOL SETUP
|
|
|
85
85
|
$ sudo unbound nuke Wipe everything on the device (MDM + user)
|
|
86
86
|
$ unbound nuke Wipe just your tools + credentials (no sudo)
|
|
87
87
|
|
|
88
|
-
MDM SETUP (
|
|
89
|
-
$ sudo unbound setup
|
|
90
|
-
$ sudo unbound setup
|
|
91
|
-
$ sudo unbound setup
|
|
92
|
-
$ sudo unbound setup
|
|
88
|
+
MDM SETUP (all users on the device — run setup with sudo)
|
|
89
|
+
$ sudo unbound setup --all --api-key KEY
|
|
90
|
+
$ sudo unbound setup cursor copilot codex-subscription --api-key KEY
|
|
91
|
+
$ sudo unbound setup claude-code-subscription codex-subscription gemini-cli --api-key KEY
|
|
92
|
+
$ sudo unbound setup --clear cursor codex-subscription (no API key needed to clear)
|
|
93
93
|
|
|
94
94
|
MDM AI TOOLS DISCOVERY
|
|
95
95
|
--domain defaults to the configured backend URL (set via "unbound config set-backend-url")
|
|
@@ -178,8 +178,8 @@ LEARN MORE
|
|
|
178
178
|
// Register all command modules
|
|
179
179
|
require('./commands/login').register(program);
|
|
180
180
|
require('./commands/logout').register(program);
|
|
181
|
-
require('./commands/whoami').register(program);
|
|
182
181
|
require('./commands/status').register(program);
|
|
182
|
+
require('./commands/doctor').register(program);
|
|
183
183
|
require('./commands/policy').register(program);
|
|
184
184
|
require('./commands/users').register(program);
|
|
185
185
|
require('./commands/user-groups').register(program);
|
|
@@ -241,7 +241,7 @@ Use this on a fresh install for tenant deployments. Positional order is fixed:
|
|
|
241
241
|
2. <frontend-url> — Frontend host (e.g. https://gateway.acme.com)
|
|
242
242
|
Used by the browser login flow.
|
|
243
243
|
3. <backend-url> — REST API host (e.g. https://backend.acme.com)
|
|
244
|
-
All "unbound *" commands (
|
|
244
|
+
All "unbound *" commands (status, doctor, policies, ...) hit this.
|
|
245
245
|
|
|
246
246
|
Bare hostnames are accepted; "https://" is added automatically.
|
|
247
247
|
The three values are written atomically to ~/.unbound/config.json.
|