add-skill-lazy 1.0.30
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 +269 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2557 -0
- package/package.json +90 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2557 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import * as p2 from "@clack/prompts";
|
|
6
|
+
import chalk2 from "chalk";
|
|
7
|
+
|
|
8
|
+
// src/source-parser.ts
|
|
9
|
+
import { isAbsolute, resolve } from "path";
|
|
10
|
+
function getOwnerRepo(parsed) {
|
|
11
|
+
if (parsed.type === "local") {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const match = parsed.url.match(/(?:github|gitlab)\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
15
|
+
if (match) {
|
|
16
|
+
return `${match[1]}/${match[2]}`;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function isLocalPath(input) {
|
|
21
|
+
return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || // Windows absolute paths like C:\ or D:\
|
|
22
|
+
/^[a-zA-Z]:[/\\]/.test(input);
|
|
23
|
+
}
|
|
24
|
+
function isDirectSkillUrl(input) {
|
|
25
|
+
if (!input.startsWith("http://") && !input.startsWith("https://")) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (!input.toLowerCase().endsWith("/skill.md")) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (input.includes("github.com/") && !input.includes("raw.githubusercontent.com")) {
|
|
32
|
+
if (!input.includes("/blob/") && !input.includes("/raw/")) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (input.includes("gitlab.com/") && !input.includes("/-/raw/")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function parseSource(input) {
|
|
42
|
+
if (isLocalPath(input)) {
|
|
43
|
+
const resolvedPath = resolve(input);
|
|
44
|
+
return {
|
|
45
|
+
type: "local",
|
|
46
|
+
url: resolvedPath,
|
|
47
|
+
// Store resolved path in url for consistency
|
|
48
|
+
localPath: resolvedPath
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (isDirectSkillUrl(input)) {
|
|
52
|
+
return {
|
|
53
|
+
type: "direct-url",
|
|
54
|
+
url: input
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
|
|
58
|
+
if (githubTreeWithPathMatch) {
|
|
59
|
+
const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
|
|
60
|
+
return {
|
|
61
|
+
type: "github",
|
|
62
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
63
|
+
ref,
|
|
64
|
+
subpath
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
|
|
68
|
+
if (githubTreeMatch) {
|
|
69
|
+
const [, owner, repo, ref] = githubTreeMatch;
|
|
70
|
+
return {
|
|
71
|
+
type: "github",
|
|
72
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
73
|
+
ref
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
77
|
+
if (githubRepoMatch) {
|
|
78
|
+
const [, owner, repo] = githubRepoMatch;
|
|
79
|
+
const cleanRepo = repo.replace(/\.git$/, "");
|
|
80
|
+
return {
|
|
81
|
+
type: "github",
|
|
82
|
+
url: `https://github.com/${owner}/${cleanRepo}.git`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const gitlabTreeWithPathMatch = input.match(
|
|
86
|
+
/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)\/(.+)/
|
|
87
|
+
);
|
|
88
|
+
if (gitlabTreeWithPathMatch) {
|
|
89
|
+
const [, owner, repo, ref, subpath] = gitlabTreeWithPathMatch;
|
|
90
|
+
return {
|
|
91
|
+
type: "gitlab",
|
|
92
|
+
url: `https://gitlab.com/${owner}/${repo}.git`,
|
|
93
|
+
ref,
|
|
94
|
+
subpath
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const gitlabTreeMatch = input.match(/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)$/);
|
|
98
|
+
if (gitlabTreeMatch) {
|
|
99
|
+
const [, owner, repo, ref] = gitlabTreeMatch;
|
|
100
|
+
return {
|
|
101
|
+
type: "gitlab",
|
|
102
|
+
url: `https://gitlab.com/${owner}/${repo}.git`,
|
|
103
|
+
ref
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const gitlabRepoMatch = input.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
|
|
107
|
+
if (gitlabRepoMatch) {
|
|
108
|
+
const [, owner, repo] = gitlabRepoMatch;
|
|
109
|
+
const cleanRepo = repo.replace(/\.git$/, "");
|
|
110
|
+
return {
|
|
111
|
+
type: "gitlab",
|
|
112
|
+
url: `https://gitlab.com/${owner}/${cleanRepo}.git`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
|
|
116
|
+
if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
|
|
117
|
+
const [, owner, repo, subpath] = shorthandMatch;
|
|
118
|
+
return {
|
|
119
|
+
type: "github",
|
|
120
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
121
|
+
subpath
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
type: "git",
|
|
126
|
+
url: input
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/git.ts
|
|
131
|
+
import simpleGit from "simple-git";
|
|
132
|
+
import { join, normalize, resolve as resolve2, sep } from "path";
|
|
133
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
134
|
+
import { tmpdir } from "os";
|
|
135
|
+
async function cloneRepo(url, ref) {
|
|
136
|
+
const tempDir = await mkdtemp(join(tmpdir(), "add-skill-"));
|
|
137
|
+
const git = simpleGit();
|
|
138
|
+
const cloneOptions = ref ? ["--depth", "1", "--branch", ref] : ["--depth", "1"];
|
|
139
|
+
await git.clone(url, tempDir, cloneOptions);
|
|
140
|
+
return tempDir;
|
|
141
|
+
}
|
|
142
|
+
async function cleanupTempDir(dir) {
|
|
143
|
+
const normalizedDir = normalize(resolve2(dir));
|
|
144
|
+
const normalizedTmpDir = normalize(resolve2(tmpdir()));
|
|
145
|
+
if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) {
|
|
146
|
+
throw new Error("Attempted to clean up directory outside of temp directory");
|
|
147
|
+
}
|
|
148
|
+
await rm(dir, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/skills.ts
|
|
152
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
153
|
+
import { join as join2, basename, dirname } from "path";
|
|
154
|
+
import matter from "gray-matter";
|
|
155
|
+
var SKIP_DIRS = ["node_modules", ".git", "dist", "build", "__pycache__"];
|
|
156
|
+
async function hasSkillMd(dir) {
|
|
157
|
+
try {
|
|
158
|
+
const skillPath = join2(dir, "SKILL.md");
|
|
159
|
+
const stats = await stat(skillPath);
|
|
160
|
+
return stats.isFile();
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function parseSkillMd(skillMdPath) {
|
|
166
|
+
try {
|
|
167
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
168
|
+
const { data } = matter(content);
|
|
169
|
+
if (!data.name || !data.description) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
name: data.name,
|
|
174
|
+
description: data.description,
|
|
175
|
+
path: dirname(skillMdPath),
|
|
176
|
+
rawContent: content,
|
|
177
|
+
metadata: data.metadata
|
|
178
|
+
};
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
184
|
+
const skillDirs = [];
|
|
185
|
+
if (depth > maxDepth) return skillDirs;
|
|
186
|
+
try {
|
|
187
|
+
if (await hasSkillMd(dir)) {
|
|
188
|
+
skillDirs.push(dir);
|
|
189
|
+
}
|
|
190
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (entry.isDirectory() && !SKIP_DIRS.includes(entry.name)) {
|
|
193
|
+
const subDirs = await findSkillDirs(join2(dir, entry.name), depth + 1, maxDepth);
|
|
194
|
+
skillDirs.push(...subDirs);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
return skillDirs;
|
|
200
|
+
}
|
|
201
|
+
async function discoverSkills(basePath, subpath) {
|
|
202
|
+
const skills = [];
|
|
203
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
204
|
+
const searchPath = subpath ? join2(basePath, subpath) : basePath;
|
|
205
|
+
if (await hasSkillMd(searchPath)) {
|
|
206
|
+
const skill = await parseSkillMd(join2(searchPath, "SKILL.md"));
|
|
207
|
+
if (skill) {
|
|
208
|
+
skills.push(skill);
|
|
209
|
+
return skills;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const prioritySearchDirs = [
|
|
213
|
+
searchPath,
|
|
214
|
+
join2(searchPath, "skills"),
|
|
215
|
+
join2(searchPath, "skills/.curated"),
|
|
216
|
+
join2(searchPath, "skills/.experimental"),
|
|
217
|
+
join2(searchPath, "skills/.system"),
|
|
218
|
+
join2(searchPath, ".agent/skills"),
|
|
219
|
+
join2(searchPath, ".agents/skills"),
|
|
220
|
+
join2(searchPath, ".claude/skills"),
|
|
221
|
+
join2(searchPath, ".cline/skills"),
|
|
222
|
+
join2(searchPath, ".codex/skills"),
|
|
223
|
+
join2(searchPath, ".commandcode/skills"),
|
|
224
|
+
join2(searchPath, ".cursor/skills"),
|
|
225
|
+
join2(searchPath, ".github/skills"),
|
|
226
|
+
join2(searchPath, ".goose/skills"),
|
|
227
|
+
join2(searchPath, ".kilocode/skills"),
|
|
228
|
+
join2(searchPath, ".kiro/skills"),
|
|
229
|
+
join2(searchPath, ".neovate/skills"),
|
|
230
|
+
join2(searchPath, ".opencode/skills"),
|
|
231
|
+
join2(searchPath, ".openhands/skills"),
|
|
232
|
+
join2(searchPath, ".pi/skills"),
|
|
233
|
+
join2(searchPath, ".qoder/skills"),
|
|
234
|
+
join2(searchPath, ".roo/skills"),
|
|
235
|
+
join2(searchPath, ".trae/skills"),
|
|
236
|
+
join2(searchPath, ".windsurf/skills"),
|
|
237
|
+
join2(searchPath, ".zencoder/skills")
|
|
238
|
+
];
|
|
239
|
+
for (const dir of prioritySearchDirs) {
|
|
240
|
+
try {
|
|
241
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
if (entry.isDirectory()) {
|
|
244
|
+
const skillDir = join2(dir, entry.name);
|
|
245
|
+
if (await hasSkillMd(skillDir)) {
|
|
246
|
+
const skill = await parseSkillMd(join2(skillDir, "SKILL.md"));
|
|
247
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
248
|
+
skills.push(skill);
|
|
249
|
+
seenNames.add(skill.name);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (skills.length === 0) {
|
|
258
|
+
const allSkillDirs = await findSkillDirs(searchPath);
|
|
259
|
+
for (const skillDir of allSkillDirs) {
|
|
260
|
+
const skill = await parseSkillMd(join2(skillDir, "SKILL.md"));
|
|
261
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
262
|
+
skills.push(skill);
|
|
263
|
+
seenNames.add(skill.name);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return skills;
|
|
268
|
+
}
|
|
269
|
+
function getSkillDisplayName(skill) {
|
|
270
|
+
return skill.name || basename(skill.path);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/installer.ts
|
|
274
|
+
import { mkdir, cp, access, readdir as readdir2, symlink, lstat, rm as rm2, readlink, writeFile } from "fs/promises";
|
|
275
|
+
import { join as join4, basename as basename2, normalize as normalize2, resolve as resolve3, sep as sep2, relative } from "path";
|
|
276
|
+
import { homedir as homedir2, platform } from "os";
|
|
277
|
+
|
|
278
|
+
// src/agents.ts
|
|
279
|
+
import { homedir } from "os";
|
|
280
|
+
import { join as join3 } from "path";
|
|
281
|
+
import { existsSync } from "fs";
|
|
282
|
+
var home = homedir();
|
|
283
|
+
var agents = {
|
|
284
|
+
amp: {
|
|
285
|
+
name: "amp",
|
|
286
|
+
displayName: "Amp",
|
|
287
|
+
skillsDir: ".agents/skills",
|
|
288
|
+
globalSkillsDir: join3(home, ".config/agents/skills"),
|
|
289
|
+
detectInstalled: async () => {
|
|
290
|
+
return existsSync(join3(home, ".config/amp"));
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
antigravity: {
|
|
294
|
+
name: "antigravity",
|
|
295
|
+
displayName: "Antigravity",
|
|
296
|
+
skillsDir: ".agent/skills",
|
|
297
|
+
globalSkillsDir: join3(home, ".gemini/antigravity/skills"),
|
|
298
|
+
detectInstalled: async () => {
|
|
299
|
+
return existsSync(join3(process.cwd(), ".agent")) || existsSync(join3(home, ".gemini/antigravity"));
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
"claude-code": {
|
|
303
|
+
name: "claude-code",
|
|
304
|
+
displayName: "Claude Code",
|
|
305
|
+
skillsDir: ".claude/skills",
|
|
306
|
+
globalSkillsDir: join3(home, ".claude/skills"),
|
|
307
|
+
detectInstalled: async () => {
|
|
308
|
+
return existsSync(join3(home, ".claude"));
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
clawdbot: {
|
|
312
|
+
name: "clawdbot",
|
|
313
|
+
displayName: "Clawdbot",
|
|
314
|
+
skillsDir: "skills",
|
|
315
|
+
globalSkillsDir: join3(home, ".clawdbot/skills"),
|
|
316
|
+
detectInstalled: async () => {
|
|
317
|
+
return existsSync(join3(home, ".clawdbot"));
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
cline: {
|
|
321
|
+
name: "cline",
|
|
322
|
+
displayName: "Cline",
|
|
323
|
+
skillsDir: ".cline/skills",
|
|
324
|
+
globalSkillsDir: join3(home, ".cline/skills"),
|
|
325
|
+
detectInstalled: async () => {
|
|
326
|
+
return existsSync(join3(home, ".cline"));
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
codex: {
|
|
330
|
+
name: "codex",
|
|
331
|
+
displayName: "Codex",
|
|
332
|
+
skillsDir: ".codex/skills",
|
|
333
|
+
globalSkillsDir: join3(home, ".codex/skills"),
|
|
334
|
+
detectInstalled: async () => {
|
|
335
|
+
return existsSync(join3(home, ".codex"));
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
"command-code": {
|
|
339
|
+
name: "command-code",
|
|
340
|
+
displayName: "Command Code",
|
|
341
|
+
skillsDir: ".commandcode/skills",
|
|
342
|
+
globalSkillsDir: join3(home, ".commandcode/skills"),
|
|
343
|
+
detectInstalled: async () => {
|
|
344
|
+
return existsSync(join3(home, ".commandcode"));
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
cursor: {
|
|
348
|
+
name: "cursor",
|
|
349
|
+
displayName: "Cursor",
|
|
350
|
+
skillsDir: ".cursor/skills",
|
|
351
|
+
globalSkillsDir: join3(home, ".cursor/skills"),
|
|
352
|
+
detectInstalled: async () => {
|
|
353
|
+
return existsSync(join3(home, ".cursor"));
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
droid: {
|
|
357
|
+
name: "droid",
|
|
358
|
+
displayName: "Droid",
|
|
359
|
+
skillsDir: ".factory/skills",
|
|
360
|
+
globalSkillsDir: join3(home, ".factory/skills"),
|
|
361
|
+
detectInstalled: async () => {
|
|
362
|
+
return existsSync(join3(home, ".factory/skills"));
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
"gemini-cli": {
|
|
366
|
+
name: "gemini-cli",
|
|
367
|
+
displayName: "Gemini CLI",
|
|
368
|
+
skillsDir: ".gemini/skills",
|
|
369
|
+
globalSkillsDir: join3(home, ".gemini/skills"),
|
|
370
|
+
detectInstalled: async () => {
|
|
371
|
+
return existsSync(join3(home, ".gemini"));
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
"github-copilot": {
|
|
375
|
+
name: "github-copilot",
|
|
376
|
+
displayName: "GitHub Copilot",
|
|
377
|
+
skillsDir: ".github/skills",
|
|
378
|
+
globalSkillsDir: join3(home, ".copilot/skills"),
|
|
379
|
+
detectInstalled: async () => {
|
|
380
|
+
return existsSync(join3(process.cwd(), ".github")) || existsSync(join3(home, ".copilot"));
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
goose: {
|
|
384
|
+
name: "goose",
|
|
385
|
+
displayName: "Goose",
|
|
386
|
+
skillsDir: ".goose/skills",
|
|
387
|
+
globalSkillsDir: join3(home, ".config/goose/skills"),
|
|
388
|
+
detectInstalled: async () => {
|
|
389
|
+
return existsSync(join3(home, ".config/goose"));
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
kilo: {
|
|
393
|
+
name: "kilo",
|
|
394
|
+
displayName: "Kilo Code",
|
|
395
|
+
skillsDir: ".kilocode/skills",
|
|
396
|
+
globalSkillsDir: join3(home, ".kilocode/skills"),
|
|
397
|
+
detectInstalled: async () => {
|
|
398
|
+
return existsSync(join3(home, ".kilocode"));
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
"kiro-cli": {
|
|
402
|
+
name: "kiro-cli",
|
|
403
|
+
displayName: "Kiro CLI",
|
|
404
|
+
skillsDir: ".kiro/skills",
|
|
405
|
+
globalSkillsDir: join3(home, ".kiro/skills"),
|
|
406
|
+
detectInstalled: async () => {
|
|
407
|
+
return existsSync(join3(home, ".kiro"));
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
mcpjam: {
|
|
411
|
+
name: "mcpjam",
|
|
412
|
+
displayName: "MCPJam",
|
|
413
|
+
skillsDir: ".mcpjam/skills",
|
|
414
|
+
globalSkillsDir: join3(home, ".mcpjam/skills"),
|
|
415
|
+
detectInstalled: async () => {
|
|
416
|
+
return existsSync(join3(home, ".mcpjam"));
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
opencode: {
|
|
420
|
+
name: "opencode",
|
|
421
|
+
displayName: "OpenCode",
|
|
422
|
+
skillsDir: ".opencode/skills",
|
|
423
|
+
globalSkillsDir: join3(home, ".config/opencode/skills"),
|
|
424
|
+
detectInstalled: async () => {
|
|
425
|
+
return existsSync(join3(home, ".config/opencode")) || existsSync(join3(home, ".claude/skills"));
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
openhands: {
|
|
429
|
+
name: "openhands",
|
|
430
|
+
displayName: "OpenHands",
|
|
431
|
+
skillsDir: ".openhands/skills",
|
|
432
|
+
globalSkillsDir: join3(home, ".openhands/skills"),
|
|
433
|
+
detectInstalled: async () => {
|
|
434
|
+
return existsSync(join3(home, ".openhands"));
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
pi: {
|
|
438
|
+
name: "pi",
|
|
439
|
+
displayName: "Pi",
|
|
440
|
+
skillsDir: ".pi/skills",
|
|
441
|
+
globalSkillsDir: join3(home, ".pi/agent/skills"),
|
|
442
|
+
detectInstalled: async () => {
|
|
443
|
+
return existsSync(join3(home, ".pi/agent"));
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
qoder: {
|
|
447
|
+
name: "qoder",
|
|
448
|
+
displayName: "Qoder",
|
|
449
|
+
skillsDir: ".qoder/skills",
|
|
450
|
+
globalSkillsDir: join3(home, ".qoder/skills"),
|
|
451
|
+
detectInstalled: async () => {
|
|
452
|
+
return existsSync(join3(home, ".qoder"));
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
"qwen-code": {
|
|
456
|
+
name: "qwen-code",
|
|
457
|
+
displayName: "Qwen Code",
|
|
458
|
+
skillsDir: ".qwen/skills",
|
|
459
|
+
globalSkillsDir: join3(home, ".qwen/skills"),
|
|
460
|
+
detectInstalled: async () => {
|
|
461
|
+
return existsSync(join3(home, ".qwen"));
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
roo: {
|
|
465
|
+
name: "roo",
|
|
466
|
+
displayName: "Roo Code",
|
|
467
|
+
skillsDir: ".roo/skills",
|
|
468
|
+
globalSkillsDir: join3(home, ".roo/skills"),
|
|
469
|
+
detectInstalled: async () => {
|
|
470
|
+
return existsSync(join3(home, ".roo"));
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
trae: {
|
|
474
|
+
name: "trae",
|
|
475
|
+
displayName: "Trae",
|
|
476
|
+
skillsDir: ".trae/skills",
|
|
477
|
+
globalSkillsDir: join3(home, ".trae/skills"),
|
|
478
|
+
detectInstalled: async () => {
|
|
479
|
+
return existsSync(join3(home, ".trae"));
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
windsurf: {
|
|
483
|
+
name: "windsurf",
|
|
484
|
+
displayName: "Windsurf",
|
|
485
|
+
skillsDir: ".windsurf/skills",
|
|
486
|
+
globalSkillsDir: join3(home, ".codeium/windsurf/skills"),
|
|
487
|
+
detectInstalled: async () => {
|
|
488
|
+
return existsSync(join3(home, ".codeium/windsurf"));
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
zencoder: {
|
|
492
|
+
name: "zencoder",
|
|
493
|
+
displayName: "Zencoder",
|
|
494
|
+
skillsDir: ".zencoder/skills",
|
|
495
|
+
globalSkillsDir: join3(home, ".zencoder/skills"),
|
|
496
|
+
detectInstalled: async () => {
|
|
497
|
+
return existsSync(join3(home, ".zencoder"));
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
neovate: {
|
|
501
|
+
name: "neovate",
|
|
502
|
+
displayName: "Neovate",
|
|
503
|
+
skillsDir: ".neovate/skills",
|
|
504
|
+
globalSkillsDir: join3(home, ".neovate/skills"),
|
|
505
|
+
detectInstalled: async () => {
|
|
506
|
+
return existsSync(join3(home, ".neovate"));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
async function detectInstalledAgents() {
|
|
511
|
+
const installed = [];
|
|
512
|
+
for (const [type, config] of Object.entries(agents)) {
|
|
513
|
+
if (await config.detectInstalled()) {
|
|
514
|
+
installed.push(type);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return installed;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/installer.ts
|
|
521
|
+
var AGENTS_DIR = ".agents";
|
|
522
|
+
var SKILLS_SUBDIR = "skills";
|
|
523
|
+
function sanitizeName(name) {
|
|
524
|
+
let sanitized = name.replace(/[\/\\:\0]/g, "");
|
|
525
|
+
sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, "");
|
|
526
|
+
sanitized = sanitized.replace(/^\.+/, "");
|
|
527
|
+
if (!sanitized || sanitized.length === 0) {
|
|
528
|
+
sanitized = "unnamed-skill";
|
|
529
|
+
}
|
|
530
|
+
if (sanitized.length > 255) {
|
|
531
|
+
sanitized = sanitized.substring(0, 255);
|
|
532
|
+
}
|
|
533
|
+
return sanitized;
|
|
534
|
+
}
|
|
535
|
+
function isPathSafe(basePath, targetPath) {
|
|
536
|
+
const normalizedBase = normalize2(resolve3(basePath));
|
|
537
|
+
const normalizedTarget = normalize2(resolve3(targetPath));
|
|
538
|
+
return normalizedTarget.startsWith(normalizedBase + sep2) || normalizedTarget === normalizedBase;
|
|
539
|
+
}
|
|
540
|
+
function getCanonicalSkillsDir(global, cwd) {
|
|
541
|
+
const baseDir = global ? homedir2() : cwd || process.cwd();
|
|
542
|
+
return join4(baseDir, AGENTS_DIR, SKILLS_SUBDIR);
|
|
543
|
+
}
|
|
544
|
+
async function createSymlink(target, linkPath) {
|
|
545
|
+
try {
|
|
546
|
+
try {
|
|
547
|
+
const stats = await lstat(linkPath);
|
|
548
|
+
if (stats.isSymbolicLink()) {
|
|
549
|
+
const existingTarget = await readlink(linkPath);
|
|
550
|
+
if (resolve3(existingTarget) === resolve3(target)) {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
await rm2(linkPath);
|
|
554
|
+
} else {
|
|
555
|
+
await rm2(linkPath, { recursive: true });
|
|
556
|
+
}
|
|
557
|
+
} catch (err) {
|
|
558
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") {
|
|
559
|
+
try {
|
|
560
|
+
await rm2(linkPath, { force: true });
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const linkDir = join4(linkPath, "..");
|
|
566
|
+
await mkdir(linkDir, { recursive: true });
|
|
567
|
+
const relativePath = relative(linkDir, target);
|
|
568
|
+
const symlinkType = platform() === "win32" ? "junction" : void 0;
|
|
569
|
+
await symlink(relativePath, linkPath, symlinkType);
|
|
570
|
+
return true;
|
|
571
|
+
} catch {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async function installSkillForAgent(skill, agentType, options = {}) {
|
|
576
|
+
const agent = agents[agentType];
|
|
577
|
+
const isGlobal = options.global ?? false;
|
|
578
|
+
const cwd = options.cwd || process.cwd();
|
|
579
|
+
const rawSkillName = skill.name || basename2(skill.path);
|
|
580
|
+
const skillName = sanitizeName(rawSkillName);
|
|
581
|
+
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
582
|
+
const canonicalDir = join4(canonicalBase, skillName);
|
|
583
|
+
const agentBase = isGlobal ? agent.globalSkillsDir : join4(cwd, agent.skillsDir);
|
|
584
|
+
const agentDir = join4(agentBase, skillName);
|
|
585
|
+
const installMode = options.mode ?? "symlink";
|
|
586
|
+
if (!isPathSafe(canonicalBase, canonicalDir)) {
|
|
587
|
+
return {
|
|
588
|
+
success: false,
|
|
589
|
+
path: agentDir,
|
|
590
|
+
mode: installMode,
|
|
591
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
if (!isPathSafe(agentBase, agentDir)) {
|
|
595
|
+
return {
|
|
596
|
+
success: false,
|
|
597
|
+
path: agentDir,
|
|
598
|
+
mode: installMode,
|
|
599
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
if (installMode === "copy") {
|
|
604
|
+
await mkdir(agentDir, { recursive: true });
|
|
605
|
+
await copyDirectory(skill.path, agentDir);
|
|
606
|
+
return {
|
|
607
|
+
success: true,
|
|
608
|
+
path: agentDir,
|
|
609
|
+
mode: "copy"
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
await mkdir(canonicalDir, { recursive: true });
|
|
613
|
+
await copyDirectory(skill.path, canonicalDir);
|
|
614
|
+
const symlinkCreated = await createSymlink(canonicalDir, agentDir);
|
|
615
|
+
if (!symlinkCreated) {
|
|
616
|
+
try {
|
|
617
|
+
await rm2(agentDir, { recursive: true, force: true });
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
await mkdir(agentDir, { recursive: true });
|
|
621
|
+
await copyDirectory(skill.path, agentDir);
|
|
622
|
+
return {
|
|
623
|
+
success: true,
|
|
624
|
+
path: agentDir,
|
|
625
|
+
canonicalPath: canonicalDir,
|
|
626
|
+
mode: "symlink",
|
|
627
|
+
symlinkFailed: true
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return {
|
|
631
|
+
success: true,
|
|
632
|
+
path: agentDir,
|
|
633
|
+
canonicalPath: canonicalDir,
|
|
634
|
+
mode: "symlink"
|
|
635
|
+
};
|
|
636
|
+
} catch (error) {
|
|
637
|
+
return {
|
|
638
|
+
success: false,
|
|
639
|
+
path: agentDir,
|
|
640
|
+
mode: installMode,
|
|
641
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
var EXCLUDE_FILES = /* @__PURE__ */ new Set(["README.md", "metadata.json"]);
|
|
646
|
+
var isExcluded = (name) => {
|
|
647
|
+
if (EXCLUDE_FILES.has(name)) return true;
|
|
648
|
+
if (name.startsWith("_")) return true;
|
|
649
|
+
return false;
|
|
650
|
+
};
|
|
651
|
+
async function copyDirectory(src, dest) {
|
|
652
|
+
await mkdir(dest, { recursive: true });
|
|
653
|
+
const entries = await readdir2(src, { withFileTypes: true });
|
|
654
|
+
for (const entry of entries) {
|
|
655
|
+
if (isExcluded(entry.name)) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const srcPath = join4(src, entry.name);
|
|
659
|
+
const destPath = join4(dest, entry.name);
|
|
660
|
+
if (entry.isDirectory()) {
|
|
661
|
+
await copyDirectory(srcPath, destPath);
|
|
662
|
+
} else {
|
|
663
|
+
await cp(srcPath, destPath);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
668
|
+
const agent = agents[agentType];
|
|
669
|
+
const sanitized = sanitizeName(skillName);
|
|
670
|
+
const targetBase = options.global ? agent.globalSkillsDir : join4(options.cwd || process.cwd(), agent.skillsDir);
|
|
671
|
+
const skillDir = join4(targetBase, sanitized);
|
|
672
|
+
if (!isPathSafe(targetBase, skillDir)) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
await access(skillDir);
|
|
677
|
+
return true;
|
|
678
|
+
} catch {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function getCanonicalPath(skillName, options = {}) {
|
|
683
|
+
const sanitized = sanitizeName(skillName);
|
|
684
|
+
const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
|
|
685
|
+
const canonicalPath = join4(canonicalBase, sanitized);
|
|
686
|
+
if (!isPathSafe(canonicalBase, canonicalPath)) {
|
|
687
|
+
throw new Error("Invalid skill name: potential path traversal detected");
|
|
688
|
+
}
|
|
689
|
+
return canonicalPath;
|
|
690
|
+
}
|
|
691
|
+
async function installMintlifySkillForAgent(skill, agentType, options = {}) {
|
|
692
|
+
const agent = agents[agentType];
|
|
693
|
+
const isGlobal = options.global ?? false;
|
|
694
|
+
const cwd = options.cwd || process.cwd();
|
|
695
|
+
const installMode = options.mode ?? "symlink";
|
|
696
|
+
const skillName = sanitizeName(skill.mintlifySite);
|
|
697
|
+
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
698
|
+
const canonicalDir = join4(canonicalBase, skillName);
|
|
699
|
+
const agentBase = isGlobal ? agent.globalSkillsDir : join4(cwd, agent.skillsDir);
|
|
700
|
+
const agentDir = join4(agentBase, skillName);
|
|
701
|
+
if (!isPathSafe(canonicalBase, canonicalDir)) {
|
|
702
|
+
return {
|
|
703
|
+
success: false,
|
|
704
|
+
path: agentDir,
|
|
705
|
+
mode: installMode,
|
|
706
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (!isPathSafe(agentBase, agentDir)) {
|
|
710
|
+
return {
|
|
711
|
+
success: false,
|
|
712
|
+
path: agentDir,
|
|
713
|
+
mode: installMode,
|
|
714
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
if (installMode === "copy") {
|
|
719
|
+
await mkdir(agentDir, { recursive: true });
|
|
720
|
+
const skillMdPath2 = join4(agentDir, "SKILL.md");
|
|
721
|
+
await writeFile(skillMdPath2, skill.content, "utf-8");
|
|
722
|
+
return {
|
|
723
|
+
success: true,
|
|
724
|
+
path: agentDir,
|
|
725
|
+
mode: "copy"
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
await mkdir(canonicalDir, { recursive: true });
|
|
729
|
+
const skillMdPath = join4(canonicalDir, "SKILL.md");
|
|
730
|
+
await writeFile(skillMdPath, skill.content, "utf-8");
|
|
731
|
+
const symlinkCreated = await createSymlink(canonicalDir, agentDir);
|
|
732
|
+
if (!symlinkCreated) {
|
|
733
|
+
try {
|
|
734
|
+
await rm2(agentDir, { recursive: true, force: true });
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
await mkdir(agentDir, { recursive: true });
|
|
738
|
+
const agentSkillMdPath = join4(agentDir, "SKILL.md");
|
|
739
|
+
await writeFile(agentSkillMdPath, skill.content, "utf-8");
|
|
740
|
+
return {
|
|
741
|
+
success: true,
|
|
742
|
+
path: agentDir,
|
|
743
|
+
canonicalPath: canonicalDir,
|
|
744
|
+
mode: "symlink",
|
|
745
|
+
symlinkFailed: true
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
success: true,
|
|
750
|
+
path: agentDir,
|
|
751
|
+
canonicalPath: canonicalDir,
|
|
752
|
+
mode: "symlink"
|
|
753
|
+
};
|
|
754
|
+
} catch (error) {
|
|
755
|
+
return {
|
|
756
|
+
success: false,
|
|
757
|
+
path: agentDir,
|
|
758
|
+
mode: installMode,
|
|
759
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function installRemoteSkillForAgent(skill, agentType, options = {}) {
|
|
764
|
+
const agent = agents[agentType];
|
|
765
|
+
const isGlobal = options.global ?? false;
|
|
766
|
+
const cwd = options.cwd || process.cwd();
|
|
767
|
+
const installMode = options.mode ?? "symlink";
|
|
768
|
+
const skillName = sanitizeName(skill.installName);
|
|
769
|
+
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
770
|
+
const canonicalDir = join4(canonicalBase, skillName);
|
|
771
|
+
const agentBase = isGlobal ? agent.globalSkillsDir : join4(cwd, agent.skillsDir);
|
|
772
|
+
const agentDir = join4(agentBase, skillName);
|
|
773
|
+
if (!isPathSafe(canonicalBase, canonicalDir)) {
|
|
774
|
+
return {
|
|
775
|
+
success: false,
|
|
776
|
+
path: agentDir,
|
|
777
|
+
mode: installMode,
|
|
778
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
if (!isPathSafe(agentBase, agentDir)) {
|
|
782
|
+
return {
|
|
783
|
+
success: false,
|
|
784
|
+
path: agentDir,
|
|
785
|
+
mode: installMode,
|
|
786
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
if (installMode === "copy") {
|
|
791
|
+
await mkdir(agentDir, { recursive: true });
|
|
792
|
+
const skillMdPath2 = join4(agentDir, "SKILL.md");
|
|
793
|
+
await writeFile(skillMdPath2, skill.content, "utf-8");
|
|
794
|
+
return {
|
|
795
|
+
success: true,
|
|
796
|
+
path: agentDir,
|
|
797
|
+
mode: "copy"
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
await mkdir(canonicalDir, { recursive: true });
|
|
801
|
+
const skillMdPath = join4(canonicalDir, "SKILL.md");
|
|
802
|
+
await writeFile(skillMdPath, skill.content, "utf-8");
|
|
803
|
+
const symlinkCreated = await createSymlink(canonicalDir, agentDir);
|
|
804
|
+
if (!symlinkCreated) {
|
|
805
|
+
try {
|
|
806
|
+
await rm2(agentDir, { recursive: true, force: true });
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
await mkdir(agentDir, { recursive: true });
|
|
810
|
+
const agentSkillMdPath = join4(agentDir, "SKILL.md");
|
|
811
|
+
await writeFile(agentSkillMdPath, skill.content, "utf-8");
|
|
812
|
+
return {
|
|
813
|
+
success: true,
|
|
814
|
+
path: agentDir,
|
|
815
|
+
canonicalPath: canonicalDir,
|
|
816
|
+
mode: "symlink",
|
|
817
|
+
symlinkFailed: true
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
success: true,
|
|
822
|
+
path: agentDir,
|
|
823
|
+
canonicalPath: canonicalDir,
|
|
824
|
+
mode: "symlink"
|
|
825
|
+
};
|
|
826
|
+
} catch (error) {
|
|
827
|
+
return {
|
|
828
|
+
success: false,
|
|
829
|
+
path: agentDir,
|
|
830
|
+
mode: installMode,
|
|
831
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/index.ts
|
|
837
|
+
import { homedir as homedir4 } from "os";
|
|
838
|
+
|
|
839
|
+
// src/telemetry.ts
|
|
840
|
+
var TELEMETRY_URL = "https://add-skill.vercel.sh/t";
|
|
841
|
+
var cliVersion = null;
|
|
842
|
+
function isCI() {
|
|
843
|
+
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);
|
|
844
|
+
}
|
|
845
|
+
function isEnabled() {
|
|
846
|
+
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
|
|
847
|
+
}
|
|
848
|
+
function setVersion(version2) {
|
|
849
|
+
cliVersion = version2;
|
|
850
|
+
}
|
|
851
|
+
function track(data) {
|
|
852
|
+
if (!isEnabled()) return;
|
|
853
|
+
try {
|
|
854
|
+
const params = new URLSearchParams();
|
|
855
|
+
if (cliVersion) {
|
|
856
|
+
params.set("v", cliVersion);
|
|
857
|
+
}
|
|
858
|
+
if (isCI()) {
|
|
859
|
+
params.set("ci", "1");
|
|
860
|
+
}
|
|
861
|
+
for (const [key, value] of Object.entries(data)) {
|
|
862
|
+
if (value !== void 0 && value !== null) {
|
|
863
|
+
params.set(key, String(value));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {
|
|
867
|
+
});
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/mintlify.ts
|
|
873
|
+
import matter2 from "gray-matter";
|
|
874
|
+
async function fetchMintlifySkill(url) {
|
|
875
|
+
try {
|
|
876
|
+
const response = await fetch(url);
|
|
877
|
+
if (!response.ok) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
const content = await response.text();
|
|
881
|
+
const { data } = matter2(content);
|
|
882
|
+
const mintlifySite = data.metadata?.["mintlify-proj"];
|
|
883
|
+
if (!mintlifySite) {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
if (!data.name || !data.description) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
name: data.name,
|
|
891
|
+
description: data.description,
|
|
892
|
+
content,
|
|
893
|
+
// Full content including frontmatter
|
|
894
|
+
mintlifySite,
|
|
895
|
+
sourceUrl: url
|
|
896
|
+
};
|
|
897
|
+
} catch {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/providers/registry.ts
|
|
903
|
+
var ProviderRegistryImpl = class {
|
|
904
|
+
providers = [];
|
|
905
|
+
register(provider) {
|
|
906
|
+
if (this.providers.some((p3) => p3.id === provider.id)) {
|
|
907
|
+
throw new Error(`Provider with id "${provider.id}" already registered`);
|
|
908
|
+
}
|
|
909
|
+
this.providers.push(provider);
|
|
910
|
+
}
|
|
911
|
+
findProvider(url) {
|
|
912
|
+
for (const provider of this.providers) {
|
|
913
|
+
const match = provider.match(url);
|
|
914
|
+
if (match.matches) {
|
|
915
|
+
return provider;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
getProviders() {
|
|
921
|
+
return [...this.providers];
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
var registry = new ProviderRegistryImpl();
|
|
925
|
+
function registerProvider(provider) {
|
|
926
|
+
registry.register(provider);
|
|
927
|
+
}
|
|
928
|
+
function findProvider(url) {
|
|
929
|
+
return registry.findProvider(url);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/providers/mintlify.ts
|
|
933
|
+
import matter3 from "gray-matter";
|
|
934
|
+
var MintlifyProvider = class {
|
|
935
|
+
id = "mintlify";
|
|
936
|
+
displayName = "Mintlify";
|
|
937
|
+
match(url) {
|
|
938
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
939
|
+
return { matches: false };
|
|
940
|
+
}
|
|
941
|
+
if (!url.toLowerCase().endsWith("/skill.md")) {
|
|
942
|
+
return { matches: false };
|
|
943
|
+
}
|
|
944
|
+
if (url.includes("github.com") || url.includes("gitlab.com")) {
|
|
945
|
+
return { matches: false };
|
|
946
|
+
}
|
|
947
|
+
if (url.includes("huggingface.co")) {
|
|
948
|
+
return { matches: false };
|
|
949
|
+
}
|
|
950
|
+
return { matches: true };
|
|
951
|
+
}
|
|
952
|
+
async fetchSkill(url) {
|
|
953
|
+
try {
|
|
954
|
+
const response = await fetch(url);
|
|
955
|
+
if (!response.ok) {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
const content = await response.text();
|
|
959
|
+
const { data } = matter3(content);
|
|
960
|
+
const mintlifySite = data.metadata?.["mintlify-proj"];
|
|
961
|
+
if (!mintlifySite) {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
if (!data.name || !data.description) {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
name: data.name,
|
|
969
|
+
description: data.description,
|
|
970
|
+
content,
|
|
971
|
+
installName: mintlifySite,
|
|
972
|
+
sourceUrl: url,
|
|
973
|
+
metadata: data.metadata
|
|
974
|
+
};
|
|
975
|
+
} catch {
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
toRawUrl(url) {
|
|
980
|
+
return url;
|
|
981
|
+
}
|
|
982
|
+
getSourceIdentifier(url) {
|
|
983
|
+
return "mintlify/com";
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
var mintlifyProvider = new MintlifyProvider();
|
|
987
|
+
|
|
988
|
+
// src/providers/huggingface.ts
|
|
989
|
+
import matter4 from "gray-matter";
|
|
990
|
+
var HuggingFaceProvider = class {
|
|
991
|
+
id = "huggingface";
|
|
992
|
+
displayName = "HuggingFace";
|
|
993
|
+
HOST = "huggingface.co";
|
|
994
|
+
match(url) {
|
|
995
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
996
|
+
return { matches: false };
|
|
997
|
+
}
|
|
998
|
+
try {
|
|
999
|
+
const parsed = new URL(url);
|
|
1000
|
+
if (parsed.hostname !== this.HOST) {
|
|
1001
|
+
return { matches: false };
|
|
1002
|
+
}
|
|
1003
|
+
} catch {
|
|
1004
|
+
return { matches: false };
|
|
1005
|
+
}
|
|
1006
|
+
if (!url.toLowerCase().endsWith("/skill.md")) {
|
|
1007
|
+
return { matches: false };
|
|
1008
|
+
}
|
|
1009
|
+
if (!url.includes("/spaces/")) {
|
|
1010
|
+
return { matches: false };
|
|
1011
|
+
}
|
|
1012
|
+
return { matches: true };
|
|
1013
|
+
}
|
|
1014
|
+
async fetchSkill(url) {
|
|
1015
|
+
try {
|
|
1016
|
+
const rawUrl = this.toRawUrl(url);
|
|
1017
|
+
const response = await fetch(rawUrl);
|
|
1018
|
+
if (!response.ok) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
const content = await response.text();
|
|
1022
|
+
const { data } = matter4(content);
|
|
1023
|
+
if (!data.name || !data.description) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
const parsed = this.parseUrl(url);
|
|
1027
|
+
if (!parsed) {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
const installName = data.metadata?.["install-name"] || parsed.repo;
|
|
1031
|
+
return {
|
|
1032
|
+
name: data.name,
|
|
1033
|
+
description: data.description,
|
|
1034
|
+
content,
|
|
1035
|
+
installName,
|
|
1036
|
+
sourceUrl: url,
|
|
1037
|
+
metadata: data.metadata
|
|
1038
|
+
};
|
|
1039
|
+
} catch {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
toRawUrl(url) {
|
|
1044
|
+
return url.replace("/blob/", "/raw/");
|
|
1045
|
+
}
|
|
1046
|
+
getSourceIdentifier(url) {
|
|
1047
|
+
const parsed = this.parseUrl(url);
|
|
1048
|
+
if (!parsed) {
|
|
1049
|
+
return "huggingface/unknown";
|
|
1050
|
+
}
|
|
1051
|
+
return `huggingface/${parsed.owner}/${parsed.repo}`;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Parse a HuggingFace Spaces URL to extract owner and repo.
|
|
1055
|
+
*/
|
|
1056
|
+
parseUrl(url) {
|
|
1057
|
+
const match = url.match(/\/spaces\/([^/]+)\/([^/]+)/);
|
|
1058
|
+
if (!match || !match[1] || !match[2]) {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
owner: match[1],
|
|
1063
|
+
repo: match[2]
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
var huggingFaceProvider = new HuggingFaceProvider();
|
|
1068
|
+
|
|
1069
|
+
// src/providers/index.ts
|
|
1070
|
+
registerProvider(mintlifyProvider);
|
|
1071
|
+
registerProvider(huggingFaceProvider);
|
|
1072
|
+
|
|
1073
|
+
// src/skill-lock.ts
|
|
1074
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1075
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
1076
|
+
import { homedir as homedir3 } from "os";
|
|
1077
|
+
var AGENTS_DIR2 = ".agents";
|
|
1078
|
+
var LOCK_FILE = ".skill-lock.json";
|
|
1079
|
+
var CURRENT_VERSION = 3;
|
|
1080
|
+
function getSkillLockPath() {
|
|
1081
|
+
return join5(homedir3(), AGENTS_DIR2, LOCK_FILE);
|
|
1082
|
+
}
|
|
1083
|
+
async function readSkillLock() {
|
|
1084
|
+
const lockPath = getSkillLockPath();
|
|
1085
|
+
try {
|
|
1086
|
+
const content = await readFile2(lockPath, "utf-8");
|
|
1087
|
+
const parsed = JSON.parse(content);
|
|
1088
|
+
if (typeof parsed.version !== "number" || !parsed.skills) {
|
|
1089
|
+
return createEmptyLockFile();
|
|
1090
|
+
}
|
|
1091
|
+
if (parsed.version < CURRENT_VERSION) {
|
|
1092
|
+
return createEmptyLockFile();
|
|
1093
|
+
}
|
|
1094
|
+
return parsed;
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
return createEmptyLockFile();
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async function writeSkillLock(lock) {
|
|
1100
|
+
const lockPath = getSkillLockPath();
|
|
1101
|
+
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
1102
|
+
const content = JSON.stringify(lock, null, 2);
|
|
1103
|
+
await writeFile2(lockPath, content, "utf-8");
|
|
1104
|
+
}
|
|
1105
|
+
async function fetchSkillFolderHash(ownerRepo, skillPath) {
|
|
1106
|
+
let folderPath = skillPath;
|
|
1107
|
+
if (folderPath.endsWith("/SKILL.md")) {
|
|
1108
|
+
folderPath = folderPath.slice(0, -9);
|
|
1109
|
+
} else if (folderPath.endsWith("SKILL.md")) {
|
|
1110
|
+
folderPath = folderPath.slice(0, -8);
|
|
1111
|
+
}
|
|
1112
|
+
if (folderPath.endsWith("/")) {
|
|
1113
|
+
folderPath = folderPath.slice(0, -1);
|
|
1114
|
+
}
|
|
1115
|
+
const branches = ["main", "master"];
|
|
1116
|
+
for (const branch of branches) {
|
|
1117
|
+
try {
|
|
1118
|
+
const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`;
|
|
1119
|
+
const response = await fetch(url, {
|
|
1120
|
+
headers: {
|
|
1121
|
+
Accept: "application/vnd.github.v3+json",
|
|
1122
|
+
"User-Agent": "add-skill-cli"
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
if (!response.ok) continue;
|
|
1126
|
+
const data = await response.json();
|
|
1127
|
+
if (!folderPath) {
|
|
1128
|
+
return data.sha;
|
|
1129
|
+
}
|
|
1130
|
+
const folderEntry = data.tree.find(
|
|
1131
|
+
(entry) => entry.type === "tree" && entry.path === folderPath
|
|
1132
|
+
);
|
|
1133
|
+
if (folderEntry) {
|
|
1134
|
+
return folderEntry.sha;
|
|
1135
|
+
}
|
|
1136
|
+
} catch {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
async function addSkillToLock(skillName, entry) {
|
|
1143
|
+
const lock = await readSkillLock();
|
|
1144
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1145
|
+
const existingEntry = lock.skills[skillName];
|
|
1146
|
+
lock.skills[skillName] = {
|
|
1147
|
+
...entry,
|
|
1148
|
+
installedAt: existingEntry?.installedAt ?? now,
|
|
1149
|
+
updatedAt: now
|
|
1150
|
+
};
|
|
1151
|
+
await writeSkillLock(lock);
|
|
1152
|
+
}
|
|
1153
|
+
function createEmptyLockFile() {
|
|
1154
|
+
return {
|
|
1155
|
+
version: CURRENT_VERSION,
|
|
1156
|
+
skills: {}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/search.ts
|
|
1161
|
+
import matter5 from "gray-matter";
|
|
1162
|
+
import chalk from "chalk";
|
|
1163
|
+
import * as p from "@clack/prompts";
|
|
1164
|
+
import {
|
|
1165
|
+
chromium
|
|
1166
|
+
} from "patchright";
|
|
1167
|
+
import fs from "fs";
|
|
1168
|
+
import path from "path";
|
|
1169
|
+
import os from "os";
|
|
1170
|
+
import readline from "readline";
|
|
1171
|
+
var ENGINES = [
|
|
1172
|
+
{
|
|
1173
|
+
name: "DuckDuckGo",
|
|
1174
|
+
initialUrl: (q) => `https://duckduckgo.com/?q=${encodeURIComponent('(site:github.com OR site:gitlab.com) inurl:"SKILL.md" ' + q)}`,
|
|
1175
|
+
nextPageSelector: "button#more-results",
|
|
1176
|
+
checkBlock: async (page) => {
|
|
1177
|
+
const title = await page.title();
|
|
1178
|
+
if (title.toLowerCase().includes("ducking...")) return "Robocheck page detected";
|
|
1179
|
+
const h1 = await page.locator("h1").first().textContent().catch(() => "");
|
|
1180
|
+
if (h1?.includes("Bots use DuckDuckGo too")) return "Bot traffic detected";
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
name: "Google",
|
|
1186
|
+
initialUrl: (q) => `https://www.google.com/search?q=${encodeURIComponent('(site:github.com OR site:gitlab.com) filetype:md inurl:"SKILL.md" ' + q)}`,
|
|
1187
|
+
nextPageSelector: "a#pnnext",
|
|
1188
|
+
checkBlock: async (page) => {
|
|
1189
|
+
const url = page.url();
|
|
1190
|
+
if (url.includes("google.com/sorry/index")) return 'Google "sorry" block page';
|
|
1191
|
+
const title = await page.title();
|
|
1192
|
+
if (title.includes("unusual traffic")) return "Google traffic limit";
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
];
|
|
1197
|
+
async function searchSkills(query) {
|
|
1198
|
+
const userDataDir = path.join(
|
|
1199
|
+
os.tmpdir(),
|
|
1200
|
+
`add-skill-chrome-${Math.random().toString(36).slice(2)}`
|
|
1201
|
+
);
|
|
1202
|
+
try {
|
|
1203
|
+
const shuffledEngines = [...ENGINES].sort(() => Math.random() - 0.5);
|
|
1204
|
+
let results = await performPass(query, userDataDir, false, shuffledEngines);
|
|
1205
|
+
cleanupDir(userDataDir);
|
|
1206
|
+
if (process.stdin.isPaused()) {
|
|
1207
|
+
process.stdin.resume();
|
|
1208
|
+
}
|
|
1209
|
+
return results;
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
cleanupDir(userDataDir);
|
|
1212
|
+
return [];
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async function performPass(query, userDataDir, headless, engines) {
|
|
1216
|
+
let context;
|
|
1217
|
+
let client;
|
|
1218
|
+
let windowId;
|
|
1219
|
+
let allLinks = /* @__PURE__ */ new Set();
|
|
1220
|
+
const spinner3 = p.spinner();
|
|
1221
|
+
try {
|
|
1222
|
+
context = await chromium.launchPersistentContext(userDataDir, {
|
|
1223
|
+
channel: "chrome",
|
|
1224
|
+
headless: false,
|
|
1225
|
+
viewport: null
|
|
1226
|
+
// do NOT add custom browser headers or userAgent
|
|
1227
|
+
});
|
|
1228
|
+
const page = context.pages()[0] || await context.newPage();
|
|
1229
|
+
try {
|
|
1230
|
+
client = await page.context().newCDPSession(page);
|
|
1231
|
+
const result = await client.send("Browser.getWindowForTarget");
|
|
1232
|
+
windowId = result.windowId;
|
|
1233
|
+
await client.send("Browser.setWindowBounds", {
|
|
1234
|
+
windowId,
|
|
1235
|
+
bounds: { windowState: "minimized" }
|
|
1236
|
+
});
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
}
|
|
1239
|
+
await page.goto("chrome://version/");
|
|
1240
|
+
await page.waitForLoadState("domcontentloaded");
|
|
1241
|
+
console.log(chalk.bold.cyan("\n Launching search browser."));
|
|
1242
|
+
for (const engine of engines) {
|
|
1243
|
+
let currentPage = 0;
|
|
1244
|
+
const maxPages = 3;
|
|
1245
|
+
let lastCount = allLinks.size;
|
|
1246
|
+
let retries = 0;
|
|
1247
|
+
const maxRetries = 2;
|
|
1248
|
+
let success = false;
|
|
1249
|
+
while (retries <= maxRetries && !success) {
|
|
1250
|
+
if (retries > 0) {
|
|
1251
|
+
spinner3.message(`${engine.name} error, retrying (${retries}/${maxRetries})...`);
|
|
1252
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1253
|
+
}
|
|
1254
|
+
spinner3.start(`Searching ${engine.name} for "${query}"...`);
|
|
1255
|
+
try {
|
|
1256
|
+
page.on("dialog", async (dialog) => {
|
|
1257
|
+
await dialog.dismiss().catch(() => {
|
|
1258
|
+
});
|
|
1259
|
+
});
|
|
1260
|
+
await page.goto(engine.initialUrl(query), {
|
|
1261
|
+
waitUntil: "domcontentloaded",
|
|
1262
|
+
timeout: 2e4
|
|
1263
|
+
});
|
|
1264
|
+
while (currentPage < maxPages) {
|
|
1265
|
+
await page.waitForTimeout(2e3);
|
|
1266
|
+
let blockReason = await engine.checkBlock(page);
|
|
1267
|
+
if (blockReason) {
|
|
1268
|
+
spinner3.stop(`${engine.name} blocked: ${blockReason}`);
|
|
1269
|
+
p.log.message(chalk.bold.cyan(`
|
|
1270
|
+
\u26A0\uFE0F ${engine.name} requires human verification.`));
|
|
1271
|
+
p.log.message(
|
|
1272
|
+
chalk.dim(
|
|
1273
|
+
" Please solve the challenge in the browser window and press ENTER here to continue..."
|
|
1274
|
+
)
|
|
1275
|
+
);
|
|
1276
|
+
if (client && windowId !== void 0) {
|
|
1277
|
+
await client.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "normal" } }).catch(() => {
|
|
1278
|
+
});
|
|
1279
|
+
await page.bringToFront().catch(() => {
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
await askToContinue();
|
|
1283
|
+
if (client && windowId !== void 0) {
|
|
1284
|
+
await client.send("Browser.setWindowBounds", {
|
|
1285
|
+
windowId,
|
|
1286
|
+
bounds: { windowState: "minimized" }
|
|
1287
|
+
}).catch(() => {
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
blockReason = await engine.checkBlock(page);
|
|
1291
|
+
}
|
|
1292
|
+
if (blockReason) {
|
|
1293
|
+
spinner3.stop(`${engine.name} remains blocked (${blockReason})`);
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
const engineLinks = await extractLinksFromPage(page);
|
|
1297
|
+
engineLinks.forEach((link) => allLinks.add(link));
|
|
1298
|
+
const uniqueRepos = new Set(Array.from(allLinks).map(extractRepoUrl));
|
|
1299
|
+
if (uniqueRepos.size >= 10) {
|
|
1300
|
+
spinner3.stop(`${engine.name} reached max (${uniqueRepos.size} unique repos)`);
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
const nextButton = page.locator(engine.nextPageSelector).first();
|
|
1304
|
+
if (await nextButton.isVisible({ timeout: 2e3 }).catch(() => false)) {
|
|
1305
|
+
try {
|
|
1306
|
+
await Promise.all([
|
|
1307
|
+
nextButton.click({ timeout: 5e3 }),
|
|
1308
|
+
page.waitForLoadState("domcontentloaded", { timeout: 1e4 })
|
|
1309
|
+
]);
|
|
1310
|
+
} catch (navErr) {
|
|
1311
|
+
}
|
|
1312
|
+
currentPage++;
|
|
1313
|
+
if (currentPage >= maxPages && allLinks.size === lastCount) {
|
|
1314
|
+
spinner3.stop(`${engine.name} stalled, found ${uniqueRepos.size} unique repos`);
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
lastCount = allLinks.size;
|
|
1318
|
+
} else {
|
|
1319
|
+
spinner3.stop(`${engine.name} found ${uniqueRepos.size} unique repos`);
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
success = true;
|
|
1324
|
+
} catch (e) {
|
|
1325
|
+
retries++;
|
|
1326
|
+
if (retries > maxRetries) {
|
|
1327
|
+
spinner3.stop(`${engine.name} error: ${e instanceof Error ? e.message : "Unknown"}`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const currentUniqueRepos = new Set(Array.from(allLinks).map(extractRepoUrl));
|
|
1332
|
+
if (currentUniqueRepos.size >= 10) break;
|
|
1333
|
+
}
|
|
1334
|
+
await context.close();
|
|
1335
|
+
context = void 0;
|
|
1336
|
+
if (allLinks.size > 0) {
|
|
1337
|
+
return await processResults(Array.from(allLinks));
|
|
1338
|
+
}
|
|
1339
|
+
return [];
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
if (context)
|
|
1342
|
+
try {
|
|
1343
|
+
await context.close();
|
|
1344
|
+
} catch (e) {
|
|
1345
|
+
}
|
|
1346
|
+
return [];
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async function extractLinksFromPage(page) {
|
|
1350
|
+
return await page.evaluate(() => {
|
|
1351
|
+
const links = /* @__PURE__ */ new Set();
|
|
1352
|
+
document.querySelectorAll("a").forEach((el) => {
|
|
1353
|
+
const href = el.href;
|
|
1354
|
+
if (href && (href.includes("github.com") || href.includes("gitlab.com"))) {
|
|
1355
|
+
let cleanUrl = href;
|
|
1356
|
+
if (href.includes("/url?q=")) {
|
|
1357
|
+
const match = href.match(/\/url\?q=([^&]+)/);
|
|
1358
|
+
if (match && match[1]) cleanUrl = decodeURIComponent(match[1]);
|
|
1359
|
+
}
|
|
1360
|
+
if ((cleanUrl.includes("github.com") || cleanUrl.includes("gitlab.com")) && (cleanUrl.toLowerCase().endsWith("skill.md") || cleanUrl.includes("/blob/"))) {
|
|
1361
|
+
links.add(cleanUrl);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
return Array.from(links);
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
async function processResults(urls) {
|
|
1369
|
+
const skills = [];
|
|
1370
|
+
const spinner3 = p.spinner();
|
|
1371
|
+
const candidates = Array.from(new Set(urls));
|
|
1372
|
+
const seenRepos = /* @__PURE__ */ new Set();
|
|
1373
|
+
const uniqueUrls = [];
|
|
1374
|
+
for (const url of candidates) {
|
|
1375
|
+
const repo = extractRepoUrl(url);
|
|
1376
|
+
if (!seenRepos.has(repo)) {
|
|
1377
|
+
seenRepos.add(repo);
|
|
1378
|
+
uniqueUrls.push(url);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (uniqueUrls.length === 0) return [];
|
|
1382
|
+
spinner3.start(`Verifying ${uniqueUrls.length} unique potential skills...`);
|
|
1383
|
+
for (const url of uniqueUrls) {
|
|
1384
|
+
try {
|
|
1385
|
+
const rawUrl = convertToRawUrl(url);
|
|
1386
|
+
const response = await fetch(rawUrl, { signal: AbortSignal.timeout(3e3) });
|
|
1387
|
+
if (response.ok) {
|
|
1388
|
+
const content = await response.text();
|
|
1389
|
+
const { data } = matter5(content);
|
|
1390
|
+
if (data.name && data.description) {
|
|
1391
|
+
skills.push({
|
|
1392
|
+
name: data.name,
|
|
1393
|
+
description: data.description,
|
|
1394
|
+
repoUrl: extractRepoUrl(url),
|
|
1395
|
+
skillUrl: url
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
spinner3.stop(`Found ${skills.length} valid unique skills`);
|
|
1403
|
+
return skills;
|
|
1404
|
+
}
|
|
1405
|
+
function convertToRawUrl(url) {
|
|
1406
|
+
if (url.includes("raw.githubusercontent.com")) return url;
|
|
1407
|
+
if (url.includes("gitlab.com")) return url.replace("/blob/", "/raw/");
|
|
1408
|
+
return url.replace("github.com", "raw.githubusercontent.com").replace("/blob/", "/");
|
|
1409
|
+
}
|
|
1410
|
+
function extractRepoUrl(url) {
|
|
1411
|
+
const match = url.match(/(https:\/\/(?:github\.com|gitlab\.com)\/[^/]+\/[^/]+)/);
|
|
1412
|
+
return match ? match[1] : url;
|
|
1413
|
+
}
|
|
1414
|
+
function cleanupDir(dir) {
|
|
1415
|
+
try {
|
|
1416
|
+
if (fs.existsSync(dir)) {
|
|
1417
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1418
|
+
}
|
|
1419
|
+
} catch (e) {
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
function askToContinue() {
|
|
1423
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1424
|
+
return new Promise((resolve4) => {
|
|
1425
|
+
rl.question("", () => {
|
|
1426
|
+
rl.close();
|
|
1427
|
+
resolve4();
|
|
1428
|
+
});
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// package.json
|
|
1433
|
+
var package_default = {
|
|
1434
|
+
name: "add-skill-lazy",
|
|
1435
|
+
version: "1.0.30",
|
|
1436
|
+
description: "Search Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
|
|
1437
|
+
type: "module",
|
|
1438
|
+
bin: {
|
|
1439
|
+
"add-skill": "./dist/index.js"
|
|
1440
|
+
},
|
|
1441
|
+
files: [
|
|
1442
|
+
"dist",
|
|
1443
|
+
"README.md"
|
|
1444
|
+
],
|
|
1445
|
+
scripts: {
|
|
1446
|
+
build: "tsup src/index.ts --format esm --dts --clean",
|
|
1447
|
+
dev: "tsx src/index.ts",
|
|
1448
|
+
test: "tsx scripts/execute-tests.ts",
|
|
1449
|
+
prepublishOnly: "npm run build",
|
|
1450
|
+
format: "prettier --write 'src/**/*.ts' 'scripts/**/*.ts' 'tests/**/*.ts'",
|
|
1451
|
+
"format:check": "prettier --check 'src/**/*.ts' 'scripts/**/*.ts' 'tests/**/*.ts'",
|
|
1452
|
+
prepare: "husky"
|
|
1453
|
+
},
|
|
1454
|
+
"lint-staged": {
|
|
1455
|
+
"src/**/*.ts": "prettier --write",
|
|
1456
|
+
"scripts/**/*.ts": "prettier --write",
|
|
1457
|
+
"tests/**/*.ts": "prettier --write"
|
|
1458
|
+
},
|
|
1459
|
+
keywords: [
|
|
1460
|
+
"cli",
|
|
1461
|
+
"agent-skills",
|
|
1462
|
+
"skills",
|
|
1463
|
+
"ai-agents",
|
|
1464
|
+
"amp",
|
|
1465
|
+
"antigravity",
|
|
1466
|
+
"claude-code",
|
|
1467
|
+
"clawdbot",
|
|
1468
|
+
"cline",
|
|
1469
|
+
"codex",
|
|
1470
|
+
"command-code",
|
|
1471
|
+
"cursor",
|
|
1472
|
+
"droid",
|
|
1473
|
+
"gemini-cli",
|
|
1474
|
+
"github-copilot",
|
|
1475
|
+
"goose",
|
|
1476
|
+
"kilo",
|
|
1477
|
+
"kiro-cli",
|
|
1478
|
+
"mcpjam",
|
|
1479
|
+
"opencode",
|
|
1480
|
+
"openhands",
|
|
1481
|
+
"pi",
|
|
1482
|
+
"qoder",
|
|
1483
|
+
"qwen-code",
|
|
1484
|
+
"roo",
|
|
1485
|
+
"trae",
|
|
1486
|
+
"windsurf",
|
|
1487
|
+
"zencoder",
|
|
1488
|
+
"neovate"
|
|
1489
|
+
],
|
|
1490
|
+
repository: {
|
|
1491
|
+
type: "git",
|
|
1492
|
+
url: "git+https://github.com/rpfilomeno/add-skill-lazy.git"
|
|
1493
|
+
},
|
|
1494
|
+
homepage: "https://github.com/rpfilomeno/add-skill-lazy#readme",
|
|
1495
|
+
bugs: {
|
|
1496
|
+
url: "https://github.com/rpfilomeno/add-skill-lazy/issues"
|
|
1497
|
+
},
|
|
1498
|
+
author: "",
|
|
1499
|
+
license: "MIT",
|
|
1500
|
+
dependencies: {
|
|
1501
|
+
"@clack/prompts": "^0.9.1",
|
|
1502
|
+
chalk: "^5.4.1",
|
|
1503
|
+
commander: "^13.1.0",
|
|
1504
|
+
"gray-matter": "^4.0.3",
|
|
1505
|
+
patchright: "^1.57.0",
|
|
1506
|
+
"python-shell": "^5.0.0",
|
|
1507
|
+
"simple-git": "^3.27.0"
|
|
1508
|
+
},
|
|
1509
|
+
devDependencies: {
|
|
1510
|
+
"@types/node": "^22.19.7",
|
|
1511
|
+
husky: "^9.1.7",
|
|
1512
|
+
"lint-staged": "^16.2.7",
|
|
1513
|
+
prettier: "^3.8.1",
|
|
1514
|
+
tsup: "^8.3.5",
|
|
1515
|
+
tsx: "^4.19.2",
|
|
1516
|
+
typescript: "^5.7.2"
|
|
1517
|
+
},
|
|
1518
|
+
engines: {
|
|
1519
|
+
node: ">=18"
|
|
1520
|
+
},
|
|
1521
|
+
packageManager: "pnpm@10.17.1"
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
// src/index.ts
|
|
1525
|
+
function shortenPath(fullPath, cwd) {
|
|
1526
|
+
const home2 = homedir4();
|
|
1527
|
+
if (fullPath.startsWith(home2)) {
|
|
1528
|
+
return fullPath.replace(home2, "~");
|
|
1529
|
+
}
|
|
1530
|
+
if (fullPath.startsWith(cwd)) {
|
|
1531
|
+
return "." + fullPath.slice(cwd.length);
|
|
1532
|
+
}
|
|
1533
|
+
return fullPath;
|
|
1534
|
+
}
|
|
1535
|
+
function formatList(items, maxShow = 5) {
|
|
1536
|
+
if (items.length <= maxShow) {
|
|
1537
|
+
return items.join(", ");
|
|
1538
|
+
}
|
|
1539
|
+
const shown = items.slice(0, maxShow);
|
|
1540
|
+
const remaining = items.length - maxShow;
|
|
1541
|
+
return `${shown.join(", ")} +${remaining} more`;
|
|
1542
|
+
}
|
|
1543
|
+
var version = package_default.version;
|
|
1544
|
+
setVersion(version);
|
|
1545
|
+
program.name("add-skill").description(
|
|
1546
|
+
"Install skills onto coding agents (OpenCode, Claude Code, Cline, Codex, Cursor, and more)"
|
|
1547
|
+
).version(version).argument(
|
|
1548
|
+
"[source]",
|
|
1549
|
+
"Git repo URL, GitHub shorthand (owner/repo), local path (./path), or direct path to skill"
|
|
1550
|
+
).option("-q, --query <query>", "Search for skills using DuckDuckGo").option("-g, --global", "Install skill globally (user-level) instead of project-level").option(
|
|
1551
|
+
"-a, --agent <agents...>",
|
|
1552
|
+
"Specify agents to install to (opencode, openhands, claude-code, cline, codex, cursor, and more)"
|
|
1553
|
+
).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("--all", "Install all skills to all agents without any prompts (implies -y -g)").configureOutput({
|
|
1554
|
+
outputError: (str, write) => {
|
|
1555
|
+
if (str.includes("missing required argument")) {
|
|
1556
|
+
console.log();
|
|
1557
|
+
console.log(
|
|
1558
|
+
chalk2.bgRed.white.bold(" ERROR ") + " " + chalk2.red("Missing required argument: source")
|
|
1559
|
+
);
|
|
1560
|
+
console.log();
|
|
1561
|
+
console.log(chalk2.dim(" Usage:"));
|
|
1562
|
+
console.log(
|
|
1563
|
+
` ${chalk2.cyan("npx add-skill")} ${chalk2.yellow("<source>")} ${chalk2.dim("[options]")}`
|
|
1564
|
+
);
|
|
1565
|
+
console.log();
|
|
1566
|
+
console.log(chalk2.dim(" Example:"));
|
|
1567
|
+
console.log(
|
|
1568
|
+
` ${chalk2.cyan("npx add-skill")} ${chalk2.yellow("rpfilomeno/agent-skills-lazy")}`
|
|
1569
|
+
);
|
|
1570
|
+
console.log();
|
|
1571
|
+
console.log(
|
|
1572
|
+
chalk2.dim(" Run") + ` ${chalk2.cyan("npx add-skill --help")} ` + chalk2.dim("for more information.")
|
|
1573
|
+
);
|
|
1574
|
+
console.log();
|
|
1575
|
+
} else {
|
|
1576
|
+
write(str);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}).action(async (source, options) => {
|
|
1580
|
+
if (!source && !options.query) {
|
|
1581
|
+
program.help();
|
|
1582
|
+
}
|
|
1583
|
+
await main(source || "", options);
|
|
1584
|
+
});
|
|
1585
|
+
program.parse();
|
|
1586
|
+
async function handleRemoteSkill(source, url, options, spinner3) {
|
|
1587
|
+
const provider = findProvider(url);
|
|
1588
|
+
if (!provider) {
|
|
1589
|
+
return handleDirectUrlSkillLegacy(source, url, options, spinner3);
|
|
1590
|
+
}
|
|
1591
|
+
spinner3.start(`Fetching skill.md from ${provider.displayName}...`);
|
|
1592
|
+
const providerSkill = await provider.fetchSkill(url);
|
|
1593
|
+
if (!providerSkill) {
|
|
1594
|
+
spinner3.stop(chalk2.red("Invalid skill"));
|
|
1595
|
+
p2.outro(
|
|
1596
|
+
chalk2.red("Could not fetch skill.md or missing required frontmatter (name, description).")
|
|
1597
|
+
);
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
const remoteSkill = {
|
|
1601
|
+
name: providerSkill.name,
|
|
1602
|
+
description: providerSkill.description,
|
|
1603
|
+
content: providerSkill.content,
|
|
1604
|
+
installName: providerSkill.installName,
|
|
1605
|
+
sourceUrl: providerSkill.sourceUrl,
|
|
1606
|
+
providerId: provider.id,
|
|
1607
|
+
sourceIdentifier: provider.getSourceIdentifier(url),
|
|
1608
|
+
metadata: providerSkill.metadata
|
|
1609
|
+
};
|
|
1610
|
+
spinner3.stop(`Found skill: ${chalk2.cyan(remoteSkill.installName)}`);
|
|
1611
|
+
p2.log.info(`Skill: ${chalk2.cyan(remoteSkill.name)}`);
|
|
1612
|
+
p2.log.message(chalk2.dim(remoteSkill.description));
|
|
1613
|
+
p2.log.message(chalk2.dim(`Source: ${remoteSkill.sourceIdentifier}`));
|
|
1614
|
+
if (options.list) {
|
|
1615
|
+
console.log();
|
|
1616
|
+
p2.log.step(chalk2.bold("Skill Details"));
|
|
1617
|
+
p2.log.message(` ${chalk2.cyan("Name:")} ${remoteSkill.name}`);
|
|
1618
|
+
p2.log.message(` ${chalk2.cyan("Install as:")} ${remoteSkill.installName}`);
|
|
1619
|
+
p2.log.message(` ${chalk2.cyan("Provider:")} ${provider.displayName}`);
|
|
1620
|
+
p2.log.message(` ${chalk2.cyan("Description:")} ${remoteSkill.description}`);
|
|
1621
|
+
console.log();
|
|
1622
|
+
p2.outro("Run without --list to install");
|
|
1623
|
+
process.exit(0);
|
|
1624
|
+
}
|
|
1625
|
+
let targetAgents;
|
|
1626
|
+
const validAgents = Object.keys(agents);
|
|
1627
|
+
if (options.agent && options.agent.length > 0) {
|
|
1628
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
1629
|
+
if (invalidAgents.length > 0) {
|
|
1630
|
+
p2.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
1631
|
+
p2.log.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
}
|
|
1634
|
+
targetAgents = options.agent;
|
|
1635
|
+
} else {
|
|
1636
|
+
spinner3.start("Detecting installed agents...");
|
|
1637
|
+
const installedAgents = await detectInstalledAgents();
|
|
1638
|
+
spinner3.stop(
|
|
1639
|
+
`Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? "s" : ""}`
|
|
1640
|
+
);
|
|
1641
|
+
if (installedAgents.length === 0) {
|
|
1642
|
+
if (options.yes) {
|
|
1643
|
+
targetAgents = validAgents;
|
|
1644
|
+
p2.log.info("Installing to all agents (none detected)");
|
|
1645
|
+
} else {
|
|
1646
|
+
p2.log.warn("No coding agents detected. You can still install skills.");
|
|
1647
|
+
const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
|
|
1648
|
+
value: key,
|
|
1649
|
+
label: config.displayName
|
|
1650
|
+
}));
|
|
1651
|
+
const selected = await p2.multiselect({
|
|
1652
|
+
message: "Select agents to install skills to",
|
|
1653
|
+
options: allAgentChoices,
|
|
1654
|
+
required: true,
|
|
1655
|
+
initialValues: Object.keys(agents)
|
|
1656
|
+
});
|
|
1657
|
+
if (p2.isCancel(selected)) {
|
|
1658
|
+
p2.cancel("Installation cancelled");
|
|
1659
|
+
process.exit(0);
|
|
1660
|
+
}
|
|
1661
|
+
targetAgents = selected;
|
|
1662
|
+
}
|
|
1663
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
1664
|
+
targetAgents = installedAgents;
|
|
1665
|
+
if (installedAgents.length === 1) {
|
|
1666
|
+
const firstAgent = installedAgents[0];
|
|
1667
|
+
p2.log.info(`Installing to: ${chalk2.cyan(agents[firstAgent].displayName)}`);
|
|
1668
|
+
} else {
|
|
1669
|
+
p2.log.info(
|
|
1670
|
+
`Installing to: ${installedAgents.map((a) => chalk2.cyan(agents[a].displayName)).join(", ")}`
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
} else {
|
|
1674
|
+
const agentChoices = installedAgents.map((a) => ({
|
|
1675
|
+
value: a,
|
|
1676
|
+
label: agents[a].displayName,
|
|
1677
|
+
hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`
|
|
1678
|
+
}));
|
|
1679
|
+
const selected = await p2.multiselect({
|
|
1680
|
+
message: "Select agents to install skills to",
|
|
1681
|
+
options: agentChoices,
|
|
1682
|
+
required: true,
|
|
1683
|
+
initialValues: installedAgents
|
|
1684
|
+
});
|
|
1685
|
+
if (p2.isCancel(selected)) {
|
|
1686
|
+
p2.cancel("Installation cancelled");
|
|
1687
|
+
process.exit(0);
|
|
1688
|
+
}
|
|
1689
|
+
targetAgents = selected;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
let installGlobally = options.global ?? false;
|
|
1693
|
+
if (options.global === void 0 && !options.yes) {
|
|
1694
|
+
const scope = await p2.select({
|
|
1695
|
+
message: "Installation scope",
|
|
1696
|
+
options: [
|
|
1697
|
+
{
|
|
1698
|
+
value: false,
|
|
1699
|
+
label: "Project",
|
|
1700
|
+
hint: "Install in current directory (committed with your project)"
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
value: true,
|
|
1704
|
+
label: "Global",
|
|
1705
|
+
hint: "Install in home directory (available across all projects)"
|
|
1706
|
+
}
|
|
1707
|
+
]
|
|
1708
|
+
});
|
|
1709
|
+
if (p2.isCancel(scope)) {
|
|
1710
|
+
p2.cancel("Installation cancelled");
|
|
1711
|
+
process.exit(0);
|
|
1712
|
+
}
|
|
1713
|
+
installGlobally = scope;
|
|
1714
|
+
}
|
|
1715
|
+
let installMode = "symlink";
|
|
1716
|
+
if (!options.yes) {
|
|
1717
|
+
const modeChoice = await p2.select({
|
|
1718
|
+
message: "Installation method",
|
|
1719
|
+
options: [
|
|
1720
|
+
{
|
|
1721
|
+
value: "symlink",
|
|
1722
|
+
label: "Symlink (Recommended)",
|
|
1723
|
+
hint: "Single source of truth, easy updates"
|
|
1724
|
+
},
|
|
1725
|
+
{ value: "copy", label: "Copy to all agents", hint: "Independent copies for each agent" }
|
|
1726
|
+
]
|
|
1727
|
+
});
|
|
1728
|
+
if (p2.isCancel(modeChoice)) {
|
|
1729
|
+
p2.cancel("Installation cancelled");
|
|
1730
|
+
process.exit(0);
|
|
1731
|
+
}
|
|
1732
|
+
installMode = modeChoice;
|
|
1733
|
+
}
|
|
1734
|
+
const cwd = process.cwd();
|
|
1735
|
+
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
1736
|
+
for (const agent of targetAgents) {
|
|
1737
|
+
overwriteStatus.set(
|
|
1738
|
+
agent,
|
|
1739
|
+
await isSkillInstalled(remoteSkill.installName, agent, {
|
|
1740
|
+
global: installGlobally
|
|
1741
|
+
})
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
const summaryLines = [];
|
|
1745
|
+
const agentNames = targetAgents.map((a) => agents[a].displayName);
|
|
1746
|
+
if (installMode === "symlink") {
|
|
1747
|
+
const canonicalPath = getCanonicalPath(remoteSkill.installName, { global: installGlobally });
|
|
1748
|
+
const shortCanonical = shortenPath(canonicalPath, cwd);
|
|
1749
|
+
summaryLines.push(`${chalk2.cyan(shortCanonical)}`);
|
|
1750
|
+
summaryLines.push(` ${chalk2.dim("symlink \u2192")} ${formatList(agentNames)}`);
|
|
1751
|
+
} else {
|
|
1752
|
+
summaryLines.push(`${chalk2.cyan(remoteSkill.installName)}`);
|
|
1753
|
+
summaryLines.push(` ${chalk2.dim("copy \u2192")} ${formatList(agentNames)}`);
|
|
1754
|
+
}
|
|
1755
|
+
const overwriteAgents = targetAgents.filter((a) => overwriteStatus.get(a)).map((a) => agents[a].displayName);
|
|
1756
|
+
if (overwriteAgents.length > 0) {
|
|
1757
|
+
summaryLines.push(` ${chalk2.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
|
|
1758
|
+
}
|
|
1759
|
+
console.log();
|
|
1760
|
+
p2.note(summaryLines.join("\n"), "Installation Summary");
|
|
1761
|
+
if (!options.yes) {
|
|
1762
|
+
const confirmed = await p2.confirm({
|
|
1763
|
+
message: "Proceed with installation?"
|
|
1764
|
+
});
|
|
1765
|
+
if (p2.isCancel(confirmed) || !confirmed) {
|
|
1766
|
+
p2.cancel("Installation cancelled");
|
|
1767
|
+
process.exit(0);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
spinner3.start("Installing skill...");
|
|
1771
|
+
const results = [];
|
|
1772
|
+
for (const agent of targetAgents) {
|
|
1773
|
+
const result = await installRemoteSkillForAgent(remoteSkill, agent, {
|
|
1774
|
+
global: installGlobally,
|
|
1775
|
+
mode: installMode
|
|
1776
|
+
});
|
|
1777
|
+
results.push({
|
|
1778
|
+
skill: remoteSkill.installName,
|
|
1779
|
+
agent: agents[agent].displayName,
|
|
1780
|
+
...result
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
spinner3.stop("Installation complete");
|
|
1784
|
+
console.log();
|
|
1785
|
+
const successful = results.filter((r) => r.success);
|
|
1786
|
+
const failed = results.filter((r) => !r.success);
|
|
1787
|
+
track({
|
|
1788
|
+
event: "install",
|
|
1789
|
+
source: remoteSkill.sourceIdentifier,
|
|
1790
|
+
skills: remoteSkill.installName,
|
|
1791
|
+
agents: targetAgents.join(","),
|
|
1792
|
+
...installGlobally && { global: "1" },
|
|
1793
|
+
skillFiles: JSON.stringify({ [remoteSkill.installName]: url }),
|
|
1794
|
+
sourceType: remoteSkill.providerId
|
|
1795
|
+
});
|
|
1796
|
+
if (successful.length > 0 && installGlobally) {
|
|
1797
|
+
try {
|
|
1798
|
+
let skillFolderHash = "";
|
|
1799
|
+
if (remoteSkill.providerId === "github") {
|
|
1800
|
+
const hash = await fetchSkillFolderHash(remoteSkill.sourceIdentifier, url);
|
|
1801
|
+
if (hash) skillFolderHash = hash;
|
|
1802
|
+
}
|
|
1803
|
+
await addSkillToLock(remoteSkill.installName, {
|
|
1804
|
+
source: remoteSkill.sourceIdentifier,
|
|
1805
|
+
sourceType: remoteSkill.providerId,
|
|
1806
|
+
sourceUrl: url,
|
|
1807
|
+
skillFolderHash
|
|
1808
|
+
});
|
|
1809
|
+
} catch {
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (successful.length > 0) {
|
|
1813
|
+
const resultLines = [];
|
|
1814
|
+
const firstResult = successful[0];
|
|
1815
|
+
if (firstResult.mode === "copy") {
|
|
1816
|
+
resultLines.push(`${chalk2.green("\u2713")} ${remoteSkill.installName} ${chalk2.dim("(copied)")}`);
|
|
1817
|
+
for (const r of successful) {
|
|
1818
|
+
const shortPath = shortenPath(r.path, cwd);
|
|
1819
|
+
resultLines.push(` ${chalk2.dim("\u2192")} ${shortPath}`);
|
|
1820
|
+
}
|
|
1821
|
+
} else {
|
|
1822
|
+
if (firstResult.canonicalPath) {
|
|
1823
|
+
const shortPath = shortenPath(firstResult.canonicalPath, cwd);
|
|
1824
|
+
resultLines.push(`${chalk2.green("\u2713")} ${shortPath}`);
|
|
1825
|
+
} else {
|
|
1826
|
+
resultLines.push(`${chalk2.green("\u2713")} ${remoteSkill.installName}`);
|
|
1827
|
+
}
|
|
1828
|
+
const symlinked = successful.filter((r) => !r.symlinkFailed).map((r) => r.agent);
|
|
1829
|
+
const copied = successful.filter((r) => r.symlinkFailed).map((r) => r.agent);
|
|
1830
|
+
if (symlinked.length > 0) {
|
|
1831
|
+
resultLines.push(` ${chalk2.dim("symlink \u2192")} ${formatList(symlinked)}`);
|
|
1832
|
+
}
|
|
1833
|
+
if (copied.length > 0) {
|
|
1834
|
+
resultLines.push(` ${chalk2.yellow("copied \u2192")} ${formatList(copied)}`);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const title = chalk2.green(
|
|
1838
|
+
`Installed 1 skill to ${successful.length} agent${successful.length !== 1 ? "s" : ""}`
|
|
1839
|
+
);
|
|
1840
|
+
p2.note(resultLines.join("\n"), title);
|
|
1841
|
+
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
1842
|
+
if (symlinkFailures.length > 0) {
|
|
1843
|
+
const copiedAgentNames = symlinkFailures.map((r) => r.agent);
|
|
1844
|
+
p2.log.warn(chalk2.yellow(`Symlinks failed for: ${formatList(copiedAgentNames)}`));
|
|
1845
|
+
p2.log.message(
|
|
1846
|
+
chalk2.dim(
|
|
1847
|
+
" Files were copied instead. On Windows, enable Developer Mode for symlink support."
|
|
1848
|
+
)
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (failed.length > 0) {
|
|
1853
|
+
console.log();
|
|
1854
|
+
p2.log.error(chalk2.red(`Failed to install ${failed.length}`));
|
|
1855
|
+
for (const r of failed) {
|
|
1856
|
+
p2.log.message(` ${chalk2.red("\u2717")} ${r.skill} \u2192 ${r.agent}: ${chalk2.dim(r.error)}`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
console.log();
|
|
1860
|
+
p2.outro(chalk2.green("Done!"));
|
|
1861
|
+
}
|
|
1862
|
+
async function handleDirectUrlSkillLegacy(source, url, options, spinner3) {
|
|
1863
|
+
spinner3.start("Fetching skill.md...");
|
|
1864
|
+
const mintlifySkill = await fetchMintlifySkill(url);
|
|
1865
|
+
if (!mintlifySkill) {
|
|
1866
|
+
spinner3.stop(chalk2.red("Invalid skill"));
|
|
1867
|
+
p2.outro(
|
|
1868
|
+
chalk2.red(
|
|
1869
|
+
"Could not fetch skill.md or missing required frontmatter (name, description, mintlify-proj)."
|
|
1870
|
+
)
|
|
1871
|
+
);
|
|
1872
|
+
process.exit(1);
|
|
1873
|
+
}
|
|
1874
|
+
spinner3.stop(`Found skill: ${chalk2.cyan(mintlifySkill.mintlifySite)}`);
|
|
1875
|
+
p2.log.info(`Skill: ${chalk2.cyan(mintlifySkill.name)}`);
|
|
1876
|
+
p2.log.message(chalk2.dim(mintlifySkill.description));
|
|
1877
|
+
p2.log.message(chalk2.dim(`Source: mintlify/skills`));
|
|
1878
|
+
if (options.list) {
|
|
1879
|
+
console.log();
|
|
1880
|
+
p2.log.step(chalk2.bold("Skill Details"));
|
|
1881
|
+
p2.log.message(` ${chalk2.cyan("Name:")} ${mintlifySkill.name}`);
|
|
1882
|
+
p2.log.message(` ${chalk2.cyan("Site:")} ${mintlifySkill.mintlifySite}`);
|
|
1883
|
+
p2.log.message(` ${chalk2.cyan("Description:")} ${mintlifySkill.description}`);
|
|
1884
|
+
console.log();
|
|
1885
|
+
p2.outro("Run without --list to install");
|
|
1886
|
+
process.exit(0);
|
|
1887
|
+
}
|
|
1888
|
+
let targetAgents;
|
|
1889
|
+
const validAgents = Object.keys(agents);
|
|
1890
|
+
if (options.agent && options.agent.length > 0) {
|
|
1891
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
1892
|
+
if (invalidAgents.length > 0) {
|
|
1893
|
+
p2.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
1894
|
+
p2.log.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
1895
|
+
process.exit(1);
|
|
1896
|
+
}
|
|
1897
|
+
targetAgents = options.agent;
|
|
1898
|
+
} else {
|
|
1899
|
+
spinner3.start("Detecting installed agents...");
|
|
1900
|
+
const installedAgents = await detectInstalledAgents();
|
|
1901
|
+
spinner3.stop(
|
|
1902
|
+
`Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? "s" : ""}`
|
|
1903
|
+
);
|
|
1904
|
+
if (installedAgents.length === 0) {
|
|
1905
|
+
if (options.yes) {
|
|
1906
|
+
targetAgents = validAgents;
|
|
1907
|
+
p2.log.info("Installing to all agents (none detected)");
|
|
1908
|
+
} else {
|
|
1909
|
+
p2.log.warn("No coding agents detected. You can still install skills.");
|
|
1910
|
+
const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
|
|
1911
|
+
value: key,
|
|
1912
|
+
label: config.displayName
|
|
1913
|
+
}));
|
|
1914
|
+
const selected = await p2.multiselect({
|
|
1915
|
+
message: "Select agents to install skills to",
|
|
1916
|
+
options: allAgentChoices,
|
|
1917
|
+
required: true,
|
|
1918
|
+
initialValues: Object.keys(agents)
|
|
1919
|
+
});
|
|
1920
|
+
if (p2.isCancel(selected)) {
|
|
1921
|
+
p2.cancel("Installation cancelled");
|
|
1922
|
+
process.exit(0);
|
|
1923
|
+
}
|
|
1924
|
+
targetAgents = selected;
|
|
1925
|
+
}
|
|
1926
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
1927
|
+
targetAgents = installedAgents;
|
|
1928
|
+
if (installedAgents.length === 1) {
|
|
1929
|
+
const firstAgent = installedAgents[0];
|
|
1930
|
+
p2.log.info(`Installing to: ${chalk2.cyan(agents[firstAgent].displayName)}`);
|
|
1931
|
+
} else {
|
|
1932
|
+
p2.log.info(
|
|
1933
|
+
`Installing to: ${installedAgents.map((a) => chalk2.cyan(agents[a].displayName)).join(", ")}`
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
} else {
|
|
1937
|
+
const agentChoices = installedAgents.map((a) => ({
|
|
1938
|
+
value: a,
|
|
1939
|
+
label: agents[a].displayName,
|
|
1940
|
+
hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`
|
|
1941
|
+
}));
|
|
1942
|
+
const selected = await p2.multiselect({
|
|
1943
|
+
message: "Select agents to install skills to",
|
|
1944
|
+
options: agentChoices,
|
|
1945
|
+
required: true,
|
|
1946
|
+
initialValues: installedAgents
|
|
1947
|
+
});
|
|
1948
|
+
if (p2.isCancel(selected)) {
|
|
1949
|
+
p2.cancel("Installation cancelled");
|
|
1950
|
+
process.exit(0);
|
|
1951
|
+
}
|
|
1952
|
+
targetAgents = selected;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
let installGlobally = options.global ?? false;
|
|
1956
|
+
if (options.global === void 0 && !options.yes) {
|
|
1957
|
+
const scope = await p2.select({
|
|
1958
|
+
message: "Installation scope",
|
|
1959
|
+
options: [
|
|
1960
|
+
{
|
|
1961
|
+
value: false,
|
|
1962
|
+
label: "Project",
|
|
1963
|
+
hint: "Install in current directory (committed with your project)"
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
value: true,
|
|
1967
|
+
label: "Global",
|
|
1968
|
+
hint: "Install in home directory (available across all projects)"
|
|
1969
|
+
}
|
|
1970
|
+
]
|
|
1971
|
+
});
|
|
1972
|
+
if (p2.isCancel(scope)) {
|
|
1973
|
+
p2.cancel("Installation cancelled");
|
|
1974
|
+
process.exit(0);
|
|
1975
|
+
}
|
|
1976
|
+
installGlobally = scope;
|
|
1977
|
+
}
|
|
1978
|
+
let installMode = "symlink";
|
|
1979
|
+
if (!options.yes) {
|
|
1980
|
+
const modeChoice = await p2.select({
|
|
1981
|
+
message: "Installation method",
|
|
1982
|
+
options: [
|
|
1983
|
+
{
|
|
1984
|
+
value: "symlink",
|
|
1985
|
+
label: "Symlink (Recommended)",
|
|
1986
|
+
hint: "Single source of truth, easy updates"
|
|
1987
|
+
},
|
|
1988
|
+
{ value: "copy", label: "Copy to all agents", hint: "Independent copies for each agent" }
|
|
1989
|
+
]
|
|
1990
|
+
});
|
|
1991
|
+
if (p2.isCancel(modeChoice)) {
|
|
1992
|
+
p2.cancel("Installation cancelled");
|
|
1993
|
+
process.exit(0);
|
|
1994
|
+
}
|
|
1995
|
+
installMode = modeChoice;
|
|
1996
|
+
}
|
|
1997
|
+
const cwd = process.cwd();
|
|
1998
|
+
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
1999
|
+
for (const agent of targetAgents) {
|
|
2000
|
+
overwriteStatus.set(
|
|
2001
|
+
agent,
|
|
2002
|
+
await isSkillInstalled(mintlifySkill.mintlifySite, agent, {
|
|
2003
|
+
global: installGlobally
|
|
2004
|
+
})
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
const summaryLines = [];
|
|
2008
|
+
const agentNames = targetAgents.map((a) => agents[a].displayName);
|
|
2009
|
+
if (installMode === "symlink") {
|
|
2010
|
+
const canonicalPath = getCanonicalPath(mintlifySkill.mintlifySite, { global: installGlobally });
|
|
2011
|
+
const shortCanonical = shortenPath(canonicalPath, cwd);
|
|
2012
|
+
summaryLines.push(`${chalk2.cyan(shortCanonical)}`);
|
|
2013
|
+
summaryLines.push(` ${chalk2.dim("symlink \u2192")} ${formatList(agentNames)}`);
|
|
2014
|
+
} else {
|
|
2015
|
+
summaryLines.push(`${chalk2.cyan(mintlifySkill.mintlifySite)}`);
|
|
2016
|
+
summaryLines.push(` ${chalk2.dim("copy \u2192")} ${formatList(agentNames)}`);
|
|
2017
|
+
}
|
|
2018
|
+
const overwriteAgents = targetAgents.filter((a) => overwriteStatus.get(a)).map((a) => agents[a].displayName);
|
|
2019
|
+
if (overwriteAgents.length > 0) {
|
|
2020
|
+
summaryLines.push(` ${chalk2.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
|
|
2021
|
+
}
|
|
2022
|
+
console.log();
|
|
2023
|
+
p2.note(summaryLines.join("\n"), "Installation Summary");
|
|
2024
|
+
if (!options.yes) {
|
|
2025
|
+
const confirmed = await p2.confirm({
|
|
2026
|
+
message: "Proceed with installation?"
|
|
2027
|
+
});
|
|
2028
|
+
if (p2.isCancel(confirmed) || !confirmed) {
|
|
2029
|
+
p2.cancel("Installation cancelled");
|
|
2030
|
+
process.exit(0);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
spinner3.start("Installing skill...");
|
|
2034
|
+
const results = [];
|
|
2035
|
+
for (const agent of targetAgents) {
|
|
2036
|
+
const result = await installMintlifySkillForAgent(mintlifySkill, agent, {
|
|
2037
|
+
global: installGlobally,
|
|
2038
|
+
mode: installMode
|
|
2039
|
+
});
|
|
2040
|
+
results.push({
|
|
2041
|
+
skill: mintlifySkill.mintlifySite,
|
|
2042
|
+
agent: agents[agent].displayName,
|
|
2043
|
+
...result
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
spinner3.stop("Installation complete");
|
|
2047
|
+
console.log();
|
|
2048
|
+
const successful = results.filter((r) => r.success);
|
|
2049
|
+
const failed = results.filter((r) => !r.success);
|
|
2050
|
+
track({
|
|
2051
|
+
event: "install",
|
|
2052
|
+
source: "mintlify/skills",
|
|
2053
|
+
skills: mintlifySkill.mintlifySite,
|
|
2054
|
+
agents: targetAgents.join(","),
|
|
2055
|
+
...installGlobally && { global: "1" },
|
|
2056
|
+
skillFiles: JSON.stringify({ [mintlifySkill.mintlifySite]: url }),
|
|
2057
|
+
sourceType: "mintlify"
|
|
2058
|
+
});
|
|
2059
|
+
if (successful.length > 0 && installGlobally) {
|
|
2060
|
+
try {
|
|
2061
|
+
await addSkillToLock(mintlifySkill.mintlifySite, {
|
|
2062
|
+
source: `mintlify/${mintlifySkill.mintlifySite}`,
|
|
2063
|
+
sourceType: "mintlify",
|
|
2064
|
+
sourceUrl: url,
|
|
2065
|
+
skillFolderHash: ""
|
|
2066
|
+
// Populated by server
|
|
2067
|
+
});
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
if (successful.length > 0) {
|
|
2072
|
+
const resultLines = [];
|
|
2073
|
+
const firstResult = successful[0];
|
|
2074
|
+
if (firstResult.mode === "copy") {
|
|
2075
|
+
resultLines.push(
|
|
2076
|
+
`${chalk2.green("\u2713")} ${mintlifySkill.mintlifySite} ${chalk2.dim("(copied)")}`
|
|
2077
|
+
);
|
|
2078
|
+
for (const r of successful) {
|
|
2079
|
+
const shortPath = shortenPath(r.path, cwd);
|
|
2080
|
+
resultLines.push(` ${chalk2.dim("\u2192")} ${shortPath}`);
|
|
2081
|
+
}
|
|
2082
|
+
} else {
|
|
2083
|
+
if (firstResult.canonicalPath) {
|
|
2084
|
+
const shortPath = shortenPath(firstResult.canonicalPath, cwd);
|
|
2085
|
+
resultLines.push(`${chalk2.green("\u2713")} ${shortPath}`);
|
|
2086
|
+
} else {
|
|
2087
|
+
resultLines.push(`${chalk2.green("\u2713")} ${mintlifySkill.mintlifySite}`);
|
|
2088
|
+
}
|
|
2089
|
+
const symlinked = successful.filter((r) => !r.symlinkFailed).map((r) => r.agent);
|
|
2090
|
+
const copied = successful.filter((r) => r.symlinkFailed).map((r) => r.agent);
|
|
2091
|
+
if (symlinked.length > 0) {
|
|
2092
|
+
resultLines.push(` ${chalk2.dim("symlink \u2192")} ${formatList(symlinked)}`);
|
|
2093
|
+
}
|
|
2094
|
+
if (copied.length > 0) {
|
|
2095
|
+
resultLines.push(` ${chalk2.yellow("copied \u2192")} ${formatList(copied)}`);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
const title = chalk2.green(
|
|
2099
|
+
`Installed 1 skill to ${successful.length} agent${successful.length !== 1 ? "s" : ""}`
|
|
2100
|
+
);
|
|
2101
|
+
p2.note(resultLines.join("\n"), title);
|
|
2102
|
+
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
2103
|
+
if (symlinkFailures.length > 0) {
|
|
2104
|
+
const copiedAgentNames = symlinkFailures.map((r) => r.agent);
|
|
2105
|
+
p2.log.warn(chalk2.yellow(`Symlinks failed for: ${formatList(copiedAgentNames)}`));
|
|
2106
|
+
p2.log.message(
|
|
2107
|
+
chalk2.dim(
|
|
2108
|
+
" Files were copied instead. On Windows, enable Developer Mode for symlink support."
|
|
2109
|
+
)
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
if (failed.length > 0) {
|
|
2114
|
+
console.log();
|
|
2115
|
+
p2.log.error(chalk2.red(`Failed to install ${failed.length}`));
|
|
2116
|
+
for (const r of failed) {
|
|
2117
|
+
p2.log.message(` ${chalk2.red("\u2717")} ${r.skill} \u2192 ${r.agent}: ${chalk2.dim(r.error)}`);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
console.log();
|
|
2121
|
+
p2.outro(chalk2.green("Done!"));
|
|
2122
|
+
}
|
|
2123
|
+
async function main(source, options) {
|
|
2124
|
+
if (options.all) {
|
|
2125
|
+
options.yes = true;
|
|
2126
|
+
options.global = true;
|
|
2127
|
+
}
|
|
2128
|
+
console.log();
|
|
2129
|
+
p2.intro(chalk2.bgCyan.black(" skills "));
|
|
2130
|
+
let tempDir = null;
|
|
2131
|
+
try {
|
|
2132
|
+
const spinner3 = p2.spinner();
|
|
2133
|
+
if (options.query) {
|
|
2134
|
+
await handleSearch(options.query, options);
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
spinner3.start("Parsing source...");
|
|
2138
|
+
const parsed = parseSource(source);
|
|
2139
|
+
spinner3.stop(
|
|
2140
|
+
`Source: ${chalk2.cyan(parsed.type === "local" ? parsed.localPath : parsed.url)}${parsed.subpath ? ` (${parsed.subpath})` : ""}`
|
|
2141
|
+
);
|
|
2142
|
+
if (parsed.type === "direct-url") {
|
|
2143
|
+
await handleRemoteSkill(source, parsed.url, options, spinner3);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
let skillsDir;
|
|
2147
|
+
if (parsed.type === "local") {
|
|
2148
|
+
spinner3.start("Validating local path...");
|
|
2149
|
+
const { existsSync: existsSync2 } = await import("fs");
|
|
2150
|
+
if (!existsSync2(parsed.localPath)) {
|
|
2151
|
+
spinner3.stop(chalk2.red("Path not found"));
|
|
2152
|
+
p2.outro(chalk2.red(`Local path does not exist: ${parsed.localPath}`));
|
|
2153
|
+
process.exit(1);
|
|
2154
|
+
}
|
|
2155
|
+
skillsDir = parsed.localPath;
|
|
2156
|
+
spinner3.stop("Local path validated");
|
|
2157
|
+
} else {
|
|
2158
|
+
spinner3.start("Cloning repository...");
|
|
2159
|
+
tempDir = await cloneRepo(parsed.url);
|
|
2160
|
+
skillsDir = tempDir;
|
|
2161
|
+
spinner3.stop("Repository cloned");
|
|
2162
|
+
}
|
|
2163
|
+
spinner3.start("Discovering skills...");
|
|
2164
|
+
const skills = await discoverSkills(skillsDir, parsed.subpath);
|
|
2165
|
+
if (skills.length === 0) {
|
|
2166
|
+
spinner3.stop(chalk2.red("No skills found"));
|
|
2167
|
+
p2.outro(
|
|
2168
|
+
chalk2.red("No valid skills found. Skills require a SKILL.md with name and description.")
|
|
2169
|
+
);
|
|
2170
|
+
await cleanup(tempDir);
|
|
2171
|
+
process.exit(1);
|
|
2172
|
+
}
|
|
2173
|
+
spinner3.stop(`Found ${chalk2.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
2174
|
+
if (options.list) {
|
|
2175
|
+
console.log();
|
|
2176
|
+
p2.log.step(chalk2.bold("Available Skills"));
|
|
2177
|
+
for (const skill of skills) {
|
|
2178
|
+
p2.log.message(` ${chalk2.cyan(getSkillDisplayName(skill))}`);
|
|
2179
|
+
p2.log.message(` ${chalk2.dim(skill.description)}`);
|
|
2180
|
+
}
|
|
2181
|
+
console.log();
|
|
2182
|
+
p2.outro("Use --skill <name> to install specific skills");
|
|
2183
|
+
await cleanup(tempDir);
|
|
2184
|
+
process.exit(0);
|
|
2185
|
+
}
|
|
2186
|
+
let selectedSkills;
|
|
2187
|
+
if (options.skill && options.skill.length > 0) {
|
|
2188
|
+
selectedSkills = skills.filter(
|
|
2189
|
+
(s) => options.skill.some(
|
|
2190
|
+
(name) => s.name.toLowerCase() === name.toLowerCase() || getSkillDisplayName(s).toLowerCase() === name.toLowerCase()
|
|
2191
|
+
)
|
|
2192
|
+
);
|
|
2193
|
+
if (selectedSkills.length === 0) {
|
|
2194
|
+
p2.log.error(`No matching skills found for: ${options.skill.join(", ")}`);
|
|
2195
|
+
p2.log.info("Available skills:");
|
|
2196
|
+
for (const s of skills) {
|
|
2197
|
+
p2.log.message(` - ${getSkillDisplayName(s)}`);
|
|
2198
|
+
}
|
|
2199
|
+
await cleanup(tempDir);
|
|
2200
|
+
process.exit(1);
|
|
2201
|
+
}
|
|
2202
|
+
p2.log.info(
|
|
2203
|
+
`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => chalk2.cyan(getSkillDisplayName(s))).join(", ")}`
|
|
2204
|
+
);
|
|
2205
|
+
} else if (skills.length === 1) {
|
|
2206
|
+
selectedSkills = skills;
|
|
2207
|
+
const firstSkill = skills[0];
|
|
2208
|
+
p2.log.info(`Skill: ${chalk2.cyan(getSkillDisplayName(firstSkill))}`);
|
|
2209
|
+
p2.log.message(chalk2.dim(firstSkill.description));
|
|
2210
|
+
} else if (options.yes) {
|
|
2211
|
+
selectedSkills = skills;
|
|
2212
|
+
p2.log.info(`Installing all ${skills.length} skills`);
|
|
2213
|
+
} else {
|
|
2214
|
+
const skillChoices = skills.map((s) => ({
|
|
2215
|
+
value: s,
|
|
2216
|
+
label: getSkillDisplayName(s),
|
|
2217
|
+
hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
|
|
2218
|
+
}));
|
|
2219
|
+
const selected = await p2.multiselect({
|
|
2220
|
+
message: "Select skills to install",
|
|
2221
|
+
options: skillChoices,
|
|
2222
|
+
required: true
|
|
2223
|
+
});
|
|
2224
|
+
if (p2.isCancel(selected)) {
|
|
2225
|
+
p2.cancel("Installation cancelled");
|
|
2226
|
+
await cleanup(tempDir);
|
|
2227
|
+
process.exit(0);
|
|
2228
|
+
}
|
|
2229
|
+
selectedSkills = selected;
|
|
2230
|
+
}
|
|
2231
|
+
let targetAgents;
|
|
2232
|
+
const validAgents = Object.keys(agents);
|
|
2233
|
+
if (options.agent && options.agent.length > 0) {
|
|
2234
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
2235
|
+
if (invalidAgents.length > 0) {
|
|
2236
|
+
p2.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
2237
|
+
p2.log.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
2238
|
+
await cleanup(tempDir);
|
|
2239
|
+
process.exit(1);
|
|
2240
|
+
}
|
|
2241
|
+
targetAgents = options.agent;
|
|
2242
|
+
} else if (options.all) {
|
|
2243
|
+
targetAgents = validAgents;
|
|
2244
|
+
p2.log.info(`Installing to all ${targetAgents.length} agents`);
|
|
2245
|
+
} else {
|
|
2246
|
+
spinner3.start("Detecting installed agents...");
|
|
2247
|
+
const installedAgents = await detectInstalledAgents();
|
|
2248
|
+
spinner3.stop(
|
|
2249
|
+
`Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? "s" : ""}`
|
|
2250
|
+
);
|
|
2251
|
+
if (installedAgents.length === 0) {
|
|
2252
|
+
if (options.yes) {
|
|
2253
|
+
targetAgents = validAgents;
|
|
2254
|
+
p2.log.info("Installing to all agents (none detected)");
|
|
2255
|
+
} else {
|
|
2256
|
+
p2.log.warn("No coding agents detected. You can still install skills.");
|
|
2257
|
+
const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
|
|
2258
|
+
value: key,
|
|
2259
|
+
label: config.displayName
|
|
2260
|
+
}));
|
|
2261
|
+
const selected = await p2.multiselect({
|
|
2262
|
+
message: "Select agents to install skills to",
|
|
2263
|
+
options: allAgentChoices,
|
|
2264
|
+
required: true,
|
|
2265
|
+
initialValues: Object.keys(agents)
|
|
2266
|
+
});
|
|
2267
|
+
if (p2.isCancel(selected)) {
|
|
2268
|
+
p2.cancel("Installation cancelled");
|
|
2269
|
+
await cleanup(tempDir);
|
|
2270
|
+
process.exit(0);
|
|
2271
|
+
}
|
|
2272
|
+
targetAgents = selected;
|
|
2273
|
+
}
|
|
2274
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
2275
|
+
targetAgents = installedAgents;
|
|
2276
|
+
if (installedAgents.length === 1) {
|
|
2277
|
+
const firstAgent = installedAgents[0];
|
|
2278
|
+
p2.log.info(`Installing to: ${chalk2.cyan(agents[firstAgent].displayName)}`);
|
|
2279
|
+
} else {
|
|
2280
|
+
p2.log.info(
|
|
2281
|
+
`Installing to: ${installedAgents.map((a) => chalk2.cyan(agents[a].displayName)).join(", ")}`
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
} else {
|
|
2285
|
+
const agentChoices = installedAgents.map((a) => ({
|
|
2286
|
+
value: a,
|
|
2287
|
+
label: agents[a].displayName,
|
|
2288
|
+
hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`
|
|
2289
|
+
}));
|
|
2290
|
+
const selected = await p2.multiselect({
|
|
2291
|
+
message: "Select agents to install skills to",
|
|
2292
|
+
options: agentChoices,
|
|
2293
|
+
required: true,
|
|
2294
|
+
initialValues: installedAgents
|
|
2295
|
+
});
|
|
2296
|
+
if (p2.isCancel(selected)) {
|
|
2297
|
+
p2.cancel("Installation cancelled");
|
|
2298
|
+
await cleanup(tempDir);
|
|
2299
|
+
process.exit(0);
|
|
2300
|
+
}
|
|
2301
|
+
targetAgents = selected;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
let installGlobally = options.global ?? false;
|
|
2305
|
+
if (options.global === void 0 && !options.yes) {
|
|
2306
|
+
const scope = await p2.select({
|
|
2307
|
+
message: "Installation scope",
|
|
2308
|
+
options: [
|
|
2309
|
+
{
|
|
2310
|
+
value: false,
|
|
2311
|
+
label: "Project",
|
|
2312
|
+
hint: "Install in current directory (committed with your project)"
|
|
2313
|
+
},
|
|
2314
|
+
{
|
|
2315
|
+
value: true,
|
|
2316
|
+
label: "Global",
|
|
2317
|
+
hint: "Install in home directory (available across all projects)"
|
|
2318
|
+
}
|
|
2319
|
+
]
|
|
2320
|
+
});
|
|
2321
|
+
if (p2.isCancel(scope)) {
|
|
2322
|
+
p2.cancel("Installation cancelled");
|
|
2323
|
+
await cleanup(tempDir);
|
|
2324
|
+
process.exit(0);
|
|
2325
|
+
}
|
|
2326
|
+
installGlobally = scope;
|
|
2327
|
+
}
|
|
2328
|
+
let installMode = "symlink";
|
|
2329
|
+
if (!options.yes) {
|
|
2330
|
+
const modeChoice = await p2.select({
|
|
2331
|
+
message: "Installation method",
|
|
2332
|
+
options: [
|
|
2333
|
+
{
|
|
2334
|
+
value: "symlink",
|
|
2335
|
+
label: "Symlink (Recommended)",
|
|
2336
|
+
hint: "Single source of truth, easy updates"
|
|
2337
|
+
},
|
|
2338
|
+
{ value: "copy", label: "Copy to all agents", hint: "Independent copies for each agent" }
|
|
2339
|
+
]
|
|
2340
|
+
});
|
|
2341
|
+
if (p2.isCancel(modeChoice)) {
|
|
2342
|
+
p2.cancel("Installation cancelled");
|
|
2343
|
+
await cleanup(tempDir);
|
|
2344
|
+
process.exit(0);
|
|
2345
|
+
}
|
|
2346
|
+
installMode = modeChoice;
|
|
2347
|
+
}
|
|
2348
|
+
const cwd = process.cwd();
|
|
2349
|
+
const summaryLines = [];
|
|
2350
|
+
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
2351
|
+
for (const skill of selectedSkills) {
|
|
2352
|
+
const agentStatus = /* @__PURE__ */ new Map();
|
|
2353
|
+
for (const agent of targetAgents) {
|
|
2354
|
+
agentStatus.set(
|
|
2355
|
+
agent,
|
|
2356
|
+
await isSkillInstalled(skill.name, agent, { global: installGlobally })
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
overwriteStatus.set(skill.name, agentStatus);
|
|
2360
|
+
}
|
|
2361
|
+
const agentNames = targetAgents.map((a) => agents[a].displayName);
|
|
2362
|
+
const hasOverwrites = Array.from(overwriteStatus.values()).some(
|
|
2363
|
+
(agentMap) => Array.from(agentMap.values()).some((v) => v)
|
|
2364
|
+
);
|
|
2365
|
+
for (const skill of selectedSkills) {
|
|
2366
|
+
if (summaryLines.length > 0) summaryLines.push("");
|
|
2367
|
+
if (installMode === "symlink") {
|
|
2368
|
+
const canonicalPath = getCanonicalPath(skill.name, { global: installGlobally });
|
|
2369
|
+
const shortCanonical = shortenPath(canonicalPath, cwd);
|
|
2370
|
+
summaryLines.push(`${chalk2.cyan(shortCanonical)}`);
|
|
2371
|
+
summaryLines.push(` ${chalk2.dim("symlink \u2192")} ${formatList(agentNames)}`);
|
|
2372
|
+
} else {
|
|
2373
|
+
summaryLines.push(`${chalk2.cyan(getSkillDisplayName(skill))}`);
|
|
2374
|
+
summaryLines.push(` ${chalk2.dim("copy \u2192")} ${formatList(agentNames)}`);
|
|
2375
|
+
}
|
|
2376
|
+
const skillOverwrites = overwriteStatus.get(skill.name);
|
|
2377
|
+
const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
|
|
2378
|
+
if (overwriteAgents.length > 0) {
|
|
2379
|
+
summaryLines.push(` ${chalk2.yellow("overwrites:")} ${formatList(overwriteAgents)}`);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
console.log();
|
|
2383
|
+
p2.note(summaryLines.join("\n"), "Installation Summary");
|
|
2384
|
+
if (!options.yes) {
|
|
2385
|
+
const confirmed = await p2.confirm({
|
|
2386
|
+
message: "Proceed with installation?"
|
|
2387
|
+
});
|
|
2388
|
+
if (p2.isCancel(confirmed) || !confirmed) {
|
|
2389
|
+
p2.cancel("Installation cancelled");
|
|
2390
|
+
await cleanup(tempDir);
|
|
2391
|
+
process.exit(0);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
spinner3.start("Installing skills...");
|
|
2395
|
+
const results = [];
|
|
2396
|
+
for (const skill of selectedSkills) {
|
|
2397
|
+
for (const agent of targetAgents) {
|
|
2398
|
+
const result = await installSkillForAgent(skill, agent, {
|
|
2399
|
+
global: installGlobally,
|
|
2400
|
+
mode: installMode
|
|
2401
|
+
});
|
|
2402
|
+
results.push({
|
|
2403
|
+
skill: getSkillDisplayName(skill),
|
|
2404
|
+
agent: agents[agent].displayName,
|
|
2405
|
+
...result
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
spinner3.stop("Installation complete");
|
|
2410
|
+
console.log();
|
|
2411
|
+
const successful = results.filter((r) => r.success);
|
|
2412
|
+
const failed = results.filter((r) => !r.success);
|
|
2413
|
+
const skillFiles = {};
|
|
2414
|
+
for (const skill of selectedSkills) {
|
|
2415
|
+
let relativePath;
|
|
2416
|
+
if (tempDir && skill.path === tempDir) {
|
|
2417
|
+
relativePath = "SKILL.md";
|
|
2418
|
+
} else if (tempDir && skill.path.startsWith(tempDir + "/")) {
|
|
2419
|
+
relativePath = skill.path.slice(tempDir.length + 1) + "/SKILL.md";
|
|
2420
|
+
} else {
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
skillFiles[skill.name] = relativePath;
|
|
2424
|
+
}
|
|
2425
|
+
const normalizedSource = getOwnerRepo(parsed);
|
|
2426
|
+
if (normalizedSource) {
|
|
2427
|
+
track({
|
|
2428
|
+
event: "install",
|
|
2429
|
+
source: normalizedSource,
|
|
2430
|
+
skills: selectedSkills.map((s) => s.name).join(","),
|
|
2431
|
+
agents: targetAgents.join(","),
|
|
2432
|
+
...installGlobally && { global: "1" },
|
|
2433
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
if (successful.length > 0 && installGlobally && normalizedSource) {
|
|
2437
|
+
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
2438
|
+
for (const skill of selectedSkills) {
|
|
2439
|
+
const skillDisplayName = getSkillDisplayName(skill);
|
|
2440
|
+
if (successfulSkillNames.has(skillDisplayName)) {
|
|
2441
|
+
try {
|
|
2442
|
+
let skillFolderHash = "";
|
|
2443
|
+
const skillPathValue = skillFiles[skill.name];
|
|
2444
|
+
if (parsed.type === "github" && skillPathValue) {
|
|
2445
|
+
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue);
|
|
2446
|
+
if (hash) skillFolderHash = hash;
|
|
2447
|
+
}
|
|
2448
|
+
await addSkillToLock(skill.name, {
|
|
2449
|
+
source: normalizedSource,
|
|
2450
|
+
sourceType: parsed.type,
|
|
2451
|
+
sourceUrl: parsed.url,
|
|
2452
|
+
skillPath: skillPathValue,
|
|
2453
|
+
skillFolderHash
|
|
2454
|
+
});
|
|
2455
|
+
} catch {
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
if (successful.length > 0) {
|
|
2461
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
2462
|
+
for (const r of successful) {
|
|
2463
|
+
const skillResults = bySkill.get(r.skill) || [];
|
|
2464
|
+
skillResults.push(r);
|
|
2465
|
+
bySkill.set(r.skill, skillResults);
|
|
2466
|
+
}
|
|
2467
|
+
const skillCount = bySkill.size;
|
|
2468
|
+
const agentCount = new Set(successful.map((r) => r.agent)).size;
|
|
2469
|
+
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
2470
|
+
const copiedAgents = symlinkFailures.map((r) => r.agent);
|
|
2471
|
+
const resultLines = [];
|
|
2472
|
+
for (const [skillName, skillResults] of bySkill) {
|
|
2473
|
+
const firstResult = skillResults[0];
|
|
2474
|
+
if (firstResult.mode === "copy") {
|
|
2475
|
+
resultLines.push(`${chalk2.green("\u2713")} ${skillName} ${chalk2.dim("(copied)")}`);
|
|
2476
|
+
for (const r of skillResults) {
|
|
2477
|
+
const shortPath = shortenPath(r.path, cwd);
|
|
2478
|
+
resultLines.push(` ${chalk2.dim("\u2192")} ${shortPath}`);
|
|
2479
|
+
}
|
|
2480
|
+
} else {
|
|
2481
|
+
if (firstResult.canonicalPath) {
|
|
2482
|
+
const shortPath = shortenPath(firstResult.canonicalPath, cwd);
|
|
2483
|
+
resultLines.push(`${chalk2.green("\u2713")} ${shortPath}`);
|
|
2484
|
+
}
|
|
2485
|
+
const symlinked = skillResults.filter((r) => !r.symlinkFailed).map((r) => r.agent);
|
|
2486
|
+
const copied = skillResults.filter((r) => r.symlinkFailed).map((r) => r.agent);
|
|
2487
|
+
if (symlinked.length > 0) {
|
|
2488
|
+
resultLines.push(` ${chalk2.dim("symlink \u2192")} ${formatList(symlinked)}`);
|
|
2489
|
+
}
|
|
2490
|
+
if (copied.length > 0) {
|
|
2491
|
+
resultLines.push(` ${chalk2.yellow("copied \u2192")} ${formatList(copied)}`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
const title = chalk2.green(
|
|
2496
|
+
`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""} to ${agentCount} agent${agentCount !== 1 ? "s" : ""}`
|
|
2497
|
+
);
|
|
2498
|
+
p2.note(resultLines.join("\n"), title);
|
|
2499
|
+
if (symlinkFailures.length > 0) {
|
|
2500
|
+
p2.log.warn(chalk2.yellow(`Symlinks failed for: ${formatList(copiedAgents)}`));
|
|
2501
|
+
p2.log.message(
|
|
2502
|
+
chalk2.dim(
|
|
2503
|
+
" Files were copied instead. On Windows, enable Developer Mode for symlink support."
|
|
2504
|
+
)
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (failed.length > 0) {
|
|
2509
|
+
console.log();
|
|
2510
|
+
p2.log.error(chalk2.red(`Failed to install ${failed.length}`));
|
|
2511
|
+
for (const r of failed) {
|
|
2512
|
+
p2.log.message(` ${chalk2.red("\u2717")} ${r.skill} \u2192 ${r.agent}: ${chalk2.dim(r.error)}`);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
console.log();
|
|
2516
|
+
p2.outro(chalk2.green("Done!"));
|
|
2517
|
+
} catch (error) {
|
|
2518
|
+
p2.log.error(error instanceof Error ? error.message : "Unknown error occurred");
|
|
2519
|
+
p2.outro(chalk2.red("Installation failed"));
|
|
2520
|
+
return;
|
|
2521
|
+
} finally {
|
|
2522
|
+
await cleanup(tempDir);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
async function cleanup(tempDir) {
|
|
2526
|
+
if (tempDir) {
|
|
2527
|
+
try {
|
|
2528
|
+
await cleanupTempDir(tempDir);
|
|
2529
|
+
} catch {
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
async function handleSearch(query, options) {
|
|
2534
|
+
const skills = await searchSkills(query);
|
|
2535
|
+
if (skills.length === 0) {
|
|
2536
|
+
p2.outro(chalk2.yellow(`No skills found for query: ${query}`));
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
const choices = skills.map((s) => ({
|
|
2540
|
+
value: s,
|
|
2541
|
+
label: s.name,
|
|
2542
|
+
hint: `${s.description} (${chalk2.dim(s.repoUrl)})`
|
|
2543
|
+
}));
|
|
2544
|
+
const selected = await p2.select({
|
|
2545
|
+
message: "Select a skill to install",
|
|
2546
|
+
options: choices
|
|
2547
|
+
});
|
|
2548
|
+
if (p2.isCancel(selected)) {
|
|
2549
|
+
p2.cancel("Search cancelled");
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
const selectedSkill = selected;
|
|
2553
|
+
p2.log.info(`You selected: ${chalk2.cyan(selectedSkill.name)}`);
|
|
2554
|
+
p2.log.message(chalk2.dim(`Repository: ${selectedSkill.repoUrl}`));
|
|
2555
|
+
const installOptions = { ...options, query: void 0 };
|
|
2556
|
+
await main(selectedSkill.repoUrl, installOptions);
|
|
2557
|
+
}
|