Initial commit
This commit is contained in:
159
skills/browser-tools/scripts/screenshot.js
Executable file
159
skills/browser-tools/scripts/screenshot.js
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user