import { defineCommand } from "citty"; import { resolve } from "node:path"; import type { Example } from "./_examples.js"; export const examples: Example[] = [ ["Capture a website", "hyperframes capture https://stripe.com"], ["Capture to a specific directory", "hyperframes capture https://linear.app -o linear-video"], ["JSON output for AI agents", "hyperframes capture https://example.com --json"], ]; export default defineCommand({ meta: { name: "capture", description: "Capture a website as editable HyperFrames components", }, args: { url: { type: "positional", description: "Website URL to capture", required: true, }, output: { type: "string", description: "Output directory name", alias: "o", }, "skip-assets": { type: "boolean", description: "Skip downloading assets (images, SVGs)", default: false, }, "max-screenshots": { type: "string", description: "Maximum screenshots to capture (default: 24)", }, timeout: { type: "string", description: "Page load timeout in ms (default: 120000)", }, json: { type: "boolean", description: "Output JSON (for AI agents / programmatic use)", default: false, }, }, async run({ args }) { const url = args.url as string; // Validate URL try { new URL(url); } catch { console.error(`Invalid URL: ${url}`); process.exit(1); } // Determine output directory — default to captures/ to keep repo root clean let outputName = args.output as string | undefined; if (!outputName) { const hostname = new URL(url).hostname.replace(/^www\./, ""); outputName = `captures/${hostname.replace(/\./g, "-")}`; } const outputDir = resolve(outputName); const isJson = args.json as boolean; if (!isJson) { const { c } = await import("../ui/colors.js"); console.log(); console.log(c.dim("◆") + " Capturing " + c.bold(url)); console.log(); } const { captureWebsite } = await import("../capture/index.js"); try { const result = await captureWebsite( { url, outputDir, skipAssets: args["skip-assets"] as boolean, maxScreenshots: args["max-screenshots"] ? parseInt(args["max-screenshots"] as string) : undefined, timeout: args.timeout ? parseInt(args.timeout as string) : undefined, json: isJson, }, isJson ? undefined : (stage: string, detail?: string) => { const stages: Record = { browser: " Launching browser...", navigate: " Loading page...", extract: " Extracting HTML & CSS...", tokens: " Extracting design tokens...", screenshots: " Capturing screenshots...", assets: " Downloading assets...", style: " Generating visual style...", done: " Done", }; const label = stages[stage] || ` ${stage}`; console.log(detail ? `${label} ${detail}` : label); }, ); if (isJson) { // Output structured JSON for Claude Code / programmatic use console.log( JSON.stringify( { ok: result.ok, projectDir: result.projectDir, url: result.url, title: result.title, screenshots: result.screenshots.length, assets: result.assets.length, detectedSections: result.tokens.sections.length, fonts: result.tokens.fonts.map((f) => f.family), fontsDetailed: result.tokens.fonts, animations: result.animationCatalog?.summary, warnings: result.warnings, }, null, 2, ), ); } else { const { c } = await import("../ui/colors.js"); console.log(); console.log(c.success("◇") + ` Captured ${c.bold(result.title)} → ${c.dim(outputDir)}`); console.log(); console.log(` ${c.dim("Screenshots:")} ${result.screenshots.length}`); console.log(` ${c.dim("Assets:")} ${result.assets.length}`); console.log(` ${c.dim("Sections:")} ${result.tokens.sections.length}`); console.log( ` ${c.dim("Fonts:")} ${result.tokens.fonts .map(function (f) { return ( f.family + " (" + (f.variable && f.weightRange ? f.weightRange[0] + "-" + f.weightRange[1] + " variable" : f.weights.join(",")) + ")" ); }) .join(", ")}`, ); if (result.warnings.length > 0) { console.log(); for (const w of result.warnings) { console.log(` ${c.warn("⚠")} ${w}`); } } console.log(); } } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); // Write BLOCKED.md so the user/agent knows the capture failed try { const { mkdirSync, writeFileSync } = await import("node:fs"); mkdirSync(outputDir, { recursive: true }); const isTimeout = /timeout|timed out/i.test(errMsg); const reason = isTimeout ? "Page navigation timed out — the site may be blocking headless browsers or requires authentication." : `Capture failed: ${errMsg}`; writeFileSync( `${outputDir}/BLOCKED.md`, `# Capture Failed\n\n${reason}\n\nURL: ${url}\n\n## What to try\n\n- Re-run with a longer timeout: \`--timeout 60000\`\n- The site may block headless browsers (anti-bot protection)\n- Try capturing a different page on the same domain\n`, "utf-8", ); } catch { /* best-effort */ } if (isJson) { console.log(JSON.stringify({ ok: false, error: errMsg })); } else { console.error(`\n ✗ Capture failed: ${errMsg}\n`); } process.exit(1); } }, });