UNPKG

chromancer

Version:

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

297 lines (296 loc) • 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const core_1 = require("@oclif/core"); const net = tslib_1.__importStar(require("net")); const path = tslib_1.__importStar(require("path")); const os = tslib_1.__importStar(require("os")); const child_process_1 = require("child_process"); const session_js_1 = require("../session.js"); const chrome_scanner_js_1 = require("../utils/chrome-scanner.js"); class Spawn extends core_1.Command { static description = 'Launch Chrome with remote debugging enabled using Playwright for reliable automation - supports auto-launch, profiles, and headless mode'; static examples = [ '<%= config.bin %> <%= command.id %> # Default: temporary profile', '<%= config.bin %> <%= command.id %> --profile work # Use saved profile', '<%= config.bin %> <%= command.id %> --port 9223 # Different port', '<%= config.bin %> <%= command.id %> --headless # Headless mode', '<%= config.bin %> <%= command.id %> https://example.com # Open URL', ]; static flags = { port: core_1.Flags.integer({ char: 'p', description: 'Remote debugging port', default: 9222, }), headless: core_1.Flags.boolean({ description: 'Run Chrome in headless mode', default: false, }), profile: core_1.Flags.string({ description: 'Chrome profile name or path to use', }), 'use-default-profile': core_1.Flags.boolean({ description: 'Use default Chrome profile (WARNING: shows profile picker, breaks debugging)', default: false, }), 'wait-for-ready': core_1.Flags.boolean({ description: 'Wait for Chrome to be fully ready before returning', default: true, allowNo: true, }), }; static args = { url: core_1.Args.string({ description: 'URL to open in Chrome', required: false, default: 'about:blank', }), }; 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 run() { const { args, flags } = await this.parse(Spawn); try { // First, scan for any existing Chrome instances const existingInstances = await (0, chrome_scanner_js_1.scanForChromeInstances)(); if (existingInstances.length > 0) { this.log(`šŸ” Found existing Chrome instance(s):`); for (const instance of existingInstances) { this.log(` Port ${instance.port}: ${instance.version?.Browser || 'Chrome'}`); } // Check if our target port is already in use if (existingInstances.some(i => i.port === flags.port)) { this.log(`\nāš ļø Chrome is already running on port ${flags.port}`); this.log(`šŸ’” Use 'chromancer stop' to close it first, or use a different port with --port`); return; } } this.log(`šŸš€ Launching Chrome with remote debugging on port ${flags.port}...`); // Find Chrome executable const chromeExecutable = this.findChromeExecutable(); if (!chromeExecutable) { this.error('Chrome executable not found. Please install Chrome or Chromium.'); } this.log(`šŸ“ Found Chrome at: ${chromeExecutable}`); // Build Chrome args const chromeArgs = [ `--remote-debugging-port=${flags.port}`, '--no-first-run', '--no-default-browser-check', ]; // Add container-specific flags if needed if (process.env.DEVCONTAINER === 'true' || process.env.CHROME_FLAGS) { chromeArgs.push('--no-sandbox', '--disable-dev-shm-usage'); } if (flags.headless) { chromeArgs.push('--headless'); } if (flags.profile) { // Use specified profile const profilePath = this.getProfilePath(flags.profile); this.log(`šŸ“ Using Chrome profile: ${profilePath}`); chromeArgs.push(`--user-data-dir=${profilePath}`); } else if (flags['use-default-profile']) { // User explicitly wants default profile - warn them this.warn(`āš ļø WARNING: Using default Chrome profile will show profile picker`); this.warn(`āš ļø The debugger CANNOT connect when profile picker is shown!`); this.log(`\nā“ Continue anyway? The debugger will likely fail to connect.`); // Add a delay to let user read the warning await new Promise(resolve => setTimeout(resolve, 2000)); // Don't add any user-data-dir, let Chrome use its default } else { // Default behavior: use a temporary profile to avoid profile picker const tempDir = path.join(os.tmpdir(), `chromancer-temp-${Date.now()}`); chromeArgs.push(`--user-data-dir=${tempDir}`); this.log(`šŸ”’ Using temporary profile (avoids profile picker)`); this.log(`šŸ’” Use --profile NAME to use a specific profile`); } // Add the URL if provided if (args.url && args.url !== 'about:blank') { chromeArgs.push(args.url); } // Launch Chrome detached const spawnOptions = { detached: true, stdio: 'ignore', }; // On Windows, we need to use shell to properly detach if (process.platform === 'win32') { spawnOptions.shell = true; } const chromeProcess = (0, child_process_1.spawn)(chromeExecutable, chromeArgs, spawnOptions); // Allow the parent process to exit chromeProcess.unref(); // Save session info session_js_1.SessionManager.saveSession({ port: flags.port, pid: chromeProcess.pid, startTime: Date.now(), url: args.url, }); // Wait for Chrome to be ready if requested let chromeStarted = false; if (flags['wait-for-ready']) { this.log(`ā³ Waiting for Chrome to be ready...`); chromeStarted = await (0, chrome_scanner_js_1.waitForChromeReady)(flags.port, 'localhost', 10, 1000); if (!chromeStarted) { // Try scanning for Chrome on other ports in case profile picker changed the port this.log(`šŸ” Scanning for Chrome instances...`); const instances = await (0, chrome_scanner_js_1.scanForChromeInstances)(); if (instances.length > 0) { this.log(`\nāš ļø Chrome may have started on a different port:`); for (const instance of instances) { this.log(` Port ${instance.port}: ${instance.version?.Browser || 'Chrome'}`); } this.log(`\nšŸ’” Try connecting with: chromancer navigate example.com --port ${instances[0].port}`); return; } } } else { // Don't wait, just return immediately this.log(`šŸš€ Chrome launching in background (not waiting for ready state)`); } if (chromeStarted) { this.log(`āœ… Chrome launched successfully!`); this.log(`šŸ”— Remote debugging URL: http://localhost:${flags.port}`); if (flags.profile) { this.log(`šŸ“ Profile: ${flags.profile}`); } this.log(`\nšŸ’” Chrome is running in the background. Use these commands:`); this.log(` chromancer navigate example.com`); this.log(` chromancer click "#button"`); this.log(` chromancer stop # To close Chrome`); } else { // Even if we can't verify, Chrome might still be starting up this.log(`āš ļø Chrome launched but could not verify connection on port ${flags.port}`); this.log(` Chrome may still be starting up. Try these commands in a moment:`); this.log(` chromancer navigate example.com`); this.log(` chromancer stop # To close Chrome`); } } catch (error) { this.error(`Failed to spawn Chrome: ${error.message}`); } } async checkPort(port) { return new Promise((resolve) => { const server = net.createServer(); server.once('error', () => resolve(true)); server.once('listening', () => { server.close(); resolve(false); }); server.listen(port); }); } findChromeExecutable() { const platform = process.platform; // Common Chrome executable names const executables = [ 'google-chrome', 'google-chrome-stable', 'google-chrome-beta', 'google-chrome-dev', 'chromium', 'chromium-browser', 'chrome', ]; try { if (platform === 'darwin') { // macOS paths const paths = [ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', '/usr/bin/chromium-browser', '/usr/bin/google-chrome', ]; for (const chromePath of paths) { try { (0, child_process_1.execSync)(`test -f "${chromePath}"`, { stdio: 'ignore' }); return chromePath; } catch { // Continue checking } } } else if (platform === 'win32') { // Windows paths const paths = [ process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe', process.env.PROGRAMFILES + '\\Google\\Chrome\\Application\\chrome.exe', process.env['PROGRAMFILES(X86)'] + '\\Google\\Chrome\\Application\\chrome.exe', process.env.LOCALAPPDATA + '\\Chromium\\Application\\chrome.exe', ]; for (const chromePath of paths) { try { (0, child_process_1.execSync)(`if exist "${chromePath}" exit 0`, { stdio: 'ignore', shell: 'cmd.exe' }); return chromePath; } catch { // Continue checking } } } else { // Linux - check common paths first const linuxPaths = [ '/usr/bin/google-chrome-stable', '/usr/bin/google-chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium', '/snap/bin/chromium', ]; for (const chromePath of linuxPaths) { try { (0, child_process_1.execSync)(`test -f "${chromePath}"`, { stdio: 'ignore' }); return chromePath; } catch { // Continue checking } } // Fall back to which command for (const exe of executables) { try { const result = (0, child_process_1.execSync)(`which ${exe} 2>/dev/null`, { encoding: 'utf8' }).trim(); if (result) { return result; } } catch { // Continue checking } } } } catch (error) { // Ignore errors and return null } return null; } } exports.default = Spawn;