chrome-devtools
Browser automation, debugging, and performance analysis using Puppeteer CLI scripts. Use for automating browsers, taking screenshots, analyzing performance, monitoring network traffic, web scraping, form automation, and JavaScript debugging.
Install
npx skills add https://github.com/VoDaiLocz/kilo-kit-mcp/tree/main/skills/operations/chrome-devtools
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vodailocz-kilo-kit-mcp@llmmart
git clone https://github.com/VoDaiLocz/kilo-kit-mcp.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vodailocz/kilo-kit-mcp collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Chrome DevTools Agent Skill
Browser automation via executable Puppeteer scripts. All scripts output JSON for easy parsing.
Quick Start
CRITICAL: Always check pwd before running scripts.
Installation
Step 1: Install System Dependencies (Linux/WSL only)
On Linux/WSL, Chrome requires system libraries. Install them first:
pwd # Should show current working directory
cd .claude/skills/chrome-devtools/scripts
./install-deps.sh # Auto-detects OS and installs required libs
Supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro
macOS/Windows: Skip this step (dependencies bundled with Chrome)
Step 2: Install Node Dependencies
npm install # Installs puppeteer, debug, yargs
Step 3: Install ImageMagick (Optional, Recommended)
ImageMagick enables automatic screenshot compression to keep files under 5MB:
macOS:
brew install imagemagick
Ubuntu/Debian/WSL:
sudo apt-get install imagemagick
Verify:
magick -version # or: convert -version
Without ImageMagick, screenshots >5MB will not be compressed (may fail to load in Gemini/Claude).
Test
node navigate.js --url https://example.com
# Output: {"success": true, "url": "https://example.com", "title": "Example Domain"}
Available Scripts
All scripts are in .claude/skills/chrome-devtools/scripts/
CRITICAL: Always check pwd before running scripts.
Script Usage
./scripts/README.md
Core Automation
navigate.js- Navigate to URLsscreenshot.js- Capture screenshots (full page or element)click.js- Click elementsfill.js- Fill form fieldsevaluate.js- Execute JavaScript in page context
Analysis & Monitoring
snapshot.js- Extract interactive elements with metadataconsole.js- Monitor console messages/errorsnetwork.js- Track HTTP requests/responsesperformance.js- Measure Core Web Vitals + record traces
Usage Patterns
Single Command
pwd # Should show current working directory
cd .claude/skills/chrome-devtools/scripts
node screenshot.js --url https://example.com --output ./docs/screenshots/page.png
Important: Always save screenshots to ./docs/screenshots directory.
Automatic Image Compression
Screenshots are automatically compressed if they exceed 5MB to ensure compatibility with Gemini API and Claude Code (which have 5MB limits). This uses ImageMagick internally:
# Default: auto-compress if >5MB
node screenshot.js --url https://example.com --output page.png
# Custom size threshold (e.g., 3MB)
node screenshot.js --url https://example.com --output page.png --max-size 3
# Disable compression
node screenshot.js --url https://example.com --output page.png --no-compress
Compression behavior:
- PNG: Resizes to 90% + quality 85 (or 75% + quality 70 if still too large)
- JPEG: Quality 80 + progressive encoding (or quality 60 if still too large)
- Other formats: Converted to JPEG with compression
- Requires ImageMagick installed (see imagemagick skill)
Output includes compression info:
{
"success": true,
"output": "/path/to/page.png",
"compressed": true,
"originalSize": 8388608,
"size": 3145728,
"compressionRatio": "62.50%",
"url": "https://example.com"
}
Chain Commands (reuse browser)
# Keep browser open with --close false
node navigate.js --url https://example.com/login --close false
node fill.js --selector "#email" --value "user@example.com" --close false
node fill.js --selector "#password" --value "secret" --close false
node click.js --selector "button[type=submit]"
Parse JSON Output
# Extract specific fields with jq
node performance.js --url https://example.com | jq '.vitals.LCP'
# Save to file
node network.js --url https://example.com --output /tmp/requests.json
Execution Protocol
Working Directory Verification
BEFORE executing any script:
- Check current working directory with
pwd - Verify in
.claude/skills/chrome-devtools/scripts/directory - If wrong directory,
cdto correct location - Use absolute paths for all output files
Example:
pwd # Should show: .../chrome-devtools/scripts
# If wrong:
cd .claude/skills/chrome-devtools/scripts
Output Validation
AFTER screenshot/capture operations:
- Verify file created with
ls -lh <output-path> - Read screenshot using Read tool to confirm content
- Check JSON output for success:true
- Report file size and compression status
Example:
node screenshot.js --url https://example.com --output ./docs/screenshots/page.png
ls -lh ./docs/screenshots/page.png # Verify file exists
# Then use Read tool to visually inspect
- Restart working directory to the project root.
Error Recovery
If script fails:
- Check error message for selector issues
- Use snapshot.js to discover correct selectors
- Try XPath selector if CSS selector fails
- Verify element is visible and interactive
Example:
# CSS selector fails
node click.js --url https://example.com --selector ".btn-submit"
# Error: waiting for selector ".btn-submit" failed
# Discover correct selector
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'
# Try XPath
node click.js --url https://example.com --selector "//button[contains(text(),'Submit')]"
Common Mistakes
❌ Wrong working directory → output files go to wrong location ❌ Skipping output validation → silent failures ❌ Using complex CSS selectors without testing → selector errors ❌ Not checking element visibility → timeout errors
✅ Always verify pwd before running scripts
✅ Always validate output after screenshots
✅ Use snapshot.js to discover selectors
✅ Test selectors with simple commands first
Common Workflows
Web Scraping
node evaluate.js --url https://example.com --script "
Array.from(document.querySelectorAll('.item')).map(el => ({
title: el.querySelector('h2')?.textContent,
link: el.querySelector('a')?.href
}))
" | jq '.result'
Performance Testing
PERF=$(node performance.js --url https://example.com)
LCP=$(echo $PERF | jq '.vitals.LCP')
if (( $(echo "$LCP < 2500" | bc -l) )); then
echo "✓ LCP passed: ${LCP}ms"
else
echo "✗ LCP failed: ${LCP}ms"
fi
Form Automation
node fill.js --url https://example.com --selector "#search" --value "query" --close false
node click.js --selector "button[type=submit]"
Error Monitoring
node console.js --url https://example.com --types error,warn --duration 5000 | jq '.messageCount'
Script Options
All scripts support:
--headless false- Show browser window--close false- Keep browser open for chaining--timeout 30000- Set timeout (milliseconds)--wait-until networkidle2- Wait strategy
See ./scripts/README.md for complete options.
Output Format
All scripts output JSON to stdout:
{
"success": true,
"url": "https://example.com",
... // script-specific data
}
Errors go to stderr:
{
"success": false,
"error": "Error message"
}
Finding Elements
Use snapshot.js to discover selectors:
node snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'
Troubleshooting
Common Errors
"Cannot find package 'puppeteer'"
- Run:
npm installin the scripts directory
"error while loading shared libraries: libnss3.so" (Linux/WSL)
- Missing system dependencies
- Fix: Run
./install-deps.shin scripts directory - Manual install:
sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1
"Failed to launch the browser process"
- Check system dependencies installed (Linux/WSL)
- Verify Chrome downloaded:
ls ~/.cache/puppeteer - Try:
npm rebuildthennpm install
Chrome not found
- Puppeteer auto-downloads Chrome during
npm install - If failed, manually trigger:
npx puppeteer browsers install chrome
Script Issues
Element not found
- Get snapshot first to find correct selector:
node snapshot.js --url <url>
Script hangs
- Increase timeout:
--timeout 60000 - Change wait strategy:
--wait-until loador--wait-until domcontentloaded
Blank screenshot
- Wait for page load:
--wait-until networkidle2 - Increase timeout:
--timeout 30000
Permission denied on scripts
- Make executable:
chmod +x *.sh
Screenshot too large (>5MB)
- Install ImageMagick for automatic compression
- Manually set lower threshold:
--max-size 3 - Use JPEG format instead of PNG:
--format jpeg --quality 80 - Capture specific element instead of full page:
--selector .main-content
Compression not working
- Verify ImageMagick installed:
magick -versionorconvert -version - Check file was actually compressed in output JSON:
"compressed": true - For very large pages, use
--selectorto capture only needed area
Resilience and Subagent Stability
When using the browser subagent or Puppeteer scripts, you may encounter connectivity issues (e.g., "connection is reset") or tab loss. Follow these guardrails for professional-grade reliability:
1. State Verification First
Rule: Always call list_pages (or a similar verification script) at the start of a new subagent task or if an error occurs.
- Do not assume the tab you just used is still active.
- If a "Connection Reset" error occurs, it usually means the browser instance crashed or closed. Immediately attempt to re-launch or re-initiate the session.
2. Handling Authenticated Sessions
Pattern: Use Persistent Browser Mode for sites requiring login or complex state.
- Launch with
node launch-persistent.js --headless false. - This ensures the browser process stays alive even if the subagent session times out.
- Subsequent subagents can reconnect via the WebSocket endpoint in
.browser-endpoint.
3. Progressive Handoff
Rule: Use --close false consistently if the workflow requires multiple steps.
- When finishing a subagent task, explicitly check if the browser should remain open for the next phase.
- Provide the user with the URL and a screenshot of the final state to confirm continuity.
4. Error Recovery Strategy
If a "Page not found" or "Target closed" error occurs:
- List all open pages to see if the tab was accidentally closed or navigated away.
- Check for session tokens (cookies/local storage) if the site allows session persistence.
- Re-navigate to the last known URL if the tab is truly lost.
- Notify the user clearly about the reset instead of retrying silently 10 times.
5. Interaction Heartbeats
Before a complex click or fill sequence, take a small snapshot or evaluate a simple script (e.g., () => document.readyState) to "ping" the page. If it fails, trigger the recovery strategy early.
Reference Documentation
Detailed guides available in ./references/:
- CDP Domains Reference - 47 Chrome DevTools Protocol domains
- Puppeteer Quick Reference - Complete Puppeteer API patterns
- Performance Analysis Guide - Core Web Vitals optimization
Advanced Usage
Custom Scripts
Create custom scripts using shared library:
import { getBrowser, getPage, closeBrowser, outputJSON } from './lib/browser.js';
// Your automation logic
Direct CDP Access
const client = await page.createCDPSession();
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
See reference documentation for advanced patterns and complete API coverage.
External Resources
Files (kilo-kit-mcp)
-
references
-
cdp-domains.md 17.4 KB
# Chrome DevTools Protocol (CDP) Domains Reference Complete reference of CDP domains and their capabilities for browser automation and debugging. ## Overview CDP is organized into **47 domains**, each providing specific browser capabilities. Domains are grouped by functionality: - **Core** - Fundamental browser control - **DOM & Styling** - Page structure and styling - **Network & Fetch** - HTTP traffic management - **Page & Navigation** - Page lifecycle control - **Storage & Data** - Browser storage APIs - **Performance & Profiling** - Metrics and analysis - **Emulation & Simulation** - Device and network emulation - **Worker & Service** - Background tasks - **Developer Tools** - Debugging support --- ## Core Domains ### Runtime **Purpose:** Execute JavaScript, manage objects, handle promises **Key Commands:** - `Runtime.evaluate(expression)` - Execute JavaScript - `Runtime.callFunctionOn(functionDeclaration, objectId)` - Call function on object - `Runtime.getProperties(objectId)` - Get object properties - `Runtime.awaitPromise(promiseObjectId)` - Wait for promise resolution **Key Events:** - `Runtime.consoleAPICalled` - Console message logged - `Runtime.exceptionThrown` - Uncaught exception **Use Cases:** - Execute custom JavaScript - Access page data - Monitor console output - Handle exceptions --- ### Debugger **Purpose:** JavaScript debugging, breakpoints, stack traces **Key Commands:** - `Debugger.enable()` - Enable debugger - `Debugger.setBreakpoint(location)` - Set breakpoint - `Debugger.pause()` - Pause execution - `Debugger.resume()` - Resume execution - `Debugger.stepOver/stepInto/stepOut()` - Step through code **Key Events:** - `Debugger.paused` - Execution paused - `Debugger.resumed` - Execution resumed - `Debugger.scriptParsed` - Script loaded **Use Cases:** - Debug JavaScript errors - Inspect call stacks - Set conditional breakpoints - Source map support --- ### Console (Deprecated - Use Runtime/Log) **Purpose:** Legacy console message access **Note:** Use `Runtime.consoleAPICalled` event instead for new implementations. --- ## DOM & Styling Domains ### DOM **Purpose:** Access and manipulate DOM tree **Key Commands:** - `DOM.getDocument()` - Get root document node - `DOM.querySelector(nodeId, selector)` - Query selector - `DOM.querySelectorAll(nodeId, selector)` - Query all - `DOM.getAttributes(nodeId)` - Get element attributes - `DOM.setOuterHTML(nodeId, outerHTML)` - Replace element - `DOM.getBoxModel(nodeId)` - Get element layout box - `DOM.focus(nodeId)` - Focus element **Key Events:** - `DOM.documentUpdated` - Document changed - `DOM.setChildNodes` - Child nodes updated **Use Cases:** - Navigate DOM tree - Query elements - Modify DOM structure - Get element positions --- ### CSS **Purpose:** Inspect and modify CSS styles **Key Commands:** - `CSS.enable()` - Enable CSS domain - `CSS.getComputedStyleForNode(nodeId)` - Get computed styles - `CSS.getInlineStylesForNode(nodeId)` - Get inline styles - `CSS.getMatchedStylesForNode(nodeId)` - Get matched CSS rules - `CSS.setStyleTexts(edits)` - Modify styles **Key Events:** - `CSS.styleSheetAdded` - Stylesheet added - `CSS.styleSheetChanged` - Stylesheet modified **Use Cases:** - Inspect element styles - Debug CSS issues - Modify styles dynamically - Extract stylesheet data --- ### Accessibility **Purpose:** Access accessibility tree **Key Commands:** - `Accessibility.enable()` - Enable accessibility - `Accessibility.getFullAXTree()` - Get complete AX tree - `Accessibility.getPartialAXTree(nodeId)` - Get node subtree - `Accessibility.queryAXTree(nodeId, role, name)` - Query AX tree **Use Cases:** - Accessibility testing - Screen reader simulation - ARIA attribute inspection - AX tree analysis --- ## Network & Fetch Domains ### Network **Purpose:** Monitor and control HTTP traffic **Key Commands:** - `Network.enable()` - Enable network tracking - `Network.setCacheDisabled(cacheDisabled)` - Disable cache - `Network.setExtraHTTPHeaders(headers)` - Add custom headers - `Network.getCookies(urls)` - Get cookies - `Network.setCookie(name, value, domain)` - Set cookie - `Network.getResponseBody(requestId)` - Get response body - `Network.emulateNetworkConditions(offline, latency, downloadThroughput, uploadThroughput)` - Throttle network **Key Events:** - `Network.requestWillBeSent` - Request starting - `Network.responseReceived` - Response received - `Network.loadingFinished` - Request completed - `Network.loadingFailed` - Request failed **Use Cases:** - Monitor API calls - Intercept requests - Analyze response data - Simulate slow networks - Manage cookies --- ### Fetch **Purpose:** Intercept and modify network requests **Key Commands:** - `Fetch.enable(patterns)` - Enable request interception - `Fetch.continueRequest(requestId, url, method, headers)` - Continue/modify request - `Fetch.fulfillRequest(requestId, responseCode, headers, body)` - Mock response - `Fetch.failRequest(requestId, errorReason)` - Fail request **Key Events:** - `Fetch.requestPaused` - Request intercepted **Use Cases:** - Mock API responses - Block requests - Modify request/response - Test error scenarios --- ## Page & Navigation Domains ### Page **Purpose:** Control page lifecycle and navigation **Key Commands:** - `Page.enable()` - Enable page domain - `Page.navigate(url)` - Navigate to URL - `Page.reload(ignoreCache)` - Reload page - `Page.goBack()/goForward()` - Navigate history - `Page.captureScreenshot(format, quality)` - Take screenshot - `Page.printToPDF(landscape, displayHeaderFooter)` - Generate PDF - `Page.getLayoutMetrics()` - Get page dimensions - `Page.createIsolatedWorld(frameId)` - Create isolated context - `Page.handleJavaScriptDialog(accept, promptText)` - Handle alerts/confirms **Key Events:** - `Page.loadEventFired` - Page loaded - `Page.domContentEventFired` - DOM ready - `Page.frameNavigated` - Frame navigated - `Page.javascriptDialogOpening` - Alert/confirm shown **Use Cases:** - Navigate pages - Capture screenshots - Generate PDFs - Handle popups - Monitor page lifecycle --- ### Target **Purpose:** Manage browser targets (tabs, workers, frames) **Key Commands:** - `Target.getTargets()` - List all targets - `Target.createTarget(url)` - Open new tab - `Target.closeTarget(targetId)` - Close tab - `Target.attachToTarget(targetId)` - Attach debugger - `Target.detachFromTarget(sessionId)` - Detach debugger - `Target.setDiscoverTargets(discover)` - Auto-discover targets **Key Events:** - `Target.targetCreated` - New target created - `Target.targetDestroyed` - Target closed - `Target.targetInfoChanged` - Target updated **Use Cases:** - Multi-tab automation - Service worker debugging - Frame inspection - Extension debugging --- ### Input **Purpose:** Simulate user input **Key Commands:** - `Input.dispatchKeyEvent(type, key, code)` - Keyboard input - `Input.dispatchMouseEvent(type, x, y, button)` - Mouse input - `Input.dispatchTouchEvent(type, touchPoints)` - Touch input - `Input.synthesizePinchGesture(x, y, scaleFactor)` - Pinch gesture - `Input.synthesizeScrollGesture(x, y, xDistance, yDistance)` - Scroll **Use Cases:** - Simulate clicks - Type text - Drag and drop - Touch gestures - Scroll pages --- ## Storage & Data Domains ### Storage **Purpose:** Manage browser storage **Key Commands:** - `Storage.getCookies(browserContextId)` - Get cookies - `Storage.setCookies(cookies)` - Set cookies - `Storage.clearCookies(browserContextId)` - Clear cookies - `Storage.clearDataForOrigin(origin, storageTypes)` - Clear storage - `Storage.getUsageAndQuota(origin)` - Get storage usage **Storage Types:** - appcache, cookies, file_systems, indexeddb, local_storage, shader_cache, websql, service_workers, cache_storage **Use Cases:** - Cookie management - Clear browser data - Inspect storage usage - Test quota limits --- ### DOMStorage **Purpose:** Access localStorage/sessionStorage **Key Commands:** - `DOMStorage.enable()` - Enable storage tracking - `DOMStorage.getDOMStorageItems(storageId)` - Get items - `DOMStorage.setDOMStorageItem(storageId, key, value)` - Set item - `DOMStorage.removeDOMStorageItem(storageId, key)` - Remove item **Key Events:** - `DOMStorage.domStorageItemsCleared` - Storage cleared - `DOMStorage.domStorageItemAdded/Updated/Removed` - Item changed --- ### IndexedDB **Purpose:** Query IndexedDB databases **Key Commands:** - `IndexedDB.requestDatabaseNames(securityOrigin)` - List databases - `IndexedDB.requestDatabase(securityOrigin, databaseName)` - Get DB structure - `IndexedDB.requestData(securityOrigin, databaseName, objectStoreName)` - Query data **Use Cases:** - Inspect IndexedDB data - Debug database issues - Extract stored data --- ### CacheStorage **Purpose:** Manage Cache API **Key Commands:** - `CacheStorage.requestCacheNames(securityOrigin)` - List caches - `CacheStorage.requestCachedResponses(cacheId, securityOrigin)` - List cached responses - `CacheStorage.deleteCache(cacheId)` - Delete cache **Use Cases:** - Service worker cache inspection - Offline functionality testing --- ## Performance & Profiling Domains ### Performance **Purpose:** Collect performance metrics **Key Commands:** - `Performance.enable()` - Enable performance tracking - `Performance.disable()` - Disable tracking - `Performance.getMetrics()` - Get current metrics **Metrics:** - Timestamp, Documents, Frames, JSEventListeners, Nodes, LayoutCount, RecalcStyleCount, LayoutDuration, RecalcStyleDuration, ScriptDuration, TaskDuration, JSHeapUsedSize, JSHeapTotalSize **Use Cases:** - Monitor page metrics - Track memory usage - Measure render times --- ### PerformanceTimeline **Purpose:** Access Performance Timeline API **Key Commands:** - `PerformanceTimeline.enable(eventTypes)` - Subscribe to events **Event Types:** - mark, measure, navigation, resource, longtask, paint, layout-shift **Key Events:** - `PerformanceTimeline.timelineEventAdded` - New performance entry --- ### Tracing **Purpose:** Record Chrome trace **Key Commands:** - `Tracing.start(categories, options)` - Start recording - `Tracing.end()` - Stop recording - `Tracing.requestMemoryDump()` - Capture memory snapshot **Trace Categories:** - blink, cc, devtools, gpu, loading, navigation, rendering, v8, disabled-by-default-* **Key Events:** - `Tracing.dataCollected` - Trace chunk received - `Tracing.tracingComplete` - Recording finished **Use Cases:** - Deep performance analysis - Frame rendering profiling - CPU flame graphs - Memory profiling --- ### Profiler **Purpose:** CPU profiling **Key Commands:** - `Profiler.enable()` - Enable profiler - `Profiler.start()` - Start CPU profiling - `Profiler.stop()` - Stop and get profile **Use Cases:** - Find CPU bottlenecks - Optimize JavaScript - Generate flame graphs --- ### HeapProfiler (via Memory domain) **Purpose:** Memory profiling **Key Commands:** - `Memory.getDOMCounters()` - Get DOM object counts - `Memory.prepareForLeakDetection()` - Prepare leak detection - `Memory.forciblyPurgeJavaScriptMemory()` - Force GC - `Memory.setPressureNotificationsSuppressed(suppressed)` - Control memory warnings - `Memory.simulatePressureNotification(level)` - Simulate memory pressure **Use Cases:** - Detect memory leaks - Analyze heap snapshots - Monitor object counts --- ## Emulation & Simulation Domains ### Emulation **Purpose:** Emulate device conditions **Key Commands:** - `Emulation.setDeviceMetricsOverride(width, height, deviceScaleFactor, mobile)` - Emulate device - `Emulation.setGeolocationOverride(latitude, longitude, accuracy)` - Fake location - `Emulation.setEmulatedMedia(media, features)` - Emulate media type - `Emulation.setTimezoneOverride(timezoneId)` - Override timezone - `Emulation.setLocaleOverride(locale)` - Override language - `Emulation.setUserAgentOverride(userAgent)` - Change user agent **Use Cases:** - Mobile device testing - Geolocation testing - Print media emulation - Timezone/locale testing --- ### DeviceOrientation **Purpose:** Simulate device orientation **Key Commands:** - `DeviceOrientation.setDeviceOrientationOverride(alpha, beta, gamma)` - Set orientation **Use Cases:** - Test accelerometer features - Orientation-dependent layouts --- ## Worker & Service Domains ### ServiceWorker **Purpose:** Manage service workers **Key Commands:** - `ServiceWorker.enable()` - Enable tracking - `ServiceWorker.unregister(scopeURL)` - Unregister worker - `ServiceWorker.startWorker(scopeURL)` - Start worker - `ServiceWorker.stopWorker(versionId)` - Stop worker - `ServiceWorker.inspectWorker(versionId)` - Debug worker **Key Events:** - `ServiceWorker.workerRegistrationUpdated` - Registration changed - `ServiceWorker.workerVersionUpdated` - Version updated --- ### WebAuthn **Purpose:** Simulate WebAuthn/FIDO2 **Key Commands:** - `WebAuthn.enable()` - Enable virtual authenticators - `WebAuthn.addVirtualAuthenticator(options)` - Add virtual device - `WebAuthn.removeVirtualAuthenticator(authenticatorId)` - Remove device - `WebAuthn.addCredential(authenticatorId, credential)` - Add credential **Use Cases:** - Test WebAuthn flows - Simulate biometric auth - Test security keys --- ## Developer Tools Support ### Inspector **Purpose:** Protocol-level debugging **Key Events:** - `Inspector.detached` - Debugger disconnected - `Inspector.targetCrashed` - Target crashed --- ### Log **Purpose:** Collect browser logs **Key Commands:** - `Log.enable()` - Enable log collection - `Log.clear()` - Clear logs **Key Events:** - `Log.entryAdded` - New log entry **Use Cases:** - Collect console logs - Monitor violations - Track deprecations --- ### DOMDebugger **Purpose:** DOM-level debugging **Key Commands:** - `DOMDebugger.setDOMBreakpoint(nodeId, type)` - Break on DOM changes - `DOMDebugger.setEventListenerBreakpoint(eventName)` - Break on event - `DOMDebugger.setXHRBreakpoint(url)` - Break on XHR **Breakpoint Types:** - subtree-modified, attribute-modified, node-removed --- ### DOMSnapshot **Purpose:** Capture complete DOM snapshot **Key Commands:** - `DOMSnapshot.captureSnapshot(computedStyles)` - Capture full DOM **Use Cases:** - Export page structure - Offline analysis - DOM diffing --- ### Audits (Lighthouse Integration) **Purpose:** Run automated audits **Key Commands:** - `Audits.enable()` - Enable audits - `Audits.getEncodingIssues()` - Check encoding issues --- ### LayerTree **Purpose:** Inspect rendering layers **Key Commands:** - `LayerTree.enable()` - Enable layer tracking - `LayerTree.compositingReasons(layerId)` - Get why layer created **Key Events:** - `LayerTree.layerTreeDidChange` - Layers changed **Use Cases:** - Debug rendering performance - Identify layer creation - Optimize compositing --- ## Other Domains ### Browser **Purpose:** Browser-level control **Key Commands:** - `Browser.getVersion()` - Get browser info - `Browser.getBrowserCommandLine()` - Get launch args - `Browser.setPermission(permission, setting, origin)` - Set permissions - `Browser.grantPermissions(permissions, origin)` - Grant permissions **Permissions:** - geolocation, midi, notifications, push, camera, microphone, background-sync, sensors, accessibility-events, clipboard-read, clipboard-write, payment-handler --- ### IO **Purpose:** File I/O operations **Key Commands:** - `IO.read(handle, offset, size)` - Read stream - `IO.close(handle)` - Close stream **Use Cases:** - Read large response bodies - Process binary data --- ### Media **Purpose:** Inspect media players **Key Commands:** - `Media.enable()` - Track media players **Key Events:** - `Media.playerPropertiesChanged` - Player state changed - `Media.playerEventsAdded` - Player events --- ### BackgroundService **Purpose:** Track background services **Key Commands:** - `BackgroundService.startObserving(service)` - Track service **Services:** - backgroundFetch, backgroundSync, pushMessaging, notifications, paymentHandler, periodicBackgroundSync --- ## Domain Dependencies Some domains depend on others and must be enabled in order: ``` Runtime (no dependencies) ↓ DOM (depends on Runtime) ↓ CSS (depends on DOM) Network (no dependencies) Page (depends on Runtime) ↓ Target (depends on Page) Debugger (depends on Runtime) ``` ## Quick Command Reference ### Most Common Commands ```javascript // Navigation Page.navigate(url) Page.reload() // JavaScript Execution Runtime.evaluate(expression) // DOM Access DOM.getDocument() DOM.querySelector(nodeId, selector) // Screenshots Page.captureScreenshot(format, quality) // Network Monitoring Network.enable() // Listen for Network.requestWillBeSent events // Console Messages // Listen for Runtime.consoleAPICalled events // Cookies Network.getCookies(urls) Network.setCookie(...) // Device Emulation Emulation.setDeviceMetricsOverride(width, height, ...) // Performance Performance.getMetrics() Tracing.start(categories) Tracing.end() ``` --- ## Best Practices 1. **Enable domains before use:** Always call `.enable()` for stateful domains 2. **Handle events:** Subscribe to events for real-time updates 3. **Clean up:** Disable domains when done to reduce overhead 4. **Use sessions:** Attach to specific targets for isolated debugging 5. **Handle errors:** Implement proper error handling for command failures 6. **Version awareness:** Check browser version for experimental API support --- ## Additional Resources - [Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/) - Interactive domain browser - [Protocol JSON](https://chromedevtools.github.io/devtools-protocol/tot/json) - Machine-readable specification - [Getting Started with CDP](https://github.com/aslushnikov/getting-started-with-cdp) - [devtools-protocol NPM](https://www.npmjs.com/package/devtools-protocol) - TypeScript definitions -
performance-guide.md 21 KB
# Performance Analysis Guide Comprehensive guide to analyzing web performance using Chrome DevTools Protocol, Puppeteer, and chrome-devtools skill. ## Table of Contents - [Core Web Vitals](#core-web-vitals) - [Performance Tracing](#performance-tracing) - [Network Analysis](#network-analysis) - [JavaScript Performance](#javascript-performance) - [Rendering Performance](#rendering-performance) - [Memory Analysis](#memory-analysis) - [Optimization Strategies](#optimization-strategies) --- ## Core Web Vitals ### Overview Core Web Vitals are Google's standardized metrics for measuring user experience: - **LCP (Largest Contentful Paint)** - Loading performance (< 2.5s good) - **FID (First Input Delay)** - Interactivity (< 100ms good) - **CLS (Cumulative Layout Shift)** - Visual stability (< 0.1 good) ### Measuring with chrome-devtools-mcp ```javascript // Start performance trace await useTool('performance_start_trace', { categories: ['loading', 'rendering', 'scripting'] }); // Navigate to page await useTool('navigate_page', { url: 'https://example.com' }); // Wait for complete load await useTool('wait_for', { waitUntil: 'networkidle' }); // Stop trace and get data await useTool('performance_stop_trace'); // Get AI-powered insights const insights = await useTool('performance_analyze_insight'); // insights will include: // - LCP timing // - FID analysis // - CLS score // - Performance recommendations ``` ### Measuring with Puppeteer ```javascript import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); const page = await browser.newPage(); // Measure Core Web Vitals await page.goto('https://example.com', { waitUntil: 'networkidle2' }); const vitals = await page.evaluate(() => { return new Promise((resolve) => { const vitals = { LCP: null, FID: null, CLS: 0 }; // LCP new PerformanceObserver((list) => { const entries = list.getEntries(); vitals.LCP = entries[entries.length - 1].renderTime || entries[entries.length - 1].loadTime; }).observe({ entryTypes: ['largest-contentful-paint'] }); // FID new PerformanceObserver((list) => { vitals.FID = list.getEntries()[0].processingStart - list.getEntries()[0].startTime; }).observe({ entryTypes: ['first-input'] }); // CLS new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (!entry.hadRecentInput) { vitals.CLS += entry.value; } }); }).observe({ entryTypes: ['layout-shift'] }); // Wait 5 seconds for metrics setTimeout(() => resolve(vitals), 5000); }); }); console.log('Core Web Vitals:', vitals); ``` ### Other Important Metrics **TTFB (Time to First Byte)** ```javascript const ttfb = await page.evaluate(() => { const [navigationEntry] = performance.getEntriesByType('navigation'); return navigationEntry.responseStart - navigationEntry.requestStart; }); ``` **FCP (First Contentful Paint)** ```javascript const fcp = await page.evaluate(() => { const paintEntries = performance.getEntriesByType('paint'); const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint'); return fcpEntry ? fcpEntry.startTime : null; }); ``` **TTI (Time to Interactive)** ```javascript // Requires lighthouse or manual calculation const tti = await page.evaluate(() => { // Complex calculation based on network idle and long tasks // Best to use Lighthouse for accurate TTI }); ``` --- ## Performance Tracing ### Chrome Trace Categories **Loading:** - Page load events - Resource loading - Parser activity **Rendering:** - Layout calculations - Paint operations - Compositing **Scripting:** - JavaScript execution - V8 compilation - Garbage collection **Network:** - HTTP requests - WebSocket traffic - Resource fetching **Input:** - User input processing - Touch/scroll events **GPU:** - GPU operations - Compositing work ### Record Performance Trace **Using chrome-devtools-mcp:** ```javascript // Start trace with specific categories await useTool('performance_start_trace', { categories: ['loading', 'rendering', 'scripting', 'network'] }); // Perform actions await useTool('navigate_page', { url: 'https://example.com' }); await useTool('wait_for', { waitUntil: 'networkidle' }); // Optional: Interact with page await useTool('click', { uid: 'button-uid' }); // Stop trace const traceData = await useTool('performance_stop_trace'); // Analyze trace const insights = await useTool('performance_analyze_insight'); ``` **Using Puppeteer:** ```javascript // Start tracing await page.tracing.start({ path: 'trace.json', categories: [ 'devtools.timeline', 'disabled-by-default-devtools.timeline', 'disabled-by-default-v8.cpu_profiler' ] }); // Navigate await page.goto('https://example.com', { waitUntil: 'networkidle2' }); // Stop tracing await page.tracing.stop(); // Analyze in Chrome DevTools (chrome://tracing) ``` ### Analyze Trace Data **Key Metrics from Trace:** 1. **Main Thread Activity** - JavaScript execution time - Layout/reflow time - Paint time - Long tasks (> 50ms) 2. **Network Waterfall** - Request start times - DNS lookup - Connection time - Download time 3. **Rendering Pipeline** - DOM construction - Style calculation - Layout - Paint - Composite **Common Issues to Look For:** - Long tasks blocking main thread - Excessive JavaScript execution - Layout thrashing - Unnecessary repaints - Slow network requests - Large bundle sizes --- ## Network Analysis ### Monitor Network Requests **Using chrome-devtools-mcp:** ```javascript // Navigate to page await useTool('navigate_page', { url: 'https://example.com' }); // Wait for all requests await useTool('wait_for', { waitUntil: 'networkidle' }); // List all requests const requests = await useTool('list_network_requests', { resourceTypes: ['Document', 'Script', 'Stylesheet', 'Image', 'XHR', 'Fetch'], pageSize: 100 }); // Analyze specific request for (const req of requests.requests) { const details = await useTool('get_network_request', { requestId: req.id }); console.log({ url: details.url, method: details.method, status: details.status, size: details.encodedDataLength, time: details.timing.receiveHeadersEnd - details.timing.requestTime, cached: details.fromCache }); } ``` **Using Puppeteer:** ```javascript const requests = []; // Capture all requests page.on('request', (request) => { requests.push({ url: request.url(), method: request.method(), resourceType: request.resourceType(), headers: request.headers() }); }); // Capture responses page.on('response', (response) => { const request = response.request(); console.log({ url: response.url(), status: response.status(), size: response.headers()['content-length'], cached: response.fromCache(), timing: response.timing() }); }); await page.goto('https://example.com'); ``` ### Network Performance Metrics **Calculate Total Page Weight:** ```javascript let totalBytes = 0; let resourceCounts = {}; page.on('response', async (response) => { const type = response.request().resourceType(); const buffer = await response.buffer(); totalBytes += buffer.length; resourceCounts[type] = (resourceCounts[type] || 0) + 1; }); await page.goto('https://example.com'); console.log('Total size:', (totalBytes / 1024 / 1024).toFixed(2), 'MB'); console.log('Resources:', resourceCounts); ``` **Identify Slow Requests:** ```javascript page.on('response', (response) => { const timing = response.timing(); const totalTime = timing.receiveHeadersEnd - timing.requestTime; if (totalTime > 1000) { // Slower than 1 second console.log('Slow request:', { url: response.url(), time: totalTime.toFixed(2) + 'ms', size: response.headers()['content-length'] }); } }); ``` ### Network Throttling **Simulate Slow Connection:** ```javascript // Using chrome-devtools-mcp await useTool('emulate_network', { throttlingOption: 'Slow 3G' // or 'Fast 3G', 'Slow 4G' }); // Using Puppeteer const client = await page.createCDPSession(); await client.send('Network.emulateNetworkConditions', { offline: false, downloadThroughput: 400 * 1024 / 8, // 400 Kbps uploadThroughput: 400 * 1024 / 8, latency: 2000 // 2000ms RTT }); ``` --- ## JavaScript Performance ### Identify Long Tasks **Using Performance Observer:** ```javascript await page.evaluate(() => { return new Promise((resolve) => { const longTasks = []; const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { longTasks.push({ name: entry.name, duration: entry.duration, startTime: entry.startTime }); }); }); observer.observe({ entryTypes: ['longtask'] }); // Collect for 10 seconds setTimeout(() => { observer.disconnect(); resolve(longTasks); }, 10000); }); }); ``` ### CPU Profiling **Using Puppeteer:** ```javascript // Start CPU profiling const client = await page.createCDPSession(); await client.send('Profiler.enable'); await client.send('Profiler.start'); // Navigate and interact await page.goto('https://example.com'); await page.click('.button'); // Stop profiling const { profile } = await client.send('Profiler.stop'); // Analyze profile (flame graph data) // Import into Chrome DevTools for visualization ``` ### JavaScript Coverage **Identify Unused Code:** ```javascript // Start coverage await Promise.all([ page.coverage.startJSCoverage(), page.coverage.startCSSCoverage() ]); // Navigate await page.goto('https://example.com'); // Stop coverage const [jsCoverage, cssCoverage] = await Promise.all([ page.coverage.stopJSCoverage(), page.coverage.stopCSSCoverage() ]); // Calculate unused bytes function calculateUnusedBytes(coverage) { let usedBytes = 0; let totalBytes = 0; for (const entry of coverage) { totalBytes += entry.text.length; for (const range of entry.ranges) { usedBytes += range.end - range.start - 1; } } return { usedBytes, totalBytes, unusedBytes: totalBytes - usedBytes, unusedPercentage: ((totalBytes - usedBytes) / totalBytes * 100).toFixed(2) }; } console.log('JS Coverage:', calculateUnusedBytes(jsCoverage)); console.log('CSS Coverage:', calculateUnusedBytes(cssCoverage)); ``` ### Bundle Size Analysis **Analyze JavaScript Bundles:** ```javascript page.on('response', async (response) => { const url = response.url(); const type = response.request().resourceType(); if (type === 'script') { const buffer = await response.buffer(); const size = buffer.length; console.log({ url: url.split('/').pop(), size: (size / 1024).toFixed(2) + ' KB', gzipped: response.headers()['content-encoding'] === 'gzip' }); } }); ``` --- ## Rendering Performance ### Layout Thrashing Detection **Monitor Layout Recalculations:** ```javascript // Using Performance Observer await page.evaluate(() => { return new Promise((resolve) => { const measurements = []; const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (entry.entryType === 'measure' && entry.name.includes('layout')) { measurements.push({ name: entry.name, duration: entry.duration, startTime: entry.startTime }); } }); }); observer.observe({ entryTypes: ['measure'] }); setTimeout(() => { observer.disconnect(); resolve(measurements); }, 5000); }); }); ``` ### Paint and Composite Metrics **Get Paint Metrics:** ```javascript const paintMetrics = await page.evaluate(() => { const paints = performance.getEntriesByType('paint'); return { firstPaint: paints.find(p => p.name === 'first-paint')?.startTime, firstContentfulPaint: paints.find(p => p.name === 'first-contentful-paint')?.startTime }; }); ``` ### Frame Rate Analysis **Monitor FPS:** ```javascript await page.evaluate(() => { return new Promise((resolve) => { let frames = 0; let lastTime = performance.now(); function countFrames() { frames++; requestAnimationFrame(countFrames); } countFrames(); setTimeout(() => { const now = performance.now(); const elapsed = (now - lastTime) / 1000; const fps = frames / elapsed; resolve(fps); }, 5000); }); }); ``` ### Layout Shifts (CLS) **Track Individual Shifts:** ```javascript await page.evaluate(() => { return new Promise((resolve) => { const shifts = []; let totalCLS = 0; const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (!entry.hadRecentInput) { totalCLS += entry.value; shifts.push({ value: entry.value, time: entry.startTime, elements: entry.sources?.map(s => s.node) }); } }); }); observer.observe({ entryTypes: ['layout-shift'] }); setTimeout(() => { observer.disconnect(); resolve({ totalCLS, shifts }); }, 10000); }); }); ``` --- ## Memory Analysis ### Memory Metrics **Get Memory Usage:** ```javascript // Using chrome-devtools-mcp await useTool('evaluate_script', { expression: ` ({ usedJSHeapSize: performance.memory?.usedJSHeapSize, totalJSHeapSize: performance.memory?.totalJSHeapSize, jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit }) `, returnByValue: true }); // Using Puppeteer const metrics = await page.metrics(); console.log({ jsHeapUsed: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2) + ' MB', jsHeapTotal: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2) + ' MB', domNodes: metrics.Nodes, documents: metrics.Documents, jsEventListeners: metrics.JSEventListeners }); ``` ### Memory Leak Detection **Monitor Memory Over Time:** ```javascript async function detectMemoryLeak(page, duration = 30000) { const samples = []; const interval = 1000; // Sample every second const samples_count = duration / interval; for (let i = 0; i < samples_count; i++) { const metrics = await page.metrics(); samples.push({ time: i, heapUsed: metrics.JSHeapUsedSize }); await page.waitForTimeout(interval); } // Analyze trend const firstSample = samples[0].heapUsed; const lastSample = samples[samples.length - 1].heapUsed; const increase = ((lastSample - firstSample) / firstSample * 100).toFixed(2); return { samples, memoryIncrease: increase + '%', possibleLeak: increase > 50 // > 50% increase indicates possible leak }; } const leakAnalysis = await detectMemoryLeak(page, 30000); console.log('Memory Analysis:', leakAnalysis); ``` ### Heap Snapshot **Capture Heap Snapshot:** ```javascript const client = await page.createCDPSession(); // Take snapshot await client.send('HeapProfiler.enable'); const { result } = await client.send('HeapProfiler.takeHeapSnapshot'); // Snapshot is streamed in chunks // Save to file or analyze programmatically ``` --- ## Optimization Strategies ### Image Optimization **Detect Unoptimized Images:** ```javascript const images = await page.evaluate(() => { const images = Array.from(document.querySelectorAll('img')); return images.map(img => ({ src: img.src, naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight, displayWidth: img.width, displayHeight: img.height, oversized: img.naturalWidth > img.width * 1.5 || img.naturalHeight > img.height * 1.5 })); }); const oversizedImages = images.filter(img => img.oversized); console.log('Oversized images:', oversizedImages); ``` ### Font Loading **Detect Render-Blocking Fonts:** ```javascript const fonts = await page.evaluate(() => { return Array.from(document.fonts).map(font => ({ family: font.family, weight: font.weight, style: font.style, status: font.status, loaded: font.status === 'loaded' })); }); console.log('Fonts:', fonts); ``` ### Third-Party Scripts **Measure Third-Party Impact:** ```javascript const thirdPartyDomains = ['googletagmanager.com', 'facebook.net', 'doubleclick.net']; page.on('response', async (response) => { const url = response.url(); const isThirdParty = thirdPartyDomains.some(domain => url.includes(domain)); if (isThirdParty) { const buffer = await response.buffer(); console.log({ url: url, size: (buffer.length / 1024).toFixed(2) + ' KB', type: response.request().resourceType() }); } }); ``` ### Critical Rendering Path **Identify Render-Blocking Resources:** ```javascript await page.goto('https://example.com'); const renderBlockingResources = await page.evaluate(() => { const resources = performance.getEntriesByType('resource'); return resources.filter(resource => { return (resource.initiatorType === 'link' && resource.name.includes('.css')) || (resource.initiatorType === 'script' && !resource.name.includes('async')); }).map(r => ({ url: r.name, duration: r.duration, startTime: r.startTime })); }); console.log('Render-blocking resources:', renderBlockingResources); ``` ### Lighthouse Integration **Run Lighthouse Audit:** ```javascript import lighthouse from 'lighthouse'; import { launch } from 'chrome-launcher'; // Launch Chrome const chrome = await launch({ chromeFlags: ['--headless'] }); // Run Lighthouse const { lhr } = await lighthouse('https://example.com', { port: chrome.port, onlyCategories: ['performance'] }); // Get scores console.log({ performanceScore: lhr.categories.performance.score * 100, metrics: { FCP: lhr.audits['first-contentful-paint'].displayValue, LCP: lhr.audits['largest-contentful-paint'].displayValue, TBT: lhr.audits['total-blocking-time'].displayValue, CLS: lhr.audits['cumulative-layout-shift'].displayValue, SI: lhr.audits['speed-index'].displayValue }, opportunities: lhr.audits['opportunities'] }); await chrome.kill(); ``` --- ## Performance Budgets ### Set Performance Budgets ```javascript const budgets = { // Core Web Vitals LCP: 2500, // ms FID: 100, // ms CLS: 0.1, // score // Other metrics FCP: 1800, // ms TTI: 3800, // ms TBT: 300, // ms // Resource budgets totalPageSize: 2 * 1024 * 1024, // 2 MB jsSize: 500 * 1024, // 500 KB cssSize: 100 * 1024, // 100 KB imageSize: 1 * 1024 * 1024, // 1 MB // Request counts totalRequests: 50, jsRequests: 10, cssRequests: 5 }; async function checkBudgets(page, budgets) { // Measure actual values const vitals = await measureCoreWebVitals(page); const resources = await analyzeResources(page); // Compare against budgets const violations = []; if (vitals.LCP > budgets.LCP) { violations.push(`LCP: ${vitals.LCP}ms exceeds budget of ${budgets.LCP}ms`); } if (resources.totalSize > budgets.totalPageSize) { violations.push(`Page size: ${resources.totalSize} exceeds budget of ${budgets.totalPageSize}`); } // ... check other budgets return { passed: violations.length === 0, violations }; } ``` --- ## Automated Performance Testing ### CI/CD Integration ```javascript // performance-test.js import puppeteer from 'puppeteer'; async function performanceTest(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); // Measure metrics await page.goto(url, { waitUntil: 'networkidle2' }); const metrics = await page.metrics(); const vitals = await measureCoreWebVitals(page); await browser.close(); // Check against thresholds const thresholds = { LCP: 2500, FID: 100, CLS: 0.1, jsHeapSize: 50 * 1024 * 1024 // 50 MB }; const failed = []; if (vitals.LCP > thresholds.LCP) failed.push('LCP'); if (vitals.FID > thresholds.FID) failed.push('FID'); if (vitals.CLS > thresholds.CLS) failed.push('CLS'); if (metrics.JSHeapUsedSize > thresholds.jsHeapSize) failed.push('Memory'); if (failed.length > 0) { console.error('Performance test failed:', failed); process.exit(1); } console.log('Performance test passed'); } performanceTest(process.env.TEST_URL); ``` --- ## Best Practices ### Performance Testing Checklist 1. **Measure Multiple Times** - Run tests 3-5 times - Use median values - Account for variance 2. **Test Different Conditions** - Fast 3G - Slow 3G - Offline - CPU throttling 3. **Test Different Devices** - Mobile (low-end) - Mobile (high-end) - Desktop - Tablet 4. **Monitor Over Time** - Track metrics in CI/CD - Set up alerts for regressions - Create performance dashboards 5. **Focus on User Experience** - Prioritize Core Web Vitals - Test real user journeys - Consider perceived performance 6. **Optimize Critical Path** - Minimize render-blocking resources - Defer non-critical JavaScript - Optimize font loading - Lazy load images --- ## Resources - [Web.dev Performance](https://web.dev/performance/) - [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/) - [Core Web Vitals](https://web.dev/vitals/) - [Lighthouse](https://developer.chrome.com/docs/lighthouse/) - [WebPageTest](https://www.webpagetest.org/) -
puppeteer-reference.md 18.6 KB
# Puppeteer Quick Reference Complete guide to browser automation with Puppeteer - a high-level API over Chrome DevTools Protocol. ## Table of Contents - [Setup](#setup) - [Browser & Page Management](#browser--page-management) - [Navigation](#navigation) - [Element Interaction](#element-interaction) - [JavaScript Execution](#javascript-execution) - [Screenshots & PDFs](#screenshots--pdfs) - [Network Interception](#network-interception) - [Device Emulation](#device-emulation) - [Performance](#performance) - [Common Patterns](#common-patterns) --- ## Setup ### Installation ```bash # Install Puppeteer npm install puppeteer # Install core only (bring your own Chrome) npm install puppeteer-core ``` ### Basic Usage ```javascript import puppeteer from 'puppeteer'; // Launch browser const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] }); // Open page const page = await browser.newPage(); // Navigate await page.goto('https://example.com'); // Do work... // Cleanup await browser.close(); ``` --- ## Browser & Page Management ### Launch Browser ```javascript const browser = await puppeteer.launch({ // Visibility headless: false, // Show browser UI headless: 'new', // New headless mode (Chrome 112+) // Chrome location executablePath: '/path/to/chrome', channel: 'chrome', // or 'chrome-canary', 'chrome-beta' // Browser context userDataDir: './user-data', // Persistent profile // Window size defaultViewport: { width: 1920, height: 1080, deviceScaleFactor: 1, isMobile: false }, // Advanced options args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-web-security', '--disable-features=IsolateOrigins', '--disable-site-isolation-trials', '--start-maximized' ], // Debugging devtools: true, // Open DevTools automatically slowMo: 250, // Slow down by 250ms per action // Network proxy: { server: 'http://proxy.com:8080' } }); ``` ### Connect to Running Browser ```javascript // Launch Chrome with debugging // google-chrome --remote-debugging-port=9222 const browser = await puppeteer.connect({ browserURL: 'http://localhost:9222', // or browserWSEndpoint: 'ws://localhost:9222/devtools/browser/...' }); ``` ### Page Management ```javascript // Create new page const page = await browser.newPage(); // Get all pages const pages = await browser.pages(); // Close page await page.close(); // Multiple pages const page1 = await browser.newPage(); const page2 = await browser.newPage(); // Switch between pages await page1.bringToFront(); ``` ### Browser Context (Incognito) ```javascript // Create isolated context const context = await browser.createBrowserContext(); const page = await context.newPage(); // Cleanup context await context.close(); ``` --- ## Navigation ### Basic Navigation ```javascript // Navigate to URL await page.goto('https://example.com'); // Navigate with options await page.goto('https://example.com', { waitUntil: 'networkidle2', // or 'load', 'domcontentloaded', 'networkidle0' timeout: 30000 // Max wait time (ms) }); // Reload page await page.reload({ waitUntil: 'networkidle2' }); // Navigation history await page.goBack(); await page.goForward(); // Wait for navigation await page.waitForNavigation({ waitUntil: 'networkidle2' }); ``` ### Wait Until Options - `load` - Wait for load event - `domcontentloaded` - Wait for DOMContentLoaded event - `networkidle0` - Wait until no network connections for 500ms - `networkidle2` - Wait until max 2 network connections for 500ms --- ## Element Interaction ### Selectors ```javascript // CSS selectors await page.$('#id'); await page.$('.class'); await page.$('div > p'); // XPath await page.$x('//button[text()="Submit"]'); // Get all matching elements await page.$$('.item'); await page.$$x('//div[@class="item"]'); ``` ### Click Elements ```javascript // Click by selector await page.click('.button'); // Click with options await page.click('.button', { button: 'left', // or 'right', 'middle' clickCount: 1, // 2 for double-click delay: 100 // Delay between mousedown and mouseup }); // ElementHandle click const button = await page.$('.button'); await button.click(); ``` ### Type Text ```javascript // Type into input await page.type('#search', 'query text'); // Type with delay await page.type('#search', 'slow typing', { delay: 100 }); // Clear and type await page.$eval('#search', el => el.value = ''); await page.type('#search', 'new text'); ``` ### Form Interaction ```javascript // Fill input await page.type('#username', 'john@example.com'); await page.type('#password', 'secret123'); // Select dropdown option await page.select('#country', 'US'); // By value await page.select('#country', 'USA', 'UK'); // Multiple // Check/uncheck checkbox await page.click('input[type="checkbox"]'); // Choose radio button await page.click('input[value="option2"]'); // Upload file const input = await page.$('input[type="file"]'); await input.uploadFile('/path/to/file.pdf'); // Submit form await page.click('button[type="submit"]'); await page.waitForNavigation(); ``` ### Hover & Focus ```javascript // Hover over element await page.hover('.menu-item'); // Focus element await page.focus('#input'); // Blur await page.$eval('#input', el => el.blur()); ``` ### Drag & Drop ```javascript const source = await page.$('.draggable'); const target = await page.$('.drop-zone'); await source.drag(target); await source.drop(target); ``` --- ## JavaScript Execution ### Evaluate in Page Context ```javascript // Execute JavaScript const title = await page.evaluate(() => document.title); // With arguments const text = await page.evaluate( (selector) => document.querySelector(selector).textContent, '.heading' ); // Return complex data const data = await page.evaluate(() => ({ title: document.title, url: location.href, cookies: document.cookie })); // With ElementHandle const element = await page.$('.button'); const text = await page.evaluate(el => el.textContent, element); ``` ### Query & Modify DOM ```javascript // Get element property const value = await page.$eval('#input', el => el.value); // Get multiple elements const items = await page.$$eval('.item', elements => elements.map(el => el.textContent) ); // Modify element await page.$eval('#input', (el, value) => { el.value = value; }, 'new value'); // Add class await page.$eval('.element', el => el.classList.add('active')); ``` ### Expose Functions ```javascript // Expose Node.js function to page await page.exposeFunction('md5', (text) => crypto.createHash('md5').update(text).digest('hex') ); // Call from page context const hash = await page.evaluate(async () => { return await window.md5('hello world'); }); ``` --- ## Screenshots & PDFs ### Screenshots ```javascript // Full page screenshot await page.screenshot({ path: 'screenshot.png', fullPage: true }); // Viewport screenshot await page.screenshot({ path: 'viewport.png', fullPage: false }); // Element screenshot const element = await page.$('.chart'); await element.screenshot({ path: 'chart.png' }); // Screenshot options await page.screenshot({ path: 'page.png', type: 'png', // or 'jpeg', 'webp' quality: 80, // JPEG quality (0-100) clip: { // Crop region x: 0, y: 0, width: 500, height: 500 }, omitBackground: true // Transparent background }); // Screenshot to buffer const buffer = await page.screenshot(); ``` ### PDF Generation ```javascript // Generate PDF await page.pdf({ path: 'page.pdf', format: 'A4', // or 'Letter', 'Legal', etc. printBackground: true, margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' } }); // Custom page size await page.pdf({ path: 'custom.pdf', width: '8.5in', height: '11in', landscape: true }); // Header and footer await page.pdf({ path: 'report.pdf', displayHeaderFooter: true, headerTemplate: '<div style="font-size:10px;">Header</div>', footerTemplate: '<div style="font-size:10px;">Page <span class="pageNumber"></span></div>' }); ``` --- ## Network Interception ### Request Interception ```javascript // Enable request interception await page.setRequestInterception(true); // Intercept requests page.on('request', (request) => { // Block specific resource types if (request.resourceType() === 'image') { request.abort(); } // Block URLs else if (request.url().includes('ads')) { request.abort(); } // Modify request else if (request.url().includes('api')) { request.continue({ headers: { ...request.headers(), 'Authorization': 'Bearer token' } }); } // Continue normally else { request.continue(); } }); ``` ### Mock Responses ```javascript await page.setRequestInterception(true); page.on('request', (request) => { if (request.url().includes('/api/user')) { request.respond({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 1, name: 'Mock User' }) }); } else { request.continue(); } }); ``` ### Monitor Network ```javascript // Track requests page.on('request', (request) => { console.log('Request:', request.method(), request.url()); }); // Track responses page.on('response', (response) => { console.log('Response:', response.status(), response.url()); }); // Track failed requests page.on('requestfailed', (request) => { console.log('Failed:', request.failure().errorText, request.url()); }); // Get response body page.on('response', async (response) => { if (response.url().includes('/api/data')) { const json = await response.json(); console.log('API Data:', json); } }); ``` --- ## Device Emulation ### Predefined Devices ```javascript import { devices } from 'puppeteer'; // Emulate iPhone const iPhone = devices['iPhone 13 Pro']; await page.emulate(iPhone); // Common devices const iPad = devices['iPad Pro']; const pixel = devices['Pixel 5']; const galaxy = devices['Galaxy S9+']; // Navigate after emulation await page.goto('https://example.com'); ``` ### Custom Device ```javascript await page.emulate({ viewport: { width: 375, height: 812, deviceScaleFactor: 3, isMobile: true, hasTouch: true, isLandscape: false }, userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)...' }); ``` ### Viewport Only ```javascript await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 }); ``` ### Geolocation ```javascript // Set geolocation await page.setGeolocation({ latitude: 37.7749, longitude: -122.4194, accuracy: 100 }); // Grant permissions const context = browser.defaultBrowserContext(); await context.overridePermissions('https://example.com', ['geolocation']); ``` ### Timezone & Locale ```javascript // Set timezone await page.emulateTimezone('America/New_York'); // Set locale await page.emulateMediaType('screen'); await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'language', { get: () => 'en-US' }); }); ``` --- ## Performance ### CPU & Network Throttling ```javascript // CPU throttling const client = await page.createCDPSession(); await client.send('Emulation.setCPUThrottlingRate', { rate: 4 }); // Network throttling await page.emulateNetworkConditions({ offline: false, downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps uploadThroughput: 750 * 1024 / 8, // 750 Kbps latency: 40 // 40ms RTT }); // Predefined profiles await page.emulateNetworkConditions( puppeteer.networkConditions['Fast 3G'] ); // Disable throttling await page.emulateNetworkConditions({ offline: false, downloadThroughput: -1, uploadThroughput: -1, latency: 0 }); ``` ### Performance Metrics ```javascript // Get metrics const metrics = await page.metrics(); console.log(metrics); // { // Timestamp, Documents, Frames, JSEventListeners, // Nodes, LayoutCount, RecalcStyleCount, // LayoutDuration, RecalcStyleDuration, // ScriptDuration, TaskDuration, // JSHeapUsedSize, JSHeapTotalSize // } ``` ### Performance Tracing ```javascript // Start tracing await page.tracing.start({ path: 'trace.json', categories: [ 'devtools.timeline', 'disabled-by-default-devtools.timeline' ] }); // Navigate await page.goto('https://example.com'); // Stop tracing await page.tracing.stop(); // Analyze trace in chrome://tracing ``` ### Coverage (Code Usage) ```javascript // Start JS coverage await page.coverage.startJSCoverage(); // Start CSS coverage await page.coverage.startCSSCoverage(); // Navigate await page.goto('https://example.com'); // Stop and get coverage const jsCoverage = await page.coverage.stopJSCoverage(); const cssCoverage = await page.coverage.stopCSSCoverage(); // Calculate unused bytes let totalBytes = 0; let usedBytes = 0; for (const entry of [...jsCoverage, ...cssCoverage]) { totalBytes += entry.text.length; for (const range of entry.ranges) { usedBytes += range.end - range.start - 1; } } console.log(`Used: ${usedBytes / totalBytes * 100}%`); ``` --- ## Common Patterns ### Wait for Elements ```javascript // Wait for selector await page.waitForSelector('.element', { visible: true, timeout: 5000 }); // Wait for XPath await page.waitForXPath('//button[text()="Submit"]'); // Wait for function await page.waitForFunction( () => document.querySelector('.loading') === null, { timeout: 10000 } ); // Wait for timeout await page.waitForTimeout(2000); ``` ### Handle Dialogs ```javascript // Alert, confirm, prompt page.on('dialog', async (dialog) => { console.log(dialog.type(), dialog.message()); // Accept await dialog.accept(); // or reject // await dialog.dismiss(); // or provide input for prompt // await dialog.accept('input text'); }); ``` ### Handle Downloads ```javascript // Set download path const client = await page.createCDPSession(); await client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: '/path/to/downloads' }); // Trigger download await page.click('a[download]'); ``` ### Multiple Pages (Tabs) ```javascript // Listen for new pages browser.on('targetcreated', async (target) => { if (target.type() === 'page') { const newPage = await target.page(); console.log('New page opened:', newPage.url()); } }); // Click link that opens new tab const [newPage] = await Promise.all([ new Promise(resolve => browser.once('targetcreated', target => resolve(target.page()))), page.click('a[target="_blank"]') ]); console.log('New page URL:', newPage.url()); ``` ### Frames (iframes) ```javascript // Get all frames const frames = page.frames(); // Find frame by name const frame = page.frames().find(f => f.name() === 'myframe'); // Find frame by URL const frame = page.frames().find(f => f.url().includes('example.com')); // Main frame const mainFrame = page.mainFrame(); // Interact with frame await frame.click('.button'); await frame.type('#input', 'text'); ``` ### Infinite Scroll ```javascript async function autoScroll(page) { await page.evaluate(async () => { await new Promise((resolve) => { let totalHeight = 0; const distance = 100; const timer = setInterval(() => { const scrollHeight = document.body.scrollHeight; window.scrollBy(0, distance); totalHeight += distance; if (totalHeight >= scrollHeight) { clearInterval(timer); resolve(); } }, 100); }); }); } await autoScroll(page); ``` ### Cookies ```javascript // Get cookies const cookies = await page.cookies(); // Set cookies await page.setCookie({ name: 'session', value: 'abc123', domain: 'example.com', path: '/', httpOnly: true, secure: true, sameSite: 'Strict' }); // Delete cookies await page.deleteCookie({ name: 'session' }); ``` ### Local Storage ```javascript // Set localStorage await page.evaluate(() => { localStorage.setItem('key', 'value'); }); // Get localStorage const value = await page.evaluate(() => { return localStorage.getItem('key'); }); // Clear localStorage await page.evaluate(() => localStorage.clear()); ``` ### Error Handling ```javascript try { await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 30000 }); } catch (error) { if (error.name === 'TimeoutError') { console.error('Page load timeout'); } else { console.error('Navigation failed:', error); } // Take screenshot on error await page.screenshot({ path: 'error.png' }); } ``` ### Stealth Mode (Avoid Detection) ```javascript // Hide automation indicators await page.evaluateOnNewDocument(() => { // Override navigator.webdriver Object.defineProperty(navigator, 'webdriver', { get: () => false }); // Mock chrome object window.chrome = { runtime: {} }; // Mock permissions const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => ( parameters.name === 'notifications' ? Promise.resolve({ state: 'granted' }) : originalQuery(parameters) ); }); // Set realistic user agent await page.setUserAgent( 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' ); ``` --- ## Debugging Tips ### Take Screenshots on Error ```javascript page.on('pageerror', async (error) => { console.error('Page error:', error); await page.screenshot({ path: `error-${Date.now()}.png` }); }); ``` ### Console Logging ```javascript // Forward console to Node page.on('console', (msg) => { console.log('PAGE LOG:', msg.text()); }); ``` ### Slow Down Execution ```javascript const browser = await puppeteer.launch({ slowMo: 250 // 250ms delay between actions }); ``` ### Keep Browser Open ```javascript const browser = await puppeteer.launch({ headless: false, devtools: true }); // Prevent auto-close await page.evaluate(() => debugger); ``` --- ## Best Practices 1. **Always close browser:** Use try/finally or process cleanup 2. **Wait appropriately:** Use waitForSelector, not setTimeout 3. **Handle errors:** Wrap navigation in try/catch 4. **Optimize selectors:** Use specific selectors for reliability 5. **Avoid race conditions:** Wait for navigation after clicks 6. **Reuse pages:** Don't create new pages unnecessarily 7. **Set timeouts:** Always specify reasonable timeouts 8. **Clean up:** Close unused pages and contexts --- ## Resources - [Puppeteer Documentation](https://pptr.dev/) - [Puppeteer API](https://pptr.dev/api) - [Puppeteer Examples](https://github.com/puppeteer/puppeteer/tree/main/examples) - [Awesome Puppeteer](https://github.com/transitive-bullshit/awesome-puppeteer)
-
-
scripts
-
lib
-
browser.js 3.2 KB
/** * Shared browser utilities for Chrome DevTools scripts */ import puppeteer from 'puppeteer'; import debug from 'debug'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ENDPOINT_FILE = path.join(__dirname, '..', '.browser-endpoint'); const log = debug('chrome-devtools:browser'); let browserInstance = null; let pageInstance = null; /** * Launch or connect to browser */ export async function getBrowser(options = {}) { if (browserInstance && browserInstance.isConnected()) { log('Reusing existing browser instance'); return browserInstance; } // Check for persistent browser endpoint if (!options.browserUrl && !options.wsEndpoint && fs.existsSync(ENDPOINT_FILE)) { try { const wsEndpoint = fs.readFileSync(ENDPOINT_FILE, 'utf8').trim(); log('Found persistent browser endpoint, connecting...'); browserInstance = await puppeteer.connect({ browserWSEndpoint: wsEndpoint }); return browserInstance; } catch (error) { log('Failed to connect to persistent browser, launching new one:', error.message); // Clean up stale endpoint file if (fs.existsSync(ENDPOINT_FILE)) { fs.unlinkSync(ENDPOINT_FILE); } } } const launchOptions = { headless: options.headless !== false, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', ...(options.args || []) ], defaultViewport: options.viewport || { width: 1920, height: 1080 }, ...options }; if (options.browserUrl || options.wsEndpoint) { log('Connecting to existing browser'); browserInstance = await puppeteer.connect({ browserURL: options.browserUrl, browserWSEndpoint: options.wsEndpoint }); } else { log('Launching new browser'); browserInstance = await puppeteer.launch(launchOptions); } return browserInstance; } /** * Get current page or create new one */ export async function getPage(browser) { if (pageInstance && !pageInstance.isClosed()) { log('Reusing existing page'); return pageInstance; } const pages = await browser.pages(); if (pages.length > 0) { pageInstance = pages[0]; } else { pageInstance = await browser.newPage(); } return pageInstance; } /** * Close browser */ export async function closeBrowser() { if (browserInstance) { await browserInstance.close(); browserInstance = null; pageInstance = null; } } /** * Parse command line arguments */ export function parseArgs(argv, options = {}) { const args = {}; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg.startsWith('--')) { const key = arg.slice(2); const nextArg = argv[i + 1]; if (nextArg && !nextArg.startsWith('--')) { args[key] = nextArg; i++; } else { args[key] = true; } } } return args; } /** * Output JSON result */ export function outputJSON(data) { console.log(JSON.stringify(data, null, 2)); } /** * Output error */ export function outputError(error) { console.error(JSON.stringify({ success: false, error: error.message, stack: error.stack }, null, 2)); process.exit(1); } -
selector.js 5.3 KB
/** * Shared selector parsing and validation library * Supports CSS and XPath selectors with security validation */ /** * Parse and validate selector * @param {string} selector - CSS or XPath selector * @returns {{type: 'css'|'xpath', selector: string}} * @throws {Error} If XPath contains injection patterns */ export function parseSelector(selector) { if (!selector || typeof selector !== 'string') { throw new Error('Selector must be a non-empty string'); } // Detect XPath selectors if (selector.startsWith('/') || selector.startsWith('(//')) { // XPath injection prevention validateXPath(selector); return { type: 'xpath', selector }; } // CSS selector return { type: 'css', selector }; } /** * Validate XPath selector for security * @param {string} xpath - XPath expression to validate * @throws {Error} If XPath contains dangerous patterns */ function validateXPath(xpath) { const dangerous = [ 'javascript:', '<script', 'onerror=', 'onload=', 'onclick=', 'onmouseover=', 'eval(', 'Function(', 'constructor(', ]; const lower = xpath.toLowerCase(); for (const pattern of dangerous) { if (lower.includes(pattern.toLowerCase())) { throw new Error(`Potential XPath injection detected: ${pattern}`); } } // Additional validation: check for extremely long selectors (potential DoS) if (xpath.length > 1000) { throw new Error('XPath selector too long (max 1000 characters)'); } } /** * Wait for element based on selector type * @param {Object} page - Puppeteer page instance * @param {{type: string, selector: string}} parsed - Parsed selector * @param {Object} options - Wait options (visible, timeout) * @returns {Promise<void>} */ export async function waitForElement(page, parsed, options = {}) { const defaultOptions = { visible: true, timeout: 5000, ...options }; if (parsed.type === 'xpath') { // Use locator API for XPath (Puppeteer v24+) const locator = page.locator(`::-p-xpath(${parsed.selector})`); // setVisibility and setTimeout are the locator options await locator .setVisibility(defaultOptions.visible ? 'visible' : null) .setTimeout(defaultOptions.timeout) .wait(); } else { await page.waitForSelector(parsed.selector, defaultOptions); } } /** * Click element based on selector type * @param {Object} page - Puppeteer page instance * @param {{type: string, selector: string}} parsed - Parsed selector * @returns {Promise<void>} */ export async function clickElement(page, parsed) { if (parsed.type === 'xpath') { // Use locator API for XPath (Puppeteer v24+) const locator = page.locator(`::-p-xpath(${parsed.selector})`); await locator.click(); } else { await page.click(parsed.selector); } } /** * Type into element based on selector type * @param {Object} page - Puppeteer page instance * @param {{type: string, selector: string}} parsed - Parsed selector * @param {string} value - Text to type * @param {Object} options - Type options (delay, clear) * @returns {Promise<void>} */ export async function typeIntoElement(page, parsed, value, options = {}) { if (parsed.type === 'xpath') { // Use locator API for XPath (Puppeteer v24+) const locator = page.locator(`::-p-xpath(${parsed.selector})`); // Clear if requested if (options.clear) { await locator.fill(''); } await locator.fill(value); } else { // CSS selector if (options.clear) { await page.$eval(parsed.selector, el => el.value = ''); } await page.type(parsed.selector, value, { delay: options.delay || 0 }); } } /** * Get element handle based on selector type * @param {Object} page - Puppeteer page instance * @param {{type: string, selector: string}} parsed - Parsed selector * @returns {Promise<ElementHandle|null>} */ export async function getElement(page, parsed) { if (parsed.type === 'xpath') { // For XPath, use page.evaluate with XPath evaluation // This returns the first matching element const element = await page.evaluateHandle((xpath) => { const result = document.evaluate( xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ); return result.singleNodeValue; }, parsed.selector); // Convert JSHandle to ElementHandle const elementHandle = element.asElement(); return elementHandle; } else { return await page.$(parsed.selector); } } /** * Get enhanced error message for selector failures * @param {Error} error - Original error * @param {string} selector - Selector that failed * @returns {Error} Enhanced error with troubleshooting tips */ export function enhanceError(error, selector) { if (error.message.includes('waiting for selector') || error.message.includes('waiting for XPath') || error.message.includes('No node found')) { error.message += '\n\nTroubleshooting:\n' + '1. Use snapshot.js to find correct selector: node snapshot.js --url <url>\n' + '2. Try XPath selector: //button[text()="Click"] or //button[contains(text(),"Click")]\n' + '3. Check element is visible on page (not display:none or hidden)\n' + '4. Increase --timeout value: --timeout 10000\n' + '5. Change wait strategy: --wait-until load or --wait-until domcontentloaded'; } return error; }
-
-
__tests__
-
selector.test.js 6.4 KB
/** * Tests for selector parsing library * Run with: node --test __tests__/selector.test.js */ import { describe, it } from 'node:test'; import assert from 'node:assert'; import { parseSelector } from '../lib/selector.js'; describe('parseSelector', () => { describe('CSS Selectors', () => { it('should detect simple CSS selectors', () => { const result = parseSelector('button'); assert.strictEqual(result.type, 'css'); assert.strictEqual(result.selector, 'button'); }); it('should detect class selectors', () => { const result = parseSelector('.btn-submit'); assert.strictEqual(result.type, 'css'); assert.strictEqual(result.selector, '.btn-submit'); }); it('should detect ID selectors', () => { const result = parseSelector('#email-input'); assert.strictEqual(result.type, 'css'); assert.strictEqual(result.selector, '#email-input'); }); it('should detect attribute selectors', () => { const result = parseSelector('button[type="submit"]'); assert.strictEqual(result.type, 'css'); assert.strictEqual(result.selector, 'button[type="submit"]'); }); it('should detect complex CSS selectors', () => { const result = parseSelector('div.container > button.btn-primary:hover'); assert.strictEqual(result.type, 'css'); }); }); describe('XPath Selectors', () => { it('should detect absolute XPath', () => { const result = parseSelector('/html/body/button'); assert.strictEqual(result.type, 'xpath'); assert.strictEqual(result.selector, '/html/body/button'); }); it('should detect relative XPath', () => { const result = parseSelector('//button'); assert.strictEqual(result.type, 'xpath'); assert.strictEqual(result.selector, '//button'); }); it('should detect XPath with text matching', () => { const result = parseSelector('//button[text()="Click Me"]'); assert.strictEqual(result.type, 'xpath'); }); it('should detect XPath with contains', () => { const result = parseSelector('//button[contains(text(),"Submit")]'); assert.strictEqual(result.type, 'xpath'); }); it('should detect XPath with attributes', () => { const result = parseSelector('//input[@type="email"]'); assert.strictEqual(result.type, 'xpath'); }); it('should detect grouped XPath', () => { const result = parseSelector('(//button)[1]'); assert.strictEqual(result.type, 'xpath'); }); }); describe('Security Validation', () => { it('should block javascript: injection', () => { assert.throws( () => parseSelector('//button[@onclick="javascript:alert(1)"]'), /XPath injection detected.*javascript:/i ); }); it('should block <script tag injection', () => { assert.throws( () => parseSelector('//div[contains(text(),"<script>alert(1)</script>")]'), /XPath injection detected.*<script/i ); }); it('should block onerror= injection', () => { assert.throws( () => parseSelector('//img[@onerror="alert(1)"]'), /XPath injection detected.*onerror=/i ); }); it('should block onload= injection', () => { assert.throws( () => parseSelector('//body[@onload="malicious()"]'), /XPath injection detected.*onload=/i ); }); it('should block onclick= injection', () => { assert.throws( () => parseSelector('//a[@onclick="steal()"]'), /XPath injection detected.*onclick=/i ); }); it('should block eval( injection', () => { assert.throws( () => parseSelector('//div[eval("malicious")]'), /XPath injection detected.*eval\(/i ); }); it('should block Function( injection', () => { assert.throws( () => parseSelector('//div[Function("return 1")()]'), /XPath injection detected.*Function\(/i ); }); it('should block constructor( injection', () => { assert.throws( () => parseSelector('//div[constructor("alert(1)")()]'), /XPath injection detected.*constructor\(/i ); }); it('should be case-insensitive for security checks', () => { assert.throws( () => parseSelector('//div[@ONERROR="alert(1)"]'), /XPath injection detected/i ); }); it('should block extremely long selectors (DoS prevention)', () => { const longSelector = '//' + 'a'.repeat(1001); assert.throws( () => parseSelector(longSelector), /XPath selector too long/i ); }); }); describe('Edge Cases', () => { it('should throw on empty string', () => { assert.throws( () => parseSelector(''), /Selector must be a non-empty string/ ); }); it('should throw on null', () => { assert.throws( () => parseSelector(null), /Selector must be a non-empty string/ ); }); it('should throw on undefined', () => { assert.throws( () => parseSelector(undefined), /Selector must be a non-empty string/ ); }); it('should throw on non-string input', () => { assert.throws( () => parseSelector(123), /Selector must be a non-empty string/ ); }); it('should handle selectors with special characters', () => { const result = parseSelector('button[data-test="submit-form"]'); assert.strictEqual(result.type, 'css'); }); it('should allow safe XPath with parentheses', () => { const result = parseSelector('//button[contains(text(),"Save")]'); assert.strictEqual(result.type, 'xpath'); // Should not throw }); }); describe('Real-World Examples', () => { it('should handle common button selector', () => { const result = parseSelector('//button[contains(text(),"Submit")]'); assert.strictEqual(result.type, 'xpath'); }); it('should handle complex form selector', () => { const result = parseSelector('//form[@id="login-form"]//input[@type="email"]'); assert.strictEqual(result.type, 'xpath'); }); it('should handle descendant selector', () => { const result = parseSelector('//div[@class="modal"]//button[@class="close"]'); assert.strictEqual(result.type, 'xpath'); }); it('should handle nth-child equivalent', () => { const result = parseSelector('(//li)[3]'); assert.strictEqual(result.type, 'xpath'); }); }); });
-
-
.gitignore 12 B · in bundle
-
click.js 2.2 KB
#!/usr/bin/env node /** * Click an element * Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"] * Supports both CSS and XPath selectors: * - CSS: node click.js --selector "button.submit" * - XPath: node click.js --selector "//button[contains(text(),'Submit')]" */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; import { parseSelector, waitForElement, clickElement, enhanceError } from './lib/selector.js'; async function click() { const args = parseArgs(process.argv.slice(2)); if (!args.selector) { outputError(new Error('--selector is required')); return; } try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); // Navigate if URL provided if (args.url) { await page.goto(args.url, { waitUntil: args['wait-until'] || 'networkidle2' }); } // Parse and validate selector const parsed = parseSelector(args.selector); // Wait for element based on selector type await waitForElement(page, parsed, { visible: true, timeout: parseInt(args.timeout || '5000') }); // Set up navigation promise BEFORE clicking (in case click triggers immediate navigation) const navigationPromise = page.waitForNavigation({ waitUntil: 'load', timeout: 5000 }).catch(() => null); // Catch timeout - navigation may not occur // Click element await clickElement(page, parsed); // Wait for optional selector after click if (args['wait-for']) { await page.waitForSelector(args['wait-for'], { timeout: parseInt(args.timeout || '5000') }); } else { // Wait for navigation to complete (or timeout if no navigation) await navigationPromise; } outputJSON({ success: true, url: page.url(), title: await page.title() }); if (args.close !== 'false') { await closeBrowser(); } } catch (error) { // Enhance error message with troubleshooting tips const enhanced = enhanceError(error, args.selector); outputError(enhanced); process.exit(1); } } click(); -
close-persistent.js 974 B
#!/usr/bin/env node /** * Close the persistent Chrome browser */ import puppeteer from 'puppeteer'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ENDPOINT_FILE = path.join(__dirname, '.browser-endpoint'); async function main() { if (!fs.existsSync(ENDPOINT_FILE)) { console.log('No persistent browser found.'); process.exit(0); } const wsEndpoint = fs.readFileSync(ENDPOINT_FILE, 'utf8'); console.log('Connecting to browser...'); try { const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint }); await browser.close(); fs.unlinkSync(ENDPOINT_FILE); console.log('✓ Browser closed successfully.'); } catch (error) { console.error('Error closing browser:', error.message); // Clean up stale endpoint file if (fs.existsSync(ENDPOINT_FILE)) { fs.unlinkSync(ENDPOINT_FILE); } } } main(); -
console.js 1.7 KB
#!/usr/bin/env node /** * Monitor console messages * Usage: node console.js --url https://example.com [--types error,warn] [--duration 5000] */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; async function monitorConsole() { const args = parseArgs(process.argv.slice(2)); if (!args.url) { outputError(new Error('--url is required')); return; } try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); const messages = []; const filterTypes = args.types ? args.types.split(',') : null; // Listen for console messages page.on('console', (msg) => { const type = msg.type(); if (!filterTypes || filterTypes.includes(type)) { messages.push({ type: type, text: msg.text(), location: msg.location(), timestamp: Date.now() }); } }); // Listen for page errors page.on('pageerror', (error) => { messages.push({ type: 'pageerror', text: error.message, stack: error.stack, timestamp: Date.now() }); }); // Navigate await page.goto(args.url, { waitUntil: args['wait-until'] || 'networkidle2' }); // Wait for additional time if specified if (args.duration) { await new Promise(resolve => setTimeout(resolve, parseInt(args.duration))); } outputJSON({ success: true, url: page.url(), messageCount: messages.length, messages: messages }); if (args.close !== 'false') { await closeBrowser(); } } catch (error) { outputError(error); } } monitorConsole(); -
evaluate.js 1.1 KB
#!/usr/bin/env node /** * Execute JavaScript in page context * Usage: node evaluate.js --script "document.title" [--url https://example.com] */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; async function evaluate() { const args = parseArgs(process.argv.slice(2)); if (!args.script) { outputError(new Error('--script is required')); return; } try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); // Navigate if URL provided if (args.url) { await page.goto(args.url, { waitUntil: args['wait-until'] || 'networkidle2' }); } const result = await page.evaluate((script) => { // eslint-disable-next-line no-eval return eval(script); }, args.script); outputJSON({ success: true, result: result, url: page.url() }); if (args.close !== 'false') { await closeBrowser(); } } catch (error) { outputError(error); } } evaluate(); -
fill.js 1.9 KB
#!/usr/bin/env node /** * Fill form fields * Usage: node fill.js --selector "#input" --value "text" [--url https://example.com] * Supports both CSS and XPath selectors: * - CSS: node fill.js --selector "#email" --value "user@example.com" * - XPath: node fill.js --selector "//input[@type='email']" --value "user@example.com" */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; import { parseSelector, waitForElement, typeIntoElement, enhanceError } from './lib/selector.js'; async function fill() { const args = parseArgs(process.argv.slice(2)); if (!args.selector) { outputError(new Error('--selector is required')); return; } if (!args.value) { outputError(new Error('--value is required')); return; } try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); // Navigate if URL provided if (args.url) { await page.goto(args.url, { waitUntil: args['wait-until'] || 'networkidle2' }); } // Parse and validate selector const parsed = parseSelector(args.selector); // Wait for element based on selector type await waitForElement(page, parsed, { visible: true, timeout: parseInt(args.timeout || '5000') }); // Type into element await typeIntoElement(page, parsed, args.value, { clear: args.clear === 'true', delay: parseInt(args.delay || '0') }); outputJSON({ success: true, selector: args.selector, value: args.value, url: page.url() }); if (args.close !== 'false') { await closeBrowser(); } } catch (error) { // Enhance error message with troubleshooting tips const enhanced = enhanceError(error, args.selector); outputError(enhanced); process.exit(1); } } fill(); -
install-deps.sh 4.3 KB
#!/bin/bash # System dependencies installation script for Chrome DevTools Agent Skill # This script installs required system libraries for running Chrome/Chromium set -e echo "🚀 Installing system dependencies for Chrome/Chromium..." echo "" # Detect OS if [ -f /etc/os-release ]; then . /etc/os-release OS=$ID else echo "❌ Cannot detect OS. This script supports Debian/Ubuntu-based systems." exit 1 fi # Check if running as root if [ "$EUID" -ne 0 ]; then SUDO="sudo" echo "⚠️ This script requires root privileges to install system packages." echo " You may be prompted for your password." echo "" else SUDO="" fi # Install dependencies based on OS case $OS in ubuntu|debian|pop) echo "Detected: $PRETTY_NAME" echo "Installing dependencies with apt..." echo "" $SUDO apt-get update # Install Chrome dependencies $SUDO apt-get install -y \ ca-certificates \ fonts-liberation \ libasound2t64 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgbm1 \ libgcc1 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libnss3 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc++6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ lsb-release \ wget \ xdg-utils echo "" echo "✅ System dependencies installed successfully!" ;; fedora|rhel|centos) echo "Detected: $PRETTY_NAME" echo "Installing dependencies with dnf/yum..." echo "" # Try dnf first, fallback to yum if command -v dnf &> /dev/null; then PKG_MGR="dnf" else PKG_MGR="yum" fi $SUDO $PKG_MGR install -y \ alsa-lib \ atk \ at-spi2-atk \ cairo \ cups-libs \ dbus-libs \ expat \ fontconfig \ glib2 \ gtk3 \ libdrm \ libgbm \ libX11 \ libxcb \ libXcomposite \ libXcursor \ libXdamage \ libXext \ libXfixes \ libXi \ libxkbcommon \ libXrandr \ libXrender \ libXScrnSaver \ libXtst \ mesa-libgbm \ nspr \ nss \ pango echo "" echo "✅ System dependencies installed successfully!" ;; arch|manjaro) echo "Detected: $PRETTY_NAME" echo "Installing dependencies with pacman..." echo "" $SUDO pacman -Sy --noconfirm \ alsa-lib \ at-spi2-core \ cairo \ cups \ dbus \ expat \ glib2 \ gtk3 \ libdrm \ libx11 \ libxcb \ libxcomposite \ libxcursor \ libxdamage \ libxext \ libxfixes \ libxi \ libxkbcommon \ libxrandr \ libxrender \ libxshmfence \ libxss \ libxtst \ mesa \ nspr \ nss \ pango echo "" echo "✅ System dependencies installed successfully!" ;; *) echo "❌ Unsupported OS: $OS" echo " This script supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro" echo "" echo " Please install Chrome/Chromium dependencies manually for your OS." echo " See: https://pptr.dev/troubleshooting" exit 1 ;; esac echo "" echo "📝 Next steps:" echo " 1. Run: cd $(dirname "$0")" echo " 2. Run: npm install" echo " 3. Test: node navigate.js --url https://example.com" echo "" -
install.sh 2.1 KB
#!/bin/bash # Installation script for Chrome DevTools Agent Skill set -e echo "🚀 Installing Chrome DevTools Agent Skill..." echo "" # Check Node.js version echo "Checking Node.js version..." NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1) if [ "$NODE_VERSION" -lt 18 ]; then echo "❌ Error: Node.js 18+ is required. Current version: $(node --version)" echo " Please upgrade Node.js: https://nodejs.org/" exit 1 fi echo "✓ Node.js version: $(node --version)" echo "" # Check for system dependencies (Linux only) if [[ "$OSTYPE" == "linux-gnu"* ]]; then echo "Checking system dependencies (Linux)..." # Check for critical Chrome dependencies MISSING_DEPS=() if ! ldconfig -p | grep -q libnss3.so; then MISSING_DEPS+=("libnss3") fi if ! ldconfig -p | grep -q libnspr4.so; then MISSING_DEPS+=("libnspr4") fi if ! ldconfig -p | grep -q libgbm.so; then MISSING_DEPS+=("libgbm1") fi if [ ${#MISSING_DEPS[@]} -gt 0 ]; then echo "⚠️ Missing system dependencies: ${MISSING_DEPS[*]}" echo "" echo " Chrome/Chromium requires system libraries to run." echo " Install them with:" echo "" echo " ./install-deps.sh" echo "" echo " Or manually:" echo " sudo apt-get install -y libnss3 libnspr4 libgbm1 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2" echo "" read -p " Continue anyway? (y/N) " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Installation cancelled." exit 1 fi else echo "✓ System dependencies found" fi echo "" elif [[ "$OSTYPE" == "darwin"* ]]; then echo "Platform: macOS (no system dependencies needed)" echo "" elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then echo "Platform: Windows (no system dependencies needed)" echo "" fi # Install Node.js dependencies echo "Installing Node.js dependencies..." npm install echo "" echo "✅ Installation complete!" echo "" echo "Test the installation:" echo " node navigate.js --url https://example.com" echo "" echo "For more information:" echo " cat README.md" echo "" -
launch-persistent.js 2.1 KB
#!/usr/bin/env node /** * Launch a persistent Chrome browser that can be reused across multiple commands * Saves the WebSocket endpoint to a file for other scripts to connect to */ import puppeteer from 'puppeteer'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ENDPOINT_FILE = path.join(__dirname, '.browser-endpoint'); async function main() { // Parse command line arguments const args = process.argv.slice(2); const headless = !args.includes('--headless=false') && !args.includes('--no-headless'); const url = args.find(arg => arg.startsWith('--url='))?.split('=')[1] || 'about:blank'; console.log('Launching persistent Chrome browser...'); const browser = await puppeteer.launch({ headless, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--remote-debugging-port=9222' ], defaultViewport: { width: 1920, height: 1080 } }); const wsEndpoint = browser.wsEndpoint(); // Save endpoint to file fs.writeFileSync(ENDPOINT_FILE, wsEndpoint); console.log(`Browser launched. WebSocket endpoint saved to: ${ENDPOINT_FILE}`); console.log(`WebSocket: ${wsEndpoint}`); // Navigate to initial URL if provided if (url !== 'about:blank') { const page = (await browser.pages())[0]; console.log(`Navigating to: ${url}`); await page.goto(url, { waitUntil: 'networkidle2' }); } console.log('\n✓ Browser is ready for commands!'); console.log(' Use other scripts normally - they will connect to this browser.'); console.log(' Run "node close-persistent.js" or press Ctrl+C to close.\n'); // Keep process alive process.on('SIGINT', async () => { console.log('\nClosing browser...'); await browser.close(); if (fs.existsSync(ENDPOINT_FILE)) { fs.unlinkSync(ENDPOINT_FILE); } process.exit(0); }); // Keep alive indefinitely await new Promise(() => {}); } main().catch(error => { console.error('Error launching browser:', error); process.exit(1); }); -
navigate.js 994 B
#!/usr/bin/env node /** * Navigate to a URL * Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000] */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; async function navigate() { const args = parseArgs(process.argv.slice(2)); if (!args.url) { outputError(new Error('--url is required')); return; } try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); const options = { waitUntil: args['wait-until'] || 'networkidle2', timeout: parseInt(args.timeout || '30000') }; await page.goto(args.url, options); const result = { success: true, url: page.url(), title: await page.title() }; outputJSON(result); if (args.close !== 'false') { await closeBrowser(); } } catch (error) { outputError(error); } } navigate(); -
network.js 2.7 KB
#!/usr/bin/env node /** * Monitor network requests * Usage: node network.js --url https://example.com [--types xhr,fetch] [--output requests.json] */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; import fs from 'fs/promises'; async function monitorNetwork() { const args = parseArgs(process.argv.slice(2)); if (!args.url) { outputError(new Error('--url is required')); return; } try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); const requests = []; const filterTypes = args.types ? args.types.split(',').map(t => t.toLowerCase()) : null; // Monitor requests page.on('request', (request) => { const resourceType = request.resourceType().toLowerCase(); if (!filterTypes || filterTypes.includes(resourceType)) { requests.push({ id: request._requestId || requests.length, url: request.url(), method: request.method(), resourceType: resourceType, headers: request.headers(), postData: request.postData(), timestamp: Date.now() }); } }); // Monitor responses const responses = new Map(); page.on('response', async (response) => { const request = response.request(); const resourceType = request.resourceType().toLowerCase(); if (!filterTypes || filterTypes.includes(resourceType)) { try { responses.set(request._requestId || request.url(), { status: response.status(), statusText: response.statusText(), headers: response.headers(), fromCache: response.fromCache(), timing: response.timing() }); } catch (e) { // Ignore errors for some response types } } }); // Navigate await page.goto(args.url, { waitUntil: args['wait-until'] || 'networkidle2' }); // Merge requests with responses const combined = requests.map(req => ({ ...req, response: responses.get(req.id) || responses.get(req.url) || null })); const result = { success: true, url: page.url(), requestCount: combined.length, requests: combined }; if (args.output) { await fs.writeFile(args.output, JSON.stringify(result, null, 2)); outputJSON({ success: true, output: args.output, requestCount: combined.length }); } else { outputJSON(result); } if (args.close !== 'false') { await closeBrowser(); } } catch (error) { outputError(error); } } monitorNetwork(); -
package-lock.json 42.1 KB
{ "name": "chrome-devtools-scripts", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chrome-devtools-scripts", "version": "1.0.0", "dependencies": { "debug": "^4.4.0", "puppeteer": "^23.11.1", "yargs": "^17.7.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@puppeteer/browsers": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", "license": "Apache-2.0", "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" }, "engines": { "node": ">=18" } }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, "node_modules/@types/node": { "version": "24.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", "optional": true, "dependencies": { "undici-types": "~7.16.0" } }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" } }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, "engines": { "node": ">=4" } }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" }, "peerDependenciesMeta": { "react-native-b4a": { "optional": true } } }, "node_modules/bare-events": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.1.tgz", "integrity": "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==", "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" }, "peerDependenciesMeta": { "bare-abort-controller": { "optional": true } } }, "node_modules/bare-fs": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.0.tgz", "integrity": "sha512-GljgCjeupKZJNetTqxKaQArLK10vpmK28or0+RwWjEl5Rk+/xG3wkpmkv+WrcBm3q1BwHKlnhXzR8O37kcvkXQ==", "license": "Apache-2.0", "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "engines": { "bare": ">=1.16.0" }, "peerDependencies": { "bare-buffer": "*" }, "peerDependenciesMeta": { "bare-buffer": { "optional": true } } }, "node_modules/bare-os": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", "license": "Apache-2.0", "optional": true, "engines": { "bare": ">=1.14.0" } }, "node_modules/bare-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "license": "Apache-2.0", "optional": true, "dependencies": { "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", "license": "Apache-2.0", "optional": true, "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "peerDependenciesMeta": { "bare-buffer": { "optional": true }, "bare-events": { "optional": true } } }, "node_modules/bare-url": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz", "integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==", "license": "Apache-2.0", "optional": true, "dependencies": { "bare-path": "^3.0.0" } }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "license": "MIT" }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "license": "MIT", "engines": { "node": "*" } }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/chromium-bidi": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", "license": "Apache-2.0", "dependencies": { "mitt": "3.0.1", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" }, "engines": { "node": ">=12" } }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, "engines": { "node": ">=7.0.0" } }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "license": "MIT", "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { "typescript": ">=4.9.5" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" }, "engines": { "node": ">= 14" } }, "node_modules/devtools-protocol": { "version": "0.0.1367902", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", "license": "BSD-3-Clause" }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/escodegen": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "bin": { "escodegen": "bin/escodegen.js", "esgenerate": "bin/esgenerate.js" }, "engines": { "node": ">=6.0" }, "optionalDependencies": { "source-map": "~0.6.1" } }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" }, "engines": { "node": ">=4" } }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.7.0" } }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "bin": { "extract-zip": "cli.js" }, "engines": { "node": ">= 10.17.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" } }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "license": "MIT", "dependencies": { "pend": "~1.2.0" } }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "license": "MIT", "dependencies": { "pump": "^3.0.0" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/get-uri": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" }, "engines": { "node": ">= 14" } }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" }, "engines": { "node": ">= 14" } }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" }, "engines": { "node": ">= 14" } }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "license": "BSD-3-Clause" }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ip-address": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "license": "MIT", "engines": { "node": ">= 0.4.0" } }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">= 14" } }, "node_modules/pac-resolver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" }, "engines": { "node": ">= 14" } }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/proxy-agent": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">= 14" } }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "node_modules/puppeteer": { "version": "23.11.1", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz", "integrity": "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==", "deprecated": "< 24.15.0 is no longer supported", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1367902", "puppeteer-core": "23.11.1", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" }, "engines": { "node": ">=18" } }, "node_modules/puppeteer-core": { "version": "23.11.1", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", "debug": "^4.4.0", "devtools-protocol": "0.0.1367902", "typed-query-selector": "^2.12.0", "ws": "^8.18.0" }, "engines": { "node": ">=18" } }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "license": "MIT", "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" }, "engines": { "node": ">= 14" } }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/streamx": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "license": "MIT", "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/tar-fs": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "license": "MIT", "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "node_modules/text-decoder": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/typed-query-selector": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", "license": "MIT" }, "node_modules/unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "license": "MIT", "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT", "optional": true }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { "optional": true }, "utf-8-validate": { "optional": true } } }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" } }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } } } } -
package.json 321 B
{ "name": "chrome-devtools-scripts", "version": "1.0.0", "description": "Browser automation scripts for Chrome DevTools Agent Skill", "type": "module", "scripts": {}, "dependencies": { "puppeteer": "^24.15.0", "debug": "^4.4.0", "yargs": "^17.7.2" }, "engines": { "node": ">=18.0.0" } } -
performance.js 3.8 KB
#!/usr/bin/env node /** * Measure performance metrics and record trace * Usage: node performance.js --url https://example.com [--trace trace.json] [--metrics] */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; import fs from 'fs/promises'; async function measurePerformance() { const args = parseArgs(process.argv.slice(2)); if (!args.url) { outputError(new Error('--url is required')); return; } try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); // Start tracing if requested if (args.trace) { await page.tracing.start({ path: args.trace, categories: [ 'devtools.timeline', 'disabled-by-default-devtools.timeline', 'disabled-by-default-devtools.timeline.frame' ] }); } // Navigate await page.goto(args.url, { waitUntil: 'networkidle2' }); // Stop tracing if (args.trace) { await page.tracing.stop(); } // Get performance metrics const metrics = await page.metrics(); // Get Core Web Vitals const vitals = await page.evaluate(() => { return new Promise((resolve) => { const vitals = { LCP: null, FID: null, CLS: 0, FCP: null, TTFB: null }; // LCP try { new PerformanceObserver((list) => { const entries = list.getEntries(); if (entries.length > 0) { const lastEntry = entries[entries.length - 1]; vitals.LCP = lastEntry.renderTime || lastEntry.loadTime; } }).observe({ entryTypes: ['largest-contentful-paint'], buffered: true }); } catch (e) {} // CLS try { new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (!entry.hadRecentInput) { vitals.CLS += entry.value; } }); }).observe({ entryTypes: ['layout-shift'], buffered: true }); } catch (e) {} // FCP try { const paintEntries = performance.getEntriesByType('paint'); const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint'); if (fcpEntry) { vitals.FCP = fcpEntry.startTime; } } catch (e) {} // TTFB try { const [navigationEntry] = performance.getEntriesByType('navigation'); if (navigationEntry) { vitals.TTFB = navigationEntry.responseStart - navigationEntry.requestStart; } } catch (e) {} // Wait a bit for metrics to stabilize setTimeout(() => resolve(vitals), 1000); }); }); // Get resource timing const resources = await page.evaluate(() => { return performance.getEntriesByType('resource').map(r => ({ name: r.name, type: r.initiatorType, duration: r.duration, size: r.transferSize, startTime: r.startTime })); }); const result = { success: true, url: page.url(), metrics: { ...metrics, JSHeapUsedSizeMB: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2), JSHeapTotalSizeMB: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2) }, vitals: vitals, resources: { count: resources.length, totalDuration: resources.reduce((sum, r) => sum + r.duration, 0), items: args.resources === 'true' ? resources : undefined } }; if (args.trace) { result.trace = args.trace; } outputJSON(result); if (args.close !== 'false') { await closeBrowser(); } } catch (error) { outputError(error); } } measurePerformance(); -
PERSISTENT-BROWSER.md 3.1 KB
# Persistent Browser Mode These scripts enable you to launch Chrome once and keep it open for multiple commands, making it easier to interact with authenticated sessions and multi-step workflows. ## Quick Start ### 1. Launch Persistent Browser ```bash # Launch browser (visible window) node launch-persistent.js --headless=false # Launch with initial URL node launch-persistent.js --headless=false --url=https://example.com/login # Launch headless node launch-persistent.js ``` The browser will stay open and print a message confirming it's ready. ### 2. Run Commands All existing scripts will automatically connect to the persistent browser: ```bash # Navigate to a page node navigate.js --url https://example.com/dashboard # Take a screenshot node screenshot.js --output ./screenshot.png # Run JavaScript node evaluate.js --script "document.title" # Get page snapshot node snapshot.js # Fill forms node fill.js --selector "#username" --value "user@example.com" node click.js --selector "button[type=submit]" ``` ### 3. Close Browser ```bash # Close the persistent browser node close-persistent.js # Or press Ctrl+C in the launch-persistent.js terminal ``` ## How It Works 1. **launch-persistent.js** starts Chrome with remote debugging enabled and saves the WebSocket endpoint to `.browser-endpoint` 2. All other scripts check for this file first and connect to the existing browser if found 3. If no persistent browser exists, scripts fall back to launching their own temporary browser (original behavior) ## Use Cases ### Authenticated Sessions ```bash # Launch browser node launch-persistent.js --headless=false --url=https://app.example.com/login # Manually log in through the visible browser window # Then run commands on authenticated pages node navigate.js --url=https://app.example.com/dashboard node screenshot.js --output ./dashboard.png node evaluate.js --script "document.querySelector('.user-name').textContent" ``` ### Multi-Step Workflows ```bash # Launch browser node launch-persistent.js --headless=false # Fill out multi-page form node navigate.js --url=https://example.com/signup node fill.js --selector "#email" --value "user@example.com" node click.js --selector ".next-button" # Wait for page to load, then continue... node fill.js --selector "#password" --value "secret123" node click.js --selector ".submit-button" ``` ### Accessibility Audits on Live Pages ```bash # Launch and navigate manually to complex authenticated state node launch-persistent.js --headless=false # After manual navigation/interaction, run audits node evaluate.js --script "/* accessibility check script */" node screenshot.js --output ./audit.png ``` ## Advantages - **Manual interaction**: Log in, navigate, or manipulate the page manually - **Persistent state**: Cookies, localStorage, and session state preserved - **Faster iterations**: No browser startup delay for each command - **Real-world testing**: Test on actual authenticated or complex application states ## Backwards Compatibility All existing scripts work exactly as before if no persistent browser is running. The enhancement is completely opt-in. -
README.md 5.2 KB
# Chrome DevTools Scripts CLI scripts for browser automation using Puppeteer. **CRITICAL**: Always check `pwd` before running scripts. ## Installation ### Quick Install ```bash pwd # Should show current working directory cd .claude/skills/chrome-devtools/scripts ./install.sh # Auto-checks dependencies and installs ``` ### Manual Installation **Linux/WSL** - Install system dependencies first: ```bash ./install-deps.sh # Auto-detects OS (Ubuntu, Debian, Fedora, etc.) ``` Or manually: ```bash sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 ``` **All platforms** - Install Node dependencies: ```bash npm install ``` ## Scripts **CRITICAL**: Always check `pwd` before running scripts. ### navigate.js Navigate to a URL. ```bash node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000] ``` ### screenshot.js Take a screenshot with automatic compression. **Important**: Always save screenshots to `./docs/screenshots` directory. ```bash node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress] ``` **Automatic Compression**: Screenshots >5MB are automatically compressed using ImageMagick to ensure compatibility with Gemini API and Claude Code. Install ImageMagick for this feature: - macOS: `brew install imagemagick` - Linux: `sudo apt-get install imagemagick` Options: - `--max-size N` - Custom size threshold in MB (default: 5) - `--no-compress` - Disable automatic compression - `--format png|jpeg` - Output format (default: png) - `--quality N` - JPEG quality 0-100 (default: auto) ### click.js Click an element. ```bash node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"] ``` ### fill.js Fill form fields. ```bash node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true] ``` ### evaluate.js Execute JavaScript in page context. ```bash node evaluate.js --script "document.title" [--url https://example.com] ``` ### snapshot.js Get DOM snapshot with interactive elements. ```bash node snapshot.js [--url https://example.com] [--output snapshot.json] ``` ### console.js Monitor console messages. ```bash node console.js --url https://example.com [--types error,warn] [--duration 5000] ``` ### network.js Monitor network requests. ```bash node network.js --url https://example.com [--types xhr,fetch] [--output requests.json] ``` ### performance.js Measure performance metrics and record trace. ```bash node performance.js --url https://example.com [--trace trace.json] [--metrics] [--resources true] ``` ## Common Options - `--headless false` - Show browser window - `--close false` - Keep browser open - `--timeout 30000` - Set timeout in milliseconds - `--wait-until networkidle2` - Wait strategy (load, domcontentloaded, networkidle0, networkidle2) ## Selector Support Scripts that accept `--selector` (click.js, fill.js, screenshot.js) support both **CSS** and **XPath** selectors. ### CSS Selectors (Default) ```bash # Element tag node click.js --selector "button" --url https://example.com # Class selector node click.js --selector ".btn-submit" --url https://example.com # ID selector node fill.js --selector "#email" --value "user@example.com" --url https://example.com # Attribute selector node click.js --selector 'button[type="submit"]' --url https://example.com # Complex selector node screenshot.js --selector "div.container > button.btn-primary" --output btn.png ``` ### XPath Selectors XPath selectors start with `/` or `(//` and are automatically detected: ```bash # Text matching - exact node click.js --selector '//button[text()="Submit"]' --url https://example.com # Text matching - contains node click.js --selector '//button[contains(text(),"Submit")]' --url https://example.com # Attribute matching node fill.js --selector '//input[@type="email"]' --value "user@example.com" # Multiple conditions node click.js --selector '//button[@type="submit" and contains(text(),"Save")]' # Descendant selection node screenshot.js --selector '//div[@class="modal"]//button[@class="close"]' --output modal.png # Nth element node click.js --selector '(//button)[2]' # Second button on page ``` ### Discovering Selectors Use `snapshot.js` to discover correct selectors: ```bash # Get all interactive elements node snapshot.js --url https://example.com | jq '.elements[]' # Find buttons node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")' # Find inputs node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="INPUT")' ``` ### Security XPath selectors are validated to prevent injection attacks. The following patterns are blocked: - `javascript:` - `<script` - `onerror=`, `onload=`, `onclick=` - `eval(`, `Function(`, `constructor(` Selectors exceeding 1000 characters are rejected (DoS prevention). ## Output Format All scripts output JSON to stdout: ```json { "success": true, "url": "https://example.com", "title": "Example Domain", ... } ``` Errors are output to stderr: ```json { "success": false, "error": "Error message", "stack": "..." } ``` -
screenshot.js 5.9 KB
#!/usr/bin/env node /** * Take a screenshot * Usage: node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress] * Supports both CSS and XPath selectors: * - CSS: node screenshot.js --selector ".main-content" --output page.png * - XPath: node screenshot.js --selector "//div[@class='main-content']" --output page.png */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; import { parseSelector, getElement, enhanceError } from './lib/selector.js'; import fs from 'fs/promises'; import path from 'path'; import { execSync } from 'child_process'; /** * Compress image using ImageMagick if it exceeds max size * @param {string} filePath - Path to the image file * @param {number} maxSizeMB - Maximum file size in MB (default: 5) * @returns {Promise<{compressed: boolean, originalSize: number, finalSize: number}>} */ async function compressImageIfNeeded(filePath, maxSizeMB = 5) { const stats = await fs.stat(filePath); const originalSize = stats.size; const maxSizeBytes = maxSizeMB * 1024 * 1024; if (originalSize <= maxSizeBytes) { return { compressed: false, originalSize, finalSize: originalSize }; } try { // Check if ImageMagick is available try { execSync('magick -version', { stdio: 'pipe' }); } catch { try { execSync('convert -version', { stdio: 'pipe' }); } catch { console.error('Warning: ImageMagick not found. Install it to enable automatic compression.'); return { compressed: false, originalSize, finalSize: originalSize }; } } const ext = path.extname(filePath).toLowerCase(); const tempPath = filePath.replace(ext, `.temp${ext}`); // Determine compression strategy based on file type let compressionCmd; if (ext === '.png') { // For PNG: resize and compress with quality compressionCmd = `magick "${filePath}" -strip -resize 90% -quality 85 "${tempPath}"`; } else if (ext === '.jpg' || ext === '.jpeg') { // For JPEG: compress with quality and progressive compressionCmd = `magick "${filePath}" -strip -quality 80 -interlace Plane "${tempPath}"`; } else { // For other formats: convert to JPEG with compression compressionCmd = `magick "${filePath}" -strip -quality 80 "${tempPath.replace(ext, '.jpg')}"`; } // Try compression execSync(compressionCmd, { stdio: 'pipe' }); const compressedStats = await fs.stat(tempPath); const compressedSize = compressedStats.size; // If still too large, try more aggressive compression if (compressedSize > maxSizeBytes) { const finalPath = filePath.replace(ext, `.final${ext}`); let aggressiveCmd; if (ext === '.png') { aggressiveCmd = `magick "${tempPath}" -strip -resize 75% -quality 70 "${finalPath}"`; } else { aggressiveCmd = `magick "${tempPath}" -strip -quality 60 -sampling-factor 4:2:0 "${finalPath}"`; } execSync(aggressiveCmd, { stdio: 'pipe' }); await fs.unlink(tempPath); await fs.rename(finalPath, filePath); } else { await fs.rename(tempPath, filePath); } const finalStats = await fs.stat(filePath); return { compressed: true, originalSize, finalSize: finalStats.size }; } catch (error) { console.error('Compression error:', error.message); // If compression fails, keep original file try { const tempPath = filePath.replace(path.extname(filePath), '.temp' + path.extname(filePath)); await fs.unlink(tempPath).catch(() => {}); } catch {} return { compressed: false, originalSize, finalSize: originalSize }; } } async function screenshot() { const args = parseArgs(process.argv.slice(2)); if (!args.output) { outputError(new Error('--output is required')); return; } try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); // Navigate if URL provided if (args.url) { await page.goto(args.url, { waitUntil: args['wait-until'] || 'networkidle2' }); } const screenshotOptions = { path: args.output, type: args.format || 'png', fullPage: args['full-page'] === 'true' }; if (args.quality) { screenshotOptions.quality = parseInt(args.quality); } let buffer; if (args.selector) { // Parse and validate selector const parsed = parseSelector(args.selector); // Get element based on selector type const element = await getElement(page, parsed); if (!element) { throw new Error(`Element not found: ${args.selector}`); } buffer = await element.screenshot(screenshotOptions); } else { buffer = await page.screenshot(screenshotOptions); } const result = { success: true, output: path.resolve(args.output), size: buffer.length, url: page.url() }; // Compress image if needed (unless --no-compress flag is set) if (args['no-compress'] !== 'true') { const maxSize = args['max-size'] ? parseFloat(args['max-size']) : 5; const compressionResult = await compressImageIfNeeded(args.output, maxSize); if (compressionResult.compressed) { result.compressed = true; result.originalSize = compressionResult.originalSize; result.size = compressionResult.finalSize; result.compressionRatio = ((1 - compressionResult.finalSize / compressionResult.originalSize) * 100).toFixed(2) + '%'; } } outputJSON(result); if (args.close !== 'false') { await closeBrowser(); } } catch (error) { // Enhance error message if selector-related if (args.selector) { const enhanced = enhanceError(error, args.selector); outputError(enhanced); } else { outputError(error); } process.exit(1); } } screenshot(); -
snapshot.js 3.4 KB
#!/usr/bin/env node /** * Get DOM snapshot with selectors * Usage: node snapshot.js [--url https://example.com] [--output snapshot.json] */ import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js'; import fs from 'fs/promises'; async function snapshot() { const args = parseArgs(process.argv.slice(2)); try { const browser = await getBrowser({ headless: args.headless !== 'false' }); const page = await getPage(browser); // Navigate if URL provided if (args.url) { await page.goto(args.url, { waitUntil: args['wait-until'] || 'networkidle2' }); } // Get interactive elements with metadata const elements = await page.evaluate(() => { const interactiveSelectors = [ 'a[href]', 'button', 'input', 'textarea', 'select', '[onclick]', '[role="button"]', '[role="link"]', '[contenteditable]' ]; const elements = []; const selector = interactiveSelectors.join(', '); const nodes = document.querySelectorAll(selector); nodes.forEach((el, index) => { const rect = el.getBoundingClientRect(); // Generate unique selector let uniqueSelector = ''; if (el.id) { uniqueSelector = `#${el.id}`; } else if (el.className) { const classes = Array.from(el.classList).join('.'); uniqueSelector = `${el.tagName.toLowerCase()}.${classes}`; } else { uniqueSelector = el.tagName.toLowerCase(); } elements.push({ index: index, tagName: el.tagName.toLowerCase(), type: el.type || null, id: el.id || null, className: el.className || null, name: el.name || null, value: el.value || null, text: el.textContent?.trim().substring(0, 100) || null, href: el.href || null, selector: uniqueSelector, xpath: getXPath(el), visible: rect.width > 0 && rect.height > 0, position: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } }); }); function getXPath(element) { if (element.id) { return `//*[@id="${element.id}"]`; } if (element === document.body) { return '/html/body'; } let ix = 0; const siblings = element.parentNode?.childNodes || []; for (let i = 0; i < siblings.length; i++) { const sibling = siblings[i]; if (sibling === element) { return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']'; } if (sibling.nodeType === 1 && sibling.tagName === element.tagName) { ix++; } } return ''; } return elements; }); const result = { success: true, url: page.url(), title: await page.title(), elementCount: elements.length, elements: elements }; if (args.output) { await fs.writeFile(args.output, JSON.stringify(result, null, 2)); outputJSON({ success: true, output: args.output, elementCount: elements.length }); } else { outputJSON(result); } if (args.close !== 'false') { await closeBrowser(); } } catch (error) { outputError(error); } } snapshot();
-
-
.gitignore 17 B · in bundle
-
SKILL.md 12.1 KB
--- name: chrome-devtools description: Browser automation, debugging, and performance analysis using Puppeteer CLI scripts. Use for automating browsers, taking screenshots, analyzing performance, monitoring network traffic, web scraping, form automation, and JavaScript debugging. license: Apache-2.0 --- # Chrome DevTools Agent Skill Browser automation via executable Puppeteer scripts. All scripts output JSON for easy parsing. ## Quick Start **CRITICAL**: Always check `pwd` before running scripts. ### Installation #### Step 1: Install System Dependencies (Linux/WSL only) On Linux/WSL, Chrome requires system libraries. Install them first: ```bash pwd # Should show current working directory cd .claude/skills/chrome-devtools/scripts ./install-deps.sh # Auto-detects OS and installs required libs ``` Supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro **macOS/Windows**: Skip this step (dependencies bundled with Chrome) #### Step 2: Install Node Dependencies ```bash npm install # Installs puppeteer, debug, yargs ``` #### Step 3: Install ImageMagick (Optional, Recommended) ImageMagick enables automatic screenshot compression to keep files under 5MB: **macOS:** ```bash brew install imagemagick ``` **Ubuntu/Debian/WSL:** ```bash sudo apt-get install imagemagick ``` **Verify:** ```bash magick -version # or: convert -version ``` Without ImageMagick, screenshots >5MB will not be compressed (may fail to load in Gemini/Claude). ### Test ```bash node navigate.js --url https://example.com # Output: {"success": true, "url": "https://example.com", "title": "Example Domain"} ``` ## Available Scripts All scripts are in `.claude/skills/chrome-devtools/scripts/` **CRITICAL**: Always check `pwd` before running scripts. ### Script Usage - `./scripts/README.md` ### Core Automation - `navigate.js` - Navigate to URLs - `screenshot.js` - Capture screenshots (full page or element) - `click.js` - Click elements - `fill.js` - Fill form fields - `evaluate.js` - Execute JavaScript in page context ### Analysis & Monitoring - `snapshot.js` - Extract interactive elements with metadata - `console.js` - Monitor console messages/errors - `network.js` - Track HTTP requests/responses - `performance.js` - Measure Core Web Vitals + record traces ## Usage Patterns ### Single Command ```bash pwd # Should show current working directory cd .claude/skills/chrome-devtools/scripts node screenshot.js --url https://example.com --output ./docs/screenshots/page.png ``` **Important**: Always save screenshots to `./docs/screenshots` directory. ### Automatic Image Compression Screenshots are **automatically compressed** if they exceed 5MB to ensure compatibility with Gemini API and Claude Code (which have 5MB limits). This uses ImageMagick internally: ```bash # Default: auto-compress if >5MB node screenshot.js --url https://example.com --output page.png # Custom size threshold (e.g., 3MB) node screenshot.js --url https://example.com --output page.png --max-size 3 # Disable compression node screenshot.js --url https://example.com --output page.png --no-compress ``` **Compression behavior:** - PNG: Resizes to 90% + quality 85 (or 75% + quality 70 if still too large) - JPEG: Quality 80 + progressive encoding (or quality 60 if still too large) - Other formats: Converted to JPEG with compression - Requires ImageMagick installed (see imagemagick skill) **Output includes compression info:** ```json { "success": true, "output": "/path/to/page.png", "compressed": true, "originalSize": 8388608, "size": 3145728, "compressionRatio": "62.50%", "url": "https://example.com" } ``` ### Chain Commands (reuse browser) ```bash # Keep browser open with --close false node navigate.js --url https://example.com/login --close false node fill.js --selector "#email" --value "user@example.com" --close false node fill.js --selector "#password" --value "secret" --close false node click.js --selector "button[type=submit]" ``` ### Parse JSON Output ```bash # Extract specific fields with jq node performance.js --url https://example.com | jq '.vitals.LCP' # Save to file node network.js --url https://example.com --output /tmp/requests.json ``` ## Execution Protocol ### Working Directory Verification BEFORE executing any script: 1. Check current working directory with `pwd` 2. Verify in `.claude/skills/chrome-devtools/scripts/` directory 3. If wrong directory, `cd` to correct location 4. Use absolute paths for all output files Example: ```bash pwd # Should show: .../chrome-devtools/scripts # If wrong: cd .claude/skills/chrome-devtools/scripts ``` ### Output Validation AFTER screenshot/capture operations: 1. Verify file created with `ls -lh <output-path>` 2. Read screenshot using Read tool to confirm content 3. Check JSON output for success:true 4. Report file size and compression status Example: ```bash node screenshot.js --url https://example.com --output ./docs/screenshots/page.png ls -lh ./docs/screenshots/page.png # Verify file exists # Then use Read tool to visually inspect ``` 5. Restart working directory to the project root. ### Error Recovery If script fails: 1. Check error message for selector issues 2. Use snapshot.js to discover correct selectors 3. Try XPath selector if CSS selector fails 4. Verify element is visible and interactive Example: ```bash # CSS selector fails node click.js --url https://example.com --selector ".btn-submit" # Error: waiting for selector ".btn-submit" failed # Discover correct selector node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")' # Try XPath node click.js --url https://example.com --selector "//button[contains(text(),'Submit')]" ``` ### Common Mistakes ❌ Wrong working directory → output files go to wrong location ❌ Skipping output validation → silent failures ❌ Using complex CSS selectors without testing → selector errors ❌ Not checking element visibility → timeout errors ✅ Always verify `pwd` before running scripts ✅ Always validate output after screenshots ✅ Use snapshot.js to discover selectors ✅ Test selectors with simple commands first ## Common Workflows ### Web Scraping ```bash node evaluate.js --url https://example.com --script " Array.from(document.querySelectorAll('.item')).map(el => ({ title: el.querySelector('h2')?.textContent, link: el.querySelector('a')?.href })) " | jq '.result' ``` ### Performance Testing ```bash PERF=$(node performance.js --url https://example.com) LCP=$(echo $PERF | jq '.vitals.LCP') if (( $(echo "$LCP < 2500" | bc -l) )); then echo "✓ LCP passed: ${LCP}ms" else echo "✗ LCP failed: ${LCP}ms" fi ``` ### Form Automation ```bash node fill.js --url https://example.com --selector "#search" --value "query" --close false node click.js --selector "button[type=submit]" ``` ### Error Monitoring ```bash node console.js --url https://example.com --types error,warn --duration 5000 | jq '.messageCount' ``` ## Script Options All scripts support: - `--headless false` - Show browser window - `--close false` - Keep browser open for chaining - `--timeout 30000` - Set timeout (milliseconds) - `--wait-until networkidle2` - Wait strategy See `./scripts/README.md` for complete options. ## Output Format All scripts output JSON to stdout: ```json { "success": true, "url": "https://example.com", ... // script-specific data } ``` Errors go to stderr: ```json { "success": false, "error": "Error message" } ``` ## Finding Elements Use `snapshot.js` to discover selectors: ```bash node snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}' ``` ## Troubleshooting ### Common Errors **"Cannot find package 'puppeteer'"** - Run: `npm install` in the scripts directory **"error while loading shared libraries: libnss3.so"** (Linux/WSL) - Missing system dependencies - Fix: Run `./install-deps.sh` in scripts directory - Manual install: `sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1` **"Failed to launch the browser process"** - Check system dependencies installed (Linux/WSL) - Verify Chrome downloaded: `ls ~/.cache/puppeteer` - Try: `npm rebuild` then `npm install` **Chrome not found** - Puppeteer auto-downloads Chrome during `npm install` - If failed, manually trigger: `npx puppeteer browsers install chrome` ### Script Issues **Element not found** - Get snapshot first to find correct selector: `node snapshot.js --url <url>` **Script hangs** - Increase timeout: `--timeout 60000` - Change wait strategy: `--wait-until load` or `--wait-until domcontentloaded` **Blank screenshot** - Wait for page load: `--wait-until networkidle2` - Increase timeout: `--timeout 30000` **Permission denied on scripts** - Make executable: `chmod +x *.sh` **Screenshot too large (>5MB)** - Install ImageMagick for automatic compression - Manually set lower threshold: `--max-size 3` - Use JPEG format instead of PNG: `--format jpeg --quality 80` - Capture specific element instead of full page: `--selector .main-content` **Compression not working** - Verify ImageMagick installed: `magick -version` or `convert -version` - Check file was actually compressed in output JSON: `"compressed": true` - For very large pages, use `--selector` to capture only needed area ## Resilience and Subagent Stability When using the browser subagent or Puppeteer scripts, you may encounter connectivity issues (e.g., "connection is reset") or tab loss. Follow these guardrails for professional-grade reliability: ### 1. State Verification First **Rule**: Always call `list_pages` (or a similar verification script) at the start of a new subagent task or if an error occurs. - Do not assume the tab you just used is still active. - If a "Connection Reset" error occurs, it usually means the browser instance crashed or closed. Immediately attempt to re-launch or re-initiate the session. ### 2. Handling Authenticated Sessions **Pattern**: Use Persistent Browser Mode for sites requiring login or complex state. - Launch with `node launch-persistent.js --headless false`. - This ensures the browser process stays alive even if the subagent session times out. - Subsequent subagents can reconnect via the WebSocket endpoint in `.browser-endpoint`. ### 3. Progressive Handoff **Rule**: Use `--close false` consistently if the workflow requires multiple steps. - When finishing a subagent task, explicitly check if the browser should remain open for the next phase. - Provide the user with the URL and a screenshot of the final state to confirm continuity. ### 4. Error Recovery Strategy If a "Page not found" or "Target closed" error occurs: 1. **List all open pages** to see if the tab was accidentally closed or navigated away. 2. **Check for session tokens** (cookies/local storage) if the site allows session persistence. 3. **Re-navigate** to the last known URL if the tab is truly lost. 4. **Notify the user** clearly about the reset instead of retrying silently 10 times. ### 5. Interaction Heartbeats Before a complex `click` or `fill` sequence, take a small snapshot or evaluate a simple script (e.g., `() => document.readyState`) to "ping" the page. If it fails, trigger the recovery strategy early. --- ## Reference Documentation Detailed guides available in `./references/`: - [CDP Domains Reference](./references/cdp-domains.md) - 47 Chrome DevTools Protocol domains - [Puppeteer Quick Reference](./references/puppeteer-reference.md) - Complete Puppeteer API patterns - [Performance Analysis Guide](./references/performance-guide.md) - Core Web Vitals optimization ## Advanced Usage ### Custom Scripts Create custom scripts using shared library: ```javascript import { getBrowser, getPage, closeBrowser, outputJSON } from './lib/browser.js'; // Your automation logic ``` ### Direct CDP Access ```javascript const client = await page.createCDPSession(); await client.send('Emulation.setCPUThrottlingRate', { rate: 4 }); ``` See reference documentation for advanced patterns and complete API coverage. ## External Resources - [Puppeteer Documentation](https://pptr.dev/) - [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) - [Scripts README](./scripts/README.md)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.