@saptools/cf-inspector 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dongtran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ <div align="center">
2
+
3
+ # ๐Ÿ” `@saptools/cf-inspector`
4
+
5
+ **Set breakpoints, capture variable snapshots, and evaluate expressions on a remote Node.js process โ€” over the Chrome DevTools Protocol, no IDE required.**
6
+
7
+ 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.
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@saptools/cf-inspector.svg?style=flat&color=CB3837&logo=npm)](https://www.npmjs.com/package/@saptools/cf-inspector)
10
+ [![license](https://img.shields.io/npm/l/@saptools/cf-inspector.svg?style=flat&color=blue)](./LICENSE)
11
+ [![node](https://img.shields.io/node/v/@saptools/cf-inspector.svg?style=flat&color=339933&logo=node.js&logoColor=white)](https://nodejs.org)
12
+
13
+ [Install](#-install) โ€ข [Quick Start](#-quick-start) โ€ข [CLI](#-cli) โ€ข [API](#-programmatic-usage) โ€ข [How it works](#-how-it-works)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## โœจ Features
20
+
21
+ - ๐ŸŽฏ **One-shot snapshot** โ€” `cf-inspector snapshot --bp src/handler.ts:42` sets the breakpoint, waits for it to hit, captures the scope, auto-resumes, prints JSON, exits
22
+ - โœ… **Conditional breakpoints** โ€” `--condition 'req.userId === "abc"'` only pauses when the predicate is truthy
23
+ - ๐ŸŽญ **Multi-breakpoint** โ€” repeat `--bp` to race several locations; first hit wins
24
+ - ๐Ÿ“ก **Non-pausing logpoints** โ€” `cf-inspector log --at file:line --expr 'JSON.stringify({โ€ฆ})'` streams JSON Lines as the line executes, **without ever pausing the inspectee** (safe for production traffic)
25
+ - ๐Ÿง  **Agent-friendly** โ€” JSON-by-default I/O, deterministic shape, sensitive-name redaction (`password`, `token`, `secret`, `cookie`, โ€ฆ) baked in
26
+ - ๐Ÿงญ **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`)
27
+ - ๐Ÿ” **Composes with `cf-debugger`** โ€” pass `--app/--region/--org/--space` and the tunnel is opened automatically; pass `--port` to attach to anything CDP-speaking
28
+ - ๐Ÿชถ **Tiny dependency footprint** โ€” `commander` + `ws` only, no heavy CDP framework
29
+ - ๐Ÿงฉ **Typed API** โ€” every CLI command has a programmatic equivalent with full TypeScript definitions
30
+
31
+ ---
32
+
33
+ ## ๐Ÿ“ฆ Install
34
+
35
+ ```bash
36
+ npm install -g @saptools/cf-inspector
37
+ # or
38
+ pnpm add @saptools/cf-inspector
39
+ ```
40
+
41
+ > [!NOTE]
42
+ > Requires **Node.js โ‰ฅ 20**.
43
+ > For Cloud Foundry targets, also install [`@saptools/cf-debugger`](https://www.npmjs.com/package/@saptools/cf-debugger) (added automatically as a peer-style runtime dep).
44
+
45
+ ---
46
+
47
+ ## ๐Ÿš€ Quick Start
48
+
49
+ ### Local Node process
50
+
51
+ ```bash
52
+ # Terminal 1 โ€” run any Node app with the inspector enabled
53
+ node --inspect=9229 my-app.js
54
+
55
+ # Terminal 2 โ€” capture a snapshot when handler.ts:42 hits
56
+ cf-inspector snapshot \
57
+ --port 9229 \
58
+ --bp src/handler.ts:42 \
59
+ --capture 'this.user, req.body' \
60
+ --timeout 30
61
+ ```
62
+
63
+ ### Cloud Foundry app (auto-tunnel)
64
+
65
+ ```bash
66
+ export SAP_EMAIL=...
67
+ export SAP_PASSWORD=...
68
+
69
+ cf-inspector snapshot \
70
+ --region eu10 --org my-org --space dev --app my-srv \
71
+ --bp src/handler.ts:42 \
72
+ --remote-root 'regex:^/(home/vcap/app|srv-.*)$'
73
+ ```
74
+
75
+ The first form connects directly to `localhost:9229`. The second internally calls `@saptools/cf-debugger` to open the SSH tunnel, runs the snapshot through it, and tears the tunnel down on exit.
76
+
77
+ ---
78
+
79
+ ## ๐Ÿงฐ CLI
80
+
81
+ ### ๐Ÿ“ธ `cf-inspector snapshot`
82
+
83
+ Set one or more breakpoints, wait for any of them to hit, capture the scope, auto-resume, exit.
84
+
85
+ ```bash
86
+ # Simple snapshot
87
+ cf-inspector snapshot \
88
+ --port 9229 \
89
+ --bp src/handler.ts:42 \
90
+ --capture 'this.user, req.body' \
91
+ --timeout 30
92
+
93
+ # Conditional snapshot โ€” only pauses for the user we care about
94
+ cf-inspector snapshot --port 9229 \
95
+ --bp src/handler.ts:42 \
96
+ --condition 'req.userId === "abc"' \
97
+ --capture 'req.body'
98
+
99
+ # Multi-breakpoint โ€” first hit wins (useful when you don't know which path is taken)
100
+ cf-inspector snapshot --port 9229 \
101
+ --bp src/auth.ts:120 \
102
+ --bp src/auth.ts:155 \
103
+ --bp src/auth.ts:180 \
104
+ --capture 'req.url, this.user'
105
+ ```
106
+
107
+ | Flag | Description |
108
+ | --- | --- |
109
+ | `--port <number>` | Local port the inspector or tunnel listens on. **Required** unless `--app/--region/--org/--space` are all set |
110
+ | `--bp <file:line>` | **Required.** Source location to break at. Pass multiple times to race several locations โ€” the first one to hit wins |
111
+ | `--condition <expr>` | Only pause when this JS expression evaluates truthy in the paused frame. Errors in the condition are silently treated as `false` by V8 |
112
+ | `--capture <expr,โ€ฆ>` | Comma-separated expressions to evaluate in the paused frame; results merged into the snapshot |
113
+ | `--timeout <seconds>` | How long to wait for the breakpoint to hit (default: `30`) |
114
+ | `--remote-root <value>` | Optional path-mapping anchor: literal path or `regex:<pattern>` / `/pattern/flags` |
115
+ | `--no-json` | Print a human-readable summary instead of JSON |
116
+ | `--keep-paused` | Skip the auto-resume after capture (useful for diagnostics) |
117
+
118
+ For Cloud Foundry targets, replace `--port` with `--region/--org/--space/--app` (and optionally `--cf-timeout <seconds>` for the tunnel).
119
+
120
+ ### ๐Ÿ“ก `cf-inspector log`
121
+
122
+ Set a non-pausing logpoint and stream the evaluated expression each time the line executes. Safe for production traffic โ€” the inspectee **never pauses**.
123
+
124
+ ```bash
125
+ # Stream user IDs hitting handler.ts:42 for 30 seconds
126
+ cf-inspector log \
127
+ --port 9229 \
128
+ --at src/handler.ts:42 \
129
+ --expr 'JSON.stringify({ user: req.user, body: req.body })' \
130
+ --duration 30
131
+ ```
132
+
133
+ Output is **JSON Lines** on stdout (one event per line) plus a summary trailer on stderr:
134
+
135
+ ```jsonc
136
+ {"ts":"2026-04-29T...","at":"src/handler.ts:42","value":"{\"user\":\"alice\",\"body\":{}}"}
137
+ {"ts":"2026-04-29T...","at":"src/handler.ts:42","value":"{\"user\":\"bob\",\"body\":{}}"}
138
+ // stderr:
139
+ {"stopped":"duration","emitted":2}
140
+ ```
141
+
142
+ When the user expression throws, the event is emitted with `error` instead of `value` so the stream never silently gaps:
143
+
144
+ ```jsonc
145
+ {"ts":"โ€ฆ","at":"src/handler.ts:42","error":"Cannot read properties of undefined (reading 'user')"}
146
+ ```
147
+
148
+ | Flag | Description |
149
+ | --- | --- |
150
+ | `--port <number>` | Local port the inspector or tunnel listens on. **Required** unless `--app/--region/--org/--space` are all set |
151
+ | `--at <file:line>` | **Required.** Source location to log at |
152
+ | `--expr <expression>` | **Required.** JS expression to evaluate at each hit (wrapped in try/catch on the inspectee side) |
153
+ | `--duration <seconds>` | Stop streaming after N seconds (default: run until SIGINT) |
154
+ | `--remote-root <value>` | Optional path-mapping anchor (same DSL as `snapshot`) |
155
+ | `--no-json` | Print human-readable lines instead of JSON Lines |
156
+
157
+ ### ๐Ÿงฎ `cf-inspector eval`
158
+
159
+ Evaluate one expression and print the result. If a breakpoint is currently paused, it runs in the top frame; otherwise it runs against `Runtime.evaluate` in the global scope.
160
+
161
+ ```bash
162
+ cf-inspector eval --port 9229 --expr 'process.uptime()'
163
+ ```
164
+
165
+ ### ๐Ÿ“œ `cf-inspector list-scripts`
166
+
167
+ Print every script the V8 instance knows about (useful for debugging path-mapping issues).
168
+
169
+ ```bash
170
+ cf-inspector list-scripts --port 9229
171
+ ```
172
+
173
+ ### ๐Ÿ”— `cf-inspector attach`
174
+
175
+ Connect, fetch the runtime version, print it, disconnect. Useful as a smoke-test that the tunnel is healthy.
176
+
177
+ ```bash
178
+ cf-inspector attach --port 9229
179
+ ```
180
+
181
+ ---
182
+
183
+ ## ๐Ÿง‘โ€๐Ÿ’ป Programmatic Usage
184
+
185
+ ```ts
186
+ import {
187
+ connectInspector,
188
+ setBreakpoint,
189
+ waitForPause,
190
+ captureSnapshot,
191
+ evaluateOnFrame,
192
+ resume,
193
+ } from "@saptools/cf-inspector";
194
+
195
+ const session = await connectInspector({ port: 9229 });
196
+ const bp = await setBreakpoint(session, {
197
+ file: "src/handler.ts",
198
+ line: 42,
199
+ });
200
+ const pause = await waitForPause(session, { timeoutMs: 30_000 });
201
+ const snapshot = await captureSnapshot(session, pause);
202
+ const customValue = await evaluateOnFrame(session, pause.callFrames[0]!.callFrameId, "this.user");
203
+ await resume(session);
204
+ await session.dispose();
205
+
206
+ console.log({ bp, snapshot, customValue });
207
+ ```
208
+
209
+ <details>
210
+ <summary><b>๐Ÿ“š Full export list</b></summary>
211
+
212
+ | Export | Description |
213
+ | --- | --- |
214
+ | `connectInspector(options)` | Open a CDP WebSocket session against a port |
215
+ | `setBreakpoint(session, location)` | Set a breakpoint by file/line + optional remote root |
216
+ | `removeBreakpoint(session, id)` | Remove a breakpoint by id |
217
+ | `waitForPause(session, options)` | Resolve when the next `Debugger.paused` event fires |
218
+ | `captureSnapshot(session, pause)` | Build a structured snapshot of the paused scope |
219
+ | `evaluateOnFrame(session, frameId, expression)` | Evaluate in a paused frame |
220
+ | `evaluateGlobal(session, expression)` | Evaluate against the global Runtime |
221
+ | `listScripts(session)` | Return the scripts the V8 instance knows about |
222
+ | `resume(session)` | Resume execution |
223
+ | `streamLogpoint(session, options)` | Stream a non-pausing logpoint until duration / signal / transport-close |
224
+ | `buildLogpointCondition(sentinel, expression)` | Build the CDP `condition` string for a logpoint (low-level helper) |
225
+ | `parseRemoteRoot(value)` | Parse a literal/regex remote-root setting |
226
+ | `buildBreakpointUrlRegex(input)` | Build a CDP `urlRegex` for a file path |
227
+ | `CfInspectorError` | Rich error class with typed `code` |
228
+
229
+ </details>
230
+
231
+ <details>
232
+ <summary><b>๐Ÿงช Error codes</b></summary>
233
+
234
+ | Code | When |
235
+ | --- | --- |
236
+ | `INVALID_BREAKPOINT` | `--bp` is not in `file:line` form, or line is not a positive integer |
237
+ | `INVALID_REMOTE_ROOT` | `--remote-root` regex did not compile |
238
+ | `INSPECTOR_DISCOVERY_FAILED` | `/json/list` did not return a usable WebSocket URL |
239
+ | `INSPECTOR_CONNECTION_FAILED` | WebSocket handshake failed |
240
+ | `CDP_REQUEST_FAILED` | A CDP method returned an error result |
241
+ | `BREAKPOINT_NOT_HIT` | The breakpoint did not hit before the timeout elapsed |
242
+ | `EVALUATION_FAILED` | `Runtime.evaluate` / `Debugger.evaluateOnCallFrame` returned a remote exception |
243
+ | `MISSING_TARGET` | Neither `--port` nor a CF target was provided |
244
+
245
+ </details>
246
+
247
+ ---
248
+
249
+ ## ๐Ÿ”ญ How it works
250
+
251
+ ```
252
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 1. GET http://127.0.0.1:<port>/json/list
253
+ โ”‚ cf-inspector โ”‚ 2. Open ws:// debugger URL
254
+ โ”‚ snapshot --bp X:Y โ”‚ โ”€โ–บ3. Debugger.enable + Runtime.enable
255
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 4. Debugger.setBreakpointByUrl({ urlRegex, lineNumber: Y - 1 })
256
+ โ”‚ 5. Wait for `Debugger.paused`
257
+ โ–ผ 6. Debugger.evaluateOnCallFrame(...) for each --capture expression
258
+ JSON snapshot 7. Runtime.getProperties(scopeChain[i].object.objectId)
259
+ 8. Debugger.resume (unless --keep-paused)
260
+ ```
261
+
262
+ Path mapping uses CDP's first-class `urlRegex`:
263
+
264
+ | `--remote-root` | Resulting urlRegex (line `42` of `src/handler.ts`) |
265
+ | --- | --- |
266
+ | _omitted_ | `(?:^|/)src/handler\.(?:ts\|js)$` |
267
+ | `/home/vcap/app` (literal) | `^file:///home/vcap/app/src/handler\.(?:ts\|js)$` |
268
+ | `regex:^/srv-.*$` | `^file:///srv-[^/]+/src/handler\.(?:ts\|js)$` |
269
+ | `/^/srv-.*$/` | same as above |
270
+
271
+ `.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.
272
+
273
+ ---
274
+
275
+ ## โš™๏ธ Composing with `cf-debugger`
276
+
277
+ If `--port` is omitted but `--region/--org/--space/--app` are given, the CLI internally 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.
278
+
279
+ ```bash
280
+ cf-inspector snapshot \
281
+ --region eu10 --org my-org --space dev --app my-srv \
282
+ --bp src/handler.ts:42 \
283
+ --capture 'req.url, this.user'
284
+ ```
285
+
286
+ ---
287
+
288
+ ## ๐Ÿ› ๏ธ Development
289
+
290
+ From the monorepo root:
291
+
292
+ ```bash
293
+ pnpm install
294
+ pnpm --filter @saptools/cf-inspector build
295
+ pnpm --filter @saptools/cf-inspector typecheck
296
+ pnpm --filter @saptools/cf-inspector lint
297
+ pnpm --filter @saptools/cf-inspector test:unit
298
+ pnpm --filter @saptools/cf-inspector test:e2e
299
+ ```
300
+
301
+ The e2e suite is fully self-contained: it spawns a small Node fixture under `--inspect=0`, drives the CLI against it, and asserts the JSON output. No CF / live network required.
302
+
303
+ ---
304
+
305
+ ## ๐ŸŒ Related
306
+
307
+ - ๐Ÿ› [`@saptools/cf-debugger`](https://www.npmjs.com/package/@saptools/cf-debugger) โ€” opens the SSH inspector tunnel
308
+ - โ˜๏ธ [`@saptools/cf-sync`](https://www.npmjs.com/package/@saptools/cf-sync) โ€” snapshot CF topology + DB bindings into JSON
309
+ - ๐Ÿ—‚๏ธ [saptools monorepo](https://github.com/dongitran/saptools) โ€” the full toolbox
310
+
311
+ ---
312
+
313
+ ## ๐Ÿ‘จโ€๐Ÿ’ป Author
314
+
315
+ **dongtran** โœจ
316
+
317
+ ## ๐Ÿ“„ License
318
+
319
+ MIT
320
+
321
+ ---
322
+
323
+ Made with โค๏ธ to make your work life easier!
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare function main(argv: readonly string[]): Promise<void>;
2
+
3
+ export { main };