6.3 KiB
Skillit - Plan de Implementacion
Contexto
App web para gestionar y distribuir Claude Code skills. Los usuarios suben/editan skills via web, y Claude Code las descarga ejecutando un script de sync (curl ... | bash).
Stack: Astro 5 (SSR) + Vue 3 (islands) + TailwindCSS 4 + Node adapter Deploy: Coolify (Docker) Storage: Filesystem directo (no Content Collections, no DB) Auth: Ninguna
Decision arquitectonica clave
No usar Astro Content Collections. Las Content Collections cachean datos en build time, pero esta app necesita CRUD en tiempo real. Usamos gray-matter + fs/promises directamente. Los skills se guardan en data/skills/ (fuera de src/content/) para evitar conflictos con Astro.
Estructura de archivos
skillit/
├── astro.config.mjs
├── tsconfig.json
├── package.json
├── Dockerfile
├── .dockerignore
├── data/
│ └── skills/ # Skills .md (target del CRUD)
│ └── example-skill.md # Seed data
├── src/
│ ├── styles/global.css # @import "tailwindcss"
│ ├── lib/skills.ts # CRUD filesystem helpers
│ ├── components/
│ │ ├── SkillCard.astro # Card para el catalogo
│ │ ├── SkillEditor.vue # Editor markdown + preview
│ │ └── DeleteButton.vue # Boton eliminar con confirmacion
│ ├── layouts/Base.astro # HTML shell, nav, CSS
│ └── pages/
│ ├── index.astro # Catalogo (grid de cards)
│ ├── skills/
│ │ ├── [slug].astro # Ver skill (markdown renderizado)
│ │ ├── new.astro # Crear skill (monta SkillEditor)
│ │ └── [slug]/edit.astro # Editar skill
│ └── api/
│ ├── skills/index.ts # GET lista + POST crear
│ ├── skills/[slug].ts # GET raw + PUT + DELETE
│ └── sync.ts # GET -> script bash de sync
Fases de implementacion
Fase 0: Scaffolding
-
Crear proyecto Astro en el directorio actual
npm create astro@latest . -- --template minimal --typescript strict --install --git -
Instalar dependencias
npx astro add node vue tailwind npm install gray-matter marked -
Configurar
astro.config.mjsoutput: 'server'(SSR)adapter: node({ mode: 'standalone' })- Integraciones: Vue, TailwindCSS vite plugin
-
src/styles/global.css: solo@import "tailwindcss" -
Crear
data/skills/y el seedexample-skill.md
Fase 1: Core library
src/lib/skills.ts- Modulo central de CRUD:listSkills()- lee directorio, parsea con gray-mattergetSkill(slug)- lee un .md, devuelve null si no existecreateSkill(slug, content)- escribe .md, error si ya existeupdateSkill(slug, content)- sobreescribe .mddeleteSkill(slug)- elimina .mdisValidSlug()- valida/^[a-z0-9][a-z0-9-]*[a-z0-9]$/, max 64 charsSKILLS_DIRconfigurable via env var, defaultdata/skills/
Fase 2: API endpoints
-
src/pages/api/skills/index.ts- GET: lista skills como JSON
[{slug, name, description, allowedTools}] - POST: crea skill, body
{slug, content}, returns 201/400/409
- GET: lista skills como JSON
-
src/pages/api/skills/[slug].ts- GET: devuelve raw .md (
Content-Type: text/markdown) - PUT: actualiza skill, body
{content}, returns 200/404 - DELETE: elimina skill, returns 204/404
- GET: devuelve raw .md (
-
src/pages/api/sync.ts- GET: genera script bash que:
- Crea
~/.claude/skills/si no existe - Para cada skill:
mkdir -p+curldel raw .md aSKILL.md
- Crea
- Uso:
curl -fsSL https://skillit.example.com/api/sync | bash
- GET: genera script bash que:
Fase 3: UI read-only
-
src/layouts/Base.astro- HTML shell con nav (logo + link "New Skill") -
src/components/SkillCard.astro- Card con nombre, descripcion truncada, badges de tools -
src/pages/index.astro- Catalogo: llamalistSkills(), renderiza grid de SkillCards. Empty state si no hay skills. -
src/pages/skills/[slug].astro- Vista detalle: renderiza markdown conmarked, muestra metadata, botones Edit/Delete
Fase 4: UI write
-
src/components/SkillEditor.vue(islandclient:load)- Props:
initialContent?,slug?,mode: 'create' | 'edit' - Layout 2 paneles: textarea izquierda + preview derecha
- Campos de formulario arriba: name (auto-genera slug), description, allowed-tools
- Preview en tiempo real con
marked(debounced 300ms) - Save: POST o PUT segun mode, redirect al detalle
- Props:
-
src/pages/skills/new.astro- Monta SkillEditor en modo create -
src/pages/skills/[slug]/edit.astro- Carga skill, monta SkillEditor en modo edit con datos -
src/components/DeleteButton.vue(islandclient:load)- Prop:
slug - Click -> confirm ->
fetch DELETE-> redirect a/
- Prop:
Fase 5: Deployment
-
Dockerfile (multi-stage):
- Build:
node:22-alpine,npm install,npm run build - Runtime: copia
dist/+node_modules(prod) +data/skills/ ENV SKILLS_DIR=/app/data/skillsCMD ["node", "./dist/server/entry.mjs"]- Puerto 4321
- Build:
-
.dockerignore:
node_modules,dist,.git,.env
Verificacion
Tras Fase 2 (API):
npm run dev
curl http://localhost:4321/api/skills # JSON array
curl http://localhost:4321/api/skills/example-skill # raw .md
Tras Fase 3 (UI read-only):
- Visitar
/-> ver card del example-skill - Click card -> ver skill renderizada
Tras Fase 4 (CRUD completo):
/skills/new-> crear skill -> aparece en catalogo- Editar skill -> cambios persistidos
- Eliminar skill -> desaparece
curl http://localhost:4321/api/sync-> script bash funcionalcurl -fsSL http://localhost:4321/api/sync | bash && ls ~/.claude/skills/
Tras Fase 5 (Docker):
docker build -t skillit .
docker run -p 4321:4321 -v skillit-data:/app/data/skills skillit
Notas para Coolify
- Build pack: Dockerfile
- Volumen persistente: montar en
/app/data/skillspara que los skills sobrevivan rebuilds - Puerto: 4321
- Env var opcional:
SKILLS_DIR(default ya configurado en Dockerfile)