@saptools/cf-inspector 0.7.0 → 0.7.1

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,13 @@ 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
392
  `);
393
393
  }
394
394
  function writeJson(value) {
395
- process.stdout.write(`${JSON.stringify(value, null, 2)}
395
+ process2.stdout.write(`${JSON.stringify(value, null, 2)}
396
396
  `);
397
397
  }
398
398
  function writeHumanSnapshot(snapshot) {
@@ -423,7 +423,7 @@ function writeHumanSnapshot(snapshot) {
423
423
  appendStackFrameLine(lines, frame);
424
424
  }
425
425
  }
426
- process.stdout.write(`${lines.join("\n")}
426
+ process2.stdout.write(`${lines.join("\n")}
427
427
  `);
428
428
  }
429
429
  function appendFrameLines(lines, frame) {
@@ -466,36 +466,36 @@ function appendExceptionLines(lines, exception) {
466
466
  }
467
467
  function writeLogEvent(event, json) {
468
468
  if (json) {
469
- process.stdout.write(`${JSON.stringify(event)}
469
+ process2.stdout.write(`${JSON.stringify(event)}
470
470
  `);
471
471
  return;
472
472
  }
473
473
  const isolateSuffix = event.isolate === void 0 ? "" : ` (${formatIsolate(event.isolate)})`;
474
474
  if (event.error !== void 0) {
475
- process.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} !err ${renderTruncated(event.error, event)}
475
+ process2.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} !err ${renderTruncated(event.error, event)}
476
476
  `);
477
477
  return;
478
478
  }
479
- process.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} ${renderTruncated(event.value ?? "", event)}
479
+ process2.stdout.write(`[${event.ts}] ${event.at}${isolateSuffix} ${renderTruncated(event.value ?? "", event)}
480
480
  `);
481
481
  }
482
482
  function writeWatchEvent(event, json) {
483
483
  if (json) {
484
- process.stdout.write(`${JSON.stringify(event)}
484
+ process2.stdout.write(`${JSON.stringify(event)}
485
485
  `);
486
486
  return;
487
487
  }
488
- process.stdout.write(
488
+ process2.stdout.write(
489
489
  `[${event.ts}] hit#${event.hit.toString()} ${event.at} (${formatIsolate(event.isolate)})
490
490
  `
491
491
  );
492
492
  if (event.exception !== void 0) {
493
- process.stdout.write(` exception: ${renderExceptionDetail(event.exception)}
493
+ process2.stdout.write(` exception: ${renderExceptionDetail(event.exception)}
494
494
  `);
495
495
  }
496
496
  for (const capture of event.captures) {
497
497
  const detail = capture.error ?? capture.value ?? "undefined";
498
- process.stdout.write(` ${capture.expression} = ${renderTruncated(detail, capture)}
498
+ process2.stdout.write(` ${capture.expression} = ${renderTruncated(detail, capture)}
499
499
  `);
500
500
  }
501
501
  }
@@ -1776,9 +1776,247 @@ var DEFAULT_BREAKPOINT_TIMEOUT_SEC = 30;
1776
1776
  var DEFAULT_CF_TIMEOUT_SEC = 180;
1777
1777
  var DEFAULT_EXCEPTION_TIMEOUT_SEC = 30;
1778
1778
 
1779
+ // src/cli/sessionLock.ts
1780
+ init_types();
1781
+ import { execFileSync } from "child_process";
1782
+ import { createHash, randomUUID } from "crypto";
1783
+ import { constants } from "fs";
1784
+ import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from "fs/promises";
1785
+ import { homedir } from "os";
1786
+ import { join } from "path";
1787
+ var ELECTION_WINDOW_MS = 25;
1788
+ var LOCK_FILE_SUFFIX = ".lock";
1789
+ async function acquireDebugSessionLock(target, options = {}) {
1790
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1791
+ const pid = options.pid ?? process.pid;
1792
+ const getProcessStart = options.getProcessStart ?? processStart;
1793
+ const ownerProcessStart = getProcessStart(pid);
1794
+ const token = options.token?.() ?? randomUUID();
1795
+ const targetIdentity = debugTargetIdentity(target);
1796
+ const key = createHash("sha256").update(targetIdentity).digest("hex");
1797
+ const lockRoot = options.stateRoot ?? defaultStateRoot();
1798
+ const lockDirectory = join(lockRoot, "cf-inspector", "locks");
1799
+ const ownPath = join(lockDirectory, `${key}.${pid.toString()}.${token}${LOCK_FILE_SUFFIX}`);
1800
+ const metadata = {
1801
+ pid,
1802
+ ...ownerProcessStart === void 0 ? {} : { processStart: ownerProcessStart },
1803
+ state: "pending",
1804
+ startedAt: now().toISOString(),
1805
+ token,
1806
+ target: targetIdentity
1807
+ };
1808
+ await mkdir(lockDirectory, { recursive: true, mode: 448 });
1809
+ const handle = await open(ownPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
1810
+ try {
1811
+ await handle.writeFile(`${JSON.stringify(metadata)}
1812
+ `, "utf8");
1813
+ } finally {
1814
+ await handle.close();
1815
+ }
1816
+ try {
1817
+ await new Promise((resolve) => {
1818
+ setTimeout(resolve, ELECTION_WINDOW_MS);
1819
+ });
1820
+ const contenders = await findLiveContenders(
1821
+ lockDirectory,
1822
+ key,
1823
+ ownPath,
1824
+ options.isProcessAlive ?? processIsAlive,
1825
+ getProcessStart
1826
+ );
1827
+ 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];
1828
+ if (owner !== void 0) {
1829
+ throw alreadyDebuggedError(owner);
1830
+ }
1831
+ await writeLockMetadata(ownPath, { ...metadata, state: "owned" });
1832
+ } catch (error) {
1833
+ await unlink(ownPath).catch(() => {
1834
+ });
1835
+ throw error;
1836
+ }
1837
+ let released = false;
1838
+ return {
1839
+ path: ownPath,
1840
+ release: async () => {
1841
+ if (released) {
1842
+ return;
1843
+ }
1844
+ released = true;
1845
+ const current = await readLockMetadata(ownPath);
1846
+ if (current?.token !== token || current.pid !== pid) {
1847
+ return;
1848
+ }
1849
+ await unlink(ownPath).catch((error) => {
1850
+ if (!isNodeError(error, "ENOENT")) {
1851
+ throw error;
1852
+ }
1853
+ });
1854
+ }
1855
+ };
1856
+ }
1857
+ function debugTargetIdentity(target) {
1858
+ const targetIndex = target.targetIndex ?? 0;
1859
+ if (target.kind === "port") {
1860
+ return JSON.stringify({
1861
+ kind: "port",
1862
+ host: normalizeHost(target.host),
1863
+ port: target.port,
1864
+ targetIndex
1865
+ });
1866
+ }
1867
+ return JSON.stringify({
1868
+ kind: "cf",
1869
+ region: target.region,
1870
+ org: target.org,
1871
+ space: target.space,
1872
+ app: target.app,
1873
+ targetIndex
1874
+ });
1875
+ }
1876
+ function defaultStateRoot() {
1877
+ const configured = process.env["CF_INSPECTOR_STATE_DIR"]?.trim();
1878
+ return configured === void 0 || configured.length === 0 ? join(homedir(), ".saptools") : configured;
1879
+ }
1880
+ async function findLiveContenders(lockDirectory, key, ownPath, isProcessAlive, getProcessStart) {
1881
+ const prefix = `${key}.`;
1882
+ const names = await readdir(lockDirectory);
1883
+ const contenders = [];
1884
+ for (const name of names) {
1885
+ if (!name.startsWith(prefix) || !name.endsWith(LOCK_FILE_SUFFIX)) {
1886
+ continue;
1887
+ }
1888
+ const path = join(lockDirectory, name);
1889
+ if (path === ownPath) {
1890
+ continue;
1891
+ }
1892
+ const info = await stat(path).catch(() => {
1893
+ });
1894
+ const metadata = await readLockMetadata(path) ?? (info === void 0 ? void 0 : metadataFromFilename(name, key, info.mtimeMs));
1895
+ if (metadata !== void 0) {
1896
+ if (ownerIsAlive(metadata, isProcessAlive, getProcessStart)) {
1897
+ contenders.push(metadata);
1898
+ } else {
1899
+ await unlink(path).catch(() => {
1900
+ });
1901
+ }
1902
+ continue;
1903
+ }
1904
+ if (info !== void 0) {
1905
+ contenders.push({
1906
+ pid: 0,
1907
+ state: "owned",
1908
+ startedAt: new Date(info.mtimeMs).toISOString(),
1909
+ token: "unknown",
1910
+ target: "unknown"
1911
+ });
1912
+ }
1913
+ }
1914
+ return contenders;
1915
+ }
1916
+ async function readLockMetadata(path) {
1917
+ try {
1918
+ const parsed = JSON.parse(await readFile(path, "utf8"));
1919
+ if (!isRecord2(parsed)) {
1920
+ return void 0;
1921
+ }
1922
+ const pid = parsed["pid"];
1923
+ const processStart2 = parsed["processStart"];
1924
+ const state = parsed["state"];
1925
+ const startedAt = parsed["startedAt"];
1926
+ const token = parsed["token"];
1927
+ const target = parsed["target"];
1928
+ 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) {
1929
+ return void 0;
1930
+ }
1931
+ return {
1932
+ pid,
1933
+ ...typeof processStart2 === "string" ? { processStart: processStart2 } : {},
1934
+ state,
1935
+ startedAt,
1936
+ token,
1937
+ target
1938
+ };
1939
+ } catch {
1940
+ return void 0;
1941
+ }
1942
+ }
1943
+ async function writeLockMetadata(path, metadata) {
1944
+ await writeFile(path, `${JSON.stringify(metadata)}
1945
+ `, { encoding: "utf8", mode: 384 });
1946
+ }
1947
+ function metadataFromFilename(name, key, mtimeMs) {
1948
+ const match = new RegExp(`^${key}\\.(\\d+)\\.(.+)\\${LOCK_FILE_SUFFIX}$`, "u").exec(name);
1949
+ const rawPid = match?.[1];
1950
+ const token = match?.[2];
1951
+ if (rawPid === void 0 || token === void 0 || token.length === 0) {
1952
+ return void 0;
1953
+ }
1954
+ const pid = Number.parseInt(rawPid, 10);
1955
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
1956
+ return void 0;
1957
+ }
1958
+ return {
1959
+ pid,
1960
+ state: "owned",
1961
+ startedAt: new Date(mtimeMs).toISOString(),
1962
+ token,
1963
+ target: "unknown"
1964
+ };
1965
+ }
1966
+ function ownerIsAlive(metadata, isProcessAlive, getProcessStart) {
1967
+ if (!isProcessAlive(metadata.pid)) {
1968
+ return false;
1969
+ }
1970
+ const currentStart = getProcessStart(metadata.pid);
1971
+ return metadata.processStart === void 0 || currentStart === void 0 || metadata.processStart === currentStart;
1972
+ }
1973
+ function processIsAlive(pid) {
1974
+ try {
1975
+ process.kill(pid, 0);
1976
+ const status = processStatus(pid);
1977
+ return !status?.startsWith("Z");
1978
+ } catch (error) {
1979
+ return isNodeError(error, "EPERM");
1980
+ }
1981
+ }
1982
+ function processStatus(pid) {
1983
+ return runPs(pid, "stat=");
1984
+ }
1985
+ function processStart(pid) {
1986
+ return runPs(pid, "lstart=");
1987
+ }
1988
+ function runPs(pid, field) {
1989
+ try {
1990
+ const value = execFileSync("ps", ["-o", field, "-p", pid.toString()], {
1991
+ encoding: "utf8",
1992
+ stdio: ["ignore", "pipe", "ignore"]
1993
+ }).trim();
1994
+ return value.length === 0 ? void 0 : value;
1995
+ } catch {
1996
+ return void 0;
1997
+ }
1998
+ }
1999
+ function alreadyDebuggedError(owner) {
2000
+ const ownerLabel = owner.pid > 0 ? `PID ${owner.pid.toString()}` : "an unknown process";
2001
+ return new CfInspectorError(
2002
+ "TARGET_ALREADY_DEBUGGED",
2003
+ `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.`
2004
+ );
2005
+ }
2006
+ function normalizeHost(host) {
2007
+ const normalized = host.trim().toLowerCase();
2008
+ return normalized === "localhost" || normalized === "::1" ? "127.0.0.1" : normalized;
2009
+ }
2010
+ function isRecord2(value) {
2011
+ return typeof value === "object" && value !== null;
2012
+ }
2013
+ function isNodeError(error, code) {
2014
+ return error instanceof Error && "code" in error && error.code === code;
2015
+ }
2016
+
1779
2017
  // src/cli/warnings.ts
1780
2018
  init_types();
1781
- import process2 from "process";
2019
+ import process3 from "process";
1782
2020
 
1783
2021
  // src/cli/captureParser.ts
1784
2022
  function parseCaptureList(raw) {
@@ -1893,7 +2131,7 @@ function warnOnCaptureMutationRisk(expressions, allowMutation) {
1893
2131
  return;
1894
2132
  }
1895
2133
  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(
2134
+ process3.stderr.write(
1897
2135
  `[cf-inspector] warning: ${riskyCount.toString()} capture ${riskyCount === 1 ? "expression looks" : "expressions look"} mutation-capable and ${suffix}
1898
2136
  `
1899
2137
  );
@@ -1908,7 +2146,7 @@ function enforceNativeConditionMutationPolicy(expression, allowMutation, context
1908
2146
  `${context} looks mutation-capable. Native breakpoint conditions cannot be protected by V8's side-effect guard; pass --allow-mutation to arm it explicitly.`
1909
2147
  );
1910
2148
  }
1911
- process2.stderr.write(
2149
+ process3.stderr.write(
1912
2150
  `[cf-inspector] warning: ${context} looks mutation-capable and will run as a native breakpoint condition; native conditions cannot be side-effect-gated.
1913
2151
  `
1914
2152
  );
@@ -1917,7 +2155,7 @@ function warnOnMutationRisk(expression, context) {
1917
2155
  if (!looksLikeMutation(expression)) {
1918
2156
  return;
1919
2157
  }
1920
- process2.stderr.write(
2158
+ process3.stderr.write(
1921
2159
  `[cf-inspector] warning: ${context} looks mutation-capable and will execute against the live inspectee without a side-effect guard.
1922
2160
  `
1923
2161
  );
@@ -1926,7 +2164,7 @@ function warnOnUnboundBreakpoints(handles) {
1926
2164
  for (const handle of handles) {
1927
2165
  if (handle.resolvedLocations.length === 0) {
1928
2166
  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(
2167
+ process3.stderr.write(
1930
2168
  `[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
2169
  `
1932
2170
  );
@@ -1937,14 +2175,14 @@ function warnOnImplicitInspectorSelection(session, targetWasExplicit, workerWasE
1937
2175
  const targetCount = session.targetCount ?? 1;
1938
2176
  const targetIndex = session.targetIndex ?? 0;
1939
2177
  if (!targetWasExplicit && targetCount > 1) {
1940
- process2.stderr.write(
2178
+ process3.stderr.write(
1941
2179
  `[cf-inspector] notice: attached to inspector target ${targetIndex.toString()} of ${targetCount.toString()}; pass --target <index> to pick another.
1942
2180
  `
1943
2181
  );
1944
2182
  }
1945
2183
  const workerCount = session.workerTargets?.length ?? 0;
1946
2184
  if (!workerWasExplicit && workerCount > 0) {
1947
- process2.stderr.write(
2185
+ process3.stderr.write(
1948
2186
  `[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
2187
  `
1950
2188
  );
@@ -1957,7 +2195,7 @@ function warnOnBoundBreakpointWithoutHit(handles) {
1957
2195
  if (boundCount === 0) {
1958
2196
  return;
1959
2197
  }
1960
- process2.stderr.write(
2198
+ process3.stderr.write(
1961
2199
  `[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
2200
  `
1963
2201
  );
@@ -1967,7 +2205,7 @@ function roundDurationMs(durationMs) {
1967
2205
  }
1968
2206
  function warnOnUnmatchedPause(pause) {
1969
2207
  const reason = pause.reason.length > 0 ? pause.reason : "unknown";
1970
- process2.stderr.write(
2208
+ process3.stderr.write(
1971
2209
  `[cf-inspector] warning: target is paused by another debugger event (${reason} at ${formatPauseLocation(pause)}); waiting for it to resume...
1972
2210
  `
1973
2211
  );
@@ -2142,65 +2380,75 @@ function parseWorkerId(value) {
2142
2380
  return trimmed;
2143
2381
  }
2144
2382
  async function withSession(target, fn, reportProgress, signal) {
2145
- const tunnel = await openTarget(target, reportProgress, signal);
2146
- let session;
2147
- 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;
2383
+ const lock = await acquireDebugSessionLock(target);
2176
2384
  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({
2385
+ const tunnel = await openTarget(target, reportProgress, signal);
2386
+ let session;
2387
+ try {
2388
+ reportProgress?.(
2389
+ `Connecting to the Node.js inspector at ${tunnel.host}:${tunnel.port.toString()}...`
2390
+ );
2391
+ session = await connectInspector({
2185
2392
  port: tunnel.port,
2186
2393
  host: tunnel.host,
2187
- ...selectionOptions(target.targetIndex, target.workerIndex, target.workerId)
2394
+ ...selectionOptions(target.targetIndex, target.workerIndex),
2395
+ ...target.workerId === void 0 ? {} : { workerId: target.workerId }
2188
2396
  });
2189
- group = singleSessionGroup(session);
2397
+ warnOnImplicitInspectorSelection(
2398
+ session,
2399
+ target.targetIndex !== void 0,
2400
+ target.workerIndex !== void 0 || target.workerId !== void 0
2401
+ );
2402
+ reportProgress?.("Inspector session is ready.");
2403
+ return await fn(session, tunnel.port);
2404
+ } finally {
2405
+ if (session) {
2406
+ reportProgress?.("Closing the inspector session...");
2407
+ await session.dispose();
2408
+ reportProgress?.("Inspector session closed.");
2409
+ }
2410
+ await tunnel.dispose();
2190
2411
  }
2191
- reportProgress?.("Inspector session is ready.");
2192
- return await fn(group, tunnel.port);
2193
2412
  } finally {
2413
+ await lock.release();
2414
+ }
2415
+ }
2416
+ async function withSessions(target, fn, reportProgress, signal) {
2417
+ const lock = await acquireDebugSessionLock(target);
2418
+ try {
2419
+ const tunnel = await openTarget(target, reportProgress, signal);
2420
+ let group;
2194
2421
  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.");
2422
+ reportProgress?.(
2423
+ `Connecting to the Node.js inspector at ${tunnel.host}:${tunnel.port.toString()}...`
2424
+ );
2425
+ const autoAttach = target.targetIndex === void 0 && target.workerIndex === void 0 && target.workerId === void 0 && target.mainOnly !== true;
2426
+ if (autoAttach) {
2427
+ group = await connectInspectorGroup({ port: tunnel.port, host: tunnel.host });
2428
+ } else {
2429
+ const session = await connectInspector({
2430
+ port: tunnel.port,
2431
+ host: tunnel.host,
2432
+ ...selectionOptions(target.targetIndex, target.workerIndex, target.workerId)
2433
+ });
2434
+ group = singleSessionGroup(session);
2200
2435
  }
2436
+ reportProgress?.("Inspector session is ready.");
2437
+ return await fn(group, tunnel.port);
2201
2438
  } finally {
2202
- await tunnel.dispose();
2439
+ try {
2440
+ if (group !== void 0) {
2441
+ const sessionCount = group.list().length;
2442
+ reportProgress?.(sessionCount === 1 ? "Closing the inspector session..." : `Closing ${sessionCount.toString()} inspector sessions...`);
2443
+ await group.dispose();
2444
+ reportProgress?.(sessionCount === 1 ? "Inspector session closed." : "Inspector sessions closed.");
2445
+ }
2446
+ } finally {
2447
+ await tunnel.dispose();
2448
+ }
2203
2449
  }
2450
+ } finally {
2451
+ await lock.release();
2204
2452
  }
2205
2453
  }
2206
2454
  function singleSessionGroup(session) {
@@ -2262,7 +2510,7 @@ async function handleAttach(opts) {
2262
2510
  writeJson({ host: tunnel.host, port: tunnel.port, ...version });
2263
2511
  return;
2264
2512
  }
2265
- process3.stdout.write(
2513
+ process4.stdout.write(
2266
2514
  `Connected to ${tunnel.host}:${tunnel.port.toString()}
2267
2515
  Browser: ${version.browser}
2268
2516
  Protocol: ${version.protocolVersion}
@@ -2274,7 +2522,7 @@ async function handleAttach(opts) {
2274
2522
  }
2275
2523
 
2276
2524
  // src/cli/commands/checkBreakpoint.ts
2277
- import process4 from "process";
2525
+ import process5 from "process";
2278
2526
 
2279
2527
  // src/pathMapper.ts
2280
2528
  init_types();
@@ -2561,25 +2809,25 @@ async function checkSession(session, matcher, requestedLine) {
2561
2809
  }
2562
2810
  function writeHumanCheck(result) {
2563
2811
  if (result.status === "script-not-loaded") {
2564
- process4.stdout.write(
2812
+ process5.stdout.write(
2565
2813
  `${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
2814
  `
2567
2815
  );
2568
2816
  return;
2569
2817
  }
2570
2818
  if (result.status === "unbreakable") {
2571
- process4.stdout.write(
2819
+ process5.stdout.write(
2572
2820
  `${result.file}:${result.line.toString()} matches a loaded script, but this exact line has no breakable location. Try a neighboring executable line.
2573
2821
  `
2574
2822
  );
2575
2823
  return;
2576
2824
  }
2577
- process4.stdout.write(`${result.file}:${result.line.toString()} is breakable:
2825
+ process5.stdout.write(`${result.file}:${result.line.toString()} is breakable:
2578
2826
  `);
2579
2827
  for (const script of result.scripts) {
2580
2828
  for (const location of script.locations) {
2581
2829
  const isolate = script.isolate.kind === "main" ? "main" : `worker ${script.isolate.workerId}`;
2582
- process4.stdout.write(
2830
+ process5.stdout.write(
2583
2831
  ` ${isolate} ${script.url} line ${(location.lineNumber + 1).toString()}:${((location.columnNumber ?? 0) + 1).toString()}
2584
2832
  `
2585
2833
  );
@@ -2588,7 +2836,7 @@ function writeHumanCheck(result) {
2588
2836
  }
2589
2837
 
2590
2838
  // src/cli/commands/eval.ts
2591
- import process5 from "process";
2839
+ import process6 from "process";
2592
2840
 
2593
2841
  // src/inspector/runtime.ts
2594
2842
  init_types();
@@ -2685,7 +2933,7 @@ async function handleEval(opts) {
2685
2933
  if (opts.json) {
2686
2934
  writeJson(result);
2687
2935
  if (result.exceptionDetails !== void 0) {
2688
- process5.exitCode = 1;
2936
+ process6.exitCode = 1;
2689
2937
  }
2690
2938
  return;
2691
2939
  }
@@ -2694,33 +2942,33 @@ async function handleEval(opts) {
2694
2942
  function writeHumanEvalResult(result) {
2695
2943
  if (result.exceptionDetails !== void 0) {
2696
2944
  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}
2945
+ process6.stderr.write(`${detail}
2698
2946
  `);
2699
- process5.exitCode = 1;
2947
+ process6.exitCode = 1;
2700
2948
  return;
2701
2949
  }
2702
2950
  const inner = result.result;
2703
2951
  if (inner === void 0) {
2704
- process5.stdout.write("\n");
2952
+ process6.stdout.write("\n");
2705
2953
  return;
2706
2954
  }
2707
2955
  if (typeof inner.value === "string") {
2708
- process5.stdout.write(`${inner.value}
2956
+ process6.stdout.write(`${inner.value}
2709
2957
  `);
2710
2958
  return;
2711
2959
  }
2712
2960
  if (typeof inner.description === "string") {
2713
- process5.stdout.write(`${inner.description}
2961
+ process6.stdout.write(`${inner.description}
2714
2962
  `);
2715
2963
  return;
2716
2964
  }
2717
- process5.stdout.write(`${JSON.stringify(inner.value)}
2965
+ process6.stdout.write(`${JSON.stringify(inner.value)}
2718
2966
  `);
2719
2967
  }
2720
2968
 
2721
2969
  // src/cli/commands/exception.ts
2722
2970
  import { performance as performance5 } from "perf_hooks";
2723
- import process7 from "process";
2971
+ import process8 from "process";
2724
2972
 
2725
2973
  // src/inspector/pause.ts
2726
2974
  init_types();
@@ -3814,19 +4062,19 @@ async function captureExpression(session, callFrameId, expression, maxValueLengt
3814
4062
  init_types();
3815
4063
 
3816
4064
  // src/cli/signals.ts
3817
- import process6 from "process";
4065
+ import process7 from "process";
3818
4066
  async function withTerminationSignal(fn) {
3819
4067
  const abort = new AbortController();
3820
4068
  const onSignal = () => {
3821
4069
  abort.abort();
3822
4070
  };
3823
- process6.once("SIGINT", onSignal);
3824
- process6.once("SIGTERM", onSignal);
4071
+ process7.once("SIGINT", onSignal);
4072
+ process7.once("SIGTERM", onSignal);
3825
4073
  try {
3826
4074
  return await fn(abort.signal);
3827
4075
  } finally {
3828
- process6.off("SIGINT", onSignal);
3829
- process6.off("SIGTERM", onSignal);
4076
+ process7.off("SIGINT", onSignal);
4077
+ process7.off("SIGTERM", onSignal);
3830
4078
  }
3831
4079
  }
3832
4080
 
@@ -3913,7 +4161,7 @@ async function resumeAfterException(session, snapshot, pausedStartedAt) {
3913
4161
  await resume(session);
3914
4162
  return withPausedDuration(snapshot, roundDurationMs(performance5.now() - pausedStartedAt));
3915
4163
  } catch {
3916
- process7.stderr.write(
4164
+ process8.stderr.write(
3917
4165
  "[cf-inspector] warning: Debugger.resume failed after exception capture; pausedDurationMs is unknown.\n"
3918
4166
  );
3919
4167
  return withPausedDuration(snapshot, null);
@@ -3927,7 +4175,7 @@ async function disablePauseOnExceptionsBestEffort(session) {
3927
4175
  }
3928
4176
 
3929
4177
  // src/cli/commands/listScripts.ts
3930
- import process8 from "process";
4178
+ import process9 from "process";
3931
4179
  async function handleListScripts(opts) {
3932
4180
  const target = await resolveTargetWithCurrentCfTarget(opts);
3933
4181
  const filter = compileScriptUrlFilter(opts.filter);
@@ -3937,7 +4185,7 @@ async function handleListScripts(opts) {
3937
4185
  return;
3938
4186
  }
3939
4187
  for (const script of scripts) {
3940
- process8.stdout.write(`${script.scriptId} ${script.url}
4188
+ process9.stdout.write(`${script.scriptId} ${script.url}
3941
4189
  `);
3942
4190
  }
3943
4191
  }
@@ -3968,7 +4216,7 @@ async function buildListedTargets(targets) {
3968
4216
  return buildListedTarget(target, index, workerResult.supported, workerResult.workers);
3969
4217
  } catch (error) {
3970
4218
  const message = error instanceof Error ? error.message : String(error);
3971
- process8.stderr.write(
4219
+ process9.stderr.write(
3972
4220
  `[cf-inspector] warning: worker discovery failed for raw target ${index.toString()}: ${message}
3973
4221
  `
3974
4222
  );
@@ -3995,7 +4243,7 @@ function looksLikeWorkerTarget(target) {
3995
4243
  return `${target.type} ${target.title} ${target.url}`.toLowerCase().includes("worker");
3996
4244
  }
3997
4245
  function writeTargetCountSummary(targetCount, workerCount) {
3998
- process8.stderr.write(
4246
+ process9.stderr.write(
3999
4247
  `[cf-inspector] ${targetCount.toString()} raw inspector ${targetCount === 1 ? "target" : "targets"}; ${workerCount.toString()} ${workerCount === 1 ? "worker" : "workers"}.
4000
4248
  `
4001
4249
  );
@@ -4006,7 +4254,7 @@ function warnOnMissingWorkers(targetCount, workerCount, targets) {
4006
4254
  }
4007
4255
  const supported = targets[0]?.workerDiscoverySupported === true;
4008
4256
  const supportHint = supported ? "NodeWorker discovery is available, but no live worker attached." : "This runtime did not expose NodeWorker discovery.";
4009
- process8.stderr.write(
4257
+ process9.stderr.write(
4010
4258
  `[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
4259
  `
4012
4260
  );
@@ -4014,12 +4262,12 @@ function warnOnMissingWorkers(targetCount, workerCount, targets) {
4014
4262
  function writeHumanTargets(targets) {
4015
4263
  for (const target of targets) {
4016
4264
  const workerLabel = target.likelyWorker ? " likely-worker" : "";
4017
- process8.stdout.write(
4265
+ process9.stdout.write(
4018
4266
  `${target.index.toString()} target ${target.type} ${target.title} ${target.url}${workerLabel}
4019
4267
  `
4020
4268
  );
4021
4269
  for (const worker of target.workers) {
4022
- process8.stdout.write(
4270
+ process9.stdout.write(
4023
4271
  ` ${worker.index.toString()} worker ${worker.type} ${worker.title} ${worker.url}
4024
4272
  `
4025
4273
  );
@@ -4096,7 +4344,7 @@ function matchesFilterTokens(value, tokens) {
4096
4344
  }
4097
4345
 
4098
4346
  // src/cli/commands/log.ts
4099
- import process9 from "process";
4347
+ import process10 from "process";
4100
4348
 
4101
4349
  // src/logpoint/stream.ts
4102
4350
  init_types();
@@ -4503,11 +4751,11 @@ function isZeroHitStop(reason) {
4503
4751
  }
4504
4752
  function writeLogSummary(stoppedReason, emitted, json) {
4505
4753
  if (json) {
4506
- process9.stderr.write(`${JSON.stringify({ stopped: stoppedReason, emitted })}
4754
+ process10.stderr.write(`${JSON.stringify({ stopped: stoppedReason, emitted })}
4507
4755
  `);
4508
4756
  return;
4509
4757
  }
4510
- process9.stderr.write(
4758
+ process10.stderr.write(
4511
4759
  `Stopped (${stoppedReason}); emitted ${emitted.toString()} log ${emitted === 1 ? "entry" : "entries"}.
4512
4760
  `
4513
4761
  );
@@ -4515,7 +4763,7 @@ function writeLogSummary(stoppedReason, emitted, json) {
4515
4763
 
4516
4764
  // src/cli/commands/snapshot.ts
4517
4765
  import { performance as performance6 } from "perf_hooks";
4518
- import process10 from "process";
4766
+ import process11 from "process";
4519
4767
  init_types();
4520
4768
  async function handleSnapshot(opts) {
4521
4769
  const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
@@ -4694,7 +4942,7 @@ async function resumeAfterSnapshot(session, snapshot, pausedStartedAt, reportPro
4694
4942
  reportProgress?.("Target resumed.");
4695
4943
  return withPausedDuration(snapshot, roundDurationMs(performance6.now() - pausedStartedAt));
4696
4944
  } catch {
4697
- process10.stderr.write(
4945
+ process11.stderr.write(
4698
4946
  "[cf-inspector] warning: Debugger.resume failed after snapshot; pausedDurationMs is unknown.\n"
4699
4947
  );
4700
4948
  return withPausedDuration(snapshot, null);
@@ -4707,7 +4955,7 @@ function parseSetupEvals(raw) {
4707
4955
 
4708
4956
  // src/cli/commands/watch.ts
4709
4957
  import { performance as performance7 } from "perf_hooks";
4710
- import process11 from "process";
4958
+ import process12 from "process";
4711
4959
  init_types();
4712
4960
  async function handleWatch(opts) {
4713
4961
  const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
@@ -4806,7 +5054,7 @@ async function runWatchGroup(group, command, opts, signal) {
4806
5054
  await resume(hit.session);
4807
5055
  hit.session.debuggerState.paused = false;
4808
5056
  } catch {
4809
- process11.stderr.write("[cf-inspector] warning: Debugger.resume failed during watch.\n");
5057
+ process12.stderr.write("[cf-inspector] warning: Debugger.resume failed during watch.\n");
4810
5058
  stoppedReason = "transport-closed";
4811
5059
  break;
4812
5060
  }
@@ -4820,7 +5068,7 @@ async function runWatchGroup(group, command, opts, signal) {
4820
5068
  }
4821
5069
  } finally {
4822
5070
  const cleanup = await fanout.cleanup();
4823
- process11.stderr.write(
5071
+ process12.stderr.write(
4824
5072
  `[cf-inspector] breakpoint cleanup: cleared ${cleanup.cleared.toString()} of ${cleanup.attempted.toString()}; resumed ${cleanup.resumed.toString()} paused isolates.
4825
5073
  `
4826
5074
  );
@@ -4918,11 +5166,11 @@ function formatLocation(command, topFrame) {
4918
5166
  }
4919
5167
  function writeWatchSummary(reason, emitted, json) {
4920
5168
  if (json) {
4921
- process11.stderr.write(`${JSON.stringify({ stopped: reason, emitted })}
5169
+ process12.stderr.write(`${JSON.stringify({ stopped: reason, emitted })}
4922
5170
  `);
4923
5171
  return;
4924
5172
  }
4925
- process11.stderr.write(
5173
+ process12.stderr.write(
4926
5174
  `Stopped (${reason}); emitted ${emitted.toString()} watch ${emitted === 1 ? "event" : "events"}.
4927
5175
  `
4928
5176
  );
@@ -4952,7 +5200,7 @@ var collectStrings = (value, prev = []) => [
4952
5200
  function readPackageVersion() {
4953
5201
  let current = dirname(fileURLToPath(import.meta.url));
4954
5202
  for (let depth = 0; depth < 4; depth += 1) {
4955
- const candidate = join(current, "package.json");
5203
+ const candidate = join2(current, "package.json");
4956
5204
  try {
4957
5205
  const parsed = JSON.parse(readFileSync(candidate, "utf8"));
4958
5206
  if (typeof parsed === "object" && parsed !== null) {
@@ -5060,20 +5308,20 @@ function registerAttach(program) {
5060
5308
  // src/cli.ts
5061
5309
  init_types();
5062
5310
  try {
5063
- await main(process12.argv);
5311
+ await main(process13.argv);
5064
5312
  } catch (err) {
5065
5313
  if (err instanceof CfInspectorError) {
5066
- process12.stderr.write(`Error [${err.code}]: ${err.message}
5314
+ process13.stderr.write(`Error [${err.code}]: ${err.message}
5067
5315
  `);
5068
5316
  if (err.detail !== void 0) {
5069
- process12.stderr.write(` detail: ${err.detail}
5317
+ process13.stderr.write(` detail: ${err.detail}
5070
5318
  `);
5071
5319
  }
5072
- process12.exit(1);
5320
+ process13.exit(1);
5073
5321
  }
5074
5322
  const message = err instanceof Error ? err.message : String(err);
5075
- process12.stderr.write(`Error: ${message}
5323
+ process13.stderr.write(`Error: ${message}
5076
5324
  `);
5077
- process12.exit(1);
5325
+ process13.exit(1);
5078
5326
  }
5079
5327
  //# sourceMappingURL=cli.js.map