@iceinvein/agent-skills 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 +58 -0
- package/dist/cli/index.js +819 -0
- package/package.json +27 -0
- package/skills/code-intelligence/skill.json +31 -0
- package/skills/codebase-architecture/SKILL.md +183 -0
- package/skills/codebase-architecture/patterns-reference.md +168 -0
- package/skills/codebase-architecture/skill.json +36 -0
- package/skills/design-review/SKILL.md +150 -0
- package/skills/design-review/skill.json +26 -0
- package/skills/index.json +20 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// src/cli/detect.ts
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
var TOOL_MARKERS = {
|
|
10
|
+
claude: (cwd) => existsSync(join(cwd, ".claude")),
|
|
11
|
+
cursor: (cwd) => existsSync(join(cwd, ".cursor")),
|
|
12
|
+
codex: (cwd) => existsSync(join(cwd, "AGENTS.md")),
|
|
13
|
+
gemini: (cwd) => existsSync(join(cwd, ".gemini"))
|
|
14
|
+
};
|
|
15
|
+
async function detectTools(cwd) {
|
|
16
|
+
const detected = [];
|
|
17
|
+
for (const [tool, check] of Object.entries(TOOL_MARKERS)) {
|
|
18
|
+
if (check(cwd)) {
|
|
19
|
+
detected.push(tool);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return detected;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/cli/types.ts
|
|
26
|
+
var TOOL_NAMES = ["claude", "cursor", "codex", "gemini"];
|
|
27
|
+
var SKILL_TYPES = ["prompt", "code", "hybrid"];
|
|
28
|
+
function validateManifest(data) {
|
|
29
|
+
if (typeof data !== "object" || data === null) {
|
|
30
|
+
return { ok: false, error: "Manifest must be an object" };
|
|
31
|
+
}
|
|
32
|
+
const d = data;
|
|
33
|
+
if (typeof d.name !== "string" || d.name.length === 0) {
|
|
34
|
+
return { ok: false, error: "Missing or invalid 'name'" };
|
|
35
|
+
}
|
|
36
|
+
if (typeof d.version !== "string") {
|
|
37
|
+
return { ok: false, error: "Missing or invalid 'version'" };
|
|
38
|
+
}
|
|
39
|
+
if (typeof d.description !== "string") {
|
|
40
|
+
return { ok: false, error: "Missing or invalid 'description'" };
|
|
41
|
+
}
|
|
42
|
+
if (typeof d.author !== "string") {
|
|
43
|
+
return { ok: false, error: "Missing or invalid 'author'" };
|
|
44
|
+
}
|
|
45
|
+
if (!SKILL_TYPES.includes(d.type)) {
|
|
46
|
+
return { ok: false, error: `Invalid 'type': must be one of ${SKILL_TYPES.join(", ")}` };
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(d.tools) || d.tools.length === 0) {
|
|
49
|
+
return { ok: false, error: "Missing or empty 'tools' array" };
|
|
50
|
+
}
|
|
51
|
+
for (const tool of d.tools) {
|
|
52
|
+
if (!TOOL_NAMES.includes(tool)) {
|
|
53
|
+
return { ok: false, error: `Invalid tool '${tool}': must be one of ${TOOL_NAMES.join(", ")}` };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (typeof d.install !== "object" || d.install === null) {
|
|
57
|
+
return { ok: false, error: "Missing 'install' configuration" };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, manifest: d };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/cli/github.ts
|
|
63
|
+
var REPO = "iceinvein/agent-skills";
|
|
64
|
+
var BRANCH = "main";
|
|
65
|
+
var BASE = `https://raw.githubusercontent.com/${REPO}/${BRANCH}`;
|
|
66
|
+
function buildRawUrl(skillName, filePath) {
|
|
67
|
+
return `${BASE}/skills/${skillName}/${filePath}`;
|
|
68
|
+
}
|
|
69
|
+
async function fetchSkillManifest(skillName) {
|
|
70
|
+
const url = buildRawUrl(skillName, "skill.json");
|
|
71
|
+
const res = await fetch(url);
|
|
72
|
+
if (res.status === 404) {
|
|
73
|
+
return { ok: false, error: `Skill '${skillName}' not found` };
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
return { ok: false, error: `Failed to fetch manifest: HTTP ${res.status}` };
|
|
77
|
+
}
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
return validateManifest(data);
|
|
80
|
+
}
|
|
81
|
+
async function fetchSkillFile(skillName, filePath) {
|
|
82
|
+
const url = buildRawUrl(skillName, filePath);
|
|
83
|
+
const res = await fetch(url);
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
return { ok: false, error: `Failed to fetch '${filePath}': HTTP ${res.status}` };
|
|
86
|
+
}
|
|
87
|
+
const content = await res.text();
|
|
88
|
+
return { ok: true, content };
|
|
89
|
+
}
|
|
90
|
+
async function fetchAllSkillFiles(skillName, manifest) {
|
|
91
|
+
const files = new Map;
|
|
92
|
+
if (manifest.files?.prompt) {
|
|
93
|
+
const result = await fetchSkillFile(skillName, manifest.files.prompt);
|
|
94
|
+
if (!result.ok)
|
|
95
|
+
return { error: result.error };
|
|
96
|
+
files.set(manifest.files.prompt, result.content);
|
|
97
|
+
}
|
|
98
|
+
if (manifest.files?.supporting) {
|
|
99
|
+
for (const supportFile of manifest.files.supporting) {
|
|
100
|
+
const result = await fetchSkillFile(skillName, supportFile);
|
|
101
|
+
if (!result.ok)
|
|
102
|
+
return { error: result.error };
|
|
103
|
+
files.set(supportFile, result.content);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return files;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/cli/adapters/claude.ts
|
|
110
|
+
import { existsSync as existsSync2, mkdirSync, rmSync, unlinkSync } from "node:fs";
|
|
111
|
+
import { dirname, join as join2 } from "node:path";
|
|
112
|
+
var claudeAdapter = {
|
|
113
|
+
name: "claude",
|
|
114
|
+
async install(cwd, manifest, files) {
|
|
115
|
+
const installed = [];
|
|
116
|
+
const config = manifest.install.claude;
|
|
117
|
+
if (!config)
|
|
118
|
+
return installed;
|
|
119
|
+
if (config.prompt && files.has(manifest.files?.prompt ?? "")) {
|
|
120
|
+
const targetPath = join2(cwd, config.prompt);
|
|
121
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
122
|
+
await Bun.write(targetPath, files.get(manifest.files.prompt));
|
|
123
|
+
installed.push(config.prompt);
|
|
124
|
+
}
|
|
125
|
+
if (config.supporting) {
|
|
126
|
+
for (const [sourceFile, targetRel] of Object.entries(config.supporting)) {
|
|
127
|
+
if (files.has(sourceFile)) {
|
|
128
|
+
const targetPath = join2(cwd, targetRel);
|
|
129
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
130
|
+
await Bun.write(targetPath, files.get(sourceFile));
|
|
131
|
+
installed.push(targetRel);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (config.mcpServers) {
|
|
136
|
+
const settingsPath = join2(cwd, ".claude/settings.json");
|
|
137
|
+
let settings = {};
|
|
138
|
+
if (existsSync2(settingsPath)) {
|
|
139
|
+
settings = await Bun.file(settingsPath).json();
|
|
140
|
+
}
|
|
141
|
+
if (!settings.mcpServers)
|
|
142
|
+
settings.mcpServers = {};
|
|
143
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
144
|
+
settings.mcpServers[name] = serverConfig;
|
|
145
|
+
}
|
|
146
|
+
await Bun.write(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
147
|
+
`);
|
|
148
|
+
installed.push(".claude/settings.json");
|
|
149
|
+
}
|
|
150
|
+
return installed;
|
|
151
|
+
},
|
|
152
|
+
async remove(cwd, manifest, installedFiles) {
|
|
153
|
+
const config = manifest.install.claude;
|
|
154
|
+
if (!config)
|
|
155
|
+
return;
|
|
156
|
+
for (const file of installedFiles) {
|
|
157
|
+
if (file === ".claude/settings.json")
|
|
158
|
+
continue;
|
|
159
|
+
const fullPath = join2(cwd, file);
|
|
160
|
+
if (existsSync2(fullPath)) {
|
|
161
|
+
unlinkSync(fullPath);
|
|
162
|
+
const dir = dirname(fullPath);
|
|
163
|
+
try {
|
|
164
|
+
rmSync(dir, { recursive: false });
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (config.mcpServers) {
|
|
169
|
+
const settingsPath = join2(cwd, ".claude/settings.json");
|
|
170
|
+
if (existsSync2(settingsPath)) {
|
|
171
|
+
const settings = await Bun.file(settingsPath).json();
|
|
172
|
+
if (settings.mcpServers) {
|
|
173
|
+
for (const name of Object.keys(config.mcpServers)) {
|
|
174
|
+
delete settings.mcpServers[name];
|
|
175
|
+
}
|
|
176
|
+
await Bun.write(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
177
|
+
`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/cli/adapters/cursor.ts
|
|
185
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, rmSync as rmSync2 } from "node:fs";
|
|
186
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
187
|
+
var cursorAdapter = {
|
|
188
|
+
name: "cursor",
|
|
189
|
+
async install(cwd, manifest, files) {
|
|
190
|
+
const installed = [];
|
|
191
|
+
const config = manifest.install.cursor;
|
|
192
|
+
if (!config)
|
|
193
|
+
return installed;
|
|
194
|
+
if (config.prompt && files.has(manifest.files?.prompt ?? "")) {
|
|
195
|
+
const targetPath = join3(cwd, config.prompt);
|
|
196
|
+
mkdirSync2(dirname2(targetPath), { recursive: true });
|
|
197
|
+
await Bun.write(targetPath, files.get(manifest.files.prompt));
|
|
198
|
+
installed.push(config.prompt);
|
|
199
|
+
}
|
|
200
|
+
if (config.supporting) {
|
|
201
|
+
for (const [sourceFile, targetRel] of Object.entries(config.supporting)) {
|
|
202
|
+
if (files.has(sourceFile)) {
|
|
203
|
+
const targetPath = join3(cwd, targetRel);
|
|
204
|
+
mkdirSync2(dirname2(targetPath), { recursive: true });
|
|
205
|
+
await Bun.write(targetPath, files.get(sourceFile));
|
|
206
|
+
installed.push(targetRel);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (config.mcpServers) {
|
|
211
|
+
const mcpPath = join3(cwd, ".cursor/mcp.json");
|
|
212
|
+
let mcpConfig = {};
|
|
213
|
+
if (existsSync3(mcpPath)) {
|
|
214
|
+
mcpConfig = await Bun.file(mcpPath).json();
|
|
215
|
+
}
|
|
216
|
+
if (!mcpConfig.mcpServers)
|
|
217
|
+
mcpConfig.mcpServers = {};
|
|
218
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
219
|
+
mcpConfig.mcpServers[name] = serverConfig;
|
|
220
|
+
}
|
|
221
|
+
await Bun.write(mcpPath, JSON.stringify(mcpConfig, null, 2) + `
|
|
222
|
+
`);
|
|
223
|
+
installed.push(".cursor/mcp.json");
|
|
224
|
+
}
|
|
225
|
+
return installed;
|
|
226
|
+
},
|
|
227
|
+
async remove(cwd, manifest, installedFiles) {
|
|
228
|
+
const config = manifest.install.cursor;
|
|
229
|
+
if (!config)
|
|
230
|
+
return;
|
|
231
|
+
for (const file of installedFiles) {
|
|
232
|
+
if (file === ".cursor/mcp.json")
|
|
233
|
+
continue;
|
|
234
|
+
const fullPath = join3(cwd, file);
|
|
235
|
+
if (existsSync3(fullPath)) {
|
|
236
|
+
unlinkSync2(fullPath);
|
|
237
|
+
try {
|
|
238
|
+
rmSync2(dirname2(fullPath), { recursive: false });
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (config.mcpServers) {
|
|
243
|
+
const mcpPath = join3(cwd, ".cursor/mcp.json");
|
|
244
|
+
if (existsSync3(mcpPath)) {
|
|
245
|
+
const mcpConfig = await Bun.file(mcpPath).json();
|
|
246
|
+
if (mcpConfig.mcpServers) {
|
|
247
|
+
for (const name of Object.keys(config.mcpServers)) {
|
|
248
|
+
delete mcpConfig.mcpServers[name];
|
|
249
|
+
}
|
|
250
|
+
await Bun.write(mcpPath, JSON.stringify(mcpConfig, null, 2) + `
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/cli/adapters/codex.ts
|
|
259
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
260
|
+
import { join as join4 } from "node:path";
|
|
261
|
+
var codexAdapter = {
|
|
262
|
+
name: "codex",
|
|
263
|
+
async install(cwd, manifest, files) {
|
|
264
|
+
const installed = [];
|
|
265
|
+
const config = manifest.install.codex;
|
|
266
|
+
if (!config)
|
|
267
|
+
return installed;
|
|
268
|
+
if (config.mcpServers) {
|
|
269
|
+
console.warn(`⚠ Codex does not support MCP servers — skipping MCP install for '${manifest.name}'`);
|
|
270
|
+
return installed;
|
|
271
|
+
}
|
|
272
|
+
if (config.append && config.prompt && files.has(manifest.files?.prompt ?? "")) {
|
|
273
|
+
const agentsPath = join4(cwd, config.prompt);
|
|
274
|
+
let existing = "";
|
|
275
|
+
if (existsSync4(agentsPath)) {
|
|
276
|
+
existing = await Bun.file(agentsPath).text();
|
|
277
|
+
}
|
|
278
|
+
const content = files.get(manifest.files.prompt);
|
|
279
|
+
const section = [
|
|
280
|
+
`
|
|
281
|
+
<!-- agent-skills:start:${manifest.name} -->`,
|
|
282
|
+
content,
|
|
283
|
+
`<!-- agent-skills:end:${manifest.name} -->
|
|
284
|
+
`
|
|
285
|
+
].join(`
|
|
286
|
+
`);
|
|
287
|
+
await Bun.write(agentsPath, existing + section);
|
|
288
|
+
installed.push("AGENTS.md");
|
|
289
|
+
}
|
|
290
|
+
return installed;
|
|
291
|
+
},
|
|
292
|
+
async remove(cwd, manifest, _installedFiles) {
|
|
293
|
+
const config = manifest.install.codex;
|
|
294
|
+
if (!config)
|
|
295
|
+
return;
|
|
296
|
+
if (config.append && config.prompt) {
|
|
297
|
+
const agentsPath = join4(cwd, config.prompt);
|
|
298
|
+
if (existsSync4(agentsPath)) {
|
|
299
|
+
const content = await Bun.file(agentsPath).text();
|
|
300
|
+
const startMarker = `<!-- agent-skills:start:${manifest.name} -->`;
|
|
301
|
+
const endMarker = `<!-- agent-skills:end:${manifest.name} -->`;
|
|
302
|
+
const startIdx = content.indexOf(startMarker);
|
|
303
|
+
const endIdx = content.indexOf(endMarker);
|
|
304
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
305
|
+
const before = content.slice(0, startIdx).replace(/\n$/, "");
|
|
306
|
+
const after = content.slice(endIdx + endMarker.length).replace(/^\n/, "");
|
|
307
|
+
await Bun.write(agentsPath, before + after);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// src/cli/adapters/gemini.ts
|
|
315
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, unlinkSync as unlinkSync3, rmSync as rmSync3 } from "node:fs";
|
|
316
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
317
|
+
var geminiAdapter = {
|
|
318
|
+
name: "gemini",
|
|
319
|
+
async install(cwd, manifest, files) {
|
|
320
|
+
const installed = [];
|
|
321
|
+
const config = manifest.install.gemini;
|
|
322
|
+
if (!config)
|
|
323
|
+
return installed;
|
|
324
|
+
if (config.prompt && files.has(manifest.files?.prompt ?? "")) {
|
|
325
|
+
const targetPath = join5(cwd, config.prompt);
|
|
326
|
+
mkdirSync3(dirname3(targetPath), { recursive: true });
|
|
327
|
+
await Bun.write(targetPath, files.get(manifest.files.prompt));
|
|
328
|
+
installed.push(config.prompt);
|
|
329
|
+
}
|
|
330
|
+
if (config.supporting) {
|
|
331
|
+
for (const [sourceFile, targetRel] of Object.entries(config.supporting)) {
|
|
332
|
+
if (files.has(sourceFile)) {
|
|
333
|
+
const targetPath = join5(cwd, targetRel);
|
|
334
|
+
mkdirSync3(dirname3(targetPath), { recursive: true });
|
|
335
|
+
await Bun.write(targetPath, files.get(sourceFile));
|
|
336
|
+
installed.push(targetRel);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (config.mcpServers) {
|
|
341
|
+
const settingsPath = join5(cwd, ".gemini/settings.json");
|
|
342
|
+
let settings = {};
|
|
343
|
+
if (existsSync5(settingsPath)) {
|
|
344
|
+
settings = await Bun.file(settingsPath).json();
|
|
345
|
+
}
|
|
346
|
+
if (!settings.mcpServers)
|
|
347
|
+
settings.mcpServers = {};
|
|
348
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
349
|
+
settings.mcpServers[name] = serverConfig;
|
|
350
|
+
}
|
|
351
|
+
await Bun.write(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
352
|
+
`);
|
|
353
|
+
installed.push(".gemini/settings.json");
|
|
354
|
+
}
|
|
355
|
+
return installed;
|
|
356
|
+
},
|
|
357
|
+
async remove(cwd, manifest, installedFiles) {
|
|
358
|
+
const config = manifest.install.gemini;
|
|
359
|
+
if (!config)
|
|
360
|
+
return;
|
|
361
|
+
for (const file of installedFiles) {
|
|
362
|
+
if (file === ".gemini/settings.json")
|
|
363
|
+
continue;
|
|
364
|
+
const fullPath = join5(cwd, file);
|
|
365
|
+
if (existsSync5(fullPath)) {
|
|
366
|
+
unlinkSync3(fullPath);
|
|
367
|
+
try {
|
|
368
|
+
rmSync3(dirname3(fullPath), { recursive: false });
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (config.mcpServers) {
|
|
373
|
+
const settingsPath = join5(cwd, ".gemini/settings.json");
|
|
374
|
+
if (existsSync5(settingsPath)) {
|
|
375
|
+
const settings = await Bun.file(settingsPath).json();
|
|
376
|
+
if (settings.mcpServers) {
|
|
377
|
+
for (const name of Object.keys(config.mcpServers)) {
|
|
378
|
+
delete settings.mcpServers[name];
|
|
379
|
+
}
|
|
380
|
+
await Bun.write(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
381
|
+
`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/cli/adapters/index.ts
|
|
389
|
+
var adapters = {
|
|
390
|
+
claude: claudeAdapter,
|
|
391
|
+
cursor: cursorAdapter,
|
|
392
|
+
codex: codexAdapter,
|
|
393
|
+
gemini: geminiAdapter
|
|
394
|
+
};
|
|
395
|
+
function getAdapter(tool) {
|
|
396
|
+
return adapters[tool];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/cli/lockfile.ts
|
|
400
|
+
import { join as join6 } from "node:path";
|
|
401
|
+
var LOCKFILE_NAME = ".agent-skills.lock";
|
|
402
|
+
async function readLockfile(cwd) {
|
|
403
|
+
const path = join6(cwd, LOCKFILE_NAME);
|
|
404
|
+
const file = Bun.file(path);
|
|
405
|
+
if (!await file.exists()) {
|
|
406
|
+
return { skills: {} };
|
|
407
|
+
}
|
|
408
|
+
return file.json();
|
|
409
|
+
}
|
|
410
|
+
async function writeLockfile(cwd, lockfile) {
|
|
411
|
+
const path = join6(cwd, LOCKFILE_NAME);
|
|
412
|
+
await Bun.write(path, JSON.stringify(lockfile, null, 2) + `
|
|
413
|
+
`);
|
|
414
|
+
}
|
|
415
|
+
async function addSkillToLockfile(cwd, skillName, entry) {
|
|
416
|
+
const lockfile = await readLockfile(cwd);
|
|
417
|
+
lockfile.skills[skillName] = entry;
|
|
418
|
+
await writeLockfile(cwd, lockfile);
|
|
419
|
+
}
|
|
420
|
+
async function removeSkillFromLockfile(cwd, skillName) {
|
|
421
|
+
const lockfile = await readLockfile(cwd);
|
|
422
|
+
delete lockfile.skills[skillName];
|
|
423
|
+
await writeLockfile(cwd, lockfile);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/cli/commands/install.ts
|
|
427
|
+
async function installSkill(cwd, manifest, files, targetTools) {
|
|
428
|
+
const compatibleTools = targetTools.filter((t) => manifest.tools.includes(t));
|
|
429
|
+
const skipped = targetTools.filter((t) => !manifest.tools.includes(t));
|
|
430
|
+
if (compatibleTools.length === 0) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
error: `Skill '${manifest.name}' does not support any of: ${targetTools.join(", ")}. Supported: ${manifest.tools.join(", ")}`
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
const installed = {};
|
|
437
|
+
const allFiles = [];
|
|
438
|
+
for (const tool of compatibleTools) {
|
|
439
|
+
const adapter = getAdapter(tool);
|
|
440
|
+
const toolFiles = await adapter.install(cwd, manifest, files);
|
|
441
|
+
installed[tool] = toolFiles;
|
|
442
|
+
allFiles.push(...toolFiles);
|
|
443
|
+
}
|
|
444
|
+
await addSkillToLockfile(cwd, manifest.name, {
|
|
445
|
+
version: manifest.version,
|
|
446
|
+
tools: compatibleTools,
|
|
447
|
+
installedAt: new Date().toISOString(),
|
|
448
|
+
files: allFiles
|
|
449
|
+
});
|
|
450
|
+
return { ok: true, installed, skipped };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/cli/github.ts
|
|
454
|
+
var REPO2 = "iceinvein/agent-skills";
|
|
455
|
+
var BRANCH2 = "main";
|
|
456
|
+
var BASE2 = `https://raw.githubusercontent.com/${REPO2}/${BRANCH2}`;
|
|
457
|
+
function buildRawUrl2(skillName, filePath) {
|
|
458
|
+
return `${BASE2}/skills/${skillName}/${filePath}`;
|
|
459
|
+
}
|
|
460
|
+
async function fetchSkillManifest2(skillName) {
|
|
461
|
+
const url = buildRawUrl2(skillName, "skill.json");
|
|
462
|
+
const res = await fetch(url);
|
|
463
|
+
if (res.status === 404) {
|
|
464
|
+
return { ok: false, error: `Skill '${skillName}' not found` };
|
|
465
|
+
}
|
|
466
|
+
if (!res.ok) {
|
|
467
|
+
return { ok: false, error: `Failed to fetch manifest: HTTP ${res.status}` };
|
|
468
|
+
}
|
|
469
|
+
const data = await res.json();
|
|
470
|
+
return validateManifest(data);
|
|
471
|
+
}
|
|
472
|
+
async function fetchSkillFile2(skillName, filePath) {
|
|
473
|
+
const url = buildRawUrl2(skillName, filePath);
|
|
474
|
+
const res = await fetch(url);
|
|
475
|
+
if (!res.ok) {
|
|
476
|
+
return { ok: false, error: `Failed to fetch '${filePath}': HTTP ${res.status}` };
|
|
477
|
+
}
|
|
478
|
+
const content = await res.text();
|
|
479
|
+
return { ok: true, content };
|
|
480
|
+
}
|
|
481
|
+
async function fetchAllSkillFiles2(skillName, manifest) {
|
|
482
|
+
const files = new Map;
|
|
483
|
+
if (manifest.files?.prompt) {
|
|
484
|
+
const result = await fetchSkillFile2(skillName, manifest.files.prompt);
|
|
485
|
+
if (!result.ok)
|
|
486
|
+
return { error: result.error };
|
|
487
|
+
files.set(manifest.files.prompt, result.content);
|
|
488
|
+
}
|
|
489
|
+
if (manifest.files?.supporting) {
|
|
490
|
+
for (const supportFile of manifest.files.supporting) {
|
|
491
|
+
const result = await fetchSkillFile2(skillName, supportFile);
|
|
492
|
+
if (!result.ok)
|
|
493
|
+
return { error: result.error };
|
|
494
|
+
files.set(supportFile, result.content);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return files;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/cli/commands/remove.ts
|
|
501
|
+
async function removeSkill(cwd, skillName) {
|
|
502
|
+
const lockfile = await readLockfile(cwd);
|
|
503
|
+
const entry = lockfile.skills[skillName];
|
|
504
|
+
if (!entry) {
|
|
505
|
+
return { ok: false, error: `Skill '${skillName}' is not installed` };
|
|
506
|
+
}
|
|
507
|
+
const manifestResult = await fetchSkillManifest2(skillName);
|
|
508
|
+
if (manifestResult.ok) {
|
|
509
|
+
for (const tool of entry.tools) {
|
|
510
|
+
const adapter = getAdapter(tool);
|
|
511
|
+
const toolFiles = entry.files.filter((f) => {
|
|
512
|
+
const config = manifestResult.manifest.install[tool];
|
|
513
|
+
if (!config)
|
|
514
|
+
return false;
|
|
515
|
+
if (config.prompt && f === config.prompt)
|
|
516
|
+
return true;
|
|
517
|
+
if (config.supporting && Object.values(config.supporting).includes(f))
|
|
518
|
+
return true;
|
|
519
|
+
if (config.mcpServers && (f.includes(".claude/settings") || f.includes(".cursor/mcp") || f.includes(".gemini/settings")))
|
|
520
|
+
return true;
|
|
521
|
+
return false;
|
|
522
|
+
});
|
|
523
|
+
await adapter.remove(cwd, manifestResult.manifest, toolFiles);
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
const { unlinkSync: unlinkSync4, existsSync: existsSync6 } = await import("node:fs");
|
|
527
|
+
const { join: join7 } = await import("node:path");
|
|
528
|
+
for (const file of entry.files) {
|
|
529
|
+
const fullPath = join7(cwd, file);
|
|
530
|
+
if (existsSync6(fullPath)) {
|
|
531
|
+
unlinkSync4(fullPath);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
await removeSkillFromLockfile(cwd, skillName);
|
|
536
|
+
return { ok: true, removed: entry.files };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/cli/commands/list.ts
|
|
540
|
+
var REPO3 = "iceinvein/agent-skills";
|
|
541
|
+
var BRANCH3 = "main";
|
|
542
|
+
var INDEX_URL = `https://raw.githubusercontent.com/${REPO3}/${BRANCH3}/skills/index.json`;
|
|
543
|
+
async function listSkills() {
|
|
544
|
+
const res = await fetch(INDEX_URL);
|
|
545
|
+
if (!res.ok) {
|
|
546
|
+
return { ok: false, error: `Failed to fetch skill index: HTTP ${res.status}` };
|
|
547
|
+
}
|
|
548
|
+
const skills = await res.json();
|
|
549
|
+
return { ok: true, skills };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/cli/commands/info.ts
|
|
553
|
+
async function infoSkill(skillName) {
|
|
554
|
+
return fetchSkillManifest2(skillName);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/cli/commands/remove.ts
|
|
558
|
+
async function removeSkill2(cwd, skillName) {
|
|
559
|
+
const lockfile = await readLockfile(cwd);
|
|
560
|
+
const entry = lockfile.skills[skillName];
|
|
561
|
+
if (!entry) {
|
|
562
|
+
return { ok: false, error: `Skill '${skillName}' is not installed` };
|
|
563
|
+
}
|
|
564
|
+
const manifestResult = await fetchSkillManifest2(skillName);
|
|
565
|
+
if (manifestResult.ok) {
|
|
566
|
+
for (const tool of entry.tools) {
|
|
567
|
+
const adapter = getAdapter(tool);
|
|
568
|
+
const toolFiles = entry.files.filter((f) => {
|
|
569
|
+
const config = manifestResult.manifest.install[tool];
|
|
570
|
+
if (!config)
|
|
571
|
+
return false;
|
|
572
|
+
if (config.prompt && f === config.prompt)
|
|
573
|
+
return true;
|
|
574
|
+
if (config.supporting && Object.values(config.supporting).includes(f))
|
|
575
|
+
return true;
|
|
576
|
+
if (config.mcpServers && (f.includes(".claude/settings") || f.includes(".cursor/mcp") || f.includes(".gemini/settings")))
|
|
577
|
+
return true;
|
|
578
|
+
return false;
|
|
579
|
+
});
|
|
580
|
+
await adapter.remove(cwd, manifestResult.manifest, toolFiles);
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
const { unlinkSync: unlinkSync4, existsSync: existsSync6 } = await import("node:fs");
|
|
584
|
+
const { join: join7 } = await import("node:path");
|
|
585
|
+
for (const file of entry.files) {
|
|
586
|
+
const fullPath = join7(cwd, file);
|
|
587
|
+
if (existsSync6(fullPath)) {
|
|
588
|
+
unlinkSync4(fullPath);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
await removeSkillFromLockfile(cwd, skillName);
|
|
593
|
+
return { ok: true, removed: entry.files };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/cli/commands/install.ts
|
|
597
|
+
async function installSkill2(cwd, manifest, files, targetTools) {
|
|
598
|
+
const compatibleTools = targetTools.filter((t) => manifest.tools.includes(t));
|
|
599
|
+
const skipped = targetTools.filter((t) => !manifest.tools.includes(t));
|
|
600
|
+
if (compatibleTools.length === 0) {
|
|
601
|
+
return {
|
|
602
|
+
ok: false,
|
|
603
|
+
error: `Skill '${manifest.name}' does not support any of: ${targetTools.join(", ")}. Supported: ${manifest.tools.join(", ")}`
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const installed = {};
|
|
607
|
+
const allFiles = [];
|
|
608
|
+
for (const tool of compatibleTools) {
|
|
609
|
+
const adapter = getAdapter(tool);
|
|
610
|
+
const toolFiles = await adapter.install(cwd, manifest, files);
|
|
611
|
+
installed[tool] = toolFiles;
|
|
612
|
+
allFiles.push(...toolFiles);
|
|
613
|
+
}
|
|
614
|
+
await addSkillToLockfile(cwd, manifest.name, {
|
|
615
|
+
version: manifest.version,
|
|
616
|
+
tools: compatibleTools,
|
|
617
|
+
installedAt: new Date().toISOString(),
|
|
618
|
+
files: allFiles
|
|
619
|
+
});
|
|
620
|
+
return { ok: true, installed, skipped };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/cli/commands/update.ts
|
|
624
|
+
async function updateSkill(cwd, skillName) {
|
|
625
|
+
const lockfile = await readLockfile(cwd);
|
|
626
|
+
const entry = lockfile.skills[skillName];
|
|
627
|
+
if (!entry) {
|
|
628
|
+
return { ok: false, error: `Skill '${skillName}' is not installed` };
|
|
629
|
+
}
|
|
630
|
+
const oldVersion = entry.version;
|
|
631
|
+
const tools = entry.tools;
|
|
632
|
+
const manifestResult = await fetchSkillManifest2(skillName);
|
|
633
|
+
if (!manifestResult.ok) {
|
|
634
|
+
return { ok: false, error: manifestResult.error };
|
|
635
|
+
}
|
|
636
|
+
const filesResult = await fetchAllSkillFiles2(skillName, manifestResult.manifest);
|
|
637
|
+
if ("error" in filesResult) {
|
|
638
|
+
return { ok: false, error: filesResult.error };
|
|
639
|
+
}
|
|
640
|
+
await removeSkill2(cwd, skillName);
|
|
641
|
+
const installResult = await installSkill2(cwd, manifestResult.manifest, filesResult, tools);
|
|
642
|
+
if (!installResult.ok) {
|
|
643
|
+
return { ok: false, error: installResult.error };
|
|
644
|
+
}
|
|
645
|
+
return { ok: true, from: oldVersion, to: manifestResult.manifest.version };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/cli/types.ts
|
|
649
|
+
var TOOL_NAMES2 = ["claude", "cursor", "codex", "gemini"];
|
|
650
|
+
|
|
651
|
+
// src/cli/index.ts
|
|
652
|
+
function parseArgs(argv) {
|
|
653
|
+
if (argv.length === 0)
|
|
654
|
+
return { command: "help", args: [], flags: {} };
|
|
655
|
+
const command = argv[0];
|
|
656
|
+
const args = [];
|
|
657
|
+
const flags = {};
|
|
658
|
+
for (let i = 1;i < argv.length; i++) {
|
|
659
|
+
if (argv[i].startsWith("--") && i + 1 < argv.length) {
|
|
660
|
+
flags[argv[i].slice(2)] = argv[i + 1];
|
|
661
|
+
i++;
|
|
662
|
+
} else {
|
|
663
|
+
args.push(argv[i]);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return { command, args, flags };
|
|
667
|
+
}
|
|
668
|
+
function printHelp() {
|
|
669
|
+
console.log(`
|
|
670
|
+
@iceinvein/agent-skills \u2014 Install agent skills into AI coding tools
|
|
671
|
+
|
|
672
|
+
Usage:
|
|
673
|
+
agent-skills install <skill> [--tool <tool>] Install a skill
|
|
674
|
+
agent-skills remove <skill> Remove a skill
|
|
675
|
+
agent-skills update <skill> Update a skill
|
|
676
|
+
agent-skills list List available skills
|
|
677
|
+
agent-skills info <skill> Show skill details
|
|
678
|
+
|
|
679
|
+
Tools: ${TOOL_NAMES2.join(", ")}
|
|
680
|
+
|
|
681
|
+
If --tool is omitted, auto-detects tools in the current directory.
|
|
682
|
+
`);
|
|
683
|
+
}
|
|
684
|
+
function printInstalled(result, skipped) {
|
|
685
|
+
for (const [tool, files] of Object.entries(result)) {
|
|
686
|
+
console.log(` \u2713 ${tool}`);
|
|
687
|
+
for (const file of files) {
|
|
688
|
+
console.log(` \u2192 ${file}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const tool of skipped) {
|
|
692
|
+
console.log(` \u2298 ${tool} (skill does not support this tool)`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async function main() {
|
|
696
|
+
const { command, args, flags } = parseArgs(process.argv.slice(2));
|
|
697
|
+
switch (command) {
|
|
698
|
+
case "install": {
|
|
699
|
+
const skillName = args[0];
|
|
700
|
+
if (!skillName) {
|
|
701
|
+
console.error("Error: skill name required. Usage: agent-skills install <skill>");
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
console.log(`Fetching skill '${skillName}'...`);
|
|
705
|
+
const manifestResult = await fetchSkillManifest(skillName);
|
|
706
|
+
if (!manifestResult.ok) {
|
|
707
|
+
console.error(`Error: ${manifestResult.error}`);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
const filesResult = await fetchAllSkillFiles(skillName, manifestResult.manifest);
|
|
711
|
+
if ("error" in filesResult) {
|
|
712
|
+
console.error(`Error: ${filesResult.error}`);
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
let tools;
|
|
716
|
+
if (flags.tool) {
|
|
717
|
+
if (!TOOL_NAMES2.includes(flags.tool)) {
|
|
718
|
+
console.error(`Error: unknown tool '${flags.tool}'. Must be one of: ${TOOL_NAMES2.join(", ")}`);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
tools = [flags.tool];
|
|
722
|
+
} else {
|
|
723
|
+
tools = await detectTools(process.cwd());
|
|
724
|
+
if (tools.length === 0) {
|
|
725
|
+
console.error("Error: no supported tools detected. Use --tool to specify one.");
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
console.log(`Detected tools: ${tools.join(", ")}`);
|
|
729
|
+
}
|
|
730
|
+
const result = await installSkill(process.cwd(), manifestResult.manifest, filesResult, tools);
|
|
731
|
+
if (!result.ok) {
|
|
732
|
+
console.error(`Error: ${result.error}`);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
console.log(`
|
|
736
|
+
\u2713 Installed '${skillName}' v${manifestResult.manifest.version}:`);
|
|
737
|
+
printInstalled(result.installed, result.skipped);
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
case "remove": {
|
|
741
|
+
const skillName = args[0];
|
|
742
|
+
if (!skillName) {
|
|
743
|
+
console.error("Error: skill name required. Usage: agent-skills remove <skill>");
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
const result = await removeSkill(process.cwd(), skillName);
|
|
747
|
+
if (!result.ok) {
|
|
748
|
+
console.error(`Error: ${result.error}`);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
console.log(`\u2713 Removed '${skillName}'`);
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "update": {
|
|
755
|
+
const skillName = args[0];
|
|
756
|
+
if (!skillName) {
|
|
757
|
+
console.error("Error: skill name required. Usage: agent-skills update <skill>");
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
console.log(`Updating '${skillName}'...`);
|
|
761
|
+
const result = await updateSkill(process.cwd(), skillName);
|
|
762
|
+
if (!result.ok) {
|
|
763
|
+
console.error(`Error: ${result.error}`);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
console.log(`\u2713 Updated '${skillName}' from v${result.from} to v${result.to}`);
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case "list": {
|
|
770
|
+
console.log(`Fetching available skills...
|
|
771
|
+
`);
|
|
772
|
+
const result = await listSkills();
|
|
773
|
+
if (!result.ok) {
|
|
774
|
+
console.error(`Error: ${result.error}`);
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
for (const skill of result.skills) {
|
|
778
|
+
const typeTag = skill.type === "prompt" ? "\uD83D\uDCDD" : skill.type === "code" ? "\u2699\uFE0F" : "\uD83D\uDD00";
|
|
779
|
+
console.log(` ${typeTag} ${skill.name} (v${skill.version})`);
|
|
780
|
+
console.log(` ${skill.description}
|
|
781
|
+
`);
|
|
782
|
+
}
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
case "info": {
|
|
786
|
+
const skillName = args[0];
|
|
787
|
+
if (!skillName) {
|
|
788
|
+
console.error("Error: skill name required. Usage: agent-skills info <skill>");
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
const result = await infoSkill(skillName);
|
|
792
|
+
if (!result.ok) {
|
|
793
|
+
console.error(`Error: ${result.error}`);
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
const m = result.manifest;
|
|
797
|
+
console.log(`
|
|
798
|
+
${m.name} v${m.version}`);
|
|
799
|
+
console.log(` ${m.description}`);
|
|
800
|
+
console.log(` Author: ${m.author}`);
|
|
801
|
+
console.log(` Type: ${m.type}`);
|
|
802
|
+
console.log(` Tools: ${m.tools.join(", ")}`);
|
|
803
|
+
if (m.mcp)
|
|
804
|
+
console.log(` Package: ${m.mcp.package}`);
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
case "help":
|
|
808
|
+
default:
|
|
809
|
+
printHelp();
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
main().catch((err) => {
|
|
814
|
+
console.error("Fatal:", err.message);
|
|
815
|
+
process.exit(1);
|
|
816
|
+
});
|
|
817
|
+
export {
|
|
818
|
+
parseArgs
|
|
819
|
+
};
|