/** * stitch-export.ts — Export Stitch designs as HTML, image, or DESIGN.md. * * Usage: * npx tsx stitch-export.ts [--project ] [--format html|image|all] [--output ] * * 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 [--project ] [--format html|image|all] [--output ]"); 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 { 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 = {}; // 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();