UNPKG

chromancer

Version:

A powerful command-line interface for automating Chrome browser using Playwright. Perfect for web scraping, automation, testing, and browser workflows.

452 lines (451 loc) • 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const base_js_1 = require("../base.js"); const readline = tslib_1.__importStar(require("readline")); const fs = tslib_1.__importStar(require("fs")); const path = tslib_1.__importStar(require("path")); const os = tslib_1.__importStar(require("os")); class Interactive extends base_js_1.BaseCommand { static description = 'Launch an interactive REPL with command history, tab completion, and real-time browser control - explore pages interactively with full CDP access'; static examples = [ '<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --port 9222', '<%= config.bin %> <%= command.id %> --launch --profile work', ]; static flags = { ...base_js_1.BaseCommand.baseFlags, }; rl; historyFile; commandHistory = []; historyIndex = -1; commands = { navigate: 'Navigate to a URL', click: 'Click on an element', type: 'Type text into an element', evaluate: 'Evaluate JavaScript in the browser', screenshot: 'Take a screenshot', select: 'Select elements on the page', wait: 'Wait for an element or condition', hover: 'Hover over an element', back: 'Go back in browser history', forward: 'Go forward in browser history', reload: 'Reload the current page', url: 'Get the current URL', title: 'Get the page title', cookies: 'List all cookies', viewport: 'Set or get viewport size', login: 'Navigate and wait for login', help: 'Show available commands', clear: 'Clear the console', exit: 'Exit interactive mode', quit: 'Exit interactive mode', }; constructor(argv, config) { super(argv, config); this.historyFile = path.join(os.homedir(), '.chromancer_history'); } async run() { const { flags } = await this.parse(Interactive); // Connect to Chrome await this.connectToChrome(flags.port, flags.host, flags.launch, flags.profile, flags.headless, flags.verbose, true // Always keep open in interactive mode ); if (!this.page) { this.error('No page available'); } // Load command history this.loadHistory(); // Setup readline interface this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: 'chromancer> ', completer: this.completer.bind(this), terminal: true, // Force terminal mode }); // Setup key bindings for history navigation this.setupKeyBindings(); this.log('✨ Interactive CDP session started'); this.log('Type "help" for available commands, "exit" to quit'); this.log(''); // Ensure stdin stays open process.stdin.resume(); // Display prompt this.rl.prompt(); // Handle line input this.rl.on('line', async (input) => { const trimmedInput = input.trim(); if (trimmedInput) { // Add to history this.addToHistory(trimmedInput); try { await this.executeCommand(trimmedInput); } catch (error) { this.log(`āŒ Error: ${error.message}`); this.logVerbose('Command execution error', error); } } this.rl?.prompt(); }); // Handle close this.rl.on('close', () => { this.saveHistory(); this.log('\nšŸ‘‹ Goodbye!'); process.exit(0); }); } setupKeyBindings() { if (!this.rl) return; // Override keypress handling for history navigation process.stdin.on('keypress', (str, key) => { if (!key) return; if (key.name === 'up') { // Navigate backward in history if (this.historyIndex < this.commandHistory.length - 1) { this.historyIndex++; const cmd = this.commandHistory[this.commandHistory.length - 1 - this.historyIndex]; if (cmd && this.rl) { this.rl.write(null, { ctrl: true, name: 'u' }); // Clear current line this.rl.write(cmd); } } } else if (key.name === 'down') { // Navigate forward in history if (this.historyIndex > 0) { this.historyIndex--; const cmd = this.commandHistory[this.commandHistory.length - 1 - this.historyIndex]; if (cmd && this.rl) { this.rl.write(null, { ctrl: true, name: 'u' }); // Clear current line this.rl.write(cmd); } } else if (this.historyIndex === 0) { // Clear the line when reaching the end of history this.historyIndex = -1; if (this.rl) { this.rl.write(null, { ctrl: true, name: 'u' }); // Clear current line } } } }); } completer(line) { const completions = Object.keys(this.commands); const hits = completions.filter((c) => c.startsWith(line)); return [hits.length ? hits : completions, line]; } async executeCommand(input) { const parts = input.split(/\s+/); const command = parts[0].toLowerCase(); const args = parts.slice(1); switch (command) { case 'help': this.showHelp(); break; case 'clear': console.clear(); break; case 'exit': case 'quit': this.rl?.close(); break; case 'navigate': case 'goto': if (args.length === 0) { this.log('Usage: navigate <url>'); return; } await this.navigate(args[0]); break; case 'click': if (args.length === 0) { this.log('Usage: click <selector>'); return; } await this.click(args.join(' ')); break; case 'type': if (args.length < 2) { this.log('Usage: type <selector> <text>'); return; } const selector = args[0]; const text = args.slice(1).join(' '); await this.type(selector, text); break; case 'evaluate': case 'eval': if (args.length === 0) { this.log('Usage: evaluate <javascript>'); return; } await this.evaluate(args.join(' ')); break; case 'screenshot': const filename = args[0] || `screenshot-${Date.now()}.png`; await this.screenshot(filename); break; case 'select': const selectSelector = args.join(' ') || '*'; await this.select(selectSelector); break; case 'wait': if (args.length === 0) { this.log('Usage: wait <selector>'); return; } await this.wait(args.join(' ')); break; case 'hover': if (args.length === 0) { this.log('Usage: hover <selector>'); return; } await this.hover(args.join(' ')); break; case 'back': await this.goBack(); break; case 'forward': await this.goForward(); break; case 'reload': case 'refresh': await this.reload(); break; case 'url': await this.showUrl(); break; case 'title': await this.showTitle(); break; case 'cookies': await this.showCookies(); break; case 'viewport': if (args.length >= 2) { const width = parseInt(args[0]); const height = parseInt(args[1]); await this.setViewport(width, height); } else { await this.showViewport(); } break; case 'login': if (args.length === 0) { this.log('Usage: login <url> [ready-selector]'); return; } const loginUrl = args[0]; const readySelector = args[1]; await this.waitForLogin(loginUrl, readySelector); break; default: this.log(`Unknown command: ${command}`); this.log('Type "help" for available commands'); } } showHelp() { this.log('\nAvailable commands:'); this.log(''); Object.entries(this.commands).forEach(([cmd, desc]) => { this.log(` ${cmd.padEnd(15)} ${desc}`); }); this.log(''); this.log('Examples:'); this.log(' navigate https://example.com'); this.log(' click button.submit'); this.log(' type input[name="search"] hello world'); this.log(' evaluate document.title'); this.log(' screenshot output.png'); this.log(' wait .loading-complete'); this.log(' hover .dropdown-menu'); this.log(' login https://gmail.com'); this.log(''); } async navigate(url) { if (!this.page) return; // Add protocol if missing if (!url.startsWith('http://') && !url.startsWith('https://')) { url = 'https://' + url; } this.log(`🌐 Navigating to ${url}...`); await this.page.goto(url, { waitUntil: 'load' }); this.log(`āœ… Navigated to ${url}`); } async click(selector) { if (!this.page) return; this.log(`šŸ–±ļø Clicking ${selector}...`); await this.page.click(selector); this.log(`āœ… Clicked ${selector}`); } async type(selector, text) { if (!this.page) return; this.log(`āŒØļø Typing into ${selector}...`); await this.page.type(selector, text); this.log(`āœ… Typed "${text}" into ${selector}`); } async evaluate(code) { if (!this.page) return; try { const result = await this.page.evaluate(code); this.log('šŸ“¤ Result:', JSON.stringify(result, null, 2)); } catch (error) { this.log('āŒ Evaluation error:', error.message); } } async screenshot(filename) { if (!this.page) return; // Ensure filename has proper extension if (!filename.match(/\.(png|jpg|jpeg|webp)$/i)) { filename += '.png'; } this.log(`šŸ“ø Taking screenshot...`); await this.page.screenshot({ path: filename, fullPage: true }); this.log(`āœ… Screenshot saved to ${filename}`); } async select(selector) { if (!this.page) return; try { const count = await this.page.locator(selector).count(); this.log(`šŸ” Found ${count} elements matching "${selector}"`); if (count > 0 && count <= 10) { // Show details for up to 10 elements for (let i = 0; i < count; i++) { const element = this.page.locator(selector).nth(i); const tagName = await element.evaluate(el => el.tagName.toLowerCase()); const text = await element.textContent() || ''; const className = await element.getAttribute('class') || ''; this.log(`[${i}] <${tagName}${className ? ` class="${className}"` : ''}> ${text.substring(0, 50)}${text.length > 50 ? '...' : ''}`); } } } catch (error) { this.log('āŒ Selection error:', error.message); } } async wait(selector) { if (!this.page) return; this.log(`ā³ Waiting for ${selector}...`); await this.page.waitForSelector(selector, { state: 'visible' }); this.log(`āœ… Element ${selector} is visible`); } async hover(selector) { if (!this.page) return; this.log(`šŸŽÆ Hovering over ${selector}...`); await this.page.hover(selector); this.log(`āœ… Hovered over ${selector}`); } async goBack() { if (!this.page) return; this.log('ā¬…ļø Going back...'); await this.page.goBack(); this.log('āœ… Navigated back'); } async goForward() { if (!this.page) return; this.log('āž”ļø Going forward...'); await this.page.goForward(); this.log('āœ… Navigated forward'); } async reload() { if (!this.page) return; this.log('šŸ”„ Reloading page...'); await this.page.reload(); this.log('āœ… Page reloaded'); } async showUrl() { if (!this.page) return; const url = this.page.url(); this.log('šŸ”— Current URL:', url); } async showTitle() { if (!this.page) return; const title = await this.page.title(); this.log('šŸ“„ Page title:', title); } async showCookies() { if (!this.page) return; const cookies = await this.page.context().cookies(); this.log(`šŸŖ Found ${cookies.length} cookies:`); cookies.forEach(cookie => { this.log(` ${cookie.name}: ${cookie.value.substring(0, 50)}${cookie.value.length > 50 ? '...' : ''}`); }); } async setViewport(width, height) { if (!this.page) return; await this.page.setViewportSize({ width, height }); this.log(`āœ… Viewport set to ${width}x${height}`); } async showViewport() { if (!this.page) return; const viewport = this.page.viewportSize(); if (viewport) { this.log('šŸ“ Current viewport:', `${viewport.width}x${viewport.height}`); } else { this.log('No viewport set'); } } loadHistory() { try { if (fs.existsSync(this.historyFile)) { const data = fs.readFileSync(this.historyFile, 'utf-8'); this.commandHistory = data.split('\n').filter(line => line.trim()); } } catch (error) { // Ignore history load errors } } saveHistory() { try { const historyData = this.commandHistory.slice(-1000).join('\n'); // Keep last 1000 commands fs.writeFileSync(this.historyFile, historyData); } catch (error) { // Ignore history save errors } } addToHistory(command) { // Don't add duplicate consecutive commands if (this.commandHistory.length === 0 || this.commandHistory[this.commandHistory.length - 1] !== command) { this.commandHistory.push(command); } this.historyIndex = -1; // Reset history navigation } async finally() { // Don't call super.finally() in interactive mode // We want to keep the browser connection alive if (this.rl) { this.rl.close(); } this.saveHistory(); // Only disconnect when the REPL is actually closing // This will be handled by process.exit() in the close handler } } exports.default = Interactive;