skillmds 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/bin/skillmd-mcp.mjs +35 -1
- package/bin/skillmd.mjs +3 -0
- package/dist/cli.js +933 -0
- package/package.json +38 -6
- package/vendor/index.js +7656 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
// ../cli/src/index.ts
|
|
2
|
+
import { Command as Command12 } from "commander";
|
|
3
|
+
|
|
4
|
+
// ../cli/src/commands/lint.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { writeFileSync } from "fs";
|
|
7
|
+
|
|
8
|
+
// ../../packages/core/src/parse.ts
|
|
9
|
+
import { parse as parseYaml } from "yaml";
|
|
10
|
+
var MAX_SKILL_BYTES = 256 * 1024;
|
|
11
|
+
var MAX_DESCRIPTION = 500;
|
|
12
|
+
var MAX_NAME = 120;
|
|
13
|
+
var slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "skill";
|
|
14
|
+
function parseSkillMd(raw, type = "single") {
|
|
15
|
+
if (!raw || raw.length > MAX_SKILL_BYTES) return { error: "SKILL.md missing or too large (max 256KB)" };
|
|
16
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
17
|
+
if (!m) return { error: "Missing YAML frontmatter (--- ... ---)" };
|
|
18
|
+
let fm;
|
|
19
|
+
try {
|
|
20
|
+
fm = parseYaml(m[1] ?? "") ?? {};
|
|
21
|
+
} catch {
|
|
22
|
+
return { error: "Invalid YAML frontmatter" };
|
|
23
|
+
}
|
|
24
|
+
const name = String(fm.name ?? "").trim();
|
|
25
|
+
const description = String(fm.description ?? "").replace(/\s+/g, " ").trim();
|
|
26
|
+
if (!name) return { error: "frontmatter 'name' is required" };
|
|
27
|
+
if (!description) return { error: "frontmatter 'description' is required" };
|
|
28
|
+
if (description.length > MAX_DESCRIPTION) return { error: "description must be <= 500 chars" };
|
|
29
|
+
return {
|
|
30
|
+
name: name.slice(0, MAX_NAME),
|
|
31
|
+
description: description.slice(0, MAX_DESCRIPTION),
|
|
32
|
+
license: fm.license ? String(fm.license).slice(0, 64) : null,
|
|
33
|
+
body: m[2] ?? "",
|
|
34
|
+
type
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ../../packages/core/src/security.ts
|
|
39
|
+
var RULES = [
|
|
40
|
+
["executes_scripts", /```(?:sh|bash|python|py|js|ts|ruby|rb)\b|\b(subprocess|os\.system|child_process|exec\()/i],
|
|
41
|
+
["network_calls", /\b(fetch|requests\.|urllib|axios|curl|wget|https?:\/\/|websocket)\b/i],
|
|
42
|
+
["reads_secrets", /\b(api[_-]?key|secret|token|password|os\.environ|process\.env|getenv)\b/i]
|
|
43
|
+
];
|
|
44
|
+
function scanSecurity(body) {
|
|
45
|
+
const findings = [];
|
|
46
|
+
body.split(/\r?\n/).forEach((text2, i) => {
|
|
47
|
+
for (const [flag, re] of RULES) {
|
|
48
|
+
if (re.test(text2)) findings.push({ flag, line: i + 1, snippet: text2.trim().slice(0, 120) });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const flags = [...new Set(findings.map((f) => f.flag))];
|
|
52
|
+
return { flags: flags.length ? flags : ["docs_only"], findings };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ../../packages/core/src/rules.ts
|
|
56
|
+
function utf8Bytes(s) {
|
|
57
|
+
let bytes = 0;
|
|
58
|
+
for (let i = 0; i < s.length; i++) {
|
|
59
|
+
const c = s.charCodeAt(i);
|
|
60
|
+
if (c < 128) bytes += 1;
|
|
61
|
+
else if (c < 2048) bytes += 2;
|
|
62
|
+
else if (c >= 55296 && c <= 56319) {
|
|
63
|
+
bytes += 4;
|
|
64
|
+
i++;
|
|
65
|
+
} else bytes += 3;
|
|
66
|
+
}
|
|
67
|
+
return bytes;
|
|
68
|
+
}
|
|
69
|
+
var diag = (rule, message, line) => ({
|
|
70
|
+
id: rule.id,
|
|
71
|
+
severity: rule.severity,
|
|
72
|
+
message,
|
|
73
|
+
...line !== void 0 ? { line } : {}
|
|
74
|
+
});
|
|
75
|
+
var RULES2 = [
|
|
76
|
+
{
|
|
77
|
+
id: "SK002",
|
|
78
|
+
severity: "error",
|
|
79
|
+
description: "name must be present and \u2264 120 chars",
|
|
80
|
+
check(s, _ctx) {
|
|
81
|
+
const out = [];
|
|
82
|
+
if (!s.name.trim()) out.push(diag(this, "frontmatter 'name' is required"));
|
|
83
|
+
else if (s.name.length > MAX_NAME) out.push(diag(this, `name must be \u2264 ${MAX_NAME} chars`));
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "SK003",
|
|
89
|
+
severity: "error",
|
|
90
|
+
description: "description must be present and \u2264 500 chars",
|
|
91
|
+
check(s, _ctx) {
|
|
92
|
+
const out = [];
|
|
93
|
+
if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));
|
|
94
|
+
else if (s.description.length > MAX_DESCRIPTION) out.push(diag(this, `description must be \u2264 ${MAX_DESCRIPTION} chars`));
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "SK010",
|
|
100
|
+
severity: "warn",
|
|
101
|
+
description: "description should be \u2265 20 chars (not too terse)",
|
|
102
|
+
check(s, _ctx) {
|
|
103
|
+
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")] : [];
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "SK011",
|
|
108
|
+
severity: "warn",
|
|
109
|
+
description: "license should be declared in frontmatter",
|
|
110
|
+
check(s, _ctx) {
|
|
111
|
+
return s.license ? [] : [diag(this, "no 'license' declared in frontmatter")];
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "SK020",
|
|
116
|
+
severity: "warn",
|
|
117
|
+
description: "body should be \u2265 200 chars (not a stub)",
|
|
118
|
+
check(s, _ctx) {
|
|
119
|
+
return s.body.trim().length < 200 ? [diag(this, "body is very short (< 200 chars); this looks like a stub")] : [];
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "SK021",
|
|
124
|
+
severity: "warn",
|
|
125
|
+
description: "body should contain at least one Markdown heading",
|
|
126
|
+
check(s, _ctx) {
|
|
127
|
+
return /^#{1,6}\s/m.test(s.body) ? [] : [diag(this, "body has no Markdown headings; add structure with '#' headings")];
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "SK030",
|
|
132
|
+
severity: "error",
|
|
133
|
+
description: "total SKILL.md size must be \u2264 256KB",
|
|
134
|
+
check(s, _ctx) {
|
|
135
|
+
return utf8Bytes(s.body) > MAX_SKILL_BYTES ? [diag(this, "SKILL.md exceeds 256KB")] : [];
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "SK040",
|
|
140
|
+
severity: "warn",
|
|
141
|
+
description: "name should match its directory/slug",
|
|
142
|
+
check(s, ctx) {
|
|
143
|
+
if (!ctx.slug) return [];
|
|
144
|
+
const expected = slugify(s.name);
|
|
145
|
+
return expected === ctx.slug ? [] : [diag(this, `name slug '${expected}' does not match directory '${ctx.slug}'`)];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
function runRules(skill, ctx = {}) {
|
|
150
|
+
return RULES2.flatMap((r) => r.check(skill, ctx));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ../../packages/core/src/quality.ts
|
|
154
|
+
var WEIGHT = {
|
|
155
|
+
error: 25,
|
|
156
|
+
warn: 8,
|
|
157
|
+
info: 2
|
|
158
|
+
};
|
|
159
|
+
function qualityScore(diagnostics) {
|
|
160
|
+
const penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
161
|
+
return Math.max(0, Math.min(100, 100 - penalty));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ../../packages/core/src/lint.ts
|
|
165
|
+
function lint(raw, opts = {}) {
|
|
166
|
+
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
167
|
+
if ("error" in parsed) {
|
|
168
|
+
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
diagnostics: diagnostics2,
|
|
172
|
+
security: scanSecurity(raw),
|
|
173
|
+
score: qualityScore(diagnostics2)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
177
|
+
const diagnostics = runRules(parsed, ctx);
|
|
178
|
+
const security = scanSecurity(parsed.body);
|
|
179
|
+
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
180
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ../../packages/core/src/format/text.ts
|
|
184
|
+
var BADGE = { error: "ERROR", warn: " WARN", info: " INFO" };
|
|
185
|
+
function toText(results, opts = {}) {
|
|
186
|
+
const paint = opts.decorate ?? ((t) => t);
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const { file, result } of results) {
|
|
189
|
+
const status = result.ok ? paint("PASS", "ok") : paint("FAIL", "error");
|
|
190
|
+
out.push(`${status} ${file} ${paint(`score ${result.score}/100`, "dim")}`);
|
|
191
|
+
for (const d of result.diagnostics) {
|
|
192
|
+
const where = d.line !== void 0 ? `:${d.line}` : "";
|
|
193
|
+
out.push(` ${paint(BADGE[d.severity], d.severity)} ${d.id}${where} ${d.message}`);
|
|
194
|
+
}
|
|
195
|
+
if (result.security.flags.length && result.security.flags[0] !== "docs_only") {
|
|
196
|
+
out.push(` ${paint("SEC ", "warn")} flags: ${result.security.flags.join(", ")}`);
|
|
197
|
+
}
|
|
198
|
+
out.push("");
|
|
199
|
+
}
|
|
200
|
+
const failed = results.filter((r) => !r.result.ok).length;
|
|
201
|
+
const summary = failed === 0 ? paint(`\u2713 ${results.length} skill(s) passed`, "ok") : paint(`\u2717 ${failed}/${results.length} skill(s) failed`, "error");
|
|
202
|
+
out.push(summary);
|
|
203
|
+
return out.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ../../packages/core/src/format/json.ts
|
|
207
|
+
function toJson(results) {
|
|
208
|
+
return results.map(({ file, result }) => ({
|
|
209
|
+
file,
|
|
210
|
+
ok: result.ok,
|
|
211
|
+
score: result.score,
|
|
212
|
+
diagnostics: result.diagnostics,
|
|
213
|
+
security: result.security
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ../../packages/core/src/format/sarif.ts
|
|
218
|
+
var SARIF_LEVEL = { error: "error", warn: "warning", info: "note" };
|
|
219
|
+
function toSarif(results) {
|
|
220
|
+
const sarifResults = results.flatMap(
|
|
221
|
+
({ file, result }) => result.diagnostics.map((d) => ({
|
|
222
|
+
ruleId: d.id,
|
|
223
|
+
level: SARIF_LEVEL[d.severity] ?? "note",
|
|
224
|
+
message: { text: d.message },
|
|
225
|
+
locations: [
|
|
226
|
+
{
|
|
227
|
+
physicalLocation: {
|
|
228
|
+
artifactLocation: { uri: file },
|
|
229
|
+
region: { startLine: d.line ?? 1 }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
}))
|
|
234
|
+
);
|
|
235
|
+
return {
|
|
236
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
237
|
+
version: "2.1.0",
|
|
238
|
+
runs: [
|
|
239
|
+
{
|
|
240
|
+
tool: {
|
|
241
|
+
driver: {
|
|
242
|
+
name: "skillmd",
|
|
243
|
+
informationUri: "https://skillmd.com",
|
|
244
|
+
rules: RULES2.map((r) => ({
|
|
245
|
+
id: r.id,
|
|
246
|
+
shortDescription: { text: r.description },
|
|
247
|
+
defaultConfiguration: { level: SARIF_LEVEL[r.severity] ?? "note" }
|
|
248
|
+
}))
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
results: sarifResults
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ../../packages/core/src/format/github.ts
|
|
258
|
+
var LEVEL = { error: "error", warn: "warning", info: "notice" };
|
|
259
|
+
function toGithub(results) {
|
|
260
|
+
const lines = [];
|
|
261
|
+
for (const { file, result } of results) {
|
|
262
|
+
for (const d of result.diagnostics) {
|
|
263
|
+
const level = LEVEL[d.severity] ?? "notice";
|
|
264
|
+
const loc = d.line !== void 0 ? `,line=${d.line}` : "";
|
|
265
|
+
lines.push(`::${level} file=${file}${loc},title=${d.id}::${d.message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return lines.join("\n");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ../cli/src/source.ts
|
|
272
|
+
import { existsSync, statSync, readFileSync, readdirSync, mkdtempSync } from "fs";
|
|
273
|
+
import { tmpdir } from "os";
|
|
274
|
+
import { join, basename, dirname } from "path";
|
|
275
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".wrangler"]);
|
|
276
|
+
function findSkillFiles(dir) {
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
279
|
+
if (entry.isDirectory()) {
|
|
280
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
281
|
+
out.push(...findSkillFiles(join(dir, entry.name)));
|
|
282
|
+
} else if (entry.name === "SKILL.md") {
|
|
283
|
+
out.push(join(dir, entry.name));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
function loadLocal(file) {
|
|
289
|
+
return { path: file, raw: readFileSync(file, "utf8"), slug: basename(dirname(file)), file };
|
|
290
|
+
}
|
|
291
|
+
function isLikelyLocal(arg) {
|
|
292
|
+
return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync(arg);
|
|
293
|
+
}
|
|
294
|
+
async function resolveSource(arg) {
|
|
295
|
+
if (isLikelyLocal(arg)) {
|
|
296
|
+
if (!existsSync(arg)) throw new Error(`Path not found: ${arg}`);
|
|
297
|
+
const st = statSync(arg);
|
|
298
|
+
if (st.isFile()) return [loadLocal(arg)];
|
|
299
|
+
const files2 = findSkillFiles(arg);
|
|
300
|
+
if (files2.length === 0) throw new Error(`No SKILL.md found under ${arg}`);
|
|
301
|
+
return files2.map(loadLocal);
|
|
302
|
+
}
|
|
303
|
+
const { downloadTemplate } = await import("giget");
|
|
304
|
+
const input = normalizeGiget(arg);
|
|
305
|
+
const dir = mkdtempSync(join(tmpdir(), "skillmd-src-"));
|
|
306
|
+
const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
|
|
307
|
+
const files = findSkillFiles(out);
|
|
308
|
+
if (files.length === 0) throw new Error(`No SKILL.md found in ${arg}`);
|
|
309
|
+
return files.map(loadLocal);
|
|
310
|
+
}
|
|
311
|
+
function normalizeGiget(arg) {
|
|
312
|
+
const m = arg.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/[^/]+\/(.+))?/);
|
|
313
|
+
if (m) return `github:${m[1]}/${m[2]}${m[3] ? "/" + m[3] : ""}`;
|
|
314
|
+
if (arg.startsWith("github:") || arg.startsWith("gh:")) return arg;
|
|
315
|
+
return `github:${arg}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ../cli/src/ui.ts
|
|
319
|
+
import pc from "picocolors";
|
|
320
|
+
var decorate = (text2, kind) => {
|
|
321
|
+
switch (kind) {
|
|
322
|
+
case "error":
|
|
323
|
+
return pc.red(text2);
|
|
324
|
+
case "warn":
|
|
325
|
+
return pc.yellow(text2);
|
|
326
|
+
case "info":
|
|
327
|
+
return pc.cyan(text2);
|
|
328
|
+
case "ok":
|
|
329
|
+
return pc.green(text2);
|
|
330
|
+
case "dim":
|
|
331
|
+
return pc.dim(text2);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
function exitCodeFor(results, policy = {}) {
|
|
335
|
+
for (const { result } of results) {
|
|
336
|
+
const hasError = result.diagnostics.some((d) => d.severity === "error");
|
|
337
|
+
const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
|
|
338
|
+
if (hasError) return 1;
|
|
339
|
+
if ((policy.strict || policy.failOnWarning) && hasWarn) return 1;
|
|
340
|
+
}
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ../cli/src/commands/lint.ts
|
|
345
|
+
function applyLicenseFix(raw) {
|
|
346
|
+
if (/^---[\s\S]*?\blicense:\s*\S+[\s\S]*?^---/m.test(raw)) return null;
|
|
347
|
+
const m = raw.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
|
|
348
|
+
if (!m) return null;
|
|
349
|
+
return `${m[1]}${m[2]}
|
|
350
|
+
license: UNKNOWN${m[3]}${raw.slice(m[0].length)}`;
|
|
351
|
+
}
|
|
352
|
+
async function runLint(target, flags) {
|
|
353
|
+
let resolved = await resolveSource(target);
|
|
354
|
+
if (flags.fix) {
|
|
355
|
+
resolved = resolved.map((r) => {
|
|
356
|
+
if (!r.file) return r;
|
|
357
|
+
const fixed = applyLicenseFix(r.raw);
|
|
358
|
+
if (fixed) {
|
|
359
|
+
writeFileSync(r.file, fixed, "utf8");
|
|
360
|
+
return { ...r, raw: fixed };
|
|
361
|
+
}
|
|
362
|
+
return r;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const results = resolved.map((r) => ({ file: r.path, result: lint(r.raw, { slug: r.slug }), resolved: r }));
|
|
366
|
+
const exitCode = exitCodeFor(results, flags);
|
|
367
|
+
let output;
|
|
368
|
+
switch (flags.format) {
|
|
369
|
+
case "json":
|
|
370
|
+
output = JSON.stringify(toJson(results), null, 2);
|
|
371
|
+
break;
|
|
372
|
+
case "sarif":
|
|
373
|
+
output = JSON.stringify(toSarif(results), null, 2);
|
|
374
|
+
break;
|
|
375
|
+
case "github":
|
|
376
|
+
output = toGithub(results);
|
|
377
|
+
break;
|
|
378
|
+
default:
|
|
379
|
+
output = toText(results, { decorate });
|
|
380
|
+
}
|
|
381
|
+
return { results, output, exitCode };
|
|
382
|
+
}
|
|
383
|
+
function lintCommand() {
|
|
384
|
+
return new Command("lint").alias("check").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)").action(async (target, opts) => {
|
|
385
|
+
const run = await runLint(target, opts);
|
|
386
|
+
console.log(run.output);
|
|
387
|
+
process.exitCode = run.exitCode;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ../cli/src/commands/scan.ts
|
|
392
|
+
import { Command as Command2 } from "commander";
|
|
393
|
+
import pc2 from "picocolors";
|
|
394
|
+
async function runScan(target, flags) {
|
|
395
|
+
const resolved = await resolveSource(target);
|
|
396
|
+
const deny = new Set(flags.deny ?? []);
|
|
397
|
+
const entries = resolved.map((r) => {
|
|
398
|
+
const parsed = parseSkillMd(r.raw, "single");
|
|
399
|
+
const body = "error" in parsed ? r.raw : parsed.body;
|
|
400
|
+
const security = scanSecurity(body);
|
|
401
|
+
const denied = security.flags.filter((f) => deny.has(f));
|
|
402
|
+
return { file: r.path, security, denied };
|
|
403
|
+
});
|
|
404
|
+
const exitCode = entries.some((e) => e.denied.length > 0) ? 1 : 0;
|
|
405
|
+
let output;
|
|
406
|
+
if (flags.format === "json") {
|
|
407
|
+
output = JSON.stringify(entries, null, 2);
|
|
408
|
+
} else {
|
|
409
|
+
const lines = [];
|
|
410
|
+
for (const e of entries) {
|
|
411
|
+
const flagStr = e.security.flags.join(", ");
|
|
412
|
+
const head = e.denied.length ? pc2.red(`DENY ${e.file}`) : pc2.green(`OK ${e.file}`);
|
|
413
|
+
lines.push(`${head} ${pc2.dim(flagStr)}`);
|
|
414
|
+
for (const f of e.security.findings) {
|
|
415
|
+
const mark = deny.has(f.flag) ? pc2.red(f.flag) : pc2.yellow(f.flag);
|
|
416
|
+
lines.push(` ${mark} :${f.line} ${pc2.dim(f.snippet)}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
output = lines.join("\n");
|
|
420
|
+
}
|
|
421
|
+
return { entries, exitCode, output };
|
|
422
|
+
}
|
|
423
|
+
function scanCommand() {
|
|
424
|
+
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) => {
|
|
425
|
+
const run = await runScan(target, opts);
|
|
426
|
+
console.log(run.output);
|
|
427
|
+
process.exitCode = run.exitCode;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ../cli/src/commands/rules.ts
|
|
432
|
+
import { Command as Command3 } from "commander";
|
|
433
|
+
import pc3 from "picocolors";
|
|
434
|
+
function rulesCommand() {
|
|
435
|
+
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) => {
|
|
436
|
+
const list = id ? RULES2.filter((r) => r.id.toLowerCase() === id.toLowerCase()) : RULES2;
|
|
437
|
+
if (id && list.length === 0) {
|
|
438
|
+
console.error(`Unknown rule: ${id}`);
|
|
439
|
+
process.exitCode = 1;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (opts.json) {
|
|
443
|
+
console.log(JSON.stringify(list.map((r) => ({ id: r.id, severity: r.severity, description: r.description })), null, 2));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const color = (sev) => (sev === "error" ? pc3.red : sev === "warn" ? pc3.yellow : pc3.cyan)(sev.padEnd(5));
|
|
447
|
+
for (const r of list) console.log(`${pc3.bold(r.id)} ${color(r.severity)} ${r.description}`);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ../cli/src/commands/init.ts
|
|
452
|
+
import { Command as Command4 } from "commander";
|
|
453
|
+
import { mkdirSync, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
454
|
+
import { join as join2 } from "path";
|
|
455
|
+
import * as p from "@clack/prompts";
|
|
456
|
+
function skillTemplate(name, description, license) {
|
|
457
|
+
const desc = description.trim() || `A skill that helps agents with ${name}.`;
|
|
458
|
+
return `---
|
|
459
|
+
name: ${name}
|
|
460
|
+
description: ${desc}
|
|
461
|
+
license: ${license}
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
# ${name}
|
|
465
|
+
|
|
466
|
+
## When to use this skill
|
|
467
|
+
|
|
468
|
+
Describe the situations where an agent should reach for this skill. Be specific
|
|
469
|
+
about the task, the inputs it expects, and the outcome it produces.
|
|
470
|
+
|
|
471
|
+
## Instructions
|
|
472
|
+
|
|
473
|
+
1. First step the agent should take.
|
|
474
|
+
2. Second step, with any commands or file paths it needs.
|
|
475
|
+
3. How to verify the result.
|
|
476
|
+
|
|
477
|
+
## Notes
|
|
478
|
+
|
|
479
|
+
Add any constraints, edge cases, or references the agent should keep in mind.
|
|
480
|
+
`;
|
|
481
|
+
}
|
|
482
|
+
function initSkill(name, opts = {}) {
|
|
483
|
+
const slug = slugify(name);
|
|
484
|
+
const root = join2(opts.dir ?? process.cwd(), slug);
|
|
485
|
+
if (existsSync2(join2(root, "SKILL.md"))) throw new Error(`SKILL.md already exists at ${root}`);
|
|
486
|
+
mkdirSync(root, { recursive: true });
|
|
487
|
+
const file = join2(root, "SKILL.md");
|
|
488
|
+
writeFileSync2(file, skillTemplate(name, opts.description ?? "", opts.license ?? "MIT"), "utf8");
|
|
489
|
+
return file;
|
|
490
|
+
}
|
|
491
|
+
function initCommand() {
|
|
492
|
+
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("-y, --yes", "skip prompts").action(async (nameArg, opts) => {
|
|
493
|
+
let name = nameArg ?? opts.name;
|
|
494
|
+
if (!name && !opts.yes) {
|
|
495
|
+
const answer = await p.text({ message: "Skill name?", placeholder: "my-skill" });
|
|
496
|
+
if (p.isCancel(answer)) {
|
|
497
|
+
p.cancel("Cancelled.");
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
name = answer;
|
|
501
|
+
}
|
|
502
|
+
if (!name) {
|
|
503
|
+
console.error("A skill name is required (pass a name or --name).");
|
|
504
|
+
process.exitCode = 1;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const file = initSkill(name, { description: opts.description, license: opts.license });
|
|
508
|
+
console.log(`Created ${file}`);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ../cli/src/commands/search.ts
|
|
513
|
+
import { Command as Command5 } from "commander";
|
|
514
|
+
import pc4 from "picocolors";
|
|
515
|
+
|
|
516
|
+
// ../cli/src/config.ts
|
|
517
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
518
|
+
import { homedir } from "os";
|
|
519
|
+
import { join as join3 } from "path";
|
|
520
|
+
var DEFAULT_API = "https://skillmd-api.vinoor.workers.dev";
|
|
521
|
+
function configPath() {
|
|
522
|
+
return join3(homedir(), ".skillmd", "config.json");
|
|
523
|
+
}
|
|
524
|
+
function readConfig() {
|
|
525
|
+
try {
|
|
526
|
+
return JSON.parse(readFileSync2(configPath(), "utf8"));
|
|
527
|
+
} catch {
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function writeConfig(cfg) {
|
|
532
|
+
const dir = join3(homedir(), ".skillmd");
|
|
533
|
+
mkdirSync2(dir, { recursive: true });
|
|
534
|
+
writeFileSync3(configPath(), JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
|
|
535
|
+
}
|
|
536
|
+
function resolveToken(flags = {}) {
|
|
537
|
+
return flags.token || process.env.SKILLMD_TOKEN || readConfig().token || void 0;
|
|
538
|
+
}
|
|
539
|
+
function resolveApi(flags = {}) {
|
|
540
|
+
return flags.api || process.env.SKILLMD_API || readConfig().api || DEFAULT_API;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ../cli/src/api.ts
|
|
544
|
+
function createClient(flags = {}) {
|
|
545
|
+
const base = resolveApi(flags);
|
|
546
|
+
const token = resolveToken(flags);
|
|
547
|
+
async function api(path, init = {}) {
|
|
548
|
+
const headers = { accept: "application/json", ...init.headers };
|
|
549
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
550
|
+
const res = await fetch(base + path, { ...init, headers });
|
|
551
|
+
if (!res.ok) {
|
|
552
|
+
const detail = await res.text().catch(() => "");
|
|
553
|
+
throw new Error(`SkillMD API ${res.status} on ${path}: ${detail}`);
|
|
554
|
+
}
|
|
555
|
+
return await res.json();
|
|
556
|
+
}
|
|
557
|
+
return { api, base, hasToken: Boolean(token) };
|
|
558
|
+
}
|
|
559
|
+
function reconstructSkillMd(skill) {
|
|
560
|
+
const name = skill.slug.split("/")[1] || skill.title;
|
|
561
|
+
const fm = ["---", `name: ${name}`, `description: ${String(skill.description).replace(/\n/g, " ")}`];
|
|
562
|
+
if (skill.license) fm.push(`license: ${skill.license}`);
|
|
563
|
+
fm.push("---", "");
|
|
564
|
+
return fm.join("\n") + (skill.body_md || "");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ../cli/src/commands/search.ts
|
|
568
|
+
async function runSearch(query, flags) {
|
|
569
|
+
const { api } = createClient(flags);
|
|
570
|
+
const params = new URLSearchParams({ q: query, limit: String(flags.limit ?? 20) });
|
|
571
|
+
if (flags.category) params.set("category", flags.category);
|
|
572
|
+
if (flags.verified) params.set("verified", "true");
|
|
573
|
+
if (flags.type) params.set("type", flags.type);
|
|
574
|
+
if (flags.minRating != null) params.set("min_rating", String(flags.minRating));
|
|
575
|
+
const r = await api(`/api/search?${params}`);
|
|
576
|
+
const items = r.items ?? [];
|
|
577
|
+
if (flags.json) return { items, output: JSON.stringify(items, null, 2) };
|
|
578
|
+
const lines = items.map((s) => {
|
|
579
|
+
const check = s.verified ? pc4.green("\u2713") : " ";
|
|
580
|
+
const rating = s.avg_rating ? pc4.dim(`\u2605${s.avg_rating.toFixed(1)} (${s.rating_count ?? 0})`) : "";
|
|
581
|
+
return `${check} ${pc4.bold(s.slug)} ${rating}
|
|
582
|
+
${pc4.dim(s.description)}`;
|
|
583
|
+
});
|
|
584
|
+
return { items, output: lines.join("\n") || "No skills found." };
|
|
585
|
+
}
|
|
586
|
+
function searchCommand() {
|
|
587
|
+
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) => {
|
|
588
|
+
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
589
|
+
const run = await runSearch(query, merged);
|
|
590
|
+
console.log(run.output);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ../cli/src/commands/add.ts
|
|
595
|
+
import { Command as Command6 } from "commander";
|
|
596
|
+
import { existsSync as existsSync4 } from "fs";
|
|
597
|
+
import pc5 from "picocolors";
|
|
598
|
+
|
|
599
|
+
// ../cli/src/agents.ts
|
|
600
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, symlinkSync, existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
|
|
601
|
+
import { homedir as homedir2 } from "os";
|
|
602
|
+
import { join as join4, resolve, dirname as dirname2 } from "path";
|
|
603
|
+
var AGENTS = [
|
|
604
|
+
{ id: "claude-code", segments: [".claude", "skills"] },
|
|
605
|
+
{ id: "cursor", segments: [".cursor", "skills"] },
|
|
606
|
+
{ id: "codex", segments: [".codex", "skills"] },
|
|
607
|
+
{ id: "windsurf", segments: [".windsurf", "skills"] },
|
|
608
|
+
{ id: "opencode", segments: [".opencode", "skills"] }
|
|
609
|
+
];
|
|
610
|
+
function agentDir(agentId, opts = {}) {
|
|
611
|
+
const agent = AGENTS.find((a) => a.id === agentId);
|
|
612
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}. Known: ${AGENTS.map((a) => a.id).join(", ")}`);
|
|
613
|
+
const root = opts.global ? homedir2() : opts.cwd ?? process.cwd();
|
|
614
|
+
return join4(root, ...agent.segments);
|
|
615
|
+
}
|
|
616
|
+
function detectAgents(opts = {}) {
|
|
617
|
+
return AGENTS.filter((a) => existsSync3(agentDir(a.id, opts))).map((a) => a.id);
|
|
618
|
+
}
|
|
619
|
+
function writeSkill(skillName, files, targetDir, opts = {}) {
|
|
620
|
+
const dest = join4(targetDir, skillName);
|
|
621
|
+
if (opts.mode === "symlink" && opts.canonicalDir) {
|
|
622
|
+
mkdirSync3(dirname2(dest), { recursive: true });
|
|
623
|
+
rmSync(dest, { recursive: true, force: true });
|
|
624
|
+
symlinkSync(resolve(opts.canonicalDir), dest, "dir");
|
|
625
|
+
return dest;
|
|
626
|
+
}
|
|
627
|
+
mkdirSync3(dest, { recursive: true });
|
|
628
|
+
for (const f of files) {
|
|
629
|
+
const fp = join4(dest, f.path);
|
|
630
|
+
mkdirSync3(dirname2(fp), { recursive: true });
|
|
631
|
+
writeFileSync4(fp, f.contents, "utf8");
|
|
632
|
+
}
|
|
633
|
+
return dest;
|
|
634
|
+
}
|
|
635
|
+
function installedSkills(opts = {}) {
|
|
636
|
+
const scope = opts.global ? "global" : "project";
|
|
637
|
+
const out = [];
|
|
638
|
+
for (const agent of AGENTS) {
|
|
639
|
+
const dir = agentDir(agent.id, opts);
|
|
640
|
+
if (!existsSync3(dir)) continue;
|
|
641
|
+
for (const entry of readdirSync2(dir)) {
|
|
642
|
+
const skillMd = join4(dir, entry, "SKILL.md");
|
|
643
|
+
if (!existsSync3(skillMd) || !statSync2(skillMd).isFile()) continue;
|
|
644
|
+
out.push({ name: entry, agent: agent.id, scope, path: join4(dir, entry), raw: readFileSync3(skillMd, "utf8") });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return out;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ../cli/src/commands/add.ts
|
|
651
|
+
function isLocalish(arg) {
|
|
652
|
+
return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync4(arg);
|
|
653
|
+
}
|
|
654
|
+
var defaultDeps = {
|
|
655
|
+
async resolve(arg, flags) {
|
|
656
|
+
if (isLocalish(arg)) {
|
|
657
|
+
const rs2 = await resolveSource(arg);
|
|
658
|
+
return rs2.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
|
|
659
|
+
}
|
|
660
|
+
if (/^[^/\s:]+\/[^/\s:]+$/.test(arg)) {
|
|
661
|
+
const { api } = createClient(flags);
|
|
662
|
+
const [owner, name] = arg.split("/");
|
|
663
|
+
try {
|
|
664
|
+
const skill = await api(`/api/skills/${owner}/${name}`);
|
|
665
|
+
const skillName = skill.slug.split("/")[1] || name;
|
|
666
|
+
return [{ name: skillName, raw: reconstructSkillMd(skill), slug: skillName, registrySlug: arg, verified: skill.verified }];
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const rs = await resolveSource(arg);
|
|
671
|
+
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
|
|
672
|
+
},
|
|
673
|
+
fireInstall(registrySlug, flags) {
|
|
674
|
+
const { api } = createClient(flags);
|
|
675
|
+
const [owner, name] = registrySlug.split("/");
|
|
676
|
+
void api(`/api/skills/${owner}/${name}/install`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: { "content-type": "application/json" },
|
|
679
|
+
body: JSON.stringify({ via: "cli" })
|
|
680
|
+
}).catch(() => {
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
async function runAdd(arg, flags, deps = defaultDeps) {
|
|
685
|
+
let candidates = await deps.resolve(arg, flags);
|
|
686
|
+
if (flags.skill?.length) candidates = candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
|
|
687
|
+
const deny = new Set(flags.deny ?? []);
|
|
688
|
+
const written = [];
|
|
689
|
+
const blocked = [];
|
|
690
|
+
const lines = [];
|
|
691
|
+
const scope = { global: flags.global, cwd: flags.cwd };
|
|
692
|
+
const targets = flags.agent?.length ? flags.agent : detectAgents(scope).length ? detectAgents(scope) : ["claude-code"];
|
|
693
|
+
for (const c of candidates) {
|
|
694
|
+
const result = lint(c.raw, { slug: c.slug });
|
|
695
|
+
const deniedFlags = result.security.flags.filter((f) => deny.has(f));
|
|
696
|
+
if (!flags.skipLint && !result.ok) {
|
|
697
|
+
blocked.push({ name: c.name, reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}` });
|
|
698
|
+
lines.push(pc5.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (deniedFlags.length) {
|
|
702
|
+
blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
|
|
703
|
+
lines.push(pc5.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (c.registrySlug && c.verified === false && !flags.allowUnverified) {
|
|
707
|
+
blocked.push({ name: c.name, reason: "unverified registry skill (use --allow-unverified)" });
|
|
708
|
+
lines.push(pc5.yellow(`\u2717 ${c.name} blocked \u2014 unverified (use --allow-unverified)`));
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
for (const agentId of targets) {
|
|
712
|
+
const dir = agentDir(agentId, scope);
|
|
713
|
+
const out = writeSkill(c.name, [{ path: "SKILL.md", contents: c.raw }], dir, { mode: "copy" });
|
|
714
|
+
written.push({ name: c.name, dir: out });
|
|
715
|
+
lines.push(pc5.green(`\u2713 ${c.name} \u2192 ${out}`));
|
|
716
|
+
}
|
|
717
|
+
if (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
|
|
718
|
+
}
|
|
719
|
+
const exitCode = written.length === 0 ? 1 : 0;
|
|
720
|
+
return { written, blocked, exitCode, output: lines.join("\n") };
|
|
721
|
+
}
|
|
722
|
+
function addCommand() {
|
|
723
|
+
return new Command6("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 the user directory instead of the project").option("-a, --agent <agents...>", "target specific agents").option("-s, --skill <names...>", "install only specific skills by name ('*' for all)").option("--copy", "copy files (default)", true).option("-y, --yes", "skip confirmation prompts").option("--allow-unverified", "allow installing unverified registry skills").option("--skip-lint", "install even if the skill fails lint").option("--deny <flags...>", "refuse skills with these security flags").action(async (source, opts, cmd) => {
|
|
724
|
+
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
725
|
+
const run = await runAdd(source, merged);
|
|
726
|
+
console.log(run.output);
|
|
727
|
+
process.exitCode = run.exitCode;
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ../cli/src/commands/list.ts
|
|
732
|
+
import { Command as Command7 } from "commander";
|
|
733
|
+
import pc6 from "picocolors";
|
|
734
|
+
function listCommand() {
|
|
735
|
+
return new Command7("list").alias("ls").description("List installed skills across detected agents").option("-g, --global", "only global skills").option("--json", "machine-readable output").action((opts) => {
|
|
736
|
+
const scope = { global: opts.global };
|
|
737
|
+
const skills = installedSkills(scope);
|
|
738
|
+
if (opts.json) {
|
|
739
|
+
console.log(JSON.stringify(skills.map((s) => ({ name: s.name, agent: s.agent, scope: s.scope, path: s.path })), null, 2));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (skills.length === 0) {
|
|
743
|
+
console.log("No installed skills found.");
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
for (const s of skills) {
|
|
747
|
+
const parsed = parseSkillMd(s.raw);
|
|
748
|
+
const desc = "error" in parsed ? pc6.red("(invalid SKILL.md)") : pc6.dim(parsed.description.slice(0, 80));
|
|
749
|
+
console.log(`${pc6.bold(s.name)} ${pc6.dim(`[${s.agent}/${s.scope}]`)}
|
|
750
|
+
${desc}`);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ../cli/src/commands/remove.ts
|
|
756
|
+
import { Command as Command8 } from "commander";
|
|
757
|
+
import { rmSync as rmSync2 } from "fs";
|
|
758
|
+
import pc7 from "picocolors";
|
|
759
|
+
function runRemove(names, scope = {}) {
|
|
760
|
+
const installed = installedSkills(scope);
|
|
761
|
+
const targets = installed.filter((s) => names.includes(s.name));
|
|
762
|
+
const removed = [];
|
|
763
|
+
const lines = [];
|
|
764
|
+
for (const t of targets) {
|
|
765
|
+
rmSync2(t.path, { recursive: true, force: true });
|
|
766
|
+
removed.push(t.path);
|
|
767
|
+
lines.push(pc7.green(`\u2713 removed ${t.name} (${t.agent})`));
|
|
768
|
+
}
|
|
769
|
+
for (const n of names) {
|
|
770
|
+
if (!targets.some((t) => t.name === n)) lines.push(pc7.yellow(`- ${n} not installed`));
|
|
771
|
+
}
|
|
772
|
+
return { removed, output: lines.join("\n") };
|
|
773
|
+
}
|
|
774
|
+
function removeCommand() {
|
|
775
|
+
return new Command8("remove").alias("rm").description("Remove installed skills").argument("<names...>", "skill names to remove").option("-g, --global", "only global skills").action((names, opts) => {
|
|
776
|
+
const run = runRemove(names, { global: opts.global });
|
|
777
|
+
console.log(run.output);
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ../cli/src/commands/update.ts
|
|
782
|
+
import { Command as Command9 } from "commander";
|
|
783
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
784
|
+
import { join as join5 } from "path";
|
|
785
|
+
import pc8 from "picocolors";
|
|
786
|
+
async function runUpdate(names, flags, scope = {}) {
|
|
787
|
+
const { api } = createClient(flags);
|
|
788
|
+
const installed = installedSkills(scope).filter((s) => names.length === 0 || names.includes(s.name));
|
|
789
|
+
const updated = [];
|
|
790
|
+
const lines = [];
|
|
791
|
+
for (const s of installed) {
|
|
792
|
+
const slug = await findRegistrySlug(api, s);
|
|
793
|
+
if (!slug) {
|
|
794
|
+
lines.push(pc8.dim(`- ${s.name} skipped (no registry match)`));
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const [owner, name] = slug.split("/");
|
|
799
|
+
const skill = await api(`/api/skills/${owner}/${name}`);
|
|
800
|
+
const next = reconstructSkillMd(skill);
|
|
801
|
+
if (next.trim() === s.raw.trim()) {
|
|
802
|
+
lines.push(pc8.dim(`- ${s.name} already up to date`));
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
const result = lint(next, { slug: s.name });
|
|
806
|
+
if (!result.ok) {
|
|
807
|
+
lines.push(pc8.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
writeFileSync5(join5(s.path, "SKILL.md"), next, "utf8");
|
|
811
|
+
updated.push(s.name);
|
|
812
|
+
lines.push(pc8.green(`\u2713 updated ${s.name}`));
|
|
813
|
+
} catch {
|
|
814
|
+
lines.push(pc8.red(`\u2717 ${s.name} update error`));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return { updated, output: lines.join("\n") || "Nothing to update." };
|
|
818
|
+
}
|
|
819
|
+
async function findRegistrySlug(api, s) {
|
|
820
|
+
const r = await api(`/api/search?q=${encodeURIComponent(s.name)}&limit=5`).catch(() => null);
|
|
821
|
+
const hit = r?.items?.find((it) => it.slug.split("/")[1] === s.name);
|
|
822
|
+
return hit?.slug ?? null;
|
|
823
|
+
}
|
|
824
|
+
function updateCommand() {
|
|
825
|
+
return new Command9("update").description("Update installed skills from the registry (re-lints before writing)").argument("[names...]", "specific skills to update (default: all)").option("-g, --global", "only global skills").action(async (names, opts, cmd) => {
|
|
826
|
+
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
827
|
+
const run = await runUpdate(names, merged, { global: opts.global });
|
|
828
|
+
console.log(run.output);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ../cli/src/commands/publish.ts
|
|
833
|
+
import { Command as Command10 } from "commander";
|
|
834
|
+
import pc9 from "picocolors";
|
|
835
|
+
var defaultDeps2 = {
|
|
836
|
+
resolve: resolveSource,
|
|
837
|
+
async submit(payload, flags) {
|
|
838
|
+
const { api, hasToken } = createClient(flags);
|
|
839
|
+
if (!hasToken) throw new Error("No token. Run `skillmd login` or pass --token.");
|
|
840
|
+
return api(`/api/skills`, {
|
|
841
|
+
method: "POST",
|
|
842
|
+
headers: { "content-type": "application/json" },
|
|
843
|
+
body: JSON.stringify(payload)
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
async function runPublish(target, flags, deps = defaultDeps2) {
|
|
848
|
+
const sources = await deps.resolve(target);
|
|
849
|
+
if (sources.length !== 1) {
|
|
850
|
+
return { result: lint(""), published: false, exitCode: 1, output: `Expected exactly one SKILL.md, found ${sources.length}.` };
|
|
851
|
+
}
|
|
852
|
+
const src = sources[0];
|
|
853
|
+
const result = lint(src.raw, { slug: src.slug });
|
|
854
|
+
const report = toText([{ file: src.path, result }], { decorate });
|
|
855
|
+
if (!result.ok) {
|
|
856
|
+
return { result, published: false, exitCode: 1, output: `${report}
|
|
857
|
+
${pc9.red("\u2717 Refusing to publish: fix the errors above.")}` };
|
|
858
|
+
}
|
|
859
|
+
const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
|
|
860
|
+
if (hasWarn && !flags.force) {
|
|
861
|
+
return { result, published: false, exitCode: 1, output: `${report}
|
|
862
|
+
${pc9.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
|
|
863
|
+
}
|
|
864
|
+
if (flags.dryRun) {
|
|
865
|
+
return { result, published: false, exitCode: 0, output: `${report}
|
|
866
|
+
${pc9.green("\u2713 Dry run: would publish.")}` };
|
|
867
|
+
}
|
|
868
|
+
const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
|
|
869
|
+
return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
|
|
870
|
+
${pc9.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
|
|
871
|
+
}
|
|
872
|
+
function publishCommand() {
|
|
873
|
+
return new Command10("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) => {
|
|
874
|
+
const merged = { ...cmd.parent?.opts(), ...opts };
|
|
875
|
+
const run = await runPublish(target, merged);
|
|
876
|
+
console.log(run.output);
|
|
877
|
+
process.exitCode = run.exitCode;
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ../cli/src/commands/login.ts
|
|
882
|
+
import { Command as Command11 } from "commander";
|
|
883
|
+
import * as p2 from "@clack/prompts";
|
|
884
|
+
function loginCommand() {
|
|
885
|
+
return new Command11("login").description("Store a SkillMD personal access token for publishing").option("--token <token>", "token to store (otherwise prompt)").option("--api <url>", "registry API base to store").action(async (opts) => {
|
|
886
|
+
let token = opts.token;
|
|
887
|
+
if (!token) {
|
|
888
|
+
const answer = await p2.password({ message: "Paste your SkillMD token (from your account page)" });
|
|
889
|
+
if (p2.isCancel(answer)) {
|
|
890
|
+
p2.cancel("Cancelled.");
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
token = answer;
|
|
894
|
+
}
|
|
895
|
+
if (!token) {
|
|
896
|
+
console.error("A token is required.");
|
|
897
|
+
process.exitCode = 1;
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const cfg = readConfig();
|
|
901
|
+
cfg.token = token;
|
|
902
|
+
if (opts.api) cfg.api = opts.api;
|
|
903
|
+
writeConfig(cfg);
|
|
904
|
+
console.log(`Saved token to ${configPath()}`);
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
function logoutCommand() {
|
|
908
|
+
return new Command11("logout").description("Remove the stored SkillMD token").action(() => {
|
|
909
|
+
const cfg = readConfig();
|
|
910
|
+
delete cfg.token;
|
|
911
|
+
writeConfig(cfg);
|
|
912
|
+
console.log("Removed stored token.");
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ../cli/src/index.ts
|
|
917
|
+
var program = new Command12("skillmd").version("1.0.0").description("The registry that lints. Discover, validate, and install verified Agent Skills.").option("--json", "machine-readable output").option("--token <token>", "SkillMD personal access token").option("--api <url>", "override the registry API base URL");
|
|
918
|
+
program.addCommand(lintCommand());
|
|
919
|
+
program.addCommand(scanCommand());
|
|
920
|
+
program.addCommand(rulesCommand());
|
|
921
|
+
program.addCommand(initCommand());
|
|
922
|
+
program.addCommand(searchCommand());
|
|
923
|
+
program.addCommand(addCommand());
|
|
924
|
+
program.addCommand(listCommand());
|
|
925
|
+
program.addCommand(removeCommand());
|
|
926
|
+
program.addCommand(updateCommand());
|
|
927
|
+
program.addCommand(publishCommand());
|
|
928
|
+
program.addCommand(loginCommand());
|
|
929
|
+
program.addCommand(logoutCommand());
|
|
930
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
931
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
932
|
+
process.exitCode = 1;
|
|
933
|
+
});
|