Initial commit
This commit is contained in:
151
skills/README.md
Normal file
151
skills/README.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Browser Devtools
|
||||
|
||||
Use natural language prompts to debug and automate web browsers with Playwright.
|
||||
|
||||
## How to Use
|
||||
|
||||
### Basic Navigation
|
||||
|
||||
- "Navigate to https://example.com and tell me the page title"
|
||||
- "Open my local app at http://localhost:3000"
|
||||
- "Check if my website is accessible"
|
||||
- "Show me what https://github.com/vukhanhtruong/claude-rock looks like"
|
||||
|
||||
### Screenshots
|
||||
|
||||
- "Take a screenshot of the entire page at https://example.com"
|
||||
- "Capture just the header section of my local app"
|
||||
- "Screenshot the login form and save it as login-page.png"
|
||||
- "Take a full page screenshot of my dashboard"
|
||||
|
||||
### Finding Elements
|
||||
|
||||
- "Show me all the buttons on this page"
|
||||
- "What interactive elements are on https://example.com?"
|
||||
- "Find all input fields and forms on the page"
|
||||
- "List all links and their text content"
|
||||
- "Show me the CSS selectors for all buttons"
|
||||
|
||||
### Console Debugging
|
||||
|
||||
- "Check for JavaScript errors on my page"
|
||||
- "Monitor the console for warnings and errors for 10 seconds"
|
||||
- "Show me all console messages while I interact with the site"
|
||||
- "Are there any 404 errors or missing resources?"
|
||||
- "Watch the console for network failures"
|
||||
|
||||
### Network Monitoring
|
||||
|
||||
- "Show me all API calls being made by this page"
|
||||
- "Monitor network requests for the next 5 seconds"
|
||||
- "Are there any failed requests or slow loading resources?"
|
||||
- "Track all XHR and fetch requests"
|
||||
- "Show me the largest resources loading on the page"
|
||||
|
||||
### Performance Analysis
|
||||
|
||||
- "Check the Core Web Vitals for my site"
|
||||
- "What's the LCP (Largest Contentful Paint) for this page?"
|
||||
- "Is my site performing well? Give me the performance metrics"
|
||||
- "Run a performance audit and show me the results"
|
||||
- "Compare performance between my staging and production sites"
|
||||
|
||||
### Form Testing
|
||||
|
||||
- "Fill out the login form with test credentials"
|
||||
- "Enter 'test@example.com' in the email field and 'password123' in the password field"
|
||||
- "Submit the contact form and see what happens"
|
||||
- "Test the search functionality by entering 'query' and clicking search"
|
||||
- "Fill out all required fields in the registration form"
|
||||
|
||||
### Element Interaction
|
||||
|
||||
- "Click the submit button"
|
||||
- "Click on the menu toggle"
|
||||
- "Click the 'Add to Cart' button and wait for the result"
|
||||
- "Interact with the dropdown menu"
|
||||
- "Click on the first link in the navigation"
|
||||
|
||||
### JavaScript Execution
|
||||
|
||||
- "Run `document.title` to get the page title"
|
||||
- "Execute `document.querySelectorAll('.error').length` to count errors"
|
||||
- "Check if there are any console errors with JavaScript"
|
||||
- "Run `localStorage.clear()` to clear storage"
|
||||
- "Execute `window.scrollTo(0, document.body.scrollHeight)` to scroll to bottom"
|
||||
|
||||
### Debugging Workflows
|
||||
|
||||
- "I'm getting a JavaScript error on my site. Can you find it?"
|
||||
- "My form isn't submitting properly. Can you test it?"
|
||||
- "The page loads slowly. Can you analyze the performance?"
|
||||
- "Some API calls are failing. Monitor the network for me"
|
||||
- "I need to test my responsive design. Take screenshots at different sizes"
|
||||
|
||||
### Development Testing
|
||||
|
||||
- "Test my local development server at localhost:3000"
|
||||
- "Check if my staging environment is working"
|
||||
- "Monitor my app for errors while I use it"
|
||||
- "Take before and after screenshots of my changes"
|
||||
- "Verify that my new feature is working correctly"
|
||||
|
||||
## Example Conversations
|
||||
|
||||
### Debugging Console Errors
|
||||
|
||||
```
|
||||
User: "My React app is showing errors at http://localhost:3000. Can you check the console?"
|
||||
Claude: [Analyzes console output] "I found 3 JavaScript errors related to missing props..."
|
||||
```
|
||||
|
||||
### Form Testing
|
||||
|
||||
```
|
||||
User: "Test my login form here https://www.saucedemo.com/, use standard_user and secret_sauce"
|
||||
Claude: "I'll navigate to your login page. What's the URL and what credentials should I use?"
|
||||
User: ""
|
||||
Claude: [Navigates, fills form, submits] "The form submitted successfully and redirected to dashboard"
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
```
|
||||
User: "run performance test this URL https://myapp.com"
|
||||
Claude: [Analyzes performance] "Your LCP is 4.2 seconds which is slow. The main bottleneck is..."
|
||||
```
|
||||
|
||||
## Tips for Best Results
|
||||
|
||||
- **Be specific about URLs** - Provide the full URL including http:// or https://
|
||||
- **Describe what you're testing** - Explain what you expect to happen
|
||||
- **Mention local vs production** - Specify if you're testing localhost or a live site
|
||||
- **Provide credentials when needed** - For form testing, give test usernames/passwords
|
||||
- **Describe the problem** - Explain what issue you're trying to diagnose
|
||||
- **Ask for specific metrics** - Request particular performance data or error types
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### "My site is broken" workflow:
|
||||
|
||||
1. "Check if https://mysite.com loads"
|
||||
2. "Look for JavaScript errors in the console"
|
||||
3. "Monitor network requests for failures"
|
||||
4. "Take a screenshot to see what's visible"
|
||||
|
||||
### "Test my new feature" workflow:
|
||||
|
||||
1. "Navigate to http://localhost:3000/new-feature"
|
||||
2. "Click the [button/element] to activate it"
|
||||
3. "Watch the console for any errors"
|
||||
4. "Verify the expected result appears"
|
||||
|
||||
### "Performance investigation" workflow:
|
||||
|
||||
1. "Analyze performance metrics for https://mysite.com"
|
||||
2. "Show me the largest resources loading"
|
||||
3. "Check Core Web Vitals"
|
||||
4. "Identify the main bottlenecks"
|
||||
|
||||
Just describe what you want to do in plain language, and I'll handle the browser automation for you!
|
||||
|
||||
193
skills/SKILL.md
Normal file
193
skills/SKILL.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
name: browser-devtools
|
||||
description: Browser debugging and automation using Playwright. Use for web debugging, console monitoring, network analysis, screenshot capture, DOM inspection, and UI testing during development.
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Browser Devtools
|
||||
|
||||
Browser automation and debugging using Playwright for web development testing and analysis.
|
||||
|
||||
## Decision tree
|
||||
|
||||
Here the decison tree when user want to inspect the console log, apply the same pattern for other prompt.
|
||||
|
||||
```
|
||||
User task → Is URL provided?
|
||||
├─ Yes → Detect if it reachable.
|
||||
│ ├─ Success → Implement Reconnaissance-then-action (RTA)
|
||||
| | 1. Navigate to provided URL.
|
||||
| | 2. Inspect console log
|
||||
| | 3. Identify the errors
|
||||
| | 4. Execute actions with discovered issues
|
||||
│ └─ Fails → End
|
||||
│
|
||||
└─ No → Is the development server already running?
|
||||
├─ No → End
|
||||
│
|
||||
└─ Yes → Ask user if they want to test development server?
|
||||
├─ Yes → Reconnaissance-then-action (RTA)
|
||||
├─ No → End
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
cd plugins/browser-devtools/skills
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
Test installation:
|
||||
|
||||
```bash
|
||||
node dist/scripts/navigate.js --url https://example.com
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
All scripts located in `scripts/` (compiled to `dist/scripts/`):
|
||||
|
||||
**Core Commands:**
|
||||
|
||||
- `navigate.js` - Navigate to URLs
|
||||
- `screenshot.js` - Capture screenshots (full page or elements)
|
||||
- `snapshot.js` - DOM inspection and element discovery
|
||||
- `evaluate.js` - Execute JavaScript in browser context
|
||||
- `click.js` - Element interaction
|
||||
- `fill.js` - Form input testing
|
||||
|
||||
**Monitoring:**
|
||||
|
||||
- `console.js` - Console log monitoring and error tracking
|
||||
- `network.js` - Network request analysis and performance debugging
|
||||
- `performance.js` - Core Web Vitals and performance metrics
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Compliance Decision tree**: The decision tree must be followed strictly.
|
||||
- **Wait for elements**: Use appropriate wait strategies for dynamic content
|
||||
- **Clean up sessions**: Close sessions when done to free resources
|
||||
|
||||
## Usage
|
||||
|
||||
### Single Commands
|
||||
|
||||
```bash
|
||||
# Screenshot
|
||||
node dist/scripts/screenshot.js --url https://example.com --output page.png
|
||||
|
||||
# Performance analysis
|
||||
node dist/scripts/performance.js --url https://example.com | jq '.vitals.LCP'
|
||||
|
||||
# Console monitoring (10 seconds)
|
||||
node dist/scripts/console.js --url https://example.com --types error,warn --duration 10000
|
||||
```
|
||||
|
||||
### Chained Commands (reuse session)
|
||||
|
||||
```bash
|
||||
# Start session (keep browser open)
|
||||
node dist/scripts/navigate.js --url https://example.com/login --close false
|
||||
|
||||
# Continue with same browser
|
||||
node dist/scripts/fill.js --selector "#email" --value "user@example.com" --close false
|
||||
node dist/scripts/click.js --selector "button[type=submit]"
|
||||
```
|
||||
|
||||
### Common Debugging Patterns
|
||||
|
||||
```bash
|
||||
# Find elements
|
||||
node dist/scripts/snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'
|
||||
|
||||
# Check for errors
|
||||
node dist/scripts/console.js --url https://example.com --types error
|
||||
|
||||
# Monitor API calls
|
||||
node dist/scripts/network.js --url https://example.com --types xhr,fetch --duration 5000
|
||||
|
||||
# Test forms
|
||||
node dist/scripts/fill.js --url https://example.com --selector "#search" --value "query" --close false
|
||||
node dist/scripts/click.js --selector "button[type=submit]"
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
All scripts support:
|
||||
|
||||
- `--headless false` - Show browser window (default: true)
|
||||
- `--close false` - Keep browser open for chaining (default: true)
|
||||
- `--timeout 30000` - Timeout in milliseconds
|
||||
- `--browser chromium|firefox|webkit` - Browser engine (default: chromium)
|
||||
|
||||
## Output Format
|
||||
|
||||
**Success:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"url": "https://example.com",
|
||||
"title": "Example Domain",
|
||||
"sessionId": "session_1234567890_abc123",
|
||||
"timestamp": "2024-01-01T12:00:00.000Z",
|
||||
"...": "script-specific data"
|
||||
}
|
||||
```
|
||||
|
||||
**Error:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Element not found: .missing-element",
|
||||
"stack": "Error: Element not found..."
|
||||
}
|
||||
```
|
||||
|
||||
## Selectors
|
||||
|
||||
```bash
|
||||
# CSS selectors
|
||||
node dist/scripts/click.js --selector "#main .button.primary"
|
||||
|
||||
# XPath
|
||||
node dist/scripts/click.js --selector "xpath=//button[contains(text(), 'Submit')]"
|
||||
|
||||
# Text selectors
|
||||
node dist/scripts/click.js --selector "text=Click me"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Browser not installed**: Run `npm run install-browsers`
|
||||
**Element not found**: Use `snapshot.js` first to find correct selectors
|
||||
**Script timeout**: Increase with `--timeout 60000`
|
||||
**Permission denied**: Run `chmod +x scripts/install.sh`
|
||||
|
||||
**Debug mode**: Use `--headless false` to see browser actions
|
||||
**Stale sessions**: Start new session with `--close true`
|
||||
|
||||
## Integration
|
||||
|
||||
**Pre-commit testing:**
|
||||
|
||||
```bash
|
||||
node dist/scripts/navigate.js --url http://localhost:3000 && \
|
||||
node dist/scripts/performance.js --url http://localhost:3000 | jq '.vitals.LCP'
|
||||
```
|
||||
|
||||
**Performance regression:**
|
||||
|
||||
```bash
|
||||
node dist/scripts/performance.js --url $APP_URL | jq '.vitals.LCP' | \
|
||||
awk '{print $1 < 2500 ? "PASS" : "FAIL"}'
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
|
||||
- [Core Web Vitals](https://web.dev/vitals/)
|
||||
37
skills/package.json
Normal file
37
skills/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "browser-devtools",
|
||||
"version": "1.0.0",
|
||||
"description": "Browser debugging and automation skill using Playwright for developers",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"clean": "rm -rf dist",
|
||||
"install-browsers": "playwright install",
|
||||
"test": "node dist/scripts/test.js"
|
||||
},
|
||||
"keywords": [
|
||||
"browser",
|
||||
"debugging",
|
||||
"automation",
|
||||
"playwright",
|
||||
"development"
|
||||
],
|
||||
"author": "Claude Skills",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"playwright": "^1.40.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.8.0",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
91
skills/scripts/click.ts
Normal file
91
skills/scripts/click.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Element interaction for testing UI behavior
|
||||
* Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
|
||||
*/
|
||||
|
||||
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, waitForElement, getElementInfo } from '../lib/browser.js';
|
||||
|
||||
interface ClickArgs {
|
||||
selector?: string;
|
||||
url?: string;
|
||||
'wait-for'?: string;
|
||||
'wait-timeout'?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
}
|
||||
|
||||
async function click() {
|
||||
const args = parseArgs(process.argv.slice(2)) as ClickArgs;
|
||||
|
||||
if (!args.selector) {
|
||||
return outputError(new Error('--selector is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== 'false',
|
||||
});
|
||||
|
||||
// Navigate to URL if provided
|
||||
if (args.url) {
|
||||
await session.page.goto(args.url, { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
// Wait for element to be available
|
||||
const waitTimeout = args['wait-timeout'] ? parseInt(args['wait-timeout']) : 30000;
|
||||
await waitForElement(session.page, args.selector, waitTimeout);
|
||||
|
||||
// Get element info before click
|
||||
const elementInfo = await getElementInfo(session.page, args.selector);
|
||||
|
||||
// Check if element is visible and clickable
|
||||
if (!elementInfo.visible) {
|
||||
return outputError(new Error(`Element is not visible: ${args.selector}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Click the element
|
||||
await session.page.click(args.selector);
|
||||
|
||||
// Wait for navigation or new content if specified
|
||||
if (args['wait-for']) {
|
||||
try {
|
||||
await waitForElement(session.page, args['wait-for'], waitTimeout);
|
||||
} catch (error) {
|
||||
// Don't fail if wait-for element doesn't appear, just log it
|
||||
console.warn(`Warning: Wait-for element not found: ${args['wait-for']}`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: session.page.url(),
|
||||
title: await session.page.title(),
|
||||
action: {
|
||||
type: 'click',
|
||||
selector: args.selector,
|
||||
elementInfo: elementInfo,
|
||||
waitFor: args['wait-for'] || null,
|
||||
},
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
click();
|
||||
146
skills/scripts/console.ts
Normal file
146
skills/scripts/console.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Console log monitoring and error tracking
|
||||
* Usage: node console.js [--url URL] [--types error,warn] [--duration 5000] [--no-navigation]
|
||||
*
|
||||
* Options:
|
||||
* --url URL to navigate to (required if no existing browser session)
|
||||
* --types Console message types to capture (comma-separated, default: all)
|
||||
* --duration How long to monitor console (ms, default: 5000)
|
||||
* --no-navigation Don't navigate, use existing browser session
|
||||
*
|
||||
* Examples:
|
||||
* node console.js --url https://your-app.com --types error,warn
|
||||
* node console.js --no-navigation true # Uses existing session
|
||||
* node navigate.js --url https://your-app.com --close false
|
||||
* node console.js --no-navigation true --types error # Monitor existing page
|
||||
*/
|
||||
|
||||
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, getExistingSession } from '../lib/browser.js';
|
||||
|
||||
interface ConsoleArgs {
|
||||
url?: string;
|
||||
types?: string;
|
||||
duration?: string;
|
||||
timeout?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
'no-navigation'?: string | boolean;
|
||||
}
|
||||
|
||||
interface ConsoleMessage {
|
||||
type: string;
|
||||
text: string;
|
||||
location?: {
|
||||
url: string;
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
};
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
async function monitorConsole() {
|
||||
const args = parseArgs(process.argv.slice(2)) as ConsoleArgs;
|
||||
|
||||
// Smart URL handling
|
||||
let shouldNavigate = args['no-navigation'] !== 'true';
|
||||
let targetUrl = args.url;
|
||||
|
||||
// If no URL provided, try to use existing session
|
||||
if (!targetUrl) {
|
||||
const existingSession = await getExistingSession();
|
||||
if (existingSession && existingSession.browser.isConnected()) {
|
||||
// Use existing session, don't navigate
|
||||
shouldNavigate = false;
|
||||
targetUrl = existingSession.page.url();
|
||||
} else {
|
||||
// No existing session and no URL provided - require explicit URL from user
|
||||
return outputError(new Error('No browser session found. Please provide a URL with --url or navigate first using navigate.js. For example: --url https://your-site.com'));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== 'false',
|
||||
timeout: args.timeout ? parseInt(args.timeout) : undefined,
|
||||
});
|
||||
|
||||
const messages: ConsoleMessage[] = [];
|
||||
const filterTypes = args.types ? args.types.split(',').map(t => t.trim().toLowerCase()) : null;
|
||||
const duration = args.duration ? parseInt(args.duration) : 5000;
|
||||
|
||||
// Set up console message listener
|
||||
session.page.on('console', (msg) => {
|
||||
const messageType = msg.type().toLowerCase();
|
||||
|
||||
// Filter by message types if specified
|
||||
if (filterTypes && !filterTypes.includes(messageType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const consoleMessage: ConsoleMessage = {
|
||||
type: messageType,
|
||||
text: msg.text(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Add location info if available
|
||||
const location = msg.location();
|
||||
if (location) {
|
||||
consoleMessage.location = {
|
||||
url: location.url,
|
||||
lineNumber: location.lineNumber,
|
||||
columnNumber: location.columnNumber,
|
||||
};
|
||||
}
|
||||
|
||||
messages.push(consoleMessage);
|
||||
});
|
||||
|
||||
// Navigate to the URL if needed
|
||||
if (shouldNavigate && targetUrl) {
|
||||
await session.page.goto(targetUrl, { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
// Wait for the specified duration
|
||||
await new Promise(resolve => setTimeout(resolve, duration));
|
||||
|
||||
// Count messages by type
|
||||
const messageCount = messages.reduce((acc, msg) => {
|
||||
acc[msg.type] = (acc[msg.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: session.page.url(),
|
||||
title: await session.page.title(),
|
||||
monitoring: {
|
||||
duration: duration,
|
||||
types: filterTypes || 'all',
|
||||
messageCount: messages.length,
|
||||
messageCountByType: messageCount,
|
||||
usedExistingSession: !args.url && !shouldNavigate,
|
||||
autoNavigated: shouldNavigate && args.url,
|
||||
},
|
||||
messages: messages,
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
monitorConsole();
|
||||
68
skills/scripts/evaluate.ts
Normal file
68
skills/scripts/evaluate.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Execute JavaScript for debugging and analysis
|
||||
* Usage: node evaluate.js --url https://example.com --script "document.title"
|
||||
*/
|
||||
|
||||
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, executeScript } from '../lib/browser.js';
|
||||
|
||||
interface EvaluateArgs {
|
||||
url?: string;
|
||||
script?: string;
|
||||
timeout?: string;
|
||||
'wait-until'?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
}
|
||||
|
||||
async function evaluate() {
|
||||
const args = parseArgs(process.argv.slice(2)) as EvaluateArgs;
|
||||
|
||||
if (!args.script) {
|
||||
return outputError(new Error('--script is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== 'false',
|
||||
timeout: args.timeout ? parseInt(args.timeout) : undefined,
|
||||
});
|
||||
|
||||
// Navigate to URL if provided
|
||||
if (args.url) {
|
||||
await session.page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] as any || 'load',
|
||||
timeout: args.timeout ? parseInt(args.timeout) : 30000,
|
||||
});
|
||||
}
|
||||
|
||||
// Execute the script
|
||||
const result = await executeScript(session.page, args.script);
|
||||
|
||||
const responseData = {
|
||||
success: true,
|
||||
url: session.page.url(),
|
||||
title: await session.page.title(),
|
||||
script: args.script,
|
||||
result: result,
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
outputJSON(responseData);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
evaluate();
|
||||
104
skills/scripts/fill.ts
Normal file
104
skills/scripts/fill.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Form testing and input validation
|
||||
* Usage: node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true]
|
||||
*/
|
||||
|
||||
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, waitForElement, getElementInfo } from '../lib/browser.js';
|
||||
|
||||
interface FillArgs {
|
||||
selector?: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
clear?: string | boolean;
|
||||
'wait-timeout'?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
}
|
||||
|
||||
async function fill() {
|
||||
const args = parseArgs(process.argv.slice(2)) as FillArgs;
|
||||
|
||||
if (!args.selector) {
|
||||
return outputError(new Error('--selector is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.value === undefined) {
|
||||
return outputError(new Error('--value is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== 'false',
|
||||
});
|
||||
|
||||
// Navigate to URL if provided
|
||||
if (args.url) {
|
||||
await session.page.goto(args.url, { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
// Wait for element to be available
|
||||
const waitTimeout = args['wait-timeout'] ? parseInt(args['wait-timeout']) : 30000;
|
||||
await waitForElement(session.page, args.selector, waitTimeout);
|
||||
|
||||
// Get element info before filling
|
||||
const elementInfo = await getElementInfo(session.page, args.selector);
|
||||
|
||||
// Check if element is visible and fillable
|
||||
if (!elementInfo.visible) {
|
||||
return outputError(new Error(`Element is not visible: ${args.selector}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if element is an input-like element
|
||||
const fillableTags = ['input', 'textarea', 'select'];
|
||||
if (!fillableTags.includes(elementInfo.tagName)) {
|
||||
return outputError(new Error(`Element is not fillable: ${elementInfo.tagName} (${args.selector})`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the field if requested
|
||||
if (args.clear === 'true') {
|
||||
await session.page.fill(args.selector, '');
|
||||
}
|
||||
|
||||
// Fill the element with the value
|
||||
await session.page.fill(args.selector, args.value);
|
||||
|
||||
// Verify the value was set
|
||||
const actualValue = await session.page.inputValue(args.selector);
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: session.page.url(),
|
||||
title: await session.page.title(),
|
||||
action: {
|
||||
type: 'fill',
|
||||
selector: args.selector,
|
||||
elementInfo: elementInfo,
|
||||
value: args.value,
|
||||
actualValue: actualValue,
|
||||
cleared: args.clear === 'true',
|
||||
},
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
fill();
|
||||
155
skills/scripts/install.sh
Executable file
155
skills/scripts/install.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
# Installation script for browser-devtools skill
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Installing browser-devtools skill..."
|
||||
|
||||
# Check if Node.js is installed
|
||||
if ! command -v node &>/dev/null; then
|
||||
echo "❌ Node.js is not installed. Please install Node.js 18+ first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Node.js version
|
||||
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||
echo "❌ Node.js 18+ is required. Current version: $(node -v)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Node.js $(node -v) detected"
|
||||
|
||||
# Check if npm dependencies are already installed
|
||||
if [ ! -d "node_modules" ] || ! npm list playwright &>/dev/null; then
|
||||
echo "📦 Installing npm dependencies..."
|
||||
npm install
|
||||
else
|
||||
echo "✅ npm dependencies already installed"
|
||||
fi
|
||||
|
||||
# Check if build is needed
|
||||
BUILD_NEEDED=false
|
||||
if [ ! -d "dist" ]; then
|
||||
BUILD_NEEDED=true
|
||||
else
|
||||
# Check if any source files are newer than dist files
|
||||
for ts_file in scripts/*.ts lib/*.ts; do
|
||||
if [ -f "$ts_file" ]; then
|
||||
js_file="dist/${ts_file%.*}.js"
|
||||
if [ ! -f "$js_file" ] || [ "$ts_file" -nt "$js_file" ]; then
|
||||
BUILD_NEEDED=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$BUILD_NEEDED" = true ]; then
|
||||
echo "📦 Building TypeScript files..."
|
||||
npm run build
|
||||
else
|
||||
echo "✅ Build files are up to date"
|
||||
fi
|
||||
|
||||
# Check if Playwright browsers are installed
|
||||
BROWSER_CHECK=$(node -e "try { require('playwright').chromium.executablePath(); console.log('OK'); } catch(e) { console.log('MISSING'); }" 2>/dev/null || echo "MISSING")
|
||||
if [ "$BROWSER_CHECK" != "OK" ]; then
|
||||
echo "🌐 Installing Playwright browsers..."
|
||||
npx playwright install
|
||||
else
|
||||
echo "✅ Playwright browsers already installed"
|
||||
fi
|
||||
|
||||
# Install system dependencies on Linux
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
echo "🔧 Installing system dependencies for Linux..."
|
||||
|
||||
# Detect distribution
|
||||
if [ -f /etc/debian_version ]; then
|
||||
# Debian/Ubuntu
|
||||
echo "Detected Debian/Ubuntu-based system"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libnss3 \
|
||||
libnspr4 \
|
||||
libasound2t64 \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libcups2 \
|
||||
libdrm2 \
|
||||
libxkbcommon0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxrandr2 \
|
||||
libgbm1
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
# RHEL/Fedora/CentOS
|
||||
echo "Detected RedHat/Fedora-based system"
|
||||
if command -v dnf &>/dev/null; then
|
||||
sudo dnf install -y \
|
||||
nss \
|
||||
nspr \
|
||||
alsa-lib \
|
||||
atk \
|
||||
at-spi2-atk \
|
||||
cups-libs \
|
||||
libdrm \
|
||||
libxkbcommon \
|
||||
libXcomposite \
|
||||
libXdamage \
|
||||
libXfixes \
|
||||
libXrandr \
|
||||
mesa-libgbm
|
||||
else
|
||||
sudo yum install -y \
|
||||
nss \
|
||||
nspr \
|
||||
alsa-lib \
|
||||
atk \
|
||||
at-spi2-atk \
|
||||
cups-libs \
|
||||
libdrm \
|
||||
libxkbcommon \
|
||||
libXcomposite \
|
||||
libXdamage \
|
||||
libXfixes \
|
||||
libXrandr \
|
||||
mesa-libgbm
|
||||
fi
|
||||
elif [ -f /etc/arch-release ]; then
|
||||
# Arch Linux
|
||||
echo "Detected Arch-based system"
|
||||
sudo pacman -S --needed \
|
||||
nss \
|
||||
nspr \
|
||||
alsa-lib \
|
||||
atk \
|
||||
at-spi2-atk \
|
||||
cups \
|
||||
libdrm \
|
||||
libxkbcommon \
|
||||
libxcomposite \
|
||||
libxdamage \
|
||||
libxfixes \
|
||||
libxrandr \
|
||||
mesa
|
||||
else
|
||||
echo "⚠️ Unknown Linux distribution. Please install browser dependencies manually."
|
||||
echo "See: https://playwright.dev/docs/linux/"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p docs/screenshots
|
||||
mkdir -p docs/traces
|
||||
mkdir -p docs/reports
|
||||
|
||||
echo "✅ Installation completed successfully!"
|
||||
echo ""
|
||||
echo "🧪 Test the installation:"
|
||||
echo " node dist/scripts/navigate.js --url https://example.com"
|
||||
echo ""
|
||||
echo "📚 For usage examples, see SKILL.md"
|
||||
|
||||
64
skills/scripts/navigate.ts
Normal file
64
skills/scripts/navigate.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Navigate to a URL for debugging
|
||||
* Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
|
||||
*/
|
||||
|
||||
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError } from '../lib/browser.js';
|
||||
|
||||
interface NavigateArgs {
|
||||
url?: string;
|
||||
'wait-until'?: string;
|
||||
timeout?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
}
|
||||
|
||||
async function navigate() {
|
||||
const args = parseArgs(process.argv.slice(2)) as NavigateArgs;
|
||||
|
||||
if (!args.url) {
|
||||
return outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== 'false',
|
||||
timeout: args.timeout ? parseInt(args.timeout) : undefined,
|
||||
});
|
||||
|
||||
const waitUntil = args['wait-until'] as any || 'networkidle';
|
||||
|
||||
await session.page.goto(args.url, {
|
||||
waitUntil,
|
||||
timeout: args.timeout ? parseInt(args.timeout) : 30000,
|
||||
});
|
||||
|
||||
const title = await session.page.title();
|
||||
const finalUrl = session.page.url();
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: finalUrl,
|
||||
title: title,
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
navigate();
|
||||
254
skills/scripts/network.ts
Normal file
254
skills/scripts/network.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Network request debugging and performance analysis
|
||||
* Usage: node network.js --url https://example.com [--types xhr,fetch] [--duration 5000] [--output requests.json]
|
||||
*/
|
||||
|
||||
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError } from '../lib/browser.js';
|
||||
import { writeFile } from 'fs/promises';
|
||||
|
||||
interface NetworkArgs {
|
||||
url?: string;
|
||||
types?: string;
|
||||
duration?: string;
|
||||
output?: string;
|
||||
timeout?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
}
|
||||
|
||||
interface NetworkRequest {
|
||||
url: string;
|
||||
method: string;
|
||||
status: number;
|
||||
statusText: string;
|
||||
type: string;
|
||||
resourceType: string;
|
||||
size: {
|
||||
request: number;
|
||||
response: number;
|
||||
total: number;
|
||||
};
|
||||
timing: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
};
|
||||
headers: {
|
||||
request: Record<string, string>;
|
||||
response: Record<string, string>;
|
||||
};
|
||||
failed: boolean;
|
||||
errorText?: string;
|
||||
}
|
||||
|
||||
async function monitorNetwork() {
|
||||
const args = parseArgs(process.argv.slice(2)) as NetworkArgs;
|
||||
|
||||
if (!args.url) {
|
||||
return outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== 'false',
|
||||
timeout: args.timeout ? parseInt(args.timeout) : undefined,
|
||||
});
|
||||
|
||||
const requests: NetworkRequest[] = [];
|
||||
const filterTypes = args.types ? args.types.split(',').map(t => t.trim().toLowerCase()) : null;
|
||||
const duration = args.duration ? parseInt(args.duration) : 5000;
|
||||
|
||||
// Set up request and response listeners
|
||||
session.page.on('request', (request) => {
|
||||
const resourceType = request.resourceType().toLowerCase();
|
||||
|
||||
// Filter by resource types if specified
|
||||
if (filterTypes && !filterTypes.includes(resourceType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData: any = {
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
resourceType: resourceType,
|
||||
type: 'request',
|
||||
headers: request.headers(),
|
||||
size: {
|
||||
request: 0, // Playwright doesn't expose header size directly
|
||||
response: 0,
|
||||
total: 0,
|
||||
},
|
||||
timing: {
|
||||
startTime: Date.now(),
|
||||
endTime: 0,
|
||||
duration: 0,
|
||||
},
|
||||
status: 0,
|
||||
statusText: '',
|
||||
failed: false,
|
||||
};
|
||||
|
||||
// Store temporary request data
|
||||
(request as any)._requestData = requestData;
|
||||
});
|
||||
|
||||
session.page.on('response', async (response) => {
|
||||
const request = response.request() as any;
|
||||
const requestData = request._requestData;
|
||||
|
||||
if (!requestData) {
|
||||
return; // Skip if not tracked
|
||||
}
|
||||
|
||||
const resourceType = response.request().resourceType().toLowerCase();
|
||||
|
||||
// Filter by resource types if specified
|
||||
if (filterTypes && !filterTypes.includes(resourceType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const responseBody = await response.text();
|
||||
const responseSize = responseBody.length;
|
||||
|
||||
const networkRequest: NetworkRequest = {
|
||||
url: response.url(),
|
||||
method: request.method(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
type: 'response',
|
||||
resourceType: resourceType,
|
||||
size: {
|
||||
request: requestData.size.request,
|
||||
response: responseSize,
|
||||
total: requestData.size.request + responseSize,
|
||||
},
|
||||
timing: {
|
||||
startTime: requestData.timing.startTime,
|
||||
endTime: endTime,
|
||||
duration: endTime - requestData.timing.startTime,
|
||||
},
|
||||
headers: {
|
||||
request: requestData.headers,
|
||||
response: response.headers(),
|
||||
},
|
||||
failed: !response.ok(),
|
||||
errorText: !response.ok() ? response.statusText() : undefined,
|
||||
};
|
||||
|
||||
requests.push(networkRequest);
|
||||
});
|
||||
|
||||
session.page.on('requestfailed', (request) => {
|
||||
const requestData = (request as any)._requestData;
|
||||
|
||||
if (!requestData) {
|
||||
return; // Skip if not tracked
|
||||
}
|
||||
|
||||
const resourceType = request.resourceType().toLowerCase();
|
||||
|
||||
// Filter by resource types if specified
|
||||
if (filterTypes && !filterTypes.includes(resourceType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
const networkRequest: NetworkRequest = {
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
status: 0,
|
||||
statusText: 'Failed',
|
||||
type: 'failed',
|
||||
resourceType: resourceType,
|
||||
size: requestData.size,
|
||||
timing: {
|
||||
startTime: requestData.timing.startTime,
|
||||
endTime: endTime,
|
||||
duration: endTime - requestData.timing.startTime,
|
||||
},
|
||||
headers: {
|
||||
request: requestData.headers,
|
||||
response: {},
|
||||
},
|
||||
failed: true,
|
||||
errorText: (request as any).failure()?.errorText || 'Request failed',
|
||||
};
|
||||
|
||||
requests.push(networkRequest);
|
||||
});
|
||||
|
||||
// Navigate to the URL
|
||||
await session.page.goto(args.url, { waitUntil: 'networkidle' });
|
||||
|
||||
// Wait for the specified duration
|
||||
await new Promise(resolve => setTimeout(resolve, duration));
|
||||
|
||||
// Calculate summary statistics
|
||||
const summary = {
|
||||
totalRequests: requests.length,
|
||||
successfulRequests: requests.filter(r => !r.failed).length,
|
||||
failedRequests: requests.filter(r => r.failed).length,
|
||||
totalSize: requests.reduce((sum, r) => sum + r.size.total, 0),
|
||||
averageResponseTime: requests.length > 0
|
||||
? requests.reduce((sum, r) => sum + r.timing.duration, 0) / requests.length
|
||||
: 0,
|
||||
requestsByType: requests.reduce((acc, r) => {
|
||||
acc[r.resourceType] = (acc[r.resourceType] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
requestsByStatus: requests.reduce((acc, r) => {
|
||||
if (r.failed) {
|
||||
acc['failed'] = (acc['failed'] || 0) + 1;
|
||||
} else {
|
||||
const statusGroup = Math.floor(r.status / 100) * 100;
|
||||
acc[statusGroup] = (acc[statusGroup] || 0) + 1;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
};
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: session.page.url(),
|
||||
title: await session.page.title(),
|
||||
monitoring: {
|
||||
duration: duration,
|
||||
types: filterTypes || 'all',
|
||||
...summary,
|
||||
},
|
||||
requests: requests,
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Save to file if output path provided
|
||||
if (args.output) {
|
||||
await writeFile(args.output, JSON.stringify(result, null, 2));
|
||||
outputJSON({
|
||||
success: true,
|
||||
output: args.output,
|
||||
...summary,
|
||||
url: session.page.url()
|
||||
});
|
||||
} else {
|
||||
outputJSON(result);
|
||||
}
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
monitorNetwork();
|
||||
231
skills/scripts/performance.ts
Normal file
231
skills/scripts/performance.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Performance metrics and Core Web Vitals
|
||||
* Usage: node performance.js --url https://example.com [--trace trace.json] [--resources true]
|
||||
*/
|
||||
|
||||
import {
|
||||
getBrowserSession,
|
||||
closeBrowserSession,
|
||||
parseArgs,
|
||||
outputJSON,
|
||||
outputError,
|
||||
} from "../lib/browser.js";
|
||||
import { writeFile } from "fs/promises";
|
||||
|
||||
interface PerformanceArgs {
|
||||
url?: string;
|
||||
trace?: string;
|
||||
resources?: string | boolean;
|
||||
timeout?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
}
|
||||
|
||||
interface CoreWebVitals {
|
||||
LCP: number | null; // Largest Contentful Paint
|
||||
FID: number | null; // First Input Delay
|
||||
CLS: number; // Cumulative Layout Shift
|
||||
FCP: number | null; // First Contentful Paint
|
||||
TTFB: number | null; // Time to First Byte
|
||||
}
|
||||
|
||||
interface PerformanceResource {
|
||||
name: string;
|
||||
type: string;
|
||||
duration: number;
|
||||
size: number;
|
||||
startTime: number;
|
||||
responseEnd: number;
|
||||
}
|
||||
|
||||
async function measurePerformance() {
|
||||
const args = parseArgs(process.argv.slice(2)) as PerformanceArgs;
|
||||
|
||||
if (!args.url) {
|
||||
return outputError(new Error("--url is required"));
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== "false",
|
||||
timeout: args.timeout ? parseInt(args.timeout) : undefined,
|
||||
});
|
||||
|
||||
// Start tracing if requested
|
||||
if (args.trace) {
|
||||
await session.page.context().tracing.start({
|
||||
screenshots: true,
|
||||
snapshots: true,
|
||||
sources: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate to the URL
|
||||
await session.page.goto(args.url, { waitUntil: "networkidle" });
|
||||
|
||||
// Stop tracing and save if requested
|
||||
if (args.trace) {
|
||||
await session.page.context().tracing.stop({ path: args.trace });
|
||||
}
|
||||
|
||||
// Get performance metrics from Chrome DevTools Protocol
|
||||
const client = await session.page.context().newCDPSession(session.page);
|
||||
const metrics = await client.send("Performance.getMetrics");
|
||||
|
||||
// Get Core Web Vitals
|
||||
const vitals = await session.page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const vitals: CoreWebVitals = {
|
||||
LCP: null,
|
||||
FID: null,
|
||||
CLS: 0,
|
||||
FCP: null,
|
||||
TTFB: null,
|
||||
};
|
||||
|
||||
// LCP - Largest Contentful Paint
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
const lastEntry = entries[entries.length - 1] as any;
|
||||
vitals.LCP = lastEntry.renderTime || lastEntry.loadTime;
|
||||
}
|
||||
}).observe({
|
||||
entryTypes: ["largest-contentful-paint"],
|
||||
buffered: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// LCP not supported
|
||||
}
|
||||
|
||||
// CLS - Cumulative Layout Shift
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach((entry: any) => {
|
||||
if (!entry.hadRecentInput) {
|
||||
vitals.CLS += entry.value;
|
||||
}
|
||||
});
|
||||
}).observe({ entryTypes: ["layout-shift"], buffered: true });
|
||||
} catch (e) {
|
||||
// CLS not supported
|
||||
}
|
||||
|
||||
// FCP - First Contentful Paint
|
||||
try {
|
||||
const paintEntries = performance.getEntriesByType("paint");
|
||||
const fcpEntry = paintEntries.find(
|
||||
(e: any) => e.name === "first-contentful-paint",
|
||||
);
|
||||
if (fcpEntry) {
|
||||
vitals.FCP = fcpEntry.startTime;
|
||||
}
|
||||
} catch (e) {
|
||||
// FCP not supported
|
||||
}
|
||||
|
||||
// TTFB - Time to First Byte
|
||||
try {
|
||||
const [navigationEntry] = performance.getEntriesByType(
|
||||
"navigation",
|
||||
) as any[];
|
||||
if (navigationEntry) {
|
||||
vitals.TTFB =
|
||||
navigationEntry.responseStart - navigationEntry.requestStart;
|
||||
}
|
||||
} catch (e) {
|
||||
// Navigation timing not supported
|
||||
}
|
||||
|
||||
// Wait a bit for metrics to stabilize
|
||||
setTimeout(() => resolve(vitals), 1000);
|
||||
});
|
||||
});
|
||||
|
||||
// Get resource timing information
|
||||
let resources: PerformanceResource[] = [];
|
||||
if (args.resources === "true") {
|
||||
resources = await session.page.evaluate(() => {
|
||||
return performance.getEntriesByType("resource").map((r: any) => ({
|
||||
name: r.name,
|
||||
type: r.initiatorType,
|
||||
duration: r.duration,
|
||||
size: r.transferSize || 0,
|
||||
startTime: r.startTime,
|
||||
responseEnd: r.responseEnd,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
const resourceSummary = {
|
||||
count: resources.length,
|
||||
totalDuration: resources.reduce((sum, r) => sum + r.duration, 0),
|
||||
totalSize: resources.reduce((sum, r) => sum + r.size, 0),
|
||||
averageDuration:
|
||||
resources.length > 0
|
||||
? resources.reduce((sum, r) => sum + r.duration, 0) / resources.length
|
||||
: 0,
|
||||
resourcesByType: resources.reduce(
|
||||
(acc, r) => {
|
||||
acc[r.type] = (acc[r.type] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
),
|
||||
};
|
||||
|
||||
// Process metrics into a more usable format
|
||||
const processedMetrics: Record<string, any> = {};
|
||||
metrics.metrics?.forEach((metric: any) => {
|
||||
processedMetrics[metric.name] = metric.value;
|
||||
});
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: session.page.url(),
|
||||
title: await session.page.title(),
|
||||
metrics: {
|
||||
...processedMetrics,
|
||||
JSHeapUsedSizeMB: (
|
||||
(processedMetrics.JSHeapUsedSize || 0) /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2),
|
||||
JSHeapTotalSizeMB: (
|
||||
(processedMetrics.JSHeapTotalSize || 0) /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2),
|
||||
},
|
||||
vitals: vitals,
|
||||
resources:
|
||||
args.resources === "true"
|
||||
? {
|
||||
...resourceSummary,
|
||||
items: resources,
|
||||
}
|
||||
: resourceSummary,
|
||||
trace: args.trace || null,
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== "false") {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
measurePerformance();
|
||||
|
||||
91
skills/scripts/screenshot.ts
Normal file
91
skills/scripts/screenshot.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Capture screenshots for visual debugging
|
||||
* Usage: node screenshot.js --url https://example.com --output screenshot.png [--full-page true] [--selector .element]
|
||||
*/
|
||||
|
||||
import {
|
||||
getBrowserSession,
|
||||
closeBrowserSession,
|
||||
parseArgs,
|
||||
outputJSON,
|
||||
outputError,
|
||||
createScreenshot,
|
||||
getOutputDirectory,
|
||||
} from "../lib/browser.js";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
interface ScreenshotArgs {
|
||||
url?: string;
|
||||
output?: string;
|
||||
"full-page"?: string | boolean;
|
||||
selector?: string;
|
||||
format?: string;
|
||||
quality?: string;
|
||||
"wait-until"?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
}
|
||||
|
||||
async function takeScreenshot() {
|
||||
const args = parseArgs(process.argv.slice(2)) as ScreenshotArgs;
|
||||
|
||||
if (!args.url && !args.selector) {
|
||||
return outputError(new Error("Either --url or --selector is required"));
|
||||
}
|
||||
|
||||
// --output is optional - will auto-generate if not provided
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== "false",
|
||||
});
|
||||
|
||||
// Navigate to URL if provided
|
||||
if (args.url) {
|
||||
await session.page.goto(args.url, {
|
||||
waitUntil: (args["wait-until"] as any) || "load",
|
||||
});
|
||||
}
|
||||
|
||||
const outputPath =
|
||||
args.output || join(getOutputDirectory(), `screenshot_${Date.now()}.png`);
|
||||
const fullPage = args["full-page"] === "true";
|
||||
|
||||
// Create screenshot
|
||||
const result = await createScreenshot(session.page, outputPath, {
|
||||
fullPage,
|
||||
selector: args.selector,
|
||||
});
|
||||
|
||||
const screenshotData = {
|
||||
success: true,
|
||||
url: session.page.url(),
|
||||
title: await session.page.title(),
|
||||
outputPath: typeof result === "string" ? result : outputPath,
|
||||
fullPage,
|
||||
selector: args.selector || null,
|
||||
size: typeof result === "string" ? null : result.length,
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
outputJSON(screenshotData);
|
||||
|
||||
if (args.close !== "false") {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
takeScreenshot();
|
||||
172
skills/scripts/snapshot.ts
Normal file
172
skills/scripts/snapshot.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* DOM inspection and element discovery
|
||||
* Usage: node snapshot.js --url https://example.com [--output snapshot.json]
|
||||
*/
|
||||
|
||||
import { getBrowserSession, closeBrowserSession, parseArgs, outputJSON, outputError, getOutputDirectory } from '../lib/browser.js';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
interface SnapshotArgs {
|
||||
url?: string;
|
||||
output?: string;
|
||||
timeout?: string;
|
||||
headless?: string | boolean;
|
||||
close?: string | boolean;
|
||||
browser?: string;
|
||||
}
|
||||
|
||||
interface ElementInfo {
|
||||
index: number;
|
||||
tagName: string;
|
||||
id: string | null;
|
||||
className: string | null;
|
||||
name: string | null;
|
||||
value: string | null;
|
||||
type: string | null;
|
||||
text: string | null;
|
||||
href: string | null;
|
||||
selector: string;
|
||||
xpath: string;
|
||||
visible: boolean;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function takeSnapshot() {
|
||||
const args = parseArgs(process.argv.slice(2)) as SnapshotArgs;
|
||||
|
||||
try {
|
||||
const session = await getBrowserSession({
|
||||
browser: args.browser as any,
|
||||
headless: args.headless !== 'false',
|
||||
timeout: args.timeout ? parseInt(args.timeout) : undefined,
|
||||
});
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await session.page.goto(args.url, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: args.timeout ? parseInt(args.timeout) : 30000,
|
||||
});
|
||||
}
|
||||
|
||||
// Get interactive elements with metadata
|
||||
const elements = await session.page.evaluate(() => {
|
||||
const interactiveSelectors = [
|
||||
'a[href]',
|
||||
'button',
|
||||
'input',
|
||||
'textarea',
|
||||
'select',
|
||||
'[onclick]',
|
||||
'[role="button"]',
|
||||
'[role="link"]',
|
||||
'[contenteditable]',
|
||||
'[tabindex]'
|
||||
];
|
||||
|
||||
const elements: ElementInfo[] = [];
|
||||
const selector = interactiveSelectors.join(', ');
|
||||
const nodes = document.querySelectorAll(selector);
|
||||
|
||||
nodes.forEach((el, index) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const element = el as HTMLElement;
|
||||
|
||||
// Generate unique selector
|
||||
let uniqueSelector = '';
|
||||
if (element.id) {
|
||||
uniqueSelector = `#${element.id}`;
|
||||
} else if (element.className) {
|
||||
const classes = Array.from(element.classList).join('.');
|
||||
uniqueSelector = `${element.tagName.toLowerCase()}.${classes}`;
|
||||
} else {
|
||||
uniqueSelector = element.tagName.toLowerCase();
|
||||
}
|
||||
|
||||
elements.push({
|
||||
index: index,
|
||||
tagName: element.tagName.toLowerCase(),
|
||||
id: element.id || null,
|
||||
className: element.className || null,
|
||||
name: (element as any).name || null,
|
||||
value: (element as any).value || null,
|
||||
type: (element as any).type || null,
|
||||
text: element.textContent?.trim().substring(0, 100) || null,
|
||||
href: (element as HTMLAnchorElement).href || null,
|
||||
selector: uniqueSelector,
|
||||
xpath: getXPath(element),
|
||||
visible: rect.width > 0 && rect.height > 0,
|
||||
position: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getXPath(element: Element): string {
|
||||
if (element.id) {
|
||||
return `//*[@id="${element.id}"]`;
|
||||
}
|
||||
if (element === document.body) {
|
||||
return '/html/body';
|
||||
}
|
||||
let ix = 0;
|
||||
const parent = element.parentNode as Element;
|
||||
const siblings = parent?.children || [];
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
const sibling = siblings[i];
|
||||
if (sibling === element) {
|
||||
return getXPath(parent) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
|
||||
}
|
||||
if (sibling.tagName === element.tagName) {
|
||||
ix++;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return elements;
|
||||
});
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: session.page.url(),
|
||||
title: await session.page.title(),
|
||||
elementCount: elements.length,
|
||||
elements: elements,
|
||||
sessionId: session.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Save to file if output path provided, otherwise save to project directory
|
||||
const outputPath = args.output || join(getOutputDirectory(), `snapshot_${Date.now()}.json`);
|
||||
await writeFile(outputPath, JSON.stringify(result, null, 2));
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
output: outputPath,
|
||||
elementCount: elements.length,
|
||||
url: session.page.url()
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowserSession();
|
||||
} else {
|
||||
// Explicitly exit the process when keeping session open
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
return outputError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot();
|
||||
31
skills/tsconfig.json
Normal file
31
skills/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["ES2018", "DOM", "DOM.Iterable"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"scripts/**/*",
|
||||
"lib/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user