@saptools/cf-inspector 0.3.8 → 0.3.11

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
@@ -46,20 +46,6 @@ pnpm add @saptools/cf-inspector
46
46
 
47
47
  ## 🚀 Quick Start
48
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
49
  ### Cloud Foundry app (auto-tunnel)
64
50
 
65
51
  ```bash
@@ -72,7 +58,7 @@ cf-inspector snapshot \
72
58
  --remote-root 'regex:^/(home/vcap/app|example-root-.*)$'
73
59
  ```
74
60
 
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.
61
+ This command internally calls `@saptools/cf-debugger` to open the SSH tunnel, runs the snapshot through it, and tears the tunnel down on exit.
76
62
 
77
63
  ---
78
64
 
@@ -111,24 +97,21 @@ cf-inspector snapshot --port 9229 \
111
97
  | `--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
98
  | `--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
99
  | `--timeout <seconds>` | How long to wait for the breakpoint to hit (default: `30`) |
100
+ | `--max-value-length <chars>` | Maximum characters per captured value before truncation (default: `4096`) |
114
101
  | `--remote-root <value>` | Optional path-mapping anchor: literal path or `regex:<pattern>` / `/pattern/flags` |
115
102
  | `--include-scopes` | Include expanded paused-frame scopes under `topFrame.scopes`. Omitted by default to keep targeted captures concise |
116
103
  | `--no-json` | Print a human-readable summary instead of JSON |
117
- | `--keep-paused` | Skip `Debugger.resume` after capture; Node may resume when the CLI disconnects |
104
+ | `--keep-paused` | Skip `Debugger.resume` after capture |
118
105
  | `--fail-on-unmatched-pause` | Fail immediately if the target pauses somewhere else instead of waiting cooperatively |
119
106
 
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
126
- the matching pause event until `Debugger.resume` completes. It does not include
127
- the time spent waiting for the breakpoint to hit. When `--keep-paused` is used,
128
- `pausedDurationMs` is `null` because `cf-inspector` intentionally skips
129
- `Debugger.resume`. Node may resume execution when this one-shot CLI disconnects,
130
- so treat `--keep-paused` as a low-level diagnostic escape hatch, not a durable
131
- paused-session mode.
107
+ Snapshot JSON includes frame metadata and `captures` by default. `topFrame.scopes`
108
+ is only present with `--include-scopes` because scope objects can be large and
109
+ drown out targeted captures. Values are raw debugger values, so be careful when
110
+ sharing logs.
111
+
112
+ `pausedDurationMs` measures the client-observed time from receiving the matching
113
+ pause event until `Debugger.resume` completes. With `--keep-paused`, it is `null`
114
+ because resume is intentionally skipped.
132
115
 
133
116
  If the target pauses somewhere else first, for example another debugger's
134
117
  breakpoint or a `debugger;` statement, `snapshot` does not resume it by default.
@@ -221,6 +204,7 @@ const bp = await setBreakpoint(session, {
221
204
  const pause = await waitForPause(session, { timeoutMs: 30_000 });
222
205
  const snapshot = await captureSnapshot(session, pause, {
223
206
  captures: ["this.user"],
207
+ maxValueLength: 4096,
224
208
  });
225
209
  const topFrame = pause.callFrames[0];
226
210
  if (topFrame === undefined) {
@@ -242,7 +226,7 @@ console.log({ bp, snapshot, customValue });
242
226
  | `setBreakpoint(session, location)` | Set a breakpoint by file/line + optional remote root |
243
227
  | `removeBreakpoint(session, id)` | Remove a breakpoint by id |
244
228
  | `waitForPause(session, options)` | Resolve when the next `Debugger.paused` event fires |
245
- | `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes |
229
+ | `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes or `maxValueLength` to override the default captured value limit |
246
230
  | `evaluateOnFrame(session, frameId, expression)` | Evaluate in a paused frame |
247
231
  | `evaluateGlobal(session, expression)` | Evaluate against the global Runtime |
248
232
  | `listScripts(session)` | Return the scripts the V8 instance knows about |
package/dist/cli.js CHANGED
@@ -1067,11 +1067,12 @@ async function waitForStop(session, options) {
1067
1067
  }
1068
1068
 
1069
1069
  // src/snapshot.ts
1070
+ init_types();
1070
1071
  var MAX_SCOPES = 3;
1071
1072
  var MAX_SCOPE_VARIABLES = 20;
1072
1073
  var MAX_CHILD_VARIABLES = 8;
1073
1074
  var MAX_VARIABLE_DEPTH = 2;
1074
- var MAX_VALUE_LENGTH = 240;
1075
+ var DEFAULT_MAX_VALUE_LENGTH = 4096;
1075
1076
  var PRIORITY_BY_TYPE = {
1076
1077
  local: 0,
1077
1078
  arguments: 1,
@@ -1132,16 +1133,28 @@ function formatPrimitive(value) {
1132
1133
  }
1133
1134
  return String(value);
1134
1135
  }
1135
- function limitValueLength(raw) {
1136
- if (raw.length <= MAX_VALUE_LENGTH) {
1136
+ function resolveMaxValueLength(value) {
1137
+ if (value === void 0) {
1138
+ return DEFAULT_MAX_VALUE_LENGTH;
1139
+ }
1140
+ if (!Number.isInteger(value) || value <= 0) {
1141
+ throw new CfInspectorError(
1142
+ "INVALID_ARGUMENT",
1143
+ `Invalid maxValueLength: ${value.toString()} \u2014 expected a positive integer`
1144
+ );
1145
+ }
1146
+ return value;
1147
+ }
1148
+ function limitValueLength(raw, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
1149
+ if (raw.length <= maxValueLength) {
1137
1150
  return raw;
1138
1151
  }
1139
- return `${raw.slice(0, MAX_VALUE_LENGTH)}...`;
1152
+ return `${raw.slice(0, maxValueLength)}...`;
1140
1153
  }
1141
1154
  function isExpandable(type) {
1142
1155
  return type === "object" || type === "function";
1143
1156
  }
1144
- async function captureProperties(session, objectId, limit, depth) {
1157
+ async function captureProperties(session, objectId, limit, depth, maxValueLength) {
1145
1158
  const properties = await getProperties(session, objectId);
1146
1159
  const limited = properties.slice(0, limit);
1147
1160
  const variables = await Promise.all(
@@ -1155,7 +1168,8 @@ async function captureProperties(session, objectId, limit, depth) {
1155
1168
  session,
1156
1169
  described.objectId,
1157
1170
  MAX_CHILD_VARIABLES,
1158
- depth - 1
1171
+ depth - 1,
1172
+ maxValueLength
1159
1173
  );
1160
1174
  if (nested.length > 0) {
1161
1175
  children = nested;
@@ -1163,7 +1177,7 @@ async function captureProperties(session, objectId, limit, depth) {
1163
1177
  } catch {
1164
1178
  }
1165
1179
  }
1166
- const sanitizedValue = limitValueLength(described.value);
1180
+ const sanitizedValue = limitValueLength(described.value, maxValueLength);
1167
1181
  const base = { name, value: sanitizedValue };
1168
1182
  const withType = described.type === void 0 ? base : { ...base, type: described.type };
1169
1183
  return children === void 0 ? withType : { ...withType, children };
@@ -1178,7 +1192,7 @@ function selectScopes(scopeChain) {
1178
1192
  function priorityOf(type) {
1179
1193
  return PRIORITY_BY_TYPE[type] ?? Number.MAX_SAFE_INTEGER;
1180
1194
  }
1181
- async function captureScopes(session, frame) {
1195
+ async function captureScopes(session, frame, maxValueLength) {
1182
1196
  const scopes = selectScopes(frame.scopeChain);
1183
1197
  return await Promise.all(
1184
1198
  scopes.map(async (scope) => {
@@ -1187,7 +1201,13 @@ async function captureScopes(session, frame) {
1187
1201
  return { type: scope.type, variables: [] };
1188
1202
  }
1189
1203
  try {
1190
- const variables = await captureProperties(session, objectId, MAX_SCOPE_VARIABLES, MAX_VARIABLE_DEPTH);
1204
+ const variables = await captureProperties(
1205
+ session,
1206
+ objectId,
1207
+ MAX_SCOPE_VARIABLES,
1208
+ MAX_VARIABLE_DEPTH,
1209
+ maxValueLength
1210
+ );
1191
1211
  return { type: scope.type, variables };
1192
1212
  } catch {
1193
1213
  return { type: scope.type, variables: [] };
@@ -1195,10 +1215,10 @@ async function captureScopes(session, frame) {
1195
1215
  })
1196
1216
  );
1197
1217
  }
1198
- function evalResultToCaptured(expression, result) {
1218
+ function evalResultToCaptured(expression, result, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
1199
1219
  if (result.exceptionDetails !== void 0) {
1200
1220
  const text = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
1201
- return { expression, error: limitValueLength(text) };
1221
+ return { expression, error: limitValueLength(text, maxValueLength) };
1202
1222
  }
1203
1223
  const inner = result.result;
1204
1224
  if (!inner) {
@@ -1206,7 +1226,7 @@ function evalResultToCaptured(expression, result) {
1206
1226
  }
1207
1227
  const type = typeof inner.type === "string" ? inner.type : void 0;
1208
1228
  const buildCaptured = (rendered) => {
1209
- const sanitized = limitValueLength(rendered);
1229
+ const sanitized = limitValueLength(rendered, maxValueLength);
1210
1230
  const base = { expression, value: sanitized };
1211
1231
  return type === void 0 ? base : { ...base, type };
1212
1232
  };
@@ -1301,9 +1321,15 @@ function toStructuredValue(variable) {
1301
1321
  }
1302
1322
  return out;
1303
1323
  }
1304
- async function renderObjectCapture(session, objectId) {
1324
+ async function renderObjectCapture(session, objectId, maxValueLength) {
1305
1325
  try {
1306
- const properties = await captureProperties(session, objectId, MAX_SCOPE_VARIABLES, MAX_VARIABLE_DEPTH);
1326
+ const properties = await captureProperties(
1327
+ session,
1328
+ objectId,
1329
+ MAX_SCOPE_VARIABLES,
1330
+ MAX_VARIABLE_DEPTH,
1331
+ maxValueLength
1332
+ );
1307
1333
  const structured = {};
1308
1334
  for (const variable of properties) {
1309
1335
  structured[variable.name] = toStructuredValue(variable);
@@ -1322,7 +1348,7 @@ function normalizeRenderedObjectCapture(rendered, original) {
1322
1348
  }
1323
1349
  return rendered;
1324
1350
  }
1325
- async function withSerializedObjectCapture(session, expression, evalResult, captured) {
1351
+ async function withSerializedObjectCapture(session, expression, evalResult, captured, maxValueLength) {
1326
1352
  if (captured.error !== void 0 || captured.value === void 0) {
1327
1353
  return captured;
1328
1354
  }
@@ -1330,7 +1356,7 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
1330
1356
  if (objectId === void 0) {
1331
1357
  return captured;
1332
1358
  }
1333
- const rendered = await renderObjectCapture(session, objectId);
1359
+ const rendered = await renderObjectCapture(session, objectId, maxValueLength);
1334
1360
  if (rendered === void 0) {
1335
1361
  return captured;
1336
1362
  }
@@ -1338,10 +1364,11 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
1338
1364
  if (normalized === void 0) {
1339
1365
  return captured;
1340
1366
  }
1341
- const value = limitValueLength(normalized);
1367
+ const value = limitValueLength(normalized, maxValueLength);
1342
1368
  return captured.type === void 0 ? { expression, value } : { expression, value, type: captured.type };
1343
1369
  }
1344
1370
  async function captureSnapshot(session, pause, options = {}) {
1371
+ const maxValueLength = resolveMaxValueLength(options.maxValueLength);
1345
1372
  const top = pause.callFrames[0];
1346
1373
  let topFrame;
1347
1374
  let captures = [];
@@ -1353,7 +1380,7 @@ async function captureSnapshot(session, pause, options = {}) {
1353
1380
  column: top.columnNumber + 1
1354
1381
  };
1355
1382
  if (options.includeScopes === true) {
1356
- const scopes = await captureScopes(session, top);
1383
+ const scopes = await captureScopes(session, top, maxValueLength);
1357
1384
  topFrame = { ...topFrame, scopes };
1358
1385
  }
1359
1386
  if (options.captures !== void 0 && options.captures.length > 0) {
@@ -1361,11 +1388,17 @@ async function captureSnapshot(session, pause, options = {}) {
1361
1388
  options.captures.map(async (expression) => {
1362
1389
  try {
1363
1390
  const result = await evaluateOnFrame(session, top.callFrameId, expression);
1364
- const captured = evalResultToCaptured(expression, result);
1365
- return await withSerializedObjectCapture(session, expression, result, captured);
1391
+ const captured = evalResultToCaptured(expression, result, maxValueLength);
1392
+ return await withSerializedObjectCapture(
1393
+ session,
1394
+ expression,
1395
+ result,
1396
+ captured,
1397
+ maxValueLength
1398
+ );
1366
1399
  } catch (err) {
1367
1400
  const message = err instanceof Error ? err.message : String(err);
1368
- return { expression, error: limitValueLength(message) };
1401
+ return { expression, error: limitValueLength(message, maxValueLength) };
1369
1402
  }
1370
1403
  })
1371
1404
  );
@@ -1643,6 +1676,7 @@ async function handleSnapshot(opts) {
1643
1676
  const remoteRoot = parseRemoteRoot(opts.remoteRoot);
1644
1677
  const captures = parseCaptureList(opts.capture);
1645
1678
  const timeoutSec = parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_BREAKPOINT_TIMEOUT_SEC;
1679
+ const maxValueLength = parsePositiveInt(opts.maxValueLength, "--max-value-length");
1646
1680
  const timeoutMs = timeoutSec * 1e3;
1647
1681
  const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
1648
1682
  const result = await withSession(target, async (session) => {
@@ -1677,7 +1711,8 @@ async function handleSnapshot(opts) {
1677
1711
  const pausedStartedAt = pause.receivedAtMs ?? performance2.now();
1678
1712
  const snapshot = await captureSnapshot(session, pause, {
1679
1713
  captures,
1680
- includeScopes: opts.includeScopes === true
1714
+ includeScopes: opts.includeScopes === true,
1715
+ ...maxValueLength === void 0 ? {} : { maxValueLength }
1681
1716
  });
1682
1717
  if (opts.keepPaused === true) {
1683
1718
  return withPausedDuration(snapshot, null);
@@ -1848,7 +1883,7 @@ async function main(argv) {
1848
1883
  "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42",
1849
1884
  collectStrings,
1850
1885
  []
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(
1886
+ ).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("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option(
1852
1887
  "--condition <expr>",
1853
1888
  "Only pause when this JS expression evaluates truthy in the paused frame"
1854
1889
  ).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) => {