@saptools/cf-inspector 0.3.2 → 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 +26 -10
- package/dist/cli.js +252 -43
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +136 -35
- 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,16 +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
|
|
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 |
|
|
117
118
|
|
|
118
119
|
JSON output includes `pausedDurationMs`, the client-observed time from receiving
|
|
119
120
|
the matching pause event until `Debugger.resume` completes. It does not include
|
|
120
121
|
the time spent waiting for the breakpoint to hit. When `--keep-paused` is used,
|
|
121
|
-
`pausedDurationMs` is `null` because
|
|
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.
|
|
122
132
|
|
|
123
133
|
For Cloud Foundry targets, replace `--port` with `--region/--org/--space/--app` (and optionally `--cf-timeout <seconds>` for the tunnel).
|
|
124
134
|
|
|
@@ -161,7 +171,7 @@ When the user expression throws, the event is emitted with `error` instead of `v
|
|
|
161
171
|
|
|
162
172
|
### 🧮 `cf-inspector eval`
|
|
163
173
|
|
|
164
|
-
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.
|
|
165
175
|
|
|
166
176
|
```bash
|
|
167
177
|
cf-inspector eval --port 9229 --expr 'process.uptime()'
|
|
@@ -204,7 +214,11 @@ const bp = await setBreakpoint(session, {
|
|
|
204
214
|
});
|
|
205
215
|
const pause = await waitForPause(session, { timeoutMs: 30_000 });
|
|
206
216
|
const snapshot = await captureSnapshot(session, pause);
|
|
207
|
-
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");
|
|
208
222
|
await resume(session);
|
|
209
223
|
await session.dispose();
|
|
210
224
|
|
|
@@ -247,6 +261,8 @@ console.log({ bp, snapshot, customValue });
|
|
|
247
261
|
| `INSPECTOR_CONNECTION_FAILED` | WebSocket handshake failed, or the connection closed mid-request |
|
|
248
262
|
| `CDP_REQUEST_FAILED` | A CDP method returned an error result, timed out, or failed to send |
|
|
249
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 |
|
|
250
266
|
| `EVALUATION_FAILED` | Reserved for future use — current evaluation paths surface remote exceptions inline via `CapturedExpression.error` instead of throwing |
|
|
251
267
|
| `MISSING_TARGET` | Neither `--port` nor a complete CF target (`--region/--org/--space/--app`) was provided |
|
|
252
268
|
| `ABORTED` | Reserved for future use by long-running streams when an `AbortSignal` fires |
|
|
@@ -272,10 +288,10 @@ Path mapping uses CDP's first-class `urlRegex`:
|
|
|
272
288
|
|
|
273
289
|
| `--remote-root` | Resulting urlRegex (line `42` of `src/handler.ts`) |
|
|
274
290
|
| --- | --- |
|
|
275
|
-
| _omitted_ | `(?:^|/)src/handler\.(?:ts\|js)$` |
|
|
276
|
-
| `/home/vcap/app` (literal) | `^file:///home/vcap/app/src/handler\.(?:ts\|js)$` |
|
|
277
|
-
| `regex:^/example-root-.*$` | `^file:///example-root
|
|
278
|
-
|
|
|
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)$` |
|
|
279
295
|
|
|
280
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.
|
|
281
297
|
|
package/dist/cli.js
CHANGED
|
@@ -118,12 +118,13 @@ var init_wsTransport = __esm({
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
// src/cli.ts
|
|
121
|
-
import { performance } from "perf_hooks";
|
|
121
|
+
import { performance as performance2 } from "perf_hooks";
|
|
122
122
|
import process from "process";
|
|
123
123
|
import { Command } from "commander";
|
|
124
124
|
|
|
125
125
|
// src/inspector.ts
|
|
126
126
|
import { request } from "http";
|
|
127
|
+
import { performance } from "perf_hooks";
|
|
127
128
|
|
|
128
129
|
// src/cdp.ts
|
|
129
130
|
init_types();
|
|
@@ -430,6 +431,15 @@ function stripTrailingSlash(value) {
|
|
|
430
431
|
}
|
|
431
432
|
return value;
|
|
432
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
|
+
}
|
|
433
443
|
function escapeRegExp(value) {
|
|
434
444
|
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
435
445
|
}
|
|
@@ -456,10 +466,11 @@ function buildBreakpointUrlRegex(input) {
|
|
|
456
466
|
}
|
|
457
467
|
case "literal": {
|
|
458
468
|
const escapedRoot = escapeRegExp(input.remoteRoot.value);
|
|
459
|
-
return
|
|
469
|
+
return buildFileUrlRegex(escapedRoot, tail);
|
|
460
470
|
}
|
|
461
471
|
case "regex": {
|
|
462
|
-
|
|
472
|
+
const rootPattern = normalizeRegexRootPattern(input.remoteRoot.pattern);
|
|
473
|
+
return buildFileUrlRegex(rootPattern, tail);
|
|
463
474
|
}
|
|
464
475
|
}
|
|
465
476
|
}
|
|
@@ -608,21 +619,21 @@ async function connectInspector(options) {
|
|
|
608
619
|
const PAUSE_BUFFER_LIMIT = 32;
|
|
609
620
|
const pauseBuffer = [];
|
|
610
621
|
const pauseWaitGate = { active: false };
|
|
622
|
+
const debuggerState = {};
|
|
611
623
|
client.on("Debugger.paused", (raw) => {
|
|
612
624
|
if (pauseWaitGate.active) {
|
|
613
625
|
return;
|
|
614
626
|
}
|
|
615
627
|
const params = raw;
|
|
616
|
-
const event =
|
|
617
|
-
reason: asString(params.reason),
|
|
618
|
-
hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
|
|
619
|
-
callFrames: toCallFrames(params.callFrames)
|
|
620
|
-
};
|
|
628
|
+
const event = toPauseEvent(params, performance.now());
|
|
621
629
|
if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
|
|
622
630
|
pauseBuffer.shift();
|
|
623
631
|
}
|
|
624
632
|
pauseBuffer.push(event);
|
|
625
633
|
});
|
|
634
|
+
client.on("Debugger.resumed", () => {
|
|
635
|
+
debuggerState.lastResumedAtMs = performance.now();
|
|
636
|
+
});
|
|
626
637
|
await client.send("Runtime.enable");
|
|
627
638
|
await client.send("Debugger.enable");
|
|
628
639
|
return {
|
|
@@ -631,6 +642,7 @@ async function connectInspector(options) {
|
|
|
631
642
|
scripts,
|
|
632
643
|
pauseBuffer,
|
|
633
644
|
pauseWaitGate,
|
|
645
|
+
debuggerState,
|
|
634
646
|
dispose: async () => {
|
|
635
647
|
try {
|
|
636
648
|
await client.send("Debugger.disable");
|
|
@@ -748,36 +760,121 @@ function pauseMatches(pause, breakpointIds) {
|
|
|
748
760
|
}
|
|
749
761
|
return pause.hitBreakpoints.some((id) => breakpointIds.includes(id));
|
|
750
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
|
+
}
|
|
751
839
|
async function waitForPause(session, options) {
|
|
840
|
+
const deadlineMs = performance.now() + options.timeoutMs;
|
|
752
841
|
const buffer = session.pauseBuffer;
|
|
753
|
-
while (buffer.length > 0) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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);
|
|
757
852
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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();
|
|
767
865
|
return true;
|
|
768
866
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
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);
|
|
775
876
|
}
|
|
776
|
-
|
|
777
|
-
reason: asString(params.reason),
|
|
778
|
-
hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
|
|
779
|
-
callFrames: toCallFrames(params.callFrames)
|
|
780
|
-
};
|
|
877
|
+
throwBreakpointTimeout(options.timeoutMs);
|
|
781
878
|
}
|
|
782
879
|
async function resume(session) {
|
|
783
880
|
await session.client.send("Debugger.resume");
|
|
@@ -975,7 +1072,7 @@ 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 };
|
|
@@ -1248,7 +1349,82 @@ function parseCaptureList(raw) {
|
|
|
1248
1349
|
if (raw === void 0 || raw.trim().length === 0) {
|
|
1249
1350
|
return [];
|
|
1250
1351
|
}
|
|
1251
|
-
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;
|
|
1252
1428
|
}
|
|
1253
1429
|
async function withSession(target, fn) {
|
|
1254
1430
|
const tunnel = await openTarget(target);
|
|
@@ -1280,6 +1456,21 @@ function warnOnUnboundBreakpoints(handles) {
|
|
|
1280
1456
|
function roundDurationMs(durationMs) {
|
|
1281
1457
|
return Math.round(durationMs * 1e3) / 1e3;
|
|
1282
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
|
+
}
|
|
1283
1474
|
function withPausedDuration(snapshot, pausedDurationMs) {
|
|
1284
1475
|
return {
|
|
1285
1476
|
reason: snapshot.reason,
|
|
@@ -1291,7 +1482,7 @@ function withPausedDuration(snapshot, pausedDurationMs) {
|
|
|
1291
1482
|
};
|
|
1292
1483
|
}
|
|
1293
1484
|
function writeHumanSnapshot(snapshot) {
|
|
1294
|
-
const pausedDuration = snapshot.pausedDurationMs === null ? "
|
|
1485
|
+
const pausedDuration = snapshot.pausedDurationMs === null ? "unknown" : `${snapshot.pausedDurationMs.toFixed(1)}ms`;
|
|
1295
1486
|
const lines = [];
|
|
1296
1487
|
lines.push(
|
|
1297
1488
|
`Snapshot @ ${snapshot.capturedAt}`,
|
|
@@ -1352,8 +1543,20 @@ async function handleSnapshot(opts) {
|
|
|
1352
1543
|
);
|
|
1353
1544
|
warnOnUnboundBreakpoints(handles);
|
|
1354
1545
|
const breakpointIds = handles.map((h) => h.breakpointId);
|
|
1355
|
-
|
|
1356
|
-
const
|
|
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);
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
const pausedStartedAt = pause.receivedAtMs ?? performance2.now();
|
|
1357
1560
|
const snapshot = await captureSnapshot(session, pause, { captures });
|
|
1358
1561
|
if (opts.keepPaused === true) {
|
|
1359
1562
|
return withPausedDuration(snapshot, null);
|
|
@@ -1362,9 +1565,12 @@ async function handleSnapshot(opts) {
|
|
|
1362
1565
|
await resume(session);
|
|
1363
1566
|
return withPausedDuration(
|
|
1364
1567
|
snapshot,
|
|
1365
|
-
roundDurationMs(
|
|
1568
|
+
roundDurationMs(performance2.now() - pausedStartedAt)
|
|
1366
1569
|
);
|
|
1367
1570
|
} catch {
|
|
1571
|
+
process.stderr.write(
|
|
1572
|
+
"[cf-inspector] warning: Debugger.resume failed after snapshot; pausedDurationMs is unknown.\n"
|
|
1573
|
+
);
|
|
1368
1574
|
return withPausedDuration(snapshot, null);
|
|
1369
1575
|
}
|
|
1370
1576
|
});
|
|
@@ -1381,6 +1587,9 @@ async function handleEval(opts) {
|
|
|
1381
1587
|
});
|
|
1382
1588
|
if (opts.json) {
|
|
1383
1589
|
writeJson(result);
|
|
1590
|
+
if (result.exceptionDetails !== void 0) {
|
|
1591
|
+
process.exitCode = 1;
|
|
1592
|
+
}
|
|
1384
1593
|
return;
|
|
1385
1594
|
}
|
|
1386
1595
|
if (result.exceptionDetails !== void 0) {
|
|
@@ -1518,10 +1727,10 @@ async function main(argv) {
|
|
|
1518
1727
|
"Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42",
|
|
1519
1728
|
collectStrings,
|
|
1520
1729
|
[]
|
|
1521
|
-
).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(
|
|
1522
1731
|
"--condition <expr>",
|
|
1523
1732
|
"Only pause when this JS expression evaluates truthy in the paused frame"
|
|
1524
|
-
).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) => {
|
|
1525
1734
|
await handleSnapshot(opts);
|
|
1526
1735
|
});
|
|
1527
1736
|
applyTargetOptions(
|