unbound-cli 0.9.3 → 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/package.json +1 -1
- package/src/commands/discover.js +29 -6
- package/src/commands/setup.js +103 -61
- package/test/discover-exit-code.test.js +21 -0
- package/test/setup-args.test.js +85 -0
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
|
@@ -37,15 +37,15 @@ const MDM_TOOLS = {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
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'];
|
|
40
|
+
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
41
41
|
|
|
42
|
-
// Tools for `unbound setup mdm --all` —
|
|
42
|
+
// Tools for `unbound setup mdm --all` — identical to MDM_ALL_TOOLS today; split kept for future flexibility.
|
|
43
43
|
const MDM_SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
44
44
|
|
|
45
|
-
// Default tools for `unbound onboard` (Cursor, Claude Code hooks, Codex hooks; no Gemini CLI).
|
|
46
|
-
const ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription'];
|
|
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
47
|
|
|
48
|
-
// Tools for `unbound setup --all` —
|
|
48
|
+
// Tools for `unbound setup --all` — identical to ALL_TOOLS today; split kept for future flexibility.
|
|
49
49
|
const SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot'];
|
|
50
50
|
|
|
51
51
|
// Tool name → script mapping for automated tools
|
|
@@ -221,13 +221,15 @@ async function runPythonScriptWindows(scriptPath, args, { capture }) {
|
|
|
221
221
|
* have no browser-auth flow.
|
|
222
222
|
*/
|
|
223
223
|
function buildScriptArgs(apiKey, { backendUrl, frontendUrl, gatewayUrl, clear, mdm, backfill } = {}) {
|
|
224
|
-
|
|
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)}` : '';
|
|
225
227
|
if (backendUrl) args += ` --backend-url ${shellEscape(backendUrl)}`;
|
|
226
228
|
if (gatewayUrl) args += ` --gateway-url ${shellEscape(gatewayUrl)}`;
|
|
227
229
|
if (!mdm && frontendUrl) args += ` --domain ${shellEscape(frontendUrl)}`;
|
|
228
230
|
if (clear) args += ' --clear';
|
|
229
231
|
if (backfill) args += ' --backfill';
|
|
230
|
-
return args;
|
|
232
|
+
return args.trim();
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
// Backfill only applies to the hooks variants of Claude Code / Codex; gateway
|
|
@@ -346,7 +348,7 @@ function register(program) {
|
|
|
346
348
|
'Run with no arguments for interactive setup, or specify tools directly.'
|
|
347
349
|
)
|
|
348
350
|
.option('--api-key <key>', 'Authenticate with an API key (skips browser login)')
|
|
349
|
-
.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)')
|
|
350
352
|
.option('--subscription', 'Use subscription mode for Claude Code / Codex (hooks only)')
|
|
351
353
|
.option('--gateway', 'Use gateway mode for Claude Code / Codex (Unbound as AI provider)')
|
|
352
354
|
.option('--all', 'Set up the default bundle: Cursor, Copilot, Claude Code (hooks), Codex (hooks)')
|
|
@@ -393,17 +395,19 @@ Examples:
|
|
|
393
395
|
$ unbound setup cursor claude-code-gateway --api-key <key>
|
|
394
396
|
Login + set up multiple tools
|
|
395
397
|
|
|
396
|
-
Remove configuration:
|
|
398
|
+
Remove configuration (no login or API key required):
|
|
397
399
|
$ unbound setup cursor --clear Remove Cursor config
|
|
398
400
|
$ unbound setup copilot --clear Remove GitHub Copilot config
|
|
399
|
-
$ unbound setup claude-code --clear Remove Claude Code
|
|
401
|
+
$ unbound setup claude-code --clear Remove BOTH Claude Code modes (subscription + gateway)
|
|
402
|
+
$ unbound setup codex --clear Remove BOTH Codex modes (subscription + gateway)
|
|
400
403
|
|
|
401
404
|
Interactive:
|
|
402
405
|
$ unbound setup Select tools interactively
|
|
403
406
|
$ unbound setup --api-key <key> Login, then select interactively
|
|
404
407
|
|
|
405
|
-
|
|
406
|
-
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.
|
|
407
411
|
`)
|
|
408
412
|
.action(async (tools, opts) => {
|
|
409
413
|
try {
|
|
@@ -421,11 +425,15 @@ automatically to authenticate before proceeding.
|
|
|
421
425
|
const frontendUrl = written.frontend_url || config.getFrontendUrl();
|
|
422
426
|
const gatewayUrl = written.gateway_url || config.getGatewayUrl();
|
|
423
427
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
}
|
|
429
437
|
const apiKey = config.getApiKey();
|
|
430
438
|
const urlOpts = { backendUrl, frontendUrl, gatewayUrl };
|
|
431
439
|
|
|
@@ -493,30 +501,33 @@ automatically to authenticate before proceeding.
|
|
|
493
501
|
return;
|
|
494
502
|
}
|
|
495
503
|
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
+
}
|
|
507
517
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
+
}
|
|
520
531
|
}
|
|
521
532
|
|
|
522
533
|
// Validate --subscription/--gateway only with tools that need them
|
|
@@ -623,7 +634,9 @@ automatically to authenticate before proceeding.
|
|
|
623
634
|
|
|
624
635
|
// --- MDM setup ---
|
|
625
636
|
|
|
626
|
-
|
|
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(', ');
|
|
627
640
|
|
|
628
641
|
setup
|
|
629
642
|
.command('mdm')
|
|
@@ -632,31 +645,38 @@ automatically to authenticate before proceeding.
|
|
|
632
645
|
'Used by organization admins to enroll devices via MDM.'
|
|
633
646
|
)
|
|
634
647
|
.argument('[tools...]', 'Tools to set up: ' + mdmToolNames)
|
|
635
|
-
.
|
|
636
|
-
.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)')
|
|
637
650
|
.option('--all', 'Set up all available tools')
|
|
638
|
-
.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)')
|
|
639
652
|
.addHelpText('after', `
|
|
640
653
|
Available tools:
|
|
641
654
|
cursor Cursor IDE
|
|
642
655
|
copilot GitHub Copilot
|
|
643
656
|
claude-code-subscription Claude Code with your own subscription (hooks only)
|
|
644
657
|
claude-code-gateway Claude Code with Unbound as AI provider
|
|
658
|
+
claude-code Both Claude Code modes (clears both; sets up subscription)
|
|
645
659
|
gemini-cli Gemini CLI
|
|
646
660
|
codex-subscription Codex with your own subscription (hooks only)
|
|
647
661
|
codex-gateway Codex with Unbound as AI provider
|
|
662
|
+
codex Both Codex modes (clears both; sets up subscription)
|
|
648
663
|
|
|
649
|
-
Note: claude-code-subscription and claude-code-gateway are mutually exclusive
|
|
650
|
-
codex
|
|
651
|
-
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.
|
|
652
667
|
|
|
653
|
-
|
|
668
|
+
Setup examples (require --admin-api-key):
|
|
654
669
|
$ sudo unbound setup mdm --admin-api-key KEY cursor
|
|
655
670
|
$ sudo unbound setup mdm --admin-api-key KEY cursor claude-code-subscription codex-subscription
|
|
656
671
|
$ sudo unbound setup mdm --admin-api-key KEY --all
|
|
657
|
-
$ sudo unbound setup mdm --admin-api-key KEY --clear cursor codex-subscription
|
|
658
672
|
$ sudo unbound setup mdm --admin-api-key KEY claude-code-subscription --backfill
|
|
659
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
|
|
660
680
|
`)
|
|
661
681
|
.action(async (tools, opts, command) => {
|
|
662
682
|
try {
|
|
@@ -665,6 +685,13 @@ Examples:
|
|
|
665
685
|
// --backend-url, --frontend-url, --gateway-url are defined only on the parent `setup` command.
|
|
666
686
|
// Use optsWithGlobals() so they all work regardless of position relative to `mdm`.
|
|
667
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
|
+
}
|
|
668
695
|
// Persist URLs first so this MDM run wires tools at the new tenant
|
|
669
696
|
// and any subsequent non-MDM command on the same machine inherits.
|
|
670
697
|
// Prefer just-persisted values over env-var-aware getters so a stale
|
|
@@ -684,7 +711,9 @@ Examples:
|
|
|
684
711
|
|
|
685
712
|
let toolNames;
|
|
686
713
|
if (globalOpts.all) {
|
|
687
|
-
|
|
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;
|
|
688
717
|
} else if (tools.length > 0) {
|
|
689
718
|
toolNames = tools;
|
|
690
719
|
} else {
|
|
@@ -694,6 +723,14 @@ Examples:
|
|
|
694
723
|
return;
|
|
695
724
|
}
|
|
696
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
|
+
|
|
697
734
|
const invalid = toolNames.filter(t => !MDM_TOOLS[t]);
|
|
698
735
|
if (invalid.length > 0) {
|
|
699
736
|
output.error(`Unknown tool(s): ${invalid.join(', ')}`);
|
|
@@ -702,16 +739,20 @@ Examples:
|
|
|
702
739
|
return;
|
|
703
740
|
}
|
|
704
741
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
+
}
|
|
710
750
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
+
}
|
|
715
756
|
}
|
|
716
757
|
|
|
717
758
|
const resolvedTools = toolNames.map(name => ({ name, ...MDM_TOOLS[name] }));
|
|
@@ -749,7 +790,7 @@ Examples:
|
|
|
749
790
|
}
|
|
750
791
|
|
|
751
792
|
/**
|
|
752
|
-
* 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.
|
|
753
794
|
* Assumes the caller has already ensured the user is logged in.
|
|
754
795
|
* Returns true on success, false on failure.
|
|
755
796
|
*/
|
|
@@ -774,7 +815,7 @@ async function runSetupAllBundle(apiKey, { backendUrl, frontendUrl, gatewayUrl,
|
|
|
774
815
|
}
|
|
775
816
|
|
|
776
817
|
/**
|
|
777
|
-
* 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.
|
|
778
819
|
* Caller must ensure the process is running as root.
|
|
779
820
|
* Returns true on success, false on failure.
|
|
780
821
|
*/
|
|
@@ -801,4 +842,5 @@ module.exports = {
|
|
|
801
842
|
checkRoot,
|
|
802
843
|
ALL_TOOLS,
|
|
803
844
|
MDM_ALL_TOOLS,
|
|
845
|
+
buildScriptArgs,
|
|
804
846
|
};
|
|
@@ -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
|
+
});
|