npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@saptools/bruno

v0.3.1

Published

Smart runner for Bruno — CF-aware env metadata, automatic token injection, and shorthand paths

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.

npm version downloads license node types build

Install · Quick Start · CLI · API · FAQ · Roadmap


⚡ At a glance

$ saptools-bruno use ap10/demo-prod/api/orders-srv
✔ Default context set to ap10/demo-prod/api/orders-srv

$ saptools-bruno run --env dev
▶ 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 like qa-eu). Every env file is seeded with __cf_* metadata so the runner knows where to fetch a token.
  • 🧭 Shorthand pathsregion/org/space/app[/folder/file.bru] expands to the right filesystem path. No more cd-ing through nested folders.
  • 🔐 Automatic XSUAA tokens — every run fetches (or reuses) a cached token via @saptools/cf-xsuaa, writes it into the selected env file as accessToken, and still injects it for bru at execution time.
  • 📦 Bundled Bruno CLI fallback — if bru is already on your PATH, saptools-bruno uses it. If not, it falls back to the bundled @usebruno/cli.
  • 🎯 Default contextsaptools-bruno use <shorthand> pins a target so subsequent run calls need zero arguments. Feels like cf target for 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 bru binary + 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 6
saptools-bruno use ap10/demo-prod/api/orders-srv
saptools-bruno run --env dev

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/bruno now bundles @usebruno/cli automatically, but still prefers an existing bru on PATH if you already have one installed.

npm does install @saptools/cf-sync as a dependency of @saptools/bruno, but a global install of @saptools/bruno does not expose the transitive cf-sync binary on your PATH. For global CLI usage, install @saptools/cf-sync explicitly. For project-local usage, invoke the local bin through your package manager, for example npx --no-install cf-sync sync or pnpm exec cf-sync sync.


🚀 Quick Start

# 1. Sync your CF landscape once
cf-sync sync

# 2. Scaffold an app folder with seeded __cf_* metadata
saptools-bruno setup-app

# 3. Pin a default CF context so future runs need zero args
saptools-bruno use ap10/my-org/dev/my-srv

# 4. Run — XSUAA token is fetched, written to the env file, and injected automatically
saptools-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

🏗️ saptools-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.

saptools-bruno setup-app
saptools-bruno --collection ./collections setup-app

[!TIP] --collection only applies to the current command. If you omit it, saptools-bruno falls back to $SAPTOOLS_BRUNO_COLLECTION, then to your current working directory.

[!IMPORTANT] setup-app reads the cached CF landscape prepared by cf-sync. If the cache is missing or stale, run cf-sync sync first.

What you get

  • An app-level bruno.json inside region__<key>/org__<org>/space__<space>/<app>/
  • Folder tree: region__<key>/org__<org>/space__<space>/<app>/environments/
  • One .bru env file per selection, each seeded with __cf_region, __cf_org, __cf_space, __cf_app, environment, and an empty baseUrl
  • 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 like qa-eu or uat.us, it appears back in the same checklist already selected so you can review the full set before finishing.

▶️ saptools-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
saptools-bruno run --env dev

# Explicit shorthand
saptools-bruno run ap10/my-org/dev/my-srv --env dev

# Drill into a subfolder or a single file
saptools-bruno run ap10/my-org/dev/my-srv/users/get-all.bru --env dev

# Or pass a real filesystem path (absolute or relative)
saptools-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 .bru env file
  • spawns bru run <target> --env <name> --env-var accessToken=<token>

🎯 saptools-bruno use

Pin a default CF context so run can be called without arguments.

saptools-bruno use ap10/my-org/dev/my-srv
saptools-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.


🧑‍💻 Programmatic Usage

import {
  buildRunPlan,
  readContext,
  runBruno,
  scanCollection,
  setupApp,
  useContext,
} from "@saptools/bruno";

// 1. Scaffold an app folder (BYO prompts — perfect for headless/CI)
const result = await setupApp({
  root: "./collections",
  prompts: {
    selectRegion: async (choices) => choices[0]!.value,
    selectOrg:    async (choices) => choices[0]!.value,
    selectSpace:  async (choices) => choices[0]!.value,
    selectApp:    async (choices) => choices[0]!.value,
    confirmCreate: async () => true,
    selectEnvironments: async ({ common }) => [...common, "qa-eu"],
  },
});
console.log(`Created ${result.environments.length} env files at ${result.appPath}`);

// 2. Pin a default context for later runs
await useContext({ shorthand: "ap10/my-org/dev/my-srv" });

// 3. Run Bruno — token is fetched and injected for you
const run = await runBruno({
  root: "./collections",
  target: "ap10/my-org/dev/my-srv",
  environment: "dev",
});
process.exit(run.code);

// 4. Need the plan without spawning `bru`? (CI dry-runs, IDE integrations)
const plan = await buildRunPlan({
  root: "./collections",
  target: "ap10/my-org/dev/my-srv",
  environment: "dev",
});
console.log(plan.bruArgs);
// → ["run", "--env", "dev", "--env-var", "accessToken=..."]

// 5. Walk a whole collection to build a UI tree
const tree = await scanCollection("./collections");
console.log(tree.regions.map((r) => r.key));

// 6. Inspect the active default context
const ctx = await readContext();
console.log(ctx?.app);

| Export | Description | | --- | --- | | setupApp(options) | Interactive app-folder scaffolder with pluggable prompts | | COMMON_ENVIRONMENTS | Default environment-name suggestions (local, dev, staging, prod) | | runBruno(options) | Build a plan and spawn bru run with token injected | | buildRunPlan(options) | Build the plan (args, cwd, env file, token) without spawning | | useContext({ shorthand, verify }) | Pin a default region/org/space/app context | | readContext() | Read the pinned context, or undefined | | writeContext(ctx) | Persist a new default context | | scanCollection(root) | Walk the folder tree and return a typed region → org → space → app → env view | | parseShorthandPath(shorthand) | Split region/org/space/app[/file] into a typed ref | | parseBruEnvFile(raw) / writeBruEnvFile(...) | Minimal .bru env reader/writer | | readCfMetaFromFile(path) / writeCfMetaToFile(path, ref) | Round-trip __cf_* vars in an env file |


📁 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 | ❌ | ❌ | ❌ | ❌ | ✅ | | saptools-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 bru binary, 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. saptools-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-app with 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
  • [ ] saptools-bruno doctor — diagnose missing __cf_* vars, stale tokens, missing bru
  • [ ] saptools-bruno migrate — move collections from a flat layout into the CF-aware layout
  • [ ] First-class --reporter json support 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


🤝 Contributors


👨‍💻 Author

dongtran

📄 License

MIT


Made with ❤️ to make your work life easier!