Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2import puppeteer from "puppeteer-core";
3import http from "http";
4
5const CDP_HOST = "host.docker.internal";
6const CDP_PORT = 9222;
7
8function httpGet(url, headers = {}) {
9 return new Promise((resolve, reject) => {
10 const u = new URL(url);
11 const req = http.request({
12 hostname: u.hostname,
13 port: u.port,
14 path: u.pathname,
15 method: "GET",
16 headers: { Host: "localhost", ...headers }
17 }, (res) => {
18 let data = "";
19 res.on("data", chunk => data += chunk);
20 res.on("end", () => resolve(data));
21 });
22 req.on("error", reject);
23 req.end();
24 });
25}
26
27async function getWSEndpoint() {
28 const data = await httpGet(`http://${CDP_HOST}:${CDP_PORT}/json/version`);
29 const json = JSON.parse(data);
30 return json.webSocketDebuggerUrl.replace("ws://localhost", `ws://${CDP_HOST}:${CDP_PORT}`);
31}
32
33export async function getBrowser() {
34 const wsEndpoint = await getWSEndpoint();
35 return puppeteer.connect({
36 browserWSEndpoint: wsEndpoint,
37 defaultViewport: null,
38 });
39}
40
41export async function listPages() {
42 const browser = await getBrowser();
43 const pages = await browser.pages();
44 const info = await Promise.all(pages.map(async (p, i) => ({
45 index: i,
46 url: p.url(),
47 title: await p.title(),
48 })));
49 await browser.disconnect();
50 return info;
51}
52
53export async function getPage(pattern) {
54 const browser = await getBrowser();
55 const pages = await browser.pages();
56 let page = typeof pattern === "number" ? pages[pattern] : pages.find(p => p.url().includes(pattern));
57 if (!page) { await browser.disconnect(); throw new Error("No page: " + pattern); }
58 return { browser, page };
59}
60
61export class PageController {
62 constructor(page, browser) { this.page = page; this.browser = browser; }
63 static async connect(pattern) {
64 const { browser, page } = await getPage(pattern);
65 return new PageController(page, browser);
66 }
67 async goto(url, o = {}) { await this.page.goto(url, { waitUntil: "networkidle2", ...o }); return this; }
68 async click(s, o = {}) { await this.page.waitForSelector(s, { timeout: 10000, ...o }); await this.page.click(s); return this; }
69 async type(s, text, o = {}) { await this.page.waitForSelector(s, { timeout: 10000 }); await this.page.click(s); await this.page.type(s, text, o); return this; }
70 async fill(s, v) { await this.page.waitForSelector(s, { timeout: 10000 }); await this.page.$eval(s, (el, val) => { el.value = val; el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); }, v); return this; }
71 async getContent() { return this.page.evaluate(() => document.body.innerText); }
72 async getHtml() { return this.page.content(); }
73 async wait(ms) { await new Promise(r => setTimeout(r, ms)); return this; }
74 async waitNav(o = {}) { await this.page.waitForNavigation({ waitUntil: "networkidle2", ...o }); return this; }
75 async waitFor(s, o = {}) { await this.page.waitForSelector(s, { timeout: 10000, ...o }); return this; }
76 async screenshot(path) { await this.page.screenshot({ path, fullPage: true }); return this; }
77 async eval(fn, ...args) { return this.page.evaluate(fn, ...args); }
78 url() { return this.page.url(); }
79 async title() { return this.page.title(); }
80 async close() { await this.browser.disconnect(); }
81}
82
83if (import.meta.url === `file://${process.argv[1]}`) {
84 const [cmd, arg] = process.argv.slice(2);
85 if (cmd === "list") {
86 const pages = await listPages();
87 console.log("Tabs:");
88 pages.forEach(p => console.log(" [" + p.index + "] " + p.title.slice(0,40) + " - " + p.url.slice(0,60)));
89 } else if (cmd === "content") {
90 const ctrl = await PageController.connect(isNaN(arg) ? arg : parseInt(arg) || 0);
91 console.log(await ctrl.getContent());
92 await ctrl.close();
93 } else {
94 console.log("Usage: node browser.mjs list | content [pattern]");
95 }
96}