get-skill 1.0.21 → 1.3.8
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/CHANGELOG.md +242 -117
- package/LICENSE +21 -0
- package/README.md +130 -50
- package/assets/skillmd-banner.jpg +0 -0
- package/bin/skillmd-mcp.mjs +2 -330
- package/bin/skillmd.mjs +1 -1
- package/dist/chunk-MPBRLZWJ.js +919 -0
- package/dist/cli.js +1497 -1073
- package/dist/mcp.js +581 -0
- package/package.json +43 -24
- package/vendor/index.js +0 -333
package/dist/cli.js
CHANGED
|
@@ -1,433 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
IntegrityError,
|
|
3
|
+
RegistryError,
|
|
4
|
+
agentDir,
|
|
5
|
+
agentSupportsGlobal,
|
|
6
|
+
canonicalDir,
|
|
7
|
+
collectFiles,
|
|
8
|
+
configPath,
|
|
9
|
+
createClient,
|
|
10
|
+
detectAgents,
|
|
11
|
+
digestOf,
|
|
12
|
+
fetchBundle,
|
|
13
|
+
gigetInput,
|
|
14
|
+
hostOf,
|
|
15
|
+
installSkill,
|
|
16
|
+
isSkillDirOrLink,
|
|
17
|
+
listInstalled,
|
|
18
|
+
looksLikeProject,
|
|
19
|
+
missingFilesNote,
|
|
20
|
+
parseSource,
|
|
21
|
+
readConfig,
|
|
22
|
+
readLock,
|
|
23
|
+
resolveApi,
|
|
24
|
+
resolvePackFiles,
|
|
25
|
+
resolveSource,
|
|
26
|
+
resolveTree,
|
|
27
|
+
safeText,
|
|
28
|
+
skillMdFor,
|
|
29
|
+
sourceId,
|
|
30
|
+
telemetryDisabled,
|
|
31
|
+
uninstallSkill,
|
|
32
|
+
upsertEntry,
|
|
33
|
+
writeConfig
|
|
34
|
+
} from "./chunk-MPBRLZWJ.js";
|
|
35
|
+
|
|
36
|
+
// src/index.ts
|
|
2
37
|
import { Command as Command13 } from "commander";
|
|
3
|
-
import { readFileSync as
|
|
38
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
39
|
+
import pc15 from "picocolors";
|
|
4
40
|
|
|
5
|
-
//
|
|
41
|
+
// src/commands/lint.ts
|
|
6
42
|
import { Command } from "commander";
|
|
7
43
|
import { writeFileSync } from "fs";
|
|
8
|
-
import { homedir } from "os";
|
|
9
|
-
import { resolve } from "path";
|
|
44
|
+
import { homedir as homedir2 } from "os";
|
|
45
|
+
import { resolve as resolve2 } from "path";
|
|
10
46
|
import pc2 from "picocolors";
|
|
11
47
|
import * as p from "@clack/prompts";
|
|
48
|
+
import { lint, toJson, toSarif, toGithub } from "@skillmds/core";
|
|
12
49
|
|
|
13
|
-
//
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
var MAX_DESCRIPTION = 1024;
|
|
17
|
-
var MAX_NAME = 120;
|
|
18
|
-
var slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "skill";
|
|
19
|
-
function parseSkillMd(raw, type = "single") {
|
|
20
|
-
if (!raw || raw.length > MAX_SKILL_BYTES) return { error: "SKILL.md missing or too large (max 256KB)" };
|
|
21
|
-
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
22
|
-
if (!m) return { error: "Missing YAML frontmatter (--- ... ---)" };
|
|
23
|
-
let fm;
|
|
24
|
-
try {
|
|
25
|
-
fm = parseYaml(m[1] ?? "") ?? {};
|
|
26
|
-
} catch {
|
|
27
|
-
return { error: "Invalid YAML frontmatter" };
|
|
28
|
-
}
|
|
29
|
-
const name = String(fm.name ?? "").trim();
|
|
30
|
-
const description = String(fm.description ?? "").replace(/\s+/g, " ").trim();
|
|
31
|
-
if (!name) return { error: "frontmatter 'name' is required" };
|
|
32
|
-
if (!description) return { error: "frontmatter 'description' is required" };
|
|
33
|
-
if (description.length > MAX_DESCRIPTION) return { error: `description must be <= ${MAX_DESCRIPTION} chars` };
|
|
34
|
-
return {
|
|
35
|
-
name: name.slice(0, MAX_NAME),
|
|
36
|
-
description: description.slice(0, MAX_DESCRIPTION),
|
|
37
|
-
license: fm.license ? String(fm.license).slice(0, 64) : null,
|
|
38
|
-
body: m[2] ?? "",
|
|
39
|
-
type
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ../../packages/core/src/security.ts
|
|
44
|
-
var UNTRUSTED_INSTALL_RE = new RegExp(
|
|
45
|
-
[
|
|
46
|
-
/(?:npx|npm|yarn|pnpm|bun)\b[^\n]*--registry[= ]/.source,
|
|
47
|
-
// custom npm registry
|
|
48
|
-
/npm\s+config\s+set\s+registry/.source,
|
|
49
|
-
/pip3?\s+install\b[^\n]*(?:--index-url|--extra-index-url|-i\s+https?:)/.source,
|
|
50
|
-
// custom pip index
|
|
51
|
-
/(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z|fi)?sh\b/.source,
|
|
52
|
-
// curl … | sh
|
|
53
|
-
/(?:iwr|irm|invoke-webrequest|invoke-restmethod)\b[^\n|]*\|\s*iex\b/.source,
|
|
54
|
-
// PowerShell pipe-to-exec
|
|
55
|
-
/bash\s+<\(\s*(?:curl|wget)/.source,
|
|
56
|
-
// bash <(curl …)
|
|
57
|
-
/(?:pip3?|npm|npx)\s+(?:install|--yes|-y)?\s*[^\n]*https?:\/\/(?!registry\.npmjs\.org|pypi\.org|files\.pythonhosted\.org)[^\s'"`]+\.(?:whl|tar\.gz|tgz|zip)\b/.source
|
|
58
|
-
// install from raw archive URL
|
|
59
|
-
].join("|"),
|
|
60
|
-
"i"
|
|
61
|
-
);
|
|
62
|
-
var RULES = [
|
|
63
|
-
["untrusted_install", UNTRUSTED_INSTALL_RE],
|
|
64
|
-
["executes_scripts", /```(?:sh|bash|python|py|js|ts|ruby|rb)\b|\b(subprocess|os\.system|child_process|exec\()/i],
|
|
65
|
-
["network_calls", /\b(fetch|requests\.|urllib|axios|curl|wget|https?:\/\/|websocket)\b/i],
|
|
66
|
-
["reads_secrets", /\b(api[_-]?key|secret|token|password|os\.environ|process\.env|getenv)\b/i]
|
|
67
|
-
];
|
|
68
|
-
function scanSecurity(body) {
|
|
69
|
-
const findings = [];
|
|
70
|
-
body.split(/\r?\n/).forEach((text3, i) => {
|
|
71
|
-
for (const [flag, re] of RULES) {
|
|
72
|
-
if (re.test(text3)) findings.push({ flag, line: i + 1, snippet: text3.trim().slice(0, 120) });
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
const flags = [...new Set(findings.map((f) => f.flag))];
|
|
76
|
-
return { flags: flags.length ? flags : ["docs_only"], findings };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ../../packages/core/src/rules.ts
|
|
80
|
-
function utf8Bytes(s) {
|
|
81
|
-
let bytes = 0;
|
|
82
|
-
for (let i = 0; i < s.length; i++) {
|
|
83
|
-
const c = s.charCodeAt(i);
|
|
84
|
-
if (c < 128) bytes += 1;
|
|
85
|
-
else if (c < 2048) bytes += 2;
|
|
86
|
-
else if (c >= 55296 && c <= 56319) {
|
|
87
|
-
bytes += 4;
|
|
88
|
-
i++;
|
|
89
|
-
} else bytes += 3;
|
|
90
|
-
}
|
|
91
|
-
return bytes;
|
|
92
|
-
}
|
|
93
|
-
var diag = (rule, message, line) => ({
|
|
94
|
-
id: rule.id,
|
|
95
|
-
severity: rule.severity,
|
|
96
|
-
message,
|
|
97
|
-
...line !== void 0 ? { line } : {}
|
|
98
|
-
});
|
|
99
|
-
var RULES2 = [
|
|
100
|
-
{
|
|
101
|
-
id: "SK002",
|
|
102
|
-
severity: "error",
|
|
103
|
-
description: "name must be present and \u2264 120 chars",
|
|
104
|
-
check(s, _ctx) {
|
|
105
|
-
const out = [];
|
|
106
|
-
if (!s.name.trim()) out.push(diag(this, "frontmatter 'name' is required"));
|
|
107
|
-
else if (s.name.length > MAX_NAME) out.push(diag(this, `name must be \u2264 ${MAX_NAME} chars`));
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
id: "SK003",
|
|
113
|
-
severity: "error",
|
|
114
|
-
description: "description must be present and \u2264 1024 chars",
|
|
115
|
-
check(s, _ctx) {
|
|
116
|
-
const out = [];
|
|
117
|
-
if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));
|
|
118
|
-
else if (s.description.length > MAX_DESCRIPTION) out.push(diag(this, `description must be \u2264 ${MAX_DESCRIPTION} chars`));
|
|
119
|
-
return out;
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
id: "SK010",
|
|
124
|
-
severity: "warn",
|
|
125
|
-
description: "description should be \u2265 20 chars (not too terse)",
|
|
126
|
-
check(s, _ctx) {
|
|
127
|
-
return s.description.trim().length > 0 && s.description.trim().length < 20 ? [diag(this, "description is very terse (< 20 chars); add more detail to help discovery")] : [];
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: "SK011",
|
|
132
|
-
severity: "warn",
|
|
133
|
-
description: "license should be declared in frontmatter",
|
|
134
|
-
check(s, _ctx) {
|
|
135
|
-
return s.license ? [] : [diag(this, "no 'license' declared in frontmatter")];
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
id: "SK020",
|
|
140
|
-
severity: "warn",
|
|
141
|
-
description: "body should be \u2265 200 chars (not a stub)",
|
|
142
|
-
check(s, _ctx) {
|
|
143
|
-
return s.body.trim().length < 200 ? [diag(this, "body is very short (< 200 chars); this looks like a stub")] : [];
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
id: "SK021",
|
|
148
|
-
severity: "warn",
|
|
149
|
-
description: "body should contain at least one Markdown heading",
|
|
150
|
-
check(s, _ctx) {
|
|
151
|
-
return /^#{1,6}\s/m.test(s.body) ? [] : [diag(this, "body has no Markdown headings; add structure with '#' headings")];
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
id: "SK030",
|
|
156
|
-
severity: "error",
|
|
157
|
-
description: "total SKILL.md size must be \u2264 256KB",
|
|
158
|
-
check(s, _ctx) {
|
|
159
|
-
return utf8Bytes(s.body) > MAX_SKILL_BYTES ? [diag(this, "SKILL.md exceeds 256KB")] : [];
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
id: "SK040",
|
|
164
|
-
severity: "warn",
|
|
165
|
-
description: "name should match its directory/slug",
|
|
166
|
-
check(s, ctx) {
|
|
167
|
-
if (!ctx.slug) return [];
|
|
168
|
-
const expected = slugify(s.name);
|
|
169
|
-
return expected === ctx.slug ? [] : [diag(this, `name slug '${expected}' does not match directory '${ctx.slug}'`)];
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
];
|
|
173
|
-
function runRules(skill, ctx = {}) {
|
|
174
|
-
return RULES2.flatMap((r) => r.check(skill, ctx));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ../../packages/core/src/quality.ts
|
|
178
|
-
var WEIGHT = {
|
|
179
|
-
error: 30,
|
|
180
|
-
warn: 10,
|
|
181
|
-
info: 3
|
|
182
|
-
};
|
|
183
|
-
var RISK = {
|
|
184
|
-
docs_only: 0,
|
|
185
|
-
reads_secrets: 2,
|
|
186
|
-
network_calls: 1,
|
|
187
|
-
executes_scripts: 5,
|
|
188
|
-
// fetch-and-execute from a non-standard source — the supply-chain pattern the
|
|
189
|
-
// AI verdict floors to "warning"; the heaviest penalty here for the same reason
|
|
190
|
-
untrusted_install: 15
|
|
191
|
-
};
|
|
192
|
-
var VERIFIED_FLOOR = 85;
|
|
193
|
-
function qualityScore(diagnostics, security, opts = {}) {
|
|
194
|
-
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
195
|
-
if (security) {
|
|
196
|
-
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
197
|
-
}
|
|
198
|
-
const score = Math.max(0, Math.min(100, 100 - penalty));
|
|
199
|
-
const hasError = diagnostics.some((d) => d.severity === "error");
|
|
200
|
-
return opts.verified && !hasError ? Math.max(score, VERIFIED_FLOOR) : score;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ../../packages/core/src/lint.ts
|
|
204
|
-
function lint(raw, opts = {}) {
|
|
205
|
-
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
206
|
-
if ("error" in parsed) {
|
|
207
|
-
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
208
|
-
const security2 = scanSecurity(raw);
|
|
209
|
-
return {
|
|
210
|
-
ok: false,
|
|
211
|
-
diagnostics: diagnostics2,
|
|
212
|
-
security: security2,
|
|
213
|
-
score: qualityScore(diagnostics2, security2, { verified: opts.verified })
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
217
|
-
const diagnostics = runRules(parsed, ctx);
|
|
218
|
-
const security = scanSecurity(parsed.body);
|
|
219
|
-
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
220
|
-
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security, { verified: opts.verified }) };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ../../packages/core/src/format/text.ts
|
|
224
|
-
var BADGE = { error: "ERROR", warn: " WARN", info: " INFO" };
|
|
225
|
-
function toText(results, opts = {}) {
|
|
226
|
-
const paint = opts.decorate ?? ((t) => t);
|
|
227
|
-
const out = [];
|
|
228
|
-
for (const { file, result } of results) {
|
|
229
|
-
const status = result.ok ? paint("PASS", "ok") : paint("FAIL", "error");
|
|
230
|
-
out.push(`${status} ${file} ${paint(`score ${result.score}/100`, "dim")}`);
|
|
231
|
-
for (const d of result.diagnostics) {
|
|
232
|
-
const where = d.line !== void 0 ? `:${d.line}` : "";
|
|
233
|
-
out.push(` ${paint(BADGE[d.severity], d.severity)} ${d.id}${where} ${d.message}`);
|
|
234
|
-
}
|
|
235
|
-
if (result.security.flags.length && result.security.flags[0] !== "docs_only") {
|
|
236
|
-
out.push(` ${paint("SEC ", "warn")} flags: ${result.security.flags.join(", ")}`);
|
|
237
|
-
}
|
|
238
|
-
out.push("");
|
|
239
|
-
}
|
|
240
|
-
const failed = results.filter((r) => !r.result.ok).length;
|
|
241
|
-
const summary = failed === 0 ? paint(`\u2713 ${results.length} skill(s) passed`, "ok") : paint(`\u2717 ${failed}/${results.length} skill(s) failed`, "error");
|
|
242
|
-
out.push(summary);
|
|
243
|
-
return out.join("\n");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ../../packages/core/src/format/json.ts
|
|
247
|
-
function toJson(results) {
|
|
248
|
-
return results.map(({ file, result }) => ({
|
|
249
|
-
file,
|
|
250
|
-
ok: result.ok,
|
|
251
|
-
score: result.score,
|
|
252
|
-
diagnostics: result.diagnostics,
|
|
253
|
-
security: result.security
|
|
254
|
-
}));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ../../packages/core/src/format/sarif.ts
|
|
258
|
-
var SARIF_LEVEL = { error: "error", warn: "warning", info: "note" };
|
|
259
|
-
function toSarif(results) {
|
|
260
|
-
const sarifResults = results.flatMap(
|
|
261
|
-
({ file, result }) => result.diagnostics.map((d) => ({
|
|
262
|
-
ruleId: d.id,
|
|
263
|
-
level: SARIF_LEVEL[d.severity] ?? "note",
|
|
264
|
-
message: { text: d.message },
|
|
265
|
-
locations: [
|
|
266
|
-
{
|
|
267
|
-
physicalLocation: {
|
|
268
|
-
artifactLocation: { uri: file },
|
|
269
|
-
region: { startLine: d.line ?? 1 }
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
]
|
|
273
|
-
}))
|
|
274
|
-
);
|
|
275
|
-
return {
|
|
276
|
-
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
277
|
-
version: "2.1.0",
|
|
278
|
-
runs: [
|
|
279
|
-
{
|
|
280
|
-
tool: {
|
|
281
|
-
driver: {
|
|
282
|
-
name: "skillmd",
|
|
283
|
-
informationUri: "https://skillmd.com",
|
|
284
|
-
rules: RULES2.map((r) => ({
|
|
285
|
-
id: r.id,
|
|
286
|
-
shortDescription: { text: r.description },
|
|
287
|
-
defaultConfiguration: { level: SARIF_LEVEL[r.severity] ?? "note" }
|
|
288
|
-
}))
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
results: sarifResults
|
|
292
|
-
}
|
|
293
|
-
]
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// ../../packages/core/src/format/github.ts
|
|
298
|
-
var LEVEL = { error: "error", warn: "warning", info: "notice" };
|
|
299
|
-
function toGithub(results) {
|
|
300
|
-
const lines = [];
|
|
301
|
-
for (const { file, result } of results) {
|
|
302
|
-
for (const d of result.diagnostics) {
|
|
303
|
-
const level = LEVEL[d.severity] ?? "notice";
|
|
304
|
-
const loc = d.line !== void 0 ? `,line=${d.line}` : "";
|
|
305
|
-
lines.push(`::${level} file=${file}${loc},title=${d.id}::${d.message}`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return lines.join("\n");
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ../cli/src/source.ts
|
|
312
|
-
import { existsSync, statSync, lstatSync, readFileSync, readdirSync, mkdtempSync, rmSync } from "fs";
|
|
313
|
-
import { tmpdir } from "os";
|
|
314
|
-
import { join, basename, dirname, relative, sep } from "path";
|
|
315
|
-
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
316
|
-
// build/dependency dirs
|
|
317
|
-
"node_modules",
|
|
318
|
-
".git",
|
|
319
|
-
"dist",
|
|
320
|
-
"build",
|
|
321
|
-
"out",
|
|
322
|
-
".next",
|
|
323
|
-
".wrangler",
|
|
324
|
-
".cache",
|
|
325
|
-
".npm",
|
|
326
|
-
".pnpm-store",
|
|
327
|
-
".venv",
|
|
328
|
-
"venv",
|
|
329
|
-
"__pycache__",
|
|
330
|
-
"target",
|
|
331
|
-
"vendor",
|
|
332
|
-
// large OS / user dirs (mostly relevant when run from a home directory)
|
|
333
|
-
"AppData",
|
|
334
|
-
"Library",
|
|
335
|
-
"$Recycle.Bin",
|
|
336
|
-
"System Volume Information",
|
|
337
|
-
"Windows",
|
|
338
|
-
"Program Files"
|
|
339
|
-
]);
|
|
340
|
-
var MAX_DEPTH = 12;
|
|
341
|
-
function findSkillFiles(dir, depth = 0) {
|
|
342
|
-
const out = [];
|
|
343
|
-
if (depth > MAX_DEPTH) return out;
|
|
344
|
-
let entries;
|
|
345
|
-
try {
|
|
346
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
347
|
-
} catch {
|
|
348
|
-
return out;
|
|
349
|
-
}
|
|
350
|
-
for (const entry of entries) {
|
|
351
|
-
if (entry.isDirectory()) {
|
|
352
|
-
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
353
|
-
out.push(...findSkillFiles(join(dir, entry.name), depth + 1));
|
|
354
|
-
} else if (entry.name === "SKILL.md") {
|
|
355
|
-
out.push(join(dir, entry.name));
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return out;
|
|
359
|
-
}
|
|
360
|
-
function loadLocal(file) {
|
|
361
|
-
return { path: file, raw: readFileSync(file, "utf8"), slug: basename(dirname(file)), file };
|
|
362
|
-
}
|
|
363
|
-
function isLikelyLocal(arg) {
|
|
364
|
-
return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync(arg);
|
|
365
|
-
}
|
|
366
|
-
async function resolveSource(arg) {
|
|
367
|
-
if (isLikelyLocal(arg)) {
|
|
368
|
-
if (!existsSync(arg)) throw new Error(`Path not found: ${arg}`);
|
|
369
|
-
const st = statSync(arg);
|
|
370
|
-
if (st.isFile()) return [loadLocal(arg)];
|
|
371
|
-
const files2 = findSkillFiles(arg);
|
|
372
|
-
if (files2.length === 0) {
|
|
373
|
-
throw new Error(`No SKILL.md found under ${arg}. Point at a skill directory, e.g. "skillmd lint ./my-skill", or run "skillmd init" to create one.`);
|
|
374
|
-
}
|
|
375
|
-
return files2.map(loadLocal);
|
|
376
|
-
}
|
|
377
|
-
const { downloadTemplate } = await import("giget");
|
|
378
|
-
const input = normalizeGiget(arg);
|
|
379
|
-
const dir = mkdtempSync(join(tmpdir(), "skillmd-src-"));
|
|
380
|
-
const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
|
|
381
|
-
const files = findSkillFiles(out);
|
|
382
|
-
if (files.length === 0) throw new Error(`No SKILL.md found in ${arg}`);
|
|
383
|
-
return files.map(loadLocal);
|
|
384
|
-
}
|
|
385
|
-
var MAX_PACK_FILES = 200;
|
|
386
|
-
var MAX_PACK_BYTES = 20 * 1024 * 1024;
|
|
387
|
-
function collectFiles(root, dir, limits = {}) {
|
|
388
|
-
const maxFiles = limits.maxFiles ?? MAX_PACK_FILES;
|
|
389
|
-
const maxBytes = limits.maxBytes ?? MAX_PACK_BYTES;
|
|
390
|
-
const out = [];
|
|
391
|
-
let bytes = 0;
|
|
392
|
-
const walk = (d, depth) => {
|
|
393
|
-
if (depth > MAX_DEPTH) return;
|
|
394
|
-
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
395
|
-
const fp = join(d, entry.name);
|
|
396
|
-
const st = lstatSync(fp);
|
|
397
|
-
if (st.isSymbolicLink()) continue;
|
|
398
|
-
if (st.isDirectory()) {
|
|
399
|
-
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
400
|
-
walk(fp, depth + 1);
|
|
401
|
-
} else if (st.isFile()) {
|
|
402
|
-
if (out.length >= maxFiles) throw new Error(`pack too large: > ${maxFiles} files`);
|
|
403
|
-
bytes += st.size;
|
|
404
|
-
if (bytes > maxBytes) throw new Error(`pack too large: > ${Math.round(maxBytes / (1024 * 1024))}MB`);
|
|
405
|
-
out.push({ path: relative(root, fp).split(sep).join("/"), contents: readFileSync(fp) });
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
walk(dir, 0);
|
|
410
|
-
return out;
|
|
411
|
-
}
|
|
412
|
-
async function resolveTree(sourceUrl, ref) {
|
|
413
|
-
const { downloadTemplate } = await import("giget");
|
|
414
|
-
const input = normalizeGiget(sourceUrl) + (ref ? `#${ref}` : "");
|
|
415
|
-
const dir = mkdtempSync(join(tmpdir(), "skillmd-pack-"));
|
|
416
|
-
try {
|
|
417
|
-
const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
|
|
418
|
-
return collectFiles(out, out);
|
|
419
|
-
} finally {
|
|
420
|
-
rmSync(dir, { recursive: true, force: true });
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
function normalizeGiget(arg) {
|
|
424
|
-
const m = arg.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/[^/]+\/(.+))?/);
|
|
425
|
-
if (m) return `github:${m[1]}/${m[2]}${m[3] ? "/" + m[3] : ""}`;
|
|
426
|
-
if (arg.startsWith("github:") || arg.startsWith("gh:")) return arg;
|
|
427
|
-
return `github:${arg}`;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// ../cli/src/ui.ts
|
|
50
|
+
// src/ui.ts
|
|
51
|
+
import { homedir } from "os";
|
|
52
|
+
import { relative, resolve, sep } from "path";
|
|
431
53
|
import pc from "picocolors";
|
|
432
54
|
var decorate = (text3, kind) => {
|
|
433
55
|
switch (kind) {
|
|
@@ -451,7 +73,11 @@ var SEV_COLOR = {
|
|
|
451
73
|
var GLYPH = {
|
|
452
74
|
error: "\u2716",
|
|
453
75
|
warn: "\u26A0",
|
|
454
|
-
info: "\u2139"
|
|
76
|
+
info: "\u2139",
|
|
77
|
+
ok: "\u2713",
|
|
78
|
+
skip: "\u21B7",
|
|
79
|
+
blocked: "\u2717",
|
|
80
|
+
replaced: "\u26A0"
|
|
455
81
|
};
|
|
456
82
|
var SEC_GLYPH = "\u2691";
|
|
457
83
|
var SEV_ORDER = ["error", "warn", "info"];
|
|
@@ -485,7 +111,7 @@ function statusDot(result) {
|
|
|
485
111
|
return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
|
|
486
112
|
}
|
|
487
113
|
function failureBlock(file, result) {
|
|
488
|
-
const lines = [`${pc.red(
|
|
114
|
+
const lines = [`${pc.red(GLYPH.error)} ${pc.bold(label(file))}`];
|
|
489
115
|
for (const d of result.diagnostics.filter((x) => x.severity === "error")) {
|
|
490
116
|
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
491
117
|
lines.push(` ${SEV_COLOR.error(d.id)}${loc} ${d.message}`);
|
|
@@ -539,7 +165,7 @@ function renderMany(results) {
|
|
|
539
165
|
}
|
|
540
166
|
function renderFailuresView(results) {
|
|
541
167
|
const fails = results.filter((r) => !r.result.ok);
|
|
542
|
-
if (!fails.length) return pc.green(
|
|
168
|
+
if (!fails.length) return pc.green(`${GLYPH.ok} No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
|
|
543
169
|
const out = fails.map((f) => failureBlock(f.file, f.result));
|
|
544
170
|
out.push("");
|
|
545
171
|
out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
|
|
@@ -560,8 +186,90 @@ function exitCodeFor(results, policy = {}) {
|
|
|
560
186
|
}
|
|
561
187
|
return 0;
|
|
562
188
|
}
|
|
189
|
+
function shortPath(p7, ctx = {}) {
|
|
190
|
+
const home = resolve(ctx.home ?? homedir());
|
|
191
|
+
const cwd = resolve(ctx.cwd ?? process.cwd());
|
|
192
|
+
const abs = resolve(p7);
|
|
193
|
+
const norm = (s) => s.split(sep).join("/");
|
|
194
|
+
if (abs === cwd) return ".";
|
|
195
|
+
if (abs.startsWith(cwd + sep)) return `./${norm(relative(cwd, abs))}`;
|
|
196
|
+
if (abs === home) return "~";
|
|
197
|
+
if (abs.startsWith(home + sep)) return `~/${norm(relative(home, abs))}`;
|
|
198
|
+
return norm(abs);
|
|
199
|
+
}
|
|
200
|
+
var SGR = /\x1b\[[0-9;]*m/g;
|
|
201
|
+
var SGR_AT = /\x1b\[[0-9;]*m/y;
|
|
202
|
+
var RESET = "\x1B[0m";
|
|
203
|
+
function stripAnsi(s) {
|
|
204
|
+
return s.replace(SGR, "");
|
|
205
|
+
}
|
|
206
|
+
var visible = (s) => stripAnsi(s).length;
|
|
207
|
+
function truncateVisible(s, max) {
|
|
208
|
+
if (max <= 0) return "";
|
|
209
|
+
if (visible(s) <= max) return s;
|
|
210
|
+
let out = "";
|
|
211
|
+
let shown = 0;
|
|
212
|
+
let open = false;
|
|
213
|
+
for (let i = 0; i < s.length && shown < max - 1; ) {
|
|
214
|
+
if (s[i] === "\x1B") {
|
|
215
|
+
SGR_AT.lastIndex = i;
|
|
216
|
+
const m = SGR_AT.exec(s);
|
|
217
|
+
if (m) {
|
|
218
|
+
out += m[0];
|
|
219
|
+
open = m[0] !== RESET && m[0] !== "\x1B[m";
|
|
220
|
+
i += m[0].length;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
out += s[i];
|
|
225
|
+
shown++;
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
return out + "\u2026" + (open ? RESET : "");
|
|
229
|
+
}
|
|
230
|
+
var termWidth = (w) => w ?? Math.min(process.stdout.columns ?? 100, 120);
|
|
231
|
+
function table(rows, opts = {}) {
|
|
232
|
+
if (rows.length === 0) return "";
|
|
233
|
+
const width = termWidth(opts.width);
|
|
234
|
+
const indent = opts.indent ?? "";
|
|
235
|
+
const cols = Math.max(...rows.map((r) => r.length));
|
|
236
|
+
const widths = Array.from({ length: cols }, (_, i) => Math.max(...rows.map((r) => visible(r[i] ?? ""))));
|
|
237
|
+
return rows.map((r) => {
|
|
238
|
+
let line = indent;
|
|
239
|
+
for (let i = 0; i < cols; i++) {
|
|
240
|
+
const cell = r[i] ?? "";
|
|
241
|
+
if (i < cols - 1) {
|
|
242
|
+
line += cell + " ".repeat(widths[i] - visible(cell) + 2);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
line += truncateVisible(cell, Math.max(4, width - visible(line)));
|
|
246
|
+
}
|
|
247
|
+
line = line.trimEnd();
|
|
248
|
+
return visible(line) > width ? truncateVisible(line, width) : line;
|
|
249
|
+
}).join("\n");
|
|
250
|
+
}
|
|
251
|
+
function renderInstallSummary(i, ctx = {}) {
|
|
252
|
+
const width = termWidth(ctx.width);
|
|
253
|
+
const src = i.source.startsWith("local:") ? `local:${shortPath(i.source.slice(6), ctx)}` : i.source;
|
|
254
|
+
const head = truncateVisible(
|
|
255
|
+
pc.green(`${GLYPH.ok} ${i.name}`) + pc.dim(` score ${i.score}${i.flags.length ? ` ${i.flags.join(", ")}` : ""} from ${src}`),
|
|
256
|
+
width
|
|
257
|
+
);
|
|
258
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const t of i.targets) {
|
|
260
|
+
const e = byPath.get(t.path);
|
|
261
|
+
if (e) e.agents.push(t.agent);
|
|
262
|
+
else byPath.set(t.path, { agents: [t.agent], mode: t.mode });
|
|
263
|
+
}
|
|
264
|
+
const rows = [...byPath.entries()].map(([path, e]) => [pc.cyan(e.agents.join(", ")), pc.dim(e.mode), shortPath(path, ctx)]);
|
|
265
|
+
const body = rows.length ? table(rows, { width, indent: " " }) : truncateVisible(pc.dim(` ${shortPath(i.canonical, ctx)} canonical only`), width);
|
|
266
|
+
const skipped = i.skipped.map((s) => truncateVisible(pc.dim(` ${GLYPH.skip} ${s.agent}: ${s.reason}`), width));
|
|
267
|
+
const replaced = i.replaced ? [truncateVisible(pc.yellow(` ${GLYPH.replaced} replaced previous install (${i.replaced === "untracked" ? "untracked directory" : i.replaced})`), width)] : [];
|
|
268
|
+
return [head, body, ...skipped, ...replaced].join("\n");
|
|
269
|
+
}
|
|
270
|
+
var banner = () => pc.bgCyan(pc.black(" skillmd "));
|
|
563
271
|
|
|
564
|
-
//
|
|
272
|
+
// src/commands/lint.ts
|
|
565
273
|
function applyLicenseFix(raw) {
|
|
566
274
|
if (/^---[\s\S]*?\blicense:\s*\S+[\s\S]*?^---/m.test(raw)) return null;
|
|
567
275
|
const m = raw.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
|
|
@@ -601,9 +309,9 @@ async function runLint(target, flags) {
|
|
|
601
309
|
return { results, output, exitCode };
|
|
602
310
|
}
|
|
603
311
|
function lintCommand() {
|
|
604
|
-
return new Command("lint").
|
|
312
|
+
return new Command("lint").description("Validate SKILL.md file(s) against the spec and report a quality score").argument("[target]", "local path or GitHub source", ".").option("--format <fmt>", "text | json | sarif | github", "text").option("--strict", "treat warnings as errors").option("--fail-on-warning", "exit non-zero when warnings exist").option("--fix", "apply safe automatic fixes (e.g. add missing license)").option("--errors-only", "show only skills that failed").action(async (target, opts) => {
|
|
605
313
|
const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
|
|
606
|
-
if (
|
|
314
|
+
if (resolve2(target) === homedir2()) {
|
|
607
315
|
process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
|
|
608
316
|
`));
|
|
609
317
|
}
|
|
@@ -612,12 +320,12 @@ function lintCommand() {
|
|
|
612
320
|
const s = p.spinner();
|
|
613
321
|
s.start(`Scanning ${target}`);
|
|
614
322
|
try {
|
|
615
|
-
const
|
|
616
|
-
s.stop(`Scanned ${
|
|
617
|
-
console.log(
|
|
618
|
-
const failed =
|
|
323
|
+
const run3 = await runLint(target, opts);
|
|
324
|
+
s.stop(`Scanned ${run3.results.length} skill${run3.results.length === 1 ? "" : "s"}`);
|
|
325
|
+
console.log(run3.output);
|
|
326
|
+
const failed = run3.results.filter((r) => !r.result.ok).length;
|
|
619
327
|
p.outro(failed === 0 ? pc2.green("All good") : pc2.red(`${failed} skill${failed === 1 ? "" : "s"} failed \u2014 details above`));
|
|
620
|
-
process.exitCode =
|
|
328
|
+
process.exitCode = run3.exitCode;
|
|
621
329
|
} catch (err) {
|
|
622
330
|
s.stop(pc2.red("No skills found"));
|
|
623
331
|
p.outro(err instanceof Error ? err.message : String(err));
|
|
@@ -627,14 +335,15 @@ function lintCommand() {
|
|
|
627
335
|
}
|
|
628
336
|
process.stderr.write(`Looking for SKILL.md under ${target} \u2026
|
|
629
337
|
`);
|
|
630
|
-
const
|
|
631
|
-
console.log(
|
|
632
|
-
process.exitCode =
|
|
338
|
+
const run2 = await runLint(target, opts);
|
|
339
|
+
console.log(run2.output);
|
|
340
|
+
process.exitCode = run2.exitCode;
|
|
633
341
|
});
|
|
634
342
|
}
|
|
635
343
|
|
|
636
|
-
//
|
|
344
|
+
// src/commands/scan.ts
|
|
637
345
|
import { Command as Command2 } from "commander";
|
|
346
|
+
import { scanSecurity, parseSkillMd } from "@skillmds/core";
|
|
638
347
|
import pc3 from "picocolors";
|
|
639
348
|
async function runScan(target, flags) {
|
|
640
349
|
const resolved = await resolveSource(target);
|
|
@@ -667,18 +376,19 @@ async function runScan(target, flags) {
|
|
|
667
376
|
}
|
|
668
377
|
function scanCommand() {
|
|
669
378
|
return new Command2("scan").description("Security-scan SKILL.md file(s) for scripts, network calls and secret access").argument("[target]", "local path or GitHub source", ".").option("--deny <flags...>", "exit non-zero if any of these flags are present (e.g. executes_scripts)").option("--format <fmt>", "text | json", "text").action(async (target, opts) => {
|
|
670
|
-
const
|
|
671
|
-
console.log(
|
|
672
|
-
process.exitCode =
|
|
379
|
+
const run2 = await runScan(target, opts);
|
|
380
|
+
console.log(run2.output);
|
|
381
|
+
process.exitCode = run2.exitCode;
|
|
673
382
|
});
|
|
674
383
|
}
|
|
675
384
|
|
|
676
|
-
//
|
|
385
|
+
// src/commands/rules.ts
|
|
677
386
|
import { Command as Command3 } from "commander";
|
|
387
|
+
import { RULES } from "@skillmds/core";
|
|
678
388
|
import pc4 from "picocolors";
|
|
679
389
|
function rulesCommand() {
|
|
680
390
|
return new Command3("rules").description("List lint rules, or show detail for one rule id").argument("[id]", "a rule id, e.g. SK010").option("--json", "machine-readable output").action((id, opts) => {
|
|
681
|
-
const list = id ?
|
|
391
|
+
const list = id ? RULES.filter((r) => r.id.toLowerCase() === id.toLowerCase()) : RULES;
|
|
682
392
|
if (id && list.length === 0) {
|
|
683
393
|
console.error(`Unknown rule: ${id}`);
|
|
684
394
|
process.exitCode = 1;
|
|
@@ -695,11 +405,45 @@ function rulesCommand() {
|
|
|
695
405
|
});
|
|
696
406
|
}
|
|
697
407
|
|
|
698
|
-
//
|
|
408
|
+
// src/commands/init.ts
|
|
699
409
|
import { Command as Command4 } from "commander";
|
|
700
|
-
import { mkdirSync, writeFileSync as writeFileSync2, existsSync
|
|
701
|
-
import { join
|
|
410
|
+
import { mkdirSync, writeFileSync as writeFileSync2, existsSync } from "fs";
|
|
411
|
+
import { join } from "path";
|
|
412
|
+
import { slugify } from "@skillmds/core";
|
|
702
413
|
import * as p2 from "@clack/prompts";
|
|
414
|
+
import pc5 from "picocolors";
|
|
415
|
+
|
|
416
|
+
// src/env.ts
|
|
417
|
+
function detectHostAgent(env = process.env) {
|
|
418
|
+
if (env.CLAUDECODE) return "claude-code";
|
|
419
|
+
if (env.CURSOR_AGENT || env.CURSOR_EXTENSION_HOST_ROLE === "agent-exec") return "cursor";
|
|
420
|
+
if (env.CODEX_SANDBOX || env.CODEX_THREAD_ID) return "codex";
|
|
421
|
+
if (env.GEMINI_CLI) return "gemini-cli";
|
|
422
|
+
if (env.CI && env.CI !== "false" && env.CI !== "0") return "ci";
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
var realTerminal = () => ({
|
|
426
|
+
stdinTTY: Boolean(process.stdin.isTTY),
|
|
427
|
+
stdoutTTY: Boolean(process.stdout.isTTY),
|
|
428
|
+
env: process.env
|
|
429
|
+
});
|
|
430
|
+
function isInteractive(flags, term = realTerminal()) {
|
|
431
|
+
if (flags.yes || flags.json) return false;
|
|
432
|
+
if (!term.stdinTTY || !term.stdoutTTY) return false;
|
|
433
|
+
return detectHostAgent(term.env) === null;
|
|
434
|
+
}
|
|
435
|
+
function scopesFor(flags) {
|
|
436
|
+
return flags.global && !flags.project ? [true] : flags.project && !flags.global ? [false] : [false, true];
|
|
437
|
+
}
|
|
438
|
+
function nonInteractiveHint(flags) {
|
|
439
|
+
return `Interactive prompt required but stdin is not a terminal. Nothing was changed. Re-run with ${flags} to run non-interactively.`;
|
|
440
|
+
}
|
|
441
|
+
function promptIf(flags, prompt) {
|
|
442
|
+
const term = { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env: flags.env ?? process.env };
|
|
443
|
+
return isInteractive(flags, term) ? { prompt } : {};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/commands/init.ts
|
|
703
447
|
function skillTemplate(name, description, license) {
|
|
704
448
|
const desc = description.trim() || `A skill that helps agents with ${name}.`;
|
|
705
449
|
return `---
|
|
@@ -728,159 +472,54 @@ Add any constraints, edge cases, or references the agent should keep in mind.
|
|
|
728
472
|
}
|
|
729
473
|
function initSkill(name, opts = {}) {
|
|
730
474
|
const slug = slugify(name);
|
|
731
|
-
const root =
|
|
732
|
-
if (
|
|
475
|
+
const root = join(opts.dir ?? process.cwd(), slug);
|
|
476
|
+
if (existsSync(join(root, "SKILL.md"))) throw new Error(`SKILL.md already exists at ${root}`);
|
|
733
477
|
mkdirSync(root, { recursive: true });
|
|
734
|
-
const file =
|
|
478
|
+
const file = join(root, "SKILL.md");
|
|
735
479
|
writeFileSync2(file, skillTemplate(name, opts.description ?? "", opts.license ?? "MIT"), "utf8");
|
|
736
480
|
return file;
|
|
737
481
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
process.exitCode = 1;
|
|
752
|
-
return;
|
|
482
|
+
var defaultInitDeps = (flags) => promptIf(flags, async () => {
|
|
483
|
+
const answer = await p2.text({ message: "Skill name?", placeholder: "my-skill" });
|
|
484
|
+
return p2.isCancel(answer) ? null : answer;
|
|
485
|
+
});
|
|
486
|
+
function finish(flags, result, doc) {
|
|
487
|
+
return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
|
|
488
|
+
}
|
|
489
|
+
async function runInit(nameArg, flags, deps = defaultInitDeps(flags)) {
|
|
490
|
+
let name = nameArg ?? flags.name;
|
|
491
|
+
if (!name) {
|
|
492
|
+
if (!deps.prompt) {
|
|
493
|
+
const error = nonInteractiveHint("a name argument or --name <name>");
|
|
494
|
+
return finish(flags, { exitCode: 1, output: pc5.red(error) }, { ok: false, error });
|
|
753
495
|
}
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// ../cli/src/commands/search.ts
|
|
760
|
-
import { Command as Command5 } from "commander";
|
|
761
|
-
import pc5 from "picocolors";
|
|
762
|
-
|
|
763
|
-
// ../cli/src/api.ts
|
|
764
|
-
import { createHash } from "crypto";
|
|
765
|
-
import { stringify as stringifyYaml } from "yaml";
|
|
766
|
-
|
|
767
|
-
// ../cli/src/config.ts
|
|
768
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
769
|
-
import { homedir as homedir2 } from "os";
|
|
770
|
-
import { join as join3 } from "path";
|
|
771
|
-
var DEFAULT_API = "https://api.skillmd.com";
|
|
772
|
-
function configPath() {
|
|
773
|
-
return join3(homedir2(), ".skillmd", "config.json");
|
|
774
|
-
}
|
|
775
|
-
function readConfig() {
|
|
776
|
-
try {
|
|
777
|
-
return JSON.parse(readFileSync2(configPath(), "utf8"));
|
|
778
|
-
} catch {
|
|
779
|
-
return {};
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
function writeConfig(cfg) {
|
|
783
|
-
const dir = join3(homedir2(), ".skillmd");
|
|
784
|
-
mkdirSync2(dir, { recursive: true });
|
|
785
|
-
writeFileSync3(configPath(), JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
|
|
786
|
-
}
|
|
787
|
-
function resolveToken(flags = {}) {
|
|
788
|
-
return flags.token || process.env.SKILLMD_TOKEN || readConfig().token || void 0;
|
|
789
|
-
}
|
|
790
|
-
function resolveApi(flags = {}) {
|
|
791
|
-
return flags.api || process.env.SKILLMD_API || readConfig().api || DEFAULT_API;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// ../cli/src/api.ts
|
|
795
|
-
var IntegrityError = class extends Error {
|
|
796
|
-
integrity = true;
|
|
797
|
-
constructor(message) {
|
|
798
|
-
super(message);
|
|
799
|
-
this.name = "IntegrityError";
|
|
496
|
+
const answer = await deps.prompt();
|
|
497
|
+
if (answer === null) return finish(flags, { exitCode: 0, output: pc5.dim("Cancelled.") }, { ok: false, cancelled: true });
|
|
498
|
+
name = answer;
|
|
800
499
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
super(message);
|
|
805
|
-
this.status = status;
|
|
806
|
-
this.name = "RegistryError";
|
|
500
|
+
if (!name) {
|
|
501
|
+
const error = "A skill name is required (pass a name or --name).";
|
|
502
|
+
return finish(flags, { exitCode: 1, output: pc5.red(error) }, { ok: false, error });
|
|
807
503
|
}
|
|
808
|
-
status;
|
|
809
|
-
};
|
|
810
|
-
var ALLOWED_SOURCE_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com", "gist.githubusercontent.com"]);
|
|
811
|
-
function isAllowedSourceUrl(u) {
|
|
812
504
|
try {
|
|
813
|
-
const
|
|
814
|
-
return
|
|
815
|
-
} catch {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
819
|
-
function createClient(flags = {}) {
|
|
820
|
-
const base = resolveApi(flags);
|
|
821
|
-
const token = resolveToken(flags);
|
|
822
|
-
async function api(path, init = {}) {
|
|
823
|
-
const headers = { accept: "application/json", ...init.headers };
|
|
824
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
825
|
-
let res;
|
|
826
|
-
try {
|
|
827
|
-
res = await fetch(base + path, { ...init, headers });
|
|
828
|
-
} catch (e) {
|
|
829
|
-
const cause = e instanceof Error ? e.cause?.message ?? e.message : String(e);
|
|
830
|
-
throw new RegistryError(`could not reach ${base}: ${cause}`);
|
|
831
|
-
}
|
|
832
|
-
if (!res.ok) {
|
|
833
|
-
const detail = await res.text().catch(() => "");
|
|
834
|
-
throw new RegistryError(`SkillMD API ${res.status} on ${path}: ${detail}`, res.status);
|
|
835
|
-
}
|
|
836
|
-
return await res.json();
|
|
837
|
-
}
|
|
838
|
-
return { api, base, token, hasToken: Boolean(token) };
|
|
839
|
-
}
|
|
840
|
-
async function fetchBundle(base, slug, token) {
|
|
841
|
-
const headers = { accept: "application/json" };
|
|
842
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
843
|
-
const res = await fetch(`${base}/api/skills/${slug}/bundle?format=json`, { headers }).catch(() => null);
|
|
844
|
-
if (!res || !res.ok) return null;
|
|
845
|
-
const data = await res.json().catch(() => null);
|
|
846
|
-
if (!data?.files) return null;
|
|
847
|
-
const out = [];
|
|
848
|
-
for (const f of data.files) {
|
|
849
|
-
if (f.content_base64 != null) {
|
|
850
|
-
const contents = Buffer.from(f.content_base64, "base64");
|
|
851
|
-
if (f.sha256) {
|
|
852
|
-
const got = createHash("sha256").update(contents).digest("hex");
|
|
853
|
-
if (got !== f.sha256) {
|
|
854
|
-
throw new IntegrityError(`integrity check failed for "${f.path}" (expected ${f.sha256.slice(0, 12)}\u2026, got ${got.slice(0, 12)}\u2026)`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
out.push({ path: f.path, contents });
|
|
858
|
-
} else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
|
|
859
|
-
const r = await fetch(f.source_url).catch(() => null);
|
|
860
|
-
if (r && r.ok) out.push({ path: f.path, contents: Buffer.from(await r.arrayBuffer()) });
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return out;
|
|
864
|
-
}
|
|
865
|
-
function skillMdFor(skill) {
|
|
866
|
-
if (skill.raw_md) {
|
|
867
|
-
return hasFrontmatter(skill.raw_md) ? skill.raw_md : reconstructSkillMd({ ...skill, body_md: skill.raw_md });
|
|
505
|
+
const file = initSkill(name, { dir: flags.dir, description: flags.description, license: flags.license });
|
|
506
|
+
return { exitCode: 0, file, output: flags.json ? JSON.stringify({ ok: true, file }, null, 2) : `Created ${file}` };
|
|
507
|
+
} catch (e) {
|
|
508
|
+
const msg2 = e instanceof Error ? e.message : String(e);
|
|
509
|
+
return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg2 }, null, 2) : pc5.red(msg2) };
|
|
868
510
|
}
|
|
869
|
-
return reconstructSkillMd(skill);
|
|
870
511
|
}
|
|
871
|
-
function
|
|
872
|
-
return
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (skill.license) fm.license = skill.license;
|
|
878
|
-
return `---
|
|
879
|
-
${stringifyYaml(fm)}---
|
|
880
|
-
${skill.body_md || ""}`;
|
|
512
|
+
function initCommand() {
|
|
513
|
+
return new Command4("init").description("Scaffold a new SKILL.md from a template").argument("[name]", "skill name").option("--name <name>", "skill name (non-interactive)").option("--description <text>", "skill description").option("--license <license>", "license identifier", "MIT").option("--json", "machine-readable output").option("-y, --yes", "skip prompts").action(async (nameArg, opts, cmd) => {
|
|
514
|
+
const r = await runInit(nameArg, { ...cmd.parent?.opts(), ...opts });
|
|
515
|
+
console.log(r.output);
|
|
516
|
+
process.exitCode = r.exitCode;
|
|
517
|
+
});
|
|
881
518
|
}
|
|
882
519
|
|
|
883
|
-
//
|
|
520
|
+
// src/commands/search.ts
|
|
521
|
+
import { Command as Command5 } from "commander";
|
|
522
|
+
import pc6 from "picocolors";
|
|
884
523
|
var MAX_LINE_WIDTH = 100;
|
|
885
524
|
function formatStars(n) {
|
|
886
525
|
if (n >= 1e6) {
|
|
@@ -899,13 +538,13 @@ function truncate(text3, width) {
|
|
|
899
538
|
return `${text3.slice(0, width - 1).trimEnd()}\u2026`;
|
|
900
539
|
}
|
|
901
540
|
function formatSearchResult(item, width = process.stdout.columns ?? 80) {
|
|
902
|
-
const stars = item.repo_stars != null ?
|
|
903
|
-
const check = item.verified ?
|
|
904
|
-
const pack = item.type === "pack" ?
|
|
905
|
-
const head = [
|
|
541
|
+
const stars = item.repo_stars != null ? pc6.dim(`\u2605 ${formatStars(item.repo_stars)}`) : "";
|
|
542
|
+
const check = item.verified ? pc6.green("\u2713") : "";
|
|
543
|
+
const pack = item.type === "pack" ? pc6.dim("[pack]") : "";
|
|
544
|
+
const head = [pc6.bold(safeText(item.slug, 200)), stars, check, pack].filter(Boolean).join(" ");
|
|
906
545
|
const indent = " ";
|
|
907
546
|
const lineWidth = Math.max(10, Math.min(width, MAX_LINE_WIDTH) - indent.length);
|
|
908
|
-
const desc = `${indent}${
|
|
547
|
+
const desc = `${indent}${pc6.dim(truncate(safeText(item.description ?? ""), lineWidth))}`;
|
|
909
548
|
return `${head}
|
|
910
549
|
${desc}`;
|
|
911
550
|
}
|
|
@@ -925,201 +564,287 @@ async function runSearch(query, flags) {
|
|
|
925
564
|
function searchCommand() {
|
|
926
565
|
return new Command5("search").description("Search the SkillMD registry").argument("<query>", "search terms").option("--category <slug>", "filter by category").option("--verified", "only verified skills").option("--type <type>", "single | pack").option("--min-rating <n>", "minimum average rating (0-5)").option("--limit <n>", "max results", "20").option("--json", "machine-readable output").action(async (query, opts, cmd) => {
|
|
927
566
|
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
928
|
-
const
|
|
929
|
-
console.log(
|
|
567
|
+
const run2 = await runSearch(query, merged);
|
|
568
|
+
console.log(run2.output);
|
|
930
569
|
});
|
|
931
570
|
}
|
|
932
571
|
|
|
933
|
-
//
|
|
572
|
+
// src/commands/info.ts
|
|
934
573
|
import { Command as Command6 } from "commander";
|
|
935
|
-
import
|
|
936
|
-
async function runInfo(slug, flags) {
|
|
574
|
+
import pc7 from "picocolors";
|
|
575
|
+
async function runInfo(slug, flags, deps = {}) {
|
|
937
576
|
const [owner, name] = slug.split("/");
|
|
938
577
|
if (!owner || !name || slug.split("/").length !== 2) {
|
|
939
|
-
return { ok: false, exitCode: 1, output:
|
|
578
|
+
return { ok: false, exitCode: 1, output: pc7.red("Expected owner/name, e.g. anthropic/pdf") };
|
|
940
579
|
}
|
|
941
|
-
const
|
|
580
|
+
const api = deps.api ?? createClient(flags).api;
|
|
942
581
|
let s;
|
|
943
582
|
try {
|
|
944
583
|
s = await api(`/api/skills/${owner}/${name}`);
|
|
945
584
|
} catch (err) {
|
|
946
585
|
const message = err instanceof Error ? err.message : String(err);
|
|
947
586
|
if (/^SkillMD API 404 /.test(message)) {
|
|
948
|
-
return { ok: false, exitCode: 1, output:
|
|
587
|
+
return { ok: false, exitCode: 1, output: pc7.red(`Skill not found on the registry: ${slug}`) };
|
|
949
588
|
}
|
|
950
|
-
return { ok: false, exitCode: 1, output:
|
|
589
|
+
return { ok: false, exitCode: 1, output: pc7.red(`Could not reach the SkillMD registry: ${message}`) };
|
|
951
590
|
}
|
|
952
591
|
if (flags.json) return { ok: true, exitCode: 0, output: JSON.stringify(s, null, 2), skill: s };
|
|
592
|
+
const slugTxt = safeText(s.slug, 200);
|
|
953
593
|
const lines = [];
|
|
954
594
|
lines.push(
|
|
955
|
-
`${
|
|
595
|
+
`${pc7.bold(slugTxt)} ${pc7.dim(`[${safeText(s.type ?? "single", 40)}]`)} ${s.verified ? pc7.green("\u2713 verified") : pc7.yellow("unverified")}`
|
|
956
596
|
);
|
|
957
|
-
lines.push(` ${s.description}`);
|
|
597
|
+
lines.push(` ${safeText(s.description)}`);
|
|
958
598
|
lines.push(
|
|
959
|
-
|
|
960
|
-
` category: ${s.category ?? "-"} \xB7 rating: ${s.avg_rating != null ? s.avg_rating.toFixed(1) : "-"} (${s.rating_count ?? 0}) \xB7 installs: ${s.install_count ?? 0}`
|
|
599
|
+
pc7.dim(
|
|
600
|
+
` category: ${safeText(s.category ?? "-", 100) || "-"} \xB7 rating: ${s.avg_rating != null ? s.avg_rating.toFixed(1) : "-"} (${s.rating_count ?? 0}) \xB7 installs: ${s.install_count ?? 0}`
|
|
961
601
|
)
|
|
962
602
|
);
|
|
603
|
+
const securityFlags2 = (s.security_flags ?? []).map((f) => safeText(f, 60)).filter(Boolean);
|
|
963
604
|
lines.push(
|
|
964
|
-
|
|
605
|
+
pc7.dim(` security: ${securityFlags2.join(", ") || "unscanned"} \xB7 license: ${safeText(s.provenance?.license ?? s.license ?? "unknown", 100) || "unknown"}`)
|
|
965
606
|
);
|
|
966
|
-
const sourceRepo = s.provenance?.source_repo ?? s.source_repo;
|
|
967
|
-
const commitSha = s.provenance?.commit_sha ?? s.commit_sha;
|
|
968
|
-
if (sourceRepo) lines.push(
|
|
969
|
-
lines.push(
|
|
970
|
-
lines.push(
|
|
607
|
+
const sourceRepo = safeText(s.provenance?.source_repo ?? s.source_repo, 300);
|
|
608
|
+
const commitSha = safeText(s.provenance?.commit_sha ?? s.commit_sha, 100);
|
|
609
|
+
if (sourceRepo) lines.push(pc7.dim(` source: ${sourceRepo}${commitSha ? ` @ ${commitSha.slice(0, 8)}` : ""}`));
|
|
610
|
+
lines.push(pc7.dim(` page: https://skillmd.com/skills/${slugTxt}`));
|
|
611
|
+
lines.push(pc7.dim(` install: ${safeText(s.install?.cli, 300) || `npx get-skill add ${slugTxt}`}`));
|
|
971
612
|
return { ok: true, exitCode: 0, output: lines.join("\n"), skill: s };
|
|
972
613
|
}
|
|
973
614
|
function infoCommand() {
|
|
974
615
|
return new Command6("info").description("Show registry details for a skill: verification, security flags, provenance, rating").argument("<slug>", "owner/name, e.g. anthropic/pdf").action(async (slug, _opts, cmd) => {
|
|
975
616
|
const flags = { ...cmd.parent?.opts() };
|
|
976
|
-
const
|
|
977
|
-
if (
|
|
978
|
-
process.exitCode =
|
|
617
|
+
const run2 = await runInfo(slug, flags);
|
|
618
|
+
if (run2.output) (run2.ok ? console.log : console.error)(run2.output);
|
|
619
|
+
process.exitCode = run2.exitCode;
|
|
979
620
|
});
|
|
980
621
|
}
|
|
981
622
|
|
|
982
|
-
//
|
|
623
|
+
// src/commands/add.ts
|
|
983
624
|
import { Command as Command7, Option } from "commander";
|
|
984
|
-
import { existsSync as existsSync4 } from "fs";
|
|
985
625
|
import { homedir as homedir4 } from "os";
|
|
986
626
|
import * as p3 from "@clack/prompts";
|
|
987
|
-
import {
|
|
988
|
-
import
|
|
627
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
628
|
+
import pc8 from "picocolors";
|
|
629
|
+
import { lint as lint2, parseSkillMd as parseSkillMd2 } from "@skillmds/core";
|
|
989
630
|
|
|
990
|
-
//
|
|
991
|
-
import {
|
|
631
|
+
// src/claudePlugin.ts
|
|
632
|
+
import { execFile } from "child_process";
|
|
633
|
+
import { existsSync as existsSync2 } from "fs";
|
|
992
634
|
import { homedir as homedir3 } from "os";
|
|
993
|
-
import {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
{ id: "amp", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".config", "amp"]] },
|
|
1011
|
-
// — extended registry —
|
|
1012
|
-
{ id: "openclaw", project: ["skills"], global: [".openclaw", "skills"], detect: [[".openclaw"], [".clawdbot"], [".moltbot"]] },
|
|
1013
|
-
{ id: "kilo", project: [".kilocode", "skills"], global: [".kilocode", "skills"], detect: [[".kilocode"]] },
|
|
1014
|
-
{ id: "hermes-agent", project: [".hermes", "skills"], global: [".hermes", "skills"], detect: [[".hermes"]] },
|
|
1015
|
-
{ id: "continue", project: [".continue", "skills"], global: [".continue", "skills"], detect: [[".continue"]] },
|
|
1016
|
-
{ id: "devin", project: [".devin", "skills"], global: [".config", "devin", "skills"], detect: [[".config", "devin"]] },
|
|
1017
|
-
{ id: "openhands", project: [".openhands", "skills"], global: [".openhands", "skills"], detect: [[".openhands"]] },
|
|
1018
|
-
{ id: "replit", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".replit"]] },
|
|
1019
|
-
{ id: "mistral-vibe", project: [".vibe", "skills"], global: [".vibe", "skills"], detect: [[".vibe"]] },
|
|
1020
|
-
{ id: "zed", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".config", "zed"]] },
|
|
1021
|
-
{ id: "adal", project: [".adal", "skills"], global: [".adal", "skills"], detect: [[".adal"]] },
|
|
1022
|
-
{ id: "aider-desk", project: [".aider-desk", "skills"], global: [".aider-desk", "skills"], detect: [[".aider-desk"]] },
|
|
1023
|
-
{ id: "antigravity-cli", project: [".agents", "skills"], global: [".gemini", "antigravity-cli", "skills"], detect: [[".gemini", "antigravity-cli"]] },
|
|
1024
|
-
{ id: "augment", project: [".augment", "skills"], global: [".augment", "skills"], detect: [[".augment"]] },
|
|
1025
|
-
{ id: "autohand-code", project: [".autohand", "skills"], global: [".autohand", "skills"], detect: [[".autohand"]] },
|
|
1026
|
-
{ id: "bob", project: [".bob", "skills"], global: [".bob", "skills"], detect: [[".bob"]] },
|
|
1027
|
-
{ id: "codearts-agent", project: [".codeartsdoer", "skills"], global: [".codeartsdoer", "skills"], detect: [[".codeartsdoer"]] },
|
|
1028
|
-
{ id: "codebuddy", project: [".codebuddy", "skills"], global: [".codebuddy", "skills"], detect: [[".codebuddy"]] },
|
|
1029
|
-
{ id: "codemaker", project: [".codemaker", "skills"], global: [".codemaker", "skills"], detect: [[".codemaker"]] },
|
|
1030
|
-
{ id: "codestudio", project: [".codestudio", "skills"], global: [".codestudio", "skills"], detect: [[".codestudio"]] },
|
|
1031
|
-
{ id: "command-code", project: [".commandcode", "skills"], global: [".commandcode", "skills"], detect: [[".commandcode"]] },
|
|
1032
|
-
{ id: "cortex", project: [".cortex", "skills"], global: [".snowflake", "cortex", "skills"], detect: [[".snowflake", "cortex"]] },
|
|
1033
|
-
{ id: "crush", project: [".crush", "skills"], global: [".config", "crush", "skills"], detect: [[".config", "crush"]] },
|
|
1034
|
-
{ id: "deepagents", project: [".agents", "skills"], global: [".deepagents", "agent", "skills"], detect: [[".deepagents"]] },
|
|
1035
|
-
{ id: "dexto", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".dexto"]] },
|
|
1036
|
-
{ id: "droid", project: [".factory", "skills"], global: [".factory", "skills"], detect: [[".factory"]] },
|
|
1037
|
-
{ id: "firebender", project: [".agents", "skills"], global: [".firebender", "skills"], detect: [[".firebender"]] },
|
|
1038
|
-
{ id: "forgecode", project: [".forge", "skills"], global: [".forge", "skills"], detect: [[".forge"]] },
|
|
1039
|
-
{ id: "iflow-cli", project: [".iflow", "skills"], global: [".iflow", "skills"], detect: [[".iflow"]] },
|
|
1040
|
-
{ id: "inference-sh", project: [".inferencesh", "skills"], global: [".inferencesh", "skills"], detect: [[".inferencesh"]] },
|
|
1041
|
-
{ id: "jazz", project: [".jazz", "skills"], global: [".jazz", "skills"], detect: [[".jazz"]] },
|
|
1042
|
-
{ id: "junie", project: [".junie", "skills"], global: [".junie", "skills"], detect: [[".junie"]] },
|
|
1043
|
-
{ id: "kimi-code-cli", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".kimi-code"], [".kimi"]] },
|
|
1044
|
-
{ id: "kode", project: [".kode", "skills"], global: [".kode", "skills"], detect: [[".kode"]] },
|
|
1045
|
-
{ id: "lingma", project: [".lingma", "skills"], global: [".lingma", "skills"], detect: [[".lingma"]] },
|
|
1046
|
-
{ id: "loaf", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".loaf"]] },
|
|
1047
|
-
{ id: "mcpjam", project: [".mcpjam", "skills"], global: [".mcpjam", "skills"], detect: [[".mcpjam"]] },
|
|
1048
|
-
{ id: "moxby", project: [".moxby", "skills"], global: [".moxby", "skills"], detect: [[".moxby"]] },
|
|
1049
|
-
{ id: "mux", project: [".mux", "skills"], global: [".mux", "skills"], detect: [[".mux"]] },
|
|
1050
|
-
{ id: "neovate", project: [".neovate", "skills"], global: [".neovate", "skills"], detect: [[".neovate"]] },
|
|
1051
|
-
{ id: "ona", project: [".ona", "skills"], global: [".ona", "skills"], detect: [[".ona"]] },
|
|
1052
|
-
{ id: "pi", project: [".pi", "skills"], global: [".pi", "agent", "skills"], detect: [[".pi", "agent"]] },
|
|
1053
|
-
{ id: "pochi", project: [".pochi", "skills"], global: [".pochi", "skills"], detect: [[".pochi"]] },
|
|
1054
|
-
{ id: "qoder", project: [".qoder", "skills"], global: [".qoder", "skills"], detect: [[".qoder"]] },
|
|
1055
|
-
{ id: "qoder-cn", project: [".qoder", "skills"], global: [".qoder-cn", "skills"], detect: [[".qoder-cn"]] },
|
|
1056
|
-
{ id: "reasonix", project: [".reasonix", "skills"], global: [".reasonix", "skills"], detect: [[".reasonix"]] },
|
|
1057
|
-
{ id: "rovodev", project: [".rovodev", "skills"], global: [".rovodev", "skills"], detect: [[".rovodev"]] },
|
|
1058
|
-
{ id: "tabnine-cli", project: [".tabnine", "agent", "skills"], global: [".tabnine", "agent", "skills"], detect: [[".tabnine"]] },
|
|
1059
|
-
{ id: "terramind", project: [".terramind", "skills"], global: [".terramind", "skills"], detect: [[".terramind"]] },
|
|
1060
|
-
{ id: "tinycloud", project: [".tinycloud", "skills"], global: [".tinycloud", "skills"], detect: [[".tinycloud"]] },
|
|
1061
|
-
{ id: "trae-cn", project: [".trae", "skills"], global: [".trae-cn", "skills"], detect: [[".trae-cn"]] },
|
|
1062
|
-
{ id: "zcode", project: [".zcode", "skills"], global: [".zcode", "skills"], detect: [[".zcode"]] },
|
|
1063
|
-
{ id: "zencoder", project: [".zencoder", "skills"], global: [".zencoder", "skills"], detect: [[".zencoder"]] }
|
|
1064
|
-
];
|
|
1065
|
-
function agentDir(agentId, opts = {}) {
|
|
1066
|
-
const agent = AGENTS.find((a) => a.id === agentId);
|
|
1067
|
-
if (!agent) throw new Error(`Unknown agent: ${agentId}. Known: ${AGENTS.map((a) => a.id).join(", ")}`);
|
|
1068
|
-
return opts.global ? join4(opts.home ?? homedir3(), ...agent.global) : join4(opts.cwd ?? process.cwd(), ...agent.project);
|
|
1069
|
-
}
|
|
1070
|
-
function detectAgents(opts = {}) {
|
|
1071
|
-
const home = opts.home ?? homedir3();
|
|
1072
|
-
return AGENTS.filter((a) => a.detect.some((d) => existsSync3(join4(home, ...d)))).map((a) => a.id);
|
|
1073
|
-
}
|
|
1074
|
-
function writeSkill(skillName, files, targetDir, opts = {}) {
|
|
1075
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(skillName) || skillName.includes("..")) {
|
|
1076
|
-
throw new Error(`refused unsafe skill name: "${skillName}"`);
|
|
1077
|
-
}
|
|
1078
|
-
const dest = join4(targetDir, skillName);
|
|
1079
|
-
if (opts.mode === "symlink" && opts.canonicalDir) {
|
|
1080
|
-
mkdirSync3(dirname2(dest), { recursive: true });
|
|
1081
|
-
rmSync2(dest, { recursive: true, force: true });
|
|
1082
|
-
symlinkSync(resolve2(opts.canonicalDir), dest, "dir");
|
|
1083
|
-
return dest;
|
|
1084
|
-
}
|
|
1085
|
-
mkdirSync3(dest, { recursive: true });
|
|
1086
|
-
const root = resolve2(dest);
|
|
1087
|
-
for (const f of files) {
|
|
1088
|
-
const fp = resolve2(dest, f.path);
|
|
1089
|
-
if (fp !== root && !fp.startsWith(root + sep2)) {
|
|
1090
|
-
throw new Error(`refused unsafe path in skill "${skillName}": ${f.path}`);
|
|
635
|
+
import { delimiter, join as join2 } from "path";
|
|
636
|
+
import { promisify } from "util";
|
|
637
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
638
|
+
import { dirname } from "path";
|
|
639
|
+
var run = promisify(execFile);
|
|
640
|
+
function resolveClaudeBin(env = process.env) {
|
|
641
|
+
const override = env.SKILLMD_CLAUDE_BIN;
|
|
642
|
+
if (override) return existsSync2(override) ? override : null;
|
|
643
|
+
const win = process.platform === "win32";
|
|
644
|
+
const exts = win ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
645
|
+
const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
646
|
+
const home = env.USERPROFILE || env.HOME || homedir3();
|
|
647
|
+
dirs.push(join2(home, ".local", "bin"), join2(home, ".claude", "local"));
|
|
648
|
+
for (const dir of dirs) {
|
|
649
|
+
for (const ext of exts) {
|
|
650
|
+
const candidate = join2(dir, `claude${ext.toLowerCase()}`);
|
|
651
|
+
if (existsSync2(candidate)) return candidate;
|
|
1091
652
|
}
|
|
1092
|
-
mkdirSync3(dirname2(fp), { recursive: true });
|
|
1093
|
-
writeFileSync4(fp, f.contents);
|
|
1094
653
|
}
|
|
1095
|
-
return
|
|
654
|
+
return null;
|
|
1096
655
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
if (!
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
656
|
+
var needsShell = (bin) => /\.(cmd|bat)$/i.test(bin);
|
|
657
|
+
var MARKETPLACE_URL = "https://skillmd.com/.claude-plugin/marketplace.json";
|
|
658
|
+
var MARKETPLACE_NAME = "skillmd";
|
|
659
|
+
var MARKETPLACE_SCHEMA = "https://anthropic.com/claude-code/marketplace.schema.json";
|
|
660
|
+
var TIMEOUT_MS = 12e4;
|
|
661
|
+
function exec(deps) {
|
|
662
|
+
return deps.exec ?? ((file, args) => {
|
|
663
|
+
const bin = file === "claude" ? resolveClaudeBin() : file;
|
|
664
|
+
if (!bin) return Promise.reject(new Error("claude not found on PATH"));
|
|
665
|
+
return run(bin, args, { timeout: TIMEOUT_MS, windowsHide: true, shell: needsShell(bin) }).then((r) => ({
|
|
666
|
+
stdout: String(r.stdout ?? ""),
|
|
667
|
+
stderr: String(r.stderr ?? "")
|
|
668
|
+
}));
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
function marketplacePluginName(owner, slug) {
|
|
672
|
+
const clean = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
673
|
+
const base = clean(slug) || "plugin";
|
|
674
|
+
return owner.toLowerCase() === "skillmd" ? base : `${clean(owner)}-${base}`;
|
|
675
|
+
}
|
|
676
|
+
async function claudeCliAvailable(deps = {}) {
|
|
677
|
+
try {
|
|
678
|
+
await exec(deps)("claude", ["--version"]);
|
|
679
|
+
return true;
|
|
680
|
+
} catch {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async function installClaudePlugin(owner, slug, deps = {}) {
|
|
685
|
+
const sh = exec(deps);
|
|
686
|
+
const qualified = `${marketplacePluginName(owner, slug)}@${MARKETPLACE_NAME}`;
|
|
687
|
+
try {
|
|
688
|
+
await sh("claude", ["plugin", "marketplace", "add", MARKETPLACE_URL]);
|
|
689
|
+
} catch (e) {
|
|
690
|
+
return { ok: false, reason: `could not add the SkillMD marketplace: ${msg(e)}` };
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
await sh("claude", ["plugin", "marketplace", "update", MARKETPLACE_NAME]);
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
let out;
|
|
697
|
+
try {
|
|
698
|
+
const r = await sh("claude", ["plugin", "install", qualified]);
|
|
699
|
+
out = `${r.stdout}
|
|
700
|
+
${r.stderr}`;
|
|
701
|
+
} catch (e) {
|
|
702
|
+
return { ok: false, reason: msg(e) };
|
|
703
|
+
}
|
|
704
|
+
if (/Successfully installed plugin/i.test(out)) return { ok: true, name: qualified, marketplace: MARKETPLACE_NAME };
|
|
705
|
+
const failed = /Failed to install plugin[^:]*:\s*(.+)/i.exec(out);
|
|
706
|
+
return { ok: false, reason: (failed?.[1] ?? out).trim().split(/\r?\n/)[0] ?? "install did not report success" };
|
|
707
|
+
}
|
|
708
|
+
function msg(e) {
|
|
709
|
+
if (e && typeof e === "object" && "stderr" in e) {
|
|
710
|
+
const s = String(e.stderr ?? "").trim();
|
|
711
|
+
if (s) return s.split(/\r?\n/)[0];
|
|
712
|
+
}
|
|
713
|
+
return e instanceof Error ? e.message.split(/\r?\n/)[0] : String(e);
|
|
714
|
+
}
|
|
715
|
+
var SITE_URL = "https://skillmd.com";
|
|
716
|
+
function readJson(path) {
|
|
717
|
+
let text3;
|
|
718
|
+
try {
|
|
719
|
+
text3 = readFileSync(path, "utf8");
|
|
720
|
+
} catch {
|
|
721
|
+
return "absent";
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
const v = JSON.parse(text3);
|
|
725
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : "unparsable";
|
|
726
|
+
} catch {
|
|
727
|
+
return "unparsable";
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function writeJson(path, value) {
|
|
731
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
732
|
+
writeFileSync3(path, `${JSON.stringify(value, null, 2)}
|
|
733
|
+
`);
|
|
734
|
+
}
|
|
735
|
+
var obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? { ...v } : {};
|
|
736
|
+
async function installPluginNatively(owner, slug, deps = {}) {
|
|
737
|
+
const home = deps.home ?? (process.env.USERPROFILE || process.env.HOME || homedir3());
|
|
738
|
+
const claudeDir = join2(home, ".claude");
|
|
739
|
+
if (!existsSync2(claudeDir)) return { ok: false, reason: "no Claude Code directory on this machine" };
|
|
740
|
+
const name = marketplacePluginName(owner, slug);
|
|
741
|
+
const qualified = `${name}@${MARKETPLACE_NAME}`;
|
|
742
|
+
const doFetch = deps.fetch ?? fetch;
|
|
743
|
+
let entries;
|
|
744
|
+
try {
|
|
745
|
+
const res = await doFetch(`${SITE_URL}/plugins/${owner}/${slug}/plugin.zip`, { headers: { accept: "application/zip" } });
|
|
746
|
+
if (!res.ok) return { ok: false, reason: `the plugin archive answered ${res.status}` };
|
|
747
|
+
const { unzipSync } = await import("fflate");
|
|
748
|
+
entries = unzipSync(new Uint8Array(await res.arrayBuffer()));
|
|
749
|
+
} catch (e) {
|
|
750
|
+
return { ok: false, reason: msg(e) };
|
|
751
|
+
}
|
|
752
|
+
let version2 = "1.0.0";
|
|
753
|
+
const manifest = entries[".claude-plugin/plugin.json"];
|
|
754
|
+
if (manifest) {
|
|
755
|
+
try {
|
|
756
|
+
const v = JSON.parse(new TextDecoder().decode(manifest));
|
|
757
|
+
if (typeof v.version === "string" && v.version) version2 = v.version;
|
|
758
|
+
} catch {
|
|
1110
759
|
}
|
|
1111
760
|
}
|
|
1112
|
-
return
|
|
761
|
+
if (!manifest) return { ok: false, reason: "the plugin archive has no .claude-plugin/plugin.json" };
|
|
762
|
+
const pluginsDir = join2(claudeDir, "plugins");
|
|
763
|
+
const installPath = join2(pluginsDir, "cache", MARKETPLACE_NAME, name, version2);
|
|
764
|
+
const knownPath = join2(pluginsDir, "known_marketplaces.json");
|
|
765
|
+
const installedPath = join2(pluginsDir, "installed_plugins.json");
|
|
766
|
+
const settingsPath = join2(claudeDir, "settings.json");
|
|
767
|
+
const known = readJson(knownPath);
|
|
768
|
+
if (known === "unparsable") return { ok: false, reason: "known_marketplaces.json is not readable JSON \u2014 left untouched" };
|
|
769
|
+
const installed = readJson(installedPath);
|
|
770
|
+
if (installed === "unparsable") return { ok: false, reason: "installed_plugins.json is not readable JSON \u2014 left untouched" };
|
|
771
|
+
const settings = readJson(settingsPath);
|
|
772
|
+
if (settings === "unparsable") return { ok: false, reason: "settings.json is not readable JSON \u2014 left untouched" };
|
|
773
|
+
try {
|
|
774
|
+
for (const [path, data] of Object.entries(entries)) {
|
|
775
|
+
if (path.endsWith("/")) continue;
|
|
776
|
+
const dest = join2(installPath, ...path.split("/"));
|
|
777
|
+
mkdirSync2(dirname(dest), { recursive: true });
|
|
778
|
+
writeFileSync3(dest, data);
|
|
779
|
+
}
|
|
780
|
+
writeFileSync3(join2(installPath, ".in_use"), "");
|
|
781
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
782
|
+
const source = { source: "url", url: MARKETPLACE_URL };
|
|
783
|
+
const marketplaceDir = join2(pluginsDir, "marketplaces", MARKETPLACE_NAME);
|
|
784
|
+
const cachedPath = join2(marketplaceDir, ".claude-plugin", "marketplace.json");
|
|
785
|
+
const cached = readJson(cachedPath);
|
|
786
|
+
if (cached === "unparsable") return { ok: false, reason: "the cached marketplace manifest is not readable JSON \u2014 left untouched" };
|
|
787
|
+
const cachedDoc = cached === "absent" ? {
|
|
788
|
+
$schema: MARKETPLACE_SCHEMA,
|
|
789
|
+
name: MARKETPLACE_NAME,
|
|
790
|
+
owner: { name: "SkillMD", email: "hi@skillmd.com" },
|
|
791
|
+
description: "Agent Skill plugins from the SkillMD registry.",
|
|
792
|
+
plugins: []
|
|
793
|
+
} : cached;
|
|
794
|
+
const list = Array.isArray(cachedDoc.plugins) ? cachedDoc.plugins : [];
|
|
795
|
+
cachedDoc.plugins = [
|
|
796
|
+
...list.filter((e) => e?.name !== name),
|
|
797
|
+
{
|
|
798
|
+
name,
|
|
799
|
+
version: version2,
|
|
800
|
+
homepage: `${SITE_URL}/plugins/${owner}/${slug}`,
|
|
801
|
+
author: { name: owner === "skillmd" ? "SkillMD" : `@${owner}` },
|
|
802
|
+
source: { source: "archive", url: `${SITE_URL}/plugins/${owner}/${slug}/plugin.zip` }
|
|
803
|
+
}
|
|
804
|
+
];
|
|
805
|
+
writeJson(cachedPath, cachedDoc);
|
|
806
|
+
const knownDoc = known === "absent" ? {} : known;
|
|
807
|
+
if (!knownDoc[MARKETPLACE_NAME]) {
|
|
808
|
+
knownDoc[MARKETPLACE_NAME] = { source, installLocation: marketplaceDir, lastUpdated: now };
|
|
809
|
+
writeJson(knownPath, knownDoc);
|
|
810
|
+
}
|
|
811
|
+
const installedDoc = installed === "absent" ? { version: 2, plugins: {} } : installed;
|
|
812
|
+
const plugins = obj(installedDoc.plugins);
|
|
813
|
+
const prior = Array.isArray(plugins[qualified]) ? plugins[qualified] : [];
|
|
814
|
+
plugins[qualified] = [
|
|
815
|
+
...prior.filter((p7) => p7?.scope !== "user"),
|
|
816
|
+
{ scope: "user", installPath, version: version2, installedAt: now, lastUpdated: now }
|
|
817
|
+
];
|
|
818
|
+
installedDoc.plugins = plugins;
|
|
819
|
+
if (typeof installedDoc.version !== "number") installedDoc.version = 2;
|
|
820
|
+
writeJson(installedPath, installedDoc);
|
|
821
|
+
const settingsDoc = settings === "absent" ? {} : settings;
|
|
822
|
+
const declared = obj(settingsDoc.extraKnownMarketplaces);
|
|
823
|
+
if (!declared[MARKETPLACE_NAME]) declared[MARKETPLACE_NAME] = { source };
|
|
824
|
+
settingsDoc.extraKnownMarketplaces = declared;
|
|
825
|
+
settingsDoc.enabledPlugins = { ...obj(settingsDoc.enabledPlugins), [qualified]: true };
|
|
826
|
+
writeJson(settingsPath, settingsDoc);
|
|
827
|
+
} catch (e) {
|
|
828
|
+
return { ok: false, reason: msg(e) };
|
|
829
|
+
}
|
|
830
|
+
return { ok: true, name: qualified, marketplace: MARKETPLACE_NAME };
|
|
1113
831
|
}
|
|
1114
832
|
|
|
1115
|
-
//
|
|
1116
|
-
var
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
833
|
+
// src/commands/add.ts
|
|
834
|
+
var infoNotice = (text3) => ({ text: text3, style: "info" });
|
|
835
|
+
var warnNotice = (text3) => ({ text: text3, style: "warn" });
|
|
836
|
+
var skipNotice = (text3) => ({ text: text3, style: "skip" });
|
|
837
|
+
var replacedNotice = (text3) => ({ text: text3, style: "warn", suppressInSummary: true });
|
|
838
|
+
function noticeLine(n) {
|
|
839
|
+
switch (n.style) {
|
|
840
|
+
case "info":
|
|
841
|
+
return pc8.dim(`${GLYPH.info} ${n.text}`);
|
|
842
|
+
case "skip":
|
|
843
|
+
return pc8.dim(`${GLYPH.skip} ${n.text}`);
|
|
844
|
+
case "warn":
|
|
845
|
+
return pc8.yellow(`${GLYPH.warn} ${n.text}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
1123
848
|
async function promptScopeInteractive(ctx) {
|
|
1124
849
|
const choice = await p3.select({
|
|
1125
850
|
message: "Installation scope",
|
|
@@ -1131,32 +856,28 @@ async function promptScopeInteractive(ctx) {
|
|
|
1131
856
|
});
|
|
1132
857
|
return p3.isCancel(choice) ? null : choice;
|
|
1133
858
|
}
|
|
859
|
+
function agentsPromptMessage(managed) {
|
|
860
|
+
return managed ? `Claude Code already has ${managed} as a plugin. Which other agents should get its skills too?` : "Which agents do you want to install to?";
|
|
861
|
+
}
|
|
1134
862
|
async function promptAgentsInteractive(ctx) {
|
|
1135
863
|
if (ctx.detected.length < 2) return ctx.detected;
|
|
864
|
+
const remembered = (readConfig().lastAgents ?? []).filter((id) => ctx.detected.includes(id));
|
|
1136
865
|
const picked = await p3.multiselect({
|
|
1137
|
-
message:
|
|
866
|
+
message: agentsPromptMessage(ctx.managed),
|
|
1138
867
|
options: ctx.detected.map((id) => ({ value: id, label: id, hint: ctx.dirFor(id) })),
|
|
1139
|
-
initialValues: ctx.detected,
|
|
868
|
+
initialValues: remembered.length ? remembered : ctx.detected,
|
|
1140
869
|
required: true
|
|
1141
870
|
});
|
|
1142
871
|
return p3.isCancel(picked) ? null : picked;
|
|
1143
872
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
873
|
+
function unverifiedFilesNote(n) {
|
|
874
|
+
const [f, it] = n === 1 ? ["file", "it"] : ["files", "them"];
|
|
875
|
+
return `${n} companion ${f} came straight from the source repo \u2014 the registry holds no checksum for ${it}, so only the connection was verified`;
|
|
1146
876
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
for (const attempt of attempts) {
|
|
1152
|
-
try {
|
|
1153
|
-
const files = await fetchTree(skill.source_repo, attempt.ref);
|
|
1154
|
-
const sk = files.find((f) => f.path === "SKILL.md");
|
|
1155
|
-
if (sk) return { raw: sk.contents.toString("utf8"), files, pinMiss: attempt.pinMiss };
|
|
1156
|
-
} catch {
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
return null;
|
|
877
|
+
function bundleNote(bundle, missing) {
|
|
878
|
+
const unverified = bundle.filter((file) => file.unverified).length;
|
|
879
|
+
const notes = [unverified ? unverifiedFilesNote(unverified) : "", missing.length ? missingFilesNote(missing) : ""].filter(Boolean);
|
|
880
|
+
return notes.length ? notes.join("; ") : void 0;
|
|
1160
881
|
}
|
|
1161
882
|
function registryFallbackNote(host) {
|
|
1162
883
|
return `installed from GitHub because the SkillMD registry (${host}) was unreachable \u2014 this copy is not the registry-reviewed version`;
|
|
@@ -1169,84 +890,219 @@ If this environment restricts network access (sandbox, proxy, CI), ask an admini
|
|
|
1169
890
|
Registry error: ${e.message}`
|
|
1170
891
|
);
|
|
1171
892
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
893
|
+
function argWithRef(arg, ref) {
|
|
894
|
+
if (!ref) return arg;
|
|
895
|
+
try {
|
|
896
|
+
const spec = parseSource(arg, { ref });
|
|
897
|
+
return spec.kind === "github" ? sourceId(spec) : arg;
|
|
898
|
+
} catch {
|
|
899
|
+
return arg;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function provenanceFor(arg) {
|
|
903
|
+
try {
|
|
904
|
+
return sourceId(parseSource(arg));
|
|
905
|
+
} catch {
|
|
906
|
+
return `unknown:${arg}`;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async function registryCandidate(owner, name, source, client) {
|
|
910
|
+
const registrySlug = `${owner}/${name}`;
|
|
911
|
+
const skill = await client.api(`/api/skills/${owner}/${name}`);
|
|
912
|
+
const skillName = skill.slug.split("/")[1] || name;
|
|
913
|
+
const meta = {
|
|
914
|
+
name: skillName,
|
|
915
|
+
slug: skillName,
|
|
916
|
+
registrySlug,
|
|
917
|
+
source,
|
|
918
|
+
commit_sha: skill.commit_sha ?? void 0,
|
|
919
|
+
verified: skill.verified,
|
|
920
|
+
type: skill.type,
|
|
921
|
+
securityFlags: skill.security_flags
|
|
922
|
+
};
|
|
923
|
+
const missing = [];
|
|
924
|
+
const bundle = await fetchBundle(client.base, registrySlug, client.token, { missing });
|
|
925
|
+
if (bundle && bundle.length) {
|
|
926
|
+
const sk = bundle.find((file) => file.path === "SKILL.md");
|
|
927
|
+
const note2 = bundleNote(bundle, missing);
|
|
928
|
+
return {
|
|
929
|
+
...meta,
|
|
930
|
+
type: bundle.length > 1 ? "pack" : meta.type,
|
|
931
|
+
raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill),
|
|
932
|
+
files: bundle,
|
|
933
|
+
...note2 ? { note: note2 } : {}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
if (skill.type === "pack") {
|
|
937
|
+
const pack = await resolvePackFiles(skill);
|
|
938
|
+
if (pack) return { ...meta, ...pack };
|
|
939
|
+
}
|
|
940
|
+
return { ...meta, raw: skillMdFor(skill) };
|
|
941
|
+
}
|
|
942
|
+
async function fetchPackMembers(api, owner, slug) {
|
|
943
|
+
const PAGE = 200;
|
|
944
|
+
const members = [];
|
|
945
|
+
let name = slug;
|
|
946
|
+
let total = Infinity;
|
|
947
|
+
for (let offset = 0; members.length < total; offset += PAGE) {
|
|
948
|
+
const page = await api(`/api/packs/${owner}/${slug}?limit=${PAGE}&offset=${offset}`);
|
|
949
|
+
name = page.name || name;
|
|
950
|
+
total = Number(page.item_total ?? 0);
|
|
951
|
+
const got = (page.items ?? []).map((i) => ({ slug: String(i.slug ?? ""), type: i.type, verified: Boolean(i.verified) })).filter((i) => i.slug);
|
|
952
|
+
if (!got.length) break;
|
|
953
|
+
members.push(...got);
|
|
954
|
+
}
|
|
955
|
+
return { name, members };
|
|
956
|
+
}
|
|
957
|
+
function packDirNames(slugs) {
|
|
958
|
+
const used = /* @__PURE__ */ new Set();
|
|
959
|
+
return slugs.map((slug) => {
|
|
960
|
+
const [owner, name] = slug.split("/");
|
|
961
|
+
let dir = name ?? slug;
|
|
962
|
+
if (used.has(dir)) dir = `${owner}-${name}`;
|
|
963
|
+
used.add(dir);
|
|
964
|
+
return dir;
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
async function fetchPackArchive(base, owner, slug, token, doFetch = fetch) {
|
|
968
|
+
const headers = { accept: "application/zip" };
|
|
969
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
970
|
+
const res = await doFetch(`${base}/api/packs/${owner}/${slug}/download`, { headers });
|
|
971
|
+
if (!res.ok) throw new RegistryError(`SkillMD API ${res.status} on /api/packs/${owner}/${slug}/download`, res.status);
|
|
972
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
973
|
+
const { unzipSync, strFromU8 } = await import("fflate");
|
|
974
|
+
const entries = unzipSync(bytes);
|
|
975
|
+
const out = /* @__PURE__ */ new Map();
|
|
976
|
+
for (const [path, data] of Object.entries(entries)) {
|
|
977
|
+
const m = /^(.+)\/SKILL\.md$/.exec(path);
|
|
978
|
+
if (m) out.set(m[1], strFromU8(data));
|
|
979
|
+
}
|
|
980
|
+
return out;
|
|
981
|
+
}
|
|
982
|
+
function makeDefaultDeps(flags) {
|
|
983
|
+
const canPrompt = () => isInteractive(flags, {
|
|
984
|
+
stdinTTY: Boolean(process.stdin.isTTY),
|
|
985
|
+
stdoutTTY: Boolean(process.stdout.isTTY),
|
|
986
|
+
env: flags.env ?? process.env
|
|
987
|
+
});
|
|
988
|
+
return {
|
|
989
|
+
async resolve(arg, f) {
|
|
990
|
+
const spec = parseSource(arg, { ref: f.ref });
|
|
991
|
+
const source = sourceId(spec);
|
|
992
|
+
if (spec.kind === "plugin") {
|
|
993
|
+
const { api: api2, base: apiBase2, token: token2, fetch: doFetch } = createClient(f);
|
|
994
|
+
const { name: packName, members } = await fetchPackMembers(api2, spec.owner, spec.slug);
|
|
995
|
+
if (!members.length) throw new Error(`plugin ${spec.owner}/${spec.slug} has no installable skills`);
|
|
996
|
+
const archive = await fetchPackArchive(apiBase2, spec.owner, spec.slug, token2, doFetch);
|
|
997
|
+
const dirs = packDirNames(members.map((m) => m.slug));
|
|
998
|
+
const out = [];
|
|
999
|
+
const missing = [];
|
|
1000
|
+
members.forEach((m, i) => {
|
|
1001
|
+
const raw = archive.get(dirs[i]);
|
|
1002
|
+
if (!raw) {
|
|
1003
|
+
missing.push(m.slug);
|
|
1004
|
+
return;
|
|
1205
1005
|
}
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1006
|
+
const name = dirs[i];
|
|
1007
|
+
out.push({
|
|
1008
|
+
name,
|
|
1009
|
+
slug: name,
|
|
1010
|
+
registrySlug: m.slug,
|
|
1011
|
+
source,
|
|
1012
|
+
raw,
|
|
1013
|
+
verified: m.verified,
|
|
1014
|
+
type: m.type
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
const multi = out.filter((c) => c.type === "pack");
|
|
1018
|
+
if (multi.length) {
|
|
1019
|
+
let n = 0;
|
|
1020
|
+
await Promise.all(Array.from({ length: Math.min(4, multi.length) }, async () => {
|
|
1021
|
+
for (let i = n++; i < multi.length; i = n++) {
|
|
1022
|
+
const c = multi[i];
|
|
1023
|
+
try {
|
|
1024
|
+
const missing2 = [];
|
|
1025
|
+
const bundle = await fetchBundle(apiBase2, c.registrySlug, token2, { missing: missing2 });
|
|
1026
|
+
if (!bundle?.length) continue;
|
|
1027
|
+
const sk = bundle.find((file) => file.path === "SKILL.md");
|
|
1028
|
+
const note2 = bundleNote(bundle, missing2);
|
|
1029
|
+
c.files = bundle;
|
|
1030
|
+
if (sk) c.raw = sk.contents.toString("utf8");
|
|
1031
|
+
if (note2) c.note = note2;
|
|
1032
|
+
} catch {
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}));
|
|
1036
|
+
}
|
|
1037
|
+
if (!out.length) throw new Error(`plugin ${spec.owner}/${spec.slug} returned no readable skills`);
|
|
1038
|
+
const first = out[0];
|
|
1039
|
+
first.note = `${out.length} skill${out.length === 1 ? "" : "s"} from the ${packName} plugin` + (missing.length ? ` (${missing.length} member${missing.length === 1 ? "" : "s"} were not in the archive)` : "");
|
|
1040
|
+
return out;
|
|
1041
|
+
}
|
|
1042
|
+
if (spec.kind !== "slug") {
|
|
1043
|
+
const rs = await resolveSource(argWithRef(arg, f.ref));
|
|
1044
|
+
return rs.map((r) => ({
|
|
1045
|
+
name: r.slug,
|
|
1046
|
+
raw: r.raw,
|
|
1047
|
+
slug: r.slug,
|
|
1048
|
+
source,
|
|
1049
|
+
...spec.kind === "local" && r.file ? { files: collectFiles(dirname2(r.file), dirname2(r.file)) } : {}
|
|
1050
|
+
}));
|
|
1051
|
+
}
|
|
1052
|
+
let registryDown = null;
|
|
1053
|
+
const { api, base: apiBase, token } = createClient(f);
|
|
1054
|
+
const registryHost = (() => {
|
|
1055
|
+
try {
|
|
1056
|
+
return new URL(apiBase).host;
|
|
1057
|
+
} catch {
|
|
1058
|
+
return "api.skillmd.com";
|
|
1208
1059
|
}
|
|
1209
|
-
|
|
1060
|
+
})();
|
|
1061
|
+
const registrySlug = `${spec.owner}/${spec.name}`;
|
|
1062
|
+
try {
|
|
1063
|
+
return [await registryCandidate(spec.owner, spec.name, source, { api, base: apiBase, token })];
|
|
1210
1064
|
} catch (e) {
|
|
1211
1065
|
if (e instanceof IntegrityError) throw e;
|
|
1212
1066
|
if (e instanceof RegistryError && e.status !== 404) registryDown = e;
|
|
1213
1067
|
}
|
|
1068
|
+
const ghSource = sourceId({ kind: "github", owner: spec.owner, repo: spec.name, display: arg });
|
|
1069
|
+
try {
|
|
1070
|
+
const rs = await resolveSource(arg);
|
|
1071
|
+
const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
|
|
1072
|
+
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, source: ghSource, note: note2 }));
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
|
|
1075
|
+
throw e;
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
get promptScope() {
|
|
1079
|
+
return canPrompt() ? promptScopeInteractive : void 0;
|
|
1080
|
+
},
|
|
1081
|
+
get promptAgents() {
|
|
1082
|
+
return canPrompt() ? promptAgentsInteractive : void 0;
|
|
1083
|
+
},
|
|
1084
|
+
fireInstall(registrySlug, f) {
|
|
1085
|
+
const { api } = createClient(f);
|
|
1086
|
+
const [owner, name] = registrySlug.split("/");
|
|
1087
|
+
void api(`/api/skills/${owner}/${name}/install`, {
|
|
1088
|
+
method: "POST",
|
|
1089
|
+
headers: { "content-type": "application/json" },
|
|
1090
|
+
body: JSON.stringify({ via: "cli" })
|
|
1091
|
+
}).catch(() => {
|
|
1092
|
+
});
|
|
1214
1093
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
|
|
1218
|
-
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, note: note2 }));
|
|
1219
|
-
} catch (e) {
|
|
1220
|
-
if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
|
|
1221
|
-
throw e;
|
|
1222
|
-
}
|
|
1223
|
-
},
|
|
1224
|
-
get promptScope() {
|
|
1225
|
-
return interactive() ? promptScopeInteractive : void 0;
|
|
1226
|
-
},
|
|
1227
|
-
get promptAgents() {
|
|
1228
|
-
return interactive() ? promptAgentsInteractive : void 0;
|
|
1229
|
-
},
|
|
1230
|
-
fireInstall(registrySlug, flags) {
|
|
1231
|
-
const { api } = createClient(flags);
|
|
1232
|
-
const [owner, name] = registrySlug.split("/");
|
|
1233
|
-
void api(`/api/skills/${owner}/${name}/install`, {
|
|
1234
|
-
method: "POST",
|
|
1235
|
-
headers: { "content-type": "application/json" },
|
|
1236
|
-
body: JSON.stringify({ via: "cli" })
|
|
1237
|
-
}).catch(() => {
|
|
1238
|
-
});
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1241
1096
|
async function resolveScope(flags, deps) {
|
|
1242
1097
|
const home = resolve3(flags.home ?? homedir4());
|
|
1243
1098
|
const cwd = resolve3(flags.cwd ?? process.cwd());
|
|
1244
|
-
const base = { cwd: flags.cwd, home: flags.home };
|
|
1099
|
+
const base = { cwd: flags.cwd, home: flags.home, env: flags.env };
|
|
1245
1100
|
if (flags.global) return { scope: { global: true, ...base } };
|
|
1246
1101
|
if (flags.project) return { scope: { global: false, ...base } };
|
|
1247
1102
|
if (cwd === home) return { scope: { global: true, ...base } };
|
|
1248
1103
|
const isProject = looksLikeProject(cwd);
|
|
1249
|
-
|
|
1104
|
+
const hosted = detectHostAgent(flags.env ?? process.env) !== null;
|
|
1105
|
+
if (deps.promptScope && !flags.yes && !flags.json && !hosted) {
|
|
1250
1106
|
const global = await deps.promptScope({ cwd, suggestGlobal: !isProject });
|
|
1251
1107
|
if (global === null) return null;
|
|
1252
1108
|
return { scope: { global, ...base } };
|
|
@@ -1257,202 +1113,682 @@ async function resolveScope(flags, deps) {
|
|
|
1257
1113
|
note: `no project detected in ${cwd} \u2014 installing to your user-level agent folders (pass --project to install here)`
|
|
1258
1114
|
};
|
|
1259
1115
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1116
|
+
function defaultNeedsPrompt(flags) {
|
|
1117
|
+
const env = flags.env ?? process.env;
|
|
1118
|
+
if (flags.yes || flags.json || flags.global || flags.project || flags.list) return false;
|
|
1119
|
+
if (detectHostAgent(env)) return false;
|
|
1120
|
+
if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ?? homedir4())) return false;
|
|
1121
|
+
return !isInteractive(flags, { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env });
|
|
1122
|
+
}
|
|
1123
|
+
var CANCELLED_TEXT = "installation cancelled \u2014 nothing was installed";
|
|
1124
|
+
function finish2(flags, outcome) {
|
|
1125
|
+
const json = Boolean(flags.json);
|
|
1126
|
+
switch (outcome.kind) {
|
|
1127
|
+
case "install": {
|
|
1128
|
+
const installed = outcome.records.flatMap((r) => r.kind === "installed" ? [r.doc] : []);
|
|
1129
|
+
const blocked = outcome.records.flatMap((r) => r.kind === "blocked" ? [{ name: r.name, reason: r.reason }] : []);
|
|
1130
|
+
const written = installed.flatMap((d) => d.targets.map((t) => ({ name: d.name, dir: t.path })));
|
|
1131
|
+
const warnings = [
|
|
1132
|
+
...outcome.notices,
|
|
1133
|
+
...outcome.records.flatMap((r) => r.kind === "installed" ? r.notices : [])
|
|
1134
|
+
].map((n) => n.text);
|
|
1135
|
+
const exitCode = blocked.length > 0 || installed.length === 0 && !outcome.managed ? 1 : 0;
|
|
1136
|
+
const doc = { ok: exitCode === 0, scope: outcome.scope, installed, blocked, warnings };
|
|
1137
|
+
return {
|
|
1138
|
+
written,
|
|
1139
|
+
blocked,
|
|
1140
|
+
exitCode,
|
|
1141
|
+
output: json ? JSON.stringify(doc, null, 2) : renderHuman(outcome.notices, outcome.records, outcome.paths),
|
|
1142
|
+
scope: outcome.scope,
|
|
1143
|
+
agents: [...new Set(installed.flatMap((d) => d.targets.map((t) => t.agent)))]
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
case "list": {
|
|
1147
|
+
const human = outcome.skills.length ? outcome.skills.map((s) => `${pc8.bold(s.name)} ${pc8.dim(s.description)}`).join("\n") : "No skills found.";
|
|
1148
|
+
return { written: [], blocked: [], exitCode: 0, output: json ? JSON.stringify({ ok: true, skills: outcome.skills }, null, 2) : human };
|
|
1149
|
+
}
|
|
1150
|
+
case "cancel": {
|
|
1151
|
+
const doc = { ok: false, error: CANCELLED_TEXT };
|
|
1267
1152
|
return {
|
|
1268
1153
|
written: [],
|
|
1269
|
-
blocked: [
|
|
1270
|
-
exitCode:
|
|
1271
|
-
|
|
1154
|
+
blocked: [],
|
|
1155
|
+
exitCode: 0,
|
|
1156
|
+
cancelled: true,
|
|
1157
|
+
output: json ? JSON.stringify(doc, null, 2) : pc8.dim("Installation cancelled \u2014 nothing was installed.")
|
|
1272
1158
|
};
|
|
1273
1159
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const isGlobal = scope.global;
|
|
1285
|
-
const home = flags.home ?? homedir4();
|
|
1286
|
-
if (resolved.note) lines.push(pc7.dim(`\u2139 ${resolved.note}`));
|
|
1287
|
-
const detected = detectAgents(scope);
|
|
1288
|
-
let targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
|
|
1289
|
-
if (!flags.agent?.length && deps.promptAgents && !flags.yes && !flags.json) {
|
|
1290
|
-
const picked = await deps.promptAgents({ detected: targets, dirFor: (id) => agentDir(id, scope) });
|
|
1291
|
-
if (picked === null) return CANCELLED;
|
|
1292
|
-
targets = picked;
|
|
1293
|
-
}
|
|
1294
|
-
const dirAgents = /* @__PURE__ */ new Map();
|
|
1295
|
-
for (const agentId of targets) {
|
|
1296
|
-
const dir = agentDir(agentId, scope);
|
|
1297
|
-
dirAgents.set(dir, [...dirAgents.get(dir) ?? [], agentId]);
|
|
1298
|
-
}
|
|
1299
|
-
for (const c of candidates) {
|
|
1300
|
-
const result = lint(c.raw, { slug: c.slug });
|
|
1301
|
-
const allFlags = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
|
|
1302
|
-
const deniedFlags = allFlags.filter((f) => deny.has(f));
|
|
1303
|
-
if (!flags.skipLint && !result.ok) {
|
|
1304
|
-
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
1305
|
-
blocked.push({ name: c.name, reason: `lint errors: ${errors.map((d) => d.id).join(", ")}` });
|
|
1306
|
-
lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
|
|
1307
|
-
for (const d of errors) lines.push(pc7.dim(` ${d.id} ${d.message}`));
|
|
1308
|
-
continue;
|
|
1160
|
+
case "fail": {
|
|
1161
|
+
const warnings = (outcome.notices ?? []).map((n) => n.text);
|
|
1162
|
+
const doc = { ok: false, error: outcome.error };
|
|
1163
|
+
if (outcome.blocked?.length) doc.blocked = outcome.blocked;
|
|
1164
|
+
if (warnings.length) doc.warnings = warnings;
|
|
1165
|
+
const human = [
|
|
1166
|
+
...(outcome.notices ?? []).map(noticeLine),
|
|
1167
|
+
...outcome.blocked?.length ? outcome.blocked.map((b) => pc8.red(`${GLYPH.blocked} ${b.name} blocked \u2014 ${b.reason}`)) : [pc8.red(outcome.error)]
|
|
1168
|
+
].join("\n");
|
|
1169
|
+
return { written: [], blocked: outcome.blocked ?? [], exitCode: 1, output: json ? JSON.stringify(doc, null, 2) : human };
|
|
1309
1170
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function renderHuman(notices, records, paths) {
|
|
1174
|
+
const lines = notices.map(noticeLine);
|
|
1175
|
+
for (const r of records) {
|
|
1176
|
+
if (r.kind === "blocked") {
|
|
1177
|
+
lines.push(pc8.red(r.message));
|
|
1313
1178
|
continue;
|
|
1314
1179
|
}
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1180
|
+
const { doc } = r;
|
|
1181
|
+
lines.push(renderInstallSummary({
|
|
1182
|
+
name: doc.name,
|
|
1183
|
+
score: r.score,
|
|
1184
|
+
flags: r.securityFlags,
|
|
1185
|
+
source: r.source,
|
|
1186
|
+
canonical: doc.canonical,
|
|
1187
|
+
targets: doc.targets,
|
|
1188
|
+
skipped: doc.skipped,
|
|
1189
|
+
...r.replaced ? { replaced: r.replaced } : {}
|
|
1190
|
+
}, paths));
|
|
1191
|
+
for (const n of r.notices) if (!n.suppressInSummary) lines.push(noticeLine(n));
|
|
1192
|
+
}
|
|
1193
|
+
return lines.join("\n");
|
|
1194
|
+
}
|
|
1195
|
+
async function resolveCandidates(arg, flags, deps) {
|
|
1196
|
+
const candidates = await deps.resolve(arg, flags);
|
|
1197
|
+
if (!flags.skill?.length) return candidates;
|
|
1198
|
+
return candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
|
|
1199
|
+
}
|
|
1200
|
+
function gate(flags, deps, candidates) {
|
|
1201
|
+
if (flags.list) {
|
|
1202
|
+
return finish2(flags, {
|
|
1203
|
+
kind: "list",
|
|
1204
|
+
skills: candidates.map((c) => {
|
|
1205
|
+
const parsed = parseSkillMd2(c.raw);
|
|
1206
|
+
const description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
|
|
1207
|
+
return { name: c.name, description: safeText(description, 80) };
|
|
1208
|
+
})
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
if (flags.json && !flags.yes) {
|
|
1212
|
+
return finish2(flags, { kind: "fail", error: "--json requires -y (no prompts can be shown in JSON mode)" });
|
|
1213
|
+
}
|
|
1214
|
+
if (deps.needsPrompt ?? (!deps.promptScope && defaultNeedsPrompt(flags))) {
|
|
1215
|
+
return finish2(flags, { kind: "fail", error: nonInteractiveHint("-y (auto-detects the scope) or -g / -p") });
|
|
1216
|
+
}
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
async function chooseTargets(flags, deps, scope, detected, managed) {
|
|
1220
|
+
const notices = [];
|
|
1221
|
+
let targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
|
|
1222
|
+
if (scope.global) {
|
|
1223
|
+
const kept = [];
|
|
1224
|
+
for (const id of targets) {
|
|
1225
|
+
if (agentSupportsGlobal(id)) kept.push(id);
|
|
1226
|
+
else notices.push(skipNotice(`${id}: project-only agent`));
|
|
1323
1227
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1228
|
+
targets = kept;
|
|
1229
|
+
}
|
|
1230
|
+
if (!flags.agent?.length && deps.promptAgents && !flags.yes && !flags.json && !detectHostAgent(flags.env ?? process.env)) {
|
|
1231
|
+
const picked = await deps.promptAgents({ detected: targets, dirFor: (id) => agentDir(id, scope), ...managed ? { managed } : {} });
|
|
1232
|
+
if (picked === null) return null;
|
|
1233
|
+
targets = picked;
|
|
1234
|
+
if (flags.home === void 0 && picked.length) {
|
|
1235
|
+
try {
|
|
1236
|
+
writeConfig({ ...readConfig(), lastAgents: picked });
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1326
1239
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1240
|
+
}
|
|
1241
|
+
return { targets, notices };
|
|
1242
|
+
}
|
|
1243
|
+
async function installOne(c, ctx) {
|
|
1244
|
+
const result = lint2(c.raw, { slug: c.slug });
|
|
1245
|
+
const securityFlags2 = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
|
|
1246
|
+
const deniedFlags = securityFlags2.filter((f) => ctx.deny.has(f));
|
|
1247
|
+
if (!ctx.flags.skipLint && !result.ok) {
|
|
1248
|
+
return {
|
|
1249
|
+
kind: "blocked",
|
|
1250
|
+
name: c.name,
|
|
1251
|
+
reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}`,
|
|
1252
|
+
message: `${GLYPH.blocked} ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
if (deniedFlags.length) {
|
|
1256
|
+
return {
|
|
1257
|
+
kind: "blocked",
|
|
1258
|
+
name: c.name,
|
|
1259
|
+
reason: `denied security flags: ${deniedFlags.join(", ")}`,
|
|
1260
|
+
message: `${GLYPH.blocked} ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
const source = c.source ?? (c.registrySlug ? `registry:${c.registrySlug}` : provenanceFor(ctx.arg));
|
|
1264
|
+
let out;
|
|
1265
|
+
try {
|
|
1266
|
+
out = await installSkill({
|
|
1267
|
+
name: c.name,
|
|
1268
|
+
files: c.files ?? [{ path: "SKILL.md", contents: c.raw }],
|
|
1269
|
+
source,
|
|
1270
|
+
commit_sha: c.commit_sha,
|
|
1271
|
+
scope: ctx.scope,
|
|
1272
|
+
agents: ctx.targets,
|
|
1273
|
+
explicitAgents: Boolean(ctx.flags.agent?.length),
|
|
1274
|
+
mode: ctx.flags.mode,
|
|
1275
|
+
force: ctx.flags.force
|
|
1276
|
+
});
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
1279
|
+
return { kind: "blocked", name: c.name, reason, message: `${GLYPH.blocked} ${c.name} \u2014 ${reason}` };
|
|
1280
|
+
}
|
|
1281
|
+
const notices = [];
|
|
1282
|
+
if (out.replaced) notices.push(replacedNotice(`${c.name}: replaced previous install (${out.replaced})`));
|
|
1283
|
+
if (c.pinMiss) notices.push(warnNotice(`${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
|
|
1284
|
+
if (c.note) notices.push(warnNotice(`${c.name}: ${c.note}`));
|
|
1285
|
+
if (c.type === "pack" && !c.files) {
|
|
1286
|
+
notices.push(warnNotice(`${c.name} is a pack but its asset files could not be fetched \u2014 only SKILL.md was installed. It may reference missing files.`));
|
|
1287
|
+
}
|
|
1288
|
+
if (c.registrySlug && !telemetryDisabled(ctx.env)) ctx.deps.fireInstall(c.registrySlug, ctx.flags);
|
|
1289
|
+
return {
|
|
1290
|
+
kind: "installed",
|
|
1291
|
+
doc: { name: c.name, canonical: out.canonical, targets: out.targets, skipped: out.skipped, digest: out.digest },
|
|
1292
|
+
score: result.score,
|
|
1293
|
+
securityFlags: securityFlags2,
|
|
1294
|
+
notices,
|
|
1295
|
+
source,
|
|
1296
|
+
...out.replaced ? { replaced: out.replaced } : {}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
async function tryPluginInstall(arg, flags) {
|
|
1300
|
+
let spec;
|
|
1301
|
+
try {
|
|
1302
|
+
spec = parseSource(arg, { ref: flags.ref });
|
|
1303
|
+
} catch {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
if (spec.kind !== "plugin") return null;
|
|
1307
|
+
if (flags.agent?.length || flags.skill?.length || flags.list || flags.mode === "copy") return null;
|
|
1308
|
+
const viaCli = await claudeCliAvailable() ? await installClaudePlugin(spec.owner, spec.slug) : null;
|
|
1309
|
+
const res = viaCli?.ok ? viaCli : await installPluginNatively(spec.owner, spec.slug, { home: flags.home });
|
|
1310
|
+
if (!res.ok) {
|
|
1311
|
+
if (!viaCli && /no Claude Code directory/.test(res.reason)) return null;
|
|
1312
|
+
return { ok: false, reason: viaCli && !viaCli.ok ? viaCli.reason : res.reason };
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
ok: true,
|
|
1316
|
+
agent: "claude-code",
|
|
1317
|
+
name: res.name,
|
|
1318
|
+
note: `installed ${res.name} as a Claude Code plugin \u2014 manage it with \`claude plugin\` or /plugin`
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
async function runAdd(arg, flags, deps) {
|
|
1322
|
+
const d = deps ?? makeDefaultDeps(flags);
|
|
1323
|
+
const env = flags.env ?? process.env;
|
|
1324
|
+
const attempt = await tryPluginInstall(arg, flags);
|
|
1325
|
+
const managed = attempt?.ok ? attempt : null;
|
|
1326
|
+
const managedNotices = !attempt ? [] : attempt.ok ? [infoNotice(attempt.note)] : [warnNotice(`could not install this as a Claude Code plugin (${attempt.reason}) \u2014 installing its skills instead`)];
|
|
1327
|
+
let scopeOnce;
|
|
1328
|
+
const scopeOf = async () => scopeOnce ??= await resolveScope(flags, d);
|
|
1329
|
+
if (managed) {
|
|
1330
|
+
const pre = await scopeOf();
|
|
1331
|
+
if (!pre) return finish2(flags, { kind: "cancel" });
|
|
1332
|
+
const others = detectAgents(pre.scope).filter((id) => id !== managed.agent);
|
|
1333
|
+
if (!others.length) {
|
|
1334
|
+
const paths2 = { home: flags.home ?? homedir4(), cwd: flags.cwd ?? process.cwd() };
|
|
1335
|
+
return finish2(flags, {
|
|
1336
|
+
kind: "install",
|
|
1337
|
+
scope: pre.scope.global ? "global" : "project",
|
|
1338
|
+
records: [],
|
|
1339
|
+
notices: [...pre.note ? [infoNotice(pre.note)] : [], ...managedNotices],
|
|
1340
|
+
paths: paths2,
|
|
1341
|
+
managed: managed.name
|
|
1342
|
+
});
|
|
1329
1343
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1344
|
+
}
|
|
1345
|
+
let candidates;
|
|
1346
|
+
try {
|
|
1347
|
+
candidates = await resolveCandidates(arg, flags, d);
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
if (e instanceof IntegrityError) {
|
|
1350
|
+
return finish2(flags, { kind: "fail", error: e.message, blocked: [{ name: arg, reason: e.message }] });
|
|
1332
1351
|
}
|
|
1333
|
-
if (
|
|
1352
|
+
if (flags.json) return finish2(flags, { kind: "fail", error: e instanceof Error ? e.message : String(e) });
|
|
1353
|
+
throw e;
|
|
1334
1354
|
}
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1355
|
+
const early = gate(flags, d, candidates);
|
|
1356
|
+
if (early) return early;
|
|
1357
|
+
const resolved = await scopeOf();
|
|
1358
|
+
if (!resolved) return finish2(flags, { kind: "cancel" });
|
|
1359
|
+
const scope = resolved.scope;
|
|
1360
|
+
const notices = resolved.note ? [infoNotice(resolved.note)] : [];
|
|
1361
|
+
notices.push(...managedNotices);
|
|
1362
|
+
const detected = detectAgents(scope).filter((id) => id !== managed?.agent);
|
|
1363
|
+
const chosen = await chooseTargets(flags, d, scope, detected, managed?.name);
|
|
1364
|
+
if (!chosen) return finish2(flags, { kind: "cancel" });
|
|
1365
|
+
notices.push(...chosen.notices);
|
|
1366
|
+
const ctx = { arg, flags, deny: new Set(flags.deny ?? []), scope, targets: chosen.targets, env, deps: d };
|
|
1367
|
+
const records = [];
|
|
1368
|
+
for (const c of candidates) records.push(await installOne(c, ctx));
|
|
1369
|
+
const paths = { home: flags.home ?? homedir4(), cwd: flags.cwd ?? process.cwd() };
|
|
1370
|
+
return finish2(flags, { kind: "install", scope: scope.global ? "global" : "project", records, notices, paths, ...managed ? { managed: managed.name } : {} });
|
|
1371
|
+
}
|
|
1372
|
+
function addFlags(opts, parent) {
|
|
1373
|
+
const merged = { ...parent, ...opts };
|
|
1374
|
+
if (opts.copy) merged.mode = "copy";
|
|
1375
|
+
return merged;
|
|
1337
1376
|
}
|
|
1338
1377
|
function addCommand() {
|
|
1339
|
-
return new Command7("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to your user-level agent folders (available in every project)").option("-p, --project", "install into the current directory's agent folders (committed with the project)").option("-a, --agent <agents...>", "target specific agents (default: every agent detected on this machine)").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--
|
|
1340
|
-
const merged =
|
|
1341
|
-
const run = await
|
|
1342
|
-
|
|
1343
|
-
|
|
1378
|
+
return new Command7("add").description("Install a skill from the registry, a GitHub repo, or a local path (lints before writing)").argument("<source>", "registry slug (owner/name), GitHub source, or local path").option("-g, --global", "install to your user-level agent folders (available in every project)").option("-p, --project", "install into the current directory's agent folders (committed with the project)").option("-a, --agent <agents...>", "target specific agents (default: every agent detected on this machine)").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--ref <ref>", "git ref (branch, tag or commit) for GitHub sources").option("-l, --list", "list the skills a source contains without installing").addOption(new Option("--mode <mode>", "link (junction/symlink into each agent dir, default) or copy").choices(["link", "copy"]).default("link")).option("--force", "replace a skill installed from a different source, or an untracked directory").addOption(new Option("--copy", "alias of --mode copy").hideHelp()).option("-y, --yes", "skip prompts; scope is auto-detected (project if this folder looks like a project, else global)").addOption(new Option("--allow-unverified").hideHelp()).option("--skip-lint", "install even if the skill fails lint").option("--deny <flags...>", "refuse skills carrying any of these security flags (opt-in)").action(async (source, opts, cmd) => {
|
|
1379
|
+
const merged = addFlags(opts, cmd.parent?.opts());
|
|
1380
|
+
const { run: run2, printed } = await runAddWithUI(source, merged);
|
|
1381
|
+
if (!printed) {
|
|
1382
|
+
if (run2.exitCode === 1 && !merged.json) console.error(run2.output);
|
|
1383
|
+
else console.log(run2.output);
|
|
1384
|
+
}
|
|
1385
|
+
process.exitCode = run2.exitCode;
|
|
1344
1386
|
});
|
|
1345
1387
|
}
|
|
1388
|
+
var clackUI = {
|
|
1389
|
+
intro: (m) => p3.intro(m),
|
|
1390
|
+
spinner: () => p3.spinner(),
|
|
1391
|
+
message: (m) => p3.log.message(m),
|
|
1392
|
+
outro: (m) => p3.outro(m),
|
|
1393
|
+
cancel: (m) => p3.cancel(m),
|
|
1394
|
+
confirm: async (m) => {
|
|
1395
|
+
const answer = await p3.confirm({ message: m, initialValue: true });
|
|
1396
|
+
return p3.isCancel(answer) ? null : answer;
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
var FIND_SKILLS = "skillmd/find-skills";
|
|
1400
|
+
async function offerFindSkills(source, flags, run2, ui, deps) {
|
|
1401
|
+
if (!ui.confirm || run2.exitCode !== 0 || !run2.written.length || !run2.scope || !run2.agents?.length) return;
|
|
1402
|
+
if (source.replace(/^registry:/, "").toLowerCase() === FIND_SKILLS) return;
|
|
1403
|
+
const cfg = readConfig(flags.home);
|
|
1404
|
+
if (cfg.findSkillsOffered) return;
|
|
1405
|
+
const global = run2.scope === "global";
|
|
1406
|
+
const lock = readLock({ global, cwd: flags.cwd, home: flags.home });
|
|
1407
|
+
if (Object.values(lock.skills).some((e) => e.source === `registry:${FIND_SKILLS}`)) return;
|
|
1408
|
+
try {
|
|
1409
|
+
writeConfig({ ...cfg, findSkillsOffered: true }, flags.home);
|
|
1410
|
+
} catch {
|
|
1411
|
+
}
|
|
1412
|
+
const yes = await ui.confirm("Also add find-skills, so your agent can look for more skills on SkillMD when a task needs one?");
|
|
1413
|
+
if (!yes) return;
|
|
1414
|
+
const r = await runAdd(FIND_SKILLS, {
|
|
1415
|
+
...flags,
|
|
1416
|
+
yes: true,
|
|
1417
|
+
json: false,
|
|
1418
|
+
skill: void 0,
|
|
1419
|
+
list: void 0,
|
|
1420
|
+
ref: void 0,
|
|
1421
|
+
global,
|
|
1422
|
+
project: !global,
|
|
1423
|
+
agent: run2.agents
|
|
1424
|
+
}, deps);
|
|
1425
|
+
ui.message(r.exitCode === 0 ? `${pc8.green(GLYPH.ok)} find-skills added` : pc8.yellow(`${GLYPH.warn} find-skills could not be added. Try later: npx get-skill add ${FIND_SKILLS}`));
|
|
1426
|
+
}
|
|
1427
|
+
async function runAddWithUI(source, flags, opts = {}) {
|
|
1428
|
+
const term = opts.term ?? { ...realTerminal(), env: flags.env ?? process.env };
|
|
1429
|
+
const base = opts.deps ?? makeDefaultDeps(flags);
|
|
1430
|
+
if (!isInteractive(flags, term)) return { run: await runAdd(source, flags, base), printed: false };
|
|
1431
|
+
const ui = opts.ui ?? clackUI;
|
|
1432
|
+
ui.intro(banner());
|
|
1433
|
+
try {
|
|
1434
|
+
const run2 = await runAdd(source, flags, {
|
|
1435
|
+
// NOTE: spreading `base` reads makeDefaultDeps()'s promptScope/promptAgents
|
|
1436
|
+
// getters right here, freezing their values for this run. That is exactly
|
|
1437
|
+
// what we want (interactivity is decided once, up front), but it does mean
|
|
1438
|
+
// a getter added to AddDeps later stops being lazy the moment it crosses
|
|
1439
|
+
// this spread.
|
|
1440
|
+
...base,
|
|
1441
|
+
resolve: async (arg, f) => {
|
|
1442
|
+
const s = ui.spinner();
|
|
1443
|
+
s.start(`Resolving ${arg}`);
|
|
1444
|
+
try {
|
|
1445
|
+
const c = await base.resolve(arg, f);
|
|
1446
|
+
s.stop(`${c.length} skill${c.length === 1 ? "" : "s"} found`);
|
|
1447
|
+
return c;
|
|
1448
|
+
} catch (e) {
|
|
1449
|
+
s.stop(pc8.red("Could not resolve the source"));
|
|
1450
|
+
throw e;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
if (run2.cancelled) {
|
|
1455
|
+
ui.cancel("Installation cancelled \u2014 nothing was installed.");
|
|
1456
|
+
return { run: run2, printed: true };
|
|
1457
|
+
}
|
|
1458
|
+
ui.message(run2.output);
|
|
1459
|
+
await offerFindSkills(source, flags, run2, ui, base);
|
|
1460
|
+
ui.outro(run2.exitCode === 0 ? `Done. Run ${pc8.cyan("npx get-skill list")} to see everything installed.` : pc8.red("Some skills were not installed."));
|
|
1461
|
+
return { run: run2, printed: true };
|
|
1462
|
+
} catch (e) {
|
|
1463
|
+
ui.outro(pc8.red("Nothing was installed."));
|
|
1464
|
+
throw e;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1346
1467
|
|
|
1347
|
-
//
|
|
1468
|
+
// src/commands/list.ts
|
|
1348
1469
|
import { Command as Command8 } from "commander";
|
|
1349
|
-
import
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
console.log(`${pc8.bold(s.name)} ${pc8.dim(`[${s.agent}/${s.scope}]`)}
|
|
1366
|
-
${desc}`);
|
|
1470
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
1471
|
+
import { join as join4 } from "path";
|
|
1472
|
+
import pc9 from "picocolors";
|
|
1473
|
+
import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
|
|
1474
|
+
function runList(flags = {}) {
|
|
1475
|
+
const scopes = scopesFor(flags);
|
|
1476
|
+
let items = [];
|
|
1477
|
+
for (const global of scopes) {
|
|
1478
|
+
for (const s of listInstalled({ global, cwd: flags.cwd, home: flags.home })) {
|
|
1479
|
+
const md = join4(s.path, "SKILL.md");
|
|
1480
|
+
let description = "";
|
|
1481
|
+
if (existsSync3(md)) {
|
|
1482
|
+
const parsed = parseSkillMd3(readFileSync2(md, "utf8"));
|
|
1483
|
+
description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
|
|
1484
|
+
}
|
|
1485
|
+
items.push({ ...s, description: safeText(description, 80) });
|
|
1367
1486
|
}
|
|
1487
|
+
}
|
|
1488
|
+
if (flags.agent?.length) items = items.filter((i) => i.agents.some((a) => flags.agent.includes(a)));
|
|
1489
|
+
items.sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
|
|
1490
|
+
if (flags.json) {
|
|
1491
|
+
return { items, output: JSON.stringify(items.map(({ name, scope, path, agents, source, mode, tracked, description }) => ({ name, scope, path, agents, source: source ?? null, mode, tracked, description })), null, 2) };
|
|
1492
|
+
}
|
|
1493
|
+
if (items.length === 0) return { items, output: pc9.dim("No installed skills found. Install one with `npx get-skill add owner/name`.") };
|
|
1494
|
+
const w = Math.max(...items.map((i) => i.name.length), 4);
|
|
1495
|
+
const lines = items.map((i) => {
|
|
1496
|
+
const tag = i.tracked ? pc9.dim(i.source ?? "") : pc9.yellow("untracked");
|
|
1497
|
+
const modes = [...new Set(Object.values(i.mode))].join("/");
|
|
1498
|
+
return `${pc9.bold(i.name.padEnd(w))} ${pc9.cyan(i.scope.padEnd(7))} ${i.agents.join(", ").padEnd(24)} ${pc9.dim(modes.padEnd(9))} ${tag}
|
|
1499
|
+
${" ".repeat(w + 2)}${pc9.dim(i.description)}`;
|
|
1500
|
+
});
|
|
1501
|
+
return { items, output: lines.join("\n") };
|
|
1502
|
+
}
|
|
1503
|
+
function listCommand() {
|
|
1504
|
+
return new Command8("list").alias("ls").description("List installed skills (project and global) with their source and link mode").option("-g, --global", "only user-level skills").option("-p, --project", "only this project's skills").option("-a, --agent <agents...>", "only skills linked into these agents").option("--json", "machine-readable output").action((opts, cmd) => {
|
|
1505
|
+
console.log(runList({ ...cmd.parent?.opts(), ...opts }).output);
|
|
1368
1506
|
});
|
|
1369
1507
|
}
|
|
1370
1508
|
|
|
1371
|
-
//
|
|
1509
|
+
// src/commands/remove.ts
|
|
1372
1510
|
import { Command as Command9 } from "commander";
|
|
1373
|
-
import { rmSync
|
|
1374
|
-
import
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1511
|
+
import { rmSync } from "fs";
|
|
1512
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
1513
|
+
import * as p4 from "@clack/prompts";
|
|
1514
|
+
import pc10 from "picocolors";
|
|
1515
|
+
var defaultDeps = (flags) => isInteractive(flags) ? {
|
|
1516
|
+
confirm: async (msg2) => {
|
|
1517
|
+
const a = await p4.confirm({ message: msg2, initialValue: false });
|
|
1518
|
+
return !p4.isCancel(a) && a;
|
|
1519
|
+
},
|
|
1520
|
+
pick: async ({ names }) => {
|
|
1521
|
+
const a = await p4.multiselect({ message: "Which skills do you want to remove?", options: names.map((n) => ({ value: n, label: n })), required: true });
|
|
1522
|
+
return p4.isCancel(a) ? null : a;
|
|
1523
|
+
}
|
|
1524
|
+
} : {};
|
|
1525
|
+
function finish3(flags, result, doc) {
|
|
1526
|
+
return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
|
|
1527
|
+
}
|
|
1528
|
+
var scopeLabel = (scope) => scope.global ? "global" : "project";
|
|
1529
|
+
async function runRemove(names, flags, deps = defaultDeps(flags)) {
|
|
1530
|
+
const scopes = scopesFor(flags).map((global) => ({ global, cwd: flags.cwd, home: flags.home }));
|
|
1531
|
+
const where = scopes.map(scopeLabel).join(" and ");
|
|
1379
1532
|
const lines = [];
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
removed.
|
|
1383
|
-
lines.push(pc9.green(`\u2713 removed ${t.name} (${t.agent})`));
|
|
1533
|
+
if (flags.all && names.length) {
|
|
1534
|
+
const error = "--all cannot be combined with skill names";
|
|
1535
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
|
|
1384
1536
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1537
|
+
const perScope = scopes.map((scope) => ({ scope, installed: listInstalled(scope) }));
|
|
1538
|
+
const allNames = [...new Set(perScope.flatMap((s) => s.installed.map((i) => i.name)))];
|
|
1539
|
+
const nothingInstalled = `No installed skills found in ${where} scope.`;
|
|
1540
|
+
let targets = names;
|
|
1541
|
+
if (flags.all) {
|
|
1542
|
+
if (allNames.length === 0) {
|
|
1543
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
|
|
1544
|
+
}
|
|
1545
|
+
targets = allNames;
|
|
1546
|
+
} else if (targets.length === 0) {
|
|
1547
|
+
if (!deps.pick) {
|
|
1548
|
+
const error = "Give one or more skill names, or --all.";
|
|
1549
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(`${error} `) + pc10.dim(nonInteractiveHint("skill names and -y")) }, { ok: false, error });
|
|
1550
|
+
}
|
|
1551
|
+
if (allNames.length === 0) {
|
|
1552
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
|
|
1553
|
+
}
|
|
1554
|
+
const picked = await deps.pick({ names: allNames });
|
|
1555
|
+
if (!picked) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
|
|
1556
|
+
targets = picked;
|
|
1387
1557
|
}
|
|
1388
|
-
|
|
1558
|
+
const known = targets.filter((n) => allNames.includes(n));
|
|
1559
|
+
const missing = targets.filter((n) => !known.includes(n));
|
|
1560
|
+
for (const n of missing) lines.push(pc10.yellow(`- ${n} not installed (${where} scope)`));
|
|
1561
|
+
if (known.length === 0) {
|
|
1562
|
+
const output = lines.join("\n");
|
|
1563
|
+
return finish3(flags, { removed: [], exitCode: 1, output }, { ok: false, error: `not installed: ${missing.join(", ")}`, missing });
|
|
1564
|
+
}
|
|
1565
|
+
if (!flags.yes) {
|
|
1566
|
+
const what = flags.agent?.length ? `from ${flags.agent.join(", ")}` : "from every agent";
|
|
1567
|
+
const ok = deps.confirm ? await deps.confirm(`Remove ${known.length} skill${known.length === 1 ? "" : "s"} (${known.join(", ")}) ${what} in ${where} scope?`) : null;
|
|
1568
|
+
if (ok === null) {
|
|
1569
|
+
const error = nonInteractiveHint("-y");
|
|
1570
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
|
|
1571
|
+
}
|
|
1572
|
+
if (!ok) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
|
|
1573
|
+
}
|
|
1574
|
+
const removed = [];
|
|
1575
|
+
const skipped = [];
|
|
1576
|
+
let changed = 0;
|
|
1577
|
+
for (const name of known) {
|
|
1578
|
+
for (const { scope, installed } of perScope) {
|
|
1579
|
+
if (!installed.some((s) => s.name === name)) continue;
|
|
1580
|
+
const tag = scopes.length > 1 ? pc10.dim(` [${scopeLabel(scope)}]`) : "";
|
|
1581
|
+
if (flags.agent?.length) {
|
|
1582
|
+
const lock = readLock({ global: Boolean(scope.global), cwd: scope.cwd, home: scope.home });
|
|
1583
|
+
const entry = lock.skills[name];
|
|
1584
|
+
const canonRoot = resolve4(canonicalDir(scope));
|
|
1585
|
+
let acted = false;
|
|
1586
|
+
for (const a of flags.agent) {
|
|
1587
|
+
let dir;
|
|
1588
|
+
try {
|
|
1589
|
+
dir = agentDir(a, scope);
|
|
1590
|
+
} catch (e) {
|
|
1591
|
+
lines.push(pc10.yellow(`- ${a}: ${e instanceof Error ? e.message : String(e)}`));
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
if (resolve4(dir) === canonRoot || entry?.mode[a] === "canonical") {
|
|
1595
|
+
lines.push(pc10.dim(`${GLYPH.skip} ${name}: ${a} reads the canonical copy directly \u2014 unlinking is a no-op (use remove without -a to delete the skill)`));
|
|
1596
|
+
skipped.push({ skill: name, agent: a, scope: scopeLabel(scope), reason: "reads the canonical copy directly" });
|
|
1597
|
+
acted = true;
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const pth = join5(dir, name);
|
|
1601
|
+
if (isSkillDirOrLink(pth)) {
|
|
1602
|
+
rmSync(pth, { recursive: true, force: true });
|
|
1603
|
+
removed.push(pth);
|
|
1604
|
+
acted = true;
|
|
1605
|
+
lines.push(pc10.green(`${GLYPH.ok} removed ${name} from ${a}`) + tag);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
if (!acted) lines.push(pc10.yellow(`- ${name} is not linked into ${flags.agent.join(", ")}`) + tag);
|
|
1609
|
+
if (entry) {
|
|
1610
|
+
const left = entry.agents.filter((a) => !flags.agent.includes(a));
|
|
1611
|
+
if (left.length) upsertEntry({ global: Boolean(scope.global), cwd: scope.cwd, home: scope.home }, name, { ...entry, agents: left, mode: Object.fromEntries(Object.entries(entry.mode).filter(([a]) => left.includes(a))) });
|
|
1612
|
+
else {
|
|
1613
|
+
const r2 = uninstallSkill(name, scope);
|
|
1614
|
+
removed.push(...r2.removed);
|
|
1615
|
+
lines.push(pc10.dim(` (no agents left \u2014 removed the canonical copy too)`));
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (acted) changed++;
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
const r = uninstallSkill(name, scope);
|
|
1622
|
+
removed.push(...r.removed);
|
|
1623
|
+
changed++;
|
|
1624
|
+
lines.push(pc10.green(`${GLYPH.ok} removed ${name}`) + tag + pc10.dim(` (${r.removed.length} path${r.removed.length === 1 ? "" : "s"}${r.untracked ? ", untracked" : ""})`));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const exitCode = changed ? 0 : 1;
|
|
1628
|
+
const doc = { ok: exitCode === 0, removed, missing };
|
|
1629
|
+
if (skipped.length) doc.skipped = skipped;
|
|
1630
|
+
return finish3(flags, { removed, exitCode, output: lines.join("\n") }, doc);
|
|
1389
1631
|
}
|
|
1390
1632
|
function removeCommand() {
|
|
1391
|
-
return new Command9("remove").alias("rm").description("Remove installed skills").argument("
|
|
1392
|
-
const
|
|
1393
|
-
console.log(
|
|
1633
|
+
return new Command9("remove").alias("rm").description("Remove installed skills (links, canonical copy and lock entry)").argument("[names...]", "skill names (omit to pick interactively)").option("-g, --global", "only user-level skills").option("-p, --project", "only this project's skills").option("-a, --agent <agents...>", "remove only from these agents").option("--all", "remove every skill in the scope").option("-y, --yes", "skip the confirmation").option("--json", "machine-readable output").action(async (names, opts, cmd) => {
|
|
1634
|
+
const run2 = await runRemove(names, { ...cmd.parent?.opts(), ...opts });
|
|
1635
|
+
console.log(run2.output);
|
|
1636
|
+
process.exitCode = run2.exitCode;
|
|
1394
1637
|
});
|
|
1395
1638
|
}
|
|
1396
1639
|
|
|
1397
|
-
//
|
|
1640
|
+
// src/commands/update.ts
|
|
1398
1641
|
import { Command as Command10 } from "commander";
|
|
1399
|
-
import
|
|
1400
|
-
import {
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1642
|
+
import pc11 from "picocolors";
|
|
1643
|
+
import { lint as lint3 } from "@skillmds/core";
|
|
1644
|
+
function pickSkillTree(files, want) {
|
|
1645
|
+
const norm = files.map((f) => ({ path: f.path.replace(/\\/g, "/"), contents: f.contents }));
|
|
1646
|
+
const match = norm.find((f) => f.path.endsWith("/SKILL.md") && f.path.split("/").at(-2) === want);
|
|
1647
|
+
if (match) {
|
|
1648
|
+
const prefix = match.path.slice(0, -"SKILL.md".length);
|
|
1649
|
+
return norm.filter((f) => f.path.startsWith(prefix)).map((f) => ({ path: f.path.slice(prefix.length), contents: f.contents }));
|
|
1650
|
+
}
|
|
1651
|
+
if (norm.some((f) => f.path === "SKILL.md")) return norm;
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
function makeDefaultUpdateDeps(opts = {}) {
|
|
1655
|
+
const tree = opts.resolveTree ?? ((input) => resolveTree(input));
|
|
1656
|
+
const one = opts.resolveSource ?? ((url) => resolveSource(url));
|
|
1657
|
+
const client = opts.client ?? createClient;
|
|
1658
|
+
return {
|
|
1659
|
+
async fetchLatest(source, flags, name) {
|
|
1660
|
+
if (source.startsWith("registry:")) {
|
|
1661
|
+
const slug = source.slice("registry:".length);
|
|
1662
|
+
const { api, base, token } = client(flags);
|
|
1663
|
+
let skill;
|
|
1664
|
+
try {
|
|
1665
|
+
skill = await api(`/api/skills/${slug}`);
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
if (e.status === 404) return null;
|
|
1668
|
+
throw e;
|
|
1669
|
+
}
|
|
1670
|
+
const bundle = await fetchBundle(base, slug, token);
|
|
1671
|
+
if (bundle?.length) return { files: bundle, commit_sha: skill.commit_sha ?? void 0 };
|
|
1672
|
+
if (skill.type === "pack") {
|
|
1673
|
+
const pack = await resolvePackFiles(skill);
|
|
1674
|
+
if (pack) return { files: pack.files, commit_sha: skill.commit_sha ?? void 0 };
|
|
1675
|
+
}
|
|
1676
|
+
return { files: [{ path: "SKILL.md", contents: skillMdFor(skill) }], commit_sha: skill.commit_sha ?? void 0 };
|
|
1677
|
+
}
|
|
1678
|
+
if (source.startsWith("github:")) {
|
|
1679
|
+
const spec = parseSource(source);
|
|
1680
|
+
if (spec.kind !== "github") return null;
|
|
1681
|
+
const files = await tree(gigetInput(spec));
|
|
1682
|
+
const picked = pickSkillTree(files, spec.skill ?? name);
|
|
1683
|
+
return picked ? { files: picked } : null;
|
|
1684
|
+
}
|
|
1685
|
+
if (source.startsWith("gist:")) {
|
|
1686
|
+
const [user, id] = source.slice("gist:".length).split("/");
|
|
1687
|
+
const rs = await one(`https://gist.github.com/${user}/${id}`);
|
|
1688
|
+
return rs[0] ? { files: [{ path: "SKILL.md", contents: rs[0].raw }] } : null;
|
|
1689
|
+
}
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
var defaultUpdateDeps = makeDefaultUpdateDeps();
|
|
1695
|
+
async function runUpdate(names, flags, deps = defaultUpdateDeps) {
|
|
1696
|
+
const scopes = scopesFor(flags);
|
|
1405
1697
|
const updated = [];
|
|
1698
|
+
const available = [];
|
|
1699
|
+
const failed = [];
|
|
1406
1700
|
const lines = [];
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
const
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
lines.push(pc10.dim(`- ${s.name} already up to date`));
|
|
1701
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1702
|
+
for (const global of scopes) {
|
|
1703
|
+
const scope = { global, cwd: flags.cwd, home: flags.home };
|
|
1704
|
+
const lock = readLock(scope);
|
|
1705
|
+
const installed = listInstalled(scope).filter((s) => names.length === 0 || names.includes(s.name));
|
|
1706
|
+
for (const s of installed) {
|
|
1707
|
+
seen.add(s.name);
|
|
1708
|
+
const label2 = `${s.name}${scopes.length > 1 ? pc11.dim(` [${s.scope}]`) : ""}`;
|
|
1709
|
+
const entry = lock.skills[s.name];
|
|
1710
|
+
if (!entry) {
|
|
1711
|
+
lines.push(pc11.dim(`- ${label2} untracked \u2014 reinstall with \`npx get-skill add\` to track updates`));
|
|
1419
1712
|
continue;
|
|
1420
1713
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
lines.push(pc10.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
|
|
1714
|
+
if (entry.source.startsWith("local:")) {
|
|
1715
|
+
lines.push(pc11.dim(`- ${label2} local source \u2014 not auto-updated`));
|
|
1424
1716
|
continue;
|
|
1425
1717
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1718
|
+
try {
|
|
1719
|
+
const latest = await deps.fetchLatest(entry.source, flags, s.name);
|
|
1720
|
+
if (!latest) {
|
|
1721
|
+
failed.push(s.name);
|
|
1722
|
+
lines.push(pc11.red(`${GLYPH.blocked} ${label2} no longer available upstream (${entry.source})`));
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (entry.source.startsWith("registry:") && latest.commit_sha && entry.commit_sha && latest.commit_sha === entry.commit_sha) {
|
|
1726
|
+
lines.push(pc11.dim(`- ${label2} up to date`));
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
if (digestOf(latest.files) === entry.digest) {
|
|
1730
|
+
lines.push(pc11.dim(`- ${label2} up to date`));
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (flags.check) {
|
|
1734
|
+
available.push(s.name);
|
|
1735
|
+
lines.push(pc11.yellow(`\u2191 ${label2} update available`));
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
const md = latest.files.find((f) => f.path === "SKILL.md");
|
|
1739
|
+
const raw = md ? typeof md.contents === "string" ? md.contents : md.contents.toString("utf8") : "";
|
|
1740
|
+
const result = lint3(raw, { slug: s.name });
|
|
1741
|
+
if (!result.ok) {
|
|
1742
|
+
failed.push(s.name);
|
|
1743
|
+
lines.push(pc11.red(`${GLYPH.blocked} ${label2} update failed lint (${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}) \u2014 kept the installed version`));
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
await installSkill({ name: s.name, files: latest.files, source: entry.source, commit_sha: latest.commit_sha, scope, agents: entry.agents, explicitAgents: true, mode: Object.values(entry.mode).includes("copy") ? "copy" : "link" });
|
|
1747
|
+
updated.push(s.name);
|
|
1748
|
+
lines.push(pc11.green(`${GLYPH.ok} ${label2} updated`));
|
|
1749
|
+
} catch (e) {
|
|
1750
|
+
failed.push(s.name);
|
|
1751
|
+
lines.push(pc11.red(`${GLYPH.blocked} ${label2} ${e instanceof Error ? e.message : String(e)}`));
|
|
1752
|
+
}
|
|
1431
1753
|
}
|
|
1432
1754
|
}
|
|
1433
|
-
|
|
1755
|
+
for (const n of names) {
|
|
1756
|
+
if (seen.has(n)) continue;
|
|
1757
|
+
failed.push(n);
|
|
1758
|
+
lines.push(pc11.yellow(`- ${n} not installed`));
|
|
1759
|
+
}
|
|
1760
|
+
const exitCode = failed.length ? 1 : 0;
|
|
1761
|
+
if (flags.json) return { updated, available, failed, exitCode, output: JSON.stringify({ ok: exitCode === 0, updated: [...new Set(updated)], available: [...new Set(available)], failed: [...new Set(failed)] }, null, 2) };
|
|
1762
|
+
if (lines.length === 0) lines.push(pc11.dim("No installed skills found."));
|
|
1763
|
+
return { updated, available, failed, exitCode, output: lines.join("\n") };
|
|
1434
1764
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
const hit = r?.items?.find((it) => it.slug.split("/")[1] === s.name);
|
|
1438
|
-
return hit?.slug ?? null;
|
|
1765
|
+
function scopeOptions(cmd) {
|
|
1766
|
+
return cmd.option("-g, --global", "only user-level skills").option("-p, --project", "only this project's skills").option("--json", "machine-readable output");
|
|
1439
1767
|
}
|
|
1440
1768
|
function updateCommand() {
|
|
1441
|
-
return new Command10("update").description("Update installed skills from the
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1769
|
+
return scopeOptions(new Command10("update").description("Update installed skills from the source recorded in the lock file (re-lints before writing)").argument("[names...]", "specific skills (default: all tracked)").option("--check", "only report which skills have updates")).action(async (names, opts, cmd) => {
|
|
1770
|
+
const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts });
|
|
1771
|
+
console.log(run2.output);
|
|
1772
|
+
process.exitCode = run2.exitCode;
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
function checkCommand() {
|
|
1776
|
+
return scopeOptions(new Command10("check").description("Report installed skills that have updates (alias of `update --check`)").argument("[names...]", "specific skills (default: all tracked)")).action(async (names, opts, cmd) => {
|
|
1777
|
+
const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts, check: true });
|
|
1778
|
+
console.log(run2.output);
|
|
1779
|
+
process.exitCode = run2.exitCode;
|
|
1445
1780
|
});
|
|
1446
1781
|
}
|
|
1447
1782
|
|
|
1448
|
-
//
|
|
1783
|
+
// src/commands/publish.ts
|
|
1449
1784
|
import { Command as Command11 } from "commander";
|
|
1450
|
-
import
|
|
1785
|
+
import pc12 from "picocolors";
|
|
1786
|
+
import { lint as lint4, toText } from "@skillmds/core";
|
|
1451
1787
|
var defaultDeps2 = {
|
|
1452
1788
|
resolve: resolveSource,
|
|
1453
1789
|
async submit(payload, flags) {
|
|
1454
1790
|
const { api, hasToken } = createClient(flags);
|
|
1455
|
-
if (!hasToken) throw new Error("No token. Run `
|
|
1791
|
+
if (!hasToken) throw new Error("No token. Run `npx get-skill login` or pass --token.");
|
|
1456
1792
|
return api(`/api/skills`, {
|
|
1457
1793
|
method: "POST",
|
|
1458
1794
|
headers: { "content-type": "application/json" },
|
|
@@ -1463,78 +1799,114 @@ var defaultDeps2 = {
|
|
|
1463
1799
|
async function runPublish(target, flags, deps = defaultDeps2) {
|
|
1464
1800
|
const sources = await deps.resolve(target);
|
|
1465
1801
|
if (sources.length !== 1) {
|
|
1466
|
-
return { result:
|
|
1802
|
+
return { result: lint4(""), published: false, exitCode: 1, output: `Expected exactly one SKILL.md, found ${sources.length}.` };
|
|
1467
1803
|
}
|
|
1468
1804
|
const src = sources[0];
|
|
1469
|
-
const result =
|
|
1805
|
+
const result = lint4(src.raw, { slug: src.slug });
|
|
1470
1806
|
const report = toText([{ file: src.path, result }], { decorate });
|
|
1471
1807
|
if (!result.ok) {
|
|
1472
1808
|
return { result, published: false, exitCode: 1, output: `${report}
|
|
1473
|
-
${
|
|
1809
|
+
${pc12.red("\u2717 Refusing to publish: fix the errors above.")}` };
|
|
1474
1810
|
}
|
|
1475
1811
|
const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
|
|
1476
1812
|
if (hasWarn && !flags.force) {
|
|
1477
1813
|
return { result, published: false, exitCode: 1, output: `${report}
|
|
1478
|
-
${
|
|
1814
|
+
${pc12.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
|
|
1479
1815
|
}
|
|
1480
1816
|
if (flags.dryRun) {
|
|
1481
1817
|
return { result, published: false, exitCode: 0, output: `${report}
|
|
1482
|
-
${
|
|
1818
|
+
${pc12.green("\u2713 Dry run: would publish.")}` };
|
|
1483
1819
|
}
|
|
1484
1820
|
const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
|
|
1485
1821
|
return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
|
|
1486
|
-
${
|
|
1822
|
+
${pc12.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
|
|
1487
1823
|
}
|
|
1488
1824
|
function publishCommand() {
|
|
1489
1825
|
return new Command11("publish").alias("submit").description("Publish a skill to the registry (refuses to publish on lint errors)").argument("[target]", "local path to the skill", ".").option("--type <type>", "single | pack", "single").option("--force", "publish despite warnings (never bypasses errors)").option("--dry-run", "lint only; do not publish").action(async (target, opts, cmd) => {
|
|
1490
1826
|
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
1491
|
-
const
|
|
1492
|
-
console.log(
|
|
1493
|
-
process.exitCode =
|
|
1827
|
+
const run2 = await runPublish(target, merged);
|
|
1828
|
+
console.log(run2.output);
|
|
1829
|
+
process.exitCode = run2.exitCode;
|
|
1494
1830
|
});
|
|
1495
1831
|
}
|
|
1496
1832
|
|
|
1497
|
-
//
|
|
1833
|
+
// src/commands/login.ts
|
|
1498
1834
|
import { Command as Command12 } from "commander";
|
|
1499
|
-
import * as
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
return;
|
|
1835
|
+
import * as p5 from "@clack/prompts";
|
|
1836
|
+
import pc13 from "picocolors";
|
|
1837
|
+
var defaultLoginDeps = (flags) => promptIf(flags, async () => {
|
|
1838
|
+
const answer = await p5.password({ message: "Paste your SkillMD token (from your account page)" });
|
|
1839
|
+
return p5.isCancel(answer) ? null : answer;
|
|
1840
|
+
});
|
|
1841
|
+
function finish4(flags, result, doc) {
|
|
1842
|
+
return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
|
|
1843
|
+
}
|
|
1844
|
+
async function runLogin(flags, deps = defaultLoginDeps(flags)) {
|
|
1845
|
+
const env = flags.env ?? process.env;
|
|
1846
|
+
let token = flags.token;
|
|
1847
|
+
if (!token) {
|
|
1848
|
+
if (!deps.prompt) {
|
|
1849
|
+
const error = nonInteractiveHint("--token <token> (or set SKILLMD_TOKEN)");
|
|
1850
|
+
return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
|
|
1515
1851
|
}
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1852
|
+
const answer = await deps.prompt();
|
|
1853
|
+
if (answer === null) return finish4(flags, { exitCode: 0, output: pc13.dim("Cancelled.") }, { ok: false, cancelled: true });
|
|
1854
|
+
token = answer;
|
|
1855
|
+
}
|
|
1856
|
+
if (!token) {
|
|
1857
|
+
const error = "A token is required.";
|
|
1858
|
+
return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
|
|
1859
|
+
}
|
|
1860
|
+
const cfg = readConfig(flags.home);
|
|
1861
|
+
let host;
|
|
1862
|
+
try {
|
|
1863
|
+
host = hostOf(resolveApi({ api: flags.api, insecureHttp: flags.insecureHttp }, { env, config: { ...cfg, api: flags.api ?? cfg.api } }));
|
|
1864
|
+
} catch (e) {
|
|
1865
|
+
const msg2 = e instanceof Error ? e.message : String(e);
|
|
1866
|
+
return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg2 }, null, 2) : pc13.red(msg2) };
|
|
1867
|
+
}
|
|
1868
|
+
cfg.token = token;
|
|
1869
|
+
if (flags.api) cfg.api = flags.api;
|
|
1870
|
+
cfg.tokenHost = host;
|
|
1871
|
+
writeConfig(cfg, flags.home);
|
|
1872
|
+
const file = configPath(flags.home);
|
|
1873
|
+
return {
|
|
1874
|
+
exitCode: 0,
|
|
1875
|
+
output: flags.json ? JSON.stringify({ ok: true, file, host }, null, 2) : `Saved token for ${host} to ${file}`
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
function runLogout(flags) {
|
|
1879
|
+
const cfg = readConfig(flags.home);
|
|
1880
|
+
const had = Boolean(cfg.token);
|
|
1881
|
+
delete cfg.token;
|
|
1882
|
+
delete cfg.tokenHost;
|
|
1883
|
+
writeConfig(cfg, flags.home);
|
|
1884
|
+
return {
|
|
1885
|
+
exitCode: 0,
|
|
1886
|
+
output: flags.json ? JSON.stringify({ ok: true, removed: had }, null, 2) : had ? "Removed stored token." : "No stored token."
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
function loginCommand() {
|
|
1890
|
+
return new Command12("login").description("Store a SkillMD personal access token for publishing (prefer this or SKILLMD_TOKEN over --token)").option("--token <token>", "token to store (otherwise prompt)").option("--api <url>", "registry API base to store").option("--json", "machine-readable output").action(async (opts, cmd) => {
|
|
1891
|
+
const r = await runLogin({ ...cmd.parent?.opts(), ...opts });
|
|
1892
|
+
console.log(r.output);
|
|
1893
|
+
process.exitCode = r.exitCode;
|
|
1521
1894
|
});
|
|
1522
1895
|
}
|
|
1523
1896
|
function logoutCommand() {
|
|
1524
|
-
return new Command12("logout").description("Remove the stored SkillMD token").action(() => {
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
console.log("Removed stored token.");
|
|
1897
|
+
return new Command12("logout").description("Remove the stored SkillMD token").option("--json", "machine-readable output").action((opts, cmd) => {
|
|
1898
|
+
const r = runLogout({ ...cmd.parent?.opts(), ...opts });
|
|
1899
|
+
console.log(r.output);
|
|
1900
|
+
process.exitCode = r.exitCode;
|
|
1529
1901
|
});
|
|
1530
1902
|
}
|
|
1531
1903
|
|
|
1532
|
-
//
|
|
1533
|
-
import * as
|
|
1534
|
-
import
|
|
1904
|
+
// src/commands/interactive.ts
|
|
1905
|
+
import * as p6 from "@clack/prompts";
|
|
1906
|
+
import pc14 from "picocolors";
|
|
1535
1907
|
function bail(v) {
|
|
1536
|
-
if (
|
|
1537
|
-
|
|
1908
|
+
if (p6.isCancel(v)) {
|
|
1909
|
+
p6.cancel("Cancelled.");
|
|
1538
1910
|
return true;
|
|
1539
1911
|
}
|
|
1540
1912
|
return false;
|
|
@@ -1542,25 +1914,26 @@ function bail(v) {
|
|
|
1542
1914
|
async function browseSearchResults(items) {
|
|
1543
1915
|
const cols = process.stdout.columns ?? 80;
|
|
1544
1916
|
for (; ; ) {
|
|
1545
|
-
const picked = await
|
|
1917
|
+
const picked = await p6.select({
|
|
1546
1918
|
message: "Search results",
|
|
1547
1919
|
options: [
|
|
1548
1920
|
...items.map((item2) => {
|
|
1549
1921
|
const starsTxt = item2.repo_stars != null ? ` \u2605${formatStars(item2.repo_stars)}` : "";
|
|
1550
1922
|
const checkTxt = item2.verified ? " \u2713" : "";
|
|
1551
|
-
const
|
|
1552
|
-
const
|
|
1923
|
+
const slugTxt = safeText(item2.slug, 200);
|
|
1924
|
+
const plainLabel = `${slugTxt}${starsTxt}${checkTxt}`;
|
|
1925
|
+
const label2 = `${pc14.bold(slugTxt)}${starsTxt ? pc14.dim(starsTxt) : ""}${checkTxt ? pc14.green(checkTxt) : ""}`;
|
|
1553
1926
|
const hintWidth = Math.max(10, cols - plainLabel.length - 6);
|
|
1554
1927
|
return {
|
|
1555
1928
|
value: item2.slug,
|
|
1556
1929
|
label: label2,
|
|
1557
|
-
hint: truncate(item2.description ?? "", hintWidth)
|
|
1930
|
+
hint: truncate(safeText(item2.description ?? ""), hintWidth)
|
|
1558
1931
|
};
|
|
1559
1932
|
}),
|
|
1560
1933
|
{ value: "__back", label: "\u2190 Back to menu" }
|
|
1561
1934
|
]
|
|
1562
1935
|
});
|
|
1563
|
-
if (
|
|
1936
|
+
if (p6.isCancel(picked) || picked === "__back") return;
|
|
1564
1937
|
const item = items.find((i) => i.slug === picked);
|
|
1565
1938
|
if (!item) continue;
|
|
1566
1939
|
await browseOneResult(item);
|
|
@@ -1568,14 +1941,15 @@ async function browseSearchResults(items) {
|
|
|
1568
1941
|
}
|
|
1569
1942
|
async function browseOneResult(item) {
|
|
1570
1943
|
const starsTxt = item.repo_stars != null ? `\u2605 ${formatStars(item.repo_stars)} stars` : "";
|
|
1944
|
+
const slugTxt = safeText(item.slug, 200);
|
|
1571
1945
|
const meta = [
|
|
1572
1946
|
starsTxt,
|
|
1573
|
-
item.verified ?
|
|
1947
|
+
item.verified ? pc14.green("\u2713 verified") : "unverified",
|
|
1574
1948
|
item.type === "pack" ? "pack" : ""
|
|
1575
1949
|
].filter(Boolean).join(" \xB7 ");
|
|
1576
|
-
|
|
1950
|
+
p6.note([safeText(item.description ?? ""), pc14.dim(meta)].join("\n"), slugTxt);
|
|
1577
1951
|
for (; ; ) {
|
|
1578
|
-
const action = await
|
|
1952
|
+
const action = await p6.select({
|
|
1579
1953
|
message: "What next?",
|
|
1580
1954
|
options: [
|
|
1581
1955
|
{ value: "project", label: "Install to this project" },
|
|
@@ -1584,33 +1958,36 @@ async function browseOneResult(item) {
|
|
|
1584
1958
|
{ value: "back", label: "\u2190 Back to results" }
|
|
1585
1959
|
]
|
|
1586
1960
|
});
|
|
1587
|
-
if (
|
|
1961
|
+
if (p6.isCancel(action) || action === "back") return;
|
|
1588
1962
|
if (action === "url") {
|
|
1589
|
-
|
|
1963
|
+
p6.log.message(pc14.dim(`https://skillmd.com/skills/${slugTxt}`));
|
|
1590
1964
|
continue;
|
|
1591
1965
|
}
|
|
1592
|
-
|
|
1966
|
+
p6.log.step(`Installing ${slugTxt}\u2026`);
|
|
1593
1967
|
try {
|
|
1594
|
-
const
|
|
1595
|
-
console.log(
|
|
1596
|
-
if (
|
|
1597
|
-
else
|
|
1968
|
+
const run2 = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
|
|
1969
|
+
console.log(run2.output);
|
|
1970
|
+
if (run2.exitCode === 0) p6.log.success("Installed.");
|
|
1971
|
+
else p6.log.warn("Not installed.");
|
|
1598
1972
|
} catch (err) {
|
|
1599
|
-
|
|
1973
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1600
1974
|
}
|
|
1601
1975
|
}
|
|
1602
1976
|
}
|
|
1603
1977
|
async function runInteractive() {
|
|
1604
|
-
|
|
1978
|
+
p6.intro(banner());
|
|
1605
1979
|
for (; ; ) {
|
|
1606
|
-
const action = await
|
|
1980
|
+
const action = await p6.select({
|
|
1607
1981
|
message: "What would you like to do?",
|
|
1608
1982
|
options: [
|
|
1609
1983
|
{ value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
|
|
1610
1984
|
{ value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
|
|
1611
1985
|
{ value: "search", label: "Search the registry" },
|
|
1986
|
+
{ value: "install", label: "Install a skill", hint: "registry slug, GitHub URL or local path" },
|
|
1612
1987
|
{ value: "init", label: "Create a new skill" },
|
|
1613
1988
|
{ value: "list", label: "List installed skills" },
|
|
1989
|
+
{ value: "update", label: "Update installed skills" },
|
|
1990
|
+
{ value: "remove", label: "Remove skills" },
|
|
1614
1991
|
{ value: "help", label: "Show all commands" },
|
|
1615
1992
|
{ value: "quit", label: "Quit" }
|
|
1616
1993
|
]
|
|
@@ -1619,85 +1996,108 @@ async function runInteractive() {
|
|
|
1619
1996
|
switch (action) {
|
|
1620
1997
|
case "lint":
|
|
1621
1998
|
case "scan": {
|
|
1622
|
-
const path = await
|
|
1999
|
+
const path = await p6.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
|
|
1623
2000
|
if (bail(path)) break;
|
|
1624
2001
|
const target = String(path || ".");
|
|
1625
|
-
const s =
|
|
2002
|
+
const s = p6.spinner();
|
|
1626
2003
|
s.start(action === "lint" ? "Linting" : "Scanning");
|
|
1627
2004
|
try {
|
|
1628
|
-
const
|
|
2005
|
+
const run2 = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
|
|
1629
2006
|
s.stop("Done");
|
|
1630
|
-
console.log(
|
|
1631
|
-
process.exitCode =
|
|
2007
|
+
console.log(run2.output);
|
|
2008
|
+
process.exitCode = run2.exitCode;
|
|
1632
2009
|
} catch (err) {
|
|
1633
|
-
s.stop(
|
|
2010
|
+
s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
|
|
1634
2011
|
process.exitCode = 1;
|
|
1635
2012
|
}
|
|
1636
2013
|
break;
|
|
1637
2014
|
}
|
|
1638
2015
|
case "search": {
|
|
1639
|
-
const query = await
|
|
2016
|
+
const query = await p6.text({ message: "Search the registry for" });
|
|
1640
2017
|
if (bail(query)) break;
|
|
1641
|
-
const verified = await
|
|
2018
|
+
const verified = await p6.confirm({ message: "Verified skills only?", initialValue: false });
|
|
1642
2019
|
if (bail(verified)) break;
|
|
1643
|
-
const s =
|
|
2020
|
+
const s = p6.spinner();
|
|
1644
2021
|
s.start("Searching");
|
|
1645
2022
|
try {
|
|
1646
|
-
const
|
|
1647
|
-
s.stop(`${
|
|
1648
|
-
if (
|
|
1649
|
-
|
|
2023
|
+
const run2 = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
|
|
2024
|
+
s.stop(`${run2.items.length} result${run2.items.length === 1 ? "" : "s"}`);
|
|
2025
|
+
if (run2.items.length === 0) {
|
|
2026
|
+
p6.log.info("No results.");
|
|
1650
2027
|
break;
|
|
1651
2028
|
}
|
|
1652
|
-
await browseSearchResults(
|
|
2029
|
+
await browseSearchResults(run2.items);
|
|
1653
2030
|
} catch (err) {
|
|
1654
|
-
s.stop(
|
|
2031
|
+
s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
|
|
1655
2032
|
process.exitCode = 1;
|
|
1656
2033
|
}
|
|
1657
2034
|
break;
|
|
1658
2035
|
}
|
|
1659
2036
|
case "init": {
|
|
1660
|
-
const name = await
|
|
2037
|
+
const name = await p6.text({ message: "Skill name", placeholder: "my-skill" });
|
|
1661
2038
|
if (bail(name)) break;
|
|
1662
|
-
const description = await
|
|
2039
|
+
const description = await p6.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
|
|
1663
2040
|
if (bail(description)) break;
|
|
1664
2041
|
try {
|
|
1665
2042
|
const file = initSkill(String(name), { description: String(description || "") });
|
|
1666
|
-
|
|
2043
|
+
p6.log.success(`Created ${pc14.bold(file)}`);
|
|
1667
2044
|
} catch (err) {
|
|
1668
|
-
|
|
2045
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
2046
|
+
process.exitCode = 1;
|
|
2047
|
+
}
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
case "install": {
|
|
2051
|
+
const src = await p6.text({ message: "Skill source", placeholder: "owner/name, https://github.com/\u2026, or ./path" });
|
|
2052
|
+
if (bail(src)) break;
|
|
2053
|
+
try {
|
|
2054
|
+
const run2 = await runAdd(String(src), {});
|
|
2055
|
+
console.log(run2.output);
|
|
2056
|
+
process.exitCode = run2.exitCode;
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1669
2059
|
process.exitCode = 1;
|
|
1670
2060
|
}
|
|
1671
2061
|
break;
|
|
1672
2062
|
}
|
|
1673
2063
|
case "list": {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2064
|
+
console.log(runList({}).output);
|
|
2065
|
+
break;
|
|
2066
|
+
}
|
|
2067
|
+
case "update": {
|
|
2068
|
+
try {
|
|
2069
|
+
const run2 = await runUpdate([], {});
|
|
2070
|
+
console.log(run2.output);
|
|
2071
|
+
process.exitCode = run2.exitCode;
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
2074
|
+
process.exitCode = 1;
|
|
2075
|
+
}
|
|
2076
|
+
break;
|
|
2077
|
+
}
|
|
2078
|
+
case "remove": {
|
|
2079
|
+
try {
|
|
2080
|
+
const run2 = await runRemove([], {});
|
|
2081
|
+
console.log(run2.output);
|
|
2082
|
+
process.exitCode = run2.exitCode;
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
2085
|
+
process.exitCode = 1;
|
|
1686
2086
|
}
|
|
1687
2087
|
break;
|
|
1688
2088
|
}
|
|
1689
2089
|
case "help": {
|
|
1690
|
-
|
|
2090
|
+
p6.note(
|
|
1691
2091
|
[
|
|
1692
|
-
`${
|
|
1693
|
-
`${
|
|
1694
|
-
`${
|
|
1695
|
-
`${
|
|
1696
|
-
`${
|
|
1697
|
-
`${
|
|
1698
|
-
`${
|
|
1699
|
-
`${
|
|
1700
|
-
`${
|
|
2092
|
+
`${pc14.bold("lint")} [path] validate SKILL.md + quality score`,
|
|
2093
|
+
`${pc14.bold("scan")} [path] security scan`,
|
|
2094
|
+
`${pc14.bold("search")} <query> search the registry`,
|
|
2095
|
+
`${pc14.bold("add")} <source> install a skill (lints first)`,
|
|
2096
|
+
`${pc14.bold("info")} <slug> registry details for a skill`,
|
|
2097
|
+
`${pc14.bold("init")} [name] scaffold a new skill`,
|
|
2098
|
+
`${pc14.bold("list / remove / update")} manage installed skills`,
|
|
2099
|
+
`${pc14.bold("publish")} [path] publish (fails on lint errors)`,
|
|
2100
|
+
`${pc14.bold("rules / login")}`
|
|
1701
2101
|
].join("\n"),
|
|
1702
2102
|
"Commands (run: skillmd <command>)"
|
|
1703
2103
|
);
|
|
@@ -1705,16 +2105,39 @@ async function runInteractive() {
|
|
|
1705
2105
|
}
|
|
1706
2106
|
}
|
|
1707
2107
|
}
|
|
1708
|
-
|
|
2108
|
+
p6.outro(pc14.dim("skillmd.com"));
|
|
1709
2109
|
}
|
|
1710
2110
|
|
|
1711
|
-
//
|
|
2111
|
+
// src/index.ts
|
|
1712
2112
|
var version = "0.0.0";
|
|
1713
2113
|
try {
|
|
1714
|
-
version = JSON.parse(
|
|
2114
|
+
version = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1715
2115
|
} catch {
|
|
1716
2116
|
}
|
|
1717
|
-
var program = new Command13("
|
|
2117
|
+
var program = new Command13("get-skill").version(version).description("Lint, validate, and install Agent Skills from the SkillMD registry.").option("--json", "machine-readable output").option("--token <token>", "personal access token (prefer `npx get-skill login` or SKILLMD_TOKEN \u2014 command-line tokens show up in process lists)").option("--api <url>", "override the registry API base URL").option("--insecure-http", "allow an http:// --api base (local development only)");
|
|
2118
|
+
program.configureHelp({ sortSubcommands: false });
|
|
2119
|
+
program.addHelpText("after", `
|
|
2120
|
+
Examples:
|
|
2121
|
+
$ npx get-skill add anthropic/pdf install from the registry (asks project vs global)
|
|
2122
|
+
$ npx get-skill add owner/repo/skills/x#main -g a GitHub subfolder at a ref, into your user dirs
|
|
2123
|
+
$ npx get-skill add ./my-skill -a claude-code -p a local skill into this project for one agent
|
|
2124
|
+
$ npx get-skill add anthropic/pdf -y --json non-interactive, machine-readable
|
|
2125
|
+
$ npx get-skill list everything installed, project and global
|
|
2126
|
+
$ npx get-skill check which installed skills have updates
|
|
2127
|
+
$ npx get-skill update -g update your user-level skills
|
|
2128
|
+
$ npx get-skill remove pdf remove a skill from every agent
|
|
2129
|
+
|
|
2130
|
+
Quality: lint scan rules init
|
|
2131
|
+
Registry: search info add list remove update check
|
|
2132
|
+
Publish: publish login logout
|
|
2133
|
+
|
|
2134
|
+
Docs: https://skillmd.com/docs/cli`);
|
|
2135
|
+
program.hook("preAction", (thisCmd) => {
|
|
2136
|
+
const opts = thisCmd.opts();
|
|
2137
|
+
if (opts.token && process.stderr.isTTY) {
|
|
2138
|
+
console.error(pc15.yellow("\u26A0 --token is visible in your shell history and process list; prefer `npx get-skill login` or SKILLMD_TOKEN."));
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
1718
2141
|
program.addCommand(lintCommand());
|
|
1719
2142
|
program.addCommand(scanCommand());
|
|
1720
2143
|
program.addCommand(rulesCommand());
|
|
@@ -1725,6 +2148,7 @@ program.addCommand(addCommand());
|
|
|
1725
2148
|
program.addCommand(listCommand());
|
|
1726
2149
|
program.addCommand(removeCommand());
|
|
1727
2150
|
program.addCommand(updateCommand());
|
|
2151
|
+
program.addCommand(checkCommand());
|
|
1728
2152
|
program.addCommand(publishCommand());
|
|
1729
2153
|
program.addCommand(loginCommand());
|
|
1730
2154
|
program.addCommand(logoutCommand());
|