@saptools/cf-inspector 0.4.10 → 0.4.12

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
@@ -40,6 +40,8 @@ Built so an AI agent (or a CI job) can drive a debugger from a single shell comm
40
40
  npm install -g @saptools/cf-inspector
41
41
  # or
42
42
  pnpm add @saptools/cf-inspector
43
+
44
+ cf-inspector --version
43
45
  ```
44
46
 
45
47
  > [!NOTE]
@@ -94,6 +96,7 @@ cf-inspector snapshot --port 9229 \
94
96
  | `--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 |
95
97
  | `--hit-count <n>` | Skip the first N − 1 hits and only pause on the Nth (combines with `--condition` via logical AND) |
96
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 |
99
+ | `--setup-eval <expr>` | Repeatable, order-preserving global expression evaluated inside the inspected process before breakpoint setup. It can mutate runtime state, so use it only in controlled debug sessions |
97
100
  | `--stack-depth <n>` | Walk this many call frames per hit (default: `1`, top frame only). When `> 1`, the result includes a `stack` array |
98
101
  | `--stack-captures <expr,…>` | Expressions to evaluate on each call frame in the captured stack |
99
102
  | `--timeout <seconds>` | How long to wait for the breakpoint to hit (default: `30`) |
@@ -203,6 +206,7 @@ Each event is a `WatchEvent`:
203
206
  | `--port <number>` | Local port the inspector or tunnel listens on |
204
207
  | `--bp <file:line>` | **Required.** Source location to capture on (repeatable) |
205
208
  | `--capture <expr,…>` | Top-level comma-separated expressions to evaluate per hit |
209
+ | `--setup-eval <expr>` | Repeatable, order-preserving global expression evaluated inside the inspected process before breakpoint setup. It can mutate runtime state, so use it only in controlled debug sessions |
206
210
  | `--condition <expr>` | Only emit hits where this expression evaluates truthy |
207
211
  | `--hit-count <n>` | Start emitting once the line has been hit N or more times |
208
212
  | `--remote-root <value>` | Path-mapping anchor (same DSL as `snapshot`) |
@@ -282,6 +286,8 @@ cf-inspector list-targets --port 9229
282
286
  cf-inspector snapshot --port 9229 --target 1 --bp dist/worker.js:42
283
287
  ```
284
288
 
289
+ If `list-targets`, `attach`, or another command reports `ECONNREFUSED`, the local inspector or tunnel on that port is usually stale/closed. Restart the local Node inspector or tunnel and retry; for Cloud Foundry targets, prefer `--app/--region/--org/--space` so `cf-inspector` can open a fresh tunnel.
290
+
285
291
  ### 🔗 `cf-inspector attach`
286
292
 
287
293
  Connect, fetch the runtime version, print it, disconnect. Useful as a smoke-test that the tunnel is healthy.
package/dist/cli.js CHANGED
@@ -155,6 +155,9 @@ var init_wsTransport = __esm({
155
155
  import process12 from "process";
156
156
 
157
157
  // src/cli/program.ts
158
+ import { readFileSync } from "fs";
159
+ import { dirname, join } from "path";
160
+ import { fileURLToPath } from "url";
158
161
  import { Command } from "commander";
159
162
 
160
163
  // src/cli/commands/attach.ts
@@ -190,16 +193,39 @@ async function fetchJson(url, timeoutMs) {
190
193
  );
191
194
  });
192
195
  req.on("error", (err) => {
193
- reject(
194
- err instanceof CfInspectorError ? err : new CfInspectorError(
195
- "INSPECTOR_DISCOVERY_FAILED",
196
- `Inspector discovery at ${url} failed: ${err.message}`
197
- )
198
- );
196
+ reject(err instanceof CfInspectorError ? err : formatDiscoveryRequestError(url, err));
199
197
  });
200
198
  req.end();
201
199
  });
202
200
  }
201
+ function isNodeSystemError(err) {
202
+ return err instanceof Error;
203
+ }
204
+ function isConnectionRefusedOrUnreachable(code) {
205
+ return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "EHOSTUNREACH" || code === "ENETUNREACH";
206
+ }
207
+ function formatEndpoint(url, err) {
208
+ if (typeof err.address === "string" && typeof err.port === "number") {
209
+ return `${err.address}:${err.port.toString()}`;
210
+ }
211
+ const parsed = new URL(url);
212
+ return parsed.host;
213
+ }
214
+ function formatDiscoveryRequestError(url, err) {
215
+ const detail = err instanceof Error ? err.message : String(err);
216
+ if (!isNodeSystemError(err) || !isConnectionRefusedOrUnreachable(err.code)) {
217
+ return new CfInspectorError(
218
+ "INSPECTOR_DISCOVERY_FAILED",
219
+ `Inspector discovery at ${url} failed: ${detail}`
220
+ );
221
+ }
222
+ const endpoint = formatEndpoint(url, err);
223
+ return new CfInspectorError(
224
+ "INSPECTOR_DISCOVERY_FAILED",
225
+ `Cannot reach Node inspector discovery at ${url}. Nothing is listening on ${endpoint}, or the inspector tunnel is stale/closed. Restart the local inspector or tunnel and retry. If this port came from cf-debugger, stop the stale session and start a fresh tunnel, or run cf-inspector with --app/--region/--org/--space so it can open a tunnel.`,
226
+ detail
227
+ );
228
+ }
203
229
  function parseJsonResponse(chunks) {
204
230
  const text = Buffer.concat(chunks).toString("utf8");
205
231
  return JSON.parse(text);
@@ -1124,6 +1150,20 @@ async function evaluateGlobal(session, expression) {
1124
1150
  silent: true
1125
1151
  });
1126
1152
  }
1153
+ async function runSetupEvals(session, expressions) {
1154
+ for (const expression of expressions) {
1155
+ const result = await evaluateGlobal(session, expression);
1156
+ if (result.exceptionDetails !== void 0) {
1157
+ throw new CfInspectorError(
1158
+ "SETUP_EVAL_FAILED",
1159
+ exceptionDetailsMessage(result, "setup evaluation failed")
1160
+ );
1161
+ }
1162
+ }
1163
+ }
1164
+ function exceptionDetailsMessage(result, fallback) {
1165
+ return typeof result.exceptionDetails?.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails?.text === "string" ? result.exceptionDetails.text : fallback;
1166
+ }
1127
1167
  function listScripts(session) {
1128
1168
  return [...session.scripts.values()];
1129
1169
  }
@@ -2711,8 +2751,10 @@ function prepareSnapshotCommand(opts, target) {
2711
2751
  const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
2712
2752
  const hitCount = parsePositiveInt(opts.hitCount, "--hit-count");
2713
2753
  const stackDepth = parsePositiveInt(opts.stackDepth, "--stack-depth");
2754
+ const setupEvals = parseSetupEvals(opts.setupEval);
2714
2755
  return {
2715
2756
  target,
2757
+ setupEvals,
2716
2758
  breakpoints: opts.bp.map((spec) => parseBreakpointSpec(spec)),
2717
2759
  captures: parseCaptureList(opts.capture),
2718
2760
  remoteRoot: parseRemoteRoot(opts.remoteRoot),
@@ -2726,48 +2768,57 @@ function prepareSnapshotCommand(opts, target) {
2726
2768
  }
2727
2769
  async function runSnapshotCommand(command, opts, reportProgress) {
2728
2770
  return await withSession(command.target, async (session) => {
2729
- if (command.condition !== void 0) {
2730
- reportProgress?.("Validating the breakpoint condition...");
2731
- await validateExpression(session, command.condition);
2732
- reportProgress?.("Breakpoint condition is valid.");
2733
- }
2734
- const breakpointCount = command.breakpoints.length;
2735
- reportProgress?.(
2736
- `Setting ${breakpointCount.toString()} ${breakpointCount === 1 ? "breakpoint" : "breakpoints"}...`
2737
- );
2738
- const handles = await setCommandBreakpoints(session, command);
2739
- const resolvedCount = handles.reduce(
2740
- (total, handle) => total + handle.resolvedLocations.length,
2741
- 0
2742
- );
2743
- reportProgress?.(
2744
- `Breakpoint setup complete: ${resolvedCount.toString()} resolved ${resolvedCount === 1 ? "location" : "locations"}.`
2745
- );
2746
- warnOnUnboundBreakpoints(handles);
2747
- reportProgress?.(
2748
- `Waiting up to ${(command.timeoutMs / 1e3).toString()}s for a breakpoint hit...`
2749
- );
2750
- const pause = await waitForCommandPause(session, opts, handles, command.timeoutMs);
2751
- const captureCount = command.captures.length;
2752
- reportProgress?.(
2753
- `Breakpoint hit; capturing ${captureCount.toString()} ${captureCount === 1 ? "expression" : "expressions"}...`
2754
- );
2755
- const pausedStartedAt = pause.receivedAtMs ?? performance4.now();
2756
- const snapshot = await captureSnapshot(session, pause, {
2757
- captures: command.captures,
2758
- includeScopes: opts.includeScopes === true,
2759
- ...command.maxValueLength === void 0 ? {} : { maxValueLength: command.maxValueLength },
2760
- ...command.stackDepth === void 0 ? {} : { stackDepth: command.stackDepth },
2761
- stackCaptures: command.stackCaptures
2762
- });
2763
- if (opts.keepPaused === true) {
2764
- reportProgress?.("Snapshot captured; leaving the target paused as requested.");
2765
- return withPausedDuration(snapshot, null);
2766
- }
2767
- reportProgress?.("Snapshot captured; resuming the target...");
2768
- return await resumeAfterSnapshot(session, snapshot, pausedStartedAt, reportProgress);
2771
+ return await runSnapshotOnSession(session, command, opts, reportProgress);
2769
2772
  }, reportProgress);
2770
2773
  }
2774
+ async function runSnapshotOnSession(session, command, opts, reportProgress) {
2775
+ if (command.setupEvals.length > 0) {
2776
+ const setupCount = command.setupEvals.length;
2777
+ reportProgress?.(`Running ${setupCount.toString()} setup ${setupCount === 1 ? "evaluation" : "evaluations"}...`);
2778
+ await runSetupEvals(session, command.setupEvals);
2779
+ reportProgress?.("Setup evaluation complete.");
2780
+ }
2781
+ if (command.condition !== void 0) {
2782
+ reportProgress?.("Validating the breakpoint condition...");
2783
+ await validateExpression(session, command.condition);
2784
+ reportProgress?.("Breakpoint condition is valid.");
2785
+ }
2786
+ const breakpointCount = command.breakpoints.length;
2787
+ reportProgress?.(
2788
+ `Setting ${breakpointCount.toString()} ${breakpointCount === 1 ? "breakpoint" : "breakpoints"}...`
2789
+ );
2790
+ const handles = await setCommandBreakpoints(session, command);
2791
+ const resolvedCount = handles.reduce(
2792
+ (total, handle) => total + handle.resolvedLocations.length,
2793
+ 0
2794
+ );
2795
+ reportProgress?.(
2796
+ `Breakpoint setup complete: ${resolvedCount.toString()} resolved ${resolvedCount === 1 ? "location" : "locations"}.`
2797
+ );
2798
+ warnOnUnboundBreakpoints(handles);
2799
+ reportProgress?.(
2800
+ `Waiting up to ${(command.timeoutMs / 1e3).toString()}s for a breakpoint hit...`
2801
+ );
2802
+ const pause = await waitForCommandPause(session, opts, handles, command.timeoutMs);
2803
+ const captureCount = command.captures.length;
2804
+ reportProgress?.(
2805
+ `Breakpoint hit; capturing ${captureCount.toString()} ${captureCount === 1 ? "expression" : "expressions"}...`
2806
+ );
2807
+ const pausedStartedAt = pause.receivedAtMs ?? performance4.now();
2808
+ const snapshot = await captureSnapshot(session, pause, {
2809
+ captures: command.captures,
2810
+ includeScopes: opts.includeScopes === true,
2811
+ ...command.maxValueLength === void 0 ? {} : { maxValueLength: command.maxValueLength },
2812
+ ...command.stackDepth === void 0 ? {} : { stackDepth: command.stackDepth },
2813
+ stackCaptures: command.stackCaptures
2814
+ });
2815
+ if (opts.keepPaused === true) {
2816
+ reportProgress?.("Snapshot captured; leaving the target paused as requested.");
2817
+ return withPausedDuration(snapshot, null);
2818
+ }
2819
+ reportProgress?.("Snapshot captured; resuming the target...");
2820
+ return await resumeAfterSnapshot(session, snapshot, pausedStartedAt, reportProgress);
2821
+ }
2771
2822
  async function setCommandBreakpoints(session, command) {
2772
2823
  return await Promise.all(
2773
2824
  command.breakpoints.map(
@@ -2808,6 +2859,10 @@ async function resumeAfterSnapshot(session, snapshot, pausedStartedAt, reportPro
2808
2859
  return withPausedDuration(snapshot, null);
2809
2860
  }
2810
2861
  }
2862
+ function parseSetupEvals(raw) {
2863
+ const values = Array.isArray(raw) ? raw : [];
2864
+ return values.filter((expr) => typeof expr === "string" && expr.trim().length > 0).map((expr) => expr.trim());
2865
+ }
2811
2866
 
2812
2867
  // src/cli/commands/watch.ts
2813
2868
  import { performance as performance5 } from "perf_hooks";
@@ -2841,8 +2896,10 @@ function prepareWatchCommand(opts, target) {
2841
2896
  const hitCount = parsePositiveInt(opts.hitCount, "--hit-count");
2842
2897
  const stackDepth = parsePositiveInt(opts.stackDepth, "--stack-depth");
2843
2898
  const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
2899
+ const setupEvals = parseSetupEvals2(opts.setupEval);
2844
2900
  return {
2845
2901
  target,
2902
+ setupEvals,
2846
2903
  breakpoints: opts.bp.map((spec) => parseBreakpointSpec(spec)),
2847
2904
  captures: parseCaptureList(opts.capture),
2848
2905
  remoteRoot: parseRemoteRoot(opts.remoteRoot),
@@ -2857,6 +2914,9 @@ function prepareWatchCommand(opts, target) {
2857
2914
  };
2858
2915
  }
2859
2916
  async function runWatchLoop(session, command, opts, signal) {
2917
+ if (command.setupEvals.length > 0) {
2918
+ await runSetupEvals(session, command.setupEvals);
2919
+ }
2860
2920
  if (command.condition !== void 0) {
2861
2921
  await validateExpression(session, command.condition);
2862
2922
  }
@@ -3030,6 +3090,10 @@ function writeWatchSummary(reason, emitted, json) {
3030
3090
  `
3031
3091
  );
3032
3092
  }
3093
+ function parseSetupEvals2(raw) {
3094
+ const values = Array.isArray(raw) ? raw : [];
3095
+ return values.filter((expr) => typeof expr === "string" && expr.trim().length > 0).map((expr) => expr.trim());
3096
+ }
3033
3097
 
3034
3098
  // src/cli/program.ts
3035
3099
  function applyTargetOptions(cmd, options = {}) {
@@ -3040,9 +3104,27 @@ var collectStrings = (value, prev = []) => [
3040
3104
  ...prev,
3041
3105
  value
3042
3106
  ];
3107
+ function readPackageVersion() {
3108
+ let current = dirname(fileURLToPath(import.meta.url));
3109
+ for (let depth = 0; depth < 4; depth += 1) {
3110
+ const candidate = join(current, "package.json");
3111
+ try {
3112
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
3113
+ if (typeof parsed === "object" && parsed !== null) {
3114
+ const record = parsed;
3115
+ if (record["name"] === "@saptools/cf-inspector" && typeof record["version"] === "string") {
3116
+ return record["version"];
3117
+ }
3118
+ }
3119
+ } catch {
3120
+ }
3121
+ current = dirname(current);
3122
+ }
3123
+ throw new Error("Unable to read @saptools/cf-inspector package version");
3124
+ }
3043
3125
  async function main(argv) {
3044
3126
  const program = new Command();
3045
- program.name("cf-inspector").description("Drive a Node.js inspector from the command line \u2014 set breakpoints, capture snapshots, evaluate expressions");
3127
+ program.name("cf-inspector").version(readPackageVersion()).description("Drive a Node.js inspector from the command line \u2014 set breakpoints, capture snapshots, evaluate expressions");
3046
3128
  registerSnapshot(program);
3047
3129
  registerLog(program);
3048
3130
  registerWatch(program);
@@ -3057,7 +3139,7 @@ function registerSnapshot(program) {
3057
3139
  applyTargetOptions(
3058
3140
  program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume"),
3059
3141
  { includeTimeout: false }
3060
- ).option("--bp <file:line>", "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42", collectStrings, []).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("--condition <expr>", "Only pause when this JS expression evaluates truthy in the paused frame").option("--hit-count <n>", "Only pause after the breakpoint has been hit N or more times").option("--stack-depth <n>", "Walk this many call frames when capturing (default: 1, only top frame)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame in the stack").option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--no-json", "Print a human-readable summary instead of JSON").option("--quiet", "Suppress progress messages on stderr").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) => {
3142
+ ).option("--bp <file:line>", "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--setup-eval <expr>", "Evaluate a global setup expression before breakpoint setup (repeatable)", collectStrings, []).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("--condition <expr>", "Only pause when this JS expression evaluates truthy in the paused frame").option("--hit-count <n>", "Only pause after the breakpoint has been hit N or more times").option("--stack-depth <n>", "Walk this many call frames when capturing (default: 1, only top frame)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame in the stack").option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--no-json", "Print a human-readable summary instead of JSON").option("--quiet", "Suppress progress messages on stderr").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) => {
3061
3143
  await handleSnapshot(opts);
3062
3144
  });
3063
3145
  }
@@ -3072,7 +3154,7 @@ function registerWatch(program) {
3072
3154
  applyTargetOptions(
3073
3155
  program.command("watch").description("Stream a snapshot per breakpoint hit (multi-shot watch); resume between hits"),
3074
3156
  { includeTimeout: false }
3075
- ).option("--bp <file:line>", "Breakpoint location (repeatable), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate per hit").option("--condition <expr>", "Only emit hits where this JS expression evaluates truthy").option("--hit-count <n>", "Start emitting after the line has been hit N or more times").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--duration <seconds>", "Stop streaming after N seconds (default: run until SIGINT)").option("--max-events <n>", "Stop streaming after emitting N watch events").option("--timeout <seconds>", "How long to wait for the next hit before giving up (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--stack-depth <n>", "Walk this many call frames per hit (default: 1)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame").option("--include-scopes", "Include expanded paused-frame scopes per hit").option("--no-json", "Print human-readable lines instead of JSON Lines").action(async (opts) => {
3157
+ ).option("--bp <file:line>", "Breakpoint location (repeatable), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate per hit").option("--setup-eval <expr>", "Evaluate a global setup expression before breakpoint setup (repeatable)", collectStrings, []).option("--condition <expr>", "Only emit hits where this JS expression evaluates truthy").option("--hit-count <n>", "Start emitting after the line has been hit N or more times").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--duration <seconds>", "Stop streaming after N seconds (default: run until SIGINT)").option("--max-events <n>", "Stop streaming after emitting N watch events").option("--timeout <seconds>", "How long to wait for the next hit before giving up (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--stack-depth <n>", "Walk this many call frames per hit (default: 1)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame").option("--include-scopes", "Include expanded paused-frame scopes per hit").option("--no-json", "Print human-readable lines instead of JSON Lines").action(async (opts) => {
3076
3158
  await handleWatch(opts);
3077
3159
  });
3078
3160
  }