@ariasbruno/skillbase 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +41 -0
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/skillbase.js +7 -0
- package/package.json +33 -0
- package/src/cli.js +253 -0
- package/src/config.js +29 -0
- package/src/core.js +581 -0
- package/src/manifest.js +41 -0
- package/src/recommendations.js +63 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guía operativa para agentes que trabajen en este repositorio.
|
|
4
|
+
|
|
5
|
+
## Alcance
|
|
6
|
+
Estas instrucciones aplican a todo el árbol del proyecto (`skillbase`).
|
|
7
|
+
|
|
8
|
+
## Objetivo del proyecto
|
|
9
|
+
`skillbase` es una CLI para gestionar skills locales/remotas:
|
|
10
|
+
- store global por defecto: `~/.skillbase/skills`
|
|
11
|
+
- skills en proyecto: `.agents/skills`
|
|
12
|
+
- manifiesto del proyecto: `skillbase.json`
|
|
13
|
+
|
|
14
|
+
## Reglas de implementación
|
|
15
|
+
1. Mantener el código en JavaScript ESM (`"type": "module"`).
|
|
16
|
+
2. Evitar dependencias externas salvo necesidad clara.
|
|
17
|
+
3. Toda lógica de negocio va en `src/core.js`; `src/cli.js` sólo parsea/coordina comandos.
|
|
18
|
+
4. Mantener mensajes de CLI en español (consistentes con README actual).
|
|
19
|
+
5. Si agregas o cambias comandos, actualiza:
|
|
20
|
+
- ayuda en `src/cli.js`
|
|
21
|
+
- sección de comandos en `README.md`
|
|
22
|
+
|
|
23
|
+
## Flujo de recomendaciones (init)
|
|
24
|
+
- Usar integración propia (sin dependencias externas) para detectar stack.
|
|
25
|
+
- Basarse en señales del proyecto (`package.json`, `requirements.txt`, etc).
|
|
26
|
+
- Si cambias este comportamiento, documenta el motivo en README.
|
|
27
|
+
|
|
28
|
+
## Migración
|
|
29
|
+
- `skillbase migrate` migra desde `~/.agents/skills` a `~/.skillbase/skills`.
|
|
30
|
+
- `skillbase migrate --project` migra desde `.agents/skills` del cwd al mismo destino global.
|
|
31
|
+
- Mantener `--force` para sobrescritura explícita.
|
|
32
|
+
|
|
33
|
+
## Calidad mínima
|
|
34
|
+
Antes de cerrar cambios ejecutar:
|
|
35
|
+
```bash
|
|
36
|
+
npm run lint
|
|
37
|
+
node bin/skillbase.js -h
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Publicación
|
|
41
|
+
Si cambias comportamiento visible, actualiza README con ejemplos.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruno Arias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# 🧠 skillbase
|
|
2
|
+
|
|
3
|
+
**Gestor de skills locales para entornos de desarrollo con IA.**
|
|
4
|
+
|
|
5
|
+
`skillbase` es un CLI de Node.js que te permite administrar las skills de tus agentes de Inteligencia Artificial utilizando enlaces simbólicos (symlinks) o instalaciones locales por cada workspace, evitando la saturación del contexto.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🤔 El Problema: Saturación de Contexto (Context Bloat)
|
|
10
|
+
|
|
11
|
+
Cuando desarrollas asistido por IA (usando herramientas como Cursor, Copilot o agentes personalizados), tener un directorio global con cientos de skills `.agents/skills/` destruye la precisión del modelo:
|
|
12
|
+
|
|
13
|
+
* **Alucinaciones:** El LLM se confunde al tener demasiadas herramientas irrelevantes a su disposición.
|
|
14
|
+
* **Consumo de Tokens:** Enviar el manifiesto de cientos de skills en cada prompt es lento y costoso.
|
|
15
|
+
* **Falta de foco:** Un proyecto no necesita las mismas skills que otro.
|
|
16
|
+
|
|
17
|
+
## 💡 La Solución
|
|
18
|
+
|
|
19
|
+
`skillbase` mantiene un **único registro global** en tu máquina (`~/.skillbase/skills`) y permite hacer una instalación en tu proyecto actual de **solo lo que necesitas**.
|
|
20
|
+
|
|
21
|
+
Imagínate que estás trabajando en el frontend de un e-commerce. No necesitas que la IA vea herramientas de DevOps o Backend; solo necesitas tus skills de React y quizás algunas de SEO. Con `skillbase`, le das a tu agente exactamente ese contexto.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚀 Instalación
|
|
26
|
+
|
|
27
|
+
Instala el paquete globalmente:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @ariasbruno/skillbase
|
|
31
|
+
# O clona y linkea localmente:
|
|
32
|
+
# npm link
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 💻 Uso Rápido (Quickstart)
|
|
36
|
+
|
|
37
|
+
**1. Inicializa tu proyecto**
|
|
38
|
+
Detecta tecnologías y sugiere skills compatibles:
|
|
39
|
+
```bash
|
|
40
|
+
skillbase init
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Añade las skills necesarias**
|
|
44
|
+
```bash
|
|
45
|
+
skillbase add seo-analyzer
|
|
46
|
+
# O abre el selector interactivo:
|
|
47
|
+
skillbase add
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**3. Instala desde el manifiesto**
|
|
51
|
+
Si ya tienes un `skillbase.json`, recrea el entorno:
|
|
52
|
+
```bash
|
|
53
|
+
skillbase install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 🛠️ Referencia de Comandos
|
|
59
|
+
|
|
60
|
+
| Comando | Alias | Descripción |
|
|
61
|
+
| :--- | :--- | :--- |
|
|
62
|
+
| `skillbase ls` | `l` | Lista skills instaladas globalmente en `~/.skillbase/skills`. |
|
|
63
|
+
| `skillbase init [--hard]` | | Detecta tecnologías y sugiere skills (usa `--hard` para analizar `tags` en `skill.json`). |
|
|
64
|
+
| `skillbase add [<skill>] [-s]` | `a` | Instalar skill global. Sin nombre, abre el **selector interactivo**. `-s` crea symlink. |
|
|
65
|
+
| `skillbase install` | `i` | Instala desde el manifiesto `skillbase.json`. Soporta `-r` (remotas) y `-f` (forzar). |
|
|
66
|
+
| `skillbase install <ref> -r` | `i -r` | Instala skill remota (URL o GitHub). Requiere `-k <name>` si es un repo Git. |
|
|
67
|
+
| `skillbase remove <skill> [--g]` | `rm` | Elimina skill del proyecto. Con `--g` la elimina del registro global. |
|
|
68
|
+
| `skillbase check [-r]` | `c` | Busca actualizaciones. Con `-r` busca solo en fuentes remotas. |
|
|
69
|
+
| `skillbase update [<skill>] [-r] [-f]` | `up` | Actualiza una o todas las skills. `-r` para remotas, `-f` para forzar. |
|
|
70
|
+
| `skillbase migrate [-p] [-f]` | `m` | Migra desde el antiguo `.agents`. `-p` usa el dir del proyecto, `-f` sobrescribe. |
|
|
71
|
+
|
|
72
|
+
### 🚩 Flags Detalladas
|
|
73
|
+
- `-h`, `--help`: Muestra la ayuda detallada.
|
|
74
|
+
- `-s`, `--sym`: Crea un enlace simbólico en lugar de copiar los archivos (útil para desarrollo).
|
|
75
|
+
- `-r`, `--remote`: Indica que la operación debe consultar fuentes externas (GitHub o API de skills.sh).
|
|
76
|
+
- `-f`, `--force`: Ignora errores de "ya existe" y sobrescribe archivos/configuraciones.
|
|
77
|
+
- `-k`, `--skill`: Nombre de la skill específica a extraer cuando se instala desde un repositorio GitHub.
|
|
78
|
+
- `-p`, `--project`: En migraciones, indica que el origen es `.agents/skills` del proyecto actual.
|
|
79
|
+
- `--g`: Flag específica de `remove` para borrar una skill del registro global.
|
|
80
|
+
|
|
81
|
+
### ⌨️ Aliases de comandos
|
|
82
|
+
Para mayor velocidad, puedes usar las iniciales:
|
|
83
|
+
`l` (ls), `a` (add), `i` (install), `rm` (remove), `c` (check), `up` (update), `m` (migrate).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 📂 ¿Cómo funciona bajo el capó?
|
|
88
|
+
|
|
89
|
+
`skillbase` organiza tus herramientas de forma eficiente:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
Tu Computadora
|
|
93
|
+
├── ~/.skillbase/skills/ <-- (Tu registro global físico)
|
|
94
|
+
│ ├── seo-analyzer/ <-- (Código fuente real)
|
|
95
|
+
│ └── react-helper/
|
|
96
|
+
│
|
|
97
|
+
└── /Proyectos/mi-gran-app/ <-- (Tu workspace actual)
|
|
98
|
+
├── skillbase.json <-- (El manifiesto del proyecto)
|
|
99
|
+
└── .agents/skills/ <-- (Contexto limpio para la IA)
|
|
100
|
+
└── seo-analyzer/ <-- (Enlace o copia local)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Puedes cambiar la ubicación del registro global configurando la variable de entorno `SKILLBASE_HOME`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🌐 Fuentes Remotas
|
|
108
|
+
|
|
109
|
+
Instala skills directamente desde repositorios de GitHub:
|
|
110
|
+
```bash
|
|
111
|
+
skillbase install <repo-url> --remote
|
|
112
|
+
# Ejemplo:
|
|
113
|
+
skillbase install https://github.com/usuario/my-skills --remote --skill analyzer
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🤝 Contribuciones
|
|
119
|
+
|
|
120
|
+
¡Las contribuciones son bienvenidas! Si tienes ideas para mejorar la gestión del contexto, abre un Issue o un Pull Request.
|
|
121
|
+
|
|
122
|
+
## 📄 Licencia
|
|
123
|
+
|
|
124
|
+
MIT
|
package/bin/skillbase.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ariasbruno/skillbase",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI para administrar skills locales y remotas entre proyectos",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skillbase": "bin/skillbase.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/skillbase.js -h",
|
|
11
|
+
"lint": "node --check bin/skillbase.js && node --check src/cli.js && node --check src/core.js"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"src/",
|
|
20
|
+
"README.md",
|
|
21
|
+
"AGENTS.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"skills",
|
|
26
|
+
"cli",
|
|
27
|
+
"agents",
|
|
28
|
+
"ai",
|
|
29
|
+
"context",
|
|
30
|
+
"bloat",
|
|
31
|
+
"developer-tools"
|
|
32
|
+
]
|
|
33
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addSkillsInteractive,
|
|
3
|
+
addSkill,
|
|
4
|
+
checkUpdates,
|
|
5
|
+
getGlobalSkillsDir,
|
|
6
|
+
initProject,
|
|
7
|
+
installFromManifest,
|
|
8
|
+
installRemoteSkillRef,
|
|
9
|
+
listGlobalSkills,
|
|
10
|
+
migrateAgentsSkillsToSkillbase,
|
|
11
|
+
readManifest,
|
|
12
|
+
removeSkill,
|
|
13
|
+
updateSkills
|
|
14
|
+
} from './core.js';
|
|
15
|
+
|
|
16
|
+
function printHelp() {
|
|
17
|
+
console.log(`skillbase - Gestor de skills locales/remotas
|
|
18
|
+
|
|
19
|
+
Uso:
|
|
20
|
+
skillbase -h
|
|
21
|
+
skillbase h
|
|
22
|
+
skillbase ls
|
|
23
|
+
skillbase l
|
|
24
|
+
skillbase init [--hard]
|
|
25
|
+
skillbase add <skill> [--sym]
|
|
26
|
+
skillbase add
|
|
27
|
+
skillbase a <skill> [-s]
|
|
28
|
+
skillbase install
|
|
29
|
+
skillbase i
|
|
30
|
+
skillbase install <skill|repo-url> --remote [--skill <name>] [--force]
|
|
31
|
+
skillbase remove <skill> [--g]
|
|
32
|
+
skillbase rm <skill> [--g]
|
|
33
|
+
skillbase check [--remote]
|
|
34
|
+
skillbase c [-r]
|
|
35
|
+
skillbase update [<skill>] [--remote] [--force]
|
|
36
|
+
skillbase up [<skill>] [-r] [-f]
|
|
37
|
+
skillbase migrate [--project] [--force]
|
|
38
|
+
skillbase m [--project] [-f]
|
|
39
|
+
|
|
40
|
+
Descripción breve de comandos:
|
|
41
|
+
ls Lista skills instaladas globalmente en ~/.skillbase/skills.
|
|
42
|
+
init Sugiere skills globales según tecnologías del proyecto.
|
|
43
|
+
add Instala una skill global (o abre selector interactivo sin argumentos).
|
|
44
|
+
install Instala desde skillbase.json o una skill remota con --remote.
|
|
45
|
+
remove Elimina una skill del proyecto o global con --g.
|
|
46
|
+
check Revisa si hay versiones más nuevas disponibles.
|
|
47
|
+
update Actualiza skills del manifiesto (todas o una puntual).
|
|
48
|
+
migrate Migra skills desde .agents hacia ~/skillbase/skills.
|
|
49
|
+
|
|
50
|
+
Aliases:
|
|
51
|
+
l=ls, h=-h, a=add, i=install, rm=remove, c=check, up=update, m=migrate
|
|
52
|
+
|
|
53
|
+
Short flags:
|
|
54
|
+
-s=--sym, -r=--remote, -f=--force, -k=--skill, -p=--project
|
|
55
|
+
|
|
56
|
+
Notas:
|
|
57
|
+
- Carpeta global por defecto: ~/.skillbase/skills
|
|
58
|
+
- Puedes cambiarla con SKILLBASE_HOME
|
|
59
|
+
- Las skills de proyecto viven en .agents/skills
|
|
60
|
+
- El manifiesto del proyecto es skillbase.json
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printCommandList() {
|
|
65
|
+
console.log(`Comandos disponibles:
|
|
66
|
+
- skillbase ls
|
|
67
|
+
- skillbase l
|
|
68
|
+
- skillbase init [--hard]
|
|
69
|
+
- skillbase add
|
|
70
|
+
- skillbase add <skill> [--sym]
|
|
71
|
+
- skillbase a <skill> [-s]
|
|
72
|
+
- skillbase install | skillbase i
|
|
73
|
+
- skillbase install <skill|repo-url> --remote [--skill <name>] [--force]
|
|
74
|
+
- skillbase remove <skill> [--g]
|
|
75
|
+
- skillbase rm <skill> [--g]
|
|
76
|
+
- skillbase check [--remote]
|
|
77
|
+
- skillbase c [-r]
|
|
78
|
+
- skillbase update [<skill>] [--remote] [--force]
|
|
79
|
+
- skillbase up [<skill>] [-r] [-f]
|
|
80
|
+
- skillbase migrate [--project] [--force]
|
|
81
|
+
- skillbase m [--project] [-f]
|
|
82
|
+
|
|
83
|
+
Usa "skillbase -h" para ver detalles.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasFlag(args, ...flags) {
|
|
87
|
+
return args.some((arg) => flags.includes(arg));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getFlagValue(args, ...flags) {
|
|
91
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
92
|
+
if (flags.includes(args[i])) return args[i + 1] ?? null;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function runCLI(argv) {
|
|
98
|
+
const args = argv.slice(2);
|
|
99
|
+
const [rawCommand, maybeSkill] = args;
|
|
100
|
+
const commandAliases = {
|
|
101
|
+
h: '-h',
|
|
102
|
+
l: 'ls',
|
|
103
|
+
a: 'add',
|
|
104
|
+
i: 'install',
|
|
105
|
+
rm: 'remove',
|
|
106
|
+
c: 'check',
|
|
107
|
+
up: 'update',
|
|
108
|
+
m: 'migrate'
|
|
109
|
+
};
|
|
110
|
+
const command = commandAliases[rawCommand] ?? rawCommand;
|
|
111
|
+
|
|
112
|
+
if (!command) {
|
|
113
|
+
printCommandList();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (command === '-h' || command === '--help') {
|
|
118
|
+
printHelp();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
switch (command) {
|
|
123
|
+
case 'ls': {
|
|
124
|
+
const skills = await listGlobalSkills();
|
|
125
|
+
if (!skills.length) {
|
|
126
|
+
console.log(`No hay skills globales instaladas en ${getGlobalSkillsDir()}`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`Skills globales (${getGlobalSkillsDir()}):`);
|
|
129
|
+
skills.forEach((skill) => console.log(`- ${skill}`));
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'init': {
|
|
135
|
+
const result = await initProject({ hard: hasFlag(args, '--hard') });
|
|
136
|
+
if (!result.technologies.length) {
|
|
137
|
+
console.log('No se detectaron tecnologías en el proyecto.');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.log(`Tecnologías detectadas: ${result.technologies.join(', ')}`);
|
|
141
|
+
if (!result.suggested.length) {
|
|
142
|
+
console.log('No se encontraron skills compatibles en ~/.skillbase/skills.');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.log(`Skills sugeridas: ${result.suggested.join(', ')}`);
|
|
146
|
+
if (result.cancelled) {
|
|
147
|
+
console.log('Selección cancelada.');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (result.installed.length) {
|
|
151
|
+
console.log(`Skills instaladas: ${result.installed.join(', ')}`);
|
|
152
|
+
} else {
|
|
153
|
+
console.log('No se seleccionaron skills para instalar.');
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case 'add': {
|
|
159
|
+
if (!maybeSkill) {
|
|
160
|
+
const result = await addSkillsInteractive({ sym: hasFlag(args, '--sym', '-s') });
|
|
161
|
+
if (result.cancelled) return;
|
|
162
|
+
if (!result.selected.length) {
|
|
163
|
+
console.log('No se seleccionaron skills para instalar.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log(`Skills instaladas: ${result.selected.join(', ')}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await addSkill(maybeSkill, { sym: hasFlag(args, '--sym', '-s') });
|
|
170
|
+
console.log(`Skill "${maybeSkill}" instalada en el proyecto.`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case 'install': {
|
|
175
|
+
if (maybeSkill && hasFlag(args, '--remote', '-r')) {
|
|
176
|
+
await installRemoteSkillRef(maybeSkill, {
|
|
177
|
+
force: hasFlag(args, '--force', '-f'),
|
|
178
|
+
skill: getFlagValue(args, '--skill', '-k')
|
|
179
|
+
});
|
|
180
|
+
console.log(`Skill remota "${maybeSkill}" instalada.`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (maybeSkill && !hasFlag(args, '--remote', '-r')) {
|
|
184
|
+
throw new Error('Para instalar una skill puntual usa "skillbase add <skill>" (global) o "skillbase install <skill|repo-url> --remote".');
|
|
185
|
+
}
|
|
186
|
+
await installFromManifest({ remote: hasFlag(args, '--remote', '-r'), force: hasFlag(args, '--force', '-f') });
|
|
187
|
+
const manifest = await readManifest();
|
|
188
|
+
console.log(`Instaladas ${manifest.skills.length} skills desde skillbase.json.`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'remove': {
|
|
193
|
+
if (!maybeSkill) throw new Error('Debes indicar una skill: skillbase remove <skill>');
|
|
194
|
+
await removeSkill(maybeSkill, { global: hasFlag(args, '--g') });
|
|
195
|
+
if (hasFlag(args, '--g')) {
|
|
196
|
+
console.log(`Skill global "${maybeSkill}" eliminada.`);
|
|
197
|
+
} else {
|
|
198
|
+
console.log(`Skill "${maybeSkill}" eliminada del proyecto.`);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case 'check': {
|
|
204
|
+
const updates = await checkUpdates({ remoteOnly: hasFlag(args, '--remote', '-r') });
|
|
205
|
+
if (!updates.length) {
|
|
206
|
+
console.log('No hay actualizaciones disponibles.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
console.log('Actualizaciones disponibles:');
|
|
210
|
+
updates.forEach((item) => {
|
|
211
|
+
console.log(`- ${item.name}: ${item.current ?? 'desconocida'} -> ${item.latest} (${item.source})`);
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'update': {
|
|
217
|
+
const skill = maybeSkill && !maybeSkill.startsWith('-') ? maybeSkill : null;
|
|
218
|
+
await updateSkills({
|
|
219
|
+
skillName: skill,
|
|
220
|
+
remoteOnly: hasFlag(args, '--remote', '-remote', '-r'),
|
|
221
|
+
force: hasFlag(args, '--force', '-f')
|
|
222
|
+
});
|
|
223
|
+
if (skill) {
|
|
224
|
+
console.log(`Skill "${skill}" actualizada.`);
|
|
225
|
+
} else {
|
|
226
|
+
console.log('Skills actualizadas.');
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
case 'migrate': {
|
|
232
|
+
const fromProject = hasFlag(args, '--project', '-p');
|
|
233
|
+
const result = await migrateAgentsSkillsToSkillbase({
|
|
234
|
+
force: hasFlag(args, '--force', '-f'),
|
|
235
|
+
fromProject
|
|
236
|
+
});
|
|
237
|
+
console.log(`Origen de migración: ${result.sourceRoot}/skills`);
|
|
238
|
+
console.log(`Skills encontradas: ${result.totalFound}`);
|
|
239
|
+
if (result.migrated.length) {
|
|
240
|
+
console.log(`Migradas a ${getGlobalSkillsDir()}:`);
|
|
241
|
+
result.migrated.forEach((skill) => console.log(`- ${skill}`));
|
|
242
|
+
}
|
|
243
|
+
if (result.skipped.length) {
|
|
244
|
+
console.log('Omitidas (ya existen globalmente, usa --force para sobrescribir):');
|
|
245
|
+
result.skipped.forEach((skill) => console.log(`- ${skill}`));
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
default:
|
|
251
|
+
throw new Error(`Comando desconocido: ${command}. Usa skillbase -h`);
|
|
252
|
+
}
|
|
253
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const MANIFEST_FILE = 'skillbase.json';
|
|
5
|
+
export const PROJECT_AGENTS_DIR = '.agents';
|
|
6
|
+
export const PROJECT_SKILLS_DIR = 'skills';
|
|
7
|
+
export const DEFAULT_GLOBAL_ROOTNAME = '.skillbase';
|
|
8
|
+
|
|
9
|
+
export function getGlobalRootDir() {
|
|
10
|
+
const fromEnv = process.env.SKILLBASE_HOME;
|
|
11
|
+
if (fromEnv) return path.resolve(fromEnv);
|
|
12
|
+
return path.join(os.homedir(), DEFAULT_GLOBAL_ROOTNAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getGlobalSkillsDir() {
|
|
16
|
+
return path.join(getGlobalRootDir(), PROJECT_SKILLS_DIR);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getProjectRoot(cwd = process.cwd()) {
|
|
20
|
+
return cwd;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getProjectSkillsDir(cwd = process.cwd()) {
|
|
24
|
+
return path.join(getProjectRoot(cwd), PROJECT_AGENTS_DIR, PROJECT_SKILLS_DIR);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getManifestPath(cwd = process.cwd()) {
|
|
28
|
+
return path.join(getProjectRoot(cwd), MANIFEST_FILE);
|
|
29
|
+
}
|
package/src/core.js
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import * as readline from 'node:readline';
|
|
5
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { getGlobalSkillsDir, getProjectRoot, getProjectSkillsDir, PROJECT_AGENTS_DIR, PROJECT_SKILLS_DIR } from './config.js';
|
|
9
|
+
import { readManifest, removeSkillFromManifest, upsertSkill, writeJson, writeManifest } from './manifest.js';
|
|
10
|
+
import { detectProjectTechnologies } from './recommendations.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
13
|
+
function nowISO() {
|
|
14
|
+
return new Date().toISOString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { getGlobalSkillsDir, getProjectRoot, getProjectSkillsDir, readManifest };
|
|
18
|
+
|
|
19
|
+
export async function ensureDir(dir) {
|
|
20
|
+
await fs.mkdir(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function exists(target) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(target);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function listGlobalSkills() {
|
|
33
|
+
const globalDir = getGlobalSkillsDir();
|
|
34
|
+
await ensureDir(globalDir);
|
|
35
|
+
const entries = await fs.readdir(globalDir, { withFileTypes: true });
|
|
36
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function copyDir(src, dst) {
|
|
40
|
+
await fs.cp(src, dst, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function readSkillVersion(skillPath) {
|
|
44
|
+
const skillMeta = path.join(skillPath, 'skill.json');
|
|
45
|
+
if (!(await exists(skillMeta))) return null;
|
|
46
|
+
const data = JSON.parse(await fs.readFile(skillMeta, 'utf8'));
|
|
47
|
+
return typeof data.version === 'string' ? data.version : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compareVersion(a, b) {
|
|
51
|
+
if (!a || !b) return 0;
|
|
52
|
+
const aa = a.split('.').map((x) => Number.parseInt(x, 10) || 0);
|
|
53
|
+
const bb = b.split('.').map((x) => Number.parseInt(x, 10) || 0);
|
|
54
|
+
for (let i = 0; i < Math.max(aa.length, bb.length); i += 1) {
|
|
55
|
+
const av = aa[i] ?? 0;
|
|
56
|
+
const bv = bb[i] ?? 0;
|
|
57
|
+
if (av > bv) return 1;
|
|
58
|
+
if (av < bv) return -1;
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function addSkill(skillName, { sym = false, cwd = process.cwd() } = {}) {
|
|
64
|
+
const globalPath = path.join(getGlobalSkillsDir(), skillName);
|
|
65
|
+
if (!(await exists(globalPath))) {
|
|
66
|
+
throw new Error(`La skill global "${skillName}" no existe en ${getGlobalSkillsDir()}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const projectSkillsDir = getProjectSkillsDir(cwd);
|
|
70
|
+
await ensureDir(projectSkillsDir);
|
|
71
|
+
const target = path.join(projectSkillsDir, skillName);
|
|
72
|
+
|
|
73
|
+
if (await exists(target)) {
|
|
74
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sym) await fs.symlink(globalPath, target, 'dir');
|
|
78
|
+
else await copyDir(globalPath, target);
|
|
79
|
+
|
|
80
|
+
const manifest = await readManifest(cwd);
|
|
81
|
+
const version = await readSkillVersion(globalPath);
|
|
82
|
+
upsertSkill(manifest, {
|
|
83
|
+
name: skillName,
|
|
84
|
+
source: 'global',
|
|
85
|
+
linked: sym,
|
|
86
|
+
version,
|
|
87
|
+
installedAt: nowISO()
|
|
88
|
+
});
|
|
89
|
+
await writeManifest(manifest, cwd);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function addSkillsInteractive({ cwd = process.cwd(), sym = false } = {}) {
|
|
93
|
+
const skills = await listGlobalSkills();
|
|
94
|
+
if (!skills.length) return { selected: [], cancelled: false };
|
|
95
|
+
const selection = await selectSkillsFromList(skills, {
|
|
96
|
+
title: 'Selecciona skills para instalar',
|
|
97
|
+
requireTTYMessage: 'La selección interactiva requiere una terminal TTY. Usa: skillbase add <skill>.'
|
|
98
|
+
});
|
|
99
|
+
if (selection.cancelled) return { selected: [], cancelled: true };
|
|
100
|
+
const selectedSkills = selection.selected;
|
|
101
|
+
for (const skill of selectedSkills) {
|
|
102
|
+
await addSkill(skill, { cwd, sym });
|
|
103
|
+
}
|
|
104
|
+
output.write(`\nInstaladas: ${selectedSkills.join(', ') || 'ninguna'}\n`);
|
|
105
|
+
return { selected: selectedSkills, cancelled: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function selectSkillsFromList(skills, { title, requireTTYMessage } = {}) {
|
|
109
|
+
if (!input.isTTY || !output.isTTY) {
|
|
110
|
+
throw new Error(requireTTYMessage || 'La selección interactiva requiere una terminal TTY.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const selected = new Set();
|
|
114
|
+
let cursor = 0;
|
|
115
|
+
let offset = 0;
|
|
116
|
+
let renderedLines = 0;
|
|
117
|
+
let firstRender = true;
|
|
118
|
+
|
|
119
|
+
// Tamaño de página adaptativo (mínimo 5, máximo 15 por defecto)
|
|
120
|
+
const getPageSize = () => {
|
|
121
|
+
const terminalRows = output.rows || 24;
|
|
122
|
+
const reservedRows = 8; // Header (5) + Footer (2) + Margen (1)
|
|
123
|
+
return Math.max(5, Math.min(15, terminalRows - reservedRows));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
readline.emitKeypressEvents(input);
|
|
127
|
+
input.resume();
|
|
128
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(true);
|
|
129
|
+
|
|
130
|
+
const render = () => {
|
|
131
|
+
const pageSize = getPageSize();
|
|
132
|
+
|
|
133
|
+
// Ajustar ventana (offset) según el cursor
|
|
134
|
+
if (cursor < offset) {
|
|
135
|
+
offset = cursor;
|
|
136
|
+
} else if (cursor >= offset + pageSize) {
|
|
137
|
+
offset = cursor - pageSize + 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lines = [
|
|
141
|
+
'\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m',
|
|
142
|
+
`\x1b[1m${title || 'Selecciona skills'}\x1b[0m`,
|
|
143
|
+
'\x1b[2m↑/↓ navegar · espacio seleccionar · enter confirmar · esc cancelar\x1b[0m',
|
|
144
|
+
'\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m',
|
|
145
|
+
''
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
// Indicador superior si hay más arriba
|
|
149
|
+
if (offset > 0) {
|
|
150
|
+
lines.push('\x1b[2m ▲ y más...\x1b[0m');
|
|
151
|
+
} else {
|
|
152
|
+
lines.push('');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const visibleSkills = skills.slice(offset, offset + pageSize);
|
|
156
|
+
visibleSkills.forEach((skill, index) => {
|
|
157
|
+
const realIndex = index + offset;
|
|
158
|
+
const isSelected = selected.has(skill);
|
|
159
|
+
const isCursor = realIndex === cursor;
|
|
160
|
+
const mark = isSelected ? '[x]' : '[ ]';
|
|
161
|
+
const pointer = isCursor ? '\x1b[32m❯\x1b[0m' : ' ';
|
|
162
|
+
const line = `${mark} ${skill}`;
|
|
163
|
+
lines.push(isCursor ? `${pointer} \x1b[1m${line}\x1b[0m` : `${pointer} ${line}`);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Rellenar hasta pageSize para mantener altura constante (evita parpadeos)
|
|
167
|
+
for (let i = visibleSkills.length; i < pageSize; i += 1) {
|
|
168
|
+
lines.push('');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Indicador inferior si hay más abajo
|
|
172
|
+
if (offset + pageSize < skills.length) {
|
|
173
|
+
lines.push('\x1b[2m ▼ y más...\x1b[0m');
|
|
174
|
+
} else {
|
|
175
|
+
lines.push('');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!firstRender) {
|
|
179
|
+
readline.moveCursor(output, 0, -renderedLines);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
183
|
+
readline.clearLine(output, 0);
|
|
184
|
+
readline.cursorTo(output, 0);
|
|
185
|
+
output.write(lines[i]);
|
|
186
|
+
if (i < lines.length - 1) output.write('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Limpiar líneas residuales si el tamaño de renderizado cambió (vía redimensionado de terminal)
|
|
190
|
+
if (!firstRender && renderedLines > lines.length) {
|
|
191
|
+
for (let i = lines.length; i < renderedLines; i += 1) {
|
|
192
|
+
output.write('\n');
|
|
193
|
+
readline.clearLine(output, 0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
output.write('\n');
|
|
198
|
+
renderedLines = lines.length + 1;
|
|
199
|
+
firstRender = false;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
render();
|
|
203
|
+
let onKeypress;
|
|
204
|
+
const outcome = await new Promise((resolve) => {
|
|
205
|
+
onKeypress = (_, key) => {
|
|
206
|
+
if (key.name === 'up') {
|
|
207
|
+
cursor = (cursor - 1 + skills.length) % skills.length;
|
|
208
|
+
render();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (key.name === 'down') {
|
|
212
|
+
cursor = (cursor + 1) % skills.length;
|
|
213
|
+
render();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (key.name === 'space') {
|
|
217
|
+
const skill = skills[cursor];
|
|
218
|
+
if (selected.has(skill)) selected.delete(skill);
|
|
219
|
+
else selected.add(skill);
|
|
220
|
+
render();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
224
|
+
resolve({ cancelled: false });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (key.name === 'escape' || (key.name === 'c' && key.ctrl)) {
|
|
228
|
+
resolve({ cancelled: true });
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
input.on('keypress', onKeypress);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (onKeypress) input.off('keypress', onKeypress);
|
|
235
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(false);
|
|
236
|
+
input.pause();
|
|
237
|
+
output.write('\n');
|
|
238
|
+
|
|
239
|
+
if (outcome.cancelled) {
|
|
240
|
+
output.write('\nSelección cancelada.\n');
|
|
241
|
+
return { selected: [], cancelled: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { selected: Array.from(selected), cancelled: false };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function tryFetchJson(url) {
|
|
248
|
+
const response = await fetch(url);
|
|
249
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
250
|
+
return response.json();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseRemoteSkillRef(skillRef) {
|
|
254
|
+
const trimmed = skillRef.trim();
|
|
255
|
+
if (!trimmed) return { canonicalName: '', lookupKeys: [] };
|
|
256
|
+
|
|
257
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
258
|
+
try {
|
|
259
|
+
const url = new URL(trimmed);
|
|
260
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
261
|
+
// Formato esperado en skills.sh: /<owner>/skills/<skill>
|
|
262
|
+
const skillIdx = parts.indexOf('skills');
|
|
263
|
+
if (skillIdx > 0 && parts[skillIdx + 1]) {
|
|
264
|
+
const owner = parts[skillIdx - 1];
|
|
265
|
+
const name = parts[skillIdx + 1];
|
|
266
|
+
const scoped = `${owner}/${name}`;
|
|
267
|
+
return { canonicalName: scoped, lookupKeys: [scoped, name] };
|
|
268
|
+
}
|
|
269
|
+
const last = parts.at(-1) ?? trimmed;
|
|
270
|
+
return { canonicalName: last, lookupKeys: [last] };
|
|
271
|
+
} catch {
|
|
272
|
+
return { canonicalName: trimmed, lookupKeys: [trimmed] };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { canonicalName: trimmed, lookupKeys: [trimmed] };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function fetchRemoteMetadata(skillRef) {
|
|
280
|
+
const parsed = parseRemoteSkillRef(skillRef);
|
|
281
|
+
if (!parsed.lookupKeys.length) {
|
|
282
|
+
throw new Error('Debes indicar una skill remota válida.');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const key of parsed.lookupKeys) {
|
|
286
|
+
const encoded = encodeURIComponent(key);
|
|
287
|
+
const escapedPath = key.split('/').map(encodeURIComponent).join('/');
|
|
288
|
+
const candidates = [
|
|
289
|
+
`https://skills.sh/api/skills/${encoded}.json`,
|
|
290
|
+
`https://skills.sh/${escapedPath}/skill.json`,
|
|
291
|
+
`https://skills.sh/${escapedPath}/skills/${encodeURIComponent(parsed.canonicalName.split('/').at(-1) ?? key)}/skill.json`
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
for (const url of candidates) {
|
|
295
|
+
try {
|
|
296
|
+
const data = await tryFetchJson(url);
|
|
297
|
+
return { ...data, _fetchedFrom: url, _resolvedName: parsed.canonicalName };
|
|
298
|
+
} catch {
|
|
299
|
+
// next
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
throw new Error(
|
|
305
|
+
`No se pudo obtener metadata remota para "${skillRef}". Usa slug tipo "owner/skill" o URL de skills.sh.`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function downloadSkillFromRemote(skillRef, tmpDir) {
|
|
310
|
+
const metadata = await fetchRemoteMetadata(skillRef);
|
|
311
|
+
const resolvedName = metadata._resolvedName || skillRef;
|
|
312
|
+
const localName = resolvedName.split('/').at(-1) || resolvedName;
|
|
313
|
+
const sourceUrl = metadata.downloadUrl || metadata.sourceUrl || metadata.repo || metadata.url;
|
|
314
|
+
|
|
315
|
+
if (!sourceUrl) {
|
|
316
|
+
throw new Error(`La metadata remota de "${skillRef}" no contiene downloadUrl/sourceUrl/repo/url`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (metadata.archiveUrl) {
|
|
320
|
+
const archiveResponse = await fetch(metadata.archiveUrl);
|
|
321
|
+
if (!archiveResponse.ok) throw new Error(`No se pudo descargar archiveUrl (${archiveResponse.status})`);
|
|
322
|
+
const archivePath = path.join(tmpDir, `${localName}.tgz`);
|
|
323
|
+
const buffer = Buffer.from(await archiveResponse.arrayBuffer());
|
|
324
|
+
await fs.writeFile(archivePath, buffer);
|
|
325
|
+
throw new Error('archiveUrl descargado, pero falta extractor .tgz (pendiente de implementación)');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const skillPath = path.join(tmpDir, localName);
|
|
329
|
+
await ensureDir(skillPath);
|
|
330
|
+
await writeJson(path.join(skillPath, 'skill.json'), {
|
|
331
|
+
name: resolvedName,
|
|
332
|
+
version: metadata.version ?? '0.0.0',
|
|
333
|
+
sourceUrl,
|
|
334
|
+
fetchedFrom: metadata._fetchedFrom
|
|
335
|
+
});
|
|
336
|
+
return { path: skillPath, metadata, localName, resolvedName };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function installRemoteSkill(skillName, { cwd = process.cwd(), force = false } = {}) {
|
|
340
|
+
const projectSkillsDir = getProjectSkillsDir(cwd);
|
|
341
|
+
await ensureDir(projectSkillsDir);
|
|
342
|
+
|
|
343
|
+
if (/^https?:\/\/github\.com\//i.test(skillName)) {
|
|
344
|
+
throw new Error('Para instalar desde GitHub usa: skillbase install <repo-url> --remote --skill <nombre-skill>.');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const tempBase = await fs.mkdtemp(path.join(os.tmpdir(), 'skillbase-'));
|
|
348
|
+
try {
|
|
349
|
+
const downloaded = await downloadSkillFromRemote(skillName, tempBase);
|
|
350
|
+
const target = path.join(projectSkillsDir, downloaded.localName);
|
|
351
|
+
if ((await exists(target)) && !force) {
|
|
352
|
+
throw new Error(`La skill "${downloaded.localName}" ya existe en el proyecto. Usa --force para reinstalar.`);
|
|
353
|
+
}
|
|
354
|
+
if (await exists(target)) await fs.rm(target, { recursive: true, force: true });
|
|
355
|
+
await copyDir(downloaded.path, target);
|
|
356
|
+
|
|
357
|
+
const manifest = await readManifest(cwd);
|
|
358
|
+
upsertSkill(manifest, {
|
|
359
|
+
name: downloaded.resolvedName,
|
|
360
|
+
localName: downloaded.localName,
|
|
361
|
+
source: 'remote',
|
|
362
|
+
linked: false,
|
|
363
|
+
version: downloaded.metadata.version ?? '0.0.0',
|
|
364
|
+
remoteUrl: downloaded.metadata.sourceUrl || downloaded.metadata.repo || downloaded.metadata.url || null,
|
|
365
|
+
installedAt: nowISO()
|
|
366
|
+
});
|
|
367
|
+
await writeManifest(manifest, cwd);
|
|
368
|
+
} finally {
|
|
369
|
+
await fs.rm(tempBase, { recursive: true, force: true });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function installRemoteFromGitHub(repoUrl, selectedSkill, { cwd = process.cwd(), force = false } = {}) {
|
|
374
|
+
if (!selectedSkill) {
|
|
375
|
+
throw new Error('Falta --skill <nombre>. Ejemplo: skillbase install <repo-url> --remote --skill find-skills');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const projectSkillsDir = getProjectSkillsDir(cwd);
|
|
379
|
+
await ensureDir(projectSkillsDir);
|
|
380
|
+
|
|
381
|
+
const tempBase = await fs.mkdtemp(path.join(os.tmpdir(), 'skillbase-git-'));
|
|
382
|
+
const repoPath = path.join(tempBase, 'repo');
|
|
383
|
+
try {
|
|
384
|
+
await execFileAsync('git', ['clone', '--depth', '1', repoUrl, repoPath], { maxBuffer: 1024 * 1024 * 10 });
|
|
385
|
+
const candidates = [path.join(repoPath, 'skills', selectedSkill), path.join(repoPath, selectedSkill)];
|
|
386
|
+
let sourcePath = null;
|
|
387
|
+
for (const candidate of candidates) {
|
|
388
|
+
if (await exists(candidate)) {
|
|
389
|
+
sourcePath = candidate;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!sourcePath) {
|
|
394
|
+
throw new Error(`No se encontró la skill "${selectedSkill}" en el repo remoto.`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const target = path.join(projectSkillsDir, selectedSkill);
|
|
398
|
+
if ((await exists(target)) && !force) {
|
|
399
|
+
throw new Error(`La skill "${selectedSkill}" ya existe en el proyecto. Usa --force para reinstalar.`);
|
|
400
|
+
}
|
|
401
|
+
if (await exists(target)) await fs.rm(target, { recursive: true, force: true });
|
|
402
|
+
await copyDir(sourcePath, target);
|
|
403
|
+
|
|
404
|
+
const manifest = await readManifest(cwd);
|
|
405
|
+
const version = await readSkillVersion(sourcePath);
|
|
406
|
+
upsertSkill(manifest, {
|
|
407
|
+
name: selectedSkill,
|
|
408
|
+
source: 'remote',
|
|
409
|
+
linked: false,
|
|
410
|
+
version: version ?? '0.0.0',
|
|
411
|
+
remoteUrl: repoUrl,
|
|
412
|
+
installedAt: nowISO()
|
|
413
|
+
});
|
|
414
|
+
await writeManifest(manifest, cwd);
|
|
415
|
+
} finally {
|
|
416
|
+
await fs.rm(tempBase, { recursive: true, force: true });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function installRemoteSkillRef(skillRef, options = {}) {
|
|
421
|
+
if (/^https?:\/\/github\.com\//i.test(skillRef)) {
|
|
422
|
+
return installRemoteFromGitHub(skillRef, options.skill, options);
|
|
423
|
+
}
|
|
424
|
+
return installRemoteSkill(skillRef, options);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function installFromManifest({ cwd = process.cwd(), remote = false, force = false } = {}) {
|
|
428
|
+
const manifest = await readManifest(cwd);
|
|
429
|
+
if (!manifest.skills.length) {
|
|
430
|
+
throw new Error('No hay skills en skillbase.json. Usa "skillbase add <skill>" o "skillbase install <skill> --remote".');
|
|
431
|
+
}
|
|
432
|
+
for (const skill of manifest.skills) {
|
|
433
|
+
if (remote || skill.source === 'remote') await installRemoteSkill(skill.name, { cwd, force });
|
|
434
|
+
else await addSkill(skill.name, { cwd, sym: Boolean(skill.linked) });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function removeSkill(skillName, { cwd = process.cwd(), global = false } = {}) {
|
|
439
|
+
if (global) {
|
|
440
|
+
await fs.rm(path.join(getGlobalSkillsDir(), skillName), { recursive: true, force: true });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await fs.rm(path.join(getProjectSkillsDir(cwd), skillName), { recursive: true, force: true });
|
|
445
|
+
const manifest = await readManifest(cwd);
|
|
446
|
+
removeSkillFromManifest(manifest, skillName);
|
|
447
|
+
await writeManifest(manifest, cwd);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function getRemoteVersion(skillName) {
|
|
451
|
+
const metadata = await fetchRemoteMetadata(skillName);
|
|
452
|
+
return metadata.version ?? null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export async function checkUpdates({ cwd = process.cwd(), remoteOnly = false } = {}) {
|
|
456
|
+
const manifest = await readManifest(cwd);
|
|
457
|
+
const updates = [];
|
|
458
|
+
|
|
459
|
+
for (const skill of manifest.skills) {
|
|
460
|
+
if (remoteOnly && skill.source !== 'remote') continue;
|
|
461
|
+
|
|
462
|
+
if (skill.source === 'remote') {
|
|
463
|
+
const latest = await getRemoteVersion(skill.name);
|
|
464
|
+
if (latest && compareVersion(latest, skill.version) > 0) {
|
|
465
|
+
updates.push({ name: skill.name, current: skill.version, latest, source: 'remote' });
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const globalPath = path.join(getGlobalSkillsDir(), skill.name);
|
|
471
|
+
if (!(await exists(globalPath))) continue;
|
|
472
|
+
const latest = await readSkillVersion(globalPath);
|
|
473
|
+
if (latest && compareVersion(latest, skill.version) > 0) {
|
|
474
|
+
updates.push({ name: skill.name, current: skill.version, latest, source: 'global' });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return updates;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export async function updateSkills({ cwd = process.cwd(), skillName = null, remoteOnly = false } = {}) {
|
|
482
|
+
const manifest = await readManifest(cwd);
|
|
483
|
+
const selected = manifest.skills.filter((skill) => {
|
|
484
|
+
if (skillName && skill.name !== skillName) return false;
|
|
485
|
+
if (remoteOnly && skill.source !== 'remote') return false;
|
|
486
|
+
return true;
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
for (const skill of selected) {
|
|
490
|
+
if (skill.source === 'remote') {
|
|
491
|
+
await installRemoteSkill(skill.name, { cwd, force: true });
|
|
492
|
+
} else {
|
|
493
|
+
await addSkill(skill.name, { cwd, sym: Boolean(skill.linked) });
|
|
494
|
+
const globalVersion = await readSkillVersion(path.join(getGlobalSkillsDir(), skill.name));
|
|
495
|
+
const nextManifest = await readManifest(cwd);
|
|
496
|
+
upsertSkill(nextManifest, { ...skill, version: globalVersion || skill.version, installedAt: nowISO() });
|
|
497
|
+
await writeManifest(nextManifest, cwd);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function getSkillTags(skillDir) {
|
|
503
|
+
const metaPath = path.join(skillDir, 'skill.json');
|
|
504
|
+
if (!(await exists(metaPath))) return [];
|
|
505
|
+
try {
|
|
506
|
+
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
|
|
507
|
+
if (Array.isArray(meta.tags)) return meta.tags.map((tag) => String(tag).toLowerCase());
|
|
508
|
+
if (typeof meta.tags === 'string') return meta.tags.split(',').map((tag) => tag.trim().toLowerCase()).filter(Boolean);
|
|
509
|
+
return [];
|
|
510
|
+
} catch {
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export async function initProject({ cwd = process.cwd(), hard = false } = {}) {
|
|
516
|
+
const technologies = await detectProjectTechnologies(cwd, { hard });
|
|
517
|
+
if (!technologies.length) return { technologies: [], suggested: [], installed: [], cancelled: false };
|
|
518
|
+
|
|
519
|
+
const globalSkills = await listGlobalSkills();
|
|
520
|
+
const suggested = [];
|
|
521
|
+
for (const skill of globalSkills) {
|
|
522
|
+
const skillLower = skill.toLowerCase();
|
|
523
|
+
const matchesByName = technologies.some((tech) => skillLower.includes(tech));
|
|
524
|
+
if (matchesByName) {
|
|
525
|
+
suggested.push(skill);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (!hard) continue;
|
|
529
|
+
const tags = await getSkillTags(path.join(getGlobalSkillsDir(), skill));
|
|
530
|
+
if (tags.some((tag) => technologies.some((tech) => tag.includes(tech)))) {
|
|
531
|
+
suggested.push(skill);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!suggested.length) return { technologies, suggested: [], installed: [], cancelled: false };
|
|
536
|
+
|
|
537
|
+
const selection = await selectSkillsFromList(suggested, {
|
|
538
|
+
title: hard
|
|
539
|
+
? 'Init --hard: selecciona skills recomendadas por nombre y tags'
|
|
540
|
+
: 'Init: selecciona skills recomendadas por nombre'
|
|
541
|
+
});
|
|
542
|
+
if (selection.cancelled) return { technologies, suggested, installed: [], cancelled: true };
|
|
543
|
+
|
|
544
|
+
for (const skill of selection.selected) {
|
|
545
|
+
await addSkill(skill, { cwd, sym: false });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return { technologies, suggested, installed: selection.selected, cancelled: false };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export async function migrateAgentsSkillsToSkillbase({ cwd = process.cwd(), force = false, fromProject = false } = {}) {
|
|
552
|
+
const globalDir = getGlobalSkillsDir();
|
|
553
|
+
await ensureDir(globalDir);
|
|
554
|
+
|
|
555
|
+
const agentsDir = fromProject ? path.join(getProjectRoot(cwd), PROJECT_AGENTS_DIR) : path.join(os.homedir(), '.agents');
|
|
556
|
+
const agentsSkillsDir = path.join(agentsDir, PROJECT_SKILLS_DIR);
|
|
557
|
+
const toMigrate = new Map();
|
|
558
|
+
|
|
559
|
+
if (await exists(agentsSkillsDir)) {
|
|
560
|
+
const entries = await fs.readdir(agentsSkillsDir, { withFileTypes: true });
|
|
561
|
+
for (const entry of entries) {
|
|
562
|
+
if (entry.isDirectory()) toMigrate.set(entry.name, path.join(agentsSkillsDir, entry.name));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const migrated = [];
|
|
567
|
+
const skipped = [];
|
|
568
|
+
|
|
569
|
+
for (const [skillName, sourcePath] of toMigrate.entries()) {
|
|
570
|
+
const targetPath = path.join(globalDir, skillName);
|
|
571
|
+
if ((await exists(targetPath)) && !force) {
|
|
572
|
+
skipped.push(skillName);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (await exists(targetPath)) await fs.rm(targetPath, { recursive: true, force: true });
|
|
576
|
+
await copyDir(sourcePath, targetPath);
|
|
577
|
+
migrated.push(skillName);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return { migrated, skipped, totalFound: toMigrate.size, sourceRoot: agentsDir };
|
|
581
|
+
}
|
package/src/manifest.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { getManifestPath } from './config.js';
|
|
3
|
+
|
|
4
|
+
async function exists(target) {
|
|
5
|
+
try {
|
|
6
|
+
await fs.access(target);
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function safeReadJson(file, fallback) {
|
|
14
|
+
if (!(await exists(file))) return fallback;
|
|
15
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function writeJson(file, data) {
|
|
20
|
+
await fs.writeFile(file, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function readManifest(cwd = process.cwd()) {
|
|
24
|
+
const manifest = await safeReadJson(getManifestPath(cwd), { skills: [] });
|
|
25
|
+
if (!Array.isArray(manifest.skills)) manifest.skills = [];
|
|
26
|
+
return manifest;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function writeManifest(manifest, cwd = process.cwd()) {
|
|
30
|
+
await writeJson(getManifestPath(cwd), manifest);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function upsertSkill(manifest, record) {
|
|
34
|
+
const idx = manifest.skills.findIndex((skill) => skill.name === record.name);
|
|
35
|
+
if (idx === -1) manifest.skills.push(record);
|
|
36
|
+
else manifest.skills[idx] = { ...manifest.skills[idx], ...record };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function removeSkillFromManifest(manifest, skillName) {
|
|
40
|
+
manifest.skills = manifest.skills.filter((skill) => skill.name !== skillName);
|
|
41
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
async function exists(target) {
|
|
5
|
+
try {
|
|
6
|
+
await fs.access(target);
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readJsonIfExists(file) {
|
|
14
|
+
if (!(await exists(file))) return null;
|
|
15
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readTextIfExists(file) {
|
|
19
|
+
if (!(await exists(file))) return '';
|
|
20
|
+
return fs.readFile(file, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detecta tecnologías del proyecto. En modo hard hace una inspección más profunda.
|
|
25
|
+
*/
|
|
26
|
+
export async function detectProjectTechnologies(cwd = process.cwd(), { hard = false } = {}) {
|
|
27
|
+
const technologies = new Set();
|
|
28
|
+
|
|
29
|
+
const add = (...values) => values.filter(Boolean).forEach((value) => technologies.add(String(value).toLowerCase()));
|
|
30
|
+
const packageJson = await readJsonIfExists(path.join(cwd, 'package.json'));
|
|
31
|
+
if (packageJson) {
|
|
32
|
+
add('javascript', 'node', 'npm');
|
|
33
|
+
const deps = {
|
|
34
|
+
...(packageJson.dependencies ?? {}),
|
|
35
|
+
...(packageJson.devDependencies ?? {})
|
|
36
|
+
};
|
|
37
|
+
if (deps.react) add('react');
|
|
38
|
+
if (deps.vue) add('vue');
|
|
39
|
+
if (deps.svelte) add('svelte');
|
|
40
|
+
if (deps.next) add('nextjs', 'next');
|
|
41
|
+
if (deps.express || deps.fastify || deps.koa) add('api', 'backend');
|
|
42
|
+
if (deps.typescript) add('typescript', 'ts');
|
|
43
|
+
if (deps.tailwindcss) add('tailwind', 'css');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (await exists(path.join(cwd, 'requirements.txt'))) add('python');
|
|
47
|
+
if (await exists(path.join(cwd, 'pyproject.toml'))) add('python');
|
|
48
|
+
if (await exists(path.join(cwd, 'go.mod'))) add('go', 'golang');
|
|
49
|
+
if (await exists(path.join(cwd, 'Cargo.toml'))) add('rust');
|
|
50
|
+
if (await exists(path.join(cwd, 'Dockerfile'))) add('docker', 'devops');
|
|
51
|
+
if (await exists(path.join(cwd, 'docker-compose.yml'))) add('docker', 'compose', 'devops');
|
|
52
|
+
|
|
53
|
+
if (hard) {
|
|
54
|
+
const readme = await readTextIfExists(path.join(cwd, 'README.md'));
|
|
55
|
+
const content = readme.toLowerCase();
|
|
56
|
+
if (content.includes('kubernetes') || content.includes('k8s')) add('kubernetes', 'k8s');
|
|
57
|
+
if (content.includes('postgres')) add('postgres', 'postgresql');
|
|
58
|
+
if (content.includes('mysql')) add('mysql');
|
|
59
|
+
if (content.includes('mongodb')) add('mongodb');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Array.from(technologies);
|
|
63
|
+
}
|