diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 000000000..417b0f22f --- /dev/null +++ b/.vercelignore @@ -0,0 +1,4 @@ +tests/ +server/ +*.test.js + diff --git a/AGENTS.md b/AGENTS.md index ced401e27..389cce2b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,12 @@ vibe-design-plugins/ │ │ └── prompts/*.md │ ├── AGENTS.md │ └── AGENTS.*.md +├── api/ # Vercel Functions (production) +│ ├── skills.js # GET /api/skills +│ ├── commands.js # GET /api/commands +│ └── download/ +│ ├── [type]/[provider]/[id].js # Individual downloads +│ └── bundle/[provider].js # Bundle downloads ├── public/ # Website for impeccable.style │ ├── index.html # Main page │ ├── css/ # Modular CSS (9 files) @@ -56,7 +62,7 @@ vibe-design-plugins/ │ │ ├── tokens.css # Design system │ │ └── ... # Component styles │ └── app.js # Vanilla JS -├── server/ # Bun server +├── server/ # Bun server (local dev only) │ └── index.js # Serves website + API routes ├── scripts/ # Build system (Bun) │ ├── build.js # Main orchestrator @@ -78,9 +84,15 @@ vibe-design-plugins/ **Tech Stack:** - Vanilla JavaScript (no frameworks) - Modern CSS with Bun's bundler (nesting, OKLCH colors, @import) -- Bun server with HTML imports as routes +- **Local Development**: Bun server with native routes (`server/index.js`) +- **Production**: Vercel Functions with Bun runtime (`/api` directory) - Deployed on Vercel with Bun runtime +**Dual Setup:** +- `/api` directory contains individual Vercel Functions for production +- `/server` directory contains monolithic Bun server for local development +- Same logic, different structure (Vercel requires serverless functions) + **Design:** - Editorial precision aesthetic - Cormorant Garamond (display) + Instrument Sans (body) @@ -88,13 +100,12 @@ vibe-design-plugins/ - Editorial sidebar layout (title left, content right) - Modular CSS architecture (9 files) -**Routes:** -- `/` - Homepage -- `/api/skills` - JSON list -- `/api/commands` - JSON list -- `/api/download/skill/:provider/:id` - Individual file -- `/api/download/command/:provider/:id` - Individual file -- `/api/download/bundle/:provider` - ZIP bundle +**API Endpoints** (Vercel Functions): +- `/` - Homepage (static HTML) +- `/api/skills` - JSON list of all skills +- `/api/commands` - JSON list of all commands +- `/api/download/[type]/[provider]/[id]` - Individual file download +- `/api/download/bundle/[provider]` - ZIP bundle download ## Source File Format diff --git a/README.md b/README.md index 28372a815..811faed90 100644 --- a/README.md +++ b/README.md @@ -269,18 +269,27 @@ bun run dev - `source/` - Edit these! Single source of truth for all content - `dist/` - Generated provider-specific files (committed for users) +- `api/` - Vercel Functions for production (serverless) - `public/` - Website (HTML, CSS modules, vanilla JS) -- `server/` - Bun server for impeccable.style +- `server/` - Bun server for local development - `scripts/` - Build system that transforms source → dist ### Deployment -The site runs on Vercel with Bun runtime. Just push to main and it auto-deploys. +The site runs on [Vercel](https://vercel.com) with [Bun runtime](https://vercel.com/docs/functions/runtimes/bun). + +**Architecture:** +- **Local dev**: Monolithic Bun server (`server/index.js`) with native routing +- **Production**: Individual Vercel Functions (`api/` directory) for serverless deployment + +Push to main and Vercel auto-deploys: ```bash -# Production build +# Build generates dist/ files and ZIPs bun run build -bun run start # Uses --production flag + +# For local testing +bun run dev # Uses server/index.js ``` See [DEVELOP.md](DEVELOP.md) for detailed contributor guidelines. diff --git a/api/commands.js b/api/commands.js new file mode 100644 index 000000000..0ce82493d --- /dev/null +++ b/api/commands.js @@ -0,0 +1,39 @@ +import { readdir } from "fs/promises"; +import { join } from "path"; + +// Read all commands from source directory +async function getCommands() { + const sourceDir = join(process.cwd(), "source"); + const commandsDir = join(sourceDir, "commands"); + const files = await readdir(commandsDir); + const commands = []; + + for (const file of files) { + if (file.endsWith(".md")) { + const content = await Bun.file(join(commandsDir, file)).text(); + const frontmatterMatch = content.match(/^---\n([\s\S]+?)\n---/); + + if (frontmatterMatch) { + const frontmatter = frontmatterMatch[1]; + const nameMatch = frontmatter.match(/name:\s*(.+)/); + const descMatch = frontmatter.match(/description:\s*(.+)/); + + commands.push({ + id: file.replace(".md", ""), + name: nameMatch?.[1]?.trim() || file.replace(".md", ""), + description: descMatch?.[1]?.trim() || "No description available", + }); + } + } + } + + return commands; +} + +export default { + async fetch(request) { + const commands = await getCommands(); + return Response.json(commands); + }, +}; + diff --git a/api/download/[type]/[provider]/[id].js b/api/download/[type]/[provider]/[id].js new file mode 100644 index 000000000..0227c1464 --- /dev/null +++ b/api/download/[type]/[provider]/[id].js @@ -0,0 +1,72 @@ +import { join, basename } from "path"; + +// Get the appropriate file path for a provider +function getFilePath(type, provider, id) { + const distDir = join(process.cwd(), "dist"); + + if (type === "skill") { + if (provider === "cursor") { + return join(distDir, "cursor", ".cursor", "rules", `${id}.md`); + } else if (provider === "claude-code") { + return join(distDir, "claude-code", ".claude", "skills", id, "SKILL.md"); + } else if (provider === "gemini") { + return join(distDir, "gemini", `GEMINI.${id}.md`); + } else if (provider === "codex") { + return join(distDir, "codex", `AGENTS.${id}.md`); + } + } else if (type === "command") { + if (provider === "cursor") { + return join(distDir, "cursor", ".cursor", "commands", `${id}.md`); + } else if (provider === "claude-code") { + return join(distDir, "claude-code", ".claude", "commands", `${id}.md`); + } else if (provider === "gemini") { + return join(distDir, "gemini", ".gemini", "commands", `${id}.toml`); + } else if (provider === "codex") { + return join(distDir, "codex", ".codex", "prompts", `${id}.md`); + } + } + return null; +} + +export default { + async fetch(request) { + const url = new URL(request.url); + const pathParts = url.pathname.split('/').filter(Boolean); + + // Extract params from path: /api/download/[type]/[provider]/[id] + const type = pathParts[2]; // after 'api', 'download' + const provider = pathParts[3]; + const id = pathParts[4]; + + if (type !== "skill" && type !== "command") { + return new Response("Invalid type", { status: 400 }); + } + + const filePath = getFilePath(type, provider, id); + + if (!filePath) { + return new Response("Invalid provider", { status: 400 }); + } + + try { + const file = Bun.file(filePath); + const exists = await file.exists(); + + if (!exists) { + return new Response("File not found", { status: 404 }); + } + + const fileName = basename(filePath); + return new Response(file, { + headers: { + "Content-Type": "application/octet-stream", + "Content-Disposition": `attachment; filename="${fileName}"`, + }, + }); + } catch (error) { + console.error("Error downloading file:", error); + return new Response("Error downloading file", { status: 500 }); + } + }, +}; + diff --git a/api/download/bundle/[provider].js b/api/download/bundle/[provider].js new file mode 100644 index 000000000..7658152c4 --- /dev/null +++ b/api/download/bundle/[provider].js @@ -0,0 +1,34 @@ +import { join } from "path"; + +export default { + async fetch(request) { + const url = new URL(request.url); + const pathParts = url.pathname.split('/').filter(Boolean); + + // Extract provider from path: /api/download/bundle/[provider] + const provider = pathParts[3]; // after 'api', 'download', 'bundle' + + const distDir = join(process.cwd(), "dist"); + const zipPath = join(distDir, `${provider}.zip`); + + try { + const file = Bun.file(zipPath); + const exists = await file.exists(); + + if (!exists) { + return new Response("Bundle not found", { status: 404 }); + } + + return new Response(file, { + headers: { + "Content-Type": "application/zip", + "Content-Disposition": `attachment; filename="impeccable-style-${provider}.zip"`, + }, + }); + } catch (error) { + console.error("Error downloading bundle:", error); + return new Response("Error downloading bundle", { status: 500 }); + } + }, +}; + diff --git a/api/skills.js b/api/skills.js new file mode 100644 index 000000000..ecfd496a4 --- /dev/null +++ b/api/skills.js @@ -0,0 +1,39 @@ +import { readdir } from "fs/promises"; +import { join } from "path"; + +// Read all skills from source directory +async function getSkills() { + const sourceDir = join(process.cwd(), "source"); + const skillsDir = join(sourceDir, "skills"); + const files = await readdir(skillsDir); + const skills = []; + + for (const file of files) { + if (file.endsWith(".md")) { + const content = await Bun.file(join(skillsDir, file)).text(); + const frontmatterMatch = content.match(/^---\n([\s\S]+?)\n---/); + + if (frontmatterMatch) { + const frontmatter = frontmatterMatch[1]; + const nameMatch = frontmatter.match(/name:\s*(.+)/); + const descMatch = frontmatter.match(/description:\s*(.+)/); + + skills.push({ + id: file.replace(".md", ""), + name: nameMatch?.[1]?.trim() || file.replace(".md", ""), + description: descMatch?.[1]?.trim() || "No description available", + }); + } + } + } + + return skills; +} + +export default { + async fetch(request) { + const skills = await getSkills(); + return Response.json(skills); + }, +}; + diff --git a/vercel.json b/vercel.json index a38d561f8..3dde25c8f 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,7 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "bunVersion": "1.x" + "bunVersion": "1.x", + "buildCommand": "bun run build", + "outputDirectory": "public" }