@basetisia/skill-manager 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/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # @basetis/skill-manager
2
+
3
+ CLI para gestionar skills de IA (prompts reutilizables) e instalarlas en múltiples herramientas de coding: Claude Code, Gemini CLI, Codex CLI, Cursor y Windsurf.
4
+
5
+ Las skills se almacenan en un repositorio GitLab centralizado (`git@git.basetis.com:okr-ia/basetisskills.git`) con un catálogo definido en `manifest.yml`. El CLI descarga las skills desde ese catálogo y las coloca en el formato y ruta correctos para cada herramienta.
6
+
7
+ ## Prerequisitos
8
+
9
+ - **Node.js >= 24** — verifica con `node --version`
10
+ - **Token de acceso personal de GitLab** con scope `read_package_registry` en `git.basetis.com`
11
+
12
+ ## Setup para developer nuevo
13
+
14
+ La forma más rápida de instalar es ejecutar el script de setup:
15
+
16
+ ```bash
17
+ bash <(curl -fsSL https://git.basetis.com/okr-ia/basetisskills/-/raw/main/cli/scripts/setup.sh)
18
+ ```
19
+
20
+ El script te pedirá tu token personal de GitLab, configurará npm y instalará el CLI globalmente.
21
+
22
+ ### Generar el token de GitLab
23
+
24
+ 1. Ve a [git.basetis.com → Settings → Access Tokens](https://git.basetis.com/-/user_settings/personal_access_tokens)
25
+ 2. Crea un token con el scope: **read_package_registry**
26
+ 3. Copia el token (empieza por `glpat-...`)
27
+
28
+ ### Instalación manual
29
+
30
+ Si prefieres configurarlo a mano, añade a tu `~/.npmrc`:
31
+
32
+ ```ini
33
+ @basetis:registry=https://git.basetis.com/api/v4/packages/npm/
34
+ //git.basetis.com/api/v4/packages/npm/:_authToken=TU_GITLAB_TOKEN
35
+ ```
36
+
37
+ Y luego:
38
+
39
+ ```bash
40
+ npm install -g @basetis/skill-manager
41
+ ```
42
+
43
+ Verifica la instalación:
44
+
45
+ ```bash
46
+ skill-manager --version
47
+ # 0.1.0
48
+ ```
49
+
50
+ ## Primeros pasos
51
+
52
+ ### 1. Configurar el repositorio de skills
53
+
54
+ ```bash
55
+ skill-manager init
56
+ ```
57
+
58
+ ```
59
+ ? Tipo de repositorio de skills: › GitLab
60
+ ? URL base del servidor: › https://git.basetis.com
61
+ ? Project ID (numérico) o ruta (grupo/repo): › okr-ia/basetisskills
62
+ ? Token de acceso personal: › ****
63
+ ? Rama del repositorio: › main
64
+ ? Herramienta de IA por defecto: › Claude Code
65
+
66
+ ✔ Conexión verificada correctamente.
67
+ ✔ Configuración guardada correctamente.
68
+ → ~/.skill-manager/config.json
69
+ ```
70
+
71
+ ### 2. Explorar el catálogo
72
+
73
+ ```bash
74
+ skill-manager list
75
+ ```
76
+
77
+ ```
78
+ User Skills
79
+ ────────────────────────────────────────
80
+ user/development/kotlin-jvm v1.0.0
81
+ user/development/react v1.0.0
82
+
83
+ Internal Skills
84
+ ────────────────────────────────────────
85
+ internal/global v2.0.0
86
+
87
+ External Skills
88
+ ────────────────────────────────────────
89
+ external/sector/pharma v1.0.0
90
+ external/client/alexion v1.2.0 ← external/sector/pharma
91
+ external/project/alexion-cockpit v2.0.0 ← external/client/alexion
92
+
93
+ Total: 5 skills
94
+ ```
95
+
96
+ Filtrar por nombre:
97
+
98
+ ```bash
99
+ skill-manager list --filter alexion
100
+ ```
101
+
102
+ ### 3. Instalar una skill
103
+
104
+ ```bash
105
+ skill-manager install external/project/alexion-cockpit
106
+ ```
107
+
108
+ ```
109
+ ✔ Cadena resuelta: 3 skill(s).
110
+
111
+ Skills a instalar (en orden):
112
+ ├─ external/sector/pharma v1.0.0
113
+ ├─ external/client/alexion v1.2.0
114
+ └─ external/project/alexion-cockpit v2.0.0
115
+
116
+ ? ¿En qué herramientas quieres instalar? › Claude Code, Cursor
117
+ ? ¿Dónde instalar las skills? › Global (solo para ti, en ~/)
118
+
119
+ ✔ Instalada external/sector/pharma
120
+ ✔ Instalada external/client/alexion
121
+ ✔ Instalada external/project/alexion-cockpit
122
+
123
+ ✔ 3 skill(s) instalada(s) en Claude Code y Cursor (personal)
124
+ ```
125
+
126
+ Instalación directa sin prompts:
127
+
128
+ ```bash
129
+ skill-manager install external/client/alexion --tool claude,windsurf --scope global
130
+ ```
131
+
132
+ ### 4. Ver estado de las skills instaladas
133
+
134
+ ```bash
135
+ skill-manager status
136
+ ```
137
+
138
+ ```
139
+ Claude Code
140
+ ──────────────────────────────────────────────────
141
+ [Global]
142
+ external/sector/pharma 1.0.0 ✓ al día
143
+ external/client/alexion 1.1.0 ⬆ 1.2.0 disponible
144
+ external/project/alexion-cockpit 2.0.0 ✓ al día
145
+ ```
146
+
147
+ ### 5. Detectar skills recomendadas automáticamente
148
+
149
+ ```bash
150
+ cd mi-proyecto-kotlin/
151
+ skill-manager detect
152
+ ```
153
+
154
+ ```
155
+ ✔ Análisis completo.
156
+
157
+ Skills recomendadas:
158
+
159
+ internal/global v2.0.0
160
+ → Skill base recomendada para todos los proyectos.
161
+ user/development/kotlin-jvm v1.0.0
162
+ → Detectado: kotlin, jvm, gradle en el proyecto.
163
+
164
+ ? ¿Instalar las skills recomendadas? › Sí
165
+ ```
166
+
167
+ ## Referencia de comandos
168
+
169
+ | Comando | Argumentos | Descripción |
170
+ |---|---|---|
171
+ | `skill-manager init` | — | Configura el repositorio de skills y herramienta por defecto |
172
+ | `skill-manager list` | `--filter <texto>` | Lista skills del catálogo, agrupadas por sección |
173
+ | `skill-manager install <ruta>` | `--tool <herramienta>` `--scope <global\|project>` | Instala una skill y toda su cadena de dependencias |
174
+ | `skill-manager status` | — | Compara versiones instaladas vs catálogo para cada herramienta |
175
+ | `skill-manager detect` | — | Analiza el proyecto actual y recomienda skills relevantes |
176
+
177
+ ### Opciones globales
178
+
179
+ | Opción | Descripción |
180
+ |---|---|
181
+ | `--version`, `-V` | Muestra la versión del CLI |
182
+ | `--help`, `-h` | Muestra la ayuda general o de un comando |
183
+
184
+ ## Herramientas soportadas
185
+
186
+ | Herramienta | Personal (global) | Proyecto (local) |
187
+ |---|---|---|
188
+ | Claude Code | `~/.claude/skills/{path}/SKILL.md` | `.claude/skills/{path}/SKILL.md` |
189
+ | Gemini CLI | `~/.gemini/skills/{path}/SKILL.md` | `.agents/skills/{path}/SKILL.md` |
190
+ | Codex CLI | `~/.codex/{name}/SKILL.md` | `.agents/skills/{path}/SKILL.md` |
191
+ | Cursor | `~/.cursor/rules/{name}.mdc` | `.cursor/rules/{name}.mdc` |
192
+ | Windsurf | `~/.windsurf/global_rules.md` (append) | `.windsurf/rules/{name}.md` |
193
+
194
+ ## Cómo añadir soporte para una nueva herramienta
195
+
196
+ 1. Crea un nuevo archivo en `src/adapters/<herramienta>.js` exportando:
197
+
198
+ ```javascript
199
+ export const toolName = "mi-herramienta";
200
+ export const displayName = "Mi Herramienta";
201
+
202
+ export function getInstallPath(skillPath, scope) {
203
+ // Devuelve la ruta donde se instalará el SKILL.md
204
+ }
205
+
206
+ export async function install(skillPath, skillContent, scope) {
207
+ // Escribe el contenido en la ruta correcta
208
+ // Transforma el formato si es necesario (como Cursor con .mdc)
209
+ }
210
+
211
+ export async function getInstalledSkills(scope) {
212
+ // Devuelve [{ path, version }] leyendo los archivos instalados
213
+ }
214
+ ```
215
+
216
+ 2. Regístralo en `src/adapters/index.js`:
217
+
218
+ ```javascript
219
+ import * as miHerramienta from "./mi-herramienta.js";
220
+
221
+ const adapters = [claude, gemini, codex, cursor, windsurf, miHerramienta];
222
+ ```
223
+
224
+ 3. Añade la detección en `detectInstalledTools()` del mismo archivo.
225
+
226
+ 4. Escribe tests en `src/adapters/adapters.test.js` siguiendo el patrón existente.
227
+
228
+ ## Cómo contribuir al catálogo de skills
229
+
230
+ Las skills se gestionan en el repositorio [okr-ia/basetisskills](https://git.basetis.com/okr-ia/basetisskills).
231
+
232
+ Consulta el archivo `CONTRIBUTING.md` de ese repositorio para conocer:
233
+
234
+ - Estructura de carpetas y convenciones de naming
235
+ - Formato del `manifest.yml`
236
+ - Cómo escribir un `SKILL.md` con frontmatter válido
237
+ - Flujo de review y merge para nuevas skills
238
+ - Cómo usar `parent` y `upstream` para cadenas de herencia
239
+
240
+ ## Licencia
241
+
242
+ Uso interno Basetis.
package/bin/index.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { init } from "../src/commands/init.js";
5
+ import { list } from "../src/commands/list.js";
6
+ import { install } from "../src/commands/install.js";
7
+ import { status } from "../src/commands/status.js";
8
+ import { detect } from "../src/commands/detect.js";
9
+ import { login } from "../src/commands/login.js";
10
+ import { logout } from "../src/commands/logout.js";
11
+ import { whoami } from "../src/commands/whoami.js";
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name("skill-manager")
17
+ .description("CLI para gestionar skills de IA en tu editor")
18
+ .version("0.1.0");
19
+
20
+ program
21
+ .command("init")
22
+ .description("Configura el repositorio de skills y la herramienta por defecto")
23
+ .action(async () => {
24
+ try {
25
+ await init();
26
+ } catch (err) {
27
+ console.error(err.message);
28
+ process.exit(1);
29
+ }
30
+ });
31
+
32
+ program
33
+ .command("list")
34
+ .description("Lista todas las skills disponibles en el catálogo")
35
+ .option("-f, --filter <texto>", "Filtra skills por nombre")
36
+ .action(async (options) => {
37
+ try {
38
+ await list(options);
39
+ } catch (err) {
40
+ console.error(err.message);
41
+ process.exit(1);
42
+ }
43
+ });
44
+
45
+ program
46
+ .command("install <ruta>")
47
+ .description("Instala una skill y sus dependencias (cadena de parents)")
48
+ .option("-t, --tool <herramienta>", "Herramienta(s) destino, separadas por coma (claude,cursor,gemini,codex,windsurf)")
49
+ .option("-s, --scope <ámbito>", "Ámbito de instalación: global o project")
50
+ .action(async (ruta, options) => {
51
+ try {
52
+ await install(ruta, options);
53
+ } catch (err) {
54
+ console.error(err.message);
55
+ process.exit(1);
56
+ }
57
+ });
58
+
59
+ program
60
+ .command("status")
61
+ .description("Muestra el estado de las skills instaladas vs el catálogo")
62
+ .action(async () => {
63
+ try {
64
+ await status();
65
+ } catch (err) {
66
+ console.error(err.message);
67
+ process.exit(1);
68
+ }
69
+ });
70
+
71
+ program
72
+ .command("detect")
73
+ .description("Analiza el proyecto actual y recomienda skills relevantes")
74
+ .action(async () => {
75
+ try {
76
+ await detect();
77
+ } catch (err) {
78
+ console.error(err.message);
79
+ process.exit(1);
80
+ }
81
+ });
82
+
83
+ program
84
+ .command("login")
85
+ .description("Inicia sesión con tu cuenta de Basetis (Google)")
86
+ .action(async () => {
87
+ try {
88
+ await login();
89
+ } catch (err) {
90
+ console.error(err.message);
91
+ process.exit(1);
92
+ }
93
+ });
94
+
95
+ program
96
+ .command("logout")
97
+ .description("Cierra la sesión actual")
98
+ .action(async () => {
99
+ try {
100
+ await logout();
101
+ } catch (err) {
102
+ console.error(err.message);
103
+ process.exit(1);
104
+ }
105
+ });
106
+
107
+ program
108
+ .command("whoami")
109
+ .description("Muestra el usuario autenticado")
110
+ .action(async () => {
111
+ try {
112
+ await whoami();
113
+ } catch (err) {
114
+ console.error(err.message);
115
+ process.exit(1);
116
+ }
117
+ });
118
+
119
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@basetisia/skill-manager",
3
+ "version": "0.1.0",
4
+ "description": "CLI to manage AI coding skills across editors",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
9
+ "bin": {
10
+ "skill-manager": "./bin/index.js"
11
+ },
12
+ "scripts": {
13
+ "test": "node --test",
14
+ "start": "node bin/index.js"
15
+ },
16
+ "publishConfig": {
17
+ "registry": "https://registry.npmjs.org/",
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "chalk": "^5",
22
+ "commander": "^13",
23
+ "inquirer": "^12",
24
+ "js-yaml": "^4",
25
+ "ora": "^8"
26
+ }
27
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Directorio raíz del CLI (un nivel arriba de scripts/)
5
+ CLI_DIR="$(cd "$(dirname "$0")/.." && pwd)"
6
+
7
+ echo "📦 @basetis/skill-manager — publish (GitLab Package Registry)"
8
+ echo "───────────────────────────────────────────────────────────────"
9
+
10
+ # 1. Verificar token de GitLab
11
+ if [[ -z "${GITLAB_TOKEN:-}" ]]; then
12
+ echo "❌ Error: Define GITLAB_TOKEN antes de publicar."
13
+ echo " Ejemplo: export GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx"
14
+ echo ""
15
+ echo " Genera uno en: git.basetis.com → Settings → Access Tokens"
16
+ echo " Scopes necesarios: api"
17
+ exit 1
18
+ fi
19
+ echo "✔ GITLAB_TOKEN configurado."
20
+
21
+ # 2. Ejecutar tests
22
+ echo "⏳ Ejecutando tests..."
23
+ cd "$CLI_DIR"
24
+ npm run test
25
+ echo "✔ Tests pasados."
26
+
27
+ # 3. Publicar paquete
28
+ echo "⏳ Publicando en GitLab Package Registry..."
29
+ npm publish
30
+
31
+ # 4. Confirmar publicación
32
+ VERSION=$(node -p "require('./package.json').version")
33
+ echo ""
34
+ echo "───────────────────────────────────────────────────────────────"
35
+ echo "✔ Publicado @basetis/skill-manager@${VERSION}"
36
+ echo " Registry: https://git.basetis.com/okr-ia/basetisskills/-/packages"
37
+ echo " Instalar: npm install -g @basetis/skill-manager"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ echo "🔧 Setup @basetis/skill-manager"
5
+ echo "────────────────────────────────"
6
+ echo ""
7
+ echo "Necesitas un token personal de GitLab con scope read_package_registry."
8
+ echo ""
9
+ echo " 1. Ve a: https://git.basetis.com/-/user_settings/personal_access_tokens"
10
+ echo " 2. Crea un token con el scope: read_package_registry"
11
+ echo " 3. Copia el token (empieza por glpat-...)"
12
+ echo ""
13
+
14
+ read -rp "Pega tu GitLab token: " GITLAB_TOKEN
15
+
16
+ if [[ -z "$GITLAB_TOKEN" ]]; then
17
+ echo "❌ Token vacío. Abortando."
18
+ exit 1
19
+ fi
20
+
21
+ # Escribir configuración en ~/.npmrc
22
+ NPMRC="$HOME/.npmrc"
23
+
24
+ # Eliminar líneas anteriores de @basetis si existen
25
+ if [[ -f "$NPMRC" ]]; then
26
+ grep -v '@basetis:registry' "$NPMRC" | grep -v '//git.basetis.com/api/v4/packages/npm/:_authToken' > "$NPMRC.tmp" || true
27
+ mv "$NPMRC.tmp" "$NPMRC"
28
+ fi
29
+
30
+ echo "@basetis:registry=https://git.basetis.com/api/v4/packages/npm/" >> "$NPMRC"
31
+ echo "//git.basetis.com/api/v4/packages/npm/:_authToken=${GITLAB_TOKEN}" >> "$NPMRC"
32
+
33
+ echo "✔ ~/.npmrc configurado."
34
+
35
+ # Instalar el CLI globalmente
36
+ echo "⏳ Instalando @basetis/skill-manager..."
37
+ npm install -g @basetis/skill-manager
38
+
39
+ # Verificar instalación
40
+ echo ""
41
+ if skill-manager --version &>/dev/null; then
42
+ VERSION=$(skill-manager --version)
43
+ echo "────────────────────────────────"
44
+ echo "✔ skill-manager ${VERSION} instalado correctamente."
45
+ echo " Ejecuta: skill-manager init"
46
+ else
47
+ echo "❌ Error: skill-manager no se encontró en el PATH."
48
+ echo " Verifica que la carpeta global de npm está en tu PATH."
49
+ exit 1
50
+ fi
@@ -0,0 +1,175 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, rm, readFile, realpath } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ const SKILL_CONTENT = `---
8
+ name: test-skill
9
+ version: "1.0.0"
10
+ description: A test skill
11
+ ---
12
+ You are a test skill.
13
+ `;
14
+
15
+ let tmpHome, tmpCwd, origHome, origCwd;
16
+
17
+ async function setup() {
18
+ tmpHome = await realpath(await mkdtemp(join(tmpdir(), "sm-home-")));
19
+ tmpCwd = await realpath(await mkdtemp(join(tmpdir(), "sm-cwd-")));
20
+ origHome = process.env.HOME;
21
+ origCwd = process.cwd();
22
+ process.env.HOME = tmpHome;
23
+ process.chdir(tmpCwd);
24
+ }
25
+
26
+ async function teardown() {
27
+ process.env.HOME = origHome;
28
+ process.chdir(origCwd);
29
+ await rm(tmpHome, { recursive: true, force: true });
30
+ await rm(tmpCwd, { recursive: true, force: true });
31
+ }
32
+
33
+ // Re-import adapters fresh so homeDir/cwdDir pick up the new env
34
+ async function loadAdapter(name) {
35
+ const mod = await import(`./${name}.js?t=${Date.now()}`);
36
+ return mod;
37
+ }
38
+
39
+ describe("claude adapter", () => {
40
+ beforeEach(setup);
41
+ afterEach(teardown);
42
+
43
+ it("installs to personal scope at ~/.claude/skills/", async () => {
44
+ const claude = await loadAdapter("claude");
45
+ const dest = await claude.install("external/sector/pharma", SKILL_CONTENT, "personal");
46
+ assert.equal(dest, join(tmpHome, ".claude", "skills", "external", "sector", "pharma", "SKILL.md"));
47
+ const content = await readFile(dest, "utf-8");
48
+ assert.ok(content.includes("test skill"));
49
+ });
50
+
51
+ it("installs to project scope at .claude/skills/", async () => {
52
+ const claude = await loadAdapter("claude");
53
+ const dest = await claude.install("internal/tooling/eslint", SKILL_CONTENT, "project");
54
+ assert.equal(dest, join(tmpCwd, ".claude", "skills", "internal", "tooling", "eslint", "SKILL.md"));
55
+ });
56
+
57
+ it("getInstallPath returns correct path", async () => {
58
+ const claude = await loadAdapter("claude");
59
+ const p = claude.getInstallPath("a/b/c", "personal");
60
+ assert.ok(p.endsWith(join(".claude", "skills", "a", "b", "c", "SKILL.md")));
61
+ });
62
+
63
+ it("getInstalledSkills reads back installed skills", async () => {
64
+ const claude = await loadAdapter("claude");
65
+ await claude.install("sector/pharma", SKILL_CONTENT, "personal");
66
+ const skills = await claude.getInstalledSkills("personal");
67
+ assert.equal(skills.length, 1);
68
+ assert.equal(skills[0].path, "sector/pharma");
69
+ assert.equal(skills[0].version, "1.0.0");
70
+ });
71
+ });
72
+
73
+ describe("gemini adapter", () => {
74
+ beforeEach(setup);
75
+ afterEach(teardown);
76
+
77
+ it("installs to personal scope at ~/.gemini/skills/", async () => {
78
+ const gemini = await loadAdapter("gemini");
79
+ const dest = await gemini.install("sector/pharma", SKILL_CONTENT, "personal");
80
+ assert.equal(dest, join(tmpHome, ".gemini", "skills", "sector", "pharma", "SKILL.md"));
81
+ });
82
+
83
+ it("installs to project scope at .agents/skills/", async () => {
84
+ const gemini = await loadAdapter("gemini");
85
+ const dest = await gemini.install("sector/pharma", SKILL_CONTENT, "project");
86
+ assert.equal(dest, join(tmpCwd, ".agents", "skills", "sector", "pharma", "SKILL.md"));
87
+ });
88
+ });
89
+
90
+ describe("codex adapter", () => {
91
+ beforeEach(setup);
92
+ afterEach(teardown);
93
+
94
+ it("installs to personal scope at ~/.codex/{skillName}/", async () => {
95
+ const codex = await loadAdapter("codex");
96
+ const dest = await codex.install("external/sector/pharma", SKILL_CONTENT, "personal");
97
+ assert.equal(dest, join(tmpHome, ".codex", "pharma", "SKILL.md"));
98
+ });
99
+
100
+ it("installs to project scope at .agents/skills/", async () => {
101
+ const codex = await loadAdapter("codex");
102
+ const dest = await codex.install("external/sector/pharma", SKILL_CONTENT, "project");
103
+ assert.equal(dest, join(tmpCwd, ".agents", "skills", "external", "sector", "pharma", "SKILL.md"));
104
+ });
105
+ });
106
+
107
+ describe("cursor adapter", () => {
108
+ beforeEach(setup);
109
+ afterEach(teardown);
110
+
111
+ it("installs to personal scope as .mdc with cursor frontmatter", async () => {
112
+ const cursor = await loadAdapter("cursor");
113
+ const dest = await cursor.install("external/sector/pharma", SKILL_CONTENT, "personal");
114
+ assert.equal(dest, join(tmpHome, ".cursor", "rules", "pharma.mdc"));
115
+ const content = await readFile(dest, "utf-8");
116
+ assert.ok(content.startsWith("---\ndescription: A test skill\nalwaysApply: false\n---\n"));
117
+ });
118
+
119
+ it("installs to project scope at .cursor/rules/", async () => {
120
+ const cursor = await loadAdapter("cursor");
121
+ const dest = await cursor.install("sector/pharma", SKILL_CONTENT, "project");
122
+ assert.equal(dest, join(tmpCwd, ".cursor", "rules", "pharma.mdc"));
123
+ });
124
+ });
125
+
126
+ describe("windsurf adapter", () => {
127
+ beforeEach(setup);
128
+ afterEach(teardown);
129
+
130
+ it("installs to personal scope by appending to global_rules.md", async () => {
131
+ const windsurf = await loadAdapter("windsurf");
132
+ const dest = await windsurf.install("sector/pharma", SKILL_CONTENT, "personal");
133
+ assert.equal(dest, join(tmpHome, ".windsurf", "global_rules.md"));
134
+ const content = await readFile(dest, "utf-8");
135
+ assert.ok(content.includes("## pharma"));
136
+ assert.ok(content.includes("test skill"));
137
+ });
138
+
139
+ it("appends multiple skills without overwriting", async () => {
140
+ const windsurf = await loadAdapter("windsurf");
141
+ await windsurf.install("sector/pharma", SKILL_CONTENT, "personal");
142
+ await windsurf.install("sector/tech", "---\nname: tech\nversion: \"2.0.0\"\n---\nTech skill.", "personal");
143
+ const content = await readFile(join(tmpHome, ".windsurf", "global_rules.md"), "utf-8");
144
+ assert.ok(content.includes("## pharma"));
145
+ assert.ok(content.includes("## tech"));
146
+ });
147
+
148
+ it("installs to project scope at .windsurf/rules/", async () => {
149
+ const windsurf = await loadAdapter("windsurf");
150
+ const dest = await windsurf.install("sector/pharma", SKILL_CONTENT, "project");
151
+ assert.equal(dest, join(tmpCwd, ".windsurf", "rules", "pharma.md"));
152
+ });
153
+ });
154
+
155
+ describe("adapter index", () => {
156
+ it("getAdapter returns correct adapter", async () => {
157
+ const { getAdapter } = await import("./index.js");
158
+ const claude = getAdapter("claude");
159
+ assert.equal(claude.toolName, "claude");
160
+ assert.equal(claude.displayName, "Claude Code");
161
+ });
162
+
163
+ it("getAdapter throws for unknown tool", async () => {
164
+ const { getAdapter } = await import("./index.js");
165
+ assert.throws(() => getAdapter("unknown"), { message: /no soportada/ });
166
+ });
167
+
168
+ it("getAllAdapters returns 5 adapters", async () => {
169
+ const { getAllAdapters } = await import("./index.js");
170
+ const all = getAllAdapters();
171
+ assert.equal(all.length, 5);
172
+ const names = all.map((a) => a.toolName);
173
+ assert.deepEqual(names, ["claude", "gemini", "codex", "cursor", "windsurf"]);
174
+ });
175
+ });
@@ -0,0 +1,26 @@
1
+ import { join } from "node:path";
2
+ import { homeDir, cwdDir, writeFile } from "../utils/fs.js";
3
+ import { scanInstalledSkills } from "./shared.js";
4
+
5
+ export const toolName = "claude";
6
+ export const displayName = "Claude Code";
7
+
8
+ function basePath(scope) {
9
+ return scope === "personal"
10
+ ? join(homeDir(), ".claude", "skills")
11
+ : join(cwdDir(), ".claude", "skills");
12
+ }
13
+
14
+ export function getInstallPath(skillPath, scope) {
15
+ return join(basePath(scope), skillPath, "SKILL.md");
16
+ }
17
+
18
+ export async function install(skillPath, skillContent, scope) {
19
+ const dest = getInstallPath(skillPath, scope);
20
+ await writeFile(dest, skillContent);
21
+ return dest;
22
+ }
23
+
24
+ export async function getInstalledSkills(scope) {
25
+ return scanInstalledSkills(basePath(scope));
26
+ }