@saptools/cf-inspector 0.3.19 → 0.4.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/dist/cli.js CHANGED
@@ -110,7 +110,7 @@ var init_wsTransport = __esm({
110
110
  });
111
111
 
112
112
  // src/cli.ts
113
- import process8 from "process";
113
+ import process10 from "process";
114
114
 
115
115
  // src/cli/program.ts
116
116
  import { Command } from "commander";
@@ -250,8 +250,11 @@ function writeHumanSnapshot(snapshot) {
250
250
  ` reason: ${snapshot.reason}`,
251
251
  ` paused: ${pausedDuration}`
252
252
  );
253
+ if (snapshot.exception !== void 0) {
254
+ appendExceptionLines(lines, snapshot.exception);
255
+ }
253
256
  if (snapshot.topFrame) {
254
- appendFrameLines(lines, snapshot);
257
+ appendFrameLines(lines, snapshot.topFrame);
255
258
  }
256
259
  if (snapshot.captures.length > 0) {
257
260
  lines.push(" captures:");
@@ -260,14 +263,16 @@ function writeHumanSnapshot(snapshot) {
260
263
  lines.push(` ${capture.expression} = ${detail}`);
261
264
  }
262
265
  }
266
+ if (snapshot.stack !== void 0 && snapshot.stack.length > 0) {
267
+ lines.push(" stack:");
268
+ for (const frame of snapshot.stack) {
269
+ appendStackFrameLine(lines, frame);
270
+ }
271
+ }
263
272
  process.stdout.write(`${lines.join("\n")}
264
273
  `);
265
274
  }
266
- function appendFrameLines(lines, snapshot) {
267
- const frame = snapshot.topFrame;
268
- if (frame === void 0) {
269
- return;
270
- }
275
+ function appendFrameLines(lines, frame) {
271
276
  const fnName = frame.functionName.length === 0 ? "(anonymous)" : frame.functionName;
272
277
  const sourceUrl = frame.url !== void 0 && frame.url.length > 0 ? frame.url : "(unknown)";
273
278
  lines.push(
@@ -283,6 +288,25 @@ function appendFrameLines(lines, snapshot) {
283
288
  }
284
289
  }
285
290
  }
291
+ function appendStackFrameLine(lines, frame) {
292
+ const fnName = frame.functionName.length === 0 ? "(anonymous)" : frame.functionName;
293
+ const sourceUrl = frame.url !== void 0 && frame.url.length > 0 ? frame.url : "(unknown)";
294
+ lines.push(` ${fnName} ${sourceUrl}:${frame.line.toString()}:${frame.column.toString()}`);
295
+ if (frame.captures !== void 0) {
296
+ for (const capture of frame.captures) {
297
+ const detail = capture.error ?? capture.value ?? "undefined";
298
+ lines.push(` ${capture.expression} = ${detail}`);
299
+ }
300
+ }
301
+ }
302
+ function appendExceptionLines(lines, exception) {
303
+ if (exception.error !== void 0) {
304
+ lines.push(` exception: !err ${exception.error}`);
305
+ return;
306
+ }
307
+ const detail = exception.description ?? exception.value ?? "(unknown)";
308
+ lines.push(` exception: ${detail}`);
309
+ }
286
310
  function writeLogEvent(event, json) {
287
311
  if (json) {
288
312
  process.stdout.write(`${JSON.stringify(event)}
@@ -297,6 +321,25 @@ function writeLogEvent(event, json) {
297
321
  process.stdout.write(`[${event.ts}] ${event.at} ${event.value ?? ""}
298
322
  `);
299
323
  }
324
+ function writeWatchEvent(event, json) {
325
+ if (json) {
326
+ process.stdout.write(`${JSON.stringify(event)}
327
+ `);
328
+ return;
329
+ }
330
+ process.stdout.write(`[${event.ts}] hit#${event.hit.toString()} ${event.at}
331
+ `);
332
+ if (event.exception !== void 0) {
333
+ const detail = event.exception.description ?? event.exception.value ?? event.exception.error ?? "(unknown)";
334
+ process.stdout.write(` exception: ${detail}
335
+ `);
336
+ }
337
+ for (const capture of event.captures) {
338
+ const detail = capture.error ?? capture.value ?? "undefined";
339
+ process.stdout.write(` ${capture.expression} = ${detail}
340
+ `);
341
+ }
342
+ }
300
343
 
301
344
  // src/cf/tunnel.ts
302
345
  import { startDebugger } from "@saptools/cf-debugger";
@@ -634,12 +677,13 @@ function toCallFrames(value, scripts) {
634
677
  });
635
678
  }
636
679
  function toPauseEvent(params, receivedAtMs, scripts) {
637
- return {
680
+ const base = {
638
681
  reason: asString(params.reason),
639
682
  hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
640
683
  callFrames: toCallFrames(params.callFrames, scripts),
641
684
  receivedAtMs
642
685
  };
686
+ return params.data === void 0 ? base : { ...base, data: params.data };
643
687
  }
644
688
  function topFrameLocation(pause) {
645
689
  const top = pause.callFrames[0];
@@ -725,6 +769,7 @@ init_types();
725
769
  // src/cli/commandTypes.ts
726
770
  var DEFAULT_BREAKPOINT_TIMEOUT_SEC = 30;
727
771
  var DEFAULT_CF_TIMEOUT_SEC = 60;
772
+ var DEFAULT_EXCEPTION_TIMEOUT_SEC = 30;
728
773
 
729
774
  // src/cli/target.ts
730
775
  function parsePositiveInt(raw, label) {
@@ -827,6 +872,9 @@ init_types();
827
872
  async function resume(session) {
828
873
  await session.client.send("Debugger.resume");
829
874
  }
875
+ async function setPauseOnExceptions(session, state) {
876
+ await session.client.send("Debugger.setPauseOnExceptions", { state });
877
+ }
830
878
  async function evaluateOnFrame(session, callFrameId, expression) {
831
879
  return await session.client.send("Debugger.evaluateOnCallFrame", {
832
880
  callFrameId,
@@ -914,23 +962,9 @@ function writeHumanEvalResult(result) {
914
962
  `);
915
963
  }
916
964
 
917
- // src/cli/commands/listScripts.ts
918
- import process4 from "process";
919
- async function handleListScripts(opts) {
920
- const target = resolveTarget(opts);
921
- const scripts = await withSession(target, (session) => Promise.resolve(listScripts(session)));
922
- if (opts.json) {
923
- writeJson(scripts);
924
- return;
925
- }
926
- for (const script of scripts) {
927
- process4.stdout.write(`${script.scriptId} ${script.url}
928
- `);
929
- }
930
- }
931
-
932
- // src/cli/commands/log.ts
933
- import process6 from "process";
965
+ // src/cli/commands/exception.ts
966
+ import { performance as performance3 } from "perf_hooks";
967
+ import process5 from "process";
934
968
 
935
969
  // src/pathMapper.ts
936
970
  init_types();
@@ -1073,6 +1107,46 @@ function buildBreakpointUrlRegex(input) {
1073
1107
 
1074
1108
  // src/inspector/breakpoints.ts
1075
1109
  init_types();
1110
+ var HITS_GLOBAL = "globalThis.__CFI_HITS";
1111
+ var counterKeyCounter = 0;
1112
+ function nextCounterKey(file, line) {
1113
+ counterKeyCounter += 1;
1114
+ return `${file}:${line.toString()}:${counterKeyCounter.toString()}`;
1115
+ }
1116
+ function validateHitCount(hitCount) {
1117
+ if (hitCount === void 0) {
1118
+ return void 0;
1119
+ }
1120
+ if (!Number.isInteger(hitCount) || hitCount <= 0) {
1121
+ throw new CfInspectorError(
1122
+ "INVALID_HIT_COUNT",
1123
+ `hitCount must be a positive integer, received: ${hitCount.toString()}`
1124
+ );
1125
+ }
1126
+ return hitCount;
1127
+ }
1128
+ function buildHitCountedCondition(hitCount, counterKey, userCondition) {
1129
+ const keyLiteral = JSON.stringify(counterKey);
1130
+ const baseCondition = userCondition !== void 0 && userCondition.trim().length > 0 ? `(${userCondition})` : "true";
1131
+ return [
1132
+ "(function(){",
1133
+ `var m=(${HITS_GLOBAL}=${HITS_GLOBAL}||{});`,
1134
+ `var k=${keyLiteral};`,
1135
+ "m[k]=(m[k]||0)+1;",
1136
+ `if(m[k]<${hitCount.toString()})return false;`,
1137
+ `return ${baseCondition};`,
1138
+ "})()"
1139
+ ].join("");
1140
+ }
1141
+ function resolveCondition(input) {
1142
+ const condition = input.condition;
1143
+ const hitCount = validateHitCount(input.hitCount);
1144
+ if (hitCount === void 0) {
1145
+ return condition !== void 0 && condition.length > 0 ? condition : void 0;
1146
+ }
1147
+ const counterKey = nextCounterKey(input.file, input.line);
1148
+ return buildHitCountedCondition(hitCount, counterKey, condition);
1149
+ }
1076
1150
  async function setBreakpoint(session, input) {
1077
1151
  const remoteRoot = input.remoteRoot ?? { kind: "none" };
1078
1152
  const urlRegex = buildBreakpointUrlRegex({ file: input.file, remoteRoot });
@@ -1080,8 +1154,9 @@ async function setBreakpoint(session, input) {
1080
1154
  lineNumber: input.line - 1,
1081
1155
  urlRegex
1082
1156
  };
1083
- if (input.condition !== void 0 && input.condition.length > 0) {
1084
- params["condition"] = input.condition;
1157
+ const condition = resolveCondition(input);
1158
+ if (condition !== void 0) {
1159
+ params["condition"] = condition;
1085
1160
  }
1086
1161
  const result = await session.client.send(
1087
1162
  "Debugger.setBreakpointByUrl",
@@ -1106,378 +1181,130 @@ async function removeBreakpoint(session, breakpointId) {
1106
1181
  await session.client.send("Debugger.removeBreakpoint", { breakpointId });
1107
1182
  }
1108
1183
 
1109
- // src/logpoint/condition.ts
1110
- import { randomBytes } from "crypto";
1111
- var SENTINEL_PREFIX = "__CFI_LOG_";
1112
- var SENTINEL_SUFFIX = "__";
1113
- function buildLogpointCondition(sentinel, expression) {
1114
- return [
1115
- "(function(){",
1116
- `var s=${JSON.stringify(sentinel)};`,
1117
- "try{",
1118
- `var v=(${expression});`,
1119
- "var r=typeof v==='string'?v:JSON.stringify(v);",
1120
- "console.log(s, r);",
1121
- "}catch(e){",
1122
- "console.log(s, '!err:'+(e&&e.message?e.message:String(e)));",
1123
- "}",
1124
- "return false;",
1125
- "})()"
1126
- ].join("");
1184
+ // src/inspector/pause.ts
1185
+ init_types();
1186
+ import { performance as performance2 } from "perf_hooks";
1187
+ function pauseMatches(pause, breakpointIds, pauseReasons) {
1188
+ if (pauseReasons !== void 0 && pauseReasons.length > 0) {
1189
+ return pauseReasons.includes(pause.reason);
1190
+ }
1191
+ if (breakpointIds === void 0 || breakpointIds.length === 0) {
1192
+ return true;
1193
+ }
1194
+ return pause.hitBreakpoints.some((id) => breakpointIds.includes(id));
1127
1195
  }
1128
- function generateSentinel() {
1129
- return `${SENTINEL_PREFIX}${randomBytes(8).toString("hex")}${SENTINEL_SUFFIX}`;
1196
+ function remainingUntil(deadlineMs) {
1197
+ return Math.max(0, deadlineMs - performance2.now());
1130
1198
  }
1131
-
1132
- // src/logpoint/events.ts
1133
- function asString2(value) {
1134
- return typeof value === "string" ? value : void 0;
1199
+ function hasResumedSincePause(session, pause) {
1200
+ const pauseAt = pause.receivedAtMs;
1201
+ const resumedAt = session.debuggerState.lastResumedAtMs;
1202
+ return pauseAt !== void 0 && resumedAt !== void 0 && resumedAt >= pauseAt;
1135
1203
  }
1136
- function readArg(arg, index) {
1137
- if (typeof arg !== "object" || arg === null) {
1138
- return void 0;
1204
+ function throwBreakpointTimeout(timeoutMs) {
1205
+ throw new CfInspectorError(
1206
+ "BREAKPOINT_NOT_HIT",
1207
+ `Timed out waiting for matching Debugger.paused after ${timeoutMs.toString()}ms`
1208
+ );
1209
+ }
1210
+ function throwUnrelatedPauseTimeout(pause, timeoutMs) {
1211
+ throw new CfInspectorError(
1212
+ "UNRELATED_PAUSE_TIMEOUT",
1213
+ `Target stayed paused by another debugger event before this command's breakpoint could hit within ${timeoutMs.toString()}ms`,
1214
+ pauseDetail(pause)
1215
+ );
1216
+ }
1217
+ async function waitForUnmatchedPauseToResume(session, pause, deadlineMs, timeoutMs) {
1218
+ if (hasResumedSincePause(session, pause)) {
1219
+ return;
1139
1220
  }
1140
- const candidate = arg;
1141
- if (candidate.type === "string" && typeof candidate.value === "string") {
1142
- return candidate.value;
1221
+ const remainingMs = remainingUntil(deadlineMs);
1222
+ if (remainingMs <= 0) {
1223
+ throwUnrelatedPauseTimeout(pause, timeoutMs);
1143
1224
  }
1144
- const isPrimitiveType = candidate.type === "number" || candidate.type === "boolean" || candidate.type === "bigint";
1145
- const isPrimitiveValue = typeof candidate.value === "number" || typeof candidate.value === "boolean" || typeof candidate.value === "bigint";
1146
- if (isPrimitiveType && isPrimitiveValue) {
1147
- return String(candidate.value);
1225
+ try {
1226
+ await session.client.waitFor("Debugger.resumed", { timeoutMs: remainingMs });
1227
+ session.debuggerState.lastResumedAtMs = performance2.now();
1228
+ } catch (err) {
1229
+ if (err instanceof CfInspectorError && err.code === "BREAKPOINT_NOT_HIT") {
1230
+ throwUnrelatedPauseTimeout(pause, timeoutMs);
1231
+ }
1232
+ throw err;
1148
1233
  }
1149
- return index === 0 ? void 0 : "";
1150
1234
  }
1151
- function parseLogEvent(rawArgs, sentinel, location, timestamp) {
1152
- if (!Array.isArray(rawArgs) || rawArgs.length < 2) {
1153
- return void 0;
1154
- }
1155
- const tag = readArg(rawArgs[0], 0);
1156
- if (tag !== sentinel) {
1157
- return void 0;
1235
+ async function handleUnmatchedPause(session, pause, options, deadlineMs) {
1236
+ if (options.unmatchedPausePolicy === "fail") {
1237
+ throw new CfInspectorError(
1238
+ "UNRELATED_PAUSE",
1239
+ "Target paused before this command's breakpoint was reached",
1240
+ pauseDetail(pause)
1241
+ );
1158
1242
  }
1159
- const payload = readArg(rawArgs[1], 1) ?? "";
1160
- const ts = new Date(typeof timestamp === "number" ? timestamp : Date.now()).toISOString();
1161
- const at = `${location.file}:${location.line.toString()}`;
1162
- if (payload.startsWith("!err:")) {
1163
- return { ts, at, error: payload.slice("!err:".length) };
1243
+ if (hasResumedSincePause(session, pause)) {
1244
+ return;
1164
1245
  }
1165
- return parsePayload(ts, at, payload);
1246
+ options.onUnmatchedPause?.(pause);
1247
+ await waitForUnmatchedPauseToResume(session, pause, deadlineMs, options.timeoutMs);
1166
1248
  }
1167
- function parsePayload(ts, at, payload) {
1168
- try {
1169
- const parsed = JSON.parse(payload);
1170
- if (typeof parsed === "string") {
1171
- return { ts, at, value: parsed };
1249
+ async function waitForPause(session, options) {
1250
+ const deadlineMs = performance2.now() + options.timeoutMs;
1251
+ const buffer = session.pauseBuffer;
1252
+ while (buffer.length > 0 || remainingUntil(deadlineMs) > 0) {
1253
+ while (buffer.length > 0) {
1254
+ const buffered = buffer.shift();
1255
+ if (buffered === void 0) {
1256
+ continue;
1257
+ }
1258
+ if (pauseMatches(buffered, options.breakpointIds, options.pauseReasons)) {
1259
+ return buffered;
1260
+ }
1261
+ await handleUnmatchedPause(session, buffered, options, deadlineMs);
1172
1262
  }
1173
- return { ts, at, value: JSON.stringify(parsed) };
1174
- } catch {
1175
- return { ts, at, value: payload, raw: payload };
1263
+ const pause = await waitForLivePause(session, options, deadlineMs);
1264
+ if (pauseMatches(pause, options.breakpointIds, options.pauseReasons)) {
1265
+ return pause;
1266
+ }
1267
+ await handleUnmatchedPause(session, pause, options, deadlineMs);
1176
1268
  }
1269
+ throwBreakpointTimeout(options.timeoutMs);
1177
1270
  }
1178
-
1179
- // src/logpoint/stream.ts
1180
- async function streamLogpoint(session, options) {
1181
- const sentinel = generateSentinel();
1182
- const condition = buildLogpointCondition(sentinel, options.expression);
1183
- let emitted = 0;
1184
- const offEvent = session.client.on("Runtime.consoleAPICalled", (raw) => {
1185
- const event = toLogpointEvent(raw, sentinel, options.location);
1186
- if (event === void 0) {
1187
- return;
1188
- }
1189
- emitted += 1;
1190
- options.onEvent(event);
1191
- });
1192
- let handle;
1193
- try {
1194
- handle = await setBreakpoint(session, {
1195
- file: options.location.file,
1196
- line: options.location.line,
1197
- ...options.remoteRoot === void 0 ? {} : { remoteRoot: options.remoteRoot },
1198
- condition
1199
- });
1200
- } catch (err) {
1201
- offEvent();
1202
- throw err;
1271
+ async function waitForLivePause(session, options, deadlineMs) {
1272
+ const remainingMs = remainingUntil(deadlineMs);
1273
+ if (remainingMs <= 0) {
1274
+ throwBreakpointTimeout(options.timeoutMs);
1203
1275
  }
1204
- options.onBreakpointSet?.(handle);
1276
+ session.pauseWaitGate.active = true;
1277
+ let receivedAtMs;
1278
+ let params;
1205
1279
  try {
1206
- const reason = await waitForStop(session, options);
1207
- return { handle, sentinel, emitted, stoppedReason: reason };
1280
+ params = await session.client.waitFor("Debugger.paused", {
1281
+ timeoutMs: remainingMs,
1282
+ predicate: () => {
1283
+ receivedAtMs = performance2.now();
1284
+ return true;
1285
+ }
1286
+ });
1208
1287
  } finally {
1209
- offEvent();
1210
- await removeBreakpointBestEffort(session, handle.breakpointId);
1288
+ session.pauseWaitGate.active = false;
1211
1289
  }
1290
+ return toPauseEvent(params, receivedAtMs ?? performance2.now(), session.scripts);
1212
1291
  }
1213
- function toLogpointEvent(raw, sentinel, location) {
1214
- const params = raw;
1215
- if (asString2(params.type) !== "log") {
1216
- return void 0;
1217
- }
1218
- const ts = typeof params.timestamp === "number" ? params.timestamp : void 0;
1219
- return parseLogEvent(params.args, sentinel, location, ts);
1292
+
1293
+ // src/snapshot/values.ts
1294
+ init_types();
1295
+ var DEFAULT_MAX_VALUE_LENGTH = 4096;
1296
+ function isPrimitive(value) {
1297
+ const t = typeof value;
1298
+ return t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol";
1220
1299
  }
1221
- async function removeBreakpointBestEffort(session, breakpointId) {
1222
- try {
1223
- await removeBreakpoint(session, breakpointId);
1224
- } catch {
1300
+ function formatPrimitive(value) {
1301
+ if (typeof value === "symbol") {
1302
+ return value.toString();
1303
+ }
1304
+ if (typeof value === "bigint") {
1305
+ return `${value.toString()}n`;
1225
1306
  }
1226
- }
1227
- async function waitForStop(session, options) {
1228
- return await new Promise((resolve) => {
1229
- let settled = false;
1230
- const finish = (reason) => {
1231
- if (settled) {
1232
- return;
1233
- }
1234
- settled = true;
1235
- cleanup();
1236
- resolve(reason);
1237
- };
1238
- const timer = options.durationMs === void 0 ? void 0 : setTimeout(() => {
1239
- finish("duration");
1240
- }, options.durationMs);
1241
- const offClose = session.client.onClose(() => {
1242
- finish("transport-closed");
1243
- });
1244
- const onAbort = () => {
1245
- finish("signal");
1246
- };
1247
- options.signal?.addEventListener("abort", onAbort, { once: true });
1248
- if (options.signal?.aborted === true) {
1249
- finish("signal");
1250
- }
1251
- function cleanup() {
1252
- if (timer !== void 0) {
1253
- clearTimeout(timer);
1254
- }
1255
- offClose();
1256
- options.signal?.removeEventListener("abort", onAbort);
1257
- }
1258
- });
1259
- }
1260
-
1261
- // src/cli/commands/log.ts
1262
- init_types();
1263
-
1264
- // src/cli/warnings.ts
1265
- import process5 from "process";
1266
- function warnOnUnboundBreakpoints(handles) {
1267
- for (const handle of handles) {
1268
- if (handle.resolvedLocations.length === 0) {
1269
- process5.stderr.write(
1270
- `[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.
1271
- `
1272
- );
1273
- }
1274
- }
1275
- }
1276
- function roundDurationMs(durationMs) {
1277
- return Math.round(durationMs * 1e3) / 1e3;
1278
- }
1279
- function warnOnUnmatchedPause(pause) {
1280
- const reason = pause.reason.length > 0 ? pause.reason : "unknown";
1281
- process5.stderr.write(
1282
- `[cf-inspector] warning: target is paused by another debugger event (${reason} at ${formatPauseLocation(pause)}); waiting for it to resume...
1283
- `
1284
- );
1285
- }
1286
- function withPausedDuration(snapshot, pausedDurationMs) {
1287
- return {
1288
- reason: snapshot.reason,
1289
- hitBreakpoints: snapshot.hitBreakpoints,
1290
- capturedAt: snapshot.capturedAt,
1291
- pausedDurationMs,
1292
- ...snapshot.topFrame === void 0 ? {} : { topFrame: snapshot.topFrame },
1293
- captures: snapshot.captures
1294
- };
1295
- }
1296
- function formatPauseLocation(pause) {
1297
- const top = pause.callFrames[0];
1298
- if (top === void 0) {
1299
- return "(no call frame)";
1300
- }
1301
- const url = top.url !== void 0 && top.url.length > 0 ? top.url : "(unknown)";
1302
- return `${url}:${(top.lineNumber + 1).toString()}:${(top.columnNumber + 1).toString()}`;
1303
- }
1304
-
1305
- // src/cli/commands/log.ts
1306
- async function handleLog(opts) {
1307
- const target = resolveTarget(opts);
1308
- const location = parseBreakpointSpec(opts.at);
1309
- const remoteRoot = parseRemoteRoot(opts.remoteRoot);
1310
- const durationSec = parsePositiveInt(opts.duration, "--duration");
1311
- const expression = opts.expr.trim();
1312
- if (expression.length === 0) {
1313
- throw new CfInspectorError("INVALID_BREAKPOINT", "--expr must not be empty");
1314
- }
1315
- const abort = new AbortController();
1316
- const onSig = () => {
1317
- abort.abort();
1318
- };
1319
- process6.once("SIGINT", onSig);
1320
- process6.once("SIGTERM", onSig);
1321
- try {
1322
- await withSession(target, async (session) => {
1323
- await validateExpression(session, expression);
1324
- const result = await streamLogpoint(session, {
1325
- location,
1326
- expression,
1327
- remoteRoot,
1328
- ...durationSec === void 0 ? {} : { durationMs: durationSec * 1e3 },
1329
- signal: abort.signal,
1330
- onEvent: (event) => {
1331
- writeLogEvent(event, opts.json);
1332
- },
1333
- onBreakpointSet: (handle) => {
1334
- warnOnUnboundBreakpoints([handle]);
1335
- }
1336
- });
1337
- writeLogSummary(result.stoppedReason, result.emitted, opts.json);
1338
- });
1339
- } finally {
1340
- process6.off("SIGINT", onSig);
1341
- process6.off("SIGTERM", onSig);
1342
- }
1343
- }
1344
- function writeLogSummary(stoppedReason, emitted, json) {
1345
- if (json) {
1346
- process6.stderr.write(`${JSON.stringify({ stopped: stoppedReason, emitted })}
1347
- `);
1348
- return;
1349
- }
1350
- process6.stderr.write(
1351
- `Stopped (${stoppedReason}); emitted ${emitted.toString()} log ${emitted === 1 ? "entry" : "entries"}.
1352
- `
1353
- );
1354
- }
1355
-
1356
- // src/cli/commands/snapshot.ts
1357
- import { performance as performance3 } from "perf_hooks";
1358
- import process7 from "process";
1359
-
1360
- // src/inspector/pause.ts
1361
- init_types();
1362
- import { performance as performance2 } from "perf_hooks";
1363
- function pauseMatches(pause, breakpointIds) {
1364
- if (breakpointIds === void 0 || breakpointIds.length === 0) {
1365
- return true;
1366
- }
1367
- return pause.hitBreakpoints.some((id) => breakpointIds.includes(id));
1368
- }
1369
- function remainingUntil(deadlineMs) {
1370
- return Math.max(0, deadlineMs - performance2.now());
1371
- }
1372
- function hasResumedSincePause(session, pause) {
1373
- const pauseAt = pause.receivedAtMs;
1374
- const resumedAt = session.debuggerState.lastResumedAtMs;
1375
- return pauseAt !== void 0 && resumedAt !== void 0 && resumedAt >= pauseAt;
1376
- }
1377
- function throwBreakpointTimeout(timeoutMs) {
1378
- throw new CfInspectorError(
1379
- "BREAKPOINT_NOT_HIT",
1380
- `Timed out waiting for matching Debugger.paused after ${timeoutMs.toString()}ms`
1381
- );
1382
- }
1383
- function throwUnrelatedPauseTimeout(pause, timeoutMs) {
1384
- throw new CfInspectorError(
1385
- "UNRELATED_PAUSE_TIMEOUT",
1386
- `Target stayed paused by another debugger event before this command's breakpoint could hit within ${timeoutMs.toString()}ms`,
1387
- pauseDetail(pause)
1388
- );
1389
- }
1390
- async function waitForUnmatchedPauseToResume(session, pause, deadlineMs, timeoutMs) {
1391
- if (hasResumedSincePause(session, pause)) {
1392
- return;
1393
- }
1394
- const remainingMs = remainingUntil(deadlineMs);
1395
- if (remainingMs <= 0) {
1396
- throwUnrelatedPauseTimeout(pause, timeoutMs);
1397
- }
1398
- try {
1399
- await session.client.waitFor("Debugger.resumed", { timeoutMs: remainingMs });
1400
- session.debuggerState.lastResumedAtMs = performance2.now();
1401
- } catch (err) {
1402
- if (err instanceof CfInspectorError && err.code === "BREAKPOINT_NOT_HIT") {
1403
- throwUnrelatedPauseTimeout(pause, timeoutMs);
1404
- }
1405
- throw err;
1406
- }
1407
- }
1408
- async function handleUnmatchedPause(session, pause, options, deadlineMs) {
1409
- if (options.unmatchedPausePolicy === "fail") {
1410
- throw new CfInspectorError(
1411
- "UNRELATED_PAUSE",
1412
- "Target paused before this command's breakpoint was reached",
1413
- pauseDetail(pause)
1414
- );
1415
- }
1416
- if (hasResumedSincePause(session, pause)) {
1417
- return;
1418
- }
1419
- options.onUnmatchedPause?.(pause);
1420
- await waitForUnmatchedPauseToResume(session, pause, deadlineMs, options.timeoutMs);
1421
- }
1422
- async function waitForPause(session, options) {
1423
- const deadlineMs = performance2.now() + options.timeoutMs;
1424
- const buffer = session.pauseBuffer;
1425
- while (buffer.length > 0 || remainingUntil(deadlineMs) > 0) {
1426
- while (buffer.length > 0) {
1427
- const buffered = buffer.shift();
1428
- if (buffered === void 0) {
1429
- continue;
1430
- }
1431
- if (pauseMatches(buffered, options.breakpointIds)) {
1432
- return buffered;
1433
- }
1434
- await handleUnmatchedPause(session, buffered, options, deadlineMs);
1435
- }
1436
- const pause = await waitForLivePause(session, options, deadlineMs);
1437
- if (pauseMatches(pause, options.breakpointIds)) {
1438
- return pause;
1439
- }
1440
- await handleUnmatchedPause(session, pause, options, deadlineMs);
1441
- }
1442
- throwBreakpointTimeout(options.timeoutMs);
1443
- }
1444
- async function waitForLivePause(session, options, deadlineMs) {
1445
- const remainingMs = remainingUntil(deadlineMs);
1446
- if (remainingMs <= 0) {
1447
- throwBreakpointTimeout(options.timeoutMs);
1448
- }
1449
- session.pauseWaitGate.active = true;
1450
- let receivedAtMs;
1451
- let params;
1452
- try {
1453
- params = await session.client.waitFor("Debugger.paused", {
1454
- timeoutMs: remainingMs,
1455
- predicate: () => {
1456
- receivedAtMs = performance2.now();
1457
- return true;
1458
- }
1459
- });
1460
- } finally {
1461
- session.pauseWaitGate.active = false;
1462
- }
1463
- return toPauseEvent(params, receivedAtMs ?? performance2.now(), session.scripts);
1464
- }
1465
-
1466
- // src/snapshot/values.ts
1467
- init_types();
1468
- var DEFAULT_MAX_VALUE_LENGTH = 4096;
1469
- function isPrimitive(value) {
1470
- const t = typeof value;
1471
- return t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol";
1472
- }
1473
- function formatPrimitive(value) {
1474
- if (typeof value === "symbol") {
1475
- return value.toString();
1476
- }
1477
- if (typeof value === "bigint") {
1478
- return `${value.toString()}n`;
1479
- }
1480
- return String(value);
1307
+ return String(value);
1481
1308
  }
1482
1309
  function resolveMaxValueLength(value) {
1483
1310
  if (value === void 0) {
@@ -1679,19 +1506,11 @@ async function capturePropertyChildren(session, described, depth, maxValueLength
1679
1506
  }
1680
1507
  }
1681
1508
 
1682
- // src/snapshot/objects.ts
1683
- function objectIdFromEvalResult(result) {
1684
- const inner = result.result;
1685
- if (inner?.type !== "object") {
1686
- return void 0;
1687
- }
1688
- const objectId = inner.objectId;
1689
- if (typeof objectId !== "string" || objectId.length === 0) {
1690
- return void 0;
1691
- }
1692
- return objectId;
1509
+ // src/snapshot/exception.ts
1510
+ function asString2(value) {
1511
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1693
1512
  }
1694
- async function renderObjectCapture(session, objectId, maxValueLength) {
1513
+ async function materializeObject(session, objectId, maxValueLength) {
1695
1514
  try {
1696
1515
  const properties = await captureProperties(
1697
1516
  session,
@@ -1700,6 +1519,9 @@ async function renderObjectCapture(session, objectId, maxValueLength) {
1700
1519
  MAX_VARIABLE_DEPTH,
1701
1520
  maxValueLength
1702
1521
  );
1522
+ if (properties.length === 0) {
1523
+ return void 0;
1524
+ }
1703
1525
  const structured = {};
1704
1526
  for (const variable of properties) {
1705
1527
  structured[variable.name] = toStructuredValue(variable);
@@ -1709,10 +1531,102 @@ async function renderObjectCapture(session, objectId, maxValueLength) {
1709
1531
  return void 0;
1710
1532
  }
1711
1533
  }
1712
- function normalizeRenderedObjectCapture(rendered, original) {
1713
- if (rendered === "{}" && original !== "Object") {
1714
- return void 0;
1715
- }
1534
+ async function readPropertyDescription(session, objectId, name) {
1535
+ try {
1536
+ const properties = await getProperties(session, objectId);
1537
+ for (const prop of properties) {
1538
+ if (prop.name !== name) {
1539
+ continue;
1540
+ }
1541
+ const value = prop.value;
1542
+ if (value === void 0) {
1543
+ continue;
1544
+ }
1545
+ if (typeof value.value === "string") {
1546
+ return value.value;
1547
+ }
1548
+ if (typeof value.description === "string") {
1549
+ return value.description;
1550
+ }
1551
+ }
1552
+ return void 0;
1553
+ } catch {
1554
+ return void 0;
1555
+ }
1556
+ }
1557
+ async function captureException(session, pause, maxValueLength) {
1558
+ if (pause.reason !== "exception" && pause.reason !== "promiseRejection") {
1559
+ return void 0;
1560
+ }
1561
+ const data = pause.data;
1562
+ if (typeof data !== "object" || data === null) {
1563
+ return { error: "no exception data attached" };
1564
+ }
1565
+ const candidate = data;
1566
+ const type = asString2(candidate.type);
1567
+ const description = asString2(candidate.description);
1568
+ if (typeof candidate.value === "string") {
1569
+ return buildResult(type, description, JSON.stringify(candidate.value), maxValueLength);
1570
+ }
1571
+ if (typeof candidate.value === "number" || typeof candidate.value === "boolean") {
1572
+ return buildResult(type, description, String(candidate.value), maxValueLength);
1573
+ }
1574
+ const objectId = asString2(candidate.objectId);
1575
+ if (objectId === void 0) {
1576
+ if (description !== void 0) {
1577
+ return buildResult(type, description, description, maxValueLength);
1578
+ }
1579
+ return { error: "exception data has no objectId or value" };
1580
+ }
1581
+ const message = await readPropertyDescription(session, objectId, "message");
1582
+ const rendered = await materializeObject(session, objectId, maxValueLength);
1583
+ if (rendered !== void 0) {
1584
+ const result = buildResult(type, description, rendered, maxValueLength);
1585
+ return message === void 0 ? result : { ...result, description: limitValueLength(message, maxValueLength) };
1586
+ }
1587
+ return buildResult(type, description, description ?? "[exception]", maxValueLength);
1588
+ }
1589
+ function buildResult(type, description, value, maxValueLength) {
1590
+ const safeValue = limitValueLength(value, maxValueLength);
1591
+ const base = { value: safeValue };
1592
+ const withType = type === void 0 ? base : { ...base, type };
1593
+ return description === void 0 ? withType : { ...withType, description: limitValueLength(description, maxValueLength) };
1594
+ }
1595
+
1596
+ // src/snapshot/objects.ts
1597
+ function objectIdFromEvalResult(result) {
1598
+ const inner = result.result;
1599
+ if (inner?.type !== "object") {
1600
+ return void 0;
1601
+ }
1602
+ const objectId = inner.objectId;
1603
+ if (typeof objectId !== "string" || objectId.length === 0) {
1604
+ return void 0;
1605
+ }
1606
+ return objectId;
1607
+ }
1608
+ async function renderObjectCapture(session, objectId, maxValueLength) {
1609
+ try {
1610
+ const properties = await captureProperties(
1611
+ session,
1612
+ objectId,
1613
+ MAX_SCOPE_VARIABLES,
1614
+ MAX_VARIABLE_DEPTH,
1615
+ maxValueLength
1616
+ );
1617
+ const structured = {};
1618
+ for (const variable of properties) {
1619
+ structured[variable.name] = toStructuredValue(variable);
1620
+ }
1621
+ return JSON.stringify(structured);
1622
+ } catch {
1623
+ return void 0;
1624
+ }
1625
+ }
1626
+ function normalizeRenderedObjectCapture(rendered, original) {
1627
+ if (rendered === "{}" && original !== "Object") {
1628
+ return void 0;
1629
+ }
1716
1630
  if (original.startsWith("Array(") && rendered === '{"length":0}') {
1717
1631
  return "[]";
1718
1632
  }
@@ -1781,12 +1695,73 @@ async function captureScopes(session, frame, maxValueLength) {
1781
1695
  );
1782
1696
  }
1783
1697
 
1698
+ // src/snapshot/stack.ts
1699
+ var DEFAULT_STACK_DEPTH = 1;
1700
+ var MAX_STACK_DEPTH = 64;
1701
+ function clampDepth(depth, frameCount) {
1702
+ if (depth <= 0) {
1703
+ return 0;
1704
+ }
1705
+ return Math.min(depth, frameCount, MAX_STACK_DEPTH);
1706
+ }
1707
+ function buildBaseFrame(frame) {
1708
+ const base = {
1709
+ functionName: frame.functionName,
1710
+ line: frame.lineNumber + 1,
1711
+ column: frame.columnNumber + 1
1712
+ };
1713
+ return frame.url === void 0 ? base : { ...base, url: frame.url };
1714
+ }
1715
+ async function captureFrameExpression(session, callFrameId, expression, maxValueLength) {
1716
+ try {
1717
+ const result = await evaluateOnFrame(session, callFrameId, expression);
1718
+ const captured = evalResultToCaptured(expression, result, maxValueLength);
1719
+ return await withSerializedObjectCapture(session, expression, result, captured, maxValueLength);
1720
+ } catch (err) {
1721
+ const message = err instanceof Error ? err.message : String(err);
1722
+ return { expression, error: limitValueLength(message, maxValueLength) };
1723
+ }
1724
+ }
1725
+ async function captureFrameExpressions(session, frame, expressions, maxValueLength) {
1726
+ if (expressions.length === 0) {
1727
+ return [];
1728
+ }
1729
+ return await Promise.all(
1730
+ expressions.map(
1731
+ (expression) => captureFrameExpression(session, frame.callFrameId, expression, maxValueLength)
1732
+ )
1733
+ );
1734
+ }
1735
+ async function walkStack(session, callFrames, options) {
1736
+ const depth = clampDepth(options.stackDepth, callFrames.length);
1737
+ if (depth <= 1) {
1738
+ return [];
1739
+ }
1740
+ const slice = callFrames.slice(0, depth);
1741
+ return await Promise.all(
1742
+ slice.map(async (frame) => {
1743
+ const base = buildBaseFrame(frame);
1744
+ if (options.stackCaptures.length === 0) {
1745
+ return base;
1746
+ }
1747
+ const captures = await captureFrameExpressions(
1748
+ session,
1749
+ frame,
1750
+ options.stackCaptures,
1751
+ options.maxValueLength
1752
+ );
1753
+ return { ...base, captures };
1754
+ })
1755
+ );
1756
+ }
1757
+
1784
1758
  // src/snapshot/capture.ts
1785
1759
  async function captureSnapshot(session, pause, options = {}) {
1786
1760
  const maxValueLength = resolveMaxValueLength(options.maxValueLength);
1787
1761
  const top = pause.callFrames[0];
1788
1762
  let topFrame;
1789
1763
  let captures = [];
1764
+ let stack = [];
1790
1765
  if (top) {
1791
1766
  topFrame = {
1792
1767
  functionName: top.functionName,
@@ -1799,14 +1774,31 @@ async function captureSnapshot(session, pause, options = {}) {
1799
1774
  topFrame = { ...topFrame, scopes };
1800
1775
  }
1801
1776
  captures = await captureExpressions(session, top.callFrameId, options.captures, maxValueLength);
1777
+ stack = await walkStack(session, pause.callFrames, {
1778
+ stackDepth: options.stackDepth ?? DEFAULT_STACK_DEPTH,
1779
+ stackCaptures: options.stackCaptures ?? [],
1780
+ maxValueLength
1781
+ });
1802
1782
  }
1803
- return {
1804
- reason: pause.reason,
1805
- hitBreakpoints: pause.hitBreakpoints,
1783
+ const exception = await captureException(session, pause, maxValueLength);
1784
+ return buildResult2({
1785
+ pause,
1786
+ topFrame,
1787
+ captures,
1788
+ stack,
1789
+ exception
1790
+ });
1791
+ }
1792
+ function buildResult2(input) {
1793
+ const base = {
1794
+ reason: input.pause.reason,
1795
+ hitBreakpoints: input.pause.hitBreakpoints,
1806
1796
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1807
- ...topFrame === void 0 ? {} : { topFrame },
1808
- captures
1797
+ captures: input.captures
1809
1798
  };
1799
+ const withFrame = input.topFrame === void 0 ? base : { ...base, topFrame: input.topFrame };
1800
+ const withStack = input.stack.length > 0 ? { ...withFrame, stack: input.stack } : withFrame;
1801
+ return input.exception === void 0 ? withStack : { ...withStack, exception: input.exception };
1810
1802
  }
1811
1803
  async function captureExpressions(session, callFrameId, captures, maxValueLength) {
1812
1804
  if (captures === void 0 || captures.length === 0) {
@@ -1829,7 +1821,7 @@ async function captureExpression(session, callFrameId, expression, maxValueLengt
1829
1821
  }
1830
1822
  }
1831
1823
 
1832
- // src/cli/commands/snapshot.ts
1824
+ // src/cli/commands/exception.ts
1833
1825
  init_types();
1834
1826
 
1835
1827
  // src/cli/captureParser.ts
@@ -1912,123 +1904,820 @@ function splitCaptureExpressions(raw) {
1912
1904
  return state.pieces;
1913
1905
  }
1914
1906
 
1915
- // src/cli/commands/snapshot.ts
1916
- async function handleSnapshot(opts) {
1917
- const prepared = prepareSnapshotCommand(opts);
1918
- const result = await runSnapshotCommand(prepared, opts);
1907
+ // src/cli/warnings.ts
1908
+ import process4 from "process";
1909
+ function warnOnUnboundBreakpoints(handles) {
1910
+ for (const handle of handles) {
1911
+ if (handle.resolvedLocations.length === 0) {
1912
+ process4.stderr.write(
1913
+ `[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.
1914
+ `
1915
+ );
1916
+ }
1917
+ }
1918
+ }
1919
+ function roundDurationMs(durationMs) {
1920
+ return Math.round(durationMs * 1e3) / 1e3;
1921
+ }
1922
+ function warnOnUnmatchedPause(pause) {
1923
+ const reason = pause.reason.length > 0 ? pause.reason : "unknown";
1924
+ process4.stderr.write(
1925
+ `[cf-inspector] warning: target is paused by another debugger event (${reason} at ${formatPauseLocation(pause)}); waiting for it to resume...
1926
+ `
1927
+ );
1928
+ }
1929
+ function withPausedDuration(snapshot, pausedDurationMs) {
1930
+ const base = {
1931
+ reason: snapshot.reason,
1932
+ hitBreakpoints: snapshot.hitBreakpoints,
1933
+ capturedAt: snapshot.capturedAt,
1934
+ pausedDurationMs,
1935
+ captures: snapshot.captures
1936
+ };
1937
+ const withFrame = snapshot.topFrame === void 0 ? base : { ...base, topFrame: snapshot.topFrame };
1938
+ const withStack = snapshot.stack === void 0 ? withFrame : { ...withFrame, stack: snapshot.stack };
1939
+ return snapshot.exception === void 0 ? withStack : { ...withStack, exception: snapshot.exception };
1940
+ }
1941
+ function formatPauseLocation(pause) {
1942
+ const top = pause.callFrames[0];
1943
+ if (top === void 0) {
1944
+ return "(no call frame)";
1945
+ }
1946
+ const url = top.url !== void 0 && top.url.length > 0 ? top.url : "(unknown)";
1947
+ return `${url}:${(top.lineNumber + 1).toString()}:${(top.columnNumber + 1).toString()}`;
1948
+ }
1949
+
1950
+ // src/cli/commands/exception.ts
1951
+ var VALID_PAUSE_TYPES = ["uncaught", "caught", "all"];
1952
+ async function handleException(opts) {
1953
+ const prepared = prepareExceptionCommand(opts);
1954
+ const result = await runExceptionCommand(prepared, opts);
1919
1955
  if (opts.json) {
1920
1956
  writeJson(result);
1921
1957
  } else {
1922
1958
  writeHumanSnapshot(result);
1923
1959
  }
1924
1960
  }
1925
- function prepareSnapshotCommand(opts) {
1961
+ function prepareExceptionCommand(opts) {
1926
1962
  const target = resolveTarget(opts);
1927
- if (opts.bp.length === 0) {
1963
+ const stateRaw = (opts.type ?? "uncaught").trim().toLowerCase();
1964
+ if (!VALID_PAUSE_TYPES.includes(stateRaw)) {
1928
1965
  throw new CfInspectorError(
1929
- "INVALID_BREAKPOINT",
1930
- "At least one --bp <file:line> is required."
1966
+ "INVALID_PAUSE_TYPE",
1967
+ `--type must be one of ${VALID_PAUSE_TYPES.join(", ")} (received "${stateRaw}")`
1931
1968
  );
1932
1969
  }
1933
- const timeoutSec = parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_BREAKPOINT_TIMEOUT_SEC;
1970
+ const timeoutSec = parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_EXCEPTION_TIMEOUT_SEC;
1934
1971
  const maxValueLength = parsePositiveInt(opts.maxValueLength, "--max-value-length");
1935
- const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
1972
+ const stackDepth = parsePositiveInt(opts.stackDepth, "--stack-depth");
1936
1973
  return {
1937
1974
  target,
1938
- breakpoints: opts.bp.map((spec) => parseBreakpointSpec(spec)),
1975
+ state: stateRaw,
1939
1976
  captures: parseCaptureList(opts.capture),
1940
1977
  remoteRoot: parseRemoteRoot(opts.remoteRoot),
1941
1978
  timeoutMs: timeoutSec * 1e3,
1942
- ...condition === void 0 ? {} : { condition },
1943
- ...maxValueLength === void 0 ? {} : { maxValueLength }
1979
+ ...maxValueLength === void 0 ? {} : { maxValueLength },
1980
+ ...stackDepth === void 0 ? {} : { stackDepth },
1981
+ stackCaptures: parseCaptureList(opts.stackCaptures)
1944
1982
  };
1945
1983
  }
1946
- async function runSnapshotCommand(command, opts) {
1984
+ async function runExceptionCommand(command, opts) {
1947
1985
  return await withSession(command.target, async (session) => {
1948
- if (command.condition !== void 0) {
1949
- await validateExpression(session, command.condition);
1950
- }
1951
- const handles = await Promise.all(
1952
- command.breakpoints.map(
1953
- (bp) => setBreakpoint(session, {
1954
- file: bp.file,
1955
- line: bp.line,
1956
- remoteRoot: command.remoteRoot,
1957
- ...command.condition === void 0 ? {} : { condition: command.condition }
1958
- })
1959
- )
1960
- );
1961
- warnOnUnboundBreakpoints(handles);
1962
- const pause = await waitForCommandPause(session, opts, handles, command.timeoutMs);
1963
- const pausedStartedAt = pause.receivedAtMs ?? performance3.now();
1964
- const snapshot = await captureSnapshot(session, pause, {
1965
- captures: command.captures,
1966
- includeScopes: opts.includeScopes === true,
1967
- ...command.maxValueLength === void 0 ? {} : { maxValueLength: command.maxValueLength }
1968
- });
1969
- if (opts.keepPaused === true) {
1970
- return withPausedDuration(snapshot, null);
1971
- }
1972
- return await resumeAfterSnapshot(session, snapshot, pausedStartedAt);
1973
- });
1974
- }
1975
- async function waitForCommandPause(session, opts, handles, timeoutMs) {
1976
- let warnedUnmatchedPause = false;
1977
- return await waitForPause(session, {
1978
- timeoutMs,
1979
- breakpointIds: handles.map((h) => h.breakpointId),
1980
- unmatchedPausePolicy: opts.failOnUnmatchedPause === true ? "fail" : "wait-for-resume",
1981
- onUnmatchedPause: (unmatchedPause) => {
1982
- if (warnedUnmatchedPause || opts.failOnUnmatchedPause === true) {
1983
- return;
1986
+ await setPauseOnExceptions(session, command.state);
1987
+ try {
1988
+ const pause = await waitForPause(session, {
1989
+ timeoutMs: command.timeoutMs,
1990
+ pauseReasons: ["exception", "promiseRejection"],
1991
+ unmatchedPausePolicy: "wait-for-resume"
1992
+ });
1993
+ const pausedStartedAt = pause.receivedAtMs ?? performance3.now();
1994
+ const snapshot = await captureSnapshot(session, pause, {
1995
+ captures: command.captures,
1996
+ includeScopes: opts.includeScopes === true,
1997
+ ...command.maxValueLength === void 0 ? {} : { maxValueLength: command.maxValueLength },
1998
+ ...command.stackDepth === void 0 ? {} : { stackDepth: command.stackDepth },
1999
+ stackCaptures: command.stackCaptures
2000
+ });
2001
+ if (opts.keepPaused === true) {
2002
+ return withPausedDuration(snapshot, null);
1984
2003
  }
1985
- warnedUnmatchedPause = true;
1986
- warnOnUnmatchedPause(unmatchedPause);
2004
+ return await resumeAfterException(session, snapshot, pausedStartedAt);
2005
+ } finally {
2006
+ await disablePauseOnExceptionsBestEffort(session);
1987
2007
  }
1988
2008
  });
1989
2009
  }
1990
- async function resumeAfterSnapshot(session, snapshot, pausedStartedAt) {
2010
+ async function resumeAfterException(session, snapshot, pausedStartedAt) {
1991
2011
  try {
1992
2012
  await resume(session);
1993
2013
  return withPausedDuration(snapshot, roundDurationMs(performance3.now() - pausedStartedAt));
1994
2014
  } catch {
1995
- process7.stderr.write(
1996
- "[cf-inspector] warning: Debugger.resume failed after snapshot; pausedDurationMs is unknown.\n"
2015
+ process5.stderr.write(
2016
+ "[cf-inspector] warning: Debugger.resume failed after exception capture; pausedDurationMs is unknown.\n"
1997
2017
  );
1998
2018
  return withPausedDuration(snapshot, null);
1999
2019
  }
2000
2020
  }
2021
+ async function disablePauseOnExceptionsBestEffort(session) {
2022
+ try {
2023
+ await setPauseOnExceptions(session, "none");
2024
+ } catch {
2025
+ }
2026
+ }
2001
2027
 
2002
- // src/cli/program.ts
2003
- function applyTargetOptions(cmd) {
2004
- return cmd.option("--port <number>", "Local port the inspector or tunnel listens on").option("--host <host>", "Hostname (default: 127.0.0.1)", "127.0.0.1").option("--region <key>", "CF region key (e.g. eu10)").option("--org <name>", "CF org name").option("--space <name>", "CF space name").option("--app <name>", "CF app name").option("--cf-timeout <seconds>", "Timeout for CF tunnel readiness in seconds");
2028
+ // src/cli/commands/listScripts.ts
2029
+ import process6 from "process";
2030
+ async function handleListScripts(opts) {
2031
+ const target = resolveTarget(opts);
2032
+ const scripts = await withSession(target, (session) => Promise.resolve(listScripts(session)));
2033
+ if (opts.json) {
2034
+ writeJson(scripts);
2035
+ return;
2036
+ }
2037
+ for (const script of scripts) {
2038
+ process6.stdout.write(`${script.scriptId} ${script.url}
2039
+ `);
2040
+ }
2005
2041
  }
2006
- async function main(argv) {
2007
- const program = new Command();
2008
- program.name("cf-inspector").description("Drive a Node.js inspector from the command line \u2014 set breakpoints, capture snapshots, evaluate expressions");
2009
- registerSnapshot(program);
2010
- registerLog(program);
2011
- registerEval(program);
2012
- registerListScripts(program);
2013
- registerAttach(program);
2014
- await program.parseAsync([...argv]);
2042
+
2043
+ // src/cli/commands/log.ts
2044
+ import process7 from "process";
2045
+
2046
+ // src/logpoint/stream.ts
2047
+ init_types();
2048
+
2049
+ // src/logpoint/condition.ts
2050
+ import { randomBytes } from "crypto";
2051
+ var SENTINEL_PREFIX = "__CFI_LOG_";
2052
+ var SENTINEL_SUFFIX = "__";
2053
+ var HITS_GLOBAL2 = "globalThis.__CFI_LOG_HITS";
2054
+ function buildLoggingIife(sentinel, expression) {
2055
+ return [
2056
+ "(function(){",
2057
+ `var s=${JSON.stringify(sentinel)};`,
2058
+ "try{",
2059
+ `var v=(${expression});`,
2060
+ "var r=typeof v==='string'?v:JSON.stringify(v);",
2061
+ "console.log(s, r);",
2062
+ "}catch(e){",
2063
+ "console.log(s, '!err:'+(e&&e.message?e.message:String(e)));",
2064
+ "}",
2065
+ "return false;",
2066
+ "})()"
2067
+ ].join("");
2015
2068
  }
2016
- function registerSnapshot(program) {
2017
- const collectStrings = (value, prev = []) => [
2018
- ...prev,
2019
- value
2020
- ];
2021
- applyTargetOptions(
2022
- program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume")
2023
- ).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("--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) => {
2024
- await handleSnapshot(opts);
2025
- });
2069
+ function buildHitGate(hitCount, counterKey) {
2070
+ const keyLiteral = JSON.stringify(counterKey);
2071
+ return [
2072
+ "(function(){",
2073
+ `var m=(${HITS_GLOBAL2}=${HITS_GLOBAL2}||{});`,
2074
+ `var k=${keyLiteral};`,
2075
+ "m[k]=(m[k]||0)+1;",
2076
+ `return m[k]>=${hitCount.toString()};`,
2077
+ "})()"
2078
+ ].join("");
2026
2079
  }
2027
- function registerLog(program) {
2028
- applyTargetOptions(
2029
- program.command("log").description("Stream a non-pausing logpoint: log an expression each time a line executes")
2030
- ).requiredOption("--at <file:line>", "Logpoint location, e.g. src/handler.ts:42").requiredOption("--expr <expression>", "JavaScript expression to log on each hit").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("--no-json", "Print human-readable lines instead of JSON Lines").action(async (opts) => {
2031
- await handleLog(opts);
2080
+ function combineGuards(guards) {
2081
+ const filtered = guards.filter((guard) => guard.length > 0);
2082
+ if (filtered.length === 0) {
2083
+ return void 0;
2084
+ }
2085
+ if (filtered.length === 1) {
2086
+ return filtered[0];
2087
+ }
2088
+ return filtered.map((guard) => `(${guard})`).join("&&");
2089
+ }
2090
+ function buildLogpointCondition(sentinel, expression, options = {}) {
2091
+ const guards = [];
2092
+ if (options.hitCount !== void 0) {
2093
+ const counterKey = options.counterKey ?? sentinel;
2094
+ guards.push(buildHitGate(options.hitCount, counterKey));
2095
+ }
2096
+ const userPredicate = options.predicate?.trim();
2097
+ if (userPredicate !== void 0 && userPredicate.length > 0) {
2098
+ guards.push(`(${userPredicate})`);
2099
+ }
2100
+ const iife = buildLoggingIife(sentinel, expression);
2101
+ const guard = combineGuards(guards);
2102
+ if (guard === void 0) {
2103
+ return iife;
2104
+ }
2105
+ return `(${guard})?${iife}:false`;
2106
+ }
2107
+ function generateSentinel() {
2108
+ return `${SENTINEL_PREFIX}${randomBytes(8).toString("hex")}${SENTINEL_SUFFIX}`;
2109
+ }
2110
+
2111
+ // src/logpoint/events.ts
2112
+ function asString3(value) {
2113
+ return typeof value === "string" ? value : void 0;
2114
+ }
2115
+ function readArg(arg, index) {
2116
+ if (typeof arg !== "object" || arg === null) {
2117
+ return void 0;
2118
+ }
2119
+ const candidate = arg;
2120
+ if (candidate.type === "string" && typeof candidate.value === "string") {
2121
+ return candidate.value;
2122
+ }
2123
+ const isPrimitiveType = candidate.type === "number" || candidate.type === "boolean" || candidate.type === "bigint";
2124
+ const isPrimitiveValue = typeof candidate.value === "number" || typeof candidate.value === "boolean" || typeof candidate.value === "bigint";
2125
+ if (isPrimitiveType && isPrimitiveValue) {
2126
+ return String(candidate.value);
2127
+ }
2128
+ return index === 0 ? void 0 : "";
2129
+ }
2130
+ function parseLogEvent(rawArgs, sentinel, location, timestamp) {
2131
+ if (!Array.isArray(rawArgs) || rawArgs.length < 2) {
2132
+ return void 0;
2133
+ }
2134
+ const tag = readArg(rawArgs[0], 0);
2135
+ if (tag !== sentinel) {
2136
+ return void 0;
2137
+ }
2138
+ const payload = readArg(rawArgs[1], 1) ?? "";
2139
+ const ts = new Date(typeof timestamp === "number" ? timestamp : Date.now()).toISOString();
2140
+ const at = `${location.file}:${location.line.toString()}`;
2141
+ if (payload.startsWith("!err:")) {
2142
+ return { ts, at, error: payload.slice("!err:".length) };
2143
+ }
2144
+ return parsePayload(ts, at, payload);
2145
+ }
2146
+ function parsePayload(ts, at, payload) {
2147
+ try {
2148
+ const parsed = JSON.parse(payload);
2149
+ if (typeof parsed === "string") {
2150
+ return { ts, at, value: parsed };
2151
+ }
2152
+ return { ts, at, value: JSON.stringify(parsed) };
2153
+ } catch {
2154
+ return { ts, at, value: payload, raw: payload };
2155
+ }
2156
+ }
2157
+
2158
+ // src/logpoint/stream.ts
2159
+ function validateMaxEvents(maxEvents) {
2160
+ if (maxEvents === void 0) {
2161
+ return void 0;
2162
+ }
2163
+ if (!Number.isInteger(maxEvents) || maxEvents <= 0) {
2164
+ throw new CfInspectorError(
2165
+ "INVALID_ARGUMENT",
2166
+ `maxEvents must be a positive integer, received: ${maxEvents.toString()}`
2167
+ );
2168
+ }
2169
+ return maxEvents;
2170
+ }
2171
+ function validateHitCount2(hitCount) {
2172
+ if (hitCount === void 0) {
2173
+ return void 0;
2174
+ }
2175
+ if (!Number.isInteger(hitCount) || hitCount <= 0) {
2176
+ throw new CfInspectorError(
2177
+ "INVALID_HIT_COUNT",
2178
+ `hitCount must be a positive integer, received: ${hitCount.toString()}`
2179
+ );
2180
+ }
2181
+ return hitCount;
2182
+ }
2183
+ async function streamLogpoint(session, options) {
2184
+ const maxEvents = validateMaxEvents(options.maxEvents);
2185
+ const hitCount = validateHitCount2(options.hitCount);
2186
+ const sentinel = generateSentinel();
2187
+ const condition = buildLogpointCondition(sentinel, options.expression, {
2188
+ ...options.condition === void 0 ? {} : { predicate: options.condition },
2189
+ ...hitCount === void 0 ? {} : { hitCount }
2190
+ });
2191
+ let emitted = 0;
2192
+ let maxEventsReached = false;
2193
+ let stopMaxEvents;
2194
+ const offEvent = session.client.on("Runtime.consoleAPICalled", (raw) => {
2195
+ if (maxEventsReached) {
2196
+ return;
2197
+ }
2198
+ const event = toLogpointEvent(raw, sentinel, options.location);
2199
+ if (event === void 0) {
2200
+ return;
2201
+ }
2202
+ emitted += 1;
2203
+ options.onEvent(event);
2204
+ if (maxEvents !== void 0 && emitted >= maxEvents) {
2205
+ maxEventsReached = true;
2206
+ stopMaxEvents?.();
2207
+ }
2208
+ });
2209
+ let handle;
2210
+ try {
2211
+ handle = await setBreakpoint(session, {
2212
+ file: options.location.file,
2213
+ line: options.location.line,
2214
+ ...options.remoteRoot === void 0 ? {} : { remoteRoot: options.remoteRoot },
2215
+ condition
2216
+ });
2217
+ } catch (err) {
2218
+ offEvent();
2219
+ throw err;
2220
+ }
2221
+ options.onBreakpointSet?.(handle);
2222
+ try {
2223
+ const reason = await waitForStop(session, options, (signal) => {
2224
+ stopMaxEvents = signal;
2225
+ if (maxEventsReached) {
2226
+ signal();
2227
+ }
2228
+ });
2229
+ return { handle, sentinel, emitted, stoppedReason: reason };
2230
+ } finally {
2231
+ offEvent();
2232
+ await removeBreakpointBestEffort(session, handle.breakpointId);
2233
+ }
2234
+ }
2235
+ function toLogpointEvent(raw, sentinel, location) {
2236
+ const params = raw;
2237
+ if (asString3(params.type) !== "log") {
2238
+ return void 0;
2239
+ }
2240
+ const ts = typeof params.timestamp === "number" ? params.timestamp : void 0;
2241
+ return parseLogEvent(params.args, sentinel, location, ts);
2242
+ }
2243
+ async function removeBreakpointBestEffort(session, breakpointId) {
2244
+ try {
2245
+ await removeBreakpoint(session, breakpointId);
2246
+ } catch {
2247
+ }
2248
+ }
2249
+ async function waitForStop(session, options, registerMaxEventsSignal) {
2250
+ return await new Promise((resolve) => {
2251
+ let settled = false;
2252
+ const finish = (reason) => {
2253
+ if (settled) {
2254
+ return;
2255
+ }
2256
+ settled = true;
2257
+ cleanup();
2258
+ resolve(reason);
2259
+ };
2260
+ const timer = options.durationMs === void 0 ? void 0 : setTimeout(() => {
2261
+ finish("duration");
2262
+ }, options.durationMs);
2263
+ const offClose = session.client.onClose(() => {
2264
+ finish("transport-closed");
2265
+ });
2266
+ const onAbort = () => {
2267
+ finish("signal");
2268
+ };
2269
+ options.signal?.addEventListener("abort", onAbort, { once: true });
2270
+ if (options.signal?.aborted === true) {
2271
+ finish("signal");
2272
+ }
2273
+ registerMaxEventsSignal(() => {
2274
+ finish("max-events");
2275
+ });
2276
+ function cleanup() {
2277
+ if (timer !== void 0) {
2278
+ clearTimeout(timer);
2279
+ }
2280
+ offClose();
2281
+ options.signal?.removeEventListener("abort", onAbort);
2282
+ }
2283
+ });
2284
+ }
2285
+
2286
+ // src/cli/commands/log.ts
2287
+ init_types();
2288
+ async function handleLog(opts) {
2289
+ const target = resolveTarget(opts);
2290
+ const location = parseBreakpointSpec(opts.at);
2291
+ const remoteRoot = parseRemoteRoot(opts.remoteRoot);
2292
+ const durationSec = parsePositiveInt(opts.duration, "--duration");
2293
+ const maxEvents = parsePositiveInt(opts.maxEvents, "--max-events");
2294
+ const hitCount = parsePositiveInt(opts.hitCount, "--hit-count");
2295
+ const expression = opts.expr.trim();
2296
+ if (expression.length === 0) {
2297
+ throw new CfInspectorError("INVALID_EXPRESSION", "--expr must not be empty");
2298
+ }
2299
+ const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
2300
+ const abort = new AbortController();
2301
+ const onSig = () => {
2302
+ abort.abort();
2303
+ };
2304
+ process7.once("SIGINT", onSig);
2305
+ process7.once("SIGTERM", onSig);
2306
+ try {
2307
+ await withSession(target, async (session) => {
2308
+ await validateExpression(session, expression);
2309
+ if (condition !== void 0) {
2310
+ await validateExpression(session, condition);
2311
+ }
2312
+ const result = await streamLogpoint(session, {
2313
+ location,
2314
+ expression,
2315
+ remoteRoot,
2316
+ ...durationSec === void 0 ? {} : { durationMs: durationSec * 1e3 },
2317
+ ...maxEvents === void 0 ? {} : { maxEvents },
2318
+ ...hitCount === void 0 ? {} : { hitCount },
2319
+ ...condition === void 0 ? {} : { condition },
2320
+ signal: abort.signal,
2321
+ onEvent: (event) => {
2322
+ writeLogEvent(event, opts.json);
2323
+ },
2324
+ onBreakpointSet: (handle) => {
2325
+ warnOnUnboundBreakpoints([handle]);
2326
+ }
2327
+ });
2328
+ writeLogSummary(result.stoppedReason, result.emitted, opts.json);
2329
+ });
2330
+ } finally {
2331
+ process7.off("SIGINT", onSig);
2332
+ process7.off("SIGTERM", onSig);
2333
+ }
2334
+ }
2335
+ function writeLogSummary(stoppedReason, emitted, json) {
2336
+ if (json) {
2337
+ process7.stderr.write(`${JSON.stringify({ stopped: stoppedReason, emitted })}
2338
+ `);
2339
+ return;
2340
+ }
2341
+ process7.stderr.write(
2342
+ `Stopped (${stoppedReason}); emitted ${emitted.toString()} log ${emitted === 1 ? "entry" : "entries"}.
2343
+ `
2344
+ );
2345
+ }
2346
+
2347
+ // src/cli/commands/snapshot.ts
2348
+ import { performance as performance4 } from "perf_hooks";
2349
+ import process8 from "process";
2350
+ init_types();
2351
+ async function handleSnapshot(opts) {
2352
+ const prepared = prepareSnapshotCommand(opts);
2353
+ const result = await runSnapshotCommand(prepared, opts);
2354
+ if (opts.json) {
2355
+ writeJson(result);
2356
+ } else {
2357
+ writeHumanSnapshot(result);
2358
+ }
2359
+ }
2360
+ function prepareSnapshotCommand(opts) {
2361
+ const target = resolveTarget(opts);
2362
+ if (opts.bp.length === 0) {
2363
+ throw new CfInspectorError(
2364
+ "INVALID_BREAKPOINT",
2365
+ "At least one --bp <file:line> is required."
2366
+ );
2367
+ }
2368
+ const timeoutSec = parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_BREAKPOINT_TIMEOUT_SEC;
2369
+ const maxValueLength = parsePositiveInt(opts.maxValueLength, "--max-value-length");
2370
+ const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
2371
+ const hitCount = parsePositiveInt(opts.hitCount, "--hit-count");
2372
+ const stackDepth = parsePositiveInt(opts.stackDepth, "--stack-depth");
2373
+ return {
2374
+ target,
2375
+ breakpoints: opts.bp.map((spec) => parseBreakpointSpec(spec)),
2376
+ captures: parseCaptureList(opts.capture),
2377
+ remoteRoot: parseRemoteRoot(opts.remoteRoot),
2378
+ timeoutMs: timeoutSec * 1e3,
2379
+ ...condition === void 0 ? {} : { condition },
2380
+ ...maxValueLength === void 0 ? {} : { maxValueLength },
2381
+ ...hitCount === void 0 ? {} : { hitCount },
2382
+ ...stackDepth === void 0 ? {} : { stackDepth },
2383
+ stackCaptures: parseCaptureList(opts.stackCaptures)
2384
+ };
2385
+ }
2386
+ async function runSnapshotCommand(command, opts) {
2387
+ return await withSession(command.target, async (session) => {
2388
+ if (command.condition !== void 0) {
2389
+ await validateExpression(session, command.condition);
2390
+ }
2391
+ const handles = await Promise.all(
2392
+ command.breakpoints.map(
2393
+ (bp) => setBreakpoint(session, {
2394
+ file: bp.file,
2395
+ line: bp.line,
2396
+ remoteRoot: command.remoteRoot,
2397
+ ...command.condition === void 0 ? {} : { condition: command.condition },
2398
+ ...command.hitCount === void 0 ? {} : { hitCount: command.hitCount }
2399
+ })
2400
+ )
2401
+ );
2402
+ warnOnUnboundBreakpoints(handles);
2403
+ const pause = await waitForCommandPause(session, opts, handles, command.timeoutMs);
2404
+ const pausedStartedAt = pause.receivedAtMs ?? performance4.now();
2405
+ const snapshot = await captureSnapshot(session, pause, {
2406
+ captures: command.captures,
2407
+ includeScopes: opts.includeScopes === true,
2408
+ ...command.maxValueLength === void 0 ? {} : { maxValueLength: command.maxValueLength },
2409
+ ...command.stackDepth === void 0 ? {} : { stackDepth: command.stackDepth },
2410
+ stackCaptures: command.stackCaptures
2411
+ });
2412
+ if (opts.keepPaused === true) {
2413
+ return withPausedDuration(snapshot, null);
2414
+ }
2415
+ return await resumeAfterSnapshot(session, snapshot, pausedStartedAt);
2416
+ });
2417
+ }
2418
+ async function waitForCommandPause(session, opts, handles, timeoutMs) {
2419
+ let warnedUnmatchedPause = false;
2420
+ return await waitForPause(session, {
2421
+ timeoutMs,
2422
+ breakpointIds: handles.map((h) => h.breakpointId),
2423
+ unmatchedPausePolicy: opts.failOnUnmatchedPause === true ? "fail" : "wait-for-resume",
2424
+ onUnmatchedPause: (unmatchedPause) => {
2425
+ if (warnedUnmatchedPause || opts.failOnUnmatchedPause === true) {
2426
+ return;
2427
+ }
2428
+ warnedUnmatchedPause = true;
2429
+ warnOnUnmatchedPause(unmatchedPause);
2430
+ }
2431
+ });
2432
+ }
2433
+ async function resumeAfterSnapshot(session, snapshot, pausedStartedAt) {
2434
+ try {
2435
+ await resume(session);
2436
+ return withPausedDuration(snapshot, roundDurationMs(performance4.now() - pausedStartedAt));
2437
+ } catch {
2438
+ process8.stderr.write(
2439
+ "[cf-inspector] warning: Debugger.resume failed after snapshot; pausedDurationMs is unknown.\n"
2440
+ );
2441
+ return withPausedDuration(snapshot, null);
2442
+ }
2443
+ }
2444
+
2445
+ // src/cli/commands/watch.ts
2446
+ import process9 from "process";
2447
+ init_types();
2448
+ async function handleWatch(opts) {
2449
+ const prepared = prepareWatchCommand(opts);
2450
+ const abort = new AbortController();
2451
+ const onSig = () => {
2452
+ abort.abort();
2453
+ };
2454
+ process9.once("SIGINT", onSig);
2455
+ process9.once("SIGTERM", onSig);
2456
+ let stoppedReason = "signal";
2457
+ let emitted = 0;
2458
+ try {
2459
+ await withSession(prepared.target, async (session) => {
2460
+ const result = await runWatchLoop(session, prepared, opts, abort.signal);
2461
+ stoppedReason = result.stoppedReason;
2462
+ emitted = result.emitted;
2463
+ });
2464
+ } finally {
2465
+ process9.off("SIGINT", onSig);
2466
+ process9.off("SIGTERM", onSig);
2467
+ }
2468
+ writeWatchSummary(stoppedReason, emitted, opts.json);
2469
+ }
2470
+ function prepareWatchCommand(opts) {
2471
+ const target = resolveTarget(opts);
2472
+ if (opts.bp.length === 0) {
2473
+ throw new CfInspectorError(
2474
+ "INVALID_BREAKPOINT",
2475
+ "At least one --bp <file:line> is required."
2476
+ );
2477
+ }
2478
+ const perHitTimeoutSec = parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_BREAKPOINT_TIMEOUT_SEC;
2479
+ const durationSec = parsePositiveInt(opts.duration, "--duration");
2480
+ const maxEvents = parsePositiveInt(opts.maxEvents, "--max-events");
2481
+ const maxValueLength = parsePositiveInt(opts.maxValueLength, "--max-value-length");
2482
+ const hitCount = parsePositiveInt(opts.hitCount, "--hit-count");
2483
+ const stackDepth = parsePositiveInt(opts.stackDepth, "--stack-depth");
2484
+ const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
2485
+ return {
2486
+ target,
2487
+ breakpoints: opts.bp.map((spec) => parseBreakpointSpec(spec)),
2488
+ captures: parseCaptureList(opts.capture),
2489
+ remoteRoot: parseRemoteRoot(opts.remoteRoot),
2490
+ perHitTimeoutMs: perHitTimeoutSec * 1e3,
2491
+ ...durationSec === void 0 ? {} : { durationMs: durationSec * 1e3 },
2492
+ ...maxEvents === void 0 ? {} : { maxEvents },
2493
+ ...maxValueLength === void 0 ? {} : { maxValueLength },
2494
+ ...condition === void 0 ? {} : { condition },
2495
+ ...hitCount === void 0 ? {} : { hitCount },
2496
+ ...stackDepth === void 0 ? {} : { stackDepth },
2497
+ stackCaptures: parseCaptureList(opts.stackCaptures)
2498
+ };
2499
+ }
2500
+ async function runWatchLoop(session, command, opts, signal) {
2501
+ if (command.condition !== void 0) {
2502
+ await validateExpression(session, command.condition);
2503
+ }
2504
+ const handles = await Promise.all(
2505
+ command.breakpoints.map(
2506
+ (bp) => setBreakpoint(session, {
2507
+ file: bp.file,
2508
+ line: bp.line,
2509
+ remoteRoot: command.remoteRoot,
2510
+ ...command.condition === void 0 ? {} : { condition: command.condition },
2511
+ ...command.hitCount === void 0 ? {} : { hitCount: command.hitCount }
2512
+ })
2513
+ )
2514
+ );
2515
+ warnOnUnboundBreakpoints(handles);
2516
+ const deadline = computeDeadline(command.durationMs);
2517
+ let emitted = 0;
2518
+ const state = { stopped: false, reason: "signal" };
2519
+ const setStop = (reason) => {
2520
+ if (state.stopped) {
2521
+ return;
2522
+ }
2523
+ state.reason = reason;
2524
+ state.stopped = true;
2525
+ };
2526
+ const transportClosed = waitForTransportClose(session);
2527
+ transportClosed.promise.then(() => {
2528
+ setStop("transport-closed");
2529
+ }).catch(() => {
2530
+ });
2531
+ try {
2532
+ while (!state.stopped) {
2533
+ if (signal.aborted) {
2534
+ setStop("signal");
2535
+ break;
2536
+ }
2537
+ const remainingMs = remainingForLoop(deadline, command.perHitTimeoutMs);
2538
+ if (remainingMs <= 0) {
2539
+ setStop("duration");
2540
+ break;
2541
+ }
2542
+ const pause = await waitForNextWatchPause(session, handles, remainingMs, signal);
2543
+ if (pause === "signal") {
2544
+ setStop("signal");
2545
+ break;
2546
+ }
2547
+ if (pause === "timeout") {
2548
+ if (deadline !== void 0 && Date.now() >= deadline) {
2549
+ setStop("duration");
2550
+ break;
2551
+ }
2552
+ continue;
2553
+ }
2554
+ const event = await captureWatchEvent(session, command, pause, emitted + 1, opts);
2555
+ emitted += 1;
2556
+ writeWatchEvent(event, opts.json);
2557
+ try {
2558
+ await resume(session);
2559
+ } catch {
2560
+ process9.stderr.write("[cf-inspector] warning: Debugger.resume failed during watch.\n");
2561
+ setStop("transport-closed");
2562
+ break;
2563
+ }
2564
+ if (command.maxEvents !== void 0 && emitted >= command.maxEvents) {
2565
+ setStop("max-events");
2566
+ break;
2567
+ }
2568
+ }
2569
+ } finally {
2570
+ transportClosed.cancel();
2571
+ }
2572
+ return { emitted, stoppedReason: state.reason };
2573
+ }
2574
+ function computeDeadline(durationMs) {
2575
+ if (durationMs === void 0) {
2576
+ return void 0;
2577
+ }
2578
+ return Date.now() + durationMs;
2579
+ }
2580
+ function remainingForLoop(deadline, perHitTimeoutMs) {
2581
+ if (deadline === void 0) {
2582
+ return perHitTimeoutMs;
2583
+ }
2584
+ const remaining = deadline - Date.now();
2585
+ if (remaining <= 0) {
2586
+ return 0;
2587
+ }
2588
+ return Math.min(remaining, perHitTimeoutMs);
2589
+ }
2590
+ function waitForTransportClose(session) {
2591
+ let cancelled = false;
2592
+ let resolve;
2593
+ const promise = new Promise((res) => {
2594
+ resolve = res;
2595
+ });
2596
+ const off = session.client.onClose(() => {
2597
+ if (!cancelled) {
2598
+ resolve?.();
2599
+ }
2600
+ });
2601
+ return {
2602
+ promise,
2603
+ cancel: () => {
2604
+ cancelled = true;
2605
+ off();
2606
+ resolve?.();
2607
+ }
2608
+ };
2609
+ }
2610
+ async function waitForNextWatchPause(session, handles, timeoutMs, signal) {
2611
+ if (signal.aborted) {
2612
+ return "signal";
2613
+ }
2614
+ try {
2615
+ return await waitForPause(session, {
2616
+ timeoutMs,
2617
+ breakpointIds: handles.map((h) => h.breakpointId),
2618
+ unmatchedPausePolicy: "wait-for-resume"
2619
+ });
2620
+ } catch (err) {
2621
+ if (err instanceof CfInspectorError) {
2622
+ if (err.code === "BREAKPOINT_NOT_HIT") {
2623
+ return "timeout";
2624
+ }
2625
+ if (err.code === "UNRELATED_PAUSE_TIMEOUT") {
2626
+ return "timeout";
2627
+ }
2628
+ }
2629
+ throw err;
2630
+ }
2631
+ }
2632
+ async function captureWatchEvent(session, command, pause, hit, opts) {
2633
+ const snapshot = await captureSnapshot(session, pause, {
2634
+ captures: command.captures,
2635
+ includeScopes: opts.includeScopes === true,
2636
+ ...command.maxValueLength === void 0 ? {} : { maxValueLength: command.maxValueLength },
2637
+ ...command.stackDepth === void 0 ? {} : { stackDepth: command.stackDepth },
2638
+ stackCaptures: command.stackCaptures
2639
+ });
2640
+ const at = formatLocation(command, snapshot.topFrame);
2641
+ const base = {
2642
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2643
+ at,
2644
+ hit,
2645
+ reason: snapshot.reason,
2646
+ hitBreakpoints: snapshot.hitBreakpoints,
2647
+ captures: snapshot.captures
2648
+ };
2649
+ const withFrame = snapshot.topFrame === void 0 ? base : { ...base, topFrame: snapshot.topFrame };
2650
+ const withStack = snapshot.stack === void 0 ? withFrame : { ...withFrame, stack: snapshot.stack };
2651
+ return snapshot.exception === void 0 ? withStack : { ...withStack, exception: snapshot.exception };
2652
+ }
2653
+ function formatLocation(command, topFrame) {
2654
+ if (topFrame?.url !== void 0) {
2655
+ return `${topFrame.url}:${topFrame.line.toString()}`;
2656
+ }
2657
+ const first = command.breakpoints[0];
2658
+ if (first === void 0) {
2659
+ return "(unknown)";
2660
+ }
2661
+ return `${first.file}:${first.line.toString()}`;
2662
+ }
2663
+ function writeWatchSummary(reason, emitted, json) {
2664
+ if (json) {
2665
+ process9.stderr.write(`${JSON.stringify({ stopped: reason, emitted })}
2666
+ `);
2667
+ return;
2668
+ }
2669
+ process9.stderr.write(
2670
+ `Stopped (${reason}); emitted ${emitted.toString()} watch ${emitted === 1 ? "event" : "events"}.
2671
+ `
2672
+ );
2673
+ }
2674
+
2675
+ // src/cli/program.ts
2676
+ function applyTargetOptions(cmd) {
2677
+ return cmd.option("--port <number>", "Local port the inspector or tunnel listens on").option("--host <host>", "Hostname (default: 127.0.0.1)", "127.0.0.1").option("--region <key>", "CF region key (e.g. eu10)").option("--org <name>", "CF org name").option("--space <name>", "CF space name").option("--app <name>", "CF app name").option("--cf-timeout <seconds>", "Timeout for CF tunnel readiness in seconds");
2678
+ }
2679
+ var collectStrings = (value, prev = []) => [
2680
+ ...prev,
2681
+ value
2682
+ ];
2683
+ async function main(argv) {
2684
+ const program = new Command();
2685
+ program.name("cf-inspector").description("Drive a Node.js inspector from the command line \u2014 set breakpoints, capture snapshots, evaluate expressions");
2686
+ registerSnapshot(program);
2687
+ registerLog(program);
2688
+ registerWatch(program);
2689
+ registerException(program);
2690
+ registerEval(program);
2691
+ registerListScripts(program);
2692
+ registerAttach(program);
2693
+ await program.parseAsync([...argv]);
2694
+ }
2695
+ function registerSnapshot(program) {
2696
+ applyTargetOptions(
2697
+ program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume")
2698
+ ).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("--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) => {
2699
+ await handleSnapshot(opts);
2700
+ });
2701
+ }
2702
+ function registerLog(program) {
2703
+ applyTargetOptions(
2704
+ program.command("log").description("Stream a non-pausing logpoint: log an expression each time a line executes")
2705
+ ).requiredOption("--at <file:line>", "Logpoint location, e.g. src/handler.ts:42").requiredOption("--expr <expression>", "JavaScript expression to log on each hit").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 log events").option("--hit-count <n>", "Start logging once the line has been hit N or more times").option("--condition <expr>", "Only log when this JS expression evaluates truthy on the inspectee").option("--no-json", "Print human-readable lines instead of JSON Lines").action(async (opts) => {
2706
+ await handleLog(opts);
2707
+ });
2708
+ }
2709
+ function registerWatch(program) {
2710
+ applyTargetOptions(
2711
+ program.command("watch").description("Stream a snapshot per breakpoint hit (multi-shot watch); resume between hits")
2712
+ ).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) => {
2713
+ await handleWatch(opts);
2714
+ });
2715
+ }
2716
+ function registerException(program) {
2717
+ applyTargetOptions(
2718
+ program.command("exception").description("Pause on a thrown exception, capture the value and frame, then resume")
2719
+ ).option("--type <state>", "Pause type: uncaught (default), caught, or all").option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--timeout <seconds>", "How long to wait for an exception (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 when capturing (default: 1)").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("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--no-json", "Print a human-readable summary instead of JSON").action(async (opts) => {
2720
+ await handleException(opts);
2032
2721
  });
2033
2722
  }
2034
2723
  function registerEval(program) {
@@ -2056,20 +2745,20 @@ function registerAttach(program) {
2056
2745
  // src/cli.ts
2057
2746
  init_types();
2058
2747
  try {
2059
- await main(process8.argv);
2748
+ await main(process10.argv);
2060
2749
  } catch (err) {
2061
2750
  if (err instanceof CfInspectorError) {
2062
- process8.stderr.write(`Error [${err.code}]: ${err.message}
2751
+ process10.stderr.write(`Error [${err.code}]: ${err.message}
2063
2752
  `);
2064
2753
  if (err.detail !== void 0) {
2065
- process8.stderr.write(` detail: ${err.detail}
2754
+ process10.stderr.write(` detail: ${err.detail}
2066
2755
  `);
2067
2756
  }
2068
- process8.exit(1);
2757
+ process10.exit(1);
2069
2758
  }
2070
2759
  const message = err instanceof Error ? err.message : String(err);
2071
- process8.stderr.write(`Error: ${message}
2760
+ process10.stderr.write(`Error: ${message}
2072
2761
  `);
2073
- process8.exit(1);
2762
+ process10.exit(1);
2074
2763
  }
2075
2764
  //# sourceMappingURL=cli.js.map