mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
1. Catalog failure safety: warn when catalog is empty or throws, so capture doesn't silently produce zero images 2. Dead path: extract-audio-data.py → skills/gsap/scripts/ (was skills/hyperframes/scripts/) 3. --json fonts compat: emit both `fonts` (string[]) and `fontsDetailed` (FontToken[]) to avoid breaking external consumers 4. Restore .cursorrules writing alongside AGENTS.md + CLAUDE.md 5. .gitignore: remove over-broad `projects/` and `videos/` entries, keep scoped `cursor-tests/` and `launch-video*/` 6. agentPromptGenerator: mark unused params as reserved with comments, remove _animations from buildPrompt 7. Cookie filter: threshold 20 → 8 chars to preserve footer copy like "© 2026 Stripe" (16 chars) and "Privacy & Terms" (15 chars) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
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/<hostname> 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<string, string> = {
|
|
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);
|
|
}
|
|
},
|
|
});
|