174 lines
6.0 KiB
TypeScript
174 lines
6.0 KiB
TypeScript
/**
|
|
* stitch-export.ts — Export Stitch designs as HTML, image, or DESIGN.md.
|
|
*
|
|
* Usage:
|
|
* npx tsx stitch-export.ts <screen-id> [--project <id>] [--format html|image|all] [--output <dir>]
|
|
*
|
|
* Env: STITCH_API_KEY (required), STITCH_PROJECT_ID (optional default)
|
|
*/
|
|
|
|
import { stitch } from "@google/stitch-sdk";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
// -- Argument parsing --
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
function getFlag(name: string): string | undefined {
|
|
const idx = args.indexOf(`--${name}`);
|
|
if (idx === -1 || idx + 1 >= args.length) return undefined;
|
|
return args[idx + 1];
|
|
}
|
|
|
|
// Extract positional args (skip flags and their values)
|
|
function getPositionalArgs(): string[] {
|
|
const positional: string[] = [];
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i].startsWith("--")) {
|
|
i++; // skip flag value
|
|
} else {
|
|
positional.push(args[i]);
|
|
}
|
|
}
|
|
return positional;
|
|
}
|
|
|
|
const screenId = getPositionalArgs()[0];
|
|
const projectId =
|
|
getFlag("project") || process.env.STITCH_PROJECT_ID || "claudekit-default";
|
|
const format = getFlag("format") || "all";
|
|
const outputDir = getFlag("output") || "./stitch-exports";
|
|
|
|
if (!screenId) {
|
|
console.error("Usage: npx tsx stitch-export.ts <screen-id> [--project <id>] [--format html|image|all] [--output <dir>]");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!process.env.STITCH_API_KEY) {
|
|
console.error("[X] STITCH_API_KEY not set. Get one at https://stitch.withgoogle.com/settings/api");
|
|
process.exit(1);
|
|
}
|
|
|
|
// -- Helpers --
|
|
|
|
async function downloadFile(url: string, dest: string): Promise<void> {
|
|
const response = await fetch(url);
|
|
if (!response.ok) throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
fs.writeFileSync(dest, buffer);
|
|
}
|
|
|
|
/**
|
|
* Generate a DESIGN.md from HTML content by extracting Tailwind classes,
|
|
* color values, typography patterns, and component structure.
|
|
*/
|
|
function generateDesignMd(html: string): string {
|
|
const lines: string[] = ["# Design System", "", "Auto-generated from Google Stitch export.", ""];
|
|
|
|
// Extract colors from Tailwind classes and inline styles
|
|
const colorMatches = html.match(/(?:bg|text|border)-(?:\w+)-(\d+)/g) || [];
|
|
const uniqueColors = [...new Set(colorMatches)].slice(0, 20);
|
|
if (uniqueColors.length > 0) {
|
|
lines.push("## Colors", "");
|
|
uniqueColors.forEach((c) => lines.push(`- \`${c}\``));
|
|
lines.push("");
|
|
}
|
|
|
|
// Extract typography classes
|
|
const textMatches = html.match(/(?:text-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl)|font-(?:thin|light|normal|medium|semibold|bold|extrabold))/g) || [];
|
|
const uniqueText = [...new Set(textMatches)].slice(0, 15);
|
|
if (uniqueText.length > 0) {
|
|
lines.push("## Typography", "");
|
|
uniqueText.forEach((t) => lines.push(`- \`${t}\``));
|
|
lines.push("");
|
|
}
|
|
|
|
// Extract spacing patterns
|
|
const spacingMatches = html.match(/(?:p|m|gap|space)-(?:x|y)?-?\d+/g) || [];
|
|
const uniqueSpacing = [...new Set(spacingMatches)].slice(0, 15);
|
|
if (uniqueSpacing.length > 0) {
|
|
lines.push("## Spacing", "");
|
|
uniqueSpacing.forEach((s) => lines.push(`- \`${s}\``));
|
|
lines.push("");
|
|
}
|
|
|
|
// Extract component-level structure from HTML tags
|
|
const componentMatches = html.match(/<(section|nav|header|footer|main|aside|form|button|input|table|dialog)[^>]*>/gi) || [];
|
|
const uniqueComponents = [...new Set(componentMatches.map((c) => c.match(/<(\w+)/)?.[1] || ""))].filter(Boolean);
|
|
if (uniqueComponents.length > 0) {
|
|
lines.push("## Components", "");
|
|
uniqueComponents.forEach((c) => lines.push(`- \`<${c}>\``));
|
|
lines.push("");
|
|
}
|
|
|
|
lines.push("## Notes", "", "- Generated by Google Stitch AI", "- Tailwind CSS utility classes used throughout", "- Review and customize colors/typography for brand alignment");
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
// -- Main --
|
|
|
|
async function main() {
|
|
try {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
|
|
console.error(`[i] Exporting screen ${screenId} from project ${projectId}`);
|
|
// Resolve project handle — for default project, find by title first
|
|
let project;
|
|
if (projectId === "claudekit-default") {
|
|
const projects = await stitch.projects();
|
|
const found = projects.find(p => p.data?.title === "claudekit-default");
|
|
project = found || stitch.project(projectId);
|
|
} else {
|
|
project = stitch.project(projectId);
|
|
}
|
|
const screen = await project.getScreen(screenId!);
|
|
|
|
const exported: Record<string, string> = {};
|
|
|
|
// Export HTML
|
|
if (format === "html" || format === "all") {
|
|
const htmlUrl = await screen.getHtml();
|
|
const htmlPath = path.join(outputDir, "design.html");
|
|
await downloadFile(htmlUrl, htmlPath);
|
|
exported.html = htmlPath;
|
|
console.error(`[OK] HTML exported: ${htmlPath}`);
|
|
|
|
// Generate DESIGN.md from HTML
|
|
if (format === "all") {
|
|
const htmlContent = fs.readFileSync(htmlPath, "utf-8");
|
|
const designMd = generateDesignMd(htmlContent);
|
|
const designPath = path.join(outputDir, "DESIGN.md");
|
|
fs.writeFileSync(designPath, designMd);
|
|
exported.designMd = designPath;
|
|
console.error(`[OK] DESIGN.md generated: ${designPath}`);
|
|
}
|
|
}
|
|
|
|
// Export image
|
|
if (format === "image" || format === "all") {
|
|
const imageUrl = await screen.getImage();
|
|
const imagePath = path.join(outputDir, "design.png");
|
|
await downloadFile(imageUrl, imagePath);
|
|
exported.image = imagePath;
|
|
console.error(`[OK] Image exported: ${imagePath}`);
|
|
}
|
|
|
|
// Output result JSON to stdout
|
|
console.log(JSON.stringify({ screenId, projectId, format, exported }, null, 2));
|
|
} catch (error: unknown) {
|
|
const err = error as { code?: string; message?: string };
|
|
if (err.code === "NOT_FOUND") {
|
|
console.error(`[X] Screen "${screenId}" not found in project "${projectId}".`);
|
|
} else if (err.code === "AUTH_FAILED") {
|
|
console.error("[X] Authentication failed. Check STITCH_API_KEY env var.");
|
|
} else {
|
|
console.error(`[X] Export error: ${err.message || error}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|