dev-with-debug
Version:
Auto-start Chrome-based dev server with debugging enabled, streaming LLM-friendly error output
717 lines (587 loc) โข 23.8 kB
JavaScript
const fs = require('fs');
const path = require('path');
const spawn = require('cross-spawn');
const chromeLauncher = require('chrome-launcher');
const CDP = require('chrome-remote-interface');
class DevWithDebug {
constructor() {
this.chrome = null;
this.cdp = null;
this.devServer = null;
}
// Find available Chromium-based browser
findChromiumBrowser() {
const fs = require('fs');
const os = require('os');
let possiblePaths = [];
// Platform-specific browser paths
switch (os.platform()) {
case 'darwin': // macOS
possiblePaths = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
'/Applications/Opera.app/Contents/MacOS/Opera',
'/Applications/Chromium.app/Contents/MacOS/Chromium'
];
break;
case 'win32': // Windows
const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files';
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
const localAppData = process.env['LOCALAPPDATA'] || `${os.homedir()}\\AppData\\Local`;
possiblePaths = [
`${programFiles}\\Google\\Chrome\\Application\\chrome.exe`,
`${programFilesX86}\\Google\\Chrome\\Application\\chrome.exe`,
`${localAppData}\\Google\\Chrome\\Application\\chrome.exe`,
`${programFiles}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
`${programFilesX86}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
`${localAppData}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`,
`${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`,
`${programFilesX86}\\Microsoft\\Edge\\Application\\msedge.exe`,
`${programFiles}\\Opera\\opera.exe`,
`${programFilesX86}\\Opera\\opera.exe`,
`${localAppData}\\Programs\\Opera\\opera.exe`
];
break;
case 'linux': // Linux
possiblePaths = [
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/usr/bin/brave-browser',
'/usr/bin/microsoft-edge',
'/usr/bin/opera',
'/snap/bin/chromium',
'/snap/bin/brave',
'/opt/google/chrome/chrome',
'/opt/brave.com/brave/brave-browser'
];
break;
default:
console.log(`โ ๏ธ Unsupported platform: ${os.platform()}`);
return undefined;
}
for (const browserPath of possiblePaths) {
if (fs.existsSync(browserPath)) {
console.log(`๐ Using browser: ${browserPath}`);
return browserPath;
}
}
// Let chrome-launcher auto-detect if none found
return undefined;
}
// Debug what's actually listening on the system
async debugSystemPorts() {
const { spawn } = require('child_process');
console.log(`๐ Debugging system ports...`);
try {
// Check what's listening on localhost
const lsofResult = await new Promise((resolve, reject) => {
const proc = spawn('lsof', ['-i', ':9222'], { stdio: 'pipe' });
let output = '';
proc.stdout.on('data', data => output += data.toString());
proc.on('close', code => resolve(output));
proc.on('error', reject);
});
console.log(`๐ Processes on port 9222:\n${lsofResult}`);
// Check chrome processes
const psResult = await new Promise((resolve, reject) => {
const proc = spawn('ps', ['aux'], { stdio: 'pipe' });
let output = '';
proc.stdout.on('data', data => output += data.toString());
proc.on('close', code => {
const chromeLines = output.split('\n').filter(line =>
line.includes('Brave') || line.includes('Chrome') || line.includes('chromium')
);
resolve(chromeLines.join('\n'));
});
proc.on('error', reject);
});
console.log(`๐ฅ๏ธ Browser processes:\n${psResult}`);
} catch (err) {
console.log(`โ ๏ธ Could not debug system ports: ${err.message}`);
}
}
// Wait for debugging port to be ready
async waitForDebugPort(port, maxRetries = 15) {
const http = require('http');
console.log(`๐ Checking if debug port ${port} is ready...`);
// First, debug what's actually running
await this.debugSystemPorts();
for (let i = 0; i < maxRetries; i++) {
try {
console.log(`๐ Trying to connect to http://localhost:${port}/json/version`);
const response = await new Promise((resolve, reject) => {
const req = http.get(`http://localhost:${port}/json/version`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
resolve({ status: res.statusCode, data, headers: res.headers });
});
});
req.on('error', (err) => {
reject(err);
});
req.setTimeout(3000, () => {
req.destroy();
reject(new Error('Timeout'));
});
});
console.log(`๐ Debug endpoint response (${response.status}): ${response.data}`);
if (response.status === 200) {
console.log(`โ
Debug port ${port} is ready`);
return;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (err) {
console.log(`โ ๏ธ Debug port ${port} attempt ${i + 1}/${maxRetries} failed: ${err.message}`);
if (i === maxRetries - 1) {
console.error(`โ Debug port ${port} failed after ${maxRetries} attempts`);
console.error(` This suggests the browser is not running with debugging enabled`);
console.error(` Let's try to understand why...`);
// Final system debug
await this.debugSystemPorts();
throw new Error(`Debug port ${port} not ready: ${err.message}`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
}
// Detect the dev server command from package.json
detectDevCommand() {
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (!fs.existsSync(packageJsonPath)) {
throw new Error('No package.json found in current directory');
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const scripts = packageJson.scripts || {};
// Common dev server script names
const devCommands = ['dev', 'start', 'serve', 'develop'];
for (const cmd of devCommands) {
if (scripts[cmd]) {
return { command: 'npm', args: ['run', cmd] };
}
}
// Check for specific framework patterns
if (scripts.vite) return { command: 'npm', args: ['run', 'vite'] };
if (scripts['webpack-dev-server']) return { command: 'npm', args: ['run', 'webpack-dev-server'] };
throw new Error('No dev server command found. Please ensure you have a "dev", "start", or "serve" script in package.json');
}
// Parse actual DevTools port from error logs
parseActualDevToolsPort(errorMessage) {
const match = errorMessage.match(/DevTools listening on ws:\/\/127\.0\.0\.1:(\d+)/);
return match ? parseInt(match[1]) : null;
}
// Discover actual debugging port
async discoverActualPort() {
const http = require('http');
// Try to find the actual port by checking what chrome-launcher returned
console.log(`๐ Chrome launcher returned port: ${this.chrome.port}`);
console.log(`๐ Chrome launcher object keys: ${Object.keys(this.chrome)}`);
console.log(`๐ Chrome launcher full object:`, this.chrome);
// Check if there are any other port-related properties
const portProperties = Object.keys(this.chrome).filter(key =>
key.toLowerCase().includes('port') || key.toLowerCase().includes('debug')
);
console.log(`๐ Port-related properties: ${portProperties}`);
// Also try to query the browser directly for debugging endpoints
try {
const commonPorts = [this.chrome.port, 9222, 9223, 9224, 9225];
for (const port of commonPorts) {
try {
console.log(`๐ Testing port ${port} for debugging endpoint...`);
const response = await new Promise((resolve, reject) => {
const req = http.get(`http://localhost:${port}/json/version`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({ port, status: res.statusCode, data }));
});
req.on('error', reject);
req.setTimeout(1000, () => {
req.destroy();
reject(new Error('Timeout'));
});
});
if (response.status === 200) {
console.log(`โ
Found working debugging port: ${response.port}`);
console.log(`๐ Version data: ${response.data}`);
return response.port;
}
} catch (err) {
console.log(`โ Port ${port} failed: ${err.message}`);
}
}
throw new Error('No working debugging port found');
} catch (err) {
throw new Error(`Could not discover actual debugging port: ${err.message}`);
}
}
// Read DevToolsActivePort file to get actual debugging port
async readDevToolsActivePort(userDataDir) {
const fs = require('fs');
const path = require('path');
const portFilePath = path.join(userDataDir, 'DevToolsActivePort');
try {
if (fs.existsSync(portFilePath)) {
const content = fs.readFileSync(portFilePath, 'utf8');
const port = parseInt(content.split('\n')[0]);
console.log(`๐ DevToolsActivePort file shows port: ${port}`);
return port;
}
} catch (err) {
console.log(`โ ๏ธ Could not read DevToolsActivePort: ${err.message}`);
}
return null;
}
// Launch Chrome with debugging enabled
async launchChrome(devServerUrl = 'http://localhost:3000') {
console.log(`๐ Launching browser with URL: ${devServerUrl}`);
const browserPath = this.findChromiumBrowser();
const os = require('os');
const path = require('path');
const fs = require('fs');
// Create a cross-platform temp directory
const userDataDir = path.join(os.tmpdir(), 'dev-with-debug-chrome');
// Ensure the user data directory exists
if (!fs.existsSync(userDataDir)) {
fs.mkdirSync(userDataDir, { recursive: true });
console.log(`๐ Created user data directory: ${userDataDir}`);
}
console.log(`๐ง Chrome launcher config:`);
console.log(` Browser path: ${browserPath}`);
console.log(` Starting URL: ${devServerUrl}`);
console.log(` User data dir: ${userDataDir}`);
// Use FIXED debugging port as recommended in the documentation
const fixedDebugPort = 9222;
this.chrome = await chromeLauncher.launch({
chromePath: browserPath,
startingUrl: devServerUrl,
userDataDir: userDataDir,
port: fixedDebugPort,
chromeFlags: [
'--disable-features=VizDisplayCompositor',
'--no-first-run',
'--auto-open-devtools-for-tabs'
]
});
console.log(`๐ Browser launched successfully!`);
console.log(` Process ID: ${this.chrome.pid}`);
console.log(` Chrome-launcher returned port: ${this.chrome.port}`);
console.log(` Expected debugging port: ${fixedDebugPort}`);
// Force the port to our fixed port regardless of what chrome-launcher says
this.chrome.port = fixedDebugPort;
console.log(`๐ Forcing debugging port to: ${fixedDebugPort}`);
// Wait a moment for browser to fully start
await new Promise(resolve => setTimeout(resolve, 2000));
// Try to read the actual port from DevToolsActivePort file for verification
const activePort = await this.readDevToolsActivePort(userDataDir);
if (activePort && activePort !== fixedDebugPort) {
console.log(`โ ๏ธ DevToolsActivePort shows different port: ${activePort}`);
console.log(` But we'll try our fixed port first: ${fixedDebugPort}`);
}
// Verify the port is accessible
await this.waitForDebugPort(fixedDebugPort, 5); // Shorter retry since we know the port
// Connect to Chrome DevTools Protocol
await this.connectToCDP();
console.log(`โ
Connected to Chrome DevTools Protocol`);
}
// Manual port discovery without chrome-launcher
async discoverActualPortManual() {
const http = require('http');
const commonPorts = [9222, 9223, 9224, 9225, 9226, 9227, 9228, 9229];
console.log(`๐ Scanning common debugging ports...`);
for (const port of commonPorts) {
try {
const response = await new Promise((resolve, reject) => {
const req = http.get(`http://localhost:${port}/json/version`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({ port, status: res.statusCode, data }));
});
req.on('error', reject);
req.setTimeout(500, () => {
req.destroy();
reject(new Error('Timeout'));
});
});
if (response.status === 200) {
console.log(`โ
Found active debugging port: ${response.port}`);
return response.port;
}
} catch (err) {
// Port not available, continue
}
}
return null;
}
// Debug manual browser launch
async debugManualLaunch(browserPath, url) {
const { spawn } = require('child_process');
try {
console.log(`๐งช Testing manual launch: ${browserPath}`);
const testProcess = spawn(browserPath, [
'--remote-debugging-port=9222',
'--no-first-run',
url
], {
stdio: 'pipe',
detached: true
});
let output = '';
testProcess.stdout.on('data', data => output += data.toString());
testProcess.stderr.on('data', data => output += data.toString());
setTimeout(() => {
console.log(`๐ Manual launch output: ${output.slice(0, 500)}`);
testProcess.kill();
}, 3000);
} catch (err) {
console.log(`โ ๏ธ Manual launch test failed: ${err.message}`);
}
}
// Connect to CDP with retry logic
async connectToCDP(maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
console.log(`๐ Attempting CDP connection... (attempt ${i + 1}/${maxRetries})`);
this.cdp = await CDP({ port: this.chrome.port });
const { Page, Runtime, Console } = this.cdp;
// Enable domains
await Page.enable();
await Runtime.enable();
await Console.enable();
// Set up error listeners
this.setupErrorListeners();
return; // Success, exit retry loop
} catch (err) {
if (i === maxRetries - 1) {
throw new Error(`Failed to connect to CDP after ${maxRetries} attempts: ${err.message}`);
}
console.log(`โ ๏ธ CDP connection failed (attempt ${i + 1}): ${err.message}`);
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
}
// Set up Chrome DevTools Protocol listeners
setupErrorListeners() {
const { Runtime, Console } = this.cdp;
// Runtime errors
Runtime.exceptionThrown((params) => {
this.formatAndLogError('RUNTIME_ERROR', params.exceptionDetails);
});
// Console errors
Console.messageAdded((params) => {
if (params.message.level === 'error') {
this.formatAndLogError('CONSOLE_ERROR', params.message);
}
});
// Unhandled promise rejections
Runtime.consoleAPICalled((params) => {
if (params.type === 'error') {
this.formatAndLogError('CONSOLE_API_ERROR', params);
}
});
}
// Format errors for LLM consumption
formatAndLogError(type, details) {
const timestamp = new Date().toISOString();
let formatted = {
timestamp,
type,
message: '',
stack: '',
location: ''
};
try {
switch (type) {
case 'RUNTIME_ERROR':
formatted.message = details.exception?.description || details.text || 'Unknown runtime error';
formatted.stack = details.exception?.stack || '';
formatted.location = `${details.url}:${details.lineNumber}:${details.columnNumber}`;
break;
case 'CONSOLE_ERROR':
formatted.message = details.text || 'Console error';
formatted.location = `${details.url}:${details.line}:${details.column}`;
break;
case 'CONSOLE_API_ERROR':
formatted.message = details.args?.map(arg => arg.value).join(' ') || 'Console API error';
formatted.location = details.stackTrace?.callFrames?.[0] ?
`${details.stackTrace.callFrames[0].url}:${details.stackTrace.callFrames[0].lineNumber}` : '';
break;
}
// Output in LLM-friendly format
console.log('\n๐จ DEBUG ERROR:');
console.log(`Type: ${formatted.type}`);
console.log(`Time: ${formatted.timestamp}`);
console.log(`Message: ${formatted.message}`);
if (formatted.location) console.log(`Location: ${formatted.location}`);
if (formatted.stack) console.log(`Stack: ${formatted.stack.split('\n')[0]}`); // First line only for brevity
console.log('โ'.repeat(50));
} catch (err) {
console.error('Error formatting debug output:', err.message);
}
}
// Parse dev server URL from output
parseDevServerUrl(output) {
// Common patterns for dev server URLs
const patterns = [
/Local:\s+https?:\/\/[^\s]+/gi,
/http:\/\/localhost:\d+/gi,
/https:\/\/localhost:\d+/gi,
/http:\/\/127\.0\.0\.1:\d+/gi,
/https:\/\/127\.0\.0\.1:\d+/gi,
/Server running at\s+https?:\/\/[^\s]+/gi,
/Available on:\s*\n.*?https?:\/\/[^\s]+/gi,
/Dev server running at:\s*https?:\/\/[^\s]+/gi
];
for (const pattern of patterns) {
const matches = output.match(pattern);
if (matches) {
// Extract just the URL part
const urlMatch = matches[0].match(/(https?:\/\/[^\s]+)/);
if (urlMatch) {
return urlMatch[1].replace(/\/$/, ''); // Remove trailing slash
}
}
}
return null;
}
// Wait for dev server to be ready
async waitForDevServer(url, maxRetries = 20) {
const http = require('http');
console.log(`๐ Waiting for dev server at ${url}...`);
for (let i = 0; i < maxRetries; i++) {
try {
await new Promise((resolve, reject) => {
const req = http.get(url, (res) => {
resolve();
});
req.on('error', reject);
req.setTimeout(1000, () => {
req.destroy();
reject(new Error('Timeout'));
});
});
console.log(`โ
Dev server is ready at ${url}`);
return url;
} catch (err) {
if (i === maxRetries - 1) {
console.error(`โ Dev server not ready after ${maxRetries} attempts`);
throw new Error(`Dev server not ready at ${url}`);
}
console.log(`โณ Waiting for dev server... (attempt ${i + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
}
// Start the dev server
async startDevServer() {
const devCommand = this.detectDevCommand();
console.log(`๐ Starting dev server: ${devCommand.command} ${devCommand.args.join(' ')}`);
let devServerOutput = '';
let detectedUrl = null;
this.devServer = spawn(devCommand.command, devCommand.args, {
stdio: ['inherit', 'pipe', 'pipe'],
env: { ...process.env, NODE_ENV: 'development' }
});
// Capture output to detect URL
this.devServer.stdout.on('data', (data) => {
const chunk = data.toString();
process.stdout.write(chunk); // Still show output to user
devServerOutput += chunk;
// Try to detect URL from new output
if (!detectedUrl) {
detectedUrl = this.parseDevServerUrl(devServerOutput);
if (detectedUrl) {
console.log(`๐ Detected dev server URL: ${detectedUrl}`);
}
}
});
this.devServer.stderr.on('data', (data) => {
const chunk = data.toString();
process.stderr.write(chunk); // Still show errors to user
devServerOutput += chunk;
// Some dev servers output URL to stderr
if (!detectedUrl) {
detectedUrl = this.parseDevServerUrl(devServerOutput);
if (detectedUrl) {
console.log(`๐ Detected dev server URL: ${detectedUrl}`);
}
}
});
this.devServer.on('error', (err) => {
console.error('โ Dev server error:', err.message);
});
// Wait for URL detection or fallback to common ports
let attempts = 0;
const maxAttempts = 30; // 30 seconds
while (!detectedUrl && attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 1000));
attempts++;
if (attempts % 5 === 0) {
console.log(`โณ Still waiting for dev server URL... (${attempts}s)`);
}
}
// Fallback URLs if detection fails
const fallbackUrls = [
'http://localhost:3000',
'http://localhost:8080',
'http://localhost:4200',
'http://localhost:5000',
'http://localhost:8000',
'http://localhost:3001'
];
if (!detectedUrl) {
console.log(`โ ๏ธ Could not detect dev server URL from output. Trying common ports...`);
for (const url of fallbackUrls) {
try {
await this.waitForDevServer(url, 3); // Quick check
detectedUrl = url;
break;
} catch (err) {
// Continue to next URL
}
}
}
if (!detectedUrl) {
throw new Error('Could not detect dev server URL. Please ensure your dev server is running and outputs its URL.');
}
// Final verification that URL is accessible
await this.waitForDevServer(detectedUrl);
return detectedUrl;
}
// Main execution function
async start() {
try {
console.log('๐ง Starting dev-with-debug...');
const devServerUrl = await this.startDevServer();
await this.launchChrome(devServerUrl);
console.log('โ
Debug session active. Chrome DevTools connected.');
console.log('๐ Monitoring for errors...\n');
// Keep process alive
process.on('SIGINT', () => this.cleanup());
process.on('SIGTERM', () => this.cleanup());
} catch (error) {
console.error('โ Failed to start debug session:', error.message);
await this.cleanup();
process.exit(1);
}
}
// Cleanup function
async cleanup() {
console.log('\n๐ Shutting down debug session...');
if (this.cdp) {
await this.cdp.close();
}
if (this.chrome) {
await this.chrome.kill();
}
if (this.devServer) {
this.devServer.kill();
}
console.log('โ
Cleanup complete');
process.exit(0);
}
}
module.exports = DevWithDebug;