@saptools/cf-inspector 0.7.0 → 0.8.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,16 +152,16 @@ var init_wsTransport = __esm({
152
152
  });
153
153
 
154
154
  // src/cli.ts
155
- import process12 from "process";
155
+ import process13 from "process";
156
156
 
157
157
  // src/cli/program.ts
158
158
  import { readFileSync } from "fs";
159
- import { dirname, join } from "path";
159
+ import { dirname, join as join2 } from "path";
160
160
  import { fileURLToPath } from "url";
161
161
  import { Command } from "commander";
162
162
 
163
163
  // src/cli/commands/attach.ts
164
- import process3 from "process";
164
+ import process4 from "process";
165
165
 
166
166
  // src/inspector/discovery.ts
167
167
  init_types();
@@ -386,13 +386,22 @@ function startInspectorKeepalive(host, port, options = {}) {
386
386
  }
387
387
 
388
388
  // src/cli/output.ts
389
- import process from "process";
389
+ import process2 from "process";
390
390
  function writeProgress(message) {
391
- process.stderr.write(`[cf-inspector] ${message}
391
+ process2.stderr.write(`[cf-inspector] ${message}
392
+ `);
393
+ }
394
+ function writeArmedEvent(event) {
395
+ const payload = {
396
+ event: "breakpoint-armed",
397
+ schemaVersion: 1,
398
+ ...event
399
+ };
400
+ process2.stderr.write(`${JSON.stringify(payload)}
392
401
  `);
393
402
  }
394
403
  function writeJson(value) {
395
- process.stdout.write(`${JSON.stringify(value, null, 2)}
404
+ process2.stdout.write(`${JSON.stringify(value, null, 2)}
396
405
  `);
397
406
  }
398
407
  function writeHumanSnapshot(snapshot) {
@@ -423,7 +432,7 @@ function writeHumanSnapshot(snapshot) {
423
432
  appendStackFrameLine(lines, frame);
424
433
  }
425
434
  }
426
- process.stdout.write(`${lines.join("\n")}
435
+ process2.stdout.write(`${lines.join("\n")}
427
436
  `);
428
437
  }
429
438
  function appendFrameLines(lines, frame) {
@@ -466,36 +475,36 @@ function appendExceptionLines(lines, exception) {
466
475
  }
467
476
  function writeLogEvent(event, json) {
468
477
  if (json) {
469
- process.stdout.write(`${JSON.stringify(event)}
478
+ process2.stdout.write(`${JSON.stringify(event)}
470
479
  `);
471
480
  return;
472
481
  }
473
482
  const isolateSuffix = event.isolate === void 0 ? "" : ` (${formatIsolate(event.isolate)})`;
474
483
  if (event.error !== void 0) {
475
- process.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} !err ${renderTruncated(event.error, event)}
484
+ process2.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} !err ${renderTruncated(event.error, event)}
476
485
  `);
477
486
  return;
478
487
  }
479
- process.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} ${renderTruncated(event.value ?? "", event)}
488
+ process2.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} ${renderTruncated(event.value ?? "", event)}
480
489
  `);
481
490
  }
482
491
  function writeWatchEvent(event, json) {
483
492
  if (json) {
484
- process.stdout.write(`${JSON.stringify(event)}
493
+ process2.stdout.write(`${JSON.stringify(event)}
485
494
  `);
486
495
  return;
487
496
  }
488
- process.stdout.write(
497
+ process2.stdout.write(
489
498
  `[${event.ts}] hit#${event.hit.toString()} ${event.at} (${formatIsolate(event.isolate)})
490
499
  `
491
500
  );
492
501
  if (event.exception !== void 0) {
493
- process.stdout.write(` exception: ${renderExceptionDetail(event.exception)}
502
+ process2.stdout.write(` exception: ${renderExceptionDetail(event.exception)}
494
503
  `);
495
504
  }
496
505
  for (const capture of event.captures) {
497
506
  const detail = capture.error ?? capture.value ?? "undefined";
498
- process.stdout.write(` ${capture.expression} = ${renderTruncated(detail, capture)}
507
+ process2.stdout.write(` ${capture.expression} = ${renderTruncated(detail, capture)}
499
508
  `);
500
509
  }
501
510
  }
@@ -1776,9 +1785,247 @@ var DEFAULT_BREAKPOINT_TIMEOUT_SEC = 30;
1776
1785
  var DEFAULT_CF_TIMEOUT_SEC = 180;
1777
1786
  var DEFAULT_EXCEPTION_TIMEOUT_SEC = 30;
1778
1787
 
1788
+ // src/cli/sessionLock.ts
1789
+ init_types();
1790
+ import { execFileSync } from "child_process";
1791
+ import { createHash, randomUUID } from "crypto";
1792
+ import { constants } from "fs";
1793
+ import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from "fs/promises";
1794
+ import { homedir } from "os";
1795
+ import { join } from "path";
1796
+ var ELECTION_WINDOW_MS = 25;
1797
+ var LOCK_FILE_SUFFIX = ".lock";
1798
+ async function acquireDebugSessionLock(target, options = {}) {
1799
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1800
+ const pid = options.pid ?? process.pid;
1801
+ const getProcessStart = options.getProcessStart ?? processStart;
1802
+ const ownerProcessStart = getProcessStart(pid);
1803
+ const token = options.token?.() ?? randomUUID();
1804
+ const targetIdentity = debugTargetIdentity(target);
1805
+ const key = createHash("sha256").update(targetIdentity).digest("hex");
1806
+ const lockRoot = options.stateRoot ?? defaultStateRoot();
1807
+ const lockDirectory = join(lockRoot, "cf-inspector", "locks");
1808
+ const ownPath = join(lockDirectory, `${key}.${pid.toString()}.${token}${LOCK_FILE_SUFFIX}`);
1809
+ const metadata = {
1810
+ pid,
1811
+ ...ownerProcessStart === void 0 ? {} : { processStart: ownerProcessStart },
1812
+ state: "pending",
1813
+ startedAt: now().toISOString(),
1814
+ token,
1815
+ target: targetIdentity
1816
+ };
1817
+ await mkdir(lockDirectory, { recursive: true, mode: 448 });
1818
+ const handle = await open(ownPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
1819
+ try {
1820
+ await handle.writeFile(`${JSON.stringify(metadata)}
1821
+ `, "utf8");
1822
+ } finally {
1823
+ await handle.close();
1824
+ }
1825
+ try {
1826
+ await new Promise((resolve) => {
1827
+ setTimeout(resolve, ELECTION_WINDOW_MS);
1828
+ });
1829
+ const contenders = await findLiveContenders(
1830
+ lockDirectory,
1831
+ key,
1832
+ ownPath,
1833
+ options.isProcessAlive ?? processIsAlive,
1834
+ getProcessStart
1835
+ );
1836
+ const owner = contenders.find((candidate) => candidate.state === "owned") ?? contenders.filter((candidate) => candidate.state === "pending" && candidate.token < token).sort((left, right) => left.token.localeCompare(right.token))[0];
1837
+ if (owner !== void 0) {
1838
+ throw alreadyDebuggedError(owner);
1839
+ }
1840
+ await writeLockMetadata(ownPath, { ...metadata, state: "owned" });
1841
+ } catch (error) {
1842
+ await unlink(ownPath).catch(() => {
1843
+ });
1844
+ throw error;
1845
+ }
1846
+ let released = false;
1847
+ return {
1848
+ path: ownPath,
1849
+ release: async () => {
1850
+ if (released) {
1851
+ return;
1852
+ }
1853
+ released = true;
1854
+ const current = await readLockMetadata(ownPath);
1855
+ if (current?.token !== token || current.pid !== pid) {
1856
+ return;
1857
+ }
1858
+ await unlink(ownPath).catch((error) => {
1859
+ if (!isNodeError(error, "ENOENT")) {
1860
+ throw error;
1861
+ }
1862
+ });
1863
+ }
1864
+ };
1865
+ }
1866
+ function debugTargetIdentity(target) {
1867
+ const targetIndex = target.targetIndex ?? 0;
1868
+ if (target.kind === "port") {
1869
+ return JSON.stringify({
1870
+ kind: "port",
1871
+ host: normalizeHost(target.host),
1872
+ port: target.port,
1873
+ targetIndex
1874
+ });
1875
+ }
1876
+ return JSON.stringify({
1877
+ kind: "cf",
1878
+ region: target.region,
1879
+ org: target.org,
1880
+ space: target.space,
1881
+ app: target.app,
1882
+ targetIndex
1883
+ });
1884
+ }
1885
+ function defaultStateRoot() {
1886
+ const configured = process.env["CF_INSPECTOR_STATE_DIR"]?.trim();
1887
+ return configured === void 0 || configured.length === 0 ? join(homedir(), ".saptools") : configured;
1888
+ }
1889
+ async function findLiveContenders(lockDirectory, key, ownPath, isProcessAlive, getProcessStart) {
1890
+ const prefix = `${key}.`;
1891
+ const names = await readdir(lockDirectory);
1892
+ const contenders = [];
1893
+ for (const name of names) {
1894
+ if (!name.startsWith(prefix) || !name.endsWith(LOCK_FILE_SUFFIX)) {
1895
+ continue;
1896
+ }
1897
+ const path = join(lockDirectory, name);
1898
+ if (path === ownPath) {
1899
+ continue;
1900
+ }
1901
+ const info = await stat(path).catch(() => {
1902
+ });
1903
+ const metadata = await readLockMetadata(path) ?? (info === void 0 ? void 0 : metadataFromFilename(name, key, info.mtimeMs));
1904
+ if (metadata !== void 0) {
1905
+ if (ownerIsAlive(metadata, isProcessAlive, getProcessStart)) {
1906
+ contenders.push(metadata);
1907
+ } else {
1908
+ await unlink(path).catch(() => {
1909
+ });
1910
+ }
1911
+ continue;
1912
+ }
1913
+ if (info !== void 0) {
1914
+ contenders.push({
1915
+ pid: 0,
1916
+ state: "owned",
1917
+ startedAt: new Date(info.mtimeMs).toISOString(),
1918
+ token: "unknown",
1919
+ target: "unknown"
1920
+ });
1921
+ }
1922
+ }
1923
+ return contenders;
1924
+ }
1925
+ async function readLockMetadata(path) {
1926
+ try {
1927
+ const parsed = JSON.parse(await readFile(path, "utf8"));
1928
+ if (!isRecord2(parsed)) {
1929
+ return void 0;
1930
+ }
1931
+ const pid = parsed["pid"];
1932
+ const processStart2 = parsed["processStart"];
1933
+ const state = parsed["state"];
1934
+ const startedAt = parsed["startedAt"];
1935
+ const token = parsed["token"];
1936
+ const target = parsed["target"];
1937
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0 || processStart2 !== void 0 && typeof processStart2 !== "string" || state !== "pending" && state !== "owned" || typeof startedAt !== "string" || Number.isNaN(Date.parse(startedAt)) || typeof token !== "string" || token.length === 0 || typeof target !== "string" || target.length === 0) {
1938
+ return void 0;
1939
+ }
1940
+ return {
1941
+ pid,
1942
+ ...typeof processStart2 === "string" ? { processStart: processStart2 } : {},
1943
+ state,
1944
+ startedAt,
1945
+ token,
1946
+ target
1947
+ };
1948
+ } catch {
1949
+ return void 0;
1950
+ }
1951
+ }
1952
+ async function writeLockMetadata(path, metadata) {
1953
+ await writeFile(path, `${JSON.stringify(metadata)}
1954
+ `, { encoding: "utf8", mode: 384 });
1955
+ }
1956
+ function metadataFromFilename(name, key, mtimeMs) {
1957
+ const match = new RegExp(`^${key}\\.(\\d+)\\.(.+)\\${LOCK_FILE_SUFFIX}$`, "u").exec(name);
1958
+ const rawPid = match?.[1];
1959
+ const token = match?.[2];
1960
+ if (rawPid === void 0 || token === void 0 || token.length === 0) {
1961
+ return void 0;
1962
+ }
1963
+ const pid = Number.parseInt(rawPid, 10);
1964
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
1965
+ return void 0;
1966
+ }
1967
+ return {
1968
+ pid,
1969
+ state: "owned",
1970
+ startedAt: new Date(mtimeMs).toISOString(),
1971
+ token,
1972
+ target: "unknown"
1973
+ };
1974
+ }
1975
+ function ownerIsAlive(metadata, isProcessAlive, getProcessStart) {
1976
+ if (!isProcessAlive(metadata.pid)) {
1977
+ return false;
1978
+ }
1979
+ const currentStart = getProcessStart(metadata.pid);
1980
+ return metadata.processStart === void 0 || currentStart === void 0 || metadata.processStart === currentStart;
1981
+ }
1982
+ function processIsAlive(pid) {
1983
+ try {
1984
+ process.kill(pid, 0);
1985
+ const status = processStatus(pid);
1986
+ return !status?.startsWith("Z");
1987
+ } catch (error) {
1988
+ return isNodeError(error, "EPERM");
1989
+ }
1990
+ }
1991
+ function processStatus(pid) {
1992
+ return runPs(pid, "stat=");
1993
+ }
1994
+ function processStart(pid) {
1995
+ return runPs(pid, "lstart=");
1996
+ }
1997
+ function runPs(pid, field) {
1998
+ try {
1999
+ const value = execFileSync("ps", ["-o", field, "-p", pid.toString()], {
2000
+ encoding: "utf8",
2001
+ stdio: ["ignore", "pipe", "ignore"]
2002
+ }).trim();
2003
+ return value.length === 0 ? void 0 : value;
2004
+ } catch {
2005
+ return void 0;
2006
+ }
2007
+ }
2008
+ function alreadyDebuggedError(owner) {
2009
+ const ownerLabel = owner.pid > 0 ? `PID ${owner.pid.toString()}` : "an unknown process";
2010
+ return new CfInspectorError(
2011
+ "TARGET_ALREADY_DEBUGGED",
2012
+ `Another cf-inspector session (${ownerLabel}, started ${owner.startedAt}) is already actively debugging this target. Concurrent debugging sessions on the same isolate(s) can corrupt each other and disrupt real application traffic, so this attempt was refused rather than queued. Wait for the other session to finish, or confirm that it is gone before retrying; locks from dead processes are reclaimed automatically.`
2013
+ );
2014
+ }
2015
+ function normalizeHost(host) {
2016
+ const normalized = host.trim().toLowerCase();
2017
+ return normalized === "localhost" || normalized === "::1" ? "127.0.0.1" : normalized;
2018
+ }
2019
+ function isRecord2(value) {
2020
+ return typeof value === "object" && value !== null;
2021
+ }
2022
+ function isNodeError(error, code) {
2023
+ return error instanceof Error && "code" in error && error.code === code;
2024
+ }
2025
+
1779
2026
  // src/cli/warnings.ts
1780
2027
  init_types();
1781
- import process2 from "process";
2028
+ import process3 from "process";
1782
2029
 
1783
2030
  // src/cli/captureParser.ts
1784
2031
  function parseCaptureList(raw) {
@@ -1893,7 +2140,7 @@ function warnOnCaptureMutationRisk(expressions, allowMutation) {
1893
2140
  return;
1894
2141
  }
1895
2142
  const suffix = allowMutation ? "will run without the V8 side-effect guard because --allow-mutation was passed." : "will be checked by the V8 side-effect guard and blocked unless V8 proves them safe; pass --allow-mutation to run them unrestricted.";
1896
- process2.stderr.write(
2143
+ process3.stderr.write(
1897
2144
  `[cf-inspector] warning: ${riskyCount.toString()} capture ${riskyCount === 1 ? "expression looks" : "expressions look"} mutation-capable and ${suffix}
1898
2145
  `
1899
2146
  );
@@ -1908,7 +2155,7 @@ function enforceNativeConditionMutationPolicy(expression, allowMutation, context
1908
2155
  `${context} looks mutation-capable. Native breakpoint conditions cannot be protected by V8's side-effect guard; pass --allow-mutation to arm it explicitly.`
1909
2156
  );
1910
2157
  }
1911
- process2.stderr.write(
2158
+ process3.stderr.write(
1912
2159
  `[cf-inspector] warning: ${context} looks mutation-capable and will run as a native breakpoint condition; native conditions cannot be side-effect-gated.
1913
2160
  `
1914
2161
  );
@@ -1917,7 +2164,7 @@ function warnOnMutationRisk(expression, context) {
1917
2164
  if (!looksLikeMutation(expression)) {
1918
2165
  return;
1919
2166
  }
1920
- process2.stderr.write(
2167
+ process3.stderr.write(
1921
2168
  `[cf-inspector] warning: ${context} looks mutation-capable and will execute against the live inspectee without a side-effect guard.
1922
2169
  `
1923
2170
  );
@@ -1926,7 +2173,7 @@ function warnOnUnboundBreakpoints(handles) {
1926
2173
  for (const handle of handles) {
1927
2174
  if (handle.resolvedLocations.length === 0) {
1928
2175
  const tsHint = handle.file.endsWith(".ts") ? " Hint: Source TS breakpoints may not bind. Try inspecting loaded scripts with list-scripts and target the compiled .js file instead." : "";
1929
- process2.stderr.write(
2176
+ process3.stderr.write(
1930
2177
  `[cf-inspector] warning: breakpoint ${handle.file}:${handle.line.toString()} did not bind to any loaded script. Check the path or pass --remote-root. Use 'list-scripts' to inspect what V8 currently has loaded.${tsHint}
1931
2178
  `
1932
2179
  );
@@ -1937,14 +2184,14 @@ function warnOnImplicitInspectorSelection(session, targetWasExplicit, workerWasE
1937
2184
  const targetCount = session.targetCount ?? 1;
1938
2185
  const targetIndex = session.targetIndex ?? 0;
1939
2186
  if (!targetWasExplicit && targetCount > 1) {
1940
- process2.stderr.write(
2187
+ process3.stderr.write(
1941
2188
  `[cf-inspector] notice: attached to inspector target ${targetIndex.toString()} of ${targetCount.toString()}; pass --target <index> to pick another.
1942
2189
  `
1943
2190
  );
1944
2191
  }
1945
2192
  const workerCount = session.workerTargets?.length ?? 0;
1946
2193
  if (!workerWasExplicit && workerCount > 0) {
1947
- process2.stderr.write(
2194
+ process3.stderr.write(
1948
2195
  `[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.
1949
2196
  `
1950
2197
  );
@@ -1957,7 +2204,7 @@ function warnOnBoundBreakpointWithoutHit(handles) {
1957
2204
  if (boundCount === 0) {
1958
2205
  return;
1959
2206
  }
1960
- process2.stderr.write(
2207
+ process3.stderr.write(
1961
2208
  `[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.
1962
2209
  `
1963
2210
  );
@@ -1967,7 +2214,7 @@ function roundDurationMs(durationMs) {
1967
2214
  }
1968
2215
  function warnOnUnmatchedPause(pause) {
1969
2216
  const reason = pause.reason.length > 0 ? pause.reason : "unknown";
1970
- process2.stderr.write(
2217
+ process3.stderr.write(
1971
2218
  `[cf-inspector] warning: target is paused by another debugger event (${reason} at ${formatPauseLocation(pause)}); waiting for it to resume...
1972
2219
  `
1973
2220
  );
@@ -2142,65 +2389,75 @@ function parseWorkerId(value) {
2142
2389
  return trimmed;
2143
2390
  }
2144
2391
  async function withSession(target, fn, reportProgress, signal) {
2145
- const tunnel = await openTarget(target, reportProgress, signal);
2146
- let session;
2392
+ const lock = await acquireDebugSessionLock(target);
2147
2393
  try {
2148
- reportProgress?.(
2149
- `Connecting to the Node.js inspector at ${tunnel.host}:${tunnel.port.toString()}...`
2150
- );
2151
- session = await connectInspector({
2152
- port: tunnel.port,
2153
- host: tunnel.host,
2154
- ...selectionOptions(target.targetIndex, target.workerIndex),
2155
- ...target.workerId === void 0 ? {} : { workerId: target.workerId }
2156
- });
2157
- warnOnImplicitInspectorSelection(
2158
- session,
2159
- target.targetIndex !== void 0,
2160
- target.workerIndex !== void 0 || target.workerId !== void 0
2161
- );
2162
- reportProgress?.("Inspector session is ready.");
2163
- return await fn(session, tunnel.port);
2164
- } finally {
2165
- if (session) {
2166
- reportProgress?.("Closing the inspector session...");
2167
- await session.dispose();
2168
- reportProgress?.("Inspector session closed.");
2169
- }
2170
- await tunnel.dispose();
2171
- }
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({
2394
+ const tunnel = await openTarget(target, reportProgress, signal);
2395
+ let session;
2396
+ try {
2397
+ reportProgress?.(
2398
+ `Connecting to the Node.js inspector at ${tunnel.host}:${tunnel.port.toString()}...`
2399
+ );
2400
+ session = await connectInspector({
2185
2401
  port: tunnel.port,
2186
2402
  host: tunnel.host,
2187
- ...selectionOptions(target.targetIndex, target.workerIndex, target.workerId)
2403
+ ...selectionOptions(target.targetIndex, target.workerIndex),
2404
+ ...target.workerId === void 0 ? {} : { workerId: target.workerId }
2188
2405
  });
2189
- group = singleSessionGroup(session);
2406
+ warnOnImplicitInspectorSelection(
2407
+ session,
2408
+ target.targetIndex !== void 0,
2409
+ target.workerIndex !== void 0 || target.workerId !== void 0
2410
+ );
2411
+ reportProgress?.("Inspector session is ready.");
2412
+ return await fn(session, tunnel.port);
2413
+ } finally {
2414
+ if (session) {
2415
+ reportProgress?.("Closing the inspector session...");
2416
+ await session.dispose();
2417
+ reportProgress?.("Inspector session closed.");
2418
+ }
2419
+ await tunnel.dispose();
2190
2420
  }
2191
- reportProgress?.("Inspector session is ready.");
2192
- return await fn(group, tunnel.port);
2193
2421
  } finally {
2422
+ await lock.release();
2423
+ }
2424
+ }
2425
+ async function withSessions(target, fn, reportProgress, signal) {
2426
+ const lock = await acquireDebugSessionLock(target);
2427
+ try {
2428
+ const tunnel = await openTarget(target, reportProgress, signal);
2429
+ let group;
2194
2430
  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.");
2431
+ reportProgress?.(
2432
+ `Connecting to the Node.js inspector at ${tunnel.host}:${tunnel.port.toString()}...`
2433
+ );
2434
+ const autoAttach = target.targetIndex === void 0 && target.workerIndex === void 0 && target.workerId === void 0 && target.mainOnly !== true;
2435
+ if (autoAttach) {
2436
+ group = await connectInspectorGroup({ port: tunnel.port, host: tunnel.host });
2437
+ } else {
2438
+ const session = await connectInspector({
2439
+ port: tunnel.port,
2440
+ host: tunnel.host,
2441
+ ...selectionOptions(target.targetIndex, target.workerIndex, target.workerId)
2442
+ });
2443
+ group = singleSessionGroup(session);
2200
2444
  }
2445
+ reportProgress?.("Inspector session is ready.");
2446
+ return await fn(group, tunnel.port);
2201
2447
  } finally {
2202
- await tunnel.dispose();
2448
+ try {
2449
+ if (group !== void 0) {
2450
+ const sessionCount = group.list().length;
2451
+ reportProgress?.(sessionCount === 1 ? "Closing the inspector session..." : `Closing ${sessionCount.toString()} inspector sessions...`);
2452
+ await group.dispose();
2453
+ reportProgress?.(sessionCount === 1 ? "Inspector session closed." : "Inspector sessions closed.");
2454
+ }
2455
+ } finally {
2456
+ await tunnel.dispose();
2457
+ }
2203
2458
  }
2459
+ } finally {
2460
+ await lock.release();
2204
2461
  }
2205
2462
  }
2206
2463
  function singleSessionGroup(session) {
@@ -2262,7 +2519,7 @@ async function handleAttach(opts) {
2262
2519
  writeJson({ host: tunnel.host, port: tunnel.port, ...version });
2263
2520
  return;
2264
2521
  }
2265
- process3.stdout.write(
2522
+ process4.stdout.write(
2266
2523
  `Connected to ${tunnel.host}:${tunnel.port.toString()}
2267
2524
  Browser: ${version.browser}
2268
2525
  Protocol: ${version.protocolVersion}
@@ -2274,7 +2531,7 @@ async function handleAttach(opts) {
2274
2531
  }
2275
2532
 
2276
2533
  // src/cli/commands/checkBreakpoint.ts
2277
- import process4 from "process";
2534
+ import process5 from "process";
2278
2535
 
2279
2536
  // src/pathMapper.ts
2280
2537
  init_types();
@@ -2561,25 +2818,25 @@ async function checkSession(session, matcher, requestedLine) {
2561
2818
  }
2562
2819
  function writeHumanCheck(result) {
2563
2820
  if (result.status === "script-not-loaded") {
2564
- process4.stdout.write(
2821
+ process5.stdout.write(
2565
2822
  `${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
2823
  `
2567
2824
  );
2568
2825
  return;
2569
2826
  }
2570
2827
  if (result.status === "unbreakable") {
2571
- process4.stdout.write(
2828
+ process5.stdout.write(
2572
2829
  `${result.file}:${result.line.toString()} matches a loaded script, but this exact line has no breakable location. Try a neighboring executable line.
2573
2830
  `
2574
2831
  );
2575
2832
  return;
2576
2833
  }
2577
- process4.stdout.write(`${result.file}:${result.line.toString()} is breakable:
2834
+ process5.stdout.write(`${result.file}:${result.line.toString()} is breakable:
2578
2835
  `);
2579
2836
  for (const script of result.scripts) {
2580
2837
  for (const location of script.locations) {
2581
2838
  const isolate = script.isolate.kind === "main" ? "main" : `worker ${script.isolate.workerId}`;
2582
- process4.stdout.write(
2839
+ process5.stdout.write(
2583
2840
  ` ${isolate} ${script.url} line ${(location.lineNumber + 1).toString()}:${((location.columnNumber ?? 0) + 1).toString()}
2584
2841
  `
2585
2842
  );
@@ -2588,7 +2845,7 @@ function writeHumanCheck(result) {
2588
2845
  }
2589
2846
 
2590
2847
  // src/cli/commands/eval.ts
2591
- import process5 from "process";
2848
+ import process6 from "process";
2592
2849
 
2593
2850
  // src/inspector/runtime.ts
2594
2851
  init_types();
@@ -2685,7 +2942,7 @@ async function handleEval(opts) {
2685
2942
  if (opts.json) {
2686
2943
  writeJson(result);
2687
2944
  if (result.exceptionDetails !== void 0) {
2688
- process5.exitCode = 1;
2945
+ process6.exitCode = 1;
2689
2946
  }
2690
2947
  return;
2691
2948
  }
@@ -2694,33 +2951,33 @@ async function handleEval(opts) {
2694
2951
  function writeHumanEvalResult(result) {
2695
2952
  if (result.exceptionDetails !== void 0) {
2696
2953
  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}
2954
+ process6.stderr.write(`${detail}
2698
2955
  `);
2699
- process5.exitCode = 1;
2956
+ process6.exitCode = 1;
2700
2957
  return;
2701
2958
  }
2702
2959
  const inner = result.result;
2703
2960
  if (inner === void 0) {
2704
- process5.stdout.write("\n");
2961
+ process6.stdout.write("\n");
2705
2962
  return;
2706
2963
  }
2707
2964
  if (typeof inner.value === "string") {
2708
- process5.stdout.write(`${inner.value}
2965
+ process6.stdout.write(`${inner.value}
2709
2966
  `);
2710
2967
  return;
2711
2968
  }
2712
2969
  if (typeof inner.description === "string") {
2713
- process5.stdout.write(`${inner.description}
2970
+ process6.stdout.write(`${inner.description}
2714
2971
  `);
2715
2972
  return;
2716
2973
  }
2717
- process5.stdout.write(`${JSON.stringify(inner.value)}
2974
+ process6.stdout.write(`${JSON.stringify(inner.value)}
2718
2975
  `);
2719
2976
  }
2720
2977
 
2721
2978
  // src/cli/commands/exception.ts
2722
2979
  import { performance as performance5 } from "perf_hooks";
2723
- import process7 from "process";
2980
+ import process8 from "process";
2724
2981
 
2725
2982
  // src/inspector/pause.ts
2726
2983
  init_types();
@@ -2855,6 +3112,8 @@ var BreakpointFanout = class {
2855
3112
  detachError;
2856
3113
  activeRace;
2857
3114
  pauseReasons = [];
3115
+ pendingSetupError;
3116
+ preserveReadinessErrors = false;
2858
3117
  constructor(group, setupSession, pauseReasons = []) {
2859
3118
  this.pauseReasons = pauseReasons;
2860
3119
  this.detach = group.onSession((session) => {
@@ -2880,13 +3139,51 @@ var BreakpointFanout = class {
2880
3139
  this.activeRace?.remove(session);
2881
3140
  });
2882
3141
  this.detachError = group.onError((error) => {
3142
+ if (this.setupErrors.length === 0 && this.pendingSetupError === void 0) {
3143
+ this.pendingSetupError = error;
3144
+ }
2883
3145
  for (const reject of this.setupErrors) {
2884
3146
  reject(error);
2885
3147
  }
2886
3148
  });
2887
3149
  }
2888
- async ready() {
2889
- await Promise.all([...this.records.values()].map((record) => record.setup));
3150
+ async ready(options = {}) {
3151
+ if (options.includeNewSessions !== true) {
3152
+ const records = [...this.records.values()];
3153
+ await Promise.all(records.map((record) => record.setup));
3154
+ options.onReady?.(this.outcomesFor(records));
3155
+ return;
3156
+ }
3157
+ this.preserveReadinessErrors = true;
3158
+ const pendingError = this.takePendingSetupError();
3159
+ if (pendingError !== void 0) {
3160
+ throw pendingError;
3161
+ }
3162
+ let rejectGroupError = () => void 0;
3163
+ const groupError = new Promise((_resolve, reject) => {
3164
+ rejectGroupError = reject;
3165
+ });
3166
+ this.setupErrors.push(rejectGroupError);
3167
+ try {
3168
+ let stableRecords;
3169
+ while (stableRecords === void 0) {
3170
+ const records = [...this.records.values()];
3171
+ await Promise.race([
3172
+ Promise.all(records.map((record) => record.setup)),
3173
+ groupError
3174
+ ]);
3175
+ const stable = records.length === this.records.size && records.every((record) => this.records.get(record.session) === record);
3176
+ if (stable) {
3177
+ stableRecords = records;
3178
+ }
3179
+ }
3180
+ options.onReady?.(this.outcomesFor(stableRecords));
3181
+ } finally {
3182
+ const index = this.setupErrors.indexOf(rejectGroupError);
3183
+ if (index >= 0) {
3184
+ this.setupErrors.splice(index, 1);
3185
+ }
3186
+ }
2890
3187
  }
2891
3188
  trackHandle(session, handle) {
2892
3189
  const record = this.records.get(session);
@@ -2895,11 +3192,19 @@ var BreakpointFanout = class {
2895
3192
  }
2896
3193
  }
2897
3194
  availableOutcomes() {
2898
- return [...this.records.values()].map((record) => ({
3195
+ return this.outcomesFor([...this.records.values()]);
3196
+ }
3197
+ outcomesFor(records) {
3198
+ return records.map((record) => ({
2899
3199
  session: record.session,
2900
3200
  setup: { handles: record.handles }
2901
3201
  }));
2902
3202
  }
3203
+ takePendingSetupError() {
3204
+ const error = this.pendingSetupError;
3205
+ this.pendingSetupError = void 0;
3206
+ return error;
3207
+ }
2903
3208
  async waitForFirst(timeoutMs, options = {}, signal) {
2904
3209
  if (this.activeRace !== void 0) {
2905
3210
  throw new CfInspectorError("INVALID_ARGUMENT", "A fan-out pause race is already active");
@@ -2908,6 +3213,12 @@ var BreakpointFanout = class {
2908
3213
  this.pauseReasons = options.pauseReasons ?? [];
2909
3214
  this.activeRace = race;
2910
3215
  this.setupErrors.push(race.reject);
3216
+ if (this.preserveReadinessErrors) {
3217
+ const pendingError = this.takePendingSetupError();
3218
+ if (pendingError !== void 0) {
3219
+ race.reject(pendingError);
3220
+ }
3221
+ }
2911
3222
  for (const record of this.records.values()) {
2912
3223
  race.add(record);
2913
3224
  }
@@ -3814,19 +4125,19 @@ async function captureExpression(session, callFrameId, expression, maxValueLengt
3814
4125
  init_types();
3815
4126
 
3816
4127
  // src/cli/signals.ts
3817
- import process6 from "process";
4128
+ import process7 from "process";
3818
4129
  async function withTerminationSignal(fn) {
3819
4130
  const abort = new AbortController();
3820
4131
  const onSignal = () => {
3821
4132
  abort.abort();
3822
4133
  };
3823
- process6.once("SIGINT", onSignal);
3824
- process6.once("SIGTERM", onSignal);
4134
+ process7.once("SIGINT", onSignal);
4135
+ process7.once("SIGTERM", onSignal);
3825
4136
  try {
3826
4137
  return await fn(abort.signal);
3827
4138
  } finally {
3828
- process6.off("SIGINT", onSignal);
3829
- process6.off("SIGTERM", onSignal);
4139
+ process7.off("SIGINT", onSignal);
4140
+ process7.off("SIGTERM", onSignal);
3830
4141
  }
3831
4142
  }
3832
4143
 
@@ -3878,7 +4189,20 @@ async function runExceptionCommand(command, opts, signal) {
3878
4189
  let winner;
3879
4190
  let preserveWinner = false;
3880
4191
  try {
3881
- await fanout.ready();
4192
+ await fanout.ready(opts.readyEvent === true ? {
4193
+ includeNewSessions: true,
4194
+ onReady: (outcomes) => {
4195
+ if (signal?.aborted === true) {
4196
+ return;
4197
+ }
4198
+ writeArmedEvent({
4199
+ command: "exception",
4200
+ sessions: outcomes.length,
4201
+ resolvedLocations: null,
4202
+ timeoutMs: command.timeoutMs
4203
+ });
4204
+ }
4205
+ } : {});
3882
4206
  const hit = await fanout.waitForFirst(command.timeoutMs, {
3883
4207
  pauseReasons: ["exception", "promiseRejection"],
3884
4208
  unmatchedPausePolicy: "wait-for-resume"
@@ -3913,7 +4237,7 @@ async function resumeAfterException(session, snapshot, pausedStartedAt) {
3913
4237
  await resume(session);
3914
4238
  return withPausedDuration(snapshot, roundDurationMs(performance5.now() - pausedStartedAt));
3915
4239
  } catch {
3916
- process7.stderr.write(
4240
+ process8.stderr.write(
3917
4241
  "[cf-inspector] warning: Debugger.resume failed after exception capture; pausedDurationMs is unknown.\n"
3918
4242
  );
3919
4243
  return withPausedDuration(snapshot, null);
@@ -3927,20 +4251,48 @@ async function disablePauseOnExceptionsBestEffort(session) {
3927
4251
  }
3928
4252
 
3929
4253
  // src/cli/commands/listScripts.ts
3930
- import process8 from "process";
4254
+ import process9 from "process";
3931
4255
  async function handleListScripts(opts) {
3932
4256
  const target = await resolveTargetWithCurrentCfTarget(opts);
3933
4257
  const filter = compileScriptUrlFilter(opts.filter);
3934
- const scripts = (await withSession(target, (session) => Promise.resolve(listScripts(session)))).filter((script) => filter === void 0 || filter(script.url));
4258
+ const scripts = (await withSessions(target, (group) => {
4259
+ const sessions = group.list();
4260
+ warnOnListScriptsSelection(target, {
4261
+ targetCount: group.targetCount,
4262
+ targetIndex: group.targetIndex,
4263
+ ...sessions[0]?.workerTargets === void 0 ? {} : { workerTargets: sessions[0].workerTargets }
4264
+ });
4265
+ return Promise.resolve(collectListedScripts(sessions));
4266
+ })).filter((script) => filter === void 0 || filter(script.url));
3935
4267
  if (opts.json) {
3936
4268
  writeJson(scripts);
3937
4269
  return;
3938
4270
  }
3939
4271
  for (const script of scripts) {
3940
- process8.stdout.write(`${script.scriptId} ${script.url}
3941
- `);
4272
+ process9.stdout.write(
4273
+ `${script.scriptId} ${script.url} ${formatIsolate2(script.isolate)}
4274
+ `
4275
+ );
3942
4276
  }
3943
4277
  }
4278
+ function warnOnListScriptsSelection(target, selection) {
4279
+ const autoAttach = target.targetIndex === void 0 && target.workerIndex === void 0 && target.workerId === void 0 && target.mainOnly !== true;
4280
+ const isolateWasExplicit = autoAttach || target.workerIndex !== void 0 || target.workerId !== void 0 || target.mainOnly === true;
4281
+ warnOnImplicitInspectorSelection(
4282
+ selection,
4283
+ target.targetIndex !== void 0,
4284
+ isolateWasExplicit
4285
+ );
4286
+ }
4287
+ function collectListedScripts(sessions) {
4288
+ return sessions.flatMap((session) => listScripts(session).map((script) => ({
4289
+ ...script,
4290
+ isolate: session.isolate ?? { kind: "main" }
4291
+ })));
4292
+ }
4293
+ function formatIsolate2(isolate) {
4294
+ return isolate.kind === "worker" ? `worker:${isolate.workerId}` : "main";
4295
+ }
3944
4296
  async function handleListTargets(opts) {
3945
4297
  const target = await resolveTargetWithCurrentCfTarget(opts);
3946
4298
  const tunnel = await openTarget(target);
@@ -3968,7 +4320,7 @@ async function buildListedTargets(targets) {
3968
4320
  return buildListedTarget(target, index, workerResult.supported, workerResult.workers);
3969
4321
  } catch (error) {
3970
4322
  const message = error instanceof Error ? error.message : String(error);
3971
- process8.stderr.write(
4323
+ process9.stderr.write(
3972
4324
  `[cf-inspector] warning: worker discovery failed for raw target ${index.toString()}: ${message}
3973
4325
  `
3974
4326
  );
@@ -3995,7 +4347,7 @@ function looksLikeWorkerTarget(target) {
3995
4347
  return `${target.type} ${target.title} ${target.url}`.toLowerCase().includes("worker");
3996
4348
  }
3997
4349
  function writeTargetCountSummary(targetCount, workerCount) {
3998
- process8.stderr.write(
4350
+ process9.stderr.write(
3999
4351
  `[cf-inspector] ${targetCount.toString()} raw inspector ${targetCount === 1 ? "target" : "targets"}; ${workerCount.toString()} ${workerCount === 1 ? "worker" : "workers"}.
4000
4352
  `
4001
4353
  );
@@ -4006,7 +4358,7 @@ function warnOnMissingWorkers(targetCount, workerCount, targets) {
4006
4358
  }
4007
4359
  const supported = targets[0]?.workerDiscoverySupported === true;
4008
4360
  const supportHint = supported ? "NodeWorker discovery is available, but no live worker attached." : "This runtime did not expose NodeWorker discovery.";
4009
- process8.stderr.write(
4361
+ process9.stderr.write(
4010
4362
  `[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.
4011
4363
  `
4012
4364
  );
@@ -4014,12 +4366,12 @@ function warnOnMissingWorkers(targetCount, workerCount, targets) {
4014
4366
  function writeHumanTargets(targets) {
4015
4367
  for (const target of targets) {
4016
4368
  const workerLabel = target.likelyWorker ? " likely-worker" : "";
4017
- process8.stdout.write(
4369
+ process9.stdout.write(
4018
4370
  `${target.index.toString()} target ${target.type} ${target.title} ${target.url}${workerLabel}
4019
4371
  `
4020
4372
  );
4021
4373
  for (const worker of target.workers) {
4022
- process8.stdout.write(
4374
+ process9.stdout.write(
4023
4375
  ` ${worker.index.toString()} worker ${worker.type} ${worker.title} ${worker.url}
4024
4376
  `
4025
4377
  );
@@ -4096,7 +4448,7 @@ function matchesFilterTokens(value, tokens) {
4096
4448
  }
4097
4449
 
4098
4450
  // src/cli/commands/log.ts
4099
- import process9 from "process";
4451
+ import process10 from "process";
4100
4452
 
4101
4453
  // src/logpoint/stream.ts
4102
4454
  init_types();
@@ -4273,6 +4625,9 @@ async function streamLogpoint(session, options) {
4273
4625
  if (event === void 0) {
4274
4626
  return;
4275
4627
  }
4628
+ if (options.eventGate?.() === false) {
4629
+ return;
4630
+ }
4276
4631
  emitted += 1;
4277
4632
  try {
4278
4633
  options.onEvent(event);
@@ -4391,6 +4746,7 @@ async function handleLog(opts) {
4391
4746
  ...condition === void 0 ? {} : { condition },
4392
4747
  maxValueLength,
4393
4748
  json: opts.json,
4749
+ emitReadyEvent: opts.readyEvent === true,
4394
4750
  signal
4395
4751
  });
4396
4752
  writeLogSummary(result.stoppedReason, result.emitted, opts.json);
@@ -4402,9 +4758,13 @@ async function runLogGroup(group, options) {
4402
4758
  const tasks = /* @__PURE__ */ new Set();
4403
4759
  const removedSessions = /* @__PURE__ */ new Set();
4404
4760
  const results = [];
4761
+ const pendingArming = /* @__PURE__ */ new Set();
4762
+ const resolvedLocations = /* @__PURE__ */ new Map();
4405
4763
  let fatalError;
4406
4764
  let emitted = 0;
4407
4765
  let reason = "signal";
4766
+ let sessionRegistrationComplete = false;
4767
+ let readyEventEmitted = !options.emitReadyEvent;
4408
4768
  let resolveStop;
4409
4769
  const stopped = new Promise((resolve) => {
4410
4770
  resolveStop = resolve;
@@ -4427,10 +4787,35 @@ async function runLogGroup(group, options) {
4427
4787
  const timer = options.durationMs === void 0 ? void 0 : setTimeout(() => {
4428
4788
  finish("duration");
4429
4789
  }, options.durationMs);
4790
+ const detachError = options.emitReadyEvent ? group.onError((error) => {
4791
+ if (controller.signal.aborted) {
4792
+ return;
4793
+ }
4794
+ fatalError = error;
4795
+ finish("transport-closed");
4796
+ }) : () => void 0;
4797
+ const emitReadyEventIfArmed = () => {
4798
+ if (readyEventEmitted || !sessionRegistrationComplete || pendingArming.size > 0 || resolvedLocations.size === 0 || controller.signal.aborted) {
4799
+ return;
4800
+ }
4801
+ writeArmedEvent({
4802
+ command: "log",
4803
+ sessions: resolvedLocations.size,
4804
+ resolvedLocations: [...resolvedLocations.values()].reduce(
4805
+ (total, count) => total + count,
4806
+ 0
4807
+ ),
4808
+ timeoutMs: null
4809
+ });
4810
+ readyEventEmitted = true;
4811
+ };
4430
4812
  const startSession = (session) => {
4431
4813
  if (controller.signal.aborted) {
4432
4814
  return;
4433
4815
  }
4816
+ if (!readyEventEmitted) {
4817
+ pendingArming.add(session);
4818
+ }
4434
4819
  const task = (async () => {
4435
4820
  await validateExpression(session, options.expression);
4436
4821
  if (options.condition !== void 0) {
@@ -4444,6 +4829,7 @@ async function runLogGroup(group, options) {
4444
4829
  ...options.condition === void 0 ? {} : { condition: options.condition },
4445
4830
  maxValueLength: options.maxValueLength,
4446
4831
  signal: controller.signal,
4832
+ ...options.emitReadyEvent ? { eventGate: () => readyEventEmitted } : {},
4447
4833
  onEvent: (event) => {
4448
4834
  if (controller.signal.aborted) {
4449
4835
  return;
@@ -4456,6 +4842,11 @@ async function runLogGroup(group, options) {
4456
4842
  },
4457
4843
  onBreakpointSet: (handle) => {
4458
4844
  warnOnUnboundBreakpoints([handle]);
4845
+ if (!readyEventEmitted) {
4846
+ resolvedLocations.set(session, handle.resolvedLocations.length);
4847
+ pendingArming.delete(session);
4848
+ emitReadyEventIfArmed();
4849
+ }
4459
4850
  }
4460
4851
  });
4461
4852
  })();
@@ -4468,6 +4859,9 @@ async function runLogGroup(group, options) {
4468
4859
  }
4469
4860
  },
4470
4861
  (error) => {
4862
+ if (options.emitReadyEvent && (removedSessions.has(session) || fatalError !== void 0)) {
4863
+ return;
4864
+ }
4471
4865
  fatalError = error;
4472
4866
  finish("transport-closed");
4473
4867
  }
@@ -4476,8 +4870,27 @@ async function runLogGroup(group, options) {
4476
4870
  });
4477
4871
  };
4478
4872
  const detach = group.onSession(startSession);
4873
+ sessionRegistrationComplete = true;
4874
+ emitReadyEventIfArmed();
4479
4875
  const detachRemoved = group.onSessionRemoved((session) => {
4480
4876
  removedSessions.add(session);
4877
+ if (!readyEventEmitted) {
4878
+ if (controller.signal.aborted) {
4879
+ pendingArming.delete(session);
4880
+ resolvedLocations.delete(session);
4881
+ return;
4882
+ }
4883
+ if (pendingArming.has(session)) {
4884
+ fatalError = new CfInspectorError(
4885
+ "INSPECTOR_CONNECTION_FAILED",
4886
+ "A worker detached before logpoint arming completed; no readiness event was emitted."
4887
+ );
4888
+ finish("transport-closed");
4889
+ return;
4890
+ }
4891
+ resolvedLocations.delete(session);
4892
+ emitReadyEventIfArmed();
4893
+ }
4481
4894
  });
4482
4895
  try {
4483
4896
  await stopped;
@@ -4485,6 +4898,7 @@ async function runLogGroup(group, options) {
4485
4898
  } finally {
4486
4899
  detach();
4487
4900
  detachRemoved();
4901
+ detachError();
4488
4902
  if (timer !== void 0) {
4489
4903
  clearTimeout(timer);
4490
4904
  }
@@ -4503,11 +4917,11 @@ function isZeroHitStop(reason) {
4503
4917
  }
4504
4918
  function writeLogSummary(stoppedReason, emitted, json) {
4505
4919
  if (json) {
4506
- process9.stderr.write(`${JSON.stringify({ stopped: stoppedReason, emitted })}
4920
+ process10.stderr.write(`${JSON.stringify({ stopped: stoppedReason, emitted })}
4507
4921
  `);
4508
4922
  return;
4509
4923
  }
4510
- process9.stderr.write(
4924
+ process10.stderr.write(
4511
4925
  `Stopped (${stoppedReason}); emitted ${emitted.toString()} log ${emitted === 1 ? "entry" : "entries"}.
4512
4926
  `
4513
4927
  );
@@ -4515,7 +4929,7 @@ function writeLogSummary(stoppedReason, emitted, json) {
4515
4929
 
4516
4930
  // src/cli/commands/snapshot.ts
4517
4931
  import { performance as performance6 } from "perf_hooks";
4518
- import process10 from "process";
4932
+ import process11 from "process";
4519
4933
  init_types();
4520
4934
  async function handleSnapshot(opts) {
4521
4935
  const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
@@ -4591,18 +5005,38 @@ async function runSnapshotCommand(command, opts, reportProgress, signal) {
4591
5005
  let winner;
4592
5006
  let preserveWinner = false;
4593
5007
  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.");
5008
+ const reportArmedSetup = (outcomes) => {
5009
+ if (opts.readyEvent === true && signal?.aborted === true) {
5010
+ return;
5011
+ }
5012
+ if (command.setupEvals.length > 0) {
5013
+ reportProgress?.("Setup evaluation complete.");
5014
+ }
5015
+ if (command.condition !== void 0) {
5016
+ reportProgress?.("Breakpoint condition is valid.");
5017
+ }
5018
+ const setup = reportBreakpointOutcomes(outcomes, reportProgress);
5019
+ reportProgress?.(
5020
+ `Waiting up to ${(command.timeoutMs / 1e3).toString()}s for a breakpoint hit...`
5021
+ );
5022
+ if (opts.readyEvent === true && signal?.aborted !== true) {
5023
+ writeArmedEvent({
5024
+ command: "snapshot",
5025
+ sessions: setup.sessions,
5026
+ resolvedLocations: setup.resolvedLocations,
5027
+ timeoutMs: command.timeoutMs
5028
+ });
5029
+ }
5030
+ };
5031
+ if (opts.readyEvent === true) {
5032
+ await fanout.ready({
5033
+ includeNewSessions: true,
5034
+ onReady: reportArmedSetup
5035
+ });
5036
+ } else {
5037
+ await fanout.ready();
5038
+ reportArmedSetup(fanout.availableOutcomes());
4600
5039
  }
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
5040
  const hit = await fanout.waitForFirst(command.timeoutMs, {
4607
5041
  unmatchedPausePolicy: opts.failOnUnmatchedPause === true ? "fail" : "wait-for-resume",
4608
5042
  ...opts.failOnUnmatchedPause === true ? {} : { onUnmatchedPause: warnOnUnmatchedPause }
@@ -4650,11 +5084,12 @@ function reportBreakpointOutcomes(outcomes, reportProgress) {
4650
5084
  reportProgress?.(
4651
5085
  `Breakpoint setup complete: ${locations.toString()} resolved ${locations === 1 ? "location" : "locations"}.`
4652
5086
  );
4653
- return;
5087
+ return { sessions: outcomes.length, resolvedLocations: locations };
4654
5088
  }
4655
5089
  reportProgress?.(
4656
5090
  `Breakpoint setup complete: sessions=${outcomes.length.toString()} boundSessions=${boundSessions.toString()} resolvedLocations=${locations.toString()}.`
4657
5091
  );
5092
+ return { sessions: outcomes.length, resolvedLocations: locations };
4658
5093
  }
4659
5094
  async function captureSnapshotResult(session, pause, command, opts, reportProgress) {
4660
5095
  const pausedStartedAt = pause.receivedAtMs ?? performance6.now();
@@ -4694,7 +5129,7 @@ async function resumeAfterSnapshot(session, snapshot, pausedStartedAt, reportPro
4694
5129
  reportProgress?.("Target resumed.");
4695
5130
  return withPausedDuration(snapshot, roundDurationMs(performance6.now() - pausedStartedAt));
4696
5131
  } catch {
4697
- process10.stderr.write(
5132
+ process11.stderr.write(
4698
5133
  "[cf-inspector] warning: Debugger.resume failed after snapshot; pausedDurationMs is unknown.\n"
4699
5134
  );
4700
5135
  return withPausedDuration(snapshot, null);
@@ -4707,7 +5142,7 @@ function parseSetupEvals(raw) {
4707
5142
 
4708
5143
  // src/cli/commands/watch.ts
4709
5144
  import { performance as performance7 } from "perf_hooks";
4710
- import process11 from "process";
5145
+ import process12 from "process";
4711
5146
  init_types();
4712
5147
  async function handleWatch(opts) {
4713
5148
  const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
@@ -4775,7 +5210,26 @@ async function runWatchGroup(group, command, opts, signal) {
4775
5210
  let stoppedReason = "signal";
4776
5211
  const deadline = computeDeadline(command.durationMs);
4777
5212
  try {
4778
- await fanout.ready();
5213
+ await fanout.ready(opts.readyEvent === true ? {
5214
+ includeNewSessions: true,
5215
+ onReady: (outcomes) => {
5216
+ if (signal.aborted || remainingForLoop(deadline, command.perHitTimeoutMs) <= 0) {
5217
+ return;
5218
+ }
5219
+ writeArmedEvent({
5220
+ command: "watch",
5221
+ sessions: outcomes.length,
5222
+ resolvedLocations: outcomes.reduce(
5223
+ (total, outcome) => total + outcome.setup.handles.reduce(
5224
+ (sessionTotal, handle) => sessionTotal + handle.resolvedLocations.length,
5225
+ 0
5226
+ ),
5227
+ 0
5228
+ ),
5229
+ timeoutMs: command.perHitTimeoutMs
5230
+ });
5231
+ }
5232
+ } : {});
4779
5233
  while (!signal.aborted) {
4780
5234
  const remainingMs = remainingForLoop(deadline, command.perHitTimeoutMs);
4781
5235
  if (remainingMs <= 0) {
@@ -4806,7 +5260,7 @@ async function runWatchGroup(group, command, opts, signal) {
4806
5260
  await resume(hit.session);
4807
5261
  hit.session.debuggerState.paused = false;
4808
5262
  } catch {
4809
- process11.stderr.write("[cf-inspector] warning: Debugger.resume failed during watch.\n");
5263
+ process12.stderr.write("[cf-inspector] warning: Debugger.resume failed during watch.\n");
4810
5264
  stoppedReason = "transport-closed";
4811
5265
  break;
4812
5266
  }
@@ -4820,7 +5274,7 @@ async function runWatchGroup(group, command, opts, signal) {
4820
5274
  }
4821
5275
  } finally {
4822
5276
  const cleanup = await fanout.cleanup();
4823
- process11.stderr.write(
5277
+ process12.stderr.write(
4824
5278
  `[cf-inspector] breakpoint cleanup: cleared ${cleanup.cleared.toString()} of ${cleanup.attempted.toString()}; resumed ${cleanup.resumed.toString()} paused isolates.
4825
5279
  `
4826
5280
  );
@@ -4918,11 +5372,11 @@ function formatLocation(command, topFrame) {
4918
5372
  }
4919
5373
  function writeWatchSummary(reason, emitted, json) {
4920
5374
  if (json) {
4921
- process11.stderr.write(`${JSON.stringify({ stopped: reason, emitted })}
5375
+ process12.stderr.write(`${JSON.stringify({ stopped: reason, emitted })}
4922
5376
  `);
4923
5377
  return;
4924
5378
  }
4925
- process11.stderr.write(
5379
+ process12.stderr.write(
4926
5380
  `Stopped (${reason}); emitted ${emitted.toString()} watch ${emitted === 1 ? "event" : "events"}.
4927
5381
  `
4928
5382
  );
@@ -4949,10 +5403,11 @@ var collectStrings = (value, prev = []) => [
4949
5403
  ...prev,
4950
5404
  value
4951
5405
  ];
5406
+ var READY_EVENT_DESCRIPTION = "Emit a versioned breakpoint-armed JSON event on stderr after every current isolate is armed";
4952
5407
  function readPackageVersion() {
4953
5408
  let current = dirname(fileURLToPath(import.meta.url));
4954
5409
  for (let depth = 0; depth < 4; depth += 1) {
4955
- const candidate = join(current, "package.json");
5410
+ const candidate = join2(current, "package.json");
4956
5411
  try {
4957
5412
  const parsed = JSON.parse(readFileSync(candidate, "utf8"));
4958
5413
  if (typeof parsed === "object" && parsed !== null) {
@@ -4994,14 +5449,14 @@ function registerSnapshot(program) {
4994
5449
  applyTargetOptions(
4995
5450
  program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume"),
4996
5451
  { includeTimeout: false }
4997
- ).option("--bp <file:line>", "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--setup-eval <expr>", "Evaluate a global setup expression before breakpoint setup (repeatable)", collectStrings, []).option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 131072)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--condition <expr>", "Only pause when this JS expression evaluates truthy in the paused frame").option("--hit-count <n>", "Only pause after the breakpoint has been hit N or more times").option("--stack-depth <n>", "Walk this many call frames when capturing (default: 1, only top frame)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame in the stack").option("--allow-mutation", "Allow mutation-capable captures and native breakpoint conditions to run").option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--no-json", "Print a human-readable summary instead of JSON").option("--quiet", "Suppress progress messages on stderr").option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--fail-on-unmatched-pause", "Fail immediately if the target pauses somewhere else").action(async (opts) => {
5452
+ ).option("--bp <file:line>", "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--setup-eval <expr>", "Evaluate a global setup expression before breakpoint setup (repeatable)", collectStrings, []).option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 131072)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--condition <expr>", "Only pause when this JS expression evaluates truthy in the paused frame").option("--hit-count <n>", "Only pause after the breakpoint has been hit N or more times").option("--stack-depth <n>", "Walk this many call frames when capturing (default: 1, only top frame)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame in the stack").option("--allow-mutation", "Allow mutation-capable captures and native breakpoint conditions to run").option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--no-json", "Print a human-readable summary instead of JSON").option("--quiet", "Suppress progress messages on stderr").option("--ready-event", READY_EVENT_DESCRIPTION).option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--fail-on-unmatched-pause", "Fail immediately if the target pauses somewhere else").action(async (opts) => {
4998
5453
  await handleSnapshot(opts);
4999
5454
  });
5000
5455
  }
5001
5456
  function registerLog(program) {
5002
5457
  applyTargetOptions(
5003
5458
  program.command("log").description("Stream a non-pausing logpoint: log an expression each time a line executes")
5004
- ).requiredOption("--at <file:line>", "Logpoint location, e.g. src/handler.ts:42").requiredOption("--expr <expression>", "JavaScript expression to log on each hit").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--duration <seconds>", "Stop streaming after N seconds (default: run until SIGINT)").option("--max-events <n>", "Stop streaming after emitting N log events").option("--hit-count <n>", "Start logging once the line has been hit N or more times").option("--condition <expr>", "Only log when this JS expression evaluates truthy on the inspectee").option("--max-value-length <chars>", "Maximum characters per log value before truncation (default: 4096)").option("--no-json", "Print human-readable lines instead of JSON Lines").action(async (opts) => {
5459
+ ).requiredOption("--at <file:line>", "Logpoint location, e.g. src/handler.ts:42").requiredOption("--expr <expression>", "JavaScript expression to log on each hit").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--duration <seconds>", "Stop streaming after N seconds (default: run until SIGINT)").option("--max-events <n>", "Stop streaming after emitting N log events").option("--hit-count <n>", "Start logging once the line has been hit N or more times").option("--condition <expr>", "Only log when this JS expression evaluates truthy on the inspectee").option("--max-value-length <chars>", "Maximum characters per log value before truncation (default: 4096)").option("--no-json", "Print human-readable lines instead of JSON Lines").option("--ready-event", READY_EVENT_DESCRIPTION).action(async (opts) => {
5005
5460
  await handleLog(opts);
5006
5461
  });
5007
5462
  }
@@ -5009,7 +5464,7 @@ function registerWatch(program) {
5009
5464
  applyTargetOptions(
5010
5465
  program.command("watch").description("Stream a snapshot per breakpoint hit (multi-shot watch); resume between hits"),
5011
5466
  { includeTimeout: false }
5012
- ).option("--bp <file:line>", "Breakpoint location (repeatable), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate per hit").option("--setup-eval <expr>", "Evaluate a global setup expression before breakpoint setup (repeatable)", collectStrings, []).option("--condition <expr>", "Only emit hits where this JS expression evaluates truthy").option("--hit-count <n>", "Start emitting after the line has been hit N or more times").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--duration <seconds>", "Stop streaming after N seconds (default: run until SIGINT)").option("--max-events <n>", "Stop streaming after emitting N watch events").option("--timeout <seconds>", "How long to wait for the next hit before giving up (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--stack-depth <n>", "Walk this many call frames per hit (default: 1)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame").option("--allow-mutation", "Allow mutation-capable captures and native breakpoint conditions to run").option("--include-scopes", "Include expanded paused-frame scopes per hit").option("--no-json", "Print human-readable lines instead of JSON Lines").action(async (opts) => {
5467
+ ).option("--bp <file:line>", "Breakpoint location (repeatable), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate per hit").option("--setup-eval <expr>", "Evaluate a global setup expression before breakpoint setup (repeatable)", collectStrings, []).option("--condition <expr>", "Only emit hits where this JS expression evaluates truthy").option("--hit-count <n>", "Start emitting after the line has been hit N or more times").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--duration <seconds>", "Stop streaming after N seconds (default: run until SIGINT)").option("--max-events <n>", "Stop streaming after emitting N watch events").option("--timeout <seconds>", "How long to wait for the next hit before giving up (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--stack-depth <n>", "Walk this many call frames per hit (default: 1)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame").option("--allow-mutation", "Allow mutation-capable captures and native breakpoint conditions to run").option("--include-scopes", "Include expanded paused-frame scopes per hit").option("--no-json", "Print human-readable lines instead of JSON Lines").option("--ready-event", READY_EVENT_DESCRIPTION).action(async (opts) => {
5013
5468
  await handleWatch(opts);
5014
5469
  });
5015
5470
  }
@@ -5017,7 +5472,7 @@ function registerException(program) {
5017
5472
  applyTargetOptions(
5018
5473
  program.command("exception").description("Pause on a thrown exception, capture the value and frame, then resume"),
5019
5474
  { includeTimeout: false }
5020
- ).option("--type <state>", "Pause type: uncaught (default), caught, or all").option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--timeout <seconds>", "How long to wait for an exception (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 131072)").option("--stack-depth <n>", "Walk this many call frames when capturing (default: 1)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame in the stack").option("--allow-mutation", "Allow mutation-capable capture expressions to run").option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--no-json", "Print a human-readable summary instead of JSON").action(async (opts) => {
5475
+ ).option("--type <state>", "Pause type: uncaught (default), caught, or all").option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--timeout <seconds>", "How long to wait for an exception (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 131072)").option("--stack-depth <n>", "Walk this many call frames when capturing (default: 1)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame in the stack").option("--allow-mutation", "Allow mutation-capable capture expressions to run").option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--no-json", "Print a human-readable summary instead of JSON").option("--ready-event", READY_EVENT_DESCRIPTION).action(async (opts) => {
5021
5476
  await handleException(opts);
5022
5477
  });
5023
5478
  }
@@ -5030,8 +5485,10 @@ function registerEval(program) {
5030
5485
  }
5031
5486
  function registerListScripts(program) {
5032
5487
  applyTargetOptions(
5033
- program.command("list-scripts").description("Print the scripts the V8 instance currently knows about")
5034
- ).option("--filter <pattern>", "Only include script URLs matching this pattern").option("--no-json", "Print scriptId<TAB>url instead of JSON").action(async (opts) => {
5488
+ program.command("list-scripts").description(
5489
+ "Print scripts from the main isolate and every worker, tagged by isolate"
5490
+ )
5491
+ ).option("--filter <pattern>", "Only include script URLs matching this pattern").option("--no-json", "Print scriptId<TAB>url<TAB>isolate instead of JSON").action(async (opts) => {
5035
5492
  await handleListScripts(opts);
5036
5493
  });
5037
5494
  }
@@ -5060,20 +5517,20 @@ function registerAttach(program) {
5060
5517
  // src/cli.ts
5061
5518
  init_types();
5062
5519
  try {
5063
- await main(process12.argv);
5520
+ await main(process13.argv);
5064
5521
  } catch (err) {
5065
5522
  if (err instanceof CfInspectorError) {
5066
- process12.stderr.write(`Error [${err.code}]: ${err.message}
5523
+ process13.stderr.write(`Error [${err.code}]: ${err.message}
5067
5524
  `);
5068
5525
  if (err.detail !== void 0) {
5069
- process12.stderr.write(` detail: ${err.detail}
5526
+ process13.stderr.write(` detail: ${err.detail}
5070
5527
  `);
5071
5528
  }
5072
- process12.exit(1);
5529
+ process13.exit(1);
5073
5530
  }
5074
5531
  const message = err instanceof Error ? err.message : String(err);
5075
- process12.stderr.write(`Error: ${message}
5532
+ process13.stderr.write(`Error: ${message}
5076
5533
  `);
5077
- process12.exit(1);
5534
+ process13.exit(1);
5078
5535
  }
5079
5536
  //# sourceMappingURL=cli.js.map