@saptools/cf-inspector 0.2.1 → 0.3.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/README.md CHANGED
@@ -69,7 +69,7 @@ export SAP_PASSWORD=...
69
69
  cf-inspector snapshot \
70
70
  --region eu10 --org my-org --space dev --app my-srv \
71
71
  --bp src/handler.ts:42 \
72
- --remote-root 'regex:^/(home/vcap/app|srv-.*)$'
72
+ --remote-root 'regex:^/(home/vcap/app|example-root-.*)$'
73
73
  ```
74
74
 
75
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.
@@ -233,14 +233,18 @@ console.log({ bp, snapshot, customValue });
233
233
 
234
234
  | Code | When |
235
235
  | --- | --- |
236
- | `INVALID_BREAKPOINT` | `--bp` is not in `file:line` form, or line is not a positive integer |
236
+ | `INVALID_ARGUMENT` | A numeric flag (`--port`, `--timeout`, `--duration`, …) is not a positive integer |
237
+ | `INVALID_BREAKPOINT` | `--bp` / `--at` is not in `file:line` form, or line is not a positive integer |
237
238
  | `INVALID_REMOTE_ROOT` | `--remote-root` regex did not compile |
239
+ | `INVALID_EXPRESSION` | `--condition` or `--expr` failed to parse on V8 (`Runtime.compileScript` reported a SyntaxError) — fast-fail before the breakpoint is set |
240
+ | `BREAKPOINT_DID_NOT_BIND` | Reserved: a breakpoint resolved to no scripts. Currently surfaced as a stderr warning only — see `BreakpointHandle.resolvedLocations` for programmatic detection |
238
241
  | `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 |
242
+ | `INSPECTOR_CONNECTION_FAILED` | WebSocket handshake failed, or the connection closed mid-request |
243
+ | `CDP_REQUEST_FAILED` | A CDP method returned an error result, timed out, or failed to send |
241
244
  | `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 |
245
+ | `EVALUATION_FAILED` | Reserved for future use — current evaluation paths surface remote exceptions inline via `CapturedExpression.error` instead of throwing |
246
+ | `MISSING_TARGET` | Neither `--port` nor a complete CF target (`--region/--org/--space/--app`) was provided |
247
+ | `ABORTED` | Reserved for future use by long-running streams when an `AbortSignal` fires |
244
248
 
245
249
  </details>
246
250
 
@@ -265,8 +269,8 @@ Path mapping uses CDP's first-class `urlRegex`:
265
269
  | --- | --- |
266
270
  | _omitted_ | `(?:^|/)src/handler\.(?:ts\|js)$` |
267
271
  | `/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 |
272
+ | `regex:^/example-root-.*$` | `^file:///example-root-[^/]+/src/handler\.(?:ts\|js)$` |
273
+ | `/^/example-root-.*$/` | same as above |
270
274
 
271
275
  `.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
276
 
package/dist/cli.js CHANGED
@@ -606,7 +606,11 @@ async function connectInspector(options) {
606
606
  });
607
607
  const PAUSE_BUFFER_LIMIT = 32;
608
608
  const pauseBuffer = [];
609
+ const pauseWaitGate = { active: false };
609
610
  client.on("Debugger.paused", (raw) => {
611
+ if (pauseWaitGate.active) {
612
+ return;
613
+ }
610
614
  const params = raw;
611
615
  const event = {
612
616
  reason: asString(params.reason),
@@ -625,6 +629,7 @@ async function connectInspector(options) {
625
629
  target,
626
630
  scripts,
627
631
  pauseBuffer,
632
+ pauseWaitGate,
628
633
  dispose: async () => {
629
634
  try {
630
635
  await client.send("Debugger.disable");
@@ -750,17 +755,23 @@ async function waitForPause(session, options) {
750
755
  return head;
751
756
  }
752
757
  }
753
- const params = await session.client.waitFor("Debugger.paused", {
754
- timeoutMs: options.timeoutMs,
755
- predicate: (raw) => {
756
- const event = {
757
- reason: asString(raw.reason),
758
- hitBreakpoints: Array.isArray(raw.hitBreakpoints) ? raw.hitBreakpoints.filter((id) => typeof id === "string") : [],
759
- callFrames: []
760
- };
761
- return pauseMatches(event, options.breakpointIds);
762
- }
763
- });
758
+ session.pauseWaitGate.active = true;
759
+ let params;
760
+ try {
761
+ const expected = options.breakpointIds;
762
+ params = await session.client.waitFor("Debugger.paused", {
763
+ timeoutMs: options.timeoutMs,
764
+ predicate: (raw) => {
765
+ if (expected === void 0 || expected.length === 0) {
766
+ return true;
767
+ }
768
+ const ids = Array.isArray(raw.hitBreakpoints) ? raw.hitBreakpoints.filter((id) => typeof id === "string") : [];
769
+ return ids.some((id) => expected.includes(id));
770
+ }
771
+ });
772
+ } finally {
773
+ session.pauseWaitGate.active = false;
774
+ }
764
775
  return {
765
776
  reason: asString(params.reason),
766
777
  hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
@@ -790,6 +801,18 @@ async function evaluateGlobal(session, expression) {
790
801
  function listScripts(session) {
791
802
  return [...session.scripts.values()];
792
803
  }
804
+ async function validateExpression(session, expression) {
805
+ const result = await session.client.send("Runtime.compileScript", {
806
+ expression,
807
+ sourceURL: "<cf-inspector-validate>",
808
+ persistScript: false
809
+ });
810
+ if (result.exceptionDetails === void 0) {
811
+ return;
812
+ }
813
+ const description = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "expression failed to compile";
814
+ throw new CfInspectorError("INVALID_EXPRESSION", description);
815
+ }
793
816
  async function getProperties(session, objectId) {
794
817
  const result = await session.client.send("Runtime.getProperties", {
795
818
  objectId,
@@ -870,12 +893,6 @@ function generateSentinel() {
870
893
  async function streamLogpoint(session, options) {
871
894
  const sentinel = generateSentinel();
872
895
  const condition = buildLogpointCondition(sentinel, options.expression);
873
- const handle = await setBreakpoint(session, {
874
- file: options.location.file,
875
- line: options.location.line,
876
- ...options.remoteRoot === void 0 ? {} : { remoteRoot: options.remoteRoot },
877
- condition
878
- });
879
896
  let emitted = 0;
880
897
  const offEvent = session.client.on("Runtime.consoleAPICalled", (raw) => {
881
898
  const params = raw;
@@ -890,6 +907,19 @@ async function streamLogpoint(session, options) {
890
907
  emitted += 1;
891
908
  options.onEvent(event);
892
909
  });
910
+ let handle;
911
+ try {
912
+ handle = await setBreakpoint(session, {
913
+ file: options.location.file,
914
+ line: options.location.line,
915
+ ...options.remoteRoot === void 0 ? {} : { remoteRoot: options.remoteRoot },
916
+ condition
917
+ });
918
+ } catch (err) {
919
+ offEvent();
920
+ throw err;
921
+ }
922
+ options.onBreakpointSet?.(handle);
893
923
  const cleanup = async () => {
894
924
  offEvent();
895
925
  try {
@@ -1164,8 +1194,8 @@ function parsePositiveInt(raw, label) {
1164
1194
  return void 0;
1165
1195
  }
1166
1196
  const value = Number.parseInt(raw, 10);
1167
- if (Number.isNaN(value) || value <= 0) {
1168
- throw new CfInspectorError("MISSING_TARGET", `Invalid ${label}: "${raw}"`);
1197
+ if (Number.isNaN(value) || value <= 0 || value.toString() !== raw.trim()) {
1198
+ throw new CfInspectorError("INVALID_ARGUMENT", `Invalid ${label}: "${raw}" \u2014 expected a positive integer`);
1169
1199
  }
1170
1200
  return value;
1171
1201
  }
@@ -1236,6 +1266,16 @@ function writeJson(value) {
1236
1266
  process.stdout.write(`${JSON.stringify(value, null, 2)}
1237
1267
  `);
1238
1268
  }
1269
+ function warnOnUnboundBreakpoints(handles) {
1270
+ for (const handle of handles) {
1271
+ if (handle.resolvedLocations.length === 0) {
1272
+ process.stderr.write(
1273
+ `[cf-inspector] warning: breakpoint ${handle.file}:${handle.line.toString()} did not bind to any loaded script. Check the path or pass --remote-root. Use 'list-scripts' to inspect what V8 currently has loaded.
1274
+ `
1275
+ );
1276
+ }
1277
+ }
1278
+ }
1239
1279
  function writeHumanSnapshot(snapshot) {
1240
1280
  const lines = [];
1241
1281
  lines.push(`Snapshot @ ${snapshot.capturedAt}`, ` reason: ${snapshot.reason}`);
@@ -1278,6 +1318,9 @@ async function handleSnapshot(opts) {
1278
1318
  const timeoutMs = timeoutSec * 1e3;
1279
1319
  const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
1280
1320
  const result = await withSession(target, async (session) => {
1321
+ if (condition !== void 0) {
1322
+ await validateExpression(session, condition);
1323
+ }
1281
1324
  const handles = await Promise.all(
1282
1325
  breakpoints.map(
1283
1326
  (bp) => setBreakpoint(session, {
@@ -1288,6 +1331,7 @@ async function handleSnapshot(opts) {
1288
1331
  })
1289
1332
  )
1290
1333
  );
1334
+ warnOnUnboundBreakpoints(handles);
1291
1335
  const breakpointIds = handles.map((h) => h.breakpointId);
1292
1336
  const pause = await waitForPause(session, { timeoutMs, breakpointIds });
1293
1337
  const snapshot = await captureSnapshot(session, pause, { captures });
@@ -1370,6 +1414,7 @@ async function handleLog(opts) {
1370
1414
  process.once("SIGTERM", onSig);
1371
1415
  try {
1372
1416
  await withSession(target, async (session) => {
1417
+ await validateExpression(session, expression);
1373
1418
  const result = await streamLogpoint(session, {
1374
1419
  location,
1375
1420
  expression,
@@ -1378,6 +1423,9 @@ async function handleLog(opts) {
1378
1423
  signal: abort.signal,
1379
1424
  onEvent: (event) => {
1380
1425
  writeLogEvent(event, opts.json);
1426
+ },
1427
+ onBreakpointSet: (handle) => {
1428
+ warnOnUnboundBreakpoints([handle]);
1381
1429
  }
1382
1430
  });
1383
1431
  if (opts.json) {