skillmds 1.1.2 → 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/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 readFileSync4 } from "fs";
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("\u2716")} ${pc.bold(label(file))}`];
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(`\u2713 No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
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").alias("check").description("Validate SKILL.md file(s) against the spec and report a quality score").argument("[target]", "local path or GitHub source", ".").option("--format <fmt>", "text | json | sarif | github", "text").option("--strict", "treat warnings as errors").option("--fail-on-warning", "exit non-zero when warnings exist").option("--fix", "apply safe automatic fixes (e.g. add missing license)").option("--errors-only", "show only skills that failed").action(async (target, opts) => {
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 (resolve(target) === homedir()) {
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 as existsSync2 } from "fs";
406
- import { join as join2 } from "path";
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 = join2(opts.dir ?? process.cwd(), slug);
438
- if (existsSync2(join2(root, "SKILL.md"))) throw new Error(`SKILL.md already exists at ${root}`);
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 = join2(root, "SKILL.md");
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
- function initCommand() {
445
- return new Command4("init").description("Scaffold a new SKILL.md from a template").argument("[name]", "skill name").option("--name <name>", "skill name (non-interactive)").option("--description <text>", "skill description").option("--license <license>", "license identifier", "MIT").option("-y, --yes", "skip prompts").action(async (nameArg, opts) => {
446
- let name = nameArg ?? opts.name;
447
- if (!name && !opts.yes) {
448
- const answer = await p2.text({ message: "Skill name?", placeholder: "my-skill" });
449
- if (p2.isCancel(answer)) {
450
- p2.cancel("Cancelled.");
451
- return;
452
- }
453
- name = answer;
454
- }
455
- if (!name) {
456
- console.error("A skill name is required (pass a name or --name).");
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 file = initSkill(name, { description: opts.description, license: opts.license });
461
- console.log(`Created ${file}`);
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
- var RegistryError = class extends Error {
510
- constructor(message, status) {
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
- return { api, base, token, hasToken: Boolean(token) };
537
- }
538
- async function fetchBundle(base, slug, token) {
539
- const headers = { accept: "application/json" };
540
- if (token) headers.authorization = `Bearer ${token}`;
541
- const res = await fetch(`${base}/api/skills/${slug}/bundle?format=json`, { headers }).catch(() => null);
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
- return out;
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 ? pc5.dim(`\u2605 ${formatStars(item.repo_stars)}`) : "";
584
- const check = item.verified ? pc5.green("\u2713") : "";
585
- const pack = item.type === "pack" ? pc5.dim("[pack]") : "";
586
- const head = [pc5.bold(item.slug), stars, check, pack].filter(Boolean).join(" ");
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}${pc5.dim(truncate(item.description ?? "", lineWidth))}`;
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 pc6 from "picocolors";
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: pc6.red("Expected owner/name, e.g. anthropic/pdf") };
577
+ return { ok: false, exitCode: 1, output: pc7.red("Expected owner/name, e.g. anthropic/pdf") };
621
578
  }
622
- const { api } = createClient(flags);
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: pc6.red(`Skill not found on the registry: ${slug}`) };
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: pc6.red(`Could not reach the SkillMD registry: ${message}`) };
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
- `${pc6.bold(s.slug)} ${pc6.dim(`[${s.type ?? "single"}]`)} ${s.verified ? pc6.green("\u2713 verified") : pc6.yellow("unverified")}`
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
- pc6.dim(
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
- pc6.dim(` security: ${(s.security_flags ?? []).join(", ") || "unscanned"} \xB7 license: ${s.provenance?.license ?? s.license ?? "unknown"}`)
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(pc6.dim(` source: ${sourceRepo}${commitSha ? ` @ ${commitSha.slice(0, 8)}` : ""}`));
650
- lines.push(pc6.dim(` page: https://skillmd.com/skills/${s.slug}`));
651
- lines.push(pc6.dim(` install: ${s.install?.cli ?? `npx skillmds add ${s.slug}`}`));
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 { join as join4, resolve as resolve2, dirname as dirname2, sep as sep2 } from "path";
675
- var AGENTS = [
676
- { id: "claude-code", project: [".claude", "skills"], global: [".claude", "skills"], detect: [[".claude"]] },
677
- { id: "cursor", project: [".cursor", "skills"], global: [".cursor", "skills"], detect: [[".cursor"]] },
678
- { id: "codex", project: [".codex", "skills"], global: [".codex", "skills"], detect: [[".codex"]] },
679
- { id: "windsurf", project: [".windsurf", "skills"], global: [".codeium", "windsurf", "skills"], detect: [[".codeium", "windsurf"], [".windsurf"]] },
680
- { id: "opencode", project: [".opencode", "skills"], global: [".config", "opencode", "skills"], detect: [[".config", "opencode"], [".opencode"]] },
681
- { id: "gemini-cli", project: [".agents", "skills"], global: [".gemini", "skills"], detect: [[".gemini"]] },
682
- { id: "antigravity", project: [".agents", "skills"], global: [".gemini", "antigravity", "skills"], detect: [[".gemini", "antigravity"], [".antigravity"]] },
683
- { id: "kiro", project: [".kiro", "skills"], global: [".kiro", "skills"], detect: [[".kiro"]] },
684
- { id: "warp", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".warp"]] },
685
- { id: "github-copilot", project: [".agents", "skills"], global: [".copilot", "skills"], detect: [[".copilot"]] },
686
- { id: "cline", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".cline"]] },
687
- { id: "roo", project: [".roo", "skills"], global: [".roo", "skills"], detect: [[".roo"]] },
688
- { id: "goose", project: [".goose", "skills"], global: [".config", "goose", "skills"], detect: [[".config", "goose"]] },
689
- { id: "trae", project: [".trae", "skills"], global: [".trae", "skills"], detect: [[".trae"]] },
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
- // src/commands/add.ts
797
- function isLocalish(arg) {
798
- return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync4(arg);
799
- }
800
- async function resolvePackFiles(skill, fetchTree = resolveTree) {
801
- if (!skill.source_repo) return null;
802
- const attempts = [{ ref: skill.commit_sha ?? void 0, pinMiss: false }];
803
- if (skill.commit_sha) attempts.push({ ref: void 0, pinMiss: true });
804
- for (const attempt of attempts) {
805
- try {
806
- const files = await fetchTree(skill.source_repo, attempt.ref);
807
- const sk = files.find((f) => f.path === "SKILL.md");
808
- if (sk) return { raw: sk.contents.toString("utf8"), files, pinMiss: attempt.pinMiss };
809
- } catch {
810
- }
811
- }
812
- return null;
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
- var defaultDeps = {
826
- async resolve(arg, flags) {
827
- if (isLocalish(arg)) {
828
- const rs = await resolveSource(arg);
829
- return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
830
- }
831
- let registryDown = null;
832
- const registryHost = (() => {
833
- try {
834
- return new URL(createClient(flags).base).host;
835
- } catch {
836
- return "api.skillmd.com";
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
- if (/^[^/\s:]+\/[^/\s:]+$/.test(arg)) {
840
- const { api, base: apiBase, token } = createClient(flags);
841
- const [owner, name] = arg.split("/");
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: arg,
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, arg, token);
736
+ const bundle = await fetchBundle(apiBase, registrySlug, token);
855
737
  if (bundle && bundle.length) {
856
- const sk = bundle.find((f) => f.path === "SKILL.md");
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
- try {
869
- const rs = await resolveSource(arg);
870
- const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
871
- return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, note: note2 }));
872
- } catch (e) {
873
- if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
874
- throw e;
875
- }
876
- },
877
- fireInstall(registrySlug, flags) {
878
- const { api } = createClient(flags);
879
- const [owner, name] = registrySlug.split("/");
880
- void api(`/api/skills/${owner}/${name}/install`, {
881
- method: "POST",
882
- headers: { "content-type": "application/json" },
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
- async function runAdd(arg, flags, deps = defaultDeps) {
889
- let candidates;
890
- try {
891
- candidates = await deps.resolve(arg, flags);
892
- } catch (e) {
893
- if (e instanceof IntegrityError) {
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: [{ name: arg, reason: e.message }],
897
- exitCode: 1,
898
- output: pc7.red(`\u2717 ${arg} blocked \u2014 ${e.message}`)
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
- throw e;
902
- }
903
- if (flags.skill?.length) candidates = candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
904
- const deny = new Set(flags.deny ?? []);
905
- const written = [];
906
- const blocked = [];
907
- const lines = [];
908
- const home = flags.home ?? homedir4();
909
- const isGlobal = flags.global || resolve3(flags.cwd ?? process.cwd()) === resolve3(home);
910
- const scope = { global: isGlobal, cwd: flags.cwd, home: flags.home };
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
- if (deniedFlags.length) {
928
- blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
929
- lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`));
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 files = c.files ?? [{ path: "SKILL.md", contents: c.raw }];
933
- for (const [dir, agentIds] of dirAgents) {
934
- const out = writeSkill(c.name, files, dir, { mode: "copy" });
935
- written.push({ name: c.name, dir: out });
936
- lines.push(pc7.green(`\u2713 ${c.name} \u2192 ${out}`) + pc7.dim(` (${agentIds.join(", ")}; score ${result.score}${allFlags.length ? `; ${allFlags.join(", ")}` : ""})`));
937
- }
938
- if (c.pinMiss) {
939
- lines.push(pc7.yellow(`\u26A0 ${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
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
- if (c.note) {
942
- lines.push(pc7.yellow(`\u26A0 ${c.name}: ${c.note}`));
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
- if (c.type === "pack" && !c.files) {
945
- lines.push(pc7.yellow(`\u26A0 ${c.name} is a pack but its asset files could not be fetched \u2014 only SKILL.md was installed. It may reference missing files.`));
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 (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
988
+ if (flags.json) return finish2(flags, { kind: "fail", error: e instanceof Error ? e.message : String(e) });
989
+ throw e;
948
990
  }
949
- const exitCode = written.length === 0 ? 1 : 0;
950
- return { written, blocked, exitCode, output: lines.join("\n") };
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 the user directory instead of 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("--copy", "copy files (default)", true).option("-y, --yes", "skip confirmation prompts").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) => {
954
- const merged = { ...cmd.parent?.opts(), ...opts };
955
- const run = await runAdd(source, merged);
956
- console.log(run.output);
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 pc8 from "picocolors";
964
- import { parseSkillMd as parseSkillMd2 } from "@skillmds/core";
965
- function listCommand() {
966
- return new Command8("list").alias("ls").description("List installed skills across detected agents").option("-g, --global", "only global skills").option("--json", "machine-readable output").action((opts) => {
967
- const scope = { global: opts.global };
968
- const skills = installedSkills(scope);
969
- if (opts.json) {
970
- console.log(JSON.stringify(skills.map((s) => ({ name: s.name, agent: s.agent, scope: s.scope, path: s.path })), null, 2));
971
- return;
972
- }
973
- if (skills.length === 0) {
974
- console.log("No installed skills found.");
975
- return;
976
- }
977
- for (const s of skills) {
978
- const parsed = parseSkillMd2(s.raw);
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 as rmSync3 } from "fs";
989
- import pc9 from "picocolors";
990
- function runRemove(names, scope = {}) {
991
- const installed = installedSkills(scope);
992
- const targets = installed.filter((s) => names.includes(s.name));
993
- const removed = [];
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
- for (const t of targets) {
996
- rmSync3(t.path, { recursive: true, force: true });
997
- removed.push(t.path);
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
- for (const n of names) {
1001
- if (!targets.some((t) => t.name === n)) lines.push(pc9.yellow(`- ${n} not installed`));
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
- return { removed, output: lines.join("\n") };
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("<names...>", "skill names to remove").option("-g, --global", "only global skills").action((names, opts) => {
1007
- const run = runRemove(names, { global: opts.global });
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 { writeFileSync as writeFileSync5 } from "fs";
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
- async function runUpdate(names, flags, scope = {}) {
1019
- const { api } = createClient(flags);
1020
- const installed = installedSkills(scope).filter((s) => names.length === 0 || names.includes(s.name));
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
- for (const s of installed) {
1024
- const slug = await findRegistrySlug(api, s);
1025
- if (!slug) {
1026
- lines.push(pc10.dim(`- ${s.name} skipped (no registry match)`));
1027
- continue;
1028
- }
1029
- try {
1030
- const [owner, name] = slug.split("/");
1031
- const skill = await api(`/api/skills/${owner}/${name}`);
1032
- const next = skillMdFor(skill);
1033
- if (next.trim() === s.raw.trim()) {
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
- const result = lint3(next, { slug: s.name });
1038
- if (!result.ok) {
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
- writeFileSync5(join5(s.path, "SKILL.md"), next, "utf8");
1043
- updated.push(s.name);
1044
- lines.push(pc10.green(`\u2713 updated ${s.name}`));
1045
- } catch {
1046
- lines.push(pc10.red(`\u2717 ${s.name} update error`));
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
- return { updated, output: lines.join("\n") || "Nothing to update." };
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
- async function findRegistrySlug(api, s) {
1052
- const r = await api(`/api/search?q=${encodeURIComponent(s.name)}&limit=5`).catch(() => null);
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 registry (re-lints before writing)").argument("[names...]", "specific skills to update (default: all)").option("-g, --global", "only global skills").action(async (names, opts, cmd) => {
1058
- const merged = { ...cmd.parent?.opts(), ...opts };
1059
- const run = await runUpdate(names, merged, { global: opts.global });
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 pc11 from "picocolors";
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
- ${pc11.red("\u2717 Refusing to publish: fix the errors above.")}` };
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
- ${pc11.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
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
- ${pc11.green("\u2713 Dry run: would publish.")}` };
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
- ${pc11.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
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 p3 from "@clack/prompts";
1117
- function loginCommand() {
1118
- return new Command12("login").description("Store a SkillMD personal access token for publishing").option("--token <token>", "token to store (otherwise prompt)").option("--api <url>", "registry API base to store").action(async (opts) => {
1119
- let token = opts.token;
1120
- if (!token) {
1121
- const answer = await p3.password({ message: "Paste your SkillMD token (from your account page)" });
1122
- if (p3.isCancel(answer)) {
1123
- p3.cancel("Cancelled.");
1124
- return;
1125
- }
1126
- token = answer;
1127
- }
1128
- if (!token) {
1129
- console.error("A token is required.");
1130
- process.exitCode = 1;
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 cfg = readConfig();
1134
- cfg.token = token;
1135
- if (opts.api) cfg.api = opts.api;
1136
- writeConfig(cfg);
1137
- console.log(`Saved token to ${configPath()}`);
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 cfg = readConfig();
1143
- delete cfg.token;
1144
- writeConfig(cfg);
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 p4 from "@clack/prompts";
1151
- import pc12 from "picocolors";
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 (p4.isCancel(v)) {
1155
- p4.cancel("Cancelled.");
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 p4.select({
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 plainLabel = `${item2.slug}${starsTxt}${checkTxt}`;
1170
- const label2 = `${pc12.bold(item2.slug)}${starsTxt ? pc12.dim(starsTxt) : ""}${checkTxt ? pc12.green(checkTxt) : ""}`;
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 (p4.isCancel(picked) || picked === "__back") return;
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 ? pc12.green("\u2713 verified") : "unverified",
1548
+ item.verified ? pc14.green("\u2713 verified") : "unverified",
1192
1549
  item.type === "pack" ? "pack" : ""
1193
1550
  ].filter(Boolean).join(" \xB7 ");
1194
- p4.note([item.description ?? "", pc12.dim(meta)].join("\n"), item.slug);
1551
+ p6.note([safeText(item.description ?? ""), pc14.dim(meta)].join("\n"), slugTxt);
1195
1552
  for (; ; ) {
1196
- const action = await p4.select({
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 (p4.isCancel(action) || action === "back") return;
1562
+ if (p6.isCancel(action) || action === "back") return;
1206
1563
  if (action === "url") {
1207
- p4.log.message(pc12.dim(`https://skillmd.com/skills/${item.slug}`));
1564
+ p6.log.message(pc14.dim(`https://skillmd.com/skills/${slugTxt}`));
1208
1565
  continue;
1209
1566
  }
1210
- p4.log.step(`Installing ${item.slug}\u2026`);
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) p4.log.success("Installed.");
1215
- else p4.log.warn("Not installed.");
1571
+ if (run.exitCode === 0) p6.log.success("Installed.");
1572
+ else p6.log.warn("Not installed.");
1216
1573
  } catch (err) {
1217
- p4.log.error(err instanceof Error ? err.message : String(err));
1574
+ p6.log.error(err instanceof Error ? err.message : String(err));
1218
1575
  }
1219
1576
  }
1220
1577
  }
1221
1578
  async function runInteractive() {
1222
- p4.intro(pc12.bgCyan(pc12.black(" skillmd ")));
1579
+ p6.intro(banner());
1223
1580
  for (; ; ) {
1224
- const action = await p4.select({
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 p4.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
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 = p4.spinner();
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(pc12.red(err instanceof Error ? err.message : String(err)));
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 p4.text({ message: "Search the registry for" });
1617
+ const query = await p6.text({ message: "Search the registry for" });
1258
1618
  if (bail(query)) break;
1259
- const verified = await p4.confirm({ message: "Verified skills only?", initialValue: false });
1619
+ const verified = await p6.confirm({ message: "Verified skills only?", initialValue: false });
1260
1620
  if (bail(verified)) break;
1261
- const s = p4.spinner();
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
- p4.log.info("No results.");
1627
+ p6.log.info("No results.");
1268
1628
  break;
1269
1629
  }
1270
1630
  await browseSearchResults(run.items);
1271
1631
  } catch (err) {
1272
- s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
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 p4.text({ message: "Skill name", placeholder: "my-skill" });
1638
+ const name = await p6.text({ message: "Skill name", placeholder: "my-skill" });
1279
1639
  if (bail(name)) break;
1280
- const description = await p4.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
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
- p4.log.success(`Created ${pc12.bold(file)}`);
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
- p4.log.error(err instanceof Error ? err.message : String(err));
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
- const skills = [...installedSkills({}), ...installedSkills({ global: true })];
1293
- if (skills.length === 0) {
1294
- p4.log.info("No installed skills found (checked this folder and your user directory).");
1295
- } else {
1296
- p4.log.message(
1297
- skills.map((sk) => {
1298
- const parsed = parseSkillMd3(sk.raw);
1299
- const desc = "error" in parsed ? pc12.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc12.dim(parsed.description.slice(0, 70));
1300
- return `${pc12.bold(sk.name)} ${pc12.dim(`[${sk.agent}/${sk.scope}]`)}
1301
- ${desc}`;
1302
- }).join("\n")
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
- p4.note(
1691
+ p6.note(
1309
1692
  [
1310
- `${pc12.bold("lint")} [path] validate SKILL.md + quality score`,
1311
- `${pc12.bold("scan")} [path] security scan`,
1312
- `${pc12.bold("search")} <query> search the registry`,
1313
- `${pc12.bold("add")} <source> install a skill (lints first)`,
1314
- `${pc12.bold("info")} <slug> registry details for a skill`,
1315
- `${pc12.bold("init")} [name] scaffold a new skill`,
1316
- `${pc12.bold("list / remove / update")} manage installed skills`,
1317
- `${pc12.bold("publish")} [path] publish (fails on lint errors)`,
1318
- `${pc12.bold("rules / login")}`
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
- p4.outro(pc12.dim("skillmd.com"));
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(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
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>", "SkillMD personal access token").option("--api <url>", "override the registry API base URL");
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());