@saptools/bruno 0.3.1 → 0.3.3
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 +539 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +56 -56
- package/dist/index.js +262 -231
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,202 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/cli.ts
|
|
3
|
+
// src/cli/main.ts
|
|
4
4
|
import process2 from "process";
|
|
5
5
|
import { confirm, select } from "@inquirer/prompts";
|
|
6
6
|
import { Command, Option } from "commander";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
function scoreChoice(choice, normalizedTerm) {
|
|
16
|
-
const name = choice.name.toLowerCase();
|
|
17
|
-
const value = choice.value.toLowerCase();
|
|
18
|
-
if (name === normalizedTerm || value === normalizedTerm) {
|
|
19
|
-
return 0;
|
|
20
|
-
}
|
|
21
|
-
if (name.startsWith(normalizedTerm) || value.startsWith(normalizedTerm)) {
|
|
22
|
-
return 1;
|
|
23
|
-
}
|
|
24
|
-
if (name.includes(normalizedTerm) || value.includes(normalizedTerm)) {
|
|
25
|
-
return 2;
|
|
26
|
-
}
|
|
27
|
-
return Number.POSITIVE_INFINITY;
|
|
28
|
-
}
|
|
29
|
-
function noMatchChoice(term) {
|
|
30
|
-
const label = term?.trim() ?? "";
|
|
31
|
-
return {
|
|
32
|
-
value: NO_MATCHING_APP,
|
|
33
|
-
name: `No apps match "${label}"`,
|
|
34
|
-
disabled: "Type a different search term"
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function buildAppSearchChoices(choices, term) {
|
|
38
|
-
const normalizedTerm = normalizeTerm(term);
|
|
39
|
-
if (normalizedTerm.length === 0) {
|
|
40
|
-
return [...choices];
|
|
41
|
-
}
|
|
42
|
-
const rankedMatches = choices.map((choice, index) => ({ choice, index, score: scoreChoice(choice, normalizedTerm) })).filter((item) => Number.isFinite(item.score)).sort((left, right) => left.score - right.score || left.index - right.index).map((item) => item.choice);
|
|
43
|
-
if (rankedMatches.length > 0) {
|
|
44
|
-
return rankedMatches;
|
|
45
|
-
}
|
|
46
|
-
return [noMatchChoice(term)];
|
|
47
|
-
}
|
|
48
|
-
async function promptForAppSelection(choices, deps = {}) {
|
|
49
|
-
const searchPrompt = deps.searchPrompt ?? search;
|
|
50
|
-
return await searchPrompt({
|
|
51
|
-
message: "Select app",
|
|
52
|
-
pageSize: DEFAULT_PAGE_SIZE,
|
|
53
|
-
source: (term) => Promise.resolve(buildAppSearchChoices(choices, term)),
|
|
54
|
-
validate: (value) => value === NO_MATCHING_APP ? "Select a real app." : true
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// src/context.ts
|
|
59
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
60
|
-
import { dirname } from "path";
|
|
61
|
-
|
|
62
|
-
// src/paths.ts
|
|
63
|
-
import { homedir } from "os";
|
|
64
|
-
import { join } from "path";
|
|
65
|
-
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
66
|
-
var BRUNO_CONTEXT_FILENAME = "bruno-context.json";
|
|
67
|
-
var REGION_FOLDER_PREFIX = "region__";
|
|
68
|
-
var ORG_FOLDER_PREFIX = "org__";
|
|
69
|
-
var SPACE_FOLDER_PREFIX = "space__";
|
|
70
|
-
var ENVIRONMENTS_DIR = "environments";
|
|
71
|
-
function saptoolsDir() {
|
|
72
|
-
return join(homedir(), SAPTOOLS_DIR_NAME);
|
|
73
|
-
}
|
|
74
|
-
function brunoContextPath() {
|
|
75
|
-
return join(saptoolsDir(), BRUNO_CONTEXT_FILENAME);
|
|
76
|
-
}
|
|
77
|
-
function regionFolderName(key) {
|
|
78
|
-
return `${REGION_FOLDER_PREFIX}${key}`;
|
|
79
|
-
}
|
|
80
|
-
function orgFolderName(name) {
|
|
81
|
-
return `${ORG_FOLDER_PREFIX}${name}`;
|
|
82
|
-
}
|
|
83
|
-
function spaceFolderName(name) {
|
|
84
|
-
return `${SPACE_FOLDER_PREFIX}${name}`;
|
|
85
|
-
}
|
|
86
|
-
function parsePrefixedName(dirName, prefix) {
|
|
87
|
-
if (!dirName.startsWith(prefix)) {
|
|
88
|
-
return void 0;
|
|
89
|
-
}
|
|
90
|
-
return dirName.slice(prefix.length);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// src/context.ts
|
|
94
|
-
async function readContext() {
|
|
95
|
-
try {
|
|
96
|
-
const raw = await readFile(brunoContextPath(), "utf8");
|
|
97
|
-
return JSON.parse(raw);
|
|
98
|
-
} catch (err) {
|
|
99
|
-
if (err.code === "ENOENT") {
|
|
100
|
-
return void 0;
|
|
101
|
-
}
|
|
102
|
-
throw err;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
async function writeContext(ctx) {
|
|
106
|
-
const updated = { ...ctx, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
107
|
-
const path = brunoContextPath();
|
|
108
|
-
await mkdir(dirname(path), { recursive: true });
|
|
109
|
-
await writeFile(path, `${JSON.stringify(updated, null, 2)}
|
|
110
|
-
`, "utf8");
|
|
111
|
-
return updated;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// src/environment-prompt.ts
|
|
115
|
-
import { Separator, checkbox, input } from "@inquirer/prompts";
|
|
116
|
-
|
|
117
|
-
// src/setup-app.ts
|
|
118
|
-
import { mkdir as mkdir2, readdir, writeFile as writeFile3 } from "fs/promises";
|
|
119
|
-
import { basename, join as join2 } from "path";
|
|
120
|
-
|
|
121
|
-
// src/cf-info.ts
|
|
122
|
-
import {
|
|
123
|
-
getRegionView as getRegionViewApi,
|
|
124
|
-
readRegionsView,
|
|
125
|
-
readRegionView,
|
|
126
|
-
readStructureView,
|
|
127
|
-
REGION_KEYS
|
|
128
|
-
} from "@saptools/cf-sync";
|
|
129
|
-
var defaultCfInfoDeps = {
|
|
130
|
-
readStructureView,
|
|
131
|
-
readRegionsView,
|
|
132
|
-
readRegionView,
|
|
133
|
-
getRegionView: getRegionViewApi
|
|
134
|
-
};
|
|
135
|
-
function isValidRegionKey(value) {
|
|
136
|
-
return REGION_KEYS.includes(value);
|
|
137
|
-
}
|
|
138
|
-
async function getStructureSnapshot(deps = defaultCfInfoDeps) {
|
|
139
|
-
const view = await deps.readStructureView();
|
|
140
|
-
if (!view) {
|
|
141
|
-
return {
|
|
142
|
-
source: "empty",
|
|
143
|
-
structure: void 0,
|
|
144
|
-
stale: true,
|
|
145
|
-
message: "No CF structure cached. Run `cf-sync sync` first."
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
const stale = view.source === "runtime" && view.metadata?.status === "running";
|
|
149
|
-
return {
|
|
150
|
-
source: view.source,
|
|
151
|
-
structure: view.structure,
|
|
152
|
-
stale,
|
|
153
|
-
message: stale ? "A CF sync is still running \u2014 showing partial data." : void 0
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
async function listRegionsWithContent(deps = defaultCfInfoDeps) {
|
|
157
|
-
const snapshot = await getStructureSnapshot(deps);
|
|
158
|
-
if (!snapshot.structure) {
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
return snapshot.structure.regions.filter((r) => r.accessible && r.orgs.length > 0).map((r) => ({ key: r.key, label: r.label, orgCount: r.orgs.length }));
|
|
162
|
-
}
|
|
163
|
-
async function getRegion(key, deps = defaultCfInfoDeps) {
|
|
164
|
-
const view = await deps.readRegionView(key);
|
|
165
|
-
return view?.region;
|
|
166
|
-
}
|
|
167
|
-
function findOrg(region, orgName) {
|
|
168
|
-
return region.orgs.find((o) => o.name === orgName);
|
|
169
|
-
}
|
|
170
|
-
function findSpace(org, spaceName) {
|
|
171
|
-
return org.spaces.find((s) => s.name === spaceName);
|
|
172
|
-
}
|
|
173
|
-
function findApp(space, appName) {
|
|
174
|
-
return space.apps.find((a) => a.name === appName);
|
|
175
|
-
}
|
|
176
|
-
async function resolveRef(ref, deps = defaultCfInfoDeps) {
|
|
177
|
-
const region = await getRegion(ref.region, deps);
|
|
178
|
-
if (!region) {
|
|
179
|
-
return void 0;
|
|
180
|
-
}
|
|
181
|
-
const org = findOrg(region, ref.org);
|
|
182
|
-
if (!org) {
|
|
183
|
-
return void 0;
|
|
184
|
-
}
|
|
185
|
-
const space = findSpace(org, ref.space);
|
|
186
|
-
if (!space) {
|
|
187
|
-
return void 0;
|
|
188
|
-
}
|
|
189
|
-
const app = findApp(space, ref.app);
|
|
190
|
-
if (!app) {
|
|
191
|
-
return void 0;
|
|
192
|
-
}
|
|
193
|
-
return { region, org, space, app };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// src/cf-meta.ts
|
|
197
|
-
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
8
|
+
// src/commands/run.ts
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import { readFile as readFile3, stat, writeFile as writeFile2 } from "fs/promises";
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import { delimiter, dirname, isAbsolute, join as join3, relative, resolve, sep } from "path";
|
|
13
|
+
import { getTokenCached as getTokenCachedApi } from "@saptools/cf-xsuaa";
|
|
198
14
|
|
|
199
|
-
// src/
|
|
15
|
+
// src/bruno/parser.ts
|
|
200
16
|
var HEADER_REGEX = /(^|\n)[^\S\n]*([a-zA-Z][a-zA-Z0-9:_-]*)[^\S\n]*([{[])/g;
|
|
201
17
|
function findMatchingClose(raw, open, openIdx) {
|
|
202
18
|
const close = open === "{" ? "}" : "]";
|
|
@@ -284,7 +100,7 @@ function parseBruEnvFile(raw) {
|
|
|
284
100
|
return { vars: { entries }, secrets };
|
|
285
101
|
}
|
|
286
102
|
|
|
287
|
-
// src/
|
|
103
|
+
// src/bruno/writer.ts
|
|
288
104
|
function formatVarsBlock(entries) {
|
|
289
105
|
const lines = [];
|
|
290
106
|
for (const [key, value] of entries) {
|
|
@@ -323,7 +139,8 @@ ${formatVarsBlock(existing)}
|
|
|
323
139
|
return { content: `${before}${rebuilt}${after}`, changed: true };
|
|
324
140
|
}
|
|
325
141
|
|
|
326
|
-
// src/cf
|
|
142
|
+
// src/cf/meta.ts
|
|
143
|
+
import { readFile, writeFile } from "fs/promises";
|
|
327
144
|
function readCfMetaFromVars(vars) {
|
|
328
145
|
const region = vars.get("__cf_region");
|
|
329
146
|
const org = vars.get("__cf_org");
|
|
@@ -351,287 +168,74 @@ function buildCfMetaUpdates(ref, baseUrl) {
|
|
|
351
168
|
return updates;
|
|
352
169
|
}
|
|
353
170
|
async function readCfMetaFromFile(path) {
|
|
354
|
-
const raw = await
|
|
171
|
+
const raw = await readFile(path, "utf8");
|
|
355
172
|
const parsed = parseBruEnvFile(raw);
|
|
356
173
|
return readCfMetaFromVars(parsed.vars.entries);
|
|
357
174
|
}
|
|
358
175
|
async function writeCfMetaToFile(path, ref, baseUrl) {
|
|
359
|
-
const raw = await
|
|
176
|
+
const raw = await readFile(path, "utf8");
|
|
360
177
|
const updates = buildCfMetaUpdates(ref, baseUrl);
|
|
361
178
|
const { content, changed } = upsertVars(raw, updates);
|
|
362
179
|
if (changed) {
|
|
363
|
-
await
|
|
180
|
+
await writeFile(path, content, "utf8");
|
|
364
181
|
}
|
|
365
182
|
return changed;
|
|
366
183
|
}
|
|
367
184
|
|
|
368
|
-
// src/
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
185
|
+
// src/collection/folder-scan.ts
|
|
186
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
187
|
+
import { join as join2 } from "path";
|
|
188
|
+
|
|
189
|
+
// src/collection/paths.ts
|
|
190
|
+
import { homedir } from "os";
|
|
191
|
+
import { join } from "path";
|
|
192
|
+
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
193
|
+
var BRUNO_CONTEXT_FILENAME = "bruno-context.json";
|
|
194
|
+
var REGION_FOLDER_PREFIX = "region__";
|
|
195
|
+
var ORG_FOLDER_PREFIX = "org__";
|
|
196
|
+
var SPACE_FOLDER_PREFIX = "space__";
|
|
197
|
+
var ENVIRONMENTS_DIR = "environments";
|
|
198
|
+
function saptoolsDir() {
|
|
199
|
+
return join(homedir(), SAPTOOLS_DIR_NAME);
|
|
378
200
|
}
|
|
379
|
-
function
|
|
380
|
-
|
|
381
|
-
"vars {",
|
|
382
|
-
` __cf_region: ${ref.region}`,
|
|
383
|
-
` __cf_org: ${ref.org}`,
|
|
384
|
-
` __cf_space: ${ref.space}`,
|
|
385
|
-
` __cf_app: ${ref.app}`,
|
|
386
|
-
` environment: ${envName}`,
|
|
387
|
-
" baseUrl: ",
|
|
388
|
-
"}",
|
|
389
|
-
""
|
|
390
|
-
];
|
|
391
|
-
return lines.join("\n");
|
|
201
|
+
function brunoContextPath() {
|
|
202
|
+
return join(saptoolsDir(), BRUNO_CONTEXT_FILENAME);
|
|
392
203
|
}
|
|
393
|
-
function
|
|
394
|
-
|
|
395
|
-
return candidate.length > 0 ? candidate : "bruno-collection";
|
|
204
|
+
function regionFolderName(key) {
|
|
205
|
+
return `${REGION_FOLDER_PREFIX}${key}`;
|
|
396
206
|
}
|
|
397
|
-
function
|
|
398
|
-
return `${
|
|
399
|
-
{
|
|
400
|
-
version: "1",
|
|
401
|
-
name: normalizeCollectionName(root),
|
|
402
|
-
type: "collection",
|
|
403
|
-
ignore: ["node_modules", ".git"]
|
|
404
|
-
},
|
|
405
|
-
null,
|
|
406
|
-
2
|
|
407
|
-
)}
|
|
408
|
-
`;
|
|
207
|
+
function orgFolderName(name) {
|
|
208
|
+
return `${ORG_FOLDER_PREFIX}${name}`;
|
|
409
209
|
}
|
|
410
|
-
|
|
411
|
-
|
|
210
|
+
function spaceFolderName(name) {
|
|
211
|
+
return `${SPACE_FOLDER_PREFIX}${name}`;
|
|
212
|
+
}
|
|
213
|
+
function parsePrefixedName(dirName, prefix) {
|
|
214
|
+
if (!dirName.startsWith(prefix)) {
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
return dirName.slice(prefix.length);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/collection/folder-scan.ts
|
|
221
|
+
async function safeReaddir(path) {
|
|
412
222
|
try {
|
|
413
|
-
await
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
223
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
224
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
225
|
+
} catch {
|
|
226
|
+
return [];
|
|
418
227
|
}
|
|
419
228
|
}
|
|
420
|
-
async function
|
|
421
|
-
const envDir = join2(appPath, ENVIRONMENTS_DIR);
|
|
422
|
-
await mkdir2(envDir, { recursive: true });
|
|
423
|
-
const filePath = join2(envDir, `${envName}.bru`);
|
|
229
|
+
async function listFiles(path) {
|
|
424
230
|
try {
|
|
425
|
-
await
|
|
426
|
-
} catch (err) {
|
|
427
|
-
if (err.code !== "EEXIST") {
|
|
428
|
-
throw err;
|
|
429
|
-
}
|
|
430
|
-
await writeCfMetaToFile(filePath, ref);
|
|
431
|
-
}
|
|
432
|
-
return filePath;
|
|
433
|
-
}
|
|
434
|
-
function pickRegion(regions) {
|
|
435
|
-
return regions.map((r) => ({ value: r.key, name: `${r.key} \u2014 ${r.label} (${r.orgCount.toString()} org${r.orgCount === 1 ? "" : "s"})` }));
|
|
436
|
-
}
|
|
437
|
-
function pickOrg(region) {
|
|
438
|
-
return region.orgs.map((o) => ({ value: o.name, name: `${o.name} (${o.spaces.length.toString()} space${o.spaces.length === 1 ? "" : "s"})` }));
|
|
439
|
-
}
|
|
440
|
-
function pickSpace(org) {
|
|
441
|
-
return org.spaces.map((s) => ({ value: s.name, name: `${s.name} (${s.apps.length.toString()} app${s.apps.length === 1 ? "" : "s"})` }));
|
|
442
|
-
}
|
|
443
|
-
function pickApp(space) {
|
|
444
|
-
return space.apps.map((a) => ({ value: a.name, name: a.name }));
|
|
445
|
-
}
|
|
446
|
-
async function setupApp(options) {
|
|
447
|
-
const deps = options.deps ?? defaultCfInfoDeps;
|
|
448
|
-
const log = options.log ?? (() => void 0);
|
|
449
|
-
const regions = await listRegionsWithContent(deps);
|
|
450
|
-
if (regions.length === 0) {
|
|
451
|
-
throw new Error("No CF regions with orgs are cached. Run `cf-sync sync` first.");
|
|
452
|
-
}
|
|
453
|
-
const regionKey = await options.prompts.selectRegion(pickRegion(regions));
|
|
454
|
-
const regionView = await deps.readRegionView(regionKey);
|
|
455
|
-
if (!regionView) {
|
|
456
|
-
throw new Error(`Region ${regionKey} is not cached. Run \`cf-sync region ${regionKey}\` or \`cf-sync sync\` first.`);
|
|
457
|
-
}
|
|
458
|
-
const region = regionView.region;
|
|
459
|
-
if (region.orgs.length === 0) {
|
|
460
|
-
throw new Error(`Region ${regionKey} has no accessible orgs.`);
|
|
461
|
-
}
|
|
462
|
-
const orgName = await options.prompts.selectOrg(pickOrg(region));
|
|
463
|
-
const org = region.orgs.find((o) => o.name === orgName);
|
|
464
|
-
if (!org) {
|
|
465
|
-
throw new Error(`Org ${orgName} not found in region ${regionKey}`);
|
|
466
|
-
}
|
|
467
|
-
if (org.spaces.length === 0) {
|
|
468
|
-
throw new Error(`Org ${orgName} has no spaces.`);
|
|
469
|
-
}
|
|
470
|
-
const spaceName = await options.prompts.selectSpace(pickSpace(org));
|
|
471
|
-
const space = org.spaces.find((s) => s.name === spaceName);
|
|
472
|
-
if (!space) {
|
|
473
|
-
throw new Error(`Space ${spaceName} not found in org ${orgName}`);
|
|
474
|
-
}
|
|
475
|
-
if (space.apps.length === 0) {
|
|
476
|
-
throw new Error(`Space ${spaceName} has no apps.`);
|
|
477
|
-
}
|
|
478
|
-
const appName = await options.prompts.selectApp(pickApp(space));
|
|
479
|
-
const ref = { region: regionKey, org: orgName, space: spaceName, app: appName };
|
|
480
|
-
const appPath = join2(
|
|
481
|
-
options.root,
|
|
482
|
-
regionFolderName(regionKey),
|
|
483
|
-
orgFolderName(orgName),
|
|
484
|
-
spaceFolderName(spaceName),
|
|
485
|
-
appName
|
|
486
|
-
);
|
|
487
|
-
const confirmed = await options.prompts.confirmCreate(appPath);
|
|
488
|
-
if (!confirmed) {
|
|
489
|
-
return { ref, appPath, environments: [], created: false };
|
|
490
|
-
}
|
|
491
|
-
await mkdir2(appPath, { recursive: true });
|
|
492
|
-
await ensureCollectionConfig(appPath);
|
|
493
|
-
const existingEnvs = await listExistingEnvs(appPath);
|
|
494
|
-
const common = [...COMMON_ENVIRONMENTS];
|
|
495
|
-
const selected = await options.prompts.selectEnvironments({ common, existing: existingEnvs });
|
|
496
|
-
const merged = [];
|
|
497
|
-
for (const name of selected) {
|
|
498
|
-
const trimmed = name.trim();
|
|
499
|
-
if (trimmed.length === 0 || merged.includes(trimmed)) {
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
assertValidEnvName(trimmed);
|
|
503
|
-
merged.push(trimmed);
|
|
504
|
-
}
|
|
505
|
-
if (merged.length === 0) {
|
|
506
|
-
throw new Error("At least one environment is required.");
|
|
507
|
-
}
|
|
508
|
-
const created = [];
|
|
509
|
-
for (const envName of merged) {
|
|
510
|
-
const path = await ensureEnvFile(appPath, envName, ref);
|
|
511
|
-
created.push(path);
|
|
512
|
-
log(`\u2022 ${path}`);
|
|
513
|
-
}
|
|
514
|
-
return { ref, appPath, environments: created, created: true };
|
|
515
|
-
}
|
|
516
|
-
async function listExistingEnvs(appPath) {
|
|
517
|
-
try {
|
|
518
|
-
const entries = await readdir(join2(appPath, ENVIRONMENTS_DIR), { withFileTypes: true });
|
|
519
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".bru")).map((e) => e.name.replace(/\.bru$/, ""));
|
|
520
|
-
} catch {
|
|
521
|
-
return [];
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// src/environment-prompt.ts
|
|
526
|
-
var ADD_CUSTOM_ENVIRONMENT = "__saptools_add_custom_environment__";
|
|
527
|
-
function uniqueNames(names) {
|
|
528
|
-
const merged = [];
|
|
529
|
-
for (const name of names) {
|
|
530
|
-
if (!merged.includes(name)) {
|
|
531
|
-
merged.push(name);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
return merged;
|
|
535
|
-
}
|
|
536
|
-
function validateEnvironmentSelection(choices) {
|
|
537
|
-
const selected = choices.map((choice) => choice.value);
|
|
538
|
-
const hasEnvironment = selected.some((value) => value !== ADD_CUSTOM_ENVIRONMENT);
|
|
539
|
-
if (hasEnvironment || selected.includes(ADD_CUSTOM_ENVIRONMENT)) {
|
|
540
|
-
return true;
|
|
541
|
-
}
|
|
542
|
-
return 'Select at least one environment, or choose "Add custom environment".';
|
|
543
|
-
}
|
|
544
|
-
function buildEnvironmentChoices(names, selected) {
|
|
545
|
-
return [
|
|
546
|
-
...names.map((name) => ({
|
|
547
|
-
value: name,
|
|
548
|
-
name,
|
|
549
|
-
checked: selected.has(name)
|
|
550
|
-
})),
|
|
551
|
-
new Separator(),
|
|
552
|
-
{
|
|
553
|
-
value: ADD_CUSTOM_ENVIRONMENT,
|
|
554
|
-
name: "Add custom environment",
|
|
555
|
-
description: "Create another environment name and return to this menu"
|
|
556
|
-
}
|
|
557
|
-
];
|
|
558
|
-
}
|
|
559
|
-
function validateCustomEnvironmentName(value) {
|
|
560
|
-
const trimmed = value.trim();
|
|
561
|
-
if (trimmed.length === 0) {
|
|
562
|
-
return true;
|
|
563
|
-
}
|
|
564
|
-
try {
|
|
565
|
-
assertValidEnvName(trimmed);
|
|
566
|
-
return true;
|
|
567
|
-
} catch (err) {
|
|
568
|
-
return err instanceof Error ? err.message : String(err);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
async function promptForEnvironments(opts, deps = {}) {
|
|
572
|
-
const checkboxPrompt = deps.checkboxPrompt ?? checkbox;
|
|
573
|
-
const inputPrompt = deps.inputPrompt ?? input;
|
|
574
|
-
const selected = new Set(opts.existing);
|
|
575
|
-
const customNames = [];
|
|
576
|
-
for (; ; ) {
|
|
577
|
-
const names = uniqueNames([...opts.common, ...opts.existing, ...customNames]);
|
|
578
|
-
const answers = await checkboxPrompt({
|
|
579
|
-
message: "Environments to create (space to toggle, enter to continue)",
|
|
580
|
-
choices: buildEnvironmentChoices(names, selected),
|
|
581
|
-
validate: validateEnvironmentSelection
|
|
582
|
-
});
|
|
583
|
-
selected.clear();
|
|
584
|
-
for (const name of answers) {
|
|
585
|
-
if (name !== ADD_CUSTOM_ENVIRONMENT) {
|
|
586
|
-
selected.add(name);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
if (!answers.includes(ADD_CUSTOM_ENVIRONMENT)) {
|
|
590
|
-
return [...selected];
|
|
591
|
-
}
|
|
592
|
-
const custom = (await inputPrompt({
|
|
593
|
-
message: "Custom environment name (leave empty to go back)",
|
|
594
|
-
default: "",
|
|
595
|
-
validate: validateCustomEnvironmentName
|
|
596
|
-
})).trim();
|
|
597
|
-
if (custom.length === 0) {
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
if (!customNames.includes(custom) && !names.includes(custom)) {
|
|
601
|
-
customNames.push(custom);
|
|
602
|
-
}
|
|
603
|
-
selected.add(custom);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// src/run.ts
|
|
608
|
-
import { spawn } from "child_process";
|
|
609
|
-
import { readFile as readFile4, stat, writeFile as writeFile4 } from "fs/promises";
|
|
610
|
-
import { createRequire } from "module";
|
|
611
|
-
import { delimiter, dirname as dirname2, isAbsolute, join as join4, relative, resolve, sep } from "path";
|
|
612
|
-
import { getTokenCached as getTokenCachedApi } from "@saptools/cf-xsuaa";
|
|
613
|
-
|
|
614
|
-
// src/folder-scan.ts
|
|
615
|
-
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
616
|
-
import { join as join3 } from "path";
|
|
617
|
-
async function safeReaddir(path) {
|
|
618
|
-
try {
|
|
619
|
-
const entries = await readdir2(path, { withFileTypes: true });
|
|
620
|
-
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
621
|
-
} catch {
|
|
622
|
-
return [];
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
async function listFiles(path) {
|
|
626
|
-
try {
|
|
627
|
-
const entries = await readdir2(path, { withFileTypes: true });
|
|
231
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
628
232
|
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
629
233
|
} catch {
|
|
630
234
|
return [];
|
|
631
235
|
}
|
|
632
236
|
}
|
|
633
237
|
async function loadEnvFile(path, name) {
|
|
634
|
-
const raw = await
|
|
238
|
+
const raw = await readFile2(path, "utf8");
|
|
635
239
|
const parsed = parseBruEnvFile(raw);
|
|
636
240
|
return {
|
|
637
241
|
path,
|
|
@@ -642,17 +246,17 @@ async function loadEnvFile(path, name) {
|
|
|
642
246
|
};
|
|
643
247
|
}
|
|
644
248
|
async function scanAppEnvironments(appPath) {
|
|
645
|
-
const envDir =
|
|
249
|
+
const envDir = join2(appPath, ENVIRONMENTS_DIR);
|
|
646
250
|
const files = await listFiles(envDir);
|
|
647
251
|
const bruFiles = files.filter((f) => f.endsWith(".bru"));
|
|
648
252
|
const loaded = [];
|
|
649
253
|
for (const file of bruFiles) {
|
|
650
|
-
loaded.push(await loadEnvFile(
|
|
254
|
+
loaded.push(await loadEnvFile(join2(envDir, file), file));
|
|
651
255
|
}
|
|
652
256
|
return loaded;
|
|
653
257
|
}
|
|
654
258
|
async function scanApp(spacePath, name) {
|
|
655
|
-
const appPath =
|
|
259
|
+
const appPath = join2(spacePath, name);
|
|
656
260
|
const environments = await scanAppEnvironments(appPath);
|
|
657
261
|
return { path: appPath, name, environments };
|
|
658
262
|
}
|
|
@@ -661,7 +265,7 @@ async function scanSpace(orgPath, dirName) {
|
|
|
661
265
|
if (name === void 0) {
|
|
662
266
|
return void 0;
|
|
663
267
|
}
|
|
664
|
-
const spacePath =
|
|
268
|
+
const spacePath = join2(orgPath, dirName);
|
|
665
269
|
const appDirs = await safeReaddir(spacePath);
|
|
666
270
|
const apps = [];
|
|
667
271
|
for (const appDir of appDirs) {
|
|
@@ -674,7 +278,7 @@ async function scanOrg(regionPath, dirName) {
|
|
|
674
278
|
if (name === void 0) {
|
|
675
279
|
return void 0;
|
|
676
280
|
}
|
|
677
|
-
const orgPath =
|
|
281
|
+
const orgPath = join2(regionPath, dirName);
|
|
678
282
|
const spaceDirs = await safeReaddir(orgPath);
|
|
679
283
|
const spaces = [];
|
|
680
284
|
for (const spaceDir of spaceDirs) {
|
|
@@ -690,7 +294,7 @@ async function scanRegion(root, dirName) {
|
|
|
690
294
|
if (key === void 0) {
|
|
691
295
|
return void 0;
|
|
692
296
|
}
|
|
693
|
-
const regionPath =
|
|
297
|
+
const regionPath = join2(root, dirName);
|
|
694
298
|
const orgDirs = await safeReaddir(regionPath);
|
|
695
299
|
const orgs = [];
|
|
696
300
|
for (const orgDir of orgDirs) {
|
|
@@ -731,7 +335,7 @@ function parseShorthandPath(shorthand) {
|
|
|
731
335
|
return environment ? { region, org, space, app, environment, filePath } : { region, org, space, app, filePath };
|
|
732
336
|
}
|
|
733
337
|
|
|
734
|
-
// src/run.ts
|
|
338
|
+
// src/commands/run.ts
|
|
735
339
|
var require2 = createRequire(import.meta.url);
|
|
736
340
|
function pathEntries(env) {
|
|
737
341
|
const value = env["PATH"] ?? process.env["PATH"] ?? "";
|
|
@@ -748,7 +352,7 @@ async function findCommandOnPath(command, env) {
|
|
|
748
352
|
const candidates = pathCandidates(command, env);
|
|
749
353
|
for (const entry of pathEntries(env)) {
|
|
750
354
|
for (const candidate of candidates) {
|
|
751
|
-
const fullPath =
|
|
355
|
+
const fullPath = join3(entry, candidate);
|
|
752
356
|
if (await exists(fullPath)) {
|
|
753
357
|
return fullPath;
|
|
754
358
|
}
|
|
@@ -777,7 +381,7 @@ function defaultResolvePackageJsonPath() {
|
|
|
777
381
|
return require2.resolve("@usebruno/cli/package.json");
|
|
778
382
|
}
|
|
779
383
|
async function defaultReadTextFile(path) {
|
|
780
|
-
return await
|
|
384
|
+
return await readFile3(path, "utf8");
|
|
781
385
|
}
|
|
782
386
|
async function resolveBundledBruBinPath(deps) {
|
|
783
387
|
try {
|
|
@@ -787,7 +391,7 @@ async function resolveBundledBruBinPath(deps) {
|
|
|
787
391
|
if (!binPath) {
|
|
788
392
|
return void 0;
|
|
789
393
|
}
|
|
790
|
-
return resolve(
|
|
394
|
+
return resolve(dirname(packageJsonPath), binPath);
|
|
791
395
|
} catch {
|
|
792
396
|
return void 0;
|
|
793
397
|
}
|
|
@@ -843,7 +447,7 @@ async function resolveTarget(root, target) {
|
|
|
843
447
|
throw new Error(`Target not found: ${target}`);
|
|
844
448
|
}
|
|
845
449
|
const { region, org, space, app, filePath } = shorthand;
|
|
846
|
-
const appDir =
|
|
450
|
+
const appDir = join3(
|
|
847
451
|
root,
|
|
848
452
|
regionFolderName(region),
|
|
849
453
|
orgFolderName(org),
|
|
@@ -853,7 +457,7 @@ async function resolveTarget(root, target) {
|
|
|
853
457
|
if (!filePath) {
|
|
854
458
|
return { filePath: appDir, shorthand };
|
|
855
459
|
}
|
|
856
|
-
const candidate =
|
|
460
|
+
const candidate = join3(appDir, filePath);
|
|
857
461
|
if (await exists(candidate)) {
|
|
858
462
|
return { filePath: candidate, shorthand };
|
|
859
463
|
}
|
|
@@ -865,7 +469,7 @@ async function resolveTarget(root, target) {
|
|
|
865
469
|
}
|
|
866
470
|
async function chooseEnvironmentFile(appDir, environment) {
|
|
867
471
|
if (environment) {
|
|
868
|
-
const envFile =
|
|
472
|
+
const envFile = join3(appDir, ENVIRONMENTS_DIR, `${environment}.bru`);
|
|
869
473
|
if (!await exists(envFile)) {
|
|
870
474
|
throw new Error(`Environment file not found: ${envFile}`);
|
|
871
475
|
}
|
|
@@ -897,13 +501,13 @@ function findAppDirFromFile(filePath, root) {
|
|
|
897
501
|
if (!regionDir || !orgDir || !spaceDir || !appDir) {
|
|
898
502
|
throw new Error(`File is not inside a CF-structured bruno collection: ${filePath}`);
|
|
899
503
|
}
|
|
900
|
-
return
|
|
504
|
+
return join3(root, regionDir, orgDir, spaceDir, appDir);
|
|
901
505
|
}
|
|
902
506
|
async function persistAccessToken(envFile, token) {
|
|
903
|
-
const raw = await
|
|
507
|
+
const raw = await readFile3(envFile, "utf8");
|
|
904
508
|
const { content, changed } = upsertVars(raw, /* @__PURE__ */ new Map([["accessToken", token]]));
|
|
905
509
|
if (changed) {
|
|
906
|
-
await
|
|
510
|
+
await writeFile2(envFile, content, "utf8");
|
|
907
511
|
}
|
|
908
512
|
}
|
|
909
513
|
async function buildRunPlan(options) {
|
|
@@ -955,20 +559,308 @@ async function runBruno(options) {
|
|
|
955
559
|
return { ...plan, ...result };
|
|
956
560
|
}
|
|
957
561
|
|
|
958
|
-
// src/
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
562
|
+
// src/commands/setup-app.ts
|
|
563
|
+
import { mkdir, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
|
|
564
|
+
import { basename, join as join4 } from "path";
|
|
565
|
+
|
|
566
|
+
// src/cf/info.ts
|
|
567
|
+
import {
|
|
568
|
+
getRegionView as getRegionViewApi,
|
|
569
|
+
readRegionsView,
|
|
570
|
+
readRegionView,
|
|
571
|
+
readStructureView,
|
|
572
|
+
REGION_KEYS
|
|
573
|
+
} from "@saptools/cf-sync";
|
|
574
|
+
var defaultCfInfoDeps = {
|
|
575
|
+
readStructureView,
|
|
576
|
+
readRegionsView,
|
|
577
|
+
readRegionView,
|
|
578
|
+
getRegionView: getRegionViewApi
|
|
579
|
+
};
|
|
580
|
+
function isValidRegionKey(value) {
|
|
581
|
+
return REGION_KEYS.includes(value);
|
|
582
|
+
}
|
|
583
|
+
async function getStructureSnapshot(deps = defaultCfInfoDeps) {
|
|
584
|
+
const view = await deps.readStructureView();
|
|
585
|
+
if (!view) {
|
|
586
|
+
return {
|
|
587
|
+
source: "empty",
|
|
588
|
+
structure: void 0,
|
|
589
|
+
stale: true,
|
|
590
|
+
message: "No CF structure cached. Run `cf-sync sync` first."
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const stale = view.source === "runtime" && view.metadata?.status === "running";
|
|
594
|
+
return {
|
|
595
|
+
source: view.source,
|
|
596
|
+
structure: view.structure,
|
|
597
|
+
stale,
|
|
598
|
+
message: stale ? "A CF sync is still running \u2014 showing partial data." : void 0
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async function listRegionsWithContent(deps = defaultCfInfoDeps) {
|
|
602
|
+
const snapshot = await getStructureSnapshot(deps);
|
|
603
|
+
if (!snapshot.structure) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
return snapshot.structure.regions.filter((r) => r.accessible && r.orgs.length > 0).map((r) => ({ key: r.key, label: r.label, orgCount: r.orgs.length }));
|
|
607
|
+
}
|
|
608
|
+
async function getRegion(key, deps = defaultCfInfoDeps) {
|
|
609
|
+
const view = await deps.readRegionView(key);
|
|
610
|
+
return view?.region;
|
|
611
|
+
}
|
|
612
|
+
function findOrg(region, orgName) {
|
|
613
|
+
return region.orgs.find((o) => o.name === orgName);
|
|
614
|
+
}
|
|
615
|
+
function findSpace(org, spaceName) {
|
|
616
|
+
return org.spaces.find((s) => s.name === spaceName);
|
|
617
|
+
}
|
|
618
|
+
function findApp(space, appName) {
|
|
619
|
+
return space.apps.find((a) => a.name === appName);
|
|
620
|
+
}
|
|
621
|
+
async function resolveRef(ref, deps = defaultCfInfoDeps) {
|
|
622
|
+
const region = await getRegion(ref.region, deps);
|
|
623
|
+
if (!region) {
|
|
962
624
|
return void 0;
|
|
963
625
|
}
|
|
964
|
-
const
|
|
965
|
-
if (!
|
|
626
|
+
const org = findOrg(region, ref.org);
|
|
627
|
+
if (!org) {
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
630
|
+
const space = findSpace(org, ref.space);
|
|
631
|
+
if (!space) {
|
|
632
|
+
return void 0;
|
|
633
|
+
}
|
|
634
|
+
const app = findApp(space, ref.app);
|
|
635
|
+
if (!app) {
|
|
966
636
|
return void 0;
|
|
967
637
|
}
|
|
968
638
|
return { region, org, space, app };
|
|
969
639
|
}
|
|
970
|
-
|
|
971
|
-
|
|
640
|
+
|
|
641
|
+
// src/commands/setup-app.ts
|
|
642
|
+
var COMMON_ENVIRONMENTS = ["local", "dev", "staging", "prod"];
|
|
643
|
+
var BRUNO_COLLECTION_CONFIG_FILENAME = "bruno.json";
|
|
644
|
+
var ENV_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
645
|
+
function assertValidEnvName(name) {
|
|
646
|
+
if (!ENV_NAME_PATTERN.test(name)) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Invalid environment name '${name}': only letters, digits, dot, underscore, and dash are allowed.`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function emptyEnvContent(envName, ref) {
|
|
653
|
+
const lines = [
|
|
654
|
+
"vars {",
|
|
655
|
+
` __cf_region: ${ref.region}`,
|
|
656
|
+
` __cf_org: ${ref.org}`,
|
|
657
|
+
` __cf_space: ${ref.space}`,
|
|
658
|
+
` __cf_app: ${ref.app}`,
|
|
659
|
+
` environment: ${envName}`,
|
|
660
|
+
" baseUrl: ",
|
|
661
|
+
"}",
|
|
662
|
+
""
|
|
663
|
+
];
|
|
664
|
+
return lines.join("\n");
|
|
665
|
+
}
|
|
666
|
+
function normalizeCollectionName(root) {
|
|
667
|
+
const candidate = basename(root).replace(/^\.+/, "").trim();
|
|
668
|
+
return candidate.length > 0 ? candidate : "bruno-collection";
|
|
669
|
+
}
|
|
670
|
+
function defaultBrunoConfig(root) {
|
|
671
|
+
return `${JSON.stringify(
|
|
672
|
+
{
|
|
673
|
+
version: "1",
|
|
674
|
+
name: normalizeCollectionName(root),
|
|
675
|
+
type: "collection",
|
|
676
|
+
ignore: ["node_modules", ".git"]
|
|
677
|
+
},
|
|
678
|
+
null,
|
|
679
|
+
2
|
|
680
|
+
)}
|
|
681
|
+
`;
|
|
682
|
+
}
|
|
683
|
+
async function ensureCollectionConfig(root) {
|
|
684
|
+
const filePath = join4(root, BRUNO_COLLECTION_CONFIG_FILENAME);
|
|
685
|
+
try {
|
|
686
|
+
await writeFile3(filePath, defaultBrunoConfig(root), { encoding: "utf8", flag: "wx" });
|
|
687
|
+
} catch (err) {
|
|
688
|
+
if (err.code !== "EEXIST") {
|
|
689
|
+
throw err;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async function ensureEnvFile(appPath, envName, ref) {
|
|
694
|
+
const envDir = join4(appPath, ENVIRONMENTS_DIR);
|
|
695
|
+
await mkdir(envDir, { recursive: true });
|
|
696
|
+
const filePath = join4(envDir, `${envName}.bru`);
|
|
697
|
+
try {
|
|
698
|
+
await writeFile3(filePath, emptyEnvContent(envName, ref), { encoding: "utf8", flag: "wx" });
|
|
699
|
+
} catch (err) {
|
|
700
|
+
if (err.code !== "EEXIST") {
|
|
701
|
+
throw err;
|
|
702
|
+
}
|
|
703
|
+
await writeCfMetaToFile(filePath, ref);
|
|
704
|
+
}
|
|
705
|
+
return filePath;
|
|
706
|
+
}
|
|
707
|
+
function pickRegion(regions) {
|
|
708
|
+
return regions.map((r) => ({ value: r.key, name: `${r.key} \u2014 ${r.label} (${r.orgCount.toString()} org${r.orgCount === 1 ? "" : "s"})` }));
|
|
709
|
+
}
|
|
710
|
+
function pickOrg(region) {
|
|
711
|
+
return region.orgs.map((o) => ({ value: o.name, name: `${o.name} (${o.spaces.length.toString()} space${o.spaces.length === 1 ? "" : "s"})` }));
|
|
712
|
+
}
|
|
713
|
+
function pickSpace(org) {
|
|
714
|
+
return org.spaces.map((s) => ({ value: s.name, name: `${s.name} (${s.apps.length.toString()} app${s.apps.length === 1 ? "" : "s"})` }));
|
|
715
|
+
}
|
|
716
|
+
function pickApp(space) {
|
|
717
|
+
return space.apps.map((a) => ({ value: a.name, name: a.name }));
|
|
718
|
+
}
|
|
719
|
+
async function selectRegion(prompts, deps) {
|
|
720
|
+
const regions = await listRegionsWithContent(deps);
|
|
721
|
+
if (regions.length === 0) {
|
|
722
|
+
throw new Error("No CF regions with orgs are cached. Run `cf-sync sync` first.");
|
|
723
|
+
}
|
|
724
|
+
const regionKey = await prompts.selectRegion(pickRegion(regions));
|
|
725
|
+
const regionView = await deps.readRegionView(regionKey);
|
|
726
|
+
if (!regionView) {
|
|
727
|
+
throw new Error(`Region ${regionKey} is not cached. Run \`cf-sync region ${regionKey}\` or \`cf-sync sync\` first.`);
|
|
728
|
+
}
|
|
729
|
+
const region = regionView.region;
|
|
730
|
+
if (region.orgs.length === 0) {
|
|
731
|
+
throw new Error(`Region ${regionKey} has no accessible orgs.`);
|
|
732
|
+
}
|
|
733
|
+
return { regionKey, region };
|
|
734
|
+
}
|
|
735
|
+
async function selectOrg(prompts, region, regionKey) {
|
|
736
|
+
const orgName = await prompts.selectOrg(pickOrg(region));
|
|
737
|
+
const org = region.orgs.find((o) => o.name === orgName);
|
|
738
|
+
if (!org) {
|
|
739
|
+
throw new Error(`Org ${orgName} not found in region ${regionKey}`);
|
|
740
|
+
}
|
|
741
|
+
if (org.spaces.length === 0) {
|
|
742
|
+
throw new Error(`Org ${orgName} has no spaces.`);
|
|
743
|
+
}
|
|
744
|
+
return { orgName, org };
|
|
745
|
+
}
|
|
746
|
+
async function selectSpace(prompts, org, orgName) {
|
|
747
|
+
const spaceName = await prompts.selectSpace(pickSpace(org));
|
|
748
|
+
const space = org.spaces.find((s) => s.name === spaceName);
|
|
749
|
+
if (!space) {
|
|
750
|
+
throw new Error(`Space ${spaceName} not found in org ${orgName}`);
|
|
751
|
+
}
|
|
752
|
+
if (space.apps.length === 0) {
|
|
753
|
+
throw new Error(`Space ${spaceName} has no apps.`);
|
|
754
|
+
}
|
|
755
|
+
return { spaceName, space };
|
|
756
|
+
}
|
|
757
|
+
async function selectCfAppRef(prompts, deps) {
|
|
758
|
+
const { regionKey, region } = await selectRegion(prompts, deps);
|
|
759
|
+
const { orgName, org } = await selectOrg(prompts, region, regionKey);
|
|
760
|
+
const { spaceName, space } = await selectSpace(prompts, org, orgName);
|
|
761
|
+
const appName = await prompts.selectApp(pickApp(space));
|
|
762
|
+
return { region: regionKey, org: orgName, space: spaceName, app: appName };
|
|
763
|
+
}
|
|
764
|
+
function appPathFor(root, ref) {
|
|
765
|
+
return join4(
|
|
766
|
+
root,
|
|
767
|
+
regionFolderName(ref.region),
|
|
768
|
+
orgFolderName(ref.org),
|
|
769
|
+
spaceFolderName(ref.space),
|
|
770
|
+
ref.app
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
function normalizeEnvironmentNames(selected) {
|
|
774
|
+
const merged = [];
|
|
775
|
+
for (const name of selected) {
|
|
776
|
+
const trimmed = name.trim();
|
|
777
|
+
if (trimmed.length === 0 || merged.includes(trimmed)) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
assertValidEnvName(trimmed);
|
|
781
|
+
merged.push(trimmed);
|
|
782
|
+
}
|
|
783
|
+
if (merged.length === 0) {
|
|
784
|
+
throw new Error("At least one environment is required.");
|
|
785
|
+
}
|
|
786
|
+
return merged;
|
|
787
|
+
}
|
|
788
|
+
async function selectEnvironmentNames(appPath, prompts) {
|
|
789
|
+
const existingEnvs = await listExistingEnvs(appPath);
|
|
790
|
+
const common = [...COMMON_ENVIRONMENTS];
|
|
791
|
+
const selected = await prompts.selectEnvironments({ common, existing: existingEnvs });
|
|
792
|
+
return normalizeEnvironmentNames(selected);
|
|
793
|
+
}
|
|
794
|
+
async function createEnvironmentFiles(appPath, envNames, ref, log) {
|
|
795
|
+
const created = [];
|
|
796
|
+
for (const envName of envNames) {
|
|
797
|
+
const path = await ensureEnvFile(appPath, envName, ref);
|
|
798
|
+
created.push(path);
|
|
799
|
+
log(`\u2022 ${path}`);
|
|
800
|
+
}
|
|
801
|
+
return created;
|
|
802
|
+
}
|
|
803
|
+
async function setupApp(options) {
|
|
804
|
+
const deps = options.deps ?? defaultCfInfoDeps;
|
|
805
|
+
const log = options.log ?? (() => void 0);
|
|
806
|
+
const ref = await selectCfAppRef(options.prompts, deps);
|
|
807
|
+
const appPath = appPathFor(options.root, ref);
|
|
808
|
+
const confirmed = await options.prompts.confirmCreate(appPath);
|
|
809
|
+
if (!confirmed) {
|
|
810
|
+
return { ref, appPath, environments: [], created: false };
|
|
811
|
+
}
|
|
812
|
+
await mkdir(appPath, { recursive: true });
|
|
813
|
+
await ensureCollectionConfig(appPath);
|
|
814
|
+
const envNames = await selectEnvironmentNames(appPath, options.prompts);
|
|
815
|
+
const created = await createEnvironmentFiles(appPath, envNames, ref, log);
|
|
816
|
+
return { ref, appPath, environments: created, created: true };
|
|
817
|
+
}
|
|
818
|
+
async function listExistingEnvs(appPath) {
|
|
819
|
+
try {
|
|
820
|
+
const entries = await readdir2(join4(appPath, ENVIRONMENTS_DIR), { withFileTypes: true });
|
|
821
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".bru")).map((e) => e.name.replace(/\.bru$/, ""));
|
|
822
|
+
} catch {
|
|
823
|
+
return [];
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/state/context.ts
|
|
828
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
829
|
+
import { dirname as dirname2 } from "path";
|
|
830
|
+
async function readContext() {
|
|
831
|
+
try {
|
|
832
|
+
const raw = await readFile4(brunoContextPath(), "utf8");
|
|
833
|
+
return JSON.parse(raw);
|
|
834
|
+
} catch (err) {
|
|
835
|
+
if (err.code === "ENOENT") {
|
|
836
|
+
return void 0;
|
|
837
|
+
}
|
|
838
|
+
throw err;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async function writeContext(ctx) {
|
|
842
|
+
const updated = { ...ctx, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
843
|
+
const path = brunoContextPath();
|
|
844
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
845
|
+
await writeFile4(path, `${JSON.stringify(updated, null, 2)}
|
|
846
|
+
`, "utf8");
|
|
847
|
+
return updated;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/commands/use.ts
|
|
851
|
+
function parseContextShorthand(shorthand) {
|
|
852
|
+
const segs = shorthand.split("/").filter((s) => s.length > 0);
|
|
853
|
+
if (segs.length !== 4) {
|
|
854
|
+
return void 0;
|
|
855
|
+
}
|
|
856
|
+
const [region, org, space, app] = segs;
|
|
857
|
+
if (!region || !org || !space || !app) {
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
860
|
+
return { region, org, space, app };
|
|
861
|
+
}
|
|
862
|
+
async function useContext(options) {
|
|
863
|
+
const parsed = parseContextShorthand(options.shorthand);
|
|
972
864
|
if (!parsed) {
|
|
973
865
|
throw new Error(
|
|
974
866
|
`Invalid context shorthand: ${options.shorthand}. Expected <region>/<org>/<space>/<app>.`
|
|
@@ -988,7 +880,140 @@ async function useContext(options) {
|
|
|
988
880
|
return await writeContext(parsed);
|
|
989
881
|
}
|
|
990
882
|
|
|
991
|
-
// src/
|
|
883
|
+
// src/prompts/app-search.ts
|
|
884
|
+
import { search } from "@inquirer/prompts";
|
|
885
|
+
var DEFAULT_PAGE_SIZE = 12;
|
|
886
|
+
var NO_MATCHING_APP = "__saptools_no_matching_app__";
|
|
887
|
+
function normalizeTerm(term) {
|
|
888
|
+
return term?.trim().toLowerCase() ?? "";
|
|
889
|
+
}
|
|
890
|
+
function scoreChoice(choice, normalizedTerm) {
|
|
891
|
+
const name = choice.name.toLowerCase();
|
|
892
|
+
const value = choice.value.toLowerCase();
|
|
893
|
+
if (name === normalizedTerm || value === normalizedTerm) {
|
|
894
|
+
return 0;
|
|
895
|
+
}
|
|
896
|
+
if (name.startsWith(normalizedTerm) || value.startsWith(normalizedTerm)) {
|
|
897
|
+
return 1;
|
|
898
|
+
}
|
|
899
|
+
if (name.includes(normalizedTerm) || value.includes(normalizedTerm)) {
|
|
900
|
+
return 2;
|
|
901
|
+
}
|
|
902
|
+
return Number.POSITIVE_INFINITY;
|
|
903
|
+
}
|
|
904
|
+
function noMatchChoice(term) {
|
|
905
|
+
const label = term?.trim() ?? "";
|
|
906
|
+
return {
|
|
907
|
+
value: NO_MATCHING_APP,
|
|
908
|
+
name: `No apps match "${label}"`,
|
|
909
|
+
disabled: "Type a different search term"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function buildAppSearchChoices(choices, term) {
|
|
913
|
+
const normalizedTerm = normalizeTerm(term);
|
|
914
|
+
if (normalizedTerm.length === 0) {
|
|
915
|
+
return [...choices];
|
|
916
|
+
}
|
|
917
|
+
const rankedMatches = choices.map((choice, index) => ({ choice, index, score: scoreChoice(choice, normalizedTerm) })).filter((item) => Number.isFinite(item.score)).sort((left, right) => left.score - right.score || left.index - right.index).map((item) => item.choice);
|
|
918
|
+
if (rankedMatches.length > 0) {
|
|
919
|
+
return rankedMatches;
|
|
920
|
+
}
|
|
921
|
+
return [noMatchChoice(term)];
|
|
922
|
+
}
|
|
923
|
+
async function promptForAppSelection(choices, deps = {}) {
|
|
924
|
+
const searchPrompt = deps.searchPrompt ?? search;
|
|
925
|
+
return await searchPrompt({
|
|
926
|
+
message: "Select app",
|
|
927
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
928
|
+
source: (term) => Promise.resolve(buildAppSearchChoices(choices, term)),
|
|
929
|
+
validate: (value) => value === NO_MATCHING_APP ? "Select a real app." : true
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/prompts/environment.ts
|
|
934
|
+
import { Separator, checkbox, input } from "@inquirer/prompts";
|
|
935
|
+
var ADD_CUSTOM_ENVIRONMENT = "__saptools_add_custom_environment__";
|
|
936
|
+
function uniqueNames(names) {
|
|
937
|
+
const merged = [];
|
|
938
|
+
for (const name of names) {
|
|
939
|
+
if (!merged.includes(name)) {
|
|
940
|
+
merged.push(name);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return merged;
|
|
944
|
+
}
|
|
945
|
+
function validateEnvironmentSelection(choices) {
|
|
946
|
+
const selected = choices.map((choice) => choice.value);
|
|
947
|
+
const hasEnvironment = selected.some((value) => value !== ADD_CUSTOM_ENVIRONMENT);
|
|
948
|
+
if (hasEnvironment || selected.includes(ADD_CUSTOM_ENVIRONMENT)) {
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
return 'Select at least one environment, or choose "Add custom environment".';
|
|
952
|
+
}
|
|
953
|
+
function buildEnvironmentChoices(names, selected) {
|
|
954
|
+
return [
|
|
955
|
+
...names.map((name) => ({
|
|
956
|
+
value: name,
|
|
957
|
+
name,
|
|
958
|
+
checked: selected.has(name)
|
|
959
|
+
})),
|
|
960
|
+
new Separator(),
|
|
961
|
+
{
|
|
962
|
+
value: ADD_CUSTOM_ENVIRONMENT,
|
|
963
|
+
name: "Add custom environment",
|
|
964
|
+
description: "Create another environment name and return to this menu"
|
|
965
|
+
}
|
|
966
|
+
];
|
|
967
|
+
}
|
|
968
|
+
function validateCustomEnvironmentName(value) {
|
|
969
|
+
const trimmed = value.trim();
|
|
970
|
+
if (trimmed.length === 0) {
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
assertValidEnvName(trimmed);
|
|
975
|
+
return true;
|
|
976
|
+
} catch (err) {
|
|
977
|
+
return err instanceof Error ? err.message : String(err);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async function promptForEnvironments(opts, deps = {}) {
|
|
981
|
+
const checkboxPrompt = deps.checkboxPrompt ?? checkbox;
|
|
982
|
+
const inputPrompt = deps.inputPrompt ?? input;
|
|
983
|
+
const selected = new Set(opts.existing);
|
|
984
|
+
const customNames = [];
|
|
985
|
+
for (; ; ) {
|
|
986
|
+
const names = uniqueNames([...opts.common, ...opts.existing, ...customNames]);
|
|
987
|
+
const answers = await checkboxPrompt({
|
|
988
|
+
message: "Environments to create (space to toggle, enter to continue)",
|
|
989
|
+
choices: buildEnvironmentChoices(names, selected),
|
|
990
|
+
validate: validateEnvironmentSelection
|
|
991
|
+
});
|
|
992
|
+
selected.clear();
|
|
993
|
+
for (const name of answers) {
|
|
994
|
+
if (name !== ADD_CUSTOM_ENVIRONMENT) {
|
|
995
|
+
selected.add(name);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (!answers.includes(ADD_CUSTOM_ENVIRONMENT)) {
|
|
999
|
+
return [...selected];
|
|
1000
|
+
}
|
|
1001
|
+
const custom = (await inputPrompt({
|
|
1002
|
+
message: "Custom environment name (leave empty to go back)",
|
|
1003
|
+
default: "",
|
|
1004
|
+
validate: validateCustomEnvironmentName
|
|
1005
|
+
})).trim();
|
|
1006
|
+
if (custom.length === 0) {
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (!customNames.includes(custom) && !names.includes(custom)) {
|
|
1010
|
+
customNames.push(custom);
|
|
1011
|
+
}
|
|
1012
|
+
selected.add(custom);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/cli/main.ts
|
|
992
1017
|
function resolveCollectionDir(explicitCollection, explicitRoot) {
|
|
993
1018
|
if (explicitCollection) {
|
|
994
1019
|
return explicitCollection;
|
|
@@ -1004,16 +1029,18 @@ function resolveCollectionDir(explicitCollection, explicitRoot) {
|
|
|
1004
1029
|
}
|
|
1005
1030
|
return process2.cwd();
|
|
1006
1031
|
}
|
|
1007
|
-
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1032
|
+
function resolveProgramCollectionDir(program) {
|
|
1033
|
+
const opts = program.opts();
|
|
1034
|
+
return resolveCollectionDir(opts.collection, opts.root);
|
|
1035
|
+
}
|
|
1036
|
+
function writeLine(message) {
|
|
1037
|
+
process2.stdout.write(`${message}
|
|
1038
|
+
`);
|
|
1039
|
+
}
|
|
1040
|
+
function registerSetupAppCommand(program) {
|
|
1010
1041
|
program.command("setup-app").description("Interactively scaffold a bruno app folder and seed __cf_* variables").action(async () => {
|
|
1011
|
-
const collectionDir = resolveCollectionDir(
|
|
1012
|
-
program.opts().collection,
|
|
1013
|
-
program.opts().root
|
|
1014
|
-
);
|
|
1015
1042
|
const result = await setupApp({
|
|
1016
|
-
root:
|
|
1043
|
+
root: resolveProgramCollectionDir(program),
|
|
1017
1044
|
prompts: {
|
|
1018
1045
|
selectRegion: async (choices) => await select({ message: "Select region", choices: [...choices] }),
|
|
1019
1046
|
selectOrg: async (choices) => await select({ message: "Select org", choices: [...choices] }),
|
|
@@ -1022,46 +1049,41 @@ async function main(argv) {
|
|
|
1022
1049
|
confirmCreate: async (path) => await confirm({ message: `Create ${path}?`, default: true }),
|
|
1023
1050
|
selectEnvironments: async (opts) => await promptForEnvironments(opts)
|
|
1024
1051
|
},
|
|
1025
|
-
log:
|
|
1026
|
-
process2.stdout.write(`${msg}
|
|
1027
|
-
`);
|
|
1028
|
-
}
|
|
1052
|
+
log: writeLine
|
|
1029
1053
|
});
|
|
1030
1054
|
if (!result.created) {
|
|
1031
|
-
|
|
1055
|
+
writeLine("Aborted.");
|
|
1032
1056
|
return;
|
|
1033
1057
|
}
|
|
1034
|
-
|
|
1035
|
-
`);
|
|
1058
|
+
writeLine(`\u2714 App folder ready at ${result.appPath}`);
|
|
1036
1059
|
});
|
|
1060
|
+
}
|
|
1061
|
+
async function resolveRunTarget(target) {
|
|
1062
|
+
if (target) {
|
|
1063
|
+
return target;
|
|
1064
|
+
}
|
|
1065
|
+
const ctx = await readContext();
|
|
1066
|
+
if (!ctx) {
|
|
1067
|
+
throw new Error(
|
|
1068
|
+
"No target specified and no default context is set. Run `saptools-bruno use <region/org/space/app>` first."
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
return `${ctx.region}/${ctx.org}/${ctx.space}/${ctx.app}`;
|
|
1072
|
+
}
|
|
1073
|
+
function registerRunCommand(program) {
|
|
1037
1074
|
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(
|
|
1038
1075
|
async (target, opts) => {
|
|
1039
|
-
const collectionDir = resolveCollectionDir(
|
|
1040
|
-
program.opts().collection,
|
|
1041
|
-
program.opts().root
|
|
1042
|
-
);
|
|
1043
|
-
let effectiveTarget = target;
|
|
1044
|
-
if (!effectiveTarget) {
|
|
1045
|
-
const ctx = await readContext();
|
|
1046
|
-
if (!ctx) {
|
|
1047
|
-
throw new Error(
|
|
1048
|
-
"No target specified and no default context is set. Run `saptools-bruno use <region/org/space/app>` first."
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
effectiveTarget = `${ctx.region}/${ctx.org}/${ctx.space}/${ctx.app}`;
|
|
1052
|
-
}
|
|
1053
1076
|
const result = await runBruno({
|
|
1054
|
-
root:
|
|
1055
|
-
target:
|
|
1077
|
+
root: resolveProgramCollectionDir(program),
|
|
1078
|
+
target: await resolveRunTarget(target),
|
|
1056
1079
|
...opts.env ? { environment: opts.env } : {},
|
|
1057
|
-
log:
|
|
1058
|
-
process2.stdout.write(`${msg}
|
|
1059
|
-
`);
|
|
1060
|
-
}
|
|
1080
|
+
log: writeLine
|
|
1061
1081
|
});
|
|
1062
1082
|
process2.exit(result.code);
|
|
1063
1083
|
}
|
|
1064
1084
|
);
|
|
1085
|
+
}
|
|
1086
|
+
function registerUseCommand(program) {
|
|
1065
1087
|
program.command("use").description("Set the default CF context (region/org/space/app) for future `run` calls").argument("<shorthand>", "region/org/space/app").option("--no-verify", "Skip verifying the context against the cached CF structure").action(async (shorthand, opts) => {
|
|
1066
1088
|
const ctx = await useContext({
|
|
1067
1089
|
shorthand,
|
|
@@ -1070,6 +1092,13 @@ async function main(argv) {
|
|
|
1070
1092
|
process2.stdout.write(`\u2714 Default context set to ${ctx.region}/${ctx.org}/${ctx.space}/${ctx.app}
|
|
1071
1093
|
`);
|
|
1072
1094
|
});
|
|
1095
|
+
}
|
|
1096
|
+
async function main(argv) {
|
|
1097
|
+
const program = new Command();
|
|
1098
|
+
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());
|
|
1099
|
+
registerSetupAppCommand(program);
|
|
1100
|
+
registerRunCommand(program);
|
|
1101
|
+
registerUseCommand(program);
|
|
1073
1102
|
await program.parseAsync([...argv]);
|
|
1074
1103
|
}
|
|
1075
1104
|
try {
|