chore: init

This commit is contained in:
2025-11-04 18:39:56 +01:00
commit e69016e4e3
13 changed files with 1645 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules/
*.log
.DS_Store
.npmrc
pnpm-lock.yaml
package-lock.json

167
CLAUDE.md Normal file
View File

@@ -0,0 +1,167 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
WebShot is a CLI tool built with Node.js and Puppeteer for capturing screenshots, videos, and full HTML snapshots of websites. It features an interactive "shoot mode" with pastel-themed terminal UI, custom automation scripts, and support for multiple export formats (PNG, JPG, WEBP, PDF, MP4, HTML).
## Architecture
### Core Components
**Main executable** (`webshot`):
- Single-file CLI application with shebang (`#!/usr/bin/env node`)
- Self-contained with all functionality in ~800 lines
- Uses Puppeteer for browser automation
- Built-in debug mode with Node.js inspector support (restarts process with `--inspect`)
**Key architectural patterns**:
- **Spinner class** (webshot:79-114): Animated terminal progress indicator with rotating frames and pastel colors
- **Export functions** (webshot:202-470): Modular export handlers for each format (PNG, JPG, WEBP, PDF, MP4, HTML)
- **MP4 recording** (webshot:243-400): Fast screenshot-loop method that captures JPEG frames and uses ffmpeg to create video (supports both MP4 with H.264 and WebM with VP9)
- **HTML export** (webshot:403-470): Downloads complete page with all assets (images, CSS, JS) into `assets/` subdirectory
- **Interactive shoot mode** (webshot:149-200): Race between spacebar press and countdown timer
### Custom Script System
WebShot supports custom Puppeteer automation scripts via `-s/--script` flag. Scripts are evaluated with access to the `page` object and can manipulate the page before capture.
**Example scripts** (`examples/`):
- `scroll-animation.js` - Smoothly scrolls to bottom and back to top
- `dark-mode.js` - Attempts to enable dark mode using multiple detection methods
- `interact-form.js` - Fills out forms with demo data
- `hover-effects.js` - Simulates hover states on interactive elements
- `mobile-view.js` - Switches to mobile viewport (375x667, iPhone SE)
- `animated-sections.js` - Scrolls through sections to trigger scroll-based animations
Scripts are executed via `eval()` in an async context with access to `page`, `console`, and other webshot internals (webshot:722-727).
## Commands
### Basic Usage
```bash
# Install dependencies
pnpm install
# Make executable (if needed)
chmod +x webshot
# Run from anywhere (if in PATH)
webshot -u https://github.com --help
```
### Common Workflows
```bash
# Screenshot with interactive shoot mode
webshot -u https://example.com
# Screenshot without delay
webshot -u https://example.com --no-delay
# High quality WEBP
webshot -u https://example.com -f webp --quality 95
# Record 10 second video (MP4)
webshot -u https://bruno-simon.com -f mp4 --duration 10000 --fps 30
# Record as WebM instead
webshot -u https://bruno-simon.com -f mp4 -o output.webm --duration 10000
# Export as PDF
webshot -u https://github.com/features -f pdf
# Download complete HTML with assets
webshot -u https://stripe.com -f html -o stripe/index.html
# Use custom automation script
webshot -u https://example.com -s examples/dark-mode.js
# Custom viewport
webshot -u https://example.com -w 1280 -h 720
# Custom shoot delay (5 seconds)
webshot -u https://example.com 5000
```
### Debug Mode
```bash
# Enable debug logging and Node.js inspector
webshot -u https://example.com --debug
# Connect debugger on custom port
DEBUG_PORT=9230 webshot -u https://example.com --debug
# Debug with specific wait strategy (for slow sites)
webshot -u https://example.com --debug --wait-until load --timeout 60000
```
Debug mode:
- Prints detailed logs prefixed with `[DEBUG]` in magenta (webshot:36-40)
- Enables Puppeteer dumpio for browser console output (webshot:669)
- Listens for page console messages, errors, and failed requests (webshot:677-682)
- Restarts process with `--inspect` if not already in debug mode (webshot:7-19)
### Wait Strategies
The `--wait-until` flag controls when navigation is considered complete:
- `load` - Wait for window.load event (fastest, may miss dynamic content)
- `domcontentloaded` - Wait for DOM to be ready
- `networkidle2` (default) - Wait until 2 or fewer network connections remain for 500ms
- `networkidle0` - Wait until 0 network connections remain (slowest, most complete)
Use `--timeout <ms>` to increase navigation timeout for slow sites (default: 30000ms).
## Format-Specific Details
### MP4/WebM Video Recording
Video recording uses a fast screenshot-loop method:
1. Captures JPEG screenshots as fast as possible (quality: 60 for speed)
2. Saves frames to temporary directory (`.webshot-frames-<timestamp>/`)
3. Uses ffmpeg to convert frames to video
4. Automatically detects output format based on file extension (`.mp4` → H.264, `.webm` → VP9)
5. Duplicates frames to reach target FPS using ffmpeg `-r` flag
6. Cleans up temporary frames after conversion
Requires `ffmpeg` to be installed and available in PATH.
### HTML Export
HTML export downloads a complete snapshot:
1. Extracts all resource URLs (images, CSS, JS, favicon)
2. Downloads each resource using Puppeteer's page.goto()
3. Saves resources to `assets/` subdirectory relative to output file
4. Rewrites HTML to use local asset paths
5. Returns to original page after downloading
## Package Structure
- **Package manager**: pnpm (uses pnpm-workspace)
- **Main entry**: `webshot` (executable script, also set as bin in package.json)
- **Only dependency**: `puppeteer` (uses bundled Chromium)
- **Keywords**: pupeteer (sic), cli, record, screeencast (sic), shoot
## Color Palette
Terminal UI uses pastel ANSI colors defined in `colors` object (webshot:43-63):
- Pastels: pink (217), lavender (183), mint (158), peach (223), sky (153), coral (210), sage (151)
- Standard: green, yellow, red, cyan, gray
- Modifiers: bright, dim, reset
ASCII banner uses mint → sky → lavender gradient (webshot:66-76).
## Important Notes
- **Single file architecture**: All code lives in the `webshot` executable - no separate modules
- **No README**: Project lacks a README.md file, all documentation is in `--help` output
- **No tests**: No test directory or test files present
- **Script evaluation security**: Custom scripts are evaluated with `eval()` and have full access to Node.js APIs - only use trusted scripts
- **ffmpeg dependency**: MP4/WebM recording requires ffmpeg to be installed separately
- **Viewport defaults**: 1920x1080 at 2x device scale factor (webshot:684-688)
- **Interactive mode**: Uses raw terminal input with keypress events (webshot:152-154)
- **File overwrite**: Always prompts before overwriting existing files (webshot:129-146)
- **Fonts ready**: Waits for document.fonts.ready before capturing (webshot:716)

351
README.md Normal file
View File

@@ -0,0 +1,351 @@
# 📸 WebShot
> Shoot your favorite Websites!!!
A beautiful CLI tool for capturing screenshots, videos, and complete HTML snapshots of websites using Puppeteer. Features an interactive "shoot mode" with a pastel-themed terminal UI.
```
╦ ╦╔═╗╔╗ ╔═╗╦ ╦╔═╗╔╦╗
║║║║╣ ╠╩╗╚═╗╠═╣║ ║ ║
╚╩╝╚═╝╚═╝╚═╝╩ ╩╚═╝ ╩
Shoot your favorite Websites!!!
────────────────────────────────
```
## ✨ Features
- 📷 **Multiple export formats**: PNG, JPG, WEBP, PDF, MP4/WebM video, HTML with assets
- 🎯 **Interactive shoot mode**: Press spacebar when ready or wait for countdown
- 🎨 **Beautiful terminal UI**: Pastel-colored spinners and progress indicators
- 📜 **Custom automation scripts**: Execute Puppeteer code before capture
- 🐛 **Debug mode**: Built-in Node.js inspector support
-**Fast video recording**: Efficient screenshot-loop method with ffmpeg
- 🌐 **Complete HTML snapshots**: Download websites with all assets
- 📱 **Flexible viewports**: Custom dimensions or mobile presets
## 🚀 Installation
```bash
# Clone or download the repository
git clone <your-repo-url>
cd webshot
# Install dependencies
pnpm install
# Make executable (if needed)
chmod +x webshot
# Optional: Link globally to use from anywhere
npm link
# or add to your PATH
```
### Requirements
- **Node.js** (v14 or higher recommended)
- **ffmpeg** (required for MP4/WebM video recording)
```bash
# Debian/Ubuntu
sudo apt install ffmpeg
# macOS
brew install ffmpeg
```
## 📖 Usage
### Basic Commands
```bash
# Interactive screenshot with shoot mode
webshot -u https://github.com
# Quick screenshot without delay
webshot -u https://example.com --no-delay
# Specify output path
webshot -u https://example.com -o screenshots/github.png
```
### Export Formats
#### PNG (default)
```bash
webshot -u https://example.com -f png
```
#### JPEG with quality control
```bash
webshot -u https://example.com -f jpg --quality 95
```
#### WEBP for smaller file sizes
```bash
webshot -u https://example.com -f webp --quality 90
```
#### PDF documents
```bash
webshot -u https://github.com/features -f pdf
```
#### MP4 video recording
```bash
# 10 second video at 30 FPS
webshot -u https://bruno-simon.com -f mp4 --duration 10000 --fps 30
# WebM format (VP9 codec)
webshot -u https://bruno-simon.com -f mp4 -o output.webm --duration 5000
```
#### Complete HTML with assets
```bash
# Downloads all images, CSS, JS to assets/ subdirectory
webshot -u https://stripe.com -f html -o stripe/index.html
```
### Custom Automation Scripts
Execute custom Puppeteer code before capturing:
```bash
# Use a built-in example
webshot -u https://example.com -s examples/dark-mode.js
# Chain multiple behaviors
webshot -u https://example.com -s examples/scroll-animation.js -f mp4 --duration 8000
```
**Available example scripts:**
- `scroll-animation.js` - Scroll to bottom and back to top
- `dark-mode.js` - Attempt to enable dark mode
- `interact-form.js` - Fill out forms with demo data
- `hover-effects.js` - Simulate hover states
- `mobile-view.js` - Switch to mobile viewport
- `animated-sections.js` - Trigger scroll-based animations
**Create your own script:**
```javascript
// my-script.js
console.log('🎬 Running custom automation...');
// You have access to the 'page' object
await page.evaluate(() => {
document.body.style.backgroundColor = '#ff69b4';
});
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('✓ Done!');
```
### Viewport Configuration
```bash
# Custom dimensions
webshot -u https://example.com -w 1280 -h 720
# Mobile viewport (use with script)
webshot -u https://example.com -s examples/mobile-view.js
```
### Custom Shoot Delay
```bash
# 5 second delay
webshot -u https://example.com 5000
# 10 second delay
webshot -u https://example.com 10000
```
### Debug Mode
```bash
# Enable debug logging and Node.js inspector
webshot -u https://example.com --debug
# Connect to chrome://inspect in Chrome DevTools
# or use: node inspect localhost:9229
# Custom debug port
DEBUG_PORT=9230 webshot -u https://example.com --debug
# Debug slow or complex websites
webshot -u https://heavy-site.com --debug --wait-until load --timeout 60000
```
## 🎯 Wait Strategies
Control when navigation is considered complete:
```bash
# Wait for window.load event (fastest)
webshot -u https://example.com --wait-until load
# Wait for DOM ready
webshot -u https://example.com --wait-until domcontentloaded
# Wait for network to be mostly idle (default, best for most sites)
webshot -u https://example.com --wait-until networkidle2
# Wait for complete network silence (slowest, most complete)
webshot -u https://example.com --wait-until networkidle0
# Increase timeout for slow sites
webshot -u https://slow-site.com --timeout 60000
```
## 📋 Command Reference
### Options
| Option | Description | Default |
|--------|-------------|---------|
| `-u, --url <url>` | Target URL to capture | *required* |
| `-f, --format <format>` | Export format: `png`, `jpg`, `webp`, `pdf`, `mp4`, `html` | `png` |
| `-o, --output <path>` | Output file path | `<hostname>-<timestamp>.<format>` |
| `-s, --script <path>` | Custom Puppeteer automation script | - |
| `-w, --width <px>` | Viewport width | `1920` |
| `-h, --height <px>` | Viewport height | `1080` |
| `--no-delay` | Skip interactive shoot delay | `false` |
| `--full-page` | Capture full page (scrollable content) | `true` |
| `--quality <1-100>` | Image quality for JPG/WEBP | `90` |
| `--duration <ms>` | Video duration for MP4 | `5000` |
| `--fps <fps>` | Video FPS for MP4 | `30` |
| `--wait-until <state>` | Page load state: `load`, `domcontentloaded`, `networkidle0`, `networkidle2` | `networkidle2` |
| `--timeout <ms>` | Navigation timeout | `30000` |
| `--debug` | Enable debug mode with Node.js inspector | `false` |
| `--help` | Show help message | - |
### Arguments
| Argument | Description | Default |
|----------|-------------|---------|
| `[delay]` | Shoot delay in milliseconds | `3000` |
## 🌐 Cool Websites to Try
```bash
# 3D interactive portfolio
webshot -u https://bruno-simon.com -f mp4 --duration 10000
# WebGL demos and examples
webshot -u https://threejs.org/examples -f webp
# Beautiful modern design
webshot -u https://stripe.com -f pdf
# Modern UI with animations
webshot -u https://linear.app -f png
```
## 🎨 Terminal UI
WebShot features a beautiful pastel-themed terminal interface:
- **Animated spinners** with rotating frames (◐ ◓ ◑ ◒)
- **Progress bars** for video rendering
- **Color-coded output** using pastel colors (mint, sky, lavender, peach, coral)
- **Interactive countdown** with spacebar to shoot
- **File overwrite prompts** to prevent accidents
## 🔧 How It Works
### Screenshot Capture
1. Launches headless Chromium via Puppeteer
2. Sets viewport and navigates to URL
3. Waits for fonts and network activity
4. Executes custom script (if provided)
5. Shows interactive shoot mode countdown
6. Captures screenshot using Puppeteer's `page.screenshot()`
### Video Recording
1. Creates temporary frames directory
2. Captures JPEG screenshots in a fast loop
3. Writes frames to disk with sequential naming
4. Uses ffmpeg to convert frames to video:
- **MP4**: H.264 codec with `-preset fast -crf 23`
- **WebM**: VP9 codec with `-crf 30`
5. Duplicates frames to reach target FPS
6. Cleans up temporary frames
### HTML Export
1. Extracts all resource URLs (images, CSS, JS, favicon)
2. Downloads each resource using Puppeteer
3. Saves to `assets/` subdirectory
4. Rewrites HTML to use local paths
5. Returns to original page
## 📦 Package Details
- **Name**: `@valknarness/webshot`
- **Version**: 1.0.0
- **Main**: Single-file executable (`webshot`)
- **Dependencies**: `puppeteer@^24.26.0`
- **License**: MIT
- **Author**: valknar@pivoine.art
## 🤝 Contributing
This is a personal project, but suggestions and improvements are welcome!
## 📄 License
MIT License - feel free to use and modify as needed.
## ⚠️ Security Notes
- Custom scripts are executed with `eval()` and have full access to Node.js APIs
- Only use trusted scripts from reliable sources
- Be cautious when capturing websites with sensitive information
- Video recording creates temporary files that are cleaned up automatically
## 🐛 Troubleshooting
### ffmpeg not found
```bash
# Install ffmpeg
sudo apt install ffmpeg # Debian/Ubuntu
brew install ffmpeg # macOS
```
### Puppeteer installation issues
```bash
# Clear cache and reinstall
rm -rf node_modules pnpm-lock.yaml
pnpm install
```
### Timeout errors on slow sites
```bash
# Increase timeout and use faster wait strategy
webshot -u https://slow-site.com --timeout 60000 --wait-until load
```
### Video recording too slow
```bash
# Reduce FPS or duration
webshot -u https://example.com -f mp4 --fps 24 --duration 3000
```
### Debug complex issues
```bash
# Enable debug mode to see detailed logs
webshot -u https://example.com --debug
```
## 💡 Tips
- Use `--no-delay` in scripts and automation
- WebP format offers the best quality-to-size ratio for screenshots
- For video recording, 30 FPS is smooth for most content
- Use `networkidle2` (default) for most sites, `load` for fast captures
- Mobile viewport scripts work best with responsive websites
- HTML export is great for offline archiving or analysis
- Debug mode shows all network requests and page console output
---
**Made with 💜 by valknar**

92
examples/README.md Normal file
View File

@@ -0,0 +1,92 @@
# WebShot Example Scripts
Collection of example puppeteer automation scripts for WebShot.
## 📜 Available Scripts
### `scroll-animation.js`
Smoothly scrolls through the entire page before capturing. Great for long-form content and revealing lazy-loaded images.
**Usage:**
```bash
webshot -u https://example.com -s ~/webshot-examples/scroll-animation.js
```
### `dark-mode.js`
Attempts to enable dark mode on the page using various common methods. Perfect for capturing dark theme versions of websites.
**Usage:**
```bash
webshot -u https://github.com -s ~/webshot-examples/dark-mode.js
```
### `interact-form.js`
Fills out and interacts with forms on the page. Useful for capturing form states with data.
**Usage:**
```bash
webshot -u https://example.com/contact -s ~/webshot-examples/interact-form.js
```
### `hover-effects.js`
Simulates hovering over interactive elements to capture hover states.
**Usage:**
```bash
webshot -u https://example.com -s ~/webshot-examples/hover-effects.js
```
### `mobile-view.js`
Sets viewport to mobile dimensions (iPhone SE). Capture mobile-responsive designs.
**Usage:**
```bash
webshot -u https://example.com -s ~/webshot-examples/mobile-view.js
```
### `animated-sections.js`
Scrolls through page sections to trigger scroll-based animations. Perfect for modern websites with scroll-triggered effects.
**Usage:**
```bash
webshot -u https://bruno-simon.com -s ~/webshot-examples/animated-sections.js
```
## 🎯 Cool Websites to Try
- **https://bruno-simon.com** - 3D portfolio with scroll interactions
- **https://threejs.org/examples** - WebGL animations
- **https://stripe.com** - Beautiful modern design
- **https://linear.app** - Sleek UI with animations
- **https://github.com** - Try with dark-mode.js
## ✨ Creating Your Own Scripts
Scripts have access to the `page` object from Puppeteer. Here's a template:
```javascript
// Your custom automation script
console.log('🚀 Starting custom automation...');
// Do something with the page
await page.evaluate(() => {
// Manipulate DOM
document.body.style.background = '#000';
});
// Wait for animations
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('✓ Automation complete');
```
## 📚 Puppeteer API Reference
- [Page Methods](https://pptr.dev/api/puppeteer.page)
- [Browser Context](https://pptr.dev/api/puppeteer.browsercontext)
- [Element Handles](https://pptr.dev/api/puppeteer.elementhandle)
---
**WebShot** - Shoot your favorite Websites!!!

View File

@@ -0,0 +1,33 @@
// Animated Sections Example
// Scrolls through page to trigger scroll-based animations
console.log('✨ Triggering scroll animations...');
// Get all sections
const sections = await page.evaluate(() => {
const elements = document.querySelectorAll('section, .section, [class*="section"]');
return Array.from(elements).map(el => el.offsetTop);
});
console.log(`Found ${sections.length} sections to animate`);
// Scroll to each section
for (let i = 0; i < sections.length; i++) {
await page.evaluate((top) => {
window.scrollTo({ top, behavior: 'smooth' });
}, sections[i]);
console.log(`✓ Scrolled to section ${i + 1}/${sections.length}`);
// Wait for animations to trigger
await new Promise(resolve => setTimeout(resolve, 800));
}
// Scroll back to top
await page.evaluate(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('✓ All animations triggered');

48
examples/dark-mode.js Normal file
View File

@@ -0,0 +1,48 @@
// Dark Mode Toggle Example
// Attempts to enable dark mode on the page
console.log('🌙 Attempting to enable dark mode...');
// Try common dark mode implementations
await page.evaluate(() => {
// Method 1: Toggle dark class on html
if (document.documentElement.classList.contains('light')) {
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
} else if (!document.documentElement.classList.contains('dark')) {
document.documentElement.classList.add('dark');
}
// Method 2: Toggle dark class on body
if (document.body.classList.contains('light')) {
document.body.classList.remove('light');
document.body.classList.add('dark');
} else if (!document.body.classList.contains('dark')) {
document.body.classList.add('dark');
}
// Method 3: Set data-theme attribute
document.documentElement.setAttribute('data-theme', 'dark');
// Method 4: Try to click dark mode toggle button
const darkModeSelectors = [
'[aria-label*="dark" i]',
'[title*="dark" i]',
'button[class*="dark" i]',
'button[class*="theme" i]',
'[data-theme-toggle]'
];
for (const selector of darkModeSelectors) {
const element = document.querySelector(selector);
if (element && !element.classList.contains('active')) {
element.click();
break;
}
}
});
console.log('✓ Dark mode enabled');
// Wait for transitions
await new Promise(resolve => setTimeout(resolve, 500));

24
examples/hover-effects.js Normal file
View File

@@ -0,0 +1,24 @@
// Hover Effects Example
// Simulates hovering over interactive elements
console.log('👆 Simulating hover effects...');
// Hover over buttons and links
await page.evaluate(() => {
const hoverTargets = document.querySelectorAll('button, a, .card, [class*="hover"]');
hoverTargets.forEach((element, index) => {
if (index < 5) { // Hover first 5 elements
element.dispatchEvent(new MouseEvent('mouseenter', {
view: window,
bubbles: true,
cancelable: true
}));
}
});
});
console.log('✓ Hover effects activated');
// Let effects settle
await new Promise(resolve => setTimeout(resolve, 500));

41
examples/interact-form.js Normal file
View File

@@ -0,0 +1,41 @@
// Form Interaction Example
// Fills out and interacts with forms on the page
console.log('📝 Interacting with forms...');
// Find and fill input fields
await page.evaluate(() => {
// Fill text inputs
const textInputs = document.querySelectorAll('input[type="text"], input[type="email"]');
textInputs.forEach((input, index) => {
if (input.placeholder.toLowerCase().includes('email')) {
input.value = 'demo@example.com';
} else if (input.placeholder.toLowerCase().includes('name')) {
input.value = 'John Doe';
} else {
input.value = `Demo Text ${index + 1}`;
}
input.dispatchEvent(new Event('input', { bubbles: true }));
});
// Select first option in dropdowns
const selects = document.querySelectorAll('select');
selects.forEach(select => {
if (select.options.length > 1) {
select.selectedIndex = 1;
select.dispatchEvent(new Event('change', { bubbles: true }));
}
});
// Check checkboxes
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
});
});
console.log('✓ Forms filled');
// Wait for any animations
await new Promise(resolve => setTimeout(resolve, 500));

19
examples/mobile-view.js Normal file
View File

@@ -0,0 +1,19 @@
// Mobile View Example
// Sets viewport to mobile dimensions
console.log('📱 Switching to mobile view...');
await page.setViewport({
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true
});
console.log('✓ Mobile viewport set (iPhone SE)');
// Reload page for mobile view
await page.reload({ waitUntil: 'networkidle0' });
console.log('✓ Page reloaded in mobile view');

View File

@@ -0,0 +1,37 @@
// Scroll Animation Example
// Smoothly scrolls through the entire page before capturing
console.log('🎬 Starting scroll animation...');
// Scroll to bottom smoothly
await page.evaluate(async () => {
await new Promise((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
console.log('✓ Scrolled to bottom');
// Wait a bit at bottom
await new Promise(resolve => setTimeout(resolve, 1000));
// Scroll back to top
await page.evaluate(() => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
console.log('✓ Scrolled back to top');
// Wait for scroll to finish
await new Promise(resolve => setTimeout(resolve, 1000));

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "@valknarness/webshot",
"version": "1.0.0",
"description": "Shoot your favorite Websites",
"main": "webshot",
"bin": {
"webshot": "./webshot"
},
"keywords": [
"pupeteer",
"cli",
"record",
"screeencast",
"shoot"
],
"author": "valknar@pivoine.art",
"license": "MIT",
"dependencies": {
"puppeteer": "^24.26.0"
}
}

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- puppeteer

804
webshot Executable file
View File

@@ -0,0 +1,804 @@
#!/usr/bin/env node
// Check for debug mode before anything else
const DEBUG = process.argv.includes('--debug');
const DEBUG_PORT = process.env.DEBUG_PORT || 9229;
if (DEBUG && !process.execArgv.includes('--inspect')) {
// Restart with inspector
const { spawn } = require('child_process');
const args = ['--inspect=' + DEBUG_PORT, __filename, ...process.argv.slice(2).filter(arg => arg !== '--debug')];
console.log(`\x1b[36m🐛 Starting debugger on port ${DEBUG_PORT}\x1b[0m`);
console.log(`\x1b[90mChrome DevTools: chrome://inspect\x1b[0m`);
console.log(`\x1b[90mOr connect with: node inspect localhost:${DEBUG_PORT}\x1b[0m\n`);
const child = spawn(process.execPath, args, { stdio: 'inherit' });
child.on('exit', (code) => process.exit(code));
return;
}
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { spawn } = require('child_process');
// Try to load puppeteer from the specified path
let puppeteer;
try {
puppeteer = require('puppeteer');
} catch (err) {
console.error('❌ Puppeteer not found. Please install it first.');
process.exit(1);
}
// Debug logging
function debug(...args) {
if (DEBUG) {
console.log(`\x1b[35m[DEBUG]\x1b[0m`, ...args);
}
}
// ANSI colors - pastel palette
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
// Pastel colors
pink: '\x1b[38;5;217m',
lavender: '\x1b[38;5;183m',
mint: '\x1b[38;5;158m',
peach: '\x1b[38;5;223m',
sky: '\x1b[38;5;153m',
coral: '\x1b[38;5;210m',
sage: '\x1b[38;5;151m',
// Standard colors
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
gray: '\x1b[90m'
};
// ASCII Banner
function showBanner() {
const banner = `
${colors.mint}╦ ╦╔═╗╔╗ ╔═╗╦ ╦╔═╗╔╦╗${colors.reset}
${colors.sky}║║║║╣ ╠╩╗╚═╗╠═╣║ ║ ║ ${colors.reset}
${colors.lavender}╚╩╝╚═╝╚═╝╚═╝╩ ╩╚═╝ ╩ ${colors.reset}
${colors.peach}${colors.bright}Shoot your favorite Websites!!!${colors.reset}
${colors.gray}────────────────────────────────${colors.reset}
`;
console.log(banner);
}
// Progress spinner
class Spinner {
constructor(text) {
this.frames = ['◐', '◓', '◑', '◒'];
this.colorFrames = [colors.mint, colors.sky, colors.lavender, colors.peach];
this.text = text;
this.frame = 0;
this.timer = null;
}
start() {
this.timer = setInterval(() => {
const color = this.colorFrames[this.frame % this.colorFrames.length];
const frame = this.frames[this.frame % this.frames.length];
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(`${color}${frame}${colors.reset} ${this.text}`);
this.frame++;
}, 100);
}
update(text) {
this.text = text;
}
stop(finalText) {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
if (finalText) {
console.log(finalText);
}
}
}
// Progress bar
function progressBar(current, total, width = 30) {
const percentage = Math.round((current / total) * 100);
const filled = Math.round((width * current) / total);
const empty = width - filled;
const filledBar = '█'.repeat(filled);
const emptyBar = '░'.repeat(empty);
return `${colors.mint}${filledBar}${colors.gray}${emptyBar}${colors.reset} ${colors.bright}${percentage}%${colors.reset}`;
}
// Confirm overwrite
async function confirmOverwrite(filepath) {
if (!fs.existsSync(filepath)) return true;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
rl.question(
`${colors.yellow}⚠${colors.reset} File ${colors.cyan}${path.basename(filepath)}${colors.reset} already exists. Overwrite? ${colors.gray}(y/N)${colors.reset} `,
(answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
}
);
});
}
// Wait for spacebar
async function waitForSpacebar() {
return new Promise((resolve) => {
readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
const onKeypress = (str, key) => {
if (key && key.name === 'space') {
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.removeListener('keypress', onKeypress);
resolve();
} else if (key && key.ctrl && key.name === 'c') {
process.exit();
}
};
process.stdin.on('keypress', onKeypress);
});
}
// Countdown timer
async function showCountdown(delay) {
const startTime = Date.now();
const endTime = startTime + delay;
return new Promise((resolve) => {
const interval = setInterval(() => {
const now = Date.now();
const remaining = endTime - now;
const elapsed = now - startTime;
if (remaining <= 0) {
clearInterval(interval);
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
console.log(`${colors.green}✓${colors.reset} Ready to shoot!`);
resolve();
} else {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
process.stdout.write(
`${colors.mint}⏱${colors.reset} Elapsed: ${colors.bright}${elapsed}ms${colors.reset} | ` +
`Remaining: ${colors.bright}${remaining}ms${colors.reset} | ` +
`${colors.peach}Press ${colors.bright}SPACE${colors.reset}${colors.peach} to shoot!${colors.reset}`
);
}
}, 10);
});
}
// Export as PNG
async function exportPNG(page, outputPath, options = {}) {
await page.screenshot({
path: outputPath,
fullPage: options.fullPage !== false
});
}
// Export as JPG
async function exportJPG(page, outputPath, options = {}) {
await page.screenshot({
path: outputPath,
fullPage: options.fullPage !== false,
type: 'jpeg',
quality: options.quality || 90,
...options
});
}
// Export as WEBP
async function exportWEBP(page, outputPath, options = {}) {
await page.screenshot({
path: outputPath,
fullPage: options.fullPage !== false,
type: 'webp',
quality: options.quality || 90,
...options
});
}
// Export as PDF
async function exportPDF(page, outputPath, options = {}) {
await page.pdf({
path: outputPath,
format: options.format || 'A4',
printBackground: true,
...options
});
}
// Export as MP4 (screen recording)
async function exportMP4(page, outputPath, options = {}) {
const duration = options.duration || 5000;
const targetFPS = options.fps || 30;
debug('Starting MP4 recording using fast screenshot-loop...');
debug('Duration:', duration, 'ms');
debug('Target FPS:', targetFPS);
// Create temporary frames directory
const tempDir = path.join(path.dirname(outputPath), `.webshot-frames-${Date.now()}`);
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
debug('Temp frames dir:', tempDir);
const frameInterval = 1000 / targetFPS;
const targetFrames = Math.ceil((duration / 1000) * targetFPS);
debug('Frame interval:', frameInterval.toFixed(2), 'ms');
debug('Target frames:', targetFrames);
let frameCount = 0;
const startTime = Date.now();
let lastUpdateTime = startTime;
const spinner = new Spinner(`Recording video (${duration}ms, 0 frames)...`);
spinner.start();
try {
// Capture frames as fast as possible
while (Date.now() - startTime < duration) {
const captureStart = Date.now();
// Capture screenshot (JPEG for speed, lower quality)
const screenshot = await page.screenshot({
type: 'jpeg',
quality: 60, // Lower quality = faster captures
encoding: 'binary'
});
// Write frame to disk
frameCount++;
const framePath = path.join(tempDir, `frame-${String(frameCount).padStart(4, '0')}.jpg`);
fs.writeFileSync(framePath, screenshot);
const captureTime = Date.now() - captureStart;
debug(`Frame ${frameCount}: capture took ${captureTime}ms`);
// Update spinner every 500ms
if (Date.now() - lastUpdateTime > 500) {
spinner.update(`Recording video (${duration}ms, ${frameCount}/${targetFrames} frames)...`);
lastUpdateTime = Date.now();
}
// Try to maintain target frame rate (but don't slow down if already too slow)
const sleepTime = Math.max(0, frameInterval - captureTime);
if (sleepTime > 0) {
await new Promise(resolve => setTimeout(resolve, sleepTime));
}
}
spinner.stop();
const totalTime = Date.now() - startTime;
const actualFPS = (frameCount / (totalTime / 1000)).toFixed(2);
debug(`Captured ${frameCount}/${targetFrames} frames in ${totalTime}ms`);
debug(`Actual capture FPS: ${actualFPS}`);
// Detect output format
const isWebM = outputPath.toLowerCase().endsWith('.webm');
const formatName = isWebM ? 'WebM' : 'MP4';
console.log(`${colors.gray}Converting ${frameCount} frames to ${formatName}...${colors.reset}`);
const conversionSpinner = new Spinner(`Converting to ${formatName}...`);
conversionSpinner.start();
// Build ffmpeg arguments based on output format
const ffmpegArgs = [
'-y',
'-framerate', actualFPS,
'-i', path.join(tempDir, 'frame-%04d.jpg'),
'-r', targetFPS, // Duplicate frames to reach target FPS (fast)
];
if (isWebM) {
// WebM with VP9 codec
ffmpegArgs.push(
'-c:v', 'libvpx-vp9',
'-crf', '30',
'-b:v', '0'
);
} else {
// MP4 with H.264 codec
ffmpegArgs.push(
'-c:v', 'libx264',
'-preset', 'fast',
'-crf', '23',
'-pix_fmt', 'yuv420p',
'-movflags', '+faststart'
);
}
ffmpegArgs.push(outputPath);
debug('ffmpeg args:', ffmpegArgs.join(' '));
await new Promise((resolve, reject) => {
const ffmpeg = spawn('ffmpeg', ffmpegArgs, {
stdio: ['ignore', 'pipe', 'pipe']
});
let stderr = '';
ffmpeg.stderr.on('data', (data) => {
stderr += data.toString();
debug('ffmpeg stderr:', data.toString().trim());
});
ffmpeg.on('close', (code) => {
debug('ffmpeg exit code:', code);
if (code === 0) {
// Clean up temp frames directory
debug('Cleaning up temp frames:', tempDir);
try {
fs.rmSync(tempDir, { recursive: true });
} catch (e) {
debug('Cleanup error:', e);
}
resolve();
} else {
reject(new Error(`ffmpeg failed with exit code ${code}\nStderr: ${stderr.slice(-500)}`));
}
});
});
conversionSpinner.stop();
debug('Conversion complete');
} catch (error) {
debug('Recording error:', error);
spinner.stop();
// Clean up temp frames directory if it exists
if (fs.existsSync(tempDir)) {
debug('Cleaning up temp frames after error:', tempDir);
try {
fs.rmSync(tempDir, { recursive: true });
} catch (e) {
debug('Cleanup error:', e);
}
}
throw error;
}
}
// Export as HTML with assets
async function exportHTML(page, outputPath, options = {}) {
const url = page.url();
const baseDir = path.dirname(outputPath);
const assetsDir = path.join(baseDir, 'assets');
if (!fs.existsSync(assetsDir)) {
fs.mkdirSync(assetsDir, { recursive: true });
}
// Get all resources
const html = await page.content();
// Download all assets
const resources = await page.evaluate(() => {
const resources = [];
// Images
document.querySelectorAll('img[src]').forEach(img => {
resources.push({ type: 'image', url: img.src, selector: `img[src="${img.src}"]` });
});
// Stylesheets
document.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
resources.push({ type: 'css', url: link.href, selector: `link[href="${link.href}"]` });
});
// Scripts
document.querySelectorAll('script[src]').forEach(script => {
resources.push({ type: 'js', url: script.src, selector: `script[src="${script.src}"]` });
});
// Favicon
const favicon = document.querySelector('link[rel*="icon"]');
if (favicon) {
resources.push({ type: 'icon', url: favicon.href, selector: `link[href="${favicon.href}"]` });
}
return resources;
});
let processedHtml = html;
// Download resources
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
try {
const response = await page.goto(resource.url, { waitUntil: 'networkidle0', timeout: 10000 });
const buffer = await response.buffer();
const filename = path.basename(new URL(resource.url).pathname) || `asset-${i}`;
const filepath = path.join(assetsDir, filename);
fs.writeFileSync(filepath, buffer);
// Update HTML references
processedHtml = processedHtml.replace(resource.url, `assets/${filename}`);
} catch (err) {
console.log(`${colors.yellow}⚠${colors.reset} Failed to download: ${resource.url}`);
}
}
// Save HTML
fs.writeFileSync(outputPath, processedHtml);
// Go back to original page
await page.goto(url, { waitUntil: 'networkidle0' });
}
// Show help
function showHelp() {
showBanner();
console.log(`
${colors.bright}USAGE${colors.reset}
webshot -u <url> [options] [delay]
${colors.bright}OPTIONS${colors.reset}
${colors.mint}-u, --url <url>${colors.reset} Target URL to capture
${colors.mint}-f, --format <format>${colors.reset} Export format: png, jpg, webp, pdf, mp4, html
${colors.gray}(default: png)${colors.reset}
${colors.mint}-o, --output <path>${colors.reset} Output file path
${colors.mint}-s, --script <path>${colors.reset} Custom puppeteer automation script
${colors.mint}-w, --width <px>${colors.reset} Viewport width ${colors.gray}(default: 1920)${colors.reset}
${colors.mint}-h, --height <px>${colors.reset} Viewport height ${colors.gray}(default: 1080)${colors.reset}
${colors.mint}--no-delay${colors.reset} Skip interactive shoot delay
${colors.mint}--full-page${colors.reset} Capture full page ${colors.gray}(default: true)${colors.reset}
${colors.mint}--quality <1-100>${colors.reset} Image quality for jpg/webp ${colors.gray}(default: 90)${colors.reset}
${colors.mint}--duration <ms>${colors.reset} Video duration for mp4 ${colors.gray}(default: 5000)${colors.reset}
${colors.mint}--fps <fps>${colors.reset} Video FPS for mp4 ${colors.gray}(default: 30)${colors.reset}
${colors.mint}--wait-until <state>${colors.reset} Page load state: load, domcontentloaded, networkidle0, networkidle2
${colors.gray}(default: networkidle2)${colors.reset}
${colors.mint}--timeout <ms>${colors.reset} Navigation timeout ${colors.gray}(default: 30000)${colors.reset}
${colors.mint}--debug${colors.reset} Enable debug mode with Node.js inspector
${colors.mint}--help${colors.reset} Show this help message
${colors.bright}ARGUMENTS${colors.reset}
${colors.mint}[delay]${colors.reset} Shoot delay in milliseconds ${colors.gray}(default: 3000)${colors.reset}
${colors.bright}EXAMPLES${colors.reset}
${colors.gray}# Capture GitHub homepage with interactive delay${colors.reset}
webshot -u https://github.com
${colors.gray}# Export as PDF without delay${colors.reset}
webshot -u https://github.com/features -f pdf --no-delay
${colors.gray}# Record 10 second video of a website${colors.reset}
webshot -u https://bruno-simon.com -f mp4 --duration 10000
${colors.gray}# Download complete HTML with all assets${colors.reset}
webshot -u https://stripe.com -f html
${colors.gray}# High quality WEBP with custom delay${colors.reset}
webshot -u https://threejs.org/examples -f webp --quality 95 5000
${colors.gray}# Use custom puppeteer script${colors.reset}
webshot -u https://example.com -s ~/webshot-examples/scroll-animation.js
${colors.gray}# Debug mode for complex websites${colors.reset}
webshot -u https://bruno-simon.com -f mp4 --duration 10000 --wait-until load --debug
${colors.gray}# Increased timeout for slow sites${colors.reset}
webshot -u https://example.com --timeout 60000 --wait-until domcontentloaded
${colors.bright}COOL WEBSITES TO TRY${colors.reset}
${colors.sky}https://bruno-simon.com${colors.reset} ${colors.gray}# 3D portfolio${colors.reset}
${colors.lavender}https://threejs.org/examples${colors.reset} ${colors.gray}# WebGL demos${colors.reset}
${colors.peach}https://stripe.com${colors.reset} ${colors.gray}# Beautiful design${colors.reset}
${colors.mint}https://linear.app${colors.reset} ${colors.gray}# Modern UI${colors.reset}
${colors.coral}https://github.com${colors.reset} ${colors.gray}# GitHub${colors.reset}
${colors.bright}SCRIPT EXAMPLES${colors.reset}
See example scripts in: ${colors.cyan}~/webshot-examples/${colors.reset}
${colors.gray}────────────────────────────────────────────────────────────${colors.reset}
`);
}
// Parse arguments
function parseArgs(args) {
const options = {
url: null,
format: 'png',
output: null,
script: null,
width: 1920,
height: 1080,
delay: 3000,
noDelay: false,
fullPage: true,
quality: 90,
duration: 5000,
fps: 30,
waitUntil: 'networkidle2',
timeout: 30000,
debug: DEBUG
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--help') {
showHelp();
process.exit(0);
} else if (arg === '-u' || arg === '--url') {
options.url = args[++i];
} else if (arg === '-f' || arg === '--format') {
options.format = args[++i];
} else if (arg === '-o' || arg === '--output') {
options.output = args[++i];
} else if (arg === '-s' || arg === '--script') {
options.script = args[++i];
} else if (arg === '-w' || arg === '--width') {
options.width = parseInt(args[++i]);
} else if (arg === '-h' || arg === '--height') {
options.height = parseInt(args[++i]);
} else if (arg === '--no-delay') {
options.noDelay = true;
} else if (arg === '--full-page') {
options.fullPage = true;
} else if (arg === '--quality') {
options.quality = parseInt(args[++i]);
} else if (arg === '--duration') {
options.duration = parseInt(args[++i]);
} else if (arg === '--fps') {
options.fps = parseInt(args[++i]);
} else if (arg === '--wait-until') {
options.waitUntil = args[++i];
} else if (arg === '--timeout') {
options.timeout = parseInt(args[++i]);
} else if (arg === '--debug') {
options.debug = true;
} else if (!arg.startsWith('-') && !isNaN(parseInt(arg))) {
options.delay = parseInt(arg);
}
}
return options;
}
// Main function
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
showHelp();
return;
}
const options = parseArgs(args);
if (!options.url) {
console.error(`${colors.red}✗${colors.reset} URL is required. Use ${colors.cyan}-u <url>${colors.reset}`);
console.log(`Run ${colors.cyan}webshot --help${colors.reset} for more information.`);
process.exit(1);
}
showBanner();
// Generate output filename if not provided
if (!options.output) {
const urlObj = new URL(options.url);
const hostname = urlObj.hostname.replace(/\./g, '-');
const timestamp = Date.now();
options.output = `${hostname}-${timestamp}.${options.format}`;
}
// Resolve output path (handle ~ expansion)
if (options.output.startsWith('~/')) {
options.output = path.join(process.env.HOME, options.output.slice(2));
}
// Create output directory if it doesn't exist
const outputDir = path.dirname(options.output);
if (!fs.existsSync(outputDir)) {
debug('Creating output directory:', outputDir);
fs.mkdirSync(outputDir, { recursive: true });
}
// Check if output file exists
const shouldProceed = await confirmOverwrite(options.output);
if (!shouldProceed) {
console.log(`${colors.yellow}⚠${colors.reset} Operation cancelled.`);
return;
}
console.log(`${colors.mint}🌐${colors.reset} Target: ${colors.bright}${options.url}${colors.reset}`);
console.log(`${colors.sky}📁${colors.reset} Output: ${colors.bright}${options.output}${colors.reset}`);
console.log(`${colors.lavender}📊${colors.reset} Format: ${colors.bright}${options.format.toUpperCase()}${colors.reset}`);
if (options.debug) {
console.log(`${colors.coral}🐛${colors.reset} Debug: ${colors.bright}ENABLED${colors.reset}`);
console.log(`${colors.gray} Wait Until: ${options.waitUntil}${colors.reset}`);
console.log(`${colors.gray} Timeout: ${options.timeout}ms${colors.reset}`);
}
console.log();
debug('Options:', options);
// Launch browser
const spinner = new Spinner('Launching browser...');
spinner.start();
debug('Launching puppeteer...');
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
dumpio: options.debug
});
debug('Browser launched');
const page = await browser.newPage();
// Enable verbose logging in debug mode
if (options.debug) {
page.on('console', msg => debug('PAGE LOG:', msg.text()));
page.on('pageerror', error => debug('PAGE ERROR:', error.message));
page.on('request', request => debug('REQUEST:', request.url()));
page.on('requestfailed', request => debug('REQUEST FAILED:', request.url(), request.failure().errorText));
}
await page.setViewport({
width: options.width,
height: options.height,
deviceScaleFactor: 2
});
debug('Viewport set:', options.width, 'x', options.height);
spinner.update(`Loading page (${options.waitUntil})...`);
// Navigate to URL with configurable waitUntil
debug('Navigating to:', options.url);
debug('Wait until:', options.waitUntil);
debug('Timeout:', options.timeout);
try {
await page.goto(options.url, {
waitUntil: options.waitUntil,
timeout: options.timeout
});
debug('Page loaded successfully');
} catch (error) {
if (error.name === 'TimeoutError') {
debug('Navigation timeout, but continuing anyway...');
spinner.update('Navigation timeout - continuing...');
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
throw error;
}
}
spinner.update('Waiting for fonts...');
await page.evaluateHandle('document.fonts.ready');
debug('Fonts ready');
spinner.stop(`${colors.green}✓${colors.reset} Page loaded successfully!`);
// Execute custom script if provided
if (options.script) {
console.log(`${colors.peach}📜${colors.reset} Executing custom script...`);
const scriptContent = fs.readFileSync(options.script, 'utf8');
await eval(`(async () => { ${scriptContent} })()`);
console.log(`${colors.green}✓${colors.reset} Script executed!`);
}
// Interactive shoot delay
if (!options.noDelay) {
console.log();
console.log(`${colors.peach}${colors.bright}🎯 SHOOT MODE ACTIVATED${colors.reset}`);
console.log(`${colors.gray}Press SPACE when ready or wait ${options.delay}ms${colors.reset}`);
console.log();
await Promise.race([
waitForSpacebar(),
showCountdown(options.delay)
]);
// Clear the timer line
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
}
// Export based on format
console.log();
const exportSpinner = new Spinner(`Capturing ${options.format.toUpperCase()}...`);
exportSpinner.start();
try {
switch (options.format.toLowerCase()) {
case 'png':
await exportPNG(page, options.output, options);
break;
case 'jpg':
case 'jpeg':
await exportJPG(page, options.output, options);
break;
case 'webp':
await exportWEBP(page, options.output, options);
break;
case 'pdf':
await exportPDF(page, options.output, options);
break;
case 'mp4':
exportSpinner.stop();
await exportMP4(page, options.output, options);
break;
case 'html':
exportSpinner.stop();
await exportHTML(page, options.output, options);
break;
default:
throw new Error(`Unsupported format: ${options.format}`);
}
if (options.format !== 'mp4' && options.format !== 'html') {
exportSpinner.stop();
}
console.log(`${colors.green}${colors.bright}✓ Shot captured successfully!${colors.reset}`);
console.log(`${colors.gray}Saved to:${colors.reset} ${colors.cyan}${options.output}${colors.reset}`);
// Show file size
const stats = fs.statSync(options.output);
const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
console.log(`${colors.gray}File size:${colors.reset} ${colors.bright}${sizeMB} MB${colors.reset}`);
} catch (error) {
exportSpinner.stop();
console.error(`${colors.red}✗${colors.reset} Error: ${error.message}`);
await browser.close();
process.exit(1);
}
await browser.close();
console.log();
console.log(`${colors.mint}${colors.bright}Thanks for using WebShot! 📸${colors.reset}`);
}
// Run
main().catch(console.error);