@saptools/cf-inspector 0.7.1 โ†’ 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/README.md CHANGED
@@ -37,6 +37,8 @@ Built so an AI agent (or a CI job) can drive a debugger from a single shell comm
37
37
  - ๐Ÿ“ก **Non-pausing logpoints** โ€” `cf-inspector log --at file:line --expr 'JSON.stringify({โ€ฆ})'` streams JSON Lines as the line executes without pausing the inspectee, with optional `--condition`, `--hit-count`, and `--max-events`
38
38
  - ๐Ÿ›ก๏ธ **Read-only capture guard** โ€” snapshot, watch, and exception captures use V8's side-effect analysis by default; `--allow-mutation` is an explicit escape hatch
39
39
  - ๐Ÿงต **Automatic worker fan-out** โ€” snapshot, watch, exception, and log attach to the main isolate plus every current or newly-spawned NodeWorker; explicit selectors remain available for pinning
40
+ - ๐Ÿšฆ **Machine-readable readiness** โ€” opt into a versioned `breakpoint-armed` stderr event before triggering external traffic; no prose matching or guessed delay required
41
+ - ๐Ÿ“œ **Isolate-aware script listing** โ€” `list-scripts` aggregates the main isolate and every current worker, tagging every script with its isolate
40
42
  - ๐Ÿง  **Agent-friendly** โ€” JSON-by-default I/O, deterministic shapes, and explicit `truncated`/`originalLength`/`omittedCount` metadata for bounded values
41
43
  - ๐Ÿงญ **Path mapping** โ€” local `src/handler.ts:42` is matched against the remote URL via a `urlRegex`, with optional `--remote-root` literal or regex (same DSL as `cds-debug`)
42
44
  - ๐Ÿ” **Composes with `cf-debugger`** โ€” pass `--app/--region/--org/--space` and the tunnel is opened automatically; pass `--port` to attach to anything CDP-speaking
@@ -97,6 +99,42 @@ Use `--worker-id <id>` to pin a stable live worker ID from `list-targets`,
97
99
  pin a raw inspector target, or `--main-only` to deliberately ignore workers.
98
100
  Explicit selectors preserve single-isolate behavior.
99
101
 
102
+ ### Automation readiness contract
103
+
104
+ Commands that arm debugger behavior before waiting (`snapshot`, `watch`,
105
+ `exception`, and `log`) accept `--ready-event`. The flag writes exactly one
106
+ compact JSON object to `stderr` after every session present during initial
107
+ arming has completed its CDP setup and immediately before the command waits or
108
+ streams:
109
+
110
+ ```json
111
+ {"event":"breakpoint-armed","schemaVersion":1,"command":"snapshot","sessions":3,"resolvedLocations":2,"timeoutMs":30000}
112
+ ```
113
+
114
+ This is the supported synchronization contract for callers that must start the
115
+ CLI before firing an HTTP request, queue job, or other external trigger. Parse
116
+ the JSON line and require `event === "breakpoint-armed"` plus
117
+ `schemaVersion === 1`, then confirm `command` matches the invocation; do not
118
+ poll prose such as `Waiting up to`.
119
+
120
+ | Field | Contract |
121
+ | --- | --- |
122
+ | `event` | Always `"breakpoint-armed"` |
123
+ | `schemaVersion` | Event schema version; currently `1` |
124
+ | `command` | `snapshot`, `watch`, `exception`, or `log` |
125
+ | `sessions` | Number of main/worker sessions whose initial arming completed before emission |
126
+ | `resolvedLocations` | Total locations V8 had already resolved, or `null` for `exception`; `0` can still represent an accepted URL breakpoint that may resolve when a script loads |
127
+ | `timeoutMs` | Snapshot/exception wait or watch per-hit timeout; `null` for log |
128
+
129
+ Workers attaching after this one-time event are still armed dynamically, but
130
+ are not included in its `sessions` or `resolvedLocations` totals. If initial
131
+ arming fails or the command is cancelled first, no readiness event is emitted.
132
+ With `log --ready-event`, matching events received while the remaining initial
133
+ sessions are still arming are neither emitted nor counted, so the log stream
134
+ cannot precede its readiness marker.
135
+ Without `--ready-event`, stderr/stdout behavior is unchanged. An explicitly
136
+ requested event is still emitted with `snapshot --quiet`.
137
+
100
138
  ---
101
139
 
102
140
  ## ๐Ÿงฐ CLI
@@ -143,6 +181,7 @@ cf-inspector snapshot --port 9229 \
143
181
  | `--include-scopes` | Include expanded paused-frame scopes under `topFrame.scopes`. Omitted by default to keep targeted captures concise |
144
182
  | `--no-json` | Print a human-readable summary instead of JSON |
145
183
  | `--quiet` | Suppress snapshot progress messages on stderr |
184
+ | `--ready-event` | Emit the versioned `breakpoint-armed` JSON event on stderr after every current isolate is armed |
146
185
  | `--keep-paused` | Skip `Debugger.resume` after capture |
147
186
  | `--fail-on-unmatched-pause` | Fail immediately if the target pauses somewhere else instead of waiting cooperatively |
148
187
 
@@ -269,6 +308,7 @@ When the user expression throws, the event is emitted with `error` instead of `v
269
308
  | `--max-value-length <chars>` | Maximum characters per log value (streaming default: `4096`). Truncated events include `truncated` and `originalLength` |
270
309
  | `--remote-root <value>` | Optional path-mapping anchor (same DSL as `snapshot`) |
271
310
  | `--no-json` | Print human-readable lines instead of JSON Lines |
311
+ | `--ready-event` | Emit the versioned `breakpoint-armed` JSON event on stderr after every current isolate is armed |
272
312
 
273
313
  Native logpoint expressions and conditions have no V8 side-effect gate. The
274
314
  CLI warns when its best-effort syntax scan recognizes assignments or common
@@ -319,6 +359,7 @@ Each event is a `WatchEvent`:
319
359
  | `--allow-mutation` | Disable the capture side-effect guard and explicitly allow mutation-shaped native conditions |
320
360
  | `--include-scopes` | Include expanded paused-frame scopes per hit |
321
361
  | `--no-json` | Print human-readable lines instead of JSON Lines |
362
+ | `--ready-event` | Emit the versioned `breakpoint-armed` JSON event on stderr after every current isolate is armed |
322
363
 
323
364
  ### ๐Ÿ’ฅ `cf-inspector exception`
324
365
 
@@ -365,6 +406,7 @@ Result is a `SnapshotResult` with an extra `exception` field:
365
406
  | `--max-value-length <chars>` | Maximum characters per captured value (one-shot default: `131072`) |
366
407
  | `--keep-paused` | Skip `Debugger.resume` after capture |
367
408
  | `--no-json` | Print a human-readable summary instead of JSON |
409
+ | `--ready-event` | Emit the versioned `breakpoint-armed` JSON event on stderr after pause-on-exception is active in every current isolate |
368
410
 
369
411
  ### ๐Ÿงฎ `cf-inspector eval`
370
412
 
@@ -382,12 +424,23 @@ cf-inspector eval --port 9229 --expr 'process.uptime()'
382
424
 
383
425
  ### ๐Ÿ“œ `cf-inspector list-scripts`
384
426
 
385
- 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.
427
+ Print every script known by the main isolate and each currently attached
428
+ NodeWorker (useful for debugging path-mapping issues). Each JSON entry carries
429
+ `isolate: {"kind":"main"}` or
430
+ `isolate: {"kind":"worker","workerId":"โ€ฆ"}`, so duplicate per-isolate
431
+ `scriptId` values remain unambiguous. Add `--filter <pattern>` to narrow noisy
432
+ script lists with a literal/wildcard pattern; `|` separates alternatives and
433
+ `.*` / `.+` match variable text.
386
434
 
387
435
  ```bash
388
436
  cf-inspector list-scripts --port 9229 --filter 'dist/.+\.js'
389
437
  ```
390
438
 
439
+ With `--no-json`, rows are
440
+ `scriptId<TAB>url<TAB>isolate`, where isolate is `main` or
441
+ `worker:<workerId>`. `--main-only`, `--worker-id`, `--worker`, and `--target`
442
+ retain their normal narrowing behavior.
443
+
391
444
  ### ๐ŸŽฏ `cf-inspector list-targets`
392
445
 
393
446
  Print raw `/json/list` inspector targets with stable `index` values, likely
package/dist/cli.js CHANGED
@@ -391,6 +391,15 @@ function writeProgress(message) {
391
391
  process2.stderr.write(`[cf-inspector] ${message}
392
392
  `);
393
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)}
401
+ `);
402
+ }
394
403
  function writeJson(value) {
395
404
  process2.stdout.write(`${JSON.stringify(value, null, 2)}
396
405
  `);
@@ -3103,6 +3112,8 @@ var BreakpointFanout = class {
3103
3112
  detachError;
3104
3113
  activeRace;
3105
3114
  pauseReasons = [];
3115
+ pendingSetupError;
3116
+ preserveReadinessErrors = false;
3106
3117
  constructor(group, setupSession, pauseReasons = []) {
3107
3118
  this.pauseReasons = pauseReasons;
3108
3119
  this.detach = group.onSession((session) => {
@@ -3128,13 +3139,51 @@ var BreakpointFanout = class {
3128
3139
  this.activeRace?.remove(session);
3129
3140
  });
3130
3141
  this.detachError = group.onError((error) => {
3142
+ if (this.setupErrors.length === 0 && this.pendingSetupError === void 0) {
3143
+ this.pendingSetupError = error;
3144
+ }
3131
3145
  for (const reject of this.setupErrors) {
3132
3146
  reject(error);
3133
3147
  }
3134
3148
  });
3135
3149
  }
3136
- async ready() {
3137
- 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
+ }
3138
3187
  }
3139
3188
  trackHandle(session, handle) {
3140
3189
  const record = this.records.get(session);
@@ -3143,11 +3192,19 @@ var BreakpointFanout = class {
3143
3192
  }
3144
3193
  }
3145
3194
  availableOutcomes() {
3146
- return [...this.records.values()].map((record) => ({
3195
+ return this.outcomesFor([...this.records.values()]);
3196
+ }
3197
+ outcomesFor(records) {
3198
+ return records.map((record) => ({
3147
3199
  session: record.session,
3148
3200
  setup: { handles: record.handles }
3149
3201
  }));
3150
3202
  }
3203
+ takePendingSetupError() {
3204
+ const error = this.pendingSetupError;
3205
+ this.pendingSetupError = void 0;
3206
+ return error;
3207
+ }
3151
3208
  async waitForFirst(timeoutMs, options = {}, signal) {
3152
3209
  if (this.activeRace !== void 0) {
3153
3210
  throw new CfInspectorError("INVALID_ARGUMENT", "A fan-out pause race is already active");
@@ -3156,6 +3213,12 @@ var BreakpointFanout = class {
3156
3213
  this.pauseReasons = options.pauseReasons ?? [];
3157
3214
  this.activeRace = race;
3158
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
+ }
3159
3222
  for (const record of this.records.values()) {
3160
3223
  race.add(record);
3161
3224
  }
@@ -4126,7 +4189,20 @@ async function runExceptionCommand(command, opts, signal) {
4126
4189
  let winner;
4127
4190
  let preserveWinner = false;
4128
4191
  try {
4129
- 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
+ } : {});
4130
4206
  const hit = await fanout.waitForFirst(command.timeoutMs, {
4131
4207
  pauseReasons: ["exception", "promiseRejection"],
4132
4208
  unmatchedPausePolicy: "wait-for-resume"
@@ -4179,16 +4255,44 @@ import process9 from "process";
4179
4255
  async function handleListScripts(opts) {
4180
4256
  const target = await resolveTargetWithCurrentCfTarget(opts);
4181
4257
  const filter = compileScriptUrlFilter(opts.filter);
4182
- 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));
4183
4267
  if (opts.json) {
4184
4268
  writeJson(scripts);
4185
4269
  return;
4186
4270
  }
4187
4271
  for (const script of scripts) {
4188
- process9.stdout.write(`${script.scriptId} ${script.url}
4189
- `);
4272
+ process9.stdout.write(
4273
+ `${script.scriptId} ${script.url} ${formatIsolate2(script.isolate)}
4274
+ `
4275
+ );
4190
4276
  }
4191
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
+ }
4192
4296
  async function handleListTargets(opts) {
4193
4297
  const target = await resolveTargetWithCurrentCfTarget(opts);
4194
4298
  const tunnel = await openTarget(target);
@@ -4521,6 +4625,9 @@ async function streamLogpoint(session, options) {
4521
4625
  if (event === void 0) {
4522
4626
  return;
4523
4627
  }
4628
+ if (options.eventGate?.() === false) {
4629
+ return;
4630
+ }
4524
4631
  emitted += 1;
4525
4632
  try {
4526
4633
  options.onEvent(event);
@@ -4639,6 +4746,7 @@ async function handleLog(opts) {
4639
4746
  ...condition === void 0 ? {} : { condition },
4640
4747
  maxValueLength,
4641
4748
  json: opts.json,
4749
+ emitReadyEvent: opts.readyEvent === true,
4642
4750
  signal
4643
4751
  });
4644
4752
  writeLogSummary(result.stoppedReason, result.emitted, opts.json);
@@ -4650,9 +4758,13 @@ async function runLogGroup(group, options) {
4650
4758
  const tasks = /* @__PURE__ */ new Set();
4651
4759
  const removedSessions = /* @__PURE__ */ new Set();
4652
4760
  const results = [];
4761
+ const pendingArming = /* @__PURE__ */ new Set();
4762
+ const resolvedLocations = /* @__PURE__ */ new Map();
4653
4763
  let fatalError;
4654
4764
  let emitted = 0;
4655
4765
  let reason = "signal";
4766
+ let sessionRegistrationComplete = false;
4767
+ let readyEventEmitted = !options.emitReadyEvent;
4656
4768
  let resolveStop;
4657
4769
  const stopped = new Promise((resolve) => {
4658
4770
  resolveStop = resolve;
@@ -4675,10 +4787,35 @@ async function runLogGroup(group, options) {
4675
4787
  const timer = options.durationMs === void 0 ? void 0 : setTimeout(() => {
4676
4788
  finish("duration");
4677
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
+ };
4678
4812
  const startSession = (session) => {
4679
4813
  if (controller.signal.aborted) {
4680
4814
  return;
4681
4815
  }
4816
+ if (!readyEventEmitted) {
4817
+ pendingArming.add(session);
4818
+ }
4682
4819
  const task = (async () => {
4683
4820
  await validateExpression(session, options.expression);
4684
4821
  if (options.condition !== void 0) {
@@ -4692,6 +4829,7 @@ async function runLogGroup(group, options) {
4692
4829
  ...options.condition === void 0 ? {} : { condition: options.condition },
4693
4830
  maxValueLength: options.maxValueLength,
4694
4831
  signal: controller.signal,
4832
+ ...options.emitReadyEvent ? { eventGate: () => readyEventEmitted } : {},
4695
4833
  onEvent: (event) => {
4696
4834
  if (controller.signal.aborted) {
4697
4835
  return;
@@ -4704,6 +4842,11 @@ async function runLogGroup(group, options) {
4704
4842
  },
4705
4843
  onBreakpointSet: (handle) => {
4706
4844
  warnOnUnboundBreakpoints([handle]);
4845
+ if (!readyEventEmitted) {
4846
+ resolvedLocations.set(session, handle.resolvedLocations.length);
4847
+ pendingArming.delete(session);
4848
+ emitReadyEventIfArmed();
4849
+ }
4707
4850
  }
4708
4851
  });
4709
4852
  })();
@@ -4716,6 +4859,9 @@ async function runLogGroup(group, options) {
4716
4859
  }
4717
4860
  },
4718
4861
  (error) => {
4862
+ if (options.emitReadyEvent && (removedSessions.has(session) || fatalError !== void 0)) {
4863
+ return;
4864
+ }
4719
4865
  fatalError = error;
4720
4866
  finish("transport-closed");
4721
4867
  }
@@ -4724,8 +4870,27 @@ async function runLogGroup(group, options) {
4724
4870
  });
4725
4871
  };
4726
4872
  const detach = group.onSession(startSession);
4873
+ sessionRegistrationComplete = true;
4874
+ emitReadyEventIfArmed();
4727
4875
  const detachRemoved = group.onSessionRemoved((session) => {
4728
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
+ }
4729
4894
  });
4730
4895
  try {
4731
4896
  await stopped;
@@ -4733,6 +4898,7 @@ async function runLogGroup(group, options) {
4733
4898
  } finally {
4734
4899
  detach();
4735
4900
  detachRemoved();
4901
+ detachError();
4736
4902
  if (timer !== void 0) {
4737
4903
  clearTimeout(timer);
4738
4904
  }
@@ -4839,18 +5005,38 @@ async function runSnapshotCommand(command, opts, reportProgress, signal) {
4839
5005
  let winner;
4840
5006
  let preserveWinner = false;
4841
5007
  try {
4842
- await fanout.ready();
4843
- if (command.setupEvals.length > 0) {
4844
- reportProgress?.("Setup evaluation complete.");
4845
- }
4846
- if (command.condition !== void 0) {
4847
- 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());
4848
5039
  }
4849
- const outcomes = fanout.availableOutcomes();
4850
- reportBreakpointOutcomes(outcomes, reportProgress);
4851
- reportProgress?.(
4852
- `Waiting up to ${(command.timeoutMs / 1e3).toString()}s for a breakpoint hit...`
4853
- );
4854
5040
  const hit = await fanout.waitForFirst(command.timeoutMs, {
4855
5041
  unmatchedPausePolicy: opts.failOnUnmatchedPause === true ? "fail" : "wait-for-resume",
4856
5042
  ...opts.failOnUnmatchedPause === true ? {} : { onUnmatchedPause: warnOnUnmatchedPause }
@@ -4898,11 +5084,12 @@ function reportBreakpointOutcomes(outcomes, reportProgress) {
4898
5084
  reportProgress?.(
4899
5085
  `Breakpoint setup complete: ${locations.toString()} resolved ${locations === 1 ? "location" : "locations"}.`
4900
5086
  );
4901
- return;
5087
+ return { sessions: outcomes.length, resolvedLocations: locations };
4902
5088
  }
4903
5089
  reportProgress?.(
4904
5090
  `Breakpoint setup complete: sessions=${outcomes.length.toString()} boundSessions=${boundSessions.toString()} resolvedLocations=${locations.toString()}.`
4905
5091
  );
5092
+ return { sessions: outcomes.length, resolvedLocations: locations };
4906
5093
  }
4907
5094
  async function captureSnapshotResult(session, pause, command, opts, reportProgress) {
4908
5095
  const pausedStartedAt = pause.receivedAtMs ?? performance6.now();
@@ -5023,7 +5210,26 @@ async function runWatchGroup(group, command, opts, signal) {
5023
5210
  let stoppedReason = "signal";
5024
5211
  const deadline = computeDeadline(command.durationMs);
5025
5212
  try {
5026
- 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
+ } : {});
5027
5233
  while (!signal.aborted) {
5028
5234
  const remainingMs = remainingForLoop(deadline, command.perHitTimeoutMs);
5029
5235
  if (remainingMs <= 0) {
@@ -5197,6 +5403,7 @@ var collectStrings = (value, prev = []) => [
5197
5403
  ...prev,
5198
5404
  value
5199
5405
  ];
5406
+ var READY_EVENT_DESCRIPTION = "Emit a versioned breakpoint-armed JSON event on stderr after every current isolate is armed";
5200
5407
  function readPackageVersion() {
5201
5408
  let current = dirname(fileURLToPath(import.meta.url));
5202
5409
  for (let depth = 0; depth < 4; depth += 1) {
@@ -5242,14 +5449,14 @@ function registerSnapshot(program) {
5242
5449
  applyTargetOptions(
5243
5450
  program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture expressions, and resume"),
5244
5451
  { includeTimeout: false }
5245
- ).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) => {
5246
5453
  await handleSnapshot(opts);
5247
5454
  });
5248
5455
  }
5249
5456
  function registerLog(program) {
5250
5457
  applyTargetOptions(
5251
5458
  program.command("log").description("Stream a non-pausing logpoint: log an expression each time a line executes")
5252
- ).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) => {
5253
5460
  await handleLog(opts);
5254
5461
  });
5255
5462
  }
@@ -5257,7 +5464,7 @@ function registerWatch(program) {
5257
5464
  applyTargetOptions(
5258
5465
  program.command("watch").description("Stream a snapshot per breakpoint hit (multi-shot watch); resume between hits"),
5259
5466
  { includeTimeout: false }
5260
- ).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) => {
5261
5468
  await handleWatch(opts);
5262
5469
  });
5263
5470
  }
@@ -5265,7 +5472,7 @@ function registerException(program) {
5265
5472
  applyTargetOptions(
5266
5473
  program.command("exception").description("Pause on a thrown exception, capture the value and frame, then resume"),
5267
5474
  { includeTimeout: false }
5268
- ).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) => {
5269
5476
  await handleException(opts);
5270
5477
  });
5271
5478
  }
@@ -5278,8 +5485,10 @@ function registerEval(program) {
5278
5485
  }
5279
5486
  function registerListScripts(program) {
5280
5487
  applyTargetOptions(
5281
- program.command("list-scripts").description("Print the scripts the V8 instance currently knows about")
5282
- ).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) => {
5283
5492
  await handleListScripts(opts);
5284
5493
  });
5285
5494
  }