@saptools/bruno 0.1.0

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/index.js ADDED
@@ -0,0 +1,801 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/types.ts
4
+ var CF_META_KEYS = ["__cf_region", "__cf_org", "__cf_space", "__cf_app"];
5
+
6
+ // src/paths.ts
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
38
+ var HEADER_REGEX = /(^|\n)\s*([a-zA-Z][a-zA-Z0-9:_-]*)\s*([{[])/g;
39
+ function findMatchingClose(raw, open, openIdx) {
40
+ const close = open === "{" ? "}" : "]";
41
+ let depth = 1;
42
+ let i = openIdx + 1;
43
+ while (i < raw.length) {
44
+ const ch = raw[i];
45
+ if (ch === open) {
46
+ depth++;
47
+ } else if (ch === close) {
48
+ depth--;
49
+ if (depth === 0) {
50
+ return i;
51
+ }
52
+ }
53
+ i++;
54
+ }
55
+ return -1;
56
+ }
57
+ function listBlocks(raw) {
58
+ const blocks = [];
59
+ HEADER_REGEX.lastIndex = 0;
60
+ let match;
61
+ while ((match = HEADER_REGEX.exec(raw)) !== null) {
62
+ const leadingNewline = match[1] ?? "";
63
+ const header = match[2];
64
+ const open = match[3];
65
+ if (header === void 0 || open !== "{" && open !== "[") {
66
+ continue;
67
+ }
68
+ const headerStart = match.index + leadingNewline.length;
69
+ const openIdx = match.index + match[0].length - 1;
70
+ const closeIdx = findMatchingClose(raw, open, openIdx);
71
+ if (closeIdx === -1) {
72
+ continue;
73
+ }
74
+ blocks.push({
75
+ header,
76
+ start: headerStart,
77
+ end: closeIdx + 1,
78
+ bodyStart: openIdx + 1,
79
+ bodyEnd: closeIdx,
80
+ open,
81
+ close: open === "{" ? "}" : "]"
82
+ });
83
+ }
84
+ return blocks;
85
+ }
86
+ function parseKeyValueBody(body) {
87
+ const entries = /* @__PURE__ */ new Map();
88
+ for (const lineRaw of body.split("\n")) {
89
+ const line = lineRaw.trim();
90
+ if (line.length === 0 || line.startsWith("//")) {
91
+ continue;
92
+ }
93
+ const colon = line.indexOf(":");
94
+ if (colon === -1) {
95
+ continue;
96
+ }
97
+ const key = line.slice(0, colon).trim();
98
+ const value = line.slice(colon + 1).trim();
99
+ if (key.length > 0) {
100
+ entries.set(key, value);
101
+ }
102
+ }
103
+ return entries;
104
+ }
105
+ function parseListBody(body) {
106
+ const items = [];
107
+ for (const lineRaw of body.split("\n")) {
108
+ const line = lineRaw.trim();
109
+ if (line.length === 0 || line.startsWith("//")) {
110
+ continue;
111
+ }
112
+ items.push(line);
113
+ }
114
+ return items;
115
+ }
116
+ function parseBruEnvFile(raw) {
117
+ const blocks = listBlocks(raw);
118
+ const varsBlock = blocks.find((b) => b.header === "vars" && b.open === "{");
119
+ const secretsBlock = blocks.find((b) => b.header === "vars:secret" && b.open === "[");
120
+ const entries = varsBlock ? parseKeyValueBody(raw.slice(varsBlock.bodyStart, varsBlock.bodyEnd)) : /* @__PURE__ */ new Map();
121
+ const secrets = secretsBlock ? parseListBody(raw.slice(secretsBlock.bodyStart, secretsBlock.bodyEnd)) : [];
122
+ return { vars: { entries }, secrets };
123
+ }
124
+
125
+ // src/bru-writer.ts
126
+ function formatVarsBlock(entries) {
127
+ const lines = [];
128
+ for (const [key, value] of entries) {
129
+ lines.push(` ${key}: ${value}`);
130
+ }
131
+ return lines.join("\n");
132
+ }
133
+ function upsertVars(raw, updates) {
134
+ const blocks = listBlocks(raw);
135
+ const varsBlock = blocks.find((b) => b.header === "vars" && b.open === "{");
136
+ if (!varsBlock) {
137
+ const newBlock = `vars {
138
+ ${formatVarsBlock(updates)}
139
+ }
140
+ `;
141
+ const sep2 = raw.length > 0 && !raw.endsWith("\n") ? "\n\n" : raw.length > 0 ? "\n" : "";
142
+ return { content: `${raw}${sep2}${newBlock}`, changed: updates.size > 0 };
143
+ }
144
+ const body = raw.slice(varsBlock.bodyStart, varsBlock.bodyEnd);
145
+ const existing = parseKeyValueBody(body);
146
+ let changed = false;
147
+ for (const [k, v] of updates) {
148
+ if (existing.get(k) !== v) {
149
+ existing.set(k, v);
150
+ changed = true;
151
+ }
152
+ }
153
+ if (!changed) {
154
+ return { content: raw, changed: false };
155
+ }
156
+ const rebuilt = `
157
+ ${formatVarsBlock(existing)}
158
+ `;
159
+ const before = raw.slice(0, varsBlock.bodyStart);
160
+ const after = raw.slice(varsBlock.bodyEnd);
161
+ return { content: `${before}${rebuilt}${after}`, changed: true };
162
+ }
163
+ function ensureSecretEntry(raw, secretName) {
164
+ const blocks = listBlocks(raw);
165
+ const secretsBlock = blocks.find((b) => b.header === "vars:secret" && b.open === "[");
166
+ if (!secretsBlock) {
167
+ const newBlock = `vars:secret [
168
+ ${secretName}
169
+ ]
170
+ `;
171
+ const sep2 = raw.length > 0 && !raw.endsWith("\n") ? "\n\n" : raw.length > 0 ? "\n" : "";
172
+ return { content: `${raw}${sep2}${newBlock}`, changed: true };
173
+ }
174
+ const body = raw.slice(secretsBlock.bodyStart, secretsBlock.bodyEnd);
175
+ const items = body.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("//"));
176
+ if (items.includes(secretName)) {
177
+ return { content: raw, changed: false };
178
+ }
179
+ items.push(secretName);
180
+ const rebuilt = `
181
+ ${items.join("\n ")}
182
+ `;
183
+ const before = raw.slice(0, secretsBlock.bodyStart);
184
+ const after = raw.slice(secretsBlock.bodyEnd);
185
+ return { content: `${before}${rebuilt}${after}`, changed: true };
186
+ }
187
+
188
+ // src/cf-info.ts
189
+ import {
190
+ getRegionView as getRegionViewApi,
191
+ readRegionsView,
192
+ readRegionView,
193
+ readStructureView,
194
+ REGION_KEYS
195
+ } from "@saptools/cf-sync";
196
+ var defaultCfInfoDeps = {
197
+ readStructureView,
198
+ readRegionsView,
199
+ readRegionView,
200
+ getRegionView: getRegionViewApi
201
+ };
202
+ function isValidRegionKey(value) {
203
+ return REGION_KEYS.includes(value);
204
+ }
205
+ async function getStructureSnapshot(deps = defaultCfInfoDeps) {
206
+ const view = await deps.readStructureView();
207
+ if (!view) {
208
+ return {
209
+ source: "empty",
210
+ structure: void 0,
211
+ stale: true,
212
+ message: "No CF structure cached. Run `cf-sync sync` first."
213
+ };
214
+ }
215
+ const stale = view.source === "runtime" && view.metadata?.status === "running";
216
+ return {
217
+ source: view.source,
218
+ structure: view.structure,
219
+ stale,
220
+ message: stale ? "A CF sync is still running \u2014 showing partial data." : void 0
221
+ };
222
+ }
223
+ async function listRegionsWithContent(deps = defaultCfInfoDeps) {
224
+ const snapshot = await getStructureSnapshot(deps);
225
+ if (!snapshot.structure) {
226
+ return [];
227
+ }
228
+ return snapshot.structure.regions.filter((r) => r.accessible && r.orgs.length > 0).map((r) => ({ key: r.key, label: r.label, orgCount: r.orgs.length }));
229
+ }
230
+ async function getRegion(key, deps = defaultCfInfoDeps) {
231
+ const view = await deps.readRegionView(key);
232
+ return view?.region;
233
+ }
234
+ function findOrg(region, orgName) {
235
+ return region.orgs.find((o) => o.name === orgName);
236
+ }
237
+ function findSpace(org, spaceName) {
238
+ return org.spaces.find((s) => s.name === spaceName);
239
+ }
240
+ function findApp(space, appName) {
241
+ return space.apps.find((a) => a.name === appName);
242
+ }
243
+ async function resolveRef(ref, deps = defaultCfInfoDeps) {
244
+ const region = await getRegion(ref.region, deps);
245
+ if (!region) {
246
+ return void 0;
247
+ }
248
+ const org = findOrg(region, ref.org);
249
+ if (!org) {
250
+ return void 0;
251
+ }
252
+ const space = findSpace(org, ref.space);
253
+ if (!space) {
254
+ return void 0;
255
+ }
256
+ const app = findApp(space, ref.app);
257
+ if (!app) {
258
+ return void 0;
259
+ }
260
+ return { region, org, space, app };
261
+ }
262
+
263
+ // src/cf-meta.ts
264
+ import { readFile, writeFile } from "fs/promises";
265
+ function readCfMetaFromVars(vars) {
266
+ const region = vars.get("__cf_region");
267
+ const org = vars.get("__cf_org");
268
+ const space = vars.get("__cf_space");
269
+ const app = vars.get("__cf_app");
270
+ if (!region || !org || !space || !app) {
271
+ return void 0;
272
+ }
273
+ return { region, org, space, app };
274
+ }
275
+ function buildCfMetaUpdates(ref, baseUrl) {
276
+ const updates = /* @__PURE__ */ new Map();
277
+ const pairs = [
278
+ ["__cf_region", ref.region],
279
+ ["__cf_org", ref.org],
280
+ ["__cf_space", ref.space],
281
+ ["__cf_app", ref.app]
282
+ ];
283
+ for (const [k, v] of pairs) {
284
+ updates.set(k, v);
285
+ }
286
+ if (baseUrl !== void 0) {
287
+ updates.set("baseUrl", baseUrl);
288
+ }
289
+ return updates;
290
+ }
291
+ function hasCfMeta(vars) {
292
+ return CF_META_KEYS.every((k) => {
293
+ const v = vars.get(k);
294
+ return v !== void 0 && v.length > 0;
295
+ });
296
+ }
297
+ async function readCfMetaFromFile(path) {
298
+ const raw = await readFile(path, "utf8");
299
+ const parsed = parseBruEnvFile(raw);
300
+ return readCfMetaFromVars(parsed.vars.entries);
301
+ }
302
+ async function writeCfMetaToFile(path, ref, baseUrl) {
303
+ const raw = await readFile(path, "utf8");
304
+ const updates = buildCfMetaUpdates(ref, baseUrl);
305
+ const { content, changed } = upsertVars(raw, updates);
306
+ if (changed) {
307
+ await writeFile(path, content, "utf8");
308
+ }
309
+ return changed;
310
+ }
311
+
312
+ // src/folder-scan.ts
313
+ import { readdir, readFile as readFile2 } from "fs/promises";
314
+ import { join as join2 } from "path";
315
+ async function safeReaddir(path) {
316
+ try {
317
+ const entries = await readdir(path, { withFileTypes: true });
318
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
319
+ } catch {
320
+ return [];
321
+ }
322
+ }
323
+ async function listFiles(path) {
324
+ try {
325
+ const entries = await readdir(path, { withFileTypes: true });
326
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
327
+ } catch {
328
+ return [];
329
+ }
330
+ }
331
+ async function loadEnvFile(path, name) {
332
+ const raw = await readFile2(path, "utf8");
333
+ const parsed = parseBruEnvFile(raw);
334
+ return {
335
+ path,
336
+ name: name.replace(/\.bru$/, ""),
337
+ raw,
338
+ vars: parsed.vars,
339
+ secrets: parsed.secrets
340
+ };
341
+ }
342
+ async function scanAppEnvironments(appPath) {
343
+ const envDir = join2(appPath, ENVIRONMENTS_DIR);
344
+ const files = await listFiles(envDir);
345
+ const bruFiles = files.filter((f) => f.endsWith(".bru"));
346
+ const loaded = [];
347
+ for (const file of bruFiles) {
348
+ loaded.push(await loadEnvFile(join2(envDir, file), file));
349
+ }
350
+ return loaded;
351
+ }
352
+ async function scanApp(spacePath, name) {
353
+ const appPath = join2(spacePath, name);
354
+ const environments = await scanAppEnvironments(appPath);
355
+ return { path: appPath, name, environments };
356
+ }
357
+ async function scanSpace(orgPath, dirName) {
358
+ const name = parsePrefixedName(dirName, SPACE_FOLDER_PREFIX);
359
+ if (name === void 0) {
360
+ return void 0;
361
+ }
362
+ const spacePath = join2(orgPath, dirName);
363
+ const appDirs = await safeReaddir(spacePath);
364
+ const apps = [];
365
+ for (const appDir of appDirs) {
366
+ apps.push(await scanApp(spacePath, appDir));
367
+ }
368
+ return { path: spacePath, name, apps };
369
+ }
370
+ async function scanOrg(regionPath, dirName) {
371
+ const name = parsePrefixedName(dirName, ORG_FOLDER_PREFIX);
372
+ if (name === void 0) {
373
+ return void 0;
374
+ }
375
+ const orgPath = join2(regionPath, dirName);
376
+ const spaceDirs = await safeReaddir(orgPath);
377
+ const spaces = [];
378
+ for (const spaceDir of spaceDirs) {
379
+ const space = await scanSpace(orgPath, spaceDir);
380
+ if (space) {
381
+ spaces.push(space);
382
+ }
383
+ }
384
+ return { path: orgPath, name, spaces };
385
+ }
386
+ async function scanRegion(root, dirName) {
387
+ const key = parsePrefixedName(dirName, REGION_FOLDER_PREFIX);
388
+ if (key === void 0) {
389
+ return void 0;
390
+ }
391
+ const regionPath = join2(root, dirName);
392
+ const orgDirs = await safeReaddir(regionPath);
393
+ const orgs = [];
394
+ for (const orgDir of orgDirs) {
395
+ const org = await scanOrg(regionPath, orgDir);
396
+ if (org) {
397
+ orgs.push(org);
398
+ }
399
+ }
400
+ return { path: regionPath, key, orgs };
401
+ }
402
+ async function scanCollection(root) {
403
+ const regionDirs = await safeReaddir(root);
404
+ const regions = [];
405
+ for (const dir of regionDirs) {
406
+ const region = await scanRegion(root, dir);
407
+ if (region) {
408
+ regions.push(region);
409
+ }
410
+ }
411
+ return { root, regions };
412
+ }
413
+ function parseShorthandPath(shorthand) {
414
+ const cleaned = shorthand.replace(/^[./]+/, "").replace(/\\/g, "/");
415
+ const segs = cleaned.split("/").filter((s) => s.length > 0);
416
+ if (segs.length < 4) {
417
+ return void 0;
418
+ }
419
+ const [region, org, space, app, ...rest] = segs;
420
+ if (!region || !org || !space || !app) {
421
+ return void 0;
422
+ }
423
+ if (rest.length === 0) {
424
+ return { region, org, space, app };
425
+ }
426
+ const filePath = rest.join("/");
427
+ const last = rest[rest.length - 1] ?? "";
428
+ const environment = last.endsWith(".bru") ? last.replace(/\.bru$/, "") : void 0;
429
+ return environment ? { region, org, space, app, environment, filePath } : { region, org, space, app, filePath };
430
+ }
431
+
432
+ // src/context.ts
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 { join as join3 } from "path";
458
+ var COMMON_ENVIRONMENTS = ["local", "dev", "staging", "prod"];
459
+ function emptyEnvContent(envName, ref) {
460
+ const lines = [
461
+ "vars {",
462
+ ` __cf_region: ${ref.region}`,
463
+ ` __cf_org: ${ref.org}`,
464
+ ` __cf_space: ${ref.space}`,
465
+ ` __cf_app: ${ref.app}`,
466
+ ` environment: ${envName}`,
467
+ " baseUrl: ",
468
+ "}",
469
+ ""
470
+ ];
471
+ return lines.join("\n");
472
+ }
473
+ async function ensureEnvFile(appPath, envName, ref) {
474
+ const envDir = join3(appPath, ENVIRONMENTS_DIR);
475
+ await mkdir2(envDir, { recursive: true });
476
+ const filePath = join3(envDir, `${envName}.bru`);
477
+ try {
478
+ await writeFile3(filePath, emptyEnvContent(envName, ref), { encoding: "utf8", flag: "wx" });
479
+ } catch (err) {
480
+ if (err.code !== "EEXIST") {
481
+ throw err;
482
+ }
483
+ await writeCfMetaToFile(filePath, ref);
484
+ }
485
+ return filePath;
486
+ }
487
+ function pickRegion(regions) {
488
+ return regions.map((r) => ({ value: r.key, name: `${r.key} \u2014 ${r.label} (${r.orgCount.toString()} org${r.orgCount === 1 ? "" : "s"})` }));
489
+ }
490
+ function pickOrg(region) {
491
+ return region.orgs.map((o) => ({ value: o.name, name: `${o.name} (${o.spaces.length.toString()} space${o.spaces.length === 1 ? "" : "s"})` }));
492
+ }
493
+ function pickSpace(org) {
494
+ return org.spaces.map((s) => ({ value: s.name, name: `${s.name} (${s.apps.length.toString()} app${s.apps.length === 1 ? "" : "s"})` }));
495
+ }
496
+ function pickApp(space) {
497
+ return space.apps.map((a) => ({ value: a.name, name: a.name }));
498
+ }
499
+ async function setupApp(options) {
500
+ const deps = options.deps ?? defaultCfInfoDeps;
501
+ const log = options.log ?? (() => void 0);
502
+ const regions = await listRegionsWithContent(deps);
503
+ if (regions.length === 0) {
504
+ throw new Error(
505
+ "No CF regions with orgs are cached. Run `cf-sync sync` first, or pass SAP_EMAIL/SAP_PASSWORD to refresh."
506
+ );
507
+ }
508
+ const regionKey = await options.prompts.selectRegion(pickRegion(regions));
509
+ const regionView = await deps.readRegionView(regionKey);
510
+ if (!regionView) {
511
+ throw new Error(`Region ${regionKey} is not cached. Run \`cf-sync sync\` or \`cf-sync region ${regionKey}\`.`);
512
+ }
513
+ const region = regionView.region;
514
+ if (region.orgs.length === 0) {
515
+ throw new Error(`Region ${regionKey} has no accessible orgs.`);
516
+ }
517
+ const orgName = await options.prompts.selectOrg(pickOrg(region));
518
+ const org = region.orgs.find((o) => o.name === orgName);
519
+ if (!org) {
520
+ throw new Error(`Org ${orgName} not found in region ${regionKey}`);
521
+ }
522
+ if (org.spaces.length === 0) {
523
+ throw new Error(`Org ${orgName} has no spaces.`);
524
+ }
525
+ const spaceName = await options.prompts.selectSpace(pickSpace(org));
526
+ const space = org.spaces.find((s) => s.name === spaceName);
527
+ if (!space) {
528
+ throw new Error(`Space ${spaceName} not found in org ${orgName}`);
529
+ }
530
+ if (space.apps.length === 0) {
531
+ throw new Error(`Space ${spaceName} has no apps.`);
532
+ }
533
+ const appName = await options.prompts.selectApp(pickApp(space));
534
+ const ref = { region: regionKey, org: orgName, space: spaceName, app: appName };
535
+ const appPath = join3(
536
+ options.root,
537
+ regionFolderName(regionKey),
538
+ orgFolderName(orgName),
539
+ spaceFolderName(spaceName),
540
+ appName
541
+ );
542
+ const confirmed = await options.prompts.confirmCreate(appPath);
543
+ if (!confirmed) {
544
+ return { ref, appPath, environments: [], created: false };
545
+ }
546
+ await mkdir2(appPath, { recursive: true });
547
+ const existingEnvs = await listExistingEnvs(appPath);
548
+ const common = [...COMMON_ENVIRONMENTS];
549
+ const selected = await options.prompts.selectEnvironments({ common, existing: existingEnvs });
550
+ const custom = await options.prompts.inputCustomEnvName();
551
+ const merged = [];
552
+ for (const name of [...selected, ...custom ? [custom] : []]) {
553
+ const trimmed = name.trim();
554
+ if (trimmed.length > 0 && !merged.includes(trimmed)) {
555
+ merged.push(trimmed);
556
+ }
557
+ }
558
+ if (merged.length === 0) {
559
+ throw new Error("At least one environment is required.");
560
+ }
561
+ const created = [];
562
+ for (const envName of merged) {
563
+ const path = await ensureEnvFile(appPath, envName, ref);
564
+ created.push(path);
565
+ log(`\u2022 ${path}`);
566
+ }
567
+ return { ref, appPath, environments: created, created: true };
568
+ }
569
+ async function listExistingEnvs(appPath) {
570
+ try {
571
+ const entries = await readdir2(join3(appPath, ENVIRONMENTS_DIR), { withFileTypes: true });
572
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".bru")).map((e) => e.name.replace(/\.bru$/, ""));
573
+ } catch {
574
+ return [];
575
+ }
576
+ }
577
+
578
+ // src/run.ts
579
+ import { spawn } from "child_process";
580
+ import { stat } from "fs/promises";
581
+ import { isAbsolute, join as join4, relative, resolve, sep } from "path";
582
+ import { getTokenCached as getTokenCachedApi } from "@saptools/cf-xsuaa";
583
+ function defaultSpawnBru(args, env, cwd) {
584
+ return new Promise((resolvePromise, rejectPromise) => {
585
+ const child = spawn("bru", [...args], { cwd, env, stdio: "pipe" });
586
+ let stdout = "";
587
+ let stderr = "";
588
+ child.stdout.on("data", (chunk) => {
589
+ stdout += chunk.toString("utf8");
590
+ process.stdout.write(chunk);
591
+ });
592
+ child.stderr.on("data", (chunk) => {
593
+ stderr += chunk.toString("utf8");
594
+ process.stderr.write(chunk);
595
+ });
596
+ child.on("error", rejectPromise);
597
+ child.on("close", (code) => {
598
+ resolvePromise({ code: code ?? 0, stdout, stderr });
599
+ });
600
+ });
601
+ }
602
+ async function exists(path) {
603
+ try {
604
+ await stat(path);
605
+ return true;
606
+ } catch {
607
+ return false;
608
+ }
609
+ }
610
+ async function resolveTarget(root, target) {
611
+ const direct = isAbsolute(target) ? target : resolve(process.cwd(), target);
612
+ if (await exists(direct)) {
613
+ return { filePath: direct, shorthand: void 0 };
614
+ }
615
+ const shorthand = parseShorthandPath(target);
616
+ if (!shorthand) {
617
+ throw new Error(`Target not found: ${target}`);
618
+ }
619
+ const { region, org, space, app, filePath } = shorthand;
620
+ const appDir = join4(
621
+ root,
622
+ regionFolderName(region),
623
+ orgFolderName(org),
624
+ spaceFolderName(space),
625
+ app
626
+ );
627
+ if (!filePath) {
628
+ return { filePath: appDir, shorthand };
629
+ }
630
+ const candidate = join4(appDir, filePath);
631
+ if (await exists(candidate)) {
632
+ return { filePath: candidate, shorthand };
633
+ }
634
+ const withExt = candidate.endsWith(".bru") ? candidate : `${candidate}.bru`;
635
+ if (await exists(withExt)) {
636
+ return { filePath: withExt, shorthand };
637
+ }
638
+ throw new Error(`File not found: ${candidate}`);
639
+ }
640
+ async function chooseEnvironmentFile(appDir, environment) {
641
+ if (environment) {
642
+ const envFile = join4(appDir, ENVIRONMENTS_DIR, `${environment}.bru`);
643
+ if (!await exists(envFile)) {
644
+ throw new Error(`Environment file not found: ${envFile}`);
645
+ }
646
+ return { envFile, environment };
647
+ }
648
+ const collection = await scanCollection(resolve(appDir, "..", "..", "..", ".."));
649
+ for (const region of collection.regions) {
650
+ for (const org of region.orgs) {
651
+ for (const space of org.spaces) {
652
+ for (const app of space.apps) {
653
+ if (app.path === appDir && app.environments.length > 0) {
654
+ const first = app.environments[0];
655
+ if (first) {
656
+ return { envFile: first.path, environment: first.name };
657
+ }
658
+ }
659
+ }
660
+ }
661
+ }
662
+ }
663
+ throw new Error(`No environment files found under ${appDir}/${ENVIRONMENTS_DIR}`);
664
+ }
665
+ function findAppDirFromFile(filePath, root) {
666
+ const rel = relative(root, filePath).split(sep);
667
+ if (rel.length < 4) {
668
+ throw new Error(`File is not inside a CF-structured bruno collection: ${filePath}`);
669
+ }
670
+ const [regionDir, orgDir, spaceDir, appDir] = rel;
671
+ if (!regionDir || !orgDir || !spaceDir || !appDir) {
672
+ throw new Error(`File is not inside a CF-structured bruno collection: ${filePath}`);
673
+ }
674
+ return join4(root, regionDir, orgDir, spaceDir, appDir);
675
+ }
676
+ async function buildRunPlan(options) {
677
+ const { filePath } = await resolveTarget(options.root, options.target);
678
+ const stats = await stat(filePath);
679
+ let appDir;
680
+ let requestFile;
681
+ if (stats.isDirectory()) {
682
+ appDir = filePath;
683
+ requestFile = void 0;
684
+ } else {
685
+ appDir = findAppDirFromFile(filePath, options.root);
686
+ requestFile = filePath;
687
+ }
688
+ const { envFile, environment } = await chooseEnvironmentFile(appDir, options.environment);
689
+ const meta = await readCfMetaFromFile(envFile);
690
+ if (!meta) {
691
+ throw new Error(
692
+ `Missing __cf_region/__cf_org/__cf_space/__cf_app in ${envFile}. Run \`saptools-bruno setup-app\` first.`
693
+ );
694
+ }
695
+ const getToken = options.getTokenCached ?? getTokenCachedApi;
696
+ const token = await getToken(meta);
697
+ const bruArgs = ["run"];
698
+ if (requestFile) {
699
+ bruArgs.push(relative(appDir, requestFile) || ".");
700
+ }
701
+ bruArgs.push("--env", environment, "--env-var", `accessToken=${token}`);
702
+ if (options.extraArgs) {
703
+ bruArgs.push(...options.extraArgs);
704
+ }
705
+ return {
706
+ filePath,
707
+ environment,
708
+ envFile,
709
+ meta,
710
+ token,
711
+ bruArgs,
712
+ cwd: appDir
713
+ };
714
+ }
715
+ async function runBruno(options) {
716
+ const plan = await buildRunPlan(options);
717
+ const spawnFn = options.spawnBru ?? defaultSpawnBru;
718
+ const env = { ...process.env, SAPTOOLS_ACCESS_TOKEN: plan.token };
719
+ options.log?.(`\u25B6 bru ${plan.bruArgs.join(" ")} (cwd=${plan.cwd})`);
720
+ const result = await spawnFn(plan.bruArgs, env, plan.cwd);
721
+ return { ...plan, ...result };
722
+ }
723
+
724
+ // src/use.ts
725
+ function parseContextShorthand(shorthand) {
726
+ const segs = shorthand.split("/").filter((s) => s.length > 0);
727
+ if (segs.length !== 4) {
728
+ return void 0;
729
+ }
730
+ const [region, org, space, app] = segs;
731
+ if (!region || !org || !space || !app) {
732
+ return void 0;
733
+ }
734
+ return { region, org, space, app };
735
+ }
736
+ async function useContext(options) {
737
+ const parsed = parseContextShorthand(options.shorthand);
738
+ if (!parsed) {
739
+ throw new Error(
740
+ `Invalid context shorthand: ${options.shorthand}. Expected <region>/<org>/<space>/<app>.`
741
+ );
742
+ }
743
+ if (!isValidRegionKey(parsed.region)) {
744
+ throw new Error(`Unknown region key: ${parsed.region}`);
745
+ }
746
+ if (options.verify !== false) {
747
+ const resolved = await resolveRef({ ...parsed, region: parsed.region }, options.deps);
748
+ if (!resolved) {
749
+ throw new Error(
750
+ `Could not verify ${options.shorthand} against the cached CF structure. Run \`cf-sync sync\` first.`
751
+ );
752
+ }
753
+ }
754
+ return await writeContext(parsed);
755
+ }
756
+ export {
757
+ BRUNO_CONTEXT_FILENAME,
758
+ CF_META_KEYS,
759
+ COMMON_ENVIRONMENTS,
760
+ ENVIRONMENTS_DIR,
761
+ ORG_FOLDER_PREFIX,
762
+ REGION_FOLDER_PREFIX,
763
+ SAPTOOLS_DIR_NAME,
764
+ SPACE_FOLDER_PREFIX,
765
+ brunoContextPath,
766
+ buildCfMetaUpdates,
767
+ buildRunPlan,
768
+ defaultCfInfoDeps,
769
+ ensureSecretEntry,
770
+ findApp,
771
+ findOrg,
772
+ findSpace,
773
+ getRegion,
774
+ getStructureSnapshot,
775
+ hasCfMeta,
776
+ isValidRegionKey,
777
+ listBlocks,
778
+ listRegionsWithContent,
779
+ orgFolderName,
780
+ parseBruEnvFile,
781
+ parseContextShorthand,
782
+ parseKeyValueBody,
783
+ parseListBody,
784
+ parsePrefixedName,
785
+ parseShorthandPath,
786
+ readCfMetaFromFile,
787
+ readCfMetaFromVars,
788
+ readContext,
789
+ regionFolderName,
790
+ resolveRef,
791
+ runBruno,
792
+ saptoolsDir,
793
+ scanCollection,
794
+ setupApp,
795
+ spaceFolderName,
796
+ upsertVars,
797
+ useContext,
798
+ writeCfMetaToFile,
799
+ writeContext
800
+ };
801
+ //# sourceMappingURL=index.js.map