get-skill 1.0.21 → 1.3.7

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,433 +1,55 @@
1
- // ../cli/src/index.ts
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
+ missingFilesNote,
20
+ parseSource,
21
+ readConfig,
22
+ readLock,
23
+ resolveApi,
24
+ resolvePackFiles,
25
+ resolveSource,
26
+ resolveTree,
27
+ safeText,
28
+ skillMdFor,
29
+ sourceId,
30
+ telemetryDisabled,
31
+ uninstallSkill,
32
+ upsertEntry,
33
+ writeConfig
34
+ } from "./chunk-MPBRLZWJ.js";
35
+
36
+ // src/index.ts
2
37
  import { Command as Command13 } from "commander";
3
- import { readFileSync as readFileSync4 } from "fs";
38
+ import { readFileSync as readFileSync3 } from "fs";
39
+ import pc15 from "picocolors";
4
40
 
5
- // ../cli/src/commands/lint.ts
41
+ // src/commands/lint.ts
6
42
  import { Command } from "commander";
7
43
  import { writeFileSync } from "fs";
8
- import { homedir } from "os";
9
- import { resolve } from "path";
44
+ import { homedir as homedir2 } from "os";
45
+ import { resolve as resolve2 } from "path";
10
46
  import pc2 from "picocolors";
11
47
  import * as p from "@clack/prompts";
48
+ import { lint, toJson, toSarif, toGithub } from "@skillmds/core";
12
49
 
13
- // ../../packages/core/src/parse.ts
14
- import { parse as parseYaml } from "yaml";
15
- var MAX_SKILL_BYTES = 256 * 1024;
16
- var MAX_DESCRIPTION = 1024;
17
- var MAX_NAME = 120;
18
- var slugify = (s) => String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "skill";
19
- function parseSkillMd(raw, type = "single") {
20
- if (!raw || raw.length > MAX_SKILL_BYTES) return { error: "SKILL.md missing or too large (max 256KB)" };
21
- const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
22
- if (!m) return { error: "Missing YAML frontmatter (--- ... ---)" };
23
- let fm;
24
- try {
25
- fm = parseYaml(m[1] ?? "") ?? {};
26
- } catch {
27
- return { error: "Invalid YAML frontmatter" };
28
- }
29
- const name = String(fm.name ?? "").trim();
30
- const description = String(fm.description ?? "").replace(/\s+/g, " ").trim();
31
- if (!name) return { error: "frontmatter 'name' is required" };
32
- if (!description) return { error: "frontmatter 'description' is required" };
33
- if (description.length > MAX_DESCRIPTION) return { error: `description must be <= ${MAX_DESCRIPTION} chars` };
34
- return {
35
- name: name.slice(0, MAX_NAME),
36
- description: description.slice(0, MAX_DESCRIPTION),
37
- license: fm.license ? String(fm.license).slice(0, 64) : null,
38
- body: m[2] ?? "",
39
- type
40
- };
41
- }
42
-
43
- // ../../packages/core/src/security.ts
44
- var UNTRUSTED_INSTALL_RE = new RegExp(
45
- [
46
- /(?:npx|npm|yarn|pnpm|bun)\b[^\n]*--registry[= ]/.source,
47
- // custom npm registry
48
- /npm\s+config\s+set\s+registry/.source,
49
- /pip3?\s+install\b[^\n]*(?:--index-url|--extra-index-url|-i\s+https?:)/.source,
50
- // custom pip index
51
- /(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z|fi)?sh\b/.source,
52
- // curl … | sh
53
- /(?:iwr|irm|invoke-webrequest|invoke-restmethod)\b[^\n|]*\|\s*iex\b/.source,
54
- // PowerShell pipe-to-exec
55
- /bash\s+<\(\s*(?:curl|wget)/.source,
56
- // bash <(curl …)
57
- /(?:pip3?|npm|npx)\s+(?:install|--yes|-y)?\s*[^\n]*https?:\/\/(?!registry\.npmjs\.org|pypi\.org|files\.pythonhosted\.org)[^\s'"`]+\.(?:whl|tar\.gz|tgz|zip)\b/.source
58
- // install from raw archive URL
59
- ].join("|"),
60
- "i"
61
- );
62
- var RULES = [
63
- ["untrusted_install", UNTRUSTED_INSTALL_RE],
64
- ["executes_scripts", /```(?:sh|bash|python|py|js|ts|ruby|rb)\b|\b(subprocess|os\.system|child_process|exec\()/i],
65
- ["network_calls", /\b(fetch|requests\.|urllib|axios|curl|wget|https?:\/\/|websocket)\b/i],
66
- ["reads_secrets", /\b(api[_-]?key|secret|token|password|os\.environ|process\.env|getenv)\b/i]
67
- ];
68
- function scanSecurity(body) {
69
- const findings = [];
70
- body.split(/\r?\n/).forEach((text3, i) => {
71
- for (const [flag, re] of RULES) {
72
- if (re.test(text3)) findings.push({ flag, line: i + 1, snippet: text3.trim().slice(0, 120) });
73
- }
74
- });
75
- const flags = [...new Set(findings.map((f) => f.flag))];
76
- return { flags: flags.length ? flags : ["docs_only"], findings };
77
- }
78
-
79
- // ../../packages/core/src/rules.ts
80
- function utf8Bytes(s) {
81
- let bytes = 0;
82
- for (let i = 0; i < s.length; i++) {
83
- const c = s.charCodeAt(i);
84
- if (c < 128) bytes += 1;
85
- else if (c < 2048) bytes += 2;
86
- else if (c >= 55296 && c <= 56319) {
87
- bytes += 4;
88
- i++;
89
- } else bytes += 3;
90
- }
91
- return bytes;
92
- }
93
- var diag = (rule, message, line) => ({
94
- id: rule.id,
95
- severity: rule.severity,
96
- message,
97
- ...line !== void 0 ? { line } : {}
98
- });
99
- var RULES2 = [
100
- {
101
- id: "SK002",
102
- severity: "error",
103
- description: "name must be present and \u2264 120 chars",
104
- check(s, _ctx) {
105
- const out = [];
106
- if (!s.name.trim()) out.push(diag(this, "frontmatter 'name' is required"));
107
- else if (s.name.length > MAX_NAME) out.push(diag(this, `name must be \u2264 ${MAX_NAME} chars`));
108
- return out;
109
- }
110
- },
111
- {
112
- id: "SK003",
113
- severity: "error",
114
- description: "description must be present and \u2264 1024 chars",
115
- check(s, _ctx) {
116
- const out = [];
117
- if (!s.description.trim()) out.push(diag(this, "frontmatter 'description' is required"));
118
- else if (s.description.length > MAX_DESCRIPTION) out.push(diag(this, `description must be \u2264 ${MAX_DESCRIPTION} chars`));
119
- return out;
120
- }
121
- },
122
- {
123
- id: "SK010",
124
- severity: "warn",
125
- description: "description should be \u2265 20 chars (not too terse)",
126
- check(s, _ctx) {
127
- return s.description.trim().length > 0 && s.description.trim().length < 20 ? [diag(this, "description is very terse (< 20 chars); add more detail to help discovery")] : [];
128
- }
129
- },
130
- {
131
- id: "SK011",
132
- severity: "warn",
133
- description: "license should be declared in frontmatter",
134
- check(s, _ctx) {
135
- return s.license ? [] : [diag(this, "no 'license' declared in frontmatter")];
136
- }
137
- },
138
- {
139
- id: "SK020",
140
- severity: "warn",
141
- description: "body should be \u2265 200 chars (not a stub)",
142
- check(s, _ctx) {
143
- return s.body.trim().length < 200 ? [diag(this, "body is very short (< 200 chars); this looks like a stub")] : [];
144
- }
145
- },
146
- {
147
- id: "SK021",
148
- severity: "warn",
149
- description: "body should contain at least one Markdown heading",
150
- check(s, _ctx) {
151
- return /^#{1,6}\s/m.test(s.body) ? [] : [diag(this, "body has no Markdown headings; add structure with '#' headings")];
152
- }
153
- },
154
- {
155
- id: "SK030",
156
- severity: "error",
157
- description: "total SKILL.md size must be \u2264 256KB",
158
- check(s, _ctx) {
159
- return utf8Bytes(s.body) > MAX_SKILL_BYTES ? [diag(this, "SKILL.md exceeds 256KB")] : [];
160
- }
161
- },
162
- {
163
- id: "SK040",
164
- severity: "warn",
165
- description: "name should match its directory/slug",
166
- check(s, ctx) {
167
- if (!ctx.slug) return [];
168
- const expected = slugify(s.name);
169
- return expected === ctx.slug ? [] : [diag(this, `name slug '${expected}' does not match directory '${ctx.slug}'`)];
170
- }
171
- }
172
- ];
173
- function runRules(skill, ctx = {}) {
174
- return RULES2.flatMap((r) => r.check(skill, ctx));
175
- }
176
-
177
- // ../../packages/core/src/quality.ts
178
- var WEIGHT = {
179
- error: 30,
180
- warn: 10,
181
- info: 3
182
- };
183
- var RISK = {
184
- docs_only: 0,
185
- reads_secrets: 2,
186
- network_calls: 1,
187
- executes_scripts: 5,
188
- // fetch-and-execute from a non-standard source — the supply-chain pattern the
189
- // AI verdict floors to "warning"; the heaviest penalty here for the same reason
190
- untrusted_install: 15
191
- };
192
- var VERIFIED_FLOOR = 85;
193
- function qualityScore(diagnostics, security, opts = {}) {
194
- let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
195
- if (security) {
196
- for (const flag of security.flags) penalty += RISK[flag] ?? 0;
197
- }
198
- const score = Math.max(0, Math.min(100, 100 - penalty));
199
- const hasError = diagnostics.some((d) => d.severity === "error");
200
- return opts.verified && !hasError ? Math.max(score, VERIFIED_FLOOR) : score;
201
- }
202
-
203
- // ../../packages/core/src/lint.ts
204
- function lint(raw, opts = {}) {
205
- const parsed = parseSkillMd(raw, opts.type ?? "single");
206
- if ("error" in parsed) {
207
- const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
208
- const security2 = scanSecurity(raw);
209
- return {
210
- ok: false,
211
- diagnostics: diagnostics2,
212
- security: security2,
213
- score: qualityScore(diagnostics2, security2, { verified: opts.verified })
214
- };
215
- }
216
- const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
217
- const diagnostics = runRules(parsed, ctx);
218
- const security = scanSecurity(parsed.body);
219
- const ok = !diagnostics.some((d) => d.severity === "error");
220
- return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security, { verified: opts.verified }) };
221
- }
222
-
223
- // ../../packages/core/src/format/text.ts
224
- var BADGE = { error: "ERROR", warn: " WARN", info: " INFO" };
225
- function toText(results, opts = {}) {
226
- const paint = opts.decorate ?? ((t) => t);
227
- const out = [];
228
- for (const { file, result } of results) {
229
- const status = result.ok ? paint("PASS", "ok") : paint("FAIL", "error");
230
- out.push(`${status} ${file} ${paint(`score ${result.score}/100`, "dim")}`);
231
- for (const d of result.diagnostics) {
232
- const where = d.line !== void 0 ? `:${d.line}` : "";
233
- out.push(` ${paint(BADGE[d.severity], d.severity)} ${d.id}${where} ${d.message}`);
234
- }
235
- if (result.security.flags.length && result.security.flags[0] !== "docs_only") {
236
- out.push(` ${paint("SEC ", "warn")} flags: ${result.security.flags.join(", ")}`);
237
- }
238
- out.push("");
239
- }
240
- const failed = results.filter((r) => !r.result.ok).length;
241
- const summary = failed === 0 ? paint(`\u2713 ${results.length} skill(s) passed`, "ok") : paint(`\u2717 ${failed}/${results.length} skill(s) failed`, "error");
242
- out.push(summary);
243
- return out.join("\n");
244
- }
245
-
246
- // ../../packages/core/src/format/json.ts
247
- function toJson(results) {
248
- return results.map(({ file, result }) => ({
249
- file,
250
- ok: result.ok,
251
- score: result.score,
252
- diagnostics: result.diagnostics,
253
- security: result.security
254
- }));
255
- }
256
-
257
- // ../../packages/core/src/format/sarif.ts
258
- var SARIF_LEVEL = { error: "error", warn: "warning", info: "note" };
259
- function toSarif(results) {
260
- const sarifResults = results.flatMap(
261
- ({ file, result }) => result.diagnostics.map((d) => ({
262
- ruleId: d.id,
263
- level: SARIF_LEVEL[d.severity] ?? "note",
264
- message: { text: d.message },
265
- locations: [
266
- {
267
- physicalLocation: {
268
- artifactLocation: { uri: file },
269
- region: { startLine: d.line ?? 1 }
270
- }
271
- }
272
- ]
273
- }))
274
- );
275
- return {
276
- $schema: "https://json.schemastore.org/sarif-2.1.0.json",
277
- version: "2.1.0",
278
- runs: [
279
- {
280
- tool: {
281
- driver: {
282
- name: "skillmd",
283
- informationUri: "https://skillmd.com",
284
- rules: RULES2.map((r) => ({
285
- id: r.id,
286
- shortDescription: { text: r.description },
287
- defaultConfiguration: { level: SARIF_LEVEL[r.severity] ?? "note" }
288
- }))
289
- }
290
- },
291
- results: sarifResults
292
- }
293
- ]
294
- };
295
- }
296
-
297
- // ../../packages/core/src/format/github.ts
298
- var LEVEL = { error: "error", warn: "warning", info: "notice" };
299
- function toGithub(results) {
300
- const lines = [];
301
- for (const { file, result } of results) {
302
- for (const d of result.diagnostics) {
303
- const level = LEVEL[d.severity] ?? "notice";
304
- const loc = d.line !== void 0 ? `,line=${d.line}` : "";
305
- lines.push(`::${level} file=${file}${loc},title=${d.id}::${d.message}`);
306
- }
307
- }
308
- return lines.join("\n");
309
- }
310
-
311
- // ../cli/src/source.ts
312
- import { existsSync, statSync, lstatSync, readFileSync, readdirSync, mkdtempSync, rmSync } from "fs";
313
- import { tmpdir } from "os";
314
- import { join, basename, dirname, relative, sep } from "path";
315
- var IGNORE_DIRS = /* @__PURE__ */ new Set([
316
- // build/dependency dirs
317
- "node_modules",
318
- ".git",
319
- "dist",
320
- "build",
321
- "out",
322
- ".next",
323
- ".wrangler",
324
- ".cache",
325
- ".npm",
326
- ".pnpm-store",
327
- ".venv",
328
- "venv",
329
- "__pycache__",
330
- "target",
331
- "vendor",
332
- // large OS / user dirs (mostly relevant when run from a home directory)
333
- "AppData",
334
- "Library",
335
- "$Recycle.Bin",
336
- "System Volume Information",
337
- "Windows",
338
- "Program Files"
339
- ]);
340
- var MAX_DEPTH = 12;
341
- function findSkillFiles(dir, depth = 0) {
342
- const out = [];
343
- if (depth > MAX_DEPTH) return out;
344
- let entries;
345
- try {
346
- entries = readdirSync(dir, { withFileTypes: true });
347
- } catch {
348
- return out;
349
- }
350
- for (const entry of entries) {
351
- if (entry.isDirectory()) {
352
- if (IGNORE_DIRS.has(entry.name)) continue;
353
- out.push(...findSkillFiles(join(dir, entry.name), depth + 1));
354
- } else if (entry.name === "SKILL.md") {
355
- out.push(join(dir, entry.name));
356
- }
357
- }
358
- return out;
359
- }
360
- function loadLocal(file) {
361
- return { path: file, raw: readFileSync(file, "utf8"), slug: basename(dirname(file)), file };
362
- }
363
- function isLikelyLocal(arg) {
364
- return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync(arg);
365
- }
366
- async function resolveSource(arg) {
367
- if (isLikelyLocal(arg)) {
368
- if (!existsSync(arg)) throw new Error(`Path not found: ${arg}`);
369
- const st = statSync(arg);
370
- if (st.isFile()) return [loadLocal(arg)];
371
- const files2 = findSkillFiles(arg);
372
- if (files2.length === 0) {
373
- 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.`);
374
- }
375
- return files2.map(loadLocal);
376
- }
377
- const { downloadTemplate } = await import("giget");
378
- const input = normalizeGiget(arg);
379
- const dir = mkdtempSync(join(tmpdir(), "skillmd-src-"));
380
- const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
381
- const files = findSkillFiles(out);
382
- if (files.length === 0) throw new Error(`No SKILL.md found in ${arg}`);
383
- return files.map(loadLocal);
384
- }
385
- var MAX_PACK_FILES = 200;
386
- var MAX_PACK_BYTES = 20 * 1024 * 1024;
387
- function collectFiles(root, dir, limits = {}) {
388
- const maxFiles = limits.maxFiles ?? MAX_PACK_FILES;
389
- const maxBytes = limits.maxBytes ?? MAX_PACK_BYTES;
390
- const out = [];
391
- let bytes = 0;
392
- const walk = (d, depth) => {
393
- if (depth > MAX_DEPTH) return;
394
- for (const entry of readdirSync(d, { withFileTypes: true })) {
395
- const fp = join(d, entry.name);
396
- const st = lstatSync(fp);
397
- if (st.isSymbolicLink()) continue;
398
- if (st.isDirectory()) {
399
- if (entry.name === ".git" || entry.name === "node_modules") continue;
400
- walk(fp, depth + 1);
401
- } else if (st.isFile()) {
402
- if (out.length >= maxFiles) throw new Error(`pack too large: > ${maxFiles} files`);
403
- bytes += st.size;
404
- if (bytes > maxBytes) throw new Error(`pack too large: > ${Math.round(maxBytes / (1024 * 1024))}MB`);
405
- out.push({ path: relative(root, fp).split(sep).join("/"), contents: readFileSync(fp) });
406
- }
407
- }
408
- };
409
- walk(dir, 0);
410
- return out;
411
- }
412
- async function resolveTree(sourceUrl, ref) {
413
- const { downloadTemplate } = await import("giget");
414
- const input = normalizeGiget(sourceUrl) + (ref ? `#${ref}` : "");
415
- const dir = mkdtempSync(join(tmpdir(), "skillmd-pack-"));
416
- try {
417
- const { dir: out } = await downloadTemplate(input, { dir, forceClean: true });
418
- return collectFiles(out, out);
419
- } finally {
420
- rmSync(dir, { recursive: true, force: true });
421
- }
422
- }
423
- function normalizeGiget(arg) {
424
- const m = arg.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/[^/]+\/(.+))?/);
425
- if (m) return `github:${m[1]}/${m[2]}${m[3] ? "/" + m[3] : ""}`;
426
- if (arg.startsWith("github:") || arg.startsWith("gh:")) return arg;
427
- return `github:${arg}`;
428
- }
429
-
430
- // ../cli/src/ui.ts
50
+ // src/ui.ts
51
+ import { homedir } from "os";
52
+ import { relative, resolve, sep } from "path";
431
53
  import pc from "picocolors";
432
54
  var decorate = (text3, kind) => {
433
55
  switch (kind) {
@@ -451,7 +73,11 @@ var SEV_COLOR = {
451
73
  var GLYPH = {
452
74
  error: "\u2716",
453
75
  warn: "\u26A0",
454
- info: "\u2139"
76
+ info: "\u2139",
77
+ ok: "\u2713",
78
+ skip: "\u21B7",
79
+ blocked: "\u2717",
80
+ replaced: "\u26A0"
455
81
  };
456
82
  var SEC_GLYPH = "\u2691";
457
83
  var SEV_ORDER = ["error", "warn", "info"];
@@ -485,7 +111,7 @@ function statusDot(result) {
485
111
  return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
486
112
  }
487
113
  function failureBlock(file, result) {
488
- const lines = [`${pc.red("\u2716")} ${pc.bold(label(file))}`];
114
+ const lines = [`${pc.red(GLYPH.error)} ${pc.bold(label(file))}`];
489
115
  for (const d of result.diagnostics.filter((x) => x.severity === "error")) {
490
116
  const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
491
117
  lines.push(` ${SEV_COLOR.error(d.id)}${loc} ${d.message}`);
@@ -539,7 +165,7 @@ function renderMany(results) {
539
165
  }
540
166
  function renderFailuresView(results) {
541
167
  const fails = results.filter((r) => !r.result.ok);
542
- if (!fails.length) return pc.green(`\u2713 No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
168
+ if (!fails.length) return pc.green(`${GLYPH.ok} No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
543
169
  const out = fails.map((f) => failureBlock(f.file, f.result));
544
170
  out.push("");
545
171
  out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
@@ -560,8 +186,90 @@ function exitCodeFor(results, policy = {}) {
560
186
  }
561
187
  return 0;
562
188
  }
189
+ function shortPath(p7, ctx = {}) {
190
+ const home = resolve(ctx.home ?? homedir());
191
+ const cwd = resolve(ctx.cwd ?? process.cwd());
192
+ const abs = resolve(p7);
193
+ const norm = (s) => s.split(sep).join("/");
194
+ if (abs === cwd) return ".";
195
+ if (abs.startsWith(cwd + sep)) return `./${norm(relative(cwd, abs))}`;
196
+ if (abs === home) return "~";
197
+ if (abs.startsWith(home + sep)) return `~/${norm(relative(home, abs))}`;
198
+ return norm(abs);
199
+ }
200
+ var SGR = /\x1b\[[0-9;]*m/g;
201
+ var SGR_AT = /\x1b\[[0-9;]*m/y;
202
+ var RESET = "\x1B[0m";
203
+ function stripAnsi(s) {
204
+ return s.replace(SGR, "");
205
+ }
206
+ var visible = (s) => stripAnsi(s).length;
207
+ function truncateVisible(s, max) {
208
+ if (max <= 0) return "";
209
+ if (visible(s) <= max) return s;
210
+ let out = "";
211
+ let shown = 0;
212
+ let open = false;
213
+ for (let i = 0; i < s.length && shown < max - 1; ) {
214
+ if (s[i] === "\x1B") {
215
+ SGR_AT.lastIndex = i;
216
+ const m = SGR_AT.exec(s);
217
+ if (m) {
218
+ out += m[0];
219
+ open = m[0] !== RESET && m[0] !== "\x1B[m";
220
+ i += m[0].length;
221
+ continue;
222
+ }
223
+ }
224
+ out += s[i];
225
+ shown++;
226
+ i++;
227
+ }
228
+ return out + "\u2026" + (open ? RESET : "");
229
+ }
230
+ var termWidth = (w) => w ?? Math.min(process.stdout.columns ?? 100, 120);
231
+ function table(rows, opts = {}) {
232
+ if (rows.length === 0) return "";
233
+ const width = termWidth(opts.width);
234
+ const indent = opts.indent ?? "";
235
+ const cols = Math.max(...rows.map((r) => r.length));
236
+ const widths = Array.from({ length: cols }, (_, i) => Math.max(...rows.map((r) => visible(r[i] ?? ""))));
237
+ return rows.map((r) => {
238
+ let line = indent;
239
+ for (let i = 0; i < cols; i++) {
240
+ const cell = r[i] ?? "";
241
+ if (i < cols - 1) {
242
+ line += cell + " ".repeat(widths[i] - visible(cell) + 2);
243
+ continue;
244
+ }
245
+ line += truncateVisible(cell, Math.max(4, width - visible(line)));
246
+ }
247
+ line = line.trimEnd();
248
+ return visible(line) > width ? truncateVisible(line, width) : line;
249
+ }).join("\n");
250
+ }
251
+ function renderInstallSummary(i, ctx = {}) {
252
+ const width = termWidth(ctx.width);
253
+ const src = i.source.startsWith("local:") ? `local:${shortPath(i.source.slice(6), ctx)}` : i.source;
254
+ const head = truncateVisible(
255
+ pc.green(`${GLYPH.ok} ${i.name}`) + pc.dim(` score ${i.score}${i.flags.length ? ` ${i.flags.join(", ")}` : ""} from ${src}`),
256
+ width
257
+ );
258
+ const byPath = /* @__PURE__ */ new Map();
259
+ for (const t of i.targets) {
260
+ const e = byPath.get(t.path);
261
+ if (e) e.agents.push(t.agent);
262
+ else byPath.set(t.path, { agents: [t.agent], mode: t.mode });
263
+ }
264
+ const rows = [...byPath.entries()].map(([path, e]) => [pc.cyan(e.agents.join(", ")), pc.dim(e.mode), shortPath(path, ctx)]);
265
+ const body = rows.length ? table(rows, { width, indent: " " }) : truncateVisible(pc.dim(` ${shortPath(i.canonical, ctx)} canonical only`), width);
266
+ const skipped = i.skipped.map((s) => truncateVisible(pc.dim(` ${GLYPH.skip} ${s.agent}: ${s.reason}`), width));
267
+ const replaced = i.replaced ? [truncateVisible(pc.yellow(` ${GLYPH.replaced} replaced previous install (${i.replaced === "untracked" ? "untracked directory" : i.replaced})`), width)] : [];
268
+ return [head, body, ...skipped, ...replaced].join("\n");
269
+ }
270
+ var banner = () => pc.bgCyan(pc.black(" skillmd "));
563
271
 
564
- // ../cli/src/commands/lint.ts
272
+ // src/commands/lint.ts
565
273
  function applyLicenseFix(raw) {
566
274
  if (/^---[\s\S]*?\blicense:\s*\S+[\s\S]*?^---/m.test(raw)) return null;
567
275
  const m = raw.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
@@ -601,9 +309,9 @@ async function runLint(target, flags) {
601
309
  return { results, output, exitCode };
602
310
  }
603
311
  function lintCommand() {
604
- 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) => {
312
+ 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) => {
605
313
  const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
606
- if (resolve(target) === homedir()) {
314
+ if (resolve2(target) === homedir2()) {
607
315
  process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
608
316
  `));
609
317
  }
@@ -612,12 +320,12 @@ function lintCommand() {
612
320
  const s = p.spinner();
613
321
  s.start(`Scanning ${target}`);
614
322
  try {
615
- const run2 = await runLint(target, opts);
616
- s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
617
- console.log(run2.output);
618
- const failed = run2.results.filter((r) => !r.result.ok).length;
323
+ const run3 = await runLint(target, opts);
324
+ s.stop(`Scanned ${run3.results.length} skill${run3.results.length === 1 ? "" : "s"}`);
325
+ console.log(run3.output);
326
+ const failed = run3.results.filter((r) => !r.result.ok).length;
619
327
  p.outro(failed === 0 ? pc2.green("All good") : pc2.red(`${failed} skill${failed === 1 ? "" : "s"} failed \u2014 details above`));
620
- process.exitCode = run2.exitCode;
328
+ process.exitCode = run3.exitCode;
621
329
  } catch (err) {
622
330
  s.stop(pc2.red("No skills found"));
623
331
  p.outro(err instanceof Error ? err.message : String(err));
@@ -627,14 +335,15 @@ function lintCommand() {
627
335
  }
628
336
  process.stderr.write(`Looking for SKILL.md under ${target} \u2026
629
337
  `);
630
- const run = await runLint(target, opts);
631
- console.log(run.output);
632
- process.exitCode = run.exitCode;
338
+ const run2 = await runLint(target, opts);
339
+ console.log(run2.output);
340
+ process.exitCode = run2.exitCode;
633
341
  });
634
342
  }
635
343
 
636
- // ../cli/src/commands/scan.ts
344
+ // src/commands/scan.ts
637
345
  import { Command as Command2 } from "commander";
346
+ import { scanSecurity, parseSkillMd } from "@skillmds/core";
638
347
  import pc3 from "picocolors";
639
348
  async function runScan(target, flags) {
640
349
  const resolved = await resolveSource(target);
@@ -667,18 +376,19 @@ async function runScan(target, flags) {
667
376
  }
668
377
  function scanCommand() {
669
378
  return new Command2("scan").description("Security-scan SKILL.md file(s) for scripts, network calls and secret access").argument("[target]", "local path or GitHub source", ".").option("--deny <flags...>", "exit non-zero if any of these flags are present (e.g. executes_scripts)").option("--format <fmt>", "text | json", "text").action(async (target, opts) => {
670
- const run = await runScan(target, opts);
671
- console.log(run.output);
672
- process.exitCode = run.exitCode;
379
+ const run2 = await runScan(target, opts);
380
+ console.log(run2.output);
381
+ process.exitCode = run2.exitCode;
673
382
  });
674
383
  }
675
384
 
676
- // ../cli/src/commands/rules.ts
385
+ // src/commands/rules.ts
677
386
  import { Command as Command3 } from "commander";
387
+ import { RULES } from "@skillmds/core";
678
388
  import pc4 from "picocolors";
679
389
  function rulesCommand() {
680
390
  return new Command3("rules").description("List lint rules, or show detail for one rule id").argument("[id]", "a rule id, e.g. SK010").option("--json", "machine-readable output").action((id, opts) => {
681
- const list = id ? RULES2.filter((r) => r.id.toLowerCase() === id.toLowerCase()) : RULES2;
391
+ const list = id ? RULES.filter((r) => r.id.toLowerCase() === id.toLowerCase()) : RULES;
682
392
  if (id && list.length === 0) {
683
393
  console.error(`Unknown rule: ${id}`);
684
394
  process.exitCode = 1;
@@ -695,11 +405,45 @@ function rulesCommand() {
695
405
  });
696
406
  }
697
407
 
698
- // ../cli/src/commands/init.ts
408
+ // src/commands/init.ts
699
409
  import { Command as Command4 } from "commander";
700
- import { mkdirSync, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
701
- import { join as join2 } from "path";
410
+ import { mkdirSync, writeFileSync as writeFileSync2, existsSync } from "fs";
411
+ import { join } from "path";
412
+ import { slugify } from "@skillmds/core";
702
413
  import * as p2 from "@clack/prompts";
414
+ import pc5 from "picocolors";
415
+
416
+ // src/env.ts
417
+ function detectHostAgent(env = process.env) {
418
+ if (env.CLAUDECODE) return "claude-code";
419
+ if (env.CURSOR_AGENT || env.CURSOR_EXTENSION_HOST_ROLE === "agent-exec") return "cursor";
420
+ if (env.CODEX_SANDBOX || env.CODEX_THREAD_ID) return "codex";
421
+ if (env.GEMINI_CLI) return "gemini-cli";
422
+ if (env.CI && env.CI !== "false" && env.CI !== "0") return "ci";
423
+ return null;
424
+ }
425
+ var realTerminal = () => ({
426
+ stdinTTY: Boolean(process.stdin.isTTY),
427
+ stdoutTTY: Boolean(process.stdout.isTTY),
428
+ env: process.env
429
+ });
430
+ function isInteractive(flags, term = realTerminal()) {
431
+ if (flags.yes || flags.json) return false;
432
+ if (!term.stdinTTY || !term.stdoutTTY) return false;
433
+ return detectHostAgent(term.env) === null;
434
+ }
435
+ function scopesFor(flags) {
436
+ return flags.global && !flags.project ? [true] : flags.project && !flags.global ? [false] : [false, true];
437
+ }
438
+ function nonInteractiveHint(flags) {
439
+ return `Interactive prompt required but stdin is not a terminal. Nothing was changed. Re-run with ${flags} to run non-interactively.`;
440
+ }
441
+ function promptIf(flags, prompt) {
442
+ const term = { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env: flags.env ?? process.env };
443
+ return isInteractive(flags, term) ? { prompt } : {};
444
+ }
445
+
446
+ // src/commands/init.ts
703
447
  function skillTemplate(name, description, license) {
704
448
  const desc = description.trim() || `A skill that helps agents with ${name}.`;
705
449
  return `---
@@ -728,159 +472,54 @@ Add any constraints, edge cases, or references the agent should keep in mind.
728
472
  }
729
473
  function initSkill(name, opts = {}) {
730
474
  const slug = slugify(name);
731
- const root = join2(opts.dir ?? process.cwd(), slug);
732
- if (existsSync2(join2(root, "SKILL.md"))) throw new Error(`SKILL.md already exists at ${root}`);
475
+ const root = join(opts.dir ?? process.cwd(), slug);
476
+ if (existsSync(join(root, "SKILL.md"))) throw new Error(`SKILL.md already exists at ${root}`);
733
477
  mkdirSync(root, { recursive: true });
734
- const file = join2(root, "SKILL.md");
478
+ const file = join(root, "SKILL.md");
735
479
  writeFileSync2(file, skillTemplate(name, opts.description ?? "", opts.license ?? "MIT"), "utf8");
736
480
  return file;
737
481
  }
738
- function initCommand() {
739
- 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) => {
740
- let name = nameArg ?? opts.name;
741
- if (!name && !opts.yes) {
742
- const answer = await p2.text({ message: "Skill name?", placeholder: "my-skill" });
743
- if (p2.isCancel(answer)) {
744
- p2.cancel("Cancelled.");
745
- return;
746
- }
747
- name = answer;
748
- }
749
- if (!name) {
750
- console.error("A skill name is required (pass a name or --name).");
751
- process.exitCode = 1;
752
- return;
482
+ var defaultInitDeps = (flags) => promptIf(flags, async () => {
483
+ const answer = await p2.text({ message: "Skill name?", placeholder: "my-skill" });
484
+ return p2.isCancel(answer) ? null : answer;
485
+ });
486
+ function finish(flags, result, doc) {
487
+ return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
488
+ }
489
+ async function runInit(nameArg, flags, deps = defaultInitDeps(flags)) {
490
+ let name = nameArg ?? flags.name;
491
+ if (!name) {
492
+ if (!deps.prompt) {
493
+ const error = nonInteractiveHint("a name argument or --name <name>");
494
+ return finish(flags, { exitCode: 1, output: pc5.red(error) }, { ok: false, error });
753
495
  }
754
- const file = initSkill(name, { description: opts.description, license: opts.license });
755
- console.log(`Created ${file}`);
756
- });
757
- }
758
-
759
- // ../cli/src/commands/search.ts
760
- import { Command as Command5 } from "commander";
761
- import pc5 from "picocolors";
762
-
763
- // ../cli/src/api.ts
764
- import { createHash } from "crypto";
765
- import { stringify as stringifyYaml } from "yaml";
766
-
767
- // ../cli/src/config.ts
768
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
769
- import { homedir as homedir2 } from "os";
770
- import { join as join3 } from "path";
771
- var DEFAULT_API = "https://api.skillmd.com";
772
- function configPath() {
773
- return join3(homedir2(), ".skillmd", "config.json");
774
- }
775
- function readConfig() {
776
- try {
777
- return JSON.parse(readFileSync2(configPath(), "utf8"));
778
- } catch {
779
- return {};
780
- }
781
- }
782
- function writeConfig(cfg) {
783
- const dir = join3(homedir2(), ".skillmd");
784
- mkdirSync2(dir, { recursive: true });
785
- writeFileSync3(configPath(), JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
786
- }
787
- function resolveToken(flags = {}) {
788
- return flags.token || process.env.SKILLMD_TOKEN || readConfig().token || void 0;
789
- }
790
- function resolveApi(flags = {}) {
791
- return flags.api || process.env.SKILLMD_API || readConfig().api || DEFAULT_API;
792
- }
793
-
794
- // ../cli/src/api.ts
795
- var IntegrityError = class extends Error {
796
- integrity = true;
797
- constructor(message) {
798
- super(message);
799
- this.name = "IntegrityError";
496
+ const answer = await deps.prompt();
497
+ if (answer === null) return finish(flags, { exitCode: 0, output: pc5.dim("Cancelled.") }, { ok: false, cancelled: true });
498
+ name = answer;
800
499
  }
801
- };
802
- var RegistryError = class extends Error {
803
- constructor(message, status) {
804
- super(message);
805
- this.status = status;
806
- this.name = "RegistryError";
500
+ if (!name) {
501
+ const error = "A skill name is required (pass a name or --name).";
502
+ return finish(flags, { exitCode: 1, output: pc5.red(error) }, { ok: false, error });
807
503
  }
808
- status;
809
- };
810
- var ALLOWED_SOURCE_HOSTS = /* @__PURE__ */ new Set(["raw.githubusercontent.com", "gist.githubusercontent.com"]);
811
- function isAllowedSourceUrl(u) {
812
504
  try {
813
- const parsed = new URL(u);
814
- return parsed.protocol === "https:" && ALLOWED_SOURCE_HOSTS.has(parsed.hostname.toLowerCase());
815
- } catch {
816
- return false;
817
- }
818
- }
819
- function createClient(flags = {}) {
820
- const base = resolveApi(flags);
821
- const token = resolveToken(flags);
822
- async function api(path, init = {}) {
823
- const headers = { accept: "application/json", ...init.headers };
824
- if (token) headers.authorization = `Bearer ${token}`;
825
- let res;
826
- try {
827
- res = await fetch(base + path, { ...init, headers });
828
- } catch (e) {
829
- const cause = e instanceof Error ? e.cause?.message ?? e.message : String(e);
830
- throw new RegistryError(`could not reach ${base}: ${cause}`);
831
- }
832
- if (!res.ok) {
833
- const detail = await res.text().catch(() => "");
834
- throw new RegistryError(`SkillMD API ${res.status} on ${path}: ${detail}`, res.status);
835
- }
836
- return await res.json();
837
- }
838
- return { api, base, token, hasToken: Boolean(token) };
839
- }
840
- async function fetchBundle(base, slug, token) {
841
- const headers = { accept: "application/json" };
842
- if (token) headers.authorization = `Bearer ${token}`;
843
- const res = await fetch(`${base}/api/skills/${slug}/bundle?format=json`, { headers }).catch(() => null);
844
- if (!res || !res.ok) return null;
845
- const data = await res.json().catch(() => null);
846
- if (!data?.files) return null;
847
- const out = [];
848
- for (const f of data.files) {
849
- if (f.content_base64 != null) {
850
- const contents = Buffer.from(f.content_base64, "base64");
851
- if (f.sha256) {
852
- const got = createHash("sha256").update(contents).digest("hex");
853
- if (got !== f.sha256) {
854
- throw new IntegrityError(`integrity check failed for "${f.path}" (expected ${f.sha256.slice(0, 12)}\u2026, got ${got.slice(0, 12)}\u2026)`);
855
- }
856
- }
857
- out.push({ path: f.path, contents });
858
- } else if (f.source_url && isAllowedSourceUrl(f.source_url)) {
859
- const r = await fetch(f.source_url).catch(() => null);
860
- if (r && r.ok) out.push({ path: f.path, contents: Buffer.from(await r.arrayBuffer()) });
861
- }
862
- }
863
- return out;
864
- }
865
- function skillMdFor(skill) {
866
- if (skill.raw_md) {
867
- return hasFrontmatter(skill.raw_md) ? skill.raw_md : reconstructSkillMd({ ...skill, body_md: skill.raw_md });
505
+ const file = initSkill(name, { dir: flags.dir, description: flags.description, license: flags.license });
506
+ return { exitCode: 0, file, output: flags.json ? JSON.stringify({ ok: true, file }, null, 2) : `Created ${file}` };
507
+ } catch (e) {
508
+ const msg2 = e instanceof Error ? e.message : String(e);
509
+ return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg2 }, null, 2) : pc5.red(msg2) };
868
510
  }
869
- return reconstructSkillMd(skill);
870
511
  }
871
- function hasFrontmatter(md) {
872
- return /^\uFEFF?\s*---\r?\n/.test(md);
873
- }
874
- function reconstructSkillMd(skill) {
875
- const name = skill.slug.split("/")[1] || skill.title;
876
- const fm = { name, description: String(skill.description).replace(/\n/g, " ") };
877
- if (skill.license) fm.license = skill.license;
878
- return `---
879
- ${stringifyYaml(fm)}---
880
- ${skill.body_md || ""}`;
512
+ function initCommand() {
513
+ 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) => {
514
+ const r = await runInit(nameArg, { ...cmd.parent?.opts(), ...opts });
515
+ console.log(r.output);
516
+ process.exitCode = r.exitCode;
517
+ });
881
518
  }
882
519
 
883
- // ../cli/src/commands/search.ts
520
+ // src/commands/search.ts
521
+ import { Command as Command5 } from "commander";
522
+ import pc6 from "picocolors";
884
523
  var MAX_LINE_WIDTH = 100;
885
524
  function formatStars(n) {
886
525
  if (n >= 1e6) {
@@ -899,13 +538,13 @@ function truncate(text3, width) {
899
538
  return `${text3.slice(0, width - 1).trimEnd()}\u2026`;
900
539
  }
901
540
  function formatSearchResult(item, width = process.stdout.columns ?? 80) {
902
- const stars = item.repo_stars != null ? pc5.dim(`\u2605 ${formatStars(item.repo_stars)}`) : "";
903
- const check = item.verified ? pc5.green("\u2713") : "";
904
- const pack = item.type === "pack" ? pc5.dim("[pack]") : "";
905
- const head = [pc5.bold(item.slug), stars, check, pack].filter(Boolean).join(" ");
541
+ const stars = item.repo_stars != null ? pc6.dim(`\u2605 ${formatStars(item.repo_stars)}`) : "";
542
+ const check = item.verified ? pc6.green("\u2713") : "";
543
+ const pack = item.type === "pack" ? pc6.dim("[pack]") : "";
544
+ const head = [pc6.bold(safeText(item.slug, 200)), stars, check, pack].filter(Boolean).join(" ");
906
545
  const indent = " ";
907
546
  const lineWidth = Math.max(10, Math.min(width, MAX_LINE_WIDTH) - indent.length);
908
- const desc = `${indent}${pc5.dim(truncate(item.description ?? "", lineWidth))}`;
547
+ const desc = `${indent}${pc6.dim(truncate(safeText(item.description ?? ""), lineWidth))}`;
909
548
  return `${head}
910
549
  ${desc}`;
911
550
  }
@@ -925,201 +564,287 @@ async function runSearch(query, flags) {
925
564
  function searchCommand() {
926
565
  return new Command5("search").description("Search the SkillMD registry").argument("<query>", "search terms").option("--category <slug>", "filter by category").option("--verified", "only verified skills").option("--type <type>", "single | pack").option("--min-rating <n>", "minimum average rating (0-5)").option("--limit <n>", "max results", "20").option("--json", "machine-readable output").action(async (query, opts, cmd) => {
927
566
  const merged = { ...cmd.parent?.opts(), ...opts };
928
- const run = await runSearch(query, merged);
929
- console.log(run.output);
567
+ const run2 = await runSearch(query, merged);
568
+ console.log(run2.output);
930
569
  });
931
570
  }
932
571
 
933
- // ../cli/src/commands/info.ts
572
+ // src/commands/info.ts
934
573
  import { Command as Command6 } from "commander";
935
- import pc6 from "picocolors";
936
- async function runInfo(slug, flags) {
574
+ import pc7 from "picocolors";
575
+ async function runInfo(slug, flags, deps = {}) {
937
576
  const [owner, name] = slug.split("/");
938
577
  if (!owner || !name || slug.split("/").length !== 2) {
939
- return { ok: false, exitCode: 1, output: pc6.red("Expected owner/name, e.g. anthropic/pdf") };
578
+ return { ok: false, exitCode: 1, output: pc7.red("Expected owner/name, e.g. anthropic/pdf") };
940
579
  }
941
- const { api } = createClient(flags);
580
+ const api = deps.api ?? createClient(flags).api;
942
581
  let s;
943
582
  try {
944
583
  s = await api(`/api/skills/${owner}/${name}`);
945
584
  } catch (err) {
946
585
  const message = err instanceof Error ? err.message : String(err);
947
586
  if (/^SkillMD API 404 /.test(message)) {
948
- return { ok: false, exitCode: 1, output: pc6.red(`Skill not found on the registry: ${slug}`) };
587
+ return { ok: false, exitCode: 1, output: pc7.red(`Skill not found on the registry: ${slug}`) };
949
588
  }
950
- return { ok: false, exitCode: 1, output: pc6.red(`Could not reach the SkillMD registry: ${message}`) };
589
+ return { ok: false, exitCode: 1, output: pc7.red(`Could not reach the SkillMD registry: ${message}`) };
951
590
  }
952
591
  if (flags.json) return { ok: true, exitCode: 0, output: JSON.stringify(s, null, 2), skill: s };
592
+ const slugTxt = safeText(s.slug, 200);
953
593
  const lines = [];
954
594
  lines.push(
955
- `${pc6.bold(s.slug)} ${pc6.dim(`[${s.type ?? "single"}]`)} ${s.verified ? pc6.green("\u2713 verified") : pc6.yellow("unverified")}`
595
+ `${pc7.bold(slugTxt)} ${pc7.dim(`[${safeText(s.type ?? "single", 40)}]`)} ${s.verified ? pc7.green("\u2713 verified") : pc7.yellow("unverified")}`
956
596
  );
957
- lines.push(` ${s.description}`);
597
+ lines.push(` ${safeText(s.description)}`);
958
598
  lines.push(
959
- pc6.dim(
960
- ` category: ${s.category ?? "-"} \xB7 rating: ${s.avg_rating != null ? s.avg_rating.toFixed(1) : "-"} (${s.rating_count ?? 0}) \xB7 installs: ${s.install_count ?? 0}`
599
+ pc7.dim(
600
+ ` 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}`
961
601
  )
962
602
  );
603
+ const securityFlags2 = (s.security_flags ?? []).map((f) => safeText(f, 60)).filter(Boolean);
963
604
  lines.push(
964
- pc6.dim(` security: ${(s.security_flags ?? []).join(", ") || "unscanned"} \xB7 license: ${s.provenance?.license ?? s.license ?? "unknown"}`)
605
+ pc7.dim(` security: ${securityFlags2.join(", ") || "unscanned"} \xB7 license: ${safeText(s.provenance?.license ?? s.license ?? "unknown", 100) || "unknown"}`)
965
606
  );
966
- const sourceRepo = s.provenance?.source_repo ?? s.source_repo;
967
- const commitSha = s.provenance?.commit_sha ?? s.commit_sha;
968
- if (sourceRepo) lines.push(pc6.dim(` source: ${sourceRepo}${commitSha ? ` @ ${commitSha.slice(0, 8)}` : ""}`));
969
- lines.push(pc6.dim(` page: https://skillmd.com/skills/${s.slug}`));
970
- lines.push(pc6.dim(` install: ${s.install?.cli ?? `npx skillmds add ${s.slug}`}`));
607
+ const sourceRepo = safeText(s.provenance?.source_repo ?? s.source_repo, 300);
608
+ const commitSha = safeText(s.provenance?.commit_sha ?? s.commit_sha, 100);
609
+ if (sourceRepo) lines.push(pc7.dim(` source: ${sourceRepo}${commitSha ? ` @ ${commitSha.slice(0, 8)}` : ""}`));
610
+ lines.push(pc7.dim(` page: https://skillmd.com/skills/${slugTxt}`));
611
+ lines.push(pc7.dim(` install: ${safeText(s.install?.cli, 300) || `npx get-skill add ${slugTxt}`}`));
971
612
  return { ok: true, exitCode: 0, output: lines.join("\n"), skill: s };
972
613
  }
973
614
  function infoCommand() {
974
615
  return new Command6("info").description("Show registry details for a skill: verification, security flags, provenance, rating").argument("<slug>", "owner/name, e.g. anthropic/pdf").action(async (slug, _opts, cmd) => {
975
616
  const flags = { ...cmd.parent?.opts() };
976
- const run = await runInfo(slug, flags);
977
- if (run.output) (run.ok ? console.log : console.error)(run.output);
978
- process.exitCode = run.exitCode;
617
+ const run2 = await runInfo(slug, flags);
618
+ if (run2.output) (run2.ok ? console.log : console.error)(run2.output);
619
+ process.exitCode = run2.exitCode;
979
620
  });
980
621
  }
981
622
 
982
- // ../cli/src/commands/add.ts
623
+ // src/commands/add.ts
983
624
  import { Command as Command7, Option } from "commander";
984
- import { existsSync as existsSync4 } from "fs";
985
625
  import { homedir as homedir4 } from "os";
986
626
  import * as p3 from "@clack/prompts";
987
- import { join as join5, relative as relative2, resolve as resolve3 } from "path";
988
- import pc7 from "picocolors";
627
+ import { dirname as dirname2, resolve as resolve3 } from "path";
628
+ import pc8 from "picocolors";
629
+ import { lint as lint2, parseSkillMd as parseSkillMd2 } from "@skillmds/core";
989
630
 
990
- // ../cli/src/agents.ts
991
- 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";
631
+ // src/claudePlugin.ts
632
+ import { execFile } from "child_process";
633
+ import { existsSync as existsSync2 } from "fs";
992
634
  import { homedir as homedir3 } from "os";
993
- import { join as join4, resolve as resolve2, dirname as dirname2, sep as sep2 } from "path";
994
- var AGENTS = [
995
- { id: "claude-code", project: [".claude", "skills"], global: [".claude", "skills"], detect: [[".claude"]] },
996
- { id: "cursor", project: [".cursor", "skills"], global: [".cursor", "skills"], detect: [[".cursor"]] },
997
- { id: "codex", project: [".codex", "skills"], global: [".codex", "skills"], detect: [[".codex"]] },
998
- { id: "windsurf", project: [".windsurf", "skills"], global: [".codeium", "windsurf", "skills"], detect: [[".codeium", "windsurf"], [".windsurf"]] },
999
- { id: "opencode", project: [".opencode", "skills"], global: [".config", "opencode", "skills"], detect: [[".config", "opencode"], [".opencode"]] },
1000
- { id: "gemini-cli", project: [".agents", "skills"], global: [".gemini", "skills"], detect: [[".gemini"]] },
1001
- { id: "antigravity", project: [".agents", "skills"], global: [".gemini", "antigravity", "skills"], detect: [[".gemini", "antigravity"], [".antigravity"]] },
1002
- { id: "kiro", project: [".kiro", "skills"], global: [".kiro", "skills"], detect: [[".kiro"]] },
1003
- { id: "warp", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".warp"]] },
1004
- { id: "github-copilot", project: [".agents", "skills"], global: [".copilot", "skills"], detect: [[".copilot"]] },
1005
- { id: "cline", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".cline"]] },
1006
- { id: "roo", project: [".roo", "skills"], global: [".roo", "skills"], detect: [[".roo"]] },
1007
- { id: "goose", project: [".goose", "skills"], global: [".config", "goose", "skills"], detect: [[".config", "goose"]] },
1008
- { id: "trae", project: [".trae", "skills"], global: [".trae", "skills"], detect: [[".trae"]] },
1009
- { id: "qwen-code", project: [".qwen", "skills"], global: [".qwen", "skills"], detect: [[".qwen"]] },
1010
- { id: "amp", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".config", "amp"]] },
1011
- // — extended registry —
1012
- { id: "openclaw", project: ["skills"], global: [".openclaw", "skills"], detect: [[".openclaw"], [".clawdbot"], [".moltbot"]] },
1013
- { id: "kilo", project: [".kilocode", "skills"], global: [".kilocode", "skills"], detect: [[".kilocode"]] },
1014
- { id: "hermes-agent", project: [".hermes", "skills"], global: [".hermes", "skills"], detect: [[".hermes"]] },
1015
- { id: "continue", project: [".continue", "skills"], global: [".continue", "skills"], detect: [[".continue"]] },
1016
- { id: "devin", project: [".devin", "skills"], global: [".config", "devin", "skills"], detect: [[".config", "devin"]] },
1017
- { id: "openhands", project: [".openhands", "skills"], global: [".openhands", "skills"], detect: [[".openhands"]] },
1018
- { id: "replit", project: [".agents", "skills"], global: [".config", "agents", "skills"], detect: [[".replit"]] },
1019
- { id: "mistral-vibe", project: [".vibe", "skills"], global: [".vibe", "skills"], detect: [[".vibe"]] },
1020
- { id: "zed", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".config", "zed"]] },
1021
- { id: "adal", project: [".adal", "skills"], global: [".adal", "skills"], detect: [[".adal"]] },
1022
- { id: "aider-desk", project: [".aider-desk", "skills"], global: [".aider-desk", "skills"], detect: [[".aider-desk"]] },
1023
- { id: "antigravity-cli", project: [".agents", "skills"], global: [".gemini", "antigravity-cli", "skills"], detect: [[".gemini", "antigravity-cli"]] },
1024
- { id: "augment", project: [".augment", "skills"], global: [".augment", "skills"], detect: [[".augment"]] },
1025
- { id: "autohand-code", project: [".autohand", "skills"], global: [".autohand", "skills"], detect: [[".autohand"]] },
1026
- { id: "bob", project: [".bob", "skills"], global: [".bob", "skills"], detect: [[".bob"]] },
1027
- { id: "codearts-agent", project: [".codeartsdoer", "skills"], global: [".codeartsdoer", "skills"], detect: [[".codeartsdoer"]] },
1028
- { id: "codebuddy", project: [".codebuddy", "skills"], global: [".codebuddy", "skills"], detect: [[".codebuddy"]] },
1029
- { id: "codemaker", project: [".codemaker", "skills"], global: [".codemaker", "skills"], detect: [[".codemaker"]] },
1030
- { id: "codestudio", project: [".codestudio", "skills"], global: [".codestudio", "skills"], detect: [[".codestudio"]] },
1031
- { id: "command-code", project: [".commandcode", "skills"], global: [".commandcode", "skills"], detect: [[".commandcode"]] },
1032
- { id: "cortex", project: [".cortex", "skills"], global: [".snowflake", "cortex", "skills"], detect: [[".snowflake", "cortex"]] },
1033
- { id: "crush", project: [".crush", "skills"], global: [".config", "crush", "skills"], detect: [[".config", "crush"]] },
1034
- { id: "deepagents", project: [".agents", "skills"], global: [".deepagents", "agent", "skills"], detect: [[".deepagents"]] },
1035
- { id: "dexto", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".dexto"]] },
1036
- { id: "droid", project: [".factory", "skills"], global: [".factory", "skills"], detect: [[".factory"]] },
1037
- { id: "firebender", project: [".agents", "skills"], global: [".firebender", "skills"], detect: [[".firebender"]] },
1038
- { id: "forgecode", project: [".forge", "skills"], global: [".forge", "skills"], detect: [[".forge"]] },
1039
- { id: "iflow-cli", project: [".iflow", "skills"], global: [".iflow", "skills"], detect: [[".iflow"]] },
1040
- { id: "inference-sh", project: [".inferencesh", "skills"], global: [".inferencesh", "skills"], detect: [[".inferencesh"]] },
1041
- { id: "jazz", project: [".jazz", "skills"], global: [".jazz", "skills"], detect: [[".jazz"]] },
1042
- { id: "junie", project: [".junie", "skills"], global: [".junie", "skills"], detect: [[".junie"]] },
1043
- { id: "kimi-code-cli", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".kimi-code"], [".kimi"]] },
1044
- { id: "kode", project: [".kode", "skills"], global: [".kode", "skills"], detect: [[".kode"]] },
1045
- { id: "lingma", project: [".lingma", "skills"], global: [".lingma", "skills"], detect: [[".lingma"]] },
1046
- { id: "loaf", project: [".agents", "skills"], global: [".agents", "skills"], detect: [[".loaf"]] },
1047
- { id: "mcpjam", project: [".mcpjam", "skills"], global: [".mcpjam", "skills"], detect: [[".mcpjam"]] },
1048
- { id: "moxby", project: [".moxby", "skills"], global: [".moxby", "skills"], detect: [[".moxby"]] },
1049
- { id: "mux", project: [".mux", "skills"], global: [".mux", "skills"], detect: [[".mux"]] },
1050
- { id: "neovate", project: [".neovate", "skills"], global: [".neovate", "skills"], detect: [[".neovate"]] },
1051
- { id: "ona", project: [".ona", "skills"], global: [".ona", "skills"], detect: [[".ona"]] },
1052
- { id: "pi", project: [".pi", "skills"], global: [".pi", "agent", "skills"], detect: [[".pi", "agent"]] },
1053
- { id: "pochi", project: [".pochi", "skills"], global: [".pochi", "skills"], detect: [[".pochi"]] },
1054
- { id: "qoder", project: [".qoder", "skills"], global: [".qoder", "skills"], detect: [[".qoder"]] },
1055
- { id: "qoder-cn", project: [".qoder", "skills"], global: [".qoder-cn", "skills"], detect: [[".qoder-cn"]] },
1056
- { id: "reasonix", project: [".reasonix", "skills"], global: [".reasonix", "skills"], detect: [[".reasonix"]] },
1057
- { id: "rovodev", project: [".rovodev", "skills"], global: [".rovodev", "skills"], detect: [[".rovodev"]] },
1058
- { id: "tabnine-cli", project: [".tabnine", "agent", "skills"], global: [".tabnine", "agent", "skills"], detect: [[".tabnine"]] },
1059
- { id: "terramind", project: [".terramind", "skills"], global: [".terramind", "skills"], detect: [[".terramind"]] },
1060
- { id: "tinycloud", project: [".tinycloud", "skills"], global: [".tinycloud", "skills"], detect: [[".tinycloud"]] },
1061
- { id: "trae-cn", project: [".trae", "skills"], global: [".trae-cn", "skills"], detect: [[".trae-cn"]] },
1062
- { id: "zcode", project: [".zcode", "skills"], global: [".zcode", "skills"], detect: [[".zcode"]] },
1063
- { id: "zencoder", project: [".zencoder", "skills"], global: [".zencoder", "skills"], detect: [[".zencoder"]] }
1064
- ];
1065
- function agentDir(agentId, opts = {}) {
1066
- const agent = AGENTS.find((a) => a.id === agentId);
1067
- if (!agent) throw new Error(`Unknown agent: ${agentId}. Known: ${AGENTS.map((a) => a.id).join(", ")}`);
1068
- return opts.global ? join4(opts.home ?? homedir3(), ...agent.global) : join4(opts.cwd ?? process.cwd(), ...agent.project);
1069
- }
1070
- function detectAgents(opts = {}) {
1071
- const home = opts.home ?? homedir3();
1072
- return AGENTS.filter((a) => a.detect.some((d) => existsSync3(join4(home, ...d)))).map((a) => a.id);
1073
- }
1074
- function writeSkill(skillName, files, targetDir, opts = {}) {
1075
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(skillName) || skillName.includes("..")) {
1076
- throw new Error(`refused unsafe skill name: "${skillName}"`);
1077
- }
1078
- const dest = join4(targetDir, skillName);
1079
- if (opts.mode === "symlink" && opts.canonicalDir) {
1080
- mkdirSync3(dirname2(dest), { recursive: true });
1081
- rmSync2(dest, { recursive: true, force: true });
1082
- symlinkSync(resolve2(opts.canonicalDir), dest, "dir");
1083
- return dest;
1084
- }
1085
- mkdirSync3(dest, { recursive: true });
1086
- const root = resolve2(dest);
1087
- for (const f of files) {
1088
- const fp = resolve2(dest, f.path);
1089
- if (fp !== root && !fp.startsWith(root + sep2)) {
1090
- throw new Error(`refused unsafe path in skill "${skillName}": ${f.path}`);
635
+ import { delimiter, join as join2 } from "path";
636
+ import { promisify } from "util";
637
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync3 } from "fs";
638
+ import { dirname } from "path";
639
+ var run = promisify(execFile);
640
+ function resolveClaudeBin(env = process.env) {
641
+ const override = env.SKILLMD_CLAUDE_BIN;
642
+ if (override) return existsSync2(override) ? override : null;
643
+ const win = process.platform === "win32";
644
+ const exts = win ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
645
+ const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
646
+ const home = env.USERPROFILE || env.HOME || homedir3();
647
+ dirs.push(join2(home, ".local", "bin"), join2(home, ".claude", "local"));
648
+ for (const dir of dirs) {
649
+ for (const ext of exts) {
650
+ const candidate = join2(dir, `claude${ext.toLowerCase()}`);
651
+ if (existsSync2(candidate)) return candidate;
1091
652
  }
1092
- mkdirSync3(dirname2(fp), { recursive: true });
1093
- writeFileSync4(fp, f.contents);
1094
653
  }
1095
- return dest;
654
+ return null;
1096
655
  }
1097
- function installedSkills(opts = {}) {
1098
- const scope = opts.global ? "global" : "project";
1099
- const out = [];
1100
- const seen = /* @__PURE__ */ new Set();
1101
- for (const agent of AGENTS) {
1102
- const dir = agentDir(agent.id, opts);
1103
- if (seen.has(dir)) continue;
1104
- seen.add(dir);
1105
- if (!existsSync3(dir)) continue;
1106
- for (const entry of readdirSync2(dir)) {
1107
- const skillMd = join4(dir, entry, "SKILL.md");
1108
- if (!existsSync3(skillMd) || !statSync2(skillMd).isFile()) continue;
1109
- out.push({ name: entry, agent: agent.id, scope, path: join4(dir, entry), raw: readFileSync3(skillMd, "utf8") });
656
+ var needsShell = (bin) => /\.(cmd|bat)$/i.test(bin);
657
+ var MARKETPLACE_URL = "https://skillmd.com/.claude-plugin/marketplace.json";
658
+ var MARKETPLACE_NAME = "skillmd";
659
+ var MARKETPLACE_SCHEMA = "https://anthropic.com/claude-code/marketplace.schema.json";
660
+ var TIMEOUT_MS = 12e4;
661
+ function exec(deps) {
662
+ return deps.exec ?? ((file, args) => {
663
+ const bin = file === "claude" ? resolveClaudeBin() : file;
664
+ if (!bin) return Promise.reject(new Error("claude not found on PATH"));
665
+ return run(bin, args, { timeout: TIMEOUT_MS, windowsHide: true, shell: needsShell(bin) }).then((r) => ({
666
+ stdout: String(r.stdout ?? ""),
667
+ stderr: String(r.stderr ?? "")
668
+ }));
669
+ });
670
+ }
671
+ function marketplacePluginName(owner, slug) {
672
+ const clean = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
673
+ const base = clean(slug) || "plugin";
674
+ return owner.toLowerCase() === "skillmd" ? base : `${clean(owner)}-${base}`;
675
+ }
676
+ async function claudeCliAvailable(deps = {}) {
677
+ try {
678
+ await exec(deps)("claude", ["--version"]);
679
+ return true;
680
+ } catch {
681
+ return false;
682
+ }
683
+ }
684
+ async function installClaudePlugin(owner, slug, deps = {}) {
685
+ const sh = exec(deps);
686
+ const qualified = `${marketplacePluginName(owner, slug)}@${MARKETPLACE_NAME}`;
687
+ try {
688
+ await sh("claude", ["plugin", "marketplace", "add", MARKETPLACE_URL]);
689
+ } catch (e) {
690
+ return { ok: false, reason: `could not add the SkillMD marketplace: ${msg(e)}` };
691
+ }
692
+ try {
693
+ await sh("claude", ["plugin", "marketplace", "update", MARKETPLACE_NAME]);
694
+ } catch {
695
+ }
696
+ let out;
697
+ try {
698
+ const r = await sh("claude", ["plugin", "install", qualified]);
699
+ out = `${r.stdout}
700
+ ${r.stderr}`;
701
+ } catch (e) {
702
+ return { ok: false, reason: msg(e) };
703
+ }
704
+ if (/Successfully installed plugin/i.test(out)) return { ok: true, name: qualified, marketplace: MARKETPLACE_NAME };
705
+ const failed = /Failed to install plugin[^:]*:\s*(.+)/i.exec(out);
706
+ return { ok: false, reason: (failed?.[1] ?? out).trim().split(/\r?\n/)[0] ?? "install did not report success" };
707
+ }
708
+ function msg(e) {
709
+ if (e && typeof e === "object" && "stderr" in e) {
710
+ const s = String(e.stderr ?? "").trim();
711
+ if (s) return s.split(/\r?\n/)[0];
712
+ }
713
+ return e instanceof Error ? e.message.split(/\r?\n/)[0] : String(e);
714
+ }
715
+ var SITE_URL = "https://skillmd.com";
716
+ function readJson(path) {
717
+ let text3;
718
+ try {
719
+ text3 = readFileSync(path, "utf8");
720
+ } catch {
721
+ return "absent";
722
+ }
723
+ try {
724
+ const v = JSON.parse(text3);
725
+ return v && typeof v === "object" && !Array.isArray(v) ? v : "unparsable";
726
+ } catch {
727
+ return "unparsable";
728
+ }
729
+ }
730
+ function writeJson(path, value) {
731
+ mkdirSync2(dirname(path), { recursive: true });
732
+ writeFileSync3(path, `${JSON.stringify(value, null, 2)}
733
+ `);
734
+ }
735
+ var obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? { ...v } : {};
736
+ async function installPluginNatively(owner, slug, deps = {}) {
737
+ const home = deps.home ?? (process.env.USERPROFILE || process.env.HOME || homedir3());
738
+ const claudeDir = join2(home, ".claude");
739
+ if (!existsSync2(claudeDir)) return { ok: false, reason: "no Claude Code directory on this machine" };
740
+ const name = marketplacePluginName(owner, slug);
741
+ const qualified = `${name}@${MARKETPLACE_NAME}`;
742
+ const doFetch = deps.fetch ?? fetch;
743
+ let entries;
744
+ try {
745
+ const res = await doFetch(`${SITE_URL}/plugins/${owner}/${slug}/plugin.zip`, { headers: { accept: "application/zip" } });
746
+ if (!res.ok) return { ok: false, reason: `the plugin archive answered ${res.status}` };
747
+ const { unzipSync } = await import("fflate");
748
+ entries = unzipSync(new Uint8Array(await res.arrayBuffer()));
749
+ } catch (e) {
750
+ return { ok: false, reason: msg(e) };
751
+ }
752
+ let version2 = "1.0.0";
753
+ const manifest = entries[".claude-plugin/plugin.json"];
754
+ if (manifest) {
755
+ try {
756
+ const v = JSON.parse(new TextDecoder().decode(manifest));
757
+ if (typeof v.version === "string" && v.version) version2 = v.version;
758
+ } catch {
1110
759
  }
1111
760
  }
1112
- return out;
761
+ if (!manifest) return { ok: false, reason: "the plugin archive has no .claude-plugin/plugin.json" };
762
+ const pluginsDir = join2(claudeDir, "plugins");
763
+ const installPath = join2(pluginsDir, "cache", MARKETPLACE_NAME, name, version2);
764
+ const knownPath = join2(pluginsDir, "known_marketplaces.json");
765
+ const installedPath = join2(pluginsDir, "installed_plugins.json");
766
+ const settingsPath = join2(claudeDir, "settings.json");
767
+ const known = readJson(knownPath);
768
+ if (known === "unparsable") return { ok: false, reason: "known_marketplaces.json is not readable JSON \u2014 left untouched" };
769
+ const installed = readJson(installedPath);
770
+ if (installed === "unparsable") return { ok: false, reason: "installed_plugins.json is not readable JSON \u2014 left untouched" };
771
+ const settings = readJson(settingsPath);
772
+ if (settings === "unparsable") return { ok: false, reason: "settings.json is not readable JSON \u2014 left untouched" };
773
+ try {
774
+ for (const [path, data] of Object.entries(entries)) {
775
+ if (path.endsWith("/")) continue;
776
+ const dest = join2(installPath, ...path.split("/"));
777
+ mkdirSync2(dirname(dest), { recursive: true });
778
+ writeFileSync3(dest, data);
779
+ }
780
+ writeFileSync3(join2(installPath, ".in_use"), "");
781
+ const now = (/* @__PURE__ */ new Date()).toISOString();
782
+ const source = { source: "url", url: MARKETPLACE_URL };
783
+ const marketplaceDir = join2(pluginsDir, "marketplaces", MARKETPLACE_NAME);
784
+ const cachedPath = join2(marketplaceDir, ".claude-plugin", "marketplace.json");
785
+ const cached = readJson(cachedPath);
786
+ if (cached === "unparsable") return { ok: false, reason: "the cached marketplace manifest is not readable JSON \u2014 left untouched" };
787
+ const cachedDoc = cached === "absent" ? {
788
+ $schema: MARKETPLACE_SCHEMA,
789
+ name: MARKETPLACE_NAME,
790
+ owner: { name: "SkillMD", email: "hi@skillmd.com" },
791
+ description: "Agent Skill plugins from the SkillMD registry.",
792
+ plugins: []
793
+ } : cached;
794
+ const list = Array.isArray(cachedDoc.plugins) ? cachedDoc.plugins : [];
795
+ cachedDoc.plugins = [
796
+ ...list.filter((e) => e?.name !== name),
797
+ {
798
+ name,
799
+ version: version2,
800
+ homepage: `${SITE_URL}/plugins/${owner}/${slug}`,
801
+ author: { name: owner === "skillmd" ? "SkillMD" : `@${owner}` },
802
+ source: { source: "archive", url: `${SITE_URL}/plugins/${owner}/${slug}/plugin.zip` }
803
+ }
804
+ ];
805
+ writeJson(cachedPath, cachedDoc);
806
+ const knownDoc = known === "absent" ? {} : known;
807
+ if (!knownDoc[MARKETPLACE_NAME]) {
808
+ knownDoc[MARKETPLACE_NAME] = { source, installLocation: marketplaceDir, lastUpdated: now };
809
+ writeJson(knownPath, knownDoc);
810
+ }
811
+ const installedDoc = installed === "absent" ? { version: 2, plugins: {} } : installed;
812
+ const plugins = obj(installedDoc.plugins);
813
+ const prior = Array.isArray(plugins[qualified]) ? plugins[qualified] : [];
814
+ plugins[qualified] = [
815
+ ...prior.filter((p7) => p7?.scope !== "user"),
816
+ { scope: "user", installPath, version: version2, installedAt: now, lastUpdated: now }
817
+ ];
818
+ installedDoc.plugins = plugins;
819
+ if (typeof installedDoc.version !== "number") installedDoc.version = 2;
820
+ writeJson(installedPath, installedDoc);
821
+ const settingsDoc = settings === "absent" ? {} : settings;
822
+ const declared = obj(settingsDoc.extraKnownMarketplaces);
823
+ if (!declared[MARKETPLACE_NAME]) declared[MARKETPLACE_NAME] = { source };
824
+ settingsDoc.extraKnownMarketplaces = declared;
825
+ settingsDoc.enabledPlugins = { ...obj(settingsDoc.enabledPlugins), [qualified]: true };
826
+ writeJson(settingsPath, settingsDoc);
827
+ } catch (e) {
828
+ return { ok: false, reason: msg(e) };
829
+ }
830
+ return { ok: true, name: qualified, marketplace: MARKETPLACE_NAME };
1113
831
  }
1114
832
 
1115
- // ../cli/src/commands/add.ts
1116
- var PROJECT_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "AGENTS.md", "CLAUDE.md", "skills-lock.json"];
1117
- function looksLikeProject(dir) {
1118
- if (PROJECT_MARKERS.some((m) => existsSync4(join5(dir, m)))) return true;
1119
- const agentRoots = new Set(AGENTS.map((a) => a.project[0]).filter((seg) => seg.startsWith(".")));
1120
- return [...agentRoots].some((seg) => existsSync4(join5(dir, seg)));
1121
- }
1122
- var interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
833
+ // src/commands/add.ts
834
+ var infoNotice = (text3) => ({ text: text3, style: "info" });
835
+ var warnNotice = (text3) => ({ text: text3, style: "warn" });
836
+ var skipNotice = (text3) => ({ text: text3, style: "skip" });
837
+ var replacedNotice = (text3) => ({ text: text3, style: "warn", suppressInSummary: true });
838
+ function noticeLine(n) {
839
+ switch (n.style) {
840
+ case "info":
841
+ return pc8.dim(`${GLYPH.info} ${n.text}`);
842
+ case "skip":
843
+ return pc8.dim(`${GLYPH.skip} ${n.text}`);
844
+ case "warn":
845
+ return pc8.yellow(`${GLYPH.warn} ${n.text}`);
846
+ }
847
+ }
1123
848
  async function promptScopeInteractive(ctx) {
1124
849
  const choice = await p3.select({
1125
850
  message: "Installation scope",
@@ -1131,32 +856,28 @@ async function promptScopeInteractive(ctx) {
1131
856
  });
1132
857
  return p3.isCancel(choice) ? null : choice;
1133
858
  }
859
+ function agentsPromptMessage(managed) {
860
+ return managed ? `Claude Code already has ${managed} as a plugin. Which other agents should get its skills too?` : "Which agents do you want to install to?";
861
+ }
1134
862
  async function promptAgentsInteractive(ctx) {
1135
863
  if (ctx.detected.length < 2) return ctx.detected;
864
+ const remembered = (readConfig().lastAgents ?? []).filter((id) => ctx.detected.includes(id));
1136
865
  const picked = await p3.multiselect({
1137
- message: "Which agents do you want to install to?",
866
+ message: agentsPromptMessage(ctx.managed),
1138
867
  options: ctx.detected.map((id) => ({ value: id, label: id, hint: ctx.dirFor(id) })),
1139
- initialValues: ctx.detected,
868
+ initialValues: remembered.length ? remembered : ctx.detected,
1140
869
  required: true
1141
870
  });
1142
871
  return p3.isCancel(picked) ? null : picked;
1143
872
  }
1144
- function isLocalish(arg) {
1145
- return arg === "." || arg.startsWith("./") || arg.startsWith("../") || arg.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(arg) || existsSync4(arg);
873
+ function unverifiedFilesNote(n) {
874
+ const [f, it] = n === 1 ? ["file", "it"] : ["files", "them"];
875
+ return `${n} companion ${f} came straight from the source repo \u2014 the registry holds no checksum for ${it}, so only the connection was verified`;
1146
876
  }
1147
- async function resolvePackFiles(skill, fetchTree = resolveTree) {
1148
- if (!skill.source_repo) return null;
1149
- const attempts = [{ ref: skill.commit_sha ?? void 0, pinMiss: false }];
1150
- if (skill.commit_sha) attempts.push({ ref: void 0, pinMiss: true });
1151
- for (const attempt of attempts) {
1152
- try {
1153
- const files = await fetchTree(skill.source_repo, attempt.ref);
1154
- const sk = files.find((f) => f.path === "SKILL.md");
1155
- if (sk) return { raw: sk.contents.toString("utf8"), files, pinMiss: attempt.pinMiss };
1156
- } catch {
1157
- }
1158
- }
1159
- return null;
877
+ function bundleNote(bundle, missing) {
878
+ const unverified = bundle.filter((file) => file.unverified).length;
879
+ const notes = [unverified ? unverifiedFilesNote(unverified) : "", missing.length ? missingFilesNote(missing) : ""].filter(Boolean);
880
+ return notes.length ? notes.join("; ") : void 0;
1160
881
  }
1161
882
  function registryFallbackNote(host) {
1162
883
  return `installed from GitHub because the SkillMD registry (${host}) was unreachable \u2014 this copy is not the registry-reviewed version`;
@@ -1169,84 +890,218 @@ If this environment restricts network access (sandbox, proxy, CI), ask an admini
1169
890
  Registry error: ${e.message}`
1170
891
  );
1171
892
  }
1172
- var defaultDeps = {
1173
- async resolve(arg, flags) {
1174
- if (isLocalish(arg)) {
1175
- const rs = await resolveSource(arg);
1176
- return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug }));
893
+ function argWithRef(arg, ref) {
894
+ if (!ref) return arg;
895
+ try {
896
+ const spec = parseSource(arg, { ref });
897
+ return spec.kind === "github" ? sourceId(spec) : arg;
898
+ } catch {
899
+ return arg;
900
+ }
901
+ }
902
+ function provenanceFor(arg) {
903
+ try {
904
+ return sourceId(parseSource(arg));
905
+ } catch {
906
+ return `unknown:${arg}`;
907
+ }
908
+ }
909
+ async function registryCandidate(owner, name, source, client) {
910
+ const registrySlug = `${owner}/${name}`;
911
+ const skill = await client.api(`/api/skills/${owner}/${name}`);
912
+ const skillName = skill.slug.split("/")[1] || name;
913
+ const meta = {
914
+ name: skillName,
915
+ slug: skillName,
916
+ registrySlug,
917
+ source,
918
+ commit_sha: skill.commit_sha ?? void 0,
919
+ verified: skill.verified,
920
+ type: skill.type,
921
+ securityFlags: skill.security_flags
922
+ };
923
+ if (skill.type === "pack") {
924
+ const missing = [];
925
+ const bundle = await fetchBundle(client.base, registrySlug, client.token, { missing });
926
+ if (bundle && bundle.length) {
927
+ const sk = bundle.find((file) => file.path === "SKILL.md");
928
+ const note2 = bundleNote(bundle, missing);
929
+ return {
930
+ ...meta,
931
+ raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill),
932
+ files: bundle,
933
+ ...note2 ? { note: note2 } : {}
934
+ };
1177
935
  }
1178
- let registryDown = null;
1179
- const registryHost = (() => {
1180
- try {
1181
- return new URL(createClient(flags).base).host;
1182
- } catch {
1183
- return "api.skillmd.com";
1184
- }
1185
- })();
1186
- if (/^[^/\s:]+\/[^/\s:]+$/.test(arg)) {
1187
- const { api, base: apiBase, token } = createClient(flags);
1188
- const [owner, name] = arg.split("/");
1189
- try {
1190
- const skill = await api(`/api/skills/${owner}/${name}`);
1191
- const skillName = skill.slug.split("/")[1] || name;
1192
- const meta = {
1193
- name: skillName,
1194
- slug: skillName,
1195
- registrySlug: arg,
1196
- verified: skill.verified,
1197
- type: skill.type,
1198
- securityFlags: skill.security_flags
1199
- };
1200
- if (skill.type === "pack") {
1201
- const bundle = await fetchBundle(apiBase, arg, token);
1202
- if (bundle && bundle.length) {
1203
- const sk = bundle.find((f) => f.path === "SKILL.md");
1204
- return [{ ...meta, raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill), files: bundle }];
936
+ const pack = await resolvePackFiles(skill);
937
+ if (pack) return { ...meta, ...pack };
938
+ }
939
+ return { ...meta, raw: skillMdFor(skill) };
940
+ }
941
+ async function fetchPackMembers(api, owner, slug) {
942
+ const PAGE = 200;
943
+ const members = [];
944
+ let name = slug;
945
+ let total = Infinity;
946
+ for (let offset = 0; members.length < total; offset += PAGE) {
947
+ const page = await api(`/api/packs/${owner}/${slug}?limit=${PAGE}&offset=${offset}`);
948
+ name = page.name || name;
949
+ total = Number(page.item_total ?? 0);
950
+ const got = (page.items ?? []).map((i) => ({ slug: String(i.slug ?? ""), type: i.type, verified: Boolean(i.verified) })).filter((i) => i.slug);
951
+ if (!got.length) break;
952
+ members.push(...got);
953
+ }
954
+ return { name, members };
955
+ }
956
+ function packDirNames(slugs) {
957
+ const used = /* @__PURE__ */ new Set();
958
+ return slugs.map((slug) => {
959
+ const [owner, name] = slug.split("/");
960
+ let dir = name ?? slug;
961
+ if (used.has(dir)) dir = `${owner}-${name}`;
962
+ used.add(dir);
963
+ return dir;
964
+ });
965
+ }
966
+ async function fetchPackArchive(base, owner, slug, token, doFetch = fetch) {
967
+ const headers = { accept: "application/zip" };
968
+ if (token) headers.authorization = `Bearer ${token}`;
969
+ const res = await doFetch(`${base}/api/packs/${owner}/${slug}/download`, { headers });
970
+ if (!res.ok) throw new RegistryError(`SkillMD API ${res.status} on /api/packs/${owner}/${slug}/download`, res.status);
971
+ const bytes = new Uint8Array(await res.arrayBuffer());
972
+ const { unzipSync, strFromU8 } = await import("fflate");
973
+ const entries = unzipSync(bytes);
974
+ const out = /* @__PURE__ */ new Map();
975
+ for (const [path, data] of Object.entries(entries)) {
976
+ const m = /^(.+)\/SKILL\.md$/.exec(path);
977
+ if (m) out.set(m[1], strFromU8(data));
978
+ }
979
+ return out;
980
+ }
981
+ function makeDefaultDeps(flags) {
982
+ const canPrompt = () => isInteractive(flags, {
983
+ stdinTTY: Boolean(process.stdin.isTTY),
984
+ stdoutTTY: Boolean(process.stdout.isTTY),
985
+ env: flags.env ?? process.env
986
+ });
987
+ return {
988
+ async resolve(arg, f) {
989
+ const spec = parseSource(arg, { ref: f.ref });
990
+ const source = sourceId(spec);
991
+ if (spec.kind === "plugin") {
992
+ const { api: api2, base: apiBase2, token: token2, fetch: doFetch } = createClient(f);
993
+ const { name: packName, members } = await fetchPackMembers(api2, spec.owner, spec.slug);
994
+ if (!members.length) throw new Error(`plugin ${spec.owner}/${spec.slug} has no installable skills`);
995
+ const archive = await fetchPackArchive(apiBase2, spec.owner, spec.slug, token2, doFetch);
996
+ const dirs = packDirNames(members.map((m) => m.slug));
997
+ const out = [];
998
+ const missing = [];
999
+ members.forEach((m, i) => {
1000
+ const raw = archive.get(dirs[i]);
1001
+ if (!raw) {
1002
+ missing.push(m.slug);
1003
+ return;
1205
1004
  }
1206
- const pack = await resolvePackFiles(skill);
1207
- if (pack) return [{ ...meta, ...pack }];
1005
+ const name = dirs[i];
1006
+ out.push({
1007
+ name,
1008
+ slug: name,
1009
+ registrySlug: m.slug,
1010
+ source,
1011
+ raw,
1012
+ verified: m.verified,
1013
+ type: m.type
1014
+ });
1015
+ });
1016
+ const multi = out.filter((c) => c.type === "pack");
1017
+ if (multi.length) {
1018
+ let n = 0;
1019
+ await Promise.all(Array.from({ length: Math.min(4, multi.length) }, async () => {
1020
+ for (let i = n++; i < multi.length; i = n++) {
1021
+ const c = multi[i];
1022
+ try {
1023
+ const missing2 = [];
1024
+ const bundle = await fetchBundle(apiBase2, c.registrySlug, token2, { missing: missing2 });
1025
+ if (!bundle?.length) continue;
1026
+ const sk = bundle.find((file) => file.path === "SKILL.md");
1027
+ const note2 = bundleNote(bundle, missing2);
1028
+ c.files = bundle;
1029
+ if (sk) c.raw = sk.contents.toString("utf8");
1030
+ if (note2) c.note = note2;
1031
+ } catch {
1032
+ }
1033
+ }
1034
+ }));
1035
+ }
1036
+ if (!out.length) throw new Error(`plugin ${spec.owner}/${spec.slug} returned no readable skills`);
1037
+ const first = out[0];
1038
+ first.note = `${out.length} skill${out.length === 1 ? "" : "s"} from the ${packName} plugin` + (missing.length ? ` (${missing.length} member${missing.length === 1 ? "" : "s"} were not in the archive)` : "");
1039
+ return out;
1040
+ }
1041
+ if (spec.kind !== "slug") {
1042
+ const rs = await resolveSource(argWithRef(arg, f.ref));
1043
+ return rs.map((r) => ({
1044
+ name: r.slug,
1045
+ raw: r.raw,
1046
+ slug: r.slug,
1047
+ source,
1048
+ ...spec.kind === "local" && r.file ? { files: collectFiles(dirname2(r.file), dirname2(r.file)) } : {}
1049
+ }));
1050
+ }
1051
+ let registryDown = null;
1052
+ const { api, base: apiBase, token } = createClient(f);
1053
+ const registryHost = (() => {
1054
+ try {
1055
+ return new URL(apiBase).host;
1056
+ } catch {
1057
+ return "api.skillmd.com";
1208
1058
  }
1209
- return [{ ...meta, raw: skillMdFor(skill) }];
1059
+ })();
1060
+ const registrySlug = `${spec.owner}/${spec.name}`;
1061
+ try {
1062
+ return [await registryCandidate(spec.owner, spec.name, source, { api, base: apiBase, token })];
1210
1063
  } catch (e) {
1211
1064
  if (e instanceof IntegrityError) throw e;
1212
1065
  if (e instanceof RegistryError && e.status !== 404) registryDown = e;
1213
1066
  }
1067
+ const ghSource = sourceId({ kind: "github", owner: spec.owner, repo: spec.name, display: arg });
1068
+ try {
1069
+ const rs = await resolveSource(arg);
1070
+ const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
1071
+ return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, source: ghSource, note: note2 }));
1072
+ } catch (e) {
1073
+ if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
1074
+ throw e;
1075
+ }
1076
+ },
1077
+ get promptScope() {
1078
+ return canPrompt() ? promptScopeInteractive : void 0;
1079
+ },
1080
+ get promptAgents() {
1081
+ return canPrompt() ? promptAgentsInteractive : void 0;
1082
+ },
1083
+ fireInstall(registrySlug, f) {
1084
+ const { api } = createClient(f);
1085
+ const [owner, name] = registrySlug.split("/");
1086
+ void api(`/api/skills/${owner}/${name}/install`, {
1087
+ method: "POST",
1088
+ headers: { "content-type": "application/json" },
1089
+ body: JSON.stringify({ via: "cli" })
1090
+ }).catch(() => {
1091
+ });
1214
1092
  }
1215
- try {
1216
- const rs = await resolveSource(arg);
1217
- const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
1218
- return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, note: note2 }));
1219
- } catch (e) {
1220
- if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
1221
- throw e;
1222
- }
1223
- },
1224
- get promptScope() {
1225
- return interactive() ? promptScopeInteractive : void 0;
1226
- },
1227
- get promptAgents() {
1228
- return interactive() ? promptAgentsInteractive : void 0;
1229
- },
1230
- fireInstall(registrySlug, flags) {
1231
- const { api } = createClient(flags);
1232
- const [owner, name] = registrySlug.split("/");
1233
- void api(`/api/skills/${owner}/${name}/install`, {
1234
- method: "POST",
1235
- headers: { "content-type": "application/json" },
1236
- body: JSON.stringify({ via: "cli" })
1237
- }).catch(() => {
1238
- });
1239
- }
1240
- };
1093
+ };
1094
+ }
1241
1095
  async function resolveScope(flags, deps) {
1242
1096
  const home = resolve3(flags.home ?? homedir4());
1243
1097
  const cwd = resolve3(flags.cwd ?? process.cwd());
1244
- const base = { cwd: flags.cwd, home: flags.home };
1098
+ const base = { cwd: flags.cwd, home: flags.home, env: flags.env };
1245
1099
  if (flags.global) return { scope: { global: true, ...base } };
1246
1100
  if (flags.project) return { scope: { global: false, ...base } };
1247
1101
  if (cwd === home) return { scope: { global: true, ...base } };
1248
1102
  const isProject = looksLikeProject(cwd);
1249
- if (deps.promptScope && !flags.yes && !flags.json) {
1103
+ const hosted = detectHostAgent(flags.env ?? process.env) !== null;
1104
+ if (deps.promptScope && !flags.yes && !flags.json && !hosted) {
1250
1105
  const global = await deps.promptScope({ cwd, suggestGlobal: !isProject });
1251
1106
  if (global === null) return null;
1252
1107
  return { scope: { global, ...base } };
@@ -1257,202 +1112,682 @@ async function resolveScope(flags, deps) {
1257
1112
  note: `no project detected in ${cwd} \u2014 installing to your user-level agent folders (pass --project to install here)`
1258
1113
  };
1259
1114
  }
1260
- var CANCELLED = { written: [], blocked: [], exitCode: 0, cancelled: true, output: pc7.dim("Installation cancelled \u2014 nothing was installed.") };
1261
- async function runAdd(arg, flags, deps = defaultDeps) {
1262
- let candidates;
1263
- try {
1264
- candidates = await deps.resolve(arg, flags);
1265
- } catch (e) {
1266
- if (e instanceof IntegrityError) {
1115
+ function defaultNeedsPrompt(flags) {
1116
+ const env = flags.env ?? process.env;
1117
+ if (flags.yes || flags.json || flags.global || flags.project || flags.list) return false;
1118
+ if (detectHostAgent(env)) return false;
1119
+ if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ?? homedir4())) return false;
1120
+ return !isInteractive(flags, { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env });
1121
+ }
1122
+ var CANCELLED_TEXT = "installation cancelled \u2014 nothing was installed";
1123
+ function finish2(flags, outcome) {
1124
+ const json = Boolean(flags.json);
1125
+ switch (outcome.kind) {
1126
+ case "install": {
1127
+ const installed = outcome.records.flatMap((r) => r.kind === "installed" ? [r.doc] : []);
1128
+ const blocked = outcome.records.flatMap((r) => r.kind === "blocked" ? [{ name: r.name, reason: r.reason }] : []);
1129
+ const written = installed.flatMap((d) => d.targets.map((t) => ({ name: d.name, dir: t.path })));
1130
+ const warnings = [
1131
+ ...outcome.notices,
1132
+ ...outcome.records.flatMap((r) => r.kind === "installed" ? r.notices : [])
1133
+ ].map((n) => n.text);
1134
+ const exitCode = blocked.length > 0 || installed.length === 0 && !outcome.managed ? 1 : 0;
1135
+ const doc = { ok: exitCode === 0, scope: outcome.scope, installed, blocked, warnings };
1136
+ return {
1137
+ written,
1138
+ blocked,
1139
+ exitCode,
1140
+ output: json ? JSON.stringify(doc, null, 2) : renderHuman(outcome.notices, outcome.records, outcome.paths),
1141
+ scope: outcome.scope,
1142
+ agents: [...new Set(installed.flatMap((d) => d.targets.map((t) => t.agent)))]
1143
+ };
1144
+ }
1145
+ case "list": {
1146
+ const human = outcome.skills.length ? outcome.skills.map((s) => `${pc8.bold(s.name)} ${pc8.dim(s.description)}`).join("\n") : "No skills found.";
1147
+ return { written: [], blocked: [], exitCode: 0, output: json ? JSON.stringify({ ok: true, skills: outcome.skills }, null, 2) : human };
1148
+ }
1149
+ case "cancel": {
1150
+ const doc = { ok: false, error: CANCELLED_TEXT };
1267
1151
  return {
1268
1152
  written: [],
1269
- blocked: [{ name: arg, reason: e.message }],
1270
- exitCode: 1,
1271
- output: pc7.red(`\u2717 ${arg} blocked \u2014 ${e.message}`)
1153
+ blocked: [],
1154
+ exitCode: 0,
1155
+ cancelled: true,
1156
+ output: json ? JSON.stringify(doc, null, 2) : pc8.dim("Installation cancelled \u2014 nothing was installed.")
1272
1157
  };
1273
1158
  }
1274
- throw e;
1275
- }
1276
- if (flags.skill?.length) candidates = candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
1277
- const deny = new Set(flags.deny ?? []);
1278
- const written = [];
1279
- const blocked = [];
1280
- const lines = [];
1281
- const resolved = await resolveScope(flags, deps);
1282
- if (!resolved) return CANCELLED;
1283
- const { scope } = resolved;
1284
- const isGlobal = scope.global;
1285
- const home = flags.home ?? homedir4();
1286
- if (resolved.note) lines.push(pc7.dim(`\u2139 ${resolved.note}`));
1287
- const detected = detectAgents(scope);
1288
- let targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
1289
- if (!flags.agent?.length && deps.promptAgents && !flags.yes && !flags.json) {
1290
- const picked = await deps.promptAgents({ detected: targets, dirFor: (id) => agentDir(id, scope) });
1291
- if (picked === null) return CANCELLED;
1292
- targets = picked;
1293
- }
1294
- const dirAgents = /* @__PURE__ */ new Map();
1295
- for (const agentId of targets) {
1296
- const dir = agentDir(agentId, scope);
1297
- dirAgents.set(dir, [...dirAgents.get(dir) ?? [], agentId]);
1298
- }
1299
- for (const c of candidates) {
1300
- const result = lint(c.raw, { slug: c.slug });
1301
- const allFlags = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
1302
- const deniedFlags = allFlags.filter((f) => deny.has(f));
1303
- if (!flags.skipLint && !result.ok) {
1304
- const errors = result.diagnostics.filter((d) => d.severity === "error");
1305
- blocked.push({ name: c.name, reason: `lint errors: ${errors.map((d) => d.id).join(", ")}` });
1306
- lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
1307
- for (const d of errors) lines.push(pc7.dim(` ${d.id} ${d.message}`));
1308
- continue;
1159
+ case "fail": {
1160
+ const warnings = (outcome.notices ?? []).map((n) => n.text);
1161
+ const doc = { ok: false, error: outcome.error };
1162
+ if (outcome.blocked?.length) doc.blocked = outcome.blocked;
1163
+ if (warnings.length) doc.warnings = warnings;
1164
+ const human = [
1165
+ ...(outcome.notices ?? []).map(noticeLine),
1166
+ ...outcome.blocked?.length ? outcome.blocked.map((b) => pc8.red(`${GLYPH.blocked} ${b.name} blocked \u2014 ${b.reason}`)) : [pc8.red(outcome.error)]
1167
+ ].join("\n");
1168
+ return { written: [], blocked: outcome.blocked ?? [], exitCode: 1, output: json ? JSON.stringify(doc, null, 2) : human };
1309
1169
  }
1310
- if (deniedFlags.length) {
1311
- blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
1312
- lines.push(pc7.red(`\u2717 ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`));
1170
+ }
1171
+ }
1172
+ function renderHuman(notices, records, paths) {
1173
+ const lines = notices.map(noticeLine);
1174
+ for (const r of records) {
1175
+ if (r.kind === "blocked") {
1176
+ lines.push(pc8.red(r.message));
1313
1177
  continue;
1314
1178
  }
1315
- const files = c.files ?? [{ path: "SKILL.md", contents: c.raw }];
1316
- const base = isGlobal ? home : resolve3(flags.cwd ?? process.cwd());
1317
- lines.push(pc7.green(`\u2713 ${c.name}`) + pc7.dim(` score ${result.score}${allFlags.length ? ` ${allFlags.join(", ")}` : ""}`));
1318
- for (const [dir, agentIds] of dirAgents) {
1319
- const out = writeSkill(c.name, files, dir, { mode: "copy" });
1320
- written.push({ name: c.name, dir: out });
1321
- const rel = relative2(base, out).split("\\").join("/") || out;
1322
- lines.push(pc7.dim(` ${rel.padEnd(28)} ${agentIds.join(", ")}`));
1179
+ const { doc } = r;
1180
+ lines.push(renderInstallSummary({
1181
+ name: doc.name,
1182
+ score: r.score,
1183
+ flags: r.securityFlags,
1184
+ source: r.source,
1185
+ canonical: doc.canonical,
1186
+ targets: doc.targets,
1187
+ skipped: doc.skipped,
1188
+ ...r.replaced ? { replaced: r.replaced } : {}
1189
+ }, paths));
1190
+ for (const n of r.notices) if (!n.suppressInSummary) lines.push(noticeLine(n));
1191
+ }
1192
+ return lines.join("\n");
1193
+ }
1194
+ async function resolveCandidates(arg, flags, deps) {
1195
+ const candidates = await deps.resolve(arg, flags);
1196
+ if (!flags.skill?.length) return candidates;
1197
+ return candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
1198
+ }
1199
+ function gate(flags, deps, candidates) {
1200
+ if (flags.list) {
1201
+ return finish2(flags, {
1202
+ kind: "list",
1203
+ skills: candidates.map((c) => {
1204
+ const parsed = parseSkillMd2(c.raw);
1205
+ const description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
1206
+ return { name: c.name, description: safeText(description, 80) };
1207
+ })
1208
+ });
1209
+ }
1210
+ if (flags.json && !flags.yes) {
1211
+ return finish2(flags, { kind: "fail", error: "--json requires -y (no prompts can be shown in JSON mode)" });
1212
+ }
1213
+ if (deps.needsPrompt ?? (!deps.promptScope && defaultNeedsPrompt(flags))) {
1214
+ return finish2(flags, { kind: "fail", error: nonInteractiveHint("-y (auto-detects the scope) or -g / -p") });
1215
+ }
1216
+ return null;
1217
+ }
1218
+ async function chooseTargets(flags, deps, scope, detected, managed) {
1219
+ const notices = [];
1220
+ let targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
1221
+ if (scope.global) {
1222
+ const kept = [];
1223
+ for (const id of targets) {
1224
+ if (agentSupportsGlobal(id)) kept.push(id);
1225
+ else notices.push(skipNotice(`${id}: project-only agent`));
1323
1226
  }
1324
- if (c.pinMiss) {
1325
- lines.push(pc7.yellow(`\u26A0 ${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
1227
+ targets = kept;
1228
+ }
1229
+ if (!flags.agent?.length && deps.promptAgents && !flags.yes && !flags.json && !detectHostAgent(flags.env ?? process.env)) {
1230
+ const picked = await deps.promptAgents({ detected: targets, dirFor: (id) => agentDir(id, scope), ...managed ? { managed } : {} });
1231
+ if (picked === null) return null;
1232
+ targets = picked;
1233
+ if (flags.home === void 0 && picked.length) {
1234
+ try {
1235
+ writeConfig({ ...readConfig(), lastAgents: picked });
1236
+ } catch {
1237
+ }
1326
1238
  }
1327
- if (c.note) {
1328
- lines.push(pc7.yellow(`\u26A0 ${c.name}: ${c.note}`));
1239
+ }
1240
+ return { targets, notices };
1241
+ }
1242
+ async function installOne(c, ctx) {
1243
+ const result = lint2(c.raw, { slug: c.slug });
1244
+ const securityFlags2 = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
1245
+ const deniedFlags = securityFlags2.filter((f) => ctx.deny.has(f));
1246
+ if (!ctx.flags.skipLint && !result.ok) {
1247
+ return {
1248
+ kind: "blocked",
1249
+ name: c.name,
1250
+ reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}`,
1251
+ message: `${GLYPH.blocked} ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`
1252
+ };
1253
+ }
1254
+ if (deniedFlags.length) {
1255
+ return {
1256
+ kind: "blocked",
1257
+ name: c.name,
1258
+ reason: `denied security flags: ${deniedFlags.join(", ")}`,
1259
+ message: `${GLYPH.blocked} ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`
1260
+ };
1261
+ }
1262
+ const source = c.source ?? (c.registrySlug ? `registry:${c.registrySlug}` : provenanceFor(ctx.arg));
1263
+ let out;
1264
+ try {
1265
+ out = await installSkill({
1266
+ name: c.name,
1267
+ files: c.files ?? [{ path: "SKILL.md", contents: c.raw }],
1268
+ source,
1269
+ commit_sha: c.commit_sha,
1270
+ scope: ctx.scope,
1271
+ agents: ctx.targets,
1272
+ explicitAgents: Boolean(ctx.flags.agent?.length),
1273
+ mode: ctx.flags.mode,
1274
+ force: ctx.flags.force
1275
+ });
1276
+ } catch (e) {
1277
+ const reason = e instanceof Error ? e.message : String(e);
1278
+ return { kind: "blocked", name: c.name, reason, message: `${GLYPH.blocked} ${c.name} \u2014 ${reason}` };
1279
+ }
1280
+ const notices = [];
1281
+ if (out.replaced) notices.push(replacedNotice(`${c.name}: replaced previous install (${out.replaced})`));
1282
+ if (c.pinMiss) notices.push(warnNotice(`${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
1283
+ if (c.note) notices.push(warnNotice(`${c.name}: ${c.note}`));
1284
+ if (c.type === "pack" && !c.files) {
1285
+ 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.`));
1286
+ }
1287
+ if (c.registrySlug && !telemetryDisabled(ctx.env)) ctx.deps.fireInstall(c.registrySlug, ctx.flags);
1288
+ return {
1289
+ kind: "installed",
1290
+ doc: { name: c.name, canonical: out.canonical, targets: out.targets, skipped: out.skipped, digest: out.digest },
1291
+ score: result.score,
1292
+ securityFlags: securityFlags2,
1293
+ notices,
1294
+ source,
1295
+ ...out.replaced ? { replaced: out.replaced } : {}
1296
+ };
1297
+ }
1298
+ async function tryPluginInstall(arg, flags) {
1299
+ let spec;
1300
+ try {
1301
+ spec = parseSource(arg, { ref: flags.ref });
1302
+ } catch {
1303
+ return null;
1304
+ }
1305
+ if (spec.kind !== "plugin") return null;
1306
+ if (flags.agent?.length || flags.skill?.length || flags.list || flags.mode === "copy") return null;
1307
+ const viaCli = await claudeCliAvailable() ? await installClaudePlugin(spec.owner, spec.slug) : null;
1308
+ const res = viaCli?.ok ? viaCli : await installPluginNatively(spec.owner, spec.slug, { home: flags.home });
1309
+ if (!res.ok) {
1310
+ if (!viaCli && /no Claude Code directory/.test(res.reason)) return null;
1311
+ return { ok: false, reason: viaCli && !viaCli.ok ? viaCli.reason : res.reason };
1312
+ }
1313
+ return {
1314
+ ok: true,
1315
+ agent: "claude-code",
1316
+ name: res.name,
1317
+ note: `installed ${res.name} as a Claude Code plugin \u2014 manage it with \`claude plugin\` or /plugin`
1318
+ };
1319
+ }
1320
+ async function runAdd(arg, flags, deps) {
1321
+ const d = deps ?? makeDefaultDeps(flags);
1322
+ const env = flags.env ?? process.env;
1323
+ const attempt = await tryPluginInstall(arg, flags);
1324
+ const managed = attempt?.ok ? attempt : null;
1325
+ const managedNotices = !attempt ? [] : attempt.ok ? [infoNotice(attempt.note)] : [warnNotice(`could not install this as a Claude Code plugin (${attempt.reason}) \u2014 installing its skills instead`)];
1326
+ let scopeOnce;
1327
+ const scopeOf = async () => scopeOnce ??= await resolveScope(flags, d);
1328
+ if (managed) {
1329
+ const pre = await scopeOf();
1330
+ if (!pre) return finish2(flags, { kind: "cancel" });
1331
+ const others = detectAgents(pre.scope).filter((id) => id !== managed.agent);
1332
+ if (!others.length) {
1333
+ const paths2 = { home: flags.home ?? homedir4(), cwd: flags.cwd ?? process.cwd() };
1334
+ return finish2(flags, {
1335
+ kind: "install",
1336
+ scope: pre.scope.global ? "global" : "project",
1337
+ records: [],
1338
+ notices: [...pre.note ? [infoNotice(pre.note)] : [], ...managedNotices],
1339
+ paths: paths2,
1340
+ managed: managed.name
1341
+ });
1329
1342
  }
1330
- if (c.type === "pack" && !c.files) {
1331
- 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.`));
1343
+ }
1344
+ let candidates;
1345
+ try {
1346
+ candidates = await resolveCandidates(arg, flags, d);
1347
+ } catch (e) {
1348
+ if (e instanceof IntegrityError) {
1349
+ return finish2(flags, { kind: "fail", error: e.message, blocked: [{ name: arg, reason: e.message }] });
1332
1350
  }
1333
- if (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
1351
+ if (flags.json) return finish2(flags, { kind: "fail", error: e instanceof Error ? e.message : String(e) });
1352
+ throw e;
1334
1353
  }
1335
- const exitCode = written.length === 0 ? 1 : 0;
1336
- return { written, blocked, exitCode, output: lines.join("\n") };
1354
+ const early = gate(flags, d, candidates);
1355
+ if (early) return early;
1356
+ const resolved = await scopeOf();
1357
+ if (!resolved) return finish2(flags, { kind: "cancel" });
1358
+ const scope = resolved.scope;
1359
+ const notices = resolved.note ? [infoNotice(resolved.note)] : [];
1360
+ notices.push(...managedNotices);
1361
+ const detected = detectAgents(scope).filter((id) => id !== managed?.agent);
1362
+ const chosen = await chooseTargets(flags, d, scope, detected, managed?.name);
1363
+ if (!chosen) return finish2(flags, { kind: "cancel" });
1364
+ notices.push(...chosen.notices);
1365
+ const ctx = { arg, flags, deny: new Set(flags.deny ?? []), scope, targets: chosen.targets, env, deps: d };
1366
+ const records = [];
1367
+ for (const c of candidates) records.push(await installOne(c, ctx));
1368
+ const paths = { home: flags.home ?? homedir4(), cwd: flags.cwd ?? process.cwd() };
1369
+ return finish2(flags, { kind: "install", scope: scope.global ? "global" : "project", records, notices, paths, ...managed ? { managed: managed.name } : {} });
1370
+ }
1371
+ function addFlags(opts, parent) {
1372
+ const merged = { ...parent, ...opts };
1373
+ if (opts.copy) merged.mode = "copy";
1374
+ return merged;
1337
1375
  }
1338
1376
  function addCommand() {
1339
- 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("--copy", "copy files (default)", true).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) => {
1340
- const merged = { ...cmd.parent?.opts(), ...opts };
1341
- const run = await runAdd(source, merged);
1342
- console.log(run.output);
1343
- process.exitCode = run.exitCode;
1377
+ 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) => {
1378
+ const merged = addFlags(opts, cmd.parent?.opts());
1379
+ const { run: run2, printed } = await runAddWithUI(source, merged);
1380
+ if (!printed) {
1381
+ if (run2.exitCode === 1 && !merged.json) console.error(run2.output);
1382
+ else console.log(run2.output);
1383
+ }
1384
+ process.exitCode = run2.exitCode;
1344
1385
  });
1345
1386
  }
1387
+ var clackUI = {
1388
+ intro: (m) => p3.intro(m),
1389
+ spinner: () => p3.spinner(),
1390
+ message: (m) => p3.log.message(m),
1391
+ outro: (m) => p3.outro(m),
1392
+ cancel: (m) => p3.cancel(m),
1393
+ confirm: async (m) => {
1394
+ const answer = await p3.confirm({ message: m, initialValue: true });
1395
+ return p3.isCancel(answer) ? null : answer;
1396
+ }
1397
+ };
1398
+ var FIND_SKILLS = "skillmd/find-skills";
1399
+ async function offerFindSkills(source, flags, run2, ui, deps) {
1400
+ if (!ui.confirm || run2.exitCode !== 0 || !run2.written.length || !run2.scope || !run2.agents?.length) return;
1401
+ if (source.replace(/^registry:/, "").toLowerCase() === FIND_SKILLS) return;
1402
+ const cfg = readConfig(flags.home);
1403
+ if (cfg.findSkillsOffered) return;
1404
+ const global = run2.scope === "global";
1405
+ const lock = readLock({ global, cwd: flags.cwd, home: flags.home });
1406
+ if (Object.values(lock.skills).some((e) => e.source === `registry:${FIND_SKILLS}`)) return;
1407
+ try {
1408
+ writeConfig({ ...cfg, findSkillsOffered: true }, flags.home);
1409
+ } catch {
1410
+ }
1411
+ const yes = await ui.confirm("Also add find-skills, so your agent can look for more skills on SkillMD when a task needs one?");
1412
+ if (!yes) return;
1413
+ const r = await runAdd(FIND_SKILLS, {
1414
+ ...flags,
1415
+ yes: true,
1416
+ json: false,
1417
+ skill: void 0,
1418
+ list: void 0,
1419
+ ref: void 0,
1420
+ global,
1421
+ project: !global,
1422
+ agent: run2.agents
1423
+ }, deps);
1424
+ ui.message(r.exitCode === 0 ? `${pc8.green(GLYPH.ok)} find-skills added` : pc8.yellow(`${GLYPH.warn} find-skills could not be added. Try later: npx get-skill add ${FIND_SKILLS}`));
1425
+ }
1426
+ async function runAddWithUI(source, flags, opts = {}) {
1427
+ const term = opts.term ?? { ...realTerminal(), env: flags.env ?? process.env };
1428
+ const base = opts.deps ?? makeDefaultDeps(flags);
1429
+ if (!isInteractive(flags, term)) return { run: await runAdd(source, flags, base), printed: false };
1430
+ const ui = opts.ui ?? clackUI;
1431
+ ui.intro(banner());
1432
+ try {
1433
+ const run2 = await runAdd(source, flags, {
1434
+ // NOTE: spreading `base` reads makeDefaultDeps()'s promptScope/promptAgents
1435
+ // getters right here, freezing their values for this run. That is exactly
1436
+ // what we want (interactivity is decided once, up front), but it does mean
1437
+ // a getter added to AddDeps later stops being lazy the moment it crosses
1438
+ // this spread.
1439
+ ...base,
1440
+ resolve: async (arg, f) => {
1441
+ const s = ui.spinner();
1442
+ s.start(`Resolving ${arg}`);
1443
+ try {
1444
+ const c = await base.resolve(arg, f);
1445
+ s.stop(`${c.length} skill${c.length === 1 ? "" : "s"} found`);
1446
+ return c;
1447
+ } catch (e) {
1448
+ s.stop(pc8.red("Could not resolve the source"));
1449
+ throw e;
1450
+ }
1451
+ }
1452
+ });
1453
+ if (run2.cancelled) {
1454
+ ui.cancel("Installation cancelled \u2014 nothing was installed.");
1455
+ return { run: run2, printed: true };
1456
+ }
1457
+ ui.message(run2.output);
1458
+ await offerFindSkills(source, flags, run2, ui, base);
1459
+ ui.outro(run2.exitCode === 0 ? `Done. Run ${pc8.cyan("npx get-skill list")} to see everything installed.` : pc8.red("Some skills were not installed."));
1460
+ return { run: run2, printed: true };
1461
+ } catch (e) {
1462
+ ui.outro(pc8.red("Nothing was installed."));
1463
+ throw e;
1464
+ }
1465
+ }
1346
1466
 
1347
- // ../cli/src/commands/list.ts
1467
+ // src/commands/list.ts
1348
1468
  import { Command as Command8 } from "commander";
1349
- import pc8 from "picocolors";
1350
- function listCommand() {
1351
- 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) => {
1352
- const scope = { global: opts.global };
1353
- const skills = installedSkills(scope);
1354
- if (opts.json) {
1355
- console.log(JSON.stringify(skills.map((s) => ({ name: s.name, agent: s.agent, scope: s.scope, path: s.path })), null, 2));
1356
- return;
1357
- }
1358
- if (skills.length === 0) {
1359
- console.log("No installed skills found.");
1360
- return;
1361
- }
1362
- for (const s of skills) {
1363
- const parsed = parseSkillMd(s.raw);
1364
- const desc = "error" in parsed ? pc8.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc8.dim(parsed.description.slice(0, 80));
1365
- console.log(`${pc8.bold(s.name)} ${pc8.dim(`[${s.agent}/${s.scope}]`)}
1366
- ${desc}`);
1469
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1470
+ import { join as join4 } from "path";
1471
+ import pc9 from "picocolors";
1472
+ import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
1473
+ function runList(flags = {}) {
1474
+ const scopes = scopesFor(flags);
1475
+ let items = [];
1476
+ for (const global of scopes) {
1477
+ for (const s of listInstalled({ global, cwd: flags.cwd, home: flags.home })) {
1478
+ const md = join4(s.path, "SKILL.md");
1479
+ let description = "";
1480
+ if (existsSync3(md)) {
1481
+ const parsed = parseSkillMd3(readFileSync2(md, "utf8"));
1482
+ description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
1483
+ }
1484
+ items.push({ ...s, description: safeText(description, 80) });
1367
1485
  }
1486
+ }
1487
+ if (flags.agent?.length) items = items.filter((i) => i.agents.some((a) => flags.agent.includes(a)));
1488
+ items.sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
1489
+ if (flags.json) {
1490
+ 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) };
1491
+ }
1492
+ if (items.length === 0) return { items, output: pc9.dim("No installed skills found. Install one with `npx get-skill add owner/name`.") };
1493
+ const w = Math.max(...items.map((i) => i.name.length), 4);
1494
+ const lines = items.map((i) => {
1495
+ const tag = i.tracked ? pc9.dim(i.source ?? "") : pc9.yellow("untracked");
1496
+ const modes = [...new Set(Object.values(i.mode))].join("/");
1497
+ return `${pc9.bold(i.name.padEnd(w))} ${pc9.cyan(i.scope.padEnd(7))} ${i.agents.join(", ").padEnd(24)} ${pc9.dim(modes.padEnd(9))} ${tag}
1498
+ ${" ".repeat(w + 2)}${pc9.dim(i.description)}`;
1499
+ });
1500
+ return { items, output: lines.join("\n") };
1501
+ }
1502
+ function listCommand() {
1503
+ 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) => {
1504
+ console.log(runList({ ...cmd.parent?.opts(), ...opts }).output);
1368
1505
  });
1369
1506
  }
1370
1507
 
1371
- // ../cli/src/commands/remove.ts
1508
+ // src/commands/remove.ts
1372
1509
  import { Command as Command9 } from "commander";
1373
- import { rmSync as rmSync3 } from "fs";
1374
- import pc9 from "picocolors";
1375
- function runRemove(names, scope = {}) {
1376
- const installed = installedSkills(scope);
1377
- const targets = installed.filter((s) => names.includes(s.name));
1378
- const removed = [];
1510
+ import { rmSync } from "fs";
1511
+ import { join as join5, resolve as resolve4 } from "path";
1512
+ import * as p4 from "@clack/prompts";
1513
+ import pc10 from "picocolors";
1514
+ var defaultDeps = (flags) => isInteractive(flags) ? {
1515
+ confirm: async (msg2) => {
1516
+ const a = await p4.confirm({ message: msg2, initialValue: false });
1517
+ return !p4.isCancel(a) && a;
1518
+ },
1519
+ pick: async ({ names }) => {
1520
+ const a = await p4.multiselect({ message: "Which skills do you want to remove?", options: names.map((n) => ({ value: n, label: n })), required: true });
1521
+ return p4.isCancel(a) ? null : a;
1522
+ }
1523
+ } : {};
1524
+ function finish3(flags, result, doc) {
1525
+ return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
1526
+ }
1527
+ var scopeLabel = (scope) => scope.global ? "global" : "project";
1528
+ async function runRemove(names, flags, deps = defaultDeps(flags)) {
1529
+ const scopes = scopesFor(flags).map((global) => ({ global, cwd: flags.cwd, home: flags.home }));
1530
+ const where = scopes.map(scopeLabel).join(" and ");
1379
1531
  const lines = [];
1380
- for (const t of targets) {
1381
- rmSync3(t.path, { recursive: true, force: true });
1382
- removed.push(t.path);
1383
- lines.push(pc9.green(`\u2713 removed ${t.name} (${t.agent})`));
1532
+ if (flags.all && names.length) {
1533
+ const error = "--all cannot be combined with skill names";
1534
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
1384
1535
  }
1385
- for (const n of names) {
1386
- if (!targets.some((t) => t.name === n)) lines.push(pc9.yellow(`- ${n} not installed`));
1536
+ const perScope = scopes.map((scope) => ({ scope, installed: listInstalled(scope) }));
1537
+ const allNames = [...new Set(perScope.flatMap((s) => s.installed.map((i) => i.name)))];
1538
+ const nothingInstalled = `No installed skills found in ${where} scope.`;
1539
+ let targets = names;
1540
+ if (flags.all) {
1541
+ if (allNames.length === 0) {
1542
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
1543
+ }
1544
+ targets = allNames;
1545
+ } else if (targets.length === 0) {
1546
+ if (!deps.pick) {
1547
+ const error = "Give one or more skill names, or --all.";
1548
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(`${error} `) + pc10.dim(nonInteractiveHint("skill names and -y")) }, { ok: false, error });
1549
+ }
1550
+ if (allNames.length === 0) {
1551
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
1552
+ }
1553
+ const picked = await deps.pick({ names: allNames });
1554
+ if (!picked) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
1555
+ targets = picked;
1387
1556
  }
1388
- return { removed, output: lines.join("\n") };
1557
+ const known = targets.filter((n) => allNames.includes(n));
1558
+ const missing = targets.filter((n) => !known.includes(n));
1559
+ for (const n of missing) lines.push(pc10.yellow(`- ${n} not installed (${where} scope)`));
1560
+ if (known.length === 0) {
1561
+ const output = lines.join("\n");
1562
+ return finish3(flags, { removed: [], exitCode: 1, output }, { ok: false, error: `not installed: ${missing.join(", ")}`, missing });
1563
+ }
1564
+ if (!flags.yes) {
1565
+ const what = flags.agent?.length ? `from ${flags.agent.join(", ")}` : "from every agent";
1566
+ const ok = deps.confirm ? await deps.confirm(`Remove ${known.length} skill${known.length === 1 ? "" : "s"} (${known.join(", ")}) ${what} in ${where} scope?`) : null;
1567
+ if (ok === null) {
1568
+ const error = nonInteractiveHint("-y");
1569
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
1570
+ }
1571
+ if (!ok) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
1572
+ }
1573
+ const removed = [];
1574
+ const skipped = [];
1575
+ let changed = 0;
1576
+ for (const name of known) {
1577
+ for (const { scope, installed } of perScope) {
1578
+ if (!installed.some((s) => s.name === name)) continue;
1579
+ const tag = scopes.length > 1 ? pc10.dim(` [${scopeLabel(scope)}]`) : "";
1580
+ if (flags.agent?.length) {
1581
+ const lock = readLock({ global: Boolean(scope.global), cwd: scope.cwd, home: scope.home });
1582
+ const entry = lock.skills[name];
1583
+ const canonRoot = resolve4(canonicalDir(scope));
1584
+ let acted = false;
1585
+ for (const a of flags.agent) {
1586
+ let dir;
1587
+ try {
1588
+ dir = agentDir(a, scope);
1589
+ } catch (e) {
1590
+ lines.push(pc10.yellow(`- ${a}: ${e instanceof Error ? e.message : String(e)}`));
1591
+ continue;
1592
+ }
1593
+ if (resolve4(dir) === canonRoot || entry?.mode[a] === "canonical") {
1594
+ 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)`));
1595
+ skipped.push({ skill: name, agent: a, scope: scopeLabel(scope), reason: "reads the canonical copy directly" });
1596
+ acted = true;
1597
+ continue;
1598
+ }
1599
+ const pth = join5(dir, name);
1600
+ if (isSkillDirOrLink(pth)) {
1601
+ rmSync(pth, { recursive: true, force: true });
1602
+ removed.push(pth);
1603
+ acted = true;
1604
+ lines.push(pc10.green(`${GLYPH.ok} removed ${name} from ${a}`) + tag);
1605
+ }
1606
+ }
1607
+ if (!acted) lines.push(pc10.yellow(`- ${name} is not linked into ${flags.agent.join(", ")}`) + tag);
1608
+ if (entry) {
1609
+ const left = entry.agents.filter((a) => !flags.agent.includes(a));
1610
+ 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))) });
1611
+ else {
1612
+ const r2 = uninstallSkill(name, scope);
1613
+ removed.push(...r2.removed);
1614
+ lines.push(pc10.dim(` (no agents left \u2014 removed the canonical copy too)`));
1615
+ }
1616
+ }
1617
+ if (acted) changed++;
1618
+ continue;
1619
+ }
1620
+ const r = uninstallSkill(name, scope);
1621
+ removed.push(...r.removed);
1622
+ changed++;
1623
+ lines.push(pc10.green(`${GLYPH.ok} removed ${name}`) + tag + pc10.dim(` (${r.removed.length} path${r.removed.length === 1 ? "" : "s"}${r.untracked ? ", untracked" : ""})`));
1624
+ }
1625
+ }
1626
+ const exitCode = changed ? 0 : 1;
1627
+ const doc = { ok: exitCode === 0, removed, missing };
1628
+ if (skipped.length) doc.skipped = skipped;
1629
+ return finish3(flags, { removed, exitCode, output: lines.join("\n") }, doc);
1389
1630
  }
1390
1631
  function removeCommand() {
1391
- 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) => {
1392
- const run = runRemove(names, { global: opts.global });
1393
- console.log(run.output);
1632
+ 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) => {
1633
+ const run2 = await runRemove(names, { ...cmd.parent?.opts(), ...opts });
1634
+ console.log(run2.output);
1635
+ process.exitCode = run2.exitCode;
1394
1636
  });
1395
1637
  }
1396
1638
 
1397
- // ../cli/src/commands/update.ts
1639
+ // src/commands/update.ts
1398
1640
  import { Command as Command10 } from "commander";
1399
- import { writeFileSync as writeFileSync5 } from "fs";
1400
- import { join as join6 } from "path";
1401
- import pc10 from "picocolors";
1402
- async function runUpdate(names, flags, scope = {}) {
1403
- const { api } = createClient(flags);
1404
- const installed = installedSkills(scope).filter((s) => names.length === 0 || names.includes(s.name));
1641
+ import pc11 from "picocolors";
1642
+ import { lint as lint3 } from "@skillmds/core";
1643
+ function pickSkillTree(files, want) {
1644
+ const norm = files.map((f) => ({ path: f.path.replace(/\\/g, "/"), contents: f.contents }));
1645
+ const match = norm.find((f) => f.path.endsWith("/SKILL.md") && f.path.split("/").at(-2) === want);
1646
+ if (match) {
1647
+ const prefix = match.path.slice(0, -"SKILL.md".length);
1648
+ return norm.filter((f) => f.path.startsWith(prefix)).map((f) => ({ path: f.path.slice(prefix.length), contents: f.contents }));
1649
+ }
1650
+ if (norm.some((f) => f.path === "SKILL.md")) return norm;
1651
+ return null;
1652
+ }
1653
+ function makeDefaultUpdateDeps(opts = {}) {
1654
+ const tree = opts.resolveTree ?? ((input) => resolveTree(input));
1655
+ const one = opts.resolveSource ?? ((url) => resolveSource(url));
1656
+ const client = opts.client ?? createClient;
1657
+ return {
1658
+ async fetchLatest(source, flags, name) {
1659
+ if (source.startsWith("registry:")) {
1660
+ const slug = source.slice("registry:".length);
1661
+ const { api, base, token } = client(flags);
1662
+ let skill;
1663
+ try {
1664
+ skill = await api(`/api/skills/${slug}`);
1665
+ } catch (e) {
1666
+ if (e.status === 404) return null;
1667
+ throw e;
1668
+ }
1669
+ if (skill.type === "pack") {
1670
+ const bundle = await fetchBundle(base, slug, token);
1671
+ if (bundle?.length) return { files: bundle, commit_sha: skill.commit_sha ?? void 0 };
1672
+ const pack = await resolvePackFiles(skill);
1673
+ if (pack) return { files: pack.files, commit_sha: skill.commit_sha ?? void 0 };
1674
+ }
1675
+ return { files: [{ path: "SKILL.md", contents: skillMdFor(skill) }], commit_sha: skill.commit_sha ?? void 0 };
1676
+ }
1677
+ if (source.startsWith("github:")) {
1678
+ const spec = parseSource(source);
1679
+ if (spec.kind !== "github") return null;
1680
+ const files = await tree(gigetInput(spec));
1681
+ const picked = pickSkillTree(files, spec.skill ?? name);
1682
+ return picked ? { files: picked } : null;
1683
+ }
1684
+ if (source.startsWith("gist:")) {
1685
+ const [user, id] = source.slice("gist:".length).split("/");
1686
+ const rs = await one(`https://gist.github.com/${user}/${id}`);
1687
+ return rs[0] ? { files: [{ path: "SKILL.md", contents: rs[0].raw }] } : null;
1688
+ }
1689
+ return null;
1690
+ }
1691
+ };
1692
+ }
1693
+ var defaultUpdateDeps = makeDefaultUpdateDeps();
1694
+ async function runUpdate(names, flags, deps = defaultUpdateDeps) {
1695
+ const scopes = scopesFor(flags);
1405
1696
  const updated = [];
1697
+ const available = [];
1698
+ const failed = [];
1406
1699
  const lines = [];
1407
- for (const s of installed) {
1408
- const slug = await findRegistrySlug(api, s);
1409
- if (!slug) {
1410
- lines.push(pc10.dim(`- ${s.name} skipped (no registry match)`));
1411
- continue;
1412
- }
1413
- try {
1414
- const [owner, name] = slug.split("/");
1415
- const skill = await api(`/api/skills/${owner}/${name}`);
1416
- const next = skillMdFor(skill);
1417
- if (next.trim() === s.raw.trim()) {
1418
- lines.push(pc10.dim(`- ${s.name} already up to date`));
1700
+ const seen = /* @__PURE__ */ new Set();
1701
+ for (const global of scopes) {
1702
+ const scope = { global, cwd: flags.cwd, home: flags.home };
1703
+ const lock = readLock(scope);
1704
+ const installed = listInstalled(scope).filter((s) => names.length === 0 || names.includes(s.name));
1705
+ for (const s of installed) {
1706
+ seen.add(s.name);
1707
+ const label2 = `${s.name}${scopes.length > 1 ? pc11.dim(` [${s.scope}]`) : ""}`;
1708
+ const entry = lock.skills[s.name];
1709
+ if (!entry) {
1710
+ lines.push(pc11.dim(`- ${label2} untracked \u2014 reinstall with \`npx get-skill add\` to track updates`));
1419
1711
  continue;
1420
1712
  }
1421
- const result = lint(next, { slug: s.name });
1422
- if (!result.ok) {
1423
- lines.push(pc10.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
1713
+ if (entry.source.startsWith("local:")) {
1714
+ lines.push(pc11.dim(`- ${label2} local source \u2014 not auto-updated`));
1424
1715
  continue;
1425
1716
  }
1426
- writeFileSync5(join6(s.path, "SKILL.md"), next, "utf8");
1427
- updated.push(s.name);
1428
- lines.push(pc10.green(`\u2713 updated ${s.name}`));
1429
- } catch {
1430
- lines.push(pc10.red(`\u2717 ${s.name} update error`));
1717
+ try {
1718
+ const latest = await deps.fetchLatest(entry.source, flags, s.name);
1719
+ if (!latest) {
1720
+ failed.push(s.name);
1721
+ lines.push(pc11.red(`${GLYPH.blocked} ${label2} no longer available upstream (${entry.source})`));
1722
+ continue;
1723
+ }
1724
+ if (entry.source.startsWith("registry:") && latest.commit_sha && entry.commit_sha && latest.commit_sha === entry.commit_sha) {
1725
+ lines.push(pc11.dim(`- ${label2} up to date`));
1726
+ continue;
1727
+ }
1728
+ if (digestOf(latest.files) === entry.digest) {
1729
+ lines.push(pc11.dim(`- ${label2} up to date`));
1730
+ continue;
1731
+ }
1732
+ if (flags.check) {
1733
+ available.push(s.name);
1734
+ lines.push(pc11.yellow(`\u2191 ${label2} update available`));
1735
+ continue;
1736
+ }
1737
+ const md = latest.files.find((f) => f.path === "SKILL.md");
1738
+ const raw = md ? typeof md.contents === "string" ? md.contents : md.contents.toString("utf8") : "";
1739
+ const result = lint3(raw, { slug: s.name });
1740
+ if (!result.ok) {
1741
+ failed.push(s.name);
1742
+ 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`));
1743
+ continue;
1744
+ }
1745
+ 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" });
1746
+ updated.push(s.name);
1747
+ lines.push(pc11.green(`${GLYPH.ok} ${label2} updated`));
1748
+ } catch (e) {
1749
+ failed.push(s.name);
1750
+ lines.push(pc11.red(`${GLYPH.blocked} ${label2} ${e instanceof Error ? e.message : String(e)}`));
1751
+ }
1431
1752
  }
1432
1753
  }
1433
- return { updated, output: lines.join("\n") || "Nothing to update." };
1754
+ for (const n of names) {
1755
+ if (seen.has(n)) continue;
1756
+ failed.push(n);
1757
+ lines.push(pc11.yellow(`- ${n} not installed`));
1758
+ }
1759
+ const exitCode = failed.length ? 1 : 0;
1760
+ 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) };
1761
+ if (lines.length === 0) lines.push(pc11.dim("No installed skills found."));
1762
+ return { updated, available, failed, exitCode, output: lines.join("\n") };
1434
1763
  }
1435
- async function findRegistrySlug(api, s) {
1436
- const r = await api(`/api/search?q=${encodeURIComponent(s.name)}&limit=5`).catch(() => null);
1437
- const hit = r?.items?.find((it) => it.slug.split("/")[1] === s.name);
1438
- return hit?.slug ?? null;
1764
+ function scopeOptions(cmd) {
1765
+ return cmd.option("-g, --global", "only user-level skills").option("-p, --project", "only this project's skills").option("--json", "machine-readable output");
1439
1766
  }
1440
1767
  function updateCommand() {
1441
- 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) => {
1442
- const merged = { ...cmd.parent?.opts(), ...opts };
1443
- const run = await runUpdate(names, merged, { global: opts.global });
1444
- console.log(run.output);
1768
+ 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) => {
1769
+ const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts });
1770
+ console.log(run2.output);
1771
+ process.exitCode = run2.exitCode;
1772
+ });
1773
+ }
1774
+ function checkCommand() {
1775
+ 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) => {
1776
+ const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts, check: true });
1777
+ console.log(run2.output);
1778
+ process.exitCode = run2.exitCode;
1445
1779
  });
1446
1780
  }
1447
1781
 
1448
- // ../cli/src/commands/publish.ts
1782
+ // src/commands/publish.ts
1449
1783
  import { Command as Command11 } from "commander";
1450
- import pc11 from "picocolors";
1784
+ import pc12 from "picocolors";
1785
+ import { lint as lint4, toText } from "@skillmds/core";
1451
1786
  var defaultDeps2 = {
1452
1787
  resolve: resolveSource,
1453
1788
  async submit(payload, flags) {
1454
1789
  const { api, hasToken } = createClient(flags);
1455
- if (!hasToken) throw new Error("No token. Run `skillmd login` or pass --token.");
1790
+ if (!hasToken) throw new Error("No token. Run `npx get-skill login` or pass --token.");
1456
1791
  return api(`/api/skills`, {
1457
1792
  method: "POST",
1458
1793
  headers: { "content-type": "application/json" },
@@ -1463,78 +1798,114 @@ var defaultDeps2 = {
1463
1798
  async function runPublish(target, flags, deps = defaultDeps2) {
1464
1799
  const sources = await deps.resolve(target);
1465
1800
  if (sources.length !== 1) {
1466
- return { result: lint(""), published: false, exitCode: 1, output: `Expected exactly one SKILL.md, found ${sources.length}.` };
1801
+ return { result: lint4(""), published: false, exitCode: 1, output: `Expected exactly one SKILL.md, found ${sources.length}.` };
1467
1802
  }
1468
1803
  const src = sources[0];
1469
- const result = lint(src.raw, { slug: src.slug });
1804
+ const result = lint4(src.raw, { slug: src.slug });
1470
1805
  const report = toText([{ file: src.path, result }], { decorate });
1471
1806
  if (!result.ok) {
1472
1807
  return { result, published: false, exitCode: 1, output: `${report}
1473
- ${pc11.red("\u2717 Refusing to publish: fix the errors above.")}` };
1808
+ ${pc12.red("\u2717 Refusing to publish: fix the errors above.")}` };
1474
1809
  }
1475
1810
  const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
1476
1811
  if (hasWarn && !flags.force) {
1477
1812
  return { result, published: false, exitCode: 1, output: `${report}
1478
- ${pc11.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
1813
+ ${pc12.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
1479
1814
  }
1480
1815
  if (flags.dryRun) {
1481
1816
  return { result, published: false, exitCode: 0, output: `${report}
1482
- ${pc11.green("\u2713 Dry run: would publish.")}` };
1817
+ ${pc12.green("\u2713 Dry run: would publish.")}` };
1483
1818
  }
1484
1819
  const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
1485
1820
  return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
1486
- ${pc11.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
1821
+ ${pc12.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
1487
1822
  }
1488
1823
  function publishCommand() {
1489
1824
  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) => {
1490
1825
  const merged = { ...cmd.parent?.opts(), ...opts };
1491
- const run = await runPublish(target, merged);
1492
- console.log(run.output);
1493
- process.exitCode = run.exitCode;
1826
+ const run2 = await runPublish(target, merged);
1827
+ console.log(run2.output);
1828
+ process.exitCode = run2.exitCode;
1494
1829
  });
1495
1830
  }
1496
1831
 
1497
- // ../cli/src/commands/login.ts
1832
+ // src/commands/login.ts
1498
1833
  import { Command as Command12 } from "commander";
1499
- import * as p4 from "@clack/prompts";
1500
- function loginCommand() {
1501
- 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) => {
1502
- let token = opts.token;
1503
- if (!token) {
1504
- const answer = await p4.password({ message: "Paste your SkillMD token (from your account page)" });
1505
- if (p4.isCancel(answer)) {
1506
- p4.cancel("Cancelled.");
1507
- return;
1508
- }
1509
- token = answer;
1510
- }
1511
- if (!token) {
1512
- console.error("A token is required.");
1513
- process.exitCode = 1;
1514
- return;
1834
+ import * as p5 from "@clack/prompts";
1835
+ import pc13 from "picocolors";
1836
+ var defaultLoginDeps = (flags) => promptIf(flags, async () => {
1837
+ const answer = await p5.password({ message: "Paste your SkillMD token (from your account page)" });
1838
+ return p5.isCancel(answer) ? null : answer;
1839
+ });
1840
+ function finish4(flags, result, doc) {
1841
+ return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
1842
+ }
1843
+ async function runLogin(flags, deps = defaultLoginDeps(flags)) {
1844
+ const env = flags.env ?? process.env;
1845
+ let token = flags.token;
1846
+ if (!token) {
1847
+ if (!deps.prompt) {
1848
+ const error = nonInteractiveHint("--token <token> (or set SKILLMD_TOKEN)");
1849
+ return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
1515
1850
  }
1516
- const cfg = readConfig();
1517
- cfg.token = token;
1518
- if (opts.api) cfg.api = opts.api;
1519
- writeConfig(cfg);
1520
- console.log(`Saved token to ${configPath()}`);
1851
+ const answer = await deps.prompt();
1852
+ if (answer === null) return finish4(flags, { exitCode: 0, output: pc13.dim("Cancelled.") }, { ok: false, cancelled: true });
1853
+ token = answer;
1854
+ }
1855
+ if (!token) {
1856
+ const error = "A token is required.";
1857
+ return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
1858
+ }
1859
+ const cfg = readConfig(flags.home);
1860
+ let host;
1861
+ try {
1862
+ host = hostOf(resolveApi({ api: flags.api, insecureHttp: flags.insecureHttp }, { env, config: { ...cfg, api: flags.api ?? cfg.api } }));
1863
+ } catch (e) {
1864
+ const msg2 = e instanceof Error ? e.message : String(e);
1865
+ return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg2 }, null, 2) : pc13.red(msg2) };
1866
+ }
1867
+ cfg.token = token;
1868
+ if (flags.api) cfg.api = flags.api;
1869
+ cfg.tokenHost = host;
1870
+ writeConfig(cfg, flags.home);
1871
+ const file = configPath(flags.home);
1872
+ return {
1873
+ exitCode: 0,
1874
+ output: flags.json ? JSON.stringify({ ok: true, file, host }, null, 2) : `Saved token for ${host} to ${file}`
1875
+ };
1876
+ }
1877
+ function runLogout(flags) {
1878
+ const cfg = readConfig(flags.home);
1879
+ const had = Boolean(cfg.token);
1880
+ delete cfg.token;
1881
+ delete cfg.tokenHost;
1882
+ writeConfig(cfg, flags.home);
1883
+ return {
1884
+ exitCode: 0,
1885
+ output: flags.json ? JSON.stringify({ ok: true, removed: had }, null, 2) : had ? "Removed stored token." : "No stored token."
1886
+ };
1887
+ }
1888
+ function loginCommand() {
1889
+ 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) => {
1890
+ const r = await runLogin({ ...cmd.parent?.opts(), ...opts });
1891
+ console.log(r.output);
1892
+ process.exitCode = r.exitCode;
1521
1893
  });
1522
1894
  }
1523
1895
  function logoutCommand() {
1524
- return new Command12("logout").description("Remove the stored SkillMD token").action(() => {
1525
- const cfg = readConfig();
1526
- delete cfg.token;
1527
- writeConfig(cfg);
1528
- console.log("Removed stored token.");
1896
+ return new Command12("logout").description("Remove the stored SkillMD token").option("--json", "machine-readable output").action((opts, cmd) => {
1897
+ const r = runLogout({ ...cmd.parent?.opts(), ...opts });
1898
+ console.log(r.output);
1899
+ process.exitCode = r.exitCode;
1529
1900
  });
1530
1901
  }
1531
1902
 
1532
- // ../cli/src/commands/interactive.ts
1533
- import * as p5 from "@clack/prompts";
1534
- import pc12 from "picocolors";
1903
+ // src/commands/interactive.ts
1904
+ import * as p6 from "@clack/prompts";
1905
+ import pc14 from "picocolors";
1535
1906
  function bail(v) {
1536
- if (p5.isCancel(v)) {
1537
- p5.cancel("Cancelled.");
1907
+ if (p6.isCancel(v)) {
1908
+ p6.cancel("Cancelled.");
1538
1909
  return true;
1539
1910
  }
1540
1911
  return false;
@@ -1542,25 +1913,26 @@ function bail(v) {
1542
1913
  async function browseSearchResults(items) {
1543
1914
  const cols = process.stdout.columns ?? 80;
1544
1915
  for (; ; ) {
1545
- const picked = await p5.select({
1916
+ const picked = await p6.select({
1546
1917
  message: "Search results",
1547
1918
  options: [
1548
1919
  ...items.map((item2) => {
1549
1920
  const starsTxt = item2.repo_stars != null ? ` \u2605${formatStars(item2.repo_stars)}` : "";
1550
1921
  const checkTxt = item2.verified ? " \u2713" : "";
1551
- const plainLabel = `${item2.slug}${starsTxt}${checkTxt}`;
1552
- const label2 = `${pc12.bold(item2.slug)}${starsTxt ? pc12.dim(starsTxt) : ""}${checkTxt ? pc12.green(checkTxt) : ""}`;
1922
+ const slugTxt = safeText(item2.slug, 200);
1923
+ const plainLabel = `${slugTxt}${starsTxt}${checkTxt}`;
1924
+ const label2 = `${pc14.bold(slugTxt)}${starsTxt ? pc14.dim(starsTxt) : ""}${checkTxt ? pc14.green(checkTxt) : ""}`;
1553
1925
  const hintWidth = Math.max(10, cols - plainLabel.length - 6);
1554
1926
  return {
1555
1927
  value: item2.slug,
1556
1928
  label: label2,
1557
- hint: truncate(item2.description ?? "", hintWidth)
1929
+ hint: truncate(safeText(item2.description ?? ""), hintWidth)
1558
1930
  };
1559
1931
  }),
1560
1932
  { value: "__back", label: "\u2190 Back to menu" }
1561
1933
  ]
1562
1934
  });
1563
- if (p5.isCancel(picked) || picked === "__back") return;
1935
+ if (p6.isCancel(picked) || picked === "__back") return;
1564
1936
  const item = items.find((i) => i.slug === picked);
1565
1937
  if (!item) continue;
1566
1938
  await browseOneResult(item);
@@ -1568,14 +1940,15 @@ async function browseSearchResults(items) {
1568
1940
  }
1569
1941
  async function browseOneResult(item) {
1570
1942
  const starsTxt = item.repo_stars != null ? `\u2605 ${formatStars(item.repo_stars)} stars` : "";
1943
+ const slugTxt = safeText(item.slug, 200);
1571
1944
  const meta = [
1572
1945
  starsTxt,
1573
- item.verified ? pc12.green("\u2713 verified") : "unverified",
1946
+ item.verified ? pc14.green("\u2713 verified") : "unverified",
1574
1947
  item.type === "pack" ? "pack" : ""
1575
1948
  ].filter(Boolean).join(" \xB7 ");
1576
- p5.note([item.description ?? "", pc12.dim(meta)].join("\n"), item.slug);
1949
+ p6.note([safeText(item.description ?? ""), pc14.dim(meta)].join("\n"), slugTxt);
1577
1950
  for (; ; ) {
1578
- const action = await p5.select({
1951
+ const action = await p6.select({
1579
1952
  message: "What next?",
1580
1953
  options: [
1581
1954
  { value: "project", label: "Install to this project" },
@@ -1584,33 +1957,36 @@ async function browseOneResult(item) {
1584
1957
  { value: "back", label: "\u2190 Back to results" }
1585
1958
  ]
1586
1959
  });
1587
- if (p5.isCancel(action) || action === "back") return;
1960
+ if (p6.isCancel(action) || action === "back") return;
1588
1961
  if (action === "url") {
1589
- p5.log.message(pc12.dim(`https://skillmd.com/skills/${item.slug}`));
1962
+ p6.log.message(pc14.dim(`https://skillmd.com/skills/${slugTxt}`));
1590
1963
  continue;
1591
1964
  }
1592
- p5.log.step(`Installing ${item.slug}\u2026`);
1965
+ p6.log.step(`Installing ${slugTxt}\u2026`);
1593
1966
  try {
1594
- const run = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
1595
- console.log(run.output);
1596
- if (run.exitCode === 0) p5.log.success("Installed.");
1597
- else p5.log.warn("Not installed.");
1967
+ const run2 = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
1968
+ console.log(run2.output);
1969
+ if (run2.exitCode === 0) p6.log.success("Installed.");
1970
+ else p6.log.warn("Not installed.");
1598
1971
  } catch (err) {
1599
- p5.log.error(err instanceof Error ? err.message : String(err));
1972
+ p6.log.error(err instanceof Error ? err.message : String(err));
1600
1973
  }
1601
1974
  }
1602
1975
  }
1603
1976
  async function runInteractive() {
1604
- p5.intro(pc12.bgCyan(pc12.black(" skillmd ")));
1977
+ p6.intro(banner());
1605
1978
  for (; ; ) {
1606
- const action = await p5.select({
1979
+ const action = await p6.select({
1607
1980
  message: "What would you like to do?",
1608
1981
  options: [
1609
1982
  { value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
1610
1983
  { value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
1611
1984
  { value: "search", label: "Search the registry" },
1985
+ { value: "install", label: "Install a skill", hint: "registry slug, GitHub URL or local path" },
1612
1986
  { value: "init", label: "Create a new skill" },
1613
1987
  { value: "list", label: "List installed skills" },
1988
+ { value: "update", label: "Update installed skills" },
1989
+ { value: "remove", label: "Remove skills" },
1614
1990
  { value: "help", label: "Show all commands" },
1615
1991
  { value: "quit", label: "Quit" }
1616
1992
  ]
@@ -1619,85 +1995,108 @@ async function runInteractive() {
1619
1995
  switch (action) {
1620
1996
  case "lint":
1621
1997
  case "scan": {
1622
- const path = await p5.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1998
+ const path = await p6.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1623
1999
  if (bail(path)) break;
1624
2000
  const target = String(path || ".");
1625
- const s = p5.spinner();
2001
+ const s = p6.spinner();
1626
2002
  s.start(action === "lint" ? "Linting" : "Scanning");
1627
2003
  try {
1628
- const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
2004
+ const run2 = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1629
2005
  s.stop("Done");
1630
- console.log(run.output);
1631
- process.exitCode = run.exitCode;
2006
+ console.log(run2.output);
2007
+ process.exitCode = run2.exitCode;
1632
2008
  } catch (err) {
1633
- s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
2009
+ s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
1634
2010
  process.exitCode = 1;
1635
2011
  }
1636
2012
  break;
1637
2013
  }
1638
2014
  case "search": {
1639
- const query = await p5.text({ message: "Search the registry for" });
2015
+ const query = await p6.text({ message: "Search the registry for" });
1640
2016
  if (bail(query)) break;
1641
- const verified = await p5.confirm({ message: "Verified skills only?", initialValue: false });
2017
+ const verified = await p6.confirm({ message: "Verified skills only?", initialValue: false });
1642
2018
  if (bail(verified)) break;
1643
- const s = p5.spinner();
2019
+ const s = p6.spinner();
1644
2020
  s.start("Searching");
1645
2021
  try {
1646
- const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
1647
- s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
1648
- if (run.items.length === 0) {
1649
- p5.log.info("No results.");
2022
+ const run2 = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
2023
+ s.stop(`${run2.items.length} result${run2.items.length === 1 ? "" : "s"}`);
2024
+ if (run2.items.length === 0) {
2025
+ p6.log.info("No results.");
1650
2026
  break;
1651
2027
  }
1652
- await browseSearchResults(run.items);
2028
+ await browseSearchResults(run2.items);
1653
2029
  } catch (err) {
1654
- s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
2030
+ s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
1655
2031
  process.exitCode = 1;
1656
2032
  }
1657
2033
  break;
1658
2034
  }
1659
2035
  case "init": {
1660
- const name = await p5.text({ message: "Skill name", placeholder: "my-skill" });
2036
+ const name = await p6.text({ message: "Skill name", placeholder: "my-skill" });
1661
2037
  if (bail(name)) break;
1662
- const description = await p5.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
2038
+ const description = await p6.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
1663
2039
  if (bail(description)) break;
1664
2040
  try {
1665
2041
  const file = initSkill(String(name), { description: String(description || "") });
1666
- p5.log.success(`Created ${pc12.bold(file)}`);
2042
+ p6.log.success(`Created ${pc14.bold(file)}`);
1667
2043
  } catch (err) {
1668
- p5.log.error(err instanceof Error ? err.message : String(err));
2044
+ p6.log.error(err instanceof Error ? err.message : String(err));
2045
+ process.exitCode = 1;
2046
+ }
2047
+ break;
2048
+ }
2049
+ case "install": {
2050
+ const src = await p6.text({ message: "Skill source", placeholder: "owner/name, https://github.com/\u2026, or ./path" });
2051
+ if (bail(src)) break;
2052
+ try {
2053
+ const run2 = await runAdd(String(src), {});
2054
+ console.log(run2.output);
2055
+ process.exitCode = run2.exitCode;
2056
+ } catch (err) {
2057
+ p6.log.error(err instanceof Error ? err.message : String(err));
1669
2058
  process.exitCode = 1;
1670
2059
  }
1671
2060
  break;
1672
2061
  }
1673
2062
  case "list": {
1674
- const skills = [...installedSkills({}), ...installedSkills({ global: true })];
1675
- if (skills.length === 0) {
1676
- p5.log.info("No installed skills found (checked this folder and your user directory).");
1677
- } else {
1678
- p5.log.message(
1679
- skills.map((sk) => {
1680
- const parsed = parseSkillMd(sk.raw);
1681
- const desc = "error" in parsed ? pc12.red(`(invalid SKILL.md \u2014 ${parsed.error})`) : pc12.dim(parsed.description.slice(0, 70));
1682
- return `${pc12.bold(sk.name)} ${pc12.dim(`[${sk.agent}/${sk.scope}]`)}
1683
- ${desc}`;
1684
- }).join("\n")
1685
- );
2063
+ console.log(runList({}).output);
2064
+ break;
2065
+ }
2066
+ case "update": {
2067
+ try {
2068
+ const run2 = await runUpdate([], {});
2069
+ console.log(run2.output);
2070
+ process.exitCode = run2.exitCode;
2071
+ } catch (err) {
2072
+ p6.log.error(err instanceof Error ? err.message : String(err));
2073
+ process.exitCode = 1;
2074
+ }
2075
+ break;
2076
+ }
2077
+ case "remove": {
2078
+ try {
2079
+ const run2 = await runRemove([], {});
2080
+ console.log(run2.output);
2081
+ process.exitCode = run2.exitCode;
2082
+ } catch (err) {
2083
+ p6.log.error(err instanceof Error ? err.message : String(err));
2084
+ process.exitCode = 1;
1686
2085
  }
1687
2086
  break;
1688
2087
  }
1689
2088
  case "help": {
1690
- p5.note(
2089
+ p6.note(
1691
2090
  [
1692
- `${pc12.bold("lint")} [path] validate SKILL.md + quality score`,
1693
- `${pc12.bold("scan")} [path] security scan`,
1694
- `${pc12.bold("search")} <query> search the registry`,
1695
- `${pc12.bold("add")} <source> install a skill (lints first)`,
1696
- `${pc12.bold("info")} <slug> registry details for a skill`,
1697
- `${pc12.bold("init")} [name] scaffold a new skill`,
1698
- `${pc12.bold("list / remove / update")} manage installed skills`,
1699
- `${pc12.bold("publish")} [path] publish (fails on lint errors)`,
1700
- `${pc12.bold("rules / login")}`
2091
+ `${pc14.bold("lint")} [path] validate SKILL.md + quality score`,
2092
+ `${pc14.bold("scan")} [path] security scan`,
2093
+ `${pc14.bold("search")} <query> search the registry`,
2094
+ `${pc14.bold("add")} <source> install a skill (lints first)`,
2095
+ `${pc14.bold("info")} <slug> registry details for a skill`,
2096
+ `${pc14.bold("init")} [name] scaffold a new skill`,
2097
+ `${pc14.bold("list / remove / update")} manage installed skills`,
2098
+ `${pc14.bold("publish")} [path] publish (fails on lint errors)`,
2099
+ `${pc14.bold("rules / login")}`
1701
2100
  ].join("\n"),
1702
2101
  "Commands (run: skillmd <command>)"
1703
2102
  );
@@ -1705,16 +2104,39 @@ async function runInteractive() {
1705
2104
  }
1706
2105
  }
1707
2106
  }
1708
- p5.outro(pc12.dim("skillmd.com"));
2107
+ p6.outro(pc14.dim("skillmd.com"));
1709
2108
  }
1710
2109
 
1711
- // ../cli/src/index.ts
2110
+ // src/index.ts
1712
2111
  var version = "0.0.0";
1713
2112
  try {
1714
- version = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
2113
+ version = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
1715
2114
  } catch {
1716
2115
  }
1717
- 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");
2116
+ var program = new Command13("get-skill").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 `npx get-skill 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)");
2117
+ program.configureHelp({ sortSubcommands: false });
2118
+ program.addHelpText("after", `
2119
+ Examples:
2120
+ $ npx get-skill add anthropic/pdf install from the registry (asks project vs global)
2121
+ $ npx get-skill add owner/repo/skills/x#main -g a GitHub subfolder at a ref, into your user dirs
2122
+ $ npx get-skill add ./my-skill -a claude-code -p a local skill into this project for one agent
2123
+ $ npx get-skill add anthropic/pdf -y --json non-interactive, machine-readable
2124
+ $ npx get-skill list everything installed, project and global
2125
+ $ npx get-skill check which installed skills have updates
2126
+ $ npx get-skill update -g update your user-level skills
2127
+ $ npx get-skill remove pdf remove a skill from every agent
2128
+
2129
+ Quality: lint scan rules init
2130
+ Registry: search info add list remove update check
2131
+ Publish: publish login logout
2132
+
2133
+ Docs: https://skillmd.com/docs/cli`);
2134
+ program.hook("preAction", (thisCmd) => {
2135
+ const opts = thisCmd.opts();
2136
+ if (opts.token && process.stderr.isTTY) {
2137
+ console.error(pc15.yellow("\u26A0 --token is visible in your shell history and process list; prefer `npx get-skill login` or SKILLMD_TOKEN."));
2138
+ }
2139
+ });
1718
2140
  program.addCommand(lintCommand());
1719
2141
  program.addCommand(scanCommand());
1720
2142
  program.addCommand(rulesCommand());
@@ -1725,6 +2147,7 @@ program.addCommand(addCommand());
1725
2147
  program.addCommand(listCommand());
1726
2148
  program.addCommand(removeCommand());
1727
2149
  program.addCommand(updateCommand());
2150
+ program.addCommand(checkCommand());
1728
2151
  program.addCommand(publishCommand());
1729
2152
  program.addCommand(loginCommand());
1730
2153
  program.addCommand(logoutCommand());