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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbound-cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.6",
4
4
  "description": "CLI tool for Unbound - AI Gateway management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- if (code === 0) {
75
- resolve();
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
- if (code === 0) resolve();
133
- else reject(new Error(`Discovery script failed with exit code ${code}`));
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 };
@@ -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` — same as the onboard-mdm bundle plus Copilot.
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` — same as the onboard bundle plus Copilot.
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
- let args = `--api-key ${shellEscape(apiKey)}`;
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 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)
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
- If not logged in and --api-key is not provided, the browser will open
406
- automatically to authenticate before proceeding.
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
- await ensureLoggedIn({
425
- apiKey: opts.apiKey,
426
- baseUrl: written.base_url,
427
- frontendUrl: written.frontend_url,
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
- // Validate mutual exclusivity
497
- if (tools.includes('claude-code-subscription') && tools.includes('claude-code-gateway')) {
498
- output.error('Cannot set up both claude-code-subscription and claude-code-gateway. Choose one.');
499
- process.exitCode = 1;
500
- return;
501
- }
502
- if (tools.includes('codex-subscription') && tools.includes('codex-gateway')) {
503
- output.error('Cannot set up both codex-subscription and codex-gateway. Choose one.');
504
- process.exitCode = 1;
505
- return;
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
- // Validate no bare + suffixed name conflicts
509
- if (tools.includes('claude-code') && (tools.includes('claude-code-subscription') || tools.includes('claude-code-gateway'))) {
510
- output.error('Cannot combine claude-code with claude-code-subscription or claude-code-gateway.');
511
- console.error(' Use --subscription or --gateway with claude-code, or use the explicit name directly.');
512
- process.exitCode = 1;
513
- return;
514
- }
515
- if (tools.includes('codex') && (tools.includes('codex-subscription') || tools.includes('codex-gateway'))) {
516
- output.error('Cannot combine codex with codex-subscription or codex-gateway.');
517
- console.error(' Use --subscription or --gateway with codex, or use the explicit name directly.');
518
- process.exitCode = 1;
519
- return;
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
- const mdmToolNames = Object.keys(MDM_TOOLS).join(', ');
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
- .requiredOption('--admin-api-key <key>', 'Admin API key for MDM enrollment')
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-subscription and codex-gateway are mutually exclusive.
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
- Examples:
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
- toolNames = MDM_SETUP_ALL_TOOLS;
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
- if (toolNames.includes('claude-code-subscription') && toolNames.includes('claude-code-gateway')) {
706
- output.error('Cannot use both claude-code-subscription and claude-code-gateway. Choose one.');
707
- process.exitCode = 1;
708
- return;
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
- if (toolNames.includes('codex-subscription') && toolNames.includes('codex-gateway')) {
712
- output.error('Cannot use both codex-subscription and codex-gateway. Choose one.');
713
- process.exitCode = 1;
714
- return;
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
+ });