@zot24/add-skill 1.0.10
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 +270 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1120 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
// src/git.ts
|
|
9
|
+
import simpleGit from "simple-git";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
function parseSource(input) {
|
|
14
|
+
const githubTreeMatch = input.match(
|
|
15
|
+
/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/
|
|
16
|
+
);
|
|
17
|
+
if (githubTreeMatch) {
|
|
18
|
+
const [, owner, repo, , subpath] = githubTreeMatch;
|
|
19
|
+
return {
|
|
20
|
+
type: "github",
|
|
21
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
22
|
+
subpath
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
26
|
+
if (githubRepoMatch) {
|
|
27
|
+
const [, owner, repo] = githubRepoMatch;
|
|
28
|
+
const cleanRepo = repo.replace(/\.git$/, "");
|
|
29
|
+
return {
|
|
30
|
+
type: "github",
|
|
31
|
+
url: `https://github.com/${owner}/${cleanRepo}.git`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const gitlabTreeMatch = input.match(
|
|
35
|
+
/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)\/(.+)/
|
|
36
|
+
);
|
|
37
|
+
if (gitlabTreeMatch) {
|
|
38
|
+
const [, owner, repo, , subpath] = gitlabTreeMatch;
|
|
39
|
+
return {
|
|
40
|
+
type: "gitlab",
|
|
41
|
+
url: `https://gitlab.com/${owner}/${repo}.git`,
|
|
42
|
+
subpath
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const gitlabRepoMatch = input.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
|
|
46
|
+
if (gitlabRepoMatch) {
|
|
47
|
+
const [, owner, repo] = gitlabRepoMatch;
|
|
48
|
+
const cleanRepo = repo.replace(/\.git$/, "");
|
|
49
|
+
return {
|
|
50
|
+
type: "gitlab",
|
|
51
|
+
url: `https://gitlab.com/${owner}/${cleanRepo}.git`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
|
|
55
|
+
if (shorthandMatch && !input.includes(":")) {
|
|
56
|
+
const [, owner, repo, subpath] = shorthandMatch;
|
|
57
|
+
return {
|
|
58
|
+
type: "github",
|
|
59
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
60
|
+
subpath
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
type: "git",
|
|
65
|
+
url: input
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function cloneRepo(url) {
|
|
69
|
+
const tempDir = await mkdtemp(join(tmpdir(), "add-skill-"));
|
|
70
|
+
const git = simpleGit();
|
|
71
|
+
await git.clone(url, tempDir, ["--depth", "1"]);
|
|
72
|
+
return tempDir;
|
|
73
|
+
}
|
|
74
|
+
async function cloneRepoAtVersion(url, version2) {
|
|
75
|
+
const tempDir = await mkdtemp(join(tmpdir(), "add-skill-"));
|
|
76
|
+
const git = simpleGit();
|
|
77
|
+
if (!version2) {
|
|
78
|
+
await git.clone(url, tempDir, ["--depth", "1"]);
|
|
79
|
+
const repoGit2 = simpleGit(tempDir);
|
|
80
|
+
const log3 = await repoGit2.log(["-1", "--format=%H"]);
|
|
81
|
+
return {
|
|
82
|
+
tempDir,
|
|
83
|
+
resolvedRef: log3.latest?.hash || "HEAD"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const tagVariants = [`v${version2}`, version2];
|
|
87
|
+
for (const tag of tagVariants) {
|
|
88
|
+
try {
|
|
89
|
+
await git.clone(url, tempDir, ["--depth", "1", "--branch", tag]);
|
|
90
|
+
const repoGit2 = simpleGit(tempDir);
|
|
91
|
+
const log3 = await repoGit2.log(["-1", "--format=%H"]);
|
|
92
|
+
return {
|
|
93
|
+
tempDir,
|
|
94
|
+
resolvedRef: log3.latest?.hash || "HEAD"
|
|
95
|
+
};
|
|
96
|
+
} catch {
|
|
97
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {
|
|
98
|
+
});
|
|
99
|
+
await mkdtemp(join(tmpdir(), "add-skill-")).then((dir) => {
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const fallbackDir = await mkdtemp(join(tmpdir(), "add-skill-"));
|
|
104
|
+
await git.clone(url, fallbackDir, ["--depth", "1"]);
|
|
105
|
+
const repoGit = simpleGit(fallbackDir);
|
|
106
|
+
const log2 = await repoGit.log(["-1", "--format=%H"]);
|
|
107
|
+
return {
|
|
108
|
+
tempDir: fallbackDir,
|
|
109
|
+
resolvedRef: log2.latest?.hash || "HEAD"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function cleanupTempDir(dir) {
|
|
113
|
+
await rm(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/skills.ts
|
|
117
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
118
|
+
import { join as join2, basename, dirname } from "path";
|
|
119
|
+
import matter from "gray-matter";
|
|
120
|
+
var SKIP_DIRS = ["node_modules", ".git", "dist", "build", "__pycache__"];
|
|
121
|
+
async function hasSkillMd(dir) {
|
|
122
|
+
try {
|
|
123
|
+
const skillPath = join2(dir, "SKILL.md");
|
|
124
|
+
const stats = await stat(skillPath);
|
|
125
|
+
return stats.isFile();
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function parseSkillMd(skillMdPath) {
|
|
131
|
+
try {
|
|
132
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
133
|
+
const { data } = matter(content);
|
|
134
|
+
if (!data.name || !data.description) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
let version2;
|
|
138
|
+
if (data.version && typeof data.version === "string") {
|
|
139
|
+
version2 = {
|
|
140
|
+
version: data.version,
|
|
141
|
+
source: "frontmatter"
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
name: data.name,
|
|
146
|
+
description: data.description,
|
|
147
|
+
path: dirname(skillMdPath),
|
|
148
|
+
metadata: data.metadata,
|
|
149
|
+
version: version2
|
|
150
|
+
};
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
156
|
+
const skillDirs = [];
|
|
157
|
+
if (depth > maxDepth) return skillDirs;
|
|
158
|
+
try {
|
|
159
|
+
if (await hasSkillMd(dir)) {
|
|
160
|
+
skillDirs.push(dir);
|
|
161
|
+
}
|
|
162
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
if (entry.isDirectory() && !SKIP_DIRS.includes(entry.name)) {
|
|
165
|
+
const subDirs = await findSkillDirs(join2(dir, entry.name), depth + 1, maxDepth);
|
|
166
|
+
skillDirs.push(...subDirs);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
return skillDirs;
|
|
172
|
+
}
|
|
173
|
+
async function discoverSkills(basePath, subpath) {
|
|
174
|
+
const skills = [];
|
|
175
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
176
|
+
const searchPath = subpath ? join2(basePath, subpath) : basePath;
|
|
177
|
+
if (await hasSkillMd(searchPath)) {
|
|
178
|
+
const skill = await parseSkillMd(join2(searchPath, "SKILL.md"));
|
|
179
|
+
if (skill) {
|
|
180
|
+
skills.push(skill);
|
|
181
|
+
return skills;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const prioritySearchDirs = [
|
|
185
|
+
searchPath,
|
|
186
|
+
join2(searchPath, "skills"),
|
|
187
|
+
join2(searchPath, "skills/.curated"),
|
|
188
|
+
join2(searchPath, "skills/.experimental"),
|
|
189
|
+
join2(searchPath, "skills/.system"),
|
|
190
|
+
join2(searchPath, ".codex/skills"),
|
|
191
|
+
join2(searchPath, ".claude/skills"),
|
|
192
|
+
join2(searchPath, ".opencode/skill"),
|
|
193
|
+
join2(searchPath, ".cursor/skills"),
|
|
194
|
+
join2(searchPath, ".agents/skills"),
|
|
195
|
+
join2(searchPath, ".kilocode/skills"),
|
|
196
|
+
join2(searchPath, ".roo/skills"),
|
|
197
|
+
join2(searchPath, ".goose/skills"),
|
|
198
|
+
join2(searchPath, ".agent/skills")
|
|
199
|
+
];
|
|
200
|
+
for (const dir of prioritySearchDirs) {
|
|
201
|
+
try {
|
|
202
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (entry.isDirectory()) {
|
|
205
|
+
const skillDir = join2(dir, entry.name);
|
|
206
|
+
if (await hasSkillMd(skillDir)) {
|
|
207
|
+
const skill = await parseSkillMd(join2(skillDir, "SKILL.md"));
|
|
208
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
209
|
+
skills.push(skill);
|
|
210
|
+
seenNames.add(skill.name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (skills.length === 0) {
|
|
219
|
+
const allSkillDirs = await findSkillDirs(searchPath);
|
|
220
|
+
for (const skillDir of allSkillDirs) {
|
|
221
|
+
const skill = await parseSkillMd(join2(skillDir, "SKILL.md"));
|
|
222
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
223
|
+
skills.push(skill);
|
|
224
|
+
seenNames.add(skill.name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return skills;
|
|
229
|
+
}
|
|
230
|
+
function getSkillDisplayName(skill) {
|
|
231
|
+
return skill.name || basename(skill.path);
|
|
232
|
+
}
|
|
233
|
+
function validateSkillVersion(skill, requestedVersion) {
|
|
234
|
+
const actual = skill.version?.version;
|
|
235
|
+
if (!actual) {
|
|
236
|
+
return {
|
|
237
|
+
valid: true,
|
|
238
|
+
// Allow unversioned skills with a warning
|
|
239
|
+
actual: void 0,
|
|
240
|
+
message: `Skill "${skill.name}" has no version in SKILL.md. Installing from repository tag/branch.`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (actual === requestedVersion) {
|
|
244
|
+
return { valid: true, actual };
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
valid: false,
|
|
248
|
+
actual,
|
|
249
|
+
message: `Version mismatch for "${skill.name}": requested ${requestedVersion}, found ${actual}`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/installer.ts
|
|
254
|
+
import { mkdir, cp, access, readdir as readdir2 } from "fs/promises";
|
|
255
|
+
import { join as join4, basename as basename2 } from "path";
|
|
256
|
+
|
|
257
|
+
// src/agents.ts
|
|
258
|
+
import { homedir } from "os";
|
|
259
|
+
import { join as join3 } from "path";
|
|
260
|
+
import { existsSync } from "fs";
|
|
261
|
+
var home = homedir();
|
|
262
|
+
var agents = {
|
|
263
|
+
opencode: {
|
|
264
|
+
name: "opencode",
|
|
265
|
+
displayName: "OpenCode",
|
|
266
|
+
skillsDir: ".opencode/skill",
|
|
267
|
+
globalSkillsDir: join3(home, ".config/opencode/skill"),
|
|
268
|
+
detectInstalled: async () => {
|
|
269
|
+
return existsSync(join3(home, ".config/opencode")) || existsSync(join3(home, ".claude/skills"));
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"claude-code": {
|
|
273
|
+
name: "claude-code",
|
|
274
|
+
displayName: "Claude Code",
|
|
275
|
+
skillsDir: ".claude/skills",
|
|
276
|
+
globalSkillsDir: join3(home, ".claude/skills"),
|
|
277
|
+
detectInstalled: async () => {
|
|
278
|
+
return existsSync(join3(home, ".claude"));
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
codex: {
|
|
282
|
+
name: "codex",
|
|
283
|
+
displayName: "Codex",
|
|
284
|
+
skillsDir: ".codex/skills",
|
|
285
|
+
globalSkillsDir: join3(home, ".codex/skills"),
|
|
286
|
+
detectInstalled: async () => {
|
|
287
|
+
return existsSync(join3(home, ".codex"));
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
cursor: {
|
|
291
|
+
name: "cursor",
|
|
292
|
+
displayName: "Cursor",
|
|
293
|
+
skillsDir: ".cursor/skills",
|
|
294
|
+
globalSkillsDir: join3(home, ".cursor/skills"),
|
|
295
|
+
detectInstalled: async () => {
|
|
296
|
+
return existsSync(join3(home, ".cursor"));
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
amp: {
|
|
300
|
+
name: "amp",
|
|
301
|
+
displayName: "Amp",
|
|
302
|
+
skillsDir: ".agents/skills",
|
|
303
|
+
globalSkillsDir: join3(home, ".config/agents/skills"),
|
|
304
|
+
detectInstalled: async () => {
|
|
305
|
+
return existsSync(join3(home, ".config/amp"));
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
kilo: {
|
|
309
|
+
name: "kilo",
|
|
310
|
+
displayName: "Kilo Code",
|
|
311
|
+
skillsDir: ".kilocode/skills",
|
|
312
|
+
globalSkillsDir: join3(home, ".kilocode/skills"),
|
|
313
|
+
detectInstalled: async () => {
|
|
314
|
+
return existsSync(join3(home, ".kilocode"));
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
roo: {
|
|
318
|
+
name: "roo",
|
|
319
|
+
displayName: "Roo Code",
|
|
320
|
+
skillsDir: ".roo/skills",
|
|
321
|
+
globalSkillsDir: join3(home, ".roo/skills"),
|
|
322
|
+
detectInstalled: async () => {
|
|
323
|
+
return existsSync(join3(home, ".roo"));
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
goose: {
|
|
327
|
+
name: "goose",
|
|
328
|
+
displayName: "Goose",
|
|
329
|
+
skillsDir: ".goose/skills",
|
|
330
|
+
globalSkillsDir: join3(home, ".config/goose/skills"),
|
|
331
|
+
detectInstalled: async () => {
|
|
332
|
+
return existsSync(join3(home, ".config/goose"));
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
antigravity: {
|
|
336
|
+
name: "antigravity",
|
|
337
|
+
displayName: "Antigravity",
|
|
338
|
+
skillsDir: ".agent/skills",
|
|
339
|
+
globalSkillsDir: join3(home, ".gemini/antigravity/skills"),
|
|
340
|
+
detectInstalled: async () => {
|
|
341
|
+
return existsSync(join3(process.cwd(), ".agent")) || existsSync(join3(home, ".gemini/antigravity"));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
async function detectInstalledAgents() {
|
|
346
|
+
const installed = [];
|
|
347
|
+
for (const [type, config] of Object.entries(agents)) {
|
|
348
|
+
if (await config.detectInstalled()) {
|
|
349
|
+
installed.push(type);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return installed;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/installer.ts
|
|
356
|
+
async function installSkillForAgent(skill, agentType, options = {}) {
|
|
357
|
+
const agent = agents[agentType];
|
|
358
|
+
const skillName = skill.name || basename2(skill.path);
|
|
359
|
+
const targetBase = options.global ? agent.globalSkillsDir : join4(options.cwd || process.cwd(), agent.skillsDir);
|
|
360
|
+
const targetDir = join4(targetBase, skillName);
|
|
361
|
+
try {
|
|
362
|
+
await mkdir(targetDir, { recursive: true });
|
|
363
|
+
await copyDirectory(skill.path, targetDir);
|
|
364
|
+
return { success: true, path: targetDir };
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return {
|
|
367
|
+
success: false,
|
|
368
|
+
path: targetDir,
|
|
369
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
var EXCLUDE_FILES = /* @__PURE__ */ new Set([
|
|
374
|
+
"README.md",
|
|
375
|
+
"metadata.json"
|
|
376
|
+
]);
|
|
377
|
+
var isExcluded = (name) => {
|
|
378
|
+
if (EXCLUDE_FILES.has(name)) return true;
|
|
379
|
+
if (name.startsWith("_")) return true;
|
|
380
|
+
return false;
|
|
381
|
+
};
|
|
382
|
+
async function copyDirectory(src, dest) {
|
|
383
|
+
await mkdir(dest, { recursive: true });
|
|
384
|
+
const entries = await readdir2(src, { withFileTypes: true });
|
|
385
|
+
for (const entry of entries) {
|
|
386
|
+
if (isExcluded(entry.name)) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const srcPath = join4(src, entry.name);
|
|
390
|
+
const destPath = join4(dest, entry.name);
|
|
391
|
+
if (entry.isDirectory()) {
|
|
392
|
+
await copyDirectory(srcPath, destPath);
|
|
393
|
+
} else {
|
|
394
|
+
await cp(srcPath, destPath);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
399
|
+
const agent = agents[agentType];
|
|
400
|
+
const targetBase = options.global ? agent.globalSkillsDir : join4(options.cwd || process.cwd(), agent.skillsDir);
|
|
401
|
+
const skillDir = join4(targetBase, skillName);
|
|
402
|
+
try {
|
|
403
|
+
await access(skillDir);
|
|
404
|
+
return true;
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function getInstallPath(skillName, agentType, options = {}) {
|
|
410
|
+
const agent = agents[agentType];
|
|
411
|
+
const targetBase = options.global ? agent.globalSkillsDir : join4(options.cwd || process.cwd(), agent.skillsDir);
|
|
412
|
+
return join4(targetBase, skillName);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/telemetry.ts
|
|
416
|
+
var TELEMETRY_URL = "https://add-skill.vercel.sh/t";
|
|
417
|
+
var cliVersion = null;
|
|
418
|
+
function isCI() {
|
|
419
|
+
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
|
|
420
|
+
}
|
|
421
|
+
function isEnabled() {
|
|
422
|
+
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
|
|
423
|
+
}
|
|
424
|
+
function setVersion(version2) {
|
|
425
|
+
cliVersion = version2;
|
|
426
|
+
}
|
|
427
|
+
function track(data) {
|
|
428
|
+
if (!isEnabled()) return;
|
|
429
|
+
try {
|
|
430
|
+
const params = new URLSearchParams();
|
|
431
|
+
if (cliVersion) {
|
|
432
|
+
params.set("v", cliVersion);
|
|
433
|
+
}
|
|
434
|
+
if (isCI()) {
|
|
435
|
+
params.set("ci", "1");
|
|
436
|
+
}
|
|
437
|
+
for (const [key, value] of Object.entries(data)) {
|
|
438
|
+
if (value !== void 0 && value !== null) {
|
|
439
|
+
params.set(key, String(value));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {
|
|
443
|
+
});
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/manifest.ts
|
|
449
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
450
|
+
import { dirname as dirname2 } from "path";
|
|
451
|
+
import { parse, stringify } from "smol-toml";
|
|
452
|
+
var ManifestParseError = class extends Error {
|
|
453
|
+
constructor(message, filePath) {
|
|
454
|
+
super(message);
|
|
455
|
+
this.filePath = filePath;
|
|
456
|
+
this.name = "ManifestParseError";
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var SkillNotFoundError = class extends Error {
|
|
460
|
+
constructor(skillName, source, availableSkills) {
|
|
461
|
+
super(`Skill "${skillName}" not found in ${source}. Available: ${availableSkills.join(", ") || "none"}`);
|
|
462
|
+
this.skillName = skillName;
|
|
463
|
+
this.source = source;
|
|
464
|
+
this.availableSkills = availableSkills;
|
|
465
|
+
this.name = "SkillNotFoundError";
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
async function parseManifestFile(filePath) {
|
|
469
|
+
let content;
|
|
470
|
+
try {
|
|
471
|
+
content = await readFile2(filePath, "utf-8");
|
|
472
|
+
} catch (error) {
|
|
473
|
+
throw new ManifestParseError(
|
|
474
|
+
`Could not read manifest file: ${error.message}`,
|
|
475
|
+
filePath
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
let parsed;
|
|
479
|
+
try {
|
|
480
|
+
parsed = parse(content);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
throw new ManifestParseError(
|
|
483
|
+
`Invalid TOML: ${error.message}`,
|
|
484
|
+
filePath
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
if (!parsed.skills || !Array.isArray(parsed.skills)) {
|
|
488
|
+
throw new ManifestParseError(
|
|
489
|
+
"Manifest must contain a [[skills]] array",
|
|
490
|
+
filePath
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const skills = [];
|
|
494
|
+
for (let i = 0; i < parsed.skills.length; i++) {
|
|
495
|
+
const entry = parsed.skills[i];
|
|
496
|
+
if (!entry || typeof entry !== "object") {
|
|
497
|
+
throw new ManifestParseError(
|
|
498
|
+
`Invalid skill entry at index ${i}`,
|
|
499
|
+
filePath
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (!entry.source || typeof entry.source !== "string") {
|
|
503
|
+
throw new ManifestParseError(
|
|
504
|
+
`Skill entry ${i} missing required "source" field`,
|
|
505
|
+
filePath
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (!entry.name || typeof entry.name !== "string") {
|
|
509
|
+
throw new ManifestParseError(
|
|
510
|
+
`Skill entry ${i} missing required "name" field`,
|
|
511
|
+
filePath
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
const manifestEntry = {
|
|
515
|
+
source: entry.source,
|
|
516
|
+
name: entry.name
|
|
517
|
+
};
|
|
518
|
+
if (entry.version && typeof entry.version === "string") {
|
|
519
|
+
manifestEntry.version = entry.version;
|
|
520
|
+
}
|
|
521
|
+
validateManifestEntry(manifestEntry);
|
|
522
|
+
skills.push(manifestEntry);
|
|
523
|
+
}
|
|
524
|
+
if (skills.length === 0) {
|
|
525
|
+
throw new ManifestParseError(
|
|
526
|
+
"Manifest file contains no skill entries",
|
|
527
|
+
filePath
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
return { skills };
|
|
531
|
+
}
|
|
532
|
+
function validateManifestEntry(entry) {
|
|
533
|
+
const isShorthand = /^[^/]+\/[^/]+$/.test(entry.source);
|
|
534
|
+
const isUrl = entry.source.includes("://") || entry.source.startsWith("git@");
|
|
535
|
+
if (!isShorthand && !isUrl) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Invalid source "${entry.source}". Use "owner/repo" or a full git URL.`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
if (entry.version && entry.version !== "latest") {
|
|
541
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/;
|
|
542
|
+
if (!semverRegex.test(entry.version)) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Invalid version "${entry.version}" for skill "${entry.name}". Use semantic versioning (e.g., 1.0.0) or "latest".`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function getLockFilePath(manifestPath) {
|
|
550
|
+
const dir = dirname2(manifestPath);
|
|
551
|
+
const baseName = manifestPath.replace(/\.toml$/, "");
|
|
552
|
+
return `${baseName}-lock.toml`;
|
|
553
|
+
}
|
|
554
|
+
async function writeLockFile(lockPath, entries) {
|
|
555
|
+
const lockFile = {
|
|
556
|
+
lockVersion: 1,
|
|
557
|
+
skills: entries.map((entry) => ({
|
|
558
|
+
source: entry.source,
|
|
559
|
+
name: entry.name,
|
|
560
|
+
version: entry.version,
|
|
561
|
+
resolvedRef: entry.resolvedRef,
|
|
562
|
+
installedAt: entry.installedAt
|
|
563
|
+
}))
|
|
564
|
+
};
|
|
565
|
+
const tomlContent = stringify(lockFile);
|
|
566
|
+
await writeFile(lockPath, tomlContent, "utf-8");
|
|
567
|
+
}
|
|
568
|
+
function groupSkillsBySource(skills) {
|
|
569
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
570
|
+
for (const skill of skills) {
|
|
571
|
+
const key = skill.version ? `${skill.source}@${skill.version}` : skill.source;
|
|
572
|
+
const existing = grouped.get(key) || [];
|
|
573
|
+
existing.push(skill);
|
|
574
|
+
grouped.set(key, existing);
|
|
575
|
+
}
|
|
576
|
+
return grouped;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// package.json
|
|
580
|
+
var package_default = {
|
|
581
|
+
name: "@zot24/add-skill",
|
|
582
|
+
version: "1.0.10",
|
|
583
|
+
description: "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
|
|
584
|
+
type: "module",
|
|
585
|
+
bin: {
|
|
586
|
+
"add-skill": "./dist/index.js"
|
|
587
|
+
},
|
|
588
|
+
files: [
|
|
589
|
+
"dist",
|
|
590
|
+
"README.md"
|
|
591
|
+
],
|
|
592
|
+
scripts: {
|
|
593
|
+
build: "tsup src/index.ts --format esm --dts --clean",
|
|
594
|
+
dev: "tsx src/index.ts",
|
|
595
|
+
prepublishOnly: "npm run build"
|
|
596
|
+
},
|
|
597
|
+
keywords: [
|
|
598
|
+
"cli",
|
|
599
|
+
"skills",
|
|
600
|
+
"opencode",
|
|
601
|
+
"claude-code",
|
|
602
|
+
"codex",
|
|
603
|
+
"cursor",
|
|
604
|
+
"antigravity",
|
|
605
|
+
"ai-agents"
|
|
606
|
+
],
|
|
607
|
+
repository: {
|
|
608
|
+
type: "git",
|
|
609
|
+
url: "git+https://github.com/vercel-labs/add-skill.git"
|
|
610
|
+
},
|
|
611
|
+
homepage: "https://github.com/vercel-labs/add-skill#readme",
|
|
612
|
+
bugs: {
|
|
613
|
+
url: "https://github.com/vercel-labs/add-skill/issues"
|
|
614
|
+
},
|
|
615
|
+
author: "",
|
|
616
|
+
license: "MIT",
|
|
617
|
+
dependencies: {
|
|
618
|
+
"@clack/prompts": "^0.9.1",
|
|
619
|
+
chalk: "^5.4.1",
|
|
620
|
+
commander: "^13.1.0",
|
|
621
|
+
"gray-matter": "^4.0.3",
|
|
622
|
+
"simple-git": "^3.27.0",
|
|
623
|
+
"smol-toml": "^1.3.1"
|
|
624
|
+
},
|
|
625
|
+
devDependencies: {
|
|
626
|
+
"@types/node": "^22.10.0",
|
|
627
|
+
tsup: "^8.3.5",
|
|
628
|
+
tsx: "^4.19.2",
|
|
629
|
+
typescript: "^5.7.2"
|
|
630
|
+
},
|
|
631
|
+
engines: {
|
|
632
|
+
node: ">=18"
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// src/index.ts
|
|
637
|
+
var version = package_default.version;
|
|
638
|
+
setVersion(version);
|
|
639
|
+
program.name("add-skill").description("Install skills onto coding agents (OpenCode, Claude Code, Codex, Cursor, Antigravity)").version(version).argument("[source]", "Git repo URL, GitHub shorthand (owner/repo), or direct path to skill").option("-g, --global", "Install skill globally (user-level) instead of project-level").option("-a, --agent <agents...>", "Specify agents to install to (opencode, claude-code, codex, cursor)").option("-s, --skill <skills...>", "Specify skill names to install (skip selection prompt)").option("-l, --list", "List available skills in the repository without installing").option("-y, --yes", "Skip confirmation prompts").option("-f, --from-file <path>", "Install skills from a TOML manifest file").option("--no-lock", "Skip generating/updating lock file when using --from-file").action(async (source, options) => {
|
|
640
|
+
if (options.fromFile) {
|
|
641
|
+
await installFromManifest(options.fromFile, options);
|
|
642
|
+
} else if (source) {
|
|
643
|
+
await main(source, options);
|
|
644
|
+
} else {
|
|
645
|
+
p.log.error("Missing required argument: source");
|
|
646
|
+
p.log.info("Usage: add-skill <source> or add-skill --from-file <path>");
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
program.parse();
|
|
651
|
+
async function main(source, options) {
|
|
652
|
+
console.log();
|
|
653
|
+
p.intro(chalk.bgCyan.black(" add-skill "));
|
|
654
|
+
let tempDir = null;
|
|
655
|
+
try {
|
|
656
|
+
const spinner2 = p.spinner();
|
|
657
|
+
spinner2.start("Parsing source...");
|
|
658
|
+
const parsed = parseSource(source);
|
|
659
|
+
spinner2.stop(`Source: ${chalk.cyan(parsed.url)}${parsed.subpath ? ` (${parsed.subpath})` : ""}`);
|
|
660
|
+
spinner2.start("Cloning repository...");
|
|
661
|
+
tempDir = await cloneRepo(parsed.url);
|
|
662
|
+
spinner2.stop("Repository cloned");
|
|
663
|
+
spinner2.start("Discovering skills...");
|
|
664
|
+
const skills = await discoverSkills(tempDir, parsed.subpath);
|
|
665
|
+
if (skills.length === 0) {
|
|
666
|
+
spinner2.stop(chalk.red("No skills found"));
|
|
667
|
+
p.outro(chalk.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
668
|
+
await cleanup(tempDir);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
spinner2.stop(`Found ${chalk.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
672
|
+
if (options.list) {
|
|
673
|
+
console.log();
|
|
674
|
+
p.log.step(chalk.bold("Available Skills"));
|
|
675
|
+
for (const skill of skills) {
|
|
676
|
+
p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
|
|
677
|
+
p.log.message(` ${chalk.dim(skill.description)}`);
|
|
678
|
+
}
|
|
679
|
+
console.log();
|
|
680
|
+
p.outro("Use --skill <name> to install specific skills");
|
|
681
|
+
await cleanup(tempDir);
|
|
682
|
+
process.exit(0);
|
|
683
|
+
}
|
|
684
|
+
let selectedSkills;
|
|
685
|
+
if (options.skill && options.skill.length > 0) {
|
|
686
|
+
selectedSkills = skills.filter(
|
|
687
|
+
(s) => options.skill.some(
|
|
688
|
+
(name) => s.name.toLowerCase() === name.toLowerCase() || getSkillDisplayName(s).toLowerCase() === name.toLowerCase()
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
if (selectedSkills.length === 0) {
|
|
692
|
+
p.log.error(`No matching skills found for: ${options.skill.join(", ")}`);
|
|
693
|
+
p.log.info("Available skills:");
|
|
694
|
+
for (const s of skills) {
|
|
695
|
+
p.log.message(` - ${getSkillDisplayName(s)}`);
|
|
696
|
+
}
|
|
697
|
+
await cleanup(tempDir);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
p.log.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => chalk.cyan(getSkillDisplayName(s))).join(", ")}`);
|
|
701
|
+
} else if (skills.length === 1) {
|
|
702
|
+
selectedSkills = skills;
|
|
703
|
+
const firstSkill = skills[0];
|
|
704
|
+
p.log.info(`Skill: ${chalk.cyan(getSkillDisplayName(firstSkill))}`);
|
|
705
|
+
p.log.message(chalk.dim(firstSkill.description));
|
|
706
|
+
} else if (options.yes) {
|
|
707
|
+
selectedSkills = skills;
|
|
708
|
+
p.log.info(`Installing all ${skills.length} skills`);
|
|
709
|
+
} else {
|
|
710
|
+
const skillChoices = skills.map((s) => ({
|
|
711
|
+
value: s,
|
|
712
|
+
label: getSkillDisplayName(s),
|
|
713
|
+
hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
|
|
714
|
+
}));
|
|
715
|
+
const selected = await p.multiselect({
|
|
716
|
+
message: "Select skills to install",
|
|
717
|
+
options: skillChoices,
|
|
718
|
+
required: true
|
|
719
|
+
});
|
|
720
|
+
if (p.isCancel(selected)) {
|
|
721
|
+
p.cancel("Installation cancelled");
|
|
722
|
+
await cleanup(tempDir);
|
|
723
|
+
process.exit(0);
|
|
724
|
+
}
|
|
725
|
+
selectedSkills = selected;
|
|
726
|
+
}
|
|
727
|
+
let targetAgents;
|
|
728
|
+
if (options.agent && options.agent.length > 0) {
|
|
729
|
+
const validAgents = ["opencode", "claude-code", "codex", "cursor", "antigravity"];
|
|
730
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
731
|
+
if (invalidAgents.length > 0) {
|
|
732
|
+
p.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
733
|
+
p.log.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
734
|
+
await cleanup(tempDir);
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
targetAgents = options.agent;
|
|
738
|
+
} else {
|
|
739
|
+
spinner2.start("Detecting installed agents...");
|
|
740
|
+
const installedAgents = await detectInstalledAgents();
|
|
741
|
+
spinner2.stop(`Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? "s" : ""}`);
|
|
742
|
+
if (installedAgents.length === 0) {
|
|
743
|
+
if (options.yes) {
|
|
744
|
+
targetAgents = ["opencode", "claude-code", "codex", "cursor", "antigravity"];
|
|
745
|
+
p.log.info("Installing to all agents (none detected)");
|
|
746
|
+
} else {
|
|
747
|
+
p.log.warn("No coding agents detected. You can still install skills.");
|
|
748
|
+
const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
|
|
749
|
+
value: key,
|
|
750
|
+
label: config.displayName
|
|
751
|
+
}));
|
|
752
|
+
const selected = await p.multiselect({
|
|
753
|
+
message: "Select agents to install skills to",
|
|
754
|
+
options: allAgentChoices,
|
|
755
|
+
required: true
|
|
756
|
+
});
|
|
757
|
+
if (p.isCancel(selected)) {
|
|
758
|
+
p.cancel("Installation cancelled");
|
|
759
|
+
await cleanup(tempDir);
|
|
760
|
+
process.exit(0);
|
|
761
|
+
}
|
|
762
|
+
targetAgents = selected;
|
|
763
|
+
}
|
|
764
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
765
|
+
targetAgents = installedAgents;
|
|
766
|
+
if (installedAgents.length === 1) {
|
|
767
|
+
const firstAgent = installedAgents[0];
|
|
768
|
+
p.log.info(`Installing to: ${chalk.cyan(agents[firstAgent].displayName)}`);
|
|
769
|
+
} else {
|
|
770
|
+
p.log.info(`Installing to: ${installedAgents.map((a) => chalk.cyan(agents[a].displayName)).join(", ")}`);
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
const agentChoices = installedAgents.map((a) => ({
|
|
774
|
+
value: a,
|
|
775
|
+
label: agents[a].displayName,
|
|
776
|
+
hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`
|
|
777
|
+
}));
|
|
778
|
+
const selected = await p.multiselect({
|
|
779
|
+
message: "Select agents to install skills to",
|
|
780
|
+
options: agentChoices,
|
|
781
|
+
required: true,
|
|
782
|
+
initialValues: installedAgents
|
|
783
|
+
});
|
|
784
|
+
if (p.isCancel(selected)) {
|
|
785
|
+
p.cancel("Installation cancelled");
|
|
786
|
+
await cleanup(tempDir);
|
|
787
|
+
process.exit(0);
|
|
788
|
+
}
|
|
789
|
+
targetAgents = selected;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
let installGlobally = options.global ?? false;
|
|
793
|
+
if (options.global === void 0 && !options.yes) {
|
|
794
|
+
const scope = await p.select({
|
|
795
|
+
message: "Installation scope",
|
|
796
|
+
options: [
|
|
797
|
+
{ value: false, label: "Project", hint: "Install in current directory (committed with your project)" },
|
|
798
|
+
{ value: true, label: "Global", hint: "Install in home directory (available across all projects)" }
|
|
799
|
+
]
|
|
800
|
+
});
|
|
801
|
+
if (p.isCancel(scope)) {
|
|
802
|
+
p.cancel("Installation cancelled");
|
|
803
|
+
await cleanup(tempDir);
|
|
804
|
+
process.exit(0);
|
|
805
|
+
}
|
|
806
|
+
installGlobally = scope;
|
|
807
|
+
}
|
|
808
|
+
console.log();
|
|
809
|
+
p.log.step(chalk.bold("Installation Summary"));
|
|
810
|
+
for (const skill of selectedSkills) {
|
|
811
|
+
p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
|
|
812
|
+
for (const agent of targetAgents) {
|
|
813
|
+
const path = getInstallPath(skill.name, agent, { global: installGlobally });
|
|
814
|
+
const installed = await isSkillInstalled(skill.name, agent, { global: installGlobally });
|
|
815
|
+
const status = installed ? chalk.yellow(" (will overwrite)") : "";
|
|
816
|
+
p.log.message(` ${chalk.dim("\u2192")} ${agents[agent].displayName}: ${chalk.dim(path)}${status}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
console.log();
|
|
820
|
+
if (!options.yes) {
|
|
821
|
+
const confirmed = await p.confirm({ message: "Proceed with installation?" });
|
|
822
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
823
|
+
p.cancel("Installation cancelled");
|
|
824
|
+
await cleanup(tempDir);
|
|
825
|
+
process.exit(0);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
spinner2.start("Installing skills...");
|
|
829
|
+
const results = [];
|
|
830
|
+
for (const skill of selectedSkills) {
|
|
831
|
+
for (const agent of targetAgents) {
|
|
832
|
+
const result = await installSkillForAgent(skill, agent, { global: installGlobally });
|
|
833
|
+
results.push({
|
|
834
|
+
skill: getSkillDisplayName(skill),
|
|
835
|
+
agent: agents[agent].displayName,
|
|
836
|
+
...result
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
spinner2.stop("Installation complete");
|
|
841
|
+
console.log();
|
|
842
|
+
const successful = results.filter((r) => r.success);
|
|
843
|
+
const failed = results.filter((r) => !r.success);
|
|
844
|
+
track({
|
|
845
|
+
event: "install",
|
|
846
|
+
source,
|
|
847
|
+
skills: selectedSkills.map((s) => s.name).join(","),
|
|
848
|
+
agents: targetAgents.join(","),
|
|
849
|
+
...installGlobally && { global: "1" }
|
|
850
|
+
});
|
|
851
|
+
if (successful.length > 0) {
|
|
852
|
+
p.log.success(chalk.green(`Successfully installed ${successful.length} skill${successful.length !== 1 ? "s" : ""}`));
|
|
853
|
+
for (const r of successful) {
|
|
854
|
+
p.log.message(` ${chalk.green("\u2713")} ${r.skill} \u2192 ${r.agent}`);
|
|
855
|
+
p.log.message(` ${chalk.dim(r.path)}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (failed.length > 0) {
|
|
859
|
+
console.log();
|
|
860
|
+
p.log.error(chalk.red(`Failed to install ${failed.length} skill${failed.length !== 1 ? "s" : ""}`));
|
|
861
|
+
for (const r of failed) {
|
|
862
|
+
p.log.message(` ${chalk.red("\u2717")} ${r.skill} \u2192 ${r.agent}`);
|
|
863
|
+
p.log.message(` ${chalk.dim(r.error)}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
console.log();
|
|
867
|
+
p.outro(chalk.green("Done!"));
|
|
868
|
+
} catch (error) {
|
|
869
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error occurred");
|
|
870
|
+
p.outro(chalk.red("Installation failed"));
|
|
871
|
+
process.exit(1);
|
|
872
|
+
} finally {
|
|
873
|
+
await cleanup(tempDir);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function cleanup(tempDir) {
|
|
877
|
+
if (tempDir) {
|
|
878
|
+
try {
|
|
879
|
+
await cleanupTempDir(tempDir);
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async function installFromManifest(manifestPath, options) {
|
|
885
|
+
console.log();
|
|
886
|
+
p.intro(chalk.bgCyan.black(" add-skill ") + chalk.dim(" (manifest mode)"));
|
|
887
|
+
const tempDirs = [];
|
|
888
|
+
try {
|
|
889
|
+
const spinner2 = p.spinner();
|
|
890
|
+
if (options.skill && options.skill.length > 0) {
|
|
891
|
+
p.log.error("Cannot use --skill with --from-file. Skills are specified in the manifest file.");
|
|
892
|
+
process.exit(1);
|
|
893
|
+
}
|
|
894
|
+
if (options.list) {
|
|
895
|
+
p.log.error("Cannot use --list with --from-file. Use without --from-file to list skills.");
|
|
896
|
+
process.exit(1);
|
|
897
|
+
}
|
|
898
|
+
spinner2.start("Parsing manifest file...");
|
|
899
|
+
let manifest;
|
|
900
|
+
try {
|
|
901
|
+
manifest = await parseManifestFile(manifestPath);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
spinner2.stop(chalk.red("Failed to parse manifest"));
|
|
904
|
+
if (error instanceof ManifestParseError) {
|
|
905
|
+
p.log.error(error.message);
|
|
906
|
+
} else {
|
|
907
|
+
p.log.error(error.message);
|
|
908
|
+
}
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
spinner2.stop(`Found ${chalk.green(manifest.skills.length)} skill${manifest.skills.length !== 1 ? "s" : ""} in manifest`);
|
|
912
|
+
const skillsBySource = groupSkillsBySource(manifest.skills);
|
|
913
|
+
p.log.info(`From ${chalk.cyan(skillsBySource.size)} source${skillsBySource.size !== 1 ? "s" : ""}`);
|
|
914
|
+
const skillsToInstall = [];
|
|
915
|
+
const lockEntries = [];
|
|
916
|
+
for (const [sourceKey, entries] of skillsBySource) {
|
|
917
|
+
const firstEntry = entries[0];
|
|
918
|
+
const source = firstEntry.source;
|
|
919
|
+
const version2 = firstEntry.version;
|
|
920
|
+
spinner2.start(`Cloning ${chalk.cyan(source)}${version2 ? ` @ ${version2}` : ""}...`);
|
|
921
|
+
const parsed = parseSource(source);
|
|
922
|
+
const { tempDir, resolvedRef } = await cloneRepoAtVersion(parsed.url, version2);
|
|
923
|
+
tempDirs.push(tempDir);
|
|
924
|
+
spinner2.stop(`Cloned ${chalk.cyan(source)} (${chalk.dim(resolvedRef.slice(0, 7))})`);
|
|
925
|
+
spinner2.start("Discovering skills...");
|
|
926
|
+
const discoveredSkills = await discoverSkills(tempDir, parsed.subpath);
|
|
927
|
+
spinner2.stop(`Found ${discoveredSkills.length} skill${discoveredSkills.length !== 1 ? "s" : ""}`);
|
|
928
|
+
for (const entry of entries) {
|
|
929
|
+
const skill = discoveredSkills.find(
|
|
930
|
+
(s) => s.name.toLowerCase() === entry.name.toLowerCase()
|
|
931
|
+
);
|
|
932
|
+
if (!skill) {
|
|
933
|
+
throw new SkillNotFoundError(
|
|
934
|
+
entry.name,
|
|
935
|
+
entry.source,
|
|
936
|
+
discoveredSkills.map((s) => s.name)
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
if (entry.version) {
|
|
940
|
+
const validation = validateSkillVersion(skill, entry.version);
|
|
941
|
+
if (validation.message) {
|
|
942
|
+
p.log.warn(validation.message);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
skillsToInstall.push({ skill, entry, resolvedRef });
|
|
946
|
+
lockEntries.push({
|
|
947
|
+
source: entry.source,
|
|
948
|
+
name: entry.name,
|
|
949
|
+
version: entry.version || skill.version?.version || "latest",
|
|
950
|
+
resolvedRef,
|
|
951
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
let targetAgents;
|
|
956
|
+
if (options.agent && options.agent.length > 0) {
|
|
957
|
+
const validAgents = ["opencode", "claude-code", "codex", "cursor", "antigravity"];
|
|
958
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
959
|
+
if (invalidAgents.length > 0) {
|
|
960
|
+
p.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
961
|
+
p.log.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
962
|
+
await cleanupAll(tempDirs);
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
targetAgents = options.agent;
|
|
966
|
+
} else {
|
|
967
|
+
spinner2.start("Detecting installed agents...");
|
|
968
|
+
const installedAgents = await detectInstalledAgents();
|
|
969
|
+
spinner2.stop(`Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? "s" : ""}`);
|
|
970
|
+
if (installedAgents.length === 0) {
|
|
971
|
+
if (options.yes) {
|
|
972
|
+
targetAgents = ["opencode", "claude-code", "codex", "cursor", "antigravity"];
|
|
973
|
+
p.log.info("Installing to all agents (none detected)");
|
|
974
|
+
} else {
|
|
975
|
+
p.log.warn("No coding agents detected. You can still install skills.");
|
|
976
|
+
const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
|
|
977
|
+
value: key,
|
|
978
|
+
label: config.displayName
|
|
979
|
+
}));
|
|
980
|
+
const selected = await p.multiselect({
|
|
981
|
+
message: "Select agents to install skills to",
|
|
982
|
+
options: allAgentChoices,
|
|
983
|
+
required: true
|
|
984
|
+
});
|
|
985
|
+
if (p.isCancel(selected)) {
|
|
986
|
+
p.cancel("Installation cancelled");
|
|
987
|
+
await cleanupAll(tempDirs);
|
|
988
|
+
process.exit(0);
|
|
989
|
+
}
|
|
990
|
+
targetAgents = selected;
|
|
991
|
+
}
|
|
992
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
993
|
+
targetAgents = installedAgents;
|
|
994
|
+
p.log.info(`Installing to: ${targetAgents.map((a) => chalk.cyan(agents[a].displayName)).join(", ")}`);
|
|
995
|
+
} else {
|
|
996
|
+
const agentChoices = installedAgents.map((a) => ({
|
|
997
|
+
value: a,
|
|
998
|
+
label: agents[a].displayName,
|
|
999
|
+
hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`
|
|
1000
|
+
}));
|
|
1001
|
+
const selected = await p.multiselect({
|
|
1002
|
+
message: "Select agents to install skills to",
|
|
1003
|
+
options: agentChoices,
|
|
1004
|
+
required: true,
|
|
1005
|
+
initialValues: installedAgents
|
|
1006
|
+
});
|
|
1007
|
+
if (p.isCancel(selected)) {
|
|
1008
|
+
p.cancel("Installation cancelled");
|
|
1009
|
+
await cleanupAll(tempDirs);
|
|
1010
|
+
process.exit(0);
|
|
1011
|
+
}
|
|
1012
|
+
targetAgents = selected;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
let installGlobally = options.global ?? false;
|
|
1016
|
+
if (options.global === void 0 && !options.yes) {
|
|
1017
|
+
const scope = await p.select({
|
|
1018
|
+
message: "Installation scope",
|
|
1019
|
+
options: [
|
|
1020
|
+
{ value: false, label: "Project", hint: "Install in current directory" },
|
|
1021
|
+
{ value: true, label: "Global", hint: "Install in home directory" }
|
|
1022
|
+
]
|
|
1023
|
+
});
|
|
1024
|
+
if (p.isCancel(scope)) {
|
|
1025
|
+
p.cancel("Installation cancelled");
|
|
1026
|
+
await cleanupAll(tempDirs);
|
|
1027
|
+
process.exit(0);
|
|
1028
|
+
}
|
|
1029
|
+
installGlobally = scope;
|
|
1030
|
+
}
|
|
1031
|
+
console.log();
|
|
1032
|
+
p.log.step(chalk.bold("Installation Summary"));
|
|
1033
|
+
for (const { skill, entry } of skillsToInstall) {
|
|
1034
|
+
const versionStr = entry.version ? ` @ ${entry.version}` : "";
|
|
1035
|
+
p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}${chalk.dim(versionStr)}`);
|
|
1036
|
+
p.log.message(` ${chalk.dim("from")} ${entry.source}`);
|
|
1037
|
+
for (const agent of targetAgents) {
|
|
1038
|
+
const path = getInstallPath(skill.name, agent, { global: installGlobally });
|
|
1039
|
+
const installed = await isSkillInstalled(skill.name, agent, { global: installGlobally });
|
|
1040
|
+
const status = installed ? chalk.yellow(" (will overwrite)") : "";
|
|
1041
|
+
p.log.message(` ${chalk.dim("\u2192")} ${agents[agent].displayName}: ${chalk.dim(path)}${status}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
console.log();
|
|
1045
|
+
if (!options.yes) {
|
|
1046
|
+
const confirmed = await p.confirm({ message: "Proceed with installation?" });
|
|
1047
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
1048
|
+
p.cancel("Installation cancelled");
|
|
1049
|
+
await cleanupAll(tempDirs);
|
|
1050
|
+
process.exit(0);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
spinner2.start("Installing skills...");
|
|
1054
|
+
const results = [];
|
|
1055
|
+
for (const { skill } of skillsToInstall) {
|
|
1056
|
+
for (const agent of targetAgents) {
|
|
1057
|
+
const result = await installSkillForAgent(skill, agent, { global: installGlobally });
|
|
1058
|
+
results.push({
|
|
1059
|
+
skill: getSkillDisplayName(skill),
|
|
1060
|
+
agent: agents[agent].displayName,
|
|
1061
|
+
...result
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
spinner2.stop("Installation complete");
|
|
1066
|
+
if (options.lock !== false) {
|
|
1067
|
+
const lockPath = getLockFilePath(manifestPath);
|
|
1068
|
+
await writeLockFile(lockPath, lockEntries);
|
|
1069
|
+
p.log.info(`Lock file written to ${chalk.dim(lockPath)}`);
|
|
1070
|
+
}
|
|
1071
|
+
console.log();
|
|
1072
|
+
const successful = results.filter((r) => r.success);
|
|
1073
|
+
const failed = results.filter((r) => !r.success);
|
|
1074
|
+
track({
|
|
1075
|
+
event: "install",
|
|
1076
|
+
source: `manifest:${manifest.skills.length}`,
|
|
1077
|
+
skills: skillsToInstall.map((s) => s.skill.name).join(","),
|
|
1078
|
+
agents: targetAgents.join(","),
|
|
1079
|
+
...installGlobally && { global: "1" }
|
|
1080
|
+
});
|
|
1081
|
+
if (successful.length > 0) {
|
|
1082
|
+
p.log.success(chalk.green(`Successfully installed ${successful.length} skill${successful.length !== 1 ? "s" : ""}`));
|
|
1083
|
+
for (const r of successful) {
|
|
1084
|
+
p.log.message(` ${chalk.green("\u2713")} ${r.skill} \u2192 ${r.agent}`);
|
|
1085
|
+
p.log.message(` ${chalk.dim(r.path)}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (failed.length > 0) {
|
|
1089
|
+
console.log();
|
|
1090
|
+
p.log.error(chalk.red(`Failed to install ${failed.length} skill${failed.length !== 1 ? "s" : ""}`));
|
|
1091
|
+
for (const r of failed) {
|
|
1092
|
+
p.log.message(` ${chalk.red("\u2717")} ${r.skill} \u2192 ${r.agent}`);
|
|
1093
|
+
p.log.message(` ${chalk.dim(r.error)}`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
console.log();
|
|
1097
|
+
p.outro(chalk.green("Done!"));
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
if (error instanceof SkillNotFoundError) {
|
|
1100
|
+
p.log.error(error.message);
|
|
1101
|
+
if (error.availableSkills.length > 0) {
|
|
1102
|
+
p.log.info("Available skills:");
|
|
1103
|
+
for (const name of error.availableSkills) {
|
|
1104
|
+
p.log.message(` - ${name}`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
} else {
|
|
1108
|
+
p.log.error(error instanceof Error ? error.message : "Unknown error occurred");
|
|
1109
|
+
}
|
|
1110
|
+
p.outro(chalk.red("Installation failed"));
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
} finally {
|
|
1113
|
+
await cleanupAll(tempDirs);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function cleanupAll(tempDirs) {
|
|
1117
|
+
for (const dir of tempDirs) {
|
|
1118
|
+
await cleanup(dir);
|
|
1119
|
+
}
|
|
1120
|
+
}
|