@saptools/bruno
A command line that runs Bruno collections against XSUAA-protected apps on Cloud FoundrySAP BTP, Cloud Foundry RuntimeThe Cloud Foundry runtime of SAP BTP: memory for applications in a Cloud Foundry org, run from SAP's Java, Node.js and Python buildpacks or community buildpacks and managed with the standard Cloud Foundry tools.sap official, btp, fetching the app's token and writing it into the selected environment file before each run.
Bruno does not mint OAuth2 tokens, so running a collection with bru run against a service behind XSUAASAP Authorization and Trust Management serviceThe OAuth server behind a BTP application: issues the JWT the app checks, holds the scopes and role collections it checks against, and brokers trust to an identity provider. Bound, never installed.sap official, btp means pasting a fresh token into its environment file each time the last one expires. bruno run gets the token through @saptools/cf-xsuaa@saptools/cf-xsuaaA command line and Node.js API that reads the XSUAA binding of a Cloud Foundry app and prints an OAuth2 client_credentials token for it, cached until it expires.independent, btp, writes it as accessToken into the chosen .bru environment file and passes it to bru with --env-var, using the bru on the PATH or else the bundled @usebruno/cli. bruno setup-app scaffolds a region__<key>/org__<org>/space__<space>/<app> folder from the landscape cached by @saptools/cf-sync@saptools/cf-syncA command line and Node.js API that logs into Cloud Foundry once, walks every region, org, space and app the user can reach, and caches that topology as JSON for the other @saptools packages to read.independent, btp, with __cf_* variables in each environment file that tell the runner which app's token to fetch, and bruno use pins a default target. The requests and assertions stay ordinary Bruno files; what it adds is the token and the folder layout.
Facts
Where it sits
Who is behind it
In the atlas: maintains 18, contributes to 18@dongitranmaintainer
What it declares
Every artifact
- Type
- CLI
- Origin
- Independently maintained
- Runs on
- Cloud Foundry
As a CLI
- Distributed through
- npm
Connections
Proving a service still does what it did, without a deployment
Cloud Foundry topology cache for the other @saptools packages
OAuth2 token fetcher for a Cloud Foundry app's XSUAA binding
A cross-platform JavaScript runtime that servers, command line tools and scripts written in JavaScript are run with. For the things here, the prerequisite a repository names when its build is a set of npm scripts.
Download
Install
npm add @saptools/brunoDerived from the package name. No recipe recorded; the README has the configuration.
Architecture
Download
README
quoted from dongitran/saptools under MIT, read🎯 @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
Without @saptools/bruno |
With @saptools/bruno |
|---|---|
|
That'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 dev
After setup-app, your workspace looks like this:
.
└── region__ap10/
└── org__my-org/
└── space__dev/
└── my-srv/
├── bruno.json
└── environments/
├── dev.bru
└── prod.bru
Each 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/collection
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.
🏗️ 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>
🔬 Shape of an env file after setup-app
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
Why does Bruno need a wrapper — can't I just call bru run?
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.
Do I have to re-run setup-app when CF changes?
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.
How do I add an env that isn't local / dev / staging / prod?
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.
Where does the token come from?
@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.
Can I use this without the CF-structured folder layout?
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.
How do I use this in CI?
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
-
setup-appwith selectable environments and custom-name input - Shorthand path resolution (
region/org/space/app[/file]) - Default CF context via
use - 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:e2e
The 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 command
If 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/.
History
Others providing
Compare all 8 on the Testing page →- com.sap.cds:cds-services-bomcom.sap.cds:cds-services-bomThe Maven bill of materials for CAP Java, pinning every com.sap.cds module in a release, from the API and core runtime to the Spring Boot starters and the CQN engine, to the same version.sap official, capMaven bill of materials pinning CAP Java module versionsfrom CAP
- @cap-js/cds-test@cap-js/cds-testTest support for CAP Node.js, installed as a dev dependency: it starts the project's server for the test, hands the test an HTTP client bound to it, and carries the assertions.sap official, capTest support for CAP Node.js: server start, HTTP client, assertionsfrom CAP
- cds-cucumbercds-cucumberBehaviour-driven tests for CAP applications: Gherkin scenarios that drive a running service rather than assertions written against handlers.community org, capGherkin scenarios driving a running CAP servicefrom CAP
- sapbrunosapbrunoA terminal UI that runs Bruno collections against a CAP service, fetching an OAuth2 client_credentials token before each run so collections that fail with 401 under bru run work in CI.independent, capTerminal UI running Bruno collections against a CAP servicefrom CAP
- cds-launchpad-plugincds-launchpad-pluginIndexes every SAPUI5 application in a workspace into one sandbox launchpad served by the CDS server, so all of them can be opened from a single page while developing.independent, capSandbox launchpad for all SAPUI5 apps in a CAP workspacefrom CAP
- enforce-export-coverageenforce-export-coverageA static-analysis command line that checks every exported TypeScript function has a corresponding test, without running the test suite.independent, generalStatic check that each exported TypeScript function has a testfrom General
- @sap-ux/fe-mockserver-plugin-cds@sap-ux/fe-mockserver-plugin-cdsLets the Fiori Elements mock server take a CDS file as its metadata source, so a UI is developed against the real model before a service exists.sap official, capFiori Elements mock server plugin taking a CDS file as metadatafrom CAP
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
History in the atlas
- First seen.
For your agent
- Ref
artifact:saptools-bruno- JSON
- /artifacts/saptools-bruno.json
- MCP
- Connecting an agent