Initial commit
This commit is contained in:
213
skills/chrome-devtools/scripts/README.md
Normal file
213
skills/chrome-devtools/scripts/README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Chrome DevTools Scripts
|
||||
|
||||
CLI scripts for browser automation using Puppeteer.
|
||||
|
||||
**CRITICAL**: Always check `pwd` before running scripts.
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Install
|
||||
|
||||
```bash
|
||||
pwd # Should show current working directory
|
||||
cd .claude/skills/chrome-devtools/scripts
|
||||
./install.sh # Auto-checks dependencies and installs
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
|
||||
**Linux/WSL** - Install system dependencies first:
|
||||
```bash
|
||||
./install-deps.sh # Auto-detects OS (Ubuntu, Debian, Fedora, etc.)
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```bash
|
||||
sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1
|
||||
```
|
||||
|
||||
**All platforms** - Install Node dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
**CRITICAL**: Always check `pwd` before running scripts.
|
||||
|
||||
### navigate.js
|
||||
Navigate to a URL.
|
||||
|
||||
```bash
|
||||
node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
|
||||
```
|
||||
|
||||
### screenshot.js
|
||||
Take a screenshot with automatic compression.
|
||||
|
||||
**Important**: Always save screenshots to `./docs/screenshots` directory.
|
||||
|
||||
```bash
|
||||
node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress]
|
||||
```
|
||||
|
||||
**Automatic Compression**: Screenshots >5MB are automatically compressed using ImageMagick to ensure compatibility with Gemini API and Claude Code. Install ImageMagick for this feature:
|
||||
- macOS: `brew install imagemagick`
|
||||
- Linux: `sudo apt-get install imagemagick`
|
||||
|
||||
Options:
|
||||
- `--max-size N` - Custom size threshold in MB (default: 5)
|
||||
- `--no-compress` - Disable automatic compression
|
||||
- `--format png|jpeg` - Output format (default: png)
|
||||
- `--quality N` - JPEG quality 0-100 (default: auto)
|
||||
|
||||
### click.js
|
||||
Click an element.
|
||||
|
||||
```bash
|
||||
node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
|
||||
```
|
||||
|
||||
### fill.js
|
||||
Fill form fields.
|
||||
|
||||
```bash
|
||||
node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true]
|
||||
```
|
||||
|
||||
### evaluate.js
|
||||
Execute JavaScript in page context.
|
||||
|
||||
```bash
|
||||
node evaluate.js --script "document.title" [--url https://example.com]
|
||||
```
|
||||
|
||||
### snapshot.js
|
||||
Get DOM snapshot with interactive elements.
|
||||
|
||||
```bash
|
||||
node snapshot.js [--url https://example.com] [--output snapshot.json]
|
||||
```
|
||||
|
||||
### console.js
|
||||
Monitor console messages.
|
||||
|
||||
```bash
|
||||
node console.js --url https://example.com [--types error,warn] [--duration 5000]
|
||||
```
|
||||
|
||||
### network.js
|
||||
Monitor network requests.
|
||||
|
||||
```bash
|
||||
node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]
|
||||
```
|
||||
|
||||
### performance.js
|
||||
Measure performance metrics and record trace.
|
||||
|
||||
```bash
|
||||
node performance.js --url https://example.com [--trace trace.json] [--metrics] [--resources true]
|
||||
```
|
||||
|
||||
## Common Options
|
||||
|
||||
- `--headless false` - Show browser window
|
||||
- `--close false` - Keep browser open
|
||||
- `--timeout 30000` - Set timeout in milliseconds
|
||||
- `--wait-until networkidle2` - Wait strategy (load, domcontentloaded, networkidle0, networkidle2)
|
||||
|
||||
## Selector Support
|
||||
|
||||
Scripts that accept `--selector` (click.js, fill.js, screenshot.js) support both **CSS** and **XPath** selectors.
|
||||
|
||||
### CSS Selectors (Default)
|
||||
|
||||
```bash
|
||||
# Element tag
|
||||
node click.js --selector "button" --url https://example.com
|
||||
|
||||
# Class selector
|
||||
node click.js --selector ".btn-submit" --url https://example.com
|
||||
|
||||
# ID selector
|
||||
node fill.js --selector "#email" --value "user@example.com" --url https://example.com
|
||||
|
||||
# Attribute selector
|
||||
node click.js --selector 'button[type="submit"]' --url https://example.com
|
||||
|
||||
# Complex selector
|
||||
node screenshot.js --selector "div.container > button.btn-primary" --output btn.png
|
||||
```
|
||||
|
||||
### XPath Selectors
|
||||
|
||||
XPath selectors start with `/` or `(//` and are automatically detected:
|
||||
|
||||
```bash
|
||||
# Text matching - exact
|
||||
node click.js --selector '//button[text()="Submit"]' --url https://example.com
|
||||
|
||||
# Text matching - contains
|
||||
node click.js --selector '//button[contains(text(),"Submit")]' --url https://example.com
|
||||
|
||||
# Attribute matching
|
||||
node fill.js --selector '//input[@type="email"]' --value "user@example.com"
|
||||
|
||||
# Multiple conditions
|
||||
node click.js --selector '//button[@type="submit" and contains(text(),"Save")]'
|
||||
|
||||
# Descendant selection
|
||||
node screenshot.js --selector '//div[@class="modal"]//button[@class="close"]' --output modal.png
|
||||
|
||||
# Nth element
|
||||
node click.js --selector '(//button)[2]' # Second button on page
|
||||
```
|
||||
|
||||
### Discovering Selectors
|
||||
|
||||
Use `snapshot.js` to discover correct selectors:
|
||||
|
||||
```bash
|
||||
# Get all interactive elements
|
||||
node snapshot.js --url https://example.com | jq '.elements[]'
|
||||
|
||||
# Find buttons
|
||||
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'
|
||||
|
||||
# Find inputs
|
||||
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="INPUT")'
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
XPath selectors are validated to prevent injection attacks. The following patterns are blocked:
|
||||
- `javascript:`
|
||||
- `<script`
|
||||
- `onerror=`, `onload=`, `onclick=`
|
||||
- `eval(`, `Function(`, `constructor(`
|
||||
|
||||
Selectors exceeding 1000 characters are rejected (DoS prevention).
|
||||
|
||||
## Output Format
|
||||
|
||||
All scripts output JSON to stdout:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"url": "https://example.com",
|
||||
"title": "Example Domain",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Errors are output to stderr:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message",
|
||||
"stack": "..."
|
||||
}
|
||||
```
|
||||
210
skills/chrome-devtools/scripts/__tests__/selector.test.js
Normal file
210
skills/chrome-devtools/scripts/__tests__/selector.test.js
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Tests for selector parsing library
|
||||
* Run with: node --test __tests__/selector.test.js
|
||||
*/
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { parseSelector } from '../lib/selector.js';
|
||||
|
||||
describe('parseSelector', () => {
|
||||
describe('CSS Selectors', () => {
|
||||
it('should detect simple CSS selectors', () => {
|
||||
const result = parseSelector('button');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
assert.strictEqual(result.selector, 'button');
|
||||
});
|
||||
|
||||
it('should detect class selectors', () => {
|
||||
const result = parseSelector('.btn-submit');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
assert.strictEqual(result.selector, '.btn-submit');
|
||||
});
|
||||
|
||||
it('should detect ID selectors', () => {
|
||||
const result = parseSelector('#email-input');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
assert.strictEqual(result.selector, '#email-input');
|
||||
});
|
||||
|
||||
it('should detect attribute selectors', () => {
|
||||
const result = parseSelector('button[type="submit"]');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
assert.strictEqual(result.selector, 'button[type="submit"]');
|
||||
});
|
||||
|
||||
it('should detect complex CSS selectors', () => {
|
||||
const result = parseSelector('div.container > button.btn-primary:hover');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
});
|
||||
});
|
||||
|
||||
describe('XPath Selectors', () => {
|
||||
it('should detect absolute XPath', () => {
|
||||
const result = parseSelector('/html/body/button');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
assert.strictEqual(result.selector, '/html/body/button');
|
||||
});
|
||||
|
||||
it('should detect relative XPath', () => {
|
||||
const result = parseSelector('//button');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
assert.strictEqual(result.selector, '//button');
|
||||
});
|
||||
|
||||
it('should detect XPath with text matching', () => {
|
||||
const result = parseSelector('//button[text()="Click Me"]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should detect XPath with contains', () => {
|
||||
const result = parseSelector('//button[contains(text(),"Submit")]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should detect XPath with attributes', () => {
|
||||
const result = parseSelector('//input[@type="email"]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should detect grouped XPath', () => {
|
||||
const result = parseSelector('(//button)[1]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security Validation', () => {
|
||||
it('should block javascript: injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//button[@onclick="javascript:alert(1)"]'),
|
||||
/XPath injection detected.*javascript:/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block <script tag injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[contains(text(),"<script>alert(1)</script>")]'),
|
||||
/XPath injection detected.*<script/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block onerror= injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//img[@onerror="alert(1)"]'),
|
||||
/XPath injection detected.*onerror=/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block onload= injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//body[@onload="malicious()"]'),
|
||||
/XPath injection detected.*onload=/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block onclick= injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//a[@onclick="steal()"]'),
|
||||
/XPath injection detected.*onclick=/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block eval( injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[eval("malicious")]'),
|
||||
/XPath injection detected.*eval\(/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block Function( injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[Function("return 1")()]'),
|
||||
/XPath injection detected.*Function\(/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block constructor( injection', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[constructor("alert(1)")()]'),
|
||||
/XPath injection detected.*constructor\(/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should be case-insensitive for security checks', () => {
|
||||
assert.throws(
|
||||
() => parseSelector('//div[@ONERROR="alert(1)"]'),
|
||||
/XPath injection detected/i
|
||||
);
|
||||
});
|
||||
|
||||
it('should block extremely long selectors (DoS prevention)', () => {
|
||||
const longSelector = '//' + 'a'.repeat(1001);
|
||||
assert.throws(
|
||||
() => parseSelector(longSelector),
|
||||
/XPath selector too long/i
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should throw on empty string', () => {
|
||||
assert.throws(
|
||||
() => parseSelector(''),
|
||||
/Selector must be a non-empty string/
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on null', () => {
|
||||
assert.throws(
|
||||
() => parseSelector(null),
|
||||
/Selector must be a non-empty string/
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on undefined', () => {
|
||||
assert.throws(
|
||||
() => parseSelector(undefined),
|
||||
/Selector must be a non-empty string/
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on non-string input', () => {
|
||||
assert.throws(
|
||||
() => parseSelector(123),
|
||||
/Selector must be a non-empty string/
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle selectors with special characters', () => {
|
||||
const result = parseSelector('button[data-test="submit-form"]');
|
||||
assert.strictEqual(result.type, 'css');
|
||||
});
|
||||
|
||||
it('should allow safe XPath with parentheses', () => {
|
||||
const result = parseSelector('//button[contains(text(),"Save")]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
// Should not throw
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-World Examples', () => {
|
||||
it('should handle common button selector', () => {
|
||||
const result = parseSelector('//button[contains(text(),"Submit")]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should handle complex form selector', () => {
|
||||
const result = parseSelector('//form[@id="login-form"]//input[@type="email"]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should handle descendant selector', () => {
|
||||
const result = parseSelector('//div[@class="modal"]//button[@class="close"]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
|
||||
it('should handle nth-child equivalent', () => {
|
||||
const result = parseSelector('(//li)[3]');
|
||||
assert.strictEqual(result.type, 'xpath');
|
||||
});
|
||||
});
|
||||
});
|
||||
79
skills/chrome-devtools/scripts/click.js
Normal file
79
skills/chrome-devtools/scripts/click.js
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Click an element
|
||||
* Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
|
||||
* Supports both CSS and XPath selectors:
|
||||
* - CSS: node click.js --selector "button.submit"
|
||||
* - XPath: node click.js --selector "//button[contains(text(),'Submit')]"
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import { parseSelector, waitForElement, clickElement, enhanceError } from './lib/selector.js';
|
||||
|
||||
async function click() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.selector) {
|
||||
outputError(new Error('--selector is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse and validate selector
|
||||
const parsed = parseSelector(args.selector);
|
||||
|
||||
// Wait for element based on selector type
|
||||
await waitForElement(page, parsed, {
|
||||
visible: true,
|
||||
timeout: parseInt(args.timeout || '5000')
|
||||
});
|
||||
|
||||
// Set up navigation promise BEFORE clicking (in case click triggers immediate navigation)
|
||||
const navigationPromise = page.waitForNavigation({
|
||||
waitUntil: 'load',
|
||||
timeout: 5000
|
||||
}).catch(() => null); // Catch timeout - navigation may not occur
|
||||
|
||||
// Click element
|
||||
await clickElement(page, parsed);
|
||||
|
||||
// Wait for optional selector after click
|
||||
if (args['wait-for']) {
|
||||
await page.waitForSelector(args['wait-for'], {
|
||||
timeout: parseInt(args.timeout || '5000')
|
||||
});
|
||||
} else {
|
||||
// Wait for navigation to complete (or timeout if no navigation)
|
||||
await navigationPromise;
|
||||
}
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
url: page.url(),
|
||||
title: await page.title()
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhance error message with troubleshooting tips
|
||||
const enhanced = enhanceError(error, args.selector);
|
||||
outputError(enhanced);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
click();
|
||||
75
skills/chrome-devtools/scripts/console.js
Normal file
75
skills/chrome-devtools/scripts/console.js
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Monitor console messages
|
||||
* Usage: node console.js --url https://example.com [--types error,warn] [--duration 5000]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
|
||||
async function monitorConsole() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.url) {
|
||||
outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
const messages = [];
|
||||
const filterTypes = args.types ? args.types.split(',') : null;
|
||||
|
||||
// Listen for console messages
|
||||
page.on('console', (msg) => {
|
||||
const type = msg.type();
|
||||
|
||||
if (!filterTypes || filterTypes.includes(type)) {
|
||||
messages.push({
|
||||
type: type,
|
||||
text: msg.text(),
|
||||
location: msg.location(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for page errors
|
||||
page.on('pageerror', (error) => {
|
||||
messages.push({
|
||||
type: 'pageerror',
|
||||
text: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
// Navigate
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
|
||||
// Wait for additional time if specified
|
||||
if (args.duration) {
|
||||
await new Promise(resolve => setTimeout(resolve, parseInt(args.duration)));
|
||||
}
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
url: page.url(),
|
||||
messageCount: messages.length,
|
||||
messages: messages
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
monitorConsole();
|
||||
49
skills/chrome-devtools/scripts/evaluate.js
Normal file
49
skills/chrome-devtools/scripts/evaluate.js
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Execute JavaScript in page context
|
||||
* Usage: node evaluate.js --script "document.title" [--url https://example.com]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
|
||||
async function evaluate() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.script) {
|
||||
outputError(new Error('--script is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await page.evaluate((script) => {
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(script);
|
||||
}, args.script);
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
result: result,
|
||||
url: page.url()
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
evaluate();
|
||||
72
skills/chrome-devtools/scripts/fill.js
Normal file
72
skills/chrome-devtools/scripts/fill.js
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fill form fields
|
||||
* Usage: node fill.js --selector "#input" --value "text" [--url https://example.com]
|
||||
* Supports both CSS and XPath selectors:
|
||||
* - CSS: node fill.js --selector "#email" --value "user@example.com"
|
||||
* - XPath: node fill.js --selector "//input[@type='email']" --value "user@example.com"
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import { parseSelector, waitForElement, typeIntoElement, enhanceError } from './lib/selector.js';
|
||||
|
||||
async function fill() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.selector) {
|
||||
outputError(new Error('--selector is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.value) {
|
||||
outputError(new Error('--value is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse and validate selector
|
||||
const parsed = parseSelector(args.selector);
|
||||
|
||||
// Wait for element based on selector type
|
||||
await waitForElement(page, parsed, {
|
||||
visible: true,
|
||||
timeout: parseInt(args.timeout || '5000')
|
||||
});
|
||||
|
||||
// Type into element
|
||||
await typeIntoElement(page, parsed, args.value, {
|
||||
clear: args.clear === 'true',
|
||||
delay: parseInt(args.delay || '0')
|
||||
});
|
||||
|
||||
outputJSON({
|
||||
success: true,
|
||||
selector: args.selector,
|
||||
value: args.value,
|
||||
url: page.url()
|
||||
});
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhance error message with troubleshooting tips
|
||||
const enhanced = enhanceError(error, args.selector);
|
||||
outputError(enhanced);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fill();
|
||||
181
skills/chrome-devtools/scripts/install-deps.sh
Normal file
181
skills/chrome-devtools/scripts/install-deps.sh
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/bin/bash
|
||||
# System dependencies installation script for Chrome DevTools Agent Skill
|
||||
# This script installs required system libraries for running Chrome/Chromium
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Installing system dependencies for Chrome/Chromium..."
|
||||
echo ""
|
||||
|
||||
# Detect OS
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
else
|
||||
echo "❌ Cannot detect OS. This script supports Debian/Ubuntu-based systems."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
SUDO="sudo"
|
||||
echo "⚠️ This script requires root privileges to install system packages."
|
||||
echo " You may be prompted for your password."
|
||||
echo ""
|
||||
else
|
||||
SUDO=""
|
||||
fi
|
||||
|
||||
# Install dependencies based on OS
|
||||
case $OS in
|
||||
ubuntu|debian|pop)
|
||||
echo "Detected: $PRETTY_NAME"
|
||||
echo "Installing dependencies with apt..."
|
||||
echo ""
|
||||
|
||||
$SUDO apt-get update
|
||||
|
||||
# Install Chrome dependencies
|
||||
$SUDO apt-get install -y \
|
||||
ca-certificates \
|
||||
fonts-liberation \
|
||||
libasound2t64 \
|
||||
libatk-bridge2.0-0 \
|
||||
libatk1.0-0 \
|
||||
libc6 \
|
||||
libcairo2 \
|
||||
libcups2 \
|
||||
libdbus-1-3 \
|
||||
libexpat1 \
|
||||
libfontconfig1 \
|
||||
libgbm1 \
|
||||
libgcc1 \
|
||||
libglib2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libnspr4 \
|
||||
libnss3 \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libstdc++6 \
|
||||
libx11-6 \
|
||||
libx11-xcb1 \
|
||||
libxcb1 \
|
||||
libxcomposite1 \
|
||||
libxcursor1 \
|
||||
libxdamage1 \
|
||||
libxext6 \
|
||||
libxfixes3 \
|
||||
libxi6 \
|
||||
libxrandr2 \
|
||||
libxrender1 \
|
||||
libxss1 \
|
||||
libxtst6 \
|
||||
lsb-release \
|
||||
wget \
|
||||
xdg-utils
|
||||
|
||||
echo ""
|
||||
echo "✅ System dependencies installed successfully!"
|
||||
;;
|
||||
|
||||
fedora|rhel|centos)
|
||||
echo "Detected: $PRETTY_NAME"
|
||||
echo "Installing dependencies with dnf/yum..."
|
||||
echo ""
|
||||
|
||||
# Try dnf first, fallback to yum
|
||||
if command -v dnf &> /dev/null; then
|
||||
PKG_MGR="dnf"
|
||||
else
|
||||
PKG_MGR="yum"
|
||||
fi
|
||||
|
||||
$SUDO $PKG_MGR install -y \
|
||||
alsa-lib \
|
||||
atk \
|
||||
at-spi2-atk \
|
||||
cairo \
|
||||
cups-libs \
|
||||
dbus-libs \
|
||||
expat \
|
||||
fontconfig \
|
||||
glib2 \
|
||||
gtk3 \
|
||||
libdrm \
|
||||
libgbm \
|
||||
libX11 \
|
||||
libxcb \
|
||||
libXcomposite \
|
||||
libXcursor \
|
||||
libXdamage \
|
||||
libXext \
|
||||
libXfixes \
|
||||
libXi \
|
||||
libxkbcommon \
|
||||
libXrandr \
|
||||
libXrender \
|
||||
libXScrnSaver \
|
||||
libXtst \
|
||||
mesa-libgbm \
|
||||
nspr \
|
||||
nss \
|
||||
pango
|
||||
|
||||
echo ""
|
||||
echo "✅ System dependencies installed successfully!"
|
||||
;;
|
||||
|
||||
arch|manjaro)
|
||||
echo "Detected: $PRETTY_NAME"
|
||||
echo "Installing dependencies with pacman..."
|
||||
echo ""
|
||||
|
||||
$SUDO pacman -Sy --noconfirm \
|
||||
alsa-lib \
|
||||
at-spi2-core \
|
||||
cairo \
|
||||
cups \
|
||||
dbus \
|
||||
expat \
|
||||
glib2 \
|
||||
gtk3 \
|
||||
libdrm \
|
||||
libx11 \
|
||||
libxcb \
|
||||
libxcomposite \
|
||||
libxcursor \
|
||||
libxdamage \
|
||||
libxext \
|
||||
libxfixes \
|
||||
libxi \
|
||||
libxkbcommon \
|
||||
libxrandr \
|
||||
libxrender \
|
||||
libxshmfence \
|
||||
libxss \
|
||||
libxtst \
|
||||
mesa \
|
||||
nspr \
|
||||
nss \
|
||||
pango
|
||||
|
||||
echo ""
|
||||
echo "✅ System dependencies installed successfully!"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "❌ Unsupported OS: $OS"
|
||||
echo " This script supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro"
|
||||
echo ""
|
||||
echo " Please install Chrome/Chromium dependencies manually for your OS."
|
||||
echo " See: https://pptr.dev/troubleshooting"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "📝 Next steps:"
|
||||
echo " 1. Run: cd $(dirname "$0")"
|
||||
echo " 2. Run: npm install"
|
||||
echo " 3. Test: node navigate.js --url https://example.com"
|
||||
echo ""
|
||||
83
skills/chrome-devtools/scripts/install.sh
Normal file
83
skills/chrome-devtools/scripts/install.sh
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
# Installation script for Chrome DevTools Agent Skill
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Installing Chrome DevTools Agent Skill..."
|
||||
echo ""
|
||||
|
||||
# Check Node.js version
|
||||
echo "Checking Node.js version..."
|
||||
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
|
||||
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||
echo "❌ Error: Node.js 18+ is required. Current version: $(node --version)"
|
||||
echo " Please upgrade Node.js: https://nodejs.org/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Node.js version: $(node --version)"
|
||||
echo ""
|
||||
|
||||
# Check for system dependencies (Linux only)
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
echo "Checking system dependencies (Linux)..."
|
||||
|
||||
# Check for critical Chrome dependencies
|
||||
MISSING_DEPS=()
|
||||
|
||||
if ! ldconfig -p | grep -q libnss3.so; then
|
||||
MISSING_DEPS+=("libnss3")
|
||||
fi
|
||||
|
||||
if ! ldconfig -p | grep -q libnspr4.so; then
|
||||
MISSING_DEPS+=("libnspr4")
|
||||
fi
|
||||
|
||||
if ! ldconfig -p | grep -q libgbm.so; then
|
||||
MISSING_DEPS+=("libgbm1")
|
||||
fi
|
||||
|
||||
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
||||
echo "⚠️ Missing system dependencies: ${MISSING_DEPS[*]}"
|
||||
echo ""
|
||||
echo " Chrome/Chromium requires system libraries to run."
|
||||
echo " Install them with:"
|
||||
echo ""
|
||||
echo " ./install-deps.sh"
|
||||
echo ""
|
||||
echo " Or manually:"
|
||||
echo " sudo apt-get install -y libnss3 libnspr4 libgbm1 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2"
|
||||
echo ""
|
||||
|
||||
read -p " Continue anyway? (y/N) " -n 1 -r
|
||||
echo ""
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Installation cancelled."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "✓ System dependencies found"
|
||||
fi
|
||||
echo ""
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo "Platform: macOS (no system dependencies needed)"
|
||||
echo ""
|
||||
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
echo "Platform: Windows (no system dependencies needed)"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Install Node.js dependencies
|
||||
echo "Installing Node.js dependencies..."
|
||||
npm install
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation complete!"
|
||||
echo ""
|
||||
echo "Test the installation:"
|
||||
echo " node navigate.js --url https://example.com"
|
||||
echo ""
|
||||
echo "For more information:"
|
||||
echo " cat README.md"
|
||||
echo ""
|
||||
46
skills/chrome-devtools/scripts/navigate.js
Normal file
46
skills/chrome-devtools/scripts/navigate.js
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Navigate to a URL
|
||||
* Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
|
||||
async function navigate() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.url) {
|
||||
outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
const options = {
|
||||
waitUntil: args['wait-until'] || 'networkidle2',
|
||||
timeout: parseInt(args.timeout || '30000')
|
||||
};
|
||||
|
||||
await page.goto(args.url, options);
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: page.url(),
|
||||
title: await page.title()
|
||||
};
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
navigate();
|
||||
102
skills/chrome-devtools/scripts/network.js
Normal file
102
skills/chrome-devtools/scripts/network.js
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Monitor network requests
|
||||
* Usage: node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
async function monitorNetwork() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.url) {
|
||||
outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
const requests = [];
|
||||
const filterTypes = args.types ? args.types.split(',').map(t => t.toLowerCase()) : null;
|
||||
|
||||
// Monitor requests
|
||||
page.on('request', (request) => {
|
||||
const resourceType = request.resourceType().toLowerCase();
|
||||
|
||||
if (!filterTypes || filterTypes.includes(resourceType)) {
|
||||
requests.push({
|
||||
id: request._requestId || requests.length,
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
resourceType: resourceType,
|
||||
headers: request.headers(),
|
||||
postData: request.postData(),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor responses
|
||||
const responses = new Map();
|
||||
page.on('response', async (response) => {
|
||||
const request = response.request();
|
||||
const resourceType = request.resourceType().toLowerCase();
|
||||
|
||||
if (!filterTypes || filterTypes.includes(resourceType)) {
|
||||
try {
|
||||
responses.set(request._requestId || request.url(), {
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
headers: response.headers(),
|
||||
fromCache: response.fromCache(),
|
||||
timing: response.timing()
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore errors for some response types
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
|
||||
// Merge requests with responses
|
||||
const combined = requests.map(req => ({
|
||||
...req,
|
||||
response: responses.get(req.id) || responses.get(req.url) || null
|
||||
}));
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: page.url(),
|
||||
requestCount: combined.length,
|
||||
requests: combined
|
||||
};
|
||||
|
||||
if (args.output) {
|
||||
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
|
||||
outputJSON({
|
||||
success: true,
|
||||
output: args.output,
|
||||
requestCount: combined.length
|
||||
});
|
||||
} else {
|
||||
outputJSON(result);
|
||||
}
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
monitorNetwork();
|
||||
15
skills/chrome-devtools/scripts/package.json
Normal file
15
skills/chrome-devtools/scripts/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "chrome-devtools-scripts",
|
||||
"version": "1.0.0",
|
||||
"description": "Browser automation scripts for Chrome DevTools Agent Skill",
|
||||
"type": "module",
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"puppeteer": "^24.15.0",
|
||||
"debug": "^4.4.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
145
skills/chrome-devtools/scripts/performance.js
Normal file
145
skills/chrome-devtools/scripts/performance.js
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Measure performance metrics and record trace
|
||||
* Usage: node performance.js --url https://example.com [--trace trace.json] [--metrics]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
async function measurePerformance() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.url) {
|
||||
outputError(new Error('--url is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Start tracing if requested
|
||||
if (args.trace) {
|
||||
await page.tracing.start({
|
||||
path: args.trace,
|
||||
categories: [
|
||||
'devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline.frame'
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate
|
||||
await page.goto(args.url, {
|
||||
waitUntil: 'networkidle2'
|
||||
});
|
||||
|
||||
// Stop tracing
|
||||
if (args.trace) {
|
||||
await page.tracing.stop();
|
||||
}
|
||||
|
||||
// Get performance metrics
|
||||
const metrics = await page.metrics();
|
||||
|
||||
// Get Core Web Vitals
|
||||
const vitals = await page.evaluate(() => {
|
||||
return new Promise((resolve) => {
|
||||
const vitals = {
|
||||
LCP: null,
|
||||
FID: null,
|
||||
CLS: 0,
|
||||
FCP: null,
|
||||
TTFB: null
|
||||
};
|
||||
|
||||
// LCP
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
vitals.LCP = lastEntry.renderTime || lastEntry.loadTime;
|
||||
}
|
||||
}).observe({ entryTypes: ['largest-contentful-paint'], buffered: true });
|
||||
} catch (e) {}
|
||||
|
||||
// CLS
|
||||
try {
|
||||
new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach((entry) => {
|
||||
if (!entry.hadRecentInput) {
|
||||
vitals.CLS += entry.value;
|
||||
}
|
||||
});
|
||||
}).observe({ entryTypes: ['layout-shift'], buffered: true });
|
||||
} catch (e) {}
|
||||
|
||||
// FCP
|
||||
try {
|
||||
const paintEntries = performance.getEntriesByType('paint');
|
||||
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
|
||||
if (fcpEntry) {
|
||||
vitals.FCP = fcpEntry.startTime;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// TTFB
|
||||
try {
|
||||
const [navigationEntry] = performance.getEntriesByType('navigation');
|
||||
if (navigationEntry) {
|
||||
vitals.TTFB = navigationEntry.responseStart - navigationEntry.requestStart;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Wait a bit for metrics to stabilize
|
||||
setTimeout(() => resolve(vitals), 1000);
|
||||
});
|
||||
});
|
||||
|
||||
// Get resource timing
|
||||
const resources = await page.evaluate(() => {
|
||||
return performance.getEntriesByType('resource').map(r => ({
|
||||
name: r.name,
|
||||
type: r.initiatorType,
|
||||
duration: r.duration,
|
||||
size: r.transferSize,
|
||||
startTime: r.startTime
|
||||
}));
|
||||
});
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: page.url(),
|
||||
metrics: {
|
||||
...metrics,
|
||||
JSHeapUsedSizeMB: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2),
|
||||
JSHeapTotalSizeMB: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2)
|
||||
},
|
||||
vitals: vitals,
|
||||
resources: {
|
||||
count: resources.length,
|
||||
totalDuration: resources.reduce((sum, r) => sum + r.duration, 0),
|
||||
items: args.resources === 'true' ? resources : undefined
|
||||
}
|
||||
};
|
||||
|
||||
if (args.trace) {
|
||||
result.trace = args.trace;
|
||||
}
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
measurePerformance();
|
||||
180
skills/chrome-devtools/scripts/screenshot.js
Normal file
180
skills/chrome-devtools/scripts/screenshot.js
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Take a screenshot
|
||||
* Usage: node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress]
|
||||
* Supports both CSS and XPath selectors:
|
||||
* - CSS: node screenshot.js --selector ".main-content" --output page.png
|
||||
* - XPath: node screenshot.js --selector "//div[@class='main-content']" --output page.png
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import { parseSelector, getElement, enhanceError } from './lib/selector.js';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* Compress image using ImageMagick if it exceeds max size
|
||||
* @param {string} filePath - Path to the image file
|
||||
* @param {number} maxSizeMB - Maximum file size in MB (default: 5)
|
||||
* @returns {Promise<{compressed: boolean, originalSize: number, finalSize: number}>}
|
||||
*/
|
||||
async function compressImageIfNeeded(filePath, maxSizeMB = 5) {
|
||||
const stats = await fs.stat(filePath);
|
||||
const originalSize = stats.size;
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
||||
|
||||
if (originalSize <= maxSizeBytes) {
|
||||
return { compressed: false, originalSize, finalSize: originalSize };
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if ImageMagick is available
|
||||
try {
|
||||
execSync('magick -version', { stdio: 'pipe' });
|
||||
} catch {
|
||||
try {
|
||||
execSync('convert -version', { stdio: 'pipe' });
|
||||
} catch {
|
||||
console.error('Warning: ImageMagick not found. Install it to enable automatic compression.');
|
||||
return { compressed: false, originalSize, finalSize: originalSize };
|
||||
}
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const tempPath = filePath.replace(ext, `.temp${ext}`);
|
||||
|
||||
// Determine compression strategy based on file type
|
||||
let compressionCmd;
|
||||
if (ext === '.png') {
|
||||
// For PNG: resize and compress with quality
|
||||
compressionCmd = `magick "${filePath}" -strip -resize 90% -quality 85 "${tempPath}"`;
|
||||
} else if (ext === '.jpg' || ext === '.jpeg') {
|
||||
// For JPEG: compress with quality and progressive
|
||||
compressionCmd = `magick "${filePath}" -strip -quality 80 -interlace Plane "${tempPath}"`;
|
||||
} else {
|
||||
// For other formats: convert to JPEG with compression
|
||||
compressionCmd = `magick "${filePath}" -strip -quality 80 "${tempPath.replace(ext, '.jpg')}"`;
|
||||
}
|
||||
|
||||
// Try compression
|
||||
execSync(compressionCmd, { stdio: 'pipe' });
|
||||
|
||||
const compressedStats = await fs.stat(tempPath);
|
||||
const compressedSize = compressedStats.size;
|
||||
|
||||
// If still too large, try more aggressive compression
|
||||
if (compressedSize > maxSizeBytes) {
|
||||
const finalPath = filePath.replace(ext, `.final${ext}`);
|
||||
let aggressiveCmd;
|
||||
|
||||
if (ext === '.png') {
|
||||
aggressiveCmd = `magick "${tempPath}" -strip -resize 75% -quality 70 "${finalPath}"`;
|
||||
} else {
|
||||
aggressiveCmd = `magick "${tempPath}" -strip -quality 60 -sampling-factor 4:2:0 "${finalPath}"`;
|
||||
}
|
||||
|
||||
execSync(aggressiveCmd, { stdio: 'pipe' });
|
||||
await fs.unlink(tempPath);
|
||||
await fs.rename(finalPath, filePath);
|
||||
} else {
|
||||
await fs.rename(tempPath, filePath);
|
||||
}
|
||||
|
||||
const finalStats = await fs.stat(filePath);
|
||||
return { compressed: true, originalSize, finalSize: finalStats.size };
|
||||
} catch (error) {
|
||||
console.error('Compression error:', error.message);
|
||||
// If compression fails, keep original file
|
||||
try {
|
||||
const tempPath = filePath.replace(path.extname(filePath), '.temp' + path.extname(filePath));
|
||||
await fs.unlink(tempPath).catch(() => {});
|
||||
} catch {}
|
||||
return { compressed: false, originalSize, finalSize: originalSize };
|
||||
}
|
||||
}
|
||||
|
||||
async function screenshot() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (!args.output) {
|
||||
outputError(new Error('--output is required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
const screenshotOptions = {
|
||||
path: args.output,
|
||||
type: args.format || 'png',
|
||||
fullPage: args['full-page'] === 'true'
|
||||
};
|
||||
|
||||
if (args.quality) {
|
||||
screenshotOptions.quality = parseInt(args.quality);
|
||||
}
|
||||
|
||||
let buffer;
|
||||
if (args.selector) {
|
||||
// Parse and validate selector
|
||||
const parsed = parseSelector(args.selector);
|
||||
|
||||
// Get element based on selector type
|
||||
const element = await getElement(page, parsed);
|
||||
if (!element) {
|
||||
throw new Error(`Element not found: ${args.selector}`);
|
||||
}
|
||||
buffer = await element.screenshot(screenshotOptions);
|
||||
} else {
|
||||
buffer = await page.screenshot(screenshotOptions);
|
||||
}
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
output: path.resolve(args.output),
|
||||
size: buffer.length,
|
||||
url: page.url()
|
||||
};
|
||||
|
||||
// Compress image if needed (unless --no-compress flag is set)
|
||||
if (args['no-compress'] !== 'true') {
|
||||
const maxSize = args['max-size'] ? parseFloat(args['max-size']) : 5;
|
||||
const compressionResult = await compressImageIfNeeded(args.output, maxSize);
|
||||
|
||||
if (compressionResult.compressed) {
|
||||
result.compressed = true;
|
||||
result.originalSize = compressionResult.originalSize;
|
||||
result.size = compressionResult.finalSize;
|
||||
result.compressionRatio = ((1 - compressionResult.finalSize / compressionResult.originalSize) * 100).toFixed(2) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
outputJSON(result);
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhance error message if selector-related
|
||||
if (args.selector) {
|
||||
const enhanced = enhanceError(error, args.selector);
|
||||
outputError(enhanced);
|
||||
} else {
|
||||
outputError(error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
screenshot();
|
||||
131
skills/chrome-devtools/scripts/snapshot.js
Normal file
131
skills/chrome-devtools/scripts/snapshot.js
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Get DOM snapshot with selectors
|
||||
* Usage: node snapshot.js [--url https://example.com] [--output snapshot.json]
|
||||
*/
|
||||
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
async function snapshot() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
try {
|
||||
const browser = await getBrowser({
|
||||
headless: args.headless !== 'false'
|
||||
});
|
||||
|
||||
const page = await getPage(browser);
|
||||
|
||||
// Navigate if URL provided
|
||||
if (args.url) {
|
||||
await page.goto(args.url, {
|
||||
waitUntil: args['wait-until'] || 'networkidle2'
|
||||
});
|
||||
}
|
||||
|
||||
// Get interactive elements with metadata
|
||||
const elements = await page.evaluate(() => {
|
||||
const interactiveSelectors = [
|
||||
'a[href]',
|
||||
'button',
|
||||
'input',
|
||||
'textarea',
|
||||
'select',
|
||||
'[onclick]',
|
||||
'[role="button"]',
|
||||
'[role="link"]',
|
||||
'[contenteditable]'
|
||||
];
|
||||
|
||||
const elements = [];
|
||||
const selector = interactiveSelectors.join(', ');
|
||||
const nodes = document.querySelectorAll(selector);
|
||||
|
||||
nodes.forEach((el, index) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
// Generate unique selector
|
||||
let uniqueSelector = '';
|
||||
if (el.id) {
|
||||
uniqueSelector = `#${el.id}`;
|
||||
} else if (el.className) {
|
||||
const classes = Array.from(el.classList).join('.');
|
||||
uniqueSelector = `${el.tagName.toLowerCase()}.${classes}`;
|
||||
} else {
|
||||
uniqueSelector = el.tagName.toLowerCase();
|
||||
}
|
||||
|
||||
elements.push({
|
||||
index: index,
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
type: el.type || null,
|
||||
id: el.id || null,
|
||||
className: el.className || null,
|
||||
name: el.name || null,
|
||||
value: el.value || null,
|
||||
text: el.textContent?.trim().substring(0, 100) || null,
|
||||
href: el.href || null,
|
||||
selector: uniqueSelector,
|
||||
xpath: getXPath(el),
|
||||
visible: rect.width > 0 && rect.height > 0,
|
||||
position: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getXPath(element) {
|
||||
if (element.id) {
|
||||
return `//*[@id="${element.id}"]`;
|
||||
}
|
||||
if (element === document.body) {
|
||||
return '/html/body';
|
||||
}
|
||||
let ix = 0;
|
||||
const siblings = element.parentNode?.childNodes || [];
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
const sibling = siblings[i];
|
||||
if (sibling === element) {
|
||||
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
|
||||
}
|
||||
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
|
||||
ix++;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return elements;
|
||||
});
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
url: page.url(),
|
||||
title: await page.title(),
|
||||
elementCount: elements.length,
|
||||
elements: elements
|
||||
};
|
||||
|
||||
if (args.output) {
|
||||
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
|
||||
outputJSON({
|
||||
success: true,
|
||||
output: args.output,
|
||||
elementCount: elements.length
|
||||
});
|
||||
} else {
|
||||
outputJSON(result);
|
||||
}
|
||||
|
||||
if (args.close !== 'false') {
|
||||
await closeBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
outputError(error);
|
||||
}
|
||||
}
|
||||
|
||||
snapshot();
|
||||
Reference in New Issue
Block a user