@saptools/bruno 0.3.1 → 0.3.2
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 +490 -494
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +56 -56
- package/dist/index.js +233 -231
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,38 +3,7 @@
|
|
|
3
3
|
// src/types.ts
|
|
4
4
|
var CF_META_KEYS = ["__cf_region", "__cf_org", "__cf_space", "__cf_app"];
|
|
5
5
|
|
|
6
|
-
// src/
|
|
7
|
-
import { homedir } from "os";
|
|
8
|
-
import { join } from "path";
|
|
9
|
-
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
10
|
-
var BRUNO_CONTEXT_FILENAME = "bruno-context.json";
|
|
11
|
-
var REGION_FOLDER_PREFIX = "region__";
|
|
12
|
-
var ORG_FOLDER_PREFIX = "org__";
|
|
13
|
-
var SPACE_FOLDER_PREFIX = "space__";
|
|
14
|
-
var ENVIRONMENTS_DIR = "environments";
|
|
15
|
-
function saptoolsDir() {
|
|
16
|
-
return join(homedir(), SAPTOOLS_DIR_NAME);
|
|
17
|
-
}
|
|
18
|
-
function brunoContextPath() {
|
|
19
|
-
return join(saptoolsDir(), BRUNO_CONTEXT_FILENAME);
|
|
20
|
-
}
|
|
21
|
-
function regionFolderName(key) {
|
|
22
|
-
return `${REGION_FOLDER_PREFIX}${key}`;
|
|
23
|
-
}
|
|
24
|
-
function orgFolderName(name) {
|
|
25
|
-
return `${ORG_FOLDER_PREFIX}${name}`;
|
|
26
|
-
}
|
|
27
|
-
function spaceFolderName(name) {
|
|
28
|
-
return `${SPACE_FOLDER_PREFIX}${name}`;
|
|
29
|
-
}
|
|
30
|
-
function parsePrefixedName(dirName, prefix) {
|
|
31
|
-
if (!dirName.startsWith(prefix)) {
|
|
32
|
-
return void 0;
|
|
33
|
-
}
|
|
34
|
-
return dirName.slice(prefix.length);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// src/bru-parser.ts
|
|
6
|
+
// src/bruno/parser.ts
|
|
38
7
|
var HEADER_REGEX = /(^|\n)[^\S\n]*([a-zA-Z][a-zA-Z0-9:_-]*)[^\S\n]*([{[])/g;
|
|
39
8
|
function findMatchingClose(raw, open, openIdx) {
|
|
40
9
|
const close = open === "{" ? "}" : "]";
|
|
@@ -122,7 +91,7 @@ function parseBruEnvFile(raw) {
|
|
|
122
91
|
return { vars: { entries }, secrets };
|
|
123
92
|
}
|
|
124
93
|
|
|
125
|
-
// src/
|
|
94
|
+
// src/bruno/writer.ts
|
|
126
95
|
function formatVarsBlock(entries) {
|
|
127
96
|
const lines = [];
|
|
128
97
|
for (const [key, value] of entries) {
|
|
@@ -185,7 +154,7 @@ function ensureSecretEntry(raw, secretName) {
|
|
|
185
154
|
return { content: `${before}${rebuilt}${after}`, changed: true };
|
|
186
155
|
}
|
|
187
156
|
|
|
188
|
-
// src/cf
|
|
157
|
+
// src/cf/info.ts
|
|
189
158
|
import {
|
|
190
159
|
getRegionView as getRegionViewApi,
|
|
191
160
|
readRegionsView,
|
|
@@ -260,7 +229,7 @@ async function resolveRef(ref, deps = defaultCfInfoDeps) {
|
|
|
260
229
|
return { region, org, space, app };
|
|
261
230
|
}
|
|
262
231
|
|
|
263
|
-
// src/cf
|
|
232
|
+
// src/cf/meta.ts
|
|
264
233
|
import { readFile, writeFile } from "fs/promises";
|
|
265
234
|
function readCfMetaFromVars(vars) {
|
|
266
235
|
const region = vars.get("__cf_region");
|
|
@@ -309,9 +278,42 @@ async function writeCfMetaToFile(path, ref, baseUrl) {
|
|
|
309
278
|
return changed;
|
|
310
279
|
}
|
|
311
280
|
|
|
312
|
-
// src/folder-scan.ts
|
|
281
|
+
// src/collection/folder-scan.ts
|
|
313
282
|
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
314
283
|
import { join as join2 } from "path";
|
|
284
|
+
|
|
285
|
+
// src/collection/paths.ts
|
|
286
|
+
import { homedir } from "os";
|
|
287
|
+
import { join } from "path";
|
|
288
|
+
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
289
|
+
var BRUNO_CONTEXT_FILENAME = "bruno-context.json";
|
|
290
|
+
var REGION_FOLDER_PREFIX = "region__";
|
|
291
|
+
var ORG_FOLDER_PREFIX = "org__";
|
|
292
|
+
var SPACE_FOLDER_PREFIX = "space__";
|
|
293
|
+
var ENVIRONMENTS_DIR = "environments";
|
|
294
|
+
function saptoolsDir() {
|
|
295
|
+
return join(homedir(), SAPTOOLS_DIR_NAME);
|
|
296
|
+
}
|
|
297
|
+
function brunoContextPath() {
|
|
298
|
+
return join(saptoolsDir(), BRUNO_CONTEXT_FILENAME);
|
|
299
|
+
}
|
|
300
|
+
function regionFolderName(key) {
|
|
301
|
+
return `${REGION_FOLDER_PREFIX}${key}`;
|
|
302
|
+
}
|
|
303
|
+
function orgFolderName(name) {
|
|
304
|
+
return `${ORG_FOLDER_PREFIX}${name}`;
|
|
305
|
+
}
|
|
306
|
+
function spaceFolderName(name) {
|
|
307
|
+
return `${SPACE_FOLDER_PREFIX}${name}`;
|
|
308
|
+
}
|
|
309
|
+
function parsePrefixedName(dirName, prefix) {
|
|
310
|
+
if (!dirName.startsWith(prefix)) {
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
return dirName.slice(prefix.length);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/collection/folder-scan.ts
|
|
315
317
|
async function safeReaddir(path) {
|
|
316
318
|
try {
|
|
317
319
|
const entries = await readdir(path, { withFileTypes: true });
|
|
@@ -429,193 +431,11 @@ function parseShorthandPath(shorthand) {
|
|
|
429
431
|
return environment ? { region, org, space, app, environment, filePath } : { region, org, space, app, filePath };
|
|
430
432
|
}
|
|
431
433
|
|
|
432
|
-
// src/
|
|
433
|
-
import { mkdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
434
|
-
import { dirname } from "path";
|
|
435
|
-
async function readContext() {
|
|
436
|
-
try {
|
|
437
|
-
const raw = await readFile3(brunoContextPath(), "utf8");
|
|
438
|
-
return JSON.parse(raw);
|
|
439
|
-
} catch (err) {
|
|
440
|
-
if (err.code === "ENOENT") {
|
|
441
|
-
return void 0;
|
|
442
|
-
}
|
|
443
|
-
throw err;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
async function writeContext(ctx) {
|
|
447
|
-
const updated = { ...ctx, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
448
|
-
const path = brunoContextPath();
|
|
449
|
-
await mkdir(dirname(path), { recursive: true });
|
|
450
|
-
await writeFile2(path, `${JSON.stringify(updated, null, 2)}
|
|
451
|
-
`, "utf8");
|
|
452
|
-
return updated;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// src/setup-app.ts
|
|
456
|
-
import { mkdir as mkdir2, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
|
|
457
|
-
import { basename, join as join3 } from "path";
|
|
458
|
-
var COMMON_ENVIRONMENTS = ["local", "dev", "staging", "prod"];
|
|
459
|
-
var BRUNO_COLLECTION_CONFIG_FILENAME = "bruno.json";
|
|
460
|
-
var ENV_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
461
|
-
function assertValidEnvName(name) {
|
|
462
|
-
if (!ENV_NAME_PATTERN.test(name)) {
|
|
463
|
-
throw new Error(
|
|
464
|
-
`Invalid environment name '${name}': only letters, digits, dot, underscore, and dash are allowed.`
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
function emptyEnvContent(envName, ref) {
|
|
469
|
-
const lines = [
|
|
470
|
-
"vars {",
|
|
471
|
-
` __cf_region: ${ref.region}`,
|
|
472
|
-
` __cf_org: ${ref.org}`,
|
|
473
|
-
` __cf_space: ${ref.space}`,
|
|
474
|
-
` __cf_app: ${ref.app}`,
|
|
475
|
-
` environment: ${envName}`,
|
|
476
|
-
" baseUrl: ",
|
|
477
|
-
"}",
|
|
478
|
-
""
|
|
479
|
-
];
|
|
480
|
-
return lines.join("\n");
|
|
481
|
-
}
|
|
482
|
-
function normalizeCollectionName(root) {
|
|
483
|
-
const candidate = basename(root).replace(/^\.+/, "").trim();
|
|
484
|
-
return candidate.length > 0 ? candidate : "bruno-collection";
|
|
485
|
-
}
|
|
486
|
-
function defaultBrunoConfig(root) {
|
|
487
|
-
return `${JSON.stringify(
|
|
488
|
-
{
|
|
489
|
-
version: "1",
|
|
490
|
-
name: normalizeCollectionName(root),
|
|
491
|
-
type: "collection",
|
|
492
|
-
ignore: ["node_modules", ".git"]
|
|
493
|
-
},
|
|
494
|
-
null,
|
|
495
|
-
2
|
|
496
|
-
)}
|
|
497
|
-
`;
|
|
498
|
-
}
|
|
499
|
-
async function ensureCollectionConfig(root) {
|
|
500
|
-
const filePath = join3(root, BRUNO_COLLECTION_CONFIG_FILENAME);
|
|
501
|
-
try {
|
|
502
|
-
await writeFile3(filePath, defaultBrunoConfig(root), { encoding: "utf8", flag: "wx" });
|
|
503
|
-
} catch (err) {
|
|
504
|
-
if (err.code !== "EEXIST") {
|
|
505
|
-
throw err;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
async function ensureEnvFile(appPath, envName, ref) {
|
|
510
|
-
const envDir = join3(appPath, ENVIRONMENTS_DIR);
|
|
511
|
-
await mkdir2(envDir, { recursive: true });
|
|
512
|
-
const filePath = join3(envDir, `${envName}.bru`);
|
|
513
|
-
try {
|
|
514
|
-
await writeFile3(filePath, emptyEnvContent(envName, ref), { encoding: "utf8", flag: "wx" });
|
|
515
|
-
} catch (err) {
|
|
516
|
-
if (err.code !== "EEXIST") {
|
|
517
|
-
throw err;
|
|
518
|
-
}
|
|
519
|
-
await writeCfMetaToFile(filePath, ref);
|
|
520
|
-
}
|
|
521
|
-
return filePath;
|
|
522
|
-
}
|
|
523
|
-
function pickRegion(regions) {
|
|
524
|
-
return regions.map((r) => ({ value: r.key, name: `${r.key} \u2014 ${r.label} (${r.orgCount.toString()} org${r.orgCount === 1 ? "" : "s"})` }));
|
|
525
|
-
}
|
|
526
|
-
function pickOrg(region) {
|
|
527
|
-
return region.orgs.map((o) => ({ value: o.name, name: `${o.name} (${o.spaces.length.toString()} space${o.spaces.length === 1 ? "" : "s"})` }));
|
|
528
|
-
}
|
|
529
|
-
function pickSpace(org) {
|
|
530
|
-
return org.spaces.map((s) => ({ value: s.name, name: `${s.name} (${s.apps.length.toString()} app${s.apps.length === 1 ? "" : "s"})` }));
|
|
531
|
-
}
|
|
532
|
-
function pickApp(space) {
|
|
533
|
-
return space.apps.map((a) => ({ value: a.name, name: a.name }));
|
|
534
|
-
}
|
|
535
|
-
async function setupApp(options) {
|
|
536
|
-
const deps = options.deps ?? defaultCfInfoDeps;
|
|
537
|
-
const log = options.log ?? (() => void 0);
|
|
538
|
-
const regions = await listRegionsWithContent(deps);
|
|
539
|
-
if (regions.length === 0) {
|
|
540
|
-
throw new Error("No CF regions with orgs are cached. Run `cf-sync sync` first.");
|
|
541
|
-
}
|
|
542
|
-
const regionKey = await options.prompts.selectRegion(pickRegion(regions));
|
|
543
|
-
const regionView = await deps.readRegionView(regionKey);
|
|
544
|
-
if (!regionView) {
|
|
545
|
-
throw new Error(`Region ${regionKey} is not cached. Run \`cf-sync region ${regionKey}\` or \`cf-sync sync\` first.`);
|
|
546
|
-
}
|
|
547
|
-
const region = regionView.region;
|
|
548
|
-
if (region.orgs.length === 0) {
|
|
549
|
-
throw new Error(`Region ${regionKey} has no accessible orgs.`);
|
|
550
|
-
}
|
|
551
|
-
const orgName = await options.prompts.selectOrg(pickOrg(region));
|
|
552
|
-
const org = region.orgs.find((o) => o.name === orgName);
|
|
553
|
-
if (!org) {
|
|
554
|
-
throw new Error(`Org ${orgName} not found in region ${regionKey}`);
|
|
555
|
-
}
|
|
556
|
-
if (org.spaces.length === 0) {
|
|
557
|
-
throw new Error(`Org ${orgName} has no spaces.`);
|
|
558
|
-
}
|
|
559
|
-
const spaceName = await options.prompts.selectSpace(pickSpace(org));
|
|
560
|
-
const space = org.spaces.find((s) => s.name === spaceName);
|
|
561
|
-
if (!space) {
|
|
562
|
-
throw new Error(`Space ${spaceName} not found in org ${orgName}`);
|
|
563
|
-
}
|
|
564
|
-
if (space.apps.length === 0) {
|
|
565
|
-
throw new Error(`Space ${spaceName} has no apps.`);
|
|
566
|
-
}
|
|
567
|
-
const appName = await options.prompts.selectApp(pickApp(space));
|
|
568
|
-
const ref = { region: regionKey, org: orgName, space: spaceName, app: appName };
|
|
569
|
-
const appPath = join3(
|
|
570
|
-
options.root,
|
|
571
|
-
regionFolderName(regionKey),
|
|
572
|
-
orgFolderName(orgName),
|
|
573
|
-
spaceFolderName(spaceName),
|
|
574
|
-
appName
|
|
575
|
-
);
|
|
576
|
-
const confirmed = await options.prompts.confirmCreate(appPath);
|
|
577
|
-
if (!confirmed) {
|
|
578
|
-
return { ref, appPath, environments: [], created: false };
|
|
579
|
-
}
|
|
580
|
-
await mkdir2(appPath, { recursive: true });
|
|
581
|
-
await ensureCollectionConfig(appPath);
|
|
582
|
-
const existingEnvs = await listExistingEnvs(appPath);
|
|
583
|
-
const common = [...COMMON_ENVIRONMENTS];
|
|
584
|
-
const selected = await options.prompts.selectEnvironments({ common, existing: existingEnvs });
|
|
585
|
-
const merged = [];
|
|
586
|
-
for (const name of selected) {
|
|
587
|
-
const trimmed = name.trim();
|
|
588
|
-
if (trimmed.length === 0 || merged.includes(trimmed)) {
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
assertValidEnvName(trimmed);
|
|
592
|
-
merged.push(trimmed);
|
|
593
|
-
}
|
|
594
|
-
if (merged.length === 0) {
|
|
595
|
-
throw new Error("At least one environment is required.");
|
|
596
|
-
}
|
|
597
|
-
const created = [];
|
|
598
|
-
for (const envName of merged) {
|
|
599
|
-
const path = await ensureEnvFile(appPath, envName, ref);
|
|
600
|
-
created.push(path);
|
|
601
|
-
log(`\u2022 ${path}`);
|
|
602
|
-
}
|
|
603
|
-
return { ref, appPath, environments: created, created: true };
|
|
604
|
-
}
|
|
605
|
-
async function listExistingEnvs(appPath) {
|
|
606
|
-
try {
|
|
607
|
-
const entries = await readdir2(join3(appPath, ENVIRONMENTS_DIR), { withFileTypes: true });
|
|
608
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".bru")).map((e) => e.name.replace(/\.bru$/, ""));
|
|
609
|
-
} catch {
|
|
610
|
-
return [];
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// src/run.ts
|
|
434
|
+
// src/commands/run.ts
|
|
615
435
|
import { spawn } from "child_process";
|
|
616
|
-
import { readFile as
|
|
436
|
+
import { readFile as readFile3, stat, writeFile as writeFile2 } from "fs/promises";
|
|
617
437
|
import { createRequire } from "module";
|
|
618
|
-
import { delimiter, dirname
|
|
438
|
+
import { delimiter, dirname, isAbsolute, join as join3, relative, resolve, sep } from "path";
|
|
619
439
|
import { getTokenCached as getTokenCachedApi } from "@saptools/cf-xsuaa";
|
|
620
440
|
var require2 = createRequire(import.meta.url);
|
|
621
441
|
function pathEntries(env) {
|
|
@@ -633,7 +453,7 @@ async function findCommandOnPath(command, env) {
|
|
|
633
453
|
const candidates = pathCandidates(command, env);
|
|
634
454
|
for (const entry of pathEntries(env)) {
|
|
635
455
|
for (const candidate of candidates) {
|
|
636
|
-
const fullPath =
|
|
456
|
+
const fullPath = join3(entry, candidate);
|
|
637
457
|
if (await exists(fullPath)) {
|
|
638
458
|
return fullPath;
|
|
639
459
|
}
|
|
@@ -662,7 +482,7 @@ function defaultResolvePackageJsonPath() {
|
|
|
662
482
|
return require2.resolve("@usebruno/cli/package.json");
|
|
663
483
|
}
|
|
664
484
|
async function defaultReadTextFile(path) {
|
|
665
|
-
return await
|
|
485
|
+
return await readFile3(path, "utf8");
|
|
666
486
|
}
|
|
667
487
|
async function resolveBundledBruBinPath(deps) {
|
|
668
488
|
try {
|
|
@@ -672,7 +492,7 @@ async function resolveBundledBruBinPath(deps) {
|
|
|
672
492
|
if (!binPath) {
|
|
673
493
|
return void 0;
|
|
674
494
|
}
|
|
675
|
-
return resolve(
|
|
495
|
+
return resolve(dirname(packageJsonPath), binPath);
|
|
676
496
|
} catch {
|
|
677
497
|
return void 0;
|
|
678
498
|
}
|
|
@@ -728,7 +548,7 @@ async function resolveTarget(root, target) {
|
|
|
728
548
|
throw new Error(`Target not found: ${target}`);
|
|
729
549
|
}
|
|
730
550
|
const { region, org, space, app, filePath } = shorthand;
|
|
731
|
-
const appDir =
|
|
551
|
+
const appDir = join3(
|
|
732
552
|
root,
|
|
733
553
|
regionFolderName(region),
|
|
734
554
|
orgFolderName(org),
|
|
@@ -738,7 +558,7 @@ async function resolveTarget(root, target) {
|
|
|
738
558
|
if (!filePath) {
|
|
739
559
|
return { filePath: appDir, shorthand };
|
|
740
560
|
}
|
|
741
|
-
const candidate =
|
|
561
|
+
const candidate = join3(appDir, filePath);
|
|
742
562
|
if (await exists(candidate)) {
|
|
743
563
|
return { filePath: candidate, shorthand };
|
|
744
564
|
}
|
|
@@ -750,7 +570,7 @@ async function resolveTarget(root, target) {
|
|
|
750
570
|
}
|
|
751
571
|
async function chooseEnvironmentFile(appDir, environment) {
|
|
752
572
|
if (environment) {
|
|
753
|
-
const envFile =
|
|
573
|
+
const envFile = join3(appDir, ENVIRONMENTS_DIR, `${environment}.bru`);
|
|
754
574
|
if (!await exists(envFile)) {
|
|
755
575
|
throw new Error(`Environment file not found: ${envFile}`);
|
|
756
576
|
}
|
|
@@ -782,13 +602,13 @@ function findAppDirFromFile(filePath, root) {
|
|
|
782
602
|
if (!regionDir || !orgDir || !spaceDir || !appDir) {
|
|
783
603
|
throw new Error(`File is not inside a CF-structured bruno collection: ${filePath}`);
|
|
784
604
|
}
|
|
785
|
-
return
|
|
605
|
+
return join3(root, regionDir, orgDir, spaceDir, appDir);
|
|
786
606
|
}
|
|
787
607
|
async function persistAccessToken(envFile, token) {
|
|
788
|
-
const raw = await
|
|
608
|
+
const raw = await readFile3(envFile, "utf8");
|
|
789
609
|
const { content, changed } = upsertVars(raw, /* @__PURE__ */ new Map([["accessToken", token]]));
|
|
790
610
|
if (changed) {
|
|
791
|
-
await
|
|
611
|
+
await writeFile2(envFile, content, "utf8");
|
|
792
612
|
}
|
|
793
613
|
}
|
|
794
614
|
async function buildRunPlan(options) {
|
|
@@ -840,7 +660,189 @@ async function runBruno(options) {
|
|
|
840
660
|
return { ...plan, ...result };
|
|
841
661
|
}
|
|
842
662
|
|
|
843
|
-
// src/
|
|
663
|
+
// src/commands/setup-app.ts
|
|
664
|
+
import { mkdir, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
|
|
665
|
+
import { basename, join as join4 } from "path";
|
|
666
|
+
var COMMON_ENVIRONMENTS = ["local", "dev", "staging", "prod"];
|
|
667
|
+
var BRUNO_COLLECTION_CONFIG_FILENAME = "bruno.json";
|
|
668
|
+
var ENV_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
669
|
+
function assertValidEnvName(name) {
|
|
670
|
+
if (!ENV_NAME_PATTERN.test(name)) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
`Invalid environment name '${name}': only letters, digits, dot, underscore, and dash are allowed.`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function emptyEnvContent(envName, ref) {
|
|
677
|
+
const lines = [
|
|
678
|
+
"vars {",
|
|
679
|
+
` __cf_region: ${ref.region}`,
|
|
680
|
+
` __cf_org: ${ref.org}`,
|
|
681
|
+
` __cf_space: ${ref.space}`,
|
|
682
|
+
` __cf_app: ${ref.app}`,
|
|
683
|
+
` environment: ${envName}`,
|
|
684
|
+
" baseUrl: ",
|
|
685
|
+
"}",
|
|
686
|
+
""
|
|
687
|
+
];
|
|
688
|
+
return lines.join("\n");
|
|
689
|
+
}
|
|
690
|
+
function normalizeCollectionName(root) {
|
|
691
|
+
const candidate = basename(root).replace(/^\.+/, "").trim();
|
|
692
|
+
return candidate.length > 0 ? candidate : "bruno-collection";
|
|
693
|
+
}
|
|
694
|
+
function defaultBrunoConfig(root) {
|
|
695
|
+
return `${JSON.stringify(
|
|
696
|
+
{
|
|
697
|
+
version: "1",
|
|
698
|
+
name: normalizeCollectionName(root),
|
|
699
|
+
type: "collection",
|
|
700
|
+
ignore: ["node_modules", ".git"]
|
|
701
|
+
},
|
|
702
|
+
null,
|
|
703
|
+
2
|
|
704
|
+
)}
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
async function ensureCollectionConfig(root) {
|
|
708
|
+
const filePath = join4(root, BRUNO_COLLECTION_CONFIG_FILENAME);
|
|
709
|
+
try {
|
|
710
|
+
await writeFile3(filePath, defaultBrunoConfig(root), { encoding: "utf8", flag: "wx" });
|
|
711
|
+
} catch (err) {
|
|
712
|
+
if (err.code !== "EEXIST") {
|
|
713
|
+
throw err;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function ensureEnvFile(appPath, envName, ref) {
|
|
718
|
+
const envDir = join4(appPath, ENVIRONMENTS_DIR);
|
|
719
|
+
await mkdir(envDir, { recursive: true });
|
|
720
|
+
const filePath = join4(envDir, `${envName}.bru`);
|
|
721
|
+
try {
|
|
722
|
+
await writeFile3(filePath, emptyEnvContent(envName, ref), { encoding: "utf8", flag: "wx" });
|
|
723
|
+
} catch (err) {
|
|
724
|
+
if (err.code !== "EEXIST") {
|
|
725
|
+
throw err;
|
|
726
|
+
}
|
|
727
|
+
await writeCfMetaToFile(filePath, ref);
|
|
728
|
+
}
|
|
729
|
+
return filePath;
|
|
730
|
+
}
|
|
731
|
+
function pickRegion(regions) {
|
|
732
|
+
return regions.map((r) => ({ value: r.key, name: `${r.key} \u2014 ${r.label} (${r.orgCount.toString()} org${r.orgCount === 1 ? "" : "s"})` }));
|
|
733
|
+
}
|
|
734
|
+
function pickOrg(region) {
|
|
735
|
+
return region.orgs.map((o) => ({ value: o.name, name: `${o.name} (${o.spaces.length.toString()} space${o.spaces.length === 1 ? "" : "s"})` }));
|
|
736
|
+
}
|
|
737
|
+
function pickSpace(org) {
|
|
738
|
+
return org.spaces.map((s) => ({ value: s.name, name: `${s.name} (${s.apps.length.toString()} app${s.apps.length === 1 ? "" : "s"})` }));
|
|
739
|
+
}
|
|
740
|
+
function pickApp(space) {
|
|
741
|
+
return space.apps.map((a) => ({ value: a.name, name: a.name }));
|
|
742
|
+
}
|
|
743
|
+
async function setupApp(options) {
|
|
744
|
+
const deps = options.deps ?? defaultCfInfoDeps;
|
|
745
|
+
const log = options.log ?? (() => void 0);
|
|
746
|
+
const regions = await listRegionsWithContent(deps);
|
|
747
|
+
if (regions.length === 0) {
|
|
748
|
+
throw new Error("No CF regions with orgs are cached. Run `cf-sync sync` first.");
|
|
749
|
+
}
|
|
750
|
+
const regionKey = await options.prompts.selectRegion(pickRegion(regions));
|
|
751
|
+
const regionView = await deps.readRegionView(regionKey);
|
|
752
|
+
if (!regionView) {
|
|
753
|
+
throw new Error(`Region ${regionKey} is not cached. Run \`cf-sync region ${regionKey}\` or \`cf-sync sync\` first.`);
|
|
754
|
+
}
|
|
755
|
+
const region = regionView.region;
|
|
756
|
+
if (region.orgs.length === 0) {
|
|
757
|
+
throw new Error(`Region ${regionKey} has no accessible orgs.`);
|
|
758
|
+
}
|
|
759
|
+
const orgName = await options.prompts.selectOrg(pickOrg(region));
|
|
760
|
+
const org = region.orgs.find((o) => o.name === orgName);
|
|
761
|
+
if (!org) {
|
|
762
|
+
throw new Error(`Org ${orgName} not found in region ${regionKey}`);
|
|
763
|
+
}
|
|
764
|
+
if (org.spaces.length === 0) {
|
|
765
|
+
throw new Error(`Org ${orgName} has no spaces.`);
|
|
766
|
+
}
|
|
767
|
+
const spaceName = await options.prompts.selectSpace(pickSpace(org));
|
|
768
|
+
const space = org.spaces.find((s) => s.name === spaceName);
|
|
769
|
+
if (!space) {
|
|
770
|
+
throw new Error(`Space ${spaceName} not found in org ${orgName}`);
|
|
771
|
+
}
|
|
772
|
+
if (space.apps.length === 0) {
|
|
773
|
+
throw new Error(`Space ${spaceName} has no apps.`);
|
|
774
|
+
}
|
|
775
|
+
const appName = await options.prompts.selectApp(pickApp(space));
|
|
776
|
+
const ref = { region: regionKey, org: orgName, space: spaceName, app: appName };
|
|
777
|
+
const appPath = join4(
|
|
778
|
+
options.root,
|
|
779
|
+
regionFolderName(regionKey),
|
|
780
|
+
orgFolderName(orgName),
|
|
781
|
+
spaceFolderName(spaceName),
|
|
782
|
+
appName
|
|
783
|
+
);
|
|
784
|
+
const confirmed = await options.prompts.confirmCreate(appPath);
|
|
785
|
+
if (!confirmed) {
|
|
786
|
+
return { ref, appPath, environments: [], created: false };
|
|
787
|
+
}
|
|
788
|
+
await mkdir(appPath, { recursive: true });
|
|
789
|
+
await ensureCollectionConfig(appPath);
|
|
790
|
+
const existingEnvs = await listExistingEnvs(appPath);
|
|
791
|
+
const common = [...COMMON_ENVIRONMENTS];
|
|
792
|
+
const selected = await options.prompts.selectEnvironments({ common, existing: existingEnvs });
|
|
793
|
+
const merged = [];
|
|
794
|
+
for (const name of selected) {
|
|
795
|
+
const trimmed = name.trim();
|
|
796
|
+
if (trimmed.length === 0 || merged.includes(trimmed)) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
assertValidEnvName(trimmed);
|
|
800
|
+
merged.push(trimmed);
|
|
801
|
+
}
|
|
802
|
+
if (merged.length === 0) {
|
|
803
|
+
throw new Error("At least one environment is required.");
|
|
804
|
+
}
|
|
805
|
+
const created = [];
|
|
806
|
+
for (const envName of merged) {
|
|
807
|
+
const path = await ensureEnvFile(appPath, envName, ref);
|
|
808
|
+
created.push(path);
|
|
809
|
+
log(`\u2022 ${path}`);
|
|
810
|
+
}
|
|
811
|
+
return { ref, appPath, environments: created, created: true };
|
|
812
|
+
}
|
|
813
|
+
async function listExistingEnvs(appPath) {
|
|
814
|
+
try {
|
|
815
|
+
const entries = await readdir2(join4(appPath, ENVIRONMENTS_DIR), { withFileTypes: true });
|
|
816
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".bru")).map((e) => e.name.replace(/\.bru$/, ""));
|
|
817
|
+
} catch {
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/state/context.ts
|
|
823
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
824
|
+
import { dirname as dirname2 } from "path";
|
|
825
|
+
async function readContext() {
|
|
826
|
+
try {
|
|
827
|
+
const raw = await readFile4(brunoContextPath(), "utf8");
|
|
828
|
+
return JSON.parse(raw);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
if (err.code === "ENOENT") {
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
throw err;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
async function writeContext(ctx) {
|
|
837
|
+
const updated = { ...ctx, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
838
|
+
const path = brunoContextPath();
|
|
839
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
840
|
+
await writeFile4(path, `${JSON.stringify(updated, null, 2)}
|
|
841
|
+
`, "utf8");
|
|
842
|
+
return updated;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/commands/use.ts
|
|
844
846
|
function parseContextShorthand(shorthand) {
|
|
845
847
|
const segs = shorthand.split("/").filter((s) => s.length > 0);
|
|
846
848
|
if (segs.length !== 4) {
|