skillmds 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +90 -15
- package/assets/skillmd-banner.jpg +0 -0
- package/bin/skillmd-mcp.mjs +2 -308
- package/dist/chunk-A7BKAUUO.js +850 -0
- package/dist/cli.js +1073 -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-A7BKAUUO.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,46 @@ 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);
|
|
775
|
-
}
|
|
776
|
-
return dest;
|
|
777
|
-
}
|
|
778
|
-
function installedSkills(opts = {}) {
|
|
779
|
-
const scope = opts.global ? "global" : "project";
|
|
780
|
-
const out = [];
|
|
781
|
-
const seen = /* @__PURE__ */ new Set();
|
|
782
|
-
for (const agent of AGENTS) {
|
|
783
|
-
const dir = agentDir(agent.id, opts);
|
|
784
|
-
if (seen.has(dir)) continue;
|
|
785
|
-
seen.add(dir);
|
|
786
|
-
if (!existsSync3(dir)) continue;
|
|
787
|
-
for (const entry of readdirSync2(dir)) {
|
|
788
|
-
const skillMd = join4(dir, entry, "SKILL.md");
|
|
789
|
-
if (!existsSync3(skillMd) || !statSync2(skillMd).isFile()) continue;
|
|
790
|
-
out.push({ name: entry, agent: agent.id, scope, path: join4(dir, entry), raw: readFileSync3(skillMd, "utf8") });
|
|
791
|
-
}
|
|
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}`);
|
|
792
641
|
}
|
|
793
|
-
return out;
|
|
794
642
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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;
|
|
813
664
|
}
|
|
814
665
|
function registryFallbackNote(host) {
|
|
815
666
|
return `installed from GitHub because the SkillMD registry (${host}) was unreachable \u2014 this copy is not the registry-reviewed version`;
|
|
@@ -822,38 +673,69 @@ If this environment restricts network access (sandbox, proxy, CI), ask an admini
|
|
|
822
673
|
Registry error: ${e.message}`
|
|
823
674
|
);
|
|
824
675
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
676
|
+
function argWithRef(arg, ref) {
|
|
677
|
+
if (!ref) return arg;
|
|
678
|
+
try {
|
|
679
|
+
const spec = parseSource(arg, { ref });
|
|
680
|
+
return spec.kind === "github" ? sourceId(spec) : arg;
|
|
681
|
+
} catch {
|
|
682
|
+
return arg;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function provenanceFor(arg) {
|
|
686
|
+
try {
|
|
687
|
+
return sourceId(parseSource(arg));
|
|
688
|
+
} catch {
|
|
689
|
+
return `unknown:${arg}`;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function makeDefaultDeps(flags) {
|
|
693
|
+
const canPrompt = () => isInteractive(flags, {
|
|
694
|
+
stdinTTY: Boolean(process.stdin.isTTY),
|
|
695
|
+
stdoutTTY: Boolean(process.stdout.isTTY),
|
|
696
|
+
env: flags.env ?? process.env
|
|
697
|
+
});
|
|
698
|
+
return {
|
|
699
|
+
async resolve(arg, f) {
|
|
700
|
+
const spec = parseSource(arg, { ref: f.ref });
|
|
701
|
+
const source = sourceId(spec);
|
|
702
|
+
if (spec.kind !== "slug") {
|
|
703
|
+
const rs = await resolveSource(argWithRef(arg, f.ref));
|
|
704
|
+
return rs.map((r) => ({
|
|
705
|
+
name: r.slug,
|
|
706
|
+
raw: r.raw,
|
|
707
|
+
slug: r.slug,
|
|
708
|
+
source,
|
|
709
|
+
...spec.kind === "local" && r.file ? { files: collectFiles(dirname(r.file), dirname(r.file)) } : {}
|
|
710
|
+
}));
|
|
837
711
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
712
|
+
let registryDown = null;
|
|
713
|
+
const { api, base: apiBase, token } = createClient(f);
|
|
714
|
+
const registryHost = (() => {
|
|
715
|
+
try {
|
|
716
|
+
return new URL(apiBase).host;
|
|
717
|
+
} catch {
|
|
718
|
+
return "api.skillmd.com";
|
|
719
|
+
}
|
|
720
|
+
})();
|
|
721
|
+
const registrySlug = `${spec.owner}/${spec.name}`;
|
|
842
722
|
try {
|
|
843
|
-
const skill = await api(`/api/skills/${owner}/${name}`);
|
|
844
|
-
const skillName = skill.slug.split("/")[1] || name;
|
|
723
|
+
const skill = await api(`/api/skills/${spec.owner}/${spec.name}`);
|
|
724
|
+
const skillName = skill.slug.split("/")[1] || spec.name;
|
|
845
725
|
const meta = {
|
|
846
726
|
name: skillName,
|
|
847
727
|
slug: skillName,
|
|
848
|
-
registrySlug
|
|
728
|
+
registrySlug,
|
|
729
|
+
source,
|
|
730
|
+
commit_sha: skill.commit_sha ?? void 0,
|
|
849
731
|
verified: skill.verified,
|
|
850
732
|
type: skill.type,
|
|
851
733
|
securityFlags: skill.security_flags
|
|
852
734
|
};
|
|
853
735
|
if (skill.type === "pack") {
|
|
854
|
-
const bundle = await fetchBundle(apiBase,
|
|
736
|
+
const bundle = await fetchBundle(apiBase, registrySlug, token);
|
|
855
737
|
if (bundle && bundle.length) {
|
|
856
|
-
const sk = bundle.find((
|
|
738
|
+
const sk = bundle.find((file) => file.path === "SKILL.md");
|
|
857
739
|
return [{ ...meta, raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill), files: bundle }];
|
|
858
740
|
}
|
|
859
741
|
const pack = await resolvePackFiles(skill);
|
|
@@ -864,206 +746,644 @@ var defaultDeps = {
|
|
|
864
746
|
if (e instanceof IntegrityError) throw e;
|
|
865
747
|
if (e instanceof RegistryError && e.status !== 404) registryDown = e;
|
|
866
748
|
}
|
|
749
|
+
const ghSource = sourceId({ kind: "github", owner: spec.owner, repo: spec.name, display: arg });
|
|
750
|
+
try {
|
|
751
|
+
const rs = await resolveSource(arg);
|
|
752
|
+
const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
|
|
753
|
+
return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, source: ghSource, note: note2 }));
|
|
754
|
+
} catch (e) {
|
|
755
|
+
if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
|
|
756
|
+
throw e;
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
get promptScope() {
|
|
760
|
+
return canPrompt() ? promptScopeInteractive : void 0;
|
|
761
|
+
},
|
|
762
|
+
get promptAgents() {
|
|
763
|
+
return canPrompt() ? promptAgentsInteractive : void 0;
|
|
764
|
+
},
|
|
765
|
+
fireInstall(registrySlug, f) {
|
|
766
|
+
const { api } = createClient(f);
|
|
767
|
+
const [owner, name] = registrySlug.split("/");
|
|
768
|
+
void api(`/api/skills/${owner}/${name}/install`, {
|
|
769
|
+
method: "POST",
|
|
770
|
+
headers: { "content-type": "application/json" },
|
|
771
|
+
body: JSON.stringify({ via: "cli" })
|
|
772
|
+
}).catch(() => {
|
|
773
|
+
});
|
|
867
774
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
body: JSON.stringify({ via: "cli" })
|
|
884
|
-
}).catch(() => {
|
|
885
|
-
});
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
async function resolveScope(flags, deps) {
|
|
778
|
+
const home = resolve3(flags.home ?? homedir3());
|
|
779
|
+
const cwd = resolve3(flags.cwd ?? process.cwd());
|
|
780
|
+
const base = { cwd: flags.cwd, home: flags.home, env: flags.env };
|
|
781
|
+
if (flags.global) return { scope: { global: true, ...base } };
|
|
782
|
+
if (flags.project) return { scope: { global: false, ...base } };
|
|
783
|
+
if (cwd === home) return { scope: { global: true, ...base } };
|
|
784
|
+
const isProject = looksLikeProject(cwd);
|
|
785
|
+
const hosted = detectHostAgent(flags.env ?? process.env) !== null;
|
|
786
|
+
if (deps.promptScope && !flags.yes && !flags.json && !hosted) {
|
|
787
|
+
const global = await deps.promptScope({ cwd, suggestGlobal: !isProject });
|
|
788
|
+
if (global === null) return null;
|
|
789
|
+
return { scope: { global, ...base } };
|
|
886
790
|
}
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
791
|
+
if (isProject) return { scope: { global: false, ...base } };
|
|
792
|
+
return {
|
|
793
|
+
scope: { global: true, ...base },
|
|
794
|
+
note: `no project detected in ${cwd} \u2014 installing to your user-level agent folders (pass --project to install here)`
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function defaultNeedsPrompt(flags) {
|
|
798
|
+
const env = flags.env ?? process.env;
|
|
799
|
+
if (flags.yes || flags.json || flags.global || flags.project || flags.list) return false;
|
|
800
|
+
if (detectHostAgent(env)) return false;
|
|
801
|
+
if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ?? homedir3())) return false;
|
|
802
|
+
return !isInteractive(flags, { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env });
|
|
803
|
+
}
|
|
804
|
+
var CANCELLED_TEXT = "installation cancelled \u2014 nothing was installed";
|
|
805
|
+
function finish2(flags, outcome) {
|
|
806
|
+
const json = Boolean(flags.json);
|
|
807
|
+
switch (outcome.kind) {
|
|
808
|
+
case "install": {
|
|
809
|
+
const installed = outcome.records.flatMap((r) => r.kind === "installed" ? [r.doc] : []);
|
|
810
|
+
const blocked = outcome.records.flatMap((r) => r.kind === "blocked" ? [{ name: r.name, reason: r.reason }] : []);
|
|
811
|
+
const written = installed.flatMap((d) => d.targets.map((t) => ({ name: d.name, dir: t.path })));
|
|
812
|
+
const warnings = [
|
|
813
|
+
...outcome.notices,
|
|
814
|
+
...outcome.records.flatMap((r) => r.kind === "installed" ? r.notices : [])
|
|
815
|
+
].map((n) => n.text);
|
|
816
|
+
const exitCode = blocked.length > 0 || installed.length === 0 ? 1 : 0;
|
|
817
|
+
const doc = { ok: exitCode === 0, scope: outcome.scope, installed, blocked, warnings };
|
|
818
|
+
return {
|
|
819
|
+
written,
|
|
820
|
+
blocked,
|
|
821
|
+
exitCode,
|
|
822
|
+
output: json ? JSON.stringify(doc, null, 2) : renderHuman(outcome.notices, outcome.records, outcome.paths)
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
case "list": {
|
|
826
|
+
const human = outcome.skills.length ? outcome.skills.map((s) => `${pc8.bold(s.name)} ${pc8.dim(s.description)}`).join("\n") : "No skills found.";
|
|
827
|
+
return { written: [], blocked: [], exitCode: 0, output: json ? JSON.stringify({ ok: true, skills: outcome.skills }, null, 2) : human };
|
|
828
|
+
}
|
|
829
|
+
case "cancel": {
|
|
830
|
+
const doc = { ok: false, error: CANCELLED_TEXT };
|
|
894
831
|
return {
|
|
895
832
|
written: [],
|
|
896
|
-
blocked: [
|
|
897
|
-
exitCode:
|
|
898
|
-
|
|
833
|
+
blocked: [],
|
|
834
|
+
exitCode: 0,
|
|
835
|
+
cancelled: true,
|
|
836
|
+
output: json ? JSON.stringify(doc, null, 2) : pc8.dim("Installation cancelled \u2014 nothing was installed.")
|
|
899
837
|
};
|
|
900
838
|
}
|
|
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;
|
|
839
|
+
case "fail": {
|
|
840
|
+
const warnings = (outcome.notices ?? []).map((n) => n.text);
|
|
841
|
+
const doc = { ok: false, error: outcome.error };
|
|
842
|
+
if (outcome.blocked?.length) doc.blocked = outcome.blocked;
|
|
843
|
+
if (warnings.length) doc.warnings = warnings;
|
|
844
|
+
const human = [
|
|
845
|
+
...(outcome.notices ?? []).map(noticeLine),
|
|
846
|
+
...outcome.blocked?.length ? outcome.blocked.map((b) => pc8.red(`${GLYPH.blocked} ${b.name} blocked \u2014 ${b.reason}`)) : [pc8.red(outcome.error)]
|
|
847
|
+
].join("\n");
|
|
848
|
+
return { written: [], blocked: outcome.blocked ?? [], exitCode: 1, output: json ? JSON.stringify(doc, null, 2) : human };
|
|
926
849
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function renderHuman(notices, records, paths) {
|
|
853
|
+
const lines = notices.map(noticeLine);
|
|
854
|
+
for (const r of records) {
|
|
855
|
+
if (r.kind === "blocked") {
|
|
856
|
+
lines.push(pc8.red(r.message));
|
|
930
857
|
continue;
|
|
931
858
|
}
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
859
|
+
const { doc } = r;
|
|
860
|
+
lines.push(renderInstallSummary({
|
|
861
|
+
name: doc.name,
|
|
862
|
+
score: r.score,
|
|
863
|
+
flags: r.securityFlags,
|
|
864
|
+
source: r.source,
|
|
865
|
+
canonical: doc.canonical,
|
|
866
|
+
targets: doc.targets,
|
|
867
|
+
skipped: doc.skipped,
|
|
868
|
+
...r.replaced ? { replaced: r.replaced } : {}
|
|
869
|
+
}, paths));
|
|
870
|
+
for (const n of r.notices) if (!n.suppressInSummary) lines.push(noticeLine(n));
|
|
871
|
+
}
|
|
872
|
+
return lines.join("\n");
|
|
873
|
+
}
|
|
874
|
+
async function resolveCandidates(arg, flags, deps) {
|
|
875
|
+
const candidates = await deps.resolve(arg, flags);
|
|
876
|
+
if (!flags.skill?.length) return candidates;
|
|
877
|
+
return candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
|
|
878
|
+
}
|
|
879
|
+
function gate(flags, deps, candidates) {
|
|
880
|
+
if (flags.list) {
|
|
881
|
+
return finish2(flags, {
|
|
882
|
+
kind: "list",
|
|
883
|
+
skills: candidates.map((c) => {
|
|
884
|
+
const parsed = parseSkillMd2(c.raw);
|
|
885
|
+
const description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
|
|
886
|
+
return { name: c.name, description: safeText(description, 80) };
|
|
887
|
+
})
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
if (flags.json && !flags.yes) {
|
|
891
|
+
return finish2(flags, { kind: "fail", error: "--json requires -y (no prompts can be shown in JSON mode)" });
|
|
892
|
+
}
|
|
893
|
+
if (deps.needsPrompt ?? (!deps.promptScope && defaultNeedsPrompt(flags))) {
|
|
894
|
+
return finish2(flags, { kind: "fail", error: nonInteractiveHint("-y (auto-detects the scope) or -g / -p") });
|
|
895
|
+
}
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
async function chooseTargets(flags, deps, scope, detected) {
|
|
899
|
+
const notices = [];
|
|
900
|
+
let targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
|
|
901
|
+
if (scope.global) {
|
|
902
|
+
const kept = [];
|
|
903
|
+
for (const id of targets) {
|
|
904
|
+
if (agentSupportsGlobal(id)) kept.push(id);
|
|
905
|
+
else notices.push(skipNotice(`${id}: project-only agent`));
|
|
940
906
|
}
|
|
941
|
-
|
|
942
|
-
|
|
907
|
+
targets = kept;
|
|
908
|
+
}
|
|
909
|
+
if (!flags.agent?.length && deps.promptAgents && !flags.yes && !flags.json && !detectHostAgent(flags.env ?? process.env)) {
|
|
910
|
+
const picked = await deps.promptAgents({ detected: targets, dirFor: (id) => agentDir(id, scope) });
|
|
911
|
+
if (picked === null) return null;
|
|
912
|
+
targets = picked;
|
|
913
|
+
if (flags.home === void 0 && picked.length) {
|
|
914
|
+
try {
|
|
915
|
+
writeConfig({ ...readConfig(), lastAgents: picked });
|
|
916
|
+
} catch {
|
|
917
|
+
}
|
|
943
918
|
}
|
|
944
|
-
|
|
945
|
-
|
|
919
|
+
}
|
|
920
|
+
return { targets, notices };
|
|
921
|
+
}
|
|
922
|
+
async function installOne(c, ctx) {
|
|
923
|
+
const result = lint2(c.raw, { slug: c.slug });
|
|
924
|
+
const securityFlags2 = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
|
|
925
|
+
const deniedFlags = securityFlags2.filter((f) => ctx.deny.has(f));
|
|
926
|
+
if (!ctx.flags.skipLint && !result.ok) {
|
|
927
|
+
return {
|
|
928
|
+
kind: "blocked",
|
|
929
|
+
name: c.name,
|
|
930
|
+
reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}`,
|
|
931
|
+
message: `${GLYPH.blocked} ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
if (deniedFlags.length) {
|
|
935
|
+
return {
|
|
936
|
+
kind: "blocked",
|
|
937
|
+
name: c.name,
|
|
938
|
+
reason: `denied security flags: ${deniedFlags.join(", ")}`,
|
|
939
|
+
message: `${GLYPH.blocked} ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const source = c.source ?? (c.registrySlug ? `registry:${c.registrySlug}` : provenanceFor(ctx.arg));
|
|
943
|
+
let out;
|
|
944
|
+
try {
|
|
945
|
+
out = await installSkill({
|
|
946
|
+
name: c.name,
|
|
947
|
+
files: c.files ?? [{ path: "SKILL.md", contents: c.raw }],
|
|
948
|
+
source,
|
|
949
|
+
commit_sha: c.commit_sha,
|
|
950
|
+
scope: ctx.scope,
|
|
951
|
+
agents: ctx.targets,
|
|
952
|
+
explicitAgents: Boolean(ctx.flags.agent?.length),
|
|
953
|
+
mode: ctx.flags.mode,
|
|
954
|
+
force: ctx.flags.force
|
|
955
|
+
});
|
|
956
|
+
} catch (e) {
|
|
957
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
958
|
+
return { kind: "blocked", name: c.name, reason, message: `${GLYPH.blocked} ${c.name} \u2014 ${reason}` };
|
|
959
|
+
}
|
|
960
|
+
const notices = [];
|
|
961
|
+
if (out.replaced) notices.push(replacedNotice(`${c.name}: replaced previous install (${out.replaced})`));
|
|
962
|
+
if (c.pinMiss) notices.push(warnNotice(`${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
|
|
963
|
+
if (c.note) notices.push(warnNotice(`${c.name}: ${c.note}`));
|
|
964
|
+
if (c.type === "pack" && !c.files) {
|
|
965
|
+
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.`));
|
|
966
|
+
}
|
|
967
|
+
if (c.registrySlug && !telemetryDisabled(ctx.env)) ctx.deps.fireInstall(c.registrySlug, ctx.flags);
|
|
968
|
+
return {
|
|
969
|
+
kind: "installed",
|
|
970
|
+
doc: { name: c.name, canonical: out.canonical, targets: out.targets, skipped: out.skipped, digest: out.digest },
|
|
971
|
+
score: result.score,
|
|
972
|
+
securityFlags: securityFlags2,
|
|
973
|
+
notices,
|
|
974
|
+
source,
|
|
975
|
+
...out.replaced ? { replaced: out.replaced } : {}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
async function runAdd(arg, flags, deps) {
|
|
979
|
+
const d = deps ?? makeDefaultDeps(flags);
|
|
980
|
+
const env = flags.env ?? process.env;
|
|
981
|
+
let candidates;
|
|
982
|
+
try {
|
|
983
|
+
candidates = await resolveCandidates(arg, flags, d);
|
|
984
|
+
} catch (e) {
|
|
985
|
+
if (e instanceof IntegrityError) {
|
|
986
|
+
return finish2(flags, { kind: "fail", error: e.message, blocked: [{ name: arg, reason: e.message }] });
|
|
946
987
|
}
|
|
947
|
-
if (
|
|
988
|
+
if (flags.json) return finish2(flags, { kind: "fail", error: e instanceof Error ? e.message : String(e) });
|
|
989
|
+
throw e;
|
|
948
990
|
}
|
|
949
|
-
const
|
|
950
|
-
|
|
991
|
+
const early = gate(flags, d, candidates);
|
|
992
|
+
if (early) return early;
|
|
993
|
+
const resolved = await resolveScope(flags, d);
|
|
994
|
+
if (!resolved) return finish2(flags, { kind: "cancel" });
|
|
995
|
+
const scope = resolved.scope;
|
|
996
|
+
const notices = resolved.note ? [infoNotice(resolved.note)] : [];
|
|
997
|
+
const chosen = await chooseTargets(flags, d, scope, detectAgents(scope));
|
|
998
|
+
if (!chosen) return finish2(flags, { kind: "cancel" });
|
|
999
|
+
notices.push(...chosen.notices);
|
|
1000
|
+
const ctx = { arg, flags, deny: new Set(flags.deny ?? []), scope, targets: chosen.targets, env, deps: d };
|
|
1001
|
+
const records = [];
|
|
1002
|
+
for (const c of candidates) records.push(await installOne(c, ctx));
|
|
1003
|
+
const paths = { home: flags.home ?? homedir3(), cwd: flags.cwd ?? process.cwd() };
|
|
1004
|
+
return finish2(flags, { kind: "install", scope: scope.global ? "global" : "project", records, notices, paths });
|
|
1005
|
+
}
|
|
1006
|
+
function addFlags(opts, parent) {
|
|
1007
|
+
const merged = { ...parent, ...opts };
|
|
1008
|
+
if (opts.copy) merged.mode = "copy";
|
|
1009
|
+
return merged;
|
|
951
1010
|
}
|
|
952
1011
|
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
|
-
|
|
1012
|
+
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) => {
|
|
1013
|
+
const merged = addFlags(opts, cmd.parent?.opts());
|
|
1014
|
+
const { run, printed } = await runAddWithUI(source, merged);
|
|
1015
|
+
if (!printed) {
|
|
1016
|
+
if (run.exitCode === 1 && !merged.json) console.error(run.output);
|
|
1017
|
+
else console.log(run.output);
|
|
1018
|
+
}
|
|
957
1019
|
process.exitCode = run.exitCode;
|
|
958
1020
|
});
|
|
959
1021
|
}
|
|
1022
|
+
var clackUI = {
|
|
1023
|
+
intro: (m) => p3.intro(m),
|
|
1024
|
+
spinner: () => p3.spinner(),
|
|
1025
|
+
message: (m) => p3.log.message(m),
|
|
1026
|
+
outro: (m) => p3.outro(m),
|
|
1027
|
+
cancel: (m) => p3.cancel(m)
|
|
1028
|
+
};
|
|
1029
|
+
async function runAddWithUI(source, flags, opts = {}) {
|
|
1030
|
+
const term = opts.term ?? { ...realTerminal(), env: flags.env ?? process.env };
|
|
1031
|
+
const base = opts.deps ?? makeDefaultDeps(flags);
|
|
1032
|
+
if (!isInteractive(flags, term)) return { run: await runAdd(source, flags, base), printed: false };
|
|
1033
|
+
const ui = opts.ui ?? clackUI;
|
|
1034
|
+
ui.intro(banner());
|
|
1035
|
+
try {
|
|
1036
|
+
const run = await runAdd(source, flags, {
|
|
1037
|
+
// NOTE: spreading `base` reads makeDefaultDeps()'s promptScope/promptAgents
|
|
1038
|
+
// getters right here, freezing their values for this run. That is exactly
|
|
1039
|
+
// what we want (interactivity is decided once, up front), but it does mean
|
|
1040
|
+
// a getter added to AddDeps later stops being lazy the moment it crosses
|
|
1041
|
+
// this spread.
|
|
1042
|
+
...base,
|
|
1043
|
+
resolve: async (arg, f) => {
|
|
1044
|
+
const s = ui.spinner();
|
|
1045
|
+
s.start(`Resolving ${arg}`);
|
|
1046
|
+
try {
|
|
1047
|
+
const c = await base.resolve(arg, f);
|
|
1048
|
+
s.stop(`${c.length} skill${c.length === 1 ? "" : "s"} found`);
|
|
1049
|
+
return c;
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
s.stop(pc8.red("Could not resolve the source"));
|
|
1052
|
+
throw e;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
if (run.cancelled) {
|
|
1057
|
+
ui.cancel("Installation cancelled \u2014 nothing was installed.");
|
|
1058
|
+
return { run, printed: true };
|
|
1059
|
+
}
|
|
1060
|
+
ui.message(run.output);
|
|
1061
|
+
ui.outro(run.exitCode === 0 ? `Done. Run ${pc8.cyan("skillmd list")} to see everything installed.` : pc8.red("Some skills were not installed."));
|
|
1062
|
+
return { run, printed: true };
|
|
1063
|
+
} catch (e) {
|
|
1064
|
+
ui.outro(pc8.red("Nothing was installed."));
|
|
1065
|
+
throw e;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
960
1068
|
|
|
961
1069
|
// src/commands/list.ts
|
|
962
1070
|
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}`);
|
|
1071
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
1072
|
+
import { join as join3 } from "path";
|
|
1073
|
+
import pc9 from "picocolors";
|
|
1074
|
+
import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
|
|
1075
|
+
function runList(flags = {}) {
|
|
1076
|
+
const scopes = scopesFor(flags);
|
|
1077
|
+
let items = [];
|
|
1078
|
+
for (const global of scopes) {
|
|
1079
|
+
for (const s of listInstalled({ global, cwd: flags.cwd, home: flags.home })) {
|
|
1080
|
+
const md = join3(s.path, "SKILL.md");
|
|
1081
|
+
let description = "";
|
|
1082
|
+
if (existsSync2(md)) {
|
|
1083
|
+
const parsed = parseSkillMd3(readFileSync(md, "utf8"));
|
|
1084
|
+
description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
|
|
1085
|
+
}
|
|
1086
|
+
items.push({ ...s, description: safeText(description, 80) });
|
|
982
1087
|
}
|
|
1088
|
+
}
|
|
1089
|
+
if (flags.agent?.length) items = items.filter((i) => i.agents.some((a) => flags.agent.includes(a)));
|
|
1090
|
+
items.sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
|
|
1091
|
+
if (flags.json) {
|
|
1092
|
+
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) };
|
|
1093
|
+
}
|
|
1094
|
+
if (items.length === 0) return { items, output: pc9.dim("No installed skills found. Install one with `skillmd add owner/name`.") };
|
|
1095
|
+
const w = Math.max(...items.map((i) => i.name.length), 4);
|
|
1096
|
+
const lines = items.map((i) => {
|
|
1097
|
+
const tag = i.tracked ? pc9.dim(i.source ?? "") : pc9.yellow("untracked");
|
|
1098
|
+
const modes = [...new Set(Object.values(i.mode))].join("/");
|
|
1099
|
+
return `${pc9.bold(i.name.padEnd(w))} ${pc9.cyan(i.scope.padEnd(7))} ${i.agents.join(", ").padEnd(24)} ${pc9.dim(modes.padEnd(9))} ${tag}
|
|
1100
|
+
${" ".repeat(w + 2)}${pc9.dim(i.description)}`;
|
|
1101
|
+
});
|
|
1102
|
+
return { items, output: lines.join("\n") };
|
|
1103
|
+
}
|
|
1104
|
+
function listCommand() {
|
|
1105
|
+
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) => {
|
|
1106
|
+
console.log(runList({ ...cmd.parent?.opts(), ...opts }).output);
|
|
983
1107
|
});
|
|
984
1108
|
}
|
|
985
1109
|
|
|
986
1110
|
// src/commands/remove.ts
|
|
987
1111
|
import { Command as Command9 } from "commander";
|
|
988
|
-
import { rmSync
|
|
989
|
-
import
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1112
|
+
import { rmSync } from "fs";
|
|
1113
|
+
import { join as join4, resolve as resolve4 } from "path";
|
|
1114
|
+
import * as p4 from "@clack/prompts";
|
|
1115
|
+
import pc10 from "picocolors";
|
|
1116
|
+
var defaultDeps = (flags) => isInteractive(flags) ? {
|
|
1117
|
+
confirm: async (msg) => {
|
|
1118
|
+
const a = await p4.confirm({ message: msg, initialValue: false });
|
|
1119
|
+
return !p4.isCancel(a) && a;
|
|
1120
|
+
},
|
|
1121
|
+
pick: async ({ names }) => {
|
|
1122
|
+
const a = await p4.multiselect({ message: "Which skills do you want to remove?", options: names.map((n) => ({ value: n, label: n })), required: true });
|
|
1123
|
+
return p4.isCancel(a) ? null : a;
|
|
1124
|
+
}
|
|
1125
|
+
} : {};
|
|
1126
|
+
function finish3(flags, result, doc) {
|
|
1127
|
+
return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
|
|
1128
|
+
}
|
|
1129
|
+
var scopeLabel = (scope) => scope.global ? "global" : "project";
|
|
1130
|
+
async function runRemove(names, flags, deps = defaultDeps(flags)) {
|
|
1131
|
+
const scopes = scopesFor(flags).map((global) => ({ global, cwd: flags.cwd, home: flags.home }));
|
|
1132
|
+
const where = scopes.map(scopeLabel).join(" and ");
|
|
994
1133
|
const lines = [];
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
removed.
|
|
998
|
-
lines.push(pc9.green(`\u2713 removed ${t.name} (${t.agent})`));
|
|
1134
|
+
if (flags.all && names.length) {
|
|
1135
|
+
const error = "--all cannot be combined with skill names";
|
|
1136
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
|
|
999
1137
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1138
|
+
const perScope = scopes.map((scope) => ({ scope, installed: listInstalled(scope) }));
|
|
1139
|
+
const allNames = [...new Set(perScope.flatMap((s) => s.installed.map((i) => i.name)))];
|
|
1140
|
+
const nothingInstalled = `No installed skills found in ${where} scope.`;
|
|
1141
|
+
let targets = names;
|
|
1142
|
+
if (flags.all) {
|
|
1143
|
+
if (allNames.length === 0) {
|
|
1144
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
|
|
1145
|
+
}
|
|
1146
|
+
targets = allNames;
|
|
1147
|
+
} else if (targets.length === 0) {
|
|
1148
|
+
if (!deps.pick) {
|
|
1149
|
+
const error = "Give one or more skill names, or --all.";
|
|
1150
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(`${error} `) + pc10.dim(nonInteractiveHint("skill names and -y")) }, { ok: false, error });
|
|
1151
|
+
}
|
|
1152
|
+
if (allNames.length === 0) {
|
|
1153
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
|
|
1154
|
+
}
|
|
1155
|
+
const picked = await deps.pick({ names: allNames });
|
|
1156
|
+
if (!picked) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
|
|
1157
|
+
targets = picked;
|
|
1002
1158
|
}
|
|
1003
|
-
|
|
1159
|
+
const known = targets.filter((n) => allNames.includes(n));
|
|
1160
|
+
const missing = targets.filter((n) => !known.includes(n));
|
|
1161
|
+
for (const n of missing) lines.push(pc10.yellow(`- ${n} not installed (${where} scope)`));
|
|
1162
|
+
if (known.length === 0) {
|
|
1163
|
+
const output = lines.join("\n");
|
|
1164
|
+
return finish3(flags, { removed: [], exitCode: 1, output }, { ok: false, error: `not installed: ${missing.join(", ")}`, missing });
|
|
1165
|
+
}
|
|
1166
|
+
if (!flags.yes) {
|
|
1167
|
+
const what = flags.agent?.length ? `from ${flags.agent.join(", ")}` : "from every agent";
|
|
1168
|
+
const ok = deps.confirm ? await deps.confirm(`Remove ${known.length} skill${known.length === 1 ? "" : "s"} (${known.join(", ")}) ${what} in ${where} scope?`) : null;
|
|
1169
|
+
if (ok === null) {
|
|
1170
|
+
const error = nonInteractiveHint("-y");
|
|
1171
|
+
return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
|
|
1172
|
+
}
|
|
1173
|
+
if (!ok) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
|
|
1174
|
+
}
|
|
1175
|
+
const removed = [];
|
|
1176
|
+
const skipped = [];
|
|
1177
|
+
let changed = 0;
|
|
1178
|
+
for (const name of known) {
|
|
1179
|
+
for (const { scope, installed } of perScope) {
|
|
1180
|
+
if (!installed.some((s) => s.name === name)) continue;
|
|
1181
|
+
const tag = scopes.length > 1 ? pc10.dim(` [${scopeLabel(scope)}]`) : "";
|
|
1182
|
+
if (flags.agent?.length) {
|
|
1183
|
+
const lock = readLock({ global: Boolean(scope.global), cwd: scope.cwd, home: scope.home });
|
|
1184
|
+
const entry = lock.skills[name];
|
|
1185
|
+
const canonRoot = resolve4(canonicalDir(scope));
|
|
1186
|
+
let acted = false;
|
|
1187
|
+
for (const a of flags.agent) {
|
|
1188
|
+
let dir;
|
|
1189
|
+
try {
|
|
1190
|
+
dir = agentDir(a, scope);
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
lines.push(pc10.yellow(`- ${a}: ${e instanceof Error ? e.message : String(e)}`));
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (resolve4(dir) === canonRoot || entry?.mode[a] === "canonical") {
|
|
1196
|
+
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)`));
|
|
1197
|
+
skipped.push({ skill: name, agent: a, scope: scopeLabel(scope), reason: "reads the canonical copy directly" });
|
|
1198
|
+
acted = true;
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const pth = join4(dir, name);
|
|
1202
|
+
if (isSkillDirOrLink(pth)) {
|
|
1203
|
+
rmSync(pth, { recursive: true, force: true });
|
|
1204
|
+
removed.push(pth);
|
|
1205
|
+
acted = true;
|
|
1206
|
+
lines.push(pc10.green(`${GLYPH.ok} removed ${name} from ${a}`) + tag);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (!acted) lines.push(pc10.yellow(`- ${name} is not linked into ${flags.agent.join(", ")}`) + tag);
|
|
1210
|
+
if (entry) {
|
|
1211
|
+
const left = entry.agents.filter((a) => !flags.agent.includes(a));
|
|
1212
|
+
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))) });
|
|
1213
|
+
else {
|
|
1214
|
+
const r2 = uninstallSkill(name, scope);
|
|
1215
|
+
removed.push(...r2.removed);
|
|
1216
|
+
lines.push(pc10.dim(` (no agents left \u2014 removed the canonical copy too)`));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (acted) changed++;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
const r = uninstallSkill(name, scope);
|
|
1223
|
+
removed.push(...r.removed);
|
|
1224
|
+
changed++;
|
|
1225
|
+
lines.push(pc10.green(`${GLYPH.ok} removed ${name}`) + tag + pc10.dim(` (${r.removed.length} path${r.removed.length === 1 ? "" : "s"}${r.untracked ? ", untracked" : ""})`));
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const exitCode = changed ? 0 : 1;
|
|
1229
|
+
const doc = { ok: exitCode === 0, removed, missing };
|
|
1230
|
+
if (skipped.length) doc.skipped = skipped;
|
|
1231
|
+
return finish3(flags, { removed, exitCode, output: lines.join("\n") }, doc);
|
|
1004
1232
|
}
|
|
1005
1233
|
function removeCommand() {
|
|
1006
|
-
return new Command9("remove").alias("rm").description("Remove installed skills").argument("
|
|
1007
|
-
const run = runRemove(names, {
|
|
1234
|
+
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) => {
|
|
1235
|
+
const run = await runRemove(names, { ...cmd.parent?.opts(), ...opts });
|
|
1008
1236
|
console.log(run.output);
|
|
1237
|
+
process.exitCode = run.exitCode;
|
|
1009
1238
|
});
|
|
1010
1239
|
}
|
|
1011
1240
|
|
|
1012
1241
|
// src/commands/update.ts
|
|
1013
1242
|
import { Command as Command10 } from "commander";
|
|
1014
|
-
import
|
|
1015
|
-
import { join as join5 } from "path";
|
|
1016
|
-
import pc10 from "picocolors";
|
|
1243
|
+
import pc11 from "picocolors";
|
|
1017
1244
|
import { lint as lint3 } from "@skillmds/core";
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1245
|
+
function pickSkillTree(files, want) {
|
|
1246
|
+
const norm = files.map((f) => ({ path: f.path.replace(/\\/g, "/"), contents: f.contents }));
|
|
1247
|
+
const match = norm.find((f) => f.path.endsWith("/SKILL.md") && f.path.split("/").at(-2) === want);
|
|
1248
|
+
if (match) {
|
|
1249
|
+
const prefix = match.path.slice(0, -"SKILL.md".length);
|
|
1250
|
+
return norm.filter((f) => f.path.startsWith(prefix)).map((f) => ({ path: f.path.slice(prefix.length), contents: f.contents }));
|
|
1251
|
+
}
|
|
1252
|
+
if (norm.some((f) => f.path === "SKILL.md")) return norm;
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
function makeDefaultUpdateDeps(opts = {}) {
|
|
1256
|
+
const tree = opts.resolveTree ?? ((input) => resolveTree(input));
|
|
1257
|
+
const one = opts.resolveSource ?? ((url) => resolveSource(url));
|
|
1258
|
+
const client = opts.client ?? createClient;
|
|
1259
|
+
return {
|
|
1260
|
+
async fetchLatest(source, flags, name) {
|
|
1261
|
+
if (source.startsWith("registry:")) {
|
|
1262
|
+
const slug = source.slice("registry:".length);
|
|
1263
|
+
const { api, base, token } = client(flags);
|
|
1264
|
+
let skill;
|
|
1265
|
+
try {
|
|
1266
|
+
skill = await api(`/api/skills/${slug}`);
|
|
1267
|
+
} catch (e) {
|
|
1268
|
+
if (e.status === 404) return null;
|
|
1269
|
+
throw e;
|
|
1270
|
+
}
|
|
1271
|
+
if (skill.type === "pack") {
|
|
1272
|
+
const bundle = await fetchBundle(base, slug, token);
|
|
1273
|
+
if (bundle?.length) return { files: bundle, commit_sha: skill.commit_sha ?? void 0 };
|
|
1274
|
+
const pack = await resolvePackFiles(skill);
|
|
1275
|
+
if (pack) return { files: pack.files, commit_sha: skill.commit_sha ?? void 0 };
|
|
1276
|
+
}
|
|
1277
|
+
return { files: [{ path: "SKILL.md", contents: skillMdFor(skill) }], commit_sha: skill.commit_sha ?? void 0 };
|
|
1278
|
+
}
|
|
1279
|
+
if (source.startsWith("github:")) {
|
|
1280
|
+
const spec = parseSource(source);
|
|
1281
|
+
if (spec.kind !== "github") return null;
|
|
1282
|
+
const files = await tree(gigetInput(spec));
|
|
1283
|
+
const picked = pickSkillTree(files, spec.skill ?? name);
|
|
1284
|
+
return picked ? { files: picked } : null;
|
|
1285
|
+
}
|
|
1286
|
+
if (source.startsWith("gist:")) {
|
|
1287
|
+
const [user, id] = source.slice("gist:".length).split("/");
|
|
1288
|
+
const rs = await one(`https://gist.github.com/${user}/${id}`);
|
|
1289
|
+
return rs[0] ? { files: [{ path: "SKILL.md", contents: rs[0].raw }] } : null;
|
|
1290
|
+
}
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
var defaultUpdateDeps = makeDefaultUpdateDeps();
|
|
1296
|
+
async function runUpdate(names, flags, deps = defaultUpdateDeps) {
|
|
1297
|
+
const scopes = scopesFor(flags);
|
|
1021
1298
|
const updated = [];
|
|
1299
|
+
const available = [];
|
|
1300
|
+
const failed = [];
|
|
1022
1301
|
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`));
|
|
1302
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1303
|
+
for (const global of scopes) {
|
|
1304
|
+
const scope = { global, cwd: flags.cwd, home: flags.home };
|
|
1305
|
+
const lock = readLock(scope);
|
|
1306
|
+
const installed = listInstalled(scope).filter((s) => names.length === 0 || names.includes(s.name));
|
|
1307
|
+
for (const s of installed) {
|
|
1308
|
+
seen.add(s.name);
|
|
1309
|
+
const label2 = `${s.name}${scopes.length > 1 ? pc11.dim(` [${s.scope}]`) : ""}`;
|
|
1310
|
+
const entry = lock.skills[s.name];
|
|
1311
|
+
if (!entry) {
|
|
1312
|
+
lines.push(pc11.dim(`- ${label2} untracked \u2014 reinstall with \`skillmd add\` to track updates`));
|
|
1035
1313
|
continue;
|
|
1036
1314
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
lines.push(pc10.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
|
|
1315
|
+
if (entry.source.startsWith("local:")) {
|
|
1316
|
+
lines.push(pc11.dim(`- ${label2} local source \u2014 not auto-updated`));
|
|
1040
1317
|
continue;
|
|
1041
1318
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1319
|
+
try {
|
|
1320
|
+
const latest = await deps.fetchLatest(entry.source, flags, s.name);
|
|
1321
|
+
if (!latest) {
|
|
1322
|
+
failed.push(s.name);
|
|
1323
|
+
lines.push(pc11.red(`${GLYPH.blocked} ${label2} no longer available upstream (${entry.source})`));
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (entry.source.startsWith("registry:") && latest.commit_sha && entry.commit_sha && latest.commit_sha === entry.commit_sha) {
|
|
1327
|
+
lines.push(pc11.dim(`- ${label2} up to date`));
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (digestOf(latest.files) === entry.digest) {
|
|
1331
|
+
lines.push(pc11.dim(`- ${label2} up to date`));
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (flags.check) {
|
|
1335
|
+
available.push(s.name);
|
|
1336
|
+
lines.push(pc11.yellow(`\u2191 ${label2} update available`));
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const md = latest.files.find((f) => f.path === "SKILL.md");
|
|
1340
|
+
const raw = md ? typeof md.contents === "string" ? md.contents : md.contents.toString("utf8") : "";
|
|
1341
|
+
const result = lint3(raw, { slug: s.name });
|
|
1342
|
+
if (!result.ok) {
|
|
1343
|
+
failed.push(s.name);
|
|
1344
|
+
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`));
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
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" });
|
|
1348
|
+
updated.push(s.name);
|
|
1349
|
+
lines.push(pc11.green(`${GLYPH.ok} ${label2} updated`));
|
|
1350
|
+
} catch (e) {
|
|
1351
|
+
failed.push(s.name);
|
|
1352
|
+
lines.push(pc11.red(`${GLYPH.blocked} ${label2} ${e instanceof Error ? e.message : String(e)}`));
|
|
1353
|
+
}
|
|
1047
1354
|
}
|
|
1048
1355
|
}
|
|
1049
|
-
|
|
1356
|
+
for (const n of names) {
|
|
1357
|
+
if (seen.has(n)) continue;
|
|
1358
|
+
failed.push(n);
|
|
1359
|
+
lines.push(pc11.yellow(`- ${n} not installed`));
|
|
1360
|
+
}
|
|
1361
|
+
const exitCode = failed.length ? 1 : 0;
|
|
1362
|
+
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) };
|
|
1363
|
+
if (lines.length === 0) lines.push(pc11.dim("No installed skills found."));
|
|
1364
|
+
return { updated, available, failed, exitCode, output: lines.join("\n") };
|
|
1050
1365
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const hit = r?.items?.find((it) => it.slug.split("/")[1] === s.name);
|
|
1054
|
-
return hit?.slug ?? null;
|
|
1366
|
+
function scopeOptions(cmd) {
|
|
1367
|
+
return cmd.option("-g, --global", "only user-level skills").option("-p, --project", "only this project's skills").option("--json", "machine-readable output");
|
|
1055
1368
|
}
|
|
1056
1369
|
function updateCommand() {
|
|
1057
|
-
return new Command10("update").description("Update installed skills from the
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1370
|
+
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) => {
|
|
1371
|
+
const run = await runUpdate(names, { ...cmd.parent?.opts(), ...opts });
|
|
1372
|
+
console.log(run.output);
|
|
1373
|
+
process.exitCode = run.exitCode;
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
function checkCommand() {
|
|
1377
|
+
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) => {
|
|
1378
|
+
const run = await runUpdate(names, { ...cmd.parent?.opts(), ...opts, check: true });
|
|
1060
1379
|
console.log(run.output);
|
|
1380
|
+
process.exitCode = run.exitCode;
|
|
1061
1381
|
});
|
|
1062
1382
|
}
|
|
1063
1383
|
|
|
1064
1384
|
// src/commands/publish.ts
|
|
1065
1385
|
import { Command as Command11 } from "commander";
|
|
1066
|
-
import
|
|
1386
|
+
import pc12 from "picocolors";
|
|
1067
1387
|
import { lint as lint4, toText } from "@skillmds/core";
|
|
1068
1388
|
var defaultDeps2 = {
|
|
1069
1389
|
resolve: resolveSource,
|
|
@@ -1087,20 +1407,20 @@ async function runPublish(target, flags, deps = defaultDeps2) {
|
|
|
1087
1407
|
const report = toText([{ file: src.path, result }], { decorate });
|
|
1088
1408
|
if (!result.ok) {
|
|
1089
1409
|
return { result, published: false, exitCode: 1, output: `${report}
|
|
1090
|
-
${
|
|
1410
|
+
${pc12.red("\u2717 Refusing to publish: fix the errors above.")}` };
|
|
1091
1411
|
}
|
|
1092
1412
|
const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
|
|
1093
1413
|
if (hasWarn && !flags.force) {
|
|
1094
1414
|
return { result, published: false, exitCode: 1, output: `${report}
|
|
1095
|
-
${
|
|
1415
|
+
${pc12.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
|
|
1096
1416
|
}
|
|
1097
1417
|
if (flags.dryRun) {
|
|
1098
1418
|
return { result, published: false, exitCode: 0, output: `${report}
|
|
1099
|
-
${
|
|
1419
|
+
${pc12.green("\u2713 Dry run: would publish.")}` };
|
|
1100
1420
|
}
|
|
1101
1421
|
const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
|
|
1102
1422
|
return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
|
|
1103
|
-
${
|
|
1423
|
+
${pc12.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
|
|
1104
1424
|
}
|
|
1105
1425
|
function publishCommand() {
|
|
1106
1426
|
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 +1433,81 @@ function publishCommand() {
|
|
|
1113
1433
|
|
|
1114
1434
|
// src/commands/login.ts
|
|
1115
1435
|
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;
|
|
1436
|
+
import * as p5 from "@clack/prompts";
|
|
1437
|
+
import pc13 from "picocolors";
|
|
1438
|
+
var defaultLoginDeps = (flags) => promptIf(flags, async () => {
|
|
1439
|
+
const answer = await p5.password({ message: "Paste your SkillMD token (from your account page)" });
|
|
1440
|
+
return p5.isCancel(answer) ? null : answer;
|
|
1441
|
+
});
|
|
1442
|
+
function finish4(flags, result, doc) {
|
|
1443
|
+
return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
|
|
1444
|
+
}
|
|
1445
|
+
async function runLogin(flags, deps = defaultLoginDeps(flags)) {
|
|
1446
|
+
const env = flags.env ?? process.env;
|
|
1447
|
+
let token = flags.token;
|
|
1448
|
+
if (!token) {
|
|
1449
|
+
if (!deps.prompt) {
|
|
1450
|
+
const error = nonInteractiveHint("--token <token> (or set SKILLMD_TOKEN)");
|
|
1451
|
+
return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
|
|
1132
1452
|
}
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1453
|
+
const answer = await deps.prompt();
|
|
1454
|
+
if (answer === null) return finish4(flags, { exitCode: 0, output: pc13.dim("Cancelled.") }, { ok: false, cancelled: true });
|
|
1455
|
+
token = answer;
|
|
1456
|
+
}
|
|
1457
|
+
if (!token) {
|
|
1458
|
+
const error = "A token is required.";
|
|
1459
|
+
return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
|
|
1460
|
+
}
|
|
1461
|
+
const cfg = readConfig(flags.home);
|
|
1462
|
+
let host;
|
|
1463
|
+
try {
|
|
1464
|
+
host = hostOf(resolveApi({ api: flags.api, insecureHttp: flags.insecureHttp }, { env, config: { ...cfg, api: flags.api ?? cfg.api } }));
|
|
1465
|
+
} catch (e) {
|
|
1466
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1467
|
+
return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg }, null, 2) : pc13.red(msg) };
|
|
1468
|
+
}
|
|
1469
|
+
cfg.token = token;
|
|
1470
|
+
if (flags.api) cfg.api = flags.api;
|
|
1471
|
+
cfg.tokenHost = host;
|
|
1472
|
+
writeConfig(cfg, flags.home);
|
|
1473
|
+
const file = configPath(flags.home);
|
|
1474
|
+
return {
|
|
1475
|
+
exitCode: 0,
|
|
1476
|
+
output: flags.json ? JSON.stringify({ ok: true, file, host }, null, 2) : `Saved token for ${host} to ${file}`
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function runLogout(flags) {
|
|
1480
|
+
const cfg = readConfig(flags.home);
|
|
1481
|
+
const had = Boolean(cfg.token);
|
|
1482
|
+
delete cfg.token;
|
|
1483
|
+
delete cfg.tokenHost;
|
|
1484
|
+
writeConfig(cfg, flags.home);
|
|
1485
|
+
return {
|
|
1486
|
+
exitCode: 0,
|
|
1487
|
+
output: flags.json ? JSON.stringify({ ok: true, removed: had }, null, 2) : had ? "Removed stored token." : "No stored token."
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function loginCommand() {
|
|
1491
|
+
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) => {
|
|
1492
|
+
const r = await runLogin({ ...cmd.parent?.opts(), ...opts });
|
|
1493
|
+
console.log(r.output);
|
|
1494
|
+
process.exitCode = r.exitCode;
|
|
1138
1495
|
});
|
|
1139
1496
|
}
|
|
1140
1497
|
function logoutCommand() {
|
|
1141
|
-
return new Command12("logout").description("Remove the stored SkillMD token").action(() => {
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
console.log("Removed stored token.");
|
|
1498
|
+
return new Command12("logout").description("Remove the stored SkillMD token").option("--json", "machine-readable output").action((opts, cmd) => {
|
|
1499
|
+
const r = runLogout({ ...cmd.parent?.opts(), ...opts });
|
|
1500
|
+
console.log(r.output);
|
|
1501
|
+
process.exitCode = r.exitCode;
|
|
1146
1502
|
});
|
|
1147
1503
|
}
|
|
1148
1504
|
|
|
1149
1505
|
// src/commands/interactive.ts
|
|
1150
|
-
import * as
|
|
1151
|
-
import
|
|
1152
|
-
import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
|
|
1506
|
+
import * as p6 from "@clack/prompts";
|
|
1507
|
+
import pc14 from "picocolors";
|
|
1153
1508
|
function bail(v) {
|
|
1154
|
-
if (
|
|
1155
|
-
|
|
1509
|
+
if (p6.isCancel(v)) {
|
|
1510
|
+
p6.cancel("Cancelled.");
|
|
1156
1511
|
return true;
|
|
1157
1512
|
}
|
|
1158
1513
|
return false;
|
|
@@ -1160,25 +1515,26 @@ function bail(v) {
|
|
|
1160
1515
|
async function browseSearchResults(items) {
|
|
1161
1516
|
const cols = process.stdout.columns ?? 80;
|
|
1162
1517
|
for (; ; ) {
|
|
1163
|
-
const picked = await
|
|
1518
|
+
const picked = await p6.select({
|
|
1164
1519
|
message: "Search results",
|
|
1165
1520
|
options: [
|
|
1166
1521
|
...items.map((item2) => {
|
|
1167
1522
|
const starsTxt = item2.repo_stars != null ? ` \u2605${formatStars(item2.repo_stars)}` : "";
|
|
1168
1523
|
const checkTxt = item2.verified ? " \u2713" : "";
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1524
|
+
const slugTxt = safeText(item2.slug, 200);
|
|
1525
|
+
const plainLabel = `${slugTxt}${starsTxt}${checkTxt}`;
|
|
1526
|
+
const label2 = `${pc14.bold(slugTxt)}${starsTxt ? pc14.dim(starsTxt) : ""}${checkTxt ? pc14.green(checkTxt) : ""}`;
|
|
1171
1527
|
const hintWidth = Math.max(10, cols - plainLabel.length - 6);
|
|
1172
1528
|
return {
|
|
1173
1529
|
value: item2.slug,
|
|
1174
1530
|
label: label2,
|
|
1175
|
-
hint: truncate(item2.description ?? "", hintWidth)
|
|
1531
|
+
hint: truncate(safeText(item2.description ?? ""), hintWidth)
|
|
1176
1532
|
};
|
|
1177
1533
|
}),
|
|
1178
1534
|
{ value: "__back", label: "\u2190 Back to menu" }
|
|
1179
1535
|
]
|
|
1180
1536
|
});
|
|
1181
|
-
if (
|
|
1537
|
+
if (p6.isCancel(picked) || picked === "__back") return;
|
|
1182
1538
|
const item = items.find((i) => i.slug === picked);
|
|
1183
1539
|
if (!item) continue;
|
|
1184
1540
|
await browseOneResult(item);
|
|
@@ -1186,14 +1542,15 @@ async function browseSearchResults(items) {
|
|
|
1186
1542
|
}
|
|
1187
1543
|
async function browseOneResult(item) {
|
|
1188
1544
|
const starsTxt = item.repo_stars != null ? `\u2605 ${formatStars(item.repo_stars)} stars` : "";
|
|
1545
|
+
const slugTxt = safeText(item.slug, 200);
|
|
1189
1546
|
const meta = [
|
|
1190
1547
|
starsTxt,
|
|
1191
|
-
item.verified ?
|
|
1548
|
+
item.verified ? pc14.green("\u2713 verified") : "unverified",
|
|
1192
1549
|
item.type === "pack" ? "pack" : ""
|
|
1193
1550
|
].filter(Boolean).join(" \xB7 ");
|
|
1194
|
-
|
|
1551
|
+
p6.note([safeText(item.description ?? ""), pc14.dim(meta)].join("\n"), slugTxt);
|
|
1195
1552
|
for (; ; ) {
|
|
1196
|
-
const action = await
|
|
1553
|
+
const action = await p6.select({
|
|
1197
1554
|
message: "What next?",
|
|
1198
1555
|
options: [
|
|
1199
1556
|
{ value: "project", label: "Install to this project" },
|
|
@@ -1202,33 +1559,36 @@ async function browseOneResult(item) {
|
|
|
1202
1559
|
{ value: "back", label: "\u2190 Back to results" }
|
|
1203
1560
|
]
|
|
1204
1561
|
});
|
|
1205
|
-
if (
|
|
1562
|
+
if (p6.isCancel(action) || action === "back") return;
|
|
1206
1563
|
if (action === "url") {
|
|
1207
|
-
|
|
1564
|
+
p6.log.message(pc14.dim(`https://skillmd.com/skills/${slugTxt}`));
|
|
1208
1565
|
continue;
|
|
1209
1566
|
}
|
|
1210
|
-
|
|
1567
|
+
p6.log.step(`Installing ${slugTxt}\u2026`);
|
|
1211
1568
|
try {
|
|
1212
|
-
const run = await runAdd(item.slug, action === "global" ? { global: true } : {});
|
|
1569
|
+
const run = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
|
|
1213
1570
|
console.log(run.output);
|
|
1214
|
-
if (run.exitCode === 0)
|
|
1215
|
-
else
|
|
1571
|
+
if (run.exitCode === 0) p6.log.success("Installed.");
|
|
1572
|
+
else p6.log.warn("Not installed.");
|
|
1216
1573
|
} catch (err) {
|
|
1217
|
-
|
|
1574
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1218
1575
|
}
|
|
1219
1576
|
}
|
|
1220
1577
|
}
|
|
1221
1578
|
async function runInteractive() {
|
|
1222
|
-
|
|
1579
|
+
p6.intro(banner());
|
|
1223
1580
|
for (; ; ) {
|
|
1224
|
-
const action = await
|
|
1581
|
+
const action = await p6.select({
|
|
1225
1582
|
message: "What would you like to do?",
|
|
1226
1583
|
options: [
|
|
1227
1584
|
{ value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
|
|
1228
1585
|
{ value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
|
|
1229
1586
|
{ value: "search", label: "Search the registry" },
|
|
1587
|
+
{ value: "install", label: "Install a skill", hint: "registry slug, GitHub URL or local path" },
|
|
1230
1588
|
{ value: "init", label: "Create a new skill" },
|
|
1231
1589
|
{ value: "list", label: "List installed skills" },
|
|
1590
|
+
{ value: "update", label: "Update installed skills" },
|
|
1591
|
+
{ value: "remove", label: "Remove skills" },
|
|
1232
1592
|
{ value: "help", label: "Show all commands" },
|
|
1233
1593
|
{ value: "quit", label: "Quit" }
|
|
1234
1594
|
]
|
|
@@ -1237,10 +1597,10 @@ async function runInteractive() {
|
|
|
1237
1597
|
switch (action) {
|
|
1238
1598
|
case "lint":
|
|
1239
1599
|
case "scan": {
|
|
1240
|
-
const path = await
|
|
1600
|
+
const path = await p6.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
|
|
1241
1601
|
if (bail(path)) break;
|
|
1242
1602
|
const target = String(path || ".");
|
|
1243
|
-
const s =
|
|
1603
|
+
const s = p6.spinner();
|
|
1244
1604
|
s.start(action === "lint" ? "Linting" : "Scanning");
|
|
1245
1605
|
try {
|
|
1246
1606
|
const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
|
|
@@ -1248,74 +1608,97 @@ async function runInteractive() {
|
|
|
1248
1608
|
console.log(run.output);
|
|
1249
1609
|
process.exitCode = run.exitCode;
|
|
1250
1610
|
} catch (err) {
|
|
1251
|
-
s.stop(
|
|
1611
|
+
s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
|
|
1252
1612
|
process.exitCode = 1;
|
|
1253
1613
|
}
|
|
1254
1614
|
break;
|
|
1255
1615
|
}
|
|
1256
1616
|
case "search": {
|
|
1257
|
-
const query = await
|
|
1617
|
+
const query = await p6.text({ message: "Search the registry for" });
|
|
1258
1618
|
if (bail(query)) break;
|
|
1259
|
-
const verified = await
|
|
1619
|
+
const verified = await p6.confirm({ message: "Verified skills only?", initialValue: false });
|
|
1260
1620
|
if (bail(verified)) break;
|
|
1261
|
-
const s =
|
|
1621
|
+
const s = p6.spinner();
|
|
1262
1622
|
s.start("Searching");
|
|
1263
1623
|
try {
|
|
1264
1624
|
const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
|
|
1265
1625
|
s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
|
|
1266
1626
|
if (run.items.length === 0) {
|
|
1267
|
-
|
|
1627
|
+
p6.log.info("No results.");
|
|
1268
1628
|
break;
|
|
1269
1629
|
}
|
|
1270
1630
|
await browseSearchResults(run.items);
|
|
1271
1631
|
} catch (err) {
|
|
1272
|
-
s.stop(
|
|
1632
|
+
s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
|
|
1273
1633
|
process.exitCode = 1;
|
|
1274
1634
|
}
|
|
1275
1635
|
break;
|
|
1276
1636
|
}
|
|
1277
1637
|
case "init": {
|
|
1278
|
-
const name = await
|
|
1638
|
+
const name = await p6.text({ message: "Skill name", placeholder: "my-skill" });
|
|
1279
1639
|
if (bail(name)) break;
|
|
1280
|
-
const description = await
|
|
1640
|
+
const description = await p6.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
|
|
1281
1641
|
if (bail(description)) break;
|
|
1282
1642
|
try {
|
|
1283
1643
|
const file = initSkill(String(name), { description: String(description || "") });
|
|
1284
|
-
|
|
1644
|
+
p6.log.success(`Created ${pc14.bold(file)}`);
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1647
|
+
process.exitCode = 1;
|
|
1648
|
+
}
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
case "install": {
|
|
1652
|
+
const src = await p6.text({ message: "Skill source", placeholder: "owner/name, https://github.com/\u2026, or ./path" });
|
|
1653
|
+
if (bail(src)) break;
|
|
1654
|
+
try {
|
|
1655
|
+
const run = await runAdd(String(src), {});
|
|
1656
|
+
console.log(run.output);
|
|
1657
|
+
process.exitCode = run.exitCode;
|
|
1285
1658
|
} catch (err) {
|
|
1286
|
-
|
|
1659
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1287
1660
|
process.exitCode = 1;
|
|
1288
1661
|
}
|
|
1289
1662
|
break;
|
|
1290
1663
|
}
|
|
1291
1664
|
case "list": {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1665
|
+
console.log(runList({}).output);
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
case "update": {
|
|
1669
|
+
try {
|
|
1670
|
+
const run = await runUpdate([], {});
|
|
1671
|
+
console.log(run.output);
|
|
1672
|
+
process.exitCode = run.exitCode;
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1675
|
+
process.exitCode = 1;
|
|
1676
|
+
}
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
case "remove": {
|
|
1680
|
+
try {
|
|
1681
|
+
const run = await runRemove([], {});
|
|
1682
|
+
console.log(run.output);
|
|
1683
|
+
process.exitCode = run.exitCode;
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
p6.log.error(err instanceof Error ? err.message : String(err));
|
|
1686
|
+
process.exitCode = 1;
|
|
1304
1687
|
}
|
|
1305
1688
|
break;
|
|
1306
1689
|
}
|
|
1307
1690
|
case "help": {
|
|
1308
|
-
|
|
1691
|
+
p6.note(
|
|
1309
1692
|
[
|
|
1310
|
-
`${
|
|
1311
|
-
`${
|
|
1312
|
-
`${
|
|
1313
|
-
`${
|
|
1314
|
-
`${
|
|
1315
|
-
`${
|
|
1316
|
-
`${
|
|
1317
|
-
`${
|
|
1318
|
-
`${
|
|
1693
|
+
`${pc14.bold("lint")} [path] validate SKILL.md + quality score`,
|
|
1694
|
+
`${pc14.bold("scan")} [path] security scan`,
|
|
1695
|
+
`${pc14.bold("search")} <query> search the registry`,
|
|
1696
|
+
`${pc14.bold("add")} <source> install a skill (lints first)`,
|
|
1697
|
+
`${pc14.bold("info")} <slug> registry details for a skill`,
|
|
1698
|
+
`${pc14.bold("init")} [name] scaffold a new skill`,
|
|
1699
|
+
`${pc14.bold("list / remove / update")} manage installed skills`,
|
|
1700
|
+
`${pc14.bold("publish")} [path] publish (fails on lint errors)`,
|
|
1701
|
+
`${pc14.bold("rules / login")}`
|
|
1319
1702
|
].join("\n"),
|
|
1320
1703
|
"Commands (run: skillmd <command>)"
|
|
1321
1704
|
);
|
|
@@ -1323,16 +1706,39 @@ async function runInteractive() {
|
|
|
1323
1706
|
}
|
|
1324
1707
|
}
|
|
1325
1708
|
}
|
|
1326
|
-
|
|
1709
|
+
p6.outro(pc14.dim("skillmd.com"));
|
|
1327
1710
|
}
|
|
1328
1711
|
|
|
1329
1712
|
// src/index.ts
|
|
1330
1713
|
var version = "0.0.0";
|
|
1331
1714
|
try {
|
|
1332
|
-
version = JSON.parse(
|
|
1715
|
+
version = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
1333
1716
|
} catch {
|
|
1334
1717
|
}
|
|
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>", "
|
|
1718
|
+
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)");
|
|
1719
|
+
program.configureHelp({ sortSubcommands: false });
|
|
1720
|
+
program.addHelpText("after", `
|
|
1721
|
+
Examples:
|
|
1722
|
+
$ skillmd add anthropic/pdf install from the registry (asks project vs global)
|
|
1723
|
+
$ skillmd add owner/repo/skills/x#main -g a GitHub subfolder at a ref, into your user dirs
|
|
1724
|
+
$ skillmd add ./my-skill -a claude-code -p a local skill into this project for one agent
|
|
1725
|
+
$ skillmd add anthropic/pdf -y --json non-interactive, machine-readable
|
|
1726
|
+
$ skillmd list everything installed, project and global
|
|
1727
|
+
$ skillmd check which installed skills have updates
|
|
1728
|
+
$ skillmd update -g update your user-level skills
|
|
1729
|
+
$ skillmd remove pdf remove a skill from every agent
|
|
1730
|
+
|
|
1731
|
+
Quality: lint scan rules init
|
|
1732
|
+
Registry: search info add list remove update check
|
|
1733
|
+
Publish: publish login logout
|
|
1734
|
+
|
|
1735
|
+
Docs: https://skillmd.com/docs/cli`);
|
|
1736
|
+
program.hook("preAction", (thisCmd) => {
|
|
1737
|
+
const opts = thisCmd.opts();
|
|
1738
|
+
if (opts.token && process.stderr.isTTY) {
|
|
1739
|
+
console.error(pc15.yellow("\u26A0 --token is visible in your shell history and process list; prefer `skillmd login` or SKILLMD_TOKEN."));
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1336
1742
|
program.addCommand(lintCommand());
|
|
1337
1743
|
program.addCommand(scanCommand());
|
|
1338
1744
|
program.addCommand(rulesCommand());
|
|
@@ -1343,6 +1749,7 @@ program.addCommand(addCommand());
|
|
|
1343
1749
|
program.addCommand(listCommand());
|
|
1344
1750
|
program.addCommand(removeCommand());
|
|
1345
1751
|
program.addCommand(updateCommand());
|
|
1752
|
+
program.addCommand(checkCommand());
|
|
1346
1753
|
program.addCommand(publishCommand());
|
|
1347
1754
|
program.addCommand(loginCommand());
|
|
1348
1755
|
program.addCommand(logoutCommand());
|