Initial commit
This commit is contained in:
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();
|
||||
Reference in New Issue
Block a user