@saptools/cf-inspector 0.3.6 → 0.3.8

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,11 +18,11 @@ 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)
25
- - 🧠 **Agent-friendly** — JSON-by-default I/O, deterministic shape, sensitive-name redaction (`password`, `credentials`, `token`, `secret`, `cookie`, â€Ķ) baked in
25
+ - 🧠 **Agent-friendly** — JSON-by-default I/O, deterministic shape, bounded value previews for large debugger payloads
26
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
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
28
  - ðŸŠķ **Tiny dependency footprint** — `commander` + `ws` only, no heavy CDP framework
@@ -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,17 @@ 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 contains raw debugger values; use it only against trusted targets and be
124
+ careful when sharing logs. The output also includes `pausedDurationMs`, the
125
+ client-observed time from receiving
120
126
  the matching pause event until `Debugger.resume` completes. It does not include
121
127
  the time spent waiting for the breakpoint to hit. When `--keep-paused` is used,
122
128
  `pausedDurationMs` is `null` because `cf-inspector` intentionally skips
@@ -213,7 +219,9 @@ const bp = await setBreakpoint(session, {
213
219
  line: 42,
214
220
  });
215
221
  const pause = await waitForPause(session, { timeoutMs: 30_000 });
216
- const snapshot = await captureSnapshot(session, pause);
222
+ const snapshot = await captureSnapshot(session, pause, {
223
+ captures: ["this.user"],
224
+ });
217
225
  const topFrame = pause.callFrames[0];
218
226
  if (topFrame === undefined) {
219
227
  throw new Error("Breakpoint paused without a call frame");
@@ -234,7 +242,7 @@ console.log({ bp, snapshot, customValue });
234
242
  | `setBreakpoint(session, location)` | Set a breakpoint by file/line + optional remote root |
235
243
  | `removeBreakpoint(session, id)` | Remove a breakpoint by id |
236
244
  | `waitForPause(session, options)` | Resolve when the next `Debugger.paused` event fires |
237
- | `captureSnapshot(session, pause)` | Build a structured snapshot of the paused scope |
245
+ | `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes |
238
246
  | `evaluateOnFrame(session, frameId, expression)` | Evaluate in a paused frame |
239
247
  | `evaluateGlobal(session, expression)` | Evaluate against the global Runtime |
240
248
  | `listScripts(session)` | Return the scripts the V8 instance knows about |
@@ -280,7 +288,7 @@ console.log({ bp, snapshot, customValue });
280
288
  └──────────────────────┘ 4. Debugger.setBreakpointByUrl({ urlRegex, lineNumber: Y - 1 })
281
289
  │ 5. Wait for `Debugger.paused`
282
290
  ▾ 6. Debugger.evaluateOnCallFrame(...) for each --capture expression
283
- JSON snapshot 7. Runtime.getProperties(scopeChain[i].object.objectId)
291
+ JSON snapshot 7. Runtime.getProperties(scopeChain[i].object.objectId) when --include-scopes is set
284
292
  8. Debugger.resume (unless --keep-paused)
285
293
  ```
286
294
 
package/dist/cli.js CHANGED
@@ -1072,7 +1072,6 @@ var MAX_SCOPE_VARIABLES = 20;
1072
1072
  var MAX_CHILD_VARIABLES = 8;
1073
1073
  var MAX_VARIABLE_DEPTH = 2;
1074
1074
  var MAX_VALUE_LENGTH = 240;
1075
- var SENSITIVE_NAME_REGEX = /(pass(?:word)?|credentials?|creds?|token|secret|api[_-]?key|authorization|cookie|session|private[_-]?key)/i;
1076
1075
  var PRIORITY_BY_TYPE = {
1077
1076
  local: 0,
1078
1077
  arguments: 1,
@@ -1133,13 +1132,7 @@ function formatPrimitive(value) {
1133
1132
  }
1134
1133
  return String(value);
1135
1134
  }
1136
- function isSensitiveName(name) {
1137
- return SENSITIVE_NAME_REGEX.test(name);
1138
- }
1139
- function sanitizeValue(name, raw) {
1140
- if (isSensitiveName(name)) {
1141
- return "[REDACTED]";
1142
- }
1135
+ function limitValueLength(raw) {
1143
1136
  if (raw.length <= MAX_VALUE_LENGTH) {
1144
1137
  return raw;
1145
1138
  }
@@ -1155,9 +1148,8 @@ async function captureProperties(session, objectId, limit, depth) {
1155
1148
  limited.map(async (prop) => {
1156
1149
  const name = typeof prop.name === "string" ? prop.name : "?";
1157
1150
  const described = describeProperty(prop);
1158
- const sensitive = isSensitiveName(name);
1159
1151
  let children;
1160
- if (!sensitive && depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
1152
+ if (depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
1161
1153
  try {
1162
1154
  const nested = await captureProperties(
1163
1155
  session,
@@ -1171,7 +1163,7 @@ async function captureProperties(session, objectId, limit, depth) {
1171
1163
  } catch {
1172
1164
  }
1173
1165
  }
1174
- const sanitizedValue = sensitive ? "[REDACTED]" : sanitizeValue(name, described.value);
1166
+ const sanitizedValue = limitValueLength(described.value);
1175
1167
  const base = { name, value: sanitizedValue };
1176
1168
  const withType = described.type === void 0 ? base : { ...base, type: described.type };
1177
1169
  return children === void 0 ? withType : { ...withType, children };
@@ -1206,7 +1198,7 @@ async function captureScopes(session, frame) {
1206
1198
  function evalResultToCaptured(expression, result) {
1207
1199
  if (result.exceptionDetails !== void 0) {
1208
1200
  const text = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
1209
- return { expression, error: sanitizeValue(expression, text) };
1201
+ return { expression, error: limitValueLength(text) };
1210
1202
  }
1211
1203
  const inner = result.result;
1212
1204
  if (!inner) {
@@ -1214,7 +1206,7 @@ function evalResultToCaptured(expression, result) {
1214
1206
  }
1215
1207
  const type = typeof inner.type === "string" ? inner.type : void 0;
1216
1208
  const buildCaptured = (rendered) => {
1217
- const sanitized = sanitizeValue(expression, rendered);
1209
+ const sanitized = limitValueLength(rendered);
1218
1210
  const base = { expression, value: sanitized };
1219
1211
  return type === void 0 ? base : { ...base, type };
1220
1212
  };
@@ -1346,7 +1338,7 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
1346
1338
  if (normalized === void 0) {
1347
1339
  return captured;
1348
1340
  }
1349
- const value = sanitizeValue(expression, normalized);
1341
+ const value = limitValueLength(normalized);
1350
1342
  return captured.type === void 0 ? { expression, value } : { expression, value, type: captured.type };
1351
1343
  }
1352
1344
  async function captureSnapshot(session, pause, options = {}) {
@@ -1354,14 +1346,16 @@ async function captureSnapshot(session, pause, options = {}) {
1354
1346
  let topFrame;
1355
1347
  let captures = [];
1356
1348
  if (top) {
1357
- const scopes = await captureScopes(session, top);
1358
1349
  topFrame = {
1359
1350
  functionName: top.functionName,
1360
1351
  ...top.url === void 0 ? {} : { url: top.url },
1361
1352
  line: top.lineNumber + 1,
1362
- column: top.columnNumber + 1,
1363
- scopes
1353
+ column: top.columnNumber + 1
1364
1354
  };
1355
+ if (options.includeScopes === true) {
1356
+ const scopes = await captureScopes(session, top);
1357
+ topFrame = { ...topFrame, scopes };
1358
+ }
1365
1359
  if (options.captures !== void 0 && options.captures.length > 0) {
1366
1360
  captures = await Promise.all(
1367
1361
  options.captures.map(async (expression) => {
@@ -1371,7 +1365,7 @@ async function captureSnapshot(session, pause, options = {}) {
1371
1365
  return await withSerializedObjectCapture(session, expression, result, captured);
1372
1366
  } catch (err) {
1373
1367
  const message = err instanceof Error ? err.message : String(err);
1374
- return { expression, error: message };
1368
+ return { expression, error: limitValueLength(message) };
1375
1369
  }
1376
1370
  })
1377
1371
  );
@@ -1618,10 +1612,12 @@ function writeHumanSnapshot(snapshot) {
1618
1612
  lines.push(
1619
1613
  ` frame: ${fnName} ${sourceUrl}:${frame.line.toString()}:${frame.column.toString()}`
1620
1614
  );
1621
- for (const scope of frame.scopes) {
1622
- lines.push(` scope ${scope.type} (${scope.variables.length.toString()} vars):`);
1623
- for (const variable of scope.variables) {
1624
- lines.push(` ${variable.name} = ${variable.value}`);
1615
+ if (frame.scopes !== void 0) {
1616
+ for (const scope of frame.scopes) {
1617
+ lines.push(` scope ${scope.type} (${scope.variables.length.toString()} vars):`);
1618
+ for (const variable of scope.variables) {
1619
+ lines.push(` ${variable.name} = ${variable.value}`);
1620
+ }
1625
1621
  }
1626
1622
  }
1627
1623
  }
@@ -1679,7 +1675,10 @@ async function handleSnapshot(opts) {
1679
1675
  }
1680
1676
  });
1681
1677
  const pausedStartedAt = pause.receivedAtMs ?? performance2.now();
1682
- const snapshot = await captureSnapshot(session, pause, { captures });
1678
+ const snapshot = await captureSnapshot(session, pause, {
1679
+ captures,
1680
+ includeScopes: opts.includeScopes === true
1681
+ });
1683
1682
  if (opts.keepPaused === true) {
1684
1683
  return withPausedDuration(snapshot, null);
1685
1684
  }
@@ -1843,7 +1842,7 @@ async function main(argv) {
1843
1842
  value
1844
1843
  ];
1845
1844
  applyTargetOptions(
1846
- program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture the scope, and resume")
1845
+ program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume")
1847
1846
  ).option(
1848
1847
  "--bp <file:line>",
1849
1848
  "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42",
@@ -1852,7 +1851,7 @@ async function main(argv) {
1852
1851
  ).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(
1853
1852
  "--condition <expr>",
1854
1853
  "Only pause when this JS expression evaluates truthy in the paused frame"
1855
- ).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) => {
1854
+ ).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) => {
1856
1855
  await handleSnapshot(opts);
1857
1856
  });
1858
1857
  applyTargetOptions(