@saptools/cf-inspector 0.3.1 → 0.3.3
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 +30 -13
- package/dist/cli.js +276 -50
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +14 -5
- package/dist/index.js +136 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Built so an AI agent (or a CI job) can drive a debugger from a single shell comm
|
|
|
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`, `token`, `secret`, `cookie`, …) baked in
|
|
25
|
+
- 🧠 **Agent-friendly** — JSON-by-default I/O, deterministic shape, sensitive-name redaction (`password`, `credentials`, `token`, `secret`, `cookie`, …) baked in
|
|
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
|
|
@@ -109,15 +109,26 @@ cf-inspector snapshot --port 9229 \
|
|
|
109
109
|
| `--port <number>` | Local port the inspector or tunnel listens on. **Required** unless `--app/--region/--org/--space` are all set |
|
|
110
110
|
| `--bp <file:line>` | **Required.** Source location to break at. Pass multiple times to race several locations — the first one to hit wins |
|
|
111
111
|
| `--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
|
-
| `--capture <expr,…>` |
|
|
112
|
+
| `--capture <expr,…>` | Top-level comma-separated expressions to evaluate in the paused frame; nested commas inside objects, arrays, calls, or strings are preserved |
|
|
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
115
|
| `--no-json` | Print a human-readable summary instead of JSON |
|
|
116
|
-
| `--keep-paused` | Skip
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
| `--keep-paused` | Skip `Debugger.resume` after capture; Node may resume when the CLI disconnects |
|
|
117
|
+
| `--fail-on-unmatched-pause` | Fail immediately if the target pauses somewhere else instead of waiting cooperatively |
|
|
118
|
+
|
|
119
|
+
JSON output includes `pausedDurationMs`, the client-observed time from receiving
|
|
120
|
+
the matching pause event until `Debugger.resume` completes. It does not include
|
|
121
|
+
the time spent waiting for the breakpoint to hit. When `--keep-paused` is used,
|
|
122
|
+
`pausedDurationMs` is `null` because `cf-inspector` intentionally skips
|
|
123
|
+
`Debugger.resume`. Node may resume execution when this one-shot CLI disconnects,
|
|
124
|
+
so treat `--keep-paused` as a low-level diagnostic escape hatch, not a durable
|
|
125
|
+
paused-session mode.
|
|
126
|
+
|
|
127
|
+
If the target pauses somewhere else first, for example another debugger's
|
|
128
|
+
breakpoint or a `debugger;` statement, `snapshot` does not resume it by default.
|
|
129
|
+
It warns once, waits for `Debugger.resumed`, then continues waiting for its own
|
|
130
|
+
breakpoint within the remaining timeout. Use `--fail-on-unmatched-pause` when a
|
|
131
|
+
strict immediate error is preferred.
|
|
121
132
|
|
|
122
133
|
For Cloud Foundry targets, replace `--port` with `--region/--org/--space/--app` (and optionally `--cf-timeout <seconds>` for the tunnel).
|
|
123
134
|
|
|
@@ -160,7 +171,7 @@ When the user expression throws, the event is emitted with `error` instead of `v
|
|
|
160
171
|
|
|
161
172
|
### 🧮 `cf-inspector eval`
|
|
162
173
|
|
|
163
|
-
Evaluate one expression
|
|
174
|
+
Evaluate one expression with `Runtime.evaluate` in the global scope and print the result. For paused-frame values, use `snapshot --capture` or the programmatic `evaluateOnFrame(...)` API.
|
|
164
175
|
|
|
165
176
|
```bash
|
|
166
177
|
cf-inspector eval --port 9229 --expr 'process.uptime()'
|
|
@@ -203,7 +214,11 @@ const bp = await setBreakpoint(session, {
|
|
|
203
214
|
});
|
|
204
215
|
const pause = await waitForPause(session, { timeoutMs: 30_000 });
|
|
205
216
|
const snapshot = await captureSnapshot(session, pause);
|
|
206
|
-
const
|
|
217
|
+
const topFrame = pause.callFrames[0];
|
|
218
|
+
if (topFrame === undefined) {
|
|
219
|
+
throw new Error("Breakpoint paused without a call frame");
|
|
220
|
+
}
|
|
221
|
+
const customValue = await evaluateOnFrame(session, topFrame.callFrameId, "this.user");
|
|
207
222
|
await resume(session);
|
|
208
223
|
await session.dispose();
|
|
209
224
|
|
|
@@ -246,6 +261,8 @@ console.log({ bp, snapshot, customValue });
|
|
|
246
261
|
| `INSPECTOR_CONNECTION_FAILED` | WebSocket handshake failed, or the connection closed mid-request |
|
|
247
262
|
| `CDP_REQUEST_FAILED` | A CDP method returned an error result, timed out, or failed to send |
|
|
248
263
|
| `BREAKPOINT_NOT_HIT` | The breakpoint did not hit before the timeout elapsed |
|
|
264
|
+
| `UNRELATED_PAUSE` | The target paused somewhere else and `--fail-on-unmatched-pause` was enabled |
|
|
265
|
+
| `UNRELATED_PAUSE_TIMEOUT` | The target stayed paused somewhere else until the snapshot timeout elapsed |
|
|
249
266
|
| `EVALUATION_FAILED` | Reserved for future use — current evaluation paths surface remote exceptions inline via `CapturedExpression.error` instead of throwing |
|
|
250
267
|
| `MISSING_TARGET` | Neither `--port` nor a complete CF target (`--region/--org/--space/--app`) was provided |
|
|
251
268
|
| `ABORTED` | Reserved for future use by long-running streams when an `AbortSignal` fires |
|
|
@@ -271,10 +288,10 @@ Path mapping uses CDP's first-class `urlRegex`:
|
|
|
271
288
|
|
|
272
289
|
| `--remote-root` | Resulting urlRegex (line `42` of `src/handler.ts`) |
|
|
273
290
|
| --- | --- |
|
|
274
|
-
| _omitted_ | `(?:^|/)src/handler\.(?:ts\|js)$` |
|
|
275
|
-
| `/home/vcap/app` (literal) | `^file:///home/vcap/app/src/handler\.(?:ts\|js)$` |
|
|
276
|
-
| `regex:^/example-root-.*$` | `^file:///example-root
|
|
277
|
-
|
|
|
291
|
+
| _omitted_ | `(?:^|/)src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$` |
|
|
292
|
+
| `/home/vcap/app` (literal) | `^file:///home/vcap/app/src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$` |
|
|
293
|
+
| `regex:^/example-root-.*$` | `^file:///example-root-.*/src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$` |
|
|
294
|
+
| `regex:^/(home/vcap/app\|example-root-.*)$` | `^file:///(home/vcap/app\|example-root-.*)/src/handler\.(?:ts\|js\|mts\|mjs\|cts\|cjs)$` |
|
|
278
295
|
|
|
279
296
|
`.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.
|
|
280
297
|
|
package/dist/cli.js
CHANGED
|
@@ -118,11 +118,13 @@ var init_wsTransport = __esm({
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
// src/cli.ts
|
|
121
|
+
import { performance as performance2 } from "perf_hooks";
|
|
121
122
|
import process from "process";
|
|
122
123
|
import { Command } from "commander";
|
|
123
124
|
|
|
124
125
|
// src/inspector.ts
|
|
125
126
|
import { request } from "http";
|
|
127
|
+
import { performance } from "perf_hooks";
|
|
126
128
|
|
|
127
129
|
// src/cdp.ts
|
|
128
130
|
init_types();
|
|
@@ -429,6 +431,15 @@ function stripTrailingSlash(value) {
|
|
|
429
431
|
}
|
|
430
432
|
return value;
|
|
431
433
|
}
|
|
434
|
+
function normalizeRegexRootPattern(pattern) {
|
|
435
|
+
const withoutStartAnchor = pattern.startsWith("^") ? pattern.slice(1) : pattern;
|
|
436
|
+
const withoutEndAnchor = withoutStartAnchor.endsWith("$") && !isEscaped(withoutStartAnchor, withoutStartAnchor.length - 1) ? withoutStartAnchor.slice(0, -1) : withoutStartAnchor;
|
|
437
|
+
return stripTrailingSlash(withoutEndAnchor);
|
|
438
|
+
}
|
|
439
|
+
function buildFileUrlRegex(rootPattern, tail) {
|
|
440
|
+
const separator = rootPattern.endsWith("/") ? "" : "/";
|
|
441
|
+
return `^file://${rootPattern}${separator}${tail}$`;
|
|
442
|
+
}
|
|
432
443
|
function escapeRegExp(value) {
|
|
433
444
|
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
434
445
|
}
|
|
@@ -455,10 +466,11 @@ function buildBreakpointUrlRegex(input) {
|
|
|
455
466
|
}
|
|
456
467
|
case "literal": {
|
|
457
468
|
const escapedRoot = escapeRegExp(input.remoteRoot.value);
|
|
458
|
-
return
|
|
469
|
+
return buildFileUrlRegex(escapedRoot, tail);
|
|
459
470
|
}
|
|
460
471
|
case "regex": {
|
|
461
|
-
|
|
472
|
+
const rootPattern = normalizeRegexRootPattern(input.remoteRoot.pattern);
|
|
473
|
+
return buildFileUrlRegex(rootPattern, tail);
|
|
462
474
|
}
|
|
463
475
|
}
|
|
464
476
|
}
|
|
@@ -607,21 +619,21 @@ async function connectInspector(options) {
|
|
|
607
619
|
const PAUSE_BUFFER_LIMIT = 32;
|
|
608
620
|
const pauseBuffer = [];
|
|
609
621
|
const pauseWaitGate = { active: false };
|
|
622
|
+
const debuggerState = {};
|
|
610
623
|
client.on("Debugger.paused", (raw) => {
|
|
611
624
|
if (pauseWaitGate.active) {
|
|
612
625
|
return;
|
|
613
626
|
}
|
|
614
627
|
const params = raw;
|
|
615
|
-
const event =
|
|
616
|
-
reason: asString(params.reason),
|
|
617
|
-
hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
|
|
618
|
-
callFrames: toCallFrames(params.callFrames)
|
|
619
|
-
};
|
|
628
|
+
const event = toPauseEvent(params, performance.now());
|
|
620
629
|
if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
|
|
621
630
|
pauseBuffer.shift();
|
|
622
631
|
}
|
|
623
632
|
pauseBuffer.push(event);
|
|
624
633
|
});
|
|
634
|
+
client.on("Debugger.resumed", () => {
|
|
635
|
+
debuggerState.lastResumedAtMs = performance.now();
|
|
636
|
+
});
|
|
625
637
|
await client.send("Runtime.enable");
|
|
626
638
|
await client.send("Debugger.enable");
|
|
627
639
|
return {
|
|
@@ -630,6 +642,7 @@ async function connectInspector(options) {
|
|
|
630
642
|
scripts,
|
|
631
643
|
pauseBuffer,
|
|
632
644
|
pauseWaitGate,
|
|
645
|
+
debuggerState,
|
|
633
646
|
dispose: async () => {
|
|
634
647
|
try {
|
|
635
648
|
await client.send("Debugger.disable");
|
|
@@ -747,36 +760,121 @@ function pauseMatches(pause, breakpointIds) {
|
|
|
747
760
|
}
|
|
748
761
|
return pause.hitBreakpoints.some((id) => breakpointIds.includes(id));
|
|
749
762
|
}
|
|
763
|
+
function toPauseEvent(params, receivedAtMs) {
|
|
764
|
+
return {
|
|
765
|
+
reason: asString(params.reason),
|
|
766
|
+
hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
|
|
767
|
+
callFrames: toCallFrames(params.callFrames),
|
|
768
|
+
receivedAtMs
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function remainingUntil(deadlineMs) {
|
|
772
|
+
return Math.max(0, deadlineMs - performance.now());
|
|
773
|
+
}
|
|
774
|
+
function topFrameLocation(pause) {
|
|
775
|
+
const top = pause.callFrames[0];
|
|
776
|
+
if (top === void 0) {
|
|
777
|
+
return "(no call frame)";
|
|
778
|
+
}
|
|
779
|
+
const url = top.url !== void 0 && top.url.length > 0 ? top.url : "(unknown)";
|
|
780
|
+
return `${url}:${(top.lineNumber + 1).toString()}:${(top.columnNumber + 1).toString()}`;
|
|
781
|
+
}
|
|
782
|
+
function pauseDetail(pause) {
|
|
783
|
+
return JSON.stringify({
|
|
784
|
+
reason: pause.reason,
|
|
785
|
+
hitBreakpoints: pause.hitBreakpoints,
|
|
786
|
+
topFrame: topFrameLocation(pause)
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
function hasResumedSincePause(session, pause) {
|
|
790
|
+
const pauseAt = pause.receivedAtMs;
|
|
791
|
+
const resumedAt = session.debuggerState.lastResumedAtMs;
|
|
792
|
+
return pauseAt !== void 0 && resumedAt !== void 0 && resumedAt >= pauseAt;
|
|
793
|
+
}
|
|
794
|
+
function throwBreakpointTimeout(timeoutMs) {
|
|
795
|
+
throw new CfInspectorError(
|
|
796
|
+
"BREAKPOINT_NOT_HIT",
|
|
797
|
+
`Timed out waiting for matching Debugger.paused after ${timeoutMs.toString()}ms`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
function throwUnrelatedPauseTimeout(pause, timeoutMs) {
|
|
801
|
+
throw new CfInspectorError(
|
|
802
|
+
"UNRELATED_PAUSE_TIMEOUT",
|
|
803
|
+
`Target stayed paused by another debugger event before this command's breakpoint could hit within ${timeoutMs.toString()}ms`,
|
|
804
|
+
pauseDetail(pause)
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
async function waitForUnmatchedPauseToResume(session, pause, deadlineMs, timeoutMs) {
|
|
808
|
+
if (hasResumedSincePause(session, pause)) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const remainingMs = remainingUntil(deadlineMs);
|
|
812
|
+
if (remainingMs <= 0) {
|
|
813
|
+
throwUnrelatedPauseTimeout(pause, timeoutMs);
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
await session.client.waitFor("Debugger.resumed", { timeoutMs: remainingMs });
|
|
817
|
+
session.debuggerState.lastResumedAtMs = performance.now();
|
|
818
|
+
} catch (err) {
|
|
819
|
+
if (err instanceof CfInspectorError && err.code === "BREAKPOINT_NOT_HIT") {
|
|
820
|
+
throwUnrelatedPauseTimeout(pause, timeoutMs);
|
|
821
|
+
}
|
|
822
|
+
throw err;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async function handleUnmatchedPause(session, pause, options, deadlineMs) {
|
|
826
|
+
if (options.unmatchedPausePolicy === "fail") {
|
|
827
|
+
throw new CfInspectorError(
|
|
828
|
+
"UNRELATED_PAUSE",
|
|
829
|
+
"Target paused before this command's breakpoint was reached",
|
|
830
|
+
pauseDetail(pause)
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
if (hasResumedSincePause(session, pause)) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
options.onUnmatchedPause?.(pause);
|
|
837
|
+
await waitForUnmatchedPauseToResume(session, pause, deadlineMs, options.timeoutMs);
|
|
838
|
+
}
|
|
750
839
|
async function waitForPause(session, options) {
|
|
840
|
+
const deadlineMs = performance.now() + options.timeoutMs;
|
|
751
841
|
const buffer = session.pauseBuffer;
|
|
752
|
-
while (buffer.length > 0) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
842
|
+
while (buffer.length > 0 || remainingUntil(deadlineMs) > 0) {
|
|
843
|
+
while (buffer.length > 0) {
|
|
844
|
+
const buffered = buffer.shift();
|
|
845
|
+
if (buffered === void 0) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (pauseMatches(buffered, options.breakpointIds)) {
|
|
849
|
+
return buffered;
|
|
850
|
+
}
|
|
851
|
+
await handleUnmatchedPause(session, buffered, options, deadlineMs);
|
|
756
852
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
853
|
+
const remainingMs = remainingUntil(deadlineMs);
|
|
854
|
+
if (remainingMs <= 0) {
|
|
855
|
+
throwBreakpointTimeout(options.timeoutMs);
|
|
856
|
+
}
|
|
857
|
+
session.pauseWaitGate.active = true;
|
|
858
|
+
let receivedAtMs;
|
|
859
|
+
let params;
|
|
860
|
+
try {
|
|
861
|
+
params = await session.client.waitFor("Debugger.paused", {
|
|
862
|
+
timeoutMs: remainingMs,
|
|
863
|
+
predicate: () => {
|
|
864
|
+
receivedAtMs = performance.now();
|
|
766
865
|
return true;
|
|
767
866
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
|
|
867
|
+
});
|
|
868
|
+
} finally {
|
|
869
|
+
session.pauseWaitGate.active = false;
|
|
870
|
+
}
|
|
871
|
+
const pause = toPauseEvent(params, receivedAtMs ?? performance.now());
|
|
872
|
+
if (pauseMatches(pause, options.breakpointIds)) {
|
|
873
|
+
return pause;
|
|
874
|
+
}
|
|
875
|
+
await handleUnmatchedPause(session, pause, options, deadlineMs);
|
|
774
876
|
}
|
|
775
|
-
|
|
776
|
-
reason: asString(params.reason),
|
|
777
|
-
hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
|
|
778
|
-
callFrames: toCallFrames(params.callFrames)
|
|
779
|
-
};
|
|
877
|
+
throwBreakpointTimeout(options.timeoutMs);
|
|
780
878
|
}
|
|
781
879
|
async function resume(session) {
|
|
782
880
|
await session.client.send("Debugger.resume");
|
|
@@ -969,13 +1067,12 @@ async function waitForStop(session, options) {
|
|
|
969
1067
|
}
|
|
970
1068
|
|
|
971
1069
|
// src/snapshot.ts
|
|
972
|
-
import { performance } from "perf_hooks";
|
|
973
1070
|
var MAX_SCOPES = 3;
|
|
974
1071
|
var MAX_SCOPE_VARIABLES = 20;
|
|
975
1072
|
var MAX_CHILD_VARIABLES = 8;
|
|
976
1073
|
var MAX_VARIABLE_DEPTH = 2;
|
|
977
1074
|
var MAX_VALUE_LENGTH = 240;
|
|
978
|
-
var SENSITIVE_NAME_REGEX = /(pass(?:word)?|token|secret|api[_-]?key|authorization|cookie|session|private[_-]?key)/i;
|
|
1075
|
+
var SENSITIVE_NAME_REGEX = /(pass(?:word)?|credentials?|creds?|token|secret|api[_-]?key|authorization|cookie|session|private[_-]?key)/i;
|
|
979
1076
|
var PRIORITY_BY_TYPE = {
|
|
980
1077
|
local: 0,
|
|
981
1078
|
arguments: 1,
|
|
@@ -1036,8 +1133,11 @@ function formatPrimitive(value) {
|
|
|
1036
1133
|
}
|
|
1037
1134
|
return String(value);
|
|
1038
1135
|
}
|
|
1136
|
+
function isSensitiveName(name) {
|
|
1137
|
+
return SENSITIVE_NAME_REGEX.test(name);
|
|
1138
|
+
}
|
|
1039
1139
|
function sanitizeValue(name, raw) {
|
|
1040
|
-
if (
|
|
1140
|
+
if (isSensitiveName(name)) {
|
|
1041
1141
|
return "[REDACTED]";
|
|
1042
1142
|
}
|
|
1043
1143
|
if (raw.length <= MAX_VALUE_LENGTH) {
|
|
@@ -1055,8 +1155,9 @@ async function captureProperties(session, objectId, limit, depth) {
|
|
|
1055
1155
|
limited.map(async (prop) => {
|
|
1056
1156
|
const name = typeof prop.name === "string" ? prop.name : "?";
|
|
1057
1157
|
const described = describeProperty(prop);
|
|
1158
|
+
const sensitive = isSensitiveName(name);
|
|
1058
1159
|
let children;
|
|
1059
|
-
if (depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
|
|
1160
|
+
if (!sensitive && depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
|
|
1060
1161
|
try {
|
|
1061
1162
|
const nested = await captureProperties(
|
|
1062
1163
|
session,
|
|
@@ -1070,7 +1171,7 @@ async function captureProperties(session, objectId, limit, depth) {
|
|
|
1070
1171
|
} catch {
|
|
1071
1172
|
}
|
|
1072
1173
|
}
|
|
1073
|
-
const sanitizedValue = sanitizeValue(name, described.value);
|
|
1174
|
+
const sanitizedValue = sensitive ? "[REDACTED]" : sanitizeValue(name, described.value);
|
|
1074
1175
|
const base = { name, value: sanitizedValue };
|
|
1075
1176
|
const withType = described.type === void 0 ? base : { ...base, type: described.type };
|
|
1076
1177
|
return children === void 0 ? withType : { ...withType, children };
|
|
@@ -1128,7 +1229,6 @@ function evalResultToCaptured(expression, result) {
|
|
|
1128
1229
|
return buildCaptured("undefined");
|
|
1129
1230
|
}
|
|
1130
1231
|
async function captureSnapshot(session, pause, options = {}) {
|
|
1131
|
-
const startedAt = performance.now();
|
|
1132
1232
|
const top = pause.callFrames[0];
|
|
1133
1233
|
let topFrame;
|
|
1134
1234
|
let captures = [];
|
|
@@ -1155,12 +1255,10 @@ async function captureSnapshot(session, pause, options = {}) {
|
|
|
1155
1255
|
);
|
|
1156
1256
|
}
|
|
1157
1257
|
}
|
|
1158
|
-
const captureDurationMs = Math.round((performance.now() - startedAt) * 1e3) / 1e3;
|
|
1159
1258
|
return {
|
|
1160
1259
|
reason: pause.reason,
|
|
1161
1260
|
hitBreakpoints: pause.hitBreakpoints,
|
|
1162
1261
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1163
|
-
captureDurationMs,
|
|
1164
1262
|
...topFrame === void 0 ? {} : { topFrame },
|
|
1165
1263
|
captures
|
|
1166
1264
|
};
|
|
@@ -1251,7 +1349,82 @@ function parseCaptureList(raw) {
|
|
|
1251
1349
|
if (raw === void 0 || raw.trim().length === 0) {
|
|
1252
1350
|
return [];
|
|
1253
1351
|
}
|
|
1254
|
-
return raw
|
|
1352
|
+
return splitCaptureExpressions(raw);
|
|
1353
|
+
}
|
|
1354
|
+
function isQuoteChar(value) {
|
|
1355
|
+
return value === "'" || value === '"' || value === "`";
|
|
1356
|
+
}
|
|
1357
|
+
function consumeQuotedChar(state, char) {
|
|
1358
|
+
if (state.quote === void 0) {
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
if (state.escaped) {
|
|
1362
|
+
state.escaped = false;
|
|
1363
|
+
return true;
|
|
1364
|
+
}
|
|
1365
|
+
if (char === "\\") {
|
|
1366
|
+
state.escaped = true;
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
if (char === state.quote) {
|
|
1370
|
+
state.quote = void 0;
|
|
1371
|
+
}
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
function updateCaptureDepth(state, char) {
|
|
1375
|
+
if (char === "(") {
|
|
1376
|
+
state.parenDepth += 1;
|
|
1377
|
+
} else if (char === ")") {
|
|
1378
|
+
state.parenDepth = Math.max(0, state.parenDepth - 1);
|
|
1379
|
+
} else if (char === "[") {
|
|
1380
|
+
state.bracketDepth += 1;
|
|
1381
|
+
} else if (char === "]") {
|
|
1382
|
+
state.bracketDepth = Math.max(0, state.bracketDepth - 1);
|
|
1383
|
+
} else if (char === "{") {
|
|
1384
|
+
state.braceDepth += 1;
|
|
1385
|
+
} else if (char === "}") {
|
|
1386
|
+
state.braceDepth = Math.max(0, state.braceDepth - 1);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function isTopLevel(state) {
|
|
1390
|
+
return state.parenDepth === 0 && state.bracketDepth === 0 && state.braceDepth === 0;
|
|
1391
|
+
}
|
|
1392
|
+
function appendCapturePiece(raw, state, end) {
|
|
1393
|
+
const piece = raw.slice(state.start, end).trim();
|
|
1394
|
+
if (piece.length > 0) {
|
|
1395
|
+
state.pieces.push(piece);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
function splitCaptureExpressions(raw) {
|
|
1399
|
+
const state = {
|
|
1400
|
+
escaped: false,
|
|
1401
|
+
parenDepth: 0,
|
|
1402
|
+
bracketDepth: 0,
|
|
1403
|
+
braceDepth: 0,
|
|
1404
|
+
quote: void 0,
|
|
1405
|
+
start: 0,
|
|
1406
|
+
pieces: []
|
|
1407
|
+
};
|
|
1408
|
+
for (let idx = 0; idx < raw.length; idx += 1) {
|
|
1409
|
+
const char = raw[idx];
|
|
1410
|
+
if (char === void 0) {
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
if (consumeQuotedChar(state, char)) {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
if (isQuoteChar(char)) {
|
|
1417
|
+
state.quote = char;
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
updateCaptureDepth(state, char);
|
|
1421
|
+
if (char === "," && isTopLevel(state)) {
|
|
1422
|
+
appendCapturePiece(raw, state, idx);
|
|
1423
|
+
state.start = idx + 1;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
appendCapturePiece(raw, state, raw.length);
|
|
1427
|
+
return state.pieces;
|
|
1255
1428
|
}
|
|
1256
1429
|
async function withSession(target, fn) {
|
|
1257
1430
|
const tunnel = await openTarget(target);
|
|
@@ -1280,12 +1453,41 @@ function warnOnUnboundBreakpoints(handles) {
|
|
|
1280
1453
|
}
|
|
1281
1454
|
}
|
|
1282
1455
|
}
|
|
1456
|
+
function roundDurationMs(durationMs) {
|
|
1457
|
+
return Math.round(durationMs * 1e3) / 1e3;
|
|
1458
|
+
}
|
|
1459
|
+
function formatPauseLocation(pause) {
|
|
1460
|
+
const top = pause.callFrames[0];
|
|
1461
|
+
if (top === void 0) {
|
|
1462
|
+
return "(no call frame)";
|
|
1463
|
+
}
|
|
1464
|
+
const url = top.url !== void 0 && top.url.length > 0 ? top.url : "(unknown)";
|
|
1465
|
+
return `${url}:${(top.lineNumber + 1).toString()}:${(top.columnNumber + 1).toString()}`;
|
|
1466
|
+
}
|
|
1467
|
+
function warnOnUnmatchedPause(pause) {
|
|
1468
|
+
const reason = pause.reason.length > 0 ? pause.reason : "unknown";
|
|
1469
|
+
process.stderr.write(
|
|
1470
|
+
`[cf-inspector] warning: target is paused by another debugger event (${reason} at ${formatPauseLocation(pause)}); waiting for it to resume...
|
|
1471
|
+
`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
function withPausedDuration(snapshot, pausedDurationMs) {
|
|
1475
|
+
return {
|
|
1476
|
+
reason: snapshot.reason,
|
|
1477
|
+
hitBreakpoints: snapshot.hitBreakpoints,
|
|
1478
|
+
capturedAt: snapshot.capturedAt,
|
|
1479
|
+
pausedDurationMs,
|
|
1480
|
+
...snapshot.topFrame === void 0 ? {} : { topFrame: snapshot.topFrame },
|
|
1481
|
+
captures: snapshot.captures
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1283
1484
|
function writeHumanSnapshot(snapshot) {
|
|
1485
|
+
const pausedDuration = snapshot.pausedDurationMs === null ? "unknown" : `${snapshot.pausedDurationMs.toFixed(1)}ms`;
|
|
1284
1486
|
const lines = [];
|
|
1285
1487
|
lines.push(
|
|
1286
1488
|
`Snapshot @ ${snapshot.capturedAt}`,
|
|
1287
1489
|
` reason: ${snapshot.reason}`,
|
|
1288
|
-
`
|
|
1490
|
+
` paused: ${pausedDuration}`
|
|
1289
1491
|
);
|
|
1290
1492
|
if (snapshot.topFrame) {
|
|
1291
1493
|
const frame = snapshot.topFrame;
|
|
@@ -1341,15 +1543,36 @@ async function handleSnapshot(opts) {
|
|
|
1341
1543
|
);
|
|
1342
1544
|
warnOnUnboundBreakpoints(handles);
|
|
1343
1545
|
const breakpointIds = handles.map((h) => h.breakpointId);
|
|
1344
|
-
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1546
|
+
let warnedUnmatchedPause = false;
|
|
1547
|
+
const pause = await waitForPause(session, {
|
|
1548
|
+
timeoutMs,
|
|
1549
|
+
breakpointIds,
|
|
1550
|
+
unmatchedPausePolicy: opts.failOnUnmatchedPause === true ? "fail" : "wait-for-resume",
|
|
1551
|
+
onUnmatchedPause: (unmatchedPause) => {
|
|
1552
|
+
if (warnedUnmatchedPause || opts.failOnUnmatchedPause === true) {
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
warnedUnmatchedPause = true;
|
|
1556
|
+
warnOnUnmatchedPause(unmatchedPause);
|
|
1350
1557
|
}
|
|
1558
|
+
});
|
|
1559
|
+
const pausedStartedAt = pause.receivedAtMs ?? performance2.now();
|
|
1560
|
+
const snapshot = await captureSnapshot(session, pause, { captures });
|
|
1561
|
+
if (opts.keepPaused === true) {
|
|
1562
|
+
return withPausedDuration(snapshot, null);
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
await resume(session);
|
|
1566
|
+
return withPausedDuration(
|
|
1567
|
+
snapshot,
|
|
1568
|
+
roundDurationMs(performance2.now() - pausedStartedAt)
|
|
1569
|
+
);
|
|
1570
|
+
} catch {
|
|
1571
|
+
process.stderr.write(
|
|
1572
|
+
"[cf-inspector] warning: Debugger.resume failed after snapshot; pausedDurationMs is unknown.\n"
|
|
1573
|
+
);
|
|
1574
|
+
return withPausedDuration(snapshot, null);
|
|
1351
1575
|
}
|
|
1352
|
-
return snapshot;
|
|
1353
1576
|
});
|
|
1354
1577
|
if (opts.json) {
|
|
1355
1578
|
writeJson(result);
|
|
@@ -1364,6 +1587,9 @@ async function handleEval(opts) {
|
|
|
1364
1587
|
});
|
|
1365
1588
|
if (opts.json) {
|
|
1366
1589
|
writeJson(result);
|
|
1590
|
+
if (result.exceptionDetails !== void 0) {
|
|
1591
|
+
process.exitCode = 1;
|
|
1592
|
+
}
|
|
1367
1593
|
return;
|
|
1368
1594
|
}
|
|
1369
1595
|
if (result.exceptionDetails !== void 0) {
|
|
@@ -1501,10 +1727,10 @@ async function main(argv) {
|
|
|
1501
1727
|
"Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42",
|
|
1502
1728
|
collectStrings,
|
|
1503
1729
|
[]
|
|
1504
|
-
).option("--capture <expr,\u2026>", "
|
|
1730
|
+
).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(
|
|
1505
1731
|
"--condition <expr>",
|
|
1506
1732
|
"Only pause when this JS expression evaluates truthy in the paused frame"
|
|
1507
|
-
).option("--no-json", "Print a human-readable summary instead of JSON").option("--keep-paused", "Skip
|
|
1733
|
+
).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) => {
|
|
1508
1734
|
await handleSnapshot(opts);
|
|
1509
1735
|
});
|
|
1510
1736
|
applyTargetOptions(
|