@saptools/bruno 0.3.3 → 0.3.5
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 +48 -25
- package/dist/cli.js +210 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -22,10 +22,12 @@ Scaffold a CF-aware collection. Resolve requests by `region/org/space/app` short
|
|
|
22
22
|
## ⚡ At a glance
|
|
23
23
|
|
|
24
24
|
```console
|
|
25
|
-
$
|
|
25
|
+
$ bruno use ap10/demo-prod/api/orders-srv
|
|
26
26
|
✔ Default context set to ap10/demo-prod/api/orders-srv
|
|
27
27
|
|
|
28
|
-
$
|
|
28
|
+
$ bruno
|
|
29
|
+
# chọn Run .bru file (folder tree)
|
|
30
|
+
# duyệt cây thư mục rồi chọn file .bru để chạy
|
|
29
31
|
▶ bru run --env dev --env-var accessToken=eyJhbGciOi… (cwd=…/orders-srv)
|
|
30
32
|
Running Folder Recursively
|
|
31
33
|
✓ GET /orders 204 OK 54ms
|
|
@@ -43,8 +45,8 @@ You just ran Bruno against a production-grade XSUAA-protected service **without
|
|
|
43
45
|
- 🏗️ **Interactive `setup-app`** — pick a region → org → space, then **search apps as you type** before choosing exactly the environments you want (or typing a custom name like `qa-eu`). Every env file is seeded with `__cf_*` metadata so the runner knows where to fetch a token.
|
|
44
46
|
- 🧭 **Shorthand paths** — `region/org/space/app[/folder/file.bru]` expands to the right filesystem path. No more `cd`-ing through nested folders.
|
|
45
47
|
- 🔐 **Automatic XSUAA tokens** — every `run` fetches (or reuses) a cached token via [`@saptools/cf-xsuaa`](https://www.npmjs.com/package/@saptools/cf-xsuaa), writes it into the selected env file as `accessToken`, and still injects it for `bru` at execution time.
|
|
46
|
-
- 📦 **Bundled Bruno CLI fallback** — if `bru` is already on your `PATH`, `
|
|
47
|
-
- 🎯 **Default context** — `
|
|
48
|
+
- 📦 **Bundled Bruno CLI fallback** — if `bru` is already on your `PATH`, `bruno` uses it. If not, it falls back to the bundled [`@usebruno/cli`](https://www.npmjs.com/package/@usebruno/cli).
|
|
49
|
+
- 🎯 **Default context** — `bruno use <shorthand>` pins a target so subsequent `run` calls need zero arguments. Feels like `cf target` for Bruno.
|
|
48
50
|
- 🧩 **CLI & typed API** — every command has a zero-config Node.js equivalent. Full TypeScript definitions shipped. Bring your own prompts for headless/CI use.
|
|
49
51
|
- 🧪 **Fully tested** — unit tests plus offline e2e coverage (stub `bru` binary + fixture CF snapshot). No network required in CI.
|
|
50
52
|
- 🪶 **Small + boring** — a small runtime surface, no background daemons, no plugin system, no magic.
|
|
@@ -80,8 +82,8 @@ You just ran Bruno against a production-grade XSUAA-protected service **without
|
|
|
80
82
|
<td valign="top">
|
|
81
83
|
|
|
82
84
|
```bash
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
bruno use ap10/demo-prod/api/orders-srv
|
|
86
|
+
bruno run --env dev
|
|
85
87
|
```
|
|
86
88
|
|
|
87
89
|
*That's it. Token is cached, refreshed on expiry, written back to the env file, and injected automatically.*
|
|
@@ -118,13 +120,13 @@ npm install @saptools/bruno
|
|
|
118
120
|
cf-sync sync
|
|
119
121
|
|
|
120
122
|
# 2. Scaffold an app folder with seeded __cf_* metadata
|
|
121
|
-
|
|
123
|
+
bruno setup-app
|
|
122
124
|
|
|
123
125
|
# 3. Pin a default CF context so future runs need zero args
|
|
124
|
-
|
|
126
|
+
bruno use ap10/my-org/dev/my-srv
|
|
125
127
|
|
|
126
128
|
# 4. Run — XSUAA token is fetched, written to the env file, and injected automatically
|
|
127
|
-
|
|
129
|
+
bruno run --env dev
|
|
128
130
|
```
|
|
129
131
|
|
|
130
132
|
After `setup-app`, your workspace looks like this:
|
|
@@ -160,17 +162,38 @@ Your `.bru` requests reference `{{accessToken}}` like any other Bruno variable
|
|
|
160
162
|
|
|
161
163
|
## 🧰 CLI
|
|
162
164
|
|
|
163
|
-
|
|
165
|
+
|
|
166
|
+
### 🗂️ `bruno` (interactive tree mode)
|
|
167
|
+
|
|
168
|
+
Chạy `bruno` không kèm subcommand sẽ mở menu tương tác:
|
|
169
|
+
- Run `.bru` file
|
|
170
|
+
- Set default context
|
|
171
|
+
- Setup app folder
|
|
172
|
+
- Set Bruno root folder
|
|
173
|
+
|
|
174
|
+
Ở chế độ chạy file, danh sách browse chỉ hiển thị cấp thư mục hiện tại (tên gần nhất). Bạn có thể duyệt vào thư mục con/back, hoặc chọn `Search .bru file` để tìm toàn cây và kết quả sẽ hiển thị theo đường dẫn tree tương đối.
|
|
175
|
+
|
|
176
|
+
### 📌 `bruno set-root [dir]`
|
|
177
|
+
|
|
178
|
+
Lưu Bruno root folder mặc định vào `~/.saptools/bruno/cli-state.json`.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
bruno set-root /path/to/your/collection
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Sau khi set, lần gọi `bruno` tiếp theo sẽ dùng root này nếu bạn không truyền `--collection` và không set env var.
|
|
185
|
+
|
|
186
|
+
### 🏗️ `bruno setup-app`
|
|
164
187
|
|
|
165
188
|
Interactively scaffold a Bruno app folder inside the current Bruno collection directory. Walks you through **region → org → space → app**, with the **app step using a searchable picker** for large spaces, then lets you **pick which environments to create** and add custom names without leaving the environment picker.
|
|
166
189
|
|
|
167
190
|
```bash
|
|
168
|
-
|
|
169
|
-
|
|
191
|
+
bruno setup-app
|
|
192
|
+
bruno --collection ./collections setup-app
|
|
170
193
|
```
|
|
171
194
|
|
|
172
195
|
> [!TIP]
|
|
173
|
-
> `--collection` only applies to the current command. If you omit it, `
|
|
196
|
+
> `--collection` only applies to the current command. If you omit it, `bruno` falls back to `$SAPTOOLS_BRUNO_COLLECTION`, then to your current working directory.
|
|
174
197
|
|
|
175
198
|
> [!IMPORTANT]
|
|
176
199
|
> `setup-app` reads the cached CF landscape prepared by `cf-sync`. If the cache is missing or stale, run `cf-sync sync` first.
|
|
@@ -185,22 +208,22 @@ saptools-bruno --collection ./collections setup-app
|
|
|
185
208
|
> [!TIP]
|
|
186
209
|
> The env prompt shows the common names (`local`, `dev`, `staging`, `prod`) plus any envs already on disk. Pre-existing envs are **pre-checked**; common ones are **not** — so you only create what you actually need. The menu also includes **Add custom environment**, and once you enter a value like `qa-eu` or `uat.us`, it appears back in the same checklist already selected so you can review the full set before finishing.
|
|
187
210
|
|
|
188
|
-
### ▶️ `
|
|
211
|
+
### ▶️ `bruno run`
|
|
189
212
|
|
|
190
213
|
Run a Bruno request or folder, refreshing `accessToken` in the chosen env file and auto-injecting the same token for the current execution.
|
|
191
214
|
|
|
192
215
|
```bash
|
|
193
216
|
# Use the default context
|
|
194
|
-
|
|
217
|
+
bruno run --env dev
|
|
195
218
|
|
|
196
219
|
# Explicit shorthand
|
|
197
|
-
|
|
220
|
+
bruno run ap10/my-org/dev/my-srv --env dev
|
|
198
221
|
|
|
199
222
|
# Drill into a subfolder or a single file
|
|
200
|
-
|
|
223
|
+
bruno run ap10/my-org/dev/my-srv/users/get-all.bru --env dev
|
|
201
224
|
|
|
202
225
|
# Or pass a real filesystem path (absolute or relative)
|
|
203
|
-
|
|
226
|
+
bruno run ./region__ap10/org__my-org/space__dev/my-srv --env dev
|
|
204
227
|
```
|
|
205
228
|
|
|
206
229
|
| Flag | Description |
|
|
@@ -213,13 +236,13 @@ Under the hood this:
|
|
|
213
236
|
- writes `accessToken: <token>` into the selected `.bru` env file
|
|
214
237
|
- spawns `bru run <target> --env <name> --env-var accessToken=<token>`
|
|
215
238
|
|
|
216
|
-
### 🎯 `
|
|
239
|
+
### 🎯 `bruno use`
|
|
217
240
|
|
|
218
241
|
Pin a default CF context so `run` can be called without arguments.
|
|
219
242
|
|
|
220
243
|
```bash
|
|
221
|
-
|
|
222
|
-
|
|
244
|
+
bruno use ap10/my-org/dev/my-srv
|
|
245
|
+
bruno use ap10/my-org/dev/my-srv --no-verify
|
|
223
246
|
```
|
|
224
247
|
|
|
225
248
|
| Flag | Description |
|
|
@@ -365,7 +388,7 @@ The `__cf_*` vars drive XSUAA lookup. `run` adds `accessToken` on the fly via `b
|
|
|
365
388
|
| Hand-edit `environments/*.bru` | ❌ manual | ❌ | ❌ | ❌ | ❌ |
|
|
366
389
|
| Bruno GUI OAuth2 | ✅ | ❌ | ❌ | partial | ❌ (GUI) |
|
|
367
390
|
| `bru run` alone | ❌ | ❌ | ❌ | ❌ | ✅ |
|
|
368
|
-
| **`
|
|
391
|
+
| **`bruno`** | ✅ **automatic** | ✅ | ✅ | ✅ | ✅ |
|
|
369
392
|
|
|
370
393
|
---
|
|
371
394
|
|
|
@@ -384,7 +407,7 @@ The `__cf_*` vars drive XSUAA lookup. `run` adds `accessToken` on the fly via `b
|
|
|
384
407
|
<details>
|
|
385
408
|
<summary><b>Why does Bruno need a wrapper — can't I just call <code>bru run</code>?</b></summary>
|
|
386
409
|
|
|
387
|
-
You can, but every CF service behind XSUAA needs a fresh OAuth2 token, and Bruno doesn't mint them. `
|
|
410
|
+
You can, but every CF service behind XSUAA needs a fresh OAuth2 token, and Bruno doesn't mint them. `bruno run` fetches the token (cached when possible), injects it as `accessToken`, and gets out of the way. Your `.bru` requests stay portable.
|
|
388
411
|
|
|
389
412
|
</details>
|
|
390
413
|
|
|
@@ -431,8 +454,8 @@ Use the programmatic API with your own prompt stubs (every field just returns th
|
|
|
431
454
|
- [x] Shorthand path resolution (`region/org/space/app[/file]`)
|
|
432
455
|
- [x] Default CF context via `use`
|
|
433
456
|
- [x] Offline e2e via stubbed `bru`
|
|
434
|
-
- [ ] `
|
|
435
|
-
- [ ] `
|
|
457
|
+
- [ ] `bruno doctor` — diagnose missing `__cf_*` vars, stale tokens, missing `bru`
|
|
458
|
+
- [ ] `bruno migrate` — move collections from a flat layout into the CF-aware layout
|
|
436
459
|
- [ ] First-class `--reporter json` support for piping test results into dashboards
|
|
437
460
|
|
|
438
461
|
Have an idea? [Open an issue](https://github.com/dongitran/saptools/issues/new) — the roadmap is driven by real use.
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/main.ts
|
|
4
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
5
|
+
import { isAbsolute as isAbsolute2, join as join5, resolve as resolve2 } from "path";
|
|
4
6
|
import process2 from "process";
|
|
5
|
-
import { confirm, select } from "@inquirer/prompts";
|
|
7
|
+
import { confirm, input as input2, search as search2, select } from "@inquirer/prompts";
|
|
6
8
|
import { Command, Option } from "commander";
|
|
7
9
|
|
|
8
10
|
// src/commands/run.ts
|
|
@@ -191,6 +193,8 @@ import { homedir } from "os";
|
|
|
191
193
|
import { join } from "path";
|
|
192
194
|
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
193
195
|
var BRUNO_CONTEXT_FILENAME = "bruno-context.json";
|
|
196
|
+
var BRUNO_DIR_NAME = "bruno";
|
|
197
|
+
var BRUNO_CLI_STATE_FILENAME = "cli-state.json";
|
|
194
198
|
var REGION_FOLDER_PREFIX = "region__";
|
|
195
199
|
var ORG_FOLDER_PREFIX = "org__";
|
|
196
200
|
var SPACE_FOLDER_PREFIX = "space__";
|
|
@@ -216,6 +220,12 @@ function parsePrefixedName(dirName, prefix) {
|
|
|
216
220
|
}
|
|
217
221
|
return dirName.slice(prefix.length);
|
|
218
222
|
}
|
|
223
|
+
function brunoDir() {
|
|
224
|
+
return join(saptoolsDir(), BRUNO_DIR_NAME);
|
|
225
|
+
}
|
|
226
|
+
function brunoCliStatePath() {
|
|
227
|
+
return join(brunoDir(), BRUNO_CLI_STATE_FILENAME);
|
|
228
|
+
}
|
|
219
229
|
|
|
220
230
|
// src/collection/folder-scan.ts
|
|
221
231
|
async function safeReaddir(path) {
|
|
@@ -846,6 +856,28 @@ async function writeContext(ctx) {
|
|
|
846
856
|
`, "utf8");
|
|
847
857
|
return updated;
|
|
848
858
|
}
|
|
859
|
+
async function readBrunoCliState() {
|
|
860
|
+
try {
|
|
861
|
+
const raw = await readFile4(brunoCliStatePath(), "utf8");
|
|
862
|
+
return JSON.parse(raw);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
if (err.code === "ENOENT") {
|
|
865
|
+
return void 0;
|
|
866
|
+
}
|
|
867
|
+
throw err;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async function writeBrunoCliState(input3) {
|
|
871
|
+
const next = {
|
|
872
|
+
rootDir: input3.rootDir,
|
|
873
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
874
|
+
};
|
|
875
|
+
const path = brunoCliStatePath();
|
|
876
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
877
|
+
await writeFile4(path, `${JSON.stringify(next, null, 2)}
|
|
878
|
+
`, "utf8");
|
|
879
|
+
return next;
|
|
880
|
+
}
|
|
849
881
|
|
|
850
882
|
// src/commands/use.ts
|
|
851
883
|
function parseContextShorthand(shorthand) {
|
|
@@ -1016,18 +1048,18 @@ async function promptForEnvironments(opts, deps = {}) {
|
|
|
1016
1048
|
// src/cli/main.ts
|
|
1017
1049
|
function resolveCollectionDir(explicitCollection, explicitRoot) {
|
|
1018
1050
|
if (explicitCollection) {
|
|
1019
|
-
return explicitCollection;
|
|
1051
|
+
return Promise.resolve(explicitCollection);
|
|
1020
1052
|
}
|
|
1021
1053
|
if (explicitRoot) {
|
|
1022
|
-
return explicitRoot;
|
|
1054
|
+
return Promise.resolve(explicitRoot);
|
|
1023
1055
|
}
|
|
1024
1056
|
if (process2.env["SAPTOOLS_BRUNO_COLLECTION"]) {
|
|
1025
|
-
return process2.env["SAPTOOLS_BRUNO_COLLECTION"];
|
|
1057
|
+
return Promise.resolve(process2.env["SAPTOOLS_BRUNO_COLLECTION"]);
|
|
1026
1058
|
}
|
|
1027
1059
|
if (process2.env["SAPTOOLS_BRUNO_ROOT"]) {
|
|
1028
|
-
return process2.env["SAPTOOLS_BRUNO_ROOT"];
|
|
1060
|
+
return Promise.resolve(process2.env["SAPTOOLS_BRUNO_ROOT"]);
|
|
1029
1061
|
}
|
|
1030
|
-
return process2.cwd();
|
|
1062
|
+
return readBrunoCliState().then((state) => state?.rootDir ?? process2.cwd());
|
|
1031
1063
|
}
|
|
1032
1064
|
function resolveProgramCollectionDir(program) {
|
|
1033
1065
|
const opts = program.opts();
|
|
@@ -1037,10 +1069,121 @@ function writeLine(message) {
|
|
|
1037
1069
|
process2.stdout.write(`${message}
|
|
1038
1070
|
`);
|
|
1039
1071
|
}
|
|
1072
|
+
async function listDir(path) {
|
|
1073
|
+
const entries = await readdir3(path, { withFileTypes: true });
|
|
1074
|
+
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
1075
|
+
const bruFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".bru")).map((entry) => entry.name).sort();
|
|
1076
|
+
return { dirs, bruFiles };
|
|
1077
|
+
}
|
|
1078
|
+
async function collectBruFiles(root, current) {
|
|
1079
|
+
const { dirs, bruFiles } = await listDir(current);
|
|
1080
|
+
const nested = await Promise.all(dirs.map(async (dir) => await collectBruFiles(root, join5(current, dir))));
|
|
1081
|
+
const currentFiles = bruFiles.map((name) => join5(current, name));
|
|
1082
|
+
return [...currentFiles, ...nested.flat()].sort();
|
|
1083
|
+
}
|
|
1084
|
+
function formatTreePath(root, fullPath) {
|
|
1085
|
+
const rel = fullPath.startsWith(root) ? fullPath.slice(root.length).replace(/^[/\\]/, "") : fullPath;
|
|
1086
|
+
return rel.replace(/\\/g, "/");
|
|
1087
|
+
}
|
|
1088
|
+
function formatTreeResultLabel(relativePath) {
|
|
1089
|
+
const segments = relativePath.split("/").filter((segment) => segment.length > 0);
|
|
1090
|
+
if (segments.length === 0) {
|
|
1091
|
+
return "\u{1F4C4}";
|
|
1092
|
+
}
|
|
1093
|
+
const lines = [];
|
|
1094
|
+
for (const [index, segment] of segments.entries()) {
|
|
1095
|
+
const isFile = index === segments.length - 1;
|
|
1096
|
+
const icon = isFile ? "\u{1F4C4}" : "\u{1F4C1}";
|
|
1097
|
+
if (index === 0) {
|
|
1098
|
+
lines.push(`${icon} ${segment}`);
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
const indent = " ".repeat(index);
|
|
1102
|
+
lines.push(`${indent}\u2514\u2500${icon} ${segment}`);
|
|
1103
|
+
}
|
|
1104
|
+
return lines.join("\n");
|
|
1105
|
+
}
|
|
1106
|
+
function formatEntryTreePath(root, current, entryName) {
|
|
1107
|
+
const fullPath = join5(current, entryName);
|
|
1108
|
+
return formatTreePath(root, fullPath);
|
|
1109
|
+
}
|
|
1110
|
+
async function browseBruFile(root) {
|
|
1111
|
+
let current = root;
|
|
1112
|
+
for (; ; ) {
|
|
1113
|
+
const currentLevel = await listDir(current);
|
|
1114
|
+
const allFiles = await collectBruFiles(root, root);
|
|
1115
|
+
const selected = await search2({
|
|
1116
|
+
message: "",
|
|
1117
|
+
source: (inputValue) => {
|
|
1118
|
+
const query = (inputValue ?? "").trim().toLowerCase();
|
|
1119
|
+
if (query.length === 0) {
|
|
1120
|
+
const localChoices = [
|
|
1121
|
+
{ name: "\u{1F50E}", value: { kind: "search", name: "" } },
|
|
1122
|
+
...currentLevel.dirs.map((dir) => ({
|
|
1123
|
+
name: `\u{1F4C1} ${dir}/`,
|
|
1124
|
+
value: { kind: "dir", name: dir }
|
|
1125
|
+
})),
|
|
1126
|
+
...currentLevel.bruFiles.map((file) => ({
|
|
1127
|
+
name: `\u{1F4C4} ${file}`,
|
|
1128
|
+
value: { kind: "file", name: file }
|
|
1129
|
+
})),
|
|
1130
|
+
...current === root ? [] : [{ name: "\u2B05 ../", value: { kind: "back", name: "" } }],
|
|
1131
|
+
{ name: "\u2716 Exit", value: { kind: "exit", name: "" } }
|
|
1132
|
+
];
|
|
1133
|
+
return localChoices;
|
|
1134
|
+
}
|
|
1135
|
+
const matchedFiles = allFiles.filter((file) => formatTreePath(root, file).toLowerCase().includes(query)).slice(0, 200).map((file) => ({
|
|
1136
|
+
name: formatTreeResultLabel(formatTreePath(root, file)),
|
|
1137
|
+
value: { kind: "file", name: formatEntryTreePath(root, root, formatTreePath(root, file)) }
|
|
1138
|
+
}));
|
|
1139
|
+
const matchedDirs = Array.from(
|
|
1140
|
+
new Set(
|
|
1141
|
+
allFiles.map((file) => formatTreePath(root, file)).flatMap((path) => {
|
|
1142
|
+
const parts = path.split("/");
|
|
1143
|
+
if (parts.length <= 1) {
|
|
1144
|
+
return [];
|
|
1145
|
+
}
|
|
1146
|
+
const folders = parts.slice(0, -1);
|
|
1147
|
+
return folders.map((_, index) => folders.slice(0, index + 1).join("/"));
|
|
1148
|
+
}).filter((dirPath) => dirPath.toLowerCase().includes(query))
|
|
1149
|
+
)
|
|
1150
|
+
).slice(0, 100).map((dirPath) => ({
|
|
1151
|
+
name: formatTreeResultLabel(dirPath),
|
|
1152
|
+
value: { kind: "dir", name: dirPath }
|
|
1153
|
+
}));
|
|
1154
|
+
return [
|
|
1155
|
+
{ name: `\u{1F50E} ${query}`, value: { kind: "search", name: query } },
|
|
1156
|
+
...matchedDirs,
|
|
1157
|
+
...matchedFiles,
|
|
1158
|
+
...current === root ? [] : [{ name: "\u2B05 ../", value: { kind: "back", name: "" } }],
|
|
1159
|
+
{ name: "\u2716 Exit", value: { kind: "exit", name: "" } }
|
|
1160
|
+
];
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
if (selected.kind === "dir") {
|
|
1164
|
+
current = selected.name.includes("/") ? join5(root, selected.name) : join5(current, selected.name);
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
if (selected.kind === "back") {
|
|
1168
|
+
current = resolve2(current, "..");
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
if (selected.kind === "search") {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (selected.kind === "file") {
|
|
1175
|
+
if (selected.name.endsWith(".bru") && selected.name.includes("/")) {
|
|
1176
|
+
return join5(root, selected.name);
|
|
1177
|
+
}
|
|
1178
|
+
return join5(current, selected.name);
|
|
1179
|
+
}
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1040
1183
|
function registerSetupAppCommand(program) {
|
|
1041
1184
|
program.command("setup-app").description("Interactively scaffold a bruno app folder and seed __cf_* variables").action(async () => {
|
|
1042
1185
|
const result = await setupApp({
|
|
1043
|
-
root: resolveProgramCollectionDir(program),
|
|
1186
|
+
root: await resolveProgramCollectionDir(program),
|
|
1044
1187
|
prompts: {
|
|
1045
1188
|
selectRegion: async (choices) => await select({ message: "Select region", choices: [...choices] }),
|
|
1046
1189
|
selectOrg: async (choices) => await select({ message: "Select org", choices: [...choices] }),
|
|
@@ -1064,41 +1207,82 @@ async function resolveRunTarget(target) {
|
|
|
1064
1207
|
}
|
|
1065
1208
|
const ctx = await readContext();
|
|
1066
1209
|
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
|
-
);
|
|
1210
|
+
throw new Error("No target specified and no default context is set. Run `bruno use <region/org/space/app>` first.");
|
|
1070
1211
|
}
|
|
1071
1212
|
return `${ctx.region}/${ctx.org}/${ctx.space}/${ctx.app}`;
|
|
1072
1213
|
}
|
|
1073
1214
|
function registerRunCommand(program) {
|
|
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(
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
}
|
|
1084
|
-
);
|
|
1215
|
+
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(async (target, opts) => {
|
|
1216
|
+
const result = await runBruno({
|
|
1217
|
+
root: await resolveProgramCollectionDir(program),
|
|
1218
|
+
target: await resolveRunTarget(target),
|
|
1219
|
+
...opts.env ? { environment: opts.env } : {},
|
|
1220
|
+
log: writeLine
|
|
1221
|
+
});
|
|
1222
|
+
process2.exit(result.code);
|
|
1223
|
+
});
|
|
1085
1224
|
}
|
|
1086
1225
|
function registerUseCommand(program) {
|
|
1087
1226
|
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) => {
|
|
1088
|
-
const ctx = await useContext({
|
|
1089
|
-
shorthand,
|
|
1090
|
-
verify: opts.verify !== false
|
|
1091
|
-
});
|
|
1227
|
+
const ctx = await useContext({ shorthand, verify: opts.verify !== false });
|
|
1092
1228
|
process2.stdout.write(`\u2714 Default context set to ${ctx.region}/${ctx.org}/${ctx.space}/${ctx.app}
|
|
1093
1229
|
`);
|
|
1094
1230
|
});
|
|
1095
1231
|
}
|
|
1232
|
+
function registerSetRootCommand(program) {
|
|
1233
|
+
program.command("set-root").description("Persist default Bruno root folder under ~/.saptools/bruno/").argument("[dir]", "Root folder path").action(async (dir) => {
|
|
1234
|
+
const raw = dir ?? await input2({ message: "Enter Bruno root folder path", default: process2.cwd() });
|
|
1235
|
+
const rootDir = isAbsolute2(raw) ? raw : resolve2(process2.cwd(), raw);
|
|
1236
|
+
const saved = await writeBrunoCliState({ rootDir });
|
|
1237
|
+
writeLine(`\u2714 Bruno root saved: ${saved.rootDir}`);
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
async function launchInteractiveMenu(program) {
|
|
1241
|
+
const root = await resolveProgramCollectionDir(program);
|
|
1242
|
+
const action = await select({
|
|
1243
|
+
message: `Bruno menu (${root}) \u2014 choose an action`,
|
|
1244
|
+
choices: [
|
|
1245
|
+
{ name: "Run .bru file", value: "run-file" },
|
|
1246
|
+
{ name: "Set default context (use)", value: "use" },
|
|
1247
|
+
{ name: "Setup app folder", value: "setup-app" },
|
|
1248
|
+
{ name: "Set Bruno root folder", value: "set-root" },
|
|
1249
|
+
{ name: "Exit", value: "exit" }
|
|
1250
|
+
]
|
|
1251
|
+
});
|
|
1252
|
+
if (action === "run-file") {
|
|
1253
|
+
const filePath = await browseBruFile(root);
|
|
1254
|
+
if (!filePath) {
|
|
1255
|
+
writeLine("Aborted.");
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const result = await runBruno({ root, target: filePath, log: writeLine });
|
|
1259
|
+
process2.exit(result.code);
|
|
1260
|
+
}
|
|
1261
|
+
if (action === "use") {
|
|
1262
|
+
const shorthand = await input2({ message: "Enter context shorthand (region/org/space/app)" });
|
|
1263
|
+
const ctx = await useContext({ shorthand, verify: true });
|
|
1264
|
+
writeLine(`\u2714 Default context set to ${ctx.region}/${ctx.org}/${ctx.space}/${ctx.app}`);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (action === "setup-app") {
|
|
1268
|
+
await program.parseAsync(["node", "bruno", "setup-app"]);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (action === "set-root") {
|
|
1272
|
+
await program.parseAsync(["node", "bruno", "set-root"]);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1096
1275
|
async function main(argv) {
|
|
1097
1276
|
const program = new Command();
|
|
1098
|
-
program.name("
|
|
1277
|
+
program.name("bruno").description("Smart runner for Bruno with CF-aware env metadata and automatic token injection").addOption(new Option("--collection <dir>", "Bruno collection directory (default: configured root, SAPTOOLS_BRUNO_COLLECTION or cwd)")).addOption(new Option("--root <dir>", "Legacy alias for --collection").hideHelp());
|
|
1099
1278
|
registerSetupAppCommand(program);
|
|
1100
1279
|
registerRunCommand(program);
|
|
1101
1280
|
registerUseCommand(program);
|
|
1281
|
+
registerSetRootCommand(program);
|
|
1282
|
+
if (argv.length <= 2) {
|
|
1283
|
+
await launchInteractiveMenu(program);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1102
1286
|
await program.parseAsync([...argv]);
|
|
1103
1287
|
}
|
|
1104
1288
|
try {
|