skillmds 1.1.2 → 1.2.1
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 +84 -0
- package/README.md +89 -14
- package/bin/skillmd-mcp.mjs +2 -1020
- package/dist/chunk-4BFQJ3YZ.js +851 -0
- package/dist/cli.js +1083 -666
- package/dist/mcp.js +579 -0
- package/package.json +2 -3
package/dist/cli.js
CHANGED
|
@@ -1,136 +1,54 @@
|
|
|
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
|
+
parseSource,
|
|
20
|
+
readConfig,
|
|
21
|
+
readLock,
|
|
22
|
+
resolveApi,
|
|
23
|
+
resolvePackFiles,
|
|
24
|
+
resolveSource,
|
|
25
|
+
resolveTree,
|
|
26
|
+
safeText,
|
|
27
|
+
skillMdFor,
|
|
28
|
+
sourceId,
|
|
29
|
+
telemetryDisabled,
|
|
30
|
+
uninstallSkill,
|
|
31
|
+
upsertEntry,
|
|
32
|
+
writeConfig
|
|
33
|
+
} from "./chunk-4BFQJ3YZ.js";
|
|
34
|
+
|
|
1
35
|
// src/index.ts
|
|
2
36
|
import { Command as Command13 } from "commander";
|
|
3
|
-
import { readFileSync as
|
|
37
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
38
|
+
import pc15 from "picocolors";
|
|
4
39
|
|
|
5
40
|
// src/commands/lint.ts
|
|
6
41
|
import { Command } from "commander";
|
|
7
42
|
import { writeFileSync } from "fs";
|
|
8
|
-
import { homedir } from "os";
|
|
9
|
-
import { resolve } from "path";
|
|
43
|
+
import { homedir as homedir2 } from "os";
|
|
44
|
+
import { resolve as resolve2 } from "path";
|
|
10
45
|
import pc2 from "picocolors";
|
|
11
46
|
import * as p from "@clack/prompts";
|
|
12
47
|
import { lint, toJson, toSarif, toGithub } from "@skillmds/core";
|
|
13
48
|
|
|
14
|
-
// src/source.ts
|
|
15
|
-
import { existsSync, statSync, lstatSync, readFileSync, readdirSync, mkdtempSync, rmSync } from "fs";
|
|
16
|
-
import { tmpdir } from "os";
|
|
17
|
-
import { join, basename, dirname, relative, sep } from "path";
|
|
18
|
-
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
19
|
-
// build/dependency dirs
|
|
20
|
-
"node_modules",
|
|
21
|
-
".git",
|
|
22
|
-
"dist",
|
|
23
|
-
"build",
|
|
24
|
-
"out",
|
|
25
|
-
".next",
|
|
26
|
-
".wrangler",
|
|
27
|
-
".cache",
|
|
28
|
-
".npm",
|
|
29
|
-
".pnpm-store",
|
|
30
|
-
".venv",
|
|
31
|
-
"venv",
|
|
32
|
-
"__pycache__",
|
|
33
|
-
"target",
|
|
34
|
-
"vendor",
|
|
35
|
-
// large OS / user dirs (mostly relevant when run from a home directory)
|
|
36
|
-
"AppData",
|
|
37
|
-
"Library",
|
|
38
|
-
"$Recycle.Bin",
|
|
39
|
-
"System Volume Information",
|
|
40
|
-
"Windows",
|
|
41
|
-
"Program Files"
|
|
42
|
-
]);
|
|
43
|
-
var MAX_DEPTH = 12;
|
|
44
|
-
function findSkillFiles(dir, depth = 0) {
|
|
45
|
-
const out = [];
|
|
46
|
-
if (depth > MAX_DEPTH) return out;
|
|
47
|
-
let entries;
|
|
48
|
-
try {
|
|
49
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
50
|
-
} catch {
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
for (const entry of entries) {
|
|
54
|
-
if (entry.isDirectory()) {
|
|
55
|
-
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
56
|
-
out.push(...findSkillFiles(join(dir, entry.name), depth + 1));
|
|
57
|
-
} else if (entry.name === "SKILL.md") {
|
|
58
|
-
out.push(join(dir, entry.name));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return out;
|
|
62
|
-
}
|
|
63
|
-
function loadLocal(file) {
|
|
64
|
-
return { path: file, raw: readFileSync(file, "utf8"), slug: basename(dirname(file)), file };
|
|
65
|
-
}
|
|
66
|
-
function isLikelyLocal(arg) {
|
|
67
|
-
return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync(arg);
|
|
68
|
-
}
|
|
69
|
-
async function resolveSource(arg) {
|
|
70
|
-
if (isLikelyLocal(arg)) {
|
|
71
|
-
if (!existsSync(arg)) throw new Error(`Path not found: ${arg}`);
|
|
72
|
-
const st = statSync(arg);
|
|
73
|
-
if (st.isFile()) return [loadLocal(arg)];
|
|
74
|
-
const files2 = findSkillFiles(arg);
|
|
75
|
-
if (files2.length === 0) {
|
|
76
|
-
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.`);
|
|
77
|
-
}
|
|
78
|
-
return files2.map(loadLocal);
|
|
79
|
-
}
|
|
80
|
-
const { downloadTemplate } = await import("giget");
|
|
81
|
-
const input = normalizeGiget(arg);
|
|
82
|
-
const dir = mkdtempSync(join(tmpdir(), "skillmd-src-"));
|
|
83
|
-
const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
|
|
84
|
-
const files = findSkillFiles(out);
|
|
85
|
-
if (files.length === 0) throw new Error(`No SKILL.md found in ${arg}`);
|
|
86
|
-
return files.map(loadLocal);
|
|
87
|
-
}
|
|
88
|
-
var MAX_PACK_FILES = 200;
|
|
89
|
-
var MAX_PACK_BYTES = 20 * 1024 * 1024;
|
|
90
|
-
function collectFiles(root, dir, limits = {}) {
|
|
91
|
-
const maxFiles = limits.maxFiles ?? MAX_PACK_FILES;
|
|
92
|
-
const maxBytes = limits.maxBytes ?? MAX_PACK_BYTES;
|
|
93
|
-
const out = [];
|
|
94
|
-
let bytes = 0;
|
|
95
|
-
const walk = (d, depth) => {
|
|
96
|
-
if (depth > MAX_DEPTH) return;
|
|
97
|
-
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
98
|
-
const fp = join(d, entry.name);
|
|
99
|
-
const st = lstatSync(fp);
|
|
100
|
-
if (st.isSymbolicLink()) continue;
|
|
101
|
-
if (st.isDirectory()) {
|
|
102
|
-
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
103
|
-
walk(fp, depth + 1);
|
|
104
|
-
} else if (st.isFile()) {
|
|
105
|
-
if (out.length >= maxFiles) throw new Error(`pack too large: > ${maxFiles} files`);
|
|
106
|
-
bytes += st.size;
|
|
107
|
-
if (bytes > maxBytes) throw new Error(`pack too large: > ${Math.round(maxBytes / (1024 * 1024))}MB`);
|
|
108
|
-
out.push({ path: relative(root, fp).split(sep).join("/"), contents: readFileSync(fp) });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
walk(dir, 0);
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
async function resolveTree(sourceUrl, ref) {
|
|
116
|
-
const { downloadTemplate } = await import("giget");
|
|
117
|
-
const input = normalizeGiget(sourceUrl) + (ref ? `#${ref}` : "");
|
|
118
|
-
const dir = mkdtempSync(join(tmpdir(), "skillmd-pack-"));
|
|
119
|
-
try {
|
|
120
|
-
const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
|
|
121
|
-
return collectFiles(out, out);
|
|
122
|
-
} finally {
|
|
123
|
-
rmSync(dir, { recursive: true, force: true });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function normalizeGiget(arg) {
|
|
127
|
-
const m = arg.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/[^/]+\/(.+))?/);
|
|
128
|
-
if (m) return `github:${m[1]}/${m[2]}${m[3] ? "/" + m[3] : ""}`;
|
|
129
|
-
if (arg.startsWith("github:") || arg.startsWith("gh:")) return arg;
|
|
130
|
-
return `github:${arg}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
49
|
// src/ui.ts
|
|
50
|
+
import { homedir } from "os";
|
|
51
|
+
import { relative, resolve, sep } from "path";
|
|
134
52
|
import pc from "picocolors";
|
|
135
53
|
var decorate = (text3, kind) => {
|
|
136
54
|
switch (kind) {
|
|
@@ -154,7 +72,11 @@ var SEV_COLOR = {
|
|
|
154
72
|
var GLYPH = {
|
|
155
73
|
error: "\u2716",
|
|
156
74
|
warn: "\u26A0",
|
|
157
|
-
info: "\u2139"
|
|
75
|
+
info: "\u2139",
|
|
76
|
+
ok: "\u2713",
|
|
77
|
+
skip: "\u21B7",
|
|
78
|
+
blocked: "\u2717",
|
|
79
|
+
replaced: "\u26A0"
|
|
158
80
|
};
|
|
159
81
|
var SEC_GLYPH = "\u2691";
|
|
160
82
|
var SEV_ORDER = ["error", "warn", "info"];
|
|
@@ -188,7 +110,7 @@ function statusDot(result) {
|
|
|
188
110
|
return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
|
|
189
111
|
}
|
|
190
112
|
function failureBlock(file, result) {
|
|
191
|
-
const lines = [`${pc.red(
|
|
113
|
+
const lines = [`${pc.red(GLYPH.error)} ${pc.bold(label(file))}`];
|
|
192
114
|
for (const d of result.diagnostics.filter((x) => x.severity === "error")) {
|
|
193
115
|
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
194
116
|
lines.push(` ${SEV_COLOR.error(d.id)}${loc} ${d.message}`);
|
|
@@ -242,7 +164,7 @@ function renderMany(results) {
|
|
|
242
164
|
}
|
|
243
165
|
function renderFailuresView(results) {
|
|
244
166
|
const fails = results.filter((r) => !r.result.ok);
|
|
245
|
-
if (!fails.length) return pc.green(
|
|
167
|
+
if (!fails.length) return pc.green(`${GLYPH.ok} No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
|
|
246
168
|
const out = fails.map((f) => failureBlock(f.file, f.result));
|
|
247
169
|
out.push("");
|
|
248
170
|
out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
|
|
@@ -263,6 +185,88 @@ function exitCodeFor(results, policy = {}) {
|
|
|
263
185
|
}
|
|
264
186
|
return 0;
|
|
265
187
|
}
|
|
188
|
+
function shortPath(p7, ctx = {}) {
|
|
189
|
+
const home = resolve(ctx.home ?? homedir());
|
|
190
|
+
const cwd = resolve(ctx.cwd ?? process.cwd());
|
|
191
|
+
const abs = resolve(p7);
|
|
192
|
+
const norm = (s) => s.split(sep).join("/");
|
|
193
|
+
if (abs === cwd) return ".";
|
|
194
|
+
if (abs.startsWith(cwd + sep)) return `./${norm(relative(cwd, abs))}`;
|
|
195
|
+
if (abs === home) return "~";
|
|
196
|
+
if (abs.startsWith(home + sep)) return `~/${norm(relative(home, abs))}`;
|
|
197
|
+
return norm(abs);
|
|
198
|
+
}
|
|
199
|
+
var SGR = /\x1b\[[0-9;]*m/g;
|
|
200
|
+
var SGR_AT = /\x1b\[[0-9;]*m/y;
|
|
201
|
+
var RESET = "\x1B[0m";
|
|
202
|
+
function stripAnsi(s) {
|
|
203
|
+
return s.replace(SGR, "");
|
|
204
|
+
}
|
|
205
|
+
var visible = (s) => stripAnsi(s).length;
|
|
206
|
+
function truncateVisible(s, max) {
|
|
207
|
+
if (max <= 0) return "";
|
|
208
|
+
if (visible(s) <= max) return s;
|
|
209
|
+
let out = "";
|
|
210
|
+
let shown = 0;
|
|
211
|
+
let open = false;
|
|
212
|
+
for (let i = 0; i < s.length && shown < max - 1; ) {
|
|
213
|
+
if (s[i] === "\x1B") {
|
|
214
|
+
SGR_AT.lastIndex = i;
|
|
215
|
+
const m = SGR_AT.exec(s);
|
|
216
|
+
if (m) {
|
|
217
|
+
out += m[0];
|
|
218
|
+
open = m[0] !== RESET && m[0] !== "\x1B[m";
|
|
219
|
+
i += m[0].length;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
out += s[i];
|
|
224
|
+
shown++;
|
|
225
|
+
i++;
|
|
226
|
+
}
|
|
227
|
+
return out + "\u2026" + (open ? RESET : "");
|
|
228
|
+
}
|
|
229
|
+
var termWidth = (w) => w ?? Math.min(process.stdout.columns ?? 100, 120);
|
|
230
|
+
function table(rows, opts = {}) {
|
|
231
|
+
if (rows.length === 0) return "";
|
|
232
|
+
const width = termWidth(opts.width);
|
|
233
|
+
const indent = opts.indent ?? "";
|
|
234
|
+
const cols = Math.max(...rows.map((r) => r.length));
|
|
235
|
+
const widths = Array.from({ length: cols }, (_, i) => Math.max(...rows.map((r) => visible(r[i] ?? ""))));
|
|
236
|
+
return rows.map((r) => {
|
|
237
|
+
let line = indent;
|
|
238
|
+
for (let i = 0; i < cols; i++) {
|
|
239
|
+
const cell = r[i] ?? "";
|
|
240
|
+
if (i < cols - 1) {
|
|
241
|
+
line += cell + " ".repeat(widths[i] - visible(cell) + 2);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
line += truncateVisible(cell, Math.max(4, width - visible(line)));
|
|
245
|
+
}
|
|
246
|
+
line = line.trimEnd();
|
|
247
|
+
return visible(line) > width ? truncateVisible(line, width) : line;
|
|
248
|
+
}).join("\n");
|
|
249
|
+
}
|
|
250
|
+
function renderInstallSummary(i, ctx = {}) {
|
|
251
|
+
const width = termWidth(ctx.width);
|
|
252
|
+
const src = i.source.startsWith("local:") ? `local:${shortPath(i.source.slice(6), ctx)}` : i.source;
|
|
253
|
+
const head = truncateVisible(
|
|
254
|
+
pc.green(`${GLYPH.ok} ${i.name}`) + pc.dim(` score ${i.score}${i.flags.length ? ` ${i.flags.join(", ")}` : ""} from ${src}`),
|
|
255
|
+
width
|
|
256
|
+
);
|
|
257
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const t of i.targets) {
|
|
259
|
+
const e = byPath.get(t.path);
|
|
260
|
+
if (e) e.agents.push(t.agent);
|
|
261
|
+
else byPath.set(t.path, { agents: [t.agent], mode: t.mode });
|
|
262
|
+
}
|
|
263
|
+
const rows = [...byPath.entries()].map(([path, e]) => [pc.cyan(e.agents.join(", ")), pc.dim(e.mode), shortPath(path, ctx)]);
|
|
264
|
+
const body = rows.length ? table(rows, { width, indent: " " }) : truncateVisible(pc.dim(` ${shortPath(i.canonical, ctx)} canonical only`), width);
|
|
265
|
+
const skipped = i.skipped.map((s) => truncateVisible(pc.dim(` ${GLYPH.skip} ${s.agent}: ${s.reason}`), width));
|
|
266
|
+
const replaced = i.replaced ? [truncateVisible(pc.yellow(` ${GLYPH.replaced} replaced previous install (${i.replaced === "untracked" ? "untracked directory" : i.replaced})`), width)] : [];
|
|
267
|
+
return [head, body, ...skipped, ...replaced].join("\n");
|
|
268
|
+
}
|
|
269
|
+
var banner = () => pc.bgCyan(pc.black(" skillmd "));
|
|
266
270
|
|
|
267
271
|
// src/commands/lint.ts
|
|
268
272
|
function applyLicenseFix(raw) {
|
|
@@ -304,9 +308,9 @@ async function runLint(target, flags) {
|
|
|
304
308
|
return { results, output, exitCode };
|
|
305
309
|
}
|
|
306
310
|
function lintCommand() {
|
|
307
|
-
return new Command("lint").
|
|
311
|
+
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) => {
|
|
308
312
|
const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
|
|
309
|
-
if (
|
|
313
|
+
if (resolve2(target) === homedir2()) {
|
|
310
314
|
process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
|
|
311
315
|
`));
|
|
312
316
|
}
|
|
@@ -402,10 +406,43 @@ function rulesCommand() {
|
|
|
402
406
|
|
|
403
407
|
// src/commands/init.ts
|
|
404
408
|
import { Command as Command4 } from "commander";
|
|
405
|
-
import { mkdirSync, writeFileSync as writeFileSync2, existsSync
|
|
406
|
-
import { join
|
|
409
|
+
import { mkdirSync, writeFileSync as writeFileSync2, existsSync } from "fs";
|
|
410
|
+
import { join } from "path";
|
|
407
411
|
import { slugify } from "@skillmds/core";
|
|
408
412
|
import * as p2 from "@clack/prompts";
|
|
413
|
+
import pc5 from "picocolors";
|
|
414
|
+
|
|
415
|
+
// src/env.ts
|
|
416
|
+
function detectHostAgent(env = process.env) {
|
|
417
|
+
if (env.CLAUDECODE) return "claude-code";
|
|
418
|
+
if (env.CURSOR_AGENT || env.CURSOR_EXTENSION_HOST_ROLE === "agent-exec") return "cursor";
|
|
419
|
+
if (env.CODEX_SANDBOX || env.CODEX_THREAD_ID) return "codex";
|
|
420
|
+
if (env.GEMINI_CLI) return "gemini-cli";
|
|
421
|
+
if (env.CI && env.CI !== "false" && env.CI !== "0") return "ci";
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
var realTerminal = () => ({
|
|
425
|
+
stdinTTY: Boolean(process.stdin.isTTY),
|
|
426
|
+
stdoutTTY: Boolean(process.stdout.isTTY),
|
|
427
|
+
env: process.env
|
|
428
|
+
});
|
|
429
|
+
function isInteractive(flags, term = realTerminal()) {
|
|
430
|
+
if (flags.yes || flags.json) return false;
|
|
431
|
+
if (!term.stdinTTY || !term.stdoutTTY) return false;
|
|
432
|
+
return detectHostAgent(term.env) === null;
|
|
433
|
+
}
|
|
434
|
+
function scopesFor(flags) {
|
|
435
|
+
return flags.global && !flags.project ? [true] : flags.project && !flags.global ? [false] : [false, true];
|
|
436
|
+
}
|
|
437
|
+
function nonInteractiveHint(flags) {
|
|
438
|
+
return `Interactive prompt required but stdin is not a terminal. Nothing was changed. Re-run with ${flags} to run non-interactively.`;
|
|
439
|
+
}
|
|
440
|
+
function promptIf(flags, prompt) {
|
|
441
|
+
const term = { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env: flags.env ?? process.env };
|
|
442
|
+
return isInteractive(flags, term) ? { prompt } : {};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/commands/init.ts
|
|
409
446
|
function skillTemplate(name, description, license) {
|
|
410
447
|
const desc = description.trim() || `A skill that helps agents with ${name}.`;
|
|
411
448
|
return `---
|
|
@@ -434,134 +471,54 @@ Add any constraints, edge cases, or references the agent should keep in mind.
|
|
|
434
471
|
}
|
|
435
472
|
function initSkill(name, opts = {}) {
|
|
436
473
|
const slug = slugify(name);
|
|
437
|
-
const root =
|
|
438
|
-
if (
|
|
474
|
+
const root = join(opts.dir ?? process.cwd(), slug);
|
|
475
|
+
if (existsSync(join(root, "SKILL.md"))) throw new Error(`SKILL.md already exists at ${root}`);
|
|
439
476
|
mkdirSync(root, { recursive: true });
|
|
440
|
-
const file =
|
|
477
|
+
const file = join(root, "SKILL.md");
|
|
441
478
|
writeFileSync2(file, skillTemplate(name, opts.description ?? "", opts.license ?? "MIT"), "utf8");
|
|
442
479
|
return file;
|
|
443
480
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
process.exitCode = 1;
|
|
458
|
-
return;
|
|
481
|
+
var defaultInitDeps = (flags) => promptIf(flags, async () => {
|
|
482
|
+
const answer = await p2.text({ message: "Skill name?", placeholder: "my-skill" });
|
|
483
|
+
return p2.isCancel(answer) ? null : answer;
|
|
484
|
+
});
|
|
485
|
+
function finish(flags, result, doc) {
|
|
486
|
+
return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
|
|
487
|
+
}
|
|
488
|
+
async function runInit(nameArg, flags, deps = defaultInitDeps(flags)) {
|
|
489
|
+
let name = nameArg ?? flags.name;
|
|
490
|
+
if (!name) {
|
|
491
|
+
if (!deps.prompt) {
|
|
492
|
+
const error = nonInteractiveHint("a name argument or --name <name>");
|
|
493
|
+
return finish(flags, { exitCode: 1, output: pc5.red(error) }, { ok: false, error });
|
|
459
494
|
}
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// src/commands/search.ts
|
|
466
|
-
import { Command as Command5 } from "commander";
|
|
467
|
-
import pc5 from "picocolors";
|
|
468
|
-
|
|
469
|
-
// src/api.ts
|
|
470
|
-
import { createHash } from "crypto";
|
|
471
|
-
import { isAllowedSourceUrl } from "@skillmds/core";
|
|
472
|
-
|
|
473
|
-
// src/config.ts
|
|
474
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
475
|
-
import { homedir as homedir2 } from "os";
|
|
476
|
-
import { join as join3 } from "path";
|
|
477
|
-
var DEFAULT_API = "https://api.skillmd.com";
|
|
478
|
-
function configPath() {
|
|
479
|
-
return join3(homedir2(), ".skillmd", "config.json");
|
|
480
|
-
}
|
|
481
|
-
function readConfig() {
|
|
482
|
-
try {
|
|
483
|
-
return JSON.parse(readFileSync2(configPath(), "utf8"));
|
|
484
|
-
} catch {
|
|
485
|
-
return {};
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function writeConfig(cfg) {
|
|
489
|
-
const dir = join3(homedir2(), ".skillmd");
|
|
490
|
-
mkdirSync2(dir, { recursive: true });
|
|
491
|
-
writeFileSync3(configPath(), JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
|
|
492
|
-
}
|
|
493
|
-
function resolveToken(flags = {}) {
|
|
494
|
-
return flags.token || process.env.SKILLMD_TOKEN || readConfig().token || void 0;
|
|
495
|
-
}
|
|
496
|
-
function resolveApi(flags = {}) {
|
|
497
|
-
return flags.api || process.env.SKILLMD_API || readConfig().api || DEFAULT_API;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// src/api.ts
|
|
501
|
-
import { isAllowedSourceUrl as isAllowedSourceUrl2, reconstructSkillMd, skillMdFor } from "@skillmds/core";
|
|
502
|
-
var IntegrityError = class extends Error {
|
|
503
|
-
integrity = true;
|
|
504
|
-
constructor(message) {
|
|
505
|
-
super(message);
|
|
506
|
-
this.name = "IntegrityError";
|
|
495
|
+
const answer = await deps.prompt();
|
|
496
|
+
if (answer === null) return finish(flags, { exitCode: 0, output: pc5.dim("Cancelled.") }, { ok: false, cancelled: true });
|
|
497
|
+
name = answer;
|
|
507
498
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
super(message);
|
|
512
|
-
this.status = status;
|
|
513
|
-
this.name = "RegistryError";
|
|
514
|
-
}
|
|
515
|
-
status;
|
|
516
|
-
};
|
|
517
|
-
function createClient(flags = {}) {
|
|
518
|
-
const base = resolveApi(flags);
|
|
519
|
-
const token = resolveToken(flags);
|
|
520
|
-
async function api(path, init = {}) {
|
|
521
|
-
const headers = { accept: "application/json", ...init.headers };
|
|
522
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
523
|
-
let res;
|
|
524
|
-
try {
|
|
525
|
-
res = await fetch(base + path, { ...init, headers });
|
|
526
|
-
} catch (e) {
|
|
527
|
-
const cause = e instanceof Error ? e.cause?.message ?? e.message : String(e);
|
|
528
|
-
throw new RegistryError(`could not reach ${base}: ${cause}`);
|
|
529
|
-
}
|
|
530
|
-
if (!res.ok) {
|
|
531
|
-
const detail = await res.text().catch(() => "");
|
|
532
|
-
throw new RegistryError(`SkillMD API ${res.status} on ${path}: ${detail}`, res.status);
|
|
533
|
-
}
|
|
534
|
-
return await res.json();
|
|
499
|
+
if (!name) {
|
|
500
|
+
const error = "A skill name is required (pass a name or --name).";
|
|
501
|
+
return finish(flags, { exitCode: 1, output: pc5.red(error) }, { ok: false, error });
|
|
535
502
|
}
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (!res || !res.ok) return null;
|
|
543
|
-
const data = await res.json().catch(() => null);
|
|
544
|
-
if (!data?.files) return null;
|
|
545
|
-
const out = [];
|
|
546
|
-
for (const f of data.files) {
|
|
547
|
-
if (f.content_base64 != null) {
|
|
548
|
-
const contents = Buffer.from(f.content_base64, "base64");
|
|
549
|
-
if (f.sha256) {
|
|
550
|
-
const got = createHash("sha256").update(contents).digest("hex");
|
|
551
|
-
if (got !== f.sha256) {
|
|
552
|
-
throw new IntegrityError(`integrity check failed for "${f.path}" (expected ${f.sha256.slice(0, 12)}\u2026, got ${got.slice(0, 12)}\u2026)`);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
out.push({ path: f.path, contents });
|
|
556
|
-
} else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
|
|
557
|
-
const r = await fetch(f.source_url).catch(() => null);
|
|
558
|
-
if (r && r.ok) out.push({ path: f.path, contents: Buffer.from(await r.arrayBuffer()) });
|
|
559
|
-
}
|
|
503
|
+
try {
|
|
504
|
+
const file = initSkill(name, { dir: flags.dir, description: flags.description, license: flags.license });
|
|
505
|
+
return { exitCode: 0, file, output: flags.json ? JSON.stringify({ ok: true, file }, null, 2) : `Created ${file}` };
|
|
506
|
+
} catch (e) {
|
|
507
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
508
|
+
return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg }, null, 2) : pc5.red(msg) };
|
|
560
509
|
}
|
|
561
|
-
|
|
510
|
+
}
|
|
511
|
+
function initCommand() {
|
|
512
|
+
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) => {
|
|
513
|
+
const r = await runInit(nameArg, { ...cmd.parent?.opts(), ...opts });
|
|
514
|
+
console.log(r.output);
|
|
515
|
+
process.exitCode = r.exitCode;
|
|
516
|
+
});
|
|
562
517
|
}
|
|
563
518
|
|
|
564
519
|
// src/commands/search.ts
|
|
520
|
+
import { Command as Command5 } from "commander";
|
|
521
|
+
import pc6 from "picocolors";
|
|
565
522
|
var MAX_LINE_WIDTH = 100;
|
|
566
523
|
function formatStars(n) {
|
|
567
524
|
if (n >= 1e6) {
|
|
@@ -580,13 +537,13 @@ function truncate(text3, width) {
|
|
|
580
537
|
return `${text3.slice(0, width - 1).trimEnd()}\u2026`;
|
|
581
538
|
}
|
|
582
539
|
function formatSearchResult(item, width = process.stdout.columns ?? 80) {
|
|
583
|
-
const stars = item.repo_stars != null ?
|
|
584
|
-
const check = item.verified ?
|
|
585
|
-
const pack = item.type === "pack" ?
|
|
586
|
-
const head = [
|
|
540
|
+
const stars = item.repo_stars != null ? pc6.dim(`\u2605 ${formatStars(item.repo_stars)}`) : "";
|
|
541
|
+
const check = item.verified ? pc6.green("\u2713") : "";
|
|
542
|
+
const pack = item.type === "pack" ? pc6.dim("[pack]") : "";
|
|
543
|
+
const head = [pc6.bold(safeText(item.slug, 200)), stars, check, pack].filter(Boolean).join(" ");
|
|
587
544
|
const indent = " ";
|
|
588
545
|
const lineWidth = Math.max(10, Math.min(width, MAX_LINE_WIDTH) - indent.length);
|
|
589
|
-
const desc = `${indent}${
|
|
546
|
+
const desc = `${indent}${pc6.dim(truncate(safeText(item.description ?? ""), lineWidth))}`;
|
|
590
547
|
return `${head}
|
|
591
548
|
${desc}`;
|
|
592
549
|
}
|
|
@@ -613,42 +570,44 @@ function searchCommand() {
|
|
|
613
570
|
|
|
614
571
|
// src/commands/info.ts
|
|
615
572
|
import { Command as Command6 } from "commander";
|
|
616
|
-
import
|
|
617
|
-
async function runInfo(slug, flags) {
|
|
573
|
+
import pc7 from "picocolors";
|
|
574
|
+
async function runInfo(slug, flags, deps = {}) {
|
|
618
575
|
const [owner, name] = slug.split("/");
|
|
619
576
|
if (!owner || !name || slug.split("/").length !== 2) {
|
|
620
|
-
return { ok: false, exitCode: 1, output:
|
|
577
|
+
return { ok: false, exitCode: 1, output: pc7.red("Expected owner/name, e.g. anthropic/pdf") };
|
|
621
578
|
}
|
|
622
|
-
const
|
|
579
|
+
const api = deps.api ?? createClient(flags).api;
|
|
623
580
|
let s;
|
|
624
581
|
try {
|
|
625
582
|
s = await api(`/api/skills/${owner}/${name}`);
|
|
626
583
|
} catch (err) {
|
|
627
584
|
const message = err instanceof Error ? err.message : String(err);
|
|
628
585
|
if (/^SkillMD API 404 /.test(message)) {
|
|
629
|
-
return { ok: false, exitCode: 1, output:
|
|
586
|
+
return { ok: false, exitCode: 1, output: pc7.red(`Skill not found on the registry: ${slug}`) };
|
|
630
587
|
}
|
|
631
|
-
return { ok: false, exitCode: 1, output:
|
|
588
|
+
return { ok: false, exitCode: 1, output: pc7.red(`Could not reach the SkillMD registry: ${message}`) };
|
|
632
589
|
}
|
|
633
590
|
if (flags.json) return { ok: true, exitCode: 0, output: JSON.stringify(s, null, 2), skill: s };
|
|
591
|
+
const slugTxt = safeText(s.slug, 200);
|
|
634
592
|
const lines = [];
|
|
635
593
|
lines.push(
|
|
636
|
-
`${
|
|
594
|
+
`${pc7.bold(slugTxt)} ${pc7.dim(`[${safeText(s.type ?? "single", 40)}]`)} ${s.verified ? pc7.green("\u2713 verified") : pc7.yellow("unverified")}`
|
|
637
595
|
);
|
|
638
|
-
lines.push(` ${s.description}`);
|
|
596
|
+
lines.push(` ${safeText(s.description)}`);
|
|
639
597
|
lines.push(
|
|
640
|
-
|
|
641
|
-
` category: ${s.category ?? "-"} \xB7 rating: ${s.avg_rating != null ? s.avg_rating.toFixed(1) : "-"} (${s.rating_count ?? 0}) \xB7 installs: ${s.install_count ?? 0}`
|
|
598
|
+
pc7.dim(
|
|
599
|
+
` 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}`
|
|
642
600
|
)
|
|
643
601
|
);
|
|
602
|
+
const securityFlags2 = (s.security_flags ?? []).map((f) => safeText(f, 60)).filter(Boolean);
|
|
644
603
|
lines.push(
|
|
645
|
-
|
|
604
|
+
pc7.dim(` security: ${securityFlags2.join(", ") || "unscanned"} \xB7 license: ${safeText(s.provenance?.license ?? s.license ?? "unknown", 100) || "unknown"}`)
|
|
646
605
|
);
|
|
647
|
-
const sourceRepo = s.provenance?.source_repo ?? s.source_repo;
|
|
648
|
-
const commitSha = s.provenance?.commit_sha ?? s.commit_sha;
|
|
649
|
-
if (sourceRepo) lines.push(
|
|
650
|
-
lines.push(
|
|
651
|
-
lines.push(
|
|
606
|
+
const sourceRepo = safeText(s.provenance?.source_repo ?? s.source_repo, 300);
|
|
607
|
+
const commitSha = safeText(s.provenance?.commit_sha ?? s.commit_sha, 100);
|
|
608
|
+
if (sourceRepo) lines.push(pc7.dim(` source: ${sourceRepo}${commitSha ? ` @ ${commitSha.slice(0, 8)}` : ""}`));
|
|
609
|
+
lines.push(pc7.dim(` page: https://skillmd.com/skills/${slugTxt}`));
|
|
610
|
+
lines.push(pc7.dim(` install: ${safeText(s.install?.cli, 300) || `npx skillmds add ${slugTxt}`}`));
|
|
652
611
|
return { ok: true, exitCode: 0, output: lines.join("\n"), skill: s };
|
|
653
612
|
}
|
|
654
613
|
function infoCommand() {
|
|
@@ -662,154 +621,50 @@ function infoCommand() {
|
|
|
662
621
|
|
|
663
622
|
// src/commands/add.ts
|
|
664
623
|
import { Command as Command7, Option } from "commander";
|
|
665
|
-
import { existsSync as existsSync4 } from "fs";
|
|
666
|
-
import { homedir as homedir4 } from "os";
|
|
667
|
-
import { resolve as resolve3 } from "path";
|
|
668
|
-
import pc7 from "picocolors";
|
|
669
|
-
import { lint as lint2 } from "@skillmds/core";
|
|
670
|
-
|
|
671
|
-
// src/agents.ts
|
|
672
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync as rmSync2, symlinkSync, existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
|
|
673
624
|
import { homedir as homedir3 } from "os";
|
|
674
|
-
import
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
{ id: "qwen-code", project: [".qwen", "skills"], global: [".qwen", "skills"], detect: [[".qwen"]] },
|
|
691
|
-
{ id: "amp", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".config", "amp"]] },
|
|
692
|
-
// — extended registry —
|
|
693
|
-
{ id: "openclaw", project: ["skills"], global: [".openclaw", "skills"], detect: [[".openclaw"], [".clawdbot"], [".moltbot"]] },
|
|
694
|
-
{ id: "kilo", project: [".kilocode", "skills"], global: [".kilocode", "skills"], detect: [[".kilocode"]] },
|
|
695
|
-
{ id: "hermes-agent", project: [".hermes", "skills"], global: [".hermes", "skills"], detect: [[".hermes"]] },
|
|
696
|
-
{ id: "continue", project: [".continue", "skills"], global: [".continue", "skills"], detect: [[".continue"]] },
|
|
697
|
-
{ id: "devin", project: [".devin", "skills"], global: [".config", "devin", "skills"], detect: [[".config", "devin"]] },
|
|
698
|
-
{ id: "openhands", project: [".openhands", "skills"], global: [".openhands", "skills"], detect: [[".openhands"]] },
|
|
699
|
-
{ id: "replit", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".replit"]] },
|
|
700
|
-
{ id: "mistral-vibe", project: [".vibe", "skills"], global: [".vibe", "skills"], detect: [[".vibe"]] },
|
|
701
|
-
{ id: "zed", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".config", "zed"]] },
|
|
702
|
-
{ id: "adal", project: [".adal", "skills"], global: [".adal", "skills"], detect: [[".adal"]] },
|
|
703
|
-
{ id: "aider-desk", project: [".aider-desk", "skills"], global: [".aider-desk", "skills"], detect: [[".aider-desk"]] },
|
|
704
|
-
{ id: "antigravity-cli", project: [".agents", "skills"], global: [".gemini", "antigravity-cli", "skills"], detect: [[".gemini", "antigravity-cli"]] },
|
|
705
|
-
{ id: "augment", project: [".augment", "skills"], global: [".augment", "skills"], detect: [[".augment"]] },
|
|
706
|
-
{ id: "autohand-code", project: [".autohand", "skills"], global: [".autohand", "skills"], detect: [[".autohand"]] },
|
|
707
|
-
{ id: "bob", project: [".bob", "skills"], global: [".bob", "skills"], detect: [[".bob"]] },
|
|
708
|
-
{ id: "codearts-agent", project: [".codeartsdoer", "skills"], global: [".codeartsdoer", "skills"], detect: [[".codeartsdoer"]] },
|
|
709
|
-
{ id: "codebuddy", project: [".codebuddy", "skills"], global: [".codebuddy", "skills"], detect: [[".codebuddy"]] },
|
|
710
|
-
{ id: "codemaker", project: [".codemaker", "skills"], global: [".codemaker", "skills"], detect: [[".codemaker"]] },
|
|
711
|
-
{ id: "codestudio", project: [".codestudio", "skills"], global: [".codestudio", "skills"], detect: [[".codestudio"]] },
|
|
712
|
-
{ id: "command-code", project: [".commandcode", "skills"], global: [".commandcode", "skills"], detect: [[".commandcode"]] },
|
|
713
|
-
{ id: "cortex", project: [".cortex", "skills"], global: [".snowflake", "cortex", "skills"], detect: [[".snowflake", "cortex"]] },
|
|
714
|
-
{ id: "crush", project: [".crush", "skills"], global: [".config", "crush", "skills"], detect: [[".config", "crush"]] },
|
|
715
|
-
{ id: "deepagents", project: [".agents", "skills"], global: [".deepagents", "agent", "skills"], detect: [[".deepagents"]] },
|
|
716
|
-
{ id: "dexto", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".dexto"]] },
|
|
717
|
-
{ id: "droid", project: [".factory", "skills"], global: [".factory", "skills"], detect: [[".factory"]] },
|
|
718
|
-
{ id: "firebender", project: [".agents", "skills"], global: [".firebender", "skills"], detect: [[".firebender"]] },
|
|
719
|
-
{ id: "forgecode", project: [".forge", "skills"], global: [".forge", "skills"], detect: [[".forge"]] },
|
|
720
|
-
{ id: "iflow-cli", project: [".iflow", "skills"], global: [".iflow", "skills"], detect: [[".iflow"]] },
|
|
721
|
-
{ id: "inference-sh", project: [".inferencesh", "skills"], global: [".inferencesh", "skills"], detect: [[".inferencesh"]] },
|
|
722
|
-
{ id: "jazz", project: [".jazz", "skills"], global: [".jazz", "skills"], detect: [[".jazz"]] },
|
|
723
|
-
{ id: "junie", project: [".junie", "skills"], global: [".junie", "skills"], detect: [[".junie"]] },
|
|
724
|
-
{ id: "kimi-code-cli", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".kimi-code"], [".kimi"]] },
|
|
725
|
-
{ id: "kode", project: [".kode", "skills"], global: [".kode", "skills"], detect: [[".kode"]] },
|
|
726
|
-
{ id: "lingma", project: [".lingma", "skills"], global: [".lingma", "skills"], detect: [[".lingma"]] },
|
|
727
|
-
{ id: "loaf", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".loaf"]] },
|
|
728
|
-
{ id: "mcpjam", project: [".mcpjam", "skills"], global: [".mcpjam", "skills"], detect: [[".mcpjam"]] },
|
|
729
|
-
{ id: "moxby", project: [".moxby", "skills"], global: [".moxby", "skills"], detect: [[".moxby"]] },
|
|
730
|
-
{ id: "mux", project: [".mux", "skills"], global: [".mux", "skills"], detect: [[".mux"]] },
|
|
731
|
-
{ id: "neovate", project: [".neovate", "skills"], global: [".neovate", "skills"], detect: [[".neovate"]] },
|
|
732
|
-
{ id: "ona", project: [".ona", "skills"], global: [".ona", "skills"], detect: [[".ona"]] },
|
|
733
|
-
{ id: "pi", project: [".pi", "skills"], global: [".pi", "agent", "skills"], detect: [[".pi", "agent"]] },
|
|
734
|
-
{ id: "pochi", project: [".pochi", "skills"], global: [".pochi", "skills"], detect: [[".pochi"]] },
|
|
735
|
-
{ id: "qoder", project: [".qoder", "skills"], global: [".qoder", "skills"], detect: [[".qoder"]] },
|
|
736
|
-
{ id: "qoder-cn", project: [".qoder", "skills"], global: [".qoder-cn", "skills"], detect: [[".qoder-cn"]] },
|
|
737
|
-
{ id: "reasonix", project: [".reasonix", "skills"], global: [".reasonix", "skills"], detect: [[".reasonix"]] },
|
|
738
|
-
{ id: "rovodev", project: [".rovodev", "skills"], global: [".rovodev", "skills"], detect: [[".rovodev"]] },
|
|
739
|
-
{ id: "tabnine-cli", project: [".tabnine", "agent", "skills"], global: [".tabnine", "agent", "skills"], detect: [[".tabnine"]] },
|
|
740
|
-
{ id: "terramind", project: [".terramind", "skills"], global: [".terramind", "skills"], detect: [[".terramind"]] },
|
|
741
|
-
{ id: "tinycloud", project: [".tinycloud", "skills"], global: [".tinycloud", "skills"], detect: [[".tinycloud"]] },
|
|
742
|
-
{ id: "trae-cn", project: [".trae", "skills"], global: [".trae-cn", "skills"], detect: [[".trae-cn"]] },
|
|
743
|
-
{ id: "zcode", project: [".zcode", "skills"], global: [".zcode", "skills"], detect: [[".zcode"]] },
|
|
744
|
-
{ id: "zencoder", project: [".zencoder", "skills"], global: [".zencoder", "skills"], detect: [[".zencoder"]] }
|
|
745
|
-
];
|
|
746
|
-
function agentDir(agentId, opts = {}) {
|
|
747
|
-
const agent = AGENTS.find((a) => a.id === agentId);
|
|
748
|
-
if (!agent) throw new Error(`Unknown agent: ${agentId}. Known: ${AGENTS.map((a) => a.id).join(", ")}`);
|
|
749
|
-
return opts.global ? join4(opts.home ?? homedir3(), ...agent.global) : join4(opts.cwd ?? process.cwd(), ...agent.project);
|
|
750
|
-
}
|
|
751
|
-
function detectAgents(opts = {}) {
|
|
752
|
-
const home = opts.home ?? homedir3();
|
|
753
|
-
return AGENTS.filter((a) => a.detect.some((d) => existsSync3(join4(home, ...d)))).map((a) => a.id);
|
|
754
|
-
}
|
|
755
|
-
function writeSkill(skillName, files, targetDir, opts = {}) {
|
|
756
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(skillName) || skillName.includes("..")) {
|
|
757
|
-
throw new Error(`refused unsafe skill name: "${skillName}"`);
|
|
758
|
-
}
|
|
759
|
-
const dest = join4(targetDir, skillName);
|
|
760
|
-
if (opts.mode === "symlink" && opts.canonicalDir) {
|
|
761
|
-
mkdirSync3(dirname2(dest), { recursive: true });
|
|
762
|
-
rmSync2(dest, { recursive: true, force: true });
|
|
763
|
-
symlinkSync(resolve2(opts.canonicalDir), dest, "dir");
|
|
764
|
-
return dest;
|
|
765
|
-
}
|
|
766
|
-
mkdirSync3(dest, { recursive: true });
|
|
767
|
-
const root = resolve2(dest);
|
|
768
|
-
for (const f of files) {
|
|
769
|
-
const fp = resolve2(dest, f.path);
|
|
770
|
-
if (fp !== root && !fp.startsWith(root + sep2)) {
|
|
771
|
-
throw new Error(`refused unsafe path in skill "${skillName}": ${f.path}`);
|
|
772
|
-
}
|
|
773
|
-
mkdirSync3(dirname2(fp), { recursive: true });
|
|
774
|
-
writeFileSync4(fp, f.contents);
|
|
625
|
+
import * as p3 from "@clack/prompts";
|
|
626
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
627
|
+
import pc8 from "picocolors";
|
|
628
|
+
import { lint as lint2, parseSkillMd as parseSkillMd2 } from "@skillmds/core";
|
|
629
|
+
var infoNotice = (text3) => ({ text: text3, style: "info" });
|
|
630
|
+
var warnNotice = (text3) => ({ text: text3, style: "warn" });
|
|
631
|
+
var skipNotice = (text3) => ({ text: text3, style: "skip" });
|
|
632
|
+
var replacedNotice = (text3) => ({ text: text3, style: "warn", suppressInSummary: true });
|
|
633
|
+
function noticeLine(n) {
|
|
634
|
+
switch (n.style) {
|
|
635
|
+
case "info":
|
|
636
|
+
return pc8.dim(`${GLYPH.info} ${n.text}`);
|
|
637
|
+
case "skip":
|
|
638
|
+
return pc8.dim(`${GLYPH.skip} ${n.text}`);
|
|
639
|
+
case "warn":
|
|
640
|
+
return pc8.yellow(`${GLYPH.warn} ${n.text}`);
|
|
775
641
|
}
|
|
776
|
-
return dest;
|
|
777
642
|
}
|
|
778
|
-
function
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
643
|
+
async function promptScopeInteractive(ctx) {
|
|
644
|
+
const choice = await p3.select({
|
|
645
|
+
message: "Installation scope",
|
|
646
|
+
initialValue: ctx.suggestGlobal,
|
|
647
|
+
options: [
|
|
648
|
+
{ value: false, label: "Project", hint: `install in ${ctx.cwd} (committed with your project)` },
|
|
649
|
+
{ value: true, label: "Global", hint: "install in your home directory (available across all projects)" }
|
|
650
|
+
]
|
|
651
|
+
});
|
|
652
|
+
return p3.isCancel(choice) ? null : choice;
|
|
653
|
+
}
|
|
654
|
+
async function promptAgentsInteractive(ctx) {
|
|
655
|
+
if (ctx.detected.length < 2) return ctx.detected;
|
|
656
|
+
const remembered = (readConfig().lastAgents ?? []).filter((id) => ctx.detected.includes(id));
|
|
657
|
+
const picked = await p3.multiselect({
|
|
658
|
+
message: "Which agents do you want to install to?",
|
|
659
|
+
options: ctx.detected.map((id) => ({ value: id, label: id, hint: ctx.dirFor(id) })),
|
|
660
|
+
initialValues: remembered.length ? remembered : ctx.detected,
|
|
661
|
+
required: true
|
|
662
|
+
});
|
|
663
|
+
return p3.isCancel(picked) ? null : picked;
|
|
794
664
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync4(arg);
|
|
799
|
-
}
|
|
800
|
-
async function resolvePackFiles(skill, fetchTree = resolveTree) {
|
|
801
|
-
if (!skill.source_repo) return null;
|
|
802
|
-
const attempts = [{ ref: skill.commit_sha ?? void 0, pinMiss: false }];
|
|
803
|
-
if (skill.commit_sha) attempts.push({ ref: void 0, pinMiss: true });
|
|
804
|
-
for (const attempt of attempts) {
|
|
805
|
-
try {
|
|
806
|
-
const files = await fetchTree(skill.source_repo, attempt.ref);
|
|
807
|
-
const sk = files.find((f) => f.path === "SKILL.md");
|
|
808
|
-
if (sk) return { raw: sk.contents.toString("utf8"), files, pinMiss: attempt.pinMiss };
|
|
809
|
-
} catch {
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return null;
|
|
665
|
+
function unverifiedFilesNote(n) {
|
|
666
|
+
const [f, it] = n === 1 ? ["file", "it"] : ["files", "them"];
|
|
667
|
+
return `${n} companion ${f} came straight from the source repo \u2014 the registry holds no checksum for ${it}, so only the connection was verified`;
|
|
813
668
|
}
|
|
814
669
|
function registryFallbackNote(host) {
|
|
815
670
|
return `installed from GitHub because the SkillMD registry (${host}) was unreachable \u2014 this copy is not the registry-reviewed version`;
|
|
@@ -822,39 +677,76 @@ If this environment restricts network access (sandbox, proxy, CI), ask an admini
|
|
|
822
677
|
Registry error: ${e.message}`
|
|
823
678
|
);
|
|
824
679
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
680
|
+
function argWithRef(arg, ref) {
|
|
681
|
+
if (!ref) return arg;
|
|
682
|
+
try {
|
|
683
|
+
const spec = parseSource(arg, { ref });
|
|
684
|
+
return spec.kind === "github" ? sourceId(spec) : arg;
|
|
685
|
+
} catch {
|
|
686
|
+
return arg;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function provenanceFor(arg) {
|
|
690
|
+
try {
|
|
691
|
+
return sourceId(parseSource(arg));
|
|
692
|
+
} catch {
|
|
693
|
+
return `unknown:${arg}`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function makeDefaultDeps(flags) {
|
|
697
|
+
const canPrompt = () => isInteractive(flags, {
|
|
698
|
+
stdinTTY: Boolean(process.stdin.isTTY),
|
|
699
|
+
stdoutTTY: Boolean(process.stdout.isTTY),
|
|
700
|
+
env: flags.env ?? process.env
|
|
701
|
+
});
|
|
702
|
+
return {
|
|
703
|
+
async resolve(arg, f) {
|
|
704
|
+
const spec = parseSource(arg, { ref: f.ref });
|
|
705
|
+
const source = sourceId(spec);
|
|
706
|
+
if (spec.kind !== "slug") {
|
|
707
|
+
const rs = await resolveSource(argWithRef(arg, f.ref));
|
|
708
|
+
return rs.map((r) => ({
|
|
709
|
+
name: r.slug,
|
|
710
|
+
raw: r.raw,
|
|
711
|
+
slug: r.slug,
|
|
712
|
+
source,
|
|
713
|
+
...spec.kind === "local" && r.file ? { files: collectFiles(dirname(r.file), dirname(r.file)) } : {}
|
|
714
|
+
}));
|
|
837
715
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
716
|
+
let registryDown = null;
|
|
717
|
+
const { api, base: apiBase, token } = createClient(f);
|
|
718
|
+
const registryHost = (() => {
|
|
719
|
+
try {
|
|
720
|
+
return new URL(apiBase).host;
|
|
721
|
+
} catch {
|
|
722
|
+
return "api.skillmd.com";
|
|
723
|
+
}
|
|
724
|
+
})();
|
|
725
|
+
const registrySlug = `${spec.owner}/${spec.name}`;
|
|
842
726
|
try {
|
|
843
|
-
const skill = await api(`/api/skills/${owner}/${name}`);
|
|
844
|
-
const skillName = skill.slug.split("/")[1] || name;
|
|
727
|
+
const skill = await api(`/api/skills/${spec.owner}/${spec.name}`);
|
|
728
|
+
const skillName = skill.slug.split("/")[1] || spec.name;
|
|
845
729
|
const meta = {
|
|
846
730
|
name: skillName,
|
|
847
731
|
slug: skillName,
|
|
848
|
-
registrySlug
|
|
732
|
+
registrySlug,
|
|
733
|
+
source,
|
|
734
|
+
commit_sha: skill.commit_sha ?? void 0,
|
|
849
735
|
verified: skill.verified,
|
|
850
736
|
type: skill.type,
|
|
851
737
|
securityFlags: skill.security_flags
|
|
852
738
|
};
|
|
853
739
|
if (skill.type === "pack") {
|
|
854
|
-
const bundle = await fetchBundle(apiBase,
|
|
740
|
+
const bundle = await fetchBundle(apiBase, registrySlug, token);
|
|
855
741
|
if (bundle && bundle.length) {
|
|
856
|
-
const sk = bundle.find((
|
|
857
|
-
|
|
742
|
+
const sk = bundle.find((file) => file.path === "SKILL.md");
|
|
743
|
+
const unverified = bundle.filter((file) => file.unverified).length;
|
|
744
|
+
return [{
|
|
745
|
+
...meta,
|
|
746
|
+
raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill),
|
|
747
|
+
files: bundle,
|
|
748
|
+
...unverified ? { note: unverifiedFilesNote(unverified) } : {}
|
|
749
|
+
}];
|
|
858
750
|
}
|
|
859
751
|
const pack = await resolvePackFiles(skill);
|
|
860
752
|
if (pack) return [{ ...meta, ...pack }];
|
|
@@ -864,206 +756,644 @@ var defaultDeps = {
|
|
|
864
756
|
if (e instanceof IntegrityError) throw e;
|
|
865
757
|
if (e instanceof RegistryError && e.status !== 404) registryDown = e;
|
|
866
758
|
}
|
|
759
|
+
const ghSource = sourceId({ kind: "github", owner: spec.owner, repo: spec.name, display: arg });
|
|
760
|
+
try {
|
|
761
|
+
const rs = await resolveSource(arg);
|
|
762
|
+
const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
|
|
763
|
+
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, source: ghSource, note: note2 }));
|
|
764
|
+
} catch (e) {
|
|
765
|
+
if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
|
|
766
|
+
throw e;
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
get promptScope() {
|
|
770
|
+
return canPrompt() ? promptScopeInteractive : void 0;
|
|
771
|
+
},
|
|
772
|
+
get promptAgents() {
|
|
773
|
+
return canPrompt() ? promptAgentsInteractive : void 0;
|
|
774
|
+
},
|
|
775
|
+
fireInstall(registrySlug, f) {
|
|
776
|
+
const { api } = createClient(f);
|
|
777
|
+
const [owner, name] = registrySlug.split("/");
|
|
778
|
+
void api(`/api/skills/${owner}/${name}/install`, {
|
|
779
|
+
method: "POST",
|
|
780
|
+
headers: { "content-type": "application/json" },
|
|
781
|
+
body: JSON.stringify({ via: "cli" })
|
|
782
|
+
}).catch(() => {
|
|
783
|
+
});
|
|
867
784
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
body: JSON.stringify({ via: "cli" })
|
|
884
|
-
}).catch(() => {
|
|
885
|
-
});
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
async function resolveScope(flags, deps) {
|
|
788
|
+
const home = resolve3(flags.home ?? homedir3());
|
|
789
|
+
const cwd = resolve3(flags.cwd ?? process.cwd());
|
|
790
|
+
const base = { cwd: flags.cwd, home: flags.home, env: flags.env };
|
|
791
|
+
if (flags.global) return { scope: { global: true, ...base } };
|
|
792
|
+
if (flags.project) return { scope: { global: false, ...base } };
|
|
793
|
+
if (cwd === home) return { scope: { global: true, ...base } };
|
|
794
|
+
const isProject = looksLikeProject(cwd);
|
|
795
|
+
const hosted = detectHostAgent(flags.env ?? process.env) !== null;
|
|
796
|
+
if (deps.promptScope && !flags.yes && !flags.json && !hosted) {
|
|
797
|
+
const global = await deps.promptScope({ cwd, suggestGlobal: !isProject });
|
|
798
|
+
if (global === null) return null;
|
|
799
|
+
return { scope: { global, ...base } };
|
|
886
800
|
}
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
801
|
+
if (isProject) return { scope: { global: false, ...base } };
|
|
802
|
+
return {
|
|
803
|
+
scope: { global: true, ...base },
|
|
804
|
+
note: `no project detected in ${cwd} \u2014 installing to your user-level agent folders (pass --project to install here)`
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function defaultNeedsPrompt(flags) {
|
|
808
|
+
const env = flags.env ?? process.env;
|
|
809
|
+
if (flags.yes || flags.json || flags.global || flags.project || flags.list) return false;
|
|
810
|
+
if (detectHostAgent(env)) return false;
|
|
811
|
+
if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ?? homedir3())) return false;
|
|
812
|
+
return !isInteractive(flags, { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env });
|
|
813
|
+
}
|
|
814
|
+
var CANCELLED_TEXT = "installation cancelled \u2014 nothing was installed";
|
|
815
|
+
function finish2(flags, outcome) {
|
|
816
|
+
const json = Boolean(flags.json);
|
|
817
|
+
switch (outcome.kind) {
|
|
818
|
+
case "install": {
|
|
819
|
+
const installed = outcome.records.flatMap((r) => r.kind === "installed" ? [r.doc] : []);
|
|
820
|
+
const blocked = outcome.records.flatMap((r) => r.kind === "blocked" ? [{ name: r.name, reason: r.reason }] : []);
|
|
821
|
+
const written = installed.flatMap((d) => d.targets.map((t) => ({ name: d.name, dir: t.path })));
|
|
822
|
+
const warnings = [
|
|
823
|
+
...outcome.notices,
|
|
824
|
+
...outcome.records.flatMap((r) => r.kind === "installed" ? r.notices : [])
|
|
825
|
+
].map((n) => n.text);
|
|
826
|
+
const exitCode = blocked.length > 0 || installed.length === 0 ? 1 : 0;
|
|
827
|
+
const doc = { ok: exitCode === 0, scope: outcome.scope, installed, blocked, warnings };
|
|
828
|
+
return {
|
|
829
|
+
written,
|
|
830
|
+
blocked,
|
|
831
|
+
exitCode,
|
|
832
|
+
output: json ? JSON.stringify(doc, null, 2) : renderHuman(outcome.notices, outcome.records, outcome.paths)
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
case "list": {
|
|
836
|
+
const human = outcome.skills.length ? outcome.skills.map((s) => `${pc8.bold(s.name)} ${pc8.dim(s.description)}`).join("\n") : "No skills found.";
|
|
837
|
+
return { written: [], blocked: [], exitCode: 0, output: json ? JSON.stringify({ ok: true, skills: outcome.skills }, null, 2) : human };
|
|
838
|
+
}
|
|
839
|
+
case "cancel": {
|
|
840
|
+
const doc = { ok: false, error: CANCELLED_TEXT };
|
|
894
841
|
return {
|
|
895
842
|
written: [],
|
|
896
|
-
blocked: [
|
|
897
|
-
exitCode:
|
|
898
|
-
|
|
843
|
+
blocked: [],
|
|
844
|
+
exitCode: 0,
|
|
845
|
+
cancelled: true,
|
|
846
|
+
output: json ? JSON.stringify(doc, null, 2) : pc8.dim("Installation cancelled \u2014 nothing was installed.")
|
|
899
847
|
};
|
|
900
848
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const detected = detectAgents(scope);
|
|
912
|
-
const targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
|
|
913
|
-
const dirAgents = /* @__PURE__ */ new Map();
|
|
914
|
-
for (const agentId of targets) {
|
|
915
|
-
const dir = agentDir(agentId, scope);
|
|
916
|
-
dirAgents.set(dir, [...dirAgents.get(dir) ?? [], agentId]);
|
|
917
|
-
}
|
|
918
|
-
for (const c of candidates) {
|
|
919
|
-
const result = lint2(c.raw, { slug: c.slug });
|
|
920
|
-
const allFlags = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
|
|
921
|
-
const deniedFlags = allFlags.filter((f) => deny.has(f));
|
|
922
|
-
if (!flags.skipLint && !result.ok) {
|
|
923
|
-
blocked.push({ name: c.name, reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}` });
|
|
924
|
-
lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
|
|
925
|
-
continue;
|
|
849
|
+
case "fail": {
|
|
850
|
+
const warnings = (outcome.notices ?? []).map((n) => n.text);
|
|
851
|
+
const doc = { ok: false, error: outcome.error };
|
|
852
|
+
if (outcome.blocked?.length) doc.blocked = outcome.blocked;
|
|
853
|
+
if (warnings.length) doc.warnings = warnings;
|
|
854
|
+
const human = [
|
|
855
|
+
...(outcome.notices ?? []).map(noticeLine),
|
|
856
|
+
...outcome.blocked?.length ? outcome.blocked.map((b) => pc8.red(`${GLYPH.blocked} ${b.name} blocked \u2014 ${b.reason}`)) : [pc8.red(outcome.error)]
|
|
857
|
+
].join("\n");
|
|
858
|
+
return { written: [], blocked: outcome.blocked ?? [], exitCode: 1, output: json ? JSON.stringify(doc, null, 2) : human };
|
|
926
859
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function renderHuman(notices, records, paths) {
|
|
863
|
+
const lines = notices.map(noticeLine);
|
|
864
|
+
for (const r of records) {
|
|
865
|
+
if (r.kind === "blocked") {
|
|
866
|
+
lines.push(pc8.red(r.message));
|
|
930
867
|
continue;
|
|
931
868
|
}
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
869
|
+
const { doc } = r;
|
|
870
|
+
lines.push(renderInstallSummary({
|
|
871
|
+
name: doc.name,
|
|
872
|
+
score: r.score,
|
|
873
|
+
flags: r.securityFlags,
|
|
874
|
+
source: r.source,
|
|
875
|
+
canonical: doc.canonical,
|
|
876
|
+
targets: doc.targets,
|
|
877
|
+
skipped: doc.skipped,
|
|
878
|
+
...r.replaced ? { replaced: r.replaced } : {}
|
|
879
|
+
}, paths));
|
|
880
|
+
for (const n of r.notices) if (!n.suppressInSummary) lines.push(noticeLine(n));
|
|
881
|
+
}
|
|
882
|
+
return lines.join("\n");
|
|
883
|
+
}
|
|
884
|
+
async function resolveCandidates(arg, flags, deps) {
|
|
885
|
+
const candidates = await deps.resolve(arg, flags);
|
|
886
|
+
if (!flags.skill?.length) return candidates;
|
|
887
|
+
return candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
|
|
888
|
+
}
|
|
889
|
+
function gate(flags, deps, candidates) {
|
|
890
|
+
if (flags.list) {
|
|
891
|
+
return finish2(flags, {
|
|
892
|
+
kind: "list",
|
|
893
|
+
skills: candidates.map((c) => {
|
|
894
|
+
const parsed = parseSkillMd2(c.raw);
|
|
895
|
+
const description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
|
|
896
|
+
return { name: c.name, description: safeText(description, 80) };
|
|
897
|
+
})
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
if (flags.json && !flags.yes) {
|
|
901
|
+
return finish2(flags, { kind: "fail", error: "--json requires -y (no prompts can be shown in JSON mode)" });
|
|
902
|
+
}
|
|
903
|
+
if (deps.needsPrompt ?? (!deps.promptScope && defaultNeedsPrompt(flags))) {
|
|
904
|
+
return finish2(flags, { kind: "fail", error: nonInteractiveHint("-y (auto-detects the scope) or -g / -p") });
|
|
905
|
+
}
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
async function chooseTargets(flags, deps, scope, detected) {
|
|
909
|
+
const notices = [];
|
|
910
|
+
let targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
|
|
911
|
+
if (scope.global) {
|
|
912
|
+
const kept = [];
|
|
913
|
+
for (const id of targets) {
|
|
914
|
+
if (agentSupportsGlobal(id)) kept.push(id);
|
|
915
|
+
else notices.push(skipNotice(`${id}: project-only agent`));
|
|
940
916
|
}
|
|
941
|
-
|
|
942
|
-
|
|
917
|
+
targets = kept;
|
|
918
|
+
}
|
|
919
|
+
if (!flags.agent?.length && deps.promptAgents && !flags.yes && !flags.json && !detectHostAgent(flags.env ?? process.env)) {
|
|
920
|
+
const picked = await deps.promptAgents({ detected: targets, dirFor: (id) => agentDir(id, scope) });
|
|
921
|
+
if (picked === null) return null;
|
|
922
|
+
targets = picked;
|
|
923
|
+
if (flags.home === void 0 && picked.length) {
|
|
924
|
+
try {
|
|
925
|
+
writeConfig({ ...readConfig(), lastAgents: picked });
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
943
928
|
}
|
|
944
|
-
|
|
945
|
-
|
|
929
|
+
}
|
|
930
|
+
return { targets, notices };
|
|
931
|
+
}
|
|
932
|
+
async function installOne(c, ctx) {
|
|
933
|
+
const result = lint2(c.raw, { slug: c.slug });
|
|
934
|
+
const securityFlags2 = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
|
|
935
|
+
const deniedFlags = securityFlags2.filter((f) => ctx.deny.has(f));
|
|
936
|
+
if (!ctx.flags.skipLint && !result.ok) {
|
|
937
|
+
return {
|
|
938
|
+
kind: "blocked",
|
|
939
|
+
name: c.name,
|
|
940
|
+
reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}`,
|
|
941
|
+
message: `${GLYPH.blocked} ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
if (deniedFlags.length) {
|
|
945
|
+
return {
|
|
946
|
+
kind: "blocked",
|
|
947
|
+
name: c.name,
|
|
948
|
+
reason: `denied security flags: ${deniedFlags.join(", ")}`,
|
|
949
|
+
message: `${GLYPH.blocked} ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const source = c.source ?? (c.registrySlug ? `registry:${c.registrySlug}` : provenanceFor(ctx.arg));
|
|
953
|
+
let out;
|
|
954
|
+
try {
|
|
955
|
+
out = await installSkill({
|
|
956
|
+
name: c.name,
|
|
957
|
+
files: c.files ?? [{ path: "SKILL.md", contents: c.raw }],
|
|
958
|
+
source,
|
|
959
|
+
commit_sha: c.commit_sha,
|
|
960
|
+
scope: ctx.scope,
|
|
961
|
+
agents: ctx.targets,
|
|
962
|
+
explicitAgents: Boolean(ctx.flags.agent?.length),
|
|
963
|
+
mode: ctx.flags.mode,
|
|
964
|
+
force: ctx.flags.force
|
|
965
|
+
});
|
|
966
|
+
} catch (e) {
|
|
967
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
968
|
+
return { kind: "blocked", name: c.name, reason, message: `${GLYPH.blocked} ${c.name} \u2014 ${reason}` };
|
|
969
|
+
}
|
|
970
|
+
const notices = [];
|
|
971
|
+
if (out.replaced) notices.push(replacedNotice(`${c.name}: replaced previous install (${out.replaced})`));
|
|
972
|
+
if (c.pinMiss) notices.push(warnNotice(`${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
|
|
973
|
+
if (c.note) notices.push(warnNotice(`${c.name}: ${c.note}`));
|
|
974
|
+
if (c.type === "pack" && !c.files) {
|
|
975
|
+
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.`));
|
|
976
|
+
}
|
|
977
|
+
if (c.registrySlug && !telemetryDisabled(ctx.env)) ctx.deps.fireInstall(c.registrySlug, ctx.flags);
|
|
978
|
+
return {
|
|
979
|
+
kind: "installed",
|
|
980
|
+
doc: { name: c.name, canonical: out.canonical, targets: out.targets, skipped: out.skipped, digest: out.digest },
|
|
981
|
+
score: result.score,
|
|
982
|
+
securityFlags: securityFlags2,
|
|
983
|
+
notices,
|
|
984
|
+
source,
|
|
985
|
+
...out.replaced ? { replaced: out.replaced } : {}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
async function runAdd(arg, flags, deps) {
|
|
989
|
+
const d = deps ?? makeDefaultDeps(flags);
|
|
990
|
+
const env = flags.env ?? process.env;
|
|
991
|
+
let candidates;
|
|
992
|
+
try {
|
|
993
|
+
candidates = await resolveCandidates(arg, flags, d);
|
|
994
|
+
} catch (e) {
|
|
995
|
+
if (e instanceof IntegrityError) {
|
|
996
|
+
return finish2(flags, { kind: "fail", error: e.message, blocked: [{ name: arg, reason: e.message }] });
|
|
946
997
|
}
|
|
947
|
-
if (
|
|
998
|
+
if (flags.json) return finish2(flags, { kind: "fail", error: e instanceof Error ? e.message : String(e) });
|
|
999
|
+
throw e;
|
|
948
1000
|
}
|
|
949
|
-
const
|
|
950
|
-
|
|
1001
|
+
const early = gate(flags, d, candidates);
|
|
1002
|
+
if (early) return early;
|
|
1003
|
+
const resolved = await resolveScope(flags, d);
|
|
1004
|
+
if (!resolved) return finish2(flags, { kind: "cancel" });
|
|
1005
|
+
const scope = resolved.scope;
|
|
1006
|
+
const notices = resolved.note ? [infoNotice(resolved.note)] : [];
|
|
1007
|
+
const chosen = await chooseTargets(flags, d, scope, detectAgents(scope));
|
|
1008
|
+
if (!chosen) return finish2(flags, { kind: "cancel" });
|
|
1009
|
+
notices.push(...chosen.notices);
|
|
1010
|
+
const ctx = { arg, flags, deny: new Set(flags.deny ?? []), scope, targets: chosen.targets, env, deps: d };
|
|
1011
|
+
const records = [];
|
|
1012
|
+
for (const c of candidates) records.push(await installOne(c, ctx));
|
|
1013
|
+
const paths = { home: flags.home ?? homedir3(), cwd: flags.cwd ?? process.cwd() };
|
|
1014
|
+
return finish2(flags, { kind: "install", scope: scope.global ? "global" : "project", records, notices, paths });
|
|
1015
|
+
}
|
|
1016
|
+
function addFlags(opts, parent) {
|
|
1017
|
+
const merged = { ...parent, ...opts };
|
|
1018
|
+
if (opts.copy) merged.mode = "copy";
|
|
1019
|
+
return merged;
|
|
951
1020
|
}
|
|
952
1021
|
function addCommand() {
|
|
953
|
-
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
|
|
954
|
-
const merged =
|
|
955
|
-
const run = await
|
|
956
|
-
|
|
1022
|
+
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) => {
|
|
1023
|
+
const merged = addFlags(opts, cmd.parent?.opts());
|
|
1024
|
+
const { run, printed } = await runAddWithUI(source, merged);
|
|
1025
|
+
if (!printed) {
|
|
1026
|
+
if (run.exitCode === 1 && !merged.json) console.error(run.output);
|
|
1027
|
+
else console.log(run.output);
|
|
1028
|
+
}
|
|
957
1029
|
process.exitCode = run.exitCode;
|
|
958
1030
|
});
|
|
959
1031
|
}
|
|
1032
|
+
var clackUI = {
|
|
1033
|
+
intro: (m) => p3.intro(m),
|
|
1034
|
+
spinner: () => p3.spinner(),
|
|
1035
|
+
message: (m) => p3.log.message(m),
|
|
1036
|
+
outro: (m) => p3.outro(m),
|
|
1037
|
+
cancel: (m) => p3.cancel(m)
|
|
1038
|
+
};
|
|
1039
|
+
async function runAddWithUI(source, flags, opts = {}) {
|
|
1040
|
+
const term = opts.term ?? { ...realTerminal(), env: flags.env ?? process.env };
|
|
1041
|
+
const base = opts.deps ?? makeDefaultDeps(flags);
|
|
1042
|
+
if (!isInteractive(flags, term)) return { run: await runAdd(source, flags, base), printed: false };
|
|
1043
|
+
const ui = opts.ui ?? clackUI;
|
|
1044
|
+
ui.intro(banner());
|
|
1045
|
+
try {
|
|
1046
|
+
const run = await runAdd(source, flags, {
|
|
1047
|
+
// NOTE: spreading `base` reads makeDefaultDeps()'s promptScope/promptAgents
|
|
1048
|
+
// getters right here, freezing their values for this run. That is exactly
|
|
1049
|
+
// what we want (interactivity is decided once, up front), but it does mean
|
|
1050
|
+
// a getter added to AddDeps later stops being lazy the moment it crosses
|
|
1051
|
+
// this spread.
|
|
1052
|
+
...base,
|
|
1053
|
+
resolve: async (arg, f) => {
|
|
1054
|
+
const s = ui.spinner();
|
|
1055
|
+
s.start(`Resolving ${arg}`);
|
|
1056
|
+
try {
|
|
1057
|
+
const c = await base.resolve(arg, f);
|
|
1058
|
+
s.stop(`${c.length} skill${c.length === 1 ? "" : "s"} found`);
|
|
1059
|
+
return c;
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
s.stop(pc8.red("Could not resolve the source"));
|
|
1062
|
+
throw e;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
if (run.cancelled) {
|
|
1067
|
+
ui.cancel("Installation cancelled \u2014 nothing was installed.");
|
|
1068
|
+
return { run, printed: true };
|
|
1069
|
+
}
|
|
1070
|
+
ui.message(run.output);
|
|
1071
|
+
ui.outro(run.exitCode === 0 ? `Done. Run ${pc8.cyan("skillmd list")} to see everything installed.` : pc8.red("Some skills were not installed."));
|
|
1072
|
+
return { run, printed: true };
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
ui.outro(pc8.red("Nothing was installed."));
|
|
1075
|
+
throw e;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
960
1078
|
|
|
961
1079
|
// src/commands/list.ts
|
|
962
1080
|
import { Command as Command8 } from "commander";
|
|
963
|
-
import
|
|
964
|
-
import {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const desc = "error" in parsed ? pc8.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc8.dim(parsed.description.slice(0, 80));
|
|
980
|
-
console.log(`${pc8.bold(s.name)} ${pc8.dim(`[${s.agent}/${s.scope}]`)}
|
|
981
|
-
${desc}`);
|
|
1081
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
1082
|
+
import { join as join3 } from "path";
|
|
1083
|
+
import pc9 from "picocolors";
|
|
1084
|
+
import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
|
|
1085
|
+
function runList(flags = {}) {
|
|
1086
|
+
const scopes = scopesFor(flags);
|
|
1087
|
+
let items = [];
|
|
1088
|
+
for (const global of scopes) {
|
|
1089
|
+
for (const s of listInstalled({ global, cwd: flags.cwd, home: flags.home })) {
|
|
1090
|
+
const md = join3(s.path, "SKILL.md");
|
|
1091
|
+
let description = "";
|
|
1092
|
+
if (existsSync2(md)) {
|
|
1093
|
+
const parsed = parseSkillMd3(readFileSync(md, "utf8"));
|
|
1094
|
+
description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
|
|
1095
|
+
}
|
|
1096
|
+
items.push({ ...s, description: safeText(description, 80) });
|
|
982
1097
|
}
|
|
1098
|
+
}
|
|
1099
|
+
if (flags.agent?.length) items = items.filter((i) => i.agents.some((a) => flags.agent.includes(a)));
|
|
1100
|
+
items.sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
|
|
1101
|
+
if (flags.json) {
|
|
1102
|
+
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) };
|
|
1103
|
+
}
|
|
1104
|
+
if (items.length === 0) return { items, output: pc9.dim("No installed skills found. Install one with `skillmd add owner/name`.") };
|
|
1105
|
+
const w = Math.max(...items.map((i) => i.name.length), 4);
|
|
1106
|
+
const lines = items.map((i) => {
|
|
1107
|
+
const tag = i.tracked ? pc9.dim(i.source ?? "") : pc9.yellow("untracked");
|
|
1108
|
+
const modes = [...new Set(Object.values(i.mode))].join("/");
|
|
1109
|
+
return `${pc9.bold(i.name.padEnd(w))} ${pc9.cyan(i.scope.padEnd(7))} ${i.agents.join(", ").padEnd(24)} ${pc9.dim(modes.padEnd(9))} ${tag}
|
|
1110
|
+
${" ".repeat(w + 2)}${pc9.dim(i.description)}`;
|
|
1111
|
+
});
|
|
1112
|
+
return { items, output: lines.join("\n") };
|
|
1113
|
+
}
|
|
1114
|
+
function listCommand() {
|
|
1115
|
+
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) => {
|
|
1116
|
+
console.log(runList({ ...cmd.parent?.opts(), ...opts }).output);
|
|
983
1117
|
});
|
|
984
1118
|
}
|
|
985
1119
|
|
|
986
1120
|
// src/commands/remove.ts
|
|
987
1121
|
import { Command as Command9 } from "commander";
|
|
988
|
-
import { rmSync
|
|
989
|
-
import
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1122
|
+
import { rmSync } from "fs";
|
|
1123
|
+
import { join as join4, resolve as resolve4 } from "path";
|
|
1124
|
+
import * as p4 from "@clack/prompts";
|
|
1125
|
+
import pc10 from "picocolors";
|
|
1126
|
+
var defaultDeps = (flags) => isInteractive(flags) ? {
|
|
1127
|
+
confirm: async (msg) => {
|
|
1128
|
+
const a = await p4.confirm({ message: msg, initialValue: false });
|
|
1129
|
+
return !p4.isCancel(a) && a;
|
|
1130
|
+
},
|
|
1131
|
+
pick: async ({ names }) => {
|
|
1132
|
+
const a = await p4.multiselect({ message: "Which skills do you want to remove?", options: names.map((n) => ({ value: n, label: n })), required: true });
|
|
1133
|
+
return p4.isCancel(a) ? null : a;
|
|
1134
|
+
}
|
|
1135
|
+
} : {};
|
|
1136
|
+
function finish3(flags, result, doc) {
|
|
1137
|
+
return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
|
|
1138
|
+
}
|
|
1139
|
+
var scopeLabel = (scope) => scope.global ? "global" : "project";
|
|
1140
|
+
async function runRemove(names, flags, deps = defaultDeps(flags)) {
|
|
1141
|
+
const scopes = scopesFor(flags).map((global) => ({ global, cwd: flags.cwd, home: flags.home }));
|
|
1142
|
+
const where = scopes.map(scopeLabel).join(" and ");
|
|
994
1143
|
const lines = [];
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
removed.
|
|
998
|
-
lines.push(pc9.green(`\u2713 removed ${t.name} (${t.agent})`));
|
|
1144
|
+
if (flags.all && names.length) {
|
|
1145
|
+
const error = "--all cannot be combined with skill names";
|
|
1146
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
|
|
999
1147
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1148
|
+
const perScope = scopes.map((scope) => ({ scope, installed: listInstalled(scope) }));
|
|
1149
|
+
const allNames = [...new Set(perScope.flatMap((s) => s.installed.map((i) => i.name)))];
|
|
1150
|
+
const nothingInstalled = `No installed skills found in ${where} scope.`;
|
|
1151
|
+
let targets = names;
|
|
1152
|
+
if (flags.all) {
|
|
1153
|
+
if (allNames.length === 0) {
|
|
1154
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
|
|
1155
|
+
}
|
|
1156
|
+
targets = allNames;
|
|
1157
|
+
} else if (targets.length === 0) {
|
|
1158
|
+
if (!deps.pick) {
|
|
1159
|
+
const error = "Give one or more skill names, or --all.";
|
|
1160
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(`${error} `) + pc10.dim(nonInteractiveHint("skill names and -y")) }, { ok: false, error });
|
|
1161
|
+
}
|
|
1162
|
+
if (allNames.length === 0) {
|
|
1163
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
|
|
1164
|
+
}
|
|
1165
|
+
const picked = await deps.pick({ names: allNames });
|
|
1166
|
+
if (!picked) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
|
|
1167
|
+
targets = picked;
|
|
1168
|
+
}
|
|
1169
|
+
const known = targets.filter((n) => allNames.includes(n));
|
|
1170
|
+
const missing = targets.filter((n) => !known.includes(n));
|
|
1171
|
+
for (const n of missing) lines.push(pc10.yellow(`- ${n} not installed (${where} scope)`));
|
|
1172
|
+
if (known.length === 0) {
|
|
1173
|
+
const output = lines.join("\n");
|
|
1174
|
+
return finish3(flags, { removed: [], exitCode: 1, output }, { ok: false, error: `not installed: ${missing.join(", ")}`, missing });
|
|
1002
1175
|
}
|
|
1003
|
-
|
|
1176
|
+
if (!flags.yes) {
|
|
1177
|
+
const what = flags.agent?.length ? `from ${flags.agent.join(", ")}` : "from every agent";
|
|
1178
|
+
const ok = deps.confirm ? await deps.confirm(`Remove ${known.length} skill${known.length === 1 ? "" : "s"} (${known.join(", ")}) ${what} in ${where} scope?`) : null;
|
|
1179
|
+
if (ok === null) {
|
|
1180
|
+
const error = nonInteractiveHint("-y");
|
|
1181
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
|
|
1182
|
+
}
|
|
1183
|
+
if (!ok) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
|
|
1184
|
+
}
|
|
1185
|
+
const removed = [];
|
|
1186
|
+
const skipped = [];
|
|
1187
|
+
let changed = 0;
|
|
1188
|
+
for (const name of known) {
|
|
1189
|
+
for (const { scope, installed } of perScope) {
|
|
1190
|
+
if (!installed.some((s) => s.name === name)) continue;
|
|
1191
|
+
const tag = scopes.length > 1 ? pc10.dim(` [${scopeLabel(scope)}]`) : "";
|
|
1192
|
+
if (flags.agent?.length) {
|
|
1193
|
+
const lock = readLock({ global: Boolean(scope.global), cwd: scope.cwd, home: scope.home });
|
|
1194
|
+
const entry = lock.skills[name];
|
|
1195
|
+
const canonRoot = resolve4(canonicalDir(scope));
|
|
1196
|
+
let acted = false;
|
|
1197
|
+
for (const a of flags.agent) {
|
|
1198
|
+
let dir;
|
|
1199
|
+
try {
|
|
1200
|
+
dir = agentDir(a, scope);
|
|
1201
|
+
} catch (e) {
|
|
1202
|
+
lines.push(pc10.yellow(`- ${a}: ${e instanceof Error ? e.message : String(e)}`));
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (resolve4(dir) === canonRoot || entry?.mode[a] === "canonical") {
|
|
1206
|
+
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)`));
|
|
1207
|
+
skipped.push({ skill: name, agent: a, scope: scopeLabel(scope), reason: "reads the canonical copy directly" });
|
|
1208
|
+
acted = true;
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const pth = join4(dir, name);
|
|
1212
|
+
if (isSkillDirOrLink(pth)) {
|
|
1213
|
+
rmSync(pth, { recursive: true, force: true });
|
|
1214
|
+
removed.push(pth);
|
|
1215
|
+
acted = true;
|
|
1216
|
+
lines.push(pc10.green(`${GLYPH.ok} removed ${name} from ${a}`) + tag);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (!acted) lines.push(pc10.yellow(`- ${name} is not linked into ${flags.agent.join(", ")}`) + tag);
|
|
1220
|
+
if (entry) {
|
|
1221
|
+
const left = entry.agents.filter((a) => !flags.agent.includes(a));
|
|
1222
|
+
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))) });
|
|
1223
|
+
else {
|
|
1224
|
+
const r2 = uninstallSkill(name, scope);
|
|
1225
|
+
removed.push(...r2.removed);
|
|
1226
|
+
lines.push(pc10.dim(` (no agents left \u2014 removed the canonical copy too)`));
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (acted) changed++;
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const r = uninstallSkill(name, scope);
|
|
1233
|
+
removed.push(...r.removed);
|
|
1234
|
+
changed++;
|
|
1235
|
+
lines.push(pc10.green(`${GLYPH.ok} removed ${name}`) + tag + pc10.dim(` (${r.removed.length} path${r.removed.length === 1 ? "" : "s"}${r.untracked ? ", untracked" : ""})`));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const exitCode = changed ? 0 : 1;
|
|
1239
|
+
const doc = { ok: exitCode === 0, removed, missing };
|
|
1240
|
+
if (skipped.length) doc.skipped = skipped;
|
|
1241
|
+
return finish3(flags, { removed, exitCode, output: lines.join("\n") }, doc);
|
|
1004
1242
|
}
|
|
1005
1243
|
function removeCommand() {
|
|
1006
|
-
return new Command9("remove").alias("rm").description("Remove installed skills").argument("
|
|
1007
|
-
const run = runRemove(names, {
|
|
1244
|
+
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) => {
|
|
1245
|
+
const run = await runRemove(names, { ...cmd.parent?.opts(), ...opts });
|
|
1008
1246
|
console.log(run.output);
|
|
1247
|
+
process.exitCode = run.exitCode;
|
|
1009
1248
|
});
|
|
1010
1249
|
}
|
|
1011
1250
|
|
|
1012
1251
|
// src/commands/update.ts
|
|
1013
1252
|
import { Command as Command10 } from "commander";
|
|
1014
|
-
import
|
|
1015
|
-
import { join as join5 } from "path";
|
|
1016
|
-
import pc10 from "picocolors";
|
|
1253
|
+
import pc11 from "picocolors";
|
|
1017
1254
|
import { lint as lint3 } from "@skillmds/core";
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1255
|
+
function pickSkillTree(files, want) {
|
|
1256
|
+
const norm = files.map((f) => ({ path: f.path.replace(/\\/g, "/"), contents: f.contents }));
|
|
1257
|
+
const match = norm.find((f) => f.path.endsWith("/SKILL.md") && f.path.split("/").at(-2) === want);
|
|
1258
|
+
if (match) {
|
|
1259
|
+
const prefix = match.path.slice(0, -"SKILL.md".length);
|
|
1260
|
+
return norm.filter((f) => f.path.startsWith(prefix)).map((f) => ({ path: f.path.slice(prefix.length), contents: f.contents }));
|
|
1261
|
+
}
|
|
1262
|
+
if (norm.some((f) => f.path === "SKILL.md")) return norm;
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
function makeDefaultUpdateDeps(opts = {}) {
|
|
1266
|
+
const tree = opts.resolveTree ?? ((input) => resolveTree(input));
|
|
1267
|
+
const one = opts.resolveSource ?? ((url) => resolveSource(url));
|
|
1268
|
+
const client = opts.client ?? createClient;
|
|
1269
|
+
return {
|
|
1270
|
+
async fetchLatest(source, flags, name) {
|
|
1271
|
+
if (source.startsWith("registry:")) {
|
|
1272
|
+
const slug = source.slice("registry:".length);
|
|
1273
|
+
const { api, base, token } = client(flags);
|
|
1274
|
+
let skill;
|
|
1275
|
+
try {
|
|
1276
|
+
skill = await api(`/api/skills/${slug}`);
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
if (e.status === 404) return null;
|
|
1279
|
+
throw e;
|
|
1280
|
+
}
|
|
1281
|
+
if (skill.type === "pack") {
|
|
1282
|
+
const bundle = await fetchBundle(base, slug, token);
|
|
1283
|
+
if (bundle?.length) return { files: bundle, commit_sha: skill.commit_sha ?? void 0 };
|
|
1284
|
+
const pack = await resolvePackFiles(skill);
|
|
1285
|
+
if (pack) return { files: pack.files, commit_sha: skill.commit_sha ?? void 0 };
|
|
1286
|
+
}
|
|
1287
|
+
return { files: [{ path: "SKILL.md", contents: skillMdFor(skill) }], commit_sha: skill.commit_sha ?? void 0 };
|
|
1288
|
+
}
|
|
1289
|
+
if (source.startsWith("github:")) {
|
|
1290
|
+
const spec = parseSource(source);
|
|
1291
|
+
if (spec.kind !== "github") return null;
|
|
1292
|
+
const files = await tree(gigetInput(spec));
|
|
1293
|
+
const picked = pickSkillTree(files, spec.skill ?? name);
|
|
1294
|
+
return picked ? { files: picked } : null;
|
|
1295
|
+
}
|
|
1296
|
+
if (source.startsWith("gist:")) {
|
|
1297
|
+
const [user, id] = source.slice("gist:".length).split("/");
|
|
1298
|
+
const rs = await one(`https://gist.github.com/${user}/${id}`);
|
|
1299
|
+
return rs[0] ? { files: [{ path: "SKILL.md", contents: rs[0].raw }] } : null;
|
|
1300
|
+
}
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
var defaultUpdateDeps = makeDefaultUpdateDeps();
|
|
1306
|
+
async function runUpdate(names, flags, deps = defaultUpdateDeps) {
|
|
1307
|
+
const scopes = scopesFor(flags);
|
|
1021
1308
|
const updated = [];
|
|
1309
|
+
const available = [];
|
|
1310
|
+
const failed = [];
|
|
1022
1311
|
const lines = [];
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
lines.push(pc10.dim(`- ${s.name} already up to date`));
|
|
1312
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1313
|
+
for (const global of scopes) {
|
|
1314
|
+
const scope = { global, cwd: flags.cwd, home: flags.home };
|
|
1315
|
+
const lock = readLock(scope);
|
|
1316
|
+
const installed = listInstalled(scope).filter((s) => names.length === 0 || names.includes(s.name));
|
|
1317
|
+
for (const s of installed) {
|
|
1318
|
+
seen.add(s.name);
|
|
1319
|
+
const label2 = `${s.name}${scopes.length > 1 ? pc11.dim(` [${s.scope}]`) : ""}`;
|
|
1320
|
+
const entry = lock.skills[s.name];
|
|
1321
|
+
if (!entry) {
|
|
1322
|
+
lines.push(pc11.dim(`- ${label2} untracked \u2014 reinstall with \`skillmd add\` to track updates`));
|
|
1035
1323
|
continue;
|
|
1036
1324
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
lines.push(pc10.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
|
|
1325
|
+
if (entry.source.startsWith("local:")) {
|
|
1326
|
+
lines.push(pc11.dim(`- ${label2} local source \u2014 not auto-updated`));
|
|
1040
1327
|
continue;
|
|
1041
1328
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1329
|
+
try {
|
|
1330
|
+
const latest = await deps.fetchLatest(entry.source, flags, s.name);
|
|
1331
|
+
if (!latest) {
|
|
1332
|
+
failed.push(s.name);
|
|
1333
|
+
lines.push(pc11.red(`${GLYPH.blocked} ${label2} no longer available upstream (${entry.source})`));
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (entry.source.startsWith("registry:") && latest.commit_sha && entry.commit_sha && latest.commit_sha === entry.commit_sha) {
|
|
1337
|
+
lines.push(pc11.dim(`- ${label2} up to date`));
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (digestOf(latest.files) === entry.digest) {
|
|
1341
|
+
lines.push(pc11.dim(`- ${label2} up to date`));
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
if (flags.check) {
|
|
1345
|
+
available.push(s.name);
|
|
1346
|
+
lines.push(pc11.yellow(`\u2191 ${label2} update available`));
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
const md = latest.files.find((f) => f.path === "SKILL.md");
|
|
1350
|
+
const raw = md ? typeof md.contents === "string" ? md.contents : md.contents.toString("utf8") : "";
|
|
1351
|
+
const result = lint3(raw, { slug: s.name });
|
|
1352
|
+
if (!result.ok) {
|
|
1353
|
+
failed.push(s.name);
|
|
1354
|
+
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`));
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
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" });
|
|
1358
|
+
updated.push(s.name);
|
|
1359
|
+
lines.push(pc11.green(`${GLYPH.ok} ${label2} updated`));
|
|
1360
|
+
} catch (e) {
|
|
1361
|
+
failed.push(s.name);
|
|
1362
|
+
lines.push(pc11.red(`${GLYPH.blocked} ${label2} ${e instanceof Error ? e.message : String(e)}`));
|
|
1363
|
+
}
|
|
1047
1364
|
}
|
|
1048
1365
|
}
|
|
1049
|
-
|
|
1366
|
+
for (const n of names) {
|
|
1367
|
+
if (seen.has(n)) continue;
|
|
1368
|
+
failed.push(n);
|
|
1369
|
+
lines.push(pc11.yellow(`- ${n} not installed`));
|
|
1370
|
+
}
|
|
1371
|
+
const exitCode = failed.length ? 1 : 0;
|
|
1372
|
+
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) };
|
|
1373
|
+
if (lines.length === 0) lines.push(pc11.dim("No installed skills found."));
|
|
1374
|
+
return { updated, available, failed, exitCode, output: lines.join("\n") };
|
|
1050
1375
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const hit = r?.items?.find((it) => it.slug.split("/")[1] === s.name);
|
|
1054
|
-
return hit?.slug ?? null;
|
|
1376
|
+
function scopeOptions(cmd) {
|
|
1377
|
+
return cmd.option("-g, --global", "only user-level skills").option("-p, --project", "only this project's skills").option("--json", "machine-readable output");
|
|
1055
1378
|
}
|
|
1056
1379
|
function updateCommand() {
|
|
1057
|
-
return new Command10("update").description("Update installed skills from the
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1380
|
+
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) => {
|
|
1381
|
+
const run = await runUpdate(names, { ...cmd.parent?.opts(), ...opts });
|
|
1382
|
+
console.log(run.output);
|
|
1383
|
+
process.exitCode = run.exitCode;
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
function checkCommand() {
|
|
1387
|
+
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) => {
|
|
1388
|
+
const run = await runUpdate(names, { ...cmd.parent?.opts(), ...opts, check: true });
|
|
1060
1389
|
console.log(run.output);
|
|
1390
|
+
process.exitCode = run.exitCode;
|
|
1061
1391
|
});
|
|
1062
1392
|
}
|
|
1063
1393
|
|
|
1064
1394
|
// src/commands/publish.ts
|
|
1065
1395
|
import { Command as Command11 } from "commander";
|
|
1066
|
-
import
|
|
1396
|
+
import pc12 from "picocolors";
|
|
1067
1397
|
import { lint as lint4, toText } from "@skillmds/core";
|
|
1068
1398
|
var defaultDeps2 = {
|
|
1069
1399
|
resolve: resolveSource,
|
|
@@ -1087,20 +1417,20 @@ async function runPublish(target, flags, deps = defaultDeps2) {
|
|
|
1087
1417
|
const report = toText([{ file: src.path, result }], { decorate });
|
|
1088
1418
|
if (!result.ok) {
|
|
1089
1419
|
return { result, published: false, exitCode: 1, output: `${report}
|
|
1090
|
-
${
|
|
1420
|
+
${pc12.red("\u2717 Refusing to publish: fix the errors above.")}` };
|
|
1091
1421
|
}
|
|
1092
1422
|
const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
|
|
1093
1423
|
if (hasWarn && !flags.force) {
|
|
1094
1424
|
return { result, published: false, exitCode: 1, output: `${report}
|
|
1095
|
-
${
|
|
1425
|
+
${pc12.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
|
|
1096
1426
|
}
|
|
1097
1427
|
if (flags.dryRun) {
|
|
1098
1428
|
return { result, published: false, exitCode: 0, output: `${report}
|
|
1099
|
-
${
|
|
1429
|
+
${pc12.green("\u2713 Dry run: would publish.")}` };
|
|
1100
1430
|
}
|
|
1101
1431
|
const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
|
|
1102
1432
|
return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
|
|
1103
|
-
${
|
|
1433
|
+
${pc12.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
|
|
1104
1434
|
}
|
|
1105
1435
|
function publishCommand() {
|
|
1106
1436
|
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) => {
|
|
@@ -1113,46 +1443,81 @@ function publishCommand() {
|
|
|
1113
1443
|
|
|
1114
1444
|
// src/commands/login.ts
|
|
1115
1445
|
import { Command as Command12 } from "commander";
|
|
1116
|
-
import * as
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
return;
|
|
1446
|
+
import * as p5 from "@clack/prompts";
|
|
1447
|
+
import pc13 from "picocolors";
|
|
1448
|
+
var defaultLoginDeps = (flags) => promptIf(flags, async () => {
|
|
1449
|
+
const answer = await p5.password({ message: "Paste your SkillMD token (from your account page)" });
|
|
1450
|
+
return p5.isCancel(answer) ? null : answer;
|
|
1451
|
+
});
|
|
1452
|
+
function finish4(flags, result, doc) {
|
|
1453
|
+
return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
|
|
1454
|
+
}
|
|
1455
|
+
async function runLogin(flags, deps = defaultLoginDeps(flags)) {
|
|
1456
|
+
const env = flags.env ?? process.env;
|
|
1457
|
+
let token = flags.token;
|
|
1458
|
+
if (!token) {
|
|
1459
|
+
if (!deps.prompt) {
|
|
1460
|
+
const error = nonInteractiveHint("--token <token> (or set SKILLMD_TOKEN)");
|
|
1461
|
+
return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
|
|
1132
1462
|
}
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1463
|
+
const answer = await deps.prompt();
|
|
1464
|
+
if (answer === null) return finish4(flags, { exitCode: 0, output: pc13.dim("Cancelled.") }, { ok: false, cancelled: true });
|
|
1465
|
+
token = answer;
|
|
1466
|
+
}
|
|
1467
|
+
if (!token) {
|
|
1468
|
+
const error = "A token is required.";
|
|
1469
|
+
return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
|
|
1470
|
+
}
|
|
1471
|
+
const cfg = readConfig(flags.home);
|
|
1472
|
+
let host;
|
|
1473
|
+
try {
|
|
1474
|
+
host = hostOf(resolveApi({ api: flags.api, insecureHttp: flags.insecureHttp }, { env, config: { ...cfg, api: flags.api ?? cfg.api } }));
|
|
1475
|
+
} catch (e) {
|
|
1476
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1477
|
+
return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg }, null, 2) : pc13.red(msg) };
|
|
1478
|
+
}
|
|
1479
|
+
cfg.token = token;
|
|
1480
|
+
if (flags.api) cfg.api = flags.api;
|
|
1481
|
+
cfg.tokenHost = host;
|
|
1482
|
+
writeConfig(cfg, flags.home);
|
|
1483
|
+
const file = configPath(flags.home);
|
|
1484
|
+
return {
|
|
1485
|
+
exitCode: 0,
|
|
1486
|
+
output: flags.json ? JSON.stringify({ ok: true, file, host }, null, 2) : `Saved token for ${host} to ${file}`
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function runLogout(flags) {
|
|
1490
|
+
const cfg = readConfig(flags.home);
|
|
1491
|
+
const had = Boolean(cfg.token);
|
|
1492
|
+
delete cfg.token;
|
|
1493
|
+
delete cfg.tokenHost;
|
|
1494
|
+
writeConfig(cfg, flags.home);
|
|
1495
|
+
return {
|
|
1496
|
+
exitCode: 0,
|
|
1497
|
+
output: flags.json ? JSON.stringify({ ok: true, removed: had }, null, 2) : had ? "Removed stored token." : "No stored token."
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function loginCommand() {
|
|
1501
|
+
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) => {
|
|
1502
|
+
const r = await runLogin({ ...cmd.parent?.opts(), ...opts });
|
|
1503
|
+
console.log(r.output);
|
|
1504
|
+
process.exitCode = r.exitCode;
|
|
1138
1505
|
});
|
|
1139
1506
|
}
|
|
1140
1507
|
function logoutCommand() {
|
|
1141
|
-
return new Command12("logout").description("Remove the stored SkillMD token").action(() => {
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
console.log("Removed stored token.");
|
|
1508
|
+
return new Command12("logout").description("Remove the stored SkillMD token").option("--json", "machine-readable output").action((opts, cmd) => {
|
|
1509
|
+
const r = runLogout({ ...cmd.parent?.opts(), ...opts });
|
|
1510
|
+
console.log(r.output);
|
|
1511
|
+
process.exitCode = r.exitCode;
|
|
1146
1512
|
});
|
|
1147
1513
|
}
|
|
1148
1514
|
|
|
1149
1515
|
// src/commands/interactive.ts
|
|
1150
|
-
import * as
|
|
1151
|
-
import
|
|
1152
|
-
import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
|
|
1516
|
+
import * as p6 from "@clack/prompts";
|
|
1517
|
+
import pc14 from "picocolors";
|
|
1153
1518
|
function bail(v) {
|
|
1154
|
-
if (
|
|
1155
|
-
|
|
1519
|
+
if (p6.isCancel(v)) {
|
|
1520
|
+
p6.cancel("Cancelled.");
|
|
1156
1521
|
return true;
|
|
1157
1522
|
}
|
|
1158
1523
|
return false;
|
|
@@ -1160,25 +1525,26 @@ function bail(v) {
|
|
|
1160
1525
|
async function browseSearchResults(items) {
|
|
1161
1526
|
const cols = process.stdout.columns ?? 80;
|
|
1162
1527
|
for (; ; ) {
|
|
1163
|
-
const picked = await
|
|
1528
|
+
const picked = await p6.select({
|
|
1164
1529
|
message: "Search results",
|
|
1165
1530
|
options: [
|
|
1166
1531
|
...items.map((item2) => {
|
|
1167
1532
|
const starsTxt = item2.repo_stars != null ? ` \u2605${formatStars(item2.repo_stars)}` : "";
|
|
1168
1533
|
const checkTxt = item2.verified ? " \u2713" : "";
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1534
|
+
const slugTxt = safeText(item2.slug, 200);
|
|
1535
|
+
const plainLabel = `${slugTxt}${starsTxt}${checkTxt}`;
|
|
1536
|
+
const label2 = `${pc14.bold(slugTxt)}${starsTxt ? pc14.dim(starsTxt) : ""}${checkTxt ? pc14.green(checkTxt) : ""}`;
|
|
1171
1537
|
const hintWidth = Math.max(10, cols - plainLabel.length - 6);
|
|
1172
1538
|
return {
|
|
1173
1539
|
value: item2.slug,
|
|
1174
1540
|
label: label2,
|
|
1175
|
-
hint: truncate(item2.description ?? "", hintWidth)
|
|
1541
|
+
hint: truncate(safeText(item2.description ?? ""), hintWidth)
|
|
1176
1542
|
};
|
|
1177
1543
|
}),
|
|
1178
1544
|
{ value: "__back", label: "\u2190 Back to menu" }
|
|
1179
1545
|
]
|
|
1180
1546
|
});
|
|
1181
|
-
if (
|
|
1547
|
+
if (p6.isCancel(picked) || picked === "__back") return;
|
|
1182
1548
|
const item = items.find((i) => i.slug === picked);
|
|
1183
1549
|
if (!item) continue;
|
|
1184
1550
|
await browseOneResult(item);
|
|
@@ -1186,14 +1552,15 @@ async function browseSearchResults(items) {
|
|
|
1186
1552
|
}
|
|
1187
1553
|
async function browseOneResult(item) {
|
|
1188
1554
|
const starsTxt = item.repo_stars != null ? `\u2605 ${formatStars(item.repo_stars)} stars` : "";
|
|
1555
|
+
const slugTxt = safeText(item.slug, 200);
|
|
1189
1556
|
const meta = [
|
|
1190
1557
|
starsTxt,
|
|
1191
|
-
item.verified ?
|
|
1558
|
+
item.verified ? pc14.green("\u2713 verified") : "unverified",
|
|
1192
1559
|
item.type === "pack" ? "pack" : ""
|
|
1193
1560
|
].filter(Boolean).join(" \xB7 ");
|
|
1194
|
-
|
|
1561
|
+
p6.note([safeText(item.description ?? ""), pc14.dim(meta)].join("\n"), slugTxt);
|
|
1195
1562
|
for (; ; ) {
|
|
1196
|
-
const action = await
|
|
1563
|
+
const action = await p6.select({
|
|
1197
1564
|
message: "What next?",
|
|
1198
1565
|
options: [
|
|
1199
1566
|
{ value: "project", label: "Install to this project" },
|
|
@@ -1202,33 +1569,36 @@ async function browseOneResult(item) {
|
|
|
1202
1569
|
{ value: "back", label: "\u2190 Back to results" }
|
|
1203
1570
|
]
|
|
1204
1571
|
});
|
|
1205
|
-
if (
|
|
1572
|
+
if (p6.isCancel(action) || action === "back") return;
|
|
1206
1573
|
if (action === "url") {
|
|
1207
|
-
|
|
1574
|
+
p6.log.message(pc14.dim(`https://skillmd.com/skills/${slugTxt}`));
|
|
1208
1575
|
continue;
|
|
1209
1576
|
}
|
|
1210
|
-
|
|
1577
|
+
p6.log.step(`Installing ${slugTxt}\u2026`);
|
|
1211
1578
|
try {
|
|
1212
|
-
const run = await runAdd(item.slug, action === "global" ? { global: true } : {});
|
|
1579
|
+
const run = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
|
|
1213
1580
|
console.log(run.output);
|
|
1214
|
-
if (run.exitCode === 0)
|
|
1215
|
-
else
|
|
1581
|
+
if (run.exitCode === 0) p6.log.success("Installed.");
|
|
1582
|
+
else p6.log.warn("Not installed.");
|
|
1216
1583
|
} catch (err) {
|
|
1217
|
-
|
|
1584
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1218
1585
|
}
|
|
1219
1586
|
}
|
|
1220
1587
|
}
|
|
1221
1588
|
async function runInteractive() {
|
|
1222
|
-
|
|
1589
|
+
p6.intro(banner());
|
|
1223
1590
|
for (; ; ) {
|
|
1224
|
-
const action = await
|
|
1591
|
+
const action = await p6.select({
|
|
1225
1592
|
message: "What would you like to do?",
|
|
1226
1593
|
options: [
|
|
1227
1594
|
{ value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
|
|
1228
1595
|
{ value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
|
|
1229
1596
|
{ value: "search", label: "Search the registry" },
|
|
1597
|
+
{ value: "install", label: "Install a skill", hint: "registry slug, GitHub URL or local path" },
|
|
1230
1598
|
{ value: "init", label: "Create a new skill" },
|
|
1231
1599
|
{ value: "list", label: "List installed skills" },
|
|
1600
|
+
{ value: "update", label: "Update installed skills" },
|
|
1601
|
+
{ value: "remove", label: "Remove skills" },
|
|
1232
1602
|
{ value: "help", label: "Show all commands" },
|
|
1233
1603
|
{ value: "quit", label: "Quit" }
|
|
1234
1604
|
]
|
|
@@ -1237,10 +1607,10 @@ async function runInteractive() {
|
|
|
1237
1607
|
switch (action) {
|
|
1238
1608
|
case "lint":
|
|
1239
1609
|
case "scan": {
|
|
1240
|
-
const path = await
|
|
1610
|
+
const path = await p6.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
|
|
1241
1611
|
if (bail(path)) break;
|
|
1242
1612
|
const target = String(path || ".");
|
|
1243
|
-
const s =
|
|
1613
|
+
const s = p6.spinner();
|
|
1244
1614
|
s.start(action === "lint" ? "Linting" : "Scanning");
|
|
1245
1615
|
try {
|
|
1246
1616
|
const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
|
|
@@ -1248,74 +1618,97 @@ async function runInteractive() {
|
|
|
1248
1618
|
console.log(run.output);
|
|
1249
1619
|
process.exitCode = run.exitCode;
|
|
1250
1620
|
} catch (err) {
|
|
1251
|
-
s.stop(
|
|
1621
|
+
s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
|
|
1252
1622
|
process.exitCode = 1;
|
|
1253
1623
|
}
|
|
1254
1624
|
break;
|
|
1255
1625
|
}
|
|
1256
1626
|
case "search": {
|
|
1257
|
-
const query = await
|
|
1627
|
+
const query = await p6.text({ message: "Search the registry for" });
|
|
1258
1628
|
if (bail(query)) break;
|
|
1259
|
-
const verified = await
|
|
1629
|
+
const verified = await p6.confirm({ message: "Verified skills only?", initialValue: false });
|
|
1260
1630
|
if (bail(verified)) break;
|
|
1261
|
-
const s =
|
|
1631
|
+
const s = p6.spinner();
|
|
1262
1632
|
s.start("Searching");
|
|
1263
1633
|
try {
|
|
1264
1634
|
const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
|
|
1265
1635
|
s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
|
|
1266
1636
|
if (run.items.length === 0) {
|
|
1267
|
-
|
|
1637
|
+
p6.log.info("No results.");
|
|
1268
1638
|
break;
|
|
1269
1639
|
}
|
|
1270
1640
|
await browseSearchResults(run.items);
|
|
1271
1641
|
} catch (err) {
|
|
1272
|
-
s.stop(
|
|
1642
|
+
s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
|
|
1273
1643
|
process.exitCode = 1;
|
|
1274
1644
|
}
|
|
1275
1645
|
break;
|
|
1276
1646
|
}
|
|
1277
1647
|
case "init": {
|
|
1278
|
-
const name = await
|
|
1648
|
+
const name = await p6.text({ message: "Skill name", placeholder: "my-skill" });
|
|
1279
1649
|
if (bail(name)) break;
|
|
1280
|
-
const description = await
|
|
1650
|
+
const description = await p6.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
|
|
1281
1651
|
if (bail(description)) break;
|
|
1282
1652
|
try {
|
|
1283
1653
|
const file = initSkill(String(name), { description: String(description || "") });
|
|
1284
|
-
|
|
1654
|
+
p6.log.success(`Created ${pc14.bold(file)}`);
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1657
|
+
process.exitCode = 1;
|
|
1658
|
+
}
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
case "install": {
|
|
1662
|
+
const src = await p6.text({ message: "Skill source", placeholder: "owner/name, https://github.com/\u2026, or ./path" });
|
|
1663
|
+
if (bail(src)) break;
|
|
1664
|
+
try {
|
|
1665
|
+
const run = await runAdd(String(src), {});
|
|
1666
|
+
console.log(run.output);
|
|
1667
|
+
process.exitCode = run.exitCode;
|
|
1285
1668
|
} catch (err) {
|
|
1286
|
-
|
|
1669
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1287
1670
|
process.exitCode = 1;
|
|
1288
1671
|
}
|
|
1289
1672
|
break;
|
|
1290
1673
|
}
|
|
1291
1674
|
case "list": {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1675
|
+
console.log(runList({}).output);
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
case "update": {
|
|
1679
|
+
try {
|
|
1680
|
+
const run = await runUpdate([], {});
|
|
1681
|
+
console.log(run.output);
|
|
1682
|
+
process.exitCode = run.exitCode;
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1685
|
+
process.exitCode = 1;
|
|
1686
|
+
}
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
case "remove": {
|
|
1690
|
+
try {
|
|
1691
|
+
const run = await runRemove([], {});
|
|
1692
|
+
console.log(run.output);
|
|
1693
|
+
process.exitCode = run.exitCode;
|
|
1694
|
+
} catch (err) {
|
|
1695
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1696
|
+
process.exitCode = 1;
|
|
1304
1697
|
}
|
|
1305
1698
|
break;
|
|
1306
1699
|
}
|
|
1307
1700
|
case "help": {
|
|
1308
|
-
|
|
1701
|
+
p6.note(
|
|
1309
1702
|
[
|
|
1310
|
-
`${
|
|
1311
|
-
`${
|
|
1312
|
-
`${
|
|
1313
|
-
`${
|
|
1314
|
-
`${
|
|
1315
|
-
`${
|
|
1316
|
-
`${
|
|
1317
|
-
`${
|
|
1318
|
-
`${
|
|
1703
|
+
`${pc14.bold("lint")} [path] validate SKILL.md + quality score`,
|
|
1704
|
+
`${pc14.bold("scan")} [path] security scan`,
|
|
1705
|
+
`${pc14.bold("search")} <query> search the registry`,
|
|
1706
|
+
`${pc14.bold("add")} <source> install a skill (lints first)`,
|
|
1707
|
+
`${pc14.bold("info")} <slug> registry details for a skill`,
|
|
1708
|
+
`${pc14.bold("init")} [name] scaffold a new skill`,
|
|
1709
|
+
`${pc14.bold("list / remove / update")} manage installed skills`,
|
|
1710
|
+
`${pc14.bold("publish")} [path] publish (fails on lint errors)`,
|
|
1711
|
+
`${pc14.bold("rules / login")}`
|
|
1319
1712
|
].join("\n"),
|
|
1320
1713
|
"Commands (run: skillmd <command>)"
|
|
1321
1714
|
);
|
|
@@ -1323,16 +1716,39 @@ async function runInteractive() {
|
|
|
1323
1716
|
}
|
|
1324
1717
|
}
|
|
1325
1718
|
}
|
|
1326
|
-
|
|
1719
|
+
p6.outro(pc14.dim("skillmd.com"));
|
|
1327
1720
|
}
|
|
1328
1721
|
|
|
1329
1722
|
// src/index.ts
|
|
1330
1723
|
var version = "0.0.0";
|
|
1331
1724
|
try {
|
|
1332
|
-
version = JSON.parse(
|
|
1725
|
+
version = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1333
1726
|
} catch {
|
|
1334
1727
|
}
|
|
1335
|
-
var program = new Command13("skillmd").version(version).description("Lint, validate, and install Agent Skills from the SkillMD registry.").option("--json", "machine-readable output").option("--token <token>", "
|
|
1728
|
+
var program = new Command13("skillmd").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 `skillmd 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)");
|
|
1729
|
+
program.configureHelp({ sortSubcommands: false });
|
|
1730
|
+
program.addHelpText("after", `
|
|
1731
|
+
Examples:
|
|
1732
|
+
$ skillmd add anthropic/pdf install from the registry (asks project vs global)
|
|
1733
|
+
$ skillmd add owner/repo/skills/x#main -g a GitHub subfolder at a ref, into your user dirs
|
|
1734
|
+
$ skillmd add ./my-skill -a claude-code -p a local skill into this project for one agent
|
|
1735
|
+
$ skillmd add anthropic/pdf -y --json non-interactive, machine-readable
|
|
1736
|
+
$ skillmd list everything installed, project and global
|
|
1737
|
+
$ skillmd check which installed skills have updates
|
|
1738
|
+
$ skillmd update -g update your user-level skills
|
|
1739
|
+
$ skillmd remove pdf remove a skill from every agent
|
|
1740
|
+
|
|
1741
|
+
Quality: lint scan rules init
|
|
1742
|
+
Registry: search info add list remove update check
|
|
1743
|
+
Publish: publish login logout
|
|
1744
|
+
|
|
1745
|
+
Docs: https://skillmd.com/docs/cli`);
|
|
1746
|
+
program.hook("preAction", (thisCmd) => {
|
|
1747
|
+
const opts = thisCmd.opts();
|
|
1748
|
+
if (opts.token && process.stderr.isTTY) {
|
|
1749
|
+
console.error(pc15.yellow("\u26A0 --token is visible in your shell history and process list; prefer `skillmd login` or SKILLMD_TOKEN."));
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1336
1752
|
program.addCommand(lintCommand());
|
|
1337
1753
|
program.addCommand(scanCommand());
|
|
1338
1754
|
program.addCommand(rulesCommand());
|
|
@@ -1343,6 +1759,7 @@ program.addCommand(addCommand());
|
|
|
1343
1759
|
program.addCommand(listCommand());
|
|
1344
1760
|
program.addCommand(removeCommand());
|
|
1345
1761
|
program.addCommand(updateCommand());
|
|
1762
|
+
program.addCommand(checkCommand());
|
|
1346
1763
|
program.addCommand(publishCommand());
|
|
1347
1764
|
program.addCommand(loginCommand());
|
|
1348
1765
|
program.addCommand(logoutCommand());
|