@toolstackhq/cdpwright 1.1.0 → 1.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 CHANGED
@@ -1323,6 +1323,11 @@ function ensureAllowedUrl(url, options = {}) {
1323
1323
  }
1324
1324
 
1325
1325
  // src/core/Page.ts
1326
+ function assertPdfBuffer(buffer) {
1327
+ if (buffer.length < 5 || buffer.subarray(0, 5).toString("utf-8") !== "%PDF-") {
1328
+ throw new Error("PDF generation failed: Chromium did not return a valid PDF");
1329
+ }
1330
+ }
1326
1331
  var Page = class {
1327
1332
  session;
1328
1333
  logger;
@@ -1442,9 +1447,19 @@ var Page = class {
1442
1447
  async findLocators(options = {}) {
1443
1448
  return this.mainFrame().findLocators(options);
1444
1449
  }
1450
+ async content() {
1451
+ await this.waitForLoad();
1452
+ return this.mainFrame().evaluate(() => {
1453
+ const doctype = document.doctype;
1454
+ const doctypeText = doctype ? `<!DOCTYPE ${doctype.name}${doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : ""}${doctype.systemId ? ` "${doctype.systemId}"` : ""}>` : "<!doctype html>";
1455
+ return `${doctypeText}
1456
+ ${document.documentElement.outerHTML}`;
1457
+ });
1458
+ }
1445
1459
  async screenshot(options = {}) {
1446
1460
  const start = Date.now();
1447
1461
  this.events.emit("action:start", { name: "screenshot", frameId: this.mainFrameId });
1462
+ await this.waitForLoad();
1448
1463
  const result = await this.session.send("Page.captureScreenshot", {
1449
1464
  format: options.format ?? "png",
1450
1465
  quality: options.quality,
@@ -1463,6 +1478,7 @@ var Page = class {
1463
1478
  async screenshotBase64(options = {}) {
1464
1479
  const start = Date.now();
1465
1480
  this.events.emit("action:start", { name: "screenshotBase64", frameId: this.mainFrameId });
1481
+ await this.waitForLoad();
1466
1482
  const result = await this.session.send("Page.captureScreenshot", {
1467
1483
  format: options.format ?? "png",
1468
1484
  quality: options.quality,
@@ -1476,26 +1492,37 @@ var Page = class {
1476
1492
  async pdf(options = {}) {
1477
1493
  const start = Date.now();
1478
1494
  this.events.emit("action:start", { name: "pdf", frameId: this.mainFrameId });
1479
- const result = await this.session.send("Page.printToPDF", {
1480
- landscape: options.landscape ?? false,
1481
- printBackground: options.printBackground ?? true,
1482
- scale: options.scale,
1483
- paperWidth: options.paperWidth,
1484
- paperHeight: options.paperHeight,
1485
- marginTop: options.marginTop,
1486
- marginBottom: options.marginBottom,
1487
- marginLeft: options.marginLeft,
1488
- marginRight: options.marginRight,
1489
- pageRanges: options.pageRanges,
1490
- preferCSSPageSize: options.preferCSSPageSize
1491
- });
1492
- const buffer = Buffer.from(result.data, "base64");
1493
- if (options.path) {
1494
- const resolved = path2.resolve(options.path);
1495
- fs2.writeFileSync(resolved, buffer);
1495
+ await this.waitForLoad();
1496
+ await this.session.send("Emulation.setEmulatedMedia", { media: "screen" });
1497
+ let buffer;
1498
+ try {
1499
+ const result = await this.session.send("Page.printToPDF", {
1500
+ landscape: options.landscape ?? false,
1501
+ printBackground: options.printBackground ?? true,
1502
+ scale: options.scale,
1503
+ paperWidth: options.paperWidth,
1504
+ paperHeight: options.paperHeight,
1505
+ marginTop: options.marginTop,
1506
+ marginBottom: options.marginBottom,
1507
+ marginLeft: options.marginLeft,
1508
+ marginRight: options.marginRight,
1509
+ pageRanges: options.pageRanges,
1510
+ preferCSSPageSize: options.preferCSSPageSize
1511
+ });
1512
+ buffer = Buffer.from(result.data, "base64");
1513
+ assertPdfBuffer(buffer);
1514
+ if (options.path) {
1515
+ const resolved = path2.resolve(options.path);
1516
+ fs2.writeFileSync(resolved, buffer);
1517
+ }
1518
+ } finally {
1519
+ try {
1520
+ await this.session.send("Emulation.setEmulatedMedia", { media: "" });
1521
+ } catch {
1522
+ }
1523
+ const duration = Date.now() - start;
1524
+ this.events.emit("action:end", { name: "pdf", frameId: this.mainFrameId, durationMs: duration });
1496
1525
  }
1497
- const duration = Date.now() - start;
1498
- this.events.emit("action:end", { name: "pdf", frameId: this.mainFrameId, durationMs: duration });
1499
1526
  return buffer;
1500
1527
  }
1501
1528
  getEvents() {
@@ -1504,6 +1531,13 @@ var Page = class {
1504
1531
  getDefaultTimeout() {
1505
1532
  return this.defaultTimeout;
1506
1533
  }
1534
+ async waitForLoad(timeoutMs = this.defaultTimeout) {
1535
+ const frame = this.mainFrame();
1536
+ await waitFor(async () => {
1537
+ const readyState = await frame.evaluate("document.readyState");
1538
+ return readyState === "complete";
1539
+ }, { timeoutMs, description: "page load" });
1540
+ }
1507
1541
  buildFrameTree(tree) {
1508
1542
  const frame = this.ensureFrame(tree.frame.id);
1509
1543
  frame.setMeta({ name: tree.frame.name, url: tree.frame.url, parentId: tree.frame.parentId });
@@ -1605,12 +1639,16 @@ var Browser = class {
1605
1639
  events;
1606
1640
  cleanupTasks;
1607
1641
  contexts = /* @__PURE__ */ new Set();
1608
- constructor(connection, child, logger, events, cleanupTasks = []) {
1642
+ wsEndpoint;
1643
+ pid;
1644
+ constructor(connection, child, logger, events, cleanupTasks = [], wsEndpoint = "") {
1609
1645
  this.connection = connection;
1610
1646
  this.process = child;
1611
1647
  this.logger = logger;
1612
1648
  this.events = events;
1613
1649
  this.cleanupTasks = cleanupTasks;
1650
+ this.wsEndpoint = wsEndpoint;
1651
+ this.pid = child?.pid ?? 0;
1614
1652
  }
1615
1653
  on(event, handler) {
1616
1654
  this.events.on(event, handler);
@@ -1632,6 +1670,23 @@ var Browser = class {
1632
1670
  await page.initialize();
1633
1671
  return page;
1634
1672
  }
1673
+ /** Attach to an existing page by target ID. */
1674
+ async attachPage(targetId) {
1675
+ const { sessionId } = await this.connection.send("Target.attachToTarget", { targetId, flatten: true });
1676
+ const session = this.connection.createSession(sessionId);
1677
+ const page = new Page(session, this.logger, this.events);
1678
+ await page.initialize();
1679
+ return page;
1680
+ }
1681
+ /** List open page targets. */
1682
+ async pages() {
1683
+ const result = await this.connection.send("Target.getTargets");
1684
+ return result.targetInfos.filter((t) => t.type === "page").map((t) => ({ targetId: t.targetId, url: t.url, title: t.title }));
1685
+ }
1686
+ /** Disconnect without killing the browser process. */
1687
+ async disconnect() {
1688
+ await this.connection.close();
1689
+ }
1635
1690
  async disposeContext(contextId) {
1636
1691
  if (!contextId) return;
1637
1692
  try {
@@ -1651,7 +1706,7 @@ var Browser = class {
1651
1706
  } catch {
1652
1707
  }
1653
1708
  await this.connection.close();
1654
- if (!this.process.killed) {
1709
+ if (this.process && !this.process.killed) {
1655
1710
  this.process.kill();
1656
1711
  }
1657
1712
  for (const task of this.cleanupTasks) {
@@ -1993,6 +2048,7 @@ var ChromiumManager = class {
1993
2048
  }
1994
2049
  if (options.maximize) {
1995
2050
  args.push("--start-maximized");
2051
+ args.push("--window-size=1920,1080");
1996
2052
  }
1997
2053
  if (options.args) {
1998
2054
  args.push(...options.args);
@@ -2021,7 +2077,7 @@ var ChromiumManager = class {
2021
2077
  logger.info(`Assertion ${payload.name}`, ...args2);
2022
2078
  });
2023
2079
  }
2024
- const browser = new Browser(connection, child, logger, events, cleanupTasks);
2080
+ const browser = new Browser(connection, child, logger, events, cleanupTasks, wsEndpoint);
2025
2081
  return browser;
2026
2082
  }
2027
2083
  resolveCacheRoot(platform) {
@@ -2463,22 +2519,89 @@ var automaton = {
2463
2519
  async launch(options = {}) {
2464
2520
  const manager = new ChromiumManager(options.logger);
2465
2521
  return manager.launch(options);
2522
+ },
2523
+ async connect(wsEndpoint, options = {}) {
2524
+ const logger = options.logger ?? new Logger(options.logLevel ?? "warn");
2525
+ const connection = new Connection(wsEndpoint, logger);
2526
+ await connection.waitForOpen();
2527
+ const events = new AutomationEvents();
2528
+ const logEvents = options.logEvents ?? true;
2529
+ const logActions = options.logActions ?? true;
2530
+ const logAssertions = options.logAssertions ?? true;
2531
+ if (logEvents && logActions) {
2532
+ events.on("action:end", (payload) => {
2533
+ const selector = payload.sensitive ? void 0 : payload.selector;
2534
+ const args = [];
2535
+ if (selector) args.push(selector);
2536
+ if (typeof payload.durationMs === "number") args.push(`${payload.durationMs}ms`);
2537
+ logger.info(`Action ${payload.name}`, ...args);
2538
+ });
2539
+ }
2540
+ if (logEvents && logAssertions) {
2541
+ events.on("assertion:end", (payload) => {
2542
+ const args = [];
2543
+ if (payload.selector) args.push(payload.selector);
2544
+ if (typeof payload.durationMs === "number") args.push(`${payload.durationMs}ms`);
2545
+ logger.info(`Assertion ${payload.name}`, ...args);
2546
+ });
2547
+ }
2548
+ return new Browser(connection, null, logger, events, [], wsEndpoint);
2466
2549
  }
2467
2550
  };
2468
2551
 
2469
2552
  // src/cli.ts
2553
+ function sessionFilePath() {
2554
+ const platform = detectPlatform();
2555
+ const cacheRoot = defaultCacheRoot(platform);
2556
+ return path5.join(cacheRoot, "session.json");
2557
+ }
2558
+ function writeSession(info) {
2559
+ const filePath = sessionFilePath();
2560
+ fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
2561
+ fs5.writeFileSync(filePath, JSON.stringify(info));
2562
+ }
2563
+ function readSession() {
2564
+ const filePath = sessionFilePath();
2565
+ if (!fs5.existsSync(filePath)) return null;
2566
+ try {
2567
+ const data = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
2568
+ if (!data.wsEndpoint || !data.pid) return null;
2569
+ try {
2570
+ process.kill(data.pid, 0);
2571
+ } catch {
2572
+ clearSession();
2573
+ return null;
2574
+ }
2575
+ return data;
2576
+ } catch {
2577
+ return null;
2578
+ }
2579
+ }
2580
+ function clearSession() {
2581
+ const filePath = sessionFilePath();
2582
+ try {
2583
+ fs5.unlinkSync(filePath);
2584
+ } catch {
2585
+ }
2586
+ }
2470
2587
  function printHelp() {
2471
2588
  console.log(`cdpwright (cpw) \u2014 Chromium automation CLI
2472
2589
 
2473
2590
  Commands:
2474
2591
  download [options] Download pinned Chromium snapshot
2475
2592
  install Alias for download
2476
- open <url> Launch headed browser and navigate to URL
2477
- screenshot <url> -o f Take a screenshot (PNG/JPEG)
2478
- pdf <url> -o file.pdf Generate PDF of page (headless only)
2479
- eval <url> <script> Run script in page, print result as JSON
2593
+ open <url> Launch browser session and navigate to URL
2594
+ close Close the running browser session
2595
+ screenshot [url] -o f Take a screenshot (PNG/JPEG)
2596
+ html [url] -o file.html Save the page HTML source
2597
+ pdf [url] -o file.pdf Generate PDF of page (headless only)
2598
+ eval [url] <script> Run script in page, print result as JSON
2480
2599
  version Print cdpwright and Chromium versions
2481
2600
 
2601
+ Session:
2602
+ 'open' starts a browser and saves a session. Other commands auto-connect
2603
+ to the running session when no URL is given. 'close' shuts it down.
2604
+
2482
2605
  Download options:
2483
2606
  --latest Download the latest Chromium revision
2484
2607
  --mirror <url> Custom mirror base URL
@@ -2521,14 +2644,46 @@ function positionalArgs(args) {
2521
2644
  }
2522
2645
  return result;
2523
2646
  }
2524
- async function withPage(url, options, fn) {
2647
+ function launchArgs() {
2525
2648
  const args = [];
2526
2649
  if (process.platform === "linux") {
2527
2650
  args.push("--no-sandbox", "--no-zygote", "--disable-dev-shm-usage");
2528
2651
  }
2652
+ return args;
2653
+ }
2654
+ function pngDimensions(buffer) {
2655
+ if (buffer.length < 24) {
2656
+ throw new Error("Invalid PNG screenshot");
2657
+ }
2658
+ if (buffer.readUInt32BE(0) !== 2303741511 || buffer.readUInt32BE(4) !== 218765834) {
2659
+ throw new Error("Invalid PNG screenshot");
2660
+ }
2661
+ return {
2662
+ width: buffer.readUInt32BE(16),
2663
+ height: buffer.readUInt32BE(20)
2664
+ };
2665
+ }
2666
+ async function connectToSession() {
2667
+ const session = readSession();
2668
+ if (!session) {
2669
+ console.error("No running session. Start one with: cpw open <url>");
2670
+ process.exit(1);
2671
+ }
2672
+ const browser = await automaton.connect(session.wsEndpoint);
2673
+ const targets = await browser.pages();
2674
+ if (targets.length === 0) {
2675
+ console.error("Session has no open pages.");
2676
+ await browser.disconnect();
2677
+ process.exit(1);
2678
+ }
2679
+ const page = await browser.attachPage(targets[0].targetId);
2680
+ return { browser, page };
2681
+ }
2682
+ async function withPage(url, options, fn) {
2529
2683
  const browser = await automaton.launch({
2530
2684
  headless: options.headless ?? true,
2531
- args,
2685
+ maximize: options.maximize ?? false,
2686
+ args: launchArgs(),
2532
2687
  logLevel: "warn"
2533
2688
  });
2534
2689
  try {
@@ -2555,19 +2710,24 @@ async function cmdOpen(rest) {
2555
2710
  process.exit(1);
2556
2711
  }
2557
2712
  const headless = resolveHeadless(rest, false);
2558
- const args = [];
2559
- if (process.platform === "linux") {
2560
- args.push("--no-sandbox", "--no-zygote", "--disable-dev-shm-usage");
2561
- }
2562
2713
  const browser = await automaton.launch({
2563
2714
  headless,
2564
- args,
2715
+ args: launchArgs(),
2565
2716
  logLevel: "warn"
2566
2717
  });
2567
2718
  const page = await browser.newPage();
2568
2719
  await page.goto(url, { waitUntil: "load" });
2720
+ writeSession({
2721
+ wsEndpoint: browser.wsEndpoint,
2722
+ pid: browser.pid || process.pid
2723
+ });
2569
2724
  console.log(`Browser open at ${url}`);
2570
- console.log("Press Ctrl+C to close.");
2725
+ console.log(`Session saved. Run commands in another terminal:`);
2726
+ console.log(` npx cpw screenshot -o shot.png`);
2727
+ console.log(` npx cpw eval "document.title"`);
2728
+ console.log(` npx cpw close`);
2729
+ console.log(`
2730
+ Press Ctrl+C to close.`);
2571
2731
  await new Promise((resolve) => {
2572
2732
  process.on("SIGINT", () => {
2573
2733
  console.log("\nClosing browser...");
@@ -2575,52 +2735,200 @@ async function cmdOpen(rest) {
2575
2735
  });
2576
2736
  process.on("SIGTERM", () => resolve());
2577
2737
  });
2738
+ clearSession();
2578
2739
  try {
2579
2740
  await browser.close();
2580
2741
  } catch {
2581
2742
  }
2582
2743
  }
2744
+ async function cmdClose() {
2745
+ const session = readSession();
2746
+ if (!session) {
2747
+ console.error("No running session.");
2748
+ process.exit(1);
2749
+ }
2750
+ try {
2751
+ const browser = await automaton.connect(session.wsEndpoint);
2752
+ await browser.close();
2753
+ } catch {
2754
+ try {
2755
+ process.kill(session.pid, "SIGTERM");
2756
+ } catch {
2757
+ }
2758
+ }
2759
+ clearSession();
2760
+ console.log("Session closed.");
2761
+ }
2583
2762
  async function cmdScreenshot(rest) {
2584
- const url = positionalArgs(rest)[0];
2763
+ const pos = positionalArgs(rest);
2764
+ const url = pos[0];
2585
2765
  const output = flagValue(rest, "-o") || flagValue(rest, "--output");
2586
- if (!url || !output) {
2587
- console.error("Usage: cpw screenshot <url> -o <file> [--full-page]");
2766
+ if (!output) {
2767
+ console.error("Usage: cpw screenshot [url] -o <file> [--full-page]");
2588
2768
  process.exit(1);
2589
2769
  }
2590
2770
  const fullPage = hasFlag(rest, "--full-page");
2591
- const headless = resolveHeadless(rest, true);
2592
2771
  const format = output.endsWith(".jpeg") || output.endsWith(".jpg") ? "jpeg" : "png";
2593
- await withPage(url, { headless }, async (page) => {
2594
- await page.screenshot({ path: output, format, fullPage });
2595
- });
2772
+ if (url) {
2773
+ const headless = resolveHeadless(rest, true);
2774
+ await withPage(url, { headless }, async (page) => {
2775
+ await page.screenshot({ path: output, format, fullPage });
2776
+ });
2777
+ } else {
2778
+ const { browser, page } = await connectToSession();
2779
+ try {
2780
+ await page.screenshot({ path: output, format, fullPage });
2781
+ } finally {
2782
+ await browser.disconnect();
2783
+ }
2784
+ }
2596
2785
  console.log(`Screenshot saved to ${output}`);
2597
2786
  }
2787
+ async function cmdHtml(rest) {
2788
+ const pos = positionalArgs(rest);
2789
+ const url = pos[0];
2790
+ const output = flagValue(rest, "-o") || flagValue(rest, "--output");
2791
+ if (!output) {
2792
+ console.error("Usage: cpw html [url] -o <file>");
2793
+ process.exit(1);
2794
+ }
2795
+ const writeHtml = async (page) => {
2796
+ const html = await page.content();
2797
+ fs5.writeFileSync(path5.resolve(output), html, "utf-8");
2798
+ };
2799
+ if (url) {
2800
+ const headless = resolveHeadless(rest, true);
2801
+ await withPage(url, { headless }, async (page) => {
2802
+ await writeHtml(page);
2803
+ });
2804
+ } else {
2805
+ const { browser, page } = await connectToSession();
2806
+ try {
2807
+ await writeHtml(page);
2808
+ } finally {
2809
+ await browser.disconnect();
2810
+ }
2811
+ }
2812
+ console.log(`HTML saved to ${output}`);
2813
+ }
2598
2814
  async function cmdPdf(rest) {
2599
- const url = positionalArgs(rest)[0];
2815
+ const pos = positionalArgs(rest);
2816
+ const url = pos[0];
2600
2817
  const output = flagValue(rest, "-o") || flagValue(rest, "--output");
2601
- if (!url || !output) {
2602
- console.error("Usage: cpw pdf <url> -o <file>");
2818
+ if (!output) {
2819
+ console.error("Usage: cpw pdf [url] -o <file>");
2603
2820
  process.exit(1);
2604
2821
  }
2605
- await withPage(url, { headless: true }, async (page) => {
2606
- await page.pdf({ path: output, printBackground: true });
2607
- });
2822
+ const renderVisualPdf = async (browser, page) => {
2823
+ const screenshot = await page.screenshotBase64({ format: "png" });
2824
+ const screenshotSize = pngDimensions(Buffer.from(screenshot, "base64"));
2825
+ const context = await browser.newContext();
2826
+ try {
2827
+ const helperPage = await context.newPage();
2828
+ const imageDataUrl = `data:image/png;base64,${screenshot}`;
2829
+ const screenshotDimensions = await helperPage.evaluate((dataUrl) => {
2830
+ document.open();
2831
+ document.write(`<!doctype html>
2832
+ <html>
2833
+ <head>
2834
+ <meta charset="utf-8">
2835
+ <style>
2836
+ html, body {
2837
+ margin: 0;
2838
+ padding: 0;
2839
+ overflow: hidden;
2840
+ background: #fff;
2841
+ }
2842
+ img {
2843
+ display: block;
2844
+ width: 100%;
2845
+ height: auto;
2846
+ }
2847
+ </style>
2848
+ </head>
2849
+ <body>
2850
+ <img id="shot" alt="page screenshot">
2851
+ </body>
2852
+ </html>`);
2853
+ document.close();
2854
+ const img = document.getElementById("shot");
2855
+ if (!(img instanceof HTMLImageElement)) {
2856
+ throw new Error("Failed to create PDF preview image");
2857
+ }
2858
+ return new Promise((resolve, reject) => {
2859
+ img.onload = () => {
2860
+ resolve({ width: img.naturalWidth || 0, height: img.naturalHeight || 0 });
2861
+ };
2862
+ img.onerror = () => reject(new Error("Failed to load screenshot image"));
2863
+ img.src = dataUrl;
2864
+ });
2865
+ }, imageDataUrl);
2866
+ await helperPage.pdf({
2867
+ path: output,
2868
+ printBackground: true,
2869
+ paperWidth: Math.max((screenshotDimensions?.width ?? screenshotSize.width) / 96, 1),
2870
+ paperHeight: Math.max((screenshotDimensions?.height ?? screenshotSize.height) / 96, 1),
2871
+ marginTop: 0,
2872
+ marginBottom: 0,
2873
+ marginLeft: 0,
2874
+ marginRight: 0,
2875
+ scale: 1,
2876
+ preferCSSPageSize: false
2877
+ });
2878
+ } finally {
2879
+ await context.close();
2880
+ }
2881
+ };
2882
+ if (url) {
2883
+ await withPage(url, { headless: true, maximize: true }, async (page, browser) => {
2884
+ await renderVisualPdf(browser, page);
2885
+ });
2886
+ } else {
2887
+ const { browser, page } = await connectToSession();
2888
+ try {
2889
+ await renderVisualPdf(browser, page);
2890
+ } finally {
2891
+ await browser.disconnect();
2892
+ }
2893
+ }
2608
2894
  console.log(`PDF saved to ${output}`);
2609
2895
  }
2610
2896
  async function cmdEval(rest) {
2611
2897
  const pos = positionalArgs(rest);
2612
- const url = pos[0];
2613
- const script = pos[1];
2614
- if (!url || !script) {
2615
- console.error("Usage: cpw eval <url> <script>");
2898
+ const session = readSession();
2899
+ let url;
2900
+ let script;
2901
+ if (pos.length >= 2) {
2902
+ url = pos[0];
2903
+ script = pos[1];
2904
+ } else if (pos.length === 1 && session) {
2905
+ script = pos[0];
2906
+ } else if (pos.length === 1) {
2907
+ console.error("Usage: cpw eval <url> <script>\n cpw eval <script> (when a session is running)");
2908
+ process.exit(1);
2909
+ return;
2910
+ } else {
2911
+ console.error("Usage: cpw eval [url] <script>");
2616
2912
  process.exit(1);
2913
+ return;
2914
+ }
2915
+ if (url) {
2916
+ const headless = resolveHeadless(rest, true);
2917
+ await withPage(url, { headless }, async (page) => {
2918
+ const result = await page.evaluate(script);
2919
+ const output = result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
2920
+ console.log(output);
2921
+ });
2922
+ } else {
2923
+ const { browser, page } = await connectToSession();
2924
+ try {
2925
+ const result = await page.evaluate(script);
2926
+ const output = result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
2927
+ console.log(output);
2928
+ } finally {
2929
+ await browser.disconnect();
2930
+ }
2617
2931
  }
2618
- const headless = resolveHeadless(rest, true);
2619
- await withPage(url, { headless }, async (page) => {
2620
- const result = await page.evaluate(script);
2621
- const output = result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
2622
- console.log(output);
2623
- });
2624
2932
  }
2625
2933
  async function cmdVersion() {
2626
2934
  const pkgPath = path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "../package.json");
@@ -2646,6 +2954,10 @@ async function cmdVersion() {
2646
2954
  } else {
2647
2955
  console.log("Chromium: not installed (run 'cpw install')");
2648
2956
  }
2957
+ const session = readSession();
2958
+ if (session) {
2959
+ console.log(`Session: active (pid ${session.pid})`);
2960
+ }
2649
2961
  }
2650
2962
  async function main() {
2651
2963
  const [, , command, ...rest] = process.argv;
@@ -2659,8 +2971,12 @@ async function main() {
2659
2971
  return cmdDownload(rest);
2660
2972
  case "open":
2661
2973
  return cmdOpen(rest);
2974
+ case "close":
2975
+ return cmdClose();
2662
2976
  case "screenshot":
2663
2977
  return cmdScreenshot(rest);
2978
+ case "html":
2979
+ return cmdHtml(rest);
2664
2980
  case "pdf":
2665
2981
  return cmdPdf(rest);
2666
2982
  case "eval":