@saptools/cf-inspector 0.3.5 → 0.3.7

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/README.md CHANGED
@@ -18,7 +18,7 @@ Built so an AI agent (or a CI job) can drive a debugger from a single shell comm
18
18
 
19
19
  ## ✨ Features
20
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
21
+ - 🎯 **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
22
22
  - ✅ **Conditional breakpoints** — `--condition 'req.userId === "abc"'` only pauses when the predicate is truthy
23
23
  - 🎭 **Multi-breakpoint** — repeat `--bp` to race several locations; first hit wins
24
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)
@@ -80,7 +80,7 @@ The first form connects directly to `localhost:9229`. The second internally call
80
80
 
81
81
  ### 📸 `cf-inspector snapshot`
82
82
 
83
- Set one or more breakpoints, wait for any of them to hit, capture the scope, auto-resume, exit.
83
+ Set one or more breakpoints, wait for any of them to hit, capture frame metadata and requested expressions, auto-resume, exit.
84
84
 
85
85
  ```bash
86
86
  # Simple snapshot
@@ -112,11 +112,15 @@ cf-inspector snapshot --port 9229 \
112
112
  | `--capture <expr,…>` | Top-level comma-separated expressions to evaluate in the paused frame; nested commas inside objects, arrays, calls, or strings are preserved. Object results are materialized to JSON strings when serializable, with fallback to CDP descriptions for non-serializable values |
113
113
  | `--timeout <seconds>` | How long to wait for the breakpoint to hit (default: `30`) |
114
114
  | `--remote-root <value>` | Optional path-mapping anchor: literal path or `regex:<pattern>` / `/pattern/flags` |
115
+ | `--include-scopes` | Include expanded paused-frame scopes under `topFrame.scopes`. Omitted by default to keep targeted captures concise |
115
116
  | `--no-json` | Print a human-readable summary instead of JSON |
116
117
  | `--keep-paused` | Skip `Debugger.resume` after capture; Node may resume when the CLI disconnects |
117
118
  | `--fail-on-unmatched-pause` | Fail immediately if the target pauses somewhere else instead of waiting cooperatively |
118
119
 
119
- JSON output includes `pausedDurationMs`, the client-observed time from receiving
120
+ JSON output includes frame metadata and `captures` by default. `topFrame.scopes`
121
+ is only present with `--include-scopes`, because Cloud Foundry Node apps often
122
+ carry large local/closure/module objects that drown out targeted captures. The
123
+ output also includes `pausedDurationMs`, the client-observed time from receiving
120
124
  the matching pause event until `Debugger.resume` completes. It does not include
121
125
  the time spent waiting for the breakpoint to hit. When `--keep-paused` is used,
122
126
  `pausedDurationMs` is `null` because `cf-inspector` intentionally skips
@@ -213,7 +217,9 @@ const bp = await setBreakpoint(session, {
213
217
  line: 42,
214
218
  });
215
219
  const pause = await waitForPause(session, { timeoutMs: 30_000 });
216
- const snapshot = await captureSnapshot(session, pause);
220
+ const snapshot = await captureSnapshot(session, pause, {
221
+ captures: ["this.user"],
222
+ });
217
223
  const topFrame = pause.callFrames[0];
218
224
  if (topFrame === undefined) {
219
225
  throw new Error("Breakpoint paused without a call frame");
@@ -234,7 +240,7 @@ console.log({ bp, snapshot, customValue });
234
240
  | `setBreakpoint(session, location)` | Set a breakpoint by file/line + optional remote root |
235
241
  | `removeBreakpoint(session, id)` | Remove a breakpoint by id |
236
242
  | `waitForPause(session, options)` | Resolve when the next `Debugger.paused` event fires |
237
- | `captureSnapshot(session, pause)` | Build a structured snapshot of the paused scope |
243
+ | `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes |
238
244
  | `evaluateOnFrame(session, frameId, expression)` | Evaluate in a paused frame |
239
245
  | `evaluateGlobal(session, expression)` | Evaluate against the global Runtime |
240
246
  | `listScripts(session)` | Return the scripts the V8 instance knows about |
@@ -280,7 +286,7 @@ console.log({ bp, snapshot, customValue });
280
286
  └──────────────────────┘ 4. Debugger.setBreakpointByUrl({ urlRegex, lineNumber: Y - 1 })
281
287
  │ 5. Wait for `Debugger.paused`
282
288
  ▼ 6. Debugger.evaluateOnCallFrame(...) for each --capture expression
283
- JSON snapshot 7. Runtime.getProperties(scopeChain[i].object.objectId)
289
+ JSON snapshot 7. Runtime.getProperties(scopeChain[i].object.objectId) when --include-scopes is set
284
290
  8. Debugger.resume (unless --keep-paused)
285
291
  ```
286
292
 
package/dist/cli.js CHANGED
@@ -1321,6 +1321,15 @@ async function renderObjectCapture(session, objectId) {
1321
1321
  return void 0;
1322
1322
  }
1323
1323
  }
1324
+ function normalizeRenderedObjectCapture(rendered, original) {
1325
+ if (rendered === "{}" && original !== "Object") {
1326
+ return void 0;
1327
+ }
1328
+ if (original.startsWith("Array(") && rendered === '{"length":0}') {
1329
+ return "[]";
1330
+ }
1331
+ return rendered;
1332
+ }
1324
1333
  async function withSerializedObjectCapture(session, expression, evalResult, captured) {
1325
1334
  if (captured.error !== void 0 || captured.value === void 0) {
1326
1335
  return captured;
@@ -1333,7 +1342,11 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
1333
1342
  if (rendered === void 0) {
1334
1343
  return captured;
1335
1344
  }
1336
- const value = sanitizeValue(expression, rendered);
1345
+ const normalized = normalizeRenderedObjectCapture(rendered, captured.value);
1346
+ if (normalized === void 0) {
1347
+ return captured;
1348
+ }
1349
+ const value = sanitizeValue(expression, normalized);
1337
1350
  return captured.type === void 0 ? { expression, value } : { expression, value, type: captured.type };
1338
1351
  }
1339
1352
  async function captureSnapshot(session, pause, options = {}) {
@@ -1341,14 +1354,16 @@ async function captureSnapshot(session, pause, options = {}) {
1341
1354
  let topFrame;
1342
1355
  let captures = [];
1343
1356
  if (top) {
1344
- const scopes = await captureScopes(session, top);
1345
1357
  topFrame = {
1346
1358
  functionName: top.functionName,
1347
1359
  ...top.url === void 0 ? {} : { url: top.url },
1348
1360
  line: top.lineNumber + 1,
1349
- column: top.columnNumber + 1,
1350
- scopes
1361
+ column: top.columnNumber + 1
1351
1362
  };
1363
+ if (options.includeScopes === true) {
1364
+ const scopes = await captureScopes(session, top);
1365
+ topFrame = { ...topFrame, scopes };
1366
+ }
1352
1367
  if (options.captures !== void 0 && options.captures.length > 0) {
1353
1368
  captures = await Promise.all(
1354
1369
  options.captures.map(async (expression) => {
@@ -1605,10 +1620,12 @@ function writeHumanSnapshot(snapshot) {
1605
1620
  lines.push(
1606
1621
  ` frame: ${fnName} ${sourceUrl}:${frame.line.toString()}:${frame.column.toString()}`
1607
1622
  );
1608
- for (const scope of frame.scopes) {
1609
- lines.push(` scope ${scope.type} (${scope.variables.length.toString()} vars):`);
1610
- for (const variable of scope.variables) {
1611
- lines.push(` ${variable.name} = ${variable.value}`);
1623
+ if (frame.scopes !== void 0) {
1624
+ for (const scope of frame.scopes) {
1625
+ lines.push(` scope ${scope.type} (${scope.variables.length.toString()} vars):`);
1626
+ for (const variable of scope.variables) {
1627
+ lines.push(` ${variable.name} = ${variable.value}`);
1628
+ }
1612
1629
  }
1613
1630
  }
1614
1631
  }
@@ -1666,7 +1683,10 @@ async function handleSnapshot(opts) {
1666
1683
  }
1667
1684
  });
1668
1685
  const pausedStartedAt = pause.receivedAtMs ?? performance2.now();
1669
- const snapshot = await captureSnapshot(session, pause, { captures });
1686
+ const snapshot = await captureSnapshot(session, pause, {
1687
+ captures,
1688
+ includeScopes: opts.includeScopes === true
1689
+ });
1670
1690
  if (opts.keepPaused === true) {
1671
1691
  return withPausedDuration(snapshot, null);
1672
1692
  }
@@ -1830,7 +1850,7 @@ async function main(argv) {
1830
1850
  value
1831
1851
  ];
1832
1852
  applyTargetOptions(
1833
- program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture the scope, and resume")
1853
+ program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume")
1834
1854
  ).option(
1835
1855
  "--bp <file:line>",
1836
1856
  "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42",
@@ -1839,7 +1859,7 @@ async function main(argv) {
1839
1859
  ).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option(
1840
1860
  "--condition <expr>",
1841
1861
  "Only pause when this JS expression evaluates truthy in the paused frame"
1842
- ).option("--no-json", "Print a human-readable summary instead of JSON").option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--fail-on-unmatched-pause", "Fail immediately if the target pauses somewhere else").action(async (opts) => {
1862
+ ).option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--no-json", "Print a human-readable summary instead of JSON").option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--fail-on-unmatched-pause", "Fail immediately if the target pauses somewhere else").action(async (opts) => {
1843
1863
  await handleSnapshot(opts);
1844
1864
  });
1845
1865
  applyTargetOptions(