Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "browser-devtools",
|
||||||
|
"description": "Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Truong Vu",
|
||||||
|
"email": "vukhanhtruong@gmail.com"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# browser-devtools
|
||||||
|
|
||||||
|
Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development.
|
||||||
117
plugin.lock.json
Normal file
117
plugin.lock.json
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:vukhanhtruong/claude-rock:plugins/browser-devtools",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "e51c3cedbeb21afc81ced385efb0aa6976fbe6c3",
|
||||||
|
"treeHash": "d6f5e26dd43ebcca96337e1538ddd9cc8f44110c8507e8a1c92364008d41a098",
|
||||||
|
"generatedAt": "2025-11-28T10:28:57.788668Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "browser-devtools",
|
||||||
|
"description": "Provides debugging web applications, monitoring network traffic, capturing screenshots, DOM inspection, performance analysis, and testing UI interactions during development.",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "38dfdbd3c10af36ebfabf1203c02270d2045aa34a7eee604137bec119b2f8028"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "76a52444895477ed9620f3df67409a932e116337aa2060661b3e3355fe241931"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/README.md",
|
||||||
|
"sha256": "e91b6a4167fa0f016c96601eb90fa165ea01a8cf13a24a0e276ca44abbd95d42"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/package.json",
|
||||||
|
"sha256": "83e99bfc5e0b167b06cfab769d9532a60c51dd10016211f82191d20e31e390db"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/SKILL.md",
|
||||||
|
"sha256": "28416849905ae07e830944ef6bdd831ab79d43068307497b79e52f9273ab63c6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/tsconfig.json",
|
||||||
|
"sha256": "25f0d1e202386f6e181165e1e5f1ca90a019d79a944d7f4f9931b4c6932a5a8c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/snapshot.ts",
|
||||||
|
"sha256": "9cccadf3bb954a526b0e670934c7532f1c53cdb49cffcbdcd424af9de300a7b7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/performance.ts",
|
||||||
|
"sha256": "dc9bb6a83df02602055e68ed1eea643c7d65ea676b8df023781f8ea1ff81e3d9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/screenshot.ts",
|
||||||
|
"sha256": "b9538022a209daf9d4dce77d4b94fa9a353316640a94f73d5c53eef66e25847d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/evaluate.ts",
|
||||||
|
"sha256": "35fa37032f7710ae4ba027d3cbaa33a19149339dcaa94924971657ee0906fb01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/network.ts",
|
||||||
|
"sha256": "3b4c6d4895b05f25b8c7b24c0d913e8dc0a56e09ab992fef2ad26e8a84399c79"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/install.sh",
|
||||||
|
"sha256": "d7e1a4602369fcf7e5bf1c567b5be584a6a75fcaf419049c0146ad68543082a0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/navigate.ts",
|
||||||
|
"sha256": "100da6c50337b7f78b59d61236d2d4f07a4700cf3076eb6f613b351a911e9601"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/fill.ts",
|
||||||
|
"sha256": "c941da84266b958d02a10017ea9b38cc3591e839400320d75623bfcefcb4dd3d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/console.ts",
|
||||||
|
"sha256": "7d7c259a15158278f39e70e6d68105ab659665523e063a2b4b4b1ec5963866da"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/scripts/click.ts",
|
||||||
|
"sha256": "f61e65228e6b2e09ee9b108d774260b9f97b890839bfe4eb698fcf29b29a65ee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/lib/browser.ts",
|
||||||
|
"sha256": "c67574013642161b989bd92c697c30328b4ea15ec83430fc55926ee674fbaaef"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/lib/browser.d.ts.map",
|
||||||
|
"sha256": "4ce628498e2ae037e302afd194f5bc03c74da42534e8603b76af80d643d89de4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/lib/browser.js.map",
|
||||||
|
"sha256": "2bb10742fba305c7e02db956a45375fb0388b61fd1c5e38a29955f33b043eadc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/lib/browser.js",
|
||||||
|
"sha256": "b8a28a98bd8ddf437dd82a9cc1ef1e4cc8abc0946e7753233fc7db7a618e8914"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/lib/browser.d.ts",
|
||||||
|
"sha256": "f6aa94f88755cb3ff4391e32d6830888cd1e668a9ca071b322bee19cdc964a26"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "d6f5e26dd43ebcca96337e1538ddd9cc8f44110c8507e8a1c92364008d41a098"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
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