Files
gh-whamp-whamp-claude-tools…/skills/browser-tools/scripts/screenshot.js
2025-11-30 09:05:55 +08:00

159 lines
4.3 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
import puppeteer from "puppeteer-core";
import {
DEFAULT_PORT,
createLogger,
fail,
getActivePage,
parseArgs,
printJSON,
resolveBrowserConnection,
normalizeNumber,
} from "./config.js";
const args = parseArgs(process.argv.slice(2), {
boolean: ["json", "quiet"],
string: ["element", "format", "ws", "host", "out"],
number: ["port", "timeout", "quality"],
alias: {
j: "json",
q: "quiet",
},
defaults: {
port: DEFAULT_PORT,
timeout: 30000,
format: "png",
},
});
const jsonOutput = Boolean(args.json);
const logger = createLogger({ quiet: Boolean(args.quiet), json: jsonOutput });
const timeout = normalizeNumber(args.timeout, 30000);
const port = normalizeNumber(args.port, DEFAULT_PORT);
const format = normalizeFormat(args.format);
if (!format) {
fail("Invalid --format. Use png or jpeg.", { json: jsonOutput });
}
const quality = determineQuality(args.quality, format);
const connectionOptions = resolveBrowserConnection({ port, host: args.host, ws: args.ws });
let browser;
try {
browser = await puppeteer.connect({
...connectionOptions,
timeout,
});
} catch (error) {
fail(`Failed to connect to browser: ${error.message}`, { json: jsonOutput });
}
const outputFile = args.out ? args.out : await allocateTempFile(format);
try {
const page = await getActivePage(browser, { index: -1 });
if (!page) {
fail("No active page found. Navigate first.", { json: jsonOutput });
}
let dimensions;
let buffer;
if (args.element) {
const handle = await page.$(args.element);
if (!handle) {
fail(`Element not found: ${args.element}`, { json: jsonOutput });
}
const box = await handle.boundingBox();
if (!box) {
fail(`Element not visible: ${args.element}`, { json: jsonOutput });
}
buffer = await handle.screenshot({
path: outputFile,
type: format,
quality,
});
dimensions = {
width: Math.round(box.width),
height: Math.round(box.height),
};
} else {
const metrics = await page.evaluate(() => {
const width = Math.max(
document.documentElement.scrollWidth,
document.body?.scrollWidth ?? 0,
window.innerWidth,
);
const height = Math.max(
document.documentElement.scrollHeight,
document.body?.scrollHeight ?? 0,
window.innerHeight,
);
return {
width: Math.round(width),
height: Math.round(height),
};
});
buffer = await page.screenshot({
path: outputFile,
type: format,
quality,
fullPage: true,
});
dimensions = metrics;
}
if (!buffer) {
fail("Screenshot failed.", { json: jsonOutput });
}
const result = {
ok: true,
path: outputFile,
format,
width: dimensions?.width ?? null,
height: dimensions?.height ?? null,
element: Boolean(args.element),
};
if (jsonOutput) {
printJSON(result);
} else {
logger.info(`📸 Screenshot saved (${result.width ?? "?"}×${result.height ?? "?"})`);
process.stdout.write(`${outputFile}\n`);
}
} catch (error) {
fail(`Screenshot failed: ${error.message}`, { json: jsonOutput });
} finally {
if (browser) await browser.disconnect();
}
function normalizeFormat(value) {
if (!value) return "png";
const normalized = String(value).toLowerCase();
if (["png", "jpeg"].includes(normalized)) return normalized;
return null;
}
function determineQuality(value, currentFormat) {
if (currentFormat !== "jpeg") return undefined;
if (value === undefined) return 80;
const numeric = normalizeNumber(value, 80);
return Math.min(100, Math.max(1, numeric));
}
async function allocateTempFile(currentFormat) {
const directory = await mkdtemp(join(tmpdir(), "browser-tools-"));
return join(directory, `screenshot-${randomUUID()}.${currentFormat}`);
}