Add hooks and CLAUDE.md resource types with install/uninstall scripts
Introduces two new resource types (hooks, claude-md) with full CRUD, visual hook config editor, section-delimited CLAUDE.md installs, uninstall endpoints, and shell injection hardening in sync scripts.
This commit is contained in:
257
PLAN.md
257
PLAN.md
@@ -1,179 +1,152 @@
|
||||
# Skillit - Plan de Implementacion
|
||||
# Plan: Soporte de Skills en formato carpeta (Folder-based resources)
|
||||
|
||||
## 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
|
||||
|
||||
El PDF oficial de Anthropic define que una skill puede ser una **carpeta** con subdirectorios:
|
||||
```
|
||||
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
|
||||
my-skill/
|
||||
├── SKILL.md # Requerido
|
||||
├── scripts/ # Opcional - codigo ejecutable
|
||||
├── references/ # Opcional - documentacion
|
||||
└── assets/ # Opcional - plantillas, fuentes, iconos
|
||||
```
|
||||
|
||||
---
|
||||
Actualmente Skillit almacena cada recurso como un unico `.md` en `data/<type>/<slug>.md`. Queremos soportar **ambos formatos**: simple (archivo .md) y carpeta (directorio con SKILL.md + subdirectorios).
|
||||
|
||||
## Archivos a modificar
|
||||
|
||||
| Archivo | Cambio |
|
||||
|---------|--------|
|
||||
| `src/lib/registry.ts` | Agregar `mainFileName` por tipo y constante `FOLDER_SUBDIRS` |
|
||||
| `src/lib/resources.ts` | Reescribir CRUD para detectar/manejar ambos formatos; nuevas funciones para sub-archivos |
|
||||
| `src/lib/skills.ts` | Actualizar interfaz `Skill` con `format` y `files` |
|
||||
| `src/lib/sync.ts` | Scripts de sync/push con soporte carpeta |
|
||||
| `src/pages/api/resources/[type]/index.ts` | Aceptar `format` en POST |
|
||||
| `src/pages/api/resources/[type]/[slug].ts` | Manejar carpeta en GET |
|
||||
| `src/pages/[type]/[slug].astro` | Mostrar arbol de archivos |
|
||||
| `src/pages/[type]/[slug]/edit.astro` | Pasar format/files al editor |
|
||||
| `src/pages/[type]/new.astro` | Soporte selector de formato |
|
||||
| `src/pages/[type]/[slug]/i.ts` | Install scripts multi-archivo |
|
||||
| `src/pages/[type]/[slug]/gi.ts` | Install global multi-archivo |
|
||||
| `src/components/ResourceEditor.vue` | Toggle formato + integrar FileManager |
|
||||
|
||||
## Archivos nuevos
|
||||
|
||||
| Archivo | Proposito |
|
||||
|---------|-----------|
|
||||
| `src/pages/api/resources/[type]/[slug]/files/index.ts` | Listar y subir sub-archivos |
|
||||
| `src/pages/api/resources/[type]/[slug]/files/[...filePath].ts` | CRUD individual de sub-archivos |
|
||||
| `src/components/FileManager.vue` | Componente Vue para gestionar sub-archivos |
|
||||
| `src/components/FolderTree.astro` | Componente Astro para mostrar arbol de archivos en detalle |
|
||||
|
||||
## Fases de implementacion
|
||||
|
||||
### Fase 0: Scaffolding
|
||||
### Fase 1: Registry (`registry.ts`)
|
||||
|
||||
1. **Crear proyecto Astro** en el directorio actual
|
||||
```bash
|
||||
npm create astro@latest . -- --template minimal --typescript strict --install --git
|
||||
```
|
||||
Agregar campo `mainFileName` a `ResourceTypeConfig`:
|
||||
- skills: `SKILL.md`
|
||||
- agents: `AGENT.md`
|
||||
- output-styles: `OUTPUT-STYLE.md`
|
||||
- rules: `RULE.md`
|
||||
|
||||
2. **Instalar dependencias**
|
||||
```bash
|
||||
npx astro add node vue tailwind
|
||||
npm install gray-matter marked
|
||||
```
|
||||
Agregar constante `FOLDER_SUBDIRS = ['scripts', 'references', 'assets']`.
|
||||
|
||||
3. **Configurar `astro.config.mjs`**
|
||||
- `output: 'server'` (SSR)
|
||||
- `adapter: node({ mode: 'standalone' })`
|
||||
- Integraciones: Vue, TailwindCSS vite plugin
|
||||
### Fase 2: Core CRUD (`resources.ts`)
|
||||
|
||||
4. **`src/styles/global.css`**: solo `@import "tailwindcss"`
|
||||
Cambios principales:
|
||||
|
||||
5. **Crear `data/skills/`** y el seed `example-skill.md`
|
||||
1. **Nuevos tipos**:
|
||||
- `ResourceFormat = 'file' | 'folder'`
|
||||
- `ResourceFileEntry = { relativePath: string; size: number }`
|
||||
- Agregar `format` y `files` a interfaz `Resource`
|
||||
|
||||
### Fase 1: Core library
|
||||
2. **`resolveResource(type, slug)`** (nueva): detecta si el recurso es archivo o carpeta. Carpeta tiene prioridad si ambos existen.
|
||||
|
||||
6. **`src/lib/skills.ts`** - Modulo central de CRUD:
|
||||
- `listSkills()` - lee directorio, parsea con gray-matter
|
||||
- `getSkill(slug)` - lee un .md, devuelve null si no existe
|
||||
- `createSkill(slug, content)` - escribe .md, error si ya existe
|
||||
- `updateSkill(slug, content)` - sobreescribe .md
|
||||
- `deleteSkill(slug)` - elimina .md
|
||||
- `isValidSlug()` - valida `/^[a-z0-9][a-z0-9-]*[a-z0-9]$/`, max 64 chars
|
||||
- `SKILLS_DIR` configurable via env var, default `data/skills/`
|
||||
3. **`listResources(type)`**: escanear tanto `*.md` como directorios con el mainFileName.
|
||||
|
||||
### Fase 2: API endpoints
|
||||
4. **`getResource(type, slug)`**: usar `resolveResource`, leer mainFile, listar sub-archivos si es carpeta.
|
||||
|
||||
7. **`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
|
||||
5. **`createResource(type, slug, content, format?)`**: parametro `format` opcional (default `'file'`). Si `'folder'`, crear directorio y escribir mainFileName.
|
||||
|
||||
8. **`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
|
||||
6. **`updateResource(type, slug, content)`**: auto-detectar formato, escribir en la ruta correcta.
|
||||
|
||||
9. **`src/pages/api/sync.ts`**
|
||||
- GET: genera script bash que:
|
||||
- Crea `~/.claude/skills/` si no existe
|
||||
- Para cada skill: `mkdir -p` + `curl` del raw .md a `SKILL.md`
|
||||
- Uso: `curl -fsSL https://skillit.example.com/api/sync | bash`
|
||||
7. **`deleteResource(type, slug)`**: auto-detectar. Para carpeta usar `fs.rm(dirPath, { recursive: true })`.
|
||||
|
||||
### Fase 3: UI read-only
|
||||
8. **Funciones nuevas para sub-archivos**:
|
||||
- `listResourceFiles(type, slug)` - listar archivos auxiliares
|
||||
- `getResourceFile(type, slug, relativePath)` - leer sub-archivo (Buffer)
|
||||
- `addResourceFile(type, slug, relativePath, data: Buffer)` - escribir sub-archivo
|
||||
- `deleteResourceFile(type, slug, relativePath)` - eliminar sub-archivo
|
||||
- `convertToFolder(type, slug)` - convertir simple a carpeta
|
||||
|
||||
10. **`src/layouts/Base.astro`** - HTML shell con nav (logo + link "New Skill")
|
||||
9. **Seguridad**: validar que `relativePath` no contenga `..`, solo permita rutas dentro de `FOLDER_SUBDIRS`.
|
||||
|
||||
11. **`src/components/SkillCard.astro`** - Card con nombre, descripcion truncada, badges de tools
|
||||
### Fase 3: API de sub-archivos
|
||||
|
||||
12. **`src/pages/index.astro`** - Catalogo: llama `listSkills()`, renderiza grid de SkillCards. Empty state si no hay skills.
|
||||
**`/api/resources/[type]/[slug]/files/index.ts`**:
|
||||
- GET: lista sub-archivos (JSON)
|
||||
- POST: subir archivo via multipart/form-data
|
||||
|
||||
13. **`src/pages/skills/[slug].astro`** - Vista detalle: renderiza markdown con `marked`, muestra metadata, botones Edit/Delete
|
||||
**`/api/resources/[type]/[slug]/files/[...filePath].ts`**:
|
||||
- GET: descargar sub-archivo
|
||||
- PUT: subir/reemplazar sub-archivo
|
||||
- DELETE: eliminar sub-archivo
|
||||
|
||||
### Fase 4: UI write
|
||||
**Modificar `/api/resources/[type]/index.ts`**:
|
||||
- POST acepta `format: 'file' | 'folder'` opcional
|
||||
|
||||
14. **`src/components/SkillEditor.vue`** (island `client: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
|
||||
### Fase 4: Pagina de detalle (`[type]/[slug].astro`)
|
||||
|
||||
15. **`src/pages/skills/new.astro`** - Monta SkillEditor en modo create
|
||||
- Comprobar `resource.format`
|
||||
- Para carpeta: renderizar seccion "Files" con `FolderTree.astro`
|
||||
- El raw markdown (para curl) sigue devolviendo solo el mainFile
|
||||
|
||||
16. **`src/pages/skills/[slug]/edit.astro`** - Carga skill, monta SkillEditor en modo edit con datos
|
||||
**`FolderTree.astro`**: componente que muestra arbol de archivos con links de descarga.
|
||||
|
||||
17. **`src/components/DeleteButton.vue`** (island `client:load`)
|
||||
- Prop: `slug`
|
||||
- Click -> confirm -> `fetch DELETE` -> redirect a `/`
|
||||
### Fase 5: Editor UI
|
||||
|
||||
### Fase 5: Deployment
|
||||
**`ResourceEditor.vue`**:
|
||||
- Nuevos props: `initialFormat`, `files`
|
||||
- Toggle formato al crear (Simple / Carpeta)
|
||||
- Seccion FileManager al editar recurso tipo carpeta
|
||||
|
||||
18. **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/skills`
|
||||
- `CMD ["node", "./dist/server/entry.mjs"]`
|
||||
- Puerto 4321
|
||||
**`FileManager.vue`** (nuevo componente Vue):
|
||||
- Lista archivos con boton eliminar
|
||||
- Boton subir: selector de subdirectorio (scripts/references/assets) + file input
|
||||
- Llamadas fetch a la API de files
|
||||
|
||||
19. **.dockerignore**: `node_modules`, `dist`, `.git`, `.env`
|
||||
### Fase 6: Scripts de instalacion
|
||||
|
||||
---
|
||||
**`[type]/[slug]/i.ts` y `gi.ts`**:
|
||||
- Para formato carpeta: generar script que crea estructura de directorios y descarga cada archivo
|
||||
- URLs de sub-archivos: `${origin}/api/resources/${type}/${slug}/files/${relativePath}`
|
||||
- `chmod +x` para archivos en `scripts/`
|
||||
|
||||
**`sync.ts`**:
|
||||
- Sync: detectar formato por recurso, generar descarga multi-archivo
|
||||
- Push: iterar tanto `*.md` como directorios; subir main content + sub-archivos
|
||||
|
||||
### Fase 7: Backward compat (`skills.ts`)
|
||||
|
||||
Actualizar interfaz `Skill` con `format` y `files`. El wrapper sigue delegando a `resources.ts`.
|
||||
|
||||
## Orden de implementacion
|
||||
|
||||
1. Fase 1 (registry) - base
|
||||
2. Fase 2 (resources.ts CRUD) - critico
|
||||
3. Fase 3 (API sub-archivos)
|
||||
4. Fase 4 (pagina detalle)
|
||||
5. Fase 5 (editor UI)
|
||||
6. Fase 6 (scripts instalacion)
|
||||
7. Fase 7 (backward compat)
|
||||
|
||||
## Verificacion
|
||||
|
||||
**Tras Fase 2 (API):**
|
||||
```bash
|
||||
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 funcional
|
||||
- `curl -fsSL http://localhost:4321/api/sync | bash && ls ~/.claude/skills/`
|
||||
|
||||
**Tras Fase 5 (Docker):**
|
||||
```bash
|
||||
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/skills` para que los skills sobrevivan rebuilds
|
||||
- Puerto: 4321
|
||||
- Env var opcional: `SKILLS_DIR` (default ya configurado en Dockerfile)
|
||||
1. Crear un skill simple (`test-simple`) via web - verificar que funciona como antes
|
||||
2. Crear un skill carpeta (`test-folder`) via web - verificar que se crea directorio con SKILL.md
|
||||
3. Subir archivos a scripts/ y references/ del skill carpeta via FileManager
|
||||
4. Ver pagina de detalle - verificar arbol de archivos
|
||||
5. Ejecutar script de instalacion (`curl .../skills/test-folder/i | bash`) - verificar descarga completa
|
||||
6. Verificar que listado muestra ambos formatos correctamente
|
||||
7. Editar ambos formatos - verificar que se actualizan
|
||||
8. Eliminar ambos formatos - verificar limpieza
|
||||
|
||||
Reference in New Issue
Block a user