@saptools/cf-inspector 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1496 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/types.ts
13
+ var CfInspectorError;
14
+ var init_types = __esm({
15
+ "src/types.ts"() {
16
+ "use strict";
17
+ CfInspectorError = class extends Error {
18
+ code;
19
+ detail;
20
+ constructor(code, message, detail) {
21
+ super(message);
22
+ this.name = "CfInspectorError";
23
+ this.code = code;
24
+ if (detail !== void 0) {
25
+ this.detail = detail;
26
+ }
27
+ }
28
+ };
29
+ }
30
+ });
31
+
32
+ // src/wsTransport.ts
33
+ var wsTransport_exports = {};
34
+ __export(wsTransport_exports, {
35
+ wsTransportFactory: () => wsTransportFactory
36
+ });
37
+ import { WebSocket } from "ws";
38
+ async function wsTransportFactory(url) {
39
+ const socket = new WebSocket(url, { perMessageDeflate: false });
40
+ await new Promise((resolve, reject) => {
41
+ const onOpen = () => {
42
+ socket.off("error", onError);
43
+ resolve();
44
+ };
45
+ const onError = (err) => {
46
+ socket.off("open", onOpen);
47
+ reject(
48
+ new CfInspectorError(
49
+ "INSPECTOR_CONNECTION_FAILED",
50
+ `Failed to connect to inspector at ${url}: ${err.message}`
51
+ )
52
+ );
53
+ };
54
+ socket.once("open", onOpen);
55
+ socket.once("error", onError);
56
+ });
57
+ const wrappers = /* @__PURE__ */ new WeakMap();
58
+ const wrapMessage = (listener) => {
59
+ const wrapped = (data) => {
60
+ listener(data.toString("utf8"));
61
+ };
62
+ wrappers.set(listener, wrapped);
63
+ return wrapped;
64
+ };
65
+ const wrapClose = (listener) => {
66
+ const wrapped = () => {
67
+ listener();
68
+ };
69
+ wrappers.set(listener, wrapped);
70
+ return wrapped;
71
+ };
72
+ const wrapError = (listener) => {
73
+ const wrapped = (err) => {
74
+ listener(err);
75
+ };
76
+ wrappers.set(listener, wrapped);
77
+ return wrapped;
78
+ };
79
+ return {
80
+ send(payload) {
81
+ socket.send(payload);
82
+ },
83
+ close() {
84
+ socket.close();
85
+ },
86
+ get readyState() {
87
+ return socket.readyState;
88
+ },
89
+ on(event, listener) {
90
+ if (event === "message") {
91
+ socket.on("message", wrapMessage(listener));
92
+ } else if (event === "close") {
93
+ socket.on("close", wrapClose(listener));
94
+ } else {
95
+ socket.on("error", wrapError(listener));
96
+ }
97
+ },
98
+ off(event, listener) {
99
+ const wrapped = wrappers.get(listener);
100
+ if (!wrapped) {
101
+ return;
102
+ }
103
+ if (event === "message") {
104
+ socket.off("message", wrapped);
105
+ } else if (event === "close") {
106
+ socket.off("close", wrapped);
107
+ } else {
108
+ socket.off("error", wrapped);
109
+ }
110
+ }
111
+ };
112
+ }
113
+ var init_wsTransport = __esm({
114
+ "src/wsTransport.ts"() {
115
+ "use strict";
116
+ init_types();
117
+ }
118
+ });
119
+
120
+ // src/cli.ts
121
+ import process from "process";
122
+ import { Command } from "commander";
123
+
124
+ // src/inspector.ts
125
+ import { request } from "http";
126
+
127
+ // src/cdp.ts
128
+ init_types();
129
+ import { EventEmitter } from "events";
130
+ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
131
+ function parseMessage(raw) {
132
+ try {
133
+ const value = JSON.parse(raw);
134
+ if (typeof value !== "object" || value === null) {
135
+ return void 0;
136
+ }
137
+ return value;
138
+ } catch {
139
+ return void 0;
140
+ }
141
+ }
142
+ async function loadDefaultFactory() {
143
+ const mod = await Promise.resolve().then(() => (init_wsTransport(), wsTransport_exports));
144
+ return mod.wsTransportFactory;
145
+ }
146
+ var CdpClient = class _CdpClient {
147
+ transport;
148
+ requestTimeoutMs;
149
+ pending = /* @__PURE__ */ new Map();
150
+ emitter = new EventEmitter();
151
+ nextId = 1;
152
+ closed = false;
153
+ closeReason;
154
+ handleMessage = (raw) => {
155
+ const parsed = parseMessage(raw);
156
+ if (!parsed) {
157
+ return;
158
+ }
159
+ if (typeof parsed.id === "number") {
160
+ const pending = this.pending.get(parsed.id);
161
+ if (!pending) {
162
+ return;
163
+ }
164
+ this.pending.delete(parsed.id);
165
+ clearTimeout(pending.timer);
166
+ if (parsed.error) {
167
+ pending.reject(
168
+ new CfInspectorError(
169
+ "CDP_REQUEST_FAILED",
170
+ `CDP request ${parsed.id.toString()} failed: ${parsed.error.message}`,
171
+ JSON.stringify(parsed.error)
172
+ )
173
+ );
174
+ return;
175
+ }
176
+ pending.resolve(parsed.result);
177
+ return;
178
+ }
179
+ if (typeof parsed.method === "string") {
180
+ this.emitter.emit(parsed.method, parsed.params);
181
+ this.emitter.emit("event", { method: parsed.method, params: parsed.params });
182
+ }
183
+ };
184
+ handleClose = () => {
185
+ this.markClosed(new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Inspector connection closed"));
186
+ };
187
+ handleError = (err) => {
188
+ this.markClosed(
189
+ err instanceof CfInspectorError ? err : new CfInspectorError("INSPECTOR_CONNECTION_FAILED", err.message)
190
+ );
191
+ };
192
+ constructor(transport, requestTimeoutMs) {
193
+ this.transport = transport;
194
+ this.requestTimeoutMs = requestTimeoutMs;
195
+ transport.on("message", this.handleMessage);
196
+ transport.on("close", this.handleClose);
197
+ transport.on("error", this.handleError);
198
+ }
199
+ static async connect(options) {
200
+ const factory = options.transportFactory ?? await loadDefaultFactory();
201
+ const transport = await factory(options.url);
202
+ return new _CdpClient(transport, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
203
+ }
204
+ async send(method, params = {}) {
205
+ if (this.closed) {
206
+ throw this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
207
+ }
208
+ const id = this.nextId++;
209
+ const payload = JSON.stringify({ id, method, params });
210
+ return await new Promise((resolve, reject) => {
211
+ const timer = setTimeout(() => {
212
+ this.pending.delete(id);
213
+ reject(
214
+ new CfInspectorError(
215
+ "CDP_REQUEST_FAILED",
216
+ `CDP method ${method} timed out after ${this.requestTimeoutMs.toString()}ms`
217
+ )
218
+ );
219
+ }, this.requestTimeoutMs);
220
+ this.pending.set(id, {
221
+ resolve: (value) => {
222
+ resolve(value);
223
+ },
224
+ reject,
225
+ timer
226
+ });
227
+ try {
228
+ this.transport.send(payload);
229
+ } catch (err) {
230
+ clearTimeout(timer);
231
+ this.pending.delete(id);
232
+ const message = err instanceof Error ? err.message : String(err);
233
+ reject(new CfInspectorError("CDP_REQUEST_FAILED", `Failed to send ${method}: ${message}`));
234
+ }
235
+ });
236
+ }
237
+ on(method, listener) {
238
+ this.emitter.on(method, listener);
239
+ return () => {
240
+ this.emitter.off(method, listener);
241
+ };
242
+ }
243
+ async waitFor(method, options = {
244
+ timeoutMs: this.requestTimeoutMs
245
+ }) {
246
+ if (this.closed) {
247
+ throw this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
248
+ }
249
+ return await new Promise((resolve, reject) => {
250
+ let settled = false;
251
+ const cleanup = () => {
252
+ clearTimeout(timer);
253
+ offEvent();
254
+ offClose();
255
+ };
256
+ const offEvent = this.on(method, (raw) => {
257
+ if (settled) {
258
+ return;
259
+ }
260
+ const params = raw;
261
+ if (options.predicate && !options.predicate(params)) {
262
+ return;
263
+ }
264
+ settled = true;
265
+ cleanup();
266
+ resolve(params);
267
+ });
268
+ const offClose = this.onClose((err) => {
269
+ if (settled) {
270
+ return;
271
+ }
272
+ settled = true;
273
+ cleanup();
274
+ reject(err);
275
+ });
276
+ const timer = setTimeout(() => {
277
+ if (settled) {
278
+ return;
279
+ }
280
+ settled = true;
281
+ cleanup();
282
+ reject(
283
+ new CfInspectorError(
284
+ "BREAKPOINT_NOT_HIT",
285
+ `Timed out waiting for ${method} after ${options.timeoutMs.toString()}ms`
286
+ )
287
+ );
288
+ }, options.timeoutMs);
289
+ });
290
+ }
291
+ onClose(listener) {
292
+ if (this.closed) {
293
+ const reason = this.closeReason ?? new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection closed");
294
+ queueMicrotask(() => {
295
+ listener(reason);
296
+ });
297
+ return () => {
298
+ };
299
+ }
300
+ this.emitter.on("__close__", listener);
301
+ return () => {
302
+ this.emitter.off("__close__", listener);
303
+ };
304
+ }
305
+ dispose() {
306
+ if (this.closed) {
307
+ return;
308
+ }
309
+ this.transport.off("message", this.handleMessage);
310
+ this.transport.off("close", this.handleClose);
311
+ this.transport.off("error", this.handleError);
312
+ try {
313
+ this.transport.close();
314
+ } catch {
315
+ }
316
+ this.markClosed(new CfInspectorError("INSPECTOR_CONNECTION_FAILED", "Connection disposed"));
317
+ }
318
+ get isClosed() {
319
+ return this.closed;
320
+ }
321
+ markClosed(reason) {
322
+ if (this.closed) {
323
+ return;
324
+ }
325
+ this.closed = true;
326
+ this.closeReason = reason;
327
+ for (const [, pending] of this.pending) {
328
+ clearTimeout(pending.timer);
329
+ pending.reject(reason);
330
+ }
331
+ this.pending.clear();
332
+ this.emitter.emit("__close__", reason);
333
+ this.emitter.removeAllListeners();
334
+ }
335
+ };
336
+
337
+ // src/pathMapper.ts
338
+ init_types();
339
+ var REGEX_PREFIX = "regex:";
340
+ var REGEX_FLAGS_PATTERN = /^[dgimsuvy]*$/;
341
+ var TS_JS_EXT_PATTERN = /\.(?:ts|js|mts|mjs|cts|cjs)$/i;
342
+ function parseBreakpointSpec(input) {
343
+ const idx = input.lastIndexOf(":");
344
+ if (idx <= 0 || idx === input.length - 1) {
345
+ throw new CfInspectorError(
346
+ "INVALID_BREAKPOINT",
347
+ `Breakpoint must be in 'file:line' form, received: "${input}"`
348
+ );
349
+ }
350
+ const file = input.slice(0, idx).trim();
351
+ const lineRaw = input.slice(idx + 1).trim();
352
+ const line = Number.parseInt(lineRaw, 10);
353
+ if (!Number.isInteger(line) || line <= 0 || line.toString() !== lineRaw) {
354
+ throw new CfInspectorError(
355
+ "INVALID_BREAKPOINT",
356
+ `Breakpoint line must be a positive integer, received: "${lineRaw}"`
357
+ );
358
+ }
359
+ if (file.length === 0) {
360
+ throw new CfInspectorError(
361
+ "INVALID_BREAKPOINT",
362
+ `Breakpoint file path is empty in "${input}"`
363
+ );
364
+ }
365
+ return { file, line };
366
+ }
367
+ function parseRemoteRoot(value) {
368
+ const trimmed = value?.trim();
369
+ if (trimmed === void 0 || trimmed.length === 0) {
370
+ return { kind: "none" };
371
+ }
372
+ if (trimmed.startsWith(REGEX_PREFIX)) {
373
+ return toRegex(trimmed.slice(REGEX_PREFIX.length), "");
374
+ }
375
+ const slashRegex = parseSlashDelimited(trimmed);
376
+ if (slashRegex !== void 0) {
377
+ return toRegex(slashRegex.pattern, slashRegex.flags);
378
+ }
379
+ return { kind: "literal", value: stripTrailingSlash(trimmed) };
380
+ }
381
+ function toRegex(pattern, flags) {
382
+ try {
383
+ const regex = new RegExp(pattern, flags);
384
+ return { kind: "regex", pattern, flags, regex };
385
+ } catch (err) {
386
+ const message = err instanceof Error ? err.message : String(err);
387
+ throw new CfInspectorError(
388
+ "INVALID_REMOTE_ROOT",
389
+ `Failed to compile remote-root regex "${pattern}" with flags "${flags}": ${message}`
390
+ );
391
+ }
392
+ }
393
+ function parseSlashDelimited(value) {
394
+ if (!value.startsWith("/")) {
395
+ return void 0;
396
+ }
397
+ const closing = findLastUnescapedSlash(value);
398
+ if (closing <= 0) {
399
+ return void 0;
400
+ }
401
+ const flags = value.slice(closing + 1);
402
+ if (flags.length === 0 || !REGEX_FLAGS_PATTERN.test(flags)) {
403
+ return void 0;
404
+ }
405
+ return { pattern: value.slice(1, closing), flags };
406
+ }
407
+ function findLastUnescapedSlash(value) {
408
+ for (let i = value.length - 1; i > 0; i--) {
409
+ if (value[i] === "/" && !isEscaped(value, i)) {
410
+ return i;
411
+ }
412
+ }
413
+ return -1;
414
+ }
415
+ function isEscaped(value, idx) {
416
+ let backslashes = 0;
417
+ for (let i = idx - 1; i >= 0; i--) {
418
+ if (value[i] === "\\") {
419
+ backslashes++;
420
+ } else {
421
+ break;
422
+ }
423
+ }
424
+ return backslashes % 2 === 1;
425
+ }
426
+ function stripTrailingSlash(value) {
427
+ if (value.length > 1 && value.endsWith("/")) {
428
+ return value.slice(0, -1);
429
+ }
430
+ return value;
431
+ }
432
+ function escapeRegExp(value) {
433
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
434
+ }
435
+ function normalizeRelative(file) {
436
+ return file.replaceAll(/^[./\\]+/g, "").replaceAll("\\", "/");
437
+ }
438
+ function dropExtension(file) {
439
+ const match = TS_JS_EXT_PATTERN.exec(file);
440
+ if (!match) {
441
+ return { stem: file, matchedExt: false };
442
+ }
443
+ return { stem: file.slice(0, match.index), matchedExt: true };
444
+ }
445
+ var EXT_GROUP = String.raw`\.(?:ts|js|mts|mjs|cts|cjs)`;
446
+ var OPTIONAL_EXT_GROUP = String.raw`(?:\.(?:ts|js|mts|mjs|cts|cjs))?`;
447
+ function buildBreakpointUrlRegex(input) {
448
+ const normalized = normalizeRelative(input.file);
449
+ const { stem, matchedExt } = dropExtension(normalized);
450
+ const escapedStem = escapeRegExp(stem);
451
+ const tail = matchedExt ? `${escapedStem}${EXT_GROUP}` : `${escapedStem}${OPTIONAL_EXT_GROUP}`;
452
+ switch (input.remoteRoot.kind) {
453
+ case "none": {
454
+ return `(?:^|/)${tail}$`;
455
+ }
456
+ case "literal": {
457
+ const escapedRoot = escapeRegExp(input.remoteRoot.value);
458
+ return `^file://${escapedRoot}/${tail}$`;
459
+ }
460
+ case "regex": {
461
+ return `^file://${input.remoteRoot.pattern}/${tail}$`;
462
+ }
463
+ }
464
+ }
465
+
466
+ // src/inspector.ts
467
+ init_types();
468
+ var DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
469
+ var DEFAULT_HOST = "127.0.0.1";
470
+ async function fetchJson(url, timeoutMs) {
471
+ return await new Promise((resolve, reject) => {
472
+ const req = request(url, { method: "GET" }, (res) => {
473
+ const chunks = [];
474
+ res.on("data", (chunk) => {
475
+ chunks.push(chunk);
476
+ });
477
+ res.on("end", () => {
478
+ try {
479
+ const text = Buffer.concat(chunks).toString("utf8");
480
+ resolve(JSON.parse(text));
481
+ } catch (err) {
482
+ const message = err instanceof Error ? err.message : String(err);
483
+ reject(
484
+ new CfInspectorError(
485
+ "INSPECTOR_DISCOVERY_FAILED",
486
+ `Failed to parse inspector discovery response from ${url}: ${message}`
487
+ )
488
+ );
489
+ }
490
+ });
491
+ res.on("error", (err) => {
492
+ reject(
493
+ new CfInspectorError(
494
+ "INSPECTOR_DISCOVERY_FAILED",
495
+ `Inspector discovery response error: ${err.message}`
496
+ )
497
+ );
498
+ });
499
+ });
500
+ req.setTimeout(timeoutMs, () => {
501
+ req.destroy(
502
+ new CfInspectorError(
503
+ "INSPECTOR_DISCOVERY_FAILED",
504
+ `Inspector discovery at ${url} timed out after ${timeoutMs.toString()}ms`
505
+ )
506
+ );
507
+ });
508
+ req.on("error", (err) => {
509
+ reject(
510
+ err instanceof CfInspectorError ? err : new CfInspectorError(
511
+ "INSPECTOR_DISCOVERY_FAILED",
512
+ `Inspector discovery at ${url} failed: ${err.message}`
513
+ )
514
+ );
515
+ });
516
+ req.end();
517
+ });
518
+ }
519
+ function toInspectorTarget(value, source) {
520
+ if (typeof value !== "object" || value === null) {
521
+ throw new CfInspectorError(
522
+ "INSPECTOR_DISCOVERY_FAILED",
523
+ `Inspector target is not an object in ${source}`
524
+ );
525
+ }
526
+ const candidate = value;
527
+ const webSocketDebuggerUrl = candidate["webSocketDebuggerUrl"];
528
+ if (typeof webSocketDebuggerUrl !== "string" || webSocketDebuggerUrl.length === 0) {
529
+ throw new CfInspectorError(
530
+ "INSPECTOR_DISCOVERY_FAILED",
531
+ `Inspector target is missing webSocketDebuggerUrl in ${source}`
532
+ );
533
+ }
534
+ return {
535
+ description: typeof candidate["description"] === "string" ? candidate["description"] : "",
536
+ id: typeof candidate["id"] === "string" ? candidate["id"] : "",
537
+ title: typeof candidate["title"] === "string" ? candidate["title"] : "",
538
+ type: typeof candidate["type"] === "string" ? candidate["type"] : "",
539
+ url: typeof candidate["url"] === "string" ? candidate["url"] : "",
540
+ webSocketDebuggerUrl,
541
+ ...typeof candidate["devtoolsFrontendUrl"] === "string" ? { devtoolsFrontendUrl: candidate["devtoolsFrontendUrl"] } : {},
542
+ ...typeof candidate["faviconUrl"] === "string" ? { faviconUrl: candidate["faviconUrl"] } : {}
543
+ };
544
+ }
545
+ async function discoverInspectorTargets(host, port, timeoutMs) {
546
+ const url = `http://${host}:${port.toString()}/json/list`;
547
+ const raw = await fetchJson(url, timeoutMs);
548
+ if (!Array.isArray(raw) || raw.length === 0) {
549
+ throw new CfInspectorError(
550
+ "INSPECTOR_DISCOVERY_FAILED",
551
+ `No inspector targets returned from ${url}`
552
+ );
553
+ }
554
+ return raw.map((entry, idx) => toInspectorTarget(entry, `${url}[${idx.toString()}]`));
555
+ }
556
+ function readVersionField(value, ...keys) {
557
+ for (const key of keys) {
558
+ const entry = value[key];
559
+ if (typeof entry === "string" && entry.length > 0) {
560
+ return entry;
561
+ }
562
+ }
563
+ return void 0;
564
+ }
565
+ async function fetchInspectorVersion(host, port, timeoutMs) {
566
+ const url = `http://${host}:${port.toString()}/json/version`;
567
+ const raw = await fetchJson(url, timeoutMs);
568
+ if (typeof raw !== "object" || raw === null) {
569
+ throw new CfInspectorError(
570
+ "INSPECTOR_DISCOVERY_FAILED",
571
+ `Unexpected /json/version response from ${url}`
572
+ );
573
+ }
574
+ const value = raw;
575
+ const browser = readVersionField(value, "Browser", "browser");
576
+ const protocolVersion = readVersionField(value, "Protocol-Version", "protocolVersion");
577
+ if (browser === void 0 || protocolVersion === void 0) {
578
+ throw new CfInspectorError(
579
+ "INSPECTOR_DISCOVERY_FAILED",
580
+ `Unexpected /json/version response from ${url}`
581
+ );
582
+ }
583
+ return { browser, protocolVersion };
584
+ }
585
+ async function connectInspector(options) {
586
+ const host = options.host ?? DEFAULT_HOST;
587
+ const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
588
+ const targets = await discoverInspectorTargets(host, options.port, connectTimeoutMs);
589
+ const target = targets[0];
590
+ if (!target) {
591
+ throw new CfInspectorError(
592
+ "INSPECTOR_DISCOVERY_FAILED",
593
+ `No inspector targets available on ${host}:${options.port.toString()}`
594
+ );
595
+ }
596
+ const client = await CdpClient.connect({ url: target.webSocketDebuggerUrl });
597
+ const scripts = /* @__PURE__ */ new Map();
598
+ client.on("Debugger.scriptParsed", (raw) => {
599
+ const params = raw;
600
+ const scriptId = asString(params.scriptId);
601
+ const url = asString(params.url);
602
+ if (scriptId.length === 0) {
603
+ return;
604
+ }
605
+ scripts.set(scriptId, { scriptId, url });
606
+ });
607
+ const PAUSE_BUFFER_LIMIT = 32;
608
+ const pauseBuffer = [];
609
+ client.on("Debugger.paused", (raw) => {
610
+ const params = raw;
611
+ const event = {
612
+ reason: asString(params.reason),
613
+ hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
614
+ callFrames: toCallFrames(params.callFrames)
615
+ };
616
+ if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
617
+ pauseBuffer.shift();
618
+ }
619
+ pauseBuffer.push(event);
620
+ });
621
+ await client.send("Runtime.enable");
622
+ await client.send("Debugger.enable");
623
+ return {
624
+ client,
625
+ target,
626
+ scripts,
627
+ pauseBuffer,
628
+ dispose: async () => {
629
+ try {
630
+ await client.send("Debugger.disable");
631
+ } catch {
632
+ }
633
+ client.dispose();
634
+ }
635
+ };
636
+ }
637
+ function asString(value, fallback = "") {
638
+ return typeof value === "string" ? value : fallback;
639
+ }
640
+ function asNumber(value, fallback = 0) {
641
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
642
+ }
643
+ function toResolvedLocations(value) {
644
+ if (!Array.isArray(value)) {
645
+ return [];
646
+ }
647
+ return value.flatMap((entry) => {
648
+ if (typeof entry !== "object" || entry === null) {
649
+ return [];
650
+ }
651
+ const candidate = entry;
652
+ const scriptId = asString(candidate.scriptId);
653
+ if (scriptId.length === 0) {
654
+ return [];
655
+ }
656
+ const url = typeof candidate.url === "string" ? candidate.url : void 0;
657
+ const lineNumber = asNumber(candidate.lineNumber);
658
+ const result = url === void 0 ? { scriptId, lineNumber, columnNumber: asNumber(candidate.columnNumber) } : { scriptId, url, lineNumber, columnNumber: asNumber(candidate.columnNumber) };
659
+ return [result];
660
+ });
661
+ }
662
+ async function setBreakpoint(session, input) {
663
+ const remoteRoot = input.remoteRoot ?? { kind: "none" };
664
+ const urlRegex = buildBreakpointUrlRegex({ file: input.file, remoteRoot });
665
+ const params = {
666
+ lineNumber: input.line - 1,
667
+ urlRegex
668
+ };
669
+ if (input.condition !== void 0 && input.condition.length > 0) {
670
+ params["condition"] = input.condition;
671
+ }
672
+ const result = await session.client.send(
673
+ "Debugger.setBreakpointByUrl",
674
+ params
675
+ );
676
+ const breakpointId = asString(result.breakpointId);
677
+ if (breakpointId.length === 0) {
678
+ throw new CfInspectorError(
679
+ "CDP_REQUEST_FAILED",
680
+ `setBreakpointByUrl did not return a breakpointId for ${input.file}:${input.line.toString()}`
681
+ );
682
+ }
683
+ return {
684
+ breakpointId,
685
+ file: input.file,
686
+ line: input.line,
687
+ urlRegex,
688
+ resolvedLocations: toResolvedLocations(result.locations)
689
+ };
690
+ }
691
+ async function removeBreakpoint(session, breakpointId) {
692
+ await session.client.send("Debugger.removeBreakpoint", { breakpointId });
693
+ }
694
+ function toScopeChain(value) {
695
+ if (!Array.isArray(value)) {
696
+ return [];
697
+ }
698
+ return value.flatMap((entry) => {
699
+ if (typeof entry !== "object" || entry === null) {
700
+ return [];
701
+ }
702
+ const candidate = entry;
703
+ const type = asString(candidate.type);
704
+ if (type.length === 0) {
705
+ return [];
706
+ }
707
+ const objectId = typeof candidate.object?.objectId === "string" ? candidate.object.objectId : void 0;
708
+ const name = typeof candidate.name === "string" ? candidate.name : void 0;
709
+ const base = name === void 0 ? { type } : { type, name };
710
+ return [objectId === void 0 ? base : { ...base, objectId }];
711
+ });
712
+ }
713
+ function toCallFrames(value) {
714
+ if (!Array.isArray(value)) {
715
+ return [];
716
+ }
717
+ return value.flatMap((entry) => {
718
+ if (typeof entry !== "object" || entry === null) {
719
+ return [];
720
+ }
721
+ const candidate = entry;
722
+ const callFrameId = asString(candidate.callFrameId);
723
+ if (callFrameId.length === 0) {
724
+ return [];
725
+ }
726
+ const url = typeof candidate.url === "string" ? candidate.url : void 0;
727
+ const lineNumber = asNumber(candidate.location?.lineNumber);
728
+ const columnNumber = asNumber(candidate.location?.columnNumber);
729
+ const base = {
730
+ callFrameId,
731
+ functionName: asString(candidate.functionName),
732
+ lineNumber,
733
+ columnNumber,
734
+ scopeChain: toScopeChain(candidate.scopeChain)
735
+ };
736
+ return [url === void 0 ? base : { ...base, url }];
737
+ });
738
+ }
739
+ function pauseMatches(pause, breakpointIds) {
740
+ if (breakpointIds === void 0 || breakpointIds.length === 0) {
741
+ return true;
742
+ }
743
+ return pause.hitBreakpoints.some((id) => breakpointIds.includes(id));
744
+ }
745
+ async function waitForPause(session, options) {
746
+ const buffer = session.pauseBuffer;
747
+ while (buffer.length > 0) {
748
+ const head = buffer.shift();
749
+ if (head !== void 0 && pauseMatches(head, options.breakpointIds)) {
750
+ return head;
751
+ }
752
+ }
753
+ const params = await session.client.waitFor("Debugger.paused", {
754
+ timeoutMs: options.timeoutMs,
755
+ predicate: (raw) => {
756
+ const event = {
757
+ reason: asString(raw.reason),
758
+ hitBreakpoints: Array.isArray(raw.hitBreakpoints) ? raw.hitBreakpoints.filter((id) => typeof id === "string") : [],
759
+ callFrames: []
760
+ };
761
+ return pauseMatches(event, options.breakpointIds);
762
+ }
763
+ });
764
+ return {
765
+ reason: asString(params.reason),
766
+ hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
767
+ callFrames: toCallFrames(params.callFrames)
768
+ };
769
+ }
770
+ async function resume(session) {
771
+ await session.client.send("Debugger.resume");
772
+ }
773
+ async function evaluateOnFrame(session, callFrameId, expression) {
774
+ return await session.client.send("Debugger.evaluateOnCallFrame", {
775
+ callFrameId,
776
+ expression,
777
+ returnByValue: false,
778
+ generatePreview: true,
779
+ silent: true
780
+ });
781
+ }
782
+ async function evaluateGlobal(session, expression) {
783
+ return await session.client.send("Runtime.evaluate", {
784
+ expression,
785
+ returnByValue: false,
786
+ generatePreview: true,
787
+ silent: true
788
+ });
789
+ }
790
+ function listScripts(session) {
791
+ return [...session.scripts.values()];
792
+ }
793
+ async function getProperties(session, objectId) {
794
+ const result = await session.client.send("Runtime.getProperties", {
795
+ objectId,
796
+ ownProperties: true,
797
+ accessorPropertiesOnly: false,
798
+ generatePreview: true
799
+ });
800
+ if (!Array.isArray(result.result)) {
801
+ return [];
802
+ }
803
+ return result.result;
804
+ }
805
+
806
+ // src/logpoint.ts
807
+ import { randomBytes } from "crypto";
808
+ var SENTINEL_PREFIX = "__CFI_LOG_";
809
+ var SENTINEL_SUFFIX = "__";
810
+ function buildLogpointCondition(sentinel, expression) {
811
+ return [
812
+ "(function(){",
813
+ `var s=${JSON.stringify(sentinel)};`,
814
+ "try{",
815
+ `var v=(${expression});`,
816
+ "var r=typeof v==='string'?v:JSON.stringify(v);",
817
+ "console.log(s, r);",
818
+ "}catch(e){",
819
+ "console.log(s, '!err:'+(e&&e.message?e.message:String(e)));",
820
+ "}",
821
+ "return false;",
822
+ "})()"
823
+ ].join("");
824
+ }
825
+ function asString2(value) {
826
+ return typeof value === "string" ? value : void 0;
827
+ }
828
+ function readArg(arg, index) {
829
+ if (typeof arg !== "object" || arg === null) {
830
+ return void 0;
831
+ }
832
+ const candidate = arg;
833
+ if (candidate.type === "string" && typeof candidate.value === "string") {
834
+ return candidate.value;
835
+ }
836
+ const isPrimitiveType = candidate.type === "number" || candidate.type === "boolean" || candidate.type === "bigint";
837
+ const isPrimitiveValue = typeof candidate.value === "number" || typeof candidate.value === "boolean" || typeof candidate.value === "bigint";
838
+ if (isPrimitiveType && isPrimitiveValue) {
839
+ return String(candidate.value);
840
+ }
841
+ return index === 0 ? void 0 : "";
842
+ }
843
+ function parseLogEvent(rawArgs, sentinel, location, timestamp) {
844
+ if (!Array.isArray(rawArgs) || rawArgs.length < 2) {
845
+ return void 0;
846
+ }
847
+ const tag = readArg(rawArgs[0], 0);
848
+ if (tag !== sentinel) {
849
+ return void 0;
850
+ }
851
+ const payload = readArg(rawArgs[1], 1) ?? "";
852
+ const ts = new Date(typeof timestamp === "number" ? timestamp : Date.now()).toISOString();
853
+ const at = `${location.file}:${location.line.toString()}`;
854
+ if (payload.startsWith("!err:")) {
855
+ return { ts, at, error: payload.slice("!err:".length) };
856
+ }
857
+ try {
858
+ const parsed = JSON.parse(payload);
859
+ if (typeof parsed === "string") {
860
+ return { ts, at, value: parsed };
861
+ }
862
+ return { ts, at, value: JSON.stringify(parsed) };
863
+ } catch {
864
+ return { ts, at, value: payload, raw: payload };
865
+ }
866
+ }
867
+ function generateSentinel() {
868
+ return `${SENTINEL_PREFIX}${randomBytes(8).toString("hex")}${SENTINEL_SUFFIX}`;
869
+ }
870
+ async function streamLogpoint(session, options) {
871
+ const sentinel = generateSentinel();
872
+ const condition = buildLogpointCondition(sentinel, options.expression);
873
+ const handle = await setBreakpoint(session, {
874
+ file: options.location.file,
875
+ line: options.location.line,
876
+ ...options.remoteRoot === void 0 ? {} : { remoteRoot: options.remoteRoot },
877
+ condition
878
+ });
879
+ let emitted = 0;
880
+ const offEvent = session.client.on("Runtime.consoleAPICalled", (raw) => {
881
+ const params = raw;
882
+ if (asString2(params.type) !== "log") {
883
+ return;
884
+ }
885
+ const ts = typeof params.timestamp === "number" ? params.timestamp : void 0;
886
+ const event = parseLogEvent(params.args, sentinel, options.location, ts);
887
+ if (event === void 0) {
888
+ return;
889
+ }
890
+ emitted += 1;
891
+ options.onEvent(event);
892
+ });
893
+ const cleanup = async () => {
894
+ offEvent();
895
+ try {
896
+ await removeBreakpoint(session, handle.breakpointId);
897
+ } catch {
898
+ }
899
+ };
900
+ try {
901
+ const reason = await waitForStop(session, options);
902
+ return { handle, sentinel, emitted, stoppedReason: reason };
903
+ } finally {
904
+ await cleanup();
905
+ }
906
+ }
907
+ async function waitForStop(session, options) {
908
+ return await new Promise((resolve) => {
909
+ let settled = false;
910
+ const finish = (reason) => {
911
+ if (settled) {
912
+ return;
913
+ }
914
+ settled = true;
915
+ cleanup();
916
+ resolve(reason);
917
+ };
918
+ const timer = options.durationMs === void 0 ? void 0 : setTimeout(() => {
919
+ finish("duration");
920
+ }, options.durationMs);
921
+ const offClose = session.client.onClose(() => {
922
+ finish("transport-closed");
923
+ });
924
+ const onAbort = () => {
925
+ finish("signal");
926
+ };
927
+ options.signal?.addEventListener("abort", onAbort, { once: true });
928
+ if (options.signal?.aborted === true) {
929
+ finish("signal");
930
+ }
931
+ function cleanup() {
932
+ if (timer !== void 0) {
933
+ clearTimeout(timer);
934
+ }
935
+ offClose();
936
+ options.signal?.removeEventListener("abort", onAbort);
937
+ }
938
+ });
939
+ }
940
+
941
+ // src/snapshot.ts
942
+ var MAX_SCOPES = 3;
943
+ var MAX_SCOPE_VARIABLES = 20;
944
+ var MAX_CHILD_VARIABLES = 8;
945
+ var MAX_VARIABLE_DEPTH = 2;
946
+ var MAX_VALUE_LENGTH = 240;
947
+ var SENSITIVE_NAME_REGEX = /(pass(?:word)?|token|secret|api[_-]?key|authorization|cookie|session|private[_-]?key)/i;
948
+ var PRIORITY_BY_TYPE = {
949
+ local: 0,
950
+ arguments: 1,
951
+ block: 2,
952
+ closure: 3,
953
+ catch: 4,
954
+ with: 5,
955
+ module: 6,
956
+ script: 7
957
+ };
958
+ function buildDescribed(value, type, objectId) {
959
+ const base = { value };
960
+ if (type !== void 0) {
961
+ base.type = type;
962
+ }
963
+ if (objectId !== void 0) {
964
+ base.objectId = objectId;
965
+ }
966
+ return base;
967
+ }
968
+ function describeProperty(prop) {
969
+ const value = prop.value;
970
+ if (value === void 0) {
971
+ return { value: "undefined" };
972
+ }
973
+ const type = typeof value.type === "string" ? value.type : void 0;
974
+ const objectId = typeof value.objectId === "string" ? value.objectId : void 0;
975
+ if (type === "undefined") {
976
+ return buildDescribed("undefined", type);
977
+ }
978
+ if (type === "string" && typeof value.value === "string") {
979
+ return buildDescribed(JSON.stringify(value.value), type);
980
+ }
981
+ if ((type === "number" || type === "boolean" || type === "bigint" || type === "symbol") && isPrimitive(value.value)) {
982
+ return buildDescribed(formatPrimitive(value.value), type);
983
+ }
984
+ if (typeof value.description === "string") {
985
+ return buildDescribed(value.description, type, objectId);
986
+ }
987
+ if (isPrimitive(value.value)) {
988
+ return buildDescribed(formatPrimitive(value.value), type);
989
+ }
990
+ if (objectId === void 0) {
991
+ return buildDescribed("undefined", type);
992
+ }
993
+ return buildDescribed("[object]", type, objectId);
994
+ }
995
+ function isPrimitive(value) {
996
+ const t = typeof value;
997
+ return t === "string" || t === "number" || t === "boolean" || t === "bigint" || t === "symbol";
998
+ }
999
+ function formatPrimitive(value) {
1000
+ if (typeof value === "symbol") {
1001
+ return value.toString();
1002
+ }
1003
+ if (typeof value === "bigint") {
1004
+ return `${value.toString()}n`;
1005
+ }
1006
+ return String(value);
1007
+ }
1008
+ function sanitizeValue(name, raw) {
1009
+ if (SENSITIVE_NAME_REGEX.test(name)) {
1010
+ return "[REDACTED]";
1011
+ }
1012
+ if (raw.length <= MAX_VALUE_LENGTH) {
1013
+ return raw;
1014
+ }
1015
+ return `${raw.slice(0, MAX_VALUE_LENGTH)}...`;
1016
+ }
1017
+ function isExpandable(type) {
1018
+ return type === "object" || type === "function";
1019
+ }
1020
+ async function captureProperties(session, objectId, limit, depth) {
1021
+ const properties = await getProperties(session, objectId);
1022
+ const limited = properties.slice(0, limit);
1023
+ const variables = await Promise.all(
1024
+ limited.map(async (prop) => {
1025
+ const name = typeof prop.name === "string" ? prop.name : "?";
1026
+ const described = describeProperty(prop);
1027
+ let children;
1028
+ if (depth > 0 && described.objectId !== void 0 && isExpandable(described.type)) {
1029
+ try {
1030
+ const nested = await captureProperties(
1031
+ session,
1032
+ described.objectId,
1033
+ MAX_CHILD_VARIABLES,
1034
+ depth - 1
1035
+ );
1036
+ if (nested.length > 0) {
1037
+ children = nested;
1038
+ }
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ const sanitizedValue = sanitizeValue(name, described.value);
1043
+ const base = { name, value: sanitizedValue };
1044
+ const withType = described.type === void 0 ? base : { ...base, type: described.type };
1045
+ return children === void 0 ? withType : { ...withType, children };
1046
+ })
1047
+ );
1048
+ return variables;
1049
+ }
1050
+ function selectScopes(scopeChain) {
1051
+ const eligible = scopeChain.filter((scope) => scope.objectId !== void 0 && scope.type !== "global");
1052
+ return [...eligible].sort((a, b) => priorityOf(a.type) - priorityOf(b.type)).slice(0, MAX_SCOPES);
1053
+ }
1054
+ function priorityOf(type) {
1055
+ return PRIORITY_BY_TYPE[type] ?? Number.MAX_SAFE_INTEGER;
1056
+ }
1057
+ async function captureScopes(session, frame) {
1058
+ const scopes = selectScopes(frame.scopeChain);
1059
+ return await Promise.all(
1060
+ scopes.map(async (scope) => {
1061
+ const objectId = scope.objectId;
1062
+ if (objectId === void 0) {
1063
+ return { type: scope.type, variables: [] };
1064
+ }
1065
+ const variables = await captureProperties(session, objectId, MAX_SCOPE_VARIABLES, MAX_VARIABLE_DEPTH);
1066
+ return { type: scope.type, variables };
1067
+ })
1068
+ );
1069
+ }
1070
+ function evalResultToCaptured(expression, result) {
1071
+ if (result.exceptionDetails !== void 0) {
1072
+ const text = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
1073
+ return { expression, error: text };
1074
+ }
1075
+ const inner = result.result;
1076
+ if (!inner) {
1077
+ return { expression, error: "no result returned" };
1078
+ }
1079
+ const type = typeof inner.type === "string" ? inner.type : void 0;
1080
+ const buildCaptured = (rendered) => {
1081
+ const sanitized = sanitizeValue(expression, rendered);
1082
+ const base = { expression, value: sanitized };
1083
+ return type === void 0 ? base : { ...base, type };
1084
+ };
1085
+ if (type === "string" && typeof inner.value === "string") {
1086
+ return buildCaptured(JSON.stringify(inner.value));
1087
+ }
1088
+ if ((type === "number" || type === "boolean" || type === "bigint") && isPrimitive(inner.value)) {
1089
+ return buildCaptured(formatPrimitive(inner.value));
1090
+ }
1091
+ if (typeof inner.description === "string") {
1092
+ return buildCaptured(inner.description);
1093
+ }
1094
+ if (isPrimitive(inner.value)) {
1095
+ return buildCaptured(formatPrimitive(inner.value));
1096
+ }
1097
+ return buildCaptured("undefined");
1098
+ }
1099
+ async function captureSnapshot(session, pause, options = {}) {
1100
+ const top = pause.callFrames[0];
1101
+ let topFrame;
1102
+ let captures = [];
1103
+ if (top) {
1104
+ const scopes = await captureScopes(session, top);
1105
+ topFrame = {
1106
+ functionName: top.functionName,
1107
+ ...top.url === void 0 ? {} : { url: top.url },
1108
+ line: top.lineNumber + 1,
1109
+ column: top.columnNumber + 1,
1110
+ scopes
1111
+ };
1112
+ if (options.captures !== void 0 && options.captures.length > 0) {
1113
+ captures = await Promise.all(
1114
+ options.captures.map(async (expression) => {
1115
+ try {
1116
+ const result = await evaluateOnFrame(session, top.callFrameId, expression);
1117
+ return evalResultToCaptured(expression, result);
1118
+ } catch (err) {
1119
+ const message = err instanceof Error ? err.message : String(err);
1120
+ return { expression, error: message };
1121
+ }
1122
+ })
1123
+ );
1124
+ }
1125
+ }
1126
+ return {
1127
+ reason: pause.reason,
1128
+ hitBreakpoints: pause.hitBreakpoints,
1129
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1130
+ ...topFrame === void 0 ? {} : { topFrame },
1131
+ captures
1132
+ };
1133
+ }
1134
+
1135
+ // src/tunnel.ts
1136
+ import { startDebugger } from "@saptools/cf-debugger";
1137
+ async function openCfTunnel(target) {
1138
+ const opts = {
1139
+ region: target.region,
1140
+ org: target.org,
1141
+ space: target.space,
1142
+ app: target.app,
1143
+ ...target.tunnelReadyTimeoutMs === void 0 ? {} : { tunnelReadyTimeoutMs: target.tunnelReadyTimeoutMs },
1144
+ ...target.preferredPort === void 0 ? {} : { preferredPort: target.preferredPort },
1145
+ ...target.verbose === void 0 ? {} : { verbose: target.verbose },
1146
+ ...target.signal === void 0 ? {} : { signal: target.signal }
1147
+ };
1148
+ const handle = await startDebugger(opts);
1149
+ return {
1150
+ localPort: handle.session.localPort,
1151
+ handle,
1152
+ dispose: async () => {
1153
+ await handle.dispose();
1154
+ }
1155
+ };
1156
+ }
1157
+
1158
+ // src/cli.ts
1159
+ init_types();
1160
+ var DEFAULT_BREAKPOINT_TIMEOUT_SEC = 30;
1161
+ var DEFAULT_CF_TIMEOUT_SEC = 60;
1162
+ function parsePositiveInt(raw, label) {
1163
+ if (raw === void 0) {
1164
+ return void 0;
1165
+ }
1166
+ const value = Number.parseInt(raw, 10);
1167
+ if (Number.isNaN(value) || value <= 0) {
1168
+ throw new CfInspectorError("MISSING_TARGET", `Invalid ${label}: "${raw}"`);
1169
+ }
1170
+ return value;
1171
+ }
1172
+ function resolveTarget(opts) {
1173
+ const port = parsePositiveInt(opts.port, "--port");
1174
+ if (port !== void 0) {
1175
+ return { kind: "port", port, host: opts.host ?? "127.0.0.1" };
1176
+ }
1177
+ if (opts.region !== void 0 && opts.org !== void 0 && opts.space !== void 0 && opts.app !== void 0) {
1178
+ const cfTimeoutSec = parsePositiveInt(opts.cfTimeout, "--cf-timeout") ?? DEFAULT_CF_TIMEOUT_SEC;
1179
+ return {
1180
+ kind: "cf",
1181
+ region: opts.region,
1182
+ org: opts.org,
1183
+ space: opts.space,
1184
+ app: opts.app,
1185
+ cfTimeoutMs: cfTimeoutSec * 1e3
1186
+ };
1187
+ }
1188
+ throw new CfInspectorError(
1189
+ "MISSING_TARGET",
1190
+ "Provide either --port (and optionally --host) or all of --region, --org, --space, --app."
1191
+ );
1192
+ }
1193
+ async function openTarget(target) {
1194
+ if (target.kind === "port") {
1195
+ return {
1196
+ port: target.port,
1197
+ host: target.host,
1198
+ dispose: () => Promise.resolve()
1199
+ };
1200
+ }
1201
+ const tunnel = await openCfTunnel({
1202
+ region: target.region,
1203
+ org: target.org,
1204
+ space: target.space,
1205
+ app: target.app,
1206
+ tunnelReadyTimeoutMs: target.cfTimeoutMs
1207
+ });
1208
+ return {
1209
+ port: tunnel.localPort,
1210
+ host: "127.0.0.1",
1211
+ dispose: async () => {
1212
+ await tunnel.dispose();
1213
+ }
1214
+ };
1215
+ }
1216
+ function parseCaptureList(raw) {
1217
+ if (raw === void 0 || raw.trim().length === 0) {
1218
+ return [];
1219
+ }
1220
+ return raw.split(",").map((piece) => piece.trim()).filter((piece) => piece.length > 0);
1221
+ }
1222
+ async function withSession(target, fn) {
1223
+ const tunnel = await openTarget(target);
1224
+ let session;
1225
+ try {
1226
+ session = await connectInspector({ port: tunnel.port, host: tunnel.host });
1227
+ return await fn(session, tunnel.port);
1228
+ } finally {
1229
+ if (session) {
1230
+ await session.dispose();
1231
+ }
1232
+ await tunnel.dispose();
1233
+ }
1234
+ }
1235
+ function writeJson(value) {
1236
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
1237
+ `);
1238
+ }
1239
+ function writeHumanSnapshot(snapshot) {
1240
+ const lines = [];
1241
+ lines.push(`Snapshot @ ${snapshot.capturedAt}`, ` reason: ${snapshot.reason}`);
1242
+ if (snapshot.topFrame) {
1243
+ const frame = snapshot.topFrame;
1244
+ const fnName = frame.functionName.length === 0 ? "(anonymous)" : frame.functionName;
1245
+ const sourceUrl = frame.url !== void 0 && frame.url.length > 0 ? frame.url : "(unknown)";
1246
+ lines.push(
1247
+ ` frame: ${fnName} ${sourceUrl}:${frame.line.toString()}:${frame.column.toString()}`
1248
+ );
1249
+ for (const scope of frame.scopes) {
1250
+ lines.push(` scope ${scope.type} (${scope.variables.length.toString()} vars):`);
1251
+ for (const variable of scope.variables) {
1252
+ lines.push(` ${variable.name} = ${variable.value}`);
1253
+ }
1254
+ }
1255
+ }
1256
+ if (snapshot.captures.length > 0) {
1257
+ lines.push(" captures:");
1258
+ for (const capture of snapshot.captures) {
1259
+ const detail = capture.error ?? capture.value ?? "undefined";
1260
+ lines.push(` ${capture.expression} = ${detail}`);
1261
+ }
1262
+ }
1263
+ process.stdout.write(`${lines.join("\n")}
1264
+ `);
1265
+ }
1266
+ async function handleSnapshot(opts) {
1267
+ const target = resolveTarget(opts);
1268
+ if (opts.bp.length === 0) {
1269
+ throw new CfInspectorError(
1270
+ "INVALID_BREAKPOINT",
1271
+ "At least one --bp <file:line> is required."
1272
+ );
1273
+ }
1274
+ const breakpoints = opts.bp.map((spec) => parseBreakpointSpec(spec));
1275
+ const remoteRoot = parseRemoteRoot(opts.remoteRoot);
1276
+ const captures = parseCaptureList(opts.capture);
1277
+ const timeoutSec = parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_BREAKPOINT_TIMEOUT_SEC;
1278
+ const timeoutMs = timeoutSec * 1e3;
1279
+ const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
1280
+ const result = await withSession(target, async (session) => {
1281
+ const handles = await Promise.all(
1282
+ breakpoints.map(
1283
+ (bp) => setBreakpoint(session, {
1284
+ file: bp.file,
1285
+ line: bp.line,
1286
+ remoteRoot,
1287
+ ...condition === void 0 ? {} : { condition }
1288
+ })
1289
+ )
1290
+ );
1291
+ const breakpointIds = handles.map((h) => h.breakpointId);
1292
+ const pause = await waitForPause(session, { timeoutMs, breakpointIds });
1293
+ const snapshot = await captureSnapshot(session, pause, { captures });
1294
+ if (opts.keepPaused !== true) {
1295
+ try {
1296
+ await resume(session);
1297
+ } catch {
1298
+ }
1299
+ }
1300
+ return snapshot;
1301
+ });
1302
+ if (opts.json) {
1303
+ writeJson(result);
1304
+ } else {
1305
+ writeHumanSnapshot(result);
1306
+ }
1307
+ }
1308
+ async function handleEval(opts) {
1309
+ const target = resolveTarget(opts);
1310
+ const result = await withSession(target, async (session) => {
1311
+ return await evaluateGlobal(session, opts.expr);
1312
+ });
1313
+ if (opts.json) {
1314
+ writeJson(result);
1315
+ return;
1316
+ }
1317
+ if (result.exceptionDetails !== void 0) {
1318
+ const detail = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
1319
+ process.stderr.write(`${detail}
1320
+ `);
1321
+ process.exitCode = 1;
1322
+ return;
1323
+ }
1324
+ const inner = result.result;
1325
+ if (inner === void 0) {
1326
+ process.stdout.write("\n");
1327
+ return;
1328
+ }
1329
+ if (typeof inner.value === "string") {
1330
+ process.stdout.write(`${inner.value}
1331
+ `);
1332
+ return;
1333
+ }
1334
+ if (typeof inner.description === "string") {
1335
+ process.stdout.write(`${inner.description}
1336
+ `);
1337
+ return;
1338
+ }
1339
+ process.stdout.write(`${JSON.stringify(inner.value)}
1340
+ `);
1341
+ }
1342
+ function writeLogEvent(event, json) {
1343
+ if (json) {
1344
+ process.stdout.write(`${JSON.stringify(event)}
1345
+ `);
1346
+ return;
1347
+ }
1348
+ if (event.error !== void 0) {
1349
+ process.stdout.write(`[${event.ts}] ${event.at} !err ${event.error}
1350
+ `);
1351
+ return;
1352
+ }
1353
+ process.stdout.write(`[${event.ts}] ${event.at} ${event.value ?? ""}
1354
+ `);
1355
+ }
1356
+ async function handleLog(opts) {
1357
+ const target = resolveTarget(opts);
1358
+ const location = parseBreakpointSpec(opts.at);
1359
+ const remoteRoot = parseRemoteRoot(opts.remoteRoot);
1360
+ const durationSec = parsePositiveInt(opts.duration, "--duration");
1361
+ const expression = opts.expr.trim();
1362
+ if (expression.length === 0) {
1363
+ throw new CfInspectorError("INVALID_BREAKPOINT", "--expr must not be empty");
1364
+ }
1365
+ const abort = new AbortController();
1366
+ const onSig = () => {
1367
+ abort.abort();
1368
+ };
1369
+ process.once("SIGINT", onSig);
1370
+ process.once("SIGTERM", onSig);
1371
+ try {
1372
+ await withSession(target, async (session) => {
1373
+ const result = await streamLogpoint(session, {
1374
+ location,
1375
+ expression,
1376
+ remoteRoot,
1377
+ ...durationSec === void 0 ? {} : { durationMs: durationSec * 1e3 },
1378
+ signal: abort.signal,
1379
+ onEvent: (event) => {
1380
+ writeLogEvent(event, opts.json);
1381
+ }
1382
+ });
1383
+ if (opts.json) {
1384
+ process.stderr.write(
1385
+ `${JSON.stringify({ stopped: result.stoppedReason, emitted: result.emitted })}
1386
+ `
1387
+ );
1388
+ } else {
1389
+ process.stderr.write(
1390
+ `Stopped (${result.stoppedReason}); emitted ${result.emitted.toString()} log ${result.emitted === 1 ? "entry" : "entries"}.
1391
+ `
1392
+ );
1393
+ }
1394
+ });
1395
+ } finally {
1396
+ process.off("SIGINT", onSig);
1397
+ process.off("SIGTERM", onSig);
1398
+ }
1399
+ }
1400
+ async function handleListScripts(opts) {
1401
+ const target = resolveTarget(opts);
1402
+ const scripts = await withSession(target, (session) => Promise.resolve(listScripts(session)));
1403
+ if (opts.json) {
1404
+ writeJson(scripts);
1405
+ return;
1406
+ }
1407
+ for (const script of scripts) {
1408
+ process.stdout.write(`${script.scriptId} ${script.url}
1409
+ `);
1410
+ }
1411
+ }
1412
+ async function handleAttach(opts) {
1413
+ const target = resolveTarget(opts);
1414
+ const tunnel = await openTarget(target);
1415
+ try {
1416
+ const version = await fetchInspectorVersion(tunnel.host, tunnel.port, 5e3);
1417
+ if (opts.json) {
1418
+ writeJson({ host: tunnel.host, port: tunnel.port, ...version });
1419
+ return;
1420
+ }
1421
+ process.stdout.write(
1422
+ `Connected to ${tunnel.host}:${tunnel.port.toString()}
1423
+ Browser: ${version.browser}
1424
+ Protocol: ${version.protocolVersion}
1425
+ `
1426
+ );
1427
+ } finally {
1428
+ await tunnel.dispose();
1429
+ }
1430
+ }
1431
+ function applyTargetOptions(cmd) {
1432
+ 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 (e.g. eu10)").option("--org <name>", "CF org name").option("--space <name>", "CF space name").option("--app <name>", "CF app name").option("--cf-timeout <seconds>", "Timeout for CF tunnel readiness in seconds");
1433
+ }
1434
+ async function main(argv) {
1435
+ const program = new Command();
1436
+ program.name("cf-inspector").description("Drive a Node.js inspector from the command line \u2014 set breakpoints, capture snapshots, evaluate expressions");
1437
+ const collectStrings = (value, prev = []) => [
1438
+ ...prev,
1439
+ value
1440
+ ];
1441
+ applyTargetOptions(
1442
+ program.command("snapshot").description("Set a breakpoint, wait for it to hit, capture the scope, and resume")
1443
+ ).option(
1444
+ "--bp <file:line>",
1445
+ "Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42",
1446
+ collectStrings,
1447
+ []
1448
+ ).option("--capture <expr,\u2026>", "Comma-separated expressions to evaluate in the paused frame").option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option(
1449
+ "--condition <expr>",
1450
+ "Only pause when this JS expression evaluates truthy in the paused frame"
1451
+ ).option("--no-json", "Print a human-readable summary instead of JSON").option("--keep-paused", "Skip the auto-resume after capture").action(async (opts) => {
1452
+ await handleSnapshot(opts);
1453
+ });
1454
+ applyTargetOptions(
1455
+ program.command("log").description("Stream a non-pausing logpoint: log an expression each time a line executes")
1456
+ ).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("--no-json", "Print human-readable lines instead of JSON Lines").action(async (opts) => {
1457
+ await handleLog(opts);
1458
+ });
1459
+ applyTargetOptions(
1460
+ program.command("eval").description("Evaluate an expression against the global Runtime")
1461
+ ).requiredOption("--expr <expression>", "JavaScript expression to evaluate").option("--no-json", "Print only the resulting value, not the full CDP envelope").action(async (opts) => {
1462
+ await handleEval(opts);
1463
+ });
1464
+ applyTargetOptions(
1465
+ program.command("list-scripts").description("Print the scripts the V8 instance currently knows about")
1466
+ ).option("--no-json", "Print scriptId<TAB>url instead of JSON").action(async (opts) => {
1467
+ await handleListScripts(opts);
1468
+ });
1469
+ applyTargetOptions(
1470
+ program.command("attach").description("Connect, fetch the inspector version, and disconnect (smoke-test)")
1471
+ ).option("--no-json", "Print a multi-line summary instead of JSON").action(async (opts) => {
1472
+ await handleAttach(opts);
1473
+ });
1474
+ await program.parseAsync([...argv]);
1475
+ }
1476
+ try {
1477
+ await main(process.argv);
1478
+ } catch (err) {
1479
+ if (err instanceof CfInspectorError) {
1480
+ process.stderr.write(`Error [${err.code}]: ${err.message}
1481
+ `);
1482
+ if (err.detail !== void 0) {
1483
+ process.stderr.write(` detail: ${err.detail}
1484
+ `);
1485
+ }
1486
+ process.exit(1);
1487
+ }
1488
+ const message = err instanceof Error ? err.message : String(err);
1489
+ process.stderr.write(`Error: ${message}
1490
+ `);
1491
+ process.exit(1);
1492
+ }
1493
+ export {
1494
+ main
1495
+ };
1496
+ //# sourceMappingURL=cli.js.map