@saptools/cf-inspector 0.4.7 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ Built so an AI agent (or a CI job) can drive a debugger from a single shell comm
10
10
  [![license](https://img.shields.io/npm/l/@saptools/cf-inspector.svg?style=flat&color=blue)](./LICENSE)
11
11
  [![node](https://img.shields.io/node/v/@saptools/cf-inspector.svg?style=flat&color=339933&logo=node.js&logoColor=white)](https://nodejs.org)
12
12
 
13
- [Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [API](#-programmatic-usage) • [How it works](#-how-it-works)
13
+ [Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [How it works](#-how-it-works)
14
14
 
15
15
  </div>
16
16
 
@@ -129,9 +129,10 @@ strict immediate error is preferred.
129
129
 
130
130
  For Cloud Foundry targets, replace `--port` with
131
131
  `--region/--org/--space/--app`. Cloud Foundry commands and tunnel readiness
132
- allow up to 180 seconds by default. `--cf-timeout <seconds>` overrides only the
133
- tunnel-readiness phase; snapshot `--timeout` separately controls how long to
134
- wait for a breakpoint hit.
132
+ allow up to 180 seconds by default. For commands without their own wait
133
+ semantics, `--timeout <seconds>` controls CF tunnel readiness. For breakpoint
134
+ and exception commands, `--timeout` is reserved for the command wait and tunnel
135
+ readiness keeps the 180-second default.
135
136
 
136
137
  ### 📡 `cf-inspector log`
137
138
 
@@ -266,105 +267,29 @@ cf-inspector eval --port 9229 --expr 'process.uptime()'
266
267
 
267
268
  ### 📜 `cf-inspector list-scripts`
268
269
 
269
- Print every script the V8 instance knows about (useful for debugging path-mapping issues).
270
+ Print every script the V8 instance knows about (useful for debugging path-mapping issues). Add `--filter <pattern>` to narrow noisy script lists with a literal/wildcard pattern; `|` separates alternatives and `.*` / `.+` match variable text.
270
271
 
271
272
  ```bash
272
- cf-inspector list-scripts --port 9229
273
+ cf-inspector list-scripts --port 9229 --filter 'dist/.+\.js'
273
274
  ```
274
275
 
275
- ### 🔗 `cf-inspector attach`
276
+ ### 🎯 `cf-inspector list-targets`
276
277
 
277
- Connect, fetch the runtime version, print it, disconnect. Useful as a smoke-test that the tunnel is healthy.
278
+ Print `/json/list` inspector targets with stable indexes. Use the index with `--target <index>` when a long-running worker thread appears as a separate target.
278
279
 
279
280
  ```bash
280
- cf-inspector attach --port 9229
281
+ cf-inspector list-targets --port 9229
282
+ cf-inspector snapshot --port 9229 --target 1 --bp dist/worker.js:42
281
283
  ```
282
284
 
283
- ---
285
+ ### 🔗 `cf-inspector attach`
284
286
 
285
- ## 🧑‍💻 Programmatic Usage
286
-
287
- ```ts
288
- import {
289
- connectInspector,
290
- setBreakpoint,
291
- waitForPause,
292
- captureSnapshot,
293
- evaluateOnFrame,
294
- resume,
295
- } from "@saptools/cf-inspector";
296
-
297
- const session = await connectInspector({ port: 9229 });
298
- const bp = await setBreakpoint(session, {
299
- file: "src/handler.ts",
300
- line: 42,
301
- });
302
- const pause = await waitForPause(session, { timeoutMs: 30_000 });
303
- const snapshot = await captureSnapshot(session, pause, {
304
- captures: ["this.user"],
305
- maxValueLength: 4096,
306
- });
307
- const topFrame = pause.callFrames[0];
308
- if (topFrame === undefined) {
309
- throw new Error("Breakpoint paused without a call frame");
310
- }
311
- const customValue = await evaluateOnFrame(session, topFrame.callFrameId, "this.user");
312
- await resume(session);
313
- await session.dispose();
287
+ Connect, fetch the runtime version, print it, disconnect. Useful as a smoke-test that the tunnel is healthy.
314
288
 
315
- console.log({ bp, snapshot, customValue });
289
+ ```bash
290
+ cf-inspector attach --port 9229
316
291
  ```
317
292
 
318
- <details>
319
- <summary><b>📚 Full export list</b></summary>
320
-
321
- | Export | Description |
322
- | --- | --- |
323
- | `connectInspector(options)` | Open a CDP WebSocket session against a port |
324
- | `setBreakpoint(session, location)` | Set a breakpoint by file/line + optional remote root and `hitCount` |
325
- | `removeBreakpoint(session, id)` | Remove a breakpoint by id |
326
- | `setPauseOnExceptions(session, state)` | Configure exception pause state: `none / uncaught / caught / all` |
327
- | `waitForPause(session, options)` | Resolve when the next `Debugger.paused` event fires; supports `pauseReasons` allow-list |
328
- | `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes, `stackDepth` + `stackCaptures` for multi-frame walks, or `maxValueLength` to override the default captured value limit |
329
- | `captureException(session, pause, maxValueLength)` | Materialize the exception attached to a `Debugger.paused` event |
330
- | `walkStack(session, frames, options)` | Walk a call stack and evaluate per-frame expressions |
331
- | `evaluateOnFrame(session, frameId, expression)` | Evaluate in a paused frame |
332
- | `evaluateGlobal(session, expression)` | Evaluate against the global Runtime |
333
- | `listScripts(session)` | Return the scripts the V8 instance knows about |
334
- | `resume(session)` | Resume execution |
335
- | `streamLogpoint(session, options)` | Stream a non-pausing logpoint until duration / signal / max-events / transport-close |
336
- | `buildLogpointCondition(sentinel, expression, options?)` | Build the CDP `condition` string for a logpoint with optional predicate / hit-count gates |
337
- | `buildHitCountedCondition(hitCount, key, userCondition?)` | Build the hit-count gate used by `setBreakpoint({ hitCount })` |
338
- | `parseRemoteRoot(value)` | Parse a literal/regex remote-root setting |
339
- | `buildBreakpointUrlRegex(input)` | Build a CDP `urlRegex` for a file path |
340
- | `CfInspectorError` | Rich error class with typed `code` |
341
-
342
- </details>
343
-
344
- <details>
345
- <summary><b>🧪 Error codes</b></summary>
346
-
347
- | Code | When |
348
- | --- | --- |
349
- | `INVALID_ARGUMENT` | A numeric flag (`--port`, `--timeout`, `--duration`, …) is not a positive integer |
350
- | `INVALID_BREAKPOINT` | `--bp` / `--at` is not in `file:line` form, or line is not a positive integer |
351
- | `INVALID_REMOTE_ROOT` | `--remote-root` regex did not compile |
352
- | `INVALID_EXPRESSION` | `--condition` or `--expr` failed to parse on V8 (`Runtime.compileScript` reported a SyntaxError) — fast-fail before the breakpoint is set |
353
- | `INVALID_HIT_COUNT` | `--hit-count` is not a positive integer |
354
- | `INVALID_PAUSE_TYPE` | `cf-inspector exception --type` is not one of `uncaught / caught / all` |
355
- | `BREAKPOINT_DID_NOT_BIND` | Reserved: a breakpoint resolved to no scripts. Currently surfaced as a stderr warning only — see `BreakpointHandle.resolvedLocations` for programmatic detection |
356
- | `INSPECTOR_DISCOVERY_FAILED` | `/json/list` did not return a usable WebSocket URL |
357
- | `INSPECTOR_CONNECTION_FAILED` | WebSocket handshake failed, or the connection closed mid-request |
358
- | `CDP_REQUEST_FAILED` | A CDP method returned an error result, timed out, or failed to send |
359
- | `BREAKPOINT_NOT_HIT` | The breakpoint did not hit before the timeout elapsed |
360
- | `UNRELATED_PAUSE` | The target paused somewhere else and `--fail-on-unmatched-pause` was enabled |
361
- | `UNRELATED_PAUSE_TIMEOUT` | The target stayed paused somewhere else until the snapshot timeout elapsed |
362
- | `EVALUATION_FAILED` | Reserved for future use — current evaluation paths surface remote exceptions inline via `CapturedExpression.error` instead of throwing |
363
- | `MISSING_TARGET` | Neither `--port` nor a complete CF target (`--region/--org/--space/--app`) was provided |
364
- | `ABORTED` | Reserved for future use by long-running streams when an `AbortSignal` fires |
365
-
366
- </details>
367
-
368
293
  ---
369
294
 
370
295
  ## 🔭 How it works
package/dist/cli.js CHANGED
@@ -409,15 +409,54 @@ async function openCfTunnel(target) {
409
409
  ...target.signal === void 0 ? {} : { signal: target.signal },
410
410
  ...target.onStatus === void 0 ? {} : { onStatus: target.onStatus }
411
411
  };
412
- const handle = await startDebugger(opts);
412
+ try {
413
+ const handle = await startDebugger(opts);
414
+ return {
415
+ localPort: handle.session.localPort,
416
+ handle,
417
+ dispose: async () => {
418
+ await handle.dispose();
419
+ }
420
+ };
421
+ } catch (err) {
422
+ return reuseExistingTunnelOrThrow(err, target.onStatus);
423
+ }
424
+ }
425
+ function reuseExistingTunnelOrThrow(err, onStatus) {
426
+ if (!isSessionAlreadyRunningError(err)) {
427
+ throw err;
428
+ }
429
+ const message = err instanceof Error ? err.message : String(err);
430
+ const port = extractExistingTunnelPort(message);
431
+ if (port === void 0) {
432
+ throw err;
433
+ }
434
+ const warning = `Reusing existing tunnel on port ${port.toString()}`;
435
+ onStatus?.("ready", warning);
413
436
  return {
414
- localPort: handle.session.localPort,
415
- handle,
416
- dispose: async () => {
417
- await handle.dispose();
418
- }
437
+ localPort: port,
438
+ dispose: () => Promise.resolve()
419
439
  };
420
440
  }
441
+ function isSessionAlreadyRunningError(err) {
442
+ if (typeof err !== "object" || err === null) {
443
+ return false;
444
+ }
445
+ const code = err.code;
446
+ return code === "SESSION_ALREADY_RUNNING";
447
+ }
448
+ function extractExistingTunnelPort(message) {
449
+ const match = /on port (\d+)/i.exec(message);
450
+ if (match === null) {
451
+ return void 0;
452
+ }
453
+ const rawPort = match[1];
454
+ if (rawPort === void 0) {
455
+ return void 0;
456
+ }
457
+ const port = Number.parseInt(rawPort, 10);
458
+ return Number.isNaN(port) ? void 0 : port;
459
+ }
421
460
 
422
461
  // src/inspector/session.ts
423
462
  import { performance } from "perf_hooks";
@@ -782,11 +821,12 @@ async function connectInspector(options) {
782
821
  const host = options.host ?? DEFAULT_HOST;
783
822
  const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
784
823
  const targets = await discoverInspectorTargets(host, options.port, connectTimeoutMs);
785
- const target = targets[0];
824
+ const targetIndex = options.targetIndex ?? 0;
825
+ const target = targets[targetIndex];
786
826
  if (!target) {
787
827
  throw new CfInspectorError(
788
828
  "INSPECTOR_DISCOVERY_FAILED",
789
- `No inspector targets available on ${host}:${options.port.toString()}`
829
+ `No inspector target at index ${targetIndex.toString()} on ${host}:${options.port.toString()} (available: ${targets.length.toString()})`
790
830
  );
791
831
  }
792
832
  const client = await CdpClient.connect({
@@ -882,22 +922,23 @@ function parsePositiveInt(raw, label) {
882
922
  }
883
923
  return value;
884
924
  }
885
- async function resolveTargetWithCurrentCfTarget(opts) {
925
+ async function resolveTargetWithCurrentCfTarget(opts, options = {}) {
886
926
  const port = parsePositiveInt(opts.port, "--port");
927
+ const targetIndex = parseTargetIndex(opts.target);
887
928
  if (port !== void 0) {
888
- return { kind: "port", port, host: opts.host ?? "127.0.0.1" };
929
+ return { kind: "port", port, host: opts.host ?? "127.0.0.1", ...targetIndexOption(targetIndex) };
889
930
  }
890
931
  const app = optionalText(opts.app);
891
932
  if (app === void 0) {
892
933
  throw missingTargetError();
893
934
  }
894
- const cfTimeoutSec = parsePositiveInt(opts.cfTimeout, "--cf-timeout") ?? DEFAULT_CF_TIMEOUT_SEC;
935
+ const tunnelTimeoutSec = parseTunnelTimeout(opts, options);
895
936
  const region = optionalText(opts.region);
896
937
  const apiEndpoint = optionalText(opts.apiEndpoint);
897
938
  const org = optionalText(opts.org);
898
939
  const space = optionalText(opts.space);
899
940
  if (region !== void 0 && org !== void 0 && space !== void 0) {
900
- return buildCfTarget(region, apiEndpoint, org, space, app, cfTimeoutSec);
941
+ return buildCfTarget(region, apiEndpoint, org, space, app, tunnelTimeoutSec, targetIndex);
901
942
  }
902
943
  const current = await readCurrentTarget();
903
944
  if (current === void 0) {
@@ -912,10 +953,30 @@ async function resolveTargetWithCurrentCfTarget(opts) {
912
953
  org ?? current.org,
913
954
  space ?? current.space,
914
955
  app,
915
- cfTimeoutSec
956
+ tunnelTimeoutSec,
957
+ targetIndex
916
958
  );
917
959
  }
918
- function buildCfTarget(region, apiEndpoint, org, space, app, cfTimeoutSec) {
960
+ function parseTunnelTimeout(opts, options) {
961
+ if (options.useTimeoutForTunnel === false) {
962
+ return DEFAULT_CF_TIMEOUT_SEC;
963
+ }
964
+ return parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_CF_TIMEOUT_SEC;
965
+ }
966
+ function parseTargetIndex(raw) {
967
+ if (raw === void 0) {
968
+ return void 0;
969
+ }
970
+ const value = Number.parseInt(raw, 10);
971
+ if (Number.isNaN(value) || value < 0 || value.toString() !== raw.trim()) {
972
+ throw new CfInspectorError("INVALID_ARGUMENT", `Invalid --target: "${raw}" \u2014 expected a non-negative integer`);
973
+ }
974
+ return value;
975
+ }
976
+ function targetIndexOption(targetIndex) {
977
+ return targetIndex === void 0 ? {} : { targetIndex };
978
+ }
979
+ function buildCfTarget(region, apiEndpoint, org, space, app, tunnelTimeoutSec, targetIndex) {
919
980
  return {
920
981
  kind: "cf",
921
982
  region,
@@ -923,7 +984,8 @@ function buildCfTarget(region, apiEndpoint, org, space, app, cfTimeoutSec) {
923
984
  org,
924
985
  space,
925
986
  app,
926
- cfTimeoutMs: cfTimeoutSec * 1e3
987
+ tunnelTimeoutMs: tunnelTimeoutSec * 1e3,
988
+ ...targetIndexOption(targetIndex)
927
989
  };
928
990
  }
929
991
  function optionalText(value) {
@@ -966,7 +1028,11 @@ async function withSession(target, fn, reportProgress) {
966
1028
  reportProgress?.(
967
1029
  `Connecting to the Node.js inspector at ${tunnel.host}:${tunnel.port.toString()}...`
968
1030
  );
969
- session = await connectInspector({ port: tunnel.port, host: tunnel.host });
1031
+ session = await connectInspector({
1032
+ port: tunnel.port,
1033
+ host: tunnel.host,
1034
+ ...targetIndexOption(target.targetIndex)
1035
+ });
970
1036
  reportProgress?.("Inspector session is ready.");
971
1037
  return await fn(session, tunnel.port);
972
1038
  } finally {
@@ -993,10 +1059,10 @@ async function openTarget(target, reportProgress) {
993
1059
  org: target.org,
994
1060
  space: target.space,
995
1061
  app: target.app,
996
- tunnelReadyTimeoutMs: target.cfTimeoutMs,
1062
+ tunnelReadyTimeoutMs: target.tunnelTimeoutMs,
997
1063
  ...reportProgress === void 0 ? {} : {
998
- onStatus: (status) => {
999
- reportProgress(formatCfTunnelStatus(status));
1064
+ onStatus: (status, message) => {
1065
+ reportProgress(message ?? formatCfTunnelStatus(status));
1000
1066
  }
1001
1067
  }
1002
1068
  });
@@ -2081,8 +2147,9 @@ import process5 from "process";
2081
2147
  function warnOnUnboundBreakpoints(handles) {
2082
2148
  for (const handle of handles) {
2083
2149
  if (handle.resolvedLocations.length === 0) {
2150
+ 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." : "";
2084
2151
  process5.stderr.write(
2085
- `[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.
2152
+ `[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}
2086
2153
  `
2087
2154
  );
2088
2155
  }
@@ -2122,7 +2189,7 @@ function formatPauseLocation(pause) {
2122
2189
  // src/cli/commands/exception.ts
2123
2190
  var VALID_PAUSE_TYPES = ["uncaught", "caught", "all"];
2124
2191
  async function handleException(opts) {
2125
- const target = await resolveTargetWithCurrentCfTarget(opts);
2192
+ const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
2126
2193
  const prepared = prepareExceptionCommand(opts, target);
2127
2194
  const result = await runExceptionCommand(prepared, opts);
2128
2195
  if (opts.json) {
@@ -2201,7 +2268,8 @@ async function disablePauseOnExceptionsBestEffort(session) {
2201
2268
  import process7 from "process";
2202
2269
  async function handleListScripts(opts) {
2203
2270
  const target = await resolveTargetWithCurrentCfTarget(opts);
2204
- const scripts = await withSession(target, (session) => Promise.resolve(listScripts(session)));
2271
+ const filter = compileScriptUrlFilter(opts.filter);
2272
+ const scripts = (await withSession(target, (session) => Promise.resolve(listScripts(session)))).filter((script) => filter === void 0 || filter(script.url));
2205
2273
  if (opts.json) {
2206
2274
  writeJson(scripts);
2207
2275
  return;
@@ -2211,6 +2279,92 @@ async function handleListScripts(opts) {
2211
2279
  `);
2212
2280
  }
2213
2281
  }
2282
+ async function handleListTargets(opts) {
2283
+ const target = await resolveTargetWithCurrentCfTarget(opts);
2284
+ const tunnel = await openTarget(target);
2285
+ try {
2286
+ const targets = await discoverInspectorTargets(tunnel.host, tunnel.port, 5e3);
2287
+ const indexedTargets = targets.map((entry, index) => ({ index, ...entry }));
2288
+ if (opts.json) {
2289
+ writeJson(indexedTargets);
2290
+ return;
2291
+ }
2292
+ for (const entry of indexedTargets) {
2293
+ process7.stdout.write(`${entry.index.toString()} ${entry.type} ${entry.title} ${entry.url}
2294
+ `);
2295
+ }
2296
+ } finally {
2297
+ await tunnel.dispose();
2298
+ }
2299
+ }
2300
+ function compileScriptUrlFilter(pattern) {
2301
+ if (pattern === void 0 || pattern.length === 0) {
2302
+ return void 0;
2303
+ }
2304
+ const alternatives = splitPatternAlternatives(pattern).map((alternative) => parseFilterTokens(alternative)).filter((tokens) => tokens.length > 0);
2305
+ return (url) => alternatives.some((tokens) => matchesFilterTokens(url, tokens));
2306
+ }
2307
+ function splitPatternAlternatives(pattern) {
2308
+ const alternatives = [];
2309
+ let current = "";
2310
+ for (let index = 0; index < pattern.length; index++) {
2311
+ const char = pattern[index] ?? "";
2312
+ if (char === "\\" && index + 1 < pattern.length) {
2313
+ current += `${char}${pattern[index + 1] ?? ""}`;
2314
+ index++;
2315
+ } else if (char === "|") {
2316
+ alternatives.push(current);
2317
+ current = "";
2318
+ } else {
2319
+ current += char;
2320
+ }
2321
+ }
2322
+ alternatives.push(current);
2323
+ return alternatives;
2324
+ }
2325
+ function parseFilterTokens(pattern) {
2326
+ const tokens = [];
2327
+ let literal = "";
2328
+ for (let index = 0; index < pattern.length; index++) {
2329
+ const char = pattern[index] ?? "";
2330
+ const nextChar = pattern[index + 1];
2331
+ if (char === "\\" && nextChar !== void 0) {
2332
+ literal += nextChar;
2333
+ index++;
2334
+ } else if (char === "." && (nextChar === "*" || nextChar === "+")) {
2335
+ if (literal.length > 0) {
2336
+ tokens.push(literal);
2337
+ }
2338
+ literal = "";
2339
+ tokens.push({ kind: "wildcard", minChars: nextChar === "+" ? 1 : 0 });
2340
+ index++;
2341
+ } else {
2342
+ literal += char;
2343
+ }
2344
+ }
2345
+ if (literal.length > 0) {
2346
+ tokens.push(literal);
2347
+ }
2348
+ return tokens;
2349
+ }
2350
+ function matchesFilterTokens(value, tokens) {
2351
+ let position = 0;
2352
+ for (const token of tokens) {
2353
+ if (typeof token === "string") {
2354
+ const nextPosition = value.indexOf(token, position);
2355
+ if (nextPosition === -1) {
2356
+ return false;
2357
+ }
2358
+ position = nextPosition + token.length;
2359
+ } else {
2360
+ if (token.minChars === 1 && position >= value.length) {
2361
+ return false;
2362
+ }
2363
+ position += token.minChars;
2364
+ }
2365
+ }
2366
+ return true;
2367
+ }
2214
2368
 
2215
2369
  // src/cli/commands/log.ts
2216
2370
  import process9 from "process";
@@ -2534,7 +2688,7 @@ import { performance as performance4 } from "perf_hooks";
2534
2688
  import process10 from "process";
2535
2689
  init_types();
2536
2690
  async function handleSnapshot(opts) {
2537
- const target = await resolveTargetWithCurrentCfTarget(opts);
2691
+ const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
2538
2692
  const prepared = prepareSnapshotCommand(opts, target);
2539
2693
  const reportProgress = opts.quiet === true ? void 0 : writeProgress;
2540
2694
  const result = await runSnapshotCommand(prepared, opts, reportProgress);
@@ -2660,7 +2814,7 @@ import { performance as performance5 } from "perf_hooks";
2660
2814
  import process11 from "process";
2661
2815
  init_types();
2662
2816
  async function handleWatch(opts) {
2663
- const target = await resolveTargetWithCurrentCfTarget(opts);
2817
+ const target = await resolveTargetWithCurrentCfTarget(opts, { useTimeoutForTunnel: false });
2664
2818
  const prepared = prepareWatchCommand(opts, target);
2665
2819
  let stoppedReason = "signal";
2666
2820
  let emitted = 0;
@@ -2878,8 +3032,9 @@ function writeWatchSummary(reason, emitted, json) {
2878
3032
  }
2879
3033
 
2880
3034
  // src/cli/program.ts
2881
- function applyTargetOptions(cmd) {
2882
- return cmd.option("--port <number>", "Local port the inspector or tunnel listens on").option("--host <host>", "Hostname (default: 127.0.0.1)", "127.0.0.1").option("--region <key>", "CF region key (default: current cf target)").option("--api-endpoint <url>", "CF API endpoint override for --region").option("--org <name>", "CF org name (default: current cf target)").option("--space <name>", "CF space name (default: current cf target)").option("--app <name>", "CF app name when not using --port").option("--cf-timeout <seconds>", "Timeout for CF tunnel readiness in seconds (default: 180)");
3035
+ function applyTargetOptions(cmd, options = {}) {
3036
+ const withBaseOptions = cmd.option("--port <number>", "Local port the inspector or tunnel listens on").option("--host <host>", "Hostname (default: 127.0.0.1)", "127.0.0.1").option("--region <key>", "CF region key (default: current cf target)").option("--api-endpoint <url>", "CF API endpoint override for --region").option("--org <name>", "CF org name (default: current cf target)").option("--space <name>", "CF space name (default: current cf target)").option("--app <name>", "CF app name when not using --port").option("--target <index>", "Inspector target index from /json/list (default: 0)");
3037
+ return options.includeTimeout === false ? withBaseOptions : withBaseOptions.option("--timeout <seconds>", "Timeout for CF tunnel readiness in seconds (default: 180)");
2883
3038
  }
2884
3039
  var collectStrings = (value, prev = []) => [
2885
3040
  ...prev,
@@ -2894,12 +3049,14 @@ async function main(argv) {
2894
3049
  registerException(program);
2895
3050
  registerEval(program);
2896
3051
  registerListScripts(program);
3052
+ registerListTargets(program);
2897
3053
  registerAttach(program);
2898
3054
  await program.parseAsync([...argv]);
2899
3055
  }
2900
3056
  function registerSnapshot(program) {
2901
3057
  applyTargetOptions(
2902
- program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume")
3058
+ program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume"),
3059
+ { includeTimeout: false }
2903
3060
  ).option("--bp <file:line>", "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--condition <expr>", "Only pause when this JS expression evaluates truthy in the paused frame").option("--hit-count <n>", "Only pause after the breakpoint has been hit N or more times").option("--stack-depth <n>", "Walk this many call frames when capturing (default: 1, only top frame)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame in the stack").option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--no-json", "Print a human-readable summary instead of JSON").option("--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) => {
2904
3061
  await handleSnapshot(opts);
2905
3062
  });
@@ -2913,14 +3070,16 @@ function registerLog(program) {
2913
3070
  }
2914
3071
  function registerWatch(program) {
2915
3072
  applyTargetOptions(
2916
- program.command("watch").description("Stream a snapshot per breakpoint hit (multi-shot watch); resume between hits")
3073
+ program.command("watch").description("Stream a snapshot per breakpoint hit (multi-shot watch); resume between hits"),
3074
+ { includeTimeout: false }
2917
3075
  ).option("--bp <file:line>", "Breakpoint location (repeatable), e.g. src/handler.ts:42", collectStrings, []).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate per hit").option("--condition <expr>", "Only emit hits where this JS expression evaluates truthy").option("--hit-count <n>", "Start emitting after the line has been hit N or more times").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--duration <seconds>", "Stop streaming after N seconds (default: run until SIGINT)").option("--max-events <n>", "Stop streaming after emitting N watch events").option("--timeout <seconds>", "How long to wait for the next hit before giving up (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--stack-depth <n>", "Walk this many call frames per hit (default: 1)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame").option("--include-scopes", "Include expanded paused-frame scopes per hit").option("--no-json", "Print human-readable lines instead of JSON Lines").action(async (opts) => {
2918
3076
  await handleWatch(opts);
2919
3077
  });
2920
3078
  }
2921
3079
  function registerException(program) {
2922
3080
  applyTargetOptions(
2923
- program.command("exception").description("Pause on a thrown exception, capture the value and frame, then resume")
3081
+ program.command("exception").description("Pause on a thrown exception, capture the value and frame, then resume"),
3082
+ { includeTimeout: false }
2924
3083
  ).option("--type <state>", "Pause type: uncaught (default), caught, or all").option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option("--timeout <seconds>", "How long to wait for an exception (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--stack-depth <n>", "Walk this many call frames when capturing (default: 1)").option("--stack-captures <expr,\u2026>", "Expressions to evaluate on each call frame in the stack").option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--no-json", "Print a human-readable summary instead of JSON").action(async (opts) => {
2925
3084
  await handleException(opts);
2926
3085
  });
@@ -2935,10 +3094,17 @@ function registerEval(program) {
2935
3094
  function registerListScripts(program) {
2936
3095
  applyTargetOptions(
2937
3096
  program.command("list-scripts").description("Print the scripts the V8 instance currently knows about")
2938
- ).option("--no-json", "Print scriptId<TAB>url instead of JSON").action(async (opts) => {
3097
+ ).option("--filter <pattern>", "Only include script URLs matching this pattern").option("--no-json", "Print scriptId<TAB>url instead of JSON").action(async (opts) => {
2939
3098
  await handleListScripts(opts);
2940
3099
  });
2941
3100
  }
3101
+ function registerListTargets(program) {
3102
+ applyTargetOptions(
3103
+ program.command("list-targets").description("Print inspector targets from /json/list for selecting workers with --target")
3104
+ ).option("--no-json", "Print index<TAB>type<TAB>title<TAB>url instead of JSON").action(async (opts) => {
3105
+ await handleListTargets(opts);
3106
+ });
3107
+ }
2942
3108
  function registerAttach(program) {
2943
3109
  applyTargetOptions(
2944
3110
  program.command("attach").description("Connect, fetch the inspector version, and disconnect (smoke-test)")