# ๐ `@saptools/cf-inspector`
**Set breakpoints, capture variable snapshots, and evaluate expressions on a remote Node.js process โ over the Chrome DevTools Protocol, no IDE required.**
Built so an AI agent (or a CI job) can drive a debugger from a single shell command. Pairs with [`@saptools/cf-debugger`](https://www.npmjs.com/package/@saptools/cf-debugger) when the target lives behind a Cloud Foundry SSH tunnel.
[](https://www.npmjs.com/package/@saptools/cf-inspector)
[](./LICENSE)
[](https://nodejs.org)
[Install](#-install) โข [Quick Start](#-quick-start) โข [CLI](#-cli) โข [How it works](#-how-it-works)
---
> **Debug-session safety:** only one local `cf-inspector` process may actively
> debug a given target at a time. Concurrent CDP debugger clients can receive
> the same pause and race `Debugger.resume`, disrupting the target and real
> application traffic. A second local invocation now fails before connecting
> with `TARGET_ALREADY_DEBUGGED`; wait for the named owner process to finish.
> Locks left by dead processes are reclaimed automatically. Debuggers running
> on another machine or outside `cf-inspector` cannot be detected by this local
> guard and must still be coordinated operationally.
## โจ Features
- ๐ฏ **One-shot snapshot** โ `cf-inspector snapshot --bp src/handler.ts:42` sets the breakpoint, waits for it to hit, captures requested expressions, auto-resumes, prints JSON, exits
- โ
**Conditional breakpoints** โ `--condition 'req.userId === "abc"'` only pauses when the predicate is truthy
- ๐ข **Hit-count breakpoints** โ `--hit-count 5` skips the first N โ 1 hits and pauses on the Nth, on every command (snapshot, log, watch)
- ๐ญ **Multi-breakpoint** โ repeat `--bp` to race several locations; first hit wins
- ๐ช **Stack capture** โ `--stack-depth N --stack-captures 'this, args'` walks call frames and evaluates expressions per frame
- ๐ **Watch streaming** โ `cf-inspector watch --bp file:line --capture user.id --duration 30` re-captures on every hit and emits JSON Lines (the streaming counterpart of `snapshot`)
- ๐ฅ **Exception breakpoints** โ `cf-inspector exception --type uncaught --capture err.message` pauses on the next thrown error and materializes the exception value
- ๐ก **Non-pausing logpoints** โ `cf-inspector log --at file:line --expr 'JSON.stringify({โฆ})'` streams JSON Lines as the line executes without pausing the inspectee, with optional `--condition`, `--hit-count`, and `--max-events`
- ๐ก๏ธ **Read-only capture guard** โ snapshot, watch, and exception captures use V8's side-effect analysis by default; `--allow-mutation` is an explicit escape hatch
- ๐งต **Automatic worker fan-out** โ snapshot, watch, exception, and log attach to the main isolate plus every current or newly-spawned NodeWorker; explicit selectors remain available for pinning
- ๐ฆ **Machine-readable readiness** โ opt into a versioned `breakpoint-armed` stderr event before triggering external traffic; no prose matching or guessed delay required
- ๐ **Isolate-aware script listing** โ `list-scripts` aggregates the main isolate and every current worker, tagging every script with its isolate
- ๐ง **Agent-friendly** โ JSON-by-default I/O, deterministic shapes, and explicit `truncated`/`originalLength`/`omittedCount` metadata for bounded values
- ๐งญ **Path mapping** โ local `src/handler.ts:42` is matched against the remote URL via a `urlRegex`, with optional `--remote-root` literal or regex (same DSL as `cds-debug`)
- ๐ **Composes with `cf-debugger`** โ pass `--app/--region/--org/--space` and the tunnel is opened automatically; pass `--port` to attach to anything CDP-speaking
- ๐ชถ **Tiny dependency footprint** โ `@saptools/cf-debugger` + `commander` + `ws`, with no heavy CDP framework
- ๐งฉ **Typed API** โ every CLI command has a programmatic equivalent with full TypeScript definitions
---
## ๐ฆ Install
```bash
npm install -g @saptools/cf-inspector
# or
pnpm add @saptools/cf-inspector
cf-inspector --version
```
> [!NOTE]
> Requires **Node.js โฅ 20**.
> Cloud Foundry support uses [`@saptools/cf-debugger`](https://www.npmjs.com/package/@saptools/cf-debugger), which is installed automatically as a normal runtime dependency.
---
## ๐ Quick Start
### Cloud Foundry app (auto-tunnel)
```bash
export SAP_EMAIL=...
export SAP_PASSWORD=...
cf-inspector snapshot \
--region eu10 --org my-org --space dev --app my-srv \
--bp src/handler.ts:42 \
--remote-root 'regex:^/(home/vcap/app|example-root-.*)$'
```
This command internally calls `@saptools/cf-debugger` to open the SSH tunnel, runs the snapshot through it, and tears the tunnel down on exit.
Cloud Foundry targeting is deliberately deterministic: `--app` requires
`--region`, `--org`, and `--space`. The CLI never inherits missing selectors
from ambient `cf target` state. Use `--api-endpoint` only when the selected
region needs an explicit endpoint override.
### Worker behavior
When no isolate selector is passed, `snapshot`, `watch`, `exception`, `log`, and
`check-breakpoint` attach to the main isolate and every NodeWorker under the
selected raw target. Breakpoints are mirrored to workers that attach later, the
first matching pause wins, and JSON/human output identifies the winning
`isolate` as either `{"kind":"main"}` or
`{"kind":"worker","workerId":"โฆ"}`. Losing isolates that also paused are
resumed before the command continues or exits.
Use `--worker-id ` to pin a stable live worker ID from `list-targets`,
`--worker ` for the legacy positional selector, `--target ` to
pin a raw inspector target, or `--main-only` to deliberately ignore workers.
Explicit selectors preserve single-isolate behavior.
### Automation readiness contract
Commands that arm debugger behavior before waiting (`snapshot`, `watch`,
`exception`, and `log`) accept `--ready-event`. The flag writes exactly one
compact JSON object to `stderr` after every session present during initial
arming has completed its CDP setup and immediately before the command waits or
streams:
```json
{"event":"breakpoint-armed","schemaVersion":1,"command":"snapshot","sessions":3,"resolvedLocations":2,"timeoutMs":30000}
```
This is the supported synchronization contract for callers that must start the
CLI before firing an HTTP request, queue job, or other external trigger. Parse
the JSON line and require `event === "breakpoint-armed"` plus
`schemaVersion === 1`, then confirm `command` matches the invocation; do not
poll prose such as `Waiting up to`.
| Field | Contract |
| --- | --- |
| `event` | Always `"breakpoint-armed"` |
| `schemaVersion` | Event schema version; currently `1` |
| `command` | `snapshot`, `watch`, `exception`, or `log` |
| `sessions` | Number of main/worker sessions whose initial arming completed before emission |
| `resolvedLocations` | Total locations V8 had already resolved, or `null` for `exception`; `0` can still represent an accepted URL breakpoint that may resolve when a script loads |
| `timeoutMs` | Snapshot/exception wait or watch per-hit timeout; `null` for log |
Workers attaching after this one-time event are still armed dynamically, but
are not included in its `sessions` or `resolvedLocations` totals. If initial
arming fails or the command is cancelled first, no readiness event is emitted.
With `log --ready-event`, matching events received while the remaining initial
sessions are still arming are neither emitted nor counted, so the log stream
cannot precede its readiness marker.
Without `--ready-event`, stderr/stdout behavior is unchanged. An explicitly
requested event is still emitted with `snapshot --quiet`.
---
## ๐งฐ CLI
### ๐ธ `cf-inspector snapshot`
Set one or more breakpoints, wait for any of them to hit, capture frame metadata and requested expressions, auto-resume, exit.
```bash
# Conditional snapshot โ only pauses for the user we care about
cf-inspector snapshot --port 9229 \
--bp src/handler.ts:42 \
--condition 'req.userId === "abc"' \
--capture 'req.body'
# Multi-breakpoint โ first hit wins (useful when you don't know which path is taken)
cf-inspector snapshot --port 9229 \
--bp src/auth.ts:120 \
--bp src/auth.ts:155 \
--bp src/auth.ts:180 \
--capture 'req.url, this.user'
```
| Flag | Description |
| --- | --- |
| `--port ` | Local port the inspector or tunnel listens on. **Required** unless `--app/--region/--org/--space` are all set |
| `--region/--org/--space/--app` | Explicit Cloud Foundry target. All four are required when `--port` is omitted; ambient `cf target` is never consulted |
| `--api-endpoint ` | Override the API endpoint resolved from `--region` |
| `--target ` | Raw `/json/list` target index (default: `0`) |
| `--worker ` | Nested NodeWorker index reported under the selected raw target by `list-targets` |
| `--worker-id ` | Stable live NodeWorker ID reported by `list-targets` |
| `--main-only` | Attach only to the main isolate and ignore NodeWorkers |
| `--bp ` | **Required.** Source location to break at. Pass multiple times to race several locations โ the first one to hit wins |
| `--condition ` | Native breakpoint condition. It is compile-checked before arming; mutation-shaped conditions require `--allow-mutation` because CDP provides no side-effect guard for native conditions |
| `--hit-count ` | Skip the first N โ 1 hits and only pause on the Nth (combines with `--condition` via logical AND) |
| `--capture ` | Top-level comma-separated expressions evaluated in the paused frame under V8's side-effect guard. Nested commas inside objects, arrays, calls, or strings are preserved. Objects are materialized to JSON strings when serializable |
| `--setup-eval ` | Repeatable, order-preserving global expression evaluated before breakpoint setup. It is mutation-capable and only receives an advisory warning |
| `--stack-depth ` | Walk this many call frames per hit (default: `1`, top frame only). When `> 1`, the result includes a `stack` array |
| `--stack-captures ` | Expressions evaluated on each captured call frame under the same side-effect guard as `--capture` |
| `--allow-mutation` | Disable V8's capture side-effect guard and explicitly allow mutation-shaped native conditions. Heuristic matches are annotated with `mutationRisk: true` |
| `--timeout ` | How long to wait for the breakpoint to hit (default: `30`) |
| `--max-value-length ` | Maximum characters per captured value before truncation (one-shot default: `131072`). Explicit values are honored exactly |
| `--remote-root ` | Optional path-mapping anchor: literal path or `regex:` / `/pattern/flags` |
| `--include-scopes` | Include expanded paused-frame scopes under `topFrame.scopes`. Omitted by default to keep targeted captures concise |
| `--no-json` | Print a human-readable summary instead of JSON |
| `--quiet` | Suppress snapshot progress messages on stderr |
| `--ready-event` | Emit the versioned `breakpoint-armed` JSON event on stderr after every current isolate is armed |
| `--keep-paused` | Skip `Debugger.resume` after capture |
| `--fail-on-unmatched-pause` | Fail immediately if the target pauses somewhere else instead of waiting cooperatively |
Snapshot progress is printed to `stderr` by default, including Cloud Foundry
login/tunnel setup, inspector connection, breakpoint binding, the breakpoint
wait, capture, resume, and cleanup phases. The final JSON document remains the
only content written to `stdout`, so piping it to `jq` or another parser stays
safe. Pass `--quiet` to suppress these progress lines; warnings and errors still
use `stderr`.
Snapshot JSON includes frame metadata and `captures` by default. `topFrame.scopes`
is only present with `--include-scopes` because scope objects can be large and
drown out targeted captures. Values are raw debugger values, so be careful when
sharing logs.
Capture expressions are read-only by default. `cf-inspector` sends
`throwOnSideEffect: true` to V8 for both `--capture` and `--stack-captures`.
Assignments, mutating methods such as `push`, and calls V8 cannot prove pure
are returned as a blocked capture with `blocked: true`, `mutationRisk: true`,
and a `MUTATION_NOT_ALLOWED` error. Pass `--allow-mutation` only when changing
the live inspectee is intentional. The opt-in disables the V8 guard and adds
`mutationRisk: true` when the advisory syntax scan recognizes a likely
mutation; arbitrary function calls can still mutate without being recognized.
`--condition` is different: V8 executes it internally as a native breakpoint
condition, where CDP has no `throwOnSideEffect` option. Mutation-shaped native
conditions are rejected unless `--allow-mutation` is present. `--setup-eval`
and the standalone `eval` command remain mutation-capable by design and emit
advisory warnings for recognizable mutation syntax.
A blocked capture remains a normal additive `CapturedExpression`, so one unsafe
expression does not corrupt the rest of the snapshot:
```json
{"expression":"items.push(1)","error":"MUTATION_NOT_ALLOWED: V8 blocked the capture expression ...","mutationRisk":true,"blocked":true}
```
With `--allow-mutation`, recognized mutation syntax runs and the result carries
`"mutationRisk": true`; ordinary reads do not gain the field.
#### Truncation contract
JSON truncation is always out-of-band. A text value longer than the effective
limit is cut to exactly that many JavaScript characters and gains
`"truncated": true` plus its full `"originalLength"`; no ellipsis is appended
to the JSON value. These fields are absent when no cut occurs. Human output may
add a visual ellipsis.
Expanded objects and scopes also report bounded structural capture. The
`VariableSnapshot`, `ScopeSnapshot`, or `FrameSnapshot` whose properties,
variables, or scopes were cut gains `truncated: true` and the exact direct
`omittedCount`. A serialized object capture propagates the aggregate known
omission count to its `CapturedExpression`. `ExceptionSnapshot` additionally
uses `valueOriginalLength` and `descriptionOriginalLength` so consumers can
identify which field was cut; its compatibility `originalLength` is the larger
reported field length.
One-shot `snapshot` and `exception` commands default to `131072` characters.
Repeated `watch` and `log` events default to `4096`. All four accept
`--max-value-length`, and an explicit limit is applied exactly.
```json
{"expression":"largeText","value":"exactly-N-characters","type":"string","truncated":true,"originalLength":250000}
```
`pausedDurationMs` measures the client-observed time from receiving the matching
pause event until `Debugger.resume` completes. With `--keep-paused`, it is `null`
because resume is intentionally skipped.
If the target pauses somewhere else first, for example another debugger's
breakpoint or a `debugger;` statement, `snapshot` does not resume it by default.
It warns once, waits for `Debugger.resumed`, then continues waiting for its own
breakpoint within the remaining timeout. Use `--fail-on-unmatched-pause` when a
strict immediate error is preferred.
For Cloud Foundry targets, replace `--port` with
`--region/--org/--space/--app`. Cloud Foundry commands and tunnel readiness
allow up to 180 seconds by default. For commands without their own wait
semantics, `--timeout ` controls CF tunnel readiness. For breakpoint
and exception commands, `--timeout` is reserved for the command wait and tunnel
readiness keeps the 180-second default.
### ๐ก `cf-inspector log`
Set a non-pausing logpoint and stream the evaluated expression each time the
line executes. The inspectee does not pause, but the expression and condition
still execute against live state and can mutate it.
```bash
# Stream user IDs hitting handler.ts:42 for 30 seconds
cf-inspector log \
--port 9229 \
--at src/handler.ts:42 \
--expr 'JSON.stringify({ user: req.user, body: req.body })' \
--duration 30
```
Output is **JSON Lines** on stdout (one event per line) plus a summary trailer on stderr:
```jsonc
{"ts":"2026-04-29T...","at":"src/handler.ts:42","value":"{\"user\":\"alice\",\"body\":{}}"}
{"ts":"2026-04-29T...","at":"src/handler.ts:42","value":"{\"user\":\"bob\",\"body\":{}}"}
// stderr:
{"stopped":"duration","emitted":2}
```
When the user expression throws, the event is emitted with `error` instead of `value` so the stream never silently gaps:
```jsonc
{"ts":"โฆ","at":"src/handler.ts:42","error":"Cannot read properties of undefined (reading 'user')"}
```
| Flag | Description |
| --- | --- |
| `--port ` | Local port the inspector or tunnel listens on. **Required** unless `--app/--region/--org/--space` are all set |
| `--target ` / `--worker ` / `--worker-id ` | Pin one raw target or worker instead of automatic fan-out |
| `--main-only` | Ignore workers and attach only to the main isolate |
| `--at ` | **Required.** Source location to log at |
| `--expr ` | **Required.** JavaScript expression evaluated at each hit, wrapped in try/catch on the inspectee side. It is mutation-capable; recognizable risks produce a warning |
| `--duration ` | Stop streaming after N seconds (default: run until SIGINT) |
| `--max-events ` | Stop streaming after emitting N log events. The trailer reports `stopped: "max-events"` |
| `--hit-count ` | Start emitting once the line has been hit N or more times |
| `--condition ` | Mutation-capable native condition evaluated on the inspectee. Recognizable risks produce a warning. Composes with `--hit-count` via logical AND |
| `--max-value-length ` | Maximum characters per log value (streaming default: `4096`). Truncated events include `truncated` and `originalLength` |
| `--remote-root ` | Optional path-mapping anchor (same DSL as `snapshot`) |
| `--no-json` | Print human-readable lines instead of JSON Lines |
| `--ready-event` | Emit the versioned `breakpoint-armed` JSON event on stderr after every current isolate is armed |
Native logpoint expressions and conditions have no V8 side-effect gate. The
CLI warns when its best-effort syntax scan recognizes assignments or common
mutating calls, but that scan cannot prove an arbitrary function is pure. Treat
logpoint expressions as executable live code.
### ๐ `cf-inspector watch`
Stream a snapshot per breakpoint hit. The inspectee is paused briefly while
captures are evaluated, then resumed automatically; output is JSON Lines on
stdout with a trailer on stderr (same shape as `log`).
```bash
cf-inspector watch --port 9229 \
--bp src/handler.ts:42 \
--capture 'user.id, payload' \
--condition 'user.id !== "system"' \
--duration 30 \
--max-events 50
```
Each event is a `WatchEvent`:
```jsonc
{"ts":"2026-04-29T...","at":"file:///app/src/handler.ts:42","hit":1,"reason":"other","hitBreakpoints":["..."],"captures":[{"expression":"user.id","value":"\"alice\""}]}
{"ts":"2026-04-29T...","at":"file:///app/src/handler.ts:42","hit":2,"reason":"other","hitBreakpoints":["..."],"captures":[{"expression":"user.id","value":"\"bob\""}]}
// stderr trailer:
{"stopped":"max-events","emitted":50}
```
| Flag | Description |
| --- | --- |
| `--port ` | Local port the inspector or tunnel listens on. Otherwise pass all explicit Cloud Foundry selectors |
| `--target ` / `--worker ` / `--worker-id ` | Pin one raw target or worker instead of automatic fan-out |
| `--main-only` | Ignore workers and attach only to the main isolate |
| `--bp ` | **Required.** Source location to capture on (repeatable) |
| `--capture ` | Top-level comma-separated expressions evaluated per hit under V8's side-effect guard |
| `--setup-eval ` | Repeatable mutation-capable global expression evaluated before breakpoint setup; recognizable risks produce a warning |
| `--condition ` | Native condition; mutation-shaped conditions require `--allow-mutation` |
| `--hit-count ` | Start emitting once the line has been hit N or more times |
| `--remote-root ` | Path-mapping anchor (same DSL as `snapshot`) |
| `--duration ` | Stop streaming after N seconds (default: until SIGINT) |
| `--max-events ` | Stop streaming after emitting N events |
| `--timeout ` | How long to wait for the next hit before giving up (default: `30`) |
| `--max-value-length ` | Maximum characters per captured value (streaming default: `4096`) |
| `--stack-depth ` | Walk this many call frames per hit (default: `1`) |
| `--stack-captures ` | Expressions evaluated on each call frame under the capture side-effect guard |
| `--allow-mutation` | Disable the capture side-effect guard and explicitly allow mutation-shaped native conditions |
| `--include-scopes` | Include expanded paused-frame scopes per hit |
| `--no-json` | Print human-readable lines instead of JSON Lines |
| `--ready-event` | Emit the versioned `breakpoint-armed` JSON event on stderr after every current isolate is armed |
### ๐ฅ `cf-inspector exception`
Pause on a thrown exception, capture the exception value plus the paused
frame, then resume.
```bash
cf-inspector exception --port 9229 \
--type uncaught \
--capture 'this' \
--stack-depth 4 \
--stack-captures 'arguments[0]' \
--timeout 30
```
Result is a `SnapshotResult` with an extra `exception` field:
```jsonc
{
"reason": "exception",
"hitBreakpoints": [],
"capturedAt": "2026-04-29T...",
"pausedDurationMs": 0.5,
"topFrame": {"functionName": "validate", "url": "...", "line": 42, "column": 5},
"exception": {"value": "{\"message\":\"missing field\",\"name\":\"Error\"}", "type": "object", "description": "missing field"},
"captures": [],
"stack": [...]
}
```
| Flag | Description |
| --- | --- |
| `--port` or explicit `--region/--org/--space/--app` | Select the local inspector or deterministic Cloud Foundry target |
| `--target ` / `--worker ` / `--worker-id ` | Pin one raw target or worker instead of automatic fan-out |
| `--main-only` | Ignore workers and attach only to the main isolate |
| `--type ` | Pause on which exceptions: `uncaught` (default), `caught`, or `all` |
| `--capture ` | Top-level expressions evaluated in the paused frame under V8's side-effect guard |
| `--stack-depth ` | Walk this many call frames (default: `1`) |
| `--stack-captures ` | Expressions evaluated on each frame under V8's side-effect guard |
| `--allow-mutation` | Disable the capture side-effect guard; heuristic matches gain `mutationRisk: true` |
| `--include-scopes` | Include paused-frame scopes |
| `--remote-root ` | Path-mapping anchor (only used if you also wire snapshot helpers) |
| `--timeout ` | How long to wait for an exception (default: `30`) |
| `--max-value-length ` | Maximum characters per captured value (one-shot default: `131072`) |
| `--keep-paused` | Skip `Debugger.resume` after capture |
| `--no-json` | Print a human-readable summary instead of JSON |
| `--ready-event` | Emit the versioned `breakpoint-armed` JSON event on stderr after pause-on-exception is active in every current isolate |
### ๐งฎ `cf-inspector eval`
Evaluate one expression with `Runtime.evaluate` in the selected isolate's
global scope and print the result. `eval` is intentionally mutation-capable and
has no side-effect gate; recognizable mutation syntax emits an advisory warning
to `stderr`. For read-only paused-frame values, use `snapshot --capture` or call
the programmatic `evaluateOnFrame(..., { throwOnSideEffect: true })` API. Plain
`evaluateOnFrame(...)` remains unrestricted by default for backward
compatibility.
```bash
cf-inspector eval --port 9229 --expr 'process.uptime()'
```
### ๐ `cf-inspector list-scripts`
Print every script known by the main isolate and each currently attached
NodeWorker (useful for debugging path-mapping issues). Each JSON entry carries
`isolate: {"kind":"main"}` or
`isolate: {"kind":"worker","workerId":"โฆ"}`, so duplicate per-isolate
`scriptId` values remain unambiguous. Add `--filter ` to narrow noisy
script lists with a literal/wildcard pattern; `|` separates alternatives and
`.*` / `.+` match variable text.
```bash
cf-inspector list-scripts --port 9229 --filter 'dist/.+\.js'
```
With `--no-json`, rows are
`scriptIdurlisolate`, where isolate is `main` or
`worker:`. `--main-only`, `--worker-id`, `--worker`, and `--target`
retain their normal narrowing behavior.
### ๐ฏ `cf-inspector list-targets`
Print raw `/json/list` inspector targets with stable `index` values, likely
worker labels, and the total target/worker counts on `stderr`. For each raw
target, the command also probes Node's `NodeWorker` CDP domain and lists live
nested workers with their own indexes.
```bash
cf-inspector list-targets --port 9229
cf-inspector snapshot --port 9229 --bp dist/worker.js:42
cf-inspector snapshot --port 9229 --worker-id 1 --bp dist/worker.js:42
# If a runtime publishes a worker as another raw /json/list target instead:
cf-inspector snapshot --port 9229 --target 1 --bp dist/worker.js:42
```
JSON output nests workers beneath their raw target:
```json
[
{
"index": 0,
"description": "node.js instance",
"id": "target-id",
"type": "node",
"title": "app.mjs",
"url": "file:///app/app.mjs",
"webSocketDebuggerUrl": "ws://127.0.0.1:9229/target-id",
"likelyWorker": false,
"workerDiscoverySupported": true,
"workers": [
{"index": 0, "workerId": "1", "type": "worker", "title": "jobs", "url": "file:///app/worker.mjs"}
]
}
]
```
`--target` selects a complete raw inspector endpoint. `--worker-id` selects a
live nested worker by its stable ID, while `--worker` retains positional-index
selection for compatibility. Modern Node.js 20โ25 verification found workers on
the `NodeWorker` path, including workers already alive before post-hoc
`SIGUSR1` inspector activation; the raw-target selector remains supported for
runtimes that publish that shape.
When no selector is passed, breakpoint-oriented commands attach to raw target
`0` and automatically fan out across its main isolate and nested workers. If only one raw target
and no workers are visible, `list-targets` explains that the worker may have
exited, the runtime may not expose NodeWorker discovery, or a separate worker
port may be unreachable through the single Cloud Foundry tunnel. Rerun the
command while the worker is alive before pinning its `workerId`.
If `list-targets`, `attach`, or another command reports `ECONNREFUSED`, the local inspector or tunnel on that port is usually stale/closed. Restart the local Node inspector or tunnel and retry; for Cloud Foundry targets, pass the complete `--region/--org/--space/--app` selector so `cf-inspector` can open a fresh tunnel.
### โ
`cf-inspector check-breakpoint`
Check whether a `file:line` can accept a breakpoint before arming one. The
command uses the same path mapping as `--bp` and checks every currently attached
isolate by default.
```bash
cf-inspector check-breakpoint --port 9229 --bp dist/handler.js:42
```
`status: "script-not-loaded"` means no loaded script matched the file/path
mapping; use `list-scripts`, adjust `--remote-root`, or trigger lazy loading.
`status: "unbreakable"` means the script is loaded but that exact line has no
V8 break location; choose a neighboring executable line. `status: "breakable"`
includes the concrete script IDs, isolate identities, lines, and columns.
### ๐ `cf-inspector attach`
Connect, fetch the runtime version, print it, disconnect. Useful as a smoke-test that the tunnel is healthy.
```bash
cf-inspector attach --port 9229
```
`attach` checks the port-level `/json/version` endpoint, so raw-target and
worker selectors do not apply to this smoke test.
### Exact CDP tracing APIs
The library also exports lower-level, validated primitives for tools that need to plan and step an
exact loaded function instead of using a URL-regex breakpoint:
```ts
const scripts = listScripts(session);
const source = await getScriptSource(session, scripts[0].scriptId);
const locations = await getPossibleBreakpoints(session, {
start: { scriptId: scripts[0].scriptId, lineNumber: 10 },
restrictToFunction: true,
});
const breakpoint = await setBreakpointAtLocation(session, { location: locations[0] });
const pause = await waitForPause(session, {
timeoutMs: 30_000,
breakpointIds: [breakpoint.breakpointId],
signal: abortController.signal,
});
await stepOver(session);
await releaseObject(session, objectId);
```
`ScriptLocation` and `BreakLocation` use CDP-native zero-based line and column numbers. Exact
breakpoint setup returns both the requested and actual location, then fails closed and removes the
breakpoint if V8 resolves a different script, line, or column. An omitted column means CDP column
zero. `RemoteObjectInfo.completeness` is `truncated` for logical values stored in internal slots
(including maps, sets, promises, and dates) and `unavailable` for proxies; it is omitted for
ordinary objects. `waitForPause` accepts an `AbortSignal` and cleans its event listener and timer
on success, timeout, abort, or session close. The same layer exports `stepInto`, `stepOver`,
`stepOut`, `releaseObject`, and `releaseObjectGroup` for bounded controllers.
Programmatic Cloud Foundry tunnels can select the same process instance and Node PID used for both
the remote signal and SSH forwarding:
```ts
const tunnel = await openCfTunnel({
region: "eu10",
org: "my-org",
space: "dev",
app: "orders-srv",
process: "worker",
instance: 2,
nodePid: 4312,
});
```
`openCfTunnel` retains backward-compatible reuse when `cf-debugger` reports a healthy local port
for an existing session. Controllers that must prove they own and can dispose the tunnel should use
`openOwnedCfTunnel`; it propagates `SESSION_ALREADY_RUNNING` instead of parsing or borrowing a
pre-existing session.
---
## ๐ญ How it works
```
โโโโโโโโโโโโโโโโโโโโโโโโ 1. GET http://127.0.0.1:/json/list
โ cf-inspector โ 2. Open the selected raw WebSocket target
โ snapshot --bp X:Y โ โโบ3. Auto-attach to current and future NodeWorker sub-sessions
โโโโโโโโโโโโโโโโโโโโโโโโ 4. Debugger.enable + Runtime.enable
โ 5. Debugger.setBreakpointByUrl({ urlRegex, lineNumber: Y - 1 })
โผ 6. Wait for `Debugger.paused`
JSON snapshot 7. Debugger.evaluateOnCallFrame({ throwOnSideEffect: true, ... })
8. Runtime.getProperties(...) when object/scopes are expanded
9. Debugger.resume (unless --keep-paused)
```
Path mapping uses CDP's first-class `urlRegex`:
| `--remote-root` | Resulting urlRegex (line `42` of `src/handler.ts`) |
| --- | --- |
| _omitted_ | `(?:^|/)src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$` |
| `/home/vcap/app` (literal) | `^file:///home/vcap/app/src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$` |
| `regex:^/example-root-.*$` | `^file:///example-root-.*/src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$` |
| `regex:^/(home/vcap/app\|example-root-.*)$` | `^file:///(home/vcap/app\|example-root-.*)/src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$` |
`.ts โ .js` is folded into the regex automatically because Node's V8 inspector normally serves both the source-mapped TypeScript URL and the runtime JavaScript URL โ matching either is correct.
---
## โ๏ธ Composing with `cf-debugger`
If `--port` is omitted, all of `--region/--org/--space/--app` are required. The
CLI does not read ambient `cf target` state. It calls `startDebugger(...)` from
`@saptools/cf-debugger`, attaches over the SSH tunnel, and disposes the tunnel
on exit. You get the same one-shot UX whether the target is local or in CF.
The tunnel forwards one inspector port. Nested NodeWorker sessions carried by
that inspector connection are auto-attached by breakpoint-oriented commands and
can be pinned with `--worker-id`; a worker exposing
only an unrelated separate port is outside that tunnel's reach.
```bash
cf-inspector snapshot \
--region eu10 --org my-org --space dev --app my-srv \
--bp src/handler.ts:42 \
--capture 'req.url, this.user'
```
---
## ๐ Related
- ๐ [`@saptools/cf-debugger`](https://www.npmjs.com/package/@saptools/cf-debugger) โ opens the SSH inspector tunnel
- โ๏ธ [`@saptools/cf-sync`](https://www.npmjs.com/package/@saptools/cf-sync) โ snapshot CF topology + DB bindings into JSON
- ๐๏ธ [saptools monorepo](https://github.com/dongitran/saptools) โ the full toolbox
---
## ๐จโ๐ป Author
**dongtran** โจ
## ๐ License
MIT
---
Made with โค๏ธ to make your work life easier!