@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/README.md +8 -6
- package/dist/cli.js +347 -265
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +34 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import process2 from "process";
|
|
5
|
-
import {
|
|
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/
|
|
65
|
-
import {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (
|
|
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}
|