skillmds 1.1.2 → 1.2.1

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