@saptools/bruno
v0.4.1
Published
Smart runner for Bruno — CF-aware env metadata, automatic token injection, and shorthand paths
Maintainers
Readme
🎯 @saptools/bruno
A smart runner for Bruno collections on SAP BTP Cloud Foundry.
Scaffold a CF-aware collection. Resolve requests by region/org/space/app shorthand. Let every bru run start with a fresh XSUAA token already injected and written back to the selected env file — no more pasting Authorization headers into env files, no more manual token refresh dances.
Install · Quick Start · CLI · FAQ · Roadmap
⚡ At a glance
$ bruno use ap10/demo-prod/api/orders-srv
✔ Default context set to ap10/demo-prod/api/orders-srv
$ bruno
# chọn Run .bru file (folder tree)
# duyệt cây thư mục rồi chọn file .bru để chạy
▶ bru run --env dev --env-var accessToken=eyJhbGciOi… (cwd=…/orders-srv)
Running Folder Recursively
✓ GET /orders 204 OK 54ms
✓ POST /orders 201 Created 120ms
✓ GET /orders/:id 200 OK 48ms
All assertions passed ✓You just ran Bruno against a production-grade XSUAA-protected service without ever touching a token. That's the entire pitch.
✨ Features
- 🏗️ 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 likeqa-eu). Every env file is seeded with__cf_*metadata so the runner knows where to fetch a token. - 🧭 Shorthand paths —
region/org/space/app[/folder/file.bru]expands to the right filesystem path. No morecd-ing through nested folders. - 🔐 Automatic XSUAA tokens — every
runfetches (or reuses) a cached token via@saptools/cf-xsuaa, writes it into the selected env file asaccessToken, and still injects it forbruat execution time. - 📦 Bundled Bruno CLI fallback — if
bruis already on yourPATH,brunouses it. If not, it falls back to the bundled@usebruno/cli. - 🎯 Default context —
bruno use <shorthand>pins a target so subsequentruncalls need zero arguments. Feels likecf targetfor Bruno. - 🧩 CLI & typed API — every command has a zero-config Node.js equivalent. Full TypeScript definitions shipped. Bring your own prompts for headless/CI use.
- 🧪 Fully tested — unit tests plus offline e2e coverage (stub
brubinary + fixture CF snapshot). No network required in CI. - 🪶 Small + boring — a small runtime surface, no background daemons, no plugin system, no magic.
😩 Before → 😎 After
# 1. Find the service creds on Cockpit
# 2. cf target -o demo-prod -s api
# 3. cf create-service-key orders-srv bruno-key
# 4. cf service-key orders-srv bruno-key
# 5. Copy clientid / clientsecret / url
# 6. curl -X POST $URL/oauth/token \
# -u $CLIENT_ID:$CLIENT_SECRET \
# -d grant_type=client_credentials
# 7. Copy access_token
# 8. Paste into environments/dev.bru
# 9. bru run --env dev
# 10. Token expires → goto 6bruno use ap10/demo-prod/api/orders-srv
bruno run --env devThat's it. Token is cached, refreshed on expiry, written back to the env file, and injected automatically.
📦 Install
# Global CLIs
npm install -g @saptools/cf-sync @saptools/bruno
# Or as a project dependency
npm install @saptools/bruno
# pnpm add @saptools/bruno
# yarn add @saptools/bruno[!NOTE] Requires Node.js ≥ 20 and a cached CF landscape from
@saptools/cf-sync.@saptools/brunonow bundles@usebruno/cliautomatically, but still prefers an existingbruonPATHif you already have one installed.
npmdoes install@saptools/cf-syncas a dependency of@saptools/bruno, but a global install of@saptools/brunodoes not expose the transitivecf-syncbinary on yourPATH. For global CLI usage, install@saptools/cf-syncexplicitly. For project-local usage, invoke the local bin through your package manager, for examplenpx --no-install cf-sync syncorpnpm exec cf-sync sync.
🚀 Quick Start
# 1. Sync your CF landscape once
cf-sync sync
# 2. Scaffold an app folder with seeded __cf_* metadata
bruno setup-app
# 3. Pin a default CF context so future runs need zero args
bruno use ap10/my-org/dev/my-srv
# 4. Run — XSUAA token is fetched, written to the env file, and injected automatically
bruno run --env devAfter setup-app, your workspace looks like this:
.
└── region__ap10/
└── org__my-org/
└── space__dev/
└── my-srv/
├── bruno.json
└── environments/
├── dev.bru
└── prod.bruEach env file starts with the CF coordinates needed for token lookup:
vars {
__cf_region: ap10
__cf_org: my-org
__cf_space: dev
__cf_app: my-srv
environment: dev
baseUrl:
}Your .bru requests reference {{accessToken}} like any other Bruno variable — the runner refreshes it into the selected env file before spawning Bruno.
🧰 CLI
🗂️ bruno (interactive tree mode)
Chạy bruno không kèm subcommand sẽ mở menu tương tác:
- Run
.brufile - Set default context
- Setup app folder
- Set Bruno root folder
Ở 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.
📌 bruno set-root [dir]
Lưu Bruno root folder mặc định vào ~/.saptools/bruno/cli-state.json.
bruno set-root /path/to/your/collectionSau 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.
🏗️ bruno setup-app
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.
bruno setup-app
bruno --collection ./collections setup-app[!TIP]
--collectiononly applies to the current command. If you omit it,brunofalls back to$SAPTOOLS_BRUNO_COLLECTION, then to your current working directory.
[!IMPORTANT]
setup-appreads the cached CF landscape prepared bycf-sync. If the cache is missing or stale, runcf-sync syncfirst.
What you get
- An app-level
bruno.jsoninsideregion__<key>/org__<org>/space__<space>/<app>/ - Folder tree:
region__<key>/org__<org>/space__<space>/<app>/environments/ - One
.bruenv file per selection, each seeded with__cf_region,__cf_org,__cf_space,__cf_app,environment, and an emptybaseUrl - Existing env files are preserved; only missing
__cf_*vars are patched back in
[!TIP] 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 likeqa-euoruat.us, it appears back in the same checklist already selected so you can review the full set before finishing.
▶️ bruno run
Run a Bruno request or folder, refreshing accessToken in the chosen env file and auto-injecting the same token for the current execution.
# Use the default context
bruno run --env dev
# Explicit shorthand
bruno run ap10/my-org/dev/my-srv --env dev
# Drill into a subfolder or a single file
bruno run ap10/my-org/dev/my-srv/users/get-all.bru --env dev
# Or pass a real filesystem path (absolute or relative)
bruno run ./region__ap10/org__my-org/space__dev/my-srv --env dev| Flag | Description |
| --- | --- |
| -e, --env <name> | Environment name (default: current context or first discovered env) |
| --collection <dir> | Bruno collection directory (default: $SAPTOOLS_BRUNO_COLLECTION or cwd) |
Under the hood this:
- fetches or reuses a token via
@saptools/cf-xsuaa - writes
accessToken: <token>into the selected.bruenv file - spawns
bru run <target> --env <name> --env-var accessToken=<token>
🎯 bruno use
Pin a default CF context so run can be called without arguments.
bruno use ap10/my-org/dev/my-srv
bruno use ap10/my-org/dev/my-srv --no-verify| Flag | Description |
| --- | --- |
| --no-verify | Skip verifying the shorthand against the cached CF structure |
The context lives at ~/.saptools/bruno-context.json.
📁 Folder Layout
All state lives under your home directory or your collection root:
~/.saptools/bruno-context.json # pinned region/org/space/app + updatedAt
<root>/
├── bruno.json
└── region__<key>/
└── org__<org>/
└── space__<space>/
└── <app>/
├── environments/
│ ├── dev.bru # vars { __cf_region, __cf_org, ... }
│ └── prod.bru
└── <your .bru requests>vars {
__cf_region: ap10
__cf_org: my-org
__cf_space: dev
__cf_app: my-srv
environment: dev
baseUrl:
}The __cf_* vars drive XSUAA lookup. run adds accessToken on the fly via bru --env-var, so your requests can simply reference {{accessToken}}.
[!IMPORTANT] Prefer the CLI or the exported APIs over hand-editing these files — the on-disk format is parsed and rewritten by
setup-app, and re-setup will patch missing__cf_*vars back in.
🌱 Environment Variables
| Variable | Purpose |
| --- | --- |
| SAPTOOLS_BRUNO_COLLECTION | Default Bruno collection directory when --collection isn't passed |
| SAPTOOLS_ACCESS_TOKEN | Exported to the spawned bru process (alongside --env-var accessToken=…) |
| SAP_EMAIL / SAP_PASSWORD | Consumed by @saptools/cf-xsuaa when the token cache is cold |
🧭 How it compares
| Approach | XSUAA handling | Shorthand paths | CF-aware scaffolding | Cache/refresh | Works in CI |
| --- | :-: | :-: | :-: | :-: | :-: |
| Hand-edit environments/*.bru | ❌ manual | ❌ | ❌ | ❌ | ❌ |
| Bruno GUI OAuth2 | ✅ | ❌ | ❌ | partial | ❌ (GUI) |
| bru run alone | ❌ | ❌ | ❌ | ❌ | ✅ |
| bruno | ✅ automatic | ✅ | ✅ | ✅ | ✅ |
🧪 Quality
- 74 unit tests via Vitest (strict TS · ESLint · 80%+ branch coverage on core flows)
- 4 end-to-end tests via Playwright's test runner — stubbed
brubinary, fixture CF snapshot, zero network - Type-checked under
strict + exactOptionalPropertyTypes + noUncheckedIndexedAccess— the strictest realistic TS profile - CI on every push (lint · typecheck · build · unit · e2e ·
npm pack --dry-run) - npm publishes with provenance via GitHub OIDC trusted publishing
❓ FAQ
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.
Only when you add a new app folder. setup-app on an existing app is idempotent — it pre-checks existing envs, preserves their contents, and patches missing __cf_* vars back in.
Choose Add custom environment inside the checkbox list. After you type any [A-Za-z0-9._-]+ name (for example qa-eu or uat.us), the prompt returns to the same checklist with that new environment already selected.
@saptools/cf-xsuaa. runBruno calls getTokenCached({ region, org, space, app }) and reuses the local cache until it expires. You can inject your own fetcher via the getTokenCached option when using the API.
run accepts both shorthand (region/org/space/app/...) and real filesystem paths. However, __cf_region/__cf_org/__cf_space/__cf_app must be present in the env file — those are what drive the XSUAA lookup. Run setup-app once to bootstrap them.
Use the programmatic API with your own prompt stubs (every field just returns the value you want), or drive the CLI after injecting SAP_EMAIL / SAP_PASSWORD so the token cache can be populated on first run. The e2e suite of this repo is itself a CI-safe example.
🗺️ Roadmap
- [x]
setup-appwith selectable environments and custom-name input - [x] Shorthand path resolution (
region/org/space/app[/file]) - [x] Default CF context via
use - [x] Offline e2e via stubbed
bru - [ ]
bruno doctor— diagnose missing__cf_*vars, stale tokens, missingbru - [ ]
bruno migrate— move collections from a flat layout into the CF-aware layout - [ ] First-class
--reporter jsonsupport for piping test results into dashboards
Have an idea? Open an issue — the roadmap is driven by real use.
🛠️ Development
From the monorepo root:
pnpm install
pnpm --filter @saptools/bruno build
pnpm --filter @saptools/bruno typecheck
pnpm --filter @saptools/bruno test:unit
pnpm --filter @saptools/bruno test:e2eThe e2e suite uses a stub bru binary and fixture CF snapshots, so it runs fully offline. Contributions, bug reports, and feature requests are all welcome — see the issues tab.
🌐 Related
- ☁️
@saptools/cf-sync— sync every region / org / space / app into a single cached JSON file - 🔐
@saptools/cf-xsuaa— XSUAA credentials and cached OAuth2 tokens for any CF app - 🐛
@saptools/cf-debugger— open an SSH debug tunnel to any CF Node.js app from your terminal - 🗂️ saptools monorepo — the full toolbox
🤝 Contributors
👨💻 Author
dongtran ✨
📄 License
MIT
Made with ❤️ to make your work life easier!
Updates
Every command first checks npm for a newer @saptools/bruno (at most once an hour, one small request
with a 2-second timeout) and, when one exists, installs that exact version with the package manager
that owns the running binary and re-runs the command you typed on the new version. Both steps are
announced on stderr; nothing is printed when the install is already current:
saptools-bruno: updating 0.4.0 -> 0.5.0 ...
saptools-bruno: updated to 0.5.0; re-running the commandIf the install cannot complete, one stderr line gives the manual command and the command runs on the
installed version; that version is not retried for a day. saptools-bruno self-update forces the check and
install now; saptools-bruno self-update --check only reports.
| Control | Effect |
| --- | --- |
| SAPTOOLS_AUTO_UPDATE=on\|notify\|off | on (default) installs and re-runs; notify prints the manual command once per version; off never checks. Applies to every @saptools CLI. |
| BRUNO_AUTO_UPDATE | same values, this CLI only; wins over the global variable |
| SAPTOOLS_UPDATE_INTERVAL_MINUTES | minutes between checks (default 60; 0 checks on every run) |
| SAPTOOLS_NPM_REGISTRY | registry to check and install from (default: npm's configured registry, then npmjs) |
| SAPTOOLS_UPDATE_DEBUG=1 | explain on stderr why nothing happened |
The updater switches itself off in CI (CI set), under NODE_ENV=test or NO_UPDATE_NOTIFIER, when
the binary runs from a source checkout, an npm link or an npx cache, and inside the re-run itself.
It never writes to stdout, never asks for input, never uses sudo, and never moves onto a prerelease.
Its state lives in ~/.saptools/updates/.
