@skillsmanagerbasetis/skill-manager-local 0.4.2
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/bin/index.js +257 -0
- package/package.json +23 -0
- package/src/adapters/adapters.test.js +175 -0
- package/src/adapters/aider.js +41 -0
- package/src/adapters/claude.js +26 -0
- package/src/adapters/codex.js +31 -0
- package/src/adapters/copilot.js +42 -0
- package/src/adapters/cursor.js +54 -0
- package/src/adapters/gemini.js +26 -0
- package/src/adapters/index.js +57 -0
- package/src/adapters/opencode.js +26 -0
- package/src/adapters/qwen.js +26 -0
- package/src/adapters/roocode.js +41 -0
- package/src/adapters/shared.js +48 -0
- package/src/adapters/windsurf.js +93 -0
- package/src/adapters/zed.js +41 -0
- package/src/commands/detect.js +135 -0
- package/src/commands/init.js +139 -0
- package/src/commands/install-bulk.js +170 -0
- package/src/commands/install.js +216 -0
- package/src/commands/list.js +116 -0
- package/src/commands/login.js +29 -0
- package/src/commands/logout.js +13 -0
- package/src/commands/push.js +104 -0
- package/src/commands/read.js +139 -0
- package/src/commands/remove.js +115 -0
- package/src/commands/search.js +107 -0
- package/src/commands/status.js +87 -0
- package/src/commands/sync.js +20 -0
- package/src/commands/update.js +140 -0
- package/src/commands/whoami.js +29 -0
- package/src/env.js +3 -0
- package/src/manifest.js +84 -0
- package/src/manifest.test.js +183 -0
- package/src/utils/api.js +40 -0
- package/src/utils/auth.js +289 -0
- package/src/utils/config.js +47 -0
- package/src/utils/find-skill.js +129 -0
- package/src/utils/fs.js +39 -0
- package/src/utils/gitlab.js +78 -0
- package/src/utils/installed.js +88 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
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 { installBulk } from "../src/commands/install-bulk.js";
|
|
8
|
+
import { status } from "../src/commands/status.js";
|
|
9
|
+
import { detect } from "../src/commands/detect.js";
|
|
10
|
+
import { login } from "../src/commands/login.js";
|
|
11
|
+
import { logout } from "../src/commands/logout.js";
|
|
12
|
+
import { whoami } from "../src/commands/whoami.js";
|
|
13
|
+
import { sync } from "../src/commands/sync.js";
|
|
14
|
+
import { push } from "../src/commands/push.js";
|
|
15
|
+
import { update } from "../src/commands/update.js";
|
|
16
|
+
import { remove } from "../src/commands/remove.js";
|
|
17
|
+
import { read } from "../src/commands/read.js";
|
|
18
|
+
import { search } from "../src/commands/search.js";
|
|
19
|
+
|
|
20
|
+
const program = new Command();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name("skill-manager")
|
|
24
|
+
.description("CLI para gestionar skills de IA en tu editor")
|
|
25
|
+
.version("0.4.0");
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command("init")
|
|
29
|
+
.description("Configura el repositorio de skills y la herramienta por defecto")
|
|
30
|
+
.action(async () => {
|
|
31
|
+
try {
|
|
32
|
+
await init();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(err.message);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command("list")
|
|
41
|
+
.description("Lista todas las skills disponibles en el catálogo")
|
|
42
|
+
.option("-f, --filter <texto>", "Filtra skills por nombre")
|
|
43
|
+
.option("-j, --json", "Salida en formato JSON")
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
try {
|
|
46
|
+
await list(options);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(err.message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command("search [texto]")
|
|
55
|
+
.description("Busca skills por texto, tipo, nivel, entidad o proyecto (solo las que puedes ver)")
|
|
56
|
+
.option("-t, --type <tipo>", "Filtrar por tipo: contexto, habilidad, agente")
|
|
57
|
+
.option("-l, --level <nivel>", "Filtrar por nivel: root, user, habilidades, area, team, sector, proyecto")
|
|
58
|
+
.option("-e, --entity <slug>", "Filtrar por entidad (slug del area/team/sector)")
|
|
59
|
+
.option("-p, --project <slug>", "Filtrar por proyecto (slug)")
|
|
60
|
+
.option("-j, --json", "Salida en formato JSON")
|
|
61
|
+
.action(async (texto, options) => {
|
|
62
|
+
try {
|
|
63
|
+
await search(texto, options);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(err.message);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command("install <ruta>")
|
|
72
|
+
.description("Instala una skill y sus dependencias (cadena de parents)")
|
|
73
|
+
.option("-t, --tool <herramienta>", "Herramienta(s) destino, separadas por coma (claude,cursor,gemini,codex,windsurf,opencode,copilot,aider,zed,roocode,qwen)")
|
|
74
|
+
.option("-s, --scope <ámbito>", "Ámbito de instalación: global o project")
|
|
75
|
+
.option("-f, --force", "Sobreescribir sin confirmar si la skill ya existe")
|
|
76
|
+
.action(async (ruta, options) => {
|
|
77
|
+
try {
|
|
78
|
+
await install(ruta, options);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(err.message);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Bulk install commands: install-project / install-area / install-team / install-sector
|
|
86
|
+
const bulkOpts = (cmd) =>
|
|
87
|
+
cmd
|
|
88
|
+
.option("-t, --tool <herramienta>", "Herramienta(s) destino, separadas por coma")
|
|
89
|
+
.option("-s, --scope <ámbito>", "Ámbito de instalación: global o project")
|
|
90
|
+
.option("-y, --yes", "Asume sí en las confirmaciones");
|
|
91
|
+
|
|
92
|
+
bulkOpts(
|
|
93
|
+
program
|
|
94
|
+
.command("install-project <slug>")
|
|
95
|
+
.description("Instala todas las skills publicadas de un proyecto")
|
|
96
|
+
).action(async (slug, options) => {
|
|
97
|
+
try { await installBulk("project", slug, options); }
|
|
98
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
bulkOpts(
|
|
102
|
+
program
|
|
103
|
+
.command("install-area <slug>")
|
|
104
|
+
.description("Instala todas las skills publicadas de un área")
|
|
105
|
+
).action(async (slug, options) => {
|
|
106
|
+
try { await installBulk("area", slug, options); }
|
|
107
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
bulkOpts(
|
|
111
|
+
program
|
|
112
|
+
.command("install-team <slug>")
|
|
113
|
+
.description("Instala todas las skills publicadas de un team")
|
|
114
|
+
).action(async (slug, options) => {
|
|
115
|
+
try { await installBulk("team", slug, options); }
|
|
116
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
bulkOpts(
|
|
120
|
+
program
|
|
121
|
+
.command("install-sector <slug>")
|
|
122
|
+
.description("Instala todas las skills publicadas de un sector")
|
|
123
|
+
).action(async (slug, options) => {
|
|
124
|
+
try { await installBulk("sector", slug, options); }
|
|
125
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
program
|
|
129
|
+
.command("status")
|
|
130
|
+
.description("Muestra el estado de las skills instaladas vs el catálogo")
|
|
131
|
+
.option("-j, --json", "Salida en formato JSON")
|
|
132
|
+
.action(async (options) => {
|
|
133
|
+
try {
|
|
134
|
+
await status(options);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(err.message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
program
|
|
142
|
+
.command("detect")
|
|
143
|
+
.description("Analiza el proyecto actual y recomienda skills relevantes")
|
|
144
|
+
.action(async () => {
|
|
145
|
+
try {
|
|
146
|
+
await detect();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(err.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
program
|
|
154
|
+
.command("sync")
|
|
155
|
+
.description("Actualiza el find-skill con tus permisos más recientes")
|
|
156
|
+
.action(async () => {
|
|
157
|
+
try {
|
|
158
|
+
await sync();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(err.message);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
program
|
|
166
|
+
.command("push <ruta>")
|
|
167
|
+
.description("Publica los cambios locales de un skill al servidor")
|
|
168
|
+
.action(async (ruta) => {
|
|
169
|
+
try {
|
|
170
|
+
await push(ruta);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error(err.message);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
program
|
|
178
|
+
.command("update")
|
|
179
|
+
.description("Comprueba y aplica actualizaciones de skills instalados")
|
|
180
|
+
.option("-n, --dry-run", "Muestra qué se actualizaría sin aplicar cambios")
|
|
181
|
+
.action(async (options) => {
|
|
182
|
+
try {
|
|
183
|
+
await update(options);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(err.message);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
program
|
|
191
|
+
.command("remove <ruta>")
|
|
192
|
+
.description("Desinstala una skill del sistema de archivos local")
|
|
193
|
+
.option("-t, --tool <herramienta>", "Solo eliminar de esta herramienta (claude,cursor,gemini,codex,windsurf,opencode,copilot,aider,zed,roocode,qwen)")
|
|
194
|
+
.option("-s, --scope <ámbito>", "Solo eliminar de este ámbito: global o project")
|
|
195
|
+
.option("-y, --yes", "Asume sí en las confirmaciones")
|
|
196
|
+
.action(async (ruta, options) => {
|
|
197
|
+
try {
|
|
198
|
+
await remove(ruta, options);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error(err.message);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
program
|
|
206
|
+
.command("read <ruta>")
|
|
207
|
+
.description("Muestra el contenido de una skill instalada")
|
|
208
|
+
.option("-t, --tool <herramienta>", "Leer de esta herramienta específica")
|
|
209
|
+
.option("-s, --scope <ámbito>", "Leer de este ámbito: global o project")
|
|
210
|
+
.option("-r, --raw", "Muestra el contenido sin formateo")
|
|
211
|
+
.option("-j, --json", "Salida en formato JSON")
|
|
212
|
+
.action(async (ruta, options) => {
|
|
213
|
+
try {
|
|
214
|
+
await read(ruta, options);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(err.message);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
program
|
|
222
|
+
.command("login")
|
|
223
|
+
.description("Inicia sesión con tu cuenta de Basetis (Google)")
|
|
224
|
+
.action(async () => {
|
|
225
|
+
try {
|
|
226
|
+
await login();
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(err.message);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
program
|
|
234
|
+
.command("logout")
|
|
235
|
+
.description("Cierra la sesión actual")
|
|
236
|
+
.action(async () => {
|
|
237
|
+
try {
|
|
238
|
+
await logout();
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(err.message);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
program
|
|
246
|
+
.command("whoami")
|
|
247
|
+
.description("Muestra el usuario autenticado")
|
|
248
|
+
.action(async () => {
|
|
249
|
+
try {
|
|
250
|
+
await whoami();
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error(err.message);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skillsmanagerbasetis/skill-manager-local",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "CLI to manage AI coding skills across editors",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"skill-manager-local": "bin/index.js"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"registry": "https://registry.npmjs.org/",
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"chalk": "^5",
|
|
18
|
+
"commander": "^13",
|
|
19
|
+
"inquirer": "^12",
|
|
20
|
+
"js-yaml": "^4",
|
|
21
|
+
"ora": "^8"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -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,41 @@
|
|
|
1
|
+
import { join, basename } from "node:path";
|
|
2
|
+
import { cwdDir, writeFile, readFile } from "../utils/fs.js";
|
|
3
|
+
import { parseFrontmatter, scanDir } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export const toolName = "aider";
|
|
6
|
+
export const displayName = "Aider";
|
|
7
|
+
|
|
8
|
+
// Aider only supports project-level conventions files
|
|
9
|
+
function basePath() {
|
|
10
|
+
return join(cwdDir(), ".aider", "skills");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getInstallPath(skillPath, scope) {
|
|
14
|
+
if (scope === "personal") {
|
|
15
|
+
return join(basePath(), `${basename(skillPath)}.md`);
|
|
16
|
+
}
|
|
17
|
+
return join(basePath(), `${basename(skillPath)}.md`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function install(skillPath, skillContent, scope, _metadata = {}) {
|
|
21
|
+
const dest = getInstallPath(skillPath, scope);
|
|
22
|
+
await writeFile(dest, skillContent);
|
|
23
|
+
return dest;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function getInstalledSkills(scope) {
|
|
27
|
+
if (scope === "personal") return [];
|
|
28
|
+
const dir = basePath();
|
|
29
|
+
const files = await scanDir(dir, ".md");
|
|
30
|
+
const skills = [];
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const content = await readFile(join(dir, file));
|
|
33
|
+
if (!content) continue;
|
|
34
|
+
const { meta } = parseFrontmatter(content);
|
|
35
|
+
skills.push({
|
|
36
|
+
path: file.replace(/\.md$/, ""),
|
|
37
|
+
version: meta.version || "unknown",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return skills;
|
|
41
|
+
}
|
|
@@ -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, _metadata = {}) {
|
|
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join, basename } from "node:path";
|
|
2
|
+
import { homeDir, cwdDir, writeFile } from "../utils/fs.js";
|
|
3
|
+
import { scanInstalledSkills } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export const toolName = "codex";
|
|
6
|
+
export const displayName = "Codex CLI";
|
|
7
|
+
|
|
8
|
+
function basePath(scope) {
|
|
9
|
+
if (scope === "personal") {
|
|
10
|
+
return join(homeDir(), ".codex");
|
|
11
|
+
}
|
|
12
|
+
return join(cwdDir(), ".agents", "skills");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getInstallPath(skillPath, scope) {
|
|
16
|
+
if (scope === "personal") {
|
|
17
|
+
const skillName = basename(skillPath);
|
|
18
|
+
return join(basePath(scope), skillName, "SKILL.md");
|
|
19
|
+
}
|
|
20
|
+
return join(basePath(scope), skillPath, "SKILL.md");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function install(skillPath, skillContent, scope, _metadata = {}) {
|
|
24
|
+
const dest = getInstallPath(skillPath, scope);
|
|
25
|
+
await writeFile(dest, skillContent);
|
|
26
|
+
return dest;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function getInstalledSkills(scope) {
|
|
30
|
+
return scanInstalledSkills(basePath(scope));
|
|
31
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { join, basename } from "node:path";
|
|
2
|
+
import { cwdDir, writeFile, readFile } from "../utils/fs.js";
|
|
3
|
+
import { parseFrontmatter, scanDir } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export const toolName = "copilot";
|
|
6
|
+
export const displayName = "GitHub Copilot";
|
|
7
|
+
|
|
8
|
+
// Copilot only supports project-level skills
|
|
9
|
+
function basePath() {
|
|
10
|
+
return join(cwdDir(), ".github", "copilot", "skills");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getInstallPath(skillPath, scope) {
|
|
14
|
+
if (scope === "personal") {
|
|
15
|
+
// Copilot doesn't have a global skills directory — install to project
|
|
16
|
+
return join(basePath(), `${basename(skillPath)}.md`);
|
|
17
|
+
}
|
|
18
|
+
return join(basePath(), `${basename(skillPath)}.md`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function install(skillPath, skillContent, scope, _metadata = {}) {
|
|
22
|
+
const dest = getInstallPath(skillPath, scope);
|
|
23
|
+
await writeFile(dest, skillContent);
|
|
24
|
+
return dest;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getInstalledSkills(scope) {
|
|
28
|
+
if (scope === "personal") return [];
|
|
29
|
+
const dir = basePath();
|
|
30
|
+
const files = await scanDir(dir, ".md");
|
|
31
|
+
const skills = [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const content = await readFile(join(dir, file));
|
|
34
|
+
if (!content) continue;
|
|
35
|
+
const { meta } = parseFrontmatter(content);
|
|
36
|
+
skills.push({
|
|
37
|
+
path: file.replace(/\.md$/, ""),
|
|
38
|
+
version: meta.version || "unknown",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return skills;
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { join, basename } from "node:path";
|
|
2
|
+
import { homeDir, cwdDir, writeFile, readFile } from "../utils/fs.js";
|
|
3
|
+
import { parseFrontmatter, scanDir } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export const toolName = "cursor";
|
|
6
|
+
export const displayName = "Cursor";
|
|
7
|
+
|
|
8
|
+
function basePath(scope) {
|
|
9
|
+
return scope === "personal"
|
|
10
|
+
? join(homeDir(), ".cursor", "rules")
|
|
11
|
+
: join(cwdDir(), ".cursor", "rules");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getInstallPath(skillPath, scope) {
|
|
15
|
+
const skillName = basename(skillPath);
|
|
16
|
+
return join(basePath(scope), `${skillName}.mdc`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toCursorFormat(skillContent, metadata = {}) {
|
|
20
|
+
const { meta, body } = parseFrontmatter(skillContent);
|
|
21
|
+
const description = metadata.description || meta.description || meta.name || "Skill";
|
|
22
|
+
const alwaysApply = metadata.activationMode === "always";
|
|
23
|
+
const globs = metadata.cursorGlobs?.length
|
|
24
|
+
? metadata.cursorGlobs.join(", ")
|
|
25
|
+
: "";
|
|
26
|
+
|
|
27
|
+
let header = `---\ndescription: ${description}\nalwaysApply: ${alwaysApply}`;
|
|
28
|
+
if (globs) header += `\nglobs: ${globs}`;
|
|
29
|
+
header += "\n---\n";
|
|
30
|
+
return header + body;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function install(skillPath, skillContent, scope, metadata = {}) {
|
|
34
|
+
const dest = getInstallPath(skillPath, scope);
|
|
35
|
+
const content = toCursorFormat(skillContent, metadata);
|
|
36
|
+
await writeFile(dest, content);
|
|
37
|
+
return dest;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function getInstalledSkills(scope) {
|
|
41
|
+
const dir = basePath(scope);
|
|
42
|
+
const files = await scanDir(dir, ".mdc");
|
|
43
|
+
const skills = [];
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const content = await readFile(join(dir, file));
|
|
46
|
+
if (!content) continue;
|
|
47
|
+
const { meta } = parseFrontmatter(content);
|
|
48
|
+
skills.push({
|
|
49
|
+
path: file.replace(/\.mdc$/, ""),
|
|
50
|
+
version: meta.version || "unknown",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return skills;
|
|
54
|
+
}
|
|
@@ -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 = "gemini";
|
|
6
|
+
export const displayName = "Gemini CLI";
|
|
7
|
+
|
|
8
|
+
function basePath(scope) {
|
|
9
|
+
return scope === "personal"
|
|
10
|
+
? join(homeDir(), ".gemini", "skills")
|
|
11
|
+
: join(cwdDir(), ".agents", "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, _metadata = {}) {
|
|
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
|
+
}
|