UNPKG

chromancer

Version:

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

269 lines (267 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseCommand = void 0; const tslib_1 = require("tslib"); const core_1 = require("@oclif/core"); const playwright_1 = require("playwright"); const path = tslib_1.__importStar(require("path")); const os = tslib_1.__importStar(require("os")); const error_tips_js_1 = require("./utils/error-tips.js"); const chrome_scanner_js_1 = require("./utils/chrome-scanner.js"); class BaseCommand extends core_1.Command { static baseFlags = { port: core_1.Flags.integer({ char: 'p', description: 'Chrome debugging port', default: 9222, }), host: core_1.Flags.string({ char: 'h', description: 'Chrome debugging host', default: 'localhost', }), launch: core_1.Flags.boolean({ char: 'l', description: 'Launch Chrome automatically', default: false, }), profile: core_1.Flags.string({ description: 'Chrome profile name or path to use', }), headless: core_1.Flags.boolean({ description: 'Run Chrome in headless mode', default: false, }), verbose: core_1.Flags.boolean({ char: 'v', description: 'Enable verbose logging for debugging', default: false, }), keepOpen: core_1.Flags.boolean({ char: 'k', description: 'Keep Chrome open after the command (only relevant when this command launches Chrome)', default: false, allowNo: true, }), }; browser; context; page; isLaunched = false; verbose = false; keepOpen = false; logVerbose(message, data) { if (this.verbose) { const timestamp = new Date().toISOString(); const prefix = `[${timestamp}] [VERBOSE]`; if (data) { this.log(`${prefix} ${message}`, JSON.stringify(data, null, 2)); } else { this.log(`${prefix} ${message}`); } } } getProfilePath(profileName) { // If it's already an absolute path, use it if (path.isAbsolute(profileName)) { return profileName; } // Otherwise, create profile in user's home directory const platform = process.platform; let profileBase; if (platform === 'win32') { profileBase = path.join(os.homedir(), 'AppData', 'Local', 'chromancer', 'profiles'); } else if (platform === 'darwin') { profileBase = path.join(os.homedir(), 'Library', 'Application Support', 'chromancer', 'profiles'); } else { profileBase = path.join(os.homedir(), '.config', 'chromancer', 'profiles'); } return path.join(profileBase, profileName); } async connectToChrome(port = 9222, host = 'localhost', launch = false, profile, headless = false, verbose = false, keepOpen = true) { this.verbose = verbose; // If we *attach* to external Chrome, always auto‑disconnect unless // the caller explicitly passed --keepOpen this.keepOpen = launch ? keepOpen : false; const startTime = Date.now(); this.logVerbose('Starting Chrome connection process', { port, host, launch, profile, headless }); const browserURL = `http://${host}:${port}`; const ora = (await Promise.resolve().then(() => tslib_1.__importStar(require('ora')))).default; const connectSpinner = ora(`🔍 Attempting to connect to Chrome at ${browserURL}...`).start(); try { // First try to connect to existing Chrome instance this.browser = await playwright_1.chromium.connectOverCDP(browserURL); this.context = this.browser.contexts()[0]; connectSpinner.succeed('✅ Connected to existing Chrome instance'); this.logVerbose(`Connection successful in ${Date.now() - startTime}ms`); } catch (connectError) { connectSpinner.fail(`❌ No existing Chrome instance found at ${browserURL}`); this.logVerbose('Connection failed', { error: connectError.message }); // Scan for Chrome instances on other ports this.log(`🔍 Scanning for Chrome instances on other ports...`); const instances = await (0, chrome_scanner_js_1.scanForChromeInstances)(host); if (instances.length > 0) { this.log(`\n✨ Found Chrome instance(s) on different port(s):`); for (const instance of instances) { this.log(` Port ${instance.port}: ${instance.version?.Browser || 'Chrome'}`); } // Try to connect to the first found instance const firstInstance = instances[0]; this.log(`\n🔄 Attempting to connect to Chrome on port ${firstInstance.port}...`); try { this.browser = await playwright_1.chromium.connectOverCDP(`http://${host}:${firstInstance.port}`); this.context = this.browser.contexts()[0]; this.log(`✅ Connected to Chrome on port ${firstInstance.port}`); this.log(`💡 To use this port by default, add --port ${firstInstance.port}`); // Success! We connected to a different port return; } catch (altConnectError) { this.log(`❌ Failed to connect to Chrome on port ${firstInstance.port}`); } } if (launch) { try { this.log('🚀 Launching new Chrome instance...'); const launchOptions = { headless, args: [ `--remote-debugging-port=${port}`, '--no-first-run', '--no-default-browser-check', ], channel: 'chrome', }; // Add container-specific flags if needed if (process.env.DEVCONTAINER === 'true' || process.env.CHROME_FLAGS) { launchOptions.args.push('--no-sandbox', '--disable-dev-shm-usage'); } // Add profile support if (profile) { const profilePath = this.getProfilePath(profile); this.log(`📁 Using Chrome profile: ${profilePath}`); launchOptions.args.push(`--user-data-dir=${profilePath}`); } this.browser = await playwright_1.chromium.launch(launchOptions); this.context = await this.browser.newContext(); this.isLaunched = true; this.log(`✅ Chrome launched successfully${profile ? ` with profile "${profile}"` : ''}`); this.logVerbose(`Chrome launched in ${Date.now() - startTime}ms`); } catch (launchError) { this.error(`Failed to launch Chrome: ${launchError.message}`); } } else { this.error(`Failed to connect to Chrome at ${host}:${port}. Possible solutions: 1. Use --launch flag to start a new Chrome instance 2. Use 'chromancer spawn' to start Chrome (avoids profile picker by default) 3. Use 'chromancer spawn --profile NAME' for a specific saved profile 4. Start Chrome manually with: chrome --remote-debugging-port=${port}`); } } if (this.browser && this.context) { // Get or create a page const pages = this.context.pages(); this.page = pages[0] || await this.context.newPage(); this.logVerbose('Page setup complete', { totalPages: pages.length, newPageCreated: pages.length === 0 }); const totalTime = Date.now() - startTime; this.logVerbose(`Total connection time: ${totalTime}ms`); } } async waitForLogin(url, readySelector) { if (!this.page) { this.error('No page available'); } this.log(`🔐 Navigating to ${url}...`); await this.page.goto(url, { waitUntil: 'domcontentloaded' }); const checkSelector = readySelector || 'body'; this.log(`⏳ Waiting for you to log in...`); this.log(` (Looking for element: ${checkSelector})`); this.log(` Press Ctrl+C to cancel`); // Check if already logged in try { await this.page.waitForSelector(checkSelector, { timeout: 1000 }); this.log('✅ Already logged in!'); return; } catch { // Not logged in yet, continue waiting } // Wait for login with visual feedback let dots = 0; const loadingInterval = setInterval(() => { process.stdout.write(`\r⏳ Waiting for login${'.'.repeat(dots % 4)} `); dots++; }, 500); try { // Wait for either the ready selector or a common logged-in indicator await this.page.waitForSelector(checkSelector, { timeout: 300000 }); // 5 minute timeout clearInterval(loadingInterval); process.stdout.write('\r'); this.log('✅ Login detected! Continuing...'); // Give the page a moment to fully load after login await this.page.waitForLoadState('networkidle'); } catch (error) { clearInterval(loadingInterval); process.stdout.write('\r'); if (error.name === 'TimeoutError') { this.error('Timeout waiting for login (5 minutes)'); } throw error; } } /** * Enhanced error handler */ handleError(error) { const err = typeof error === 'string' ? new Error(error) : error; // Display error with tips if not in JSON mode if (!this.jsonEnabled()) { (0, error_tips_js_1.displayErrorWithTip)(err, this.constructor.name.toLowerCase()); } // Call parent error method this.exit(1); } /** * Log error with tips but don't exit */ logError(error, command) { if (!this.jsonEnabled()) { (0, error_tips_js_1.displayErrorWithTip)(error, command || this.constructor.name.toLowerCase()); } else { this.logToStderr(error.message); } } async finally() { if (!this.browser) return; // 1️⃣ We launched Chrome in this process ----------------------------- if (this.isLaunched) { if (this.keepOpen) { this.log('🔓 Keeping Chrome open (use --no-keepOpen to close automatically)'); } else { this.log('🔒 Closing Chrome (launched by this command)…'); await this.browser.close(); } return; } // 2️⃣ We only attached to an *external* Chrome ----------------------- // Close the CDP *connection* so Node's event‑loop can exit, but // *do not* close the user's Chrome window. this.logVerbose('Disconnecting from existing Chrome…'); await this.browser.close(); } } exports.BaseCommand = BaseCommand;