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/cf-debugger

v0.1.7

Published

Open an SSH debug tunnel to a SAP BTP Cloud Foundry app's Node.js inspector — from any terminal, no IDE required.

Downloads

1,254

Readme

🐛 @saptools/cf-debugger

Open a Node.js inspector tunnel to any SAP BTP Cloud Foundry app — in one command.

Signal the remote process, enable SSH if needed, forward 9229 to a free local port, and hand you back a ready-to-attach debugger — with first-class support for multiple concurrent tunnels across terminals.

npm version license node install size types

InstallQuick StartCLIAPIHow it worksFAQ


✨ Features

  • 🚀 One-shot tunnel — auth, target, SSH-enable, USR1 signal, port forward, readiness probe — all hidden behind cf-debugger start
  • 🧵 Multi-debugger concurrency — run N debuggers for N apps at once; each session gets its own local port, isolated CF_HOME, and an entry in the shared state file
  • 🛡️ Duplicate-session protection — the same region/org/space/app cannot be debugged twice simultaneously (returns SESSION_ALREADY_RUNNING)
  • 🧹 Crash-proof state — stale session entries are auto-pruned on next read using PID liveness checks
  • 🔌 Deterministic ports — auto-assigned from a safe range (20000–20999), or pick your own with --port
  • 🧩 CLI & typed API — every command has a zero-config Node.js equivalent with full TypeScript definitions
  • 🪶 Small + boring — one runtime dep (commander), no daemons, no magic

📦 Install

# Global CLI
npm install -g @saptools/cf-debugger

# Or as a dependency
npm install @saptools/cf-debugger
# pnpm add @saptools/cf-debugger
# yarn add @saptools/cf-debugger

[!NOTE] Requires Node.js ≥ 20 and the official cf CLI on PATH (v8 recommended).


🚀 Quick Start

# 1. Export your SAP SSO credentials (used for `cf auth` under the hood)
export SAP_EMAIL="[email protected]"
export SAP_PASSWORD="your-sap-password"

# 2. Open a debug tunnel for one app
cf-debugger start \
  --region eu10 \
  --org my-org \
  --space dev \
  --app my-app \
  --verbose

# → Debugger ready for my-app (eu10/my-org/dev).
#     Local port:  20142
#     Remote port: 9229
#     Session id:  01HXYZ...
#     PID:         83421
#   Press Ctrl+C to stop.

# 3. Attach your IDE (VSCode, Chrome DevTools, ...) to localhost:20142

Ctrl+C cleans everything up — the SSH subprocess is killed, the local port is released, and the session is removed from the shared state file.


🧰 CLI

▶️ cf-debugger start

Open a tunnel for one app and keep running until interrupted.

cf-debugger start --region eu10 --org my-org --space dev --app my-app
cf-debugger start --region eu10 --org my-org --space dev --app my-app --port 9230
cf-debugger start --region eu10 --org my-org --space dev --app my-app --timeout 60 --verbose

| Flag | Description | | --- | --- | | --region <key> | Required. CF region key (e.g. eu10, ap10, us10) | | --org <name> | Required. CF org name | | --space <name> | Required. CF space name | | --app <name> | Required. CF app name | | --port <number> | Preferred local port (auto-assigned in 20000–20999 if omitted) | | --timeout <seconds> | Tunnel-ready timeout (default: 30) | | --verbose | Print every status transition |

⏹️ cf-debugger stop

Stop a specific session or everything at once.

cf-debugger stop --region eu10 --org my-org --space dev --app my-app
cf-debugger stop --session-id 01HXYZABCD...
cf-debugger stop --all

| Flag | Description | | --- | --- | | --region / --org / --space / --app | Match session by key (all four required together) | | --session-id <id> | Match session by its ID | | --all | Stop every active session on this machine |

📋 cf-debugger list

Print every active session this machine owns as JSON.

cf-debugger list | jq '.[] | {app, localPort, status}'

🔍 cf-debugger status

Print one session by key (or null if no active session matches).

cf-debugger status --region eu10 --org my-org --space dev --app my-app

🧑‍💻 Programmatic Usage

import {
  startDebugger,
  stopDebugger,
  listSessions,
  getSession,
  resolveApiEndpoint,
} from "@saptools/cf-debugger";

const handle = await startDebugger({
  region: "eu10",
  org: "my-org",
  space: "dev",
  app: "my-app",
  email: process.env["SAP_EMAIL"],
  password: process.env["SAP_PASSWORD"],
  verbose: true,
  onStatus: (status, message) => {
    console.log(`[${status}]`, message ?? "");
  },
});

console.log(`Attach your debugger to localhost:${handle.session.localPort}`);

// Later — shut the tunnel down and clean up state:
await handle.dispose();

| Export | Description | | --- | --- | | startDebugger(options) | Open a tunnel; returns a DebuggerHandle | | stopDebugger({ sessionId?, key? }) | Stop one session by id or by key | | stopAllDebuggers() | Stop every session owned by this process/machine | | listSessions() | Return every live session as ActiveSession[] | | getSession(key) | Return one session matching { region, org, space, app } | | resolveApiEndpoint(key, override?) | Map a region key to its API endpoint | | sessionKeyString(key) | Stable string form of a session key | | CfDebuggerError | Rich error class with typed code |

| Code | When | | --- | --- | | MISSING_CREDENTIALS | No SAP_EMAIL / SAP_PASSWORD in env or options | | SESSION_ALREADY_RUNNING | A session already exists for the same region/org/space/app | | CF_LOGIN_FAILED | cf api / cf auth rejected the credentials | | CF_TARGET_FAILED | Org or space not reachable | | SSH_NOT_ENABLED | SSH disabled at space or app level and could not be enabled | | USR1_SIGNAL_FAILED | Remote kill -s USR1 could not find the node PID | | TUNNEL_NOT_READY | Inspector didn't respond on port 9229 before timeout | | PORT_UNAVAILABLE | Preferred local port is taken and could not be freed |


🔭 How it works

┌────────────────────┐    1. cf api + cf auth (retry x3)
│ cf-debugger start  │    2. cf target -o <org> -s <space>
│  region/org/       │    3. cf ssh-enabled <app>
│  space/app         │ ─► 4. cf enable-ssh + cf restart (only if needed)
└────────────────────┘    5. cf ssh <app> -c 'kill -s USR1 $(pidof node)'
          │               6. cf ssh <app> -N -L <localPort>:localhost:9229
          ▼               7. TCP probe localhost:<localPort> until ready
    DebuggerHandle        8. Save ActiveSession to ~/.saptools/cf-debugger-state.json

Each step emits a status update (logging-in, targeting, ssh-enabling, signaling, tunneling, ready, …). --verbose prints them live; the programmatic API exposes the same stream via onStatus.

Concurrency model

  • Atomic state~/.saptools/cf-debugger-state.json is written via temp-file + rename, guarded by a short-lived .lock file (open(..., "wx")).
  • Port allocation — on register, ports already used by other sessions are excluded; the first free port in 20000–20999 wins.
  • Isolated CF homes — each session runs with its own CF_HOME (~/.saptools/cf-debugger-homes/<sessionId>/), so cf target in one terminal can't clobber another.
  • Stale pruning — reading the state file checks every recorded PID with process.kill(pid, 0); dead entries are dropped before returning the list.
  • Duplicate guard — trying to start a second tunnel for the same region/org/space/app fails fast with SESSION_ALREADY_RUNNING instead of racing for the port.

📁 Output Files

All state lives under your home directory:

~/.saptools/cf-debugger-state.json       # active sessions (atomic JSON)
~/.saptools/cf-debugger-state.lock       # short-lived lock file
~/.saptools/cf-debugger-homes/<id>/      # per-session isolated CF_HOME
{
  "version": 1,
  "sessions": [
    {
      "sessionId": "01HXYZABCD...",
      "region": "eu10",
      "org": "my-org",
      "space": "dev",
      "app": "my-app",
      "localPort": 20142,
      "remotePort": 9229,
      "pid": 83421,
      "status": "ready",
      "startedAt": "2026-04-18T00:00:00.000Z"
    }
  ]
}

[!IMPORTANT] Prefer the CLI commands (list / status) or the exported APIs over parsing these files — the on-disk format is an implementation detail.


❓ FAQ

Yes — that's a core feature. Open two terminals, pick two different apps, and both tunnels come up on separate local ports. cf-debugger list shows you everything at once. The only thing you can't do is debug the same app twice in parallel.

Only if SSH is disabled. If it is, cf-debugger runs cf enable-ssh + cf restart to turn it on — otherwise it only sends a SIGUSR1 to the Node.js process (which tells Node to start its inspector). No code, no env vars, no manifest is touched.

The TCP probe will fail on reconnect and the CLI will exit with the SSH child's code. The state entry is removed on exit, so the next start for the same app works immediately.

Yes — pass --port 9230 (CLI) or preferredPort: 9230 (API). If it's occupied by a non-tunnel process, cf-debugger will try to free it once; if another tunnel already owns it, you'll get PORT_UNAVAILABLE.

You can, but it's designed for interactive debugging. CI usually wants a short-lived request against the running app, not a persistent inspector tunnel — consider cf ssh -L directly for that case.


🛠️ Development

From the monorepo root:

pnpm install
pnpm --filter @saptools/cf-debugger build
pnpm --filter @saptools/cf-debugger typecheck
pnpm --filter @saptools/cf-debugger test:unit
pnpm --filter @saptools/cf-debugger test:e2e

The e2e suite hits live SAP BTP CF. Set CF_DEBUGGER_E2E_REGIONS=eu10,ap10 (plus SAP_EMAIL / SAP_PASSWORD) to restrict which regions it searches for a running app.


🌐 Related


👨‍💻 Author

dongtran

📄 License

MIT


Made with ❤️ to make your work life easier!