Initial commit
This commit is contained in:
202
skills/website-debug/SKILL.md
Normal file
202
skills/website-debug/SKILL.md
Normal file
@@ -0,0 +1,202 @@
|
||||
---
|
||||
name: website-debug
|
||||
description: >
|
||||
Frontend website debugging toolkit using Chrome DevTools Protocol with Playwright/WebKit fallbacks.
|
||||
Use this skill when: (1) Debugging CSS, HTML, or JavaScript issues on a webpage, (2) Taking screenshots
|
||||
to verify visual changes, (3) Inspecting DOM structure or console errors, (4) Testing responsive layouts,
|
||||
(5) Extracting selectors for automation, (6) Self-debugging frontend work Claude has created,
|
||||
(7) User says "debug this page", "check my site", "why doesn't this look right", or "fix the frontend".
|
||||
Supports Chrome (primary) and Safari/WebKit (via Playwright). Designed for agent-driven debugging loops.
|
||||
---
|
||||
|
||||
# Website Debugging Skill
|
||||
|
||||
Lightweight, token-efficient browser debugging toolkit for frontend development. Uses CLI scripts instead of MCP servers to minimize context usage (~300 tokens vs 13k-18k).
|
||||
|
||||
## Quick Start
|
||||
|
||||
Use the slash commands for easiest access:
|
||||
- `/debug-page <url>` - Start debugging session
|
||||
- `/screenshot` - Take screenshot
|
||||
- `/pick-element` - Interactive element selection
|
||||
- `/test-responsive` - Test at all breakpoints
|
||||
- `/verify-changes` - Verify after making changes
|
||||
|
||||
### Or use scripts directly:
|
||||
|
||||
```bash
|
||||
# Start browser
|
||||
node scripts/browser-start.js
|
||||
node scripts/browser-start.js --profile # Preserve logins
|
||||
node scripts/browser-start.js --webkit # Safari/WebKit
|
||||
|
||||
# Navigate
|
||||
node scripts/browser-nav.js https://localhost:3000
|
||||
|
||||
# Debug
|
||||
node scripts/browser-screenshot.js
|
||||
node scripts/browser-eval.js 'document.title'
|
||||
node scripts/browser-pick.js "Select element"
|
||||
node scripts/browser-console.js --errors
|
||||
node scripts/browser-network.js --failures
|
||||
```
|
||||
|
||||
## Core Tools Reference
|
||||
|
||||
| Script | Purpose | Output |
|
||||
|--------|---------|--------|
|
||||
| `browser-start.sh` | Launch Chrome/WebKit with debug port | Status message |
|
||||
| `browser-nav.sh <url>` | Navigate to URL | Confirmation |
|
||||
| `browser-screenshot.sh` | Capture viewport | File path (PNG) |
|
||||
| `browser-eval.sh '<js>'` | Run JS in page | Result or error |
|
||||
| `browser-pick.sh "<msg>"` | Interactive selector | CSS selectors |
|
||||
| `browser-console.sh` | Get console output | Logs/errors |
|
||||
| `browser-network.sh` | Network activity | Request/response data |
|
||||
| `browser-dom.sh "<sel>"` | Get DOM snapshot | HTML fragment |
|
||||
| `browser-close.sh` | Close browser | Confirmation |
|
||||
|
||||
## Self-Debugging Workflow
|
||||
|
||||
When debugging frontend code Claude has written or modified:
|
||||
|
||||
### 1. Visual Verification Loop
|
||||
```bash
|
||||
# After making CSS/HTML changes, verify visually
|
||||
./scripts/browser-screenshot.sh
|
||||
# Claude reads the screenshot, identifies issues, iterates
|
||||
```
|
||||
|
||||
### 2. Console Error Detection
|
||||
```bash
|
||||
# Check for JavaScript errors after changes
|
||||
./scripts/browser-console.sh --errors
|
||||
# Fix any errors found, re-verify
|
||||
```
|
||||
|
||||
### 3. Responsive Testing
|
||||
```bash
|
||||
# Test at different viewport sizes
|
||||
./scripts/browser-resize.sh 375 667 # iPhone SE
|
||||
./scripts/browser-screenshot.sh
|
||||
./scripts/browser-resize.sh 768 1024 # iPad
|
||||
./scripts/browser-screenshot.sh
|
||||
./scripts/browser-resize.sh 1920 1080 # Desktop
|
||||
./scripts/browser-screenshot.sh
|
||||
```
|
||||
|
||||
### 4. Element Inspection
|
||||
```bash
|
||||
# When user reports "X looks wrong", have them select it
|
||||
./scripts/browser-pick.sh "Click on the element that looks wrong"
|
||||
# Returns detailed info including computed styles
|
||||
```
|
||||
|
||||
## Browser Engine Selection
|
||||
|
||||
### Chrome (Default)
|
||||
Primary engine. Uses Chrome DevTools Protocol on port 9222.
|
||||
- Best debugging experience
|
||||
- Full DevTools compatibility
|
||||
- Use `--profile` to preserve logins
|
||||
|
||||
### WebKit/Safari
|
||||
Fallback via Playwright's WebKit build. Closest to Safari behavior on macOS.
|
||||
```bash
|
||||
./scripts/browser-start.sh --webkit
|
||||
```
|
||||
- Use for Safari-specific testing
|
||||
- Layout verification
|
||||
- WebKit-specific bugs
|
||||
|
||||
### When to Use Each
|
||||
|
||||
| Scenario | Engine |
|
||||
|----------|--------|
|
||||
| General debugging | Chrome |
|
||||
| Safari layout issues | WebKit |
|
||||
| Testing with logins | Chrome `--profile` |
|
||||
| Cross-browser comparison | Both |
|
||||
| CI/headless testing | Chrome or WebKit |
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Detailed Documentation
|
||||
For complex scenarios, load the appropriate reference:
|
||||
|
||||
- **CSS Debugging**: See [references/css-debug.md](references/css-debug.md)
|
||||
- **JavaScript Errors**: See [references/js-debug.md](references/js-debug.md)
|
||||
- **Network Issues**: See [references/network-debug.md](references/network-debug.md)
|
||||
- **Responsive Design**: See [references/responsive-debug.md](references/responsive-debug.md)
|
||||
- **Performance**: See [references/performance-debug.md](references/performance-debug.md)
|
||||
|
||||
### Composable Output
|
||||
|
||||
All scripts output to files when practical, enabling:
|
||||
```bash
|
||||
# Capture multiple screenshots for comparison
|
||||
./scripts/browser-screenshot.sh > /tmp/before.png
|
||||
# ... make changes ...
|
||||
./scripts/browser-screenshot.sh > /tmp/after.png
|
||||
|
||||
# Save DOM snapshot for analysis
|
||||
./scripts/browser-dom.sh "body" > /tmp/page-structure.html
|
||||
|
||||
# Export console log for review
|
||||
./scripts/browser-console.sh > /tmp/console-log.txt
|
||||
```
|
||||
|
||||
### Chaining Commands
|
||||
```bash
|
||||
# Navigate and screenshot in one command
|
||||
./scripts/browser-nav.sh https://example.com && ./scripts/browser-screenshot.sh
|
||||
|
||||
# Full page audit
|
||||
./scripts/browser-nav.sh $URL && \
|
||||
./scripts/browser-console.sh --errors > /tmp/errors.txt && \
|
||||
./scripts/browser-screenshot.sh
|
||||
```
|
||||
|
||||
## Setup Requirements
|
||||
|
||||
### Chrome
|
||||
Chrome must be launchable from command line. The start script handles this automatically.
|
||||
|
||||
### WebKit (Optional)
|
||||
For Safari testing, ensure Playwright is installed:
|
||||
```bash
|
||||
npm install -g playwright
|
||||
npx playwright install webkit
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
Scripts require Node.js and puppeteer-core:
|
||||
```bash
|
||||
npm install -g puppeteer-core
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot connect to browser"
|
||||
Browser may not be running or wrong port:
|
||||
```bash
|
||||
./scripts/browser-start.sh # Restart browser
|
||||
```
|
||||
|
||||
### "Permission denied"
|
||||
Scripts may need execute permission:
|
||||
```bash
|
||||
chmod +x ./scripts/*.sh
|
||||
```
|
||||
|
||||
### Chrome already running
|
||||
Kill existing instances first:
|
||||
```bash
|
||||
killall "Google Chrome" 2>/dev/null
|
||||
./scripts/browser-start.sh
|
||||
```
|
||||
|
||||
### WebKit not found
|
||||
Install Playwright browsers:
|
||||
```bash
|
||||
npx playwright install webkit
|
||||
```
|
||||
38
skills/website-debug/package.json
Normal file
38
skills/website-debug/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "website-debug-skill",
|
||||
"version": "1.0.0",
|
||||
"description": "Frontend website debugging toolkit for Claude Code",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"setup": "bash scripts/setup.sh",
|
||||
"start": "node scripts/browser-start.js",
|
||||
"start:profile": "node scripts/browser-start.js --profile",
|
||||
"start:webkit": "node scripts/browser-start.js --webkit",
|
||||
"nav": "node scripts/browser-nav.js",
|
||||
"screenshot": "node scripts/browser-screenshot.js",
|
||||
"eval": "node scripts/browser-eval.js",
|
||||
"pick": "node scripts/browser-pick.js",
|
||||
"console": "node scripts/browser-console.js",
|
||||
"resize": "node scripts/browser-resize.js",
|
||||
"dom": "node scripts/browser-dom.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"puppeteer-core": "^23.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"playwright": "^1.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"debugging",
|
||||
"chrome",
|
||||
"devtools",
|
||||
"browser",
|
||||
"automation",
|
||||
"claude-code",
|
||||
"skill"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
290
skills/website-debug/references/css-debug.md
Normal file
290
skills/website-debug/references/css-debug.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# CSS Debugging Reference
|
||||
|
||||
Advanced CSS debugging workflows and patterns for frontend development.
|
||||
|
||||
## Quick Diagnostics
|
||||
|
||||
### Check Element Visibility
|
||||
```bash
|
||||
# Is element visible?
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".target");
|
||||
const s = getComputedStyle(el);
|
||||
return {
|
||||
display: s.display,
|
||||
visibility: s.visibility,
|
||||
opacity: s.opacity,
|
||||
hidden: el.hidden,
|
||||
height: s.height,
|
||||
width: s.width,
|
||||
overflow: s.overflow
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
### Element Not Showing?
|
||||
Common causes and checks:
|
||||
|
||||
```bash
|
||||
# Check display property
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector(".target")).display'
|
||||
# If "none" - element is hidden by CSS
|
||||
|
||||
# Check visibility
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector(".target")).visibility'
|
||||
# If "hidden" - element takes space but invisible
|
||||
|
||||
# Check opacity
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector(".target")).opacity'
|
||||
# If "0" - element is transparent
|
||||
|
||||
# Check dimensions
|
||||
./browser-eval.js 'document.querySelector(".target").getBoundingClientRect()'
|
||||
# If width/height is 0 - element has no size
|
||||
|
||||
# Check if off-screen
|
||||
./browser-eval.js '(() => {
|
||||
const r = document.querySelector(".target").getBoundingClientRect();
|
||||
return {
|
||||
inViewport: r.top >= 0 && r.left >= 0 && r.bottom <= window.innerHeight && r.right <= window.innerWidth,
|
||||
position: { top: r.top, left: r.left }
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
## Layout Issues
|
||||
|
||||
### Flexbox Debugging
|
||||
```bash
|
||||
# Check flex container
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".flex-container");
|
||||
const s = getComputedStyle(el);
|
||||
return {
|
||||
display: s.display,
|
||||
flexDirection: s.flexDirection,
|
||||
justifyContent: s.justifyContent,
|
||||
alignItems: s.alignItems,
|
||||
flexWrap: s.flexWrap,
|
||||
gap: s.gap
|
||||
};
|
||||
})()'
|
||||
|
||||
# Check flex items
|
||||
./browser-eval.js '[...document.querySelectorAll(".flex-container > *")].map(el => {
|
||||
const s = getComputedStyle(el);
|
||||
return {
|
||||
flexGrow: s.flexGrow,
|
||||
flexShrink: s.flexShrink,
|
||||
flexBasis: s.flexBasis,
|
||||
alignSelf: s.alignSelf
|
||||
};
|
||||
})'
|
||||
```
|
||||
|
||||
### Grid Debugging
|
||||
```bash
|
||||
# Check grid container
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".grid-container");
|
||||
const s = getComputedStyle(el);
|
||||
return {
|
||||
display: s.display,
|
||||
gridTemplateColumns: s.gridTemplateColumns,
|
||||
gridTemplateRows: s.gridTemplateRows,
|
||||
gap: s.gap,
|
||||
gridAutoFlow: s.gridAutoFlow
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
### Box Model
|
||||
```bash
|
||||
# Full box model breakdown
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".target");
|
||||
const s = getComputedStyle(el);
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
content: { width: r.width, height: r.height },
|
||||
padding: { top: s.paddingTop, right: s.paddingRight, bottom: s.paddingBottom, left: s.paddingLeft },
|
||||
border: { top: s.borderTopWidth, right: s.borderRightWidth, bottom: s.borderBottomWidth, left: s.borderLeftWidth },
|
||||
margin: { top: s.marginTop, right: s.marginRight, bottom: s.marginBottom, left: s.marginLeft },
|
||||
boxSizing: s.boxSizing
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
## Positioning Problems
|
||||
|
||||
### Z-Index Stacking
|
||||
```bash
|
||||
# Find all positioned elements with z-index
|
||||
./browser-eval.js '[...document.querySelectorAll("*")].filter(el => {
|
||||
const s = getComputedStyle(el);
|
||||
return s.position !== "static" && s.zIndex !== "auto";
|
||||
}).map(el => ({
|
||||
selector: el.tagName + (el.id ? "#" + el.id : "") + (el.className ? "." + el.className.split(" ")[0] : ""),
|
||||
position: getComputedStyle(el).position,
|
||||
zIndex: getComputedStyle(el).zIndex
|
||||
})).sort((a, b) => parseInt(b.zIndex) - parseInt(a.zIndex))'
|
||||
```
|
||||
|
||||
### Fixed/Sticky Elements
|
||||
```bash
|
||||
# Find fixed/sticky elements
|
||||
./browser-eval.js '[...document.querySelectorAll("*")].filter(el => {
|
||||
const pos = getComputedStyle(el).position;
|
||||
return pos === "fixed" || pos === "sticky";
|
||||
}).map(el => ({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id,
|
||||
class: el.className,
|
||||
position: getComputedStyle(el).position
|
||||
}))'
|
||||
```
|
||||
|
||||
## Responsive Issues
|
||||
|
||||
### Media Query Testing
|
||||
```bash
|
||||
# Test different breakpoints
|
||||
./browser-resize.js --mobile && ./browser-screenshot.js --output=/tmp/mobile.png
|
||||
./browser-resize.js --tablet && ./browser-screenshot.js --output=/tmp/tablet.png
|
||||
./browser-resize.js --desktop && ./browser-screenshot.js --output=/tmp/desktop.png
|
||||
```
|
||||
|
||||
### Check Current Viewport
|
||||
```bash
|
||||
./browser-eval.js '({
|
||||
viewport: { width: window.innerWidth, height: window.innerHeight },
|
||||
screen: { width: screen.width, height: screen.height },
|
||||
devicePixelRatio: window.devicePixelRatio
|
||||
})'
|
||||
```
|
||||
|
||||
## Typography Issues
|
||||
|
||||
### Font Not Loading
|
||||
```bash
|
||||
# Check computed font
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector(".text")).fontFamily'
|
||||
|
||||
# Check if webfont loaded
|
||||
./browser-eval.js 'document.fonts.check("16px YourFontName")'
|
||||
|
||||
# List all loaded fonts
|
||||
./browser-eval.js '[...document.fonts].map(f => ({ family: f.family, style: f.style, weight: f.weight, status: f.status }))'
|
||||
```
|
||||
|
||||
### Text Overflow
|
||||
```bash
|
||||
# Check text overflow settings
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".truncated-text");
|
||||
const s = getComputedStyle(el);
|
||||
return {
|
||||
overflow: s.overflow,
|
||||
textOverflow: s.textOverflow,
|
||||
whiteSpace: s.whiteSpace,
|
||||
width: s.width,
|
||||
maxWidth: s.maxWidth
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
## Color & Visual
|
||||
|
||||
### Check Colors
|
||||
```bash
|
||||
# Get all colors used on element
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".target");
|
||||
const s = getComputedStyle(el);
|
||||
return {
|
||||
color: s.color,
|
||||
backgroundColor: s.backgroundColor,
|
||||
borderColor: s.borderColor,
|
||||
outlineColor: s.outlineColor
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
### Find Elements by Color
|
||||
```bash
|
||||
# Find all elements with a specific background
|
||||
./browser-eval.js '[...document.querySelectorAll("*")].filter(el =>
|
||||
getComputedStyle(el).backgroundColor === "rgb(255, 0, 0)"
|
||||
).map(el => el.tagName + (el.id ? "#" + el.id : ""))'
|
||||
```
|
||||
|
||||
## Performance Concerns
|
||||
|
||||
### Large DOM Check
|
||||
```bash
|
||||
./browser-eval.js '({
|
||||
totalElements: document.querySelectorAll("*").length,
|
||||
deepestNesting: (() => {
|
||||
let max = 0;
|
||||
document.querySelectorAll("*").forEach(el => {
|
||||
let depth = 0, node = el;
|
||||
while (node.parentElement) { depth++; node = node.parentElement; }
|
||||
if (depth > max) max = depth;
|
||||
});
|
||||
return max;
|
||||
})()
|
||||
})'
|
||||
```
|
||||
|
||||
### Reflow Triggers
|
||||
Watch for properties that cause layout recalculation:
|
||||
- offsetTop/Left/Width/Height
|
||||
- scrollTop/Left/Width/Height
|
||||
- clientTop/Left/Width/Height
|
||||
- getComputedStyle()
|
||||
- getBoundingClientRect()
|
||||
|
||||
## Animation Debugging
|
||||
|
||||
### Check Animations
|
||||
```bash
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".animated");
|
||||
const s = getComputedStyle(el);
|
||||
return {
|
||||
animation: s.animation,
|
||||
animationName: s.animationName,
|
||||
animationDuration: s.animationDuration,
|
||||
animationTimingFunction: s.animationTimingFunction,
|
||||
animationDelay: s.animationDelay,
|
||||
transition: s.transition
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
### Force Animation State
|
||||
```bash
|
||||
# Pause all animations
|
||||
./browser-eval.js 'document.querySelectorAll("*").forEach(el => el.style.animationPlayState = "paused")'
|
||||
|
||||
# Resume animations
|
||||
./browser-eval.js 'document.querySelectorAll("*").forEach(el => el.style.animationPlayState = "")'
|
||||
```
|
||||
|
||||
## Common CSS Fixes
|
||||
|
||||
### Overflow Scroll Not Working
|
||||
Check these in order:
|
||||
1. Parent has defined height
|
||||
2. `overflow` is set to `scroll` or `auto`
|
||||
3. Content is actually taller than container
|
||||
|
||||
### Element Behind Another
|
||||
1. Check z-index values
|
||||
2. Ensure positioned (relative/absolute/fixed)
|
||||
3. Check stacking context (transform, opacity < 1, etc. create new contexts)
|
||||
|
||||
### Flexbox Not Centering
|
||||
1. Container has height
|
||||
2. `align-items` for vertical, `justify-content` for horizontal
|
||||
3. Check `flex-direction` - swaps main/cross axis
|
||||
259
skills/website-debug/references/js-debug.md
Normal file
259
skills/website-debug/references/js-debug.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# JavaScript Debugging Reference
|
||||
|
||||
Workflows for diagnosing and fixing JavaScript issues in web applications.
|
||||
|
||||
## Error Detection
|
||||
|
||||
### Capture Runtime Errors
|
||||
```bash
|
||||
# Watch for errors in real-time
|
||||
./browser-console.js --errors --watch
|
||||
|
||||
# Inject error capture (run once per page load)
|
||||
./browser-eval.js 'window.onerror = (msg, src, line, col, err) => {
|
||||
window.__jsErrors = window.__jsErrors || [];
|
||||
window.__jsErrors.push({ message: msg, source: src, line, col, stack: err?.stack });
|
||||
return false;
|
||||
}'
|
||||
|
||||
# Check captured errors
|
||||
./browser-eval.js 'window.__jsErrors || []'
|
||||
```
|
||||
|
||||
### Unhandled Promise Rejections
|
||||
```bash
|
||||
# Capture unhandled rejections
|
||||
./browser-eval.js 'window.addEventListener("unhandledrejection", e => {
|
||||
window.__promiseErrors = window.__promiseErrors || [];
|
||||
window.__promiseErrors.push({ reason: e.reason?.message || String(e.reason) });
|
||||
})'
|
||||
|
||||
# Check promise errors
|
||||
./browser-eval.js 'window.__promiseErrors || []'
|
||||
```
|
||||
|
||||
## Common Error Types
|
||||
|
||||
### TypeError: Cannot read property 'x' of undefined
|
||||
**Cause**: Accessing property on null/undefined
|
||||
**Debug**:
|
||||
```bash
|
||||
# Check if element exists
|
||||
./browser-eval.js 'document.querySelector(".my-element")'
|
||||
|
||||
# Check if variable/property exists
|
||||
./browser-eval.js 'typeof window.myApp?.myProperty'
|
||||
```
|
||||
|
||||
### ReferenceError: x is not defined
|
||||
**Cause**: Variable not in scope or not declared
|
||||
**Debug**:
|
||||
```bash
|
||||
# Check if global exists
|
||||
./browser-eval.js '"myVariable" in window'
|
||||
|
||||
# List all globals matching pattern
|
||||
./browser-eval.js 'Object.keys(window).filter(k => k.includes("my"))'
|
||||
```
|
||||
|
||||
### SyntaxError
|
||||
**Cause**: Invalid JavaScript syntax
|
||||
**Debug**: Check browser console for file and line number
|
||||
```bash
|
||||
./browser-console.js --errors
|
||||
```
|
||||
|
||||
## State Inspection
|
||||
|
||||
### Check Global Variables
|
||||
```bash
|
||||
# List all custom globals (excluding built-ins)
|
||||
./browser-eval.js '(() => {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.appendChild(iframe);
|
||||
const builtins = new Set(Object.keys(iframe.contentWindow));
|
||||
iframe.remove();
|
||||
return Object.keys(window).filter(k => !builtins.has(k) && !k.startsWith("__"));
|
||||
})()'
|
||||
```
|
||||
|
||||
### Inspect Application State
|
||||
```bash
|
||||
# React state (if using React DevTools globals)
|
||||
./browser-eval.js 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers'
|
||||
|
||||
# Redux store (if exposed)
|
||||
./browser-eval.js 'window.__store?.getState()'
|
||||
|
||||
# Vue instance (if exposed)
|
||||
./browser-eval.js 'document.querySelector("#app")?.__vue__?.$data'
|
||||
```
|
||||
|
||||
### Local Storage / Session Storage
|
||||
```bash
|
||||
# View localStorage
|
||||
./browser-eval.js 'Object.fromEntries(Object.entries(localStorage))'
|
||||
|
||||
# View sessionStorage
|
||||
./browser-eval.js 'Object.fromEntries(Object.entries(sessionStorage))'
|
||||
```
|
||||
|
||||
## Event Debugging
|
||||
|
||||
### Check Event Listeners
|
||||
```bash
|
||||
# Get listeners on element (Chrome only)
|
||||
./browser-eval.js 'getEventListeners(document.querySelector(".button"))'
|
||||
|
||||
# Alternative: Check if onclick exists
|
||||
./browser-eval.js 'typeof document.querySelector(".button").onclick'
|
||||
```
|
||||
|
||||
### Debug Event Firing
|
||||
```bash
|
||||
# Log all click events
|
||||
./browser-eval.js 'document.addEventListener("click", e => console.log("Click:", e.target.tagName, e.target.className), true)'
|
||||
|
||||
# Log all form submissions
|
||||
./browser-eval.js 'document.addEventListener("submit", e => console.log("Form submit:", e.target.action), true)'
|
||||
```
|
||||
|
||||
### Simulate Events
|
||||
```bash
|
||||
# Click element
|
||||
./browser-eval.js 'document.querySelector(".button").click()'
|
||||
|
||||
# Dispatch custom event
|
||||
./browser-eval.js 'document.querySelector(".target").dispatchEvent(new Event("input", { bubbles: true }))'
|
||||
|
||||
# Submit form
|
||||
./browser-eval.js 'document.querySelector("form").submit()'
|
||||
```
|
||||
|
||||
## Async Debugging
|
||||
|
||||
### Track Pending Promises
|
||||
```bash
|
||||
# Wrap fetch to log all requests
|
||||
./browser-eval.js '(() => {
|
||||
const orig = window.fetch;
|
||||
window.fetch = (...args) => {
|
||||
console.log("Fetch:", args[0]);
|
||||
return orig.apply(window, args);
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
### Debug setTimeout/setInterval
|
||||
```bash
|
||||
# List all active timers (approximate)
|
||||
./browser-eval.js '(() => {
|
||||
const ids = [];
|
||||
const id = setTimeout(() => {}, 0);
|
||||
clearTimeout(id);
|
||||
for (let i = 1; i < id; i++) {
|
||||
clearTimeout(i);
|
||||
clearInterval(i);
|
||||
}
|
||||
return `Cleared timers up to ID ${id}`;
|
||||
})()'
|
||||
```
|
||||
|
||||
## Network-Related JS Issues
|
||||
|
||||
### Check if Fetch Failed
|
||||
```bash
|
||||
./browser-eval.js 'fetch("/api/data").then(r => ({ status: r.status, ok: r.ok })).catch(e => ({ error: e.message }))'
|
||||
```
|
||||
|
||||
### CORS Errors
|
||||
Check console for CORS errors:
|
||||
```bash
|
||||
./browser-console.js --errors | grep -i cors
|
||||
```
|
||||
|
||||
### API Response Issues
|
||||
```bash
|
||||
# Test API and inspect response
|
||||
./browser-eval.js 'fetch("/api/endpoint").then(r => r.json()).then(d => JSON.stringify(d, null, 2)).catch(e => e.message)'
|
||||
```
|
||||
|
||||
## Module/Import Issues
|
||||
|
||||
### Check if Module Loaded
|
||||
```bash
|
||||
# Check for module in global scope
|
||||
./browser-eval.js '"ModuleName" in window'
|
||||
|
||||
# Check ES module
|
||||
./browser-eval.js 'import("./module.js").then(m => Object.keys(m)).catch(e => e.message)'
|
||||
```
|
||||
|
||||
### Script Load Order
|
||||
```bash
|
||||
# List all scripts in order
|
||||
./browser-eval.js '[...document.querySelectorAll("script")].map(s => ({ src: s.src || "(inline)", async: s.async, defer: s.defer }))'
|
||||
```
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Identify Long Tasks
|
||||
```bash
|
||||
# Monitor long tasks
|
||||
./browser-eval.js '(() => {
|
||||
const observer = new PerformanceObserver(list => {
|
||||
list.getEntries().forEach(entry => console.log("Long task:", entry.duration, "ms"));
|
||||
});
|
||||
observer.observe({ entryTypes: ["longtask"] });
|
||||
return "Long task observer started";
|
||||
})()'
|
||||
```
|
||||
|
||||
### Memory Leaks
|
||||
```bash
|
||||
# Get heap size (Chrome)
|
||||
./browser-eval.js 'performance.memory ? { usedHeap: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + "MB", totalHeap: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + "MB" } : "Not available"'
|
||||
```
|
||||
|
||||
## Framework-Specific
|
||||
|
||||
### React Debugging
|
||||
```bash
|
||||
# Find React components in DOM
|
||||
./browser-eval.js '(() => {
|
||||
const reactRoot = document.querySelector("[data-reactroot], #root, #app");
|
||||
return reactRoot ? "React app found" : "No React root detected";
|
||||
})()'
|
||||
|
||||
# Check for React errors
|
||||
./browser-console.js --errors | grep -i react
|
||||
```
|
||||
|
||||
### Vue Debugging
|
||||
```bash
|
||||
# Check Vue version
|
||||
./browser-eval.js 'window.Vue?.version || document.querySelector("[data-v-]") ? "Vue detected" : "No Vue"'
|
||||
```
|
||||
|
||||
### Next.js/Nuxt Hydration
|
||||
```bash
|
||||
# Check for hydration errors
|
||||
./browser-console.js --errors | grep -i hydrat
|
||||
```
|
||||
|
||||
## Quick Fixes
|
||||
|
||||
### Script Not Running
|
||||
1. Check if script loaded: `./browser-dom.js "script[src*='filename']"`
|
||||
2. Check for syntax errors: `./browser-console.js --errors`
|
||||
3. Check load order: Ensure dependencies load first
|
||||
|
||||
### Event Handler Not Working
|
||||
1. Check element exists when handler attached
|
||||
2. Verify selector is correct
|
||||
3. Check for event.preventDefault() or stopPropagation()
|
||||
|
||||
### Async Code Not Executing
|
||||
1. Check if Promise rejects: Add .catch() logging
|
||||
2. Verify await is in async function
|
||||
3. Check network requests complete successfully
|
||||
288
skills/website-debug/references/self-debug.md
Normal file
288
skills/website-debug/references/self-debug.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# Self-Debugging Workflow
|
||||
|
||||
Patterns for Claude and AI agents to verify and fix their own frontend work.
|
||||
|
||||
## Core Principle
|
||||
|
||||
After making any frontend change, verify it worked:
|
||||
1. **Screenshot** - Visual verification
|
||||
2. **Console** - Check for errors
|
||||
3. **DOM** - Confirm structure
|
||||
4. **Iterate** - Fix issues and repeat
|
||||
|
||||
## Basic Verification Loop
|
||||
|
||||
```bash
|
||||
# After modifying HTML/CSS/JS files:
|
||||
|
||||
# 1. Reload the page (if not using hot reload)
|
||||
./browser-nav.js http://localhost:3000
|
||||
|
||||
# 2. Take screenshot to verify visual result
|
||||
./browser-screenshot.js
|
||||
|
||||
# 3. Check for any JavaScript errors
|
||||
./browser-console.js --errors
|
||||
|
||||
# 4. If issues found, fix and repeat
|
||||
```
|
||||
|
||||
## Structured Debugging Protocol
|
||||
|
||||
### Phase 1: Initial Assessment
|
||||
```bash
|
||||
# Get page summary
|
||||
./browser-dom.js
|
||||
|
||||
# Take baseline screenshot
|
||||
./browser-screenshot.js --output=/tmp/current-state.png
|
||||
|
||||
# Capture any errors
|
||||
./browser-console.js --errors > /tmp/errors.txt
|
||||
```
|
||||
|
||||
### Phase 2: Make Changes
|
||||
After editing source files:
|
||||
```bash
|
||||
# Force reload (bypass cache)
|
||||
./browser-eval.js 'location.reload(true)'
|
||||
|
||||
# Wait for page load
|
||||
sleep 2
|
||||
|
||||
# Take new screenshot
|
||||
./browser-screenshot.js --output=/tmp/after-change.png
|
||||
```
|
||||
|
||||
### Phase 3: Verify
|
||||
```bash
|
||||
# Check for new errors
|
||||
./browser-console.js --errors
|
||||
|
||||
# Verify element exists (example: new button)
|
||||
./browser-eval.js 'document.querySelector(".new-button") ? "Found" : "Missing"'
|
||||
|
||||
# Verify styling applied
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector(".new-button")).backgroundColor'
|
||||
```
|
||||
|
||||
### Phase 4: Iterate
|
||||
If issues found:
|
||||
1. Identify the problem from screenshot/console
|
||||
2. Fix the source code
|
||||
3. Return to Phase 2
|
||||
|
||||
## Common Self-Fix Patterns
|
||||
|
||||
### CSS Not Applying
|
||||
|
||||
**Symptom**: Element exists but looks wrong
|
||||
**Diagnosis**:
|
||||
```bash
|
||||
# Check if styles computed correctly
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector(".my-element")).cssText.slice(0, 500)'
|
||||
|
||||
# Check for conflicting selectors
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".my-element");
|
||||
const sheets = [...document.styleSheets];
|
||||
const matches = [];
|
||||
sheets.forEach(sheet => {
|
||||
try {
|
||||
[...sheet.cssRules].forEach(rule => {
|
||||
if (el.matches(rule.selectorText)) matches.push(rule.selectorText);
|
||||
});
|
||||
} catch {}
|
||||
});
|
||||
return matches;
|
||||
})()'
|
||||
```
|
||||
|
||||
**Fixes to try**:
|
||||
1. Increase specificity
|
||||
2. Check for `!important` overrides
|
||||
3. Verify selector matches element
|
||||
|
||||
### Element Not Visible
|
||||
|
||||
**Symptom**: DOM shows element but not visible
|
||||
**Diagnosis**:
|
||||
```bash
|
||||
./browser-eval.js '(() => {
|
||||
const el = document.querySelector(".my-element");
|
||||
const s = getComputedStyle(el);
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
exists: !!el,
|
||||
display: s.display,
|
||||
visibility: s.visibility,
|
||||
opacity: s.opacity,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
inViewport: r.top < window.innerHeight && r.bottom > 0
|
||||
};
|
||||
})()'
|
||||
```
|
||||
|
||||
**Fixes based on result**:
|
||||
- `display: none` → Check CSS rules hiding it
|
||||
- `width/height: 0` → Add dimensions or content
|
||||
- `opacity: 0` → Check for fade animations
|
||||
- Not in viewport → Check positioning/scroll
|
||||
|
||||
### JavaScript Not Running
|
||||
|
||||
**Symptom**: Interactive features don't work
|
||||
**Diagnosis**:
|
||||
```bash
|
||||
# Check for errors
|
||||
./browser-console.js --errors
|
||||
|
||||
# Check if script loaded
|
||||
./browser-eval.js 'document.querySelectorAll("script").length'
|
||||
|
||||
# Check if function exists
|
||||
./browser-eval.js 'typeof myFunction'
|
||||
```
|
||||
|
||||
**Fixes based on result**:
|
||||
- Syntax error → Fix the JavaScript
|
||||
- Script not loaded → Check path/build process
|
||||
- Function undefined → Check load order, exports
|
||||
|
||||
### Layout Broken
|
||||
|
||||
**Symptom**: Elements positioned incorrectly
|
||||
**Diagnosis**:
|
||||
```bash
|
||||
# Take screenshot
|
||||
./browser-screenshot.js
|
||||
|
||||
# Check flex/grid container
|
||||
./browser-eval.js '(() => {
|
||||
const container = document.querySelector(".container");
|
||||
const s = getComputedStyle(container);
|
||||
return { display: s.display, flexDirection: s.flexDirection, gridTemplateColumns: s.gridTemplateColumns };
|
||||
})()'
|
||||
```
|
||||
|
||||
**Common fixes**:
|
||||
- Missing `display: flex/grid` on container
|
||||
- Wrong `flex-direction`
|
||||
- Missing `width` on flex children
|
||||
- Grid columns not matching children
|
||||
|
||||
## Responsive Verification
|
||||
|
||||
### Test All Breakpoints
|
||||
```bash
|
||||
# Mobile
|
||||
./browser-resize.js --mobile
|
||||
./browser-screenshot.js --output=/tmp/mobile.png
|
||||
./browser-console.js --errors
|
||||
|
||||
# Tablet
|
||||
./browser-resize.js --tablet
|
||||
./browser-screenshot.js --output=/tmp/tablet.png
|
||||
|
||||
# Desktop
|
||||
./browser-resize.js --desktop
|
||||
./browser-screenshot.js --output=/tmp/desktop.png
|
||||
```
|
||||
|
||||
### Compare Screenshots
|
||||
After each breakpoint, Claude should analyze the screenshot for:
|
||||
- Layout shifts
|
||||
- Overlapping elements
|
||||
- Text overflow
|
||||
- Hidden/missing elements
|
||||
- Incorrect spacing
|
||||
|
||||
## Interactive Feature Testing
|
||||
|
||||
### Form Validation
|
||||
```bash
|
||||
# Fill form with test data
|
||||
./browser-eval.js 'document.querySelector("#email").value = "test@example.com"'
|
||||
./browser-eval.js 'document.querySelector("#password").value = "test123"'
|
||||
|
||||
# Submit and check result
|
||||
./browser-eval.js 'document.querySelector("form").submit()'
|
||||
./browser-screenshot.js
|
||||
./browser-console.js --errors
|
||||
```
|
||||
|
||||
### Click/Hover States
|
||||
```bash
|
||||
# Test button click
|
||||
./browser-eval.js 'document.querySelector(".button").click()'
|
||||
./browser-screenshot.js
|
||||
|
||||
# Check state changed
|
||||
./browser-eval.js 'document.querySelector(".modal")?.style.display'
|
||||
```
|
||||
|
||||
## Automated Verification Script
|
||||
|
||||
For repeated checks, use this pattern:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# verify-page.sh - Run after changes
|
||||
|
||||
URL="${1:-http://localhost:3000}"
|
||||
OUT_DIR="/tmp/debug-$(date +%s)"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
echo "Verifying $URL..."
|
||||
|
||||
# Navigate
|
||||
./browser-nav.js "$URL"
|
||||
sleep 2
|
||||
|
||||
# Screenshot
|
||||
./browser-screenshot.js --output="$OUT_DIR/screenshot.png"
|
||||
echo "Screenshot: $OUT_DIR/screenshot.png"
|
||||
|
||||
# Errors
|
||||
./browser-console.js --errors > "$OUT_DIR/errors.txt"
|
||||
if [ -s "$OUT_DIR/errors.txt" ]; then
|
||||
echo "⚠️ Errors found:"
|
||||
cat "$OUT_DIR/errors.txt"
|
||||
else
|
||||
echo "✓ No JavaScript errors"
|
||||
fi
|
||||
|
||||
# DOM summary
|
||||
./browser-dom.js > "$OUT_DIR/dom.txt"
|
||||
echo "DOM summary saved"
|
||||
|
||||
echo "Done. Files in $OUT_DIR"
|
||||
```
|
||||
|
||||
## Error Recovery Strategies
|
||||
|
||||
### When Screenshot Shows Blank Page
|
||||
1. Check console for critical errors
|
||||
2. Verify server is running
|
||||
3. Check network requests (404, 500)
|
||||
4. Verify correct URL
|
||||
|
||||
### When Console Shows Many Errors
|
||||
1. Fix first error (often causes cascade)
|
||||
2. Reload and recheck
|
||||
3. Address errors in order
|
||||
|
||||
### When Element Is "Almost Right"
|
||||
1. Use picker to select element: `./browser-pick.js "Select the problematic element"`
|
||||
2. Get full computed styles
|
||||
3. Compare to expected values
|
||||
4. Adjust CSS incrementally
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always screenshot after changes** - Visual verification catches most issues
|
||||
2. **Check console immediately** - Catch JS errors early
|
||||
3. **Test responsive early** - Layout issues compound
|
||||
4. **Verify one change at a time** - Easier to identify what broke
|
||||
5. **Save debug output** - Reference for comparison
|
||||
6. **Use the picker for precision** - Get exact selectors from user
|
||||
72
skills/website-debug/scripts/browser-close.js
Executable file
72
skills/website-debug/scripts/browser-close.js
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-close.js - Close browser session
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-close.js # Close gracefully
|
||||
* ./browser-close.js --force # Kill all Chrome instances
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { execSync } from "node:child_process";
|
||||
import { platform } from "node:os";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const force = args.includes("--force");
|
||||
const port = args.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`
|
||||
browser-close.js - Close browser session
|
||||
|
||||
Usage:
|
||||
./browser-close.js [options]
|
||||
|
||||
Options:
|
||||
--force Kill all Chrome debug instances
|
||||
--port=PORT Connect to custom debug port (default: 9222)
|
||||
|
||||
Examples:
|
||||
./browser-close.js
|
||||
./browser-close.js --force
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (force) {
|
||||
try {
|
||||
if (platform() === "darwin") {
|
||||
execSync("killall 'Google Chrome' 2>/dev/null", { stdio: "ignore" });
|
||||
} else if (platform() === "linux") {
|
||||
execSync("pkill -f 'chrome.*remote-debugging' 2>/dev/null", { stdio: "ignore" });
|
||||
}
|
||||
console.log("✓ Force killed Chrome instances");
|
||||
} catch {
|
||||
console.log("No Chrome instances to kill");
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
console.log("✓ Browser closed");
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.log("No browser session to close");
|
||||
} else {
|
||||
// Try force close as fallback
|
||||
try {
|
||||
if (platform() === "darwin") {
|
||||
execSync("killall 'Google Chrome' 2>/dev/null", { stdio: "ignore" });
|
||||
}
|
||||
console.log("✓ Browser closed (via kill)");
|
||||
} catch {
|
||||
console.error(`Could not close browser: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
175
skills/website-debug/scripts/browser-console.js
Executable file
175
skills/website-debug/scripts/browser-console.js
Executable file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-console.js - Get console messages from page
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-console.js # Get all console messages
|
||||
* ./browser-console.js --errors # Only errors
|
||||
* ./browser-console.js --warnings # Errors and warnings
|
||||
* ./browser-console.js --watch # Watch for new messages
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const errorsOnly = args.includes("--errors");
|
||||
const warningsPlus = args.includes("--warnings");
|
||||
const watch = args.includes("--watch");
|
||||
const port = args.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`
|
||||
browser-console.js - Capture console messages
|
||||
|
||||
Usage:
|
||||
./browser-console.js [options]
|
||||
|
||||
Options:
|
||||
--errors Show only errors
|
||||
--warnings Show errors and warnings
|
||||
--watch Watch for new messages in real-time
|
||||
--port=PORT Connect to custom debug port (default: 9222)
|
||||
|
||||
Message Types:
|
||||
[ERR] - console.error, exceptions
|
||||
[WARN] - console.warn
|
||||
[LOG] - console.log
|
||||
[INFO] - console.info
|
||||
[DBG] - console.debug
|
||||
|
||||
Examples:
|
||||
./browser-console.js
|
||||
./browser-console.js --errors
|
||||
./browser-console.js --watch
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
const page = pages[pages.length - 1];
|
||||
|
||||
if (!page) {
|
||||
console.error("✗ No active tab found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (watch) {
|
||||
console.log("Watching console... (Ctrl+C to stop)\n");
|
||||
|
||||
const shouldShow = (type) => {
|
||||
if (errorsOnly) return type === "error";
|
||||
if (warningsPlus) return type === "error" || type === "warning";
|
||||
return true;
|
||||
};
|
||||
|
||||
const typeLabels = {
|
||||
error: "[ERR] ",
|
||||
warning: "[WARN]",
|
||||
log: "[LOG] ",
|
||||
info: "[INFO]",
|
||||
debug: "[DBG] "
|
||||
};
|
||||
|
||||
page.on("console", msg => {
|
||||
const type = msg.type();
|
||||
if (shouldShow(type)) {
|
||||
const label = typeLabels[type] || `[${type.toUpperCase()}]`;
|
||||
const text = msg.text();
|
||||
console.log(`${label} ${text}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on("pageerror", err => {
|
||||
console.log(`[ERR] Uncaught: ${err.message}`);
|
||||
});
|
||||
|
||||
// Keep alive
|
||||
await new Promise(() => {});
|
||||
} else {
|
||||
// Get existing console messages by injecting capture
|
||||
const messages = await page.evaluate(() => {
|
||||
// Return any cached messages if we have them
|
||||
return window.__consoleMessages || [];
|
||||
});
|
||||
|
||||
// Also get any runtime exceptions
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send("Runtime.enable");
|
||||
|
||||
// Collect current console messages by re-evaluating with capture
|
||||
const capturedMessages = await page.evaluate(() => {
|
||||
const msgs = [];
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
warn: console.warn,
|
||||
info: console.info,
|
||||
debug: console.debug
|
||||
};
|
||||
|
||||
// This is for future messages - we can't capture past ones without this being set up earlier
|
||||
// So we'll note that limitation
|
||||
return msgs;
|
||||
});
|
||||
|
||||
// Get recent exceptions from CDP
|
||||
const { result } = await client.send("Runtime.evaluate", {
|
||||
expression: `
|
||||
(function() {
|
||||
// Check for any unhandled errors stored in window
|
||||
const errors = window.__webdebugErrors || [];
|
||||
return errors;
|
||||
})()
|
||||
`,
|
||||
returnByValue: true
|
||||
});
|
||||
|
||||
if (messages.length === 0 && (!result.value || result.value.length === 0)) {
|
||||
console.log("No console messages captured.");
|
||||
console.log("\nTip: Use --watch to capture messages in real-time, or inject capture script:");
|
||||
console.log(' ./browser-eval.js "window.__consoleMessages=[];[\'log\',\'error\',\'warn\',\'info\'].forEach(t=>{const o=console[t];console[t]=(...a)=>{window.__consoleMessages.push({type:t,text:a.join(\' \'),time:Date.now()});o.apply(console,a)}});"');
|
||||
} else {
|
||||
const allMessages = [...messages, ...(result.value || [])];
|
||||
|
||||
const typeLabels = {
|
||||
error: "[ERR] ",
|
||||
warning: "[WARN]",
|
||||
warn: "[WARN]",
|
||||
log: "[LOG] ",
|
||||
info: "[INFO]",
|
||||
debug: "[DBG] "
|
||||
};
|
||||
|
||||
allMessages.forEach(msg => {
|
||||
const type = msg.type || "log";
|
||||
const shouldShow = () => {
|
||||
if (errorsOnly) return type === "error";
|
||||
if (warningsPlus) return type === "error" || type === "warning" || type === "warn";
|
||||
return true;
|
||||
};
|
||||
|
||||
if (shouldShow()) {
|
||||
const label = typeLabels[type] || `[${type.toUpperCase()}]`;
|
||||
console.log(`${label} ${msg.text || msg.message || JSON.stringify(msg)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!watch) {
|
||||
await browser.disconnect();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
|
||||
} else {
|
||||
console.error(`✗ Console capture failed: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
186
skills/website-debug/scripts/browser-dom.js
Executable file
186
skills/website-debug/scripts/browser-dom.js
Executable file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-dom.js - Get DOM snapshot or element HTML
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-dom.js # Full page structure summary
|
||||
* ./browser-dom.js "body" # Element's outer HTML
|
||||
* ./browser-dom.js ".header" --inner # Element's inner HTML
|
||||
* ./browser-dom.js --tree # DOM tree visualization
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const selector = args.find(a => !a.startsWith("--"));
|
||||
const inner = args.includes("--inner");
|
||||
const tree = args.includes("--tree");
|
||||
const depth = parseInt(args.find(a => a.startsWith("--depth="))?.split("=")[1]) || 3;
|
||||
const port = args.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`
|
||||
browser-dom.js - DOM inspection and snapshots
|
||||
|
||||
Usage:
|
||||
./browser-dom.js [selector] [options]
|
||||
|
||||
Options:
|
||||
--inner Get inner HTML instead of outer
|
||||
--tree Show DOM tree visualization
|
||||
--depth=N Tree depth (default: 3)
|
||||
--port=PORT Connect to custom debug port (default: 9222)
|
||||
|
||||
Examples:
|
||||
./browser-dom.js # Page summary
|
||||
./browser-dom.js "body" # Full body HTML
|
||||
./browser-dom.js ".nav" --inner # Nav inner HTML
|
||||
./browser-dom.js --tree # DOM tree
|
||||
./browser-dom.js --tree --depth=5 # Deeper tree
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
const page = pages[pages.length - 1];
|
||||
|
||||
if (!page) {
|
||||
console.error("✗ No active tab found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (tree) {
|
||||
// DOM tree visualization
|
||||
const treeData = await page.evaluate((maxDepth) => {
|
||||
const buildTree = (el, currentDepth, maxDepth) => {
|
||||
if (currentDepth > maxDepth) return null;
|
||||
|
||||
const tag = el.tagName?.toLowerCase() || "#text";
|
||||
if (tag === "script" || tag === "style" || tag === "#text") return null;
|
||||
|
||||
let label = tag;
|
||||
if (el.id) label += `#${el.id}`;
|
||||
if (el.className && typeof el.className === "string") {
|
||||
const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
|
||||
if (classes) label += `.${classes}`;
|
||||
}
|
||||
|
||||
const children = [];
|
||||
for (const child of el.children || []) {
|
||||
const childTree = buildTree(child, currentDepth + 1, maxDepth);
|
||||
if (childTree) children.push(childTree);
|
||||
}
|
||||
|
||||
return { label, children };
|
||||
};
|
||||
|
||||
return buildTree(document.body, 0, maxDepth);
|
||||
}, depth);
|
||||
|
||||
const printTree = (node, prefix = "", isLast = true) => {
|
||||
if (!node) return;
|
||||
const marker = isLast ? "└── " : "├── ";
|
||||
console.log(prefix + marker + node.label);
|
||||
|
||||
const childPrefix = prefix + (isLast ? " " : "│ ");
|
||||
node.children.forEach((child, i) => {
|
||||
printTree(child, childPrefix, i === node.children.length - 1);
|
||||
});
|
||||
};
|
||||
|
||||
console.log("DOM Tree:\n");
|
||||
printTree(treeData);
|
||||
|
||||
} else if (selector) {
|
||||
// Get specific element
|
||||
const html = await page.evaluate((sel, getInner) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return null;
|
||||
return getInner ? el.innerHTML : el.outerHTML;
|
||||
}, selector, inner);
|
||||
|
||||
if (html === null) {
|
||||
console.error(`✗ Element not found: ${selector}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(html);
|
||||
|
||||
} else {
|
||||
// Page summary
|
||||
const summary = await page.evaluate(() => {
|
||||
const countElements = (sel) => document.querySelectorAll(sel).length;
|
||||
|
||||
return {
|
||||
title: document.title,
|
||||
url: location.href,
|
||||
doctype: document.doctype?.name || "none",
|
||||
charset: document.characterSet,
|
||||
viewport: document.querySelector('meta[name="viewport"]')?.content || "not set",
|
||||
elements: {
|
||||
total: document.querySelectorAll("*").length,
|
||||
divs: countElements("div"),
|
||||
spans: countElements("span"),
|
||||
links: countElements("a"),
|
||||
images: countElements("img"),
|
||||
buttons: countElements("button"),
|
||||
inputs: countElements("input"),
|
||||
forms: countElements("form"),
|
||||
scripts: countElements("script"),
|
||||
styles: countElements("style, link[rel='stylesheet']")
|
||||
},
|
||||
headings: {
|
||||
h1: countElements("h1"),
|
||||
h2: countElements("h2"),
|
||||
h3: countElements("h3"),
|
||||
h4: countElements("h4")
|
||||
},
|
||||
semantics: {
|
||||
header: countElements("header"),
|
||||
nav: countElements("nav"),
|
||||
main: countElements("main"),
|
||||
article: countElements("article"),
|
||||
section: countElements("section"),
|
||||
aside: countElements("aside"),
|
||||
footer: countElements("footer")
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
console.log("Page Summary");
|
||||
console.log("============");
|
||||
console.log(`Title: ${summary.title}`);
|
||||
console.log(`URL: ${summary.url}`);
|
||||
console.log(`Charset: ${summary.charset}`);
|
||||
console.log(`Viewport: ${summary.viewport}`);
|
||||
console.log("");
|
||||
console.log("Element Counts:");
|
||||
console.log(` Total: ${summary.elements.total}`);
|
||||
console.log(` Divs: ${summary.elements.divs}, Spans: ${summary.elements.spans}`);
|
||||
console.log(` Links: ${summary.elements.links}, Images: ${summary.elements.images}`);
|
||||
console.log(` Buttons: ${summary.elements.buttons}, Inputs: ${summary.elements.inputs}, Forms: ${summary.elements.forms}`);
|
||||
console.log(` Scripts: ${summary.elements.scripts}, Stylesheets: ${summary.elements.styles}`);
|
||||
console.log("");
|
||||
console.log("Headings:", `H1:${summary.headings.h1} H2:${summary.headings.h2} H3:${summary.headings.h3} H4:${summary.headings.h4}`);
|
||||
console.log("");
|
||||
console.log("Semantic Elements:");
|
||||
Object.entries(summary.semantics).forEach(([tag, count]) => {
|
||||
if (count > 0) console.log(` <${tag}>: ${count}`);
|
||||
});
|
||||
}
|
||||
|
||||
await browser.disconnect();
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
|
||||
} else {
|
||||
console.error(`✗ DOM inspection failed: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
121
skills/website-debug/scripts/browser-eval.js
Executable file
121
skills/website-debug/scripts/browser-eval.js
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-eval.js - Execute JavaScript in page context
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-eval.js 'document.title'
|
||||
* ./browser-eval.js 'document.querySelectorAll("a").length'
|
||||
* ./browser-eval.js 'getComputedStyle(document.body).backgroundColor'
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const code = process.argv.slice(2).filter(a => !a.startsWith("--")).join(" ");
|
||||
const port = process.argv.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
const json = process.argv.includes("--json");
|
||||
|
||||
if (!code || process.argv.includes("--help")) {
|
||||
console.log(`
|
||||
browser-eval.js - Execute JavaScript in page context
|
||||
|
||||
Usage:
|
||||
./browser-eval.js '<javascript>'
|
||||
|
||||
Options:
|
||||
--json Output result as JSON
|
||||
--port=PORT Connect to custom debug port (default: 9222)
|
||||
|
||||
Examples:
|
||||
./browser-eval.js 'document.title'
|
||||
./browser-eval.js 'document.querySelectorAll("a").length'
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector(".header")).display'
|
||||
./browser-eval.js '[...document.querySelectorAll("h1")].map(e => e.textContent)'
|
||||
|
||||
CSS Debugging Examples:
|
||||
# Get computed style of an element
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector(".btn")).padding'
|
||||
|
||||
# Check if element is visible
|
||||
./browser-eval.js 'getComputedStyle(document.querySelector("#modal")).display'
|
||||
|
||||
# Get bounding rect
|
||||
./browser-eval.js 'document.querySelector(".hero").getBoundingClientRect()'
|
||||
|
||||
# Find elements with specific style
|
||||
./browser-eval.js '[...document.querySelectorAll("*")].filter(e => getComputedStyle(e).position === "fixed").length'
|
||||
|
||||
DOM Inspection Examples:
|
||||
# Get outer HTML
|
||||
./browser-eval.js 'document.querySelector("nav").outerHTML'
|
||||
|
||||
# Count elements
|
||||
./browser-eval.js 'document.querySelectorAll(".error").length'
|
||||
|
||||
# Get all class names on body
|
||||
./browser-eval.js '[...document.body.classList]'
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
const page = pages[pages.length - 1];
|
||||
|
||||
if (!page) {
|
||||
console.error("✗ No active tab found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Execute in async context to support await
|
||||
const result = await page.evaluate((c) => {
|
||||
const AsyncFunction = (async () => {}).constructor;
|
||||
return new AsyncFunction(`return (${c})`)();
|
||||
}, code);
|
||||
|
||||
// Format output
|
||||
if (json) {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else if (Array.isArray(result)) {
|
||||
if (result.length === 0) {
|
||||
console.log("(empty array)");
|
||||
} else if (typeof result[0] === "object") {
|
||||
// Array of objects - format nicely
|
||||
result.forEach((item, i) => {
|
||||
if (i > 0) console.log("");
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
console.log(`${key}: ${value}`);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Simple array
|
||||
result.forEach(item => console.log(item));
|
||||
}
|
||||
} else if (typeof result === "object" && result !== null) {
|
||||
// Single object
|
||||
Object.entries(result).forEach(([key, value]) => {
|
||||
console.log(`${key}: ${value}`);
|
||||
});
|
||||
} else if (result === undefined) {
|
||||
console.log("(undefined)");
|
||||
} else if (result === null) {
|
||||
console.log("(null)");
|
||||
} else {
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
await browser.disconnect();
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
|
||||
} else if (e.message?.includes("Evaluation failed")) {
|
||||
console.error(`✗ JavaScript error: ${e.message.replace("Evaluation failed: ", "")}`);
|
||||
} else {
|
||||
console.error(`✗ Eval failed: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
66
skills/website-debug/scripts/browser-nav.js
Executable file
66
skills/website-debug/scripts/browser-nav.js
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-nav.js - Navigate to URL in Chrome/WebKit
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-nav.js https://example.com # Navigate current tab
|
||||
* ./browser-nav.js https://example.com --new # Open in new tab
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const url = process.argv[2];
|
||||
const newTab = process.argv.includes("--new");
|
||||
const port = process.argv.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
if (!url || url.startsWith("--")) {
|
||||
console.log(`
|
||||
browser-nav.js - Navigate to URL
|
||||
|
||||
Usage:
|
||||
./browser-nav.js <url> [--new] [--port=PORT]
|
||||
|
||||
Options:
|
||||
--new Open in new tab instead of current
|
||||
--port=PORT Connect to custom debug port (default: 9222)
|
||||
|
||||
Examples:
|
||||
./browser-nav.js https://example.com
|
||||
./browser-nav.js https://localhost:3000 --new
|
||||
./browser-nav.js file:///path/to/page.html
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
let page;
|
||||
if (newTab) {
|
||||
page = await browser.newPage();
|
||||
} else {
|
||||
const pages = await browser.pages();
|
||||
page = pages[pages.length - 1] || await browser.newPage();
|
||||
}
|
||||
|
||||
await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const title = await page.title();
|
||||
console.log(`✓ ${newTab ? "Opened" : "Navigated to"}: ${url}`);
|
||||
console.log(` Title: ${title}`);
|
||||
|
||||
await browser.disconnect();
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
|
||||
} else {
|
||||
console.error(`✗ Navigation failed: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
186
skills/website-debug/scripts/browser-network.js
Executable file
186
skills/website-debug/scripts/browser-network.js
Executable file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-network.js - Monitor network requests
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-network.js # Show recent requests
|
||||
* ./browser-network.js --watch # Watch requests in real-time
|
||||
* ./browser-network.js --failures # Show only failed requests
|
||||
* ./browser-network.js --xhr # Show only XHR/fetch requests
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const watch = args.includes("--watch");
|
||||
const failures = args.includes("--failures");
|
||||
const xhrOnly = args.includes("--xhr");
|
||||
const port = args.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`
|
||||
browser-network.js - Network request monitoring
|
||||
|
||||
Usage:
|
||||
./browser-network.js [options]
|
||||
|
||||
Options:
|
||||
--watch Watch requests in real-time
|
||||
--failures Show only failed requests (4xx, 5xx, network errors)
|
||||
--xhr Show only XHR/fetch requests (API calls)
|
||||
--port=PORT Connect to custom debug port (default: 9222)
|
||||
|
||||
Output includes:
|
||||
- Request method and URL
|
||||
- Response status
|
||||
- Response time
|
||||
- Content type
|
||||
- Size (when available)
|
||||
|
||||
Examples:
|
||||
./browser-network.js
|
||||
./browser-network.js --watch
|
||||
./browser-network.js --failures
|
||||
./browser-network.js --xhr --watch
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
const page = pages[pages.length - 1];
|
||||
|
||||
if (!page) {
|
||||
console.error("✗ No active tab found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send("Network.enable");
|
||||
|
||||
const requests = new Map();
|
||||
|
||||
const formatSize = (bytes) => {
|
||||
if (!bytes) return "?";
|
||||
if (bytes < 1024) return `${bytes}B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
||||
};
|
||||
|
||||
const shouldShow = (req, resp) => {
|
||||
if (failures) {
|
||||
const status = resp?.status || 0;
|
||||
return status >= 400 || status === 0;
|
||||
}
|
||||
if (xhrOnly) {
|
||||
const type = req.type || req.resourceType;
|
||||
return type === "XHR" || type === "Fetch";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const formatRequest = (req, resp, timing) => {
|
||||
const method = req.method || "GET";
|
||||
const url = new URL(req.url);
|
||||
const path = url.pathname + url.search;
|
||||
const status = resp?.status || "pending";
|
||||
const time = timing ? `${Math.round(timing)}ms` : "?";
|
||||
const size = formatSize(resp?.encodedDataLength);
|
||||
const type = resp?.mimeType?.split("/")[1]?.split(";")[0] || "?";
|
||||
|
||||
const statusColor = status >= 400 ? "❌" : status >= 300 ? "↩️" : "✓";
|
||||
|
||||
return `${statusColor} ${method.padEnd(6)} ${status} ${path.slice(0, 60).padEnd(60)} ${time.padStart(7)} ${size.padStart(8)} ${type}`;
|
||||
};
|
||||
|
||||
if (watch) {
|
||||
console.log("Watching network requests... (Ctrl+C to stop)\n");
|
||||
console.log(" Method Status URL".padEnd(80) + "Time".padStart(10) + "Size".padStart(10) + " Type");
|
||||
console.log("-".repeat(110));
|
||||
|
||||
client.on("Network.requestWillBeSent", ({ requestId, request }) => {
|
||||
requests.set(requestId, { request, startTime: Date.now() });
|
||||
});
|
||||
|
||||
client.on("Network.responseReceived", ({ requestId, response }) => {
|
||||
const req = requests.get(requestId);
|
||||
if (req) {
|
||||
req.response = response;
|
||||
}
|
||||
});
|
||||
|
||||
client.on("Network.loadingFinished", ({ requestId, encodedDataLength }) => {
|
||||
const req = requests.get(requestId);
|
||||
if (req) {
|
||||
const timing = Date.now() - req.startTime;
|
||||
if (req.response) {
|
||||
req.response.encodedDataLength = encodedDataLength;
|
||||
}
|
||||
if (shouldShow(req.request, req.response)) {
|
||||
console.log(formatRequest(req.request, req.response, timing));
|
||||
}
|
||||
requests.delete(requestId);
|
||||
}
|
||||
});
|
||||
|
||||
client.on("Network.loadingFailed", ({ requestId, errorText }) => {
|
||||
const req = requests.get(requestId);
|
||||
if (req) {
|
||||
const timing = Date.now() - req.startTime;
|
||||
console.log(`❌ ${req.request.method.padEnd(6)} ERR ${req.request.url.slice(0, 60).padEnd(60)} ${String(timing + "ms").padStart(7)} - ${errorText}`);
|
||||
requests.delete(requestId);
|
||||
}
|
||||
});
|
||||
|
||||
// Keep alive
|
||||
await new Promise(() => {});
|
||||
} else {
|
||||
// Get recent requests via Performance API
|
||||
const requests = await page.evaluate(() => {
|
||||
return performance.getEntriesByType("resource").map(entry => ({
|
||||
name: entry.name,
|
||||
type: entry.initiatorType,
|
||||
duration: Math.round(entry.duration),
|
||||
size: entry.transferSize,
|
||||
status: entry.responseStatus
|
||||
}));
|
||||
});
|
||||
|
||||
if (requests.length === 0) {
|
||||
console.log("No requests captured. Try --watch to monitor in real-time.");
|
||||
} else {
|
||||
console.log("Recent network requests:\n");
|
||||
console.log("Type".padEnd(10) + "Status".padEnd(8) + "Duration".padEnd(10) + "Size".padEnd(10) + "URL");
|
||||
console.log("-".repeat(100));
|
||||
|
||||
requests
|
||||
.filter(r => {
|
||||
if (failures) return r.status >= 400;
|
||||
if (xhrOnly) return r.type === "xmlhttprequest" || r.type === "fetch";
|
||||
return true;
|
||||
})
|
||||
.slice(-30)
|
||||
.forEach(r => {
|
||||
const url = new URL(r.name);
|
||||
const path = url.pathname.slice(0, 50);
|
||||
console.log(
|
||||
`${r.type.slice(0, 9).padEnd(10)}${String(r.status || "?").padEnd(8)}${String(r.duration + "ms").padEnd(10)}${formatSize(r.size).padEnd(10)}${path}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
await browser.disconnect();
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
|
||||
} else {
|
||||
console.error(`✗ Network monitoring failed: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
251
skills/website-debug/scripts/browser-pick.js
Executable file
251
skills/website-debug/scripts/browser-pick.js
Executable file
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-pick.js - Interactive element picker for DOM selection
|
||||
*
|
||||
* IMPORTANT: Use this when the user wants to select specific DOM elements.
|
||||
* The user can click elements, multi-select with Cmd/Ctrl+Click, and press Enter when done.
|
||||
* Returns CSS selectors and element details.
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-pick.js "Click on the broken element"
|
||||
* ./browser-pick.js "Select the buttons to style"
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const message = process.argv.slice(2).filter(a => !a.startsWith("--")).join(" ") || "Select an element";
|
||||
const port = process.argv.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
console.log(`
|
||||
browser-pick.js - Interactive element picker
|
||||
|
||||
Usage:
|
||||
./browser-pick.js "<message>"
|
||||
|
||||
The picker lets users visually select elements:
|
||||
- Click: Select single element
|
||||
- Cmd/Ctrl+Click: Add to multi-selection
|
||||
- Enter: Finish multi-selection
|
||||
- Escape: Cancel
|
||||
|
||||
Returns:
|
||||
- CSS selector(s) for selected elements
|
||||
- Tag name, ID, classes
|
||||
- Text content preview
|
||||
- Computed styles summary
|
||||
- Parent chain for context
|
||||
|
||||
Examples:
|
||||
./browser-pick.js "Click on the element that looks wrong"
|
||||
./browser-pick.js "Select all the buttons to update"
|
||||
./browser-pick.js "Which element should I fix?"
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
const page = pages[pages.length - 1];
|
||||
|
||||
if (!page) {
|
||||
console.error("✗ No active tab found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Picker active: ${message}`);
|
||||
console.log(" Click to select, Cmd/Ctrl+Click for multi-select, Enter to finish, Escape to cancel\n");
|
||||
|
||||
// Inject picker helper
|
||||
await page.evaluate(() => {
|
||||
if (!window.__webdebugPick) {
|
||||
window.__webdebugPick = async (message) => {
|
||||
return new Promise((resolve) => {
|
||||
const selections = [];
|
||||
const selectedElements = new Set();
|
||||
|
||||
// Create overlay
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "__webdebug-picker-overlay";
|
||||
overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none";
|
||||
|
||||
// Highlight element
|
||||
const highlight = document.createElement("div");
|
||||
highlight.style.cssText = "position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.15);transition:all 0.05s;pointer-events:none";
|
||||
overlay.appendChild(highlight);
|
||||
|
||||
// Banner
|
||||
const banner = document.createElement("div");
|
||||
banner.style.cssText = "position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:white;padding:12px 24px;border-radius:8px;font:14px system-ui,sans-serif;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647;max-width:80vw;text-align:center";
|
||||
|
||||
const updateBanner = () => {
|
||||
banner.innerHTML = `<strong>${message}</strong><br><span style="opacity:0.8;font-size:12px">${selections.length} selected · Cmd/Ctrl+Click to add · Enter to finish · Escape to cancel</span>`;
|
||||
};
|
||||
updateBanner();
|
||||
|
||||
document.body.appendChild(banner);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Build unique selector
|
||||
const buildSelector = (el) => {
|
||||
if (el.id) return `#${el.id}`;
|
||||
|
||||
let selector = el.tagName.toLowerCase();
|
||||
if (el.className && typeof el.className === 'string') {
|
||||
const classes = el.className.trim().split(/\s+/).filter(c => c && !c.startsWith('__'));
|
||||
if (classes.length) selector += '.' + classes.slice(0, 3).join('.');
|
||||
}
|
||||
|
||||
// Add nth-child if needed for uniqueness
|
||||
const parent = el.parentElement;
|
||||
if (parent) {
|
||||
const siblings = [...parent.children].filter(c => c.tagName === el.tagName);
|
||||
if (siblings.length > 1) {
|
||||
const index = siblings.indexOf(el) + 1;
|
||||
selector += `:nth-child(${index})`;
|
||||
}
|
||||
}
|
||||
|
||||
return selector;
|
||||
};
|
||||
|
||||
// Get full selector path
|
||||
const getFullSelector = (el) => {
|
||||
const parts = [];
|
||||
let current = el;
|
||||
while (current && current !== document.body && parts.length < 4) {
|
||||
parts.unshift(buildSelector(current));
|
||||
current = current.parentElement;
|
||||
}
|
||||
return parts.join(' > ');
|
||||
};
|
||||
|
||||
// Build element info
|
||||
const buildElementInfo = (el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const styles = getComputedStyle(el);
|
||||
|
||||
return {
|
||||
selector: getFullSelector(el),
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || null,
|
||||
classes: el.className?.toString().trim() || null,
|
||||
text: el.textContent?.trim().slice(0, 100) || null,
|
||||
dimensions: `${Math.round(rect.width)}×${Math.round(rect.height)}`,
|
||||
position: `${Math.round(rect.left)},${Math.round(rect.top)}`,
|
||||
display: styles.display,
|
||||
visibility: styles.visibility,
|
||||
opacity: styles.opacity,
|
||||
zIndex: styles.zIndex,
|
||||
overflow: styles.overflow
|
||||
};
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
document.removeEventListener("mousemove", onMove, true);
|
||||
document.removeEventListener("click", onClick, true);
|
||||
document.removeEventListener("keydown", onKey, true);
|
||||
overlay.remove();
|
||||
banner.remove();
|
||||
selectedElements.forEach(el => {
|
||||
el.style.outline = el.__originalOutline || "";
|
||||
delete el.__originalOutline;
|
||||
});
|
||||
};
|
||||
|
||||
const onMove = (e) => {
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el || overlay.contains(el) || banner.contains(el)) {
|
||||
highlight.style.display = "none";
|
||||
return;
|
||||
}
|
||||
const r = el.getBoundingClientRect();
|
||||
highlight.style.display = "block";
|
||||
highlight.style.top = r.top + "px";
|
||||
highlight.style.left = r.left + "px";
|
||||
highlight.style.width = r.width + "px";
|
||||
highlight.style.height = r.height + "px";
|
||||
};
|
||||
|
||||
const onClick = (e) => {
|
||||
if (banner.contains(e.target)) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el || overlay.contains(el) || banner.contains(el)) return;
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
// Multi-select mode
|
||||
if (!selectedElements.has(el)) {
|
||||
selectedElements.add(el);
|
||||
el.__originalOutline = el.style.outline;
|
||||
el.style.outline = "3px solid #10b981";
|
||||
selections.push(buildElementInfo(el));
|
||||
updateBanner();
|
||||
}
|
||||
} else {
|
||||
// Single select - finish
|
||||
cleanup();
|
||||
if (selections.length > 0) {
|
||||
resolve(selections);
|
||||
} else {
|
||||
resolve([buildElementInfo(el)]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onKey = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
cleanup();
|
||||
resolve(null);
|
||||
} else if (e.key === "Enter" && selections.length > 0) {
|
||||
e.preventDefault();
|
||||
cleanup();
|
||||
resolve(selections);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", onMove, true);
|
||||
document.addEventListener("click", onClick, true);
|
||||
document.addEventListener("keydown", onKey, true);
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Run picker
|
||||
const result = await page.evaluate((msg) => window.__webdebugPick(msg), message);
|
||||
|
||||
if (result === null) {
|
||||
console.log("Picker cancelled");
|
||||
} else if (Array.isArray(result)) {
|
||||
console.log(`Selected ${result.length} element(s):\n`);
|
||||
|
||||
result.forEach((info, i) => {
|
||||
if (i > 0) console.log("---");
|
||||
console.log(`Selector: ${info.selector}`);
|
||||
console.log(`Tag: ${info.tag}${info.id ? ` #${info.id}` : ""}${info.classes ? ` .${info.classes.split(" ").join(".")}` : ""}`);
|
||||
console.log(`Size: ${info.dimensions} at (${info.position})`);
|
||||
console.log(`Display: ${info.display}, Visibility: ${info.visibility}, Opacity: ${info.opacity}`);
|
||||
if (info.zIndex !== "auto") console.log(`Z-Index: ${info.zIndex}`);
|
||||
if (info.text) console.log(`Text: "${info.text.slice(0, 50)}${info.text.length > 50 ? "..." : ""}"`);
|
||||
});
|
||||
}
|
||||
|
||||
await browser.disconnect();
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
|
||||
} else {
|
||||
console.error(`✗ Picker failed: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
102
skills/website-debug/scripts/browser-resize.js
Executable file
102
skills/website-debug/scripts/browser-resize.js
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-resize.js - Resize viewport for responsive testing
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-resize.js 375 667 # Custom width x height
|
||||
* ./browser-resize.js --mobile # iPhone SE (375x667)
|
||||
* ./browser-resize.js --tablet # iPad (768x1024)
|
||||
* ./browser-resize.js --desktop # Desktop (1920x1080)
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const port = args.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
// Preset sizes
|
||||
const presets = {
|
||||
"--mobile": { width: 375, height: 667, name: "Mobile (iPhone SE)" },
|
||||
"--iphone": { width: 390, height: 844, name: "iPhone 14" },
|
||||
"--iphone-pro": { width: 393, height: 852, name: "iPhone 14 Pro" },
|
||||
"--android": { width: 412, height: 915, name: "Android (Pixel 7)" },
|
||||
"--tablet": { width: 768, height: 1024, name: "Tablet (iPad)" },
|
||||
"--ipad-pro": { width: 1024, height: 1366, name: "iPad Pro 12.9\"" },
|
||||
"--laptop": { width: 1366, height: 768, name: "Laptop" },
|
||||
"--desktop": { width: 1920, height: 1080, name: "Desktop (1080p)" },
|
||||
"--4k": { width: 3840, height: 2160, name: "4K" }
|
||||
};
|
||||
|
||||
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
||||
console.log(`
|
||||
browser-resize.js - Resize viewport for responsive testing
|
||||
|
||||
Usage:
|
||||
./browser-resize.js <width> <height> # Custom dimensions
|
||||
./browser-resize.js --preset # Use preset size
|
||||
|
||||
Presets:
|
||||
--mobile 375×667 (iPhone SE)
|
||||
--iphone 390×844 (iPhone 14)
|
||||
--iphone-pro 393×852 (iPhone 14 Pro)
|
||||
--android 412×915 (Pixel 7)
|
||||
--tablet 768×1024 (iPad)
|
||||
--ipad-pro 1024×1366 (iPad Pro 12.9")
|
||||
--laptop 1366×768 (Laptop)
|
||||
--desktop 1920×1080 (Desktop 1080p)
|
||||
--4k 3840×2160 (4K)
|
||||
|
||||
Examples:
|
||||
./browser-resize.js 375 667
|
||||
./browser-resize.js --mobile
|
||||
./browser-resize.js --tablet && ./browser-screenshot.js
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Determine dimensions
|
||||
let width, height, name;
|
||||
|
||||
const presetArg = args.find(a => presets[a]);
|
||||
if (presetArg) {
|
||||
({ width, height, name } = presets[presetArg]);
|
||||
} else {
|
||||
width = parseInt(args[0]);
|
||||
height = parseInt(args[1]);
|
||||
|
||||
if (isNaN(width) || isNaN(height)) {
|
||||
console.error("✗ Invalid dimensions. Use: ./browser-resize.js <width> <height>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
name = `Custom (${width}×${height})`;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
const page = pages[pages.length - 1];
|
||||
|
||||
if (!page) {
|
||||
console.error("✗ No active tab found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await page.setViewport({ width, height });
|
||||
|
||||
console.log(`✓ Viewport resized to ${width}×${height}`);
|
||||
console.log(` ${name}`);
|
||||
|
||||
await browser.disconnect();
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
|
||||
} else {
|
||||
console.error(`✗ Resize failed: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
90
skills/website-debug/scripts/browser-screenshot.js
Executable file
90
skills/website-debug/scripts/browser-screenshot.js
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-screenshot.js - Capture screenshot of current viewport
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-screenshot.js # Viewport screenshot
|
||||
* ./browser-screenshot.js --full # Full page screenshot
|
||||
* ./browser-screenshot.js --selector=".main" # Element screenshot
|
||||
* ./browser-screenshot.js --output=/path.png # Custom output path
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const fullPage = args.includes("--full");
|
||||
const selector = args.find(a => a.startsWith("--selector="))?.split("=")[1];
|
||||
const outputPath = args.find(a => a.startsWith("--output="))?.split("=")[1];
|
||||
const port = args.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`
|
||||
browser-screenshot.js - Capture screenshot
|
||||
|
||||
Usage:
|
||||
./browser-screenshot.js [options]
|
||||
|
||||
Options:
|
||||
--full Capture full page (scrollable content)
|
||||
--selector=SEL Capture specific element only
|
||||
--output=PATH Save to custom path (default: temp file)
|
||||
--port=PORT Connect to custom debug port (default: 9222)
|
||||
|
||||
Examples:
|
||||
./browser-screenshot.js
|
||||
./browser-screenshot.js --full
|
||||
./browser-screenshot.js --selector=".hero-section"
|
||||
./browser-screenshot.js --output=./screenshot.png
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
|
||||
const pages = await browser.pages();
|
||||
const page = pages[pages.length - 1];
|
||||
|
||||
if (!page) {
|
||||
console.error("✗ No active tab found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||
const filename = outputPath || join(tmpdir(), `screenshot-${timestamp}.png`);
|
||||
|
||||
if (selector) {
|
||||
const element = await page.$(selector);
|
||||
if (!element) {
|
||||
console.error(`✗ Element not found: ${selector}`);
|
||||
process.exit(1);
|
||||
}
|
||||
await element.screenshot({ path: filename });
|
||||
console.log(`✓ Element screenshot saved`);
|
||||
} else {
|
||||
await page.screenshot({
|
||||
path: filename,
|
||||
fullPage
|
||||
});
|
||||
console.log(`✓ ${fullPage ? "Full page" : "Viewport"} screenshot saved`);
|
||||
}
|
||||
|
||||
console.log(` Path: ${filename}`);
|
||||
|
||||
// Output just the path for easy piping
|
||||
console.log(filename);
|
||||
|
||||
await browser.disconnect();
|
||||
} catch (e) {
|
||||
if (e.message?.includes("ECONNREFUSED")) {
|
||||
console.error("✗ Cannot connect to browser. Run: ./browser-start.js");
|
||||
} else {
|
||||
console.error(`✗ Screenshot failed: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
190
skills/website-debug/scripts/browser-start.js
Executable file
190
skills/website-debug/scripts/browser-start.js
Executable file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* browser-start.js - Start Chrome or WebKit with remote debugging
|
||||
*
|
||||
* Usage:
|
||||
* ./browser-start.js # Fresh Chrome profile
|
||||
* ./browser-start.js --profile # Chrome with user profile (preserves logins)
|
||||
* ./browser-start.js --webkit # Playwright WebKit (Safari-like)
|
||||
* ./browser-start.js --headless # Headless mode
|
||||
*/
|
||||
|
||||
import { spawn, execSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { homedir, platform, tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const useProfile = args.includes("--profile");
|
||||
const useWebKit = args.includes("--webkit");
|
||||
const headless = args.includes("--headless");
|
||||
const port = args.find(a => a.startsWith("--port="))?.split("=")[1] || "9222";
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`
|
||||
browser-start.js - Start browser with remote debugging
|
||||
|
||||
Usage:
|
||||
./browser-start.js [options]
|
||||
|
||||
Options:
|
||||
--profile Copy user's Chrome profile (preserves logins, cookies)
|
||||
--webkit Use Playwright WebKit instead of Chrome (Safari-like)
|
||||
--headless Run in headless mode
|
||||
--port=PORT Use custom debug port (default: 9222)
|
||||
--help Show this help message
|
||||
|
||||
Examples:
|
||||
./browser-start.js # Fresh Chrome profile
|
||||
./browser-start.js --profile # Chrome with your logins
|
||||
./browser-start.js --webkit # Safari/WebKit via Playwright
|
||||
./browser-start.js --headless # Headless Chrome
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const cacheDir = join(homedir(), ".cache", "website-debug");
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
|
||||
async function startChrome() {
|
||||
// Kill existing Chrome debug instances
|
||||
try {
|
||||
if (platform() === "darwin") {
|
||||
execSync("killall 'Google Chrome' 2>/dev/null", { stdio: "ignore" });
|
||||
} else if (platform() === "linux") {
|
||||
execSync("pkill -f 'chrome.*remote-debugging' 2>/dev/null", { stdio: "ignore" });
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
const profileDir = join(cacheDir, "chrome-profile");
|
||||
|
||||
if (useProfile) {
|
||||
// Find and copy user's Chrome profile
|
||||
const userProfilePaths = {
|
||||
darwin: join(homedir(), "Library/Application Support/Google/Chrome"),
|
||||
linux: join(homedir(), ".config/google-chrome"),
|
||||
win32: join(homedir(), "AppData/Local/Google/Chrome/User Data")
|
||||
};
|
||||
|
||||
const userProfile = userProfilePaths[platform()];
|
||||
if (existsSync(userProfile)) {
|
||||
console.log("Syncing user profile (this may take a moment)...");
|
||||
try {
|
||||
execSync(`rsync -a --delete "${userProfile}/" "${profileDir}/"`, { stdio: "pipe" });
|
||||
} catch (e) {
|
||||
console.log("Warning: Could not sync profile, using fresh profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find Chrome executable
|
||||
const chromePaths = {
|
||||
darwin: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
linux: "/usr/bin/google-chrome",
|
||||
win32: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||
};
|
||||
|
||||
const chromePath = chromePaths[platform()];
|
||||
if (!existsSync(chromePath)) {
|
||||
console.error(`✗ Chrome not found at ${chromePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const chromeArgs = [
|
||||
`--remote-debugging-port=${port}`,
|
||||
`--user-data-dir=${profileDir}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check"
|
||||
];
|
||||
|
||||
if (headless) {
|
||||
chromeArgs.push("--headless=new");
|
||||
}
|
||||
|
||||
// Start Chrome detached
|
||||
const chrome = spawn(chromePath, chromeArgs, {
|
||||
detached: true,
|
||||
stdio: "ignore"
|
||||
});
|
||||
chrome.unref();
|
||||
|
||||
// Wait for Chrome to be ready
|
||||
const puppeteer = await import("puppeteer-core");
|
||||
let connected = false;
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
const browser = await puppeteer.default.connect({
|
||||
browserURL: `http://localhost:${port}`,
|
||||
defaultViewport: null
|
||||
});
|
||||
await browser.disconnect();
|
||||
connected = true;
|
||||
break;
|
||||
} catch {
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
console.error("✗ Failed to connect to Chrome");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✓ Chrome started on :${port}${useProfile ? " with user profile" : ""}${headless ? " (headless)" : ""}`);
|
||||
}
|
||||
|
||||
async function startWebKit() {
|
||||
const stateFile = join(cacheDir, "webkit-state.json");
|
||||
|
||||
try {
|
||||
const { webkit } = await import("playwright");
|
||||
|
||||
console.log("Starting WebKit browser...");
|
||||
|
||||
const browser = await webkit.launchPersistentContext(join(cacheDir, "webkit-profile"), {
|
||||
headless,
|
||||
viewport: null,
|
||||
// Save browser endpoint for other scripts
|
||||
});
|
||||
|
||||
// Store connection info
|
||||
const endpoint = browser.browser()?.wsEndpoint?.() || "direct-context";
|
||||
const fs = await import("node:fs/promises");
|
||||
await fs.writeFile(stateFile, JSON.stringify({
|
||||
type: "webkit",
|
||||
endpoint,
|
||||
pid: process.pid
|
||||
}));
|
||||
|
||||
console.log(`✓ WebKit started${headless ? " (headless)" : ""}`);
|
||||
console.log(" Press Ctrl+C to stop");
|
||||
|
||||
// Keep process alive for WebKit
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\nClosing WebKit...");
|
||||
await browser.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Keep alive
|
||||
await new Promise(() => {});
|
||||
|
||||
} catch (e) {
|
||||
if (e.message?.includes("Cannot find module")) {
|
||||
console.error("✗ Playwright not installed. Run: npm install -g playwright && npx playwright install webkit");
|
||||
} else {
|
||||
console.error(`✗ Failed to start WebKit: ${e.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
if (useWebKit) {
|
||||
startWebKit();
|
||||
} else {
|
||||
startChrome();
|
||||
}
|
||||
84
skills/website-debug/scripts/setup.sh
Executable file
84
skills/website-debug/scripts/setup.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# setup.sh - Install dependencies for website-debug skill
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
echo "Setting up website-debug skill..."
|
||||
echo ""
|
||||
|
||||
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SCRIPTS_DIR="$SKILL_DIR/scripts"
|
||||
|
||||
# Check Node.js
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "❌ Node.js not found. Please install Node.js first:"
|
||||
echo " https://nodejs.org/ or: brew install node"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Node.js $(node --version)"
|
||||
|
||||
# Check npm
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "❌ npm not found. Please install npm."
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ npm $(npm --version)"
|
||||
|
||||
# Install puppeteer-core globally
|
||||
echo ""
|
||||
echo "Installing puppeteer-core..."
|
||||
npm install -g puppeteer-core 2>/dev/null || npm install puppeteer-core --save 2>/dev/null
|
||||
echo "✓ puppeteer-core installed"
|
||||
|
||||
# Optional: Install Playwright for WebKit support
|
||||
echo ""
|
||||
echo "Installing Playwright for Safari/WebKit support (optional)..."
|
||||
if npm install -g playwright 2>/dev/null; then
|
||||
npx playwright install webkit 2>/dev/null || true
|
||||
echo "✓ Playwright + WebKit installed"
|
||||
else
|
||||
echo "⚠ Playwright installation skipped (Chrome debugging will still work)"
|
||||
fi
|
||||
|
||||
# Make scripts executable
|
||||
echo ""
|
||||
echo "Making scripts executable..."
|
||||
chmod +x "$SCRIPTS_DIR"/*.js 2>/dev/null || true
|
||||
echo "✓ Scripts are executable"
|
||||
|
||||
# Create cache directory
|
||||
mkdir -p ~/.cache/website-debug
|
||||
echo "✓ Cache directory ready"
|
||||
|
||||
# Check Chrome
|
||||
echo ""
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||
if [ -f "$CHROME_PATH" ]; then
|
||||
echo "✓ Chrome found at $CHROME_PATH"
|
||||
else
|
||||
echo "⚠ Chrome not found. Install from: https://www.google.com/chrome/"
|
||||
fi
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
if command -v google-chrome &> /dev/null; then
|
||||
echo "✓ Chrome found"
|
||||
else
|
||||
echo "⚠ Chrome not found. Install with: sudo apt install google-chrome-stable"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "✓ Setup complete!"
|
||||
echo ""
|
||||
echo "Quick start:"
|
||||
echo " cd $SCRIPTS_DIR"
|
||||
echo " ./browser-start.js # Start Chrome"
|
||||
echo " ./browser-nav.js http://localhost:3000"
|
||||
echo " ./browser-screenshot.js # Take screenshot"
|
||||
echo ""
|
||||
echo "Or use with Claude Code:"
|
||||
echo " Ask Claude to 'debug this page' or 'check my site'"
|
||||
echo "=========================================="
|
||||
Reference in New Issue
Block a user