@rackerlabs/agent-skills-cli 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +603 -0
- package/ThirdPartyNoticeText.txt +125 -0
- package/bin/cli.mjs +14 -0
- package/dist/_chunks/libs/@clack/core.mjs +767 -0
- package/dist/_chunks/libs/@clack/prompts.mjs +334 -0
- package/dist/_chunks/libs/@kwsites/file-exists.mjs +562 -0
- package/dist/_chunks/libs/@kwsites/promise-deferred.mjs +37 -0
- package/dist/_chunks/libs/@simple-git/args-pathspec.mjs +12 -0
- package/dist/_chunks/libs/@simple-git/argv-parser.mjs +367 -0
- package/dist/_chunks/libs/esprima.mjs +5338 -0
- package/dist/_chunks/libs/extend-shallow.mjs +31 -0
- package/dist/_chunks/libs/gray-matter.mjs +2596 -0
- package/dist/_chunks/libs/simple-git.mjs +3560 -0
- package/dist/_chunks/libs/xdg-basedir.mjs +14 -0
- package/dist/_chunks/manifest-cli.mjs +73 -0
- package/dist/_chunks/manifest.mjs +49 -0
- package/dist/_chunks/registry-hashes.mjs +56 -0
- package/dist/_chunks/registry.mjs +47 -0
- package/dist/_chunks/rolldown-runtime.mjs +24 -0
- package/dist/_chunks/verify.mjs +170 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +4885 -0
- package/package.json +129 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,4885 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { r as __toESM } from "./_chunks/rolldown-runtime.mjs";
|
|
3
|
+
import { l as pD, u as require_picocolors } from "./_chunks/libs/@clack/core.mjs";
|
|
4
|
+
import { a as Y, c as ve, i as Se, l as xe, n as M, o as be, r as Me, s as fe, t as Ie, u as ye } from "./_chunks/libs/@clack/prompts.mjs";
|
|
5
|
+
import "./_chunks/libs/@kwsites/file-exists.mjs";
|
|
6
|
+
import "./_chunks/libs/@kwsites/promise-deferred.mjs";
|
|
7
|
+
import "./_chunks/libs/@simple-git/argv-parser.mjs";
|
|
8
|
+
import { t as esm_default } from "./_chunks/libs/simple-git.mjs";
|
|
9
|
+
import { t as require_gray_matter } from "./_chunks/libs/gray-matter.mjs";
|
|
10
|
+
import "./_chunks/libs/extend-shallow.mjs";
|
|
11
|
+
import "./_chunks/libs/esprima.mjs";
|
|
12
|
+
import { t as xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
|
|
13
|
+
import { exec, execSync, spawn, spawnSync } from "child_process";
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
15
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
16
|
+
import { homedir, platform, tmpdir } from "os";
|
|
17
|
+
import { URL as URL$1, fileURLToPath } from "url";
|
|
18
|
+
import * as readline from "readline";
|
|
19
|
+
import { Writable } from "stream";
|
|
20
|
+
import { promisify } from "util";
|
|
21
|
+
import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, writeFile } from "fs/promises";
|
|
22
|
+
import { createHash, randomBytes } from "crypto";
|
|
23
|
+
import { createServer } from "http";
|
|
24
|
+
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
25
|
+
function getOwnerRepo(parsed) {
|
|
26
|
+
if (parsed.type === "local") return null;
|
|
27
|
+
const sshMatch = parsed.url.match(/^git@[^:]+:(.+)$/);
|
|
28
|
+
if (sshMatch) {
|
|
29
|
+
let path = sshMatch[1];
|
|
30
|
+
path = path.replace(/\.git$/, "");
|
|
31
|
+
if (path.includes("/")) return path;
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!parsed.url.startsWith("http://") && !parsed.url.startsWith("https://")) return null;
|
|
35
|
+
try {
|
|
36
|
+
let path = new URL(parsed.url).pathname.slice(1);
|
|
37
|
+
path = path.replace(/\.git$/, "");
|
|
38
|
+
if (path.includes("/")) return path;
|
|
39
|
+
} catch {}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function parseOwnerRepo(ownerRepo) {
|
|
43
|
+
const match = ownerRepo.match(/^([^/]+)\/([^/]+)$/);
|
|
44
|
+
if (match) return {
|
|
45
|
+
owner: match[1],
|
|
46
|
+
repo: match[2]
|
|
47
|
+
};
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
async function isRepoPrivate(owner, repo) {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
|
|
53
|
+
if (!res.ok) return null;
|
|
54
|
+
return (await res.json()).private === true;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function sanitizeSubpath(subpath) {
|
|
60
|
+
const segments = subpath.replace(/\\/g, "/").split("/");
|
|
61
|
+
for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
|
|
62
|
+
return subpath;
|
|
63
|
+
}
|
|
64
|
+
function isLocalPath(input) {
|
|
65
|
+
return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
|
|
66
|
+
}
|
|
67
|
+
const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" };
|
|
68
|
+
function parseSource(input) {
|
|
69
|
+
const alias = SOURCE_ALIASES[input];
|
|
70
|
+
if (alias) input = alias;
|
|
71
|
+
const githubPrefixMatch = input.match(/^github:(.+)$/);
|
|
72
|
+
if (githubPrefixMatch) return parseSource(githubPrefixMatch[1]);
|
|
73
|
+
const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
|
|
74
|
+
if (gitlabPrefixMatch) return parseSource(`https://gitlab.com/${gitlabPrefixMatch[1]}`);
|
|
75
|
+
if (isLocalPath(input)) {
|
|
76
|
+
const resolvedPath = resolve(input);
|
|
77
|
+
return {
|
|
78
|
+
type: "local",
|
|
79
|
+
url: resolvedPath,
|
|
80
|
+
localPath: resolvedPath
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
|
|
84
|
+
if (githubTreeWithPathMatch) {
|
|
85
|
+
const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
|
|
86
|
+
return {
|
|
87
|
+
type: "github",
|
|
88
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
89
|
+
ref,
|
|
90
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
|
|
94
|
+
if (githubTreeMatch) {
|
|
95
|
+
const [, owner, repo, ref] = githubTreeMatch;
|
|
96
|
+
return {
|
|
97
|
+
type: "github",
|
|
98
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
99
|
+
ref
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
103
|
+
if (githubRepoMatch) {
|
|
104
|
+
const [, owner, repo] = githubRepoMatch;
|
|
105
|
+
return {
|
|
106
|
+
type: "github",
|
|
107
|
+
url: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const gitlabTreeWithPathMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)\/(.+)/);
|
|
111
|
+
if (gitlabTreeWithPathMatch) {
|
|
112
|
+
const [, protocol, hostname, repoPath, ref, subpath] = gitlabTreeWithPathMatch;
|
|
113
|
+
if (hostname !== "github.com" && repoPath) return {
|
|
114
|
+
type: "gitlab",
|
|
115
|
+
url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
|
|
116
|
+
ref,
|
|
117
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const gitlabTreeMatch = input.match(/^(https?):\/\/([^/]+)\/(.+?)\/-\/tree\/([^/]+)$/);
|
|
121
|
+
if (gitlabTreeMatch) {
|
|
122
|
+
const [, protocol, hostname, repoPath, ref] = gitlabTreeMatch;
|
|
123
|
+
if (hostname !== "github.com" && repoPath) return {
|
|
124
|
+
type: "gitlab",
|
|
125
|
+
url: `${protocol}://${hostname}/${repoPath.replace(/\.git$/, "")}.git`,
|
|
126
|
+
ref
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const gitlabRepoMatch = input.match(/gitlab\.com\/(.+?)(?:\.git)?\/?$/);
|
|
130
|
+
if (gitlabRepoMatch) {
|
|
131
|
+
const repoPath = gitlabRepoMatch[1];
|
|
132
|
+
if (repoPath.includes("/")) return {
|
|
133
|
+
type: "gitlab",
|
|
134
|
+
url: `https://gitlab.com/${repoPath}.git`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
|
|
138
|
+
if (atSkillMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
|
|
139
|
+
const [, owner, repo, skillFilter] = atSkillMatch;
|
|
140
|
+
return {
|
|
141
|
+
type: "github",
|
|
142
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
143
|
+
skillFilter
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const shorthandMatch = input.match(/^([^/]+)\/([^/#]+)(?:\/([^#]+))?(?:#(.+))?$/);
|
|
147
|
+
if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
|
|
148
|
+
const [, owner, repo, subpath, ref] = shorthandMatch;
|
|
149
|
+
return {
|
|
150
|
+
type: "github",
|
|
151
|
+
url: `https://github.com/${owner}/${repo}.git`,
|
|
152
|
+
subpath: subpath ? sanitizeSubpath(subpath) : subpath,
|
|
153
|
+
ref
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (isWellKnownUrl(input)) return {
|
|
157
|
+
type: "well-known",
|
|
158
|
+
url: input
|
|
159
|
+
};
|
|
160
|
+
const registryMatch = input.match(/^([a-zA-Z0-9-_]+)(?:@([a-zA-Z0-9-_\.]+))?$/);
|
|
161
|
+
if (registryMatch) return {
|
|
162
|
+
type: "registry",
|
|
163
|
+
url: input,
|
|
164
|
+
skillName: registryMatch[1],
|
|
165
|
+
ref: registryMatch[2]
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
type: "git",
|
|
169
|
+
url: input
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function isWellKnownUrl(input) {
|
|
173
|
+
if (!input.startsWith("http://") && !input.startsWith("https://")) return false;
|
|
174
|
+
try {
|
|
175
|
+
const parsed = new URL(input);
|
|
176
|
+
if ([
|
|
177
|
+
"github.com",
|
|
178
|
+
"gitlab.com",
|
|
179
|
+
"raw.githubusercontent.com"
|
|
180
|
+
].includes(parsed.hostname)) return false;
|
|
181
|
+
if (input.endsWith(".git")) return false;
|
|
182
|
+
return true;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const AGENTS_DIR$2 = ".agents";
|
|
188
|
+
const SKILLS_SUBDIR = "skills";
|
|
189
|
+
const DEFAULT_REGISTRY_URL = process.env.SKILLS_REGISTRY_URL || "https://skills.sh";
|
|
190
|
+
const CENTRAL_REGISTRY_REPO = "rackerlabs/central-skills-registry";
|
|
191
|
+
const SKILLS_REGISTRY_TOKEN_ENV = "SKILLS_REGISTRY_TOKEN";
|
|
192
|
+
const SKILLS_ISSUER_ENV = "SKILLS_ISSUER";
|
|
193
|
+
const SKILLS_CLIENT_ID_ENV = "SKILLS_CLIENT_ID";
|
|
194
|
+
const SKILLS_REGISTRY_URL_ENV = "SKILLS_REGISTRY_URL";
|
|
195
|
+
const SKILLS_OAUTH_AUDIENCE = "kestimator-api";
|
|
196
|
+
const SKILLS_OAUTH_SCOPES = "openid profile";
|
|
197
|
+
const DEFAULT_SKILLS_ISSUER = process.env[SKILLS_ISSUER_ENV] || "https://login.rackspace.com";
|
|
198
|
+
const DEFAULT_SKILLS_CLIENT_ID = process.env[SKILLS_CLIENT_ID_ENV] || "skills-cli";
|
|
199
|
+
const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
|
|
200
|
+
callback();
|
|
201
|
+
} });
|
|
202
|
+
const S_STEP_ACTIVE = import_picocolors.default.green("◆");
|
|
203
|
+
const S_STEP_CANCEL = import_picocolors.default.red("■");
|
|
204
|
+
const S_STEP_SUBMIT = import_picocolors.default.green("◇");
|
|
205
|
+
const S_RADIO_ACTIVE = import_picocolors.default.green("●");
|
|
206
|
+
const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
|
|
207
|
+
import_picocolors.default.green("✓");
|
|
208
|
+
const S_BULLET = import_picocolors.default.green("•");
|
|
209
|
+
const S_BAR = import_picocolors.default.dim("│");
|
|
210
|
+
const S_BAR_H = import_picocolors.default.dim("─");
|
|
211
|
+
const cancelSymbol = Symbol("cancel");
|
|
212
|
+
async function searchMultiselect(options) {
|
|
213
|
+
const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection } = options;
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
const rl = readline.createInterface({
|
|
216
|
+
input: process.stdin,
|
|
217
|
+
output: silentOutput,
|
|
218
|
+
terminal: false
|
|
219
|
+
});
|
|
220
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
221
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
222
|
+
let query = "";
|
|
223
|
+
let cursor = 0;
|
|
224
|
+
const selected = new Set(initialSelected);
|
|
225
|
+
let lastRenderHeight = 0;
|
|
226
|
+
const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : [];
|
|
227
|
+
const filter = (item, q) => {
|
|
228
|
+
if (!q) return true;
|
|
229
|
+
const lowerQ = q.toLowerCase();
|
|
230
|
+
return item.label.toLowerCase().includes(lowerQ) || String(item.value).toLowerCase().includes(lowerQ);
|
|
231
|
+
};
|
|
232
|
+
const getFiltered = () => {
|
|
233
|
+
return items.filter((item) => filter(item, query));
|
|
234
|
+
};
|
|
235
|
+
const clearRender = () => {
|
|
236
|
+
if (lastRenderHeight > 0) {
|
|
237
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
238
|
+
for (let i = 0; i < lastRenderHeight; i++) process.stdout.write("\x1B[2K\x1B[1B");
|
|
239
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const render = (state = "active") => {
|
|
243
|
+
clearRender();
|
|
244
|
+
const lines = [];
|
|
245
|
+
const filtered = getFiltered();
|
|
246
|
+
const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
|
|
247
|
+
lines.push(`${icon} ${import_picocolors.default.bold(message)}`);
|
|
248
|
+
if (state === "active") {
|
|
249
|
+
if (lockedSection && lockedSection.items.length > 0) {
|
|
250
|
+
lines.push(`${S_BAR}`);
|
|
251
|
+
const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
|
|
252
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
|
|
253
|
+
for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
|
|
254
|
+
lines.push(`${S_BAR}`);
|
|
255
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
|
|
256
|
+
}
|
|
257
|
+
const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
|
|
258
|
+
lines.push(searchLine);
|
|
259
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
|
|
260
|
+
lines.push(`${S_BAR}`);
|
|
261
|
+
const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
262
|
+
const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
|
|
263
|
+
const visibleItems = filtered.slice(visibleStart, visibleEnd);
|
|
264
|
+
if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`);
|
|
265
|
+
else {
|
|
266
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
267
|
+
const item = visibleItems[i];
|
|
268
|
+
const actualIndex = visibleStart + i;
|
|
269
|
+
const isSelected = selected.has(item.value);
|
|
270
|
+
const isCursor = actualIndex === cursor;
|
|
271
|
+
const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
|
|
272
|
+
const label = isCursor ? import_picocolors.default.underline(item.label) : item.label;
|
|
273
|
+
const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : "";
|
|
274
|
+
const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
|
|
275
|
+
lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
|
|
276
|
+
}
|
|
277
|
+
const hiddenBefore = visibleStart;
|
|
278
|
+
const hiddenAfter = filtered.length - visibleEnd;
|
|
279
|
+
if (hiddenBefore > 0 || hiddenAfter > 0) {
|
|
280
|
+
const parts = [];
|
|
281
|
+
if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
|
|
282
|
+
if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`);
|
|
283
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
lines.push(`${S_BAR}`);
|
|
287
|
+
const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
|
|
288
|
+
if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
|
|
289
|
+
else {
|
|
290
|
+
const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
|
|
291
|
+
lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
|
|
292
|
+
}
|
|
293
|
+
lines.push(`${import_picocolors.default.dim("└")}`);
|
|
294
|
+
} else if (state === "submit") {
|
|
295
|
+
const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
|
|
296
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`);
|
|
297
|
+
} else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`);
|
|
298
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
299
|
+
lastRenderHeight = lines.length;
|
|
300
|
+
};
|
|
301
|
+
const cleanup = () => {
|
|
302
|
+
process.stdin.removeListener("keypress", keypressHandler);
|
|
303
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
304
|
+
rl.close();
|
|
305
|
+
};
|
|
306
|
+
const submit = () => {
|
|
307
|
+
if (required && selected.size === 0 && lockedValues.length === 0) return;
|
|
308
|
+
render("submit");
|
|
309
|
+
cleanup();
|
|
310
|
+
resolve([...lockedValues, ...Array.from(selected)]);
|
|
311
|
+
};
|
|
312
|
+
const cancel = () => {
|
|
313
|
+
render("cancel");
|
|
314
|
+
cleanup();
|
|
315
|
+
resolve(cancelSymbol);
|
|
316
|
+
};
|
|
317
|
+
const keypressHandler = (_str, key) => {
|
|
318
|
+
if (!key) return;
|
|
319
|
+
const filtered = getFiltered();
|
|
320
|
+
if (key.name === "return") {
|
|
321
|
+
submit();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
325
|
+
cancel();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (key.name === "up") {
|
|
329
|
+
cursor = Math.max(0, cursor - 1);
|
|
330
|
+
render();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (key.name === "down") {
|
|
334
|
+
cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
335
|
+
render();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (key.name === "space") {
|
|
339
|
+
const item = filtered[cursor];
|
|
340
|
+
if (item) if (selected.has(item.value)) selected.delete(item.value);
|
|
341
|
+
else selected.add(item.value);
|
|
342
|
+
render();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (key.name === "backspace") {
|
|
346
|
+
query = query.slice(0, -1);
|
|
347
|
+
cursor = 0;
|
|
348
|
+
render();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
352
|
+
query += key.sequence;
|
|
353
|
+
cursor = 0;
|
|
354
|
+
render();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
process.stdin.on("keypress", keypressHandler);
|
|
359
|
+
render();
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const execAsync = promisify(exec);
|
|
363
|
+
const CLONE_TIMEOUT_MS = 6e4;
|
|
364
|
+
var GitCloneError = class extends Error {
|
|
365
|
+
url;
|
|
366
|
+
isTimeout;
|
|
367
|
+
isAuthError;
|
|
368
|
+
constructor(message, url, isTimeout = false, isAuthError = false) {
|
|
369
|
+
super(message);
|
|
370
|
+
this.name = "GitCloneError";
|
|
371
|
+
this.url = url;
|
|
372
|
+
this.isTimeout = isTimeout;
|
|
373
|
+
this.isAuthError = isAuthError;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
async function resolveRef(url, ref) {
|
|
377
|
+
if (ref !== "latest") return ref;
|
|
378
|
+
const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
379
|
+
if (!match) return void 0;
|
|
380
|
+
const [, owner, repo] = match;
|
|
381
|
+
try {
|
|
382
|
+
const { stdout } = await execAsync(`gh api repos/${owner}/${repo}/releases/latest --jq .tag_name`);
|
|
383
|
+
const tag = stdout.trim();
|
|
384
|
+
if (tag) return tag;
|
|
385
|
+
} catch {}
|
|
386
|
+
}
|
|
387
|
+
async function cloneRepo(url, ref) {
|
|
388
|
+
ref = await resolveRef(url, ref);
|
|
389
|
+
const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
|
|
390
|
+
if (url.includes("github.com")) try {
|
|
391
|
+
await execAsync(`gh repo clone ${url} "${tempDir}" -- ${ref ? `--depth 1 --branch ${ref}` : `--depth 1`}`);
|
|
392
|
+
return tempDir;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
await rm(tempDir, {
|
|
395
|
+
recursive: true,
|
|
396
|
+
force: true
|
|
397
|
+
}).catch(() => {});
|
|
398
|
+
throw new GitCloneError(`Failed to clone via gh CLI: ${error instanceof Error ? error.message : String(error)}\nEnsure 'gh auth status' succeeds and you have access to the repository.`, url, false, true);
|
|
399
|
+
}
|
|
400
|
+
const git = esm_default({
|
|
401
|
+
timeout: { block: CLONE_TIMEOUT_MS },
|
|
402
|
+
env: {
|
|
403
|
+
...process.env,
|
|
404
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
const cloneOptions = ref ? [
|
|
408
|
+
"--depth",
|
|
409
|
+
"1",
|
|
410
|
+
"--branch",
|
|
411
|
+
ref
|
|
412
|
+
] : ["--depth", "1"];
|
|
413
|
+
try {
|
|
414
|
+
await git.clone(url, tempDir, cloneOptions);
|
|
415
|
+
return tempDir;
|
|
416
|
+
} catch (error) {
|
|
417
|
+
await rm(tempDir, {
|
|
418
|
+
recursive: true,
|
|
419
|
+
force: true
|
|
420
|
+
}).catch(() => {});
|
|
421
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
422
|
+
const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
|
|
423
|
+
const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found");
|
|
424
|
+
if (isTimeout) throw new GitCloneError("Clone timed out after 60s. This often happens with private repos that require authentication.\n Ensure you have access and your SSH keys or credentials are configured:\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)", url, true, false);
|
|
425
|
+
if (isAuthError) throw new GitCloneError(`Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`, url, false, true);
|
|
426
|
+
throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function cleanupTempDir(dir) {
|
|
430
|
+
const normalizedDir = normalize(resolve(dir));
|
|
431
|
+
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
432
|
+
if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
|
|
433
|
+
await rm(dir, {
|
|
434
|
+
recursive: true,
|
|
435
|
+
force: true
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
var import_gray_matter = /* @__PURE__ */ __toESM(require_gray_matter(), 1);
|
|
439
|
+
function isContainedIn(targetPath, basePath) {
|
|
440
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
441
|
+
const normalizedTarget = normalize(resolve(targetPath));
|
|
442
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
443
|
+
}
|
|
444
|
+
function isValidRelativePath(path) {
|
|
445
|
+
return path.startsWith("./");
|
|
446
|
+
}
|
|
447
|
+
async function getPluginSkillPaths(basePath) {
|
|
448
|
+
const searchDirs = [];
|
|
449
|
+
const addPluginSkillPaths = (pluginBase, skills) => {
|
|
450
|
+
if (!isContainedIn(pluginBase, basePath)) return;
|
|
451
|
+
if (skills && skills.length > 0) for (const skillPath of skills) {
|
|
452
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
453
|
+
const skillDir = dirname(join(pluginBase, skillPath));
|
|
454
|
+
if (isContainedIn(skillDir, basePath)) searchDirs.push(skillDir);
|
|
455
|
+
}
|
|
456
|
+
searchDirs.push(join(pluginBase, "skills"));
|
|
457
|
+
};
|
|
458
|
+
try {
|
|
459
|
+
const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
|
|
460
|
+
const manifest = JSON.parse(content);
|
|
461
|
+
const pluginRoot = manifest.metadata?.pluginRoot;
|
|
462
|
+
if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
|
|
463
|
+
if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
|
|
464
|
+
if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
|
|
465
|
+
addPluginSkillPaths(join(basePath, pluginRoot ?? "", plugin.source ?? ""), plugin.skills);
|
|
466
|
+
}
|
|
467
|
+
} catch {}
|
|
468
|
+
try {
|
|
469
|
+
const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
|
|
470
|
+
addPluginSkillPaths(basePath, JSON.parse(content).skills);
|
|
471
|
+
} catch {}
|
|
472
|
+
return searchDirs;
|
|
473
|
+
}
|
|
474
|
+
async function getPluginGroupings(basePath) {
|
|
475
|
+
const groupings = /* @__PURE__ */ new Map();
|
|
476
|
+
try {
|
|
477
|
+
const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
|
|
478
|
+
const manifest = JSON.parse(content);
|
|
479
|
+
const pluginRoot = manifest.metadata?.pluginRoot;
|
|
480
|
+
if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
|
|
481
|
+
if (!plugin.name) continue;
|
|
482
|
+
if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
|
|
483
|
+
if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
|
|
484
|
+
const pluginBase = join(basePath, pluginRoot ?? "", plugin.source ?? "");
|
|
485
|
+
if (!isContainedIn(pluginBase, basePath)) continue;
|
|
486
|
+
if (plugin.skills && plugin.skills.length > 0) for (const skillPath of plugin.skills) {
|
|
487
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
488
|
+
const skillDir = join(pluginBase, skillPath);
|
|
489
|
+
if (isContainedIn(skillDir, basePath)) groupings.set(resolve(skillDir), plugin.name);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch {}
|
|
493
|
+
try {
|
|
494
|
+
const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
|
|
495
|
+
const manifest = JSON.parse(content);
|
|
496
|
+
if (manifest.name && manifest.skills && manifest.skills.length > 0) for (const skillPath of manifest.skills) {
|
|
497
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
498
|
+
const skillDir = join(basePath, skillPath);
|
|
499
|
+
if (isContainedIn(skillDir, basePath)) groupings.set(resolve(skillDir), manifest.name);
|
|
500
|
+
}
|
|
501
|
+
} catch {}
|
|
502
|
+
return groupings;
|
|
503
|
+
}
|
|
504
|
+
const SKIP_DIRS = [
|
|
505
|
+
"node_modules",
|
|
506
|
+
".git",
|
|
507
|
+
"dist",
|
|
508
|
+
"build",
|
|
509
|
+
"__pycache__"
|
|
510
|
+
];
|
|
511
|
+
function shouldInstallInternalSkills() {
|
|
512
|
+
const envValue = process.env.INSTALL_INTERNAL_SKILLS;
|
|
513
|
+
return envValue === "1" || envValue === "true";
|
|
514
|
+
}
|
|
515
|
+
async function hasSkillMd(dir) {
|
|
516
|
+
try {
|
|
517
|
+
return (await stat(join(dir, "SKILL.md"))).isFile();
|
|
518
|
+
} catch {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function parseSkillMd(skillMdPath, options) {
|
|
523
|
+
try {
|
|
524
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
525
|
+
const { data } = (0, import_gray_matter.default)(content);
|
|
526
|
+
if (!data.name || !data.description) return null;
|
|
527
|
+
if (typeof data.name !== "string" || typeof data.description !== "string") return null;
|
|
528
|
+
if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
|
|
529
|
+
return {
|
|
530
|
+
name: data.name,
|
|
531
|
+
description: data.description,
|
|
532
|
+
path: dirname(skillMdPath),
|
|
533
|
+
rawContent: content,
|
|
534
|
+
metadata: data.metadata
|
|
535
|
+
};
|
|
536
|
+
} catch {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
|
|
541
|
+
if (depth > maxDepth) return [];
|
|
542
|
+
try {
|
|
543
|
+
const [hasSkill, entries] = await Promise.all([hasSkillMd(dir), readdir(dir, { withFileTypes: true }).catch(() => [])]);
|
|
544
|
+
const currentDir = hasSkill ? [dir] : [];
|
|
545
|
+
const subDirResults = await Promise.all(entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.includes(entry.name)).map((entry) => findSkillDirs(join(dir, entry.name), depth + 1, maxDepth)));
|
|
546
|
+
return [...currentDir, ...subDirResults.flat()];
|
|
547
|
+
} catch {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function isSubpathSafe(basePath, subpath) {
|
|
552
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
553
|
+
const normalizedTarget = normalize(resolve(join(basePath, subpath)));
|
|
554
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
555
|
+
}
|
|
556
|
+
async function discoverSkills(basePath, subpath, options) {
|
|
557
|
+
const skills = [];
|
|
558
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
559
|
+
if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error(`Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`);
|
|
560
|
+
const searchPath = subpath ? join(basePath, subpath) : basePath;
|
|
561
|
+
const pluginGroupings = await getPluginGroupings(searchPath);
|
|
562
|
+
const enhanceSkill = (skill) => {
|
|
563
|
+
const resolvedPath = resolve(skill.path);
|
|
564
|
+
if (pluginGroupings.has(resolvedPath)) skill.pluginName = pluginGroupings.get(resolvedPath);
|
|
565
|
+
return skill;
|
|
566
|
+
};
|
|
567
|
+
if (await hasSkillMd(searchPath)) {
|
|
568
|
+
let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options);
|
|
569
|
+
if (skill) {
|
|
570
|
+
skill = enhanceSkill(skill);
|
|
571
|
+
skills.push(skill);
|
|
572
|
+
seenNames.add(skill.name);
|
|
573
|
+
if (!options?.fullDepth) return skills;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const prioritySearchDirs = [
|
|
577
|
+
searchPath,
|
|
578
|
+
join(searchPath, "skills"),
|
|
579
|
+
join(searchPath, "skills/.curated"),
|
|
580
|
+
join(searchPath, "skills/.experimental"),
|
|
581
|
+
join(searchPath, "skills/.system"),
|
|
582
|
+
join(searchPath, ".agents/skills"),
|
|
583
|
+
join(searchPath, ".claude/skills"),
|
|
584
|
+
join(searchPath, ".cline/skills"),
|
|
585
|
+
join(searchPath, ".codebuddy/skills"),
|
|
586
|
+
join(searchPath, ".codex/skills"),
|
|
587
|
+
join(searchPath, ".commandcode/skills"),
|
|
588
|
+
join(searchPath, ".continue/skills"),
|
|
589
|
+
join(searchPath, ".github/skills"),
|
|
590
|
+
join(searchPath, ".goose/skills"),
|
|
591
|
+
join(searchPath, ".iflow/skills"),
|
|
592
|
+
join(searchPath, ".junie/skills"),
|
|
593
|
+
join(searchPath, ".kilocode/skills"),
|
|
594
|
+
join(searchPath, ".kiro/skills"),
|
|
595
|
+
join(searchPath, ".mux/skills"),
|
|
596
|
+
join(searchPath, ".neovate/skills"),
|
|
597
|
+
join(searchPath, ".opencode/skills"),
|
|
598
|
+
join(searchPath, ".openhands/skills"),
|
|
599
|
+
join(searchPath, ".pi/skills"),
|
|
600
|
+
join(searchPath, ".qoder/skills"),
|
|
601
|
+
join(searchPath, ".roo/skills"),
|
|
602
|
+
join(searchPath, ".trae/skills"),
|
|
603
|
+
join(searchPath, ".windsurf/skills"),
|
|
604
|
+
join(searchPath, ".zencoder/skills")
|
|
605
|
+
];
|
|
606
|
+
prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
|
|
607
|
+
for (const dir of prioritySearchDirs) try {
|
|
608
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
609
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
610
|
+
const skillDir = join(dir, entry.name);
|
|
611
|
+
if (await hasSkillMd(skillDir)) {
|
|
612
|
+
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
613
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
614
|
+
skill = enhanceSkill(skill);
|
|
615
|
+
skills.push(skill);
|
|
616
|
+
seenNames.add(skill.name);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} catch {}
|
|
621
|
+
if (skills.length === 0 || options?.fullDepth) {
|
|
622
|
+
const allSkillDirs = await findSkillDirs(searchPath);
|
|
623
|
+
for (const skillDir of allSkillDirs) {
|
|
624
|
+
let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
|
|
625
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
626
|
+
skill = enhanceSkill(skill);
|
|
627
|
+
skills.push(skill);
|
|
628
|
+
seenNames.add(skill.name);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return skills;
|
|
633
|
+
}
|
|
634
|
+
function getSkillDisplayName(skill) {
|
|
635
|
+
return skill.name || basename(skill.path);
|
|
636
|
+
}
|
|
637
|
+
function filterSkills(skills, inputNames) {
|
|
638
|
+
const normalizedInputs = inputNames.map((n) => n.toLowerCase());
|
|
639
|
+
return skills.filter((skill) => {
|
|
640
|
+
const name = skill.name.toLowerCase();
|
|
641
|
+
const displayName = getSkillDisplayName(skill).toLowerCase();
|
|
642
|
+
return normalizedInputs.some((input) => input === name || input === displayName);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const home = homedir();
|
|
646
|
+
const configHome = xdgConfig ?? join(home, ".config");
|
|
647
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
648
|
+
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
649
|
+
function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
650
|
+
if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
|
|
651
|
+
if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
|
|
652
|
+
if (pathExists(join(homeDir, ".moltbot"))) return join(homeDir, ".moltbot/skills");
|
|
653
|
+
return join(homeDir, ".openclaw/skills");
|
|
654
|
+
}
|
|
655
|
+
const agents = {
|
|
656
|
+
amp: {
|
|
657
|
+
name: "amp",
|
|
658
|
+
displayName: "Amp",
|
|
659
|
+
skillsDir: ".agents/skills",
|
|
660
|
+
globalSkillsDir: join(configHome, "agents/skills"),
|
|
661
|
+
detectInstalled: async () => {
|
|
662
|
+
return existsSync(join(configHome, "amp"));
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
antigravity: {
|
|
666
|
+
name: "antigravity",
|
|
667
|
+
displayName: "Antigravity",
|
|
668
|
+
skillsDir: ".agents/skills",
|
|
669
|
+
globalSkillsDir: join(home, ".gemini/antigravity/skills"),
|
|
670
|
+
detectInstalled: async () => {
|
|
671
|
+
return existsSync(join(home, ".gemini/antigravity"));
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
augment: {
|
|
675
|
+
name: "augment",
|
|
676
|
+
displayName: "Augment",
|
|
677
|
+
skillsDir: ".augment/skills",
|
|
678
|
+
globalSkillsDir: join(home, ".augment/skills"),
|
|
679
|
+
detectInstalled: async () => {
|
|
680
|
+
return existsSync(join(home, ".augment"));
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
"claude-code": {
|
|
684
|
+
name: "claude-code",
|
|
685
|
+
displayName: "Claude Code",
|
|
686
|
+
skillsDir: ".claude/skills",
|
|
687
|
+
globalSkillsDir: join(claudeHome, "skills"),
|
|
688
|
+
detectInstalled: async () => {
|
|
689
|
+
return existsSync(claudeHome);
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
openclaw: {
|
|
693
|
+
name: "openclaw",
|
|
694
|
+
displayName: "OpenClaw",
|
|
695
|
+
skillsDir: "skills",
|
|
696
|
+
globalSkillsDir: getOpenClawGlobalSkillsDir(),
|
|
697
|
+
detectInstalled: async () => {
|
|
698
|
+
return existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot"));
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
cline: {
|
|
702
|
+
name: "cline",
|
|
703
|
+
displayName: "Cline",
|
|
704
|
+
skillsDir: ".agents/skills",
|
|
705
|
+
globalSkillsDir: join(home, ".agents", "skills"),
|
|
706
|
+
detectInstalled: async () => {
|
|
707
|
+
return existsSync(join(home, ".cline"));
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
codebuddy: {
|
|
711
|
+
name: "codebuddy",
|
|
712
|
+
displayName: "CodeBuddy",
|
|
713
|
+
skillsDir: ".codebuddy/skills",
|
|
714
|
+
globalSkillsDir: join(home, ".codebuddy/skills"),
|
|
715
|
+
detectInstalled: async () => {
|
|
716
|
+
return existsSync(join(process.cwd(), ".codebuddy")) || existsSync(join(home, ".codebuddy"));
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
codex: {
|
|
720
|
+
name: "codex",
|
|
721
|
+
displayName: "Codex",
|
|
722
|
+
skillsDir: ".agents/skills",
|
|
723
|
+
globalSkillsDir: join(codexHome, "skills"),
|
|
724
|
+
detectInstalled: async () => {
|
|
725
|
+
return existsSync(codexHome) || existsSync("/etc/codex");
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
"command-code": {
|
|
729
|
+
name: "command-code",
|
|
730
|
+
displayName: "Command Code",
|
|
731
|
+
skillsDir: ".commandcode/skills",
|
|
732
|
+
globalSkillsDir: join(home, ".commandcode/skills"),
|
|
733
|
+
detectInstalled: async () => {
|
|
734
|
+
return existsSync(join(home, ".commandcode"));
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
continue: {
|
|
738
|
+
name: "continue",
|
|
739
|
+
displayName: "Continue",
|
|
740
|
+
skillsDir: ".continue/skills",
|
|
741
|
+
globalSkillsDir: join(home, ".continue/skills"),
|
|
742
|
+
detectInstalled: async () => {
|
|
743
|
+
return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"));
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
cortex: {
|
|
747
|
+
name: "cortex",
|
|
748
|
+
displayName: "Cortex Code",
|
|
749
|
+
skillsDir: ".cortex/skills",
|
|
750
|
+
globalSkillsDir: join(home, ".snowflake/cortex/skills"),
|
|
751
|
+
detectInstalled: async () => {
|
|
752
|
+
return existsSync(join(home, ".snowflake/cortex"));
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
crush: {
|
|
756
|
+
name: "crush",
|
|
757
|
+
displayName: "Crush",
|
|
758
|
+
skillsDir: ".crush/skills",
|
|
759
|
+
globalSkillsDir: join(home, ".config/crush/skills"),
|
|
760
|
+
detectInstalled: async () => {
|
|
761
|
+
return existsSync(join(home, ".config/crush"));
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
cursor: {
|
|
765
|
+
name: "cursor",
|
|
766
|
+
displayName: "Cursor",
|
|
767
|
+
skillsDir: ".agents/skills",
|
|
768
|
+
globalSkillsDir: join(home, ".cursor/skills"),
|
|
769
|
+
detectInstalled: async () => {
|
|
770
|
+
return existsSync(join(home, ".cursor"));
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
deepagents: {
|
|
774
|
+
name: "deepagents",
|
|
775
|
+
displayName: "Deep Agents",
|
|
776
|
+
skillsDir: ".agents/skills",
|
|
777
|
+
globalSkillsDir: join(home, ".deepagents/agent/skills"),
|
|
778
|
+
detectInstalled: async () => {
|
|
779
|
+
return existsSync(join(home, ".deepagents"));
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
droid: {
|
|
783
|
+
name: "droid",
|
|
784
|
+
displayName: "Droid",
|
|
785
|
+
skillsDir: ".factory/skills",
|
|
786
|
+
globalSkillsDir: join(home, ".factory/skills"),
|
|
787
|
+
detectInstalled: async () => {
|
|
788
|
+
return existsSync(join(home, ".factory"));
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
firebender: {
|
|
792
|
+
name: "firebender",
|
|
793
|
+
displayName: "Firebender",
|
|
794
|
+
skillsDir: ".agents/skills",
|
|
795
|
+
globalSkillsDir: join(home, ".firebender/skills"),
|
|
796
|
+
detectInstalled: async () => {
|
|
797
|
+
return existsSync(join(home, ".firebender"));
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
"gemini-cli": {
|
|
801
|
+
name: "gemini-cli",
|
|
802
|
+
displayName: "Gemini CLI",
|
|
803
|
+
skillsDir: ".agents/skills",
|
|
804
|
+
globalSkillsDir: join(home, ".gemini/skills"),
|
|
805
|
+
detectInstalled: async () => {
|
|
806
|
+
return existsSync(join(home, ".gemini"));
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
"github-copilot": {
|
|
810
|
+
name: "github-copilot",
|
|
811
|
+
displayName: "GitHub Copilot",
|
|
812
|
+
skillsDir: ".agents/skills",
|
|
813
|
+
globalSkillsDir: join(home, ".copilot/skills"),
|
|
814
|
+
detectInstalled: async () => {
|
|
815
|
+
return existsSync(join(home, ".copilot"));
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
goose: {
|
|
819
|
+
name: "goose",
|
|
820
|
+
displayName: "Goose",
|
|
821
|
+
skillsDir: ".goose/skills",
|
|
822
|
+
globalSkillsDir: join(configHome, "goose/skills"),
|
|
823
|
+
detectInstalled: async () => {
|
|
824
|
+
return existsSync(join(configHome, "goose"));
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
junie: {
|
|
828
|
+
name: "junie",
|
|
829
|
+
displayName: "Junie",
|
|
830
|
+
skillsDir: ".junie/skills",
|
|
831
|
+
globalSkillsDir: join(home, ".junie/skills"),
|
|
832
|
+
detectInstalled: async () => {
|
|
833
|
+
return existsSync(join(home, ".junie"));
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
"iflow-cli": {
|
|
837
|
+
name: "iflow-cli",
|
|
838
|
+
displayName: "iFlow CLI",
|
|
839
|
+
skillsDir: ".iflow/skills",
|
|
840
|
+
globalSkillsDir: join(home, ".iflow/skills"),
|
|
841
|
+
detectInstalled: async () => {
|
|
842
|
+
return existsSync(join(home, ".iflow"));
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
kilo: {
|
|
846
|
+
name: "kilo",
|
|
847
|
+
displayName: "Kilo Code",
|
|
848
|
+
skillsDir: ".kilocode/skills",
|
|
849
|
+
globalSkillsDir: join(home, ".kilocode/skills"),
|
|
850
|
+
detectInstalled: async () => {
|
|
851
|
+
return existsSync(join(home, ".kilocode"));
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
"kimi-cli": {
|
|
855
|
+
name: "kimi-cli",
|
|
856
|
+
displayName: "Kimi Code CLI",
|
|
857
|
+
skillsDir: ".agents/skills",
|
|
858
|
+
globalSkillsDir: join(home, ".config/agents/skills"),
|
|
859
|
+
detectInstalled: async () => {
|
|
860
|
+
return existsSync(join(home, ".kimi"));
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
"kiro-cli": {
|
|
864
|
+
name: "kiro-cli",
|
|
865
|
+
displayName: "Kiro CLI",
|
|
866
|
+
skillsDir: ".kiro/skills",
|
|
867
|
+
globalSkillsDir: join(home, ".kiro/skills"),
|
|
868
|
+
detectInstalled: async () => {
|
|
869
|
+
return existsSync(join(home, ".kiro"));
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
kode: {
|
|
873
|
+
name: "kode",
|
|
874
|
+
displayName: "Kode",
|
|
875
|
+
skillsDir: ".kode/skills",
|
|
876
|
+
globalSkillsDir: join(home, ".kode/skills"),
|
|
877
|
+
detectInstalled: async () => {
|
|
878
|
+
return existsSync(join(home, ".kode"));
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
mcpjam: {
|
|
882
|
+
name: "mcpjam",
|
|
883
|
+
displayName: "MCPJam",
|
|
884
|
+
skillsDir: ".mcpjam/skills",
|
|
885
|
+
globalSkillsDir: join(home, ".mcpjam/skills"),
|
|
886
|
+
detectInstalled: async () => {
|
|
887
|
+
return existsSync(join(home, ".mcpjam"));
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
"mistral-vibe": {
|
|
891
|
+
name: "mistral-vibe",
|
|
892
|
+
displayName: "Mistral Vibe",
|
|
893
|
+
skillsDir: ".vibe/skills",
|
|
894
|
+
globalSkillsDir: join(home, ".vibe/skills"),
|
|
895
|
+
detectInstalled: async () => {
|
|
896
|
+
return existsSync(join(home, ".vibe"));
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
mux: {
|
|
900
|
+
name: "mux",
|
|
901
|
+
displayName: "Mux",
|
|
902
|
+
skillsDir: ".mux/skills",
|
|
903
|
+
globalSkillsDir: join(home, ".mux/skills"),
|
|
904
|
+
detectInstalled: async () => {
|
|
905
|
+
return existsSync(join(home, ".mux"));
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
opencode: {
|
|
909
|
+
name: "opencode",
|
|
910
|
+
displayName: "OpenCode",
|
|
911
|
+
skillsDir: ".agents/skills",
|
|
912
|
+
globalSkillsDir: join(configHome, "opencode/skills"),
|
|
913
|
+
detectInstalled: async () => {
|
|
914
|
+
return existsSync(join(configHome, "opencode"));
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
openhands: {
|
|
918
|
+
name: "openhands",
|
|
919
|
+
displayName: "OpenHands",
|
|
920
|
+
skillsDir: ".openhands/skills",
|
|
921
|
+
globalSkillsDir: join(home, ".openhands/skills"),
|
|
922
|
+
detectInstalled: async () => {
|
|
923
|
+
return existsSync(join(home, ".openhands"));
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
pi: {
|
|
927
|
+
name: "pi",
|
|
928
|
+
displayName: "Pi",
|
|
929
|
+
skillsDir: ".pi/skills",
|
|
930
|
+
globalSkillsDir: join(home, ".pi/agent/skills"),
|
|
931
|
+
detectInstalled: async () => {
|
|
932
|
+
return existsSync(join(home, ".pi/agent"));
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
qoder: {
|
|
936
|
+
name: "qoder",
|
|
937
|
+
displayName: "Qoder",
|
|
938
|
+
skillsDir: ".qoder/skills",
|
|
939
|
+
globalSkillsDir: join(home, ".qoder/skills"),
|
|
940
|
+
detectInstalled: async () => {
|
|
941
|
+
return existsSync(join(home, ".qoder"));
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
"qwen-code": {
|
|
945
|
+
name: "qwen-code",
|
|
946
|
+
displayName: "Qwen Code",
|
|
947
|
+
skillsDir: ".qwen/skills",
|
|
948
|
+
globalSkillsDir: join(home, ".qwen/skills"),
|
|
949
|
+
detectInstalled: async () => {
|
|
950
|
+
return existsSync(join(home, ".qwen"));
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
replit: {
|
|
954
|
+
name: "replit",
|
|
955
|
+
displayName: "Replit",
|
|
956
|
+
skillsDir: ".agents/skills",
|
|
957
|
+
globalSkillsDir: join(configHome, "agents/skills"),
|
|
958
|
+
showInUniversalList: false,
|
|
959
|
+
detectInstalled: async () => {
|
|
960
|
+
return existsSync(join(process.cwd(), ".replit"));
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
roo: {
|
|
964
|
+
name: "roo",
|
|
965
|
+
displayName: "Roo Code",
|
|
966
|
+
skillsDir: ".roo/skills",
|
|
967
|
+
globalSkillsDir: join(home, ".roo/skills"),
|
|
968
|
+
detectInstalled: async () => {
|
|
969
|
+
return existsSync(join(home, ".roo"));
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
trae: {
|
|
973
|
+
name: "trae",
|
|
974
|
+
displayName: "Trae",
|
|
975
|
+
skillsDir: ".trae/skills",
|
|
976
|
+
globalSkillsDir: join(home, ".trae/skills"),
|
|
977
|
+
detectInstalled: async () => {
|
|
978
|
+
return existsSync(join(home, ".trae"));
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
"trae-cn": {
|
|
982
|
+
name: "trae-cn",
|
|
983
|
+
displayName: "Trae CN",
|
|
984
|
+
skillsDir: ".trae/skills",
|
|
985
|
+
globalSkillsDir: join(home, ".trae-cn/skills"),
|
|
986
|
+
detectInstalled: async () => {
|
|
987
|
+
return existsSync(join(home, ".trae-cn"));
|
|
988
|
+
}
|
|
989
|
+
},
|
|
990
|
+
warp: {
|
|
991
|
+
name: "warp",
|
|
992
|
+
displayName: "Warp",
|
|
993
|
+
skillsDir: ".agents/skills",
|
|
994
|
+
globalSkillsDir: join(home, ".agents/skills"),
|
|
995
|
+
detectInstalled: async () => {
|
|
996
|
+
return existsSync(join(home, ".warp"));
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
windsurf: {
|
|
1000
|
+
name: "windsurf",
|
|
1001
|
+
displayName: "Windsurf",
|
|
1002
|
+
skillsDir: ".windsurf/skills",
|
|
1003
|
+
globalSkillsDir: join(home, ".codeium/windsurf/skills"),
|
|
1004
|
+
detectInstalled: async () => {
|
|
1005
|
+
return existsSync(join(home, ".codeium/windsurf"));
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
zencoder: {
|
|
1009
|
+
name: "zencoder",
|
|
1010
|
+
displayName: "Zencoder",
|
|
1011
|
+
skillsDir: ".zencoder/skills",
|
|
1012
|
+
globalSkillsDir: join(home, ".zencoder/skills"),
|
|
1013
|
+
detectInstalled: async () => {
|
|
1014
|
+
return existsSync(join(home, ".zencoder"));
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
neovate: {
|
|
1018
|
+
name: "neovate",
|
|
1019
|
+
displayName: "Neovate",
|
|
1020
|
+
skillsDir: ".neovate/skills",
|
|
1021
|
+
globalSkillsDir: join(home, ".neovate/skills"),
|
|
1022
|
+
detectInstalled: async () => {
|
|
1023
|
+
return existsSync(join(home, ".neovate"));
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
pochi: {
|
|
1027
|
+
name: "pochi",
|
|
1028
|
+
displayName: "Pochi",
|
|
1029
|
+
skillsDir: ".pochi/skills",
|
|
1030
|
+
globalSkillsDir: join(home, ".pochi/skills"),
|
|
1031
|
+
detectInstalled: async () => {
|
|
1032
|
+
return existsSync(join(home, ".pochi"));
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
adal: {
|
|
1036
|
+
name: "adal",
|
|
1037
|
+
displayName: "AdaL",
|
|
1038
|
+
skillsDir: ".adal/skills",
|
|
1039
|
+
globalSkillsDir: join(home, ".adal/skills"),
|
|
1040
|
+
detectInstalled: async () => {
|
|
1041
|
+
return existsSync(join(home, ".adal"));
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
universal: {
|
|
1045
|
+
name: "universal",
|
|
1046
|
+
displayName: "Universal",
|
|
1047
|
+
skillsDir: ".agents/skills",
|
|
1048
|
+
globalSkillsDir: join(configHome, "agents/skills"),
|
|
1049
|
+
showInUniversalList: false,
|
|
1050
|
+
detectInstalled: async () => false
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
async function detectInstalledAgents() {
|
|
1054
|
+
return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({
|
|
1055
|
+
type,
|
|
1056
|
+
installed: await config.detectInstalled()
|
|
1057
|
+
})))).filter((r) => r.installed).map((r) => r.type);
|
|
1058
|
+
}
|
|
1059
|
+
function getUniversalAgents() {
|
|
1060
|
+
return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
|
|
1061
|
+
}
|
|
1062
|
+
function getNonUniversalAgents() {
|
|
1063
|
+
return Object.entries(agents).filter(([_, config]) => config.skillsDir !== ".agents/skills").map(([type]) => type);
|
|
1064
|
+
}
|
|
1065
|
+
function isUniversalAgent(type) {
|
|
1066
|
+
return agents[type].skillsDir === ".agents/skills";
|
|
1067
|
+
}
|
|
1068
|
+
function sanitizeName(name) {
|
|
1069
|
+
return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
|
|
1070
|
+
}
|
|
1071
|
+
function isPathSafe(basePath, targetPath) {
|
|
1072
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
1073
|
+
const normalizedTarget = normalize(resolve(targetPath));
|
|
1074
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
1075
|
+
}
|
|
1076
|
+
function getCanonicalSkillsDir(global, cwd) {
|
|
1077
|
+
return join(global ? homedir() : cwd || process.cwd(), AGENTS_DIR$2, SKILLS_SUBDIR);
|
|
1078
|
+
}
|
|
1079
|
+
function getAgentBaseDir(agentType, global, cwd) {
|
|
1080
|
+
if (isUniversalAgent(agentType)) return getCanonicalSkillsDir(global, cwd);
|
|
1081
|
+
const agent = agents[agentType];
|
|
1082
|
+
const baseDir = global ? homedir() : cwd || process.cwd();
|
|
1083
|
+
if (global) {
|
|
1084
|
+
if (agent.globalSkillsDir === void 0) return join(baseDir, agent.skillsDir);
|
|
1085
|
+
return agent.globalSkillsDir;
|
|
1086
|
+
}
|
|
1087
|
+
return join(baseDir, agent.skillsDir);
|
|
1088
|
+
}
|
|
1089
|
+
function resolveSymlinkTarget(linkPath, linkTarget) {
|
|
1090
|
+
return resolve(dirname(linkPath), linkTarget);
|
|
1091
|
+
}
|
|
1092
|
+
async function cleanAndCreateDirectory(path) {
|
|
1093
|
+
try {
|
|
1094
|
+
await rm(path, {
|
|
1095
|
+
recursive: true,
|
|
1096
|
+
force: true
|
|
1097
|
+
});
|
|
1098
|
+
} catch {}
|
|
1099
|
+
await mkdir(path, { recursive: true });
|
|
1100
|
+
}
|
|
1101
|
+
async function resolveParentSymlinks(path) {
|
|
1102
|
+
const resolved = resolve(path);
|
|
1103
|
+
const dir = dirname(resolved);
|
|
1104
|
+
const base = basename(resolved);
|
|
1105
|
+
try {
|
|
1106
|
+
return join(await realpath(dir), base);
|
|
1107
|
+
} catch {
|
|
1108
|
+
return resolved;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async function createSymlink(target, linkPath) {
|
|
1112
|
+
try {
|
|
1113
|
+
const resolvedTarget = resolve(target);
|
|
1114
|
+
const resolvedLinkPath = resolve(linkPath);
|
|
1115
|
+
const [realTarget, realLinkPath] = await Promise.all([realpath(resolvedTarget).catch(() => resolvedTarget), realpath(resolvedLinkPath).catch(() => resolvedLinkPath)]);
|
|
1116
|
+
if (realTarget === realLinkPath) return true;
|
|
1117
|
+
if (await resolveParentSymlinks(target) === await resolveParentSymlinks(linkPath)) return true;
|
|
1118
|
+
try {
|
|
1119
|
+
if ((await lstat(linkPath)).isSymbolicLink()) {
|
|
1120
|
+
if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
|
|
1121
|
+
await rm(linkPath);
|
|
1122
|
+
} else await rm(linkPath, { recursive: true });
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
|
|
1125
|
+
await rm(linkPath, { force: true });
|
|
1126
|
+
} catch {}
|
|
1127
|
+
}
|
|
1128
|
+
const linkDir = dirname(linkPath);
|
|
1129
|
+
await mkdir(linkDir, { recursive: true });
|
|
1130
|
+
await symlink(relative(await resolveParentSymlinks(linkDir), target), linkPath, platform() === "win32" ? "junction" : void 0);
|
|
1131
|
+
return true;
|
|
1132
|
+
} catch {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
async function installSkillForAgent(skill, agentType, options = {}) {
|
|
1137
|
+
const agent = agents[agentType];
|
|
1138
|
+
const isGlobal = options.global ?? false;
|
|
1139
|
+
const cwd = options.cwd || process.cwd();
|
|
1140
|
+
if (isGlobal && agent.globalSkillsDir === void 0) return {
|
|
1141
|
+
success: false,
|
|
1142
|
+
path: "",
|
|
1143
|
+
mode: options.mode ?? "symlink",
|
|
1144
|
+
error: `${agent.displayName} does not support global skill installation`
|
|
1145
|
+
};
|
|
1146
|
+
const skillName = sanitizeName(skill.name || basename(skill.path));
|
|
1147
|
+
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
1148
|
+
const canonicalDir = join(canonicalBase, skillName);
|
|
1149
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
1150
|
+
const agentDir = join(agentBase, skillName);
|
|
1151
|
+
const installMode = options.mode ?? "symlink";
|
|
1152
|
+
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
1153
|
+
success: false,
|
|
1154
|
+
path: agentDir,
|
|
1155
|
+
mode: installMode,
|
|
1156
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
1157
|
+
};
|
|
1158
|
+
if (!isPathSafe(agentBase, agentDir)) return {
|
|
1159
|
+
success: false,
|
|
1160
|
+
path: agentDir,
|
|
1161
|
+
mode: installMode,
|
|
1162
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
1163
|
+
};
|
|
1164
|
+
try {
|
|
1165
|
+
if (installMode === "copy") {
|
|
1166
|
+
await copyAtomically(skill.path, agentDir);
|
|
1167
|
+
return {
|
|
1168
|
+
success: true,
|
|
1169
|
+
path: agentDir,
|
|
1170
|
+
mode: "copy"
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
await copyAtomically(skill.path, canonicalDir);
|
|
1174
|
+
if (isGlobal && isUniversalAgent(agentType)) return {
|
|
1175
|
+
success: true,
|
|
1176
|
+
path: canonicalDir,
|
|
1177
|
+
canonicalPath: canonicalDir,
|
|
1178
|
+
mode: "symlink"
|
|
1179
|
+
};
|
|
1180
|
+
if (!await createSymlink(canonicalDir, agentDir)) {
|
|
1181
|
+
await copyAtomically(skill.path, agentDir);
|
|
1182
|
+
return {
|
|
1183
|
+
success: true,
|
|
1184
|
+
path: agentDir,
|
|
1185
|
+
canonicalPath: canonicalDir,
|
|
1186
|
+
mode: "symlink",
|
|
1187
|
+
symlinkFailed: true
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
return {
|
|
1191
|
+
success: true,
|
|
1192
|
+
path: agentDir,
|
|
1193
|
+
canonicalPath: canonicalDir,
|
|
1194
|
+
mode: "symlink"
|
|
1195
|
+
};
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
return {
|
|
1198
|
+
success: false,
|
|
1199
|
+
path: agentDir,
|
|
1200
|
+
mode: installMode,
|
|
1201
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const EXCLUDE_FILES = new Set(["metadata.json"]);
|
|
1206
|
+
const EXCLUDE_DIRS = new Set([
|
|
1207
|
+
".git",
|
|
1208
|
+
"__pycache__",
|
|
1209
|
+
"__pypackages__"
|
|
1210
|
+
]);
|
|
1211
|
+
const isExcluded = (name, isDirectory = false) => {
|
|
1212
|
+
if (EXCLUDE_FILES.has(name)) return true;
|
|
1213
|
+
if (name.startsWith(".")) return true;
|
|
1214
|
+
if (isDirectory && EXCLUDE_DIRS.has(name)) return true;
|
|
1215
|
+
return false;
|
|
1216
|
+
};
|
|
1217
|
+
async function copyAtomically(src, dest, rootSrc) {
|
|
1218
|
+
const stagingDir = dest + ".staging-" + randomBytes(4).toString("hex");
|
|
1219
|
+
try {
|
|
1220
|
+
await cleanAndCreateDirectory(stagingDir);
|
|
1221
|
+
await copyDirectory(src, stagingDir, rootSrc);
|
|
1222
|
+
await rm(dest, {
|
|
1223
|
+
recursive: true,
|
|
1224
|
+
force: true
|
|
1225
|
+
});
|
|
1226
|
+
await rename(stagingDir, dest);
|
|
1227
|
+
} finally {
|
|
1228
|
+
await rm(stagingDir, {
|
|
1229
|
+
recursive: true,
|
|
1230
|
+
force: true
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
async function copyDirectory(src, dest, rootSrc) {
|
|
1235
|
+
const actualRootSrc = rootSrc || src;
|
|
1236
|
+
const absoluteRootSrc = resolve(actualRootSrc);
|
|
1237
|
+
await mkdir(dest, { recursive: true });
|
|
1238
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
1239
|
+
await Promise.all(entries.filter((entry) => !isExcluded(entry.name, entry.isDirectory())).map(async (entry) => {
|
|
1240
|
+
const srcPath = join(src, entry.name);
|
|
1241
|
+
const destPath = join(dest, entry.name);
|
|
1242
|
+
if (entry.isSymbolicLink()) try {
|
|
1243
|
+
const target = await readlink(srcPath);
|
|
1244
|
+
if (!resolve(dirname(srcPath), target).startsWith(absoluteRootSrc)) throw new Error(`Security Violation: Symlink at ${srcPath} escapes skill boundary`);
|
|
1245
|
+
await cp(srcPath, destPath, {
|
|
1246
|
+
dereference: true,
|
|
1247
|
+
recursive: true
|
|
1248
|
+
});
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") console.warn(`Skipping broken symlink: ${srcPath}`);
|
|
1251
|
+
else throw err;
|
|
1252
|
+
}
|
|
1253
|
+
else if (entry.isDirectory()) await copyDirectory(srcPath, destPath, actualRootSrc);
|
|
1254
|
+
else try {
|
|
1255
|
+
await cp(srcPath, destPath, {
|
|
1256
|
+
dereference: false,
|
|
1257
|
+
recursive: true
|
|
1258
|
+
});
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
throw err;
|
|
1261
|
+
}
|
|
1262
|
+
}));
|
|
1263
|
+
}
|
|
1264
|
+
async function isSkillInstalled(skillName, agentType, options = {}) {
|
|
1265
|
+
const agent = agents[agentType];
|
|
1266
|
+
const sanitized = sanitizeName(skillName);
|
|
1267
|
+
if (options.global && agent.globalSkillsDir === void 0) return false;
|
|
1268
|
+
const targetBase = options.global ? agent.globalSkillsDir : join(options.cwd || process.cwd(), agent.skillsDir);
|
|
1269
|
+
const skillDir = join(targetBase, sanitized);
|
|
1270
|
+
if (!isPathSafe(targetBase, skillDir)) return false;
|
|
1271
|
+
try {
|
|
1272
|
+
await access(skillDir);
|
|
1273
|
+
return true;
|
|
1274
|
+
} catch {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function getInstallPath(skillName, agentType, options = {}) {
|
|
1279
|
+
agents[agentType];
|
|
1280
|
+
options.cwd || process.cwd();
|
|
1281
|
+
const sanitized = sanitizeName(skillName);
|
|
1282
|
+
const targetBase = getAgentBaseDir(agentType, options.global ?? false, options.cwd);
|
|
1283
|
+
const installPath = join(targetBase, sanitized);
|
|
1284
|
+
if (!isPathSafe(targetBase, installPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1285
|
+
return installPath;
|
|
1286
|
+
}
|
|
1287
|
+
function getCanonicalPath(skillName, options = {}) {
|
|
1288
|
+
const sanitized = sanitizeName(skillName);
|
|
1289
|
+
const canonicalBase = getCanonicalSkillsDir(options.global ?? false, options.cwd);
|
|
1290
|
+
const canonicalPath = join(canonicalBase, sanitized);
|
|
1291
|
+
if (!isPathSafe(canonicalBase, canonicalPath)) throw new Error("Invalid skill name: potential path traversal detected");
|
|
1292
|
+
return canonicalPath;
|
|
1293
|
+
}
|
|
1294
|
+
async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
1295
|
+
const agent = agents[agentType];
|
|
1296
|
+
const isGlobal = options.global ?? false;
|
|
1297
|
+
const cwd = options.cwd || process.cwd();
|
|
1298
|
+
const installMode = options.mode ?? "symlink";
|
|
1299
|
+
if (isGlobal && agent.globalSkillsDir === void 0) return {
|
|
1300
|
+
success: false,
|
|
1301
|
+
path: "",
|
|
1302
|
+
mode: installMode,
|
|
1303
|
+
error: `${agent.displayName} does not support global skill installation`
|
|
1304
|
+
};
|
|
1305
|
+
const skillName = sanitizeName(skill.installName);
|
|
1306
|
+
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
1307
|
+
const canonicalDir = join(canonicalBase, skillName);
|
|
1308
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
1309
|
+
const agentDir = join(agentBase, skillName);
|
|
1310
|
+
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
1311
|
+
success: false,
|
|
1312
|
+
path: agentDir,
|
|
1313
|
+
mode: installMode,
|
|
1314
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
1315
|
+
};
|
|
1316
|
+
if (!isPathSafe(agentBase, agentDir)) return {
|
|
1317
|
+
success: false,
|
|
1318
|
+
path: agentDir,
|
|
1319
|
+
mode: installMode,
|
|
1320
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
1321
|
+
};
|
|
1322
|
+
async function writeSkillFiles(targetDir) {
|
|
1323
|
+
for (const [filePath, content] of skill.files) {
|
|
1324
|
+
const fullPath = join(targetDir, filePath);
|
|
1325
|
+
if (!isPathSafe(targetDir, fullPath)) continue;
|
|
1326
|
+
const parentDir = dirname(fullPath);
|
|
1327
|
+
if (parentDir !== targetDir) await mkdir(parentDir, { recursive: true });
|
|
1328
|
+
await writeFile(fullPath, content, "utf-8");
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
const writeWellKnownAtomically = async (targetDir) => {
|
|
1333
|
+
const stagingDir = targetDir + ".staging-" + randomBytes(4).toString("hex");
|
|
1334
|
+
try {
|
|
1335
|
+
await cleanAndCreateDirectory(stagingDir);
|
|
1336
|
+
await writeSkillFiles(stagingDir);
|
|
1337
|
+
await rm(targetDir, {
|
|
1338
|
+
recursive: true,
|
|
1339
|
+
force: true
|
|
1340
|
+
});
|
|
1341
|
+
await rename(stagingDir, targetDir);
|
|
1342
|
+
} finally {
|
|
1343
|
+
await rm(stagingDir, {
|
|
1344
|
+
recursive: true,
|
|
1345
|
+
force: true
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
if (installMode === "copy") {
|
|
1350
|
+
await writeWellKnownAtomically(agentDir);
|
|
1351
|
+
return {
|
|
1352
|
+
success: true,
|
|
1353
|
+
path: agentDir,
|
|
1354
|
+
mode: "copy"
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
await writeWellKnownAtomically(canonicalDir);
|
|
1358
|
+
if (isGlobal && isUniversalAgent(agentType)) return {
|
|
1359
|
+
success: true,
|
|
1360
|
+
path: canonicalDir,
|
|
1361
|
+
canonicalPath: canonicalDir,
|
|
1362
|
+
mode: "symlink"
|
|
1363
|
+
};
|
|
1364
|
+
if (!await createSymlink(canonicalDir, agentDir)) {
|
|
1365
|
+
await writeWellKnownAtomically(agentDir);
|
|
1366
|
+
return {
|
|
1367
|
+
success: true,
|
|
1368
|
+
path: agentDir,
|
|
1369
|
+
canonicalPath: canonicalDir,
|
|
1370
|
+
mode: "symlink",
|
|
1371
|
+
symlinkFailed: true
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
success: true,
|
|
1376
|
+
path: agentDir,
|
|
1377
|
+
canonicalPath: canonicalDir,
|
|
1378
|
+
mode: "symlink"
|
|
1379
|
+
};
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
return {
|
|
1382
|
+
success: false,
|
|
1383
|
+
path: agentDir,
|
|
1384
|
+
mode: installMode,
|
|
1385
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async function listInstalledSkills(options = {}) {
|
|
1390
|
+
const cwd = options.cwd || process.cwd();
|
|
1391
|
+
const skillsMap = /* @__PURE__ */ new Map();
|
|
1392
|
+
const scopes = [];
|
|
1393
|
+
const detectedAgents = await detectInstalledAgents();
|
|
1394
|
+
const agentFilter = options.agentFilter;
|
|
1395
|
+
const agentsToCheck = agentFilter ? detectedAgents.filter((a) => agentFilter.includes(a)) : detectedAgents;
|
|
1396
|
+
const scopeTypes = [];
|
|
1397
|
+
if (options.global === void 0) scopeTypes.push({ global: false }, { global: true });
|
|
1398
|
+
else scopeTypes.push({ global: options.global });
|
|
1399
|
+
for (const { global: isGlobal } of scopeTypes) {
|
|
1400
|
+
scopes.push({
|
|
1401
|
+
global: isGlobal,
|
|
1402
|
+
path: getCanonicalSkillsDir(isGlobal, cwd)
|
|
1403
|
+
});
|
|
1404
|
+
for (const agentType of agentsToCheck) {
|
|
1405
|
+
const agent = agents[agentType];
|
|
1406
|
+
if (isGlobal && agent.globalSkillsDir === void 0) continue;
|
|
1407
|
+
const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
|
|
1408
|
+
if (!scopes.some((s) => s.path === agentDir && s.global === isGlobal)) scopes.push({
|
|
1409
|
+
global: isGlobal,
|
|
1410
|
+
path: agentDir,
|
|
1411
|
+
agentType
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
const allAgentTypes = Object.keys(agents);
|
|
1415
|
+
for (const agentType of allAgentTypes) {
|
|
1416
|
+
if (agentsToCheck.includes(agentType)) continue;
|
|
1417
|
+
const agent = agents[agentType];
|
|
1418
|
+
if (isGlobal && agent.globalSkillsDir === void 0) continue;
|
|
1419
|
+
const agentDir = isGlobal ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
|
|
1420
|
+
if (scopes.some((s) => s.path === agentDir && s.global === isGlobal)) continue;
|
|
1421
|
+
if (existsSync(agentDir)) scopes.push({
|
|
1422
|
+
global: isGlobal,
|
|
1423
|
+
path: agentDir,
|
|
1424
|
+
agentType
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
for (const scope of scopes) try {
|
|
1429
|
+
const entries = await readdir(scope.path, { withFileTypes: true });
|
|
1430
|
+
for (const entry of entries) {
|
|
1431
|
+
if (!entry.isDirectory()) continue;
|
|
1432
|
+
const skillDir = join(scope.path, entry.name);
|
|
1433
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
1434
|
+
try {
|
|
1435
|
+
await stat(skillMdPath);
|
|
1436
|
+
} catch {
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const skill = await parseSkillMd(skillMdPath);
|
|
1440
|
+
if (!skill) continue;
|
|
1441
|
+
const scopeKey = scope.global ? "global" : "project";
|
|
1442
|
+
const skillKey = `${scopeKey}:${skill.name}`;
|
|
1443
|
+
if (scope.agentType) {
|
|
1444
|
+
if (skillsMap.has(skillKey)) {
|
|
1445
|
+
const existing = skillsMap.get(skillKey);
|
|
1446
|
+
if (!existing.agents.includes(scope.agentType)) existing.agents.push(scope.agentType);
|
|
1447
|
+
} else skillsMap.set(skillKey, {
|
|
1448
|
+
name: skill.name,
|
|
1449
|
+
description: skill.description,
|
|
1450
|
+
path: skillDir,
|
|
1451
|
+
canonicalPath: skillDir,
|
|
1452
|
+
scope: scopeKey,
|
|
1453
|
+
agents: [scope.agentType]
|
|
1454
|
+
});
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const sanitizedSkillName = sanitizeName(skill.name);
|
|
1458
|
+
const installedAgents = [];
|
|
1459
|
+
for (const agentType of agentsToCheck) {
|
|
1460
|
+
const agent = agents[agentType];
|
|
1461
|
+
if (scope.global && agent.globalSkillsDir === void 0) continue;
|
|
1462
|
+
const agentBase = scope.global ? agent.globalSkillsDir : join(cwd, agent.skillsDir);
|
|
1463
|
+
let found = false;
|
|
1464
|
+
const possibleNames = Array.from(new Set([
|
|
1465
|
+
entry.name,
|
|
1466
|
+
sanitizedSkillName,
|
|
1467
|
+
skill.name.toLowerCase().replace(/\s+/g, "-").replace(/[\/\\:\0]/g, "")
|
|
1468
|
+
]));
|
|
1469
|
+
for (const possibleName of possibleNames) {
|
|
1470
|
+
const agentSkillDir = join(agentBase, possibleName);
|
|
1471
|
+
if (!isPathSafe(agentBase, agentSkillDir)) continue;
|
|
1472
|
+
try {
|
|
1473
|
+
await access(agentSkillDir);
|
|
1474
|
+
found = true;
|
|
1475
|
+
break;
|
|
1476
|
+
} catch {}
|
|
1477
|
+
}
|
|
1478
|
+
if (!found) try {
|
|
1479
|
+
const agentEntries = await readdir(agentBase, { withFileTypes: true });
|
|
1480
|
+
for (const agentEntry of agentEntries) {
|
|
1481
|
+
if (!agentEntry.isDirectory()) continue;
|
|
1482
|
+
const candidateDir = join(agentBase, agentEntry.name);
|
|
1483
|
+
if (!isPathSafe(agentBase, candidateDir)) continue;
|
|
1484
|
+
try {
|
|
1485
|
+
const candidateSkillMd = join(candidateDir, "SKILL.md");
|
|
1486
|
+
await stat(candidateSkillMd);
|
|
1487
|
+
const candidateSkill = await parseSkillMd(candidateSkillMd);
|
|
1488
|
+
if (candidateSkill && candidateSkill.name === skill.name) {
|
|
1489
|
+
found = true;
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
} catch {}
|
|
1493
|
+
}
|
|
1494
|
+
} catch {}
|
|
1495
|
+
if (found) installedAgents.push(agentType);
|
|
1496
|
+
}
|
|
1497
|
+
if (skillsMap.has(skillKey)) {
|
|
1498
|
+
const existing = skillsMap.get(skillKey);
|
|
1499
|
+
for (const agent of installedAgents) if (!existing.agents.includes(agent)) existing.agents.push(agent);
|
|
1500
|
+
} else skillsMap.set(skillKey, {
|
|
1501
|
+
name: skill.name,
|
|
1502
|
+
description: skill.description,
|
|
1503
|
+
path: skillDir,
|
|
1504
|
+
canonicalPath: skillDir,
|
|
1505
|
+
scope: scopeKey,
|
|
1506
|
+
agents: installedAgents
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
} catch {}
|
|
1510
|
+
return Array.from(skillsMap.values());
|
|
1511
|
+
}
|
|
1512
|
+
const TELEMETRY_URL = "https://add-skill.vercel.sh/t";
|
|
1513
|
+
const AUDIT_URL = "https://add-skill.vercel.sh/audit";
|
|
1514
|
+
let cliVersion = null;
|
|
1515
|
+
function isCI() {
|
|
1516
|
+
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);
|
|
1517
|
+
}
|
|
1518
|
+
function isEnabled() {
|
|
1519
|
+
return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
|
|
1520
|
+
}
|
|
1521
|
+
function setVersion(version) {
|
|
1522
|
+
cliVersion = version;
|
|
1523
|
+
}
|
|
1524
|
+
async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
|
|
1525
|
+
if (skillSlugs.length === 0) return null;
|
|
1526
|
+
try {
|
|
1527
|
+
const params = new URLSearchParams({
|
|
1528
|
+
source,
|
|
1529
|
+
skills: skillSlugs.join(",")
|
|
1530
|
+
});
|
|
1531
|
+
const controller = new AbortController();
|
|
1532
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1533
|
+
const response = await fetch(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal });
|
|
1534
|
+
clearTimeout(timeout);
|
|
1535
|
+
if (!response.ok) return null;
|
|
1536
|
+
return await response.json();
|
|
1537
|
+
} catch {
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function track(data) {
|
|
1542
|
+
if (!isEnabled()) return;
|
|
1543
|
+
try {
|
|
1544
|
+
const params = new URLSearchParams();
|
|
1545
|
+
if (cliVersion) params.set("v", cliVersion);
|
|
1546
|
+
if (isCI()) params.set("ci", "1");
|
|
1547
|
+
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
|
|
1548
|
+
fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {});
|
|
1549
|
+
} catch {}
|
|
1550
|
+
}
|
|
1551
|
+
var ProviderRegistryImpl = class {
|
|
1552
|
+
providers = [];
|
|
1553
|
+
register(provider) {
|
|
1554
|
+
if (this.providers.some((p) => p.id === provider.id)) throw new Error(`Provider with id "${provider.id}" already registered`);
|
|
1555
|
+
this.providers.push(provider);
|
|
1556
|
+
}
|
|
1557
|
+
findProvider(url) {
|
|
1558
|
+
for (const provider of this.providers) if (provider.match(url).matches) return provider;
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
getProviders() {
|
|
1562
|
+
return [...this.providers];
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
new ProviderRegistryImpl();
|
|
1566
|
+
const CREDENTIALS_VERSION = 1;
|
|
1567
|
+
function getCredentialsPath() {
|
|
1568
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
1569
|
+
return join(xdgConfigHome && xdgConfigHome.trim() !== "" ? xdgConfigHome : join(homedir(), ".config"), "skills", "credentials.json");
|
|
1570
|
+
}
|
|
1571
|
+
function hostKeyForRegistry(registry) {
|
|
1572
|
+
try {
|
|
1573
|
+
return new URL(registry).host;
|
|
1574
|
+
} catch {
|
|
1575
|
+
return registry.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function readCredentialsFile() {
|
|
1579
|
+
const path = getCredentialsPath();
|
|
1580
|
+
try {
|
|
1581
|
+
const raw = readFileSync(path, "utf-8");
|
|
1582
|
+
const parsed = JSON.parse(raw);
|
|
1583
|
+
if (parsed && parsed.version === CREDENTIALS_VERSION && parsed.hosts && typeof parsed.hosts === "object") return {
|
|
1584
|
+
version: CREDENTIALS_VERSION,
|
|
1585
|
+
hosts: parsed.hosts
|
|
1586
|
+
};
|
|
1587
|
+
return {
|
|
1588
|
+
version: CREDENTIALS_VERSION,
|
|
1589
|
+
hosts: {}
|
|
1590
|
+
};
|
|
1591
|
+
} catch {
|
|
1592
|
+
return {
|
|
1593
|
+
version: CREDENTIALS_VERSION,
|
|
1594
|
+
hosts: {}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function writeCredentialsFile(file) {
|
|
1599
|
+
const path = getCredentialsPath();
|
|
1600
|
+
const dir = dirname(path);
|
|
1601
|
+
if (!existsSync(dir)) mkdirSync(dir, {
|
|
1602
|
+
recursive: true,
|
|
1603
|
+
mode: 448
|
|
1604
|
+
});
|
|
1605
|
+
writeFileSync(path, JSON.stringify(file, null, 2), { mode: 384 });
|
|
1606
|
+
try {
|
|
1607
|
+
chmodSync(path, 384);
|
|
1608
|
+
} catch {}
|
|
1609
|
+
}
|
|
1610
|
+
function saveToken(registry, token) {
|
|
1611
|
+
const file = readCredentialsFile();
|
|
1612
|
+
file.hosts[hostKeyForRegistry(registry)] = token;
|
|
1613
|
+
writeCredentialsFile(file);
|
|
1614
|
+
}
|
|
1615
|
+
function loadToken(registry) {
|
|
1616
|
+
return readCredentialsFile().hosts[hostKeyForRegistry(registry)] ?? null;
|
|
1617
|
+
}
|
|
1618
|
+
function clearToken(registry) {
|
|
1619
|
+
const file = readCredentialsFile();
|
|
1620
|
+
const key = hostKeyForRegistry(registry);
|
|
1621
|
+
if (!(key in file.hosts)) return false;
|
|
1622
|
+
delete file.hosts[key];
|
|
1623
|
+
if (Object.keys(file.hosts).length === 0) {
|
|
1624
|
+
try {
|
|
1625
|
+
const path = getCredentialsPath();
|
|
1626
|
+
if (existsSync(path)) unlinkSync(path);
|
|
1627
|
+
} catch {}
|
|
1628
|
+
return true;
|
|
1629
|
+
}
|
|
1630
|
+
writeCredentialsFile(file);
|
|
1631
|
+
return true;
|
|
1632
|
+
}
|
|
1633
|
+
function isTokenExpired(token, skewMs = 6e4) {
|
|
1634
|
+
if (typeof token.expiresAt !== "number") return false;
|
|
1635
|
+
return Date.now() + skewMs >= token.expiresAt;
|
|
1636
|
+
}
|
|
1637
|
+
async function discoverOidc(issuer, fetchImpl = fetch) {
|
|
1638
|
+
const base = issuer.replace(/\/$/, "");
|
|
1639
|
+
const res = await fetchImpl(`${base}/.well-known/openid-configuration`);
|
|
1640
|
+
if (!res.ok) throw new Error(`OIDC discovery failed for issuer ${issuer} (HTTP ${res.status}). Check the --issuer / ${SKILLS_ISSUER_ENV} value.`);
|
|
1641
|
+
const doc = await res.json();
|
|
1642
|
+
if (!doc.authorization_endpoint || !doc.token_endpoint) throw new Error(`OIDC discovery document for ${issuer} is missing authorization_endpoint or token_endpoint.`);
|
|
1643
|
+
return {
|
|
1644
|
+
issuer: doc.issuer ?? base,
|
|
1645
|
+
authorization_endpoint: doc.authorization_endpoint,
|
|
1646
|
+
token_endpoint: doc.token_endpoint,
|
|
1647
|
+
device_authorization_endpoint: doc.device_authorization_endpoint
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function base64UrlEncode(buf) {
|
|
1651
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1652
|
+
}
|
|
1653
|
+
function generatePkce() {
|
|
1654
|
+
const verifier = base64UrlEncode(randomBytes(32));
|
|
1655
|
+
return {
|
|
1656
|
+
verifier,
|
|
1657
|
+
challenge: base64UrlEncode(createHash("sha256").update(verifier).digest()),
|
|
1658
|
+
method: "S256"
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
function generateState() {
|
|
1662
|
+
return base64UrlEncode(randomBytes(16));
|
|
1663
|
+
}
|
|
1664
|
+
function validateState(expected, received) {
|
|
1665
|
+
if (!received || expected.length !== received.length) return false;
|
|
1666
|
+
let mismatch = 0;
|
|
1667
|
+
for (let i = 0; i < expected.length; i++) mismatch |= expected.charCodeAt(i) ^ received.charCodeAt(i);
|
|
1668
|
+
return mismatch === 0;
|
|
1669
|
+
}
|
|
1670
|
+
function toStoredToken(resp, issuer, clientId) {
|
|
1671
|
+
return {
|
|
1672
|
+
accessToken: resp.access_token,
|
|
1673
|
+
refreshToken: resp.refresh_token,
|
|
1674
|
+
tokenType: resp.token_type ?? "Bearer",
|
|
1675
|
+
scope: resp.scope,
|
|
1676
|
+
expiresAt: typeof resp.expires_in === "number" ? Date.now() + resp.expires_in * 1e3 : void 0,
|
|
1677
|
+
issuer,
|
|
1678
|
+
clientId
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
async function postForm(endpoint, params, fetchImpl) {
|
|
1682
|
+
const body = new URLSearchParams(params).toString();
|
|
1683
|
+
return await (await fetchImpl(endpoint, {
|
|
1684
|
+
method: "POST",
|
|
1685
|
+
headers: {
|
|
1686
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
1687
|
+
accept: "application/json"
|
|
1688
|
+
},
|
|
1689
|
+
body
|
|
1690
|
+
})).json().catch(() => ({}));
|
|
1691
|
+
}
|
|
1692
|
+
function openBrowser(url) {
|
|
1693
|
+
let command;
|
|
1694
|
+
let args;
|
|
1695
|
+
switch (process.platform) {
|
|
1696
|
+
case "darwin":
|
|
1697
|
+
command = "open";
|
|
1698
|
+
args = [url];
|
|
1699
|
+
break;
|
|
1700
|
+
case "win32":
|
|
1701
|
+
command = "cmd";
|
|
1702
|
+
args = [
|
|
1703
|
+
"/c",
|
|
1704
|
+
"start",
|
|
1705
|
+
"\"\"",
|
|
1706
|
+
url.replace(/&/g, "^&")
|
|
1707
|
+
];
|
|
1708
|
+
break;
|
|
1709
|
+
default:
|
|
1710
|
+
command = "xdg-open";
|
|
1711
|
+
args = [url];
|
|
1712
|
+
break;
|
|
1713
|
+
}
|
|
1714
|
+
try {
|
|
1715
|
+
const child = spawn(command, args, {
|
|
1716
|
+
stdio: "ignore",
|
|
1717
|
+
detached: true
|
|
1718
|
+
});
|
|
1719
|
+
child.on("error", () => {});
|
|
1720
|
+
child.unref();
|
|
1721
|
+
return true;
|
|
1722
|
+
} catch {
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function hasInteractiveBrowser() {
|
|
1727
|
+
if (process.env.SKILLS_NO_BROWSER === "1") return false;
|
|
1728
|
+
if (process.platform === "win32" || process.platform === "darwin") return true;
|
|
1729
|
+
return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
1730
|
+
}
|
|
1731
|
+
async function loginWithLoopback(config, deps = {}) {
|
|
1732
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1733
|
+
const open = deps.open ?? openBrowser;
|
|
1734
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
1735
|
+
const pkce = generatePkce();
|
|
1736
|
+
const state = generateState();
|
|
1737
|
+
const redirectPath = "/callback";
|
|
1738
|
+
const server = createServer();
|
|
1739
|
+
await new Promise((res, rej) => {
|
|
1740
|
+
server.once("error", rej);
|
|
1741
|
+
server.listen(0, "127.0.0.1", () => res());
|
|
1742
|
+
});
|
|
1743
|
+
const addr = server.address();
|
|
1744
|
+
const redirectUri = `http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : 0}${redirectPath}`;
|
|
1745
|
+
const authorizeUrl = new URL$1(config.oidc.authorization_endpoint);
|
|
1746
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
1747
|
+
authorizeUrl.searchParams.set("client_id", config.clientId);
|
|
1748
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
1749
|
+
authorizeUrl.searchParams.set("scope", config.scopes);
|
|
1750
|
+
authorizeUrl.searchParams.set("audience", config.audience);
|
|
1751
|
+
authorizeUrl.searchParams.set("state", state);
|
|
1752
|
+
authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
1753
|
+
authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
|
|
1754
|
+
const redirectPromise = new Promise((resolve, reject) => {
|
|
1755
|
+
const timer = setTimeout(() => {
|
|
1756
|
+
server.close();
|
|
1757
|
+
reject(/* @__PURE__ */ new Error("Timed out waiting for the browser authorization redirect."));
|
|
1758
|
+
}, 5 * 6e4);
|
|
1759
|
+
server.on("request", (req, resp) => {
|
|
1760
|
+
const reqUrl = new URL$1(req.url ?? "/", "http://127.0.0.1");
|
|
1761
|
+
if (reqUrl.pathname !== redirectPath) {
|
|
1762
|
+
resp.statusCode = 404;
|
|
1763
|
+
resp.end("Not found");
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
const error = reqUrl.searchParams.get("error");
|
|
1767
|
+
const code = reqUrl.searchParams.get("code");
|
|
1768
|
+
const recvState = reqUrl.searchParams.get("state");
|
|
1769
|
+
const ok = !error && Boolean(code);
|
|
1770
|
+
resp.statusCode = ok ? 200 : 400;
|
|
1771
|
+
resp.setHeader("content-type", "text/html; charset=utf-8");
|
|
1772
|
+
resp.end(ok ? `<html><body><h2>Login successful</h2><p>You can close this window and return to the terminal.</p></body></html>` : `<html><body><h2>Login failed</h2><p>${error ?? "no authorization code returned"}</p></body></html>`);
|
|
1773
|
+
clearTimeout(timer);
|
|
1774
|
+
server.close();
|
|
1775
|
+
if (!ok) {
|
|
1776
|
+
reject(/* @__PURE__ */ new Error(`Authorization failed: ${error ?? "no code returned"}`));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
resolve({
|
|
1780
|
+
code,
|
|
1781
|
+
state: recvState
|
|
1782
|
+
});
|
|
1783
|
+
});
|
|
1784
|
+
server.on("error", (err) => {
|
|
1785
|
+
clearTimeout(timer);
|
|
1786
|
+
reject(err);
|
|
1787
|
+
});
|
|
1788
|
+
});
|
|
1789
|
+
const url = authorizeUrl.toString();
|
|
1790
|
+
if (open(url)) {
|
|
1791
|
+
log(`Opened your browser to complete login.`);
|
|
1792
|
+
log(`If it did not open, visit:\n ${url}`);
|
|
1793
|
+
} else log(`Open this URL in your browser to complete login:\n ${url}`);
|
|
1794
|
+
const redirect = await redirectPromise;
|
|
1795
|
+
if (!validateState(state, redirect.state)) throw new Error("OAuth state mismatch — possible CSRF. Aborting login.");
|
|
1796
|
+
const tokenResp = await postForm(config.oidc.token_endpoint, {
|
|
1797
|
+
grant_type: "authorization_code",
|
|
1798
|
+
code: redirect.code,
|
|
1799
|
+
redirect_uri: redirectUri,
|
|
1800
|
+
client_id: config.clientId,
|
|
1801
|
+
code_verifier: pkce.verifier
|
|
1802
|
+
}, fetchImpl);
|
|
1803
|
+
if (tokenResp.error || !tokenResp.access_token) throw new Error(`Token exchange failed: ${tokenResp.error ?? "unknown error"}${tokenResp.error_description ? ` — ${tokenResp.error_description}` : ""}`);
|
|
1804
|
+
return toStoredToken(tokenResp, config.issuer, config.clientId);
|
|
1805
|
+
}
|
|
1806
|
+
async function loginWithDeviceCode(config, deps = {}) {
|
|
1807
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
1808
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
1809
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1810
|
+
const deviceEndpoint = config.oidc.device_authorization_endpoint;
|
|
1811
|
+
if (!deviceEndpoint) throw new Error(`The issuer ${config.issuer} does not advertise a device_authorization_endpoint; device login is unavailable. Use the default browser flow instead.`);
|
|
1812
|
+
const device = await (await fetchImpl(deviceEndpoint, {
|
|
1813
|
+
method: "POST",
|
|
1814
|
+
headers: {
|
|
1815
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
1816
|
+
accept: "application/json"
|
|
1817
|
+
},
|
|
1818
|
+
body: new URLSearchParams({
|
|
1819
|
+
client_id: config.clientId,
|
|
1820
|
+
scope: config.scopes,
|
|
1821
|
+
audience: config.audience
|
|
1822
|
+
}).toString()
|
|
1823
|
+
})).json().catch(() => ({}));
|
|
1824
|
+
if (device.error || !device.device_code || !device.user_code) throw new Error(`Device authorization request failed: ${device.error ?? "invalid response"}${device.error_description ? ` — ${device.error_description}` : ""}`);
|
|
1825
|
+
log("");
|
|
1826
|
+
log(`To finish signing in, open: ${device.verification_uri}`);
|
|
1827
|
+
log(`And enter the code: ${device.user_code}`);
|
|
1828
|
+
if (device.verification_uri_complete) log(`Or open this URL directly: ${device.verification_uri_complete}`);
|
|
1829
|
+
log("");
|
|
1830
|
+
log("Waiting for authorization...");
|
|
1831
|
+
let intervalMs = (device.interval ?? 5) * 1e3;
|
|
1832
|
+
const deadline = Date.now() + device.expires_in * 1e3;
|
|
1833
|
+
while (Date.now() < deadline) {
|
|
1834
|
+
await sleep(intervalMs);
|
|
1835
|
+
const tokenResp = await postForm(config.oidc.token_endpoint, {
|
|
1836
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
1837
|
+
device_code: device.device_code,
|
|
1838
|
+
client_id: config.clientId
|
|
1839
|
+
}, fetchImpl);
|
|
1840
|
+
if (tokenResp.access_token) return toStoredToken(tokenResp, config.issuer, config.clientId);
|
|
1841
|
+
switch (tokenResp.error) {
|
|
1842
|
+
case "authorization_pending": break;
|
|
1843
|
+
case "slow_down":
|
|
1844
|
+
intervalMs += 5e3;
|
|
1845
|
+
break;
|
|
1846
|
+
case "access_denied": throw new Error("Authorization was denied. Login aborted.");
|
|
1847
|
+
case "expired_token": throw new Error("The device code expired before authorization completed. Try again.");
|
|
1848
|
+
default: if (tokenResp.error) throw new Error(`Device login failed: ${tokenResp.error}${tokenResp.error_description ? ` — ${tokenResp.error_description}` : ""}`);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
throw new Error("Device login timed out before authorization completed.");
|
|
1852
|
+
}
|
|
1853
|
+
async function refreshToken(token, fetchImpl = fetch) {
|
|
1854
|
+
if (!token.refreshToken || !token.issuer || !token.clientId) return null;
|
|
1855
|
+
let oidc;
|
|
1856
|
+
try {
|
|
1857
|
+
oidc = await discoverOidc(token.issuer, fetchImpl);
|
|
1858
|
+
} catch {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
const resp = await postForm(oidc.token_endpoint, {
|
|
1862
|
+
grant_type: "refresh_token",
|
|
1863
|
+
refresh_token: token.refreshToken,
|
|
1864
|
+
client_id: token.clientId
|
|
1865
|
+
}, fetchImpl);
|
|
1866
|
+
if (resp.error || !resp.access_token) return null;
|
|
1867
|
+
const next = toStoredToken(resp, token.issuer, token.clientId);
|
|
1868
|
+
if (!next.refreshToken) next.refreshToken = token.refreshToken;
|
|
1869
|
+
return next;
|
|
1870
|
+
}
|
|
1871
|
+
async function resolveAuthConfig(options = {}) {
|
|
1872
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1873
|
+
const registry = options.registry || process.env[SKILLS_REGISTRY_URL_ENV] || DEFAULT_REGISTRY_URL;
|
|
1874
|
+
const advertised = await fetchRegistryAuthHints(registry, fetchImpl);
|
|
1875
|
+
const issuer = options.issuer || process.env[SKILLS_ISSUER_ENV] || advertised.issuer || DEFAULT_SKILLS_ISSUER;
|
|
1876
|
+
return {
|
|
1877
|
+
issuer,
|
|
1878
|
+
clientId: options.clientId || process.env[SKILLS_CLIENT_ID_ENV] || advertised.clientId || DEFAULT_SKILLS_CLIENT_ID,
|
|
1879
|
+
registry,
|
|
1880
|
+
audience: SKILLS_OAUTH_AUDIENCE,
|
|
1881
|
+
scopes: SKILLS_OAUTH_SCOPES,
|
|
1882
|
+
oidc: await discoverOidc(issuer, fetchImpl)
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
async function fetchRegistryAuthHints(registry, fetchImpl) {
|
|
1886
|
+
const base = registry.replace(/\/$/, "");
|
|
1887
|
+
const candidates = [`${base}/.well-known/agent-skills/index.json`, `${base}/.well-known/skills/index.json`];
|
|
1888
|
+
for (const url of candidates) try {
|
|
1889
|
+
const res = await fetchImpl(url);
|
|
1890
|
+
if (!res.ok) continue;
|
|
1891
|
+
const json = await res.json();
|
|
1892
|
+
const issuer = json.auth?.issuer ?? json.issuer;
|
|
1893
|
+
const clientId = json.auth?.client_id ?? json.client_id;
|
|
1894
|
+
if (issuer || clientId) return {
|
|
1895
|
+
issuer,
|
|
1896
|
+
clientId
|
|
1897
|
+
};
|
|
1898
|
+
} catch {}
|
|
1899
|
+
return {};
|
|
1900
|
+
}
|
|
1901
|
+
async function getAccessToken(registry = DEFAULT_REGISTRY_URL, fetchImpl = fetch) {
|
|
1902
|
+
const machineToken = process.env[SKILLS_REGISTRY_TOKEN_ENV];
|
|
1903
|
+
if (machineToken && machineToken.trim() !== "") return machineToken.trim();
|
|
1904
|
+
const stored = loadToken(registry);
|
|
1905
|
+
if (!stored) return null;
|
|
1906
|
+
if (!isTokenExpired(stored)) return stored.accessToken;
|
|
1907
|
+
const refreshed = await refreshToken(stored, fetchImpl);
|
|
1908
|
+
if (refreshed) {
|
|
1909
|
+
saveToken(registry, refreshed);
|
|
1910
|
+
return refreshed.accessToken;
|
|
1911
|
+
}
|
|
1912
|
+
return stored.accessToken;
|
|
1913
|
+
}
|
|
1914
|
+
async function getAuthHeader(registry = DEFAULT_REGISTRY_URL, fetchImpl = fetch) {
|
|
1915
|
+
const token = await getAccessToken(registry, fetchImpl);
|
|
1916
|
+
return token ? `Bearer ${token}` : void 0;
|
|
1917
|
+
}
|
|
1918
|
+
function logout(registry = DEFAULT_REGISTRY_URL) {
|
|
1919
|
+
return clearToken(registry);
|
|
1920
|
+
}
|
|
1921
|
+
async function runLogin(options = {}) {
|
|
1922
|
+
const config = await resolveAuthConfig(options);
|
|
1923
|
+
const token = options.device ?? !hasInteractiveBrowser() ? await loginWithDeviceCode(config, options) : await loginWithLoopback(config, options);
|
|
1924
|
+
saveToken(config.registry, token);
|
|
1925
|
+
return token;
|
|
1926
|
+
}
|
|
1927
|
+
var AuthRequiredError = class extends Error {
|
|
1928
|
+
url;
|
|
1929
|
+
status;
|
|
1930
|
+
constructor(url, status) {
|
|
1931
|
+
super(`Authentication required to access ${url} (HTTP ${status}). Run \`skills login\` to authenticate, or set SKILLS_REGISTRY_TOKEN for CI.`);
|
|
1932
|
+
this.name = "AuthRequiredError";
|
|
1933
|
+
this.url = url;
|
|
1934
|
+
this.status = status;
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
async function authFetch(input, init) {
|
|
1938
|
+
let headers = {};
|
|
1939
|
+
if (init?.headers) headers = { ...init.headers };
|
|
1940
|
+
try {
|
|
1941
|
+
const authHeader = await getAuthHeader(input);
|
|
1942
|
+
if (authHeader && !headers["Authorization"] && !headers["authorization"]) headers["Authorization"] = authHeader;
|
|
1943
|
+
} catch {}
|
|
1944
|
+
const res = await fetch(input, {
|
|
1945
|
+
...init,
|
|
1946
|
+
headers
|
|
1947
|
+
});
|
|
1948
|
+
if (res.status === 401 || res.status === 403) throw new AuthRequiredError(input, res.status);
|
|
1949
|
+
return res;
|
|
1950
|
+
}
|
|
1951
|
+
var WellKnownProvider = class {
|
|
1952
|
+
id = "well-known";
|
|
1953
|
+
displayName = "Well-Known Skills";
|
|
1954
|
+
WELL_KNOWN_PATHS = [".well-known/agent-skills", ".well-known/skills"];
|
|
1955
|
+
INDEX_FILE = "index.json";
|
|
1956
|
+
match(url) {
|
|
1957
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) return { matches: false };
|
|
1958
|
+
try {
|
|
1959
|
+
const parsed = new URL(url);
|
|
1960
|
+
if ([
|
|
1961
|
+
"github.com",
|
|
1962
|
+
"gitlab.com",
|
|
1963
|
+
"huggingface.co"
|
|
1964
|
+
].includes(parsed.hostname)) return { matches: false };
|
|
1965
|
+
return {
|
|
1966
|
+
matches: true,
|
|
1967
|
+
sourceIdentifier: `wellknown/${parsed.hostname}`
|
|
1968
|
+
};
|
|
1969
|
+
} catch {
|
|
1970
|
+
return { matches: false };
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
async fetchIndex(baseUrl) {
|
|
1974
|
+
try {
|
|
1975
|
+
const parsed = new URL(baseUrl);
|
|
1976
|
+
const basePath = parsed.pathname.replace(/\/$/, "");
|
|
1977
|
+
const urlsToTry = [];
|
|
1978
|
+
for (const wellKnownPath of this.WELL_KNOWN_PATHS) {
|
|
1979
|
+
urlsToTry.push({
|
|
1980
|
+
indexUrl: `${parsed.protocol}//${parsed.host}${basePath}/${wellKnownPath}/${this.INDEX_FILE}`,
|
|
1981
|
+
baseUrl: `${parsed.protocol}//${parsed.host}${basePath}`,
|
|
1982
|
+
wellKnownPath
|
|
1983
|
+
});
|
|
1984
|
+
if (basePath && basePath !== "") urlsToTry.push({
|
|
1985
|
+
indexUrl: `${parsed.protocol}//${parsed.host}/${wellKnownPath}/${this.INDEX_FILE}`,
|
|
1986
|
+
baseUrl: `${parsed.protocol}//${parsed.host}`,
|
|
1987
|
+
wellKnownPath
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
for (const { indexUrl, baseUrl: resolvedBase, wellKnownPath } of urlsToTry) try {
|
|
1991
|
+
const response = await authFetch(indexUrl);
|
|
1992
|
+
if (!response.ok) continue;
|
|
1993
|
+
const index = await response.json();
|
|
1994
|
+
if (!index.skills || !Array.isArray(index.skills)) continue;
|
|
1995
|
+
let allValid = true;
|
|
1996
|
+
for (const entry of index.skills) if (!this.isValidSkillEntry(entry)) {
|
|
1997
|
+
allValid = false;
|
|
1998
|
+
break;
|
|
1999
|
+
}
|
|
2000
|
+
if (allValid) return {
|
|
2001
|
+
index,
|
|
2002
|
+
resolvedBaseUrl: resolvedBase,
|
|
2003
|
+
resolvedWellKnownPath: wellKnownPath
|
|
2004
|
+
};
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
if (err instanceof AuthRequiredError) throw err;
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
return null;
|
|
2010
|
+
} catch (err) {
|
|
2011
|
+
if (err instanceof AuthRequiredError) throw err;
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
isValidSkillEntry(entry) {
|
|
2016
|
+
if (!entry || typeof entry !== "object") return false;
|
|
2017
|
+
const e = entry;
|
|
2018
|
+
if (typeof e.name !== "string" || !e.name) return false;
|
|
2019
|
+
if (typeof e.description !== "string" || !e.description) return false;
|
|
2020
|
+
if (!Array.isArray(e.files) || e.files.length === 0) return false;
|
|
2021
|
+
if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(e.name) && e.name.length > 1) {
|
|
2022
|
+
if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) return false;
|
|
2023
|
+
}
|
|
2024
|
+
for (const file of e.files) {
|
|
2025
|
+
if (typeof file !== "string") return false;
|
|
2026
|
+
if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
|
|
2027
|
+
}
|
|
2028
|
+
if (!e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md")) return false;
|
|
2029
|
+
return true;
|
|
2030
|
+
}
|
|
2031
|
+
async fetchSkill(url) {
|
|
2032
|
+
try {
|
|
2033
|
+
const parsed = new URL(url);
|
|
2034
|
+
const result = await this.fetchIndex(url);
|
|
2035
|
+
if (!result) return null;
|
|
2036
|
+
const { index, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2037
|
+
let skillName = null;
|
|
2038
|
+
const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/);
|
|
2039
|
+
if (pathMatch && pathMatch[1] && pathMatch[1] !== "index.json") skillName = pathMatch[1];
|
|
2040
|
+
else if (index.skills.length === 1) skillName = index.skills[0].name;
|
|
2041
|
+
if (!skillName) return null;
|
|
2042
|
+
const skillEntry = index.skills.find((s) => s.name === skillName);
|
|
2043
|
+
if (!skillEntry) return null;
|
|
2044
|
+
return this.fetchSkillByEntry(resolvedBaseUrl, skillEntry, resolvedWellKnownPath);
|
|
2045
|
+
} catch (err) {
|
|
2046
|
+
if (err instanceof AuthRequiredError) throw err;
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
async fetchSkillByEntry(baseUrl, entry, wellKnownPath) {
|
|
2051
|
+
try {
|
|
2052
|
+
const resolvedPath = wellKnownPath ?? this.WELL_KNOWN_PATHS[0];
|
|
2053
|
+
const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${resolvedPath}/${entry.name}`;
|
|
2054
|
+
const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
|
|
2055
|
+
const response = await authFetch(skillMdUrl);
|
|
2056
|
+
if (!response.ok) return null;
|
|
2057
|
+
const content = await response.text();
|
|
2058
|
+
const { data } = (0, import_gray_matter.default)(content);
|
|
2059
|
+
if (!data.name || !data.description) return null;
|
|
2060
|
+
const files = /* @__PURE__ */ new Map();
|
|
2061
|
+
files.set("SKILL.md", content);
|
|
2062
|
+
const filePromises = entry.files.filter((f) => f.toLowerCase() !== "skill.md").map(async (filePath) => {
|
|
2063
|
+
try {
|
|
2064
|
+
const fileResponse = await authFetch(`${skillBaseUrl}/${filePath}`);
|
|
2065
|
+
if (fileResponse.ok) return {
|
|
2066
|
+
path: filePath,
|
|
2067
|
+
content: await fileResponse.text()
|
|
2068
|
+
};
|
|
2069
|
+
} catch (err) {
|
|
2070
|
+
if (err instanceof AuthRequiredError) throw err;
|
|
2071
|
+
}
|
|
2072
|
+
return null;
|
|
2073
|
+
});
|
|
2074
|
+
const fileResults = await Promise.all(filePromises);
|
|
2075
|
+
for (const result of fileResults) if (result) files.set(result.path, result.content);
|
|
2076
|
+
return {
|
|
2077
|
+
name: data.name,
|
|
2078
|
+
description: data.description,
|
|
2079
|
+
content,
|
|
2080
|
+
installName: entry.name,
|
|
2081
|
+
sourceUrl: skillMdUrl,
|
|
2082
|
+
metadata: data.metadata,
|
|
2083
|
+
files,
|
|
2084
|
+
indexEntry: entry
|
|
2085
|
+
};
|
|
2086
|
+
} catch (err) {
|
|
2087
|
+
if (err instanceof AuthRequiredError) throw err;
|
|
2088
|
+
return null;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
async fetchAllSkills(url) {
|
|
2092
|
+
try {
|
|
2093
|
+
const result = await this.fetchIndex(url);
|
|
2094
|
+
if (!result) return [];
|
|
2095
|
+
const { index, resolvedBaseUrl, resolvedWellKnownPath } = result;
|
|
2096
|
+
const skillPromises = index.skills.map((entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry, resolvedWellKnownPath));
|
|
2097
|
+
return (await Promise.all(skillPromises)).filter((s) => s !== null);
|
|
2098
|
+
} catch (err) {
|
|
2099
|
+
if (err instanceof AuthRequiredError) throw err;
|
|
2100
|
+
return [];
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
toRawUrl(url) {
|
|
2104
|
+
try {
|
|
2105
|
+
const parsed = new URL(url);
|
|
2106
|
+
if (url.toLowerCase().endsWith("/skill.md")) return url;
|
|
2107
|
+
const primaryPath = this.WELL_KNOWN_PATHS[0];
|
|
2108
|
+
const pathMatch = parsed.pathname.match(/\/.well-known\/(?:agent-skills|skills)\/([^/]+)\/?$/);
|
|
2109
|
+
if (pathMatch && pathMatch[1]) {
|
|
2110
|
+
const basePath = parsed.pathname.replace(/\/.well-known\/(?:agent-skills|skills)\/.*$/, "");
|
|
2111
|
+
return `${parsed.protocol}//${parsed.host}${basePath}/${primaryPath}/${pathMatch[1]}/SKILL.md`;
|
|
2112
|
+
}
|
|
2113
|
+
const basePath = parsed.pathname.replace(/\/$/, "");
|
|
2114
|
+
return `${parsed.protocol}//${parsed.host}${basePath}/${primaryPath}/${this.INDEX_FILE}`;
|
|
2115
|
+
} catch {
|
|
2116
|
+
return url;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
getSourceIdentifier(url) {
|
|
2120
|
+
try {
|
|
2121
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
2122
|
+
} catch {
|
|
2123
|
+
return "unknown";
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
async hasSkillsIndex(url) {
|
|
2127
|
+
return await this.fetchIndex(url) !== null;
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
const wellKnownProvider = new WellKnownProvider();
|
|
2131
|
+
const AGENTS_DIR$1 = ".agents";
|
|
2132
|
+
const LOCK_FILE$1 = ".skill-lock.json";
|
|
2133
|
+
const CURRENT_VERSION$1 = 3;
|
|
2134
|
+
function getSkillLockPath$1() {
|
|
2135
|
+
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
2136
|
+
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE$1);
|
|
2137
|
+
return join(homedir(), AGENTS_DIR$1, LOCK_FILE$1);
|
|
2138
|
+
}
|
|
2139
|
+
async function readSkillLock$1() {
|
|
2140
|
+
const lockPath = getSkillLockPath$1();
|
|
2141
|
+
try {
|
|
2142
|
+
const content = await readFile(lockPath, "utf-8");
|
|
2143
|
+
const parsed = JSON.parse(content);
|
|
2144
|
+
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLockFile();
|
|
2145
|
+
if (parsed.version < CURRENT_VERSION$1) return createEmptyLockFile();
|
|
2146
|
+
return parsed;
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
return createEmptyLockFile();
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
async function writeSkillLock(lock) {
|
|
2152
|
+
const lockPath = getSkillLockPath$1();
|
|
2153
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
2154
|
+
await writeFile(lockPath, JSON.stringify(lock, null, 2), "utf-8");
|
|
2155
|
+
}
|
|
2156
|
+
function getGitHubToken() {
|
|
2157
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
2158
|
+
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
2159
|
+
try {
|
|
2160
|
+
const token = execSync("gh auth token", {
|
|
2161
|
+
encoding: "utf-8",
|
|
2162
|
+
stdio: [
|
|
2163
|
+
"pipe",
|
|
2164
|
+
"pipe",
|
|
2165
|
+
"pipe"
|
|
2166
|
+
]
|
|
2167
|
+
}).trim();
|
|
2168
|
+
if (token) return token;
|
|
2169
|
+
} catch {}
|
|
2170
|
+
return null;
|
|
2171
|
+
}
|
|
2172
|
+
async function fetchSkillFolderHash(ownerRepo, skillPath, token) {
|
|
2173
|
+
let folderPath = skillPath.replace(/\\/g, "/");
|
|
2174
|
+
if (folderPath.endsWith("/SKILL.md")) folderPath = folderPath.slice(0, -9);
|
|
2175
|
+
else if (folderPath.endsWith("SKILL.md")) folderPath = folderPath.slice(0, -8);
|
|
2176
|
+
if (folderPath.endsWith("/")) folderPath = folderPath.slice(0, -1);
|
|
2177
|
+
for (const branch of ["main", "master"]) try {
|
|
2178
|
+
const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${branch}?recursive=1`;
|
|
2179
|
+
const headers = {
|
|
2180
|
+
Accept: "application/vnd.github.v3+json",
|
|
2181
|
+
"User-Agent": "skills-cli"
|
|
2182
|
+
};
|
|
2183
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
2184
|
+
const response = await fetch(url, { headers });
|
|
2185
|
+
if (!response.ok) continue;
|
|
2186
|
+
const data = await response.json();
|
|
2187
|
+
if (!folderPath) return data.sha;
|
|
2188
|
+
const folderEntry = data.tree.find((entry) => entry.type === "tree" && entry.path === folderPath);
|
|
2189
|
+
if (folderEntry) return folderEntry.sha;
|
|
2190
|
+
} catch {
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
return null;
|
|
2194
|
+
}
|
|
2195
|
+
async function addSkillToLock(skillName, entry) {
|
|
2196
|
+
const lock = await readSkillLock$1();
|
|
2197
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2198
|
+
const existingEntry = lock.skills[skillName];
|
|
2199
|
+
lock.skills[skillName] = {
|
|
2200
|
+
...entry,
|
|
2201
|
+
installedAt: existingEntry?.installedAt ?? now,
|
|
2202
|
+
updatedAt: now
|
|
2203
|
+
};
|
|
2204
|
+
await writeSkillLock(lock);
|
|
2205
|
+
}
|
|
2206
|
+
async function removeSkillFromLock(skillName) {
|
|
2207
|
+
const lock = await readSkillLock$1();
|
|
2208
|
+
if (!(skillName in lock.skills)) return false;
|
|
2209
|
+
delete lock.skills[skillName];
|
|
2210
|
+
await writeSkillLock(lock);
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2213
|
+
async function getSkillFromLock(skillName) {
|
|
2214
|
+
return (await readSkillLock$1()).skills[skillName] ?? null;
|
|
2215
|
+
}
|
|
2216
|
+
async function getAllLockedSkills() {
|
|
2217
|
+
return (await readSkillLock$1()).skills;
|
|
2218
|
+
}
|
|
2219
|
+
function createEmptyLockFile() {
|
|
2220
|
+
return {
|
|
2221
|
+
version: CURRENT_VERSION$1,
|
|
2222
|
+
skills: {},
|
|
2223
|
+
dismissed: {}
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
async function isPromptDismissed(promptKey) {
|
|
2227
|
+
return (await readSkillLock$1()).dismissed?.[promptKey] === true;
|
|
2228
|
+
}
|
|
2229
|
+
async function dismissPrompt(promptKey) {
|
|
2230
|
+
const lock = await readSkillLock$1();
|
|
2231
|
+
if (!lock.dismissed) lock.dismissed = {};
|
|
2232
|
+
lock.dismissed[promptKey] = true;
|
|
2233
|
+
await writeSkillLock(lock);
|
|
2234
|
+
}
|
|
2235
|
+
async function getLastSelectedAgents() {
|
|
2236
|
+
return (await readSkillLock$1()).lastSelectedAgents;
|
|
2237
|
+
}
|
|
2238
|
+
async function saveSelectedAgents(agents) {
|
|
2239
|
+
const lock = await readSkillLock$1();
|
|
2240
|
+
lock.lastSelectedAgents = agents;
|
|
2241
|
+
await writeSkillLock(lock);
|
|
2242
|
+
}
|
|
2243
|
+
const LOCAL_LOCK_FILE = "skills-lock.json";
|
|
2244
|
+
const CURRENT_VERSION = 1;
|
|
2245
|
+
function getLocalLockPath(cwd) {
|
|
2246
|
+
return join(cwd || process.cwd(), LOCAL_LOCK_FILE);
|
|
2247
|
+
}
|
|
2248
|
+
async function readLocalLock(cwd) {
|
|
2249
|
+
const lockPath = getLocalLockPath(cwd);
|
|
2250
|
+
try {
|
|
2251
|
+
const content = await readFile(lockPath, "utf-8");
|
|
2252
|
+
const parsed = JSON.parse(content);
|
|
2253
|
+
if (typeof parsed.version !== "number" || !parsed.skills) return createEmptyLocalLock();
|
|
2254
|
+
if (parsed.version < CURRENT_VERSION) return createEmptyLocalLock();
|
|
2255
|
+
return parsed;
|
|
2256
|
+
} catch {
|
|
2257
|
+
return createEmptyLocalLock();
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
async function writeLocalLock(lock, cwd) {
|
|
2261
|
+
const lockPath = getLocalLockPath(cwd);
|
|
2262
|
+
const sortedSkills = {};
|
|
2263
|
+
for (const key of Object.keys(lock.skills).sort()) sortedSkills[key] = lock.skills[key];
|
|
2264
|
+
const sorted = {
|
|
2265
|
+
version: lock.version,
|
|
2266
|
+
skills: sortedSkills
|
|
2267
|
+
};
|
|
2268
|
+
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
2269
|
+
}
|
|
2270
|
+
async function computeSkillFolderHash(skillDir) {
|
|
2271
|
+
const files = [];
|
|
2272
|
+
await collectFiles(skillDir, skillDir, files);
|
|
2273
|
+
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
2274
|
+
const hash = createHash("sha256");
|
|
2275
|
+
for (const file of files) {
|
|
2276
|
+
hash.update(`${Buffer.byteLength(file.relativePath)}:${file.relativePath}`);
|
|
2277
|
+
hash.update(`${file.content.length}:`);
|
|
2278
|
+
hash.update(file.content);
|
|
2279
|
+
}
|
|
2280
|
+
return hash.digest("hex");
|
|
2281
|
+
}
|
|
2282
|
+
async function collectFiles(baseDir, currentDir, results) {
|
|
2283
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
2284
|
+
await Promise.all(entries.map(async (entry) => {
|
|
2285
|
+
const fullPath = join(currentDir, entry.name);
|
|
2286
|
+
if (entry.isDirectory()) {
|
|
2287
|
+
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
2288
|
+
await collectFiles(baseDir, fullPath, results);
|
|
2289
|
+
} else if (entry.isFile()) {
|
|
2290
|
+
const content = await readFile(fullPath);
|
|
2291
|
+
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
2292
|
+
results.push({
|
|
2293
|
+
relativePath,
|
|
2294
|
+
content
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
}));
|
|
2298
|
+
}
|
|
2299
|
+
async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
2300
|
+
const lock = await readLocalLock(cwd);
|
|
2301
|
+
lock.skills[skillName] = entry;
|
|
2302
|
+
await writeLocalLock(lock, cwd);
|
|
2303
|
+
}
|
|
2304
|
+
function createEmptyLocalLock() {
|
|
2305
|
+
return {
|
|
2306
|
+
version: CURRENT_VERSION,
|
|
2307
|
+
skills: {}
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
var version$1 = "1.5.0";
|
|
2311
|
+
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
2312
|
+
async function isSourcePrivate(source) {
|
|
2313
|
+
const ownerRepo = parseOwnerRepo(source);
|
|
2314
|
+
if (!ownerRepo) return false;
|
|
2315
|
+
return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
|
|
2316
|
+
}
|
|
2317
|
+
function initTelemetry(version) {
|
|
2318
|
+
setVersion(version);
|
|
2319
|
+
}
|
|
2320
|
+
function riskLabel(risk) {
|
|
2321
|
+
switch (risk) {
|
|
2322
|
+
case "critical": return import_picocolors.default.red(import_picocolors.default.bold("Critical Risk"));
|
|
2323
|
+
case "high": return import_picocolors.default.red("High Risk");
|
|
2324
|
+
case "medium": return import_picocolors.default.yellow("Med Risk");
|
|
2325
|
+
case "low": return import_picocolors.default.green("Low Risk");
|
|
2326
|
+
case "safe": return import_picocolors.default.green("Safe");
|
|
2327
|
+
default: return import_picocolors.default.dim("--");
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
function socketLabel(audit) {
|
|
2331
|
+
if (!audit) return import_picocolors.default.dim("--");
|
|
2332
|
+
const count = audit.alerts ?? 0;
|
|
2333
|
+
return count > 0 ? import_picocolors.default.red(`${count} alert${count !== 1 ? "s" : ""}`) : import_picocolors.default.green("0 alerts");
|
|
2334
|
+
}
|
|
2335
|
+
function padEnd(str, width) {
|
|
2336
|
+
const visible = str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2337
|
+
const pad = Math.max(0, width - visible.length);
|
|
2338
|
+
return str + " ".repeat(pad);
|
|
2339
|
+
}
|
|
2340
|
+
function buildSecurityLines(auditData, skills, source) {
|
|
2341
|
+
if (!auditData) return [];
|
|
2342
|
+
if (!skills.some((s) => {
|
|
2343
|
+
const data = auditData[s.slug];
|
|
2344
|
+
return data && Object.keys(data).length > 0;
|
|
2345
|
+
})) return [];
|
|
2346
|
+
const nameWidth = Math.min(Math.max(...skills.map((s) => s.displayName.length)), 36);
|
|
2347
|
+
const lines = [];
|
|
2348
|
+
const header = padEnd("", nameWidth + 2) + padEnd(import_picocolors.default.dim("Gen"), 18) + padEnd(import_picocolors.default.dim("Socket"), 18) + import_picocolors.default.dim("Snyk");
|
|
2349
|
+
lines.push(header);
|
|
2350
|
+
for (const skill of skills) {
|
|
2351
|
+
const data = auditData[skill.slug];
|
|
2352
|
+
const name = skill.displayName.length > nameWidth ? skill.displayName.slice(0, nameWidth - 1) + "…" : skill.displayName;
|
|
2353
|
+
const ath = data?.ath ? riskLabel(data.ath.risk) : import_picocolors.default.dim("--");
|
|
2354
|
+
const socket = data?.socket ? socketLabel(data.socket) : import_picocolors.default.dim("--");
|
|
2355
|
+
const snyk = data?.snyk ? riskLabel(data.snyk.risk) : import_picocolors.default.dim("--");
|
|
2356
|
+
lines.push(padEnd(import_picocolors.default.cyan(name), nameWidth + 2) + padEnd(ath, 18) + padEnd(socket, 18) + snyk);
|
|
2357
|
+
}
|
|
2358
|
+
lines.push("");
|
|
2359
|
+
lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
|
|
2360
|
+
return lines;
|
|
2361
|
+
}
|
|
2362
|
+
function shortenPath$2(fullPath, cwd) {
|
|
2363
|
+
const home = homedir();
|
|
2364
|
+
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
2365
|
+
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
2366
|
+
return fullPath;
|
|
2367
|
+
}
|
|
2368
|
+
function formatList$1(items, maxShow = 5) {
|
|
2369
|
+
if (items.length <= maxShow) return items.join(", ");
|
|
2370
|
+
const shown = items.slice(0, maxShow);
|
|
2371
|
+
const remaining = items.length - maxShow;
|
|
2372
|
+
return `${shown.join(", ")} +${remaining} more`;
|
|
2373
|
+
}
|
|
2374
|
+
function splitAgentsByType(agentTypes) {
|
|
2375
|
+
const universal = [];
|
|
2376
|
+
const symlinked = [];
|
|
2377
|
+
for (const a of agentTypes) if (isUniversalAgent(a)) universal.push(agents[a].displayName);
|
|
2378
|
+
else symlinked.push(agents[a].displayName);
|
|
2379
|
+
return {
|
|
2380
|
+
universal,
|
|
2381
|
+
symlinked
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
function buildAgentSummaryLines(targetAgents, installMode) {
|
|
2385
|
+
const lines = [];
|
|
2386
|
+
const { universal, symlinked } = splitAgentsByType(targetAgents);
|
|
2387
|
+
if (installMode === "symlink") {
|
|
2388
|
+
if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
|
|
2389
|
+
if (symlinked.length > 0) lines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(symlinked)}`);
|
|
2390
|
+
} else {
|
|
2391
|
+
const allNames = targetAgents.map((a) => agents[a].displayName);
|
|
2392
|
+
lines.push(` ${import_picocolors.default.dim("copy →")} ${formatList$1(allNames)}`);
|
|
2393
|
+
}
|
|
2394
|
+
return lines;
|
|
2395
|
+
}
|
|
2396
|
+
function ensureUniversalAgents(targetAgents) {
|
|
2397
|
+
const universalAgents = getUniversalAgents();
|
|
2398
|
+
const result = [...targetAgents];
|
|
2399
|
+
for (const ua of universalAgents) if (!result.includes(ua)) result.push(ua);
|
|
2400
|
+
return result;
|
|
2401
|
+
}
|
|
2402
|
+
function buildResultLines(results, targetAgents) {
|
|
2403
|
+
const lines = [];
|
|
2404
|
+
const { universal, symlinked: symlinkAgents } = splitAgentsByType(targetAgents);
|
|
2405
|
+
const successfulSymlinks = results.filter((r) => !r.symlinkFailed && !universal.includes(r.agent)).map((r) => r.agent);
|
|
2406
|
+
const failedSymlinks = results.filter((r) => r.symlinkFailed).map((r) => r.agent);
|
|
2407
|
+
if (universal.length > 0) lines.push(` ${import_picocolors.default.green("universal:")} ${formatList$1(universal)}`);
|
|
2408
|
+
if (successfulSymlinks.length > 0) lines.push(` ${import_picocolors.default.dim("symlinked:")} ${formatList$1(successfulSymlinks)}`);
|
|
2409
|
+
if (failedSymlinks.length > 0) lines.push(` ${import_picocolors.default.yellow("copied:")} ${formatList$1(failedSymlinks)}`);
|
|
2410
|
+
return lines;
|
|
2411
|
+
}
|
|
2412
|
+
function multiselect(opts) {
|
|
2413
|
+
return fe({
|
|
2414
|
+
...opts,
|
|
2415
|
+
options: opts.options,
|
|
2416
|
+
message: `${opts.message} ${import_picocolors.default.dim("(space to toggle)")}`
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
async function promptForAgents(message, choices) {
|
|
2420
|
+
let lastSelected;
|
|
2421
|
+
try {
|
|
2422
|
+
lastSelected = await getLastSelectedAgents();
|
|
2423
|
+
} catch {}
|
|
2424
|
+
const validAgents = choices.map((c) => c.value);
|
|
2425
|
+
const defaultValues = [
|
|
2426
|
+
"claude-code",
|
|
2427
|
+
"opencode",
|
|
2428
|
+
"codex"
|
|
2429
|
+
].filter((a) => validAgents.includes(a));
|
|
2430
|
+
let initialValues = [];
|
|
2431
|
+
if (lastSelected && lastSelected.length > 0) initialValues = lastSelected.filter((a) => validAgents.includes(a));
|
|
2432
|
+
if (initialValues.length === 0) initialValues = defaultValues;
|
|
2433
|
+
const selected = await searchMultiselect({
|
|
2434
|
+
message,
|
|
2435
|
+
items: choices,
|
|
2436
|
+
initialSelected: initialValues,
|
|
2437
|
+
required: true
|
|
2438
|
+
});
|
|
2439
|
+
if (!isCancelled$1(selected)) try {
|
|
2440
|
+
await saveSelectedAgents(selected);
|
|
2441
|
+
} catch {}
|
|
2442
|
+
return selected;
|
|
2443
|
+
}
|
|
2444
|
+
async function selectAgentsInteractive(options) {
|
|
2445
|
+
const supportsGlobalFilter = (a) => !options.global || agents[a].globalSkillsDir;
|
|
2446
|
+
const universalAgents = getUniversalAgents().filter(supportsGlobalFilter);
|
|
2447
|
+
const otherAgents = getNonUniversalAgents().filter(supportsGlobalFilter);
|
|
2448
|
+
const universalSection = {
|
|
2449
|
+
title: "Universal (.agents/skills)",
|
|
2450
|
+
items: universalAgents.map((a) => ({
|
|
2451
|
+
value: a,
|
|
2452
|
+
label: agents[a].displayName
|
|
2453
|
+
}))
|
|
2454
|
+
};
|
|
2455
|
+
const otherChoices = otherAgents.map((a) => ({
|
|
2456
|
+
value: a,
|
|
2457
|
+
label: agents[a].displayName,
|
|
2458
|
+
hint: options.global ? agents[a].globalSkillsDir : agents[a].skillsDir
|
|
2459
|
+
}));
|
|
2460
|
+
let lastSelected;
|
|
2461
|
+
try {
|
|
2462
|
+
lastSelected = await getLastSelectedAgents();
|
|
2463
|
+
} catch {}
|
|
2464
|
+
const selected = await searchMultiselect({
|
|
2465
|
+
message: "Which agents do you want to install to?",
|
|
2466
|
+
items: otherChoices,
|
|
2467
|
+
initialSelected: lastSelected ? lastSelected.filter((a) => otherAgents.includes(a) && !universalAgents.includes(a)) : [],
|
|
2468
|
+
lockedSection: universalSection
|
|
2469
|
+
});
|
|
2470
|
+
if (!isCancelled$1(selected)) try {
|
|
2471
|
+
await saveSelectedAgents(selected);
|
|
2472
|
+
} catch {}
|
|
2473
|
+
return selected;
|
|
2474
|
+
}
|
|
2475
|
+
setVersion(version$1);
|
|
2476
|
+
async function handleWellKnownSkills(source, url, options, spinner) {
|
|
2477
|
+
spinner.start("Discovering skills from well-known endpoint...");
|
|
2478
|
+
const skills = await wellKnownProvider.fetchAllSkills(url);
|
|
2479
|
+
if (skills.length === 0) {
|
|
2480
|
+
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
2481
|
+
Se(import_picocolors.default.red("No skills found at this URL. Make sure the server has a /.well-known/agent-skills/index.json or /.well-known/skills/index.json file."));
|
|
2482
|
+
process.exit(1);
|
|
2483
|
+
}
|
|
2484
|
+
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
2485
|
+
for (const skill of skills) {
|
|
2486
|
+
M.info(`Skill: ${import_picocolors.default.cyan(skill.installName)}`);
|
|
2487
|
+
M.message(import_picocolors.default.dim(skill.description));
|
|
2488
|
+
if (skill.files.size > 1) M.message(import_picocolors.default.dim(` Files: ${Array.from(skill.files.keys()).join(", ")}`));
|
|
2489
|
+
}
|
|
2490
|
+
if (options.list) {
|
|
2491
|
+
console.log();
|
|
2492
|
+
M.step(import_picocolors.default.bold("Available Skills"));
|
|
2493
|
+
for (const skill of skills) {
|
|
2494
|
+
M.message(` ${import_picocolors.default.cyan(skill.installName)}`);
|
|
2495
|
+
M.message(` ${import_picocolors.default.dim(skill.description)}`);
|
|
2496
|
+
if (skill.files.size > 1) M.message(` ${import_picocolors.default.dim(`Files: ${skill.files.size}`)}`);
|
|
2497
|
+
}
|
|
2498
|
+
console.log();
|
|
2499
|
+
Se("Run without --list to install");
|
|
2500
|
+
process.exit(0);
|
|
2501
|
+
}
|
|
2502
|
+
let selectedSkills;
|
|
2503
|
+
if (options.skill?.includes("*")) {
|
|
2504
|
+
selectedSkills = skills;
|
|
2505
|
+
M.info(`Installing all ${skills.length} skills`);
|
|
2506
|
+
} else if (options.skill && options.skill.length > 0) {
|
|
2507
|
+
selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()));
|
|
2508
|
+
if (selectedSkills.length === 0) {
|
|
2509
|
+
M.error(`No matching skills found for: ${options.skill.join(", ")}`);
|
|
2510
|
+
M.info("Available skills:");
|
|
2511
|
+
for (const s of skills) M.message(` - ${s.installName}`);
|
|
2512
|
+
process.exit(1);
|
|
2513
|
+
}
|
|
2514
|
+
} else if (skills.length === 1) {
|
|
2515
|
+
selectedSkills = skills;
|
|
2516
|
+
const firstSkill = skills[0];
|
|
2517
|
+
M.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
|
|
2518
|
+
} else if (options.yes) {
|
|
2519
|
+
selectedSkills = skills;
|
|
2520
|
+
M.info(`Installing all ${skills.length} skills`);
|
|
2521
|
+
} else {
|
|
2522
|
+
const selected = await multiselect({
|
|
2523
|
+
message: "Select skills to install",
|
|
2524
|
+
options: skills.map((s) => ({
|
|
2525
|
+
value: s,
|
|
2526
|
+
label: s.installName,
|
|
2527
|
+
hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
|
|
2528
|
+
})),
|
|
2529
|
+
required: true
|
|
2530
|
+
});
|
|
2531
|
+
if (pD(selected)) {
|
|
2532
|
+
xe("Installation cancelled");
|
|
2533
|
+
process.exit(0);
|
|
2534
|
+
}
|
|
2535
|
+
selectedSkills = selected;
|
|
2536
|
+
}
|
|
2537
|
+
if (options.dryRun) {
|
|
2538
|
+
spinner.start("Validating well-known files for dry-run...");
|
|
2539
|
+
const fetchPromises = selectedSkills.map(async (s) => {
|
|
2540
|
+
const skillMdUrl = s.files.get("SKILL.md") || s.files.get("skill.md");
|
|
2541
|
+
if (skillMdUrl) {
|
|
2542
|
+
if (!(await fetch(skillMdUrl)).ok) throw new Error(`Dry-run validation failed: Could not fetch ${skillMdUrl}`);
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2545
|
+
await Promise.all(fetchPromises);
|
|
2546
|
+
spinner.stop(import_picocolors.default.green(`Dry-run validation passed for ${selectedSkills.length} skill(s)`));
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
let targetAgents;
|
|
2550
|
+
const validAgents = Object.keys(agents);
|
|
2551
|
+
if (options.agent?.includes("*")) {
|
|
2552
|
+
targetAgents = validAgents;
|
|
2553
|
+
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
2554
|
+
} else if (options.agent && options.agent.length > 0) {
|
|
2555
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
2556
|
+
if (invalidAgents.length > 0) {
|
|
2557
|
+
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
2558
|
+
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
2559
|
+
process.exit(1);
|
|
2560
|
+
}
|
|
2561
|
+
targetAgents = options.agent;
|
|
2562
|
+
} else {
|
|
2563
|
+
spinner.start("Loading agents...");
|
|
2564
|
+
const installedAgents = await detectInstalledAgents();
|
|
2565
|
+
const totalAgents = Object.keys(agents).length;
|
|
2566
|
+
spinner.stop(`${totalAgents} agents`);
|
|
2567
|
+
if (installedAgents.length === 0) if (options.yes) {
|
|
2568
|
+
targetAgents = validAgents;
|
|
2569
|
+
M.info("Installing to all agents");
|
|
2570
|
+
} else {
|
|
2571
|
+
M.info("Select agents to install skills to");
|
|
2572
|
+
const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
|
|
2573
|
+
value: key,
|
|
2574
|
+
label: config.displayName
|
|
2575
|
+
})));
|
|
2576
|
+
if (pD(selected)) {
|
|
2577
|
+
xe("Installation cancelled");
|
|
2578
|
+
process.exit(0);
|
|
2579
|
+
}
|
|
2580
|
+
targetAgents = selected;
|
|
2581
|
+
}
|
|
2582
|
+
else if (installedAgents.length === 1 || options.yes) {
|
|
2583
|
+
targetAgents = ensureUniversalAgents(installedAgents);
|
|
2584
|
+
if (installedAgents.length === 1) {
|
|
2585
|
+
const firstAgent = installedAgents[0];
|
|
2586
|
+
M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
|
|
2587
|
+
} else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
|
|
2588
|
+
} else {
|
|
2589
|
+
const selected = await selectAgentsInteractive({ global: options.global });
|
|
2590
|
+
if (pD(selected)) {
|
|
2591
|
+
xe("Installation cancelled");
|
|
2592
|
+
process.exit(0);
|
|
2593
|
+
}
|
|
2594
|
+
targetAgents = selected;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
let installGlobally = options.global ?? false;
|
|
2598
|
+
const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
|
|
2599
|
+
if (options.global === void 0 && !options.yes && supportsGlobal) {
|
|
2600
|
+
const scope = await ve({
|
|
2601
|
+
message: "Installation scope",
|
|
2602
|
+
options: [{
|
|
2603
|
+
value: false,
|
|
2604
|
+
label: "Project",
|
|
2605
|
+
hint: "Install in current directory (committed with your project)"
|
|
2606
|
+
}, {
|
|
2607
|
+
value: true,
|
|
2608
|
+
label: "Global",
|
|
2609
|
+
hint: "Install in home directory (available across all projects)"
|
|
2610
|
+
}]
|
|
2611
|
+
});
|
|
2612
|
+
if (pD(scope)) {
|
|
2613
|
+
xe("Installation cancelled");
|
|
2614
|
+
process.exit(0);
|
|
2615
|
+
}
|
|
2616
|
+
installGlobally = scope;
|
|
2617
|
+
}
|
|
2618
|
+
let installMode = options.copy ? "copy" : "symlink";
|
|
2619
|
+
const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
|
|
2620
|
+
if (!options.copy && !options.yes && uniqueDirs.size > 1) {
|
|
2621
|
+
const modeChoice = await ve({
|
|
2622
|
+
message: "Installation method",
|
|
2623
|
+
options: [{
|
|
2624
|
+
value: "symlink",
|
|
2625
|
+
label: "Symlink (Recommended)",
|
|
2626
|
+
hint: "Single source of truth, easy updates"
|
|
2627
|
+
}, {
|
|
2628
|
+
value: "copy",
|
|
2629
|
+
label: "Copy to all agents",
|
|
2630
|
+
hint: "Independent copies for each agent"
|
|
2631
|
+
}]
|
|
2632
|
+
});
|
|
2633
|
+
if (pD(modeChoice)) {
|
|
2634
|
+
xe("Installation cancelled");
|
|
2635
|
+
process.exit(0);
|
|
2636
|
+
}
|
|
2637
|
+
installMode = modeChoice;
|
|
2638
|
+
} else if (uniqueDirs.size <= 1) installMode = "copy";
|
|
2639
|
+
const cwd = process.cwd();
|
|
2640
|
+
const summaryLines = [];
|
|
2641
|
+
targetAgents.map((a) => agents[a].displayName);
|
|
2642
|
+
const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
|
|
2643
|
+
skillName: skill.installName,
|
|
2644
|
+
agent,
|
|
2645
|
+
installed: await isSkillInstalled(skill.installName, agent, { global: installGlobally })
|
|
2646
|
+
}))));
|
|
2647
|
+
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
2648
|
+
for (const { skillName, agent, installed } of overwriteChecks) {
|
|
2649
|
+
if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
|
|
2650
|
+
overwriteStatus.get(skillName).set(agent, installed);
|
|
2651
|
+
}
|
|
2652
|
+
for (const skill of selectedSkills) {
|
|
2653
|
+
if (summaryLines.length > 0) summaryLines.push("");
|
|
2654
|
+
const shortCanonical = shortenPath$2(getCanonicalPath(skill.installName, { global: installGlobally }), cwd);
|
|
2655
|
+
summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
|
|
2656
|
+
summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
|
|
2657
|
+
if (skill.files.size > 1) summaryLines.push(` ${import_picocolors.default.dim("files:")} ${skill.files.size}`);
|
|
2658
|
+
const skillOverwrites = overwriteStatus.get(skill.installName);
|
|
2659
|
+
const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
|
|
2660
|
+
if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
|
|
2661
|
+
}
|
|
2662
|
+
console.log();
|
|
2663
|
+
Me(summaryLines.join("\n"), "Installation Summary");
|
|
2664
|
+
if (!options.yes) {
|
|
2665
|
+
const confirmed = await ye({ message: "Proceed with installation?" });
|
|
2666
|
+
if (pD(confirmed) || !confirmed) {
|
|
2667
|
+
xe("Installation cancelled");
|
|
2668
|
+
process.exit(0);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
spinner.start("Installing skills...");
|
|
2672
|
+
const results = [];
|
|
2673
|
+
for (const skill of selectedSkills) for (const agent of targetAgents) {
|
|
2674
|
+
const result = await installWellKnownSkillForAgent(skill, agent, {
|
|
2675
|
+
global: installGlobally,
|
|
2676
|
+
mode: installMode
|
|
2677
|
+
});
|
|
2678
|
+
results.push({
|
|
2679
|
+
skill: skill.installName,
|
|
2680
|
+
agent: agents[agent].displayName,
|
|
2681
|
+
...result
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
spinner.stop("Installation complete");
|
|
2685
|
+
console.log();
|
|
2686
|
+
const successful = results.filter((r) => r.success);
|
|
2687
|
+
const failed = results.filter((r) => !r.success);
|
|
2688
|
+
const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
|
|
2689
|
+
const skillFiles = {};
|
|
2690
|
+
for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
|
|
2691
|
+
if (await isSourcePrivate(sourceIdentifier) !== true) track({
|
|
2692
|
+
event: "install",
|
|
2693
|
+
source: sourceIdentifier,
|
|
2694
|
+
skills: selectedSkills.map((s) => s.installName).join(","),
|
|
2695
|
+
agents: targetAgents.join(","),
|
|
2696
|
+
...installGlobally && { global: "1" },
|
|
2697
|
+
skillFiles: JSON.stringify(skillFiles),
|
|
2698
|
+
sourceType: "well-known"
|
|
2699
|
+
});
|
|
2700
|
+
if (successful.length > 0 && installGlobally) {
|
|
2701
|
+
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
2702
|
+
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
2703
|
+
await addSkillToLock(skill.installName, {
|
|
2704
|
+
source: sourceIdentifier,
|
|
2705
|
+
sourceType: "well-known",
|
|
2706
|
+
sourceUrl: skill.sourceUrl,
|
|
2707
|
+
skillFolderHash: ""
|
|
2708
|
+
});
|
|
2709
|
+
} catch {}
|
|
2710
|
+
}
|
|
2711
|
+
if (successful.length > 0 && !installGlobally) {
|
|
2712
|
+
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
2713
|
+
for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
|
|
2714
|
+
const matchingResult = successful.find((r) => r.skill === skill.installName);
|
|
2715
|
+
const installDir = matchingResult?.canonicalPath || matchingResult?.path;
|
|
2716
|
+
if (installDir) {
|
|
2717
|
+
const computedHash = await computeSkillFolderHash(installDir);
|
|
2718
|
+
await addSkillToLocalLock(skill.installName, {
|
|
2719
|
+
source: sourceIdentifier,
|
|
2720
|
+
sourceType: "well-known",
|
|
2721
|
+
computedHash
|
|
2722
|
+
}, cwd);
|
|
2723
|
+
}
|
|
2724
|
+
} catch {}
|
|
2725
|
+
}
|
|
2726
|
+
if (successful.length > 0) {
|
|
2727
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
2728
|
+
for (const r of successful) {
|
|
2729
|
+
const skillResults = bySkill.get(r.skill) || [];
|
|
2730
|
+
skillResults.push(r);
|
|
2731
|
+
bySkill.set(r.skill, skillResults);
|
|
2732
|
+
}
|
|
2733
|
+
const skillCount = bySkill.size;
|
|
2734
|
+
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
2735
|
+
const copiedAgents = symlinkFailures.map((r) => r.agent);
|
|
2736
|
+
const resultLines = [];
|
|
2737
|
+
for (const [skillName, skillResults] of bySkill) {
|
|
2738
|
+
const firstResult = skillResults[0];
|
|
2739
|
+
if (firstResult.mode === "copy") {
|
|
2740
|
+
resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim("(copied)")}`);
|
|
2741
|
+
for (const r of skillResults) {
|
|
2742
|
+
const shortPath = shortenPath$2(r.path, cwd);
|
|
2743
|
+
resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
|
|
2744
|
+
}
|
|
2745
|
+
} else {
|
|
2746
|
+
if (firstResult.canonicalPath) {
|
|
2747
|
+
const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
|
|
2748
|
+
resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
|
|
2749
|
+
} else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName}`);
|
|
2750
|
+
resultLines.push(...buildResultLines(skillResults, targetAgents));
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
|
|
2754
|
+
Me(resultLines.join("\n"), title);
|
|
2755
|
+
if (symlinkFailures.length > 0) {
|
|
2756
|
+
M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
|
|
2757
|
+
M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
if (failed.length > 0) {
|
|
2761
|
+
console.log();
|
|
2762
|
+
M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
|
|
2763
|
+
for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
|
|
2764
|
+
}
|
|
2765
|
+
console.log();
|
|
2766
|
+
Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
|
|
2767
|
+
await promptForFindSkills(options, targetAgents);
|
|
2768
|
+
}
|
|
2769
|
+
async function runAdd(args, options = {}) {
|
|
2770
|
+
const source = args[0];
|
|
2771
|
+
let installTipShown = false;
|
|
2772
|
+
const showInstallTip = () => {
|
|
2773
|
+
if (installTipShown) return;
|
|
2774
|
+
M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
|
|
2775
|
+
installTipShown = true;
|
|
2776
|
+
};
|
|
2777
|
+
if (!source) {
|
|
2778
|
+
console.log();
|
|
2779
|
+
console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
|
|
2780
|
+
console.log();
|
|
2781
|
+
console.log(import_picocolors.default.dim(" Usage:"));
|
|
2782
|
+
console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("<source>")} ${import_picocolors.default.dim("[options]")}`);
|
|
2783
|
+
console.log();
|
|
2784
|
+
console.log(import_picocolors.default.dim(" Example:"));
|
|
2785
|
+
console.log(` ${import_picocolors.default.cyan("npx skills add")} ${import_picocolors.default.yellow("vercel-labs/agent-skills")}`);
|
|
2786
|
+
console.log();
|
|
2787
|
+
process.exit(1);
|
|
2788
|
+
}
|
|
2789
|
+
if (options.all) {
|
|
2790
|
+
options.skill = ["*"];
|
|
2791
|
+
options.agent = ["*"];
|
|
2792
|
+
options.yes = true;
|
|
2793
|
+
}
|
|
2794
|
+
console.log();
|
|
2795
|
+
Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
|
|
2796
|
+
if (!process.stdin.isTTY) showInstallTip();
|
|
2797
|
+
let tempDir = null;
|
|
2798
|
+
try {
|
|
2799
|
+
const spinner = Y();
|
|
2800
|
+
spinner.start("Parsing source...");
|
|
2801
|
+
let parsed = parseSource(source);
|
|
2802
|
+
if (options.ref) parsed.ref = options.ref;
|
|
2803
|
+
if (parsed.type === "registry") {
|
|
2804
|
+
const skillName = parsed.skillName;
|
|
2805
|
+
if (!skillName || /^\d+$/.test(skillName)) {
|
|
2806
|
+
spinner.stop(import_picocolors.default.red("Invalid skill name"));
|
|
2807
|
+
Se(import_picocolors.default.red(`Invalid registry skill name: "${skillName ?? ""}". A bare numeric index is not a valid skill name — this usually means a malformed group definition was iterated by position. Check that groups use the shape { "version": ..., "include": [...] } in your skills.json and that the registry group maps sources to refs.`));
|
|
2808
|
+
process.exit(1);
|
|
2809
|
+
}
|
|
2810
|
+
parsed = {
|
|
2811
|
+
type: "well-known",
|
|
2812
|
+
url: `${DEFAULT_REGISTRY_URL}/.well-known/agent-skills/${skillName}${parsed.ref ? `@${parsed.ref}` : ""}`
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
spinner.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
|
|
2816
|
+
if (parsed.type === "well-known") {
|
|
2817
|
+
await handleWellKnownSkills(source, parsed.url, options, spinner);
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
let skillsDir;
|
|
2821
|
+
if (parsed.type === "local") {
|
|
2822
|
+
spinner.start("Validating local path...");
|
|
2823
|
+
if (!existsSync(parsed.localPath)) {
|
|
2824
|
+
spinner.stop(import_picocolors.default.red("Path not found"));
|
|
2825
|
+
Se(import_picocolors.default.red(`Local path does not exist: ${parsed.localPath}`));
|
|
2826
|
+
process.exit(1);
|
|
2827
|
+
}
|
|
2828
|
+
skillsDir = parsed.localPath;
|
|
2829
|
+
spinner.stop("Local path validated");
|
|
2830
|
+
} else {
|
|
2831
|
+
spinner.start("Cloning repository...");
|
|
2832
|
+
tempDir = await cloneRepo(parsed.url, parsed.ref);
|
|
2833
|
+
skillsDir = tempDir;
|
|
2834
|
+
spinner.stop("Repository cloned");
|
|
2835
|
+
}
|
|
2836
|
+
if (parsed.skillFilter) {
|
|
2837
|
+
options.skill = options.skill || [];
|
|
2838
|
+
if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
|
|
2839
|
+
}
|
|
2840
|
+
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
2841
|
+
spinner.start("Discovering skills...");
|
|
2842
|
+
const skills = await discoverSkills(skillsDir, parsed.subpath, {
|
|
2843
|
+
includeInternal,
|
|
2844
|
+
fullDepth: options.fullDepth
|
|
2845
|
+
});
|
|
2846
|
+
if (skills.length === 0) {
|
|
2847
|
+
spinner.stop(import_picocolors.default.red("No skills found"));
|
|
2848
|
+
Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
|
|
2849
|
+
await cleanup(tempDir);
|
|
2850
|
+
process.exit(1);
|
|
2851
|
+
}
|
|
2852
|
+
spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
|
|
2853
|
+
if (options.list) {
|
|
2854
|
+
console.log();
|
|
2855
|
+
M.step(import_picocolors.default.bold("Available Skills"));
|
|
2856
|
+
const groupedSkills = {};
|
|
2857
|
+
const ungroupedSkills = [];
|
|
2858
|
+
for (const skill of skills) if (skill.pluginName) {
|
|
2859
|
+
const group = skill.pluginName;
|
|
2860
|
+
if (!groupedSkills[group]) groupedSkills[group] = [];
|
|
2861
|
+
groupedSkills[group].push(skill);
|
|
2862
|
+
} else ungroupedSkills.push(skill);
|
|
2863
|
+
const sortedGroups = Object.keys(groupedSkills).sort();
|
|
2864
|
+
for (const group of sortedGroups) {
|
|
2865
|
+
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2866
|
+
console.log(import_picocolors.default.bold(title));
|
|
2867
|
+
for (const skill of groupedSkills[group]) {
|
|
2868
|
+
M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`);
|
|
2869
|
+
M.message(` ${import_picocolors.default.dim(skill.description)}`);
|
|
2870
|
+
}
|
|
2871
|
+
console.log();
|
|
2872
|
+
}
|
|
2873
|
+
if (ungroupedSkills.length > 0) {
|
|
2874
|
+
if (sortedGroups.length > 0) console.log(import_picocolors.default.bold("General"));
|
|
2875
|
+
for (const skill of ungroupedSkills) {
|
|
2876
|
+
M.message(` ${import_picocolors.default.cyan(getSkillDisplayName(skill))}`);
|
|
2877
|
+
M.message(` ${import_picocolors.default.dim(skill.description)}`);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
console.log();
|
|
2881
|
+
Se("Use --skill <name> to install specific skills");
|
|
2882
|
+
await cleanup(tempDir);
|
|
2883
|
+
process.exit(0);
|
|
2884
|
+
}
|
|
2885
|
+
let selectedSkills;
|
|
2886
|
+
if (options.skill?.includes("*")) {
|
|
2887
|
+
selectedSkills = skills;
|
|
2888
|
+
M.info(`Installing all ${skills.length} skills`);
|
|
2889
|
+
} else if (options.skill && options.skill.length > 0) {
|
|
2890
|
+
selectedSkills = filterSkills(skills, options.skill);
|
|
2891
|
+
if (selectedSkills.length === 0) {
|
|
2892
|
+
M.error(`No matching skills found for: ${options.skill.join(", ")}`);
|
|
2893
|
+
M.info("Available skills:");
|
|
2894
|
+
for (const s of skills) M.message(` - ${getSkillDisplayName(s)}`);
|
|
2895
|
+
await cleanup(tempDir);
|
|
2896
|
+
process.exit(1);
|
|
2897
|
+
}
|
|
2898
|
+
M.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`);
|
|
2899
|
+
} else if (skills.length === 1) {
|
|
2900
|
+
selectedSkills = skills;
|
|
2901
|
+
const firstSkill = skills[0];
|
|
2902
|
+
M.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(firstSkill))}`);
|
|
2903
|
+
M.message(import_picocolors.default.dim(firstSkill.description));
|
|
2904
|
+
} else if (options.yes) {
|
|
2905
|
+
selectedSkills = skills;
|
|
2906
|
+
M.info(`Installing all ${skills.length} skills`);
|
|
2907
|
+
} else {
|
|
2908
|
+
const sortedSkills = [...skills].sort((a, b) => {
|
|
2909
|
+
if (a.pluginName && !b.pluginName) return -1;
|
|
2910
|
+
if (!a.pluginName && b.pluginName) return 1;
|
|
2911
|
+
if (a.pluginName && b.pluginName && a.pluginName !== b.pluginName) return a.pluginName.localeCompare(b.pluginName);
|
|
2912
|
+
return getSkillDisplayName(a).localeCompare(getSkillDisplayName(b));
|
|
2913
|
+
});
|
|
2914
|
+
const hasGroups = sortedSkills.some((s) => s.pluginName);
|
|
2915
|
+
let selected;
|
|
2916
|
+
if (hasGroups) {
|
|
2917
|
+
const kebabToTitle = (s) => s.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2918
|
+
const grouped = {};
|
|
2919
|
+
for (const s of sortedSkills) {
|
|
2920
|
+
const groupName = s.pluginName ? kebabToTitle(s.pluginName) : "Other";
|
|
2921
|
+
if (!grouped[groupName]) grouped[groupName] = [];
|
|
2922
|
+
grouped[groupName].push({
|
|
2923
|
+
value: s,
|
|
2924
|
+
label: getSkillDisplayName(s),
|
|
2925
|
+
hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
selected = await be({
|
|
2929
|
+
message: `Select skills to install ${import_picocolors.default.dim("(space to toggle)")}`,
|
|
2930
|
+
options: grouped,
|
|
2931
|
+
required: true
|
|
2932
|
+
});
|
|
2933
|
+
} else selected = await multiselect({
|
|
2934
|
+
message: "Select skills to install",
|
|
2935
|
+
options: sortedSkills.map((s) => ({
|
|
2936
|
+
value: s,
|
|
2937
|
+
label: getSkillDisplayName(s),
|
|
2938
|
+
hint: s.description.length > 60 ? s.description.slice(0, 57) + "..." : s.description
|
|
2939
|
+
})),
|
|
2940
|
+
required: true
|
|
2941
|
+
});
|
|
2942
|
+
if (pD(selected)) {
|
|
2943
|
+
xe("Installation cancelled");
|
|
2944
|
+
await cleanup(tempDir);
|
|
2945
|
+
process.exit(0);
|
|
2946
|
+
}
|
|
2947
|
+
selectedSkills = selected;
|
|
2948
|
+
}
|
|
2949
|
+
if (options.dryRun) {
|
|
2950
|
+
M.success(import_picocolors.default.green(`Dry-run validation passed for ${selectedSkills.length} skill(s)`));
|
|
2951
|
+
if (tempDir) await cleanup(tempDir);
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
const ownerRepoForAudit = getOwnerRepo(parsed);
|
|
2955
|
+
const auditPromise = ownerRepoForAudit ? fetchAuditData(ownerRepoForAudit, selectedSkills.map((s) => getSkillDisplayName(s))) : Promise.resolve(null);
|
|
2956
|
+
let targetAgents;
|
|
2957
|
+
const validAgents = Object.keys(agents);
|
|
2958
|
+
if (options.agent?.includes("*")) {
|
|
2959
|
+
targetAgents = validAgents;
|
|
2960
|
+
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
2961
|
+
} else if (options.agent && options.agent.length > 0) {
|
|
2962
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
2963
|
+
if (invalidAgents.length > 0) {
|
|
2964
|
+
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
2965
|
+
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
2966
|
+
await cleanup(tempDir);
|
|
2967
|
+
process.exit(1);
|
|
2968
|
+
}
|
|
2969
|
+
targetAgents = options.agent;
|
|
2970
|
+
} else {
|
|
2971
|
+
spinner.start("Loading agents...");
|
|
2972
|
+
const installedAgents = await detectInstalledAgents();
|
|
2973
|
+
const totalAgents = Object.keys(agents).length;
|
|
2974
|
+
spinner.stop(`${totalAgents} agents`);
|
|
2975
|
+
if (installedAgents.length === 0) if (options.yes) {
|
|
2976
|
+
targetAgents = validAgents;
|
|
2977
|
+
M.info("Installing to all agents");
|
|
2978
|
+
} else {
|
|
2979
|
+
M.info("Select agents to install skills to");
|
|
2980
|
+
const selected = await promptForAgents("Which agents do you want to install to?", Object.entries(agents).map(([key, config]) => ({
|
|
2981
|
+
value: key,
|
|
2982
|
+
label: config.displayName
|
|
2983
|
+
})));
|
|
2984
|
+
if (pD(selected)) {
|
|
2985
|
+
xe("Installation cancelled");
|
|
2986
|
+
await cleanup(tempDir);
|
|
2987
|
+
process.exit(0);
|
|
2988
|
+
}
|
|
2989
|
+
targetAgents = selected;
|
|
2990
|
+
}
|
|
2991
|
+
else if (installedAgents.length === 1 || options.yes) {
|
|
2992
|
+
targetAgents = ensureUniversalAgents(installedAgents);
|
|
2993
|
+
if (installedAgents.length === 1) {
|
|
2994
|
+
const firstAgent = installedAgents[0];
|
|
2995
|
+
M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
|
|
2996
|
+
} else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
|
|
2997
|
+
} else {
|
|
2998
|
+
const selected = await selectAgentsInteractive({ global: options.global });
|
|
2999
|
+
if (pD(selected)) {
|
|
3000
|
+
xe("Installation cancelled");
|
|
3001
|
+
await cleanup(tempDir);
|
|
3002
|
+
process.exit(0);
|
|
3003
|
+
}
|
|
3004
|
+
targetAgents = selected;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
let installGlobally = options.global ?? false;
|
|
3008
|
+
const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
|
|
3009
|
+
if (options.global === void 0 && !options.yes && supportsGlobal) {
|
|
3010
|
+
const scope = await ve({
|
|
3011
|
+
message: "Installation scope",
|
|
3012
|
+
options: [{
|
|
3013
|
+
value: false,
|
|
3014
|
+
label: "Project",
|
|
3015
|
+
hint: "Install in current directory (committed with your project)"
|
|
3016
|
+
}, {
|
|
3017
|
+
value: true,
|
|
3018
|
+
label: "Global",
|
|
3019
|
+
hint: "Install in home directory (available across all projects)"
|
|
3020
|
+
}]
|
|
3021
|
+
});
|
|
3022
|
+
if (pD(scope)) {
|
|
3023
|
+
xe("Installation cancelled");
|
|
3024
|
+
await cleanup(tempDir);
|
|
3025
|
+
process.exit(0);
|
|
3026
|
+
}
|
|
3027
|
+
installGlobally = scope;
|
|
3028
|
+
}
|
|
3029
|
+
let installMode = options.copy ? "copy" : "symlink";
|
|
3030
|
+
const uniqueDirs = new Set(targetAgents.map((a) => agents[a].skillsDir));
|
|
3031
|
+
if (!options.copy && !options.yes && uniqueDirs.size > 1) {
|
|
3032
|
+
const modeChoice = await ve({
|
|
3033
|
+
message: "Installation method",
|
|
3034
|
+
options: [{
|
|
3035
|
+
value: "symlink",
|
|
3036
|
+
label: "Symlink (Recommended)",
|
|
3037
|
+
hint: "Single source of truth, easy updates"
|
|
3038
|
+
}, {
|
|
3039
|
+
value: "copy",
|
|
3040
|
+
label: "Copy to all agents",
|
|
3041
|
+
hint: "Independent copies for each agent"
|
|
3042
|
+
}]
|
|
3043
|
+
});
|
|
3044
|
+
if (pD(modeChoice)) {
|
|
3045
|
+
xe("Installation cancelled");
|
|
3046
|
+
await cleanup(tempDir);
|
|
3047
|
+
process.exit(0);
|
|
3048
|
+
}
|
|
3049
|
+
installMode = modeChoice;
|
|
3050
|
+
} else if (uniqueDirs.size <= 1) installMode = "copy";
|
|
3051
|
+
const cwd = process.cwd();
|
|
3052
|
+
const summaryLines = [];
|
|
3053
|
+
targetAgents.map((a) => agents[a].displayName);
|
|
3054
|
+
const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
|
|
3055
|
+
skillName: skill.name,
|
|
3056
|
+
agent,
|
|
3057
|
+
installed: await isSkillInstalled(skill.name, agent, { global: installGlobally })
|
|
3058
|
+
}))));
|
|
3059
|
+
const overwriteStatus = /* @__PURE__ */ new Map();
|
|
3060
|
+
for (const { skillName, agent, installed } of overwriteChecks) {
|
|
3061
|
+
if (!overwriteStatus.has(skillName)) overwriteStatus.set(skillName, /* @__PURE__ */ new Map());
|
|
3062
|
+
overwriteStatus.get(skillName).set(agent, installed);
|
|
3063
|
+
}
|
|
3064
|
+
const groupedSummary = {};
|
|
3065
|
+
const ungroupedSummary = [];
|
|
3066
|
+
for (const skill of selectedSkills) if (skill.pluginName) {
|
|
3067
|
+
const group = skill.pluginName;
|
|
3068
|
+
if (!groupedSummary[group]) groupedSummary[group] = [];
|
|
3069
|
+
groupedSummary[group].push(skill);
|
|
3070
|
+
} else ungroupedSummary.push(skill);
|
|
3071
|
+
const printSkillSummary = (skills) => {
|
|
3072
|
+
for (const skill of skills) {
|
|
3073
|
+
if (summaryLines.length > 0) summaryLines.push("");
|
|
3074
|
+
const shortCanonical = shortenPath$2(getCanonicalPath(skill.name, { global: installGlobally }), cwd);
|
|
3075
|
+
summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
|
|
3076
|
+
summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
|
|
3077
|
+
const skillOverwrites = overwriteStatus.get(skill.name);
|
|
3078
|
+
const overwriteAgents = targetAgents.filter((a) => skillOverwrites?.get(a)).map((a) => agents[a].displayName);
|
|
3079
|
+
if (overwriteAgents.length > 0) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(overwriteAgents)}`);
|
|
3080
|
+
}
|
|
3081
|
+
};
|
|
3082
|
+
const sortedGroups = Object.keys(groupedSummary).sort();
|
|
3083
|
+
for (const group of sortedGroups) {
|
|
3084
|
+
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
3085
|
+
summaryLines.push("");
|
|
3086
|
+
summaryLines.push(import_picocolors.default.bold(title));
|
|
3087
|
+
printSkillSummary(groupedSummary[group]);
|
|
3088
|
+
}
|
|
3089
|
+
if (ungroupedSummary.length > 0) {
|
|
3090
|
+
if (sortedGroups.length > 0) {
|
|
3091
|
+
summaryLines.push("");
|
|
3092
|
+
summaryLines.push(import_picocolors.default.bold("General"));
|
|
3093
|
+
}
|
|
3094
|
+
printSkillSummary(ungroupedSummary);
|
|
3095
|
+
}
|
|
3096
|
+
console.log();
|
|
3097
|
+
Me(summaryLines.join("\n"), "Installation Summary");
|
|
3098
|
+
try {
|
|
3099
|
+
const auditData = await auditPromise;
|
|
3100
|
+
if (auditData && ownerRepoForAudit) {
|
|
3101
|
+
const securityLines = buildSecurityLines(auditData, selectedSkills.map((s) => ({
|
|
3102
|
+
slug: getSkillDisplayName(s),
|
|
3103
|
+
displayName: getSkillDisplayName(s)
|
|
3104
|
+
})), ownerRepoForAudit);
|
|
3105
|
+
if (securityLines.length > 0) Me(securityLines.join("\n"), "Security Risk Assessments");
|
|
3106
|
+
if (options.strictAudit) {
|
|
3107
|
+
let hasHighRisk = false;
|
|
3108
|
+
for (const s of selectedSkills) {
|
|
3109
|
+
const data = auditData[getSkillDisplayName(s)];
|
|
3110
|
+
if (!data) continue;
|
|
3111
|
+
const risks = [
|
|
3112
|
+
data.ath?.risk,
|
|
3113
|
+
data.socket?.risk,
|
|
3114
|
+
data.snyk?.risk
|
|
3115
|
+
];
|
|
3116
|
+
if (risks.includes("high") || risks.includes("critical") || risks.includes("unknown")) hasHighRisk = true;
|
|
3117
|
+
}
|
|
3118
|
+
if (hasHighRisk) {
|
|
3119
|
+
M.error(import_picocolors.default.red("Strict audit failed: High, critical, or unknown risk detected."));
|
|
3120
|
+
await cleanup(tempDir);
|
|
3121
|
+
process.exit(1);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
} else if (options.strictAudit) throw new Error("Audit data unavailable");
|
|
3125
|
+
} catch {
|
|
3126
|
+
if (options.strictAudit) {
|
|
3127
|
+
M.error(import_picocolors.default.red("Strict audit failed: Could not retrieve security audit evaluation."));
|
|
3128
|
+
await cleanup(tempDir);
|
|
3129
|
+
process.exit(1);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (!options.yes) {
|
|
3133
|
+
const confirmed = await ye({ message: "Proceed with installation?" });
|
|
3134
|
+
if (pD(confirmed) || !confirmed) {
|
|
3135
|
+
xe("Installation cancelled");
|
|
3136
|
+
await cleanup(tempDir);
|
|
3137
|
+
process.exit(0);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
spinner.start("Installing skills...");
|
|
3141
|
+
const results = [];
|
|
3142
|
+
for (const skill of selectedSkills) for (const agent of targetAgents) {
|
|
3143
|
+
const result = await installSkillForAgent(skill, agent, {
|
|
3144
|
+
global: installGlobally,
|
|
3145
|
+
mode: installMode
|
|
3146
|
+
});
|
|
3147
|
+
results.push({
|
|
3148
|
+
skill: getSkillDisplayName(skill),
|
|
3149
|
+
agent: agents[agent].displayName,
|
|
3150
|
+
pluginName: skill.pluginName,
|
|
3151
|
+
...result
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
spinner.stop("Installation complete");
|
|
3155
|
+
console.log();
|
|
3156
|
+
const successful = results.filter((r) => r.success);
|
|
3157
|
+
const failed = results.filter((r) => !r.success);
|
|
3158
|
+
const skillFiles = {};
|
|
3159
|
+
for (const skill of selectedSkills) {
|
|
3160
|
+
let relativePath;
|
|
3161
|
+
if (tempDir && skill.path === tempDir) relativePath = "SKILL.md";
|
|
3162
|
+
else if (tempDir && skill.path.startsWith(tempDir + sep)) relativePath = skill.path.slice(tempDir.length + 1).split(sep).join("/") + "/SKILL.md";
|
|
3163
|
+
else continue;
|
|
3164
|
+
skillFiles[skill.name] = relativePath;
|
|
3165
|
+
}
|
|
3166
|
+
const normalizedSource = getOwnerRepo(parsed);
|
|
3167
|
+
const lockSource = parsed.url.startsWith("git@") ? parsed.url : normalizedSource;
|
|
3168
|
+
if (normalizedSource) {
|
|
3169
|
+
const ownerRepo = parseOwnerRepo(normalizedSource);
|
|
3170
|
+
if (ownerRepo) {
|
|
3171
|
+
if (await isRepoPrivate(ownerRepo.owner, ownerRepo.repo) === false) track({
|
|
3172
|
+
event: "install",
|
|
3173
|
+
source: normalizedSource,
|
|
3174
|
+
skills: selectedSkills.map((s) => s.name).join(","),
|
|
3175
|
+
agents: targetAgents.join(","),
|
|
3176
|
+
...installGlobally && { global: "1" },
|
|
3177
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
3178
|
+
});
|
|
3179
|
+
} else track({
|
|
3180
|
+
event: "install",
|
|
3181
|
+
source: normalizedSource,
|
|
3182
|
+
skills: selectedSkills.map((s) => s.name).join(","),
|
|
3183
|
+
agents: targetAgents.join(","),
|
|
3184
|
+
...installGlobally && { global: "1" },
|
|
3185
|
+
skillFiles: JSON.stringify(skillFiles)
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
if (successful.length > 0 && installGlobally && normalizedSource) {
|
|
3189
|
+
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3190
|
+
for (const skill of selectedSkills) {
|
|
3191
|
+
const skillDisplayName = getSkillDisplayName(skill);
|
|
3192
|
+
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
3193
|
+
let skillFolderHash = "";
|
|
3194
|
+
const skillPathValue = skillFiles[skill.name];
|
|
3195
|
+
if (parsed.type === "github" && skillPathValue) {
|
|
3196
|
+
const hash = await fetchSkillFolderHash(normalizedSource, skillPathValue, getGitHubToken());
|
|
3197
|
+
if (hash) skillFolderHash = hash;
|
|
3198
|
+
}
|
|
3199
|
+
await addSkillToLock(skill.name, {
|
|
3200
|
+
source: lockSource || normalizedSource,
|
|
3201
|
+
sourceType: parsed.type,
|
|
3202
|
+
sourceUrl: parsed.url,
|
|
3203
|
+
skillPath: skillPathValue,
|
|
3204
|
+
skillFolderHash,
|
|
3205
|
+
pluginName: skill.pluginName
|
|
3206
|
+
});
|
|
3207
|
+
} catch {}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
if (successful.length > 0 && !installGlobally) {
|
|
3211
|
+
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3212
|
+
for (const skill of selectedSkills) {
|
|
3213
|
+
const skillDisplayName = getSkillDisplayName(skill);
|
|
3214
|
+
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
3215
|
+
const computedHash = await computeSkillFolderHash(skill.path);
|
|
3216
|
+
await addSkillToLocalLock(skill.name, {
|
|
3217
|
+
source: lockSource || parsed.url,
|
|
3218
|
+
sourceType: parsed.type,
|
|
3219
|
+
computedHash
|
|
3220
|
+
}, cwd);
|
|
3221
|
+
} catch {}
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
if (successful.length > 0) {
|
|
3225
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
3226
|
+
const groupedResults = {};
|
|
3227
|
+
const ungroupedResults = [];
|
|
3228
|
+
for (const r of successful) {
|
|
3229
|
+
const skillResults = bySkill.get(r.skill) || [];
|
|
3230
|
+
skillResults.push(r);
|
|
3231
|
+
bySkill.set(r.skill, skillResults);
|
|
3232
|
+
if (skillResults.length === 1) if (r.pluginName) {
|
|
3233
|
+
const group = r.pluginName;
|
|
3234
|
+
if (!groupedResults[group]) groupedResults[group] = [];
|
|
3235
|
+
groupedResults[group].push(r);
|
|
3236
|
+
} else ungroupedResults.push(r);
|
|
3237
|
+
}
|
|
3238
|
+
const skillCount = bySkill.size;
|
|
3239
|
+
const symlinkFailures = successful.filter((r) => r.mode === "symlink" && r.symlinkFailed);
|
|
3240
|
+
const copiedAgents = symlinkFailures.map((r) => r.agent);
|
|
3241
|
+
const resultLines = [];
|
|
3242
|
+
const printSkillResults = (entries) => {
|
|
3243
|
+
for (const entry of entries) {
|
|
3244
|
+
const skillResults = bySkill.get(entry.skill) || [];
|
|
3245
|
+
const firstResult = skillResults[0];
|
|
3246
|
+
if (firstResult.mode === "copy") {
|
|
3247
|
+
resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill} ${import_picocolors.default.dim("(copied)")}`);
|
|
3248
|
+
for (const r of skillResults) {
|
|
3249
|
+
const shortPath = shortenPath$2(r.path, cwd);
|
|
3250
|
+
resultLines.push(` ${import_picocolors.default.dim("→")} ${shortPath}`);
|
|
3251
|
+
}
|
|
3252
|
+
} else {
|
|
3253
|
+
if (firstResult.canonicalPath) {
|
|
3254
|
+
const shortPath = shortenPath$2(firstResult.canonicalPath, cwd);
|
|
3255
|
+
resultLines.push(`${import_picocolors.default.green("✓")} ${shortPath}`);
|
|
3256
|
+
} else resultLines.push(`${import_picocolors.default.green("✓")} ${entry.skill}`);
|
|
3257
|
+
resultLines.push(...buildResultLines(skillResults, targetAgents));
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
};
|
|
3261
|
+
const sortedResultGroups = Object.keys(groupedResults).sort();
|
|
3262
|
+
for (const group of sortedResultGroups) {
|
|
3263
|
+
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
3264
|
+
resultLines.push("");
|
|
3265
|
+
resultLines.push(import_picocolors.default.bold(title));
|
|
3266
|
+
printSkillResults(groupedResults[group]);
|
|
3267
|
+
}
|
|
3268
|
+
if (ungroupedResults.length > 0) {
|
|
3269
|
+
if (sortedResultGroups.length > 0) {
|
|
3270
|
+
resultLines.push("");
|
|
3271
|
+
resultLines.push(import_picocolors.default.bold("General"));
|
|
3272
|
+
}
|
|
3273
|
+
printSkillResults(ungroupedResults);
|
|
3274
|
+
}
|
|
3275
|
+
const title = import_picocolors.default.green(`Installed ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
|
|
3276
|
+
Me(resultLines.join("\n"), title);
|
|
3277
|
+
if (symlinkFailures.length > 0) {
|
|
3278
|
+
M.warn(import_picocolors.default.yellow(`Symlinks failed for: ${formatList$1(copiedAgents)}`));
|
|
3279
|
+
M.message(import_picocolors.default.dim(" Files were copied instead. On Windows, enable Developer Mode for symlink support."));
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (failed.length > 0) {
|
|
3283
|
+
console.log();
|
|
3284
|
+
M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
|
|
3285
|
+
for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
|
|
3286
|
+
}
|
|
3287
|
+
console.log();
|
|
3288
|
+
Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
|
|
3289
|
+
await promptForFindSkills(options, targetAgents);
|
|
3290
|
+
} catch (error) {
|
|
3291
|
+
if (error instanceof GitCloneError) {
|
|
3292
|
+
M.error(import_picocolors.default.red("Failed to clone repository"));
|
|
3293
|
+
for (const line of error.message.split("\n")) M.message(import_picocolors.default.dim(line));
|
|
3294
|
+
} else M.error(error instanceof Error ? error.message : "Unknown error occurred");
|
|
3295
|
+
showInstallTip();
|
|
3296
|
+
Se(import_picocolors.default.red("Installation failed"));
|
|
3297
|
+
process.exit(1);
|
|
3298
|
+
} finally {
|
|
3299
|
+
await cleanup(tempDir);
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
async function cleanup(tempDir) {
|
|
3303
|
+
if (tempDir) try {
|
|
3304
|
+
await cleanupTempDir(tempDir);
|
|
3305
|
+
} catch {}
|
|
3306
|
+
}
|
|
3307
|
+
async function promptForFindSkills(options, targetAgents) {
|
|
3308
|
+
if (!process.stdin.isTTY) return;
|
|
3309
|
+
if (options?.yes) return;
|
|
3310
|
+
try {
|
|
3311
|
+
if (await isPromptDismissed("findSkillsPrompt")) return;
|
|
3312
|
+
if (await isSkillInstalled("find-skills", "claude-code", { global: true })) {
|
|
3313
|
+
await dismissPrompt("findSkillsPrompt");
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
console.log();
|
|
3317
|
+
M.message(import_picocolors.default.dim("One-time prompt - you won't be asked again if you dismiss."));
|
|
3318
|
+
const install = await ye({ message: `Install the ${import_picocolors.default.cyan("find-skills")} skill? It helps your agent discover and suggest skills.` });
|
|
3319
|
+
if (pD(install)) {
|
|
3320
|
+
await dismissPrompt("findSkillsPrompt");
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
if (install) {
|
|
3324
|
+
await dismissPrompt("findSkillsPrompt");
|
|
3325
|
+
const findSkillsAgents = targetAgents?.filter((a) => a !== "replit");
|
|
3326
|
+
if (!findSkillsAgents || findSkillsAgents.length === 0) return;
|
|
3327
|
+
console.log();
|
|
3328
|
+
M.step("Installing find-skills skill...");
|
|
3329
|
+
try {
|
|
3330
|
+
await runAdd(["vercel-labs/skills"], {
|
|
3331
|
+
skill: ["find-skills"],
|
|
3332
|
+
global: true,
|
|
3333
|
+
yes: true,
|
|
3334
|
+
agent: findSkillsAgents
|
|
3335
|
+
});
|
|
3336
|
+
} catch {
|
|
3337
|
+
M.warn("Failed to install find-skills. You can try again with:");
|
|
3338
|
+
M.message(import_picocolors.default.dim(" npx skills add vercel-labs/skills@find-skills -g -y --all"));
|
|
3339
|
+
}
|
|
3340
|
+
} else {
|
|
3341
|
+
await dismissPrompt("findSkillsPrompt");
|
|
3342
|
+
M.message(import_picocolors.default.dim("You can install it later with: npx skills add vercel-labs/skills@find-skills"));
|
|
3343
|
+
}
|
|
3344
|
+
} catch {}
|
|
3345
|
+
}
|
|
3346
|
+
function parseAddOptions(args) {
|
|
3347
|
+
const options = {};
|
|
3348
|
+
const source = [];
|
|
3349
|
+
for (let i = 0; i < args.length; i++) {
|
|
3350
|
+
const arg = args[i];
|
|
3351
|
+
if (arg === "-g" || arg === "--global") options.global = true;
|
|
3352
|
+
else if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
3353
|
+
else if (arg === "-l" || arg === "--list") options.list = true;
|
|
3354
|
+
else if (arg === "--all") options.all = true;
|
|
3355
|
+
else if (arg === "-a" || arg === "--agent") {
|
|
3356
|
+
options.agent = options.agent || [];
|
|
3357
|
+
i++;
|
|
3358
|
+
let nextArg = args[i];
|
|
3359
|
+
while (i < args.length && nextArg && !nextArg.startsWith("-")) {
|
|
3360
|
+
options.agent.push(nextArg);
|
|
3361
|
+
i++;
|
|
3362
|
+
nextArg = args[i];
|
|
3363
|
+
}
|
|
3364
|
+
i--;
|
|
3365
|
+
} else if (arg === "--group") {
|
|
3366
|
+
i++;
|
|
3367
|
+
options.group = args[i];
|
|
3368
|
+
} else if (arg === "--dry-run") options.dryRun = true;
|
|
3369
|
+
else if (arg === "--ref") {
|
|
3370
|
+
i++;
|
|
3371
|
+
options.ref = args[i];
|
|
3372
|
+
} else if (arg === "-s" || arg === "--skill") {
|
|
3373
|
+
options.skill = options.skill || [];
|
|
3374
|
+
i++;
|
|
3375
|
+
let nextArg = args[i];
|
|
3376
|
+
while (i < args.length && nextArg && !nextArg.startsWith("-")) {
|
|
3377
|
+
options.skill.push(nextArg);
|
|
3378
|
+
i++;
|
|
3379
|
+
nextArg = args[i];
|
|
3380
|
+
}
|
|
3381
|
+
i--;
|
|
3382
|
+
} else if (arg === "--full-depth") options.fullDepth = true;
|
|
3383
|
+
else if (arg === "--copy") options.copy = true;
|
|
3384
|
+
else if (arg === "--strict-audit") options.strictAudit = true;
|
|
3385
|
+
else if (arg === "--strict") options.strict = true;
|
|
3386
|
+
else if (arg && !arg.startsWith("-")) source.push(arg);
|
|
3387
|
+
}
|
|
3388
|
+
return {
|
|
3389
|
+
source,
|
|
3390
|
+
options
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
const RESET$2 = "\x1B[0m";
|
|
3394
|
+
const BOLD$2 = "\x1B[1m";
|
|
3395
|
+
const DIM$2 = "\x1B[38;5;102m";
|
|
3396
|
+
const TEXT$1 = "\x1B[38;5;145m";
|
|
3397
|
+
const CYAN$1 = "\x1B[36m";
|
|
3398
|
+
const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh";
|
|
3399
|
+
function formatInstalls(count) {
|
|
3400
|
+
if (!count || count <= 0) return "";
|
|
3401
|
+
if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
|
|
3402
|
+
if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
|
|
3403
|
+
return `${count} install${count === 1 ? "" : "s"}`;
|
|
3404
|
+
}
|
|
3405
|
+
async function searchSkillsAPI(query) {
|
|
3406
|
+
try {
|
|
3407
|
+
const url = `${SEARCH_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=10`;
|
|
3408
|
+
const res = await fetch(url);
|
|
3409
|
+
if (!res.ok) return [];
|
|
3410
|
+
return (await res.json()).skills.map((skill) => ({
|
|
3411
|
+
name: skill.name,
|
|
3412
|
+
slug: skill.id,
|
|
3413
|
+
source: skill.source || "",
|
|
3414
|
+
installs: skill.installs
|
|
3415
|
+
})).sort((a, b) => (b.installs || 0) - (a.installs || 0));
|
|
3416
|
+
} catch {
|
|
3417
|
+
return [];
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
const HIDE_CURSOR = "\x1B[?25l";
|
|
3421
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
3422
|
+
const CLEAR_DOWN = "\x1B[J";
|
|
3423
|
+
const MOVE_UP = (n) => `\x1b[${n}A`;
|
|
3424
|
+
const MOVE_TO_COL = (n) => `\x1b[${n}G`;
|
|
3425
|
+
async function runSearchPrompt(initialQuery = "") {
|
|
3426
|
+
let results = [];
|
|
3427
|
+
let selectedIndex = 0;
|
|
3428
|
+
let query = initialQuery;
|
|
3429
|
+
let loading = false;
|
|
3430
|
+
let debounceTimer = null;
|
|
3431
|
+
let lastRenderedLines = 0;
|
|
3432
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
3433
|
+
readline.emitKeypressEvents(process.stdin);
|
|
3434
|
+
process.stdin.resume();
|
|
3435
|
+
process.stdout.write(HIDE_CURSOR);
|
|
3436
|
+
function render() {
|
|
3437
|
+
if (lastRenderedLines > 0) process.stdout.write(MOVE_UP(lastRenderedLines) + MOVE_TO_COL(1));
|
|
3438
|
+
process.stdout.write(CLEAR_DOWN);
|
|
3439
|
+
const lines = [];
|
|
3440
|
+
const cursor = `${BOLD$2}_${RESET$2}`;
|
|
3441
|
+
lines.push(`${TEXT$1}Search skills:${RESET$2} ${query}${cursor}`);
|
|
3442
|
+
lines.push("");
|
|
3443
|
+
if (!query || query.length < 2) lines.push(`${DIM$2}Start typing to search (min 2 chars)${RESET$2}`);
|
|
3444
|
+
else if (results.length === 0 && loading) lines.push(`${DIM$2}Searching...${RESET$2}`);
|
|
3445
|
+
else if (results.length === 0) lines.push(`${DIM$2}No skills found${RESET$2}`);
|
|
3446
|
+
else {
|
|
3447
|
+
const visible = results.slice(0, 8);
|
|
3448
|
+
for (let i = 0; i < visible.length; i++) {
|
|
3449
|
+
const skill = visible[i];
|
|
3450
|
+
const isSelected = i === selectedIndex;
|
|
3451
|
+
const arrow = isSelected ? `${BOLD$2}>${RESET$2}` : " ";
|
|
3452
|
+
const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$1}${skill.name}${RESET$2}`;
|
|
3453
|
+
const source = skill.source ? ` ${DIM$2}${skill.source}${RESET$2}` : "";
|
|
3454
|
+
const installs = formatInstalls(skill.installs);
|
|
3455
|
+
const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$2}` : "";
|
|
3456
|
+
const loadingIndicator = loading && i === 0 ? ` ${DIM$2}...${RESET$2}` : "";
|
|
3457
|
+
lines.push(` ${arrow} ${name}${source}${installsBadge}${loadingIndicator}`);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
lines.push("");
|
|
3461
|
+
lines.push(`${DIM$2}up/down navigate | enter select | esc cancel${RESET$2}`);
|
|
3462
|
+
for (const line of lines) process.stdout.write(line + "\n");
|
|
3463
|
+
lastRenderedLines = lines.length;
|
|
3464
|
+
}
|
|
3465
|
+
function triggerSearch(q) {
|
|
3466
|
+
if (debounceTimer) {
|
|
3467
|
+
clearTimeout(debounceTimer);
|
|
3468
|
+
debounceTimer = null;
|
|
3469
|
+
}
|
|
3470
|
+
loading = false;
|
|
3471
|
+
if (!q || q.length < 2) {
|
|
3472
|
+
results = [];
|
|
3473
|
+
selectedIndex = 0;
|
|
3474
|
+
render();
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
loading = true;
|
|
3478
|
+
render();
|
|
3479
|
+
const debounceMs = Math.max(150, 350 - q.length * 50);
|
|
3480
|
+
debounceTimer = setTimeout(async () => {
|
|
3481
|
+
try {
|
|
3482
|
+
results = await searchSkillsAPI(q);
|
|
3483
|
+
selectedIndex = 0;
|
|
3484
|
+
} catch {
|
|
3485
|
+
results = [];
|
|
3486
|
+
} finally {
|
|
3487
|
+
loading = false;
|
|
3488
|
+
debounceTimer = null;
|
|
3489
|
+
render();
|
|
3490
|
+
}
|
|
3491
|
+
}, debounceMs);
|
|
3492
|
+
}
|
|
3493
|
+
if (initialQuery) triggerSearch(initialQuery);
|
|
3494
|
+
render();
|
|
3495
|
+
return new Promise((resolve) => {
|
|
3496
|
+
function cleanup() {
|
|
3497
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
3498
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
3499
|
+
process.stdout.write(SHOW_CURSOR);
|
|
3500
|
+
process.stdin.pause();
|
|
3501
|
+
}
|
|
3502
|
+
function handleKeypress(_ch, key) {
|
|
3503
|
+
if (!key) return;
|
|
3504
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
3505
|
+
cleanup();
|
|
3506
|
+
resolve(null);
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
if (key.name === "return") {
|
|
3510
|
+
cleanup();
|
|
3511
|
+
resolve(results[selectedIndex] || null);
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
if (key.name === "up") {
|
|
3515
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
3516
|
+
render();
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
if (key.name === "down") {
|
|
3520
|
+
selectedIndex = Math.min(Math.max(0, results.length - 1), selectedIndex + 1);
|
|
3521
|
+
render();
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
if (key.name === "backspace") {
|
|
3525
|
+
if (query.length > 0) {
|
|
3526
|
+
query = query.slice(0, -1);
|
|
3527
|
+
triggerSearch(query);
|
|
3528
|
+
}
|
|
3529
|
+
return;
|
|
3530
|
+
}
|
|
3531
|
+
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
3532
|
+
const char = key.sequence;
|
|
3533
|
+
if (char >= " " && char <= "~") {
|
|
3534
|
+
query += char;
|
|
3535
|
+
triggerSearch(query);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
process.stdin.on("keypress", handleKeypress);
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
function getOwnerRepoFromString(pkg) {
|
|
3543
|
+
const atIndex = pkg.lastIndexOf("@");
|
|
3544
|
+
const match = (atIndex > 0 ? pkg.slice(0, atIndex) : pkg).match(/^([^/]+)\/([^/]+)$/);
|
|
3545
|
+
if (match) return {
|
|
3546
|
+
owner: match[1],
|
|
3547
|
+
repo: match[2]
|
|
3548
|
+
};
|
|
3549
|
+
return null;
|
|
3550
|
+
}
|
|
3551
|
+
async function isRepoPublic(owner, repo) {
|
|
3552
|
+
return await isRepoPrivate(owner, repo) === false;
|
|
3553
|
+
}
|
|
3554
|
+
async function runFind(args) {
|
|
3555
|
+
const query = args.join(" ");
|
|
3556
|
+
const isNonInteractive = !process.stdin.isTTY;
|
|
3557
|
+
const agentTip = `${DIM$2}Tip: if running in a coding agent, follow these steps:${RESET$2}
|
|
3558
|
+
${DIM$2} 1) npx skills find [query]${RESET$2}
|
|
3559
|
+
${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
|
|
3560
|
+
if (query) {
|
|
3561
|
+
const results = await searchSkillsAPI(query);
|
|
3562
|
+
track({
|
|
3563
|
+
event: "find",
|
|
3564
|
+
query,
|
|
3565
|
+
resultCount: String(results.length)
|
|
3566
|
+
});
|
|
3567
|
+
if (results.length === 0) {
|
|
3568
|
+
console.log(`${DIM$2}No skills found for "${query}"${RESET$2}`);
|
|
3569
|
+
return;
|
|
3570
|
+
}
|
|
3571
|
+
console.log(`${DIM$2}Install with${RESET$2} npx skills add <owner/repo@skill>`);
|
|
3572
|
+
console.log();
|
|
3573
|
+
for (const skill of results.slice(0, 6)) {
|
|
3574
|
+
const pkg = skill.source || skill.slug;
|
|
3575
|
+
const installs = formatInstalls(skill.installs);
|
|
3576
|
+
console.log(`${TEXT$1}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`);
|
|
3577
|
+
console.log(`${DIM$2}└ https://skills.sh/${skill.slug}${RESET$2}`);
|
|
3578
|
+
console.log();
|
|
3579
|
+
}
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
if (isNonInteractive) {
|
|
3583
|
+
console.log(agentTip);
|
|
3584
|
+
console.log();
|
|
3585
|
+
}
|
|
3586
|
+
const selected = await runSearchPrompt();
|
|
3587
|
+
track({
|
|
3588
|
+
event: "find",
|
|
3589
|
+
query: "",
|
|
3590
|
+
resultCount: selected ? "1" : "0",
|
|
3591
|
+
interactive: "1"
|
|
3592
|
+
});
|
|
3593
|
+
if (!selected) {
|
|
3594
|
+
console.log(`${DIM$2}Search cancelled${RESET$2}`);
|
|
3595
|
+
console.log();
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
const pkg = selected.source || selected.slug;
|
|
3599
|
+
const skillName = selected.name;
|
|
3600
|
+
console.log();
|
|
3601
|
+
console.log(`${TEXT$1}Installing ${BOLD$2}${skillName}${RESET$2} from ${DIM$2}${pkg}${RESET$2}...`);
|
|
3602
|
+
console.log();
|
|
3603
|
+
const { source, options } = parseAddOptions([
|
|
3604
|
+
pkg,
|
|
3605
|
+
"--skill",
|
|
3606
|
+
skillName
|
|
3607
|
+
]);
|
|
3608
|
+
await runAdd(source, options);
|
|
3609
|
+
console.log();
|
|
3610
|
+
const info = getOwnerRepoFromString(pkg);
|
|
3611
|
+
if (info && await isRepoPublic(info.owner, info.repo)) console.log(`${DIM$2}View the skill at${RESET$2} ${TEXT$1}https://skills.sh/${selected.slug}${RESET$2}`);
|
|
3612
|
+
else console.log(`${DIM$2}Discover more skills at${RESET$2} ${TEXT$1}https://skills.sh${RESET$2}`);
|
|
3613
|
+
console.log();
|
|
3614
|
+
}
|
|
3615
|
+
const isCancelled = (value) => typeof value === "symbol";
|
|
3616
|
+
function shortenPath$1(fullPath, cwd) {
|
|
3617
|
+
const home = homedir();
|
|
3618
|
+
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
3619
|
+
if (fullPath === cwd || fullPath.startsWith(cwd + sep)) return "." + fullPath.slice(cwd.length);
|
|
3620
|
+
return fullPath;
|
|
3621
|
+
}
|
|
3622
|
+
async function discoverNodeModuleSkills(cwd) {
|
|
3623
|
+
const nodeModulesDir = join(cwd, "node_modules");
|
|
3624
|
+
const skills = [];
|
|
3625
|
+
let topNames;
|
|
3626
|
+
try {
|
|
3627
|
+
topNames = await readdir(nodeModulesDir);
|
|
3628
|
+
} catch {
|
|
3629
|
+
return skills;
|
|
3630
|
+
}
|
|
3631
|
+
const processPackageDir = async (pkgDir, packageName) => {
|
|
3632
|
+
const rootSkill = await parseSkillMd(join(pkgDir, "SKILL.md"));
|
|
3633
|
+
if (rootSkill) {
|
|
3634
|
+
skills.push({
|
|
3635
|
+
...rootSkill,
|
|
3636
|
+
packageName
|
|
3637
|
+
});
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
const searchDirs = [
|
|
3641
|
+
pkgDir,
|
|
3642
|
+
join(pkgDir, "skills"),
|
|
3643
|
+
join(pkgDir, ".agents", "skills")
|
|
3644
|
+
];
|
|
3645
|
+
for (const searchDir of searchDirs) try {
|
|
3646
|
+
const entries = await readdir(searchDir);
|
|
3647
|
+
for (const name of entries) {
|
|
3648
|
+
const skillDir = join(searchDir, name);
|
|
3649
|
+
try {
|
|
3650
|
+
if (!(await stat(skillDir)).isDirectory()) continue;
|
|
3651
|
+
} catch {
|
|
3652
|
+
continue;
|
|
3653
|
+
}
|
|
3654
|
+
const skill = await parseSkillMd(join(skillDir, "SKILL.md"));
|
|
3655
|
+
if (skill) skills.push({
|
|
3656
|
+
...skill,
|
|
3657
|
+
packageName
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
} catch {}
|
|
3661
|
+
};
|
|
3662
|
+
await Promise.all(topNames.map(async (name) => {
|
|
3663
|
+
if (name.startsWith(".")) return;
|
|
3664
|
+
const fullPath = join(nodeModulesDir, name);
|
|
3665
|
+
try {
|
|
3666
|
+
if (!(await stat(fullPath)).isDirectory()) return;
|
|
3667
|
+
} catch {
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
if (name.startsWith("@")) try {
|
|
3671
|
+
const scopeNames = await readdir(fullPath);
|
|
3672
|
+
await Promise.all(scopeNames.map(async (scopedName) => {
|
|
3673
|
+
const scopedPath = join(fullPath, scopedName);
|
|
3674
|
+
try {
|
|
3675
|
+
if (!(await stat(scopedPath)).isDirectory()) return;
|
|
3676
|
+
} catch {
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
await processPackageDir(scopedPath, `${name}/${scopedName}`);
|
|
3680
|
+
}));
|
|
3681
|
+
} catch {}
|
|
3682
|
+
else await processPackageDir(fullPath, name);
|
|
3683
|
+
}));
|
|
3684
|
+
return skills;
|
|
3685
|
+
}
|
|
3686
|
+
async function runSync(args, options = {}) {
|
|
3687
|
+
const cwd = process.cwd();
|
|
3688
|
+
console.log();
|
|
3689
|
+
Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills experimental_sync ")));
|
|
3690
|
+
const spinner = Y();
|
|
3691
|
+
spinner.start("Scanning node_modules for skills...");
|
|
3692
|
+
const discoveredSkills = await discoverNodeModuleSkills(cwd);
|
|
3693
|
+
if (discoveredSkills.length === 0) {
|
|
3694
|
+
spinner.stop(import_picocolors.default.yellow("No skills found"));
|
|
3695
|
+
Se(import_picocolors.default.dim("No SKILL.md files found in node_modules."));
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
spinner.stop(`Found ${import_picocolors.default.green(String(discoveredSkills.length))} skill${discoveredSkills.length > 1 ? "s" : ""} in node_modules`);
|
|
3699
|
+
for (const skill of discoveredSkills) {
|
|
3700
|
+
M.info(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`from ${skill.packageName}`)}`);
|
|
3701
|
+
if (skill.description) M.message(import_picocolors.default.dim(` ${skill.description}`));
|
|
3702
|
+
}
|
|
3703
|
+
const localLock = await readLocalLock(cwd);
|
|
3704
|
+
const toInstall = [];
|
|
3705
|
+
const upToDate = [];
|
|
3706
|
+
if (options.force) {
|
|
3707
|
+
toInstall.push(...discoveredSkills);
|
|
3708
|
+
M.info(import_picocolors.default.dim("Force mode: reinstalling all skills"));
|
|
3709
|
+
} else {
|
|
3710
|
+
for (const skill of discoveredSkills) {
|
|
3711
|
+
const existingEntry = localLock.skills[skill.name];
|
|
3712
|
+
if (existingEntry) {
|
|
3713
|
+
if (await computeSkillFolderHash(skill.path) === existingEntry.computedHash) {
|
|
3714
|
+
upToDate.push(skill.name);
|
|
3715
|
+
continue;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
toInstall.push(skill);
|
|
3719
|
+
}
|
|
3720
|
+
if (upToDate.length > 0) M.info(import_picocolors.default.dim(`${upToDate.length} skill${upToDate.length !== 1 ? "s" : ""} already up to date`));
|
|
3721
|
+
if (toInstall.length === 0) {
|
|
3722
|
+
console.log();
|
|
3723
|
+
Se(import_picocolors.default.green("All skills are up to date."));
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
M.info(`${toInstall.length} skill${toInstall.length !== 1 ? "s" : ""} to install/update`);
|
|
3728
|
+
let targetAgents;
|
|
3729
|
+
const validAgents = Object.keys(agents);
|
|
3730
|
+
const universalAgents = getUniversalAgents();
|
|
3731
|
+
if (options.agent?.includes("*")) {
|
|
3732
|
+
targetAgents = validAgents;
|
|
3733
|
+
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
3734
|
+
} else if (options.agent && options.agent.length > 0) {
|
|
3735
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
3736
|
+
if (invalidAgents.length > 0) {
|
|
3737
|
+
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
3738
|
+
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
3739
|
+
process.exit(1);
|
|
3740
|
+
}
|
|
3741
|
+
targetAgents = options.agent;
|
|
3742
|
+
} else {
|
|
3743
|
+
spinner.start("Loading agents...");
|
|
3744
|
+
const installedAgents = await detectInstalledAgents();
|
|
3745
|
+
const totalAgents = Object.keys(agents).length;
|
|
3746
|
+
spinner.stop(`${totalAgents} agents`);
|
|
3747
|
+
if (installedAgents.length === 0) if (options.yes) {
|
|
3748
|
+
targetAgents = universalAgents;
|
|
3749
|
+
M.info("Installing to universal agents");
|
|
3750
|
+
} else {
|
|
3751
|
+
const selected = await searchMultiselect({
|
|
3752
|
+
message: "Which agents do you want to install to?",
|
|
3753
|
+
items: getNonUniversalAgents().map((a) => ({
|
|
3754
|
+
value: a,
|
|
3755
|
+
label: agents[a].displayName,
|
|
3756
|
+
hint: agents[a].skillsDir
|
|
3757
|
+
})),
|
|
3758
|
+
initialSelected: [],
|
|
3759
|
+
lockedSection: {
|
|
3760
|
+
title: "Universal (.agents/skills)",
|
|
3761
|
+
items: universalAgents.map((a) => ({
|
|
3762
|
+
value: a,
|
|
3763
|
+
label: agents[a].displayName
|
|
3764
|
+
}))
|
|
3765
|
+
}
|
|
3766
|
+
});
|
|
3767
|
+
if (isCancelled(selected)) {
|
|
3768
|
+
xe("Sync cancelled");
|
|
3769
|
+
process.exit(0);
|
|
3770
|
+
}
|
|
3771
|
+
targetAgents = selected;
|
|
3772
|
+
}
|
|
3773
|
+
else if (installedAgents.length === 1 || options.yes) {
|
|
3774
|
+
targetAgents = [...installedAgents];
|
|
3775
|
+
for (const ua of universalAgents) if (!targetAgents.includes(ua)) targetAgents.push(ua);
|
|
3776
|
+
} else {
|
|
3777
|
+
const selected = await searchMultiselect({
|
|
3778
|
+
message: "Which agents do you want to install to?",
|
|
3779
|
+
items: getNonUniversalAgents().filter((a) => installedAgents.includes(a)).map((a) => ({
|
|
3780
|
+
value: a,
|
|
3781
|
+
label: agents[a].displayName,
|
|
3782
|
+
hint: agents[a].skillsDir
|
|
3783
|
+
})),
|
|
3784
|
+
initialSelected: installedAgents.filter((a) => !universalAgents.includes(a)),
|
|
3785
|
+
lockedSection: {
|
|
3786
|
+
title: "Universal (.agents/skills)",
|
|
3787
|
+
items: universalAgents.map((a) => ({
|
|
3788
|
+
value: a,
|
|
3789
|
+
label: agents[a].displayName
|
|
3790
|
+
}))
|
|
3791
|
+
}
|
|
3792
|
+
});
|
|
3793
|
+
if (isCancelled(selected)) {
|
|
3794
|
+
xe("Sync cancelled");
|
|
3795
|
+
process.exit(0);
|
|
3796
|
+
}
|
|
3797
|
+
targetAgents = selected;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
const summaryLines = [];
|
|
3801
|
+
for (const skill of toInstall) {
|
|
3802
|
+
const shortCanonical = shortenPath$1(getCanonicalPath(skill.name, { global: false }), cwd);
|
|
3803
|
+
summaryLines.push(`${import_picocolors.default.cyan(skill.name)} ${import_picocolors.default.dim(`← ${skill.packageName}`)}`);
|
|
3804
|
+
summaryLines.push(` ${import_picocolors.default.dim(shortCanonical)}`);
|
|
3805
|
+
}
|
|
3806
|
+
console.log();
|
|
3807
|
+
Me(summaryLines.join("\n"), "Sync Summary");
|
|
3808
|
+
if (!options.yes) {
|
|
3809
|
+
const confirmed = await ye({ message: "Proceed with sync?" });
|
|
3810
|
+
if (pD(confirmed) || !confirmed) {
|
|
3811
|
+
xe("Sync cancelled");
|
|
3812
|
+
process.exit(0);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
spinner.start("Syncing skills...");
|
|
3816
|
+
const results = [];
|
|
3817
|
+
for (const skill of toInstall) for (const agent of targetAgents) {
|
|
3818
|
+
const result = await installSkillForAgent(skill, agent, {
|
|
3819
|
+
global: false,
|
|
3820
|
+
cwd,
|
|
3821
|
+
mode: "symlink"
|
|
3822
|
+
});
|
|
3823
|
+
results.push({
|
|
3824
|
+
skill: skill.name,
|
|
3825
|
+
packageName: skill.packageName,
|
|
3826
|
+
agent: agents[agent].displayName,
|
|
3827
|
+
success: result.success,
|
|
3828
|
+
path: result.path,
|
|
3829
|
+
canonicalPath: result.canonicalPath,
|
|
3830
|
+
error: result.error
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
spinner.stop("Sync complete");
|
|
3834
|
+
const successful = results.filter((r) => r.success);
|
|
3835
|
+
const failed = results.filter((r) => !r.success);
|
|
3836
|
+
const successfulSkillNames = new Set(successful.map((r) => r.skill));
|
|
3837
|
+
for (const skill of toInstall) if (successfulSkillNames.has(skill.name)) try {
|
|
3838
|
+
const computedHash = await computeSkillFolderHash(skill.path);
|
|
3839
|
+
await addSkillToLocalLock(skill.name, {
|
|
3840
|
+
source: skill.packageName,
|
|
3841
|
+
sourceType: "node_modules",
|
|
3842
|
+
computedHash
|
|
3843
|
+
}, cwd);
|
|
3844
|
+
} catch {}
|
|
3845
|
+
console.log();
|
|
3846
|
+
if (successful.length > 0) {
|
|
3847
|
+
const bySkill = /* @__PURE__ */ new Map();
|
|
3848
|
+
for (const r of successful) {
|
|
3849
|
+
const skillResults = bySkill.get(r.skill) || [];
|
|
3850
|
+
skillResults.push(r);
|
|
3851
|
+
bySkill.set(r.skill, skillResults);
|
|
3852
|
+
}
|
|
3853
|
+
const resultLines = [];
|
|
3854
|
+
for (const [skillName, skillResults] of bySkill) {
|
|
3855
|
+
const firstResult = skillResults[0];
|
|
3856
|
+
const pkg = toInstall.find((s) => s.name === skillName)?.packageName;
|
|
3857
|
+
if (firstResult.canonicalPath) {
|
|
3858
|
+
const shortPath = shortenPath$1(firstResult.canonicalPath, cwd);
|
|
3859
|
+
resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`);
|
|
3860
|
+
resultLines.push(` ${import_picocolors.default.dim(shortPath)}`);
|
|
3861
|
+
} else resultLines.push(`${import_picocolors.default.green("✓")} ${skillName} ${import_picocolors.default.dim(`← ${pkg}`)}`);
|
|
3862
|
+
}
|
|
3863
|
+
const skillCount = bySkill.size;
|
|
3864
|
+
const title = import_picocolors.default.green(`Synced ${skillCount} skill${skillCount !== 1 ? "s" : ""}`);
|
|
3865
|
+
Me(resultLines.join("\n"), title);
|
|
3866
|
+
}
|
|
3867
|
+
if (failed.length > 0) {
|
|
3868
|
+
console.log();
|
|
3869
|
+
M.error(import_picocolors.default.red(`Failed to install ${failed.length}`));
|
|
3870
|
+
for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill} → ${r.agent}: ${import_picocolors.default.dim(r.error)}`);
|
|
3871
|
+
}
|
|
3872
|
+
track({
|
|
3873
|
+
event: "experimental_sync",
|
|
3874
|
+
skillCount: String(toInstall.length),
|
|
3875
|
+
successCount: String(successfulSkillNames.size),
|
|
3876
|
+
agents: targetAgents.join(",")
|
|
3877
|
+
});
|
|
3878
|
+
console.log();
|
|
3879
|
+
Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
|
|
3880
|
+
}
|
|
3881
|
+
function parseSyncOptions(args) {
|
|
3882
|
+
const options = {};
|
|
3883
|
+
for (let i = 0; i < args.length; i++) {
|
|
3884
|
+
const arg = args[i];
|
|
3885
|
+
if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
3886
|
+
else if (arg === "-f" || arg === "--force") options.force = true;
|
|
3887
|
+
else if (arg === "-a" || arg === "--agent") {
|
|
3888
|
+
options.agent = options.agent || [];
|
|
3889
|
+
i++;
|
|
3890
|
+
let nextArg = args[i];
|
|
3891
|
+
while (i < args.length && nextArg && !nextArg.startsWith("-")) {
|
|
3892
|
+
options.agent.push(nextArg);
|
|
3893
|
+
i++;
|
|
3894
|
+
nextArg = args[i];
|
|
3895
|
+
}
|
|
3896
|
+
i--;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
return { options };
|
|
3900
|
+
}
|
|
3901
|
+
async function runInstallFromLock(args) {
|
|
3902
|
+
const lock = await readLocalLock(process.cwd());
|
|
3903
|
+
const skillEntries = Object.entries(lock.skills);
|
|
3904
|
+
if (skillEntries.length === 0) {
|
|
3905
|
+
M.warn("No project skills found in skills-lock.json");
|
|
3906
|
+
M.info(`Add project-level skills with ${import_picocolors.default.cyan("npx skills add <package>")} (without ${import_picocolors.default.cyan("-g")})`);
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3909
|
+
const universalAgentNames = getUniversalAgents();
|
|
3910
|
+
const nodeModuleSkills = [];
|
|
3911
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
3912
|
+
for (const [skillName, entry] of skillEntries) {
|
|
3913
|
+
if (entry.sourceType === "node_modules") {
|
|
3914
|
+
nodeModuleSkills.push(skillName);
|
|
3915
|
+
continue;
|
|
3916
|
+
}
|
|
3917
|
+
const existing = bySource.get(entry.source);
|
|
3918
|
+
if (existing) existing.skills.push(skillName);
|
|
3919
|
+
else bySource.set(entry.source, {
|
|
3920
|
+
sourceType: entry.sourceType,
|
|
3921
|
+
skills: [skillName]
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
const remoteCount = skillEntries.length - nodeModuleSkills.length;
|
|
3925
|
+
if (remoteCount > 0) M.info(`Restoring ${import_picocolors.default.cyan(String(remoteCount))} skill${remoteCount !== 1 ? "s" : ""} from skills-lock.json into ${import_picocolors.default.dim(".agents/skills/")}`);
|
|
3926
|
+
for (const [source, { skills }] of bySource) try {
|
|
3927
|
+
await runAdd([source], {
|
|
3928
|
+
skill: skills,
|
|
3929
|
+
agent: universalAgentNames,
|
|
3930
|
+
yes: true
|
|
3931
|
+
});
|
|
3932
|
+
} catch (error) {
|
|
3933
|
+
M.error(`Failed to install from ${import_picocolors.default.cyan(source)}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3934
|
+
}
|
|
3935
|
+
if (nodeModuleSkills.length > 0) {
|
|
3936
|
+
M.info(`${import_picocolors.default.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? "s" : ""} from node_modules`);
|
|
3937
|
+
try {
|
|
3938
|
+
const { options: syncOptions } = parseSyncOptions(args);
|
|
3939
|
+
await runSync(args, {
|
|
3940
|
+
...syncOptions,
|
|
3941
|
+
yes: true,
|
|
3942
|
+
agent: universalAgentNames
|
|
3943
|
+
});
|
|
3944
|
+
} catch (error) {
|
|
3945
|
+
M.error(`Failed to sync node_modules skills: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
const RESET$1 = "\x1B[0m";
|
|
3950
|
+
const BOLD$1 = "\x1B[1m";
|
|
3951
|
+
const DIM$1 = "\x1B[38;5;102m";
|
|
3952
|
+
const CYAN = "\x1B[36m";
|
|
3953
|
+
const YELLOW = "\x1B[33m";
|
|
3954
|
+
function shortenPath(fullPath, cwd) {
|
|
3955
|
+
const home = homedir();
|
|
3956
|
+
if (fullPath.startsWith(home)) return fullPath.replace(home, "~");
|
|
3957
|
+
if (fullPath.startsWith(cwd)) return "." + fullPath.slice(cwd.length);
|
|
3958
|
+
return fullPath;
|
|
3959
|
+
}
|
|
3960
|
+
function formatList(items, maxShow = 5) {
|
|
3961
|
+
if (items.length <= maxShow) return items.join(", ");
|
|
3962
|
+
const shown = items.slice(0, maxShow);
|
|
3963
|
+
const remaining = items.length - maxShow;
|
|
3964
|
+
return `${shown.join(", ")} +${remaining} more`;
|
|
3965
|
+
}
|
|
3966
|
+
function parseListOptions(args) {
|
|
3967
|
+
const options = {};
|
|
3968
|
+
for (let i = 0; i < args.length; i++) {
|
|
3969
|
+
const arg = args[i];
|
|
3970
|
+
if (arg === "-g" || arg === "--global") options.global = true;
|
|
3971
|
+
else if (arg === "--json") options.json = true;
|
|
3972
|
+
else if (arg === "-a" || arg === "--agent") {
|
|
3973
|
+
options.agent = options.agent || [];
|
|
3974
|
+
while (i + 1 < args.length && !args[i + 1].startsWith("-")) options.agent.push(args[++i]);
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
return options;
|
|
3978
|
+
}
|
|
3979
|
+
async function runList(args) {
|
|
3980
|
+
const options = parseListOptions(args);
|
|
3981
|
+
const scope = options.global === true ? true : false;
|
|
3982
|
+
let agentFilter;
|
|
3983
|
+
if (options.agent && options.agent.length > 0) {
|
|
3984
|
+
const validAgents = Object.keys(agents);
|
|
3985
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
3986
|
+
if (invalidAgents.length > 0) {
|
|
3987
|
+
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$1}`);
|
|
3988
|
+
console.log(`${DIM$1}Valid agents: ${validAgents.join(", ")}${RESET$1}`);
|
|
3989
|
+
process.exit(1);
|
|
3990
|
+
}
|
|
3991
|
+
agentFilter = options.agent;
|
|
3992
|
+
}
|
|
3993
|
+
const installedSkills = await listInstalledSkills({
|
|
3994
|
+
global: scope,
|
|
3995
|
+
agentFilter
|
|
3996
|
+
});
|
|
3997
|
+
if (options.json) {
|
|
3998
|
+
const jsonOutput = installedSkills.map((skill) => ({
|
|
3999
|
+
name: skill.name,
|
|
4000
|
+
path: skill.canonicalPath,
|
|
4001
|
+
scope: skill.scope,
|
|
4002
|
+
agents: skill.agents.map((a) => agents[a].displayName)
|
|
4003
|
+
}));
|
|
4004
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
4007
|
+
const lockedSkills = await getAllLockedSkills();
|
|
4008
|
+
const cwd = process.cwd();
|
|
4009
|
+
const scopeLabel = scope ? "Global" : "Project";
|
|
4010
|
+
if (installedSkills.length === 0) {
|
|
4011
|
+
if (options.json) {
|
|
4012
|
+
console.log("[]");
|
|
4013
|
+
return;
|
|
4014
|
+
}
|
|
4015
|
+
console.log(`${DIM$1}No ${scopeLabel.toLowerCase()} skills found.${RESET$1}`);
|
|
4016
|
+
if (scope) console.log(`${DIM$1}Try listing project skills without -g${RESET$1}`);
|
|
4017
|
+
else console.log(`${DIM$1}Try listing global skills with -g${RESET$1}`);
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
function printSkill(skill, indent = false) {
|
|
4021
|
+
const prefix = indent ? " " : "";
|
|
4022
|
+
const shortPath = shortenPath(skill.canonicalPath, cwd);
|
|
4023
|
+
const agentNames = skill.agents.map((a) => agents[a].displayName);
|
|
4024
|
+
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$1}`;
|
|
4025
|
+
console.log(`${prefix}${CYAN}${skill.name}${RESET$1} ${DIM$1}${shortPath}${RESET$1}`);
|
|
4026
|
+
console.log(`${prefix} ${DIM$1}Agents:${RESET$1} ${agentInfo}`);
|
|
4027
|
+
}
|
|
4028
|
+
console.log(`${BOLD$1}${scopeLabel} Skills${RESET$1}`);
|
|
4029
|
+
console.log();
|
|
4030
|
+
const groupedSkills = {};
|
|
4031
|
+
const ungroupedSkills = [];
|
|
4032
|
+
for (const skill of installedSkills) {
|
|
4033
|
+
const lockEntry = lockedSkills[skill.name];
|
|
4034
|
+
if (lockEntry?.pluginName) {
|
|
4035
|
+
const group = lockEntry.pluginName;
|
|
4036
|
+
if (!groupedSkills[group]) groupedSkills[group] = [];
|
|
4037
|
+
groupedSkills[group].push(skill);
|
|
4038
|
+
} else ungroupedSkills.push(skill);
|
|
4039
|
+
}
|
|
4040
|
+
if (Object.keys(groupedSkills).length > 0) {
|
|
4041
|
+
const sortedGroups = Object.keys(groupedSkills).sort();
|
|
4042
|
+
for (const group of sortedGroups) {
|
|
4043
|
+
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4044
|
+
console.log(`${BOLD$1}${title}${RESET$1}`);
|
|
4045
|
+
const skills = groupedSkills[group];
|
|
4046
|
+
if (skills) for (const skill of skills) printSkill(skill, true);
|
|
4047
|
+
console.log();
|
|
4048
|
+
}
|
|
4049
|
+
if (ungroupedSkills.length > 0) {
|
|
4050
|
+
console.log(`${BOLD$1}General${RESET$1}`);
|
|
4051
|
+
for (const skill of ungroupedSkills) printSkill(skill, true);
|
|
4052
|
+
console.log();
|
|
4053
|
+
}
|
|
4054
|
+
} else {
|
|
4055
|
+
for (const skill of installedSkills) printSkill(skill);
|
|
4056
|
+
console.log();
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
async function removeCommand(skillNames, options) {
|
|
4060
|
+
const isGlobal = options.global ?? false;
|
|
4061
|
+
const cwd = process.cwd();
|
|
4062
|
+
const spinner = Y();
|
|
4063
|
+
spinner.start("Scanning for installed skills...");
|
|
4064
|
+
const skillNamesSet = /* @__PURE__ */ new Set();
|
|
4065
|
+
const scanDir = async (dir) => {
|
|
4066
|
+
try {
|
|
4067
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
4068
|
+
for (const entry of entries) if (entry.isDirectory()) skillNamesSet.add(entry.name);
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
if (err instanceof Error && err.code !== "ENOENT") M.warn(`Could not scan directory ${dir}: ${err.message}`);
|
|
4071
|
+
}
|
|
4072
|
+
};
|
|
4073
|
+
if (isGlobal) {
|
|
4074
|
+
await scanDir(getCanonicalSkillsDir(true, cwd));
|
|
4075
|
+
for (const agent of Object.values(agents)) if (agent.globalSkillsDir !== void 0) await scanDir(agent.globalSkillsDir);
|
|
4076
|
+
} else {
|
|
4077
|
+
await scanDir(getCanonicalSkillsDir(false, cwd));
|
|
4078
|
+
for (const agent of Object.values(agents)) await scanDir(join(cwd, agent.skillsDir));
|
|
4079
|
+
}
|
|
4080
|
+
const installedSkills = Array.from(skillNamesSet).sort();
|
|
4081
|
+
spinner.stop(`Found ${installedSkills.length} unique installed skill(s)`);
|
|
4082
|
+
if (installedSkills.length === 0) {
|
|
4083
|
+
Se(import_picocolors.default.yellow("No skills found to remove."));
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
if (options.agent && options.agent.length > 0) {
|
|
4087
|
+
const validAgents = Object.keys(agents);
|
|
4088
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
4089
|
+
if (invalidAgents.length > 0) {
|
|
4090
|
+
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
4091
|
+
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
4092
|
+
process.exit(1);
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
let selectedSkills = [];
|
|
4096
|
+
if (options.all) selectedSkills = installedSkills;
|
|
4097
|
+
else if (skillNames.length > 0) {
|
|
4098
|
+
selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase()));
|
|
4099
|
+
if (selectedSkills.length === 0) {
|
|
4100
|
+
M.error(`No matching skills found for: ${skillNames.join(", ")}`);
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
} else {
|
|
4104
|
+
const choices = installedSkills.map((s) => ({
|
|
4105
|
+
value: s,
|
|
4106
|
+
label: s
|
|
4107
|
+
}));
|
|
4108
|
+
const selected = await fe({
|
|
4109
|
+
message: `Select skills to remove ${import_picocolors.default.dim("(space to toggle)")}`,
|
|
4110
|
+
options: choices,
|
|
4111
|
+
required: true
|
|
4112
|
+
});
|
|
4113
|
+
if (pD(selected)) {
|
|
4114
|
+
xe("Removal cancelled");
|
|
4115
|
+
process.exit(0);
|
|
4116
|
+
}
|
|
4117
|
+
selectedSkills = selected;
|
|
4118
|
+
}
|
|
4119
|
+
let targetAgents;
|
|
4120
|
+
if (options.agent && options.agent.length > 0) targetAgents = options.agent;
|
|
4121
|
+
else {
|
|
4122
|
+
targetAgents = Object.keys(agents);
|
|
4123
|
+
spinner.stop(`Targeting ${targetAgents.length} potential agent(s)`);
|
|
4124
|
+
}
|
|
4125
|
+
if (!options.yes) {
|
|
4126
|
+
console.log();
|
|
4127
|
+
M.info("Skills to remove:");
|
|
4128
|
+
for (const skill of selectedSkills) M.message(` ${import_picocolors.default.red("•")} ${skill}`);
|
|
4129
|
+
console.log();
|
|
4130
|
+
const confirmed = await ye({ message: `Are you sure you want to uninstall ${selectedSkills.length} skill(s)?` });
|
|
4131
|
+
if (pD(confirmed) || !confirmed) {
|
|
4132
|
+
xe("Removal cancelled");
|
|
4133
|
+
process.exit(0);
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
spinner.start("Removing skills...");
|
|
4137
|
+
const results = [];
|
|
4138
|
+
for (const skillName of selectedSkills) try {
|
|
4139
|
+
const canonicalPath = getCanonicalPath(skillName, {
|
|
4140
|
+
global: isGlobal,
|
|
4141
|
+
cwd
|
|
4142
|
+
});
|
|
4143
|
+
for (const agentKey of targetAgents) {
|
|
4144
|
+
const agent = agents[agentKey];
|
|
4145
|
+
const skillPath = getInstallPath(skillName, agentKey, {
|
|
4146
|
+
global: isGlobal,
|
|
4147
|
+
cwd
|
|
4148
|
+
});
|
|
4149
|
+
const pathsToCleanup = new Set([skillPath]);
|
|
4150
|
+
const sanitizedName = sanitizeName(skillName);
|
|
4151
|
+
if (isGlobal && agent.globalSkillsDir) pathsToCleanup.add(join(agent.globalSkillsDir, sanitizedName));
|
|
4152
|
+
else pathsToCleanup.add(join(cwd, agent.skillsDir, sanitizedName));
|
|
4153
|
+
for (const pathToCleanup of pathsToCleanup) {
|
|
4154
|
+
if (pathToCleanup === canonicalPath) continue;
|
|
4155
|
+
try {
|
|
4156
|
+
if (await lstat(pathToCleanup).catch(() => null)) await rm(pathToCleanup, {
|
|
4157
|
+
recursive: true,
|
|
4158
|
+
force: true
|
|
4159
|
+
});
|
|
4160
|
+
} catch (err) {
|
|
4161
|
+
M.warn(`Could not remove skill from ${agent.displayName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
const remainingAgents = (await detectInstalledAgents()).filter((a) => !targetAgents.includes(a));
|
|
4166
|
+
let isStillUsed = false;
|
|
4167
|
+
for (const agentKey of remainingAgents) if (await lstat(getInstallPath(skillName, agentKey, {
|
|
4168
|
+
global: isGlobal,
|
|
4169
|
+
cwd
|
|
4170
|
+
})).catch(() => null)) {
|
|
4171
|
+
isStillUsed = true;
|
|
4172
|
+
break;
|
|
4173
|
+
}
|
|
4174
|
+
if (!isStillUsed) await rm(canonicalPath, {
|
|
4175
|
+
recursive: true,
|
|
4176
|
+
force: true
|
|
4177
|
+
});
|
|
4178
|
+
const lockEntry = isGlobal ? await getSkillFromLock(skillName) : null;
|
|
4179
|
+
const effectiveSource = lockEntry?.source || "local";
|
|
4180
|
+
const effectiveSourceType = lockEntry?.sourceType || "local";
|
|
4181
|
+
if (isGlobal) await removeSkillFromLock(skillName);
|
|
4182
|
+
results.push({
|
|
4183
|
+
skill: skillName,
|
|
4184
|
+
success: true,
|
|
4185
|
+
source: effectiveSource,
|
|
4186
|
+
sourceType: effectiveSourceType
|
|
4187
|
+
});
|
|
4188
|
+
} catch (err) {
|
|
4189
|
+
results.push({
|
|
4190
|
+
skill: skillName,
|
|
4191
|
+
success: false,
|
|
4192
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4193
|
+
});
|
|
4194
|
+
}
|
|
4195
|
+
spinner.stop("Removal process complete");
|
|
4196
|
+
const successful = results.filter((r) => r.success);
|
|
4197
|
+
const failed = results.filter((r) => !r.success);
|
|
4198
|
+
if (successful.length > 0) {
|
|
4199
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
4200
|
+
for (const r of successful) {
|
|
4201
|
+
const source = r.source || "local";
|
|
4202
|
+
const existing = bySource.get(source) || { skills: [] };
|
|
4203
|
+
existing.skills.push(r.skill);
|
|
4204
|
+
existing.sourceType = r.sourceType;
|
|
4205
|
+
bySource.set(source, existing);
|
|
4206
|
+
}
|
|
4207
|
+
for (const [source, data] of bySource) track({
|
|
4208
|
+
event: "remove",
|
|
4209
|
+
source,
|
|
4210
|
+
skills: data.skills.join(","),
|
|
4211
|
+
agents: targetAgents.join(","),
|
|
4212
|
+
...isGlobal && { global: "1" },
|
|
4213
|
+
sourceType: data.sourceType
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
if (successful.length > 0) M.success(import_picocolors.default.green(`Successfully removed ${successful.length} skill(s)`));
|
|
4217
|
+
if (failed.length > 0) {
|
|
4218
|
+
M.error(import_picocolors.default.red(`Failed to remove ${failed.length} skill(s)`));
|
|
4219
|
+
for (const r of failed) M.message(` ${import_picocolors.default.red("✗")} ${r.skill}: ${r.error}`);
|
|
4220
|
+
}
|
|
4221
|
+
console.log();
|
|
4222
|
+
Se(import_picocolors.default.green("Done!"));
|
|
4223
|
+
}
|
|
4224
|
+
function parseRemoveOptions(args) {
|
|
4225
|
+
const options = {};
|
|
4226
|
+
const skills = [];
|
|
4227
|
+
for (let i = 0; i < args.length; i++) {
|
|
4228
|
+
const arg = args[i];
|
|
4229
|
+
if (arg === "-g" || arg === "--global") options.global = true;
|
|
4230
|
+
else if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
4231
|
+
else if (arg === "--all") options.all = true;
|
|
4232
|
+
else if (arg === "-a" || arg === "--agent") {
|
|
4233
|
+
options.agent = options.agent || [];
|
|
4234
|
+
i++;
|
|
4235
|
+
let nextArg = args[i];
|
|
4236
|
+
while (i < args.length && nextArg && !nextArg.startsWith("-")) {
|
|
4237
|
+
options.agent.push(nextArg);
|
|
4238
|
+
i++;
|
|
4239
|
+
nextArg = args[i];
|
|
4240
|
+
}
|
|
4241
|
+
i--;
|
|
4242
|
+
} else if (arg && !arg.startsWith("-")) skills.push(arg);
|
|
4243
|
+
}
|
|
4244
|
+
return {
|
|
4245
|
+
skills,
|
|
4246
|
+
options
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
4250
|
+
function getVersion() {
|
|
4251
|
+
try {
|
|
4252
|
+
const pkgPath = join(__dirname, "..", "package.json");
|
|
4253
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
4254
|
+
} catch {
|
|
4255
|
+
return "1.5.0";
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
const VERSION = getVersion();
|
|
4259
|
+
initTelemetry(VERSION);
|
|
4260
|
+
const RESET = "\x1B[0m";
|
|
4261
|
+
const BOLD = "\x1B[1m";
|
|
4262
|
+
const DIM = "\x1B[38;5;102m";
|
|
4263
|
+
const TEXT = "\x1B[38;5;145m";
|
|
4264
|
+
const LOGO_LINES = [
|
|
4265
|
+
"███████╗██╗ ██╗██╗██╗ ██╗ ███████╗",
|
|
4266
|
+
"██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝",
|
|
4267
|
+
"███████╗█████╔╝ ██║██║ ██║ ███████╗",
|
|
4268
|
+
"╚════██║██╔═██╗ ██║██║ ██║ ╚════██║",
|
|
4269
|
+
"███████║██║ ██╗██║███████╗███████╗███████║",
|
|
4270
|
+
"╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝"
|
|
4271
|
+
];
|
|
4272
|
+
const GRAYS = [
|
|
4273
|
+
"\x1B[38;5;250m",
|
|
4274
|
+
"\x1B[38;5;248m",
|
|
4275
|
+
"\x1B[38;5;245m",
|
|
4276
|
+
"\x1B[38;5;243m",
|
|
4277
|
+
"\x1B[38;5;240m",
|
|
4278
|
+
"\x1B[38;5;238m"
|
|
4279
|
+
];
|
|
4280
|
+
function showLogo() {
|
|
4281
|
+
console.log();
|
|
4282
|
+
LOGO_LINES.forEach((line, i) => {
|
|
4283
|
+
console.log(`${GRAYS[i]}${line}${RESET}`);
|
|
4284
|
+
});
|
|
4285
|
+
}
|
|
4286
|
+
function showBanner() {
|
|
4287
|
+
showLogo();
|
|
4288
|
+
console.log();
|
|
4289
|
+
console.log(`${DIM}The open agent skills ecosystem${RESET}`);
|
|
4290
|
+
console.log();
|
|
4291
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills add ${DIM}<package>${RESET} ${DIM}Add a new skill${RESET}`);
|
|
4292
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills remove${RESET} ${DIM}Remove installed skills${RESET}`);
|
|
4293
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills list${RESET} ${DIM}List installed skills${RESET}`);
|
|
4294
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills find ${DIM}[query]${RESET} ${DIM}Search for skills${RESET}`);
|
|
4295
|
+
console.log();
|
|
4296
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills check${RESET} ${DIM}Check for updates${RESET}`);
|
|
4297
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills update${RESET} ${DIM}Update all skills${RESET}`);
|
|
4298
|
+
console.log();
|
|
4299
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
|
|
4300
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
|
|
4301
|
+
console.log(` ${DIM}$${RESET} ${TEXT}npx skills experimental_sync${RESET} ${DIM}Sync skills from node_modules${RESET}`);
|
|
4302
|
+
console.log();
|
|
4303
|
+
console.log(`${DIM}try:${RESET} npx skills add vercel-labs/agent-skills`);
|
|
4304
|
+
console.log();
|
|
4305
|
+
console.log(`Discover more skills at ${TEXT}https://skills.sh/${RESET}`);
|
|
4306
|
+
console.log();
|
|
4307
|
+
}
|
|
4308
|
+
function showHelp() {
|
|
4309
|
+
console.log(`
|
|
4310
|
+
${BOLD}Usage:${RESET} skills <command> [options]
|
|
4311
|
+
|
|
4312
|
+
${BOLD}Manage Skills:${RESET}
|
|
4313
|
+
add <package> Add a skill package (alias: a)
|
|
4314
|
+
e.g. vercel-labs/agent-skills
|
|
4315
|
+
https://github.com/vercel-labs/agent-skills
|
|
4316
|
+
remove [skills] Remove installed skills
|
|
4317
|
+
list, ls List installed skills
|
|
4318
|
+
find [query] Search for skills interactively
|
|
4319
|
+
|
|
4320
|
+
${BOLD}Authentication:${RESET}
|
|
4321
|
+
login Authenticate to the skills registry (OAuth2)
|
|
4322
|
+
logout Clear stored registry credentials
|
|
4323
|
+
|
|
4324
|
+
${BOLD}Updates:${RESET}
|
|
4325
|
+
check Check for available skill updates
|
|
4326
|
+
update Update all skills to latest versions
|
|
4327
|
+
|
|
4328
|
+
${BOLD}Project:${RESET}
|
|
4329
|
+
experimental_install Restore skills from skills-lock.json
|
|
4330
|
+
init [name] Initialize a skill (creates <name>/SKILL.md or ./SKILL.md)
|
|
4331
|
+
experimental_sync Sync skills from node_modules into agent directories
|
|
4332
|
+
|
|
4333
|
+
${BOLD}Add Options:${RESET}
|
|
4334
|
+
-g, --global Install skill globally (user-level) instead of project-level
|
|
4335
|
+
-a, --agent <agents> Specify agents to install to (use '*' for all agents)
|
|
4336
|
+
-s, --skill <skills> Specify skill names to install (use '*' for all skills)
|
|
4337
|
+
-l, --list List available skills in the repository without installing
|
|
4338
|
+
-y, --yes Skip confirmation prompts
|
|
4339
|
+
--copy Copy files instead of symlinking to agent directories
|
|
4340
|
+
--all Shorthand for --skill '*' --agent '*' -y
|
|
4341
|
+
--full-depth Search all subdirectories even when a root SKILL.md exists
|
|
4342
|
+
--strict-audit Block installation if partner audit detects risks or fails
|
|
4343
|
+
--strict Fail if skills-lock.json already exists before install
|
|
4344
|
+
|
|
4345
|
+
${BOLD}Remove Options:${RESET}
|
|
4346
|
+
-g, --global Remove from global scope
|
|
4347
|
+
-a, --agent <agents> Remove from specific agents (use '*' for all agents)
|
|
4348
|
+
-s, --skill <skills> Specify skills to remove (use '*' for all skills)
|
|
4349
|
+
-y, --yes Skip confirmation prompts
|
|
4350
|
+
--all Shorthand for --skill '*' --agent '*' -y
|
|
4351
|
+
|
|
4352
|
+
${BOLD}Experimental Sync Options:${RESET}
|
|
4353
|
+
-a, --agent <agents> Specify agents to install to (use '*' for all agents)
|
|
4354
|
+
-y, --yes Skip confirmation prompts
|
|
4355
|
+
|
|
4356
|
+
${BOLD}List Options:${RESET}
|
|
4357
|
+
-g, --global List global skills (default: project)
|
|
4358
|
+
-a, --agent <agents> Filter by specific agents
|
|
4359
|
+
--json Output as JSON (machine-readable, no ANSI codes)
|
|
4360
|
+
|
|
4361
|
+
${BOLD}Options:${RESET}
|
|
4362
|
+
--help, -h Show this help message
|
|
4363
|
+
--version, -v Show version number
|
|
4364
|
+
|
|
4365
|
+
${BOLD}Examples:${RESET}
|
|
4366
|
+
${DIM}$${RESET} skills add vercel-labs/agent-skills
|
|
4367
|
+
${DIM}$${RESET} skills add vercel-labs/agent-skills -g
|
|
4368
|
+
${DIM}$${RESET} skills add vercel-labs/agent-skills --agent claude-code cursor
|
|
4369
|
+
${DIM}$${RESET} skills add vercel-labs/agent-skills --skill pr-review commit
|
|
4370
|
+
${DIM}$${RESET} skills remove ${DIM}# interactive remove${RESET}
|
|
4371
|
+
${DIM}$${RESET} skills remove web-design ${DIM}# remove by name${RESET}
|
|
4372
|
+
${DIM}$${RESET} skills rm --global frontend-design
|
|
4373
|
+
${DIM}$${RESET} skills list ${DIM}# list project skills${RESET}
|
|
4374
|
+
${DIM}$${RESET} skills ls -g ${DIM}# list global skills${RESET}
|
|
4375
|
+
${DIM}$${RESET} skills ls -a claude-code ${DIM}# filter by agent${RESET}
|
|
4376
|
+
${DIM}$${RESET} skills ls --json ${DIM}# JSON output${RESET}
|
|
4377
|
+
${DIM}$${RESET} skills find ${DIM}# interactive search${RESET}
|
|
4378
|
+
${DIM}$${RESET} skills find typescript ${DIM}# search by keyword${RESET}
|
|
4379
|
+
${DIM}$${RESET} skills check
|
|
4380
|
+
${DIM}$${RESET} skills update
|
|
4381
|
+
${DIM}$${RESET} skills experimental_install ${DIM}# restore from skills-lock.json${RESET}
|
|
4382
|
+
${DIM}$${RESET} skills init my-skill
|
|
4383
|
+
${DIM}$${RESET} skills experimental_sync ${DIM}# sync from node_modules${RESET}
|
|
4384
|
+
${DIM}$${RESET} skills experimental_sync -y ${DIM}# sync without prompts${RESET}
|
|
4385
|
+
|
|
4386
|
+
Discover more skills at ${TEXT}https://skills.sh/${RESET}
|
|
4387
|
+
`);
|
|
4388
|
+
}
|
|
4389
|
+
function showRemoveHelp() {
|
|
4390
|
+
console.log(`
|
|
4391
|
+
${BOLD}Usage:${RESET} skills remove [skills...] [options]
|
|
4392
|
+
|
|
4393
|
+
${BOLD}Description:${RESET}
|
|
4394
|
+
Remove installed skills from agents. If no skill names are provided,
|
|
4395
|
+
an interactive selection menu will be shown.
|
|
4396
|
+
|
|
4397
|
+
${BOLD}Arguments:${RESET}
|
|
4398
|
+
skills Optional skill names to remove (space-separated)
|
|
4399
|
+
|
|
4400
|
+
${BOLD}Options:${RESET}
|
|
4401
|
+
-g, --global Remove from global scope (~/) instead of project scope
|
|
4402
|
+
-a, --agent Remove from specific agents (use '*' for all agents)
|
|
4403
|
+
-s, --skill Specify skills to remove (use '*' for all skills)
|
|
4404
|
+
-y, --yes Skip confirmation prompts
|
|
4405
|
+
--all Shorthand for --skill '*' --agent '*' -y
|
|
4406
|
+
|
|
4407
|
+
${BOLD}Examples:${RESET}
|
|
4408
|
+
${DIM}$${RESET} skills remove ${DIM}# interactive selection${RESET}
|
|
4409
|
+
${DIM}$${RESET} skills remove my-skill ${DIM}# remove specific skill${RESET}
|
|
4410
|
+
${DIM}$${RESET} skills remove skill1 skill2 -y ${DIM}# remove multiple skills${RESET}
|
|
4411
|
+
${DIM}$${RESET} skills remove --global my-skill ${DIM}# remove from global scope${RESET}
|
|
4412
|
+
${DIM}$${RESET} skills rm --agent claude-code my-skill ${DIM}# remove from specific agent${RESET}
|
|
4413
|
+
${DIM}$${RESET} skills remove --all ${DIM}# remove all skills${RESET}
|
|
4414
|
+
${DIM}$${RESET} skills remove --skill '*' -a cursor ${DIM}# remove all skills from cursor${RESET}
|
|
4415
|
+
|
|
4416
|
+
Discover more skills at ${TEXT}https://skills.sh/${RESET}
|
|
4417
|
+
`);
|
|
4418
|
+
}
|
|
4419
|
+
function runInit(args) {
|
|
4420
|
+
const cwd = process.cwd();
|
|
4421
|
+
const skillName = args[0] || basename(cwd);
|
|
4422
|
+
const hasName = args[0] !== void 0;
|
|
4423
|
+
const skillDir = hasName ? join(cwd, skillName) : cwd;
|
|
4424
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
4425
|
+
const displayPath = hasName ? `${skillName}/SKILL.md` : "SKILL.md";
|
|
4426
|
+
if (existsSync(skillFile)) {
|
|
4427
|
+
console.log(`${TEXT}Skill already exists at ${DIM}${displayPath}${RESET}`);
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
if (hasName) mkdirSync(skillDir, { recursive: true });
|
|
4431
|
+
writeFileSync(skillFile, `---
|
|
4432
|
+
name: ${skillName}
|
|
4433
|
+
description: A brief description of what this skill does
|
|
4434
|
+
---
|
|
4435
|
+
|
|
4436
|
+
# ${skillName}
|
|
4437
|
+
|
|
4438
|
+
Instructions for the agent to follow when this skill is activated.
|
|
4439
|
+
|
|
4440
|
+
## When to use
|
|
4441
|
+
|
|
4442
|
+
Describe when this skill should be used.
|
|
4443
|
+
|
|
4444
|
+
## Instructions
|
|
4445
|
+
|
|
4446
|
+
1. First step
|
|
4447
|
+
2. Second step
|
|
4448
|
+
3. Additional steps as needed
|
|
4449
|
+
`);
|
|
4450
|
+
console.log(`${TEXT}Initialized skill: ${DIM}${skillName}${RESET}`);
|
|
4451
|
+
console.log();
|
|
4452
|
+
console.log(`${DIM}Created:${RESET}`);
|
|
4453
|
+
console.log(` ${displayPath}`);
|
|
4454
|
+
console.log();
|
|
4455
|
+
console.log(`${DIM}Next steps:${RESET}`);
|
|
4456
|
+
console.log(` 1. Edit ${TEXT}${displayPath}${RESET} to define your skill instructions`);
|
|
4457
|
+
console.log(` 2. Update the ${TEXT}name${RESET} and ${TEXT}description${RESET} in the frontmatter`);
|
|
4458
|
+
console.log();
|
|
4459
|
+
console.log(`${DIM}Publishing:${RESET}`);
|
|
4460
|
+
console.log(` ${DIM}GitHub:${RESET} Push to a repo, then ${TEXT}npx skills add <owner>/<repo>${RESET}`);
|
|
4461
|
+
console.log(` ${DIM}URL:${RESET} Host the file, then ${TEXT}npx skills add https://example.com/${displayPath}${RESET}`);
|
|
4462
|
+
console.log();
|
|
4463
|
+
console.log(`Browse existing skills for inspiration at ${TEXT}https://skills.sh/${RESET}`);
|
|
4464
|
+
console.log();
|
|
4465
|
+
}
|
|
4466
|
+
const AGENTS_DIR = ".agents";
|
|
4467
|
+
const LOCK_FILE = ".skill-lock.json";
|
|
4468
|
+
const CURRENT_LOCK_VERSION = 3;
|
|
4469
|
+
function getSkillLockPath() {
|
|
4470
|
+
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
4471
|
+
if (xdgStateHome) return join(xdgStateHome, "skills", LOCK_FILE);
|
|
4472
|
+
return join(homedir(), AGENTS_DIR, LOCK_FILE);
|
|
4473
|
+
}
|
|
4474
|
+
function readSkillLock() {
|
|
4475
|
+
const lockPath = getSkillLockPath();
|
|
4476
|
+
try {
|
|
4477
|
+
const content = readFileSync(lockPath, "utf-8");
|
|
4478
|
+
const parsed = JSON.parse(content);
|
|
4479
|
+
if (typeof parsed.version !== "number" || !parsed.skills) return {
|
|
4480
|
+
version: CURRENT_LOCK_VERSION,
|
|
4481
|
+
skills: {}
|
|
4482
|
+
};
|
|
4483
|
+
if (parsed.version < CURRENT_LOCK_VERSION) return {
|
|
4484
|
+
version: CURRENT_LOCK_VERSION,
|
|
4485
|
+
skills: {}
|
|
4486
|
+
};
|
|
4487
|
+
return parsed;
|
|
4488
|
+
} catch {
|
|
4489
|
+
return {
|
|
4490
|
+
version: CURRENT_LOCK_VERSION,
|
|
4491
|
+
skills: {}
|
|
4492
|
+
};
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
function getSkipReason(entry) {
|
|
4496
|
+
if (entry.sourceType === "local") return "Local path";
|
|
4497
|
+
if (entry.sourceType === "git") return "Git URL (hash tracking not supported)";
|
|
4498
|
+
if (!entry.skillFolderHash) return "No version hash available";
|
|
4499
|
+
if (!entry.skillPath) return "No skill path recorded";
|
|
4500
|
+
return "No version tracking";
|
|
4501
|
+
}
|
|
4502
|
+
function printSkippedSkills(skipped) {
|
|
4503
|
+
if (skipped.length === 0) return;
|
|
4504
|
+
console.log();
|
|
4505
|
+
console.log(`${DIM}${skipped.length} skill(s) cannot be checked automatically:${RESET}`);
|
|
4506
|
+
for (const skill of skipped) {
|
|
4507
|
+
console.log(` ${TEXT}•${RESET} ${skill.name} ${DIM}(${skill.reason})${RESET}`);
|
|
4508
|
+
console.log(` ${DIM}To update: ${TEXT}npx skills add ${skill.sourceUrl} -g -y${RESET}`);
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
async function runCheck(args = []) {
|
|
4512
|
+
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
4513
|
+
console.log();
|
|
4514
|
+
const lock = readSkillLock();
|
|
4515
|
+
const skillNames = Object.keys(lock.skills);
|
|
4516
|
+
if (skillNames.length === 0) {
|
|
4517
|
+
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
4518
|
+
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add <package>${RESET}`);
|
|
4519
|
+
return;
|
|
4520
|
+
}
|
|
4521
|
+
const token = getGitHubToken();
|
|
4522
|
+
const skillsBySource = /* @__PURE__ */ new Map();
|
|
4523
|
+
const skipped = [];
|
|
4524
|
+
for (const skillName of skillNames) {
|
|
4525
|
+
const entry = lock.skills[skillName];
|
|
4526
|
+
if (!entry) continue;
|
|
4527
|
+
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
4528
|
+
skipped.push({
|
|
4529
|
+
name: skillName,
|
|
4530
|
+
reason: getSkipReason(entry),
|
|
4531
|
+
sourceUrl: entry.sourceUrl
|
|
4532
|
+
});
|
|
4533
|
+
continue;
|
|
4534
|
+
}
|
|
4535
|
+
const existing = skillsBySource.get(entry.source) || [];
|
|
4536
|
+
existing.push({
|
|
4537
|
+
name: skillName,
|
|
4538
|
+
entry
|
|
4539
|
+
});
|
|
4540
|
+
skillsBySource.set(entry.source, existing);
|
|
4541
|
+
}
|
|
4542
|
+
const totalSkills = skillNames.length - skipped.length;
|
|
4543
|
+
if (totalSkills === 0) {
|
|
4544
|
+
console.log(`${DIM}No GitHub skills to check.${RESET}`);
|
|
4545
|
+
printSkippedSkills(skipped);
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
console.log(`${DIM}Checking ${totalSkills} skill(s) for updates...${RESET}`);
|
|
4549
|
+
const updates = [];
|
|
4550
|
+
const errors = [];
|
|
4551
|
+
for (const [source, skills] of skillsBySource) for (const { name, entry } of skills) try {
|
|
4552
|
+
const latestHash = await fetchSkillFolderHash(source, entry.skillPath, token);
|
|
4553
|
+
if (!latestHash) {
|
|
4554
|
+
errors.push({
|
|
4555
|
+
name,
|
|
4556
|
+
source,
|
|
4557
|
+
error: "Could not fetch from GitHub"
|
|
4558
|
+
});
|
|
4559
|
+
continue;
|
|
4560
|
+
}
|
|
4561
|
+
if (latestHash !== entry.skillFolderHash) updates.push({
|
|
4562
|
+
name,
|
|
4563
|
+
source
|
|
4564
|
+
});
|
|
4565
|
+
} catch (err) {
|
|
4566
|
+
errors.push({
|
|
4567
|
+
name,
|
|
4568
|
+
source,
|
|
4569
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
console.log();
|
|
4573
|
+
if (updates.length === 0) console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
4574
|
+
else {
|
|
4575
|
+
console.log(`${TEXT}${updates.length} update(s) available:${RESET}`);
|
|
4576
|
+
console.log();
|
|
4577
|
+
for (const update of updates) {
|
|
4578
|
+
console.log(` ${TEXT}↑${RESET} ${update.name}`);
|
|
4579
|
+
console.log(` ${DIM}source: ${update.source}${RESET}`);
|
|
4580
|
+
}
|
|
4581
|
+
console.log();
|
|
4582
|
+
console.log(`${DIM}Run${RESET} ${TEXT}npx skills update${RESET} ${DIM}to update all skills${RESET}`);
|
|
4583
|
+
}
|
|
4584
|
+
if (errors.length > 0) {
|
|
4585
|
+
console.log();
|
|
4586
|
+
console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
|
|
4587
|
+
console.log();
|
|
4588
|
+
for (const error of errors) {
|
|
4589
|
+
console.log(` ${DIM}✗${RESET} ${error.name}`);
|
|
4590
|
+
console.log(` ${DIM}source: ${error.source}${RESET}`);
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
printSkippedSkills(skipped);
|
|
4594
|
+
track({
|
|
4595
|
+
event: "check",
|
|
4596
|
+
skillCount: String(totalSkills),
|
|
4597
|
+
updatesAvailable: String(updates.length)
|
|
4598
|
+
});
|
|
4599
|
+
console.log();
|
|
4600
|
+
}
|
|
4601
|
+
async function runUpdate() {
|
|
4602
|
+
console.log(`${TEXT}Checking for skill updates...${RESET}`);
|
|
4603
|
+
console.log();
|
|
4604
|
+
const lock = readSkillLock();
|
|
4605
|
+
const skillNames = Object.keys(lock.skills);
|
|
4606
|
+
if (skillNames.length === 0) {
|
|
4607
|
+
console.log(`${DIM}No skills tracked in lock file.${RESET}`);
|
|
4608
|
+
console.log(`${DIM}Install skills with${RESET} ${TEXT}npx skills add <package>${RESET}`);
|
|
4609
|
+
return;
|
|
4610
|
+
}
|
|
4611
|
+
const token = getGitHubToken();
|
|
4612
|
+
const updates = [];
|
|
4613
|
+
const skipped = [];
|
|
4614
|
+
for (const skillName of skillNames) {
|
|
4615
|
+
const entry = lock.skills[skillName];
|
|
4616
|
+
if (!entry) continue;
|
|
4617
|
+
if (!entry.skillFolderHash || !entry.skillPath) {
|
|
4618
|
+
skipped.push({
|
|
4619
|
+
name: skillName,
|
|
4620
|
+
reason: getSkipReason(entry),
|
|
4621
|
+
sourceUrl: entry.sourceUrl
|
|
4622
|
+
});
|
|
4623
|
+
continue;
|
|
4624
|
+
}
|
|
4625
|
+
try {
|
|
4626
|
+
const latestHash = await fetchSkillFolderHash(entry.source, entry.skillPath, token);
|
|
4627
|
+
if (latestHash && latestHash !== entry.skillFolderHash) updates.push({
|
|
4628
|
+
name: skillName,
|
|
4629
|
+
source: entry.source,
|
|
4630
|
+
entry
|
|
4631
|
+
});
|
|
4632
|
+
} catch {}
|
|
4633
|
+
}
|
|
4634
|
+
if (skillNames.length - skipped.length === 0) {
|
|
4635
|
+
console.log(`${DIM}No skills to check.${RESET}`);
|
|
4636
|
+
printSkippedSkills(skipped);
|
|
4637
|
+
return;
|
|
4638
|
+
}
|
|
4639
|
+
if (updates.length === 0) {
|
|
4640
|
+
console.log(`${TEXT}✓ All skills are up to date${RESET}`);
|
|
4641
|
+
console.log();
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
console.log(`${TEXT}Found ${updates.length} update(s)${RESET}`);
|
|
4645
|
+
console.log();
|
|
4646
|
+
let successCount = 0;
|
|
4647
|
+
let failCount = 0;
|
|
4648
|
+
for (const update of updates) {
|
|
4649
|
+
console.log(`${TEXT}Updating ${update.name}...${RESET}`);
|
|
4650
|
+
let installUrl = update.entry.sourceUrl;
|
|
4651
|
+
if (update.entry.skillPath) {
|
|
4652
|
+
let skillFolder = update.entry.skillPath;
|
|
4653
|
+
if (skillFolder.endsWith("/SKILL.md")) skillFolder = skillFolder.slice(0, -9);
|
|
4654
|
+
else if (skillFolder.endsWith("SKILL.md")) skillFolder = skillFolder.slice(0, -8);
|
|
4655
|
+
if (skillFolder.endsWith("/")) skillFolder = skillFolder.slice(0, -1);
|
|
4656
|
+
installUrl = update.entry.sourceUrl.replace(/\.git$/, "").replace(/\/$/, "");
|
|
4657
|
+
installUrl = `${installUrl}/tree/main/${skillFolder}`;
|
|
4658
|
+
}
|
|
4659
|
+
const cliEntry = join(__dirname, "..", "bin", "cli.mjs");
|
|
4660
|
+
if (!existsSync(cliEntry)) {
|
|
4661
|
+
failCount++;
|
|
4662
|
+
console.log(` ${DIM}✗ Failed to update ${update.name}: CLI entrypoint not found at ${cliEntry}${RESET}`);
|
|
4663
|
+
continue;
|
|
4664
|
+
}
|
|
4665
|
+
if (spawnSync(process.execPath, [
|
|
4666
|
+
cliEntry,
|
|
4667
|
+
"add",
|
|
4668
|
+
installUrl,
|
|
4669
|
+
"-g",
|
|
4670
|
+
"-y"
|
|
4671
|
+
], {
|
|
4672
|
+
stdio: [
|
|
4673
|
+
"inherit",
|
|
4674
|
+
"pipe",
|
|
4675
|
+
"pipe"
|
|
4676
|
+
],
|
|
4677
|
+
encoding: "utf-8",
|
|
4678
|
+
shell: process.platform === "win32"
|
|
4679
|
+
}).status === 0) {
|
|
4680
|
+
successCount++;
|
|
4681
|
+
console.log(` ${TEXT}✓${RESET} Updated ${update.name}`);
|
|
4682
|
+
} else {
|
|
4683
|
+
failCount++;
|
|
4684
|
+
console.log(` ${DIM}✗ Failed to update ${update.name}${RESET}`);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
console.log();
|
|
4688
|
+
if (successCount > 0) console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
|
|
4689
|
+
if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
|
|
4690
|
+
track({
|
|
4691
|
+
event: "update",
|
|
4692
|
+
skillCount: String(updates.length),
|
|
4693
|
+
successCount: String(successCount),
|
|
4694
|
+
failCount: String(failCount)
|
|
4695
|
+
});
|
|
4696
|
+
console.log();
|
|
4697
|
+
}
|
|
4698
|
+
function parseLoginOptions(args) {
|
|
4699
|
+
const opts = {
|
|
4700
|
+
device: false,
|
|
4701
|
+
help: false
|
|
4702
|
+
};
|
|
4703
|
+
for (let i = 0; i < args.length; i++) switch (args[i]) {
|
|
4704
|
+
case "--registry":
|
|
4705
|
+
opts.registry = args[++i];
|
|
4706
|
+
break;
|
|
4707
|
+
case "--issuer":
|
|
4708
|
+
opts.issuer = args[++i];
|
|
4709
|
+
break;
|
|
4710
|
+
case "--device":
|
|
4711
|
+
opts.device = true;
|
|
4712
|
+
break;
|
|
4713
|
+
case "--help":
|
|
4714
|
+
case "-h":
|
|
4715
|
+
opts.help = true;
|
|
4716
|
+
break;
|
|
4717
|
+
}
|
|
4718
|
+
return opts;
|
|
4719
|
+
}
|
|
4720
|
+
function showLoginHelp() {
|
|
4721
|
+
console.log(`
|
|
4722
|
+
${BOLD}Usage:${RESET} skills login [options]
|
|
4723
|
+
|
|
4724
|
+
${BOLD}Description:${RESET}
|
|
4725
|
+
Authenticate against the skills registry using OAuth2. By default this opens
|
|
4726
|
+
your browser (Authorization Code + PKCE on a loopback redirect). On headless
|
|
4727
|
+
or SSH sessions it falls back to the device authorization grant.
|
|
4728
|
+
|
|
4729
|
+
${BOLD}Options:${RESET}
|
|
4730
|
+
--registry <url> Registry URL to authenticate against (default: ${DEFAULT_REGISTRY_URL})
|
|
4731
|
+
--issuer <url> OAuth2 issuer / OIDC discovery base (overrides auto-detection)
|
|
4732
|
+
--device Force the device authorization grant (for headless/SSH)
|
|
4733
|
+
--help, -h Show this help message
|
|
4734
|
+
|
|
4735
|
+
${BOLD}CI / headless:${RESET}
|
|
4736
|
+
Set ${TEXT}SKILLS_REGISTRY_TOKEN${RESET} to a machine token to skip interactive login.
|
|
4737
|
+
`);
|
|
4738
|
+
}
|
|
4739
|
+
async function runLoginCommand(args) {
|
|
4740
|
+
const opts = parseLoginOptions(args);
|
|
4741
|
+
if (opts.help) {
|
|
4742
|
+
showLoginHelp();
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
const registry = opts.registry || DEFAULT_REGISTRY_URL;
|
|
4746
|
+
console.log(`${TEXT}Logging in to ${DIM}${registry}${RESET}...`);
|
|
4747
|
+
console.log();
|
|
4748
|
+
try {
|
|
4749
|
+
const token = await runLogin({
|
|
4750
|
+
registry,
|
|
4751
|
+
issuer: opts.issuer,
|
|
4752
|
+
device: opts.device,
|
|
4753
|
+
log: (msg) => console.log(msg)
|
|
4754
|
+
});
|
|
4755
|
+
console.log();
|
|
4756
|
+
console.log(`${TEXT}✓ Logged in to ${registry}${RESET}`);
|
|
4757
|
+
if (token.expiresAt) {
|
|
4758
|
+
const mins = Math.max(0, Math.round((token.expiresAt - Date.now()) / 6e4));
|
|
4759
|
+
console.log(`${DIM}Access token valid for ~${mins} min (auto-refreshed when stored).${RESET}`);
|
|
4760
|
+
}
|
|
4761
|
+
} catch (err) {
|
|
4762
|
+
console.error(`${DIM}✗ Login failed:${RESET} ${err instanceof Error ? err.message : String(err)}`);
|
|
4763
|
+
process.exit(1);
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
function runLogoutCommand(args) {
|
|
4767
|
+
const opts = parseLoginOptions(args);
|
|
4768
|
+
if (opts.help) {
|
|
4769
|
+
console.log(`
|
|
4770
|
+
${BOLD}Usage:${RESET} skills logout [--registry <url>]
|
|
4771
|
+
|
|
4772
|
+
Clears the stored token for the registry (default: ${DEFAULT_REGISTRY_URL}).
|
|
4773
|
+
`);
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
const registry = opts.registry || DEFAULT_REGISTRY_URL;
|
|
4777
|
+
if (logout(registry)) console.log(`${TEXT}✓ Logged out of ${registry}${RESET}`);
|
|
4778
|
+
else console.log(`${DIM}No stored credentials for ${registry}.${RESET}`);
|
|
4779
|
+
}
|
|
4780
|
+
async function main() {
|
|
4781
|
+
const args = process.argv.slice(2);
|
|
4782
|
+
if (args.length === 0) {
|
|
4783
|
+
showBanner();
|
|
4784
|
+
return;
|
|
4785
|
+
}
|
|
4786
|
+
const command = args[0];
|
|
4787
|
+
const restArgs = args.slice(1);
|
|
4788
|
+
switch (command) {
|
|
4789
|
+
case "find":
|
|
4790
|
+
case "search":
|
|
4791
|
+
case "f":
|
|
4792
|
+
case "s":
|
|
4793
|
+
showLogo();
|
|
4794
|
+
console.log();
|
|
4795
|
+
await runFind(restArgs);
|
|
4796
|
+
break;
|
|
4797
|
+
case "init":
|
|
4798
|
+
showLogo();
|
|
4799
|
+
console.log();
|
|
4800
|
+
runInit(restArgs);
|
|
4801
|
+
break;
|
|
4802
|
+
case "experimental_install":
|
|
4803
|
+
showLogo();
|
|
4804
|
+
await runInstallFromLock(restArgs);
|
|
4805
|
+
break;
|
|
4806
|
+
case "i":
|
|
4807
|
+
case "install":
|
|
4808
|
+
case "a":
|
|
4809
|
+
case "add": {
|
|
4810
|
+
showLogo();
|
|
4811
|
+
const { source: addSource, options: addOpts } = parseAddOptions(restArgs);
|
|
4812
|
+
if (addSource.length === 0 && !addOpts.list) {
|
|
4813
|
+
const { runInstallFromManifest } = await import("./_chunks/manifest-cli.mjs");
|
|
4814
|
+
await runInstallFromManifest(addOpts);
|
|
4815
|
+
} else await runAdd(addSource, addOpts);
|
|
4816
|
+
break;
|
|
4817
|
+
}
|
|
4818
|
+
case "group":
|
|
4819
|
+
console.error("Error: The \"group\" command has been removed. Groups are now defined centrally in the skills registry.");
|
|
4820
|
+
process.exit(1);
|
|
4821
|
+
case "verify":
|
|
4822
|
+
case "check-manifest": {
|
|
4823
|
+
showLogo();
|
|
4824
|
+
const { runVerifyCommand } = await import("./_chunks/verify.mjs");
|
|
4825
|
+
await runVerifyCommand(restArgs);
|
|
4826
|
+
break;
|
|
4827
|
+
}
|
|
4828
|
+
case "generate-registry-hashes": {
|
|
4829
|
+
showLogo();
|
|
4830
|
+
const { generateRegistryHashesCommand } = await import("./_chunks/registry-hashes.mjs");
|
|
4831
|
+
await generateRegistryHashesCommand();
|
|
4832
|
+
break;
|
|
4833
|
+
}
|
|
4834
|
+
case "remove":
|
|
4835
|
+
case "rm":
|
|
4836
|
+
case "r":
|
|
4837
|
+
if (restArgs.includes("--help") || restArgs.includes("-h")) {
|
|
4838
|
+
showRemoveHelp();
|
|
4839
|
+
break;
|
|
4840
|
+
}
|
|
4841
|
+
const { skills, options: removeOptions } = parseRemoveOptions(restArgs);
|
|
4842
|
+
await removeCommand(skills, removeOptions);
|
|
4843
|
+
break;
|
|
4844
|
+
case "experimental_sync": {
|
|
4845
|
+
showLogo();
|
|
4846
|
+
const { options: syncOptions } = parseSyncOptions(restArgs);
|
|
4847
|
+
await runSync(restArgs, syncOptions);
|
|
4848
|
+
break;
|
|
4849
|
+
}
|
|
4850
|
+
case "list":
|
|
4851
|
+
case "ls":
|
|
4852
|
+
await runList(restArgs);
|
|
4853
|
+
break;
|
|
4854
|
+
case "login":
|
|
4855
|
+
showLogo();
|
|
4856
|
+
console.log();
|
|
4857
|
+
await runLoginCommand(restArgs);
|
|
4858
|
+
break;
|
|
4859
|
+
case "logout":
|
|
4860
|
+
showLogo();
|
|
4861
|
+
console.log();
|
|
4862
|
+
runLogoutCommand(restArgs);
|
|
4863
|
+
break;
|
|
4864
|
+
case "check":
|
|
4865
|
+
runCheck(restArgs);
|
|
4866
|
+
break;
|
|
4867
|
+
case "update":
|
|
4868
|
+
case "upgrade":
|
|
4869
|
+
runUpdate();
|
|
4870
|
+
break;
|
|
4871
|
+
case "--help":
|
|
4872
|
+
case "-h":
|
|
4873
|
+
showHelp();
|
|
4874
|
+
break;
|
|
4875
|
+
case "--version":
|
|
4876
|
+
case "-v":
|
|
4877
|
+
console.log(VERSION);
|
|
4878
|
+
break;
|
|
4879
|
+
default:
|
|
4880
|
+
console.log(`Unknown command: ${command}`);
|
|
4881
|
+
console.log(`Run ${BOLD}skills --help${RESET} for usage.`);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
main();
|
|
4885
|
+
export { getAgentBaseDir as a, detectInstalledAgents as c, readLocalLock as i, CENTRAL_REGISTRY_REPO as l, computeSkillFolderHash as n, getCanonicalSkillsDir as o, getLocalLockPath as r, agents as s, runAdd as t, parseSource as u };
|