233 lines
7.4 KiB
JavaScript
Executable File
233 lines
7.4 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Fast browser automation via Puppeteer
|
|
*
|
|
* Usage: node browser.js <command> [args]
|
|
*
|
|
* Commands:
|
|
* tabs - List open tabs
|
|
* goto <url> - Navigate to URL
|
|
* screenshot [url] [output] - Screenshot (current page or URL)
|
|
* text [url] - Get page text
|
|
* html [selector] - Get HTML
|
|
* click <selector> - Click element
|
|
* type <selector> <text> - Type into field
|
|
* eval <js> - Run JavaScript
|
|
* close [index] - Close tab
|
|
* amazon <subcmd> - Amazon: cart, search <q>, buy-again
|
|
* x <subcmd> - X/Twitter: feed, search <q>, user <handle>
|
|
* tv <symbol> - TradingView quote
|
|
*/
|
|
|
|
const puppeteer = require('puppeteer-core');
|
|
const fs = require('fs');
|
|
|
|
const BROWSER_URL = 'http://127.0.0.1:9222';
|
|
|
|
async function connect() {
|
|
return puppeteer.connect({ browserURL: BROWSER_URL, defaultViewport: null });
|
|
}
|
|
|
|
async function getPage(browser, urlFilter) {
|
|
const pages = await browser.pages();
|
|
if (urlFilter) {
|
|
return pages.find(p => p.url().includes(urlFilter)) || pages[0];
|
|
}
|
|
return pages[0];
|
|
}
|
|
|
|
const commands = {
|
|
async tabs(browser) {
|
|
const pages = await browser.pages();
|
|
pages.forEach((p, i) => console.log(`[${i}] ${p.url()}`));
|
|
},
|
|
|
|
async goto(browser, [url]) {
|
|
if (!url) throw new Error('Need URL');
|
|
const page = (await browser.pages())[0] || await browser.newPage();
|
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
console.log(`→ ${page.url()}`);
|
|
},
|
|
|
|
async screenshot(browser, [url, output = '/tmp/screenshot.png']) {
|
|
const page = url && url !== '-'
|
|
? await (async () => { const p = await browser.newPage(); await p.goto(url, {waitUntil: 'networkidle2'}); return p; })()
|
|
: (await browser.pages())[0];
|
|
await page.screenshot({ path: output });
|
|
console.log(`📸 ${output}`);
|
|
},
|
|
|
|
async text(browser, [url]) {
|
|
let page = (await browser.pages())[0];
|
|
if (url) {
|
|
page = await browser.newPage();
|
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
}
|
|
const text = await page.evaluate(() => document.body.innerText);
|
|
console.log(text.substring(0, 10000));
|
|
},
|
|
|
|
async html(browser, [selector]) {
|
|
const page = (await browser.pages())[0];
|
|
const html = await page.evaluate(s => {
|
|
const el = s ? document.querySelector(s) : document.body;
|
|
return el ? el.innerHTML : null;
|
|
}, selector);
|
|
console.log(html?.substring(0, 10000) || 'Not found');
|
|
},
|
|
|
|
async click(browser, [selector]) {
|
|
const page = (await browser.pages())[0];
|
|
await page.click(selector);
|
|
console.log(`✓ Clicked ${selector}`);
|
|
},
|
|
|
|
async type(browser, [selector, ...text]) {
|
|
const page = (await browser.pages())[0];
|
|
await page.type(selector, text.join(' '));
|
|
console.log(`✓ Typed into ${selector}`);
|
|
},
|
|
|
|
async eval(browser, args) {
|
|
const page = (await browser.pages())[0];
|
|
const result = await page.evaluate(args.join(' '));
|
|
console.log(JSON.stringify(result, null, 2));
|
|
},
|
|
|
|
async close(browser, [index = 0]) {
|
|
const pages = await browser.pages();
|
|
if (pages[+index]) {
|
|
await pages[+index].close();
|
|
console.log(`Closed tab ${index}`);
|
|
}
|
|
},
|
|
|
|
// Amazon shortcuts
|
|
async amazon(browser, [subcmd, ...args]) {
|
|
let page = await getPage(browser, 'amazon.com');
|
|
if (!page?.url().includes('amazon')) {
|
|
page = await browser.newPage();
|
|
}
|
|
|
|
switch (subcmd) {
|
|
case 'cart':
|
|
await page.goto('https://www.amazon.com/gp/cart/view.html', { waitUntil: 'networkidle2' });
|
|
const cartItems = await page.evaluate(() => {
|
|
const items = document.querySelectorAll('.sc-list-item-content');
|
|
return Array.from(items).map(item => {
|
|
const title = item.querySelector('.sc-product-title')?.textContent?.trim();
|
|
const price = item.querySelector('.sc-product-price')?.textContent?.trim();
|
|
return { title, price };
|
|
}).filter(i => i.title);
|
|
});
|
|
console.log('🛒 Cart:');
|
|
cartItems.forEach(i => console.log(` - ${i.title} (${i.price})`));
|
|
if (!cartItems.length) console.log(' (empty)');
|
|
break;
|
|
|
|
case 'search':
|
|
const q = args.join(' ');
|
|
await page.goto(`https://www.amazon.com/s?k=${encodeURIComponent(q)}`, { waitUntil: 'networkidle2' });
|
|
console.log(`🔍 Amazon search: ${q}`);
|
|
break;
|
|
|
|
case 'buy-again':
|
|
await page.goto('https://www.amazon.com/gp/buyagain', { waitUntil: 'networkidle2' });
|
|
console.log('📦 Buy Again page loaded');
|
|
break;
|
|
|
|
default:
|
|
console.log('Amazon subcmds: cart, search <query>, buy-again');
|
|
}
|
|
},
|
|
|
|
// X/Twitter shortcuts
|
|
async x(browser, [subcmd, ...args]) {
|
|
let page = await getPage(browser, 'x.com');
|
|
if (!page?.url().includes('x.com')) {
|
|
page = await browser.newPage();
|
|
}
|
|
|
|
switch (subcmd) {
|
|
case 'feed':
|
|
await page.goto('https://x.com/home', { waitUntil: 'networkidle2' });
|
|
await page.screenshot({ path: '/tmp/x-feed.png' });
|
|
console.log('📸 /tmp/x-feed.png');
|
|
break;
|
|
|
|
case 'search':
|
|
const q = args.join(' ');
|
|
await page.goto(`https://x.com/search?q=${encodeURIComponent(q)}&f=live`, { waitUntil: 'networkidle2' });
|
|
console.log(`🔍 X search: ${q}`);
|
|
await page.screenshot({ path: '/tmp/x-search.png' });
|
|
console.log('📸 /tmp/x-search.png');
|
|
break;
|
|
|
|
case 'user':
|
|
const handle = args[0]?.replace('@', '');
|
|
await page.goto(`https://x.com/${handle}`, { waitUntil: 'networkidle2' });
|
|
console.log(`👤 @${handle}`);
|
|
await page.screenshot({ path: `/tmp/x-${handle}.png` });
|
|
console.log(`📸 /tmp/x-${handle}.png`);
|
|
break;
|
|
|
|
default:
|
|
console.log('X subcmds: feed, search <query>, user <handle>');
|
|
}
|
|
},
|
|
|
|
// TradingView quote
|
|
async tv(browser, [symbol]) {
|
|
if (!symbol) throw new Error('Need symbol');
|
|
let page = await getPage(browser, 'tradingview');
|
|
if (!page?.url().includes('tradingview')) {
|
|
page = await browser.newPage();
|
|
}
|
|
await page.goto(`https://www.tradingview.com/symbols/${symbol.toUpperCase()}/`, { waitUntil: 'networkidle2' });
|
|
|
|
const data = await page.evaluate(() => {
|
|
const price = document.querySelector('[data-symbol-last-value]')?.textContent;
|
|
const change = document.querySelector('[data-symbol-change]')?.textContent;
|
|
return { price, change };
|
|
});
|
|
|
|
console.log(`📈 ${symbol.toUpperCase()}: ${data.price || 'N/A'} (${data.change || 'N/A'})`);
|
|
}
|
|
};
|
|
|
|
async function main() {
|
|
const [,, cmd, ...args] = process.argv;
|
|
|
|
if (!cmd || cmd === 'help') {
|
|
console.log(`Browser automation commands:
|
|
tabs - List open tabs
|
|
goto <url> - Navigate
|
|
screenshot [url] [out] - Screenshot
|
|
text [url] - Get page text
|
|
click <selector> - Click
|
|
type <sel> <text> - Type
|
|
eval <js> - Run JS
|
|
amazon cart|search|buy-again
|
|
x feed|search|user
|
|
tv <symbol> - Stock quote`);
|
|
return;
|
|
}
|
|
|
|
if (!commands[cmd]) {
|
|
console.error(`Unknown: ${cmd}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const browser = await connect();
|
|
try {
|
|
await commands[cmd](browser, args);
|
|
} finally {
|
|
browser.disconnect();
|
|
}
|
|
}
|
|
|
|
main().catch(e => {
|
|
console.error('Error:', e.message);
|
|
process.exit(1);
|
|
});
|