@saptools/cf-inspector 0.6.2 → 0.7.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
@@ -152,7 +152,7 @@ var init_wsTransport = __esm({
152
152
  });
153
153
 
154
154
  // src/cli.ts
155
- import process11 from "process";
155
+ import process12 from "process";
156
156
 
157
157
  // src/cli/program.ts
158
158
  import { readFileSync } from "fs";
@@ -334,6 +334,56 @@ async function fetchInspectorVersion(host, port, timeoutMs) {
334
334
  }
335
335
  return { browser, protocolVersion };
336
336
  }
337
+ function startInspectorKeepalive(host, port, options = {}) {
338
+ const intervalMs = options.intervalMs ?? 1e4;
339
+ const probeTimeoutMs = options.probeTimeoutMs ?? 2e3;
340
+ const failureThreshold = options.failureThreshold ?? 3;
341
+ const probe = options.probe ?? (async () => await fetchInspectorVersion(host, port, probeTimeoutMs));
342
+ let cancelled = false;
343
+ let consecutiveFailures = 0;
344
+ let timer;
345
+ let rejectFailure;
346
+ const failure = new Promise((_resolve, reject) => {
347
+ rejectFailure = reject;
348
+ });
349
+ const schedule = () => {
350
+ if (cancelled) {
351
+ return;
352
+ }
353
+ timer = setTimeout(() => {
354
+ void runProbe();
355
+ }, intervalMs);
356
+ };
357
+ const runProbe = async () => {
358
+ try {
359
+ await probe();
360
+ consecutiveFailures = 0;
361
+ } catch (error) {
362
+ consecutiveFailures += 1;
363
+ if (consecutiveFailures >= failureThreshold) {
364
+ cancelled = true;
365
+ const detail = error instanceof Error ? error.message : String(error);
366
+ rejectFailure?.(new CfInspectorError(
367
+ "INSPECTOR_CONNECTION_FAILED",
368
+ `Inspector tunnel ${host}:${port.toString()} failed ${failureThreshold.toString()} consecutive keepalive probes and is no longer round-tripping. Retry the command after restarting or investigating the owning tunnel session.`,
369
+ detail
370
+ ));
371
+ return;
372
+ }
373
+ }
374
+ schedule();
375
+ };
376
+ schedule();
377
+ return {
378
+ failure,
379
+ cancel: () => {
380
+ cancelled = true;
381
+ if (timer !== void 0) {
382
+ clearTimeout(timer);
383
+ }
384
+ }
385
+ };
386
+ }
337
387
 
338
388
  // src/cli/output.ts
339
389
  import process from "process";
@@ -351,6 +401,7 @@ function writeHumanSnapshot(snapshot) {
351
401
  lines.push(
352
402
  `Snapshot @ ${snapshot.capturedAt}`,
353
403
  ` reason: ${snapshot.reason}`,
404
+ ` isolate: ${formatIsolate(snapshot.isolate)}`,
354
405
  ` paused: ${pausedDuration}`
355
406
  );
356
407
  if (snapshot.exception !== void 0) {
@@ -419,12 +470,13 @@ function writeLogEvent(event, json) {
419
470
  `);
420
471
  return;
421
472
  }
473
+ const isolateSuffix = event.isolate === void 0 ? "" : ` (${formatIsolate(event.isolate)})`;
422
474
  if (event.error !== void 0) {
423
- process.stdout.write(`[${event.ts}] ${event.at} !err ${renderTruncated(event.error, event)}
475
+ process.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} !err ${renderTruncated(event.error, event)}
424
476
  `);
425
477
  return;
426
478
  }
427
- process.stdout.write(`[${event.ts}] ${event.at} ${renderTruncated(event.value ?? "", event)}
479
+ process.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} ${renderTruncated(event.value ?? "", event)}
428
480
  `);
429
481
  }
430
482
  function writeWatchEvent(event, json) {
@@ -433,8 +485,10 @@ function writeWatchEvent(event, json) {
433
485
  `);
434
486
  return;
435
487
  }
436
- process.stdout.write(`[${event.ts}] hit#${event.hit.toString()} ${event.at}
437
- `);
488
+ process.stdout.write(
489
+ `[${event.ts}] hit#${event.hit.toString()} ${event.at} (${formatIsolate(event.isolate)})
490
+ `
491
+ );
438
492
  if (event.exception !== void 0) {
439
493
  process.stdout.write(` exception: ${renderExceptionDetail(event.exception)}
440
494
  `);
@@ -445,6 +499,12 @@ function writeWatchEvent(event, json) {
445
499
  `);
446
500
  }
447
501
  }
502
+ function formatIsolate(isolate) {
503
+ if (isolate?.kind === "worker") {
504
+ return `worker ${isolate.workerId}`;
505
+ }
506
+ return "main";
507
+ }
448
508
  function renderExceptionDetail(exception) {
449
509
  if (exception.description !== void 0) {
450
510
  const originalLength = exception.descriptionOriginalLength;
@@ -486,6 +546,8 @@ import "@saptools/cf-debugger";
486
546
 
487
547
  // src/cf/tunnel.ts
488
548
  import { startDebugger } from "@saptools/cf-debugger";
549
+ init_types();
550
+ var REUSED_TUNNEL_GRACE_MS = 5e3;
489
551
  function targetOptions(target) {
490
552
  return {
491
553
  ...target.apiEndpoint === void 0 ? {} : { apiEndpoint: target.apiEndpoint },
@@ -547,6 +609,16 @@ async function openCfTunnel(target) {
547
609
  if (localPort === void 0) {
548
610
  throw error;
549
611
  }
612
+ try {
613
+ await fetchInspectorVersion("127.0.0.1", localPort, REUSED_TUNNEL_GRACE_MS);
614
+ } catch (livenessError) {
615
+ const detail = livenessError instanceof Error ? livenessError.message : String(livenessError);
616
+ throw new CfInspectorError(
617
+ "INSPECTOR_DISCOVERY_FAILED",
618
+ `Another debugger session claims tunnel port ${localPort.toString()}, but its Node inspector did not respond within ${REUSED_TUNNEL_GRACE_MS.toString()}ms. The tunnel may be stale or still finishing setup; retry shortly, or inspect and stop the owning cf-debugger session before opening a fresh tunnel.`,
619
+ detail
620
+ );
621
+ }
550
622
  target.onStatus?.("ready", `Reusing existing tunnel on port ${localPort.toString()}`);
551
623
  return { localPort, dispose: () => Promise.resolve() };
552
624
  }
@@ -983,6 +1055,19 @@ function toResolvedLocations(value) {
983
1055
  return [url === void 0 ? location : { ...location, url }];
984
1056
  });
985
1057
  }
1058
+ function toBreakLocations(value) {
1059
+ if (!Array.isArray(value)) {
1060
+ return [];
1061
+ }
1062
+ return value.flatMap((entry) => {
1063
+ const location = toScriptLocation(entry);
1064
+ if (location === void 0 || !isRecord(entry)) {
1065
+ return [];
1066
+ }
1067
+ const type = nonEmptyString(entry["type"]);
1068
+ return [type === void 0 ? location : { ...location, type }];
1069
+ });
1070
+ }
986
1071
  function remoteCompleteness(subtype) {
987
1072
  if (subtype === "proxy") {
988
1073
  return "unavailable";
@@ -1207,6 +1292,7 @@ function pauseDetail(pause) {
1207
1292
  var DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
1208
1293
  var DEFAULT_HOST = "127.0.0.1";
1209
1294
  var PAUSE_BUFFER_LIMIT = 32;
1295
+ var WORKER_DISCOVERY_SETTLE_MS = 500;
1210
1296
  var NodeWorkerDiscovery = class {
1211
1297
  constructor(client) {
1212
1298
  this.client = client;
@@ -1215,12 +1301,18 @@ var NodeWorkerDiscovery = class {
1215
1301
  const worker = toInspectorWorkerTarget(raw);
1216
1302
  if (worker !== void 0) {
1217
1303
  this.workers.set(worker.sessionId, worker);
1304
+ for (const listener of this.attachedListeners) {
1305
+ listener(worker);
1306
+ }
1218
1307
  }
1219
1308
  }),
1220
1309
  client.on("NodeWorker.detachedFromWorker", (raw) => {
1221
1310
  const sessionId = readField(raw, "sessionId");
1222
1311
  if (typeof sessionId === "string") {
1223
1312
  this.workers.delete(sessionId);
1313
+ for (const listener of this.detachedListeners) {
1314
+ listener(sessionId);
1315
+ }
1224
1316
  }
1225
1317
  })
1226
1318
  ];
@@ -1230,6 +1322,8 @@ var NodeWorkerDiscovery = class {
1230
1322
  detachListeners;
1231
1323
  supported = false;
1232
1324
  disposed = false;
1325
+ attachedListeners = /* @__PURE__ */ new Set();
1326
+ detachedListeners = /* @__PURE__ */ new Set();
1233
1327
  async enable() {
1234
1328
  try {
1235
1329
  await this.client.send("NodeWorker.enable", { waitForDebuggerOnStart: false });
@@ -1243,6 +1337,45 @@ var NodeWorkerDiscovery = class {
1243
1337
  list() {
1244
1338
  return [...this.workers.values()].sort(compareWorkers);
1245
1339
  }
1340
+ onAttached(listener) {
1341
+ this.attachedListeners.add(listener);
1342
+ return () => {
1343
+ this.attachedListeners.delete(listener);
1344
+ };
1345
+ }
1346
+ onDetached(listener) {
1347
+ this.detachedListeners.add(listener);
1348
+ return () => {
1349
+ this.detachedListeners.delete(listener);
1350
+ };
1351
+ }
1352
+ async waitFor(predicate, timeoutMs = WORKER_DISCOVERY_SETTLE_MS) {
1353
+ const existing = predicate(this.list());
1354
+ if (existing !== void 0) {
1355
+ return existing;
1356
+ }
1357
+ return await new Promise((resolve) => {
1358
+ let settled = false;
1359
+ const finish = (worker) => {
1360
+ if (settled) {
1361
+ return;
1362
+ }
1363
+ settled = true;
1364
+ clearTimeout(timer);
1365
+ detach();
1366
+ resolve(worker);
1367
+ };
1368
+ const detach = this.onAttached(() => {
1369
+ const worker = predicate(this.list());
1370
+ if (worker !== void 0) {
1371
+ finish(worker);
1372
+ }
1373
+ });
1374
+ const timer = setTimeout(() => {
1375
+ finish();
1376
+ }, timeoutMs);
1377
+ });
1378
+ }
1246
1379
  async dispose() {
1247
1380
  if (this.disposed) {
1248
1381
  return;
@@ -1257,6 +1390,8 @@ var NodeWorkerDiscovery = class {
1257
1390
  for (const detach of this.detachListeners) {
1258
1391
  detach();
1259
1392
  }
1393
+ this.attachedListeners.clear();
1394
+ this.detachedListeners.clear();
1260
1395
  }
1261
1396
  };
1262
1397
  function isUnsupportedNodeWorkerDomain(error) {
@@ -1321,14 +1456,15 @@ async function connectInspector(options) {
1321
1456
  let workerDiscovery;
1322
1457
  try {
1323
1458
  workerDiscovery = await startNodeWorkerDiscovery(client);
1324
- if (options.workerIndex === void 0) {
1325
- const session = await initSession(client, target);
1459
+ if (options.workerIndex === void 0 && options.workerId === void 0) {
1460
+ const session = await initSession(client, target, { kind: "main" });
1326
1461
  return withWorkerMetadata(session, workerDiscovery, targetIndex, targets.length);
1327
1462
  }
1328
1463
  return await initWorkerSession(
1329
1464
  client,
1330
1465
  workerDiscovery,
1331
1466
  options.workerIndex,
1467
+ options.workerId,
1332
1468
  targetIndex,
1333
1469
  targets.length
1334
1470
  );
@@ -1338,25 +1474,207 @@ async function connectInspector(options) {
1338
1474
  throw err;
1339
1475
  }
1340
1476
  }
1341
- async function initWorkerSession(parent, discovery, workerIndex, targetIndex, targetCount) {
1342
- const workers = discovery.list();
1477
+ async function initWorkerSession(parent, discovery, workerIndex, workerId, targetIndex, targetCount) {
1343
1478
  if (!discovery.supported) {
1344
1479
  throw new CfInspectorError(
1345
1480
  "INSPECTOR_DISCOVERY_FAILED",
1346
1481
  "This runtime does not expose the NodeWorker CDP domain; --worker cannot be used. Run list-targets for available raw targets."
1347
1482
  );
1348
1483
  }
1349
- const worker = workers[workerIndex];
1484
+ const worker = await discovery.waitFor((workers) => workerId === void 0 ? workerIndex === void 0 ? void 0 : workers[workerIndex] : workers.find((candidate) => candidate.workerId === workerId));
1350
1485
  if (worker === void 0) {
1486
+ const workers = discovery.list();
1487
+ const selector = workerId === void 0 ? `index ${(workerIndex ?? 0).toString()}` : `workerId ${JSON.stringify(workerId)}`;
1351
1488
  throw new CfInspectorError(
1352
1489
  "INSPECTOR_DISCOVERY_FAILED",
1353
- `No NodeWorker sub-session at index ${workerIndex.toString()} (available: ${workers.length.toString()}). Ensure the worker is alive, then rerun list-targets.`
1490
+ `No NodeWorker sub-session with ${selector} is currently attached (available: ${workers.length.toString()}). Ensure the worker is alive, then rerun list-targets.`
1354
1491
  );
1355
1492
  }
1356
1493
  const client = await createNodeWorkerClient(parent, worker.sessionId);
1357
- const session = await initSession(client, workerToInspectorTarget(worker));
1494
+ const session = await initSession(client, workerToInspectorTarget(worker), {
1495
+ kind: "worker",
1496
+ workerId: worker.workerId
1497
+ });
1358
1498
  return withWorkerMetadata(session, discovery, targetIndex, targetCount, workerIndex, parent);
1359
1499
  }
1500
+ async function connectInspectorGroup(options) {
1501
+ const host = options.host ?? DEFAULT_HOST;
1502
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
1503
+ const targets = await discoverInspectorTargets(host, options.port, connectTimeoutMs);
1504
+ const targetIndex = options.targetIndex ?? 0;
1505
+ const target = targets[targetIndex];
1506
+ if (target === void 0) {
1507
+ throw new CfInspectorError(
1508
+ "INSPECTOR_DISCOVERY_FAILED",
1509
+ `No inspector target at index ${targetIndex.toString()} on ${host}:${options.port.toString()} (available: ${targets.length.toString()})`
1510
+ );
1511
+ }
1512
+ const parent = await CdpClient.connect({ url: target.webSocketDebuggerUrl, connectTimeoutMs });
1513
+ let discovery;
1514
+ try {
1515
+ discovery = await startNodeWorkerDiscovery(parent);
1516
+ const main2 = await initSession(parent, target, { kind: "main" });
1517
+ const group = new DynamicInspectorSessionGroup(
1518
+ parent,
1519
+ discovery,
1520
+ main2,
1521
+ targetIndex,
1522
+ targets.length
1523
+ );
1524
+ await group.initialize();
1525
+ return group;
1526
+ } catch (error) {
1527
+ await discovery?.dispose();
1528
+ parent.dispose();
1529
+ throw error;
1530
+ }
1531
+ }
1532
+ var DynamicInspectorSessionGroup = class {
1533
+ constructor(parent, discovery, main2, targetIndex, targetCount) {
1534
+ this.parent = parent;
1535
+ this.discovery = discovery;
1536
+ this.targetIndex = targetIndex;
1537
+ this.targetCount = targetCount;
1538
+ this.workerDiscoverySupported = discovery.supported;
1539
+ this.sessions.set("main", main2);
1540
+ this.detachDiscoveryListeners = [
1541
+ discovery.onAttached((worker) => {
1542
+ this.queueWorker(worker);
1543
+ }),
1544
+ discovery.onDetached((sessionId) => {
1545
+ void this.detachWorker(sessionId);
1546
+ })
1547
+ ];
1548
+ }
1549
+ parent;
1550
+ discovery;
1551
+ targetIndex;
1552
+ targetCount;
1553
+ sessions = /* @__PURE__ */ new Map();
1554
+ listeners = /* @__PURE__ */ new Set();
1555
+ removedListeners = /* @__PURE__ */ new Set();
1556
+ errorListeners = /* @__PURE__ */ new Set();
1557
+ pending = /* @__PURE__ */ new Set();
1558
+ detachedSessionIds = /* @__PURE__ */ new Set();
1559
+ detachDiscoveryListeners;
1560
+ initializationError;
1561
+ initializing = true;
1562
+ disposed = false;
1563
+ workerDiscoverySupported;
1564
+ async initialize() {
1565
+ for (const worker of this.discovery.list()) {
1566
+ this.queueWorker(worker);
1567
+ }
1568
+ await this.waitForPending();
1569
+ this.initializing = false;
1570
+ if (this.initializationError !== void 0) {
1571
+ throw this.initializationError;
1572
+ }
1573
+ }
1574
+ list() {
1575
+ return [...this.sessions.values()];
1576
+ }
1577
+ onSession(listener) {
1578
+ this.listeners.add(listener);
1579
+ for (const session of this.sessions.values()) {
1580
+ listener(session);
1581
+ }
1582
+ return () => {
1583
+ this.listeners.delete(listener);
1584
+ };
1585
+ }
1586
+ onSessionRemoved(listener) {
1587
+ this.removedListeners.add(listener);
1588
+ return () => {
1589
+ this.removedListeners.delete(listener);
1590
+ };
1591
+ }
1592
+ onError(listener) {
1593
+ this.errorListeners.add(listener);
1594
+ return () => {
1595
+ this.errorListeners.delete(listener);
1596
+ };
1597
+ }
1598
+ async dispose() {
1599
+ if (this.disposed) {
1600
+ return;
1601
+ }
1602
+ this.disposed = true;
1603
+ for (const detach of this.detachDiscoveryListeners) {
1604
+ detach();
1605
+ }
1606
+ await Promise.allSettled([...this.pending]);
1607
+ const sessions = [...this.list()].reverse();
1608
+ for (const session of sessions) {
1609
+ try {
1610
+ await session.dispose();
1611
+ } catch {
1612
+ }
1613
+ }
1614
+ this.sessions.clear();
1615
+ this.listeners.clear();
1616
+ this.removedListeners.clear();
1617
+ this.errorListeners.clear();
1618
+ await this.discovery.dispose();
1619
+ this.parent.dispose();
1620
+ }
1621
+ queueWorker(worker) {
1622
+ if (this.disposed || this.sessions.has(worker.sessionId)) {
1623
+ return;
1624
+ }
1625
+ const pending = this.attachWorker(worker).catch((error) => {
1626
+ const normalized = error instanceof Error ? error : new Error("Worker inspector attachment failed");
1627
+ if (this.initializing && this.initializationError === void 0) {
1628
+ this.initializationError = normalized;
1629
+ }
1630
+ for (const listener of this.errorListeners) {
1631
+ listener(normalized);
1632
+ }
1633
+ }).finally(() => {
1634
+ this.pending.delete(pending);
1635
+ });
1636
+ this.pending.add(pending);
1637
+ }
1638
+ async attachWorker(worker) {
1639
+ const client = await createNodeWorkerClient(this.parent, worker.sessionId);
1640
+ try {
1641
+ const session = await initSession(client, workerToInspectorTarget(worker), {
1642
+ kind: "worker",
1643
+ workerId: worker.workerId
1644
+ });
1645
+ if (this.disposed || this.detachedSessionIds.delete(worker.sessionId)) {
1646
+ await session.dispose();
1647
+ return;
1648
+ }
1649
+ this.sessions.set(worker.sessionId, session);
1650
+ for (const listener of this.listeners) {
1651
+ listener(session);
1652
+ }
1653
+ } catch (error) {
1654
+ client.dispose();
1655
+ if (!this.disposed) {
1656
+ throw error;
1657
+ }
1658
+ }
1659
+ }
1660
+ async detachWorker(sessionId) {
1661
+ const session = this.sessions.get(sessionId);
1662
+ if (session === void 0) {
1663
+ this.detachedSessionIds.add(sessionId);
1664
+ return;
1665
+ }
1666
+ this.sessions.delete(sessionId);
1667
+ for (const listener of this.removedListeners) {
1668
+ listener(session);
1669
+ }
1670
+ await session.dispose();
1671
+ }
1672
+ async waitForPending() {
1673
+ while (this.pending.size > 0) {
1674
+ await Promise.all([...this.pending]);
1675
+ }
1676
+ }
1677
+ };
1360
1678
  function withWorkerMetadata(session, discovery, targetIndex, targetCount, workerIndex, parent) {
1361
1679
  return {
1362
1680
  ...session,
@@ -1393,16 +1711,16 @@ async function discoverNodeWorkerTargets(target, connectTimeoutMs = DEFAULT_CONN
1393
1711
  client.dispose();
1394
1712
  }
1395
1713
  }
1396
- async function initSession(client, target) {
1714
+ async function initSession(client, target, isolate) {
1397
1715
  const scripts = /* @__PURE__ */ new Map();
1398
1716
  registerScriptTracking(client, scripts);
1399
1717
  const pauseBuffer = [];
1400
1718
  const pauseWaitGate = { active: false };
1401
- const debuggerState = {};
1719
+ const debuggerState = { paused: false };
1402
1720
  registerPauseTracking(client, scripts, pauseBuffer, pauseWaitGate, debuggerState);
1403
1721
  await client.send("Runtime.enable");
1404
1722
  await client.send("Debugger.enable");
1405
- return createSession(client, target, scripts, pauseBuffer, pauseWaitGate, debuggerState);
1723
+ return createSession(client, target, isolate, scripts, pauseBuffer, pauseWaitGate, debuggerState);
1406
1724
  }
1407
1725
  function registerScriptTracking(client, scripts) {
1408
1726
  client.on("Debugger.scriptParsed", (raw) => {
@@ -1414,23 +1732,28 @@ function registerScriptTracking(client, scripts) {
1414
1732
  }
1415
1733
  function registerPauseTracking(client, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
1416
1734
  client.on("Debugger.paused", (raw) => {
1735
+ const event = toPauseEvent(raw, performance2.now(), scripts);
1736
+ debuggerState.paused = true;
1737
+ debuggerState.currentPause = event;
1417
1738
  if (pauseWaitGate.active) {
1418
1739
  return;
1419
1740
  }
1420
- const event = toPauseEvent(raw, performance2.now(), scripts);
1421
1741
  if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
1422
1742
  pauseBuffer.shift();
1423
1743
  }
1424
1744
  pauseBuffer.push(event);
1425
1745
  });
1426
1746
  client.on("Debugger.resumed", () => {
1747
+ debuggerState.paused = false;
1748
+ delete debuggerState.currentPause;
1427
1749
  debuggerState.lastResumedAtMs = performance2.now();
1428
1750
  });
1429
1751
  }
1430
- function createSession(client, target, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
1752
+ function createSession(client, target, isolate, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
1431
1753
  return {
1432
1754
  client,
1433
1755
  target,
1756
+ isolate,
1434
1757
  scripts,
1435
1758
  pauseBuffer,
1436
1759
  pauseWaitGate,
@@ -1622,7 +1945,7 @@ function warnOnImplicitInspectorSelection(session, targetWasExplicit, workerWasE
1622
1945
  const workerCount = session.workerTargets?.length ?? 0;
1623
1946
  if (!workerWasExplicit && workerCount > 0) {
1624
1947
  process2.stderr.write(
1625
- `[cf-inspector] notice: attached to the main isolate; ${workerCount.toString()} Node ${workerCount === 1 ? "worker is" : "workers are"} available. Run list-targets and pass --worker <index> to inspect one.
1948
+ `[cf-inspector] notice: attached to the main isolate; ${workerCount.toString()} Node ${workerCount === 1 ? "worker is" : "workers are"} available. This command is single-isolate by nature; use --worker-id <id> to inspect one worker explicitly.
1626
1949
  `
1627
1950
  );
1628
1951
  }
@@ -1635,7 +1958,7 @@ function warnOnBoundBreakpointWithoutHit(handles) {
1635
1958
  return;
1636
1959
  }
1637
1960
  process2.stderr.write(
1638
- `[cf-inspector] warning: ${boundCount.toString()} breakpoint ${boundCount === 1 ? "location bound" : "locations bound"}, but no hit was observed. The code may be running in another worker isolate. Run list-targets and retry with --worker <index> for a NodeWorker sub-session or --target <index> for a raw target.
1961
+ `[cf-inspector] warning: ${boundCount.toString()} breakpoint ${boundCount === 1 ? "location bound" : "locations bound"}, but no hit was observed. No selected isolate executed the location before the command stopped. Check conditions, hit counts, request traffic, and use check-breakpoint to verify the exact line.
1639
1962
  `
1640
1963
  );
1641
1964
  }
@@ -1701,12 +2024,15 @@ function resolveTarget(opts, options = {}) {
1701
2024
  const port = parsePositiveInt(opts.port, "--port");
1702
2025
  const targetIndex = parseTargetIndex(opts.target);
1703
2026
  const workerIndex = parseSelectionIndex(opts.worker, "--worker");
2027
+ const workerId = parseWorkerId(opts.workerId);
2028
+ const mainOnly = opts.mainOnly === true;
2029
+ validateIsolateSelectors(targetIndex, workerIndex, workerId, mainOnly);
1704
2030
  if (port !== void 0) {
1705
2031
  return {
1706
2032
  kind: "port",
1707
2033
  port,
1708
2034
  host: opts.host ?? "127.0.0.1",
1709
- ...selectionOptions(targetIndex, workerIndex)
2035
+ ...selectionOptions(targetIndex, workerIndex, workerId, mainOnly)
1710
2036
  };
1711
2037
  }
1712
2038
  const region = optionalText(opts.region);
@@ -1733,7 +2059,9 @@ function resolveTarget(opts, options = {}) {
1733
2059
  app,
1734
2060
  parseTunnelTimeout(opts, options),
1735
2061
  targetIndex,
1736
- workerIndex
2062
+ workerIndex,
2063
+ workerId,
2064
+ mainOnly
1737
2065
  );
1738
2066
  }
1739
2067
  async function resolveTargetWithCurrentCfTarget(opts, options = {}) {
@@ -1764,13 +2092,15 @@ function parseSelectionIndex(raw, label) {
1764
2092
  function targetIndexOption(targetIndex) {
1765
2093
  return targetIndex === void 0 ? {} : { targetIndex };
1766
2094
  }
1767
- function selectionOptions(targetIndex, workerIndex) {
2095
+ function selectionOptions(targetIndex, workerIndex, workerId, mainOnly) {
1768
2096
  return {
1769
2097
  ...targetIndexOption(targetIndex),
1770
- ...workerIndex === void 0 ? {} : { workerIndex }
2098
+ ...workerIndex === void 0 ? {} : { workerIndex },
2099
+ ...workerId === void 0 ? {} : { workerId },
2100
+ ...mainOnly === true ? { mainOnly: true } : {}
1771
2101
  };
1772
2102
  }
1773
- function buildCfTarget(region, apiEndpoint, org, space, app, tunnelTimeoutSec, targetIndex, workerIndex) {
2103
+ function buildCfTarget(region, apiEndpoint, org, space, app, tunnelTimeoutSec, targetIndex, workerIndex, workerId, mainOnly) {
1774
2104
  return {
1775
2105
  kind: "cf",
1776
2106
  region,
@@ -1779,13 +2109,38 @@ function buildCfTarget(region, apiEndpoint, org, space, app, tunnelTimeoutSec, t
1779
2109
  space,
1780
2110
  app,
1781
2111
  tunnelTimeoutMs: tunnelTimeoutSec * 1e3,
1782
- ...selectionOptions(targetIndex, workerIndex)
2112
+ ...selectionOptions(targetIndex, workerIndex, workerId, mainOnly)
1783
2113
  };
1784
2114
  }
2115
+ function validateIsolateSelectors(targetIndex, workerIndex, workerId, mainOnly) {
2116
+ const workerSelectors = Number(workerIndex !== void 0) + Number(workerId !== void 0);
2117
+ if (workerSelectors > 1 || mainOnly && workerSelectors > 0) {
2118
+ throw new CfInspectorError(
2119
+ "INVALID_ARGUMENT",
2120
+ "Use only one of --worker, --worker-id, or --main-only."
2121
+ );
2122
+ }
2123
+ if (targetIndex !== void 0 && workerSelectors === 0 && !mainOnly) {
2124
+ return;
2125
+ }
2126
+ }
1785
2127
  function optionalText(value) {
1786
2128
  const trimmed = value?.trim();
1787
2129
  return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
1788
2130
  }
2131
+ function parseWorkerId(value) {
2132
+ if (value === void 0) {
2133
+ return void 0;
2134
+ }
2135
+ const trimmed = value.trim();
2136
+ if (trimmed.length === 0) {
2137
+ throw new CfInspectorError(
2138
+ "INVALID_ARGUMENT",
2139
+ "Invalid --worker-id: expected a non-empty workerId from list-targets"
2140
+ );
2141
+ }
2142
+ return trimmed;
2143
+ }
1789
2144
  async function withSession(target, fn, reportProgress, signal) {
1790
2145
  const tunnel = await openTarget(target, reportProgress, signal);
1791
2146
  let session;
@@ -1796,12 +2151,13 @@ async function withSession(target, fn, reportProgress, signal) {
1796
2151
  session = await connectInspector({
1797
2152
  port: tunnel.port,
1798
2153
  host: tunnel.host,
1799
- ...selectionOptions(target.targetIndex, target.workerIndex)
2154
+ ...selectionOptions(target.targetIndex, target.workerIndex),
2155
+ ...target.workerId === void 0 ? {} : { workerId: target.workerId }
1800
2156
  });
1801
2157
  warnOnImplicitInspectorSelection(
1802
2158
  session,
1803
2159
  target.targetIndex !== void 0,
1804
- target.workerIndex !== void 0
2160
+ target.workerIndex !== void 0 || target.workerId !== void 0
1805
2161
  );
1806
2162
  reportProgress?.("Inspector session is ready.");
1807
2163
  return await fn(session, tunnel.port);
@@ -1814,6 +2170,56 @@ async function withSession(target, fn, reportProgress, signal) {
1814
2170
  await tunnel.dispose();
1815
2171
  }
1816
2172
  }
2173
+ async function withSessions(target, fn, reportProgress, signal) {
2174
+ const tunnel = await openTarget(target, reportProgress, signal);
2175
+ let group;
2176
+ try {
2177
+ reportProgress?.(
2178
+ `Connecting to the Node.js inspector at ${tunnel.host}:${tunnel.port.toString()}...`
2179
+ );
2180
+ const autoAttach = target.targetIndex === void 0 && target.workerIndex === void 0 && target.workerId === void 0 && target.mainOnly !== true;
2181
+ if (autoAttach) {
2182
+ group = await connectInspectorGroup({ port: tunnel.port, host: tunnel.host });
2183
+ } else {
2184
+ const session = await connectInspector({
2185
+ port: tunnel.port,
2186
+ host: tunnel.host,
2187
+ ...selectionOptions(target.targetIndex, target.workerIndex, target.workerId)
2188
+ });
2189
+ group = singleSessionGroup(session);
2190
+ }
2191
+ reportProgress?.("Inspector session is ready.");
2192
+ return await fn(group, tunnel.port);
2193
+ } finally {
2194
+ try {
2195
+ if (group !== void 0) {
2196
+ const sessionCount = group.list().length;
2197
+ reportProgress?.(sessionCount === 1 ? "Closing the inspector session..." : `Closing ${sessionCount.toString()} inspector sessions...`);
2198
+ await group.dispose();
2199
+ reportProgress?.(sessionCount === 1 ? "Inspector session closed." : "Inspector sessions closed.");
2200
+ }
2201
+ } finally {
2202
+ await tunnel.dispose();
2203
+ }
2204
+ }
2205
+ }
2206
+ function singleSessionGroup(session) {
2207
+ return {
2208
+ targetIndex: session.targetIndex ?? 0,
2209
+ targetCount: session.targetCount ?? 1,
2210
+ workerDiscoverySupported: session.workerDiscoverySupported ?? false,
2211
+ list: () => [session],
2212
+ onSession: (listener) => {
2213
+ listener(session);
2214
+ return () => void 0;
2215
+ },
2216
+ onSessionRemoved: () => () => void 0,
2217
+ onError: () => () => void 0,
2218
+ dispose: async () => {
2219
+ await session.dispose();
2220
+ }
2221
+ };
2222
+ }
1817
2223
  async function openTarget(target, reportProgress, signal) {
1818
2224
  if (target.kind === "port") {
1819
2225
  return {
@@ -1867,185 +2273,55 @@ async function handleAttach(opts) {
1867
2273
  }
1868
2274
  }
1869
2275
 
1870
- // src/cli/commands/eval.ts
2276
+ // src/cli/commands/checkBreakpoint.ts
1871
2277
  import process4 from "process";
1872
2278
 
1873
- // src/inspector/runtime.ts
2279
+ // src/pathMapper.ts
1874
2280
  init_types();
1875
- async function resume(session) {
1876
- await session.client.send("Debugger.resume");
2281
+ var REGEX_PREFIX = "regex:";
2282
+ var REGEX_FLAGS_PATTERN = /^[dgimsuvy]*$/;
2283
+ var TS_JS_EXT_PATTERN = /\.(?:ts|js|mts|mjs|cts|cjs)$/i;
2284
+ function parseBreakpointSpec(input) {
2285
+ const idx = input.lastIndexOf(":");
2286
+ if (idx <= 0 || idx === input.length - 1) {
2287
+ throw new CfInspectorError(
2288
+ "INVALID_BREAKPOINT",
2289
+ `Breakpoint must be in 'file:line' form, received: "${input}"`
2290
+ );
2291
+ }
2292
+ const file = input.slice(0, idx).trim();
2293
+ const lineRaw = input.slice(idx + 1).trim();
2294
+ const line = Number.parseInt(lineRaw, 10);
2295
+ if (!Number.isInteger(line) || line <= 0 || line.toString() !== lineRaw) {
2296
+ throw new CfInspectorError(
2297
+ "INVALID_BREAKPOINT",
2298
+ `Breakpoint line must be a positive integer, received: "${lineRaw}"`
2299
+ );
2300
+ }
2301
+ if (file.length === 0) {
2302
+ throw new CfInspectorError(
2303
+ "INVALID_BREAKPOINT",
2304
+ `Breakpoint file path is empty in "${input}"`
2305
+ );
2306
+ }
2307
+ return { file, line };
1877
2308
  }
1878
- async function setPauseOnExceptions(session, state) {
1879
- await session.client.send("Debugger.setPauseOnExceptions", { state });
2309
+ function parseRemoteRoot(value) {
2310
+ const trimmed = value?.trim();
2311
+ if (trimmed === void 0 || trimmed.length === 0) {
2312
+ return { kind: "none" };
2313
+ }
2314
+ if (trimmed.startsWith(REGEX_PREFIX)) {
2315
+ return toRegex(trimmed.slice(REGEX_PREFIX.length), "");
2316
+ }
2317
+ const slashRegex = parseSlashDelimited(trimmed);
2318
+ if (slashRegex !== void 0) {
2319
+ return toRegex(slashRegex.pattern, slashRegex.flags);
2320
+ }
2321
+ return { kind: "literal", value: stripTrailingSlash(trimmed) };
1880
2322
  }
1881
- async function evaluateOnFrame(session, callFrameId, expression, options = {}) {
1882
- return await session.client.send("Debugger.evaluateOnCallFrame", {
1883
- callFrameId,
1884
- expression,
1885
- returnByValue: false,
1886
- generatePreview: true,
1887
- silent: true,
1888
- ...options.throwOnSideEffect === void 0 ? {} : { throwOnSideEffect: options.throwOnSideEffect },
1889
- ...options.objectGroup === void 0 ? {} : { objectGroup: options.objectGroup }
1890
- });
1891
- }
1892
- function isSideEffectRefusal(result) {
1893
- const classNames = [
1894
- result.result?.className,
1895
- result.exceptionDetails?.exception?.className
1896
- ];
1897
- const descriptions = [
1898
- result.result?.description,
1899
- result.exceptionDetails?.exception?.description
1900
- ];
1901
- const isEvalError = classNames.includes("EvalError");
1902
- return isEvalError && descriptions.some(
1903
- (description) => typeof description === "string" && description.toLowerCase().includes("possible side-effect in debug-evaluate")
1904
- );
1905
- }
1906
- async function evaluateGlobal(session, expression) {
1907
- return await session.client.send("Runtime.evaluate", {
1908
- expression,
1909
- returnByValue: false,
1910
- generatePreview: true,
1911
- silent: true
1912
- });
1913
- }
1914
- async function runSetupEvals(session, expressions) {
1915
- for (const expression of expressions) {
1916
- const result = await evaluateGlobal(session, expression);
1917
- if (result.exceptionDetails !== void 0) {
1918
- throw new CfInspectorError(
1919
- "SETUP_EVAL_FAILED",
1920
- exceptionDetailsMessage(result, "setup evaluation failed")
1921
- );
1922
- }
1923
- }
1924
- }
1925
- function exceptionDetailsMessage(result, fallback) {
1926
- return typeof result.exceptionDetails?.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails?.text === "string" ? result.exceptionDetails.text : fallback;
1927
- }
1928
- function listScripts(session) {
1929
- return [...session.scripts.values()];
1930
- }
1931
- async function validateExpression(session, expression) {
1932
- const result = await session.client.send("Runtime.compileScript", {
1933
- expression,
1934
- sourceURL: "<cf-inspector-validate>",
1935
- persistScript: false
1936
- });
1937
- if (result.exceptionDetails === void 0) {
1938
- return;
1939
- }
1940
- const description = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "expression failed to compile";
1941
- throw new CfInspectorError("INVALID_EXPRESSION", description);
1942
- }
1943
- async function getProperties(session, objectId) {
1944
- const result = await session.client.send("Runtime.getProperties", {
1945
- objectId,
1946
- ownProperties: true,
1947
- accessorPropertiesOnly: false,
1948
- generatePreview: true
1949
- });
1950
- if (!Array.isArray(result.result)) {
1951
- return [];
1952
- }
1953
- return result.result;
1954
- }
1955
-
1956
- // src/cli/commands/eval.ts
1957
- async function handleEval(opts) {
1958
- warnOnMutationRisk(opts.expr, "eval --expr");
1959
- const target = await resolveTargetWithCurrentCfTarget(opts);
1960
- const result = await withSession(target, async (session) => {
1961
- return await evaluateGlobal(session, opts.expr);
1962
- });
1963
- if (opts.json) {
1964
- writeJson(result);
1965
- if (result.exceptionDetails !== void 0) {
1966
- process4.exitCode = 1;
1967
- }
1968
- return;
1969
- }
1970
- writeHumanEvalResult(result);
1971
- }
1972
- function writeHumanEvalResult(result) {
1973
- if (result.exceptionDetails !== void 0) {
1974
- const detail = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
1975
- process4.stderr.write(`${detail}
1976
- `);
1977
- process4.exitCode = 1;
1978
- return;
1979
- }
1980
- const inner = result.result;
1981
- if (inner === void 0) {
1982
- process4.stdout.write("\n");
1983
- return;
1984
- }
1985
- if (typeof inner.value === "string") {
1986
- process4.stdout.write(`${inner.value}
1987
- `);
1988
- return;
1989
- }
1990
- if (typeof inner.description === "string") {
1991
- process4.stdout.write(`${inner.description}
1992
- `);
1993
- return;
1994
- }
1995
- process4.stdout.write(`${JSON.stringify(inner.value)}
1996
- `);
1997
- }
1998
-
1999
- // src/cli/commands/exception.ts
2000
- import { performance as performance4 } from "perf_hooks";
2001
- import process5 from "process";
2002
-
2003
- // src/pathMapper.ts
2004
- init_types();
2005
- var REGEX_PREFIX = "regex:";
2006
- var REGEX_FLAGS_PATTERN = /^[dgimsuvy]*$/;
2007
- var TS_JS_EXT_PATTERN = /\.(?:ts|js|mts|mjs|cts|cjs)$/i;
2008
- function parseBreakpointSpec(input) {
2009
- const idx = input.lastIndexOf(":");
2010
- if (idx <= 0 || idx === input.length - 1) {
2011
- throw new CfInspectorError(
2012
- "INVALID_BREAKPOINT",
2013
- `Breakpoint must be in 'file:line' form, received: "${input}"`
2014
- );
2015
- }
2016
- const file = input.slice(0, idx).trim();
2017
- const lineRaw = input.slice(idx + 1).trim();
2018
- const line = Number.parseInt(lineRaw, 10);
2019
- if (!Number.isInteger(line) || line <= 0 || line.toString() !== lineRaw) {
2020
- throw new CfInspectorError(
2021
- "INVALID_BREAKPOINT",
2022
- `Breakpoint line must be a positive integer, received: "${lineRaw}"`
2023
- );
2024
- }
2025
- if (file.length === 0) {
2026
- throw new CfInspectorError(
2027
- "INVALID_BREAKPOINT",
2028
- `Breakpoint file path is empty in "${input}"`
2029
- );
2030
- }
2031
- return { file, line };
2032
- }
2033
- function parseRemoteRoot(value) {
2034
- const trimmed = value?.trim();
2035
- if (trimmed === void 0 || trimmed.length === 0) {
2036
- return { kind: "none" };
2037
- }
2038
- if (trimmed.startsWith(REGEX_PREFIX)) {
2039
- return toRegex(trimmed.slice(REGEX_PREFIX.length), "");
2040
- }
2041
- const slashRegex = parseSlashDelimited(trimmed);
2042
- if (slashRegex !== void 0) {
2043
- return toRegex(slashRegex.pattern, slashRegex.flags);
2044
- }
2045
- return { kind: "literal", value: stripTrailingSlash(trimmed) };
2046
- }
2047
- function toRegex(pattern, flags) {
2048
- return { kind: "regex", pattern, flags };
2323
+ function toRegex(pattern, flags) {
2324
+ return { kind: "regex", pattern, flags };
2049
2325
  }
2050
2326
  function parseSlashDelimited(value) {
2051
2327
  if (!value.startsWith("/")) {
@@ -2207,6 +2483,244 @@ async function setBreakpoint(session, input) {
2207
2483
  async function removeBreakpoint(session, breakpointId) {
2208
2484
  await session.client.send("Debugger.removeBreakpoint", { breakpointId });
2209
2485
  }
2486
+ function validateCoordinate(value, label) {
2487
+ if (!Number.isInteger(value) || value < 0) {
2488
+ throw new CfInspectorError(
2489
+ "INVALID_ARGUMENT",
2490
+ `${label} must be a non-negative integer, received: ${value.toString()}`
2491
+ );
2492
+ }
2493
+ }
2494
+ function validateScriptLocation(location, label) {
2495
+ if (location.scriptId.trim().length === 0) {
2496
+ throw new CfInspectorError("INVALID_ARGUMENT", `${label}.scriptId must not be empty`);
2497
+ }
2498
+ validateCoordinate(location.lineNumber, `${label}.lineNumber`);
2499
+ if (location.columnNumber !== void 0) {
2500
+ validateCoordinate(location.columnNumber, `${label}.columnNumber`);
2501
+ }
2502
+ }
2503
+ async function getPossibleBreakpoints(session, options) {
2504
+ validateScriptLocation(options.start, "start");
2505
+ if (options.end !== void 0) {
2506
+ validateScriptLocation(options.end, "end");
2507
+ if (options.end.scriptId !== options.start.scriptId) {
2508
+ throw new CfInspectorError("INVALID_ARGUMENT", "start and end must refer to the same scriptId");
2509
+ }
2510
+ }
2511
+ const result = await session.client.send(
2512
+ "Debugger.getPossibleBreakpoints",
2513
+ {
2514
+ start: options.start,
2515
+ ...options.end === void 0 ? {} : { end: options.end },
2516
+ ...options.restrictToFunction === void 0 ? {} : { restrictToFunction: options.restrictToFunction }
2517
+ }
2518
+ );
2519
+ if (!Array.isArray(result.locations)) {
2520
+ throw new CfInspectorError(
2521
+ "CDP_REQUEST_FAILED",
2522
+ "Debugger.getPossibleBreakpoints did not return a locations array"
2523
+ );
2524
+ }
2525
+ return toBreakLocations(result.locations);
2526
+ }
2527
+
2528
+ // src/cli/commands/checkBreakpoint.ts
2529
+ async function handleCheckBreakpoint(opts) {
2530
+ const target = await resolveTargetWithCurrentCfTarget(opts);
2531
+ const location = parseBreakpointSpec(opts.bp);
2532
+ const remoteRoot = parseRemoteRoot(opts.remoteRoot);
2533
+ const urlRegex = buildBreakpointUrlRegex({ file: location.file, remoteRoot });
2534
+ const matcher = new RegExp(urlRegex, "u");
2535
+ const result = await withSessions(target, async (group) => {
2536
+ const checks = (await Promise.all(group.list().map(async (session) => await checkSession(session, matcher, location.line)))).flat();
2537
+ const status = checks.length === 0 ? "script-not-loaded" : checks.some((check) => check.locations.length > 0) ? "breakable" : "unbreakable";
2538
+ return { file: location.file, line: location.line, status, scripts: checks };
2539
+ });
2540
+ if (opts.json) {
2541
+ writeJson(result);
2542
+ return;
2543
+ }
2544
+ writeHumanCheck(result);
2545
+ }
2546
+ async function checkSession(session, matcher, requestedLine) {
2547
+ const zeroBasedLine = requestedLine - 1;
2548
+ const scripts = [...session.scripts.values()].filter((script) => matcher.test(script.url));
2549
+ return await Promise.all(scripts.map(async (script) => {
2550
+ const locations = await getPossibleBreakpoints(session, {
2551
+ start: { scriptId: script.scriptId, lineNumber: zeroBasedLine, columnNumber: 0 },
2552
+ end: { scriptId: script.scriptId, lineNumber: zeroBasedLine + 1, columnNumber: 0 }
2553
+ });
2554
+ return {
2555
+ isolate: session.isolate ?? { kind: "main" },
2556
+ scriptId: script.scriptId,
2557
+ url: script.url,
2558
+ locations: locations.filter((candidate) => candidate.lineNumber === zeroBasedLine)
2559
+ };
2560
+ }));
2561
+ }
2562
+ function writeHumanCheck(result) {
2563
+ if (result.status === "script-not-loaded") {
2564
+ process4.stdout.write(
2565
+ `${result.file}:${result.line.toString()} does not match any loaded script. Run list-scripts and check --remote-root/path mapping, or trigger lazy module loading first.
2566
+ `
2567
+ );
2568
+ return;
2569
+ }
2570
+ if (result.status === "unbreakable") {
2571
+ process4.stdout.write(
2572
+ `${result.file}:${result.line.toString()} matches a loaded script, but this exact line has no breakable location. Try a neighboring executable line.
2573
+ `
2574
+ );
2575
+ return;
2576
+ }
2577
+ process4.stdout.write(`${result.file}:${result.line.toString()} is breakable:
2578
+ `);
2579
+ for (const script of result.scripts) {
2580
+ for (const location of script.locations) {
2581
+ const isolate = script.isolate.kind === "main" ? "main" : `worker ${script.isolate.workerId}`;
2582
+ process4.stdout.write(
2583
+ ` ${isolate} ${script.url} line ${(location.lineNumber + 1).toString()}:${((location.columnNumber ?? 0) + 1).toString()}
2584
+ `
2585
+ );
2586
+ }
2587
+ }
2588
+ }
2589
+
2590
+ // src/cli/commands/eval.ts
2591
+ import process5 from "process";
2592
+
2593
+ // src/inspector/runtime.ts
2594
+ init_types();
2595
+ async function resume(session) {
2596
+ await session.client.send("Debugger.resume");
2597
+ session.debuggerState.paused = false;
2598
+ delete session.debuggerState.currentPause;
2599
+ }
2600
+ async function setPauseOnExceptions(session, state) {
2601
+ await session.client.send("Debugger.setPauseOnExceptions", { state });
2602
+ }
2603
+ async function evaluateOnFrame(session, callFrameId, expression, options = {}) {
2604
+ return await session.client.send("Debugger.evaluateOnCallFrame", {
2605
+ callFrameId,
2606
+ expression,
2607
+ returnByValue: false,
2608
+ generatePreview: true,
2609
+ silent: true,
2610
+ ...options.throwOnSideEffect === void 0 ? {} : { throwOnSideEffect: options.throwOnSideEffect },
2611
+ ...options.objectGroup === void 0 ? {} : { objectGroup: options.objectGroup }
2612
+ });
2613
+ }
2614
+ function isSideEffectRefusal(result) {
2615
+ const classNames = [
2616
+ result.result?.className,
2617
+ result.exceptionDetails?.exception?.className
2618
+ ];
2619
+ const descriptions = [
2620
+ result.result?.description,
2621
+ result.exceptionDetails?.exception?.description
2622
+ ];
2623
+ const isEvalError = classNames.includes("EvalError");
2624
+ return isEvalError && descriptions.some(
2625
+ (description) => typeof description === "string" && description.toLowerCase().includes("possible side-effect in debug-evaluate")
2626
+ );
2627
+ }
2628
+ async function evaluateGlobal(session, expression) {
2629
+ return await session.client.send("Runtime.evaluate", {
2630
+ expression,
2631
+ returnByValue: false,
2632
+ generatePreview: true,
2633
+ silent: true
2634
+ });
2635
+ }
2636
+ async function runSetupEvals(session, expressions) {
2637
+ for (const expression of expressions) {
2638
+ const result = await evaluateGlobal(session, expression);
2639
+ if (result.exceptionDetails !== void 0) {
2640
+ throw new CfInspectorError(
2641
+ "SETUP_EVAL_FAILED",
2642
+ exceptionDetailsMessage(result, "setup evaluation failed")
2643
+ );
2644
+ }
2645
+ }
2646
+ }
2647
+ function exceptionDetailsMessage(result, fallback) {
2648
+ return typeof result.exceptionDetails?.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails?.text === "string" ? result.exceptionDetails.text : fallback;
2649
+ }
2650
+ function listScripts(session) {
2651
+ return [...session.scripts.values()];
2652
+ }
2653
+ async function validateExpression(session, expression) {
2654
+ const result = await session.client.send("Runtime.compileScript", {
2655
+ expression,
2656
+ sourceURL: "<cf-inspector-validate>",
2657
+ persistScript: false
2658
+ });
2659
+ if (result.exceptionDetails === void 0) {
2660
+ return;
2661
+ }
2662
+ const description = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "expression failed to compile";
2663
+ throw new CfInspectorError("INVALID_EXPRESSION", description);
2664
+ }
2665
+ async function getProperties(session, objectId) {
2666
+ const result = await session.client.send("Runtime.getProperties", {
2667
+ objectId,
2668
+ ownProperties: true,
2669
+ accessorPropertiesOnly: false,
2670
+ generatePreview: true
2671
+ });
2672
+ if (!Array.isArray(result.result)) {
2673
+ return [];
2674
+ }
2675
+ return result.result;
2676
+ }
2677
+
2678
+ // src/cli/commands/eval.ts
2679
+ async function handleEval(opts) {
2680
+ warnOnMutationRisk(opts.expr, "eval --expr");
2681
+ const target = await resolveTargetWithCurrentCfTarget(opts);
2682
+ const result = await withSession(target, async (session) => {
2683
+ return await evaluateGlobal(session, opts.expr);
2684
+ });
2685
+ if (opts.json) {
2686
+ writeJson(result);
2687
+ if (result.exceptionDetails !== void 0) {
2688
+ process5.exitCode = 1;
2689
+ }
2690
+ return;
2691
+ }
2692
+ writeHumanEvalResult(result);
2693
+ }
2694
+ function writeHumanEvalResult(result) {
2695
+ if (result.exceptionDetails !== void 0) {
2696
+ const detail = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
2697
+ process5.stderr.write(`${detail}
2698
+ `);
2699
+ process5.exitCode = 1;
2700
+ return;
2701
+ }
2702
+ const inner = result.result;
2703
+ if (inner === void 0) {
2704
+ process5.stdout.write("\n");
2705
+ return;
2706
+ }
2707
+ if (typeof inner.value === "string") {
2708
+ process5.stdout.write(`${inner.value}
2709
+ `);
2710
+ return;
2711
+ }
2712
+ if (typeof inner.description === "string") {
2713
+ process5.stdout.write(`${inner.description}
2714
+ `);
2715
+ return;
2716
+ }
2717
+ process5.stdout.write(`${JSON.stringify(inner.value)}
2718
+ `);
2719
+ }
2720
+
2721
+ // src/cli/commands/exception.ts
2722
+ import { performance as performance5 } from "perf_hooks";
2723
+ import process7 from "process";
2210
2724
 
2211
2725
  // src/inspector/pause.ts
2212
2726
  init_types();
@@ -2302,31 +2816,289 @@ async function waitForPause(session, options) {
2302
2816
  if (pauseMatches(pause, options.breakpointIds, options.pauseReasons)) {
2303
2817
  return pause;
2304
2818
  }
2305
- await handleUnmatchedPause(session, pause, options, deadlineMs);
2819
+ await handleUnmatchedPause(session, pause, options, deadlineMs);
2820
+ }
2821
+ throwBreakpointTimeout(options.timeoutMs);
2822
+ }
2823
+ async function waitForLivePause(session, options, deadlineMs) {
2824
+ const remainingMs = remainingUntil(deadlineMs);
2825
+ if (remainingMs <= 0) {
2826
+ throwBreakpointTimeout(options.timeoutMs);
2827
+ }
2828
+ session.pauseWaitGate.active = true;
2829
+ let receivedAtMs;
2830
+ let params;
2831
+ try {
2832
+ params = await session.client.waitFor("Debugger.paused", {
2833
+ timeoutMs: remainingMs,
2834
+ ...options.signal === void 0 ? {} : { signal: options.signal },
2835
+ predicate: () => {
2836
+ receivedAtMs = performance3.now();
2837
+ return true;
2838
+ }
2839
+ });
2840
+ } finally {
2841
+ session.pauseWaitGate.active = false;
2842
+ }
2843
+ return toPauseEvent(params, receivedAtMs ?? performance3.now(), session.scripts);
2844
+ }
2845
+
2846
+ // src/inspector/fanout.ts
2847
+ init_types();
2848
+ import { performance as performance4 } from "perf_hooks";
2849
+ var DEFAULT_CLEANUP_TIMEOUT_MS = 2e3;
2850
+ var BreakpointFanout = class {
2851
+ records = /* @__PURE__ */ new Map();
2852
+ setupErrors = [];
2853
+ detach;
2854
+ detachRemoved;
2855
+ detachError;
2856
+ activeRace;
2857
+ pauseReasons = [];
2858
+ constructor(group, setupSession, pauseReasons = []) {
2859
+ this.pauseReasons = pauseReasons;
2860
+ this.detach = group.onSession((session) => {
2861
+ const record = { session, handles: [], setup: Promise.resolve() };
2862
+ this.records.set(session, record);
2863
+ record.setup = setupSession(session, (handle) => {
2864
+ this.trackHandle(session, handle);
2865
+ }).then((result) => {
2866
+ for (const handle of result.handles) {
2867
+ this.trackHandle(session, handle);
2868
+ }
2869
+ });
2870
+ const setup = record.setup;
2871
+ setup.catch((error) => {
2872
+ for (const reject of this.setupErrors) {
2873
+ reject(error);
2874
+ }
2875
+ });
2876
+ this.activeRace?.add(record);
2877
+ });
2878
+ this.detachRemoved = group.onSessionRemoved((session) => {
2879
+ this.records.delete(session);
2880
+ this.activeRace?.remove(session);
2881
+ });
2882
+ this.detachError = group.onError((error) => {
2883
+ for (const reject of this.setupErrors) {
2884
+ reject(error);
2885
+ }
2886
+ });
2887
+ }
2888
+ async ready() {
2889
+ await Promise.all([...this.records.values()].map((record) => record.setup));
2890
+ }
2891
+ trackHandle(session, handle) {
2892
+ const record = this.records.get(session);
2893
+ if (record !== void 0 && !record.handles.some((candidate) => candidate.breakpointId === handle.breakpointId)) {
2894
+ record.handles.push(handle);
2895
+ }
2896
+ }
2897
+ availableOutcomes() {
2898
+ return [...this.records.values()].map((record) => ({
2899
+ session: record.session,
2900
+ setup: { handles: record.handles }
2901
+ }));
2902
+ }
2903
+ async waitForFirst(timeoutMs, options = {}, signal) {
2904
+ if (this.activeRace !== void 0) {
2905
+ throw new CfInspectorError("INVALID_ARGUMENT", "A fan-out pause race is already active");
2906
+ }
2907
+ const race = new ActivePauseRace(timeoutMs, options, signal);
2908
+ this.pauseReasons = options.pauseReasons ?? [];
2909
+ this.activeRace = race;
2910
+ this.setupErrors.push(race.reject);
2911
+ for (const record of this.records.values()) {
2912
+ race.add(record);
2913
+ }
2914
+ try {
2915
+ const winner = await race.result;
2916
+ await race.stopAndSettle();
2917
+ await this.resumePausedLosers(winner.session);
2918
+ return winner;
2919
+ } finally {
2920
+ this.activeRace = void 0;
2921
+ const index = this.setupErrors.indexOf(race.reject);
2922
+ if (index >= 0) {
2923
+ this.setupErrors.splice(index, 1);
2924
+ }
2925
+ await race.stopAndSettle();
2926
+ }
2927
+ }
2928
+ async resumePaused(except) {
2929
+ return await this.resumePausedLosers(except, DEFAULT_CLEANUP_TIMEOUT_MS);
2930
+ }
2931
+ async cleanup(timeoutMs = DEFAULT_CLEANUP_TIMEOUT_MS, preservePaused) {
2932
+ this.detach();
2933
+ this.detachRemoved();
2934
+ this.detachError();
2935
+ const deadline = performance4.now() + timeoutMs;
2936
+ await settleWithin(Promise.allSettled([...this.records.values()].map(async (record) => {
2937
+ await record.setup;
2938
+ })), remaining(deadline));
2939
+ const breakpointEntries = [...this.records.values()].flatMap((record) => record.handles.map((handle) => ({
2940
+ session: record.session,
2941
+ breakpointId: handle.breakpointId
2942
+ })));
2943
+ let cleared = 0;
2944
+ const clearWork = Promise.allSettled(breakpointEntries.map(async (entry) => {
2945
+ await removeBreakpoint(entry.session, entry.breakpointId);
2946
+ cleared += 1;
2947
+ }));
2948
+ await settleWithin(clearWork, remaining(deadline));
2949
+ const resumed = await this.resumePausedLosers(preservePaused, remaining(deadline));
2950
+ return { attempted: breakpointEntries.length, cleared, resumed };
2951
+ }
2952
+ async resumePausedLosers(except, timeoutMs = DEFAULT_CLEANUP_TIMEOUT_MS) {
2953
+ let resumed = 0;
2954
+ await settleWithin(Promise.allSettled([...this.records.keys()].map(async (session) => {
2955
+ if (session === except || session.debuggerState.paused !== true || session.client.isClosed || !this.ownsCurrentPause(session)) {
2956
+ return;
2957
+ }
2958
+ await resume(session);
2959
+ session.debuggerState.paused = false;
2960
+ resumed += 1;
2961
+ })), timeoutMs);
2962
+ return resumed;
2963
+ }
2964
+ ownsCurrentPause(session) {
2965
+ const pause = session.debuggerState.currentPause;
2966
+ if (pause === void 0) {
2967
+ return false;
2968
+ }
2969
+ if (this.pauseReasons.includes(pause.reason)) {
2970
+ return true;
2971
+ }
2972
+ const record = this.records.get(session);
2973
+ const breakpointIds = new Set(record?.handles.map((handle) => handle.breakpointId) ?? []);
2974
+ return pause.hitBreakpoints.some((breakpointId) => breakpointIds.has(breakpointId));
2975
+ }
2976
+ };
2977
+ var ActivePauseRace = class {
2978
+ constructor(timeoutMs, options, signal) {
2979
+ this.options = options;
2980
+ this.externalSignal = signal;
2981
+ this.deadline = performance4.now() + timeoutMs;
2982
+ let resolveResult;
2983
+ let rejectResult;
2984
+ this.result = new Promise((resolve, reject) => {
2985
+ resolveResult = resolve;
2986
+ rejectResult = reject;
2987
+ });
2988
+ this.resolveResult = (winner) => {
2989
+ if (this.settled) {
2990
+ return;
2991
+ }
2992
+ this.settled = true;
2993
+ resolveResult?.(winner);
2994
+ };
2995
+ this.reject = (error) => {
2996
+ if (this.settled) {
2997
+ return;
2998
+ }
2999
+ this.settled = true;
3000
+ rejectResult?.(error);
3001
+ };
3002
+ this.timeout = setTimeout(() => {
3003
+ this.reject(this.terminalTimeoutError ?? new CfInspectorError(
3004
+ "BREAKPOINT_NOT_HIT",
3005
+ `Timed out waiting for a matching pause in any isolate after ${timeoutMs.toString()}ms`
3006
+ ));
3007
+ this.controller.abort();
3008
+ }, timeoutMs + 25);
3009
+ if (signal !== void 0) {
3010
+ if (signal.aborted) {
3011
+ this.reject(new CfInspectorError("ABORTED", "Aborted while waiting for an isolate pause"));
3012
+ } else {
3013
+ signal.addEventListener("abort", this.onExternalAbort, { once: true });
3014
+ }
3015
+ }
3016
+ }
3017
+ options;
3018
+ controller = new AbortController();
3019
+ waits = /* @__PURE__ */ new Set();
3020
+ settled = false;
3021
+ deadline;
3022
+ timeout;
3023
+ externalSignal;
3024
+ onExternalAbort = () => {
3025
+ this.reject(new CfInspectorError("ABORTED", "Aborted while waiting for an isolate pause"));
3026
+ this.controller.abort();
3027
+ };
3028
+ resolveResult;
3029
+ terminalTimeoutError;
3030
+ removedSessions = /* @__PURE__ */ new Set();
3031
+ reject;
3032
+ result;
3033
+ add(record) {
3034
+ if (this.settled) {
3035
+ return;
3036
+ }
3037
+ const wait = this.wait(record).finally(() => {
3038
+ this.waits.delete(wait);
3039
+ });
3040
+ this.waits.add(wait);
3041
+ }
3042
+ remove(session) {
3043
+ this.removedSessions.add(session);
3044
+ }
3045
+ async stopAndSettle() {
3046
+ clearTimeout(this.timeout);
3047
+ this.externalSignal?.removeEventListener("abort", this.onExternalAbort);
3048
+ this.controller.abort();
3049
+ await Promise.allSettled([...this.waits]);
3050
+ }
3051
+ async wait(record) {
3052
+ try {
3053
+ await record.setup;
3054
+ const remainingMs = Math.max(1, this.deadline - performance4.now());
3055
+ const pause = await waitForPause(record.session, {
3056
+ ...this.options,
3057
+ timeoutMs: remainingMs,
3058
+ breakpointIds: record.handles.map((handle) => handle.breakpointId),
3059
+ signal: this.controller.signal
3060
+ });
3061
+ record.session.debuggerState.paused = true;
3062
+ this.resolveResult({ session: record.session, pause });
3063
+ this.controller.abort();
3064
+ } catch (error) {
3065
+ if (this.removedSessions.has(record.session)) {
3066
+ return;
3067
+ }
3068
+ if (error instanceof CfInspectorError && error.code === "UNRELATED_PAUSE_TIMEOUT") {
3069
+ this.terminalTimeoutError = error;
3070
+ return;
3071
+ }
3072
+ if (isExpectedRaceStop(error)) {
3073
+ return;
3074
+ }
3075
+ this.reject(error);
3076
+ this.controller.abort();
3077
+ }
2306
3078
  }
2307
- throwBreakpointTimeout(options.timeoutMs);
3079
+ };
3080
+ function isExpectedRaceStop(error) {
3081
+ return error instanceof CfInspectorError && (error.code === "ABORTED" || error.code === "BREAKPOINT_NOT_HIT" || error.code === "UNRELATED_PAUSE_TIMEOUT");
2308
3082
  }
2309
- async function waitForLivePause(session, options, deadlineMs) {
2310
- const remainingMs = remainingUntil(deadlineMs);
2311
- if (remainingMs <= 0) {
2312
- throwBreakpointTimeout(options.timeoutMs);
2313
- }
2314
- session.pauseWaitGate.active = true;
2315
- let receivedAtMs;
2316
- let params;
3083
+ function remaining(deadline) {
3084
+ return Math.max(0, deadline - performance4.now());
3085
+ }
3086
+ async function settleWithin(work, timeoutMs) {
3087
+ let timer;
2317
3088
  try {
2318
- params = await session.client.waitFor("Debugger.paused", {
2319
- timeoutMs: remainingMs,
2320
- ...options.signal === void 0 ? {} : { signal: options.signal },
2321
- predicate: () => {
2322
- receivedAtMs = performance3.now();
2323
- return true;
2324
- }
2325
- });
3089
+ return await Promise.race([
3090
+ work,
3091
+ new Promise((resolve) => {
3092
+ timer = setTimeout(() => {
3093
+ resolve(null);
3094
+ }, timeoutMs);
3095
+ })
3096
+ ]);
2326
3097
  } finally {
2327
- session.pauseWaitGate.active = false;
3098
+ if (timer !== void 0) {
3099
+ clearTimeout(timer);
3100
+ }
2328
3101
  }
2329
- return toPauseEvent(params, receivedAtMs ?? performance3.now(), session.scripts);
2330
3102
  }
2331
3103
 
2332
3104
  // src/snapshot/evaluation.ts
@@ -3040,6 +3812,25 @@ async function captureExpression(session, callFrameId, expression, maxValueLengt
3040
3812
 
3041
3813
  // src/cli/commands/exception.ts
3042
3814
  init_types();
3815
+
3816
+ // src/cli/signals.ts
3817
+ import process6 from "process";
3818
+ async function withTerminationSignal(fn) {
3819
+ const abort = new AbortController();
3820
+ const onSignal = () => {
3821
+ abort.abort();
3822
+ };
3823
+ process6.once("SIGINT", onSignal);
3824
+ process6.once("SIGTERM", onSignal);
3825
+ try {
3826
+ return await fn(abort.signal);
3827
+ } finally {
3828
+ process6.off("SIGINT", onSignal);
3829
+ process6.off("SIGTERM", onSignal);
3830
+ }
3831
+ }
3832
+
3833
+ // src/cli/commands/exception.ts
3043
3834
  var VALID_PAUSE_TYPES = ["uncaught", "caught", "all"];
3044
3835
  async function handleException(opts) {
3045
3836
  const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
@@ -3048,7 +3839,7 @@ async function handleException(opts) {
3048
3839
  [...prepared.captures, ...prepared.stackCaptures],
3049
3840
  opts.allowMutation === true
3050
3841
  );
3051
- const result = await runExceptionCommand(prepared, opts);
3842
+ const result = await withTerminationSignal(async (signal) => await runExceptionCommand(prepared, opts, signal));
3052
3843
  if (opts.json) {
3053
3844
  writeJson(result);
3054
3845
  } else {
@@ -3078,17 +3869,24 @@ function prepareExceptionCommand(opts, target) {
3078
3869
  throwOnSideEffect: opts.allowMutation !== true
3079
3870
  };
3080
3871
  }
3081
- async function runExceptionCommand(command, opts) {
3082
- return await withSession(command.target, async (session) => {
3083
- await setPauseOnExceptions(session, command.state);
3872
+ async function runExceptionCommand(command, opts, signal) {
3873
+ return await withSessions(command.target, async (group) => {
3874
+ const fanout = new BreakpointFanout(group, async (session) => {
3875
+ await setPauseOnExceptions(session, command.state);
3876
+ return { handles: [] };
3877
+ }, ["exception", "promiseRejection"]);
3878
+ let winner;
3879
+ let preserveWinner = false;
3084
3880
  try {
3085
- const pause = await waitForPause(session, {
3086
- timeoutMs: command.timeoutMs,
3881
+ await fanout.ready();
3882
+ const hit = await fanout.waitForFirst(command.timeoutMs, {
3087
3883
  pauseReasons: ["exception", "promiseRejection"],
3088
3884
  unmatchedPausePolicy: "wait-for-resume"
3089
- });
3090
- const pausedStartedAt = pause.receivedAtMs ?? performance4.now();
3091
- const snapshot = await captureSnapshot(session, pause, {
3885
+ }, signal);
3886
+ winner = hit.session;
3887
+ const pause = hit.pause;
3888
+ const pausedStartedAt = pause.receivedAtMs ?? performance5.now();
3889
+ const snapshot = await captureSnapshot(hit.session, pause, {
3092
3890
  captures: command.captures,
3093
3891
  includeScopes: opts.includeScopes === true,
3094
3892
  maxValueLength: command.maxValueLength,
@@ -3097,20 +3895,25 @@ async function runExceptionCommand(command, opts) {
3097
3895
  throwOnSideEffect: command.throwOnSideEffect
3098
3896
  });
3099
3897
  if (opts.keepPaused === true) {
3100
- return withPausedDuration(snapshot, null);
3898
+ preserveWinner = true;
3899
+ return { ...withPausedDuration(snapshot, null), isolate: hit.session.isolate ?? { kind: "main" } };
3101
3900
  }
3102
- return await resumeAfterException(session, snapshot, pausedStartedAt);
3901
+ const result = await resumeAfterException(hit.session, snapshot, pausedStartedAt);
3902
+ return { ...result, isolate: hit.session.isolate ?? { kind: "main" } };
3103
3903
  } finally {
3104
- await disablePauseOnExceptionsBestEffort(session);
3904
+ await Promise.allSettled(group.list().map(async (session) => {
3905
+ await disablePauseOnExceptionsBestEffort(session);
3906
+ }));
3907
+ await fanout.cleanup(2e3, preserveWinner ? winner : void 0);
3105
3908
  }
3106
- });
3909
+ }, void 0, signal);
3107
3910
  }
3108
3911
  async function resumeAfterException(session, snapshot, pausedStartedAt) {
3109
3912
  try {
3110
3913
  await resume(session);
3111
- return withPausedDuration(snapshot, roundDurationMs(performance4.now() - pausedStartedAt));
3914
+ return withPausedDuration(snapshot, roundDurationMs(performance5.now() - pausedStartedAt));
3112
3915
  } catch {
3113
- process5.stderr.write(
3916
+ process7.stderr.write(
3114
3917
  "[cf-inspector] warning: Debugger.resume failed after exception capture; pausedDurationMs is unknown.\n"
3115
3918
  );
3116
3919
  return withPausedDuration(snapshot, null);
@@ -3124,7 +3927,7 @@ async function disablePauseOnExceptionsBestEffort(session) {
3124
3927
  }
3125
3928
 
3126
3929
  // src/cli/commands/listScripts.ts
3127
- import process6 from "process";
3930
+ import process8 from "process";
3128
3931
  async function handleListScripts(opts) {
3129
3932
  const target = await resolveTargetWithCurrentCfTarget(opts);
3130
3933
  const filter = compileScriptUrlFilter(opts.filter);
@@ -3134,7 +3937,7 @@ async function handleListScripts(opts) {
3134
3937
  return;
3135
3938
  }
3136
3939
  for (const script of scripts) {
3137
- process6.stdout.write(`${script.scriptId} ${script.url}
3940
+ process8.stdout.write(`${script.scriptId} ${script.url}
3138
3941
  `);
3139
3942
  }
3140
3943
  }
@@ -3165,7 +3968,7 @@ async function buildListedTargets(targets) {
3165
3968
  return buildListedTarget(target, index, workerResult.supported, workerResult.workers);
3166
3969
  } catch (error) {
3167
3970
  const message = error instanceof Error ? error.message : String(error);
3168
- process6.stderr.write(
3971
+ process8.stderr.write(
3169
3972
  `[cf-inspector] warning: worker discovery failed for raw target ${index.toString()}: ${message}
3170
3973
  `
3171
3974
  );
@@ -3192,7 +3995,7 @@ function looksLikeWorkerTarget(target) {
3192
3995
  return `${target.type} ${target.title} ${target.url}`.toLowerCase().includes("worker");
3193
3996
  }
3194
3997
  function writeTargetCountSummary(targetCount, workerCount) {
3195
- process6.stderr.write(
3998
+ process8.stderr.write(
3196
3999
  `[cf-inspector] ${targetCount.toString()} raw inspector ${targetCount === 1 ? "target" : "targets"}; ${workerCount.toString()} ${workerCount === 1 ? "worker" : "workers"}.
3197
4000
  `
3198
4001
  );
@@ -3203,7 +4006,7 @@ function warnOnMissingWorkers(targetCount, workerCount, targets) {
3203
4006
  }
3204
4007
  const supported = targets[0]?.workerDiscoverySupported === true;
3205
4008
  const supportHint = supported ? "NodeWorker discovery is available, but no live worker attached." : "This runtime did not expose NodeWorker discovery.";
3206
- process6.stderr.write(
4009
+ process8.stderr.write(
3207
4010
  `[cf-inspector] warning: only the main inspector target is reachable. ${supportHint} If worker code is expected, ensure the worker is alive and rerun list-targets. A worker on a separate inspector port is not carried by a single Cloud Foundry tunnel.
3208
4011
  `
3209
4012
  );
@@ -3211,12 +4014,12 @@ function warnOnMissingWorkers(targetCount, workerCount, targets) {
3211
4014
  function writeHumanTargets(targets) {
3212
4015
  for (const target of targets) {
3213
4016
  const workerLabel = target.likelyWorker ? " likely-worker" : "";
3214
- process6.stdout.write(
4017
+ process8.stdout.write(
3215
4018
  `${target.index.toString()} target ${target.type} ${target.title} ${target.url}${workerLabel}
3216
4019
  `
3217
4020
  );
3218
4021
  for (const worker of target.workers) {
3219
- process6.stdout.write(
4022
+ process8.stdout.write(
3220
4023
  ` ${worker.index.toString()} worker ${worker.type} ${worker.title} ${worker.url}
3221
4024
  `
3222
4025
  );
@@ -3293,7 +4096,7 @@ function matchesFilterTokens(value, tokens) {
3293
4096
  }
3294
4097
 
3295
4098
  // src/cli/commands/log.ts
3296
- import process8 from "process";
4099
+ import process9 from "process";
3297
4100
 
3298
4101
  // src/logpoint/stream.ts
3299
4102
  init_types();
@@ -3559,25 +4362,6 @@ async function waitForStop(session, options, registerMaxEventsSignal) {
3559
4362
 
3560
4363
  // src/cli/commands/log.ts
3561
4364
  init_types();
3562
-
3563
- // src/cli/signals.ts
3564
- import process7 from "process";
3565
- async function withTerminationSignal(fn) {
3566
- const abort = new AbortController();
3567
- const onSignal = () => {
3568
- abort.abort();
3569
- };
3570
- process7.once("SIGINT", onSignal);
3571
- process7.once("SIGTERM", onSignal);
3572
- try {
3573
- return await fn(abort.signal);
3574
- } finally {
3575
- process7.off("SIGINT", onSignal);
3576
- process7.off("SIGTERM", onSignal);
3577
- }
3578
- }
3579
-
3580
- // src/cli/commands/log.ts
3581
4365
  async function handleLog(opts) {
3582
4366
  const target = await resolveTargetWithCurrentCfTarget(opts);
3583
4367
  const location = parseBreakpointSpec(opts.at);
@@ -3596,12 +4380,8 @@ async function handleLog(opts) {
3596
4380
  warnOnMutationRisk(condition, "log --condition");
3597
4381
  }
3598
4382
  await withTerminationSignal(async (signal) => {
3599
- await withSession(target, async (session) => {
3600
- await validateExpression(session, expression);
3601
- if (condition !== void 0) {
3602
- await validateExpression(session, condition);
3603
- }
3604
- const result = await streamLogpoint(session, {
4383
+ await withSessions(target, async (group) => {
4384
+ const result = await runLogGroup(group, {
3605
4385
  location,
3606
4386
  expression,
3607
4387
  remoteRoot,
@@ -3610,36 +4390,132 @@ async function handleLog(opts) {
3610
4390
  ...hitCount === void 0 ? {} : { hitCount },
3611
4391
  ...condition === void 0 ? {} : { condition },
3612
4392
  maxValueLength,
3613
- signal,
4393
+ json: opts.json,
4394
+ signal
4395
+ });
4396
+ writeLogSummary(result.stoppedReason, result.emitted, opts.json);
4397
+ }, void 0, signal);
4398
+ });
4399
+ }
4400
+ async function runLogGroup(group, options) {
4401
+ const controller = new AbortController();
4402
+ const tasks = /* @__PURE__ */ new Set();
4403
+ const removedSessions = /* @__PURE__ */ new Set();
4404
+ const results = [];
4405
+ let fatalError;
4406
+ let emitted = 0;
4407
+ let reason = "signal";
4408
+ let resolveStop;
4409
+ const stopped = new Promise((resolve) => {
4410
+ resolveStop = resolve;
4411
+ });
4412
+ const finish = (nextReason) => {
4413
+ if (controller.signal.aborted) {
4414
+ return;
4415
+ }
4416
+ reason = nextReason;
4417
+ controller.abort();
4418
+ resolveStop?.();
4419
+ };
4420
+ const onSignal = () => {
4421
+ finish("signal");
4422
+ };
4423
+ options.signal.addEventListener("abort", onSignal, { once: true });
4424
+ if (options.signal.aborted) {
4425
+ finish("signal");
4426
+ }
4427
+ const timer = options.durationMs === void 0 ? void 0 : setTimeout(() => {
4428
+ finish("duration");
4429
+ }, options.durationMs);
4430
+ const startSession = (session) => {
4431
+ if (controller.signal.aborted) {
4432
+ return;
4433
+ }
4434
+ const task = (async () => {
4435
+ await validateExpression(session, options.expression);
4436
+ if (options.condition !== void 0) {
4437
+ await validateExpression(session, options.condition);
4438
+ }
4439
+ return await streamLogpoint(session, {
4440
+ location: options.location,
4441
+ expression: options.expression,
4442
+ remoteRoot: options.remoteRoot,
4443
+ ...options.hitCount === void 0 ? {} : { hitCount: options.hitCount },
4444
+ ...options.condition === void 0 ? {} : { condition: options.condition },
4445
+ maxValueLength: options.maxValueLength,
4446
+ signal: controller.signal,
3614
4447
  onEvent: (event) => {
3615
- writeLogEvent(event, opts.json);
4448
+ if (controller.signal.aborted) {
4449
+ return;
4450
+ }
4451
+ emitted += 1;
4452
+ writeLogEvent({ ...event, isolate: session.isolate ?? { kind: "main" } }, options.json);
4453
+ if (options.maxEvents !== void 0 && emitted >= options.maxEvents) {
4454
+ finish("max-events");
4455
+ }
3616
4456
  },
3617
4457
  onBreakpointSet: (handle) => {
3618
4458
  warnOnUnboundBreakpoints([handle]);
3619
4459
  }
3620
4460
  });
3621
- if (result.emitted === 0 && (result.stoppedReason === "duration" || result.stoppedReason === "signal")) {
3622
- warnOnBoundBreakpointWithoutHit([result.handle]);
4461
+ })();
4462
+ tasks.add(task);
4463
+ void task.then(
4464
+ (result) => {
4465
+ results.push(result);
4466
+ if (result.stoppedReason === "transport-closed" && !removedSessions.has(session)) {
4467
+ finish("transport-closed");
4468
+ }
4469
+ },
4470
+ (error) => {
4471
+ fatalError = error;
4472
+ finish("transport-closed");
3623
4473
  }
3624
- writeLogSummary(result.stoppedReason, result.emitted, opts.json);
3625
- }, void 0, signal);
4474
+ ).finally(() => {
4475
+ tasks.delete(task);
4476
+ });
4477
+ };
4478
+ const detach = group.onSession(startSession);
4479
+ const detachRemoved = group.onSessionRemoved((session) => {
4480
+ removedSessions.add(session);
3626
4481
  });
4482
+ try {
4483
+ await stopped;
4484
+ await Promise.allSettled([...tasks]);
4485
+ } finally {
4486
+ detach();
4487
+ detachRemoved();
4488
+ if (timer !== void 0) {
4489
+ clearTimeout(timer);
4490
+ }
4491
+ options.signal.removeEventListener("abort", onSignal);
4492
+ }
4493
+ if (emitted === 0 && isZeroHitStop(reason)) {
4494
+ warnOnBoundBreakpointWithoutHit(results.map((result) => result.handle));
4495
+ }
4496
+ if (fatalError !== void 0) {
4497
+ throw fatalError instanceof Error ? fatalError : new Error("Unknown logpoint fan-out failure");
4498
+ }
4499
+ return { emitted, stoppedReason: reason };
4500
+ }
4501
+ function isZeroHitStop(reason) {
4502
+ return reason === "duration" || reason === "signal";
3627
4503
  }
3628
4504
  function writeLogSummary(stoppedReason, emitted, json) {
3629
4505
  if (json) {
3630
- process8.stderr.write(`${JSON.stringify({ stopped: stoppedReason, emitted })}
4506
+ process9.stderr.write(`${JSON.stringify({ stopped: stoppedReason, emitted })}
3631
4507
  `);
3632
4508
  return;
3633
4509
  }
3634
- process8.stderr.write(
4510
+ process9.stderr.write(
3635
4511
  `Stopped (${stoppedReason}); emitted ${emitted.toString()} log ${emitted === 1 ? "entry" : "entries"}.
3636
4512
  `
3637
4513
  );
3638
4514
  }
3639
4515
 
3640
4516
  // src/cli/commands/snapshot.ts
3641
- import { performance as performance5 } from "perf_hooks";
3642
- import process9 from "process";
4517
+ import { performance as performance6 } from "perf_hooks";
4518
+ import process10 from "process";
3643
4519
  init_types();
3644
4520
  async function handleSnapshot(opts) {
3645
4521
  const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
@@ -3652,7 +4528,7 @@ async function handleSnapshot(opts) {
3652
4528
  warnOnMutationRisk(expression, "snapshot --setup-eval");
3653
4529
  }
3654
4530
  const reportProgress = opts.quiet === true ? void 0 : writeProgress;
3655
- const result = await runSnapshotCommand(prepared, opts, reportProgress);
4531
+ const result = await withTerminationSignal(async (signal) => await runSnapshotCommand(prepared, opts, reportProgress, signal));
3656
4532
  if (opts.json) {
3657
4533
  writeJson(result);
3658
4534
  } else {
@@ -3693,45 +4569,95 @@ function prepareSnapshotCommand(opts, target) {
3693
4569
  throwOnSideEffect: opts.allowMutation !== true
3694
4570
  };
3695
4571
  }
3696
- async function runSnapshotCommand(command, opts, reportProgress) {
3697
- return await withSession(command.target, async (session) => {
3698
- return await runSnapshotOnSession(session, command, opts, reportProgress);
3699
- }, reportProgress);
4572
+ async function runSnapshotCommand(command, opts, reportProgress, signal) {
4573
+ return await withSessions(command.target, async (group) => {
4574
+ if (command.setupEvals.length > 0) {
4575
+ const setupCount = command.setupEvals.length;
4576
+ reportProgress?.(
4577
+ `Running ${setupCount.toString()} setup ${setupCount === 1 ? "evaluation" : "evaluations"}...`
4578
+ );
4579
+ }
4580
+ if (command.condition !== void 0) {
4581
+ reportProgress?.("Validating the breakpoint condition...");
4582
+ }
4583
+ const breakpointCount = command.breakpoints.length;
4584
+ reportProgress?.(
4585
+ `Setting ${breakpointCount.toString()} ${breakpointCount === 1 ? "breakpoint" : "breakpoints"}...`
4586
+ );
4587
+ const fanout = new BreakpointFanout(group, async (session, trackHandle) => {
4588
+ await prepareSnapshotSession(session, command);
4589
+ return { handles: await setCommandBreakpoints(session, command, trackHandle) };
4590
+ });
4591
+ let winner;
4592
+ let preserveWinner = false;
4593
+ try {
4594
+ await fanout.ready();
4595
+ if (command.setupEvals.length > 0) {
4596
+ reportProgress?.("Setup evaluation complete.");
4597
+ }
4598
+ if (command.condition !== void 0) {
4599
+ reportProgress?.("Breakpoint condition is valid.");
4600
+ }
4601
+ const outcomes = fanout.availableOutcomes();
4602
+ reportBreakpointOutcomes(outcomes, reportProgress);
4603
+ reportProgress?.(
4604
+ `Waiting up to ${(command.timeoutMs / 1e3).toString()}s for a breakpoint hit...`
4605
+ );
4606
+ const hit = await fanout.waitForFirst(command.timeoutMs, {
4607
+ unmatchedPausePolicy: opts.failOnUnmatchedPause === true ? "fail" : "wait-for-resume",
4608
+ ...opts.failOnUnmatchedPause === true ? {} : { onUnmatchedPause: warnOnUnmatchedPause }
4609
+ }, signal);
4610
+ winner = hit.session;
4611
+ const captureCount = command.captures.length;
4612
+ reportProgress?.(
4613
+ `Breakpoint hit; capturing ${captureCount.toString()} ${captureCount === 1 ? "expression" : "expressions"}...`
4614
+ );
4615
+ const result = await captureSnapshotResult(hit.session, hit.pause, command, opts, reportProgress);
4616
+ preserveWinner = opts.keepPaused === true;
4617
+ return { ...result, isolate: hit.session.isolate ?? { kind: "main" } };
4618
+ } catch (error) {
4619
+ if (error instanceof CfInspectorError && (error.code === "BREAKPOINT_NOT_HIT" || error.code === "UNRELATED_PAUSE_TIMEOUT")) {
4620
+ const outcomes = fanout.availableOutcomes();
4621
+ warnOnBoundBreakpointWithoutHit(outcomes.flatMap((outcome) => outcome.setup.handles));
4622
+ }
4623
+ throw error;
4624
+ } finally {
4625
+ const cleanup = await fanout.cleanup(2e3, preserveWinner ? winner : void 0);
4626
+ reportProgress?.(
4627
+ `Breakpoint cleanup: cleared ${cleanup.cleared.toString()} of ${cleanup.attempted.toString()}; resumed ${cleanup.resumed.toString()} paused losing isolates.`
4628
+ );
4629
+ }
4630
+ }, reportProgress, signal);
3700
4631
  }
3701
- async function runSnapshotOnSession(session, command, opts, reportProgress) {
4632
+ async function prepareSnapshotSession(session, command) {
3702
4633
  if (command.setupEvals.length > 0) {
3703
- const setupCount = command.setupEvals.length;
3704
- reportProgress?.(`Running ${setupCount.toString()} setup ${setupCount === 1 ? "evaluation" : "evaluations"}...`);
3705
4634
  await runSetupEvals(session, command.setupEvals);
3706
- reportProgress?.("Setup evaluation complete.");
3707
4635
  }
3708
4636
  if (command.condition !== void 0) {
3709
- reportProgress?.("Validating the breakpoint condition...");
3710
4637
  await validateExpression(session, command.condition);
3711
- reportProgress?.("Breakpoint condition is valid.");
3712
4638
  }
3713
- const breakpointCount = command.breakpoints.length;
3714
- reportProgress?.(
3715
- `Setting ${breakpointCount.toString()} ${breakpointCount === 1 ? "breakpoint" : "breakpoints"}...`
3716
- );
3717
- const handles = await setCommandBreakpoints(session, command);
3718
- const resolvedCount = handles.reduce(
3719
- (total, handle) => total + handle.resolvedLocations.length,
4639
+ }
4640
+ function reportBreakpointOutcomes(outcomes, reportProgress) {
4641
+ for (const outcome of outcomes) {
4642
+ warnOnUnboundBreakpoints(outcome.setup.handles);
4643
+ }
4644
+ const boundSessions = outcomes.filter((outcome) => outcome.setup.handles.some((handle) => handle.resolvedLocations.length > 0)).length;
4645
+ const locations = outcomes.reduce((total, outcome) => total + outcome.setup.handles.reduce(
4646
+ (sessionTotal, handle) => sessionTotal + handle.resolvedLocations.length,
3720
4647
  0
3721
- );
3722
- reportProgress?.(
3723
- `Breakpoint setup complete: ${resolvedCount.toString()} resolved ${resolvedCount === 1 ? "location" : "locations"}.`
3724
- );
3725
- warnOnUnboundBreakpoints(handles);
3726
- reportProgress?.(
3727
- `Waiting up to ${(command.timeoutMs / 1e3).toString()}s for a breakpoint hit...`
3728
- );
3729
- const pause = await waitForCommandPause(session, opts, handles, command.timeoutMs);
3730
- const captureCount = command.captures.length;
4648
+ ), 0);
4649
+ if (outcomes.length === 1) {
4650
+ reportProgress?.(
4651
+ `Breakpoint setup complete: ${locations.toString()} resolved ${locations === 1 ? "location" : "locations"}.`
4652
+ );
4653
+ return;
4654
+ }
3731
4655
  reportProgress?.(
3732
- `Breakpoint hit; capturing ${captureCount.toString()} ${captureCount === 1 ? "expression" : "expressions"}...`
4656
+ `Breakpoint setup complete: sessions=${outcomes.length.toString()} boundSessions=${boundSessions.toString()} resolvedLocations=${locations.toString()}.`
3733
4657
  );
3734
- const pausedStartedAt = pause.receivedAtMs ?? performance5.now();
4658
+ }
4659
+ async function captureSnapshotResult(session, pause, command, opts, reportProgress) {
4660
+ const pausedStartedAt = pause.receivedAtMs ?? performance6.now();
3735
4661
  const snapshot = await captureSnapshot(session, pause, {
3736
4662
  captures: command.captures,
3737
4663
  includeScopes: opts.includeScopes === true,
@@ -3741,13 +4667,12 @@ async function runSnapshotOnSession(session, command, opts, reportProgress) {
3741
4667
  throwOnSideEffect: command.throwOnSideEffect
3742
4668
  });
3743
4669
  if (opts.keepPaused === true) {
3744
- reportProgress?.("Snapshot captured; leaving the target paused as requested.");
3745
4670
  return withPausedDuration(snapshot, null);
3746
4671
  }
3747
4672
  reportProgress?.("Snapshot captured; resuming the target...");
3748
4673
  return await resumeAfterSnapshot(session, snapshot, pausedStartedAt, reportProgress);
3749
4674
  }
3750
- async function setCommandBreakpoints(session, command) {
4675
+ async function setCommandBreakpoints(session, command, onSet) {
3751
4676
  return await Promise.all(
3752
4677
  command.breakpoints.map(
3753
4678
  (bp) => setBreakpoint(session, {
@@ -3756,39 +4681,20 @@ async function setCommandBreakpoints(session, command) {
3756
4681
  remoteRoot: command.remoteRoot,
3757
4682
  ...command.condition === void 0 ? {} : { condition: command.condition },
3758
4683
  ...command.hitCount === void 0 ? {} : { hitCount: command.hitCount }
4684
+ }).then((handle) => {
4685
+ onSet?.(handle);
4686
+ return handle;
3759
4687
  })
3760
4688
  )
3761
4689
  );
3762
4690
  }
3763
- async function waitForCommandPause(session, opts, handles, timeoutMs) {
3764
- let warnedUnmatchedPause = false;
3765
- try {
3766
- return await waitForPause(session, {
3767
- timeoutMs,
3768
- breakpointIds: handles.map((h) => h.breakpointId),
3769
- unmatchedPausePolicy: opts.failOnUnmatchedPause === true ? "fail" : "wait-for-resume",
3770
- onUnmatchedPause: (unmatchedPause) => {
3771
- if (warnedUnmatchedPause || opts.failOnUnmatchedPause === true) {
3772
- return;
3773
- }
3774
- warnedUnmatchedPause = true;
3775
- warnOnUnmatchedPause(unmatchedPause);
3776
- }
3777
- });
3778
- } catch (error) {
3779
- if (error instanceof CfInspectorError && (error.code === "BREAKPOINT_NOT_HIT" || error.code === "UNRELATED_PAUSE_TIMEOUT")) {
3780
- warnOnBoundBreakpointWithoutHit(handles);
3781
- }
3782
- throw error;
3783
- }
3784
- }
3785
4691
  async function resumeAfterSnapshot(session, snapshot, pausedStartedAt, reportProgress) {
3786
4692
  try {
3787
4693
  await resume(session);
3788
4694
  reportProgress?.("Target resumed.");
3789
- return withPausedDuration(snapshot, roundDurationMs(performance5.now() - pausedStartedAt));
4695
+ return withPausedDuration(snapshot, roundDurationMs(performance6.now() - pausedStartedAt));
3790
4696
  } catch {
3791
- process9.stderr.write(
4697
+ process10.stderr.write(
3792
4698
  "[cf-inspector] warning: Debugger.resume failed after snapshot; pausedDurationMs is unknown.\n"
3793
4699
  );
3794
4700
  return withPausedDuration(snapshot, null);
@@ -3800,8 +4706,8 @@ function parseSetupEvals(raw) {
3800
4706
  }
3801
4707
 
3802
4708
  // src/cli/commands/watch.ts
3803
- import { performance as performance6 } from "perf_hooks";
3804
- import process10 from "process";
4709
+ import { performance as performance7 } from "perf_hooks";
4710
+ import process11 from "process";
3805
4711
  init_types();
3806
4712
  async function handleWatch(opts) {
3807
4713
  const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
@@ -3816,14 +4722,115 @@ async function handleWatch(opts) {
3816
4722
  let stoppedReason = "signal";
3817
4723
  let emitted = 0;
3818
4724
  await withTerminationSignal(async (signal) => {
3819
- await withSession(prepared.target, async (session) => {
3820
- const result = await runWatchLoop(session, prepared, opts, signal);
3821
- stoppedReason = result.stoppedReason;
3822
- emitted = result.emitted;
4725
+ await withSessions(prepared.target, async (group, port) => {
4726
+ const host = prepared.target.kind === "port" ? prepared.target.host : "127.0.0.1";
4727
+ const keepalive = startInspectorKeepalive(host, port);
4728
+ const commandAbort = new AbortController();
4729
+ const onSignal = () => {
4730
+ commandAbort.abort();
4731
+ };
4732
+ signal.addEventListener("abort", onSignal, { once: true });
4733
+ let keepaliveError;
4734
+ void keepalive.failure.catch((error) => {
4735
+ keepaliveError = error;
4736
+ commandAbort.abort();
4737
+ });
4738
+ try {
4739
+ const result = await runWatchGroup(group, prepared, opts, commandAbort.signal);
4740
+ stoppedReason = result.stoppedReason;
4741
+ emitted = result.emitted;
4742
+ if (keepaliveError !== void 0) {
4743
+ throw keepaliveError instanceof Error ? keepaliveError : new Error("Unknown inspector keepalive failure");
4744
+ }
4745
+ } finally {
4746
+ keepalive.cancel();
4747
+ signal.removeEventListener("abort", onSignal);
4748
+ }
3823
4749
  }, void 0, signal);
3824
4750
  });
3825
4751
  writeWatchSummary(stoppedReason, emitted, opts.json);
3826
4752
  }
4753
+ async function runWatchGroup(group, command, opts, signal) {
4754
+ const fanout = new BreakpointFanout(group, async (session, trackHandle) => {
4755
+ if (command.setupEvals.length > 0) {
4756
+ await runSetupEvals(session, command.setupEvals);
4757
+ }
4758
+ if (command.condition !== void 0) {
4759
+ await validateExpression(session, command.condition);
4760
+ }
4761
+ const handles = await Promise.all(command.breakpoints.map((bp) => setBreakpoint(session, {
4762
+ file: bp.file,
4763
+ line: bp.line,
4764
+ remoteRoot: command.remoteRoot,
4765
+ ...command.condition === void 0 ? {} : { condition: command.condition },
4766
+ ...command.hitCount === void 0 ? {} : { hitCount: command.hitCount }
4767
+ }).then((handle) => {
4768
+ trackHandle(handle);
4769
+ return handle;
4770
+ })));
4771
+ warnOnUnboundBreakpoints(handles);
4772
+ return { handles };
4773
+ });
4774
+ let emitted = 0;
4775
+ let stoppedReason = "signal";
4776
+ const deadline = computeDeadline(command.durationMs);
4777
+ try {
4778
+ await fanout.ready();
4779
+ while (!signal.aborted) {
4780
+ const remainingMs = remainingForLoop(deadline, command.perHitTimeoutMs);
4781
+ if (remainingMs <= 0) {
4782
+ stoppedReason = "duration";
4783
+ break;
4784
+ }
4785
+ let hit;
4786
+ try {
4787
+ hit = await fanout.waitForFirst(remainingMs, { unmatchedPausePolicy: "wait-for-resume" }, signal);
4788
+ } catch (error) {
4789
+ if (error instanceof CfInspectorError && error.code === "ABORTED") {
4790
+ stoppedReason = "signal";
4791
+ break;
4792
+ }
4793
+ if (error instanceof CfInspectorError && (error.code === "BREAKPOINT_NOT_HIT" || error.code === "UNRELATED_PAUSE_TIMEOUT")) {
4794
+ if (deadline !== void 0 && performance7.now() >= deadline) {
4795
+ stoppedReason = "duration";
4796
+ break;
4797
+ }
4798
+ continue;
4799
+ }
4800
+ throw error;
4801
+ }
4802
+ const event = await captureWatchEvent(hit.session, command, hit.pause, emitted + 1, opts);
4803
+ emitted += 1;
4804
+ writeWatchEvent({ ...event, isolate: hit.session.isolate ?? { kind: "main" } }, opts.json);
4805
+ try {
4806
+ await resume(hit.session);
4807
+ hit.session.debuggerState.paused = false;
4808
+ } catch {
4809
+ process11.stderr.write("[cf-inspector] warning: Debugger.resume failed during watch.\n");
4810
+ stoppedReason = "transport-closed";
4811
+ break;
4812
+ }
4813
+ if (command.maxEvents !== void 0 && emitted >= command.maxEvents) {
4814
+ stoppedReason = "max-events";
4815
+ break;
4816
+ }
4817
+ }
4818
+ if (signal.aborted) {
4819
+ stoppedReason = "signal";
4820
+ }
4821
+ } finally {
4822
+ const cleanup = await fanout.cleanup();
4823
+ process11.stderr.write(
4824
+ `[cf-inspector] breakpoint cleanup: cleared ${cleanup.cleared.toString()} of ${cleanup.attempted.toString()}; resumed ${cleanup.resumed.toString()} paused isolates.
4825
+ `
4826
+ );
4827
+ }
4828
+ if (emitted === 0 && (stoppedReason === "duration" || stoppedReason === "signal")) {
4829
+ const outcomes = fanout.availableOutcomes();
4830
+ warnOnBoundBreakpointWithoutHit(outcomes.flatMap((outcome) => outcome.setup.handles));
4831
+ }
4832
+ return { emitted, stoppedReason };
4833
+ }
3827
4834
  function prepareWatchCommand(opts, target) {
3828
4835
  if (opts.bp.length === 0) {
3829
4836
  throw new CfInspectorError(
@@ -3861,147 +4868,21 @@ function prepareWatchCommand(opts, target) {
3861
4868
  throwOnSideEffect: opts.allowMutation !== true
3862
4869
  };
3863
4870
  }
3864
- async function runWatchLoop(session, command, opts, signal) {
3865
- if (command.setupEvals.length > 0) {
3866
- await runSetupEvals(session, command.setupEvals);
3867
- }
3868
- if (command.condition !== void 0) {
3869
- await validateExpression(session, command.condition);
3870
- }
3871
- const handles = await Promise.all(
3872
- command.breakpoints.map(
3873
- (bp) => setBreakpoint(session, {
3874
- file: bp.file,
3875
- line: bp.line,
3876
- remoteRoot: command.remoteRoot,
3877
- ...command.condition === void 0 ? {} : { condition: command.condition },
3878
- ...command.hitCount === void 0 ? {} : { hitCount: command.hitCount }
3879
- })
3880
- )
3881
- );
3882
- warnOnUnboundBreakpoints(handles);
3883
- const deadline = computeDeadline(command.durationMs);
3884
- let emitted = 0;
3885
- const state = { stopped: false, reason: "signal" };
3886
- const setStop = (reason) => {
3887
- if (state.stopped) {
3888
- return;
3889
- }
3890
- state.reason = reason;
3891
- state.stopped = true;
3892
- };
3893
- const transportClosed = waitForTransportClose(session);
3894
- transportClosed.promise.then(() => {
3895
- setStop("transport-closed");
3896
- }).catch(() => {
3897
- });
3898
- try {
3899
- while (!state.stopped) {
3900
- if (signal.aborted) {
3901
- setStop("signal");
3902
- break;
3903
- }
3904
- const remainingMs = remainingForLoop(deadline, command.perHitTimeoutMs);
3905
- if (remainingMs <= 0) {
3906
- setStop("duration");
3907
- break;
3908
- }
3909
- const pause = await waitForNextWatchPause(session, handles, remainingMs, signal);
3910
- if (pause === "signal") {
3911
- setStop("signal");
3912
- break;
3913
- }
3914
- if (pause === "timeout") {
3915
- if (deadline !== void 0 && performance6.now() >= deadline) {
3916
- setStop("duration");
3917
- break;
3918
- }
3919
- continue;
3920
- }
3921
- const event = await captureWatchEvent(session, command, pause, emitted + 1, opts);
3922
- emitted += 1;
3923
- writeWatchEvent(event, opts.json);
3924
- try {
3925
- await resume(session);
3926
- } catch {
3927
- process10.stderr.write("[cf-inspector] warning: Debugger.resume failed during watch.\n");
3928
- setStop("transport-closed");
3929
- break;
3930
- }
3931
- if (command.maxEvents !== void 0 && emitted >= command.maxEvents) {
3932
- setStop("max-events");
3933
- break;
3934
- }
3935
- }
3936
- } finally {
3937
- transportClosed.cancel();
3938
- }
3939
- if (emitted === 0 && (state.reason === "duration" || state.reason === "signal")) {
3940
- warnOnBoundBreakpointWithoutHit(handles);
3941
- }
3942
- return { emitted, stoppedReason: state.reason };
3943
- }
3944
4871
  function computeDeadline(durationMs) {
3945
4872
  if (durationMs === void 0) {
3946
4873
  return void 0;
3947
4874
  }
3948
- return performance6.now() + durationMs;
4875
+ return performance7.now() + durationMs;
3949
4876
  }
3950
4877
  function remainingForLoop(deadline, perHitTimeoutMs) {
3951
4878
  if (deadline === void 0) {
3952
4879
  return perHitTimeoutMs;
3953
4880
  }
3954
- const remaining = deadline - performance6.now();
3955
- if (remaining <= 0) {
4881
+ const remaining2 = deadline - performance7.now();
4882
+ if (remaining2 <= 0) {
3956
4883
  return 0;
3957
4884
  }
3958
- return Math.min(remaining, perHitTimeoutMs);
3959
- }
3960
- function waitForTransportClose(session) {
3961
- let cancelled = false;
3962
- let resolve;
3963
- const promise = new Promise((res) => {
3964
- resolve = res;
3965
- });
3966
- const off = session.client.onClose(() => {
3967
- if (!cancelled) {
3968
- resolve?.();
3969
- }
3970
- });
3971
- return {
3972
- promise,
3973
- cancel: () => {
3974
- cancelled = true;
3975
- off();
3976
- resolve?.();
3977
- }
3978
- };
3979
- }
3980
- async function waitForNextWatchPause(session, handles, timeoutMs, signal) {
3981
- if (signal.aborted) {
3982
- return "signal";
3983
- }
3984
- try {
3985
- return await waitForPause(session, {
3986
- timeoutMs,
3987
- breakpointIds: handles.map((h) => h.breakpointId),
3988
- unmatchedPausePolicy: "wait-for-resume",
3989
- signal
3990
- });
3991
- } catch (err) {
3992
- if (err instanceof CfInspectorError) {
3993
- if (err.code === "ABORTED") {
3994
- return "signal";
3995
- }
3996
- if (err.code === "BREAKPOINT_NOT_HIT") {
3997
- return "timeout";
3998
- }
3999
- if (err.code === "UNRELATED_PAUSE_TIMEOUT") {
4000
- return "timeout";
4001
- }
4002
- }
4003
- throw err;
4004
- }
4885
+ return Math.min(remaining2, perHitTimeoutMs);
4005
4886
  }
4006
4887
  async function captureWatchEvent(session, command, pause, hit, opts) {
4007
4888
  const snapshot = await captureSnapshot(session, pause, {
@@ -4037,11 +4918,11 @@ function formatLocation(command, topFrame) {
4037
4918
  }
4038
4919
  function writeWatchSummary(reason, emitted, json) {
4039
4920
  if (json) {
4040
- process10.stderr.write(`${JSON.stringify({ stopped: reason, emitted })}
4921
+ process11.stderr.write(`${JSON.stringify({ stopped: reason, emitted })}
4041
4922
  `);
4042
4923
  return;
4043
4924
  }
4044
- process10.stderr.write(
4925
+ process11.stderr.write(
4045
4926
  `Stopped (${reason}); emitted ${emitted.toString()} watch ${emitted === 1 ? "event" : "events"}.
4046
4927
  `
4047
4928
  );
@@ -4061,7 +4942,7 @@ function applyTargetOptions(cmd, options = {}) {
4061
4942
  "--target <index>",
4062
4943
  "Inspector target index from /json/list (default: 0)"
4063
4944
  );
4064
- const withWorkerOption = options.includeWorker === false ? withTargetOption : withTargetOption.option("--worker <index>", "NodeWorker sub-session index listed by list-targets");
4945
+ const withWorkerOption = options.includeWorker === false ? withTargetOption : withTargetOption.option("--worker <index>", "NodeWorker sub-session index listed by list-targets").option("--worker-id <id>", "Stable NodeWorker workerId listed by list-targets").option("--main-only", "Attach only to the main isolate and ignore workers");
4065
4946
  return options.includeTimeout === false ? withWorkerOption : withWorkerOption.option("--timeout <seconds>", "Timeout for CF tunnel readiness in seconds (default: 180)");
4066
4947
  }
4067
4948
  var collectStrings = (value, prev = []) => [
@@ -4093,12 +4974,22 @@ async function main(argv) {
4093
4974
  registerLog(program);
4094
4975
  registerWatch(program);
4095
4976
  registerException(program);
4977
+ registerCheckBreakpoint(program);
4096
4978
  registerEval(program);
4097
4979
  registerListScripts(program);
4098
4980
  registerListTargets(program);
4099
4981
  registerAttach(program);
4100
4982
  await program.parseAsync([...argv]);
4101
4983
  }
4984
+ function registerCheckBreakpoint(program) {
4985
+ applyTargetOptions(
4986
+ program.command("check-breakpoint").description(
4987
+ "Report whether a loaded script can break at a file:line location"
4988
+ )
4989
+ ).requiredOption("--bp <file:line>", "Source location to check").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--no-json", "Print a human-readable result instead of JSON").action(async (opts) => {
4990
+ await handleCheckBreakpoint(opts);
4991
+ });
4992
+ }
4102
4993
  function registerSnapshot(program) {
4103
4994
  applyTargetOptions(
4104
4995
  program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume"),
@@ -4169,20 +5060,20 @@ function registerAttach(program) {
4169
5060
  // src/cli.ts
4170
5061
  init_types();
4171
5062
  try {
4172
- await main(process11.argv);
5063
+ await main(process12.argv);
4173
5064
  } catch (err) {
4174
5065
  if (err instanceof CfInspectorError) {
4175
- process11.stderr.write(`Error [${err.code}]: ${err.message}
5066
+ process12.stderr.write(`Error [${err.code}]: ${err.message}
4176
5067
  `);
4177
5068
  if (err.detail !== void 0) {
4178
- process11.stderr.write(` detail: ${err.detail}
5069
+ process12.stderr.write(` detail: ${err.detail}
4179
5070
  `);
4180
5071
  }
4181
- process11.exit(1);
5072
+ process12.exit(1);
4182
5073
  }
4183
5074
  const message = err instanceof Error ? err.message : String(err);
4184
- process11.stderr.write(`Error: ${message}
5075
+ process12.stderr.write(`Error: ${message}
4185
5076
  `);
4186
- process11.exit(1);
5077
+ process12.exit(1);
4187
5078
  }
4188
5079
  //# sourceMappingURL=cli.js.map