Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:55:23 +08:00
commit ff43aa6f4d
42 changed files with 4239 additions and 0 deletions

View 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}`);
}
}
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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();
}

View 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 "=========================================="