@saptools/bruno 0.2.4 → 0.2.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/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import process2 from "process";
5
- import { checkbox, confirm, input, select } from "@inquirer/prompts";
6
- import { Command } from "commander";
5
+ import { confirm, select } from "@inquirer/prompts";
6
+ import { Command, Option } from "commander";
7
7
 
8
8
  // src/context.ts
9
9
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -61,12 +61,87 @@ async function writeContext(ctx) {
61
61
  return updated;
62
62
  }
63
63
 
64
- // src/run.ts
65
- import { spawn } from "child_process";
66
- import { readFile as readFile4, stat } from "fs/promises";
67
- import { createRequire } from "module";
68
- import { delimiter, dirname as dirname2, isAbsolute, join as join3, relative, resolve, sep } from "path";
69
- import { getTokenCached as getTokenCachedApi } from "@saptools/cf-xsuaa";
64
+ // src/environment-prompt.ts
65
+ import { Separator, checkbox, input } from "@inquirer/prompts";
66
+
67
+ // src/setup-app.ts
68
+ import { mkdir as mkdir2, readdir, writeFile as writeFile3 } from "fs/promises";
69
+ import { join as join2 } from "path";
70
+
71
+ // src/cf-info.ts
72
+ import {
73
+ getRegionView as getRegionViewApi,
74
+ readRegionsView,
75
+ readRegionView,
76
+ readStructureView,
77
+ REGION_KEYS
78
+ } from "@saptools/cf-sync";
79
+ var defaultCfInfoDeps = {
80
+ readStructureView,
81
+ readRegionsView,
82
+ readRegionView,
83
+ getRegionView: getRegionViewApi
84
+ };
85
+ function isValidRegionKey(value) {
86
+ return REGION_KEYS.includes(value);
87
+ }
88
+ async function getStructureSnapshot(deps = defaultCfInfoDeps) {
89
+ const view = await deps.readStructureView();
90
+ if (!view) {
91
+ return {
92
+ source: "empty",
93
+ structure: void 0,
94
+ stale: true,
95
+ message: "No CF structure cached. Run `cf-sync sync` first."
96
+ };
97
+ }
98
+ const stale = view.source === "runtime" && view.metadata?.status === "running";
99
+ return {
100
+ source: view.source,
101
+ structure: view.structure,
102
+ stale,
103
+ message: stale ? "A CF sync is still running \u2014 showing partial data." : void 0
104
+ };
105
+ }
106
+ async function listRegionsWithContent(deps = defaultCfInfoDeps) {
107
+ const snapshot = await getStructureSnapshot(deps);
108
+ if (!snapshot.structure) {
109
+ return [];
110
+ }
111
+ return snapshot.structure.regions.filter((r) => r.accessible && r.orgs.length > 0).map((r) => ({ key: r.key, label: r.label, orgCount: r.orgs.length }));
112
+ }
113
+ async function getRegion(key, deps = defaultCfInfoDeps) {
114
+ const view = await deps.readRegionView(key);
115
+ return view?.region;
116
+ }
117
+ function findOrg(region, orgName) {
118
+ return region.orgs.find((o) => o.name === orgName);
119
+ }
120
+ function findSpace(org, spaceName) {
121
+ return org.spaces.find((s) => s.name === spaceName);
122
+ }
123
+ function findApp(space, appName) {
124
+ return space.apps.find((a) => a.name === appName);
125
+ }
126
+ async function resolveRef(ref, deps = defaultCfInfoDeps) {
127
+ const region = await getRegion(ref.region, deps);
128
+ if (!region) {
129
+ return void 0;
130
+ }
131
+ const org = findOrg(region, ref.org);
132
+ if (!org) {
133
+ return void 0;
134
+ }
135
+ const space = findSpace(org, ref.space);
136
+ if (!space) {
137
+ return void 0;
138
+ }
139
+ const app = findApp(space, ref.app);
140
+ if (!app) {
141
+ return void 0;
142
+ }
143
+ return { region, org, space, app };
144
+ }
70
145
 
71
146
  // src/cf-meta.ts
72
147
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -240,12 +315,231 @@ async function writeCfMetaToFile(path, ref, baseUrl) {
240
315
  return changed;
241
316
  }
242
317
 
318
+ // src/setup-app.ts
319
+ var COMMON_ENVIRONMENTS = ["local", "dev", "staging", "prod"];
320
+ var ENV_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
321
+ function assertValidEnvName(name) {
322
+ if (!ENV_NAME_PATTERN.test(name)) {
323
+ throw new Error(
324
+ `Invalid environment name '${name}': only letters, digits, dot, underscore, and dash are allowed.`
325
+ );
326
+ }
327
+ }
328
+ function emptyEnvContent(envName, ref) {
329
+ const lines = [
330
+ "vars {",
331
+ ` __cf_region: ${ref.region}`,
332
+ ` __cf_org: ${ref.org}`,
333
+ ` __cf_space: ${ref.space}`,
334
+ ` __cf_app: ${ref.app}`,
335
+ ` environment: ${envName}`,
336
+ " baseUrl: ",
337
+ "}",
338
+ ""
339
+ ];
340
+ return lines.join("\n");
341
+ }
342
+ async function ensureEnvFile(appPath, envName, ref) {
343
+ const envDir = join2(appPath, ENVIRONMENTS_DIR);
344
+ await mkdir2(envDir, { recursive: true });
345
+ const filePath = join2(envDir, `${envName}.bru`);
346
+ try {
347
+ await writeFile3(filePath, emptyEnvContent(envName, ref), { encoding: "utf8", flag: "wx" });
348
+ } catch (err) {
349
+ if (err.code !== "EEXIST") {
350
+ throw err;
351
+ }
352
+ await writeCfMetaToFile(filePath, ref);
353
+ }
354
+ return filePath;
355
+ }
356
+ function pickRegion(regions) {
357
+ return regions.map((r) => ({ value: r.key, name: `${r.key} \u2014 ${r.label} (${r.orgCount.toString()} org${r.orgCount === 1 ? "" : "s"})` }));
358
+ }
359
+ function pickOrg(region) {
360
+ return region.orgs.map((o) => ({ value: o.name, name: `${o.name} (${o.spaces.length.toString()} space${o.spaces.length === 1 ? "" : "s"})` }));
361
+ }
362
+ function pickSpace(org) {
363
+ return org.spaces.map((s) => ({ value: s.name, name: `${s.name} (${s.apps.length.toString()} app${s.apps.length === 1 ? "" : "s"})` }));
364
+ }
365
+ function pickApp(space) {
366
+ return space.apps.map((a) => ({ value: a.name, name: a.name }));
367
+ }
368
+ async function setupApp(options) {
369
+ const deps = options.deps ?? defaultCfInfoDeps;
370
+ const log = options.log ?? (() => void 0);
371
+ const regions = await listRegionsWithContent(deps);
372
+ if (regions.length === 0) {
373
+ throw new Error(
374
+ "No CF regions with orgs are cached. Run `cf-sync sync` first, or pass SAP_EMAIL/SAP_PASSWORD to refresh."
375
+ );
376
+ }
377
+ const regionKey = await options.prompts.selectRegion(pickRegion(regions));
378
+ const regionView = await deps.readRegionView(regionKey);
379
+ if (!regionView) {
380
+ throw new Error(`Region ${regionKey} is not cached. Run \`cf-sync sync\` or \`cf-sync region ${regionKey}\`.`);
381
+ }
382
+ const region = regionView.region;
383
+ if (region.orgs.length === 0) {
384
+ throw new Error(`Region ${regionKey} has no accessible orgs.`);
385
+ }
386
+ const orgName = await options.prompts.selectOrg(pickOrg(region));
387
+ const org = region.orgs.find((o) => o.name === orgName);
388
+ if (!org) {
389
+ throw new Error(`Org ${orgName} not found in region ${regionKey}`);
390
+ }
391
+ if (org.spaces.length === 0) {
392
+ throw new Error(`Org ${orgName} has no spaces.`);
393
+ }
394
+ const spaceName = await options.prompts.selectSpace(pickSpace(org));
395
+ const space = org.spaces.find((s) => s.name === spaceName);
396
+ if (!space) {
397
+ throw new Error(`Space ${spaceName} not found in org ${orgName}`);
398
+ }
399
+ if (space.apps.length === 0) {
400
+ throw new Error(`Space ${spaceName} has no apps.`);
401
+ }
402
+ const appName = await options.prompts.selectApp(pickApp(space));
403
+ const ref = { region: regionKey, org: orgName, space: spaceName, app: appName };
404
+ const appPath = join2(
405
+ options.root,
406
+ regionFolderName(regionKey),
407
+ orgFolderName(orgName),
408
+ spaceFolderName(spaceName),
409
+ appName
410
+ );
411
+ const confirmed = await options.prompts.confirmCreate(appPath);
412
+ if (!confirmed) {
413
+ return { ref, appPath, environments: [], created: false };
414
+ }
415
+ await mkdir2(appPath, { recursive: true });
416
+ const existingEnvs = await listExistingEnvs(appPath);
417
+ const common = [...COMMON_ENVIRONMENTS];
418
+ const selected = await options.prompts.selectEnvironments({ common, existing: existingEnvs });
419
+ const merged = [];
420
+ for (const name of selected) {
421
+ const trimmed = name.trim();
422
+ if (trimmed.length === 0 || merged.includes(trimmed)) {
423
+ continue;
424
+ }
425
+ assertValidEnvName(trimmed);
426
+ merged.push(trimmed);
427
+ }
428
+ if (merged.length === 0) {
429
+ throw new Error("At least one environment is required.");
430
+ }
431
+ const created = [];
432
+ for (const envName of merged) {
433
+ const path = await ensureEnvFile(appPath, envName, ref);
434
+ created.push(path);
435
+ log(`\u2022 ${path}`);
436
+ }
437
+ return { ref, appPath, environments: created, created: true };
438
+ }
439
+ async function listExistingEnvs(appPath) {
440
+ try {
441
+ const entries = await readdir(join2(appPath, ENVIRONMENTS_DIR), { withFileTypes: true });
442
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".bru")).map((e) => e.name.replace(/\.bru$/, ""));
443
+ } catch {
444
+ return [];
445
+ }
446
+ }
447
+
448
+ // src/environment-prompt.ts
449
+ var ADD_CUSTOM_ENVIRONMENT = "__saptools_add_custom_environment__";
450
+ function uniqueNames(names) {
451
+ const merged = [];
452
+ for (const name of names) {
453
+ if (!merged.includes(name)) {
454
+ merged.push(name);
455
+ }
456
+ }
457
+ return merged;
458
+ }
459
+ function validateEnvironmentSelection(choices) {
460
+ const selected = choices.map((choice) => choice.value);
461
+ const hasEnvironment = selected.some((value) => value !== ADD_CUSTOM_ENVIRONMENT);
462
+ if (hasEnvironment || selected.includes(ADD_CUSTOM_ENVIRONMENT)) {
463
+ return true;
464
+ }
465
+ return 'Select at least one environment, or choose "Add custom environment".';
466
+ }
467
+ function buildEnvironmentChoices(names, selected) {
468
+ return [
469
+ ...names.map((name) => ({
470
+ value: name,
471
+ name,
472
+ checked: selected.has(name)
473
+ })),
474
+ new Separator(),
475
+ {
476
+ value: ADD_CUSTOM_ENVIRONMENT,
477
+ name: "Add custom environment",
478
+ description: "Create another environment name and return to this menu"
479
+ }
480
+ ];
481
+ }
482
+ function validateCustomEnvironmentName(value) {
483
+ const trimmed = value.trim();
484
+ if (trimmed.length === 0) {
485
+ return true;
486
+ }
487
+ try {
488
+ assertValidEnvName(trimmed);
489
+ return true;
490
+ } catch (err) {
491
+ return err instanceof Error ? err.message : String(err);
492
+ }
493
+ }
494
+ async function promptForEnvironments(opts, deps = {}) {
495
+ const checkboxPrompt = deps.checkboxPrompt ?? checkbox;
496
+ const inputPrompt = deps.inputPrompt ?? input;
497
+ const selected = new Set(opts.existing);
498
+ const customNames = [];
499
+ for (; ; ) {
500
+ const names = uniqueNames([...opts.common, ...opts.existing, ...customNames]);
501
+ const answers = await checkboxPrompt({
502
+ message: "Environments to create (space to toggle, enter to continue)",
503
+ choices: buildEnvironmentChoices(names, selected),
504
+ validate: validateEnvironmentSelection
505
+ });
506
+ selected.clear();
507
+ for (const name of answers) {
508
+ if (name !== ADD_CUSTOM_ENVIRONMENT) {
509
+ selected.add(name);
510
+ }
511
+ }
512
+ if (!answers.includes(ADD_CUSTOM_ENVIRONMENT)) {
513
+ return [...selected];
514
+ }
515
+ const custom = (await inputPrompt({
516
+ message: "Custom environment name (leave empty to go back)",
517
+ default: "",
518
+ validate: validateCustomEnvironmentName
519
+ })).trim();
520
+ if (custom.length === 0) {
521
+ continue;
522
+ }
523
+ if (!customNames.includes(custom) && !names.includes(custom)) {
524
+ customNames.push(custom);
525
+ }
526
+ selected.add(custom);
527
+ }
528
+ }
529
+
530
+ // src/run.ts
531
+ import { spawn } from "child_process";
532
+ import { readFile as readFile4, stat } from "fs/promises";
533
+ import { createRequire } from "module";
534
+ import { delimiter, dirname as dirname2, isAbsolute, join as join4, relative, resolve, sep } from "path";
535
+ import { getTokenCached as getTokenCachedApi } from "@saptools/cf-xsuaa";
536
+
243
537
  // src/folder-scan.ts
244
- import { readdir, readFile as readFile3 } from "fs/promises";
245
- import { join as join2 } from "path";
538
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
539
+ import { join as join3 } from "path";
246
540
  async function safeReaddir(path) {
247
541
  try {
248
- const entries = await readdir(path, { withFileTypes: true });
542
+ const entries = await readdir2(path, { withFileTypes: true });
249
543
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
250
544
  } catch {
251
545
  return [];
@@ -253,7 +547,7 @@ async function safeReaddir(path) {
253
547
  }
254
548
  async function listFiles(path) {
255
549
  try {
256
- const entries = await readdir(path, { withFileTypes: true });
550
+ const entries = await readdir2(path, { withFileTypes: true });
257
551
  return entries.filter((e) => e.isFile()).map((e) => e.name);
258
552
  } catch {
259
553
  return [];
@@ -271,17 +565,17 @@ async function loadEnvFile(path, name) {
271
565
  };
272
566
  }
273
567
  async function scanAppEnvironments(appPath) {
274
- const envDir = join2(appPath, ENVIRONMENTS_DIR);
568
+ const envDir = join3(appPath, ENVIRONMENTS_DIR);
275
569
  const files = await listFiles(envDir);
276
570
  const bruFiles = files.filter((f) => f.endsWith(".bru"));
277
571
  const loaded = [];
278
572
  for (const file of bruFiles) {
279
- loaded.push(await loadEnvFile(join2(envDir, file), file));
573
+ loaded.push(await loadEnvFile(join3(envDir, file), file));
280
574
  }
281
575
  return loaded;
282
576
  }
283
577
  async function scanApp(spacePath, name) {
284
- const appPath = join2(spacePath, name);
578
+ const appPath = join3(spacePath, name);
285
579
  const environments = await scanAppEnvironments(appPath);
286
580
  return { path: appPath, name, environments };
287
581
  }
@@ -290,7 +584,7 @@ async function scanSpace(orgPath, dirName) {
290
584
  if (name === void 0) {
291
585
  return void 0;
292
586
  }
293
- const spacePath = join2(orgPath, dirName);
587
+ const spacePath = join3(orgPath, dirName);
294
588
  const appDirs = await safeReaddir(spacePath);
295
589
  const apps = [];
296
590
  for (const appDir of appDirs) {
@@ -303,7 +597,7 @@ async function scanOrg(regionPath, dirName) {
303
597
  if (name === void 0) {
304
598
  return void 0;
305
599
  }
306
- const orgPath = join2(regionPath, dirName);
600
+ const orgPath = join3(regionPath, dirName);
307
601
  const spaceDirs = await safeReaddir(orgPath);
308
602
  const spaces = [];
309
603
  for (const spaceDir of spaceDirs) {
@@ -319,7 +613,7 @@ async function scanRegion(root, dirName) {
319
613
  if (key === void 0) {
320
614
  return void 0;
321
615
  }
322
- const regionPath = join2(root, dirName);
616
+ const regionPath = join3(root, dirName);
323
617
  const orgDirs = await safeReaddir(regionPath);
324
618
  const orgs = [];
325
619
  for (const orgDir of orgDirs) {
@@ -377,7 +671,7 @@ async function findCommandOnPath(command, env) {
377
671
  const candidates = pathCandidates(command, env);
378
672
  for (const entry of pathEntries(env)) {
379
673
  for (const candidate of candidates) {
380
- const fullPath = join3(entry, candidate);
674
+ const fullPath = join4(entry, candidate);
381
675
  if (await exists(fullPath)) {
382
676
  return fullPath;
383
677
  }
@@ -472,7 +766,7 @@ async function resolveTarget(root, target) {
472
766
  throw new Error(`Target not found: ${target}`);
473
767
  }
474
768
  const { region, org, space, app, filePath } = shorthand;
475
- const appDir = join3(
769
+ const appDir = join4(
476
770
  root,
477
771
  regionFolderName(region),
478
772
  orgFolderName(org),
@@ -482,7 +776,7 @@ async function resolveTarget(root, target) {
482
776
  if (!filePath) {
483
777
  return { filePath: appDir, shorthand };
484
778
  }
485
- const candidate = join3(appDir, filePath);
779
+ const candidate = join4(appDir, filePath);
486
780
  if (await exists(candidate)) {
487
781
  return { filePath: candidate, shorthand };
488
782
  }
@@ -494,7 +788,7 @@ async function resolveTarget(root, target) {
494
788
  }
495
789
  async function chooseEnvironmentFile(appDir, environment) {
496
790
  if (environment) {
497
- const envFile = join3(appDir, ENVIRONMENTS_DIR, `${environment}.bru`);
791
+ const envFile = join4(appDir, ENVIRONMENTS_DIR, `${environment}.bru`);
498
792
  if (!await exists(envFile)) {
499
793
  throw new Error(`Environment file not found: ${envFile}`);
500
794
  }
@@ -526,7 +820,7 @@ function findAppDirFromFile(filePath, root) {
526
820
  if (!regionDir || !orgDir || !spaceDir || !appDir) {
527
821
  throw new Error(`File is not inside a CF-structured bruno collection: ${filePath}`);
528
822
  }
529
- return join3(root, regionDir, orgDir, spaceDir, appDir);
823
+ return join4(root, regionDir, orgDir, spaceDir, appDir);
530
824
  }
531
825
  async function buildRunPlan(options) {
532
826
  const { filePath } = await resolveTarget(options.root, options.target);
@@ -576,216 +870,6 @@ async function runBruno(options) {
576
870
  return { ...plan, ...result };
577
871
  }
578
872
 
579
- // src/setup-app.ts
580
- import { mkdir as mkdir2, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
581
- import { join as join4 } from "path";
582
-
583
- // src/cf-info.ts
584
- import {
585
- getRegionView as getRegionViewApi,
586
- readRegionsView,
587
- readRegionView,
588
- readStructureView,
589
- REGION_KEYS
590
- } from "@saptools/cf-sync";
591
- var defaultCfInfoDeps = {
592
- readStructureView,
593
- readRegionsView,
594
- readRegionView,
595
- getRegionView: getRegionViewApi
596
- };
597
- function isValidRegionKey(value) {
598
- return REGION_KEYS.includes(value);
599
- }
600
- async function getStructureSnapshot(deps = defaultCfInfoDeps) {
601
- const view = await deps.readStructureView();
602
- if (!view) {
603
- return {
604
- source: "empty",
605
- structure: void 0,
606
- stale: true,
607
- message: "No CF structure cached. Run `cf-sync sync` first."
608
- };
609
- }
610
- const stale = view.source === "runtime" && view.metadata?.status === "running";
611
- return {
612
- source: view.source,
613
- structure: view.structure,
614
- stale,
615
- message: stale ? "A CF sync is still running \u2014 showing partial data." : void 0
616
- };
617
- }
618
- async function listRegionsWithContent(deps = defaultCfInfoDeps) {
619
- const snapshot = await getStructureSnapshot(deps);
620
- if (!snapshot.structure) {
621
- return [];
622
- }
623
- return snapshot.structure.regions.filter((r) => r.accessible && r.orgs.length > 0).map((r) => ({ key: r.key, label: r.label, orgCount: r.orgs.length }));
624
- }
625
- async function getRegion(key, deps = defaultCfInfoDeps) {
626
- const view = await deps.readRegionView(key);
627
- return view?.region;
628
- }
629
- function findOrg(region, orgName) {
630
- return region.orgs.find((o) => o.name === orgName);
631
- }
632
- function findSpace(org, spaceName) {
633
- return org.spaces.find((s) => s.name === spaceName);
634
- }
635
- function findApp(space, appName) {
636
- return space.apps.find((a) => a.name === appName);
637
- }
638
- async function resolveRef(ref, deps = defaultCfInfoDeps) {
639
- const region = await getRegion(ref.region, deps);
640
- if (!region) {
641
- return void 0;
642
- }
643
- const org = findOrg(region, ref.org);
644
- if (!org) {
645
- return void 0;
646
- }
647
- const space = findSpace(org, ref.space);
648
- if (!space) {
649
- return void 0;
650
- }
651
- const app = findApp(space, ref.app);
652
- if (!app) {
653
- return void 0;
654
- }
655
- return { region, org, space, app };
656
- }
657
-
658
- // src/setup-app.ts
659
- var COMMON_ENVIRONMENTS = ["local", "dev", "staging", "prod"];
660
- var ENV_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
661
- function assertValidEnvName(name) {
662
- if (!ENV_NAME_PATTERN.test(name)) {
663
- throw new Error(
664
- `Invalid environment name '${name}': only letters, digits, dot, underscore, and dash are allowed.`
665
- );
666
- }
667
- }
668
- function emptyEnvContent(envName, ref) {
669
- const lines = [
670
- "vars {",
671
- ` __cf_region: ${ref.region}`,
672
- ` __cf_org: ${ref.org}`,
673
- ` __cf_space: ${ref.space}`,
674
- ` __cf_app: ${ref.app}`,
675
- ` environment: ${envName}`,
676
- " baseUrl: ",
677
- "}",
678
- ""
679
- ];
680
- return lines.join("\n");
681
- }
682
- async function ensureEnvFile(appPath, envName, ref) {
683
- const envDir = join4(appPath, ENVIRONMENTS_DIR);
684
- await mkdir2(envDir, { recursive: true });
685
- const filePath = join4(envDir, `${envName}.bru`);
686
- try {
687
- await writeFile3(filePath, emptyEnvContent(envName, ref), { encoding: "utf8", flag: "wx" });
688
- } catch (err) {
689
- if (err.code !== "EEXIST") {
690
- throw err;
691
- }
692
- await writeCfMetaToFile(filePath, ref);
693
- }
694
- return filePath;
695
- }
696
- function pickRegion(regions) {
697
- return regions.map((r) => ({ value: r.key, name: `${r.key} \u2014 ${r.label} (${r.orgCount.toString()} org${r.orgCount === 1 ? "" : "s"})` }));
698
- }
699
- function pickOrg(region) {
700
- return region.orgs.map((o) => ({ value: o.name, name: `${o.name} (${o.spaces.length.toString()} space${o.spaces.length === 1 ? "" : "s"})` }));
701
- }
702
- function pickSpace(org) {
703
- return org.spaces.map((s) => ({ value: s.name, name: `${s.name} (${s.apps.length.toString()} app${s.apps.length === 1 ? "" : "s"})` }));
704
- }
705
- function pickApp(space) {
706
- return space.apps.map((a) => ({ value: a.name, name: a.name }));
707
- }
708
- async function setupApp(options) {
709
- const deps = options.deps ?? defaultCfInfoDeps;
710
- const log = options.log ?? (() => void 0);
711
- const regions = await listRegionsWithContent(deps);
712
- if (regions.length === 0) {
713
- throw new Error(
714
- "No CF regions with orgs are cached. Run `cf-sync sync` first, or pass SAP_EMAIL/SAP_PASSWORD to refresh."
715
- );
716
- }
717
- const regionKey = await options.prompts.selectRegion(pickRegion(regions));
718
- const regionView = await deps.readRegionView(regionKey);
719
- if (!regionView) {
720
- throw new Error(`Region ${regionKey} is not cached. Run \`cf-sync sync\` or \`cf-sync region ${regionKey}\`.`);
721
- }
722
- const region = regionView.region;
723
- if (region.orgs.length === 0) {
724
- throw new Error(`Region ${regionKey} has no accessible orgs.`);
725
- }
726
- const orgName = await options.prompts.selectOrg(pickOrg(region));
727
- const org = region.orgs.find((o) => o.name === orgName);
728
- if (!org) {
729
- throw new Error(`Org ${orgName} not found in region ${regionKey}`);
730
- }
731
- if (org.spaces.length === 0) {
732
- throw new Error(`Org ${orgName} has no spaces.`);
733
- }
734
- const spaceName = await options.prompts.selectSpace(pickSpace(org));
735
- const space = org.spaces.find((s) => s.name === spaceName);
736
- if (!space) {
737
- throw new Error(`Space ${spaceName} not found in org ${orgName}`);
738
- }
739
- if (space.apps.length === 0) {
740
- throw new Error(`Space ${spaceName} has no apps.`);
741
- }
742
- const appName = await options.prompts.selectApp(pickApp(space));
743
- const ref = { region: regionKey, org: orgName, space: spaceName, app: appName };
744
- const appPath = join4(
745
- options.root,
746
- regionFolderName(regionKey),
747
- orgFolderName(orgName),
748
- spaceFolderName(spaceName),
749
- appName
750
- );
751
- const confirmed = await options.prompts.confirmCreate(appPath);
752
- if (!confirmed) {
753
- return { ref, appPath, environments: [], created: false };
754
- }
755
- await mkdir2(appPath, { recursive: true });
756
- const existingEnvs = await listExistingEnvs(appPath);
757
- const common = [...COMMON_ENVIRONMENTS];
758
- const selected = await options.prompts.selectEnvironments({ common, existing: existingEnvs });
759
- const custom = await options.prompts.inputCustomEnvName();
760
- const merged = [];
761
- for (const name of [...selected, ...custom ? [custom] : []]) {
762
- const trimmed = name.trim();
763
- if (trimmed.length === 0 || merged.includes(trimmed)) {
764
- continue;
765
- }
766
- assertValidEnvName(trimmed);
767
- merged.push(trimmed);
768
- }
769
- if (merged.length === 0) {
770
- throw new Error("At least one environment is required.");
771
- }
772
- const created = [];
773
- for (const envName of merged) {
774
- const path = await ensureEnvFile(appPath, envName, ref);
775
- created.push(path);
776
- log(`\u2022 ${path}`);
777
- }
778
- return { ref, appPath, environments: created, created: true };
779
- }
780
- async function listExistingEnvs(appPath) {
781
- try {
782
- const entries = await readdir2(join4(appPath, ENVIRONMENTS_DIR), { withFileTypes: true });
783
- return entries.filter((e) => e.isFile() && e.name.endsWith(".bru")).map((e) => e.name.replace(/\.bru$/, ""));
784
- } catch {
785
- return [];
786
- }
787
- }
788
-
789
873
  // src/use.ts
790
874
  function parseContextShorthand(shorthand) {
791
875
  const segs = shorthand.split("/").filter((s) => s.length > 0);
@@ -820,9 +904,15 @@ async function useContext(options) {
820
904
  }
821
905
 
822
906
  // src/cli.ts
823
- function resolveRoot(explicit) {
824
- if (explicit) {
825
- return explicit;
907
+ function resolveCollectionDir(explicitCollection, explicitRoot) {
908
+ if (explicitCollection) {
909
+ return explicitCollection;
910
+ }
911
+ if (explicitRoot) {
912
+ return explicitRoot;
913
+ }
914
+ if (process2.env["SAPTOOLS_BRUNO_COLLECTION"]) {
915
+ return process2.env["SAPTOOLS_BRUNO_COLLECTION"];
826
916
  }
827
917
  if (process2.env["SAPTOOLS_BRUNO_ROOT"]) {
828
918
  return process2.env["SAPTOOLS_BRUNO_ROOT"];
@@ -831,50 +921,21 @@ function resolveRoot(explicit) {
831
921
  }
832
922
  async function main(argv) {
833
923
  const program = new Command();
834
- program.name("saptools-bruno").description("Smart runner for Bruno with CF-aware env metadata and automatic token injection").option("--root <dir>", "Root directory of the bruno collection (default: cwd)");
924
+ program.name("saptools-bruno").description("Smart runner for Bruno with CF-aware env metadata and automatic token injection").addOption(new Option("--collection <dir>", "Bruno collection directory (default: SAPTOOLS_BRUNO_COLLECTION or cwd)")).addOption(new Option("--root <dir>", "Legacy alias for --collection").hideHelp());
835
925
  program.command("setup-app").description("Interactively scaffold a bruno app folder and seed __cf_* variables").action(async () => {
836
- const root = resolveRoot(program.opts().root);
926
+ const collectionDir = resolveCollectionDir(
927
+ program.opts().collection,
928
+ program.opts().root
929
+ );
837
930
  const result = await setupApp({
838
- root,
931
+ root: collectionDir,
839
932
  prompts: {
840
933
  selectRegion: async (choices) => await select({ message: "Select region", choices: [...choices] }),
841
934
  selectOrg: async (choices) => await select({ message: "Select org", choices: [...choices] }),
842
935
  selectSpace: async (choices) => await select({ message: "Select space", choices: [...choices] }),
843
936
  selectApp: async (choices) => await select({ message: "Select app", choices: [...choices] }),
844
937
  confirmCreate: async (path) => await confirm({ message: `Create ${path}?`, default: true }),
845
- selectEnvironments: async ({ common, existing }) => {
846
- const seen = /* @__PURE__ */ new Set();
847
- const all = [...common, ...existing].filter((name) => {
848
- if (seen.has(name)) {
849
- return false;
850
- }
851
- seen.add(name);
852
- return true;
853
- });
854
- return await checkbox({
855
- message: "Environments to create (space to toggle, enter to confirm)",
856
- choices: all.map((name) => ({
857
- name,
858
- value: name,
859
- checked: existing.includes(name)
860
- }))
861
- });
862
- },
863
- inputCustomEnvName: async () => {
864
- const raw = await input({
865
- message: "Custom environment name (leave empty to skip)",
866
- default: "",
867
- validate: (v) => {
868
- const t = v.trim();
869
- if (t.length === 0) {
870
- return true;
871
- }
872
- return /^[A-Za-z0-9._-]+$/.test(t) ? true : "Only letters, digits, dot, underscore, and dash are allowed.";
873
- }
874
- });
875
- const trimmed = raw.trim();
876
- return trimmed.length > 0 ? trimmed : null;
877
- }
938
+ selectEnvironments: async (opts) => await promptForEnvironments(opts)
878
939
  },
879
940
  log: (msg) => {
880
941
  process2.stdout.write(`${msg}
@@ -890,7 +951,10 @@ async function main(argv) {
890
951
  });
891
952
  program.command("run").description("Run a bruno request or folder, auto-injecting an XSUAA token").argument("[target]", "Shorthand path (region/org/space/app[/folder/file.bru]) or real path").option("-e, --env <name>", "Environment name (default: context or first)").action(
892
953
  async (target, opts) => {
893
- const root = resolveRoot(program.opts().root);
954
+ const collectionDir = resolveCollectionDir(
955
+ program.opts().collection,
956
+ program.opts().root
957
+ );
894
958
  let effectiveTarget = target;
895
959
  if (!effectiveTarget) {
896
960
  const ctx = await readContext();
@@ -902,7 +966,7 @@ async function main(argv) {
902
966
  effectiveTarget = `${ctx.region}/${ctx.org}/${ctx.space}/${ctx.app}`;
903
967
  }
904
968
  const result = await runBruno({
905
- root,
969
+ root: collectionDir,
906
970
  target: effectiveTarget,
907
971
  ...opts.env ? { environment: opts.env } : {},
908
972
  log: (msg) => {