@saptools/bruno 0.2.5 → 0.2.7

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,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import process2 from "process";
5
- import { checkbox, confirm, input, select } from "@inquirer/prompts";
5
+ import { confirm, select } from "@inquirer/prompts";
6
6
  import { Command, Option } from "commander";
7
7
 
8
8
  // src/context.ts
@@ -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 { basename, 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,261 @@ 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 BRUNO_COLLECTION_CONFIG_FILENAME = "bruno.json";
321
+ var ENV_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
322
+ function assertValidEnvName(name) {
323
+ if (!ENV_NAME_PATTERN.test(name)) {
324
+ throw new Error(
325
+ `Invalid environment name '${name}': only letters, digits, dot, underscore, and dash are allowed.`
326
+ );
327
+ }
328
+ }
329
+ function emptyEnvContent(envName, ref) {
330
+ const lines = [
331
+ "vars {",
332
+ ` __cf_region: ${ref.region}`,
333
+ ` __cf_org: ${ref.org}`,
334
+ ` __cf_space: ${ref.space}`,
335
+ ` __cf_app: ${ref.app}`,
336
+ ` environment: ${envName}`,
337
+ " baseUrl: ",
338
+ "}",
339
+ ""
340
+ ];
341
+ return lines.join("\n");
342
+ }
343
+ function normalizeCollectionName(root) {
344
+ const candidate = basename(root).replace(/^\.+/, "").trim();
345
+ return candidate.length > 0 ? candidate : "bruno-collection";
346
+ }
347
+ function defaultBrunoConfig(root) {
348
+ return `${JSON.stringify(
349
+ {
350
+ version: "1",
351
+ name: normalizeCollectionName(root),
352
+ type: "collection",
353
+ ignore: ["node_modules", ".git"]
354
+ },
355
+ null,
356
+ 2
357
+ )}
358
+ `;
359
+ }
360
+ async function ensureCollectionConfig(root) {
361
+ const filePath = join2(root, BRUNO_COLLECTION_CONFIG_FILENAME);
362
+ try {
363
+ await writeFile3(filePath, defaultBrunoConfig(root), { encoding: "utf8", flag: "wx" });
364
+ } catch (err) {
365
+ if (err.code !== "EEXIST") {
366
+ throw err;
367
+ }
368
+ }
369
+ }
370
+ async function ensureEnvFile(appPath, envName, ref) {
371
+ const envDir = join2(appPath, ENVIRONMENTS_DIR);
372
+ await mkdir2(envDir, { recursive: true });
373
+ const filePath = join2(envDir, `${envName}.bru`);
374
+ try {
375
+ await writeFile3(filePath, emptyEnvContent(envName, ref), { encoding: "utf8", flag: "wx" });
376
+ } catch (err) {
377
+ if (err.code !== "EEXIST") {
378
+ throw err;
379
+ }
380
+ await writeCfMetaToFile(filePath, ref);
381
+ }
382
+ return filePath;
383
+ }
384
+ function pickRegion(regions) {
385
+ return regions.map((r) => ({ value: r.key, name: `${r.key} \u2014 ${r.label} (${r.orgCount.toString()} org${r.orgCount === 1 ? "" : "s"})` }));
386
+ }
387
+ function pickOrg(region) {
388
+ return region.orgs.map((o) => ({ value: o.name, name: `${o.name} (${o.spaces.length.toString()} space${o.spaces.length === 1 ? "" : "s"})` }));
389
+ }
390
+ function pickSpace(org) {
391
+ return org.spaces.map((s) => ({ value: s.name, name: `${s.name} (${s.apps.length.toString()} app${s.apps.length === 1 ? "" : "s"})` }));
392
+ }
393
+ function pickApp(space) {
394
+ return space.apps.map((a) => ({ value: a.name, name: a.name }));
395
+ }
396
+ async function setupApp(options) {
397
+ const deps = options.deps ?? defaultCfInfoDeps;
398
+ const log = options.log ?? (() => void 0);
399
+ const regions = await listRegionsWithContent(deps);
400
+ if (regions.length === 0) {
401
+ throw new Error(
402
+ "No CF regions with orgs are cached. Run `cf-sync sync` first, or pass SAP_EMAIL/SAP_PASSWORD to refresh."
403
+ );
404
+ }
405
+ const regionKey = await options.prompts.selectRegion(pickRegion(regions));
406
+ const regionView = await deps.readRegionView(regionKey);
407
+ if (!regionView) {
408
+ throw new Error(`Region ${regionKey} is not cached. Run \`cf-sync sync\` or \`cf-sync region ${regionKey}\`.`);
409
+ }
410
+ const region = regionView.region;
411
+ if (region.orgs.length === 0) {
412
+ throw new Error(`Region ${regionKey} has no accessible orgs.`);
413
+ }
414
+ const orgName = await options.prompts.selectOrg(pickOrg(region));
415
+ const org = region.orgs.find((o) => o.name === orgName);
416
+ if (!org) {
417
+ throw new Error(`Org ${orgName} not found in region ${regionKey}`);
418
+ }
419
+ if (org.spaces.length === 0) {
420
+ throw new Error(`Org ${orgName} has no spaces.`);
421
+ }
422
+ const spaceName = await options.prompts.selectSpace(pickSpace(org));
423
+ const space = org.spaces.find((s) => s.name === spaceName);
424
+ if (!space) {
425
+ throw new Error(`Space ${spaceName} not found in org ${orgName}`);
426
+ }
427
+ if (space.apps.length === 0) {
428
+ throw new Error(`Space ${spaceName} has no apps.`);
429
+ }
430
+ const appName = await options.prompts.selectApp(pickApp(space));
431
+ const ref = { region: regionKey, org: orgName, space: spaceName, app: appName };
432
+ const appPath = join2(
433
+ options.root,
434
+ regionFolderName(regionKey),
435
+ orgFolderName(orgName),
436
+ spaceFolderName(spaceName),
437
+ appName
438
+ );
439
+ const confirmed = await options.prompts.confirmCreate(appPath);
440
+ if (!confirmed) {
441
+ return { ref, appPath, environments: [], created: false };
442
+ }
443
+ await mkdir2(options.root, { recursive: true });
444
+ await ensureCollectionConfig(options.root);
445
+ await mkdir2(appPath, { recursive: true });
446
+ const existingEnvs = await listExistingEnvs(appPath);
447
+ const common = [...COMMON_ENVIRONMENTS];
448
+ const selected = await options.prompts.selectEnvironments({ common, existing: existingEnvs });
449
+ const merged = [];
450
+ for (const name of selected) {
451
+ const trimmed = name.trim();
452
+ if (trimmed.length === 0 || merged.includes(trimmed)) {
453
+ continue;
454
+ }
455
+ assertValidEnvName(trimmed);
456
+ merged.push(trimmed);
457
+ }
458
+ if (merged.length === 0) {
459
+ throw new Error("At least one environment is required.");
460
+ }
461
+ const created = [];
462
+ for (const envName of merged) {
463
+ const path = await ensureEnvFile(appPath, envName, ref);
464
+ created.push(path);
465
+ log(`\u2022 ${path}`);
466
+ }
467
+ return { ref, appPath, environments: created, created: true };
468
+ }
469
+ async function listExistingEnvs(appPath) {
470
+ try {
471
+ const entries = await readdir(join2(appPath, ENVIRONMENTS_DIR), { withFileTypes: true });
472
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".bru")).map((e) => e.name.replace(/\.bru$/, ""));
473
+ } catch {
474
+ return [];
475
+ }
476
+ }
477
+
478
+ // src/environment-prompt.ts
479
+ var ADD_CUSTOM_ENVIRONMENT = "__saptools_add_custom_environment__";
480
+ function uniqueNames(names) {
481
+ const merged = [];
482
+ for (const name of names) {
483
+ if (!merged.includes(name)) {
484
+ merged.push(name);
485
+ }
486
+ }
487
+ return merged;
488
+ }
489
+ function validateEnvironmentSelection(choices) {
490
+ const selected = choices.map((choice) => choice.value);
491
+ const hasEnvironment = selected.some((value) => value !== ADD_CUSTOM_ENVIRONMENT);
492
+ if (hasEnvironment || selected.includes(ADD_CUSTOM_ENVIRONMENT)) {
493
+ return true;
494
+ }
495
+ return 'Select at least one environment, or choose "Add custom environment".';
496
+ }
497
+ function buildEnvironmentChoices(names, selected) {
498
+ return [
499
+ ...names.map((name) => ({
500
+ value: name,
501
+ name,
502
+ checked: selected.has(name)
503
+ })),
504
+ new Separator(),
505
+ {
506
+ value: ADD_CUSTOM_ENVIRONMENT,
507
+ name: "Add custom environment",
508
+ description: "Create another environment name and return to this menu"
509
+ }
510
+ ];
511
+ }
512
+ function validateCustomEnvironmentName(value) {
513
+ const trimmed = value.trim();
514
+ if (trimmed.length === 0) {
515
+ return true;
516
+ }
517
+ try {
518
+ assertValidEnvName(trimmed);
519
+ return true;
520
+ } catch (err) {
521
+ return err instanceof Error ? err.message : String(err);
522
+ }
523
+ }
524
+ async function promptForEnvironments(opts, deps = {}) {
525
+ const checkboxPrompt = deps.checkboxPrompt ?? checkbox;
526
+ const inputPrompt = deps.inputPrompt ?? input;
527
+ const selected = new Set(opts.existing);
528
+ const customNames = [];
529
+ for (; ; ) {
530
+ const names = uniqueNames([...opts.common, ...opts.existing, ...customNames]);
531
+ const answers = await checkboxPrompt({
532
+ message: "Environments to create (space to toggle, enter to continue)",
533
+ choices: buildEnvironmentChoices(names, selected),
534
+ validate: validateEnvironmentSelection
535
+ });
536
+ selected.clear();
537
+ for (const name of answers) {
538
+ if (name !== ADD_CUSTOM_ENVIRONMENT) {
539
+ selected.add(name);
540
+ }
541
+ }
542
+ if (!answers.includes(ADD_CUSTOM_ENVIRONMENT)) {
543
+ return [...selected];
544
+ }
545
+ const custom = (await inputPrompt({
546
+ message: "Custom environment name (leave empty to go back)",
547
+ default: "",
548
+ validate: validateCustomEnvironmentName
549
+ })).trim();
550
+ if (custom.length === 0) {
551
+ continue;
552
+ }
553
+ if (!customNames.includes(custom) && !names.includes(custom)) {
554
+ customNames.push(custom);
555
+ }
556
+ selected.add(custom);
557
+ }
558
+ }
559
+
560
+ // src/run.ts
561
+ import { spawn } from "child_process";
562
+ import { readFile as readFile4, stat } from "fs/promises";
563
+ import { createRequire } from "module";
564
+ import { delimiter, dirname as dirname2, isAbsolute, join as join4, relative, resolve, sep } from "path";
565
+ import { getTokenCached as getTokenCachedApi } from "@saptools/cf-xsuaa";
566
+
243
567
  // src/folder-scan.ts
244
- import { readdir, readFile as readFile3 } from "fs/promises";
245
- import { join as join2 } from "path";
568
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
569
+ import { join as join3 } from "path";
246
570
  async function safeReaddir(path) {
247
571
  try {
248
- const entries = await readdir(path, { withFileTypes: true });
572
+ const entries = await readdir2(path, { withFileTypes: true });
249
573
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
250
574
  } catch {
251
575
  return [];
@@ -253,7 +577,7 @@ async function safeReaddir(path) {
253
577
  }
254
578
  async function listFiles(path) {
255
579
  try {
256
- const entries = await readdir(path, { withFileTypes: true });
580
+ const entries = await readdir2(path, { withFileTypes: true });
257
581
  return entries.filter((e) => e.isFile()).map((e) => e.name);
258
582
  } catch {
259
583
  return [];
@@ -271,17 +595,17 @@ async function loadEnvFile(path, name) {
271
595
  };
272
596
  }
273
597
  async function scanAppEnvironments(appPath) {
274
- const envDir = join2(appPath, ENVIRONMENTS_DIR);
598
+ const envDir = join3(appPath, ENVIRONMENTS_DIR);
275
599
  const files = await listFiles(envDir);
276
600
  const bruFiles = files.filter((f) => f.endsWith(".bru"));
277
601
  const loaded = [];
278
602
  for (const file of bruFiles) {
279
- loaded.push(await loadEnvFile(join2(envDir, file), file));
603
+ loaded.push(await loadEnvFile(join3(envDir, file), file));
280
604
  }
281
605
  return loaded;
282
606
  }
283
607
  async function scanApp(spacePath, name) {
284
- const appPath = join2(spacePath, name);
608
+ const appPath = join3(spacePath, name);
285
609
  const environments = await scanAppEnvironments(appPath);
286
610
  return { path: appPath, name, environments };
287
611
  }
@@ -290,7 +614,7 @@ async function scanSpace(orgPath, dirName) {
290
614
  if (name === void 0) {
291
615
  return void 0;
292
616
  }
293
- const spacePath = join2(orgPath, dirName);
617
+ const spacePath = join3(orgPath, dirName);
294
618
  const appDirs = await safeReaddir(spacePath);
295
619
  const apps = [];
296
620
  for (const appDir of appDirs) {
@@ -303,7 +627,7 @@ async function scanOrg(regionPath, dirName) {
303
627
  if (name === void 0) {
304
628
  return void 0;
305
629
  }
306
- const orgPath = join2(regionPath, dirName);
630
+ const orgPath = join3(regionPath, dirName);
307
631
  const spaceDirs = await safeReaddir(orgPath);
308
632
  const spaces = [];
309
633
  for (const spaceDir of spaceDirs) {
@@ -319,7 +643,7 @@ async function scanRegion(root, dirName) {
319
643
  if (key === void 0) {
320
644
  return void 0;
321
645
  }
322
- const regionPath = join2(root, dirName);
646
+ const regionPath = join3(root, dirName);
323
647
  const orgDirs = await safeReaddir(regionPath);
324
648
  const orgs = [];
325
649
  for (const orgDir of orgDirs) {
@@ -377,7 +701,7 @@ async function findCommandOnPath(command, env) {
377
701
  const candidates = pathCandidates(command, env);
378
702
  for (const entry of pathEntries(env)) {
379
703
  for (const candidate of candidates) {
380
- const fullPath = join3(entry, candidate);
704
+ const fullPath = join4(entry, candidate);
381
705
  if (await exists(fullPath)) {
382
706
  return fullPath;
383
707
  }
@@ -472,7 +796,7 @@ async function resolveTarget(root, target) {
472
796
  throw new Error(`Target not found: ${target}`);
473
797
  }
474
798
  const { region, org, space, app, filePath } = shorthand;
475
- const appDir = join3(
799
+ const appDir = join4(
476
800
  root,
477
801
  regionFolderName(region),
478
802
  orgFolderName(org),
@@ -482,7 +806,7 @@ async function resolveTarget(root, target) {
482
806
  if (!filePath) {
483
807
  return { filePath: appDir, shorthand };
484
808
  }
485
- const candidate = join3(appDir, filePath);
809
+ const candidate = join4(appDir, filePath);
486
810
  if (await exists(candidate)) {
487
811
  return { filePath: candidate, shorthand };
488
812
  }
@@ -494,7 +818,7 @@ async function resolveTarget(root, target) {
494
818
  }
495
819
  async function chooseEnvironmentFile(appDir, environment) {
496
820
  if (environment) {
497
- const envFile = join3(appDir, ENVIRONMENTS_DIR, `${environment}.bru`);
821
+ const envFile = join4(appDir, ENVIRONMENTS_DIR, `${environment}.bru`);
498
822
  if (!await exists(envFile)) {
499
823
  throw new Error(`Environment file not found: ${envFile}`);
500
824
  }
@@ -526,7 +850,7 @@ function findAppDirFromFile(filePath, root) {
526
850
  if (!regionDir || !orgDir || !spaceDir || !appDir) {
527
851
  throw new Error(`File is not inside a CF-structured bruno collection: ${filePath}`);
528
852
  }
529
- return join3(root, regionDir, orgDir, spaceDir, appDir);
853
+ return join4(root, regionDir, orgDir, spaceDir, appDir);
530
854
  }
531
855
  async function buildRunPlan(options) {
532
856
  const { filePath } = await resolveTarget(options.root, options.target);
@@ -576,216 +900,6 @@ async function runBruno(options) {
576
900
  return { ...plan, ...result };
577
901
  }
578
902
 
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
903
  // src/use.ts
790
904
  function parseContextShorthand(shorthand) {
791
905
  const segs = shorthand.split("/").filter((s) => s.length > 0);
@@ -851,39 +965,7 @@ async function main(argv) {
851
965
  selectSpace: async (choices) => await select({ message: "Select space", choices: [...choices] }),
852
966
  selectApp: async (choices) => await select({ message: "Select app", choices: [...choices] }),
853
967
  confirmCreate: async (path) => await confirm({ message: `Create ${path}?`, default: true }),
854
- selectEnvironments: async ({ common, existing }) => {
855
- const seen = /* @__PURE__ */ new Set();
856
- const all = [...common, ...existing].filter((name) => {
857
- if (seen.has(name)) {
858
- return false;
859
- }
860
- seen.add(name);
861
- return true;
862
- });
863
- return await checkbox({
864
- message: "Environments to create (space to toggle, enter to confirm)",
865
- choices: all.map((name) => ({
866
- name,
867
- value: name,
868
- checked: existing.includes(name)
869
- }))
870
- });
871
- },
872
- inputCustomEnvName: async () => {
873
- const raw = await input({
874
- message: "Custom environment name (leave empty to skip)",
875
- default: "",
876
- validate: (v) => {
877
- const t = v.trim();
878
- if (t.length === 0) {
879
- return true;
880
- }
881
- return /^[A-Za-z0-9._-]+$/.test(t) ? true : "Only letters, digits, dot, underscore, and dash are allowed.";
882
- }
883
- });
884
- const trimmed = raw.trim();
885
- return trimmed.length > 0 ? trimmed : null;
886
- }
968
+ selectEnvironments: async (opts) => await promptForEnvironments(opts)
887
969
  },
888
970
  log: (msg) => {
889
971
  process2.stdout.write(`${msg}