@ranganathmk/trq 0.1.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.mjs ADDED
@@ -0,0 +1,1721 @@
1
+ import { createRequire } from "module"; const require = createRequire(import.meta.url);
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
+ // ../core/src/cdp.ts
13
+ function adaptCDPSession(session) {
14
+ return {
15
+ send: (method, params) => session.send(method, params),
16
+ on: (event, listener) => {
17
+ session.on(event, listener);
18
+ },
19
+ off: (event, listener) => {
20
+ session.off(event, listener);
21
+ }
22
+ };
23
+ }
24
+ var init_cdp = __esm({
25
+ "../core/src/cdp.ts"() {
26
+ "use strict";
27
+ }
28
+ });
29
+
30
+ // ../core/src/config.ts
31
+ async function safeSend(client, method, params) {
32
+ try {
33
+ await client.send(method, params);
34
+ } catch (err) {
35
+ console.warn(`[config] ${method} failed: ${err.message}`);
36
+ }
37
+ }
38
+ async function applyConfig(client, cfg, options = {}) {
39
+ if (cfg.viewport) {
40
+ await safeSend(client, "Emulation.setDeviceMetricsOverride", {
41
+ width: cfg.viewport.width,
42
+ height: cfg.viewport.height,
43
+ deviceScaleFactor: cfg.viewport.deviceScaleFactor ?? 1,
44
+ mobile: cfg.viewport.mobile ?? false,
45
+ // Critical: without this, Chromium expands the visible BrowserView area
46
+ // to match the device-metrics size, overlapping any UI below it. The
47
+ // visible bounds are owned by view.setBounds elsewhere; this command
48
+ // should only affect what the page reports as its viewport.
49
+ dontSetVisibleSize: true
50
+ });
51
+ }
52
+ if (cfg.locale) {
53
+ await safeSend(client, "Emulation.setLocaleOverride", { locale: cfg.locale });
54
+ }
55
+ if (cfg.timezone) {
56
+ await safeSend(client, "Emulation.setTimezoneOverride", {
57
+ timezoneId: cfg.timezone
58
+ });
59
+ }
60
+ if (cfg.userAgent) {
61
+ await safeSend(client, "Network.setUserAgentOverride", { userAgent: cfg.userAgent });
62
+ }
63
+ if (cfg.extraHeaders && Object.keys(cfg.extraHeaders).length > 0) {
64
+ await safeSend(client, "Network.enable", {});
65
+ await safeSend(client, "Network.setExtraHTTPHeaders", { headers: cfg.extraHeaders });
66
+ }
67
+ if (cfg.bypassCSP) {
68
+ await safeSend(client, "Page.setBypassCSP", { enabled: true });
69
+ }
70
+ if (cfg.permissions && cfg.permissions.length > 0) {
71
+ const params = {
72
+ permissions: cfg.permissions
73
+ };
74
+ if (options.origin) params.origin = options.origin;
75
+ await safeSend(client, "Browser.grantPermissions", params);
76
+ }
77
+ if (cfg.geolocation) {
78
+ await safeSend(client, "Emulation.setGeolocationOverride", {
79
+ latitude: cfg.geolocation.latitude,
80
+ longitude: cfg.geolocation.longitude,
81
+ accuracy: cfg.geolocation.accuracy ?? 10
82
+ });
83
+ }
84
+ }
85
+ var init_config = __esm({
86
+ "../core/src/config.ts"() {
87
+ "use strict";
88
+ }
89
+ });
90
+
91
+ // ../core/src/page-context.ts
92
+ var page_context_exports = {};
93
+ __export(page_context_exports, {
94
+ cdpPageContext: () => cdpPageContext,
95
+ puppeteerPageContext: () => puppeteerPageContext
96
+ });
97
+ function puppeteerPageContext(page, client) {
98
+ return {
99
+ client,
100
+ evaluate: (fnOrExpr, ...args) => page.evaluate(fnOrExpr, ...args),
101
+ url: () => page.url(),
102
+ goto: async (url, opts) => {
103
+ await page.goto(url, { waitUntil: "networkidle2", timeout: opts?.timeout ?? 3e4 });
104
+ },
105
+ waitForLoad: async (timeout) => {
106
+ await page.waitForNavigation({ timeout: timeout ?? 3e4 }).catch(() => {
107
+ });
108
+ }
109
+ };
110
+ }
111
+ function cdpPageContext(client, options = {}) {
112
+ let currentUrl = options.initialUrl ?? "about:blank";
113
+ let mainFrameId = null;
114
+ void client.send("Page.getFrameTree").then((tree) => {
115
+ const t = tree;
116
+ mainFrameId = t.frameTree?.frame?.id ?? null;
117
+ if (t.frameTree?.frame?.url) currentUrl = t.frameTree.frame.url;
118
+ }).catch(() => {
119
+ });
120
+ client.on("Page.frameNavigated", (raw) => {
121
+ const e = raw;
122
+ if (e.frame.parentId) return;
123
+ if (mainFrameId && e.frame.id !== mainFrameId) return;
124
+ currentUrl = e.frame.url;
125
+ });
126
+ client.on("Page.navigatedWithinDocument", (raw) => {
127
+ const e = raw;
128
+ if (mainFrameId && e.frameId !== mainFrameId) return;
129
+ currentUrl = e.url;
130
+ });
131
+ const waitForLoad = (timeout = 3e4) => new Promise((resolve) => {
132
+ const t = setTimeout(() => {
133
+ client.off?.("Page.loadEventFired", onLoad);
134
+ resolve();
135
+ }, timeout);
136
+ const onLoad = () => {
137
+ clearTimeout(t);
138
+ client.off?.("Page.loadEventFired", onLoad);
139
+ resolve();
140
+ };
141
+ client.on("Page.loadEventFired", onLoad);
142
+ });
143
+ const evaluateExpr = async (expression) => {
144
+ const r = await client.send("Runtime.evaluate", {
145
+ expression,
146
+ awaitPromise: true,
147
+ returnByValue: true
148
+ });
149
+ if (r.exceptionDetails) {
150
+ const desc = r.exceptionDetails.exception?.description ?? r.exceptionDetails.text ?? "evaluate failed";
151
+ throw new Error(desc);
152
+ }
153
+ return r.result?.value;
154
+ };
155
+ return {
156
+ client,
157
+ evaluate: (fnOrExpr, ...args) => {
158
+ if (typeof fnOrExpr === "string") {
159
+ return evaluateExpr(fnOrExpr);
160
+ }
161
+ const body = fnOrExpr.toString();
162
+ const jsonArgs = args.map((a) => JSON.stringify(a)).join(",");
163
+ const expression = `(${body})(${jsonArgs})`;
164
+ return evaluateExpr(expression);
165
+ },
166
+ url: () => currentUrl,
167
+ goto: async (url, opts) => {
168
+ const loaded = waitForLoad(opts?.timeout ?? 3e4);
169
+ await client.send("Page.navigate", { url });
170
+ await loaded;
171
+ await new Promise((r) => setTimeout(r, 200));
172
+ },
173
+ waitForLoad
174
+ };
175
+ }
176
+ var init_page_context = __esm({
177
+ "../core/src/page-context.ts"() {
178
+ "use strict";
179
+ }
180
+ });
181
+
182
+ // ../core/src/replay.ts
183
+ var replay_exports = {};
184
+ __export(replay_exports, {
185
+ HELPERS_PATH: () => HELPERS_PATH,
186
+ INJECT_ANIMATION_KILL: () => INJECT_ANIMATION_KILL,
187
+ isAnchorClick: () => isAnchorClick,
188
+ replayAssert: () => replayAssert,
189
+ replayClick: () => replayClick,
190
+ replayInput: () => replayInput,
191
+ replayKey: () => replayKey,
192
+ replayNavigate: () => replayNavigate,
193
+ runReplay: () => runReplay,
194
+ runReplayOnClient: () => runReplayOnClient,
195
+ setupReplayClient: () => setupReplayClient,
196
+ urlAreaOf: () => urlAreaOf,
197
+ urlInArea: () => urlInArea,
198
+ waitForAnchorClickConsequence: () => waitForAnchorClickConsequence
199
+ });
200
+ import puppeteer2 from "puppeteer";
201
+ import { promises as fs3 } from "node:fs";
202
+ import path3 from "node:path";
203
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
204
+ function modBits(mods) {
205
+ let b = 0;
206
+ for (const m of mods) {
207
+ if (m === "Alt") b |= 1;
208
+ if (m === "Control") b |= 2;
209
+ if (m === "Meta") b |= 4;
210
+ if (m === "Shift") b |= 8;
211
+ }
212
+ return b;
213
+ }
214
+ async function waitFor(predicate, timeout = 5e3, pollMs = 100) {
215
+ const start = Date.now();
216
+ while (Date.now() - start < timeout) {
217
+ if (await predicate()) return true;
218
+ await new Promise((r) => setTimeout(r, pollMs));
219
+ }
220
+ return false;
221
+ }
222
+ async function waitForResolvable(ctx, selectors, timeout = 5e3) {
223
+ const ok = await waitFor(
224
+ async () => await ctx.evaluate((sels) => {
225
+ const w = window;
226
+ return !!w.__trqResolve?.(sels)?.found;
227
+ }, selectors).catch(() => false),
228
+ timeout
229
+ );
230
+ if (!ok) {
231
+ throw new Error(`selector did not resolve within ${timeout}ms: ${selectors[0]?.[0] ?? "?"}`);
232
+ }
233
+ }
234
+ async function replayClick(e, ctx) {
235
+ await waitForResolvable(ctx, e.selectors);
236
+ const result = await ctx.evaluate(
237
+ (sels) => window.__trqClick(sels),
238
+ e.selectors
239
+ );
240
+ if (result.error || !result.rect) throw new Error(result.error || "no rect from click resolve");
241
+ const { rect } = result;
242
+ const x = rect.x + Math.min(e.click.offsetX, rect.w);
243
+ const y = rect.y + Math.min(e.click.offsetY, rect.h);
244
+ const button = MOUSE_BUTTON[e.click.button] ?? "left";
245
+ const modifiers = modBits(e.click.modifiers);
246
+ await ctx.client.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
247
+ await ctx.client.send("Input.dispatchMouseEvent", {
248
+ type: "mousePressed",
249
+ x,
250
+ y,
251
+ button,
252
+ modifiers,
253
+ clickCount: e.click.clickCount
254
+ });
255
+ await ctx.client.send("Input.dispatchMouseEvent", {
256
+ type: "mouseReleased",
257
+ x,
258
+ y,
259
+ button,
260
+ modifiers,
261
+ clickCount: e.click.clickCount
262
+ });
263
+ }
264
+ async function replayInput(e, ctx) {
265
+ await waitForResolvable(ctx, e.selectors);
266
+ const result = await ctx.evaluate(
267
+ (sels, val) => window.__trqSetInput(sels, val),
268
+ e.selectors,
269
+ e.value
270
+ );
271
+ if (result.error) throw new Error(result.error);
272
+ }
273
+ async function replayKey(e, ctx) {
274
+ await ctx.evaluate(
275
+ (sels) => window.__trqFocus?.(sels),
276
+ e.selectors
277
+ ).catch(() => {
278
+ });
279
+ const mods = modBits(e.modifiers);
280
+ await ctx.client.send("Input.dispatchKeyEvent", {
281
+ type: "keyDown",
282
+ key: e.key,
283
+ code: e.key,
284
+ modifiers: mods
285
+ });
286
+ await ctx.client.send("Input.dispatchKeyEvent", {
287
+ type: "keyUp",
288
+ key: e.key,
289
+ code: e.key,
290
+ modifiers: mods
291
+ });
292
+ }
293
+ async function replayAssert(e, ctx) {
294
+ await waitForResolvable(ctx, e.selectors);
295
+ const result = await ctx.evaluate(
296
+ (sels, spec) => window.__trqAssert(sels, spec),
297
+ e.selectors,
298
+ e.assertion
299
+ );
300
+ if (!result.ok) throw new Error(result.reason ?? "assertion failed");
301
+ }
302
+ async function replayNavigate(e, ctx, prev) {
303
+ const wasUserAction = prev && (prev.type === "click" || prev.type === "key");
304
+ const prevClickTarget = prev?.type === "click" ? prev.target : null;
305
+ const prevIsAnchorClick = !!prevClickTarget && (prevClickTarget.tagName === "A" || prevClickTarget.attributes?.target === "_blank" || !!prevClickTarget.attributes?.href);
306
+ if (e.source === "in-document") {
307
+ const ok = await waitFor(async () => ctx.url() === e.url, 5e3);
308
+ if (!ok) {
309
+ throw new Error(`in-document nav to ${e.url} did not occur (still at ${ctx.url()})`);
310
+ }
311
+ return;
312
+ }
313
+ if (prevIsAnchorClick) {
314
+ const before = ctx.url();
315
+ const ok = await waitFor(async () => ctx.url() !== before, 1e4);
316
+ if (!ok) {
317
+ throw new Error(`anchor click did not trigger a navigation (still at ${before})`);
318
+ }
319
+ await waitFor(
320
+ async () => await ctx.evaluate(() => document.readyState) === "complete",
321
+ 5e3
322
+ ).catch(() => false);
323
+ return;
324
+ }
325
+ if (wasUserAction) {
326
+ const ok = await waitFor(async () => ctx.url() === e.url, 5e3);
327
+ if (ok) return;
328
+ }
329
+ await ctx.goto(e.url, { timeout: 3e4 });
330
+ }
331
+ async function runReplay(opts) {
332
+ const events = await opts.session.readEvents();
333
+ const total = events.length;
334
+ const target = opts.toStep != null ? Math.min(opts.toStep, total) : total;
335
+ const helpers = await fs3.readFile(HELPERS_PATH, "utf8");
336
+ const config = await opts.session.readConfig();
337
+ const browser = await puppeteer2.launch({
338
+ headless: false,
339
+ defaultViewport: null,
340
+ args: ["--no-first-run", "--no-default-browser-check", "--start-maximized"]
341
+ });
342
+ browser.on("targetdestroyed", async (t) => {
343
+ if (t.type() !== "page") return;
344
+ try {
345
+ const remaining = (await browser.pages()).length;
346
+ if (remaining === 0) await browser.close();
347
+ } catch {
348
+ }
349
+ });
350
+ const onSigint = async () => {
351
+ try {
352
+ await browser.close();
353
+ } catch {
354
+ }
355
+ };
356
+ process.once("SIGINT", onSigint);
357
+ const setupPage = async (page2) => {
358
+ const client2 = await page2.createCDPSession();
359
+ await client2.send("Page.enable");
360
+ await client2.send("Runtime.enable");
361
+ await applyConfig(adaptCDPSession(client2), config);
362
+ await client2.send("Page.addScriptToEvaluateOnNewDocument", { source: helpers });
363
+ if (opts.killAnimations !== false) {
364
+ await client2.send("Page.addScriptToEvaluateOnNewDocument", {
365
+ source: INJECT_ANIMATION_KILL
366
+ });
367
+ }
368
+ await client2.send("Runtime.evaluate", { expression: helpers }).catch(() => {
369
+ });
370
+ if (opts.killAnimations !== false) {
371
+ await client2.send("Runtime.evaluate", { expression: INJECT_ANIMATION_KILL }).catch(() => {
372
+ });
373
+ }
374
+ return client2;
375
+ };
376
+ const pages = await browser.pages();
377
+ const page = pages[0] ?? await browser.newPage();
378
+ const client = await setupPage(page);
379
+ const ctx = puppeteerPageContext(page, adaptCDPSession(client));
380
+ let completed = 0;
381
+ let prev = null;
382
+ for (let i = 0; i < target; i++) {
383
+ const e = events[i];
384
+ opts.onProgress?.(i + 1, target, e, "start");
385
+ try {
386
+ switch (e.type) {
387
+ case "navigate":
388
+ await replayNavigate(e, ctx, prev);
389
+ break;
390
+ case "click":
391
+ await replayClick(e, ctx);
392
+ break;
393
+ case "input":
394
+ await replayInput(e, ctx);
395
+ break;
396
+ case "key":
397
+ await replayKey(e, ctx);
398
+ break;
399
+ case "assert":
400
+ await replayAssert(e, ctx);
401
+ break;
402
+ }
403
+ if (e.type === "click" && isAnchorClick(e)) {
404
+ const next = events[i + 1];
405
+ const expectedNextUrl = next && "url" in next ? next.url : void 0;
406
+ const expectedArea = urlAreaOf(expectedNextUrl);
407
+ const start = Date.now();
408
+ let lastUrl = null;
409
+ let stableSince = start;
410
+ while (Date.now() - start < 2e4) {
411
+ let ready = false;
412
+ try {
413
+ const v = await ctx.evaluate(() => document.readyState);
414
+ ready = v === "complete";
415
+ } catch {
416
+ }
417
+ const u = ctx.url();
418
+ if (u !== lastUrl) {
419
+ lastUrl = u;
420
+ stableSince = Date.now();
421
+ }
422
+ const inArea = expectedArea ? urlInArea(u, expectedArea) : true;
423
+ if (ready && inArea && Date.now() - stableSince >= 1200) break;
424
+ await new Promise((r) => setTimeout(r, 150));
425
+ }
426
+ }
427
+ const settle = opts.slow ? 400 : e.type === "click" || e.type === "key" ? 60 : 20;
428
+ if (settle) await new Promise((r) => setTimeout(r, settle));
429
+ completed++;
430
+ prev = e;
431
+ opts.onProgress?.(i + 1, target, e, "ok");
432
+ } catch (err) {
433
+ const msg = err.message;
434
+ opts.onProgress?.(i + 1, target, e, "fail", msg);
435
+ process.removeListener("SIGINT", onSigint);
436
+ return {
437
+ completed,
438
+ total,
439
+ failed: { step: i + 1, reason: msg, event: e },
440
+ browser
441
+ };
442
+ }
443
+ }
444
+ process.removeListener("SIGINT", onSigint);
445
+ return { completed, total, browser };
446
+ }
447
+ function isAnchorClick(e) {
448
+ if (e.type !== "click") return false;
449
+ const t = e.target;
450
+ return t.tagName === "A" || t.attributes?.target === "_blank" || !!t.attributes?.href;
451
+ }
452
+ function urlAreaOf(url) {
453
+ if (!url) return null;
454
+ try {
455
+ const u = new URL(url);
456
+ const segs = u.pathname.split("/").filter((s) => s.length > 0);
457
+ return { origin: u.origin, head: segs[0] ?? "" };
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
462
+ function urlInArea(url, area) {
463
+ try {
464
+ const u = new URL(url);
465
+ const head = u.pathname.split("/").filter((s) => s.length > 0)[0] ?? "";
466
+ return u.origin === area.origin && head === area.head;
467
+ } catch {
468
+ return false;
469
+ }
470
+ }
471
+ async function waitForAnchorClickConsequence(getActiveClient, fallbackClient, ctxFor, clientBefore, expectedNextUrl, nextEventSelectors, switchTimeoutMs = 3e3, totalTimeoutMs = 2e4, stableWindowMs = 1200) {
472
+ const start = Date.now();
473
+ while (Date.now() - start < switchTimeoutMs) {
474
+ const cNow = getActiveClient?.() ?? fallbackClient;
475
+ if (cNow !== clientBefore) break;
476
+ await new Promise((r) => setTimeout(r, 50));
477
+ }
478
+ const cFinal = getActiveClient?.() ?? fallbackClient;
479
+ const ctxFinal = ctxFor(cFinal);
480
+ const expectedArea = urlAreaOf(expectedNextUrl);
481
+ let lastUrl = null;
482
+ let stableSince = Date.now();
483
+ while (Date.now() - start < totalTimeoutMs) {
484
+ let ready = false;
485
+ try {
486
+ const v = await ctxFinal.evaluate(() => document.readyState);
487
+ ready = v === "complete";
488
+ } catch {
489
+ }
490
+ const u = ctxFinal.url();
491
+ if (u !== lastUrl) {
492
+ lastUrl = u;
493
+ stableSince = Date.now();
494
+ }
495
+ const inArea = expectedArea ? urlInArea(u, expectedArea) : true;
496
+ if (ready && inArea && Date.now() - stableSince >= stableWindowMs) break;
497
+ await new Promise((r) => setTimeout(r, 150));
498
+ }
499
+ if (nextEventSelectors && nextEventSelectors.length > 0) {
500
+ const remaining = Math.max(0, totalTimeoutMs - (Date.now() - start));
501
+ await waitFor(
502
+ async () => {
503
+ try {
504
+ const found = await ctxFinal.evaluate((sels) => {
505
+ const w = window;
506
+ return !!w.__trqResolve?.(sels)?.found;
507
+ }, nextEventSelectors);
508
+ return found;
509
+ } catch {
510
+ return false;
511
+ }
512
+ },
513
+ remaining
514
+ ).catch(() => false);
515
+ }
516
+ }
517
+ async function setupReplayClient(client, opts = {}) {
518
+ const helpers = await fs3.readFile(HELPERS_PATH, "utf8");
519
+ await client.send("Page.enable");
520
+ await client.send("Runtime.enable");
521
+ if (opts.config) {
522
+ await applyConfig(client, opts.config);
523
+ }
524
+ if (process.env.TRQ_DEBUG_REPLAY) {
525
+ const setFlag = "window.__trqDebugResolve = true;";
526
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: setFlag });
527
+ await client.send("Runtime.evaluate", { expression: setFlag }).catch(() => {
528
+ });
529
+ }
530
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: helpers });
531
+ if (opts.killAnimations !== false) {
532
+ await client.send("Page.addScriptToEvaluateOnNewDocument", {
533
+ source: INJECT_ANIMATION_KILL
534
+ });
535
+ }
536
+ await client.send("Runtime.evaluate", { expression: helpers }).catch(() => {
537
+ });
538
+ if (opts.killAnimations !== false) {
539
+ await client.send("Runtime.evaluate", { expression: INJECT_ANIMATION_KILL }).catch(() => {
540
+ });
541
+ }
542
+ }
543
+ async function runReplayOnClient(opts) {
544
+ let events = await opts.session.readEvents();
545
+ let total = events.length;
546
+ let target = opts.toStep != null ? Math.min(opts.toStep, total) : total;
547
+ const config = await opts.session.readConfig();
548
+ await setupReplayClient(opts.client, {
549
+ config,
550
+ killAnimations: opts.killAnimations
551
+ });
552
+ const { cdpPageContext: cdpPageContext2 } = await Promise.resolve().then(() => (init_page_context(), page_context_exports));
553
+ const ctxCache = /* @__PURE__ */ new Map();
554
+ const ctxFor = (c) => {
555
+ let cached = ctxCache.get(c);
556
+ if (!cached) {
557
+ cached = cdpPageContext2(c);
558
+ ctxCache.set(c, cached);
559
+ }
560
+ return cached;
561
+ };
562
+ const activeCtx = () => ctxFor(opts.getActiveClient?.() ?? opts.client);
563
+ if (opts.startUrl && opts.startUrl !== "about:blank") {
564
+ await activeCtx().goto(opts.startUrl).catch(() => {
565
+ });
566
+ }
567
+ let completed = 0;
568
+ let prev = null;
569
+ for (let i = 0; i < target; i++) {
570
+ if (opts.shouldCancel?.()) {
571
+ return { completed, total };
572
+ }
573
+ const e = events[i];
574
+ await opts.shouldWait?.(i, target, e);
575
+ if (opts.shouldCancel?.()) {
576
+ return { completed, total };
577
+ }
578
+ opts.onProgress?.(i + 1, target, e, "start");
579
+ const ctx = activeCtx();
580
+ if (process.env.TRQ_DEBUG_REPLAY) {
581
+ console.log(`[replay] step ${i + 1}/${target} ${e.type} @ ${ctx.url()}`);
582
+ }
583
+ try {
584
+ const clientBefore = opts.getActiveClient?.() ?? opts.client;
585
+ switch (e.type) {
586
+ case "navigate":
587
+ await replayNavigate(e, ctx, prev);
588
+ break;
589
+ case "click":
590
+ await replayClick(e, ctx);
591
+ break;
592
+ case "input":
593
+ await replayInput(e, ctx);
594
+ break;
595
+ case "key":
596
+ await replayKey(e, ctx);
597
+ break;
598
+ case "assert":
599
+ await replayAssert(e, ctx);
600
+ break;
601
+ }
602
+ if (e.type === "click" && isAnchorClick(e)) {
603
+ const next = events[i + 1];
604
+ const expectedNextUrl = next && "url" in next ? next.url : void 0;
605
+ const nextSelectors = next && "selectors" in next ? next.selectors : void 0;
606
+ await waitForAnchorClickConsequence(
607
+ opts.getActiveClient,
608
+ opts.client,
609
+ ctxFor,
610
+ clientBefore,
611
+ expectedNextUrl,
612
+ nextSelectors
613
+ );
614
+ }
615
+ const settle = opts.slow ? 400 : e.type === "click" || e.type === "key" ? 60 : 20;
616
+ if (settle) await new Promise((r) => setTimeout(r, settle));
617
+ completed++;
618
+ prev = e;
619
+ opts.onProgress?.(i + 1, target, e, "ok");
620
+ } catch (err) {
621
+ const msg = err.message;
622
+ opts.onProgress?.(i + 1, target, e, "fail", msg);
623
+ if (opts.shouldPauseOnFail) {
624
+ const action = await opts.shouldPauseOnFail(i, target, e, msg);
625
+ if (action === "retry" || action === "skip") {
626
+ events = await opts.session.readEvents();
627
+ total = events.length;
628
+ target = opts.toStep != null ? Math.min(opts.toStep, total) : total;
629
+ if (action === "retry") {
630
+ const newIdx2 = events.findIndex((ev) => ev.id === e.id);
631
+ i = (newIdx2 === -1 ? i : newIdx2) - 1;
632
+ continue;
633
+ }
634
+ const newIdx = events.findIndex((ev) => ev.id === e.id);
635
+ if (newIdx !== -1) i = newIdx;
636
+ prev = events[i] ?? e;
637
+ continue;
638
+ }
639
+ }
640
+ return {
641
+ completed,
642
+ total,
643
+ failed: { step: i + 1, reason: msg, event: e }
644
+ };
645
+ }
646
+ }
647
+ return { completed, total };
648
+ }
649
+ var __dirname2, HELPERS_PATH, ANIMATION_KILL_CSS, INJECT_ANIMATION_KILL, MOUSE_BUTTON;
650
+ var init_replay = __esm({
651
+ "../core/src/replay.ts"() {
652
+ "use strict";
653
+ init_cdp();
654
+ init_config();
655
+ init_page_context();
656
+ __dirname2 = path3.dirname(fileURLToPath2(import.meta.url));
657
+ HELPERS_PATH = path3.join(__dirname2, "replay-helpers.js");
658
+ ANIMATION_KILL_CSS = `
659
+ *, *::before, *::after {
660
+ animation-duration: 0s !important;
661
+ animation-delay: 0s !important;
662
+ transition-duration: 0s !important;
663
+ transition-delay: 0s !important;
664
+ scroll-behavior: auto !important;
665
+ }
666
+ `;
667
+ INJECT_ANIMATION_KILL = `
668
+ (function () {
669
+ try {
670
+ const s = document.createElement('style');
671
+ s.setAttribute('data-trq', 'animkill');
672
+ s.textContent = ${JSON.stringify(ANIMATION_KILL_CSS)};
673
+ (document.head || document.documentElement).appendChild(s);
674
+ } catch (e) {}
675
+ })();
676
+ `;
677
+ MOUSE_BUTTON = {
678
+ left: "left",
679
+ right: "right",
680
+ middle: "middle"
681
+ };
682
+ }
683
+ });
684
+
685
+ // ../core/src/resume.ts
686
+ var resume_exports = {};
687
+ __export(resume_exports, {
688
+ runResume: () => runResume,
689
+ runResumeOnClient: () => runResumeOnClient
690
+ });
691
+ import puppeteer3 from "puppeteer";
692
+ import { promises as fs4 } from "node:fs";
693
+ import path4 from "node:path";
694
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
695
+ async function runResume(opts) {
696
+ const events = await opts.session.readEvents();
697
+ const total = events.length;
698
+ const priorStepCount = opts.session.manifest.stepCount;
699
+ const helpers = await fs4.readFile(HELPERS_PATH, "utf8");
700
+ const bootstrap = await fs4.readFile(BOOTSTRAP_PATH2, "utf8");
701
+ const config = await opts.session.readConfig();
702
+ const browser = await puppeteer3.launch({
703
+ headless: false,
704
+ defaultViewport: null,
705
+ args: ["--no-first-run", "--no-default-browser-check", "--start-maximized"]
706
+ });
707
+ const closed = new Promise(
708
+ (resolve) => browser.once("disconnected", () => resolve())
709
+ );
710
+ const clients = /* @__PURE__ */ new Set();
711
+ let mode = "replaying";
712
+ let newRecorded = 0;
713
+ const broadcastStepCount2 = async () => {
714
+ const n = opts.session.manifest.stepCount;
715
+ const expr = `window.__trqSetStep && window.__trqSetStep(${n})`;
716
+ await Promise.all(
717
+ Array.from(clients).map(
718
+ (c) => c.send("Runtime.evaluate", { expression: expr }).catch(() => {
719
+ })
720
+ )
721
+ );
722
+ };
723
+ const broadcastMode = async (m) => {
724
+ const expr = `window.__trqMode = ${JSON.stringify(m)};`;
725
+ await Promise.all(
726
+ Array.from(clients).map(
727
+ (c) => c.send("Runtime.evaluate", { expression: expr }).catch(() => {
728
+ })
729
+ )
730
+ );
731
+ };
732
+ const setupPage = async (page2) => {
733
+ const client2 = await page2.createCDPSession();
734
+ clients.add(client2);
735
+ client2.on("disconnected", () => clients.delete(client2));
736
+ await client2.send("Runtime.enable");
737
+ await client2.send("Page.enable");
738
+ await applyConfig(adaptCDPSession(client2), config);
739
+ await client2.send("Runtime.addBinding", { name: BINDING_NAME2 }).catch(() => {
740
+ });
741
+ await client2.send("Page.addScriptToEvaluateOnNewDocument", {
742
+ source: `window.__trqMode = window.__trqMode || ${JSON.stringify(mode)};`
743
+ });
744
+ await client2.send("Page.addScriptToEvaluateOnNewDocument", { source: helpers });
745
+ await client2.send("Page.addScriptToEvaluateOnNewDocument", { source: bootstrap });
746
+ await client2.send("Page.addScriptToEvaluateOnNewDocument", {
747
+ source: INJECT_ANIMATION_KILL
748
+ });
749
+ client2.on("Runtime.bindingCalled", async (event) => {
750
+ if (event.name !== BINDING_NAME2) return;
751
+ try {
752
+ const incoming = JSON.parse(event.payload);
753
+ if (incoming.__trq === "ready") {
754
+ await broadcastStepCount2();
755
+ return;
756
+ }
757
+ if (mode !== "recording") return;
758
+ const stamped = await opts.session.appendEvent(
759
+ incoming
760
+ );
761
+ newRecorded++;
762
+ opts.onRecordEvent?.(stamped);
763
+ await broadcastStepCount2();
764
+ } catch (err) {
765
+ console.error("\nFailed to handle event:", err.message);
766
+ }
767
+ });
768
+ await client2.send("Runtime.evaluate", {
769
+ expression: `window.__trqMode = window.__trqMode || ${JSON.stringify(mode)};`
770
+ }).catch(() => {
771
+ });
772
+ await client2.send("Runtime.evaluate", { expression: helpers }).catch(() => {
773
+ });
774
+ await client2.send("Runtime.evaluate", { expression: bootstrap }).catch(() => {
775
+ });
776
+ await client2.send("Runtime.evaluate", { expression: INJECT_ANIMATION_KILL }).catch(() => {
777
+ });
778
+ return client2;
779
+ };
780
+ browser.on("targetdestroyed", async (t) => {
781
+ if (t.type() !== "page") return;
782
+ try {
783
+ const remaining = (await browser.pages()).length;
784
+ if (remaining === 0) await browser.close();
785
+ } catch {
786
+ }
787
+ });
788
+ const onSigint = async () => {
789
+ try {
790
+ await browser.close();
791
+ } catch {
792
+ }
793
+ };
794
+ process.once("SIGINT", onSigint);
795
+ const pages = await browser.pages();
796
+ const page = pages[0] ?? await browser.newPage();
797
+ const client = await setupPage(page);
798
+ const ctx = puppeteerPageContext(page, adaptCDPSession(client));
799
+ let prev = null;
800
+ for (let i = 0; i < total; i++) {
801
+ const e = events[i];
802
+ opts.onReplayProgress?.(i + 1, total, e, "start");
803
+ try {
804
+ switch (e.type) {
805
+ case "navigate":
806
+ await replayNavigate(e, ctx, prev);
807
+ break;
808
+ case "click":
809
+ await replayClick(e, ctx);
810
+ break;
811
+ case "input":
812
+ await replayInput(e, ctx);
813
+ break;
814
+ case "key":
815
+ await replayKey(e, ctx);
816
+ break;
817
+ case "assert":
818
+ await replayAssert(e, ctx);
819
+ break;
820
+ }
821
+ if (e.type === "click" && isAnchorClick(e)) {
822
+ const next = events[i + 1];
823
+ const expectedNextUrl = next && "url" in next ? next.url : void 0;
824
+ const expectedArea = urlAreaOf(expectedNextUrl);
825
+ const start = Date.now();
826
+ let lastUrl = null;
827
+ let stableSince = start;
828
+ while (Date.now() - start < 2e4) {
829
+ let ready = false;
830
+ try {
831
+ const v = await ctx.evaluate(() => document.readyState);
832
+ ready = v === "complete";
833
+ } catch {
834
+ }
835
+ const u = ctx.url();
836
+ if (u !== lastUrl) {
837
+ lastUrl = u;
838
+ stableSince = Date.now();
839
+ }
840
+ const inArea = expectedArea ? urlInArea(u, expectedArea) : true;
841
+ if (ready && inArea && Date.now() - stableSince >= 1200) break;
842
+ await new Promise((r) => setTimeout(r, 150));
843
+ }
844
+ }
845
+ const settle = e.type === "click" || e.type === "key" ? 60 : 20;
846
+ await new Promise((r) => setTimeout(r, settle));
847
+ prev = e;
848
+ opts.onReplayProgress?.(i + 1, total, e, "ok");
849
+ } catch (err) {
850
+ const msg = err.message;
851
+ opts.onReplayProgress?.(i + 1, total, e, "fail", msg);
852
+ process.removeListener("SIGINT", onSigint);
853
+ await closed;
854
+ return {
855
+ replayed: i,
856
+ newRecorded,
857
+ failed: { step: i + 1, reason: msg }
858
+ };
859
+ }
860
+ }
861
+ mode = "recording";
862
+ await broadcastMode("recording");
863
+ await broadcastStepCount2();
864
+ opts.onRecordingStart?.();
865
+ await closed;
866
+ process.removeListener("SIGINT", onSigint);
867
+ return { replayed: total, newRecorded };
868
+ }
869
+ async function runResumeOnClient(opts) {
870
+ let events = await opts.session.readEvents();
871
+ let total = events.length;
872
+ const helpers = await fs4.readFile(HELPERS_PATH, "utf8");
873
+ const bootstrap = await fs4.readFile(BOOTSTRAP_PATH2, "utf8");
874
+ const config = await opts.session.readConfig();
875
+ const client = opts.client;
876
+ let mode = "replaying";
877
+ const broadcastStepCount2 = async () => {
878
+ const n = opts.session.manifest.stepCount;
879
+ await client.send("Runtime.evaluate", {
880
+ expression: `window.__trqSetStep && window.__trqSetStep(${n})`
881
+ }).catch(() => {
882
+ });
883
+ };
884
+ const broadcastMode = async (m) => {
885
+ await client.send("Runtime.evaluate", {
886
+ expression: `window.__trqMode = ${JSON.stringify(m)};`
887
+ }).catch(() => {
888
+ });
889
+ };
890
+ await client.send("Runtime.enable");
891
+ await client.send("Page.enable");
892
+ await applyConfig(client, config);
893
+ await client.send("Runtime.addBinding", { name: BINDING_NAME2 }).catch(() => {
894
+ });
895
+ await client.send("Page.addScriptToEvaluateOnNewDocument", {
896
+ source: `window.__trqMode = window.__trqMode || ${JSON.stringify(mode)};`
897
+ });
898
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: helpers });
899
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: bootstrap });
900
+ if (opts.killAnimations !== false) {
901
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: INJECT_ANIMATION_KILL });
902
+ }
903
+ client.on("Runtime.bindingCalled", async (raw) => {
904
+ const event = raw;
905
+ if (event.name !== BINDING_NAME2) return;
906
+ try {
907
+ const incoming = JSON.parse(event.payload);
908
+ if (incoming.__trq === "ready") {
909
+ await broadcastStepCount2();
910
+ return;
911
+ }
912
+ if (mode !== "recording") return;
913
+ const stamped = await opts.session.appendEvent(incoming);
914
+ opts.onRecordEvent?.(stamped);
915
+ await broadcastStepCount2();
916
+ } catch (err) {
917
+ console.error("\nFailed to handle event:", err.message);
918
+ }
919
+ });
920
+ await client.send("Runtime.evaluate", {
921
+ expression: `window.__trqMode = window.__trqMode || ${JSON.stringify(mode)};`
922
+ }).catch(() => {
923
+ });
924
+ await client.send("Runtime.evaluate", { expression: helpers }).catch(() => {
925
+ });
926
+ await client.send("Runtime.evaluate", { expression: bootstrap }).catch(() => {
927
+ });
928
+ if (opts.killAnimations !== false) {
929
+ await client.send("Runtime.evaluate", { expression: INJECT_ANIMATION_KILL }).catch(() => {
930
+ });
931
+ }
932
+ const ctxCache = /* @__PURE__ */ new Map();
933
+ const ctxFor = (c) => {
934
+ let cached = ctxCache.get(c);
935
+ if (!cached) {
936
+ cached = cdpPageContext(c);
937
+ ctxCache.set(c, cached);
938
+ }
939
+ return cached;
940
+ };
941
+ const activeCtx = () => ctxFor(opts.getActiveClient?.() ?? client);
942
+ if (opts.startUrl && opts.startUrl !== "about:blank") {
943
+ await activeCtx().goto(opts.startUrl).catch(() => {
944
+ });
945
+ }
946
+ let prev = null;
947
+ for (let i = 0; i < total; i++) {
948
+ if (opts.shouldCancel?.()) return { replayed: i };
949
+ const e = events[i];
950
+ opts.onReplayProgress?.(i + 1, total, e, "start");
951
+ const ctx = activeCtx();
952
+ try {
953
+ const clientBefore = opts.getActiveClient?.() ?? client;
954
+ switch (e.type) {
955
+ case "navigate":
956
+ await replayNavigate(e, ctx, prev);
957
+ break;
958
+ case "click":
959
+ await replayClick(e, ctx);
960
+ break;
961
+ case "input":
962
+ await replayInput(e, ctx);
963
+ break;
964
+ case "key":
965
+ await replayKey(e, ctx);
966
+ break;
967
+ case "assert":
968
+ await replayAssert(e, ctx);
969
+ break;
970
+ }
971
+ if (e.type === "click" && isAnchorClick(e)) {
972
+ const next = events[i + 1];
973
+ const expectedNextUrl = next && "url" in next ? next.url : void 0;
974
+ const nextSelectors = next && "selectors" in next ? next.selectors : void 0;
975
+ await waitForAnchorClickConsequence(
976
+ opts.getActiveClient,
977
+ client,
978
+ ctxFor,
979
+ clientBefore,
980
+ expectedNextUrl,
981
+ nextSelectors
982
+ );
983
+ }
984
+ const settle = e.type === "click" || e.type === "key" ? 60 : 20;
985
+ await new Promise((r) => setTimeout(r, settle));
986
+ prev = e;
987
+ opts.onReplayProgress?.(i + 1, total, e, "ok");
988
+ } catch (err) {
989
+ const msg = err.message;
990
+ opts.onReplayProgress?.(i + 1, total, e, "fail", msg);
991
+ if (opts.shouldPauseOnFail) {
992
+ const action = await opts.shouldPauseOnFail(i, total, e, msg);
993
+ if (action === "retry" || action === "skip") {
994
+ events = await opts.session.readEvents();
995
+ total = events.length;
996
+ if (action === "retry") {
997
+ const newIdx2 = events.findIndex((ev) => ev.id === e.id);
998
+ i = (newIdx2 === -1 ? i : newIdx2) - 1;
999
+ continue;
1000
+ }
1001
+ const newIdx = events.findIndex((ev) => ev.id === e.id);
1002
+ if (newIdx !== -1) i = newIdx;
1003
+ prev = events[i] ?? e;
1004
+ continue;
1005
+ }
1006
+ }
1007
+ return { replayed: i, failed: { step: i + 1, reason: msg } };
1008
+ }
1009
+ }
1010
+ mode = "recording";
1011
+ await broadcastMode("recording");
1012
+ await broadcastStepCount2();
1013
+ opts.onRecordingStart?.();
1014
+ return { replayed: total };
1015
+ }
1016
+ var __dirname3, BOOTSTRAP_PATH2, BINDING_NAME2;
1017
+ var init_resume = __esm({
1018
+ "../core/src/resume.ts"() {
1019
+ "use strict";
1020
+ init_cdp();
1021
+ init_config();
1022
+ init_page_context();
1023
+ init_replay();
1024
+ __dirname3 = path4.dirname(fileURLToPath3(import.meta.url));
1025
+ BOOTSTRAP_PATH2 = path4.join(__dirname3, "bootstrap.js");
1026
+ BINDING_NAME2 = "__trqEmit";
1027
+ }
1028
+ });
1029
+
1030
+ // ../core/src/cli.ts
1031
+ import path5 from "node:path";
1032
+
1033
+ // ../core/src/agent.ts
1034
+ init_cdp();
1035
+ import puppeteer from "puppeteer";
1036
+ import { promises as fs } from "node:fs";
1037
+ import path from "node:path";
1038
+ import { fileURLToPath } from "node:url";
1039
+
1040
+ // ../core/src/recording.ts
1041
+ init_config();
1042
+ var BINDING_NAME = "__trqEmit";
1043
+ var isRecordableUrl = (url) => !!url && url !== "about:blank" && !url.startsWith("chrome://") && !url.startsWith("chrome-extension://") && !url.startsWith("devtools://") && !url.startsWith("data:");
1044
+ async function broadcastStepCount(ctx) {
1045
+ const n = ctx.session.manifest.stepCount;
1046
+ const expr = `window.__trqSetStep && window.__trqSetStep(${n})`;
1047
+ await Promise.all(
1048
+ Array.from(ctx.clients).map(
1049
+ (c) => c.send("Runtime.evaluate", { expression: expr }).catch(() => {
1050
+ })
1051
+ )
1052
+ );
1053
+ }
1054
+ async function recordNav(ctx, url, source) {
1055
+ if (!isRecordableUrl(url)) return;
1056
+ if (source === "load" && isAnchorClickPrev(ctx.lastEmittedEvent)) return;
1057
+ const stamped = await ctx.session.appendEvent({
1058
+ type: "navigate",
1059
+ timestamp: Date.now(),
1060
+ url,
1061
+ frame: "main",
1062
+ source
1063
+ });
1064
+ ctx.lastEmittedEvent = stamped;
1065
+ ctx.onEvent?.(stamped);
1066
+ await broadcastStepCount(ctx);
1067
+ }
1068
+ function isAnchorClickPrev(prev) {
1069
+ if (!prev || prev.type !== "click") return false;
1070
+ const t = prev.target;
1071
+ return t.tagName === "A" || t.attributes?.target === "_blank" || !!t.attributes?.href;
1072
+ }
1073
+ async function setupRecordingOnClient(client, ctx) {
1074
+ ctx.clients.add(client);
1075
+ client.on("disconnected", () => ctx.clients.delete(client));
1076
+ await client.send("Runtime.enable");
1077
+ await client.send("Page.enable");
1078
+ if (ctx.config) {
1079
+ await applyConfig(client, ctx.config);
1080
+ }
1081
+ await client.send("Runtime.addBinding", { name: BINDING_NAME }).catch(() => {
1082
+ });
1083
+ await client.send("Page.addScriptToEvaluateOnNewDocument", { source: ctx.bootstrap });
1084
+ let mainFrameId = null;
1085
+ try {
1086
+ const tree = await client.send("Page.getFrameTree");
1087
+ mainFrameId = tree.frameTree.frame.id;
1088
+ } catch {
1089
+ }
1090
+ let lastNavUrl = null;
1091
+ client.on("Runtime.bindingCalled", async (raw) => {
1092
+ const event = raw;
1093
+ if (event.name !== BINDING_NAME) return;
1094
+ try {
1095
+ const incoming = JSON.parse(event.payload);
1096
+ if (incoming.__trq === "ready") {
1097
+ await broadcastStepCount(ctx);
1098
+ return;
1099
+ }
1100
+ const stamped = await ctx.session.appendEvent(
1101
+ incoming
1102
+ );
1103
+ ctx.lastEmittedEvent = stamped;
1104
+ ctx.onEvent?.(stamped);
1105
+ await broadcastStepCount(ctx);
1106
+ } catch (err) {
1107
+ console.error("\nFailed to handle event:", err.message);
1108
+ }
1109
+ });
1110
+ client.on("Page.frameNavigated", async (raw) => {
1111
+ const e = raw;
1112
+ if (e.frame.parentId) return;
1113
+ const url = e.frame.url;
1114
+ if (!isRecordableUrl(url)) return;
1115
+ if (url === lastNavUrl) return;
1116
+ lastNavUrl = url;
1117
+ await recordNav(ctx, url, "load");
1118
+ });
1119
+ client.on("Page.navigatedWithinDocument", async (raw) => {
1120
+ const e = raw;
1121
+ if (mainFrameId && e.frameId !== mainFrameId) return;
1122
+ if (!isRecordableUrl(e.url)) return;
1123
+ if (e.url === lastNavUrl) return;
1124
+ lastNavUrl = e.url;
1125
+ await recordNav(ctx, e.url, "in-document");
1126
+ });
1127
+ await client.send("Runtime.evaluate", { expression: ctx.bootstrap }).catch(() => {
1128
+ });
1129
+ await broadcastStepCount(ctx);
1130
+ }
1131
+
1132
+ // ../core/src/agent.ts
1133
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
1134
+ var BOOTSTRAP_PATH = path.join(__dirname, "bootstrap.js");
1135
+ async function runAgent(opts) {
1136
+ const bootstrap = await fs.readFile(BOOTSTRAP_PATH, "utf8");
1137
+ const config = await opts.session.readConfig();
1138
+ const browser = await puppeteer.launch({
1139
+ headless: false,
1140
+ defaultViewport: null,
1141
+ args: ["--no-first-run", "--no-default-browser-check", "--start-maximized"]
1142
+ });
1143
+ const closed = new Promise((resolve) => browser.once("disconnected", () => resolve()));
1144
+ const ctx = {
1145
+ session: opts.session,
1146
+ bootstrap,
1147
+ clients: /* @__PURE__ */ new Set(),
1148
+ onEvent: opts.onEvent,
1149
+ config
1150
+ };
1151
+ const setupPage = async (page) => {
1152
+ try {
1153
+ const session = await page.createCDPSession();
1154
+ const client = adaptCDPSession(session);
1155
+ await setupRecordingOnClient(client, ctx);
1156
+ } catch {
1157
+ }
1158
+ };
1159
+ const pages = await browser.pages();
1160
+ for (const p of pages) await setupPage(p);
1161
+ browser.on("targetcreated", async (target) => {
1162
+ if (target.type() !== "page") return;
1163
+ const page = await target.page();
1164
+ if (page) await setupPage(page);
1165
+ });
1166
+ browser.on("targetdestroyed", async (target) => {
1167
+ if (target.type() !== "page") return;
1168
+ try {
1169
+ const remaining = (await browser.pages()).length;
1170
+ if (remaining === 0) await browser.close();
1171
+ } catch {
1172
+ }
1173
+ });
1174
+ const onSigint = async () => {
1175
+ try {
1176
+ await browser.close();
1177
+ } catch {
1178
+ }
1179
+ };
1180
+ process.once("SIGINT", onSigint);
1181
+ if (opts.startUrl && opts.startUrl !== "about:blank" && pages[0]) {
1182
+ await pages[0].goto(opts.startUrl).catch(() => {
1183
+ });
1184
+ }
1185
+ await closed;
1186
+ process.removeListener("SIGINT", onSigint);
1187
+ }
1188
+
1189
+ // ../core/src/storage.ts
1190
+ import { promises as fs2 } from "node:fs";
1191
+ import { existsSync } from "node:fs";
1192
+ import path2 from "node:path";
1193
+ var Session = class _Session {
1194
+ constructor(dir, manifest) {
1195
+ this.dir = dir;
1196
+ this.manifest = manifest;
1197
+ }
1198
+ writeChain = Promise.resolve();
1199
+ static async create(rootDir, name, initialUrl) {
1200
+ const dir = path2.join(rootDir, name);
1201
+ if (existsSync(dir)) {
1202
+ throw new Error(`Session already exists: ${name}`);
1203
+ }
1204
+ await fs2.mkdir(dir, { recursive: true });
1205
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1206
+ const manifest = {
1207
+ id: `ses_${randomId()}`,
1208
+ name,
1209
+ createdAt: now,
1210
+ updatedAt: now,
1211
+ stepCount: 0,
1212
+ initialUrl: initialUrl ?? null,
1213
+ lastUrl: initialUrl ?? null
1214
+ };
1215
+ await fs2.writeFile(path2.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2));
1216
+ await fs2.writeFile(path2.join(dir, "events.jsonl"), "");
1217
+ return new _Session(dir, manifest);
1218
+ }
1219
+ static async load(rootDir, name) {
1220
+ const dir = path2.join(rootDir, name);
1221
+ const raw = await fs2.readFile(path2.join(dir, "manifest.json"), "utf8");
1222
+ return new _Session(dir, JSON.parse(raw));
1223
+ }
1224
+ static async list(rootDir) {
1225
+ if (!existsSync(rootDir)) return [];
1226
+ const names = await fs2.readdir(rootDir);
1227
+ const out = [];
1228
+ for (const name of names) {
1229
+ const mp = path2.join(rootDir, name, "manifest.json");
1230
+ if (!existsSync(mp)) continue;
1231
+ try {
1232
+ out.push(JSON.parse(await fs2.readFile(mp, "utf8")));
1233
+ } catch {
1234
+ }
1235
+ }
1236
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1237
+ }
1238
+ // Builds an event ID from its 1-based position. Used by all mutations so
1239
+ // IDs stay sequential — after a delete or insert, every event keeps a
1240
+ // position-matching id (evt_0001..evt_000N with no gaps).
1241
+ idFor(pos1Based) {
1242
+ return `evt_${String(pos1Based).padStart(4, "0")}`;
1243
+ }
1244
+ appendEvent(event) {
1245
+ const next = this.writeChain.then(async () => {
1246
+ this.manifest.stepCount++;
1247
+ const id = this.idFor(this.manifest.stepCount);
1248
+ const stamped = { ...event, id };
1249
+ await fs2.appendFile(path2.join(this.dir, "events.jsonl"), JSON.stringify(stamped) + "\n");
1250
+ this.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1251
+ if (this.manifest.initialUrl === null) this.manifest.initialUrl = event.url;
1252
+ this.manifest.lastUrl = event.url;
1253
+ await this.flushManifest();
1254
+ return stamped;
1255
+ });
1256
+ this.writeChain = next.catch(() => {
1257
+ });
1258
+ return next;
1259
+ }
1260
+ // Replace fields on an existing event (matched by id). Selectors, value,
1261
+ // assertion spec, etc. The id itself and the event's index are preserved.
1262
+ // Use this for fixing brittle selectors, correcting an input value, etc.
1263
+ async replaceEvent(id, patch) {
1264
+ const next = this.writeChain.then(async () => {
1265
+ const events = await this.readEvents();
1266
+ const idx = events.findIndex((e) => e.id === id);
1267
+ if (idx === -1) throw new Error(`No event with id ${id}`);
1268
+ const merged = { ...events[idx], ...patch, id };
1269
+ events[idx] = merged;
1270
+ await this.writeAllEvents(events);
1271
+ this.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1272
+ await this.flushManifest();
1273
+ return merged;
1274
+ });
1275
+ this.writeChain = next.catch(() => {
1276
+ });
1277
+ return next;
1278
+ }
1279
+ // Delete an event by id. Subsequent events are renumbered so IDs stay
1280
+ // sequential (evt_0001..evt_000N with no gaps). stepCount = new length.
1281
+ async removeEvent(id) {
1282
+ const next = this.writeChain.then(async () => {
1283
+ const events = await this.readEvents();
1284
+ const idx = events.findIndex((e) => e.id === id);
1285
+ if (idx === -1) throw new Error(`No event with id ${id}`);
1286
+ events.splice(idx, 1);
1287
+ this.renumber(events);
1288
+ this.manifest.stepCount = events.length;
1289
+ this.manifest.lastUrl = events[events.length - 1]?.url ?? this.manifest.lastUrl;
1290
+ this.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1291
+ await this.writeAllEvents(events);
1292
+ await this.flushManifest();
1293
+ });
1294
+ this.writeChain = next.catch(() => {
1295
+ });
1296
+ return next;
1297
+ }
1298
+ // Insert a new event at the given 0-based index (so 0 means "before first",
1299
+ // events.length means "at end"). The whole event list is renumbered to
1300
+ // stay sequential. Returns the stamped event with its post-renumber id.
1301
+ async insertEventAt(idx, event) {
1302
+ const next = this.writeChain.then(async () => {
1303
+ const events = await this.readEvents();
1304
+ const safeIdx = Math.max(0, Math.min(idx, events.length));
1305
+ const stamped = { ...event, id: "evt_xxxx" };
1306
+ events.splice(safeIdx, 0, stamped);
1307
+ this.renumber(events);
1308
+ this.manifest.stepCount = events.length;
1309
+ if (this.manifest.initialUrl === null) this.manifest.initialUrl = event.url;
1310
+ this.manifest.lastUrl = events[events.length - 1].url;
1311
+ this.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1312
+ await this.writeAllEvents(events);
1313
+ await this.flushManifest();
1314
+ return events[safeIdx];
1315
+ });
1316
+ this.writeChain = next.catch(() => {
1317
+ });
1318
+ return next;
1319
+ }
1320
+ renumber(events) {
1321
+ for (let i = 0; i < events.length; i++) {
1322
+ events[i].id = this.idFor(i + 1);
1323
+ }
1324
+ }
1325
+ // Create a new session containing events 1..throughStep (inclusive,
1326
+ // 1-based) from this session. Copies config.json. Useful for forking a
1327
+ // recording at a known-good step to retry a divergent path.
1328
+ async branch(rootDir, newName, throughStep) {
1329
+ const events = await this.readEvents();
1330
+ const target = Math.max(0, Math.min(throughStep, events.length));
1331
+ const sliced = events.slice(0, target);
1332
+ const branched = await _Session.create(rootDir, newName, this.manifest.initialUrl);
1333
+ const lines = [];
1334
+ for (let i = 0; i < sliced.length; i++) {
1335
+ const id = `evt_${String(i + 1).padStart(4, "0")}`;
1336
+ const e = { ...sliced[i], id };
1337
+ lines.push(JSON.stringify(e));
1338
+ }
1339
+ await fs2.writeFile(path2.join(branched.dir, "events.jsonl"), lines.length ? lines.join("\n") + "\n" : "");
1340
+ branched.manifest.stepCount = sliced.length;
1341
+ branched.manifest.lastUrl = sliced[sliced.length - 1]?.url ?? branched.manifest.initialUrl;
1342
+ branched.manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1343
+ await branched.flushManifest();
1344
+ const cfg = await this.readConfig();
1345
+ if (Object.keys(cfg).length > 0) await branched.writeConfig(cfg);
1346
+ return branched;
1347
+ }
1348
+ async writeAllEvents(events) {
1349
+ const body = events.length ? events.map((e) => JSON.stringify(e)).join("\n") + "\n" : "";
1350
+ await fs2.writeFile(path2.join(this.dir, "events.jsonl"), body);
1351
+ }
1352
+ async flushManifest() {
1353
+ await fs2.writeFile(
1354
+ path2.join(this.dir, "manifest.json"),
1355
+ JSON.stringify(this.manifest, null, 2)
1356
+ );
1357
+ }
1358
+ async readEvents() {
1359
+ const raw = await fs2.readFile(path2.join(this.dir, "events.jsonl"), "utf8");
1360
+ return raw.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => JSON.parse(l));
1361
+ }
1362
+ async readConfig() {
1363
+ const p = path2.join(this.dir, "config.json");
1364
+ if (!existsSync(p)) return {};
1365
+ try {
1366
+ return JSON.parse(await fs2.readFile(p, "utf8"));
1367
+ } catch {
1368
+ return {};
1369
+ }
1370
+ }
1371
+ async writeConfig(cfg) {
1372
+ await fs2.writeFile(
1373
+ path2.join(this.dir, "config.json"),
1374
+ JSON.stringify(cfg, null, 2)
1375
+ );
1376
+ }
1377
+ };
1378
+ function randomId() {
1379
+ return Math.random().toString(36).slice(2, 8) + Date.now().toString(36).slice(-4);
1380
+ }
1381
+
1382
+ // ../core/src/cli.ts
1383
+ var SESSIONS_DIR = process.env.TRQ_SESSIONS_DIR ? path5.resolve(process.env.TRQ_SESSIONS_DIR) : path5.join(process.cwd(), "sessions");
1384
+ var argv = process.argv.slice(2);
1385
+ var cmd = argv[0];
1386
+ async function main() {
1387
+ switch (cmd) {
1388
+ case "new":
1389
+ return cmdNew(argv.slice(1));
1390
+ case "list":
1391
+ case "ls":
1392
+ return cmdList();
1393
+ case "show":
1394
+ return cmdShow(argv.slice(1));
1395
+ case "play":
1396
+ return cmdPlay(argv.slice(1));
1397
+ case "resume":
1398
+ return cmdResume(argv.slice(1));
1399
+ case "config":
1400
+ return cmdConfig(argv.slice(1));
1401
+ case "studio":
1402
+ return cmdStudio(argv.slice(1));
1403
+ case "help":
1404
+ case "-h":
1405
+ case "--help":
1406
+ case void 0:
1407
+ return printUsage(0);
1408
+ default:
1409
+ console.error(`Unknown command: ${cmd}
1410
+ `);
1411
+ printUsage(1);
1412
+ }
1413
+ }
1414
+ function printUsage(code) {
1415
+ console.log(`trq \u2014 record and replay web sessions
1416
+
1417
+ Usage:
1418
+ trq new <name> [--url <url>] Create and start recording a new session
1419
+ trq list List all sessions
1420
+ trq show <name> Show recorded events for a session
1421
+ trq play <name> [--to N] [--slow]
1422
+ Replay a session end-to-end (or up to step N)
1423
+ trq resume <name> Replay all prior steps, then continue recording
1424
+ trq config show <name> Print the session's config.json
1425
+ trq config set <name> <key> <value>
1426
+ Set a single config field (dot.notation, JSON value)
1427
+ trq config unset <name> <key> Remove a config field
1428
+ trq studio Launch the trq Studio desktop GUI
1429
+
1430
+ Sessions are stored in: ${SESSIONS_DIR}
1431
+ `);
1432
+ process.exit(code);
1433
+ }
1434
+ async function cmdNew(args) {
1435
+ const name = args[0];
1436
+ if (!name || name.startsWith("--")) {
1437
+ console.error("Usage: trq new <name> [--url <url>]");
1438
+ process.exit(1);
1439
+ }
1440
+ const urlIdx = args.indexOf("--url");
1441
+ const startUrl = urlIdx >= 0 ? args[urlIdx + 1] : "about:blank";
1442
+ const session = await Session.create(SESSIONS_DIR, name);
1443
+ console.log(`\u25B8 Session created: ${name} (${session.manifest.id})`);
1444
+ console.log(`\u25B8 Recording. Close the Chrome window to save & exit.
1445
+ `);
1446
+ let count = 0;
1447
+ await runAgent({
1448
+ session,
1449
+ startUrl,
1450
+ onEvent: (e) => {
1451
+ count++;
1452
+ const label = describeEvent(e, 40);
1453
+ process.stdout.write(
1454
+ `\r ${count} step${count === 1 ? "" : "s"} \xB7 last: ${label.padEnd(60)}`
1455
+ );
1456
+ }
1457
+ });
1458
+ console.log(`
1459
+
1460
+ \u25B8 Saved ${session.manifest.stepCount} steps to ${session.dir}`);
1461
+ if (session.manifest.lastUrl) {
1462
+ console.log(`\u25B8 Last URL: ${session.manifest.lastUrl}`);
1463
+ }
1464
+ }
1465
+ async function cmdList() {
1466
+ const sessions = await Session.list(SESSIONS_DIR);
1467
+ if (sessions.length === 0) {
1468
+ console.log("No sessions yet. Create one with: trq new <name>");
1469
+ return;
1470
+ }
1471
+ const nameW = Math.max(4, ...sessions.map((s) => s.name.length));
1472
+ console.log(`${"NAME".padEnd(nameW + 2)}STEPS UPDATED LAST URL`);
1473
+ for (const s of sessions) {
1474
+ console.log(
1475
+ `${s.name.padEnd(nameW + 2)}${String(s.stepCount).padEnd(7)}${s.updatedAt} ${s.lastUrl ?? ""}`
1476
+ );
1477
+ }
1478
+ }
1479
+ async function cmdShow(args) {
1480
+ const name = args[0];
1481
+ if (!name) {
1482
+ console.error("Usage: trq show <name>");
1483
+ process.exit(1);
1484
+ }
1485
+ const session = await Session.load(SESSIONS_DIR, name);
1486
+ const events = await session.readEvents();
1487
+ console.log(`${session.manifest.id} ${session.manifest.name} ${events.length} steps
1488
+ `);
1489
+ events.forEach((e, i) => {
1490
+ const idx = String(i + 1).padStart(3);
1491
+ console.log(` ${idx} ${describeEvent(e, 60).padEnd(64)}`);
1492
+ });
1493
+ if (events.length > 0) {
1494
+ console.log(`
1495
+ Last URL: ${session.manifest.lastUrl ?? "(none)"}`);
1496
+ }
1497
+ }
1498
+ async function cmdPlay(args) {
1499
+ const name = args[0];
1500
+ if (!name || name.startsWith("--")) {
1501
+ console.error("Usage: trq play <name> [--to N] [--slow]");
1502
+ process.exit(1);
1503
+ }
1504
+ const toIdx = args.indexOf("--to");
1505
+ const toStep = toIdx >= 0 ? parseInt(args[toIdx + 1] ?? "", 10) : void 0;
1506
+ const slow = args.includes("--slow");
1507
+ const session = await Session.load(SESSIONS_DIR, name);
1508
+ const events = await session.readEvents();
1509
+ const total = toStep != null ? Math.min(toStep, events.length) : events.length;
1510
+ console.log(`\u25B8 Replaying ${session.manifest.name} \u2014 ${total}/${events.length} steps`);
1511
+ const { runReplay: runReplay2 } = await Promise.resolve().then(() => (init_replay(), replay_exports));
1512
+ const result = await runReplay2({
1513
+ session,
1514
+ toStep,
1515
+ slow,
1516
+ onProgress: (idx, tot, e, status, detail) => {
1517
+ const label = describeEvent(e, 50);
1518
+ const head = ` ${String(idx).padStart(3)}/${tot}`;
1519
+ if (status === "start") {
1520
+ process.stdout.write(`\r${head} \u2026 ${label.padEnd(60)}`);
1521
+ } else if (status === "ok") {
1522
+ process.stdout.write(`\r${head} \u2713 ${label.padEnd(60)}
1523
+ `);
1524
+ } else if (status === "fail") {
1525
+ process.stdout.write(`\r${head} \u2717 ${label.padEnd(60)}
1526
+ `);
1527
+ if (detail) console.error(` ${detail}`);
1528
+ }
1529
+ }
1530
+ });
1531
+ if (result.failed) {
1532
+ console.error(`
1533
+ \u2717 Replay failed at step ${result.failed.step}: ${result.failed.reason}`);
1534
+ console.error(" Browser left open for inspection. Close it to exit.");
1535
+ await new Promise((r) => result.browser.once("disconnected", () => r()));
1536
+ process.exit(1);
1537
+ } else {
1538
+ console.log(`
1539
+ \u2713 Replay complete (${result.completed}/${result.total} steps).`);
1540
+ console.log(" Browser left open. Close it to exit.");
1541
+ await new Promise((r) => result.browser.once("disconnected", () => r()));
1542
+ }
1543
+ }
1544
+ async function cmdResume(args) {
1545
+ const name = args[0];
1546
+ if (!name) {
1547
+ console.error("Usage: trq resume <name>");
1548
+ process.exit(1);
1549
+ }
1550
+ const session = await Session.load(SESSIONS_DIR, name);
1551
+ const events = await session.readEvents();
1552
+ console.log(`\u25B8 Resuming ${session.manifest.name} \u2014 ${events.length} prior step${events.length === 1 ? "" : "s"}`);
1553
+ const { runResume: runResume2 } = await Promise.resolve().then(() => (init_resume(), resume_exports));
1554
+ const result = await runResume2({
1555
+ session,
1556
+ onReplayProgress: (idx, tot, e, status, detail) => {
1557
+ const label = describeEvent(e, 50);
1558
+ const head = ` ${String(idx).padStart(3)}/${tot}`;
1559
+ if (status === "start") {
1560
+ process.stdout.write(`\r${head} \u2026 ${label.padEnd(60)}`);
1561
+ } else if (status === "ok") {
1562
+ process.stdout.write(`\r${head} \u2713 ${label.padEnd(60)}
1563
+ `);
1564
+ } else if (status === "fail") {
1565
+ process.stdout.write(`\r${head} \u2717 ${label.padEnd(60)}
1566
+ `);
1567
+ if (detail) console.error(` ${detail}`);
1568
+ }
1569
+ },
1570
+ onRecordingStart: () => {
1571
+ console.log(`
1572
+ \u25B8 Replay complete. Now recording from step ${events.length + 1}.`);
1573
+ console.log(` Close the Chrome window when done.
1574
+ `);
1575
+ },
1576
+ onRecordEvent: (e) => {
1577
+ const label = describeEvent(e, 40);
1578
+ process.stdout.write(
1579
+ `\r ${session.manifest.stepCount} steps total \xB7 last: ${label.padEnd(50)}`
1580
+ );
1581
+ }
1582
+ });
1583
+ console.log("");
1584
+ if (result.failed) {
1585
+ console.error(`
1586
+ \u2717 Replay failed at step ${result.failed.step}: ${result.failed.reason}`);
1587
+ console.error(` ${result.replayed}/${events.length} steps replayed before failure.`);
1588
+ process.exit(1);
1589
+ }
1590
+ console.log(`
1591
+ \u25B8 Saved ${session.manifest.stepCount} total steps (${result.newRecorded} new).`);
1592
+ }
1593
+ function describeEvent(e, max) {
1594
+ switch (e.type) {
1595
+ case "click": {
1596
+ const text = e.target.textContent || e.selectors[0]?.[0] || "?";
1597
+ return `click "${text.slice(0, max)}"`;
1598
+ }
1599
+ case "input": {
1600
+ const label = e.target.attributes["name"] || e.target.attributes["placeholder"] || e.selectors[0]?.[0] || "?";
1601
+ const valDisplay = e.isPassword ? `<password length=${e.value.length}>` : `"${e.value.slice(0, max)}"`;
1602
+ return `input ${label.slice(0, 24)} = ${valDisplay}`;
1603
+ }
1604
+ case "key": {
1605
+ const where = e.target.tagName.toLowerCase();
1606
+ return `key ${e.key}${e.modifiers.length ? " (" + e.modifiers.join("+") + ")" : ""} on <${where}>`;
1607
+ }
1608
+ case "navigate": {
1609
+ const arrow = e.source === "in-document" ? "\u2192" : "\u21D2";
1610
+ return `navigate ${arrow} ${e.url.slice(0, max)}`;
1611
+ }
1612
+ case "assert": {
1613
+ const tag = e.target.tagName.toLowerCase();
1614
+ switch (e.assertion.kind) {
1615
+ case "visible":
1616
+ return `assert visible: <${tag}>`;
1617
+ case "text-equals":
1618
+ return `assert <${tag}> text == "${e.assertion.text.slice(0, max)}"`;
1619
+ case "value-equals":
1620
+ return `assert <${tag}> value == "${e.assertion.value.slice(0, max)}"`;
1621
+ }
1622
+ }
1623
+ }
1624
+ }
1625
+ async function cmdConfig(args) {
1626
+ const sub = args[0];
1627
+ const name = args[1];
1628
+ if (!sub || !name) {
1629
+ console.error("Usage:\n trq config show <name>\n trq config set <name> <key> <value>\n trq config unset <name> <key>");
1630
+ process.exit(1);
1631
+ }
1632
+ const session = await Session.load(SESSIONS_DIR, name);
1633
+ if (sub === "show") {
1634
+ console.log(JSON.stringify(await session.readConfig(), null, 2));
1635
+ return;
1636
+ }
1637
+ const key = args[2];
1638
+ if (!key) {
1639
+ console.error(`Usage: trq config ${sub} ${name} <key>${sub === "set" ? " <value>" : ""}`);
1640
+ process.exit(1);
1641
+ }
1642
+ const cfg = await session.readConfig();
1643
+ if (sub === "unset") {
1644
+ unsetPath(cfg, key);
1645
+ await session.writeConfig(cfg);
1646
+ console.log(`\u2713 unset ${key} in ${name}/config.json`);
1647
+ return;
1648
+ }
1649
+ if (sub === "set") {
1650
+ const rawValue = args.slice(3).join(" ");
1651
+ if (rawValue === "") {
1652
+ console.error("Usage: trq config set <name> <key> <value>");
1653
+ process.exit(1);
1654
+ }
1655
+ let value;
1656
+ try {
1657
+ value = JSON.parse(rawValue);
1658
+ } catch {
1659
+ value = rawValue;
1660
+ }
1661
+ setPath(cfg, key, value);
1662
+ await session.writeConfig(cfg);
1663
+ console.log(`\u2713 set ${key} = ${JSON.stringify(value)} in ${name}/config.json`);
1664
+ return;
1665
+ }
1666
+ console.error(`Unknown config subcommand: ${sub}`);
1667
+ process.exit(1);
1668
+ }
1669
+ async function cmdStudio(args) {
1670
+ const appDir = process.env.TRQ_STUDIO_PATH;
1671
+ if (!appDir) {
1672
+ console.error(
1673
+ "Error: TRQ_STUDIO_PATH is not set.\n This usually means the studio bundle is missing from the install.\n If running from a global install, try `npm install -g trq` again."
1674
+ );
1675
+ process.exit(1);
1676
+ }
1677
+ const { spawn } = await import("node:child_process");
1678
+ const { createRequire } = await import("node:module");
1679
+ const cliBin = process.env.npm_execpath ?? process.argv[1];
1680
+ const requireFromCli = createRequire(cliBin);
1681
+ let electronBin;
1682
+ try {
1683
+ electronBin = requireFromCli("electron");
1684
+ } catch (err) {
1685
+ console.error(
1686
+ `Error: failed to resolve \`electron\`.
1687
+ ${err.message}
1688
+ Try \`npm install -g trq\` (the install should pull electron's binary).`
1689
+ );
1690
+ process.exit(1);
1691
+ }
1692
+ const proc = spawn(electronBin, [appDir, ...args], {
1693
+ stdio: "inherit",
1694
+ env: process.env
1695
+ });
1696
+ proc.on("exit", (code) => process.exit(code ?? 0));
1697
+ }
1698
+ function setPath(obj, dotted, value) {
1699
+ const parts = dotted.split(".");
1700
+ let cur = obj;
1701
+ for (let i = 0; i < parts.length - 1; i++) {
1702
+ const k = parts[i];
1703
+ if (typeof cur[k] !== "object" || cur[k] === null) cur[k] = {};
1704
+ cur = cur[k];
1705
+ }
1706
+ cur[parts[parts.length - 1]] = value;
1707
+ }
1708
+ function unsetPath(obj, dotted) {
1709
+ const parts = dotted.split(".");
1710
+ let cur = obj;
1711
+ for (let i = 0; i < parts.length - 1; i++) {
1712
+ const k = parts[i];
1713
+ if (typeof cur[k] !== "object" || cur[k] === null) return;
1714
+ cur = cur[k];
1715
+ }
1716
+ delete cur[parts[parts.length - 1]];
1717
+ }
1718
+ main().catch((err) => {
1719
+ console.error("\nError:", err.message);
1720
+ process.exit(1);
1721
+ });