#!/usr/bin/env node /** * Capture an overview using an available non-managed backend. * Managed browser preview remains the preferred platform-level path; this CLI handles * Firefox and deterministic SVG rasterization when those binaries are installed. */ import { execFileSync, spawnSync } from 'node:child_process'; import { existsSync, mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { ScriptError, main, parseFlags } from './lib.js'; main(async () => { const flags = parseFlags(process.argv.slice(2), { required: ['output'], optional: ['url', 'svg', 'backend', 'width', 'height'], }); const output = resolve(flags.output); mkdirSync(dirname(output), { recursive: true }); const backend = chooseBackend(flags.backend, flags.url, flags.svg); if (backend === 'firefox') captureFirefox(flags.url, output, flags.width, flags.height); else captureSvg(flags.svg, output, flags.width, flags.height); return { backend, output }; }); function chooseBackend(requested, url, svg) { if (requested && requested !== 'auto' && requested !== 'firefox' && requested !== 'svg') { throw new ScriptError('BACKEND_UNKNOWN', `不支援的 capture backend:${requested}`); } if (requested === 'firefox') { requireBinary('firefox'); if (!url) throw new ScriptError('URL_REQUIRED', 'Firefox backend 需要 --url'); return 'firefox'; } if (requested === 'svg') { requireSvgInput(svg); requireAnyBinary(['rsvg-convert', 'resvg']); return 'svg'; } if (url && hasBinary('firefox')) return 'firefox'; if (svg && (hasBinary('rsvg-convert') || hasBinary('resvg'))) return 'svg'; throw new ScriptError('NO_CAPTURE_BACKEND', '沒有可用的 Firefox 或 SVG rasterizer;請使用平台 preview,或安裝 firefox、rsvg-convert、resvg'); } function captureFirefox(url, output, width, height) { const args = ['--headless']; if (width) args.push('--window-size', `${width}${height ? `,${height}` : ''}`); args.push('--screenshot', output, url); const result = spawnSync('firefox', args, { encoding: 'utf8' }); if (result.status !== 0 || !existsSync(output)) throw new ScriptError('CAPTURE_FAILED', `Firefox 截圖失敗:${result.stderr || result.stdout || '沒有輸出檔案'}`); } function captureSvg(svg, output, width, height) { requireSvgInput(svg); if (hasBinary('rsvg-convert')) { const args = [`--output=${output}`]; if (width) args.push(`--width=${width}`); if (height) args.push(`--height=${height}`); args.push(resolve(svg)); const result = spawnSync('rsvg-convert', args, { encoding: 'utf8' }); if (result.status !== 0) throw new ScriptError('CAPTURE_FAILED', `rsvg-convert 失敗:${result.stderr || result.stdout}`); } else { const args = []; if (width) args.push('-w', String(width)); args.push(resolve(svg), output); const result = spawnSync('resvg', args, { encoding: 'utf8' }); if (result.status !== 0) throw new ScriptError('CAPTURE_FAILED', `resvg 失敗:${result.stderr || result.stdout}`); } if (!existsSync(output)) throw new ScriptError('CAPTURE_FAILED', 'rasterizer 沒有產生輸出檔案'); } function requireSvgInput(svg) { if (!svg || !existsSync(resolve(svg))) throw new ScriptError('SVG_REQUIRED', 'SVG backend 需要存在的 --svg'); } function requireBinary(binary) { if (!hasBinary(binary)) throw new ScriptError('BINARY_NOT_FOUND', `找不到 ${binary}`); } function requireAnyBinary(binaries) { if (!binaries.some(hasBinary)) throw new ScriptError('BINARY_NOT_FOUND', `找不到 ${binaries.join(' 或 ')}`); } function hasBinary(binary) { try { execFileSync('sh', ['-c', `command -v ${binary}`], { stdio: 'ignore' }); return true; } catch { return false; } }