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
JavaScript
;
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;