get-skill 1.0.21 → 1.3.8

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,219 @@ 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 }));
1177
- }
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 }];
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
+ const missing = [];
924
+ const bundle = await fetchBundle(client.base, registrySlug, client.token, { missing });
925
+ if (bundle && bundle.length) {
926
+ const sk = bundle.find((file) => file.path === "SKILL.md");
927
+ const note2 = bundleNote(bundle, missing);
928
+ return {
929
+ ...meta,
930
+ type: bundle.length > 1 ? "pack" : meta.type,
931
+ raw: sk ? sk.contents.toString("utf8") : skillMdFor(skill),
932
+ files: bundle,
933
+ ...note2 ? { note: note2 } : {}
934
+ };
935
+ }
936
+ if (skill.type === "pack") {
937
+ const pack = await resolvePackFiles(skill);
938
+ if (pack) return { ...meta, ...pack };
939
+ }
940
+ return { ...meta, raw: skillMdFor(skill) };
941
+ }
942
+ async function fetchPackMembers(api, owner, slug) {
943
+ const PAGE = 200;
944
+ const members = [];
945
+ let name = slug;
946
+ let total = Infinity;
947
+ for (let offset = 0; members.length < total; offset += PAGE) {
948
+ const page = await api(`/api/packs/${owner}/${slug}?limit=${PAGE}&offset=${offset}`);
949
+ name = page.name || name;
950
+ total = Number(page.item_total ?? 0);
951
+ const got = (page.items ?? []).map((i) => ({ slug: String(i.slug ?? ""), type: i.type, verified: Boolean(i.verified) })).filter((i) => i.slug);
952
+ if (!got.length) break;
953
+ members.push(...got);
954
+ }
955
+ return { name, members };
956
+ }
957
+ function packDirNames(slugs) {
958
+ const used = /* @__PURE__ */ new Set();
959
+ return slugs.map((slug) => {
960
+ const [owner, name] = slug.split("/");
961
+ let dir = name ?? slug;
962
+ if (used.has(dir)) dir = `${owner}-${name}`;
963
+ used.add(dir);
964
+ return dir;
965
+ });
966
+ }
967
+ async function fetchPackArchive(base, owner, slug, token, doFetch = fetch) {
968
+ const headers = { accept: "application/zip" };
969
+ if (token) headers.authorization = `Bearer ${token}`;
970
+ const res = await doFetch(`${base}/api/packs/${owner}/${slug}/download`, { headers });
971
+ if (!res.ok) throw new RegistryError(`SkillMD API ${res.status} on /api/packs/${owner}/${slug}/download`, res.status);
972
+ const bytes = new Uint8Array(await res.arrayBuffer());
973
+ const { unzipSync, strFromU8 } = await import("fflate");
974
+ const entries = unzipSync(bytes);
975
+ const out = /* @__PURE__ */ new Map();
976
+ for (const [path, data] of Object.entries(entries)) {
977
+ const m = /^(.+)\/SKILL\.md$/.exec(path);
978
+ if (m) out.set(m[1], strFromU8(data));
979
+ }
980
+ return out;
981
+ }
982
+ function makeDefaultDeps(flags) {
983
+ const canPrompt = () => isInteractive(flags, {
984
+ stdinTTY: Boolean(process.stdin.isTTY),
985
+ stdoutTTY: Boolean(process.stdout.isTTY),
986
+ env: flags.env ?? process.env
987
+ });
988
+ return {
989
+ async resolve(arg, f) {
990
+ const spec = parseSource(arg, { ref: f.ref });
991
+ const source = sourceId(spec);
992
+ if (spec.kind === "plugin") {
993
+ const { api: api2, base: apiBase2, token: token2, fetch: doFetch } = createClient(f);
994
+ const { name: packName, members } = await fetchPackMembers(api2, spec.owner, spec.slug);
995
+ if (!members.length) throw new Error(`plugin ${spec.owner}/${spec.slug} has no installable skills`);
996
+ const archive = await fetchPackArchive(apiBase2, spec.owner, spec.slug, token2, doFetch);
997
+ const dirs = packDirNames(members.map((m) => m.slug));
998
+ const out = [];
999
+ const missing = [];
1000
+ members.forEach((m, i) => {
1001
+ const raw = archive.get(dirs[i]);
1002
+ if (!raw) {
1003
+ missing.push(m.slug);
1004
+ return;
1205
1005
  }
1206
- const pack = await resolvePackFiles(skill);
1207
- if (pack) return [{ ...meta, ...pack }];
1006
+ const name = dirs[i];
1007
+ out.push({
1008
+ name,
1009
+ slug: name,
1010
+ registrySlug: m.slug,
1011
+ source,
1012
+ raw,
1013
+ verified: m.verified,
1014
+ type: m.type
1015
+ });
1016
+ });
1017
+ const multi = out.filter((c) => c.type === "pack");
1018
+ if (multi.length) {
1019
+ let n = 0;
1020
+ await Promise.all(Array.from({ length: Math.min(4, multi.length) }, async () => {
1021
+ for (let i = n++; i < multi.length; i = n++) {
1022
+ const c = multi[i];
1023
+ try {
1024
+ const missing2 = [];
1025
+ const bundle = await fetchBundle(apiBase2, c.registrySlug, token2, { missing: missing2 });
1026
+ if (!bundle?.length) continue;
1027
+ const sk = bundle.find((file) => file.path === "SKILL.md");
1028
+ const note2 = bundleNote(bundle, missing2);
1029
+ c.files = bundle;
1030
+ if (sk) c.raw = sk.contents.toString("utf8");
1031
+ if (note2) c.note = note2;
1032
+ } catch {
1033
+ }
1034
+ }
1035
+ }));
1036
+ }
1037
+ if (!out.length) throw new Error(`plugin ${spec.owner}/${spec.slug} returned no readable skills`);
1038
+ const first = out[0];
1039
+ 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)` : "");
1040
+ return out;
1041
+ }
1042
+ if (spec.kind !== "slug") {
1043
+ const rs = await resolveSource(argWithRef(arg, f.ref));
1044
+ return rs.map((r) => ({
1045
+ name: r.slug,
1046
+ raw: r.raw,
1047
+ slug: r.slug,
1048
+ source,
1049
+ ...spec.kind === "local" && r.file ? { files: collectFiles(dirname2(r.file), dirname2(r.file)) } : {}
1050
+ }));
1051
+ }
1052
+ let registryDown = null;
1053
+ const { api, base: apiBase, token } = createClient(f);
1054
+ const registryHost = (() => {
1055
+ try {
1056
+ return new URL(apiBase).host;
1057
+ } catch {
1058
+ return "api.skillmd.com";
1208
1059
  }
1209
- return [{ ...meta, raw: skillMdFor(skill) }];
1060
+ })();
1061
+ const registrySlug = `${spec.owner}/${spec.name}`;
1062
+ try {
1063
+ return [await registryCandidate(spec.owner, spec.name, source, { api, base: apiBase, token })];
1210
1064
  } catch (e) {
1211
1065
  if (e instanceof IntegrityError) throw e;
1212
1066
  if (e instanceof RegistryError && e.status !== 404) registryDown = e;
1213
1067
  }
1068
+ const ghSource = sourceId({ kind: "github", owner: spec.owner, repo: spec.name, display: arg });
1069
+ try {
1070
+ const rs = await resolveSource(arg);
1071
+ const note2 = registryDown ? registryFallbackNote(registryHost) : void 0;
1072
+ return rs.map((r) => ({ name: r.slug, raw: r.raw, slug: r.slug, source: ghSource, note: note2 }));
1073
+ } catch (e) {
1074
+ if (registryDown) throw registryUnreachableError(arg, registryHost, registryDown);
1075
+ throw e;
1076
+ }
1077
+ },
1078
+ get promptScope() {
1079
+ return canPrompt() ? promptScopeInteractive : void 0;
1080
+ },
1081
+ get promptAgents() {
1082
+ return canPrompt() ? promptAgentsInteractive : void 0;
1083
+ },
1084
+ fireInstall(registrySlug, f) {
1085
+ const { api } = createClient(f);
1086
+ const [owner, name] = registrySlug.split("/");
1087
+ void api(`/api/skills/${owner}/${name}/install`, {
1088
+ method: "POST",
1089
+ headers: { "content-type": "application/json" },
1090
+ body: JSON.stringify({ via: "cli" })
1091
+ }).catch(() => {
1092
+ });
1214
1093
  }
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
- };
1094
+ };
1095
+ }
1241
1096
  async function resolveScope(flags, deps) {
1242
1097
  const home = resolve3(flags.home ?? homedir4());
1243
1098
  const cwd = resolve3(flags.cwd ?? process.cwd());
1244
- const base = { cwd: flags.cwd, home: flags.home };
1099
+ const base = { cwd: flags.cwd, home: flags.home, env: flags.env };
1245
1100
  if (flags.global) return { scope: { global: true, ...base } };
1246
1101
  if (flags.project) return { scope: { global: false, ...base } };
1247
1102
  if (cwd === home) return { scope: { global: true, ...base } };
1248
1103
  const isProject = looksLikeProject(cwd);
1249
- if (deps.promptScope && !flags.yes && !flags.json) {
1104
+ const hosted = detectHostAgent(flags.env ?? process.env) !== null;
1105
+ if (deps.promptScope && !flags.yes && !flags.json && !hosted) {
1250
1106
  const global = await deps.promptScope({ cwd, suggestGlobal: !isProject });
1251
1107
  if (global === null) return null;
1252
1108
  return { scope: { global, ...base } };
@@ -1257,202 +1113,682 @@ async function resolveScope(flags, deps) {
1257
1113
  note: `no project detected in ${cwd} \u2014 installing to your user-level agent folders (pass --project to install here)`
1258
1114
  };
1259
1115
  }
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) {
1116
+ function defaultNeedsPrompt(flags) {
1117
+ const env = flags.env ?? process.env;
1118
+ if (flags.yes || flags.json || flags.global || flags.project || flags.list) return false;
1119
+ if (detectHostAgent(env)) return false;
1120
+ if (resolve3(flags.cwd ?? process.cwd()) === resolve3(flags.home ?? homedir4())) return false;
1121
+ return !isInteractive(flags, { stdinTTY: Boolean(process.stdin.isTTY), stdoutTTY: Boolean(process.stdout.isTTY), env });
1122
+ }
1123
+ var CANCELLED_TEXT = "installation cancelled \u2014 nothing was installed";
1124
+ function finish2(flags, outcome) {
1125
+ const json = Boolean(flags.json);
1126
+ switch (outcome.kind) {
1127
+ case "install": {
1128
+ const installed = outcome.records.flatMap((r) => r.kind === "installed" ? [r.doc] : []);
1129
+ const blocked = outcome.records.flatMap((r) => r.kind === "blocked" ? [{ name: r.name, reason: r.reason }] : []);
1130
+ const written = installed.flatMap((d) => d.targets.map((t) => ({ name: d.name, dir: t.path })));
1131
+ const warnings = [
1132
+ ...outcome.notices,
1133
+ ...outcome.records.flatMap((r) => r.kind === "installed" ? r.notices : [])
1134
+ ].map((n) => n.text);
1135
+ const exitCode = blocked.length > 0 || installed.length === 0 && !outcome.managed ? 1 : 0;
1136
+ const doc = { ok: exitCode === 0, scope: outcome.scope, installed, blocked, warnings };
1137
+ return {
1138
+ written,
1139
+ blocked,
1140
+ exitCode,
1141
+ output: json ? JSON.stringify(doc, null, 2) : renderHuman(outcome.notices, outcome.records, outcome.paths),
1142
+ scope: outcome.scope,
1143
+ agents: [...new Set(installed.flatMap((d) => d.targets.map((t) => t.agent)))]
1144
+ };
1145
+ }
1146
+ case "list": {
1147
+ const human = outcome.skills.length ? outcome.skills.map((s) => `${pc8.bold(s.name)} ${pc8.dim(s.description)}`).join("\n") : "No skills found.";
1148
+ return { written: [], blocked: [], exitCode: 0, output: json ? JSON.stringify({ ok: true, skills: outcome.skills }, null, 2) : human };
1149
+ }
1150
+ case "cancel": {
1151
+ const doc = { ok: false, error: CANCELLED_TEXT };
1267
1152
  return {
1268
1153
  written: [],
1269
- blocked: [{ name: arg, reason: e.message }],
1270
- exitCode: 1,
1271
- output: pc7.red(`\u2717 ${arg} blocked \u2014 ${e.message}`)
1154
+ blocked: [],
1155
+ exitCode: 0,
1156
+ cancelled: true,
1157
+ output: json ? JSON.stringify(doc, null, 2) : pc8.dim("Installation cancelled \u2014 nothing was installed.")
1272
1158
  };
1273
1159
  }
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;
1160
+ case "fail": {
1161
+ const warnings = (outcome.notices ?? []).map((n) => n.text);
1162
+ const doc = { ok: false, error: outcome.error };
1163
+ if (outcome.blocked?.length) doc.blocked = outcome.blocked;
1164
+ if (warnings.length) doc.warnings = warnings;
1165
+ const human = [
1166
+ ...(outcome.notices ?? []).map(noticeLine),
1167
+ ...outcome.blocked?.length ? outcome.blocked.map((b) => pc8.red(`${GLYPH.blocked} ${b.name} blocked \u2014 ${b.reason}`)) : [pc8.red(outcome.error)]
1168
+ ].join("\n");
1169
+ return { written: [], blocked: outcome.blocked ?? [], exitCode: 1, output: json ? JSON.stringify(doc, null, 2) : human };
1309
1170
  }
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(", ")}`));
1171
+ }
1172
+ }
1173
+ function renderHuman(notices, records, paths) {
1174
+ const lines = notices.map(noticeLine);
1175
+ for (const r of records) {
1176
+ if (r.kind === "blocked") {
1177
+ lines.push(pc8.red(r.message));
1313
1178
  continue;
1314
1179
  }
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(", ")}`));
1180
+ const { doc } = r;
1181
+ lines.push(renderInstallSummary({
1182
+ name: doc.name,
1183
+ score: r.score,
1184
+ flags: r.securityFlags,
1185
+ source: r.source,
1186
+ canonical: doc.canonical,
1187
+ targets: doc.targets,
1188
+ skipped: doc.skipped,
1189
+ ...r.replaced ? { replaced: r.replaced } : {}
1190
+ }, paths));
1191
+ for (const n of r.notices) if (!n.suppressInSummary) lines.push(noticeLine(n));
1192
+ }
1193
+ return lines.join("\n");
1194
+ }
1195
+ async function resolveCandidates(arg, flags, deps) {
1196
+ const candidates = await deps.resolve(arg, flags);
1197
+ if (!flags.skill?.length) return candidates;
1198
+ return candidates.filter((c) => flags.skill.includes(c.name) || flags.skill.includes("*"));
1199
+ }
1200
+ function gate(flags, deps, candidates) {
1201
+ if (flags.list) {
1202
+ return finish2(flags, {
1203
+ kind: "list",
1204
+ skills: candidates.map((c) => {
1205
+ const parsed = parseSkillMd2(c.raw);
1206
+ const description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
1207
+ return { name: c.name, description: safeText(description, 80) };
1208
+ })
1209
+ });
1210
+ }
1211
+ if (flags.json && !flags.yes) {
1212
+ return finish2(flags, { kind: "fail", error: "--json requires -y (no prompts can be shown in JSON mode)" });
1213
+ }
1214
+ if (deps.needsPrompt ?? (!deps.promptScope && defaultNeedsPrompt(flags))) {
1215
+ return finish2(flags, { kind: "fail", error: nonInteractiveHint("-y (auto-detects the scope) or -g / -p") });
1216
+ }
1217
+ return null;
1218
+ }
1219
+ async function chooseTargets(flags, deps, scope, detected, managed) {
1220
+ const notices = [];
1221
+ let targets = flags.agent?.length ? flags.agent : detected.length ? detected : ["claude-code"];
1222
+ if (scope.global) {
1223
+ const kept = [];
1224
+ for (const id of targets) {
1225
+ if (agentSupportsGlobal(id)) kept.push(id);
1226
+ else notices.push(skipNotice(`${id}: project-only agent`));
1323
1227
  }
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`));
1228
+ targets = kept;
1229
+ }
1230
+ if (!flags.agent?.length && deps.promptAgents && !flags.yes && !flags.json && !detectHostAgent(flags.env ?? process.env)) {
1231
+ const picked = await deps.promptAgents({ detected: targets, dirFor: (id) => agentDir(id, scope), ...managed ? { managed } : {} });
1232
+ if (picked === null) return null;
1233
+ targets = picked;
1234
+ if (flags.home === void 0 && picked.length) {
1235
+ try {
1236
+ writeConfig({ ...readConfig(), lastAgents: picked });
1237
+ } catch {
1238
+ }
1326
1239
  }
1327
- if (c.note) {
1328
- lines.push(pc7.yellow(`\u26A0 ${c.name}: ${c.note}`));
1240
+ }
1241
+ return { targets, notices };
1242
+ }
1243
+ async function installOne(c, ctx) {
1244
+ const result = lint2(c.raw, { slug: c.slug });
1245
+ const securityFlags2 = [.../* @__PURE__ */ new Set([...result.security.flags, ...c.securityFlags ?? []])];
1246
+ const deniedFlags = securityFlags2.filter((f) => ctx.deny.has(f));
1247
+ if (!ctx.flags.skipLint && !result.ok) {
1248
+ return {
1249
+ kind: "blocked",
1250
+ name: c.name,
1251
+ reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}`,
1252
+ message: `${GLYPH.blocked} ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`
1253
+ };
1254
+ }
1255
+ if (deniedFlags.length) {
1256
+ return {
1257
+ kind: "blocked",
1258
+ name: c.name,
1259
+ reason: `denied security flags: ${deniedFlags.join(", ")}`,
1260
+ message: `${GLYPH.blocked} ${c.name} blocked \u2014 you passed --deny ${deniedFlags.join(", ")}`
1261
+ };
1262
+ }
1263
+ const source = c.source ?? (c.registrySlug ? `registry:${c.registrySlug}` : provenanceFor(ctx.arg));
1264
+ let out;
1265
+ try {
1266
+ out = await installSkill({
1267
+ name: c.name,
1268
+ files: c.files ?? [{ path: "SKILL.md", contents: c.raw }],
1269
+ source,
1270
+ commit_sha: c.commit_sha,
1271
+ scope: ctx.scope,
1272
+ agents: ctx.targets,
1273
+ explicitAgents: Boolean(ctx.flags.agent?.length),
1274
+ mode: ctx.flags.mode,
1275
+ force: ctx.flags.force
1276
+ });
1277
+ } catch (e) {
1278
+ const reason = e instanceof Error ? e.message : String(e);
1279
+ return { kind: "blocked", name: c.name, reason, message: `${GLYPH.blocked} ${c.name} \u2014 ${reason}` };
1280
+ }
1281
+ const notices = [];
1282
+ if (out.replaced) notices.push(replacedNotice(`${c.name}: replaced previous install (${out.replaced})`));
1283
+ if (c.pinMiss) notices.push(warnNotice(`${c.name}: pinned commit unavailable upstream \u2014 fetched the source repo's default branch instead`));
1284
+ if (c.note) notices.push(warnNotice(`${c.name}: ${c.note}`));
1285
+ if (c.type === "pack" && !c.files) {
1286
+ 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.`));
1287
+ }
1288
+ if (c.registrySlug && !telemetryDisabled(ctx.env)) ctx.deps.fireInstall(c.registrySlug, ctx.flags);
1289
+ return {
1290
+ kind: "installed",
1291
+ doc: { name: c.name, canonical: out.canonical, targets: out.targets, skipped: out.skipped, digest: out.digest },
1292
+ score: result.score,
1293
+ securityFlags: securityFlags2,
1294
+ notices,
1295
+ source,
1296
+ ...out.replaced ? { replaced: out.replaced } : {}
1297
+ };
1298
+ }
1299
+ async function tryPluginInstall(arg, flags) {
1300
+ let spec;
1301
+ try {
1302
+ spec = parseSource(arg, { ref: flags.ref });
1303
+ } catch {
1304
+ return null;
1305
+ }
1306
+ if (spec.kind !== "plugin") return null;
1307
+ if (flags.agent?.length || flags.skill?.length || flags.list || flags.mode === "copy") return null;
1308
+ const viaCli = await claudeCliAvailable() ? await installClaudePlugin(spec.owner, spec.slug) : null;
1309
+ const res = viaCli?.ok ? viaCli : await installPluginNatively(spec.owner, spec.slug, { home: flags.home });
1310
+ if (!res.ok) {
1311
+ if (!viaCli && /no Claude Code directory/.test(res.reason)) return null;
1312
+ return { ok: false, reason: viaCli && !viaCli.ok ? viaCli.reason : res.reason };
1313
+ }
1314
+ return {
1315
+ ok: true,
1316
+ agent: "claude-code",
1317
+ name: res.name,
1318
+ note: `installed ${res.name} as a Claude Code plugin \u2014 manage it with \`claude plugin\` or /plugin`
1319
+ };
1320
+ }
1321
+ async function runAdd(arg, flags, deps) {
1322
+ const d = deps ?? makeDefaultDeps(flags);
1323
+ const env = flags.env ?? process.env;
1324
+ const attempt = await tryPluginInstall(arg, flags);
1325
+ const managed = attempt?.ok ? attempt : null;
1326
+ 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`)];
1327
+ let scopeOnce;
1328
+ const scopeOf = async () => scopeOnce ??= await resolveScope(flags, d);
1329
+ if (managed) {
1330
+ const pre = await scopeOf();
1331
+ if (!pre) return finish2(flags, { kind: "cancel" });
1332
+ const others = detectAgents(pre.scope).filter((id) => id !== managed.agent);
1333
+ if (!others.length) {
1334
+ const paths2 = { home: flags.home ?? homedir4(), cwd: flags.cwd ?? process.cwd() };
1335
+ return finish2(flags, {
1336
+ kind: "install",
1337
+ scope: pre.scope.global ? "global" : "project",
1338
+ records: [],
1339
+ notices: [...pre.note ? [infoNotice(pre.note)] : [], ...managedNotices],
1340
+ paths: paths2,
1341
+ managed: managed.name
1342
+ });
1329
1343
  }
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.`));
1344
+ }
1345
+ let candidates;
1346
+ try {
1347
+ candidates = await resolveCandidates(arg, flags, d);
1348
+ } catch (e) {
1349
+ if (e instanceof IntegrityError) {
1350
+ return finish2(flags, { kind: "fail", error: e.message, blocked: [{ name: arg, reason: e.message }] });
1332
1351
  }
1333
- if (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
1352
+ if (flags.json) return finish2(flags, { kind: "fail", error: e instanceof Error ? e.message : String(e) });
1353
+ throw e;
1334
1354
  }
1335
- const exitCode = written.length === 0 ? 1 : 0;
1336
- return { written, blocked, exitCode, output: lines.join("\n") };
1355
+ const early = gate(flags, d, candidates);
1356
+ if (early) return early;
1357
+ const resolved = await scopeOf();
1358
+ if (!resolved) return finish2(flags, { kind: "cancel" });
1359
+ const scope = resolved.scope;
1360
+ const notices = resolved.note ? [infoNotice(resolved.note)] : [];
1361
+ notices.push(...managedNotices);
1362
+ const detected = detectAgents(scope).filter((id) => id !== managed?.agent);
1363
+ const chosen = await chooseTargets(flags, d, scope, detected, managed?.name);
1364
+ if (!chosen) return finish2(flags, { kind: "cancel" });
1365
+ notices.push(...chosen.notices);
1366
+ const ctx = { arg, flags, deny: new Set(flags.deny ?? []), scope, targets: chosen.targets, env, deps: d };
1367
+ const records = [];
1368
+ for (const c of candidates) records.push(await installOne(c, ctx));
1369
+ const paths = { home: flags.home ?? homedir4(), cwd: flags.cwd ?? process.cwd() };
1370
+ return finish2(flags, { kind: "install", scope: scope.global ? "global" : "project", records, notices, paths, ...managed ? { managed: managed.name } : {} });
1371
+ }
1372
+ function addFlags(opts, parent) {
1373
+ const merged = { ...parent, ...opts };
1374
+ if (opts.copy) merged.mode = "copy";
1375
+ return merged;
1337
1376
  }
1338
1377
  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;
1378
+ 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) => {
1379
+ const merged = addFlags(opts, cmd.parent?.opts());
1380
+ const { run: run2, printed } = await runAddWithUI(source, merged);
1381
+ if (!printed) {
1382
+ if (run2.exitCode === 1 && !merged.json) console.error(run2.output);
1383
+ else console.log(run2.output);
1384
+ }
1385
+ process.exitCode = run2.exitCode;
1344
1386
  });
1345
1387
  }
1388
+ var clackUI = {
1389
+ intro: (m) => p3.intro(m),
1390
+ spinner: () => p3.spinner(),
1391
+ message: (m) => p3.log.message(m),
1392
+ outro: (m) => p3.outro(m),
1393
+ cancel: (m) => p3.cancel(m),
1394
+ confirm: async (m) => {
1395
+ const answer = await p3.confirm({ message: m, initialValue: true });
1396
+ return p3.isCancel(answer) ? null : answer;
1397
+ }
1398
+ };
1399
+ var FIND_SKILLS = "skillmd/find-skills";
1400
+ async function offerFindSkills(source, flags, run2, ui, deps) {
1401
+ if (!ui.confirm || run2.exitCode !== 0 || !run2.written.length || !run2.scope || !run2.agents?.length) return;
1402
+ if (source.replace(/^registry:/, "").toLowerCase() === FIND_SKILLS) return;
1403
+ const cfg = readConfig(flags.home);
1404
+ if (cfg.findSkillsOffered) return;
1405
+ const global = run2.scope === "global";
1406
+ const lock = readLock({ global, cwd: flags.cwd, home: flags.home });
1407
+ if (Object.values(lock.skills).some((e) => e.source === `registry:${FIND_SKILLS}`)) return;
1408
+ try {
1409
+ writeConfig({ ...cfg, findSkillsOffered: true }, flags.home);
1410
+ } catch {
1411
+ }
1412
+ const yes = await ui.confirm("Also add find-skills, so your agent can look for more skills on SkillMD when a task needs one?");
1413
+ if (!yes) return;
1414
+ const r = await runAdd(FIND_SKILLS, {
1415
+ ...flags,
1416
+ yes: true,
1417
+ json: false,
1418
+ skill: void 0,
1419
+ list: void 0,
1420
+ ref: void 0,
1421
+ global,
1422
+ project: !global,
1423
+ agent: run2.agents
1424
+ }, deps);
1425
+ 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}`));
1426
+ }
1427
+ async function runAddWithUI(source, flags, opts = {}) {
1428
+ const term = opts.term ?? { ...realTerminal(), env: flags.env ?? process.env };
1429
+ const base = opts.deps ?? makeDefaultDeps(flags);
1430
+ if (!isInteractive(flags, term)) return { run: await runAdd(source, flags, base), printed: false };
1431
+ const ui = opts.ui ?? clackUI;
1432
+ ui.intro(banner());
1433
+ try {
1434
+ const run2 = await runAdd(source, flags, {
1435
+ // NOTE: spreading `base` reads makeDefaultDeps()'s promptScope/promptAgents
1436
+ // getters right here, freezing their values for this run. That is exactly
1437
+ // what we want (interactivity is decided once, up front), but it does mean
1438
+ // a getter added to AddDeps later stops being lazy the moment it crosses
1439
+ // this spread.
1440
+ ...base,
1441
+ resolve: async (arg, f) => {
1442
+ const s = ui.spinner();
1443
+ s.start(`Resolving ${arg}`);
1444
+ try {
1445
+ const c = await base.resolve(arg, f);
1446
+ s.stop(`${c.length} skill${c.length === 1 ? "" : "s"} found`);
1447
+ return c;
1448
+ } catch (e) {
1449
+ s.stop(pc8.red("Could not resolve the source"));
1450
+ throw e;
1451
+ }
1452
+ }
1453
+ });
1454
+ if (run2.cancelled) {
1455
+ ui.cancel("Installation cancelled \u2014 nothing was installed.");
1456
+ return { run: run2, printed: true };
1457
+ }
1458
+ ui.message(run2.output);
1459
+ await offerFindSkills(source, flags, run2, ui, base);
1460
+ ui.outro(run2.exitCode === 0 ? `Done. Run ${pc8.cyan("npx get-skill list")} to see everything installed.` : pc8.red("Some skills were not installed."));
1461
+ return { run: run2, printed: true };
1462
+ } catch (e) {
1463
+ ui.outro(pc8.red("Nothing was installed."));
1464
+ throw e;
1465
+ }
1466
+ }
1346
1467
 
1347
- // ../cli/src/commands/list.ts
1468
+ // src/commands/list.ts
1348
1469
  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}`);
1470
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1471
+ import { join as join4 } from "path";
1472
+ import pc9 from "picocolors";
1473
+ import { parseSkillMd as parseSkillMd3 } from "@skillmds/core";
1474
+ function runList(flags = {}) {
1475
+ const scopes = scopesFor(flags);
1476
+ let items = [];
1477
+ for (const global of scopes) {
1478
+ for (const s of listInstalled({ global, cwd: flags.cwd, home: flags.home })) {
1479
+ const md = join4(s.path, "SKILL.md");
1480
+ let description = "";
1481
+ if (existsSync3(md)) {
1482
+ const parsed = parseSkillMd3(readFileSync2(md, "utf8"));
1483
+ description = "error" in parsed ? `(invalid SKILL.md \u2014 ${parsed.error})` : parsed.description;
1484
+ }
1485
+ items.push({ ...s, description: safeText(description, 80) });
1367
1486
  }
1487
+ }
1488
+ if (flags.agent?.length) items = items.filter((i) => i.agents.some((a) => flags.agent.includes(a)));
1489
+ items.sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
1490
+ if (flags.json) {
1491
+ 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) };
1492
+ }
1493
+ if (items.length === 0) return { items, output: pc9.dim("No installed skills found. Install one with `npx get-skill add owner/name`.") };
1494
+ const w = Math.max(...items.map((i) => i.name.length), 4);
1495
+ const lines = items.map((i) => {
1496
+ const tag = i.tracked ? pc9.dim(i.source ?? "") : pc9.yellow("untracked");
1497
+ const modes = [...new Set(Object.values(i.mode))].join("/");
1498
+ return `${pc9.bold(i.name.padEnd(w))} ${pc9.cyan(i.scope.padEnd(7))} ${i.agents.join(", ").padEnd(24)} ${pc9.dim(modes.padEnd(9))} ${tag}
1499
+ ${" ".repeat(w + 2)}${pc9.dim(i.description)}`;
1500
+ });
1501
+ return { items, output: lines.join("\n") };
1502
+ }
1503
+ function listCommand() {
1504
+ 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) => {
1505
+ console.log(runList({ ...cmd.parent?.opts(), ...opts }).output);
1368
1506
  });
1369
1507
  }
1370
1508
 
1371
- // ../cli/src/commands/remove.ts
1509
+ // src/commands/remove.ts
1372
1510
  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 = [];
1511
+ import { rmSync } from "fs";
1512
+ import { join as join5, resolve as resolve4 } from "path";
1513
+ import * as p4 from "@clack/prompts";
1514
+ import pc10 from "picocolors";
1515
+ var defaultDeps = (flags) => isInteractive(flags) ? {
1516
+ confirm: async (msg2) => {
1517
+ const a = await p4.confirm({ message: msg2, initialValue: false });
1518
+ return !p4.isCancel(a) && a;
1519
+ },
1520
+ pick: async ({ names }) => {
1521
+ const a = await p4.multiselect({ message: "Which skills do you want to remove?", options: names.map((n) => ({ value: n, label: n })), required: true });
1522
+ return p4.isCancel(a) ? null : a;
1523
+ }
1524
+ } : {};
1525
+ function finish3(flags, result, doc) {
1526
+ return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
1527
+ }
1528
+ var scopeLabel = (scope) => scope.global ? "global" : "project";
1529
+ async function runRemove(names, flags, deps = defaultDeps(flags)) {
1530
+ const scopes = scopesFor(flags).map((global) => ({ global, cwd: flags.cwd, home: flags.home }));
1531
+ const where = scopes.map(scopeLabel).join(" and ");
1379
1532
  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})`));
1533
+ if (flags.all && names.length) {
1534
+ const error = "--all cannot be combined with skill names";
1535
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
1384
1536
  }
1385
- for (const n of names) {
1386
- if (!targets.some((t) => t.name === n)) lines.push(pc9.yellow(`- ${n} not installed`));
1537
+ const perScope = scopes.map((scope) => ({ scope, installed: listInstalled(scope) }));
1538
+ const allNames = [...new Set(perScope.flatMap((s) => s.installed.map((i) => i.name)))];
1539
+ const nothingInstalled = `No installed skills found in ${where} scope.`;
1540
+ let targets = names;
1541
+ if (flags.all) {
1542
+ if (allNames.length === 0) {
1543
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
1544
+ }
1545
+ targets = allNames;
1546
+ } else if (targets.length === 0) {
1547
+ if (!deps.pick) {
1548
+ const error = "Give one or more skill names, or --all.";
1549
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(`${error} `) + pc10.dim(nonInteractiveHint("skill names and -y")) }, { ok: false, error });
1550
+ }
1551
+ if (allNames.length === 0) {
1552
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.dim(nothingInstalled) }, { ok: false, error: nothingInstalled });
1553
+ }
1554
+ const picked = await deps.pick({ names: allNames });
1555
+ if (!picked) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
1556
+ targets = picked;
1387
1557
  }
1388
- return { removed, output: lines.join("\n") };
1558
+ const known = targets.filter((n) => allNames.includes(n));
1559
+ const missing = targets.filter((n) => !known.includes(n));
1560
+ for (const n of missing) lines.push(pc10.yellow(`- ${n} not installed (${where} scope)`));
1561
+ if (known.length === 0) {
1562
+ const output = lines.join("\n");
1563
+ return finish3(flags, { removed: [], exitCode: 1, output }, { ok: false, error: `not installed: ${missing.join(", ")}`, missing });
1564
+ }
1565
+ if (!flags.yes) {
1566
+ const what = flags.agent?.length ? `from ${flags.agent.join(", ")}` : "from every agent";
1567
+ const ok = deps.confirm ? await deps.confirm(`Remove ${known.length} skill${known.length === 1 ? "" : "s"} (${known.join(", ")}) ${what} in ${where} scope?`) : null;
1568
+ if (ok === null) {
1569
+ const error = nonInteractiveHint("-y");
1570
+ return finish3(flags, { removed: [], exitCode: 1, output: pc10.red(error) }, { ok: false, error });
1571
+ }
1572
+ if (!ok) return finish3(flags, { removed: [], exitCode: 0, cancelled: true, output: pc10.dim("Nothing removed.") }, { ok: true, removed: [], cancelled: true });
1573
+ }
1574
+ const removed = [];
1575
+ const skipped = [];
1576
+ let changed = 0;
1577
+ for (const name of known) {
1578
+ for (const { scope, installed } of perScope) {
1579
+ if (!installed.some((s) => s.name === name)) continue;
1580
+ const tag = scopes.length > 1 ? pc10.dim(` [${scopeLabel(scope)}]`) : "";
1581
+ if (flags.agent?.length) {
1582
+ const lock = readLock({ global: Boolean(scope.global), cwd: scope.cwd, home: scope.home });
1583
+ const entry = lock.skills[name];
1584
+ const canonRoot = resolve4(canonicalDir(scope));
1585
+ let acted = false;
1586
+ for (const a of flags.agent) {
1587
+ let dir;
1588
+ try {
1589
+ dir = agentDir(a, scope);
1590
+ } catch (e) {
1591
+ lines.push(pc10.yellow(`- ${a}: ${e instanceof Error ? e.message : String(e)}`));
1592
+ continue;
1593
+ }
1594
+ if (resolve4(dir) === canonRoot || entry?.mode[a] === "canonical") {
1595
+ 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)`));
1596
+ skipped.push({ skill: name, agent: a, scope: scopeLabel(scope), reason: "reads the canonical copy directly" });
1597
+ acted = true;
1598
+ continue;
1599
+ }
1600
+ const pth = join5(dir, name);
1601
+ if (isSkillDirOrLink(pth)) {
1602
+ rmSync(pth, { recursive: true, force: true });
1603
+ removed.push(pth);
1604
+ acted = true;
1605
+ lines.push(pc10.green(`${GLYPH.ok} removed ${name} from ${a}`) + tag);
1606
+ }
1607
+ }
1608
+ if (!acted) lines.push(pc10.yellow(`- ${name} is not linked into ${flags.agent.join(", ")}`) + tag);
1609
+ if (entry) {
1610
+ const left = entry.agents.filter((a) => !flags.agent.includes(a));
1611
+ 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))) });
1612
+ else {
1613
+ const r2 = uninstallSkill(name, scope);
1614
+ removed.push(...r2.removed);
1615
+ lines.push(pc10.dim(` (no agents left \u2014 removed the canonical copy too)`));
1616
+ }
1617
+ }
1618
+ if (acted) changed++;
1619
+ continue;
1620
+ }
1621
+ const r = uninstallSkill(name, scope);
1622
+ removed.push(...r.removed);
1623
+ changed++;
1624
+ lines.push(pc10.green(`${GLYPH.ok} removed ${name}`) + tag + pc10.dim(` (${r.removed.length} path${r.removed.length === 1 ? "" : "s"}${r.untracked ? ", untracked" : ""})`));
1625
+ }
1626
+ }
1627
+ const exitCode = changed ? 0 : 1;
1628
+ const doc = { ok: exitCode === 0, removed, missing };
1629
+ if (skipped.length) doc.skipped = skipped;
1630
+ return finish3(flags, { removed, exitCode, output: lines.join("\n") }, doc);
1389
1631
  }
1390
1632
  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);
1633
+ 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) => {
1634
+ const run2 = await runRemove(names, { ...cmd.parent?.opts(), ...opts });
1635
+ console.log(run2.output);
1636
+ process.exitCode = run2.exitCode;
1394
1637
  });
1395
1638
  }
1396
1639
 
1397
- // ../cli/src/commands/update.ts
1640
+ // src/commands/update.ts
1398
1641
  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));
1642
+ import pc11 from "picocolors";
1643
+ import { lint as lint3 } from "@skillmds/core";
1644
+ function pickSkillTree(files, want) {
1645
+ const norm = files.map((f) => ({ path: f.path.replace(/\\/g, "/"), contents: f.contents }));
1646
+ const match = norm.find((f) => f.path.endsWith("/SKILL.md") && f.path.split("/").at(-2) === want);
1647
+ if (match) {
1648
+ const prefix = match.path.slice(0, -"SKILL.md".length);
1649
+ return norm.filter((f) => f.path.startsWith(prefix)).map((f) => ({ path: f.path.slice(prefix.length), contents: f.contents }));
1650
+ }
1651
+ if (norm.some((f) => f.path === "SKILL.md")) return norm;
1652
+ return null;
1653
+ }
1654
+ function makeDefaultUpdateDeps(opts = {}) {
1655
+ const tree = opts.resolveTree ?? ((input) => resolveTree(input));
1656
+ const one = opts.resolveSource ?? ((url) => resolveSource(url));
1657
+ const client = opts.client ?? createClient;
1658
+ return {
1659
+ async fetchLatest(source, flags, name) {
1660
+ if (source.startsWith("registry:")) {
1661
+ const slug = source.slice("registry:".length);
1662
+ const { api, base, token } = client(flags);
1663
+ let skill;
1664
+ try {
1665
+ skill = await api(`/api/skills/${slug}`);
1666
+ } catch (e) {
1667
+ if (e.status === 404) return null;
1668
+ throw e;
1669
+ }
1670
+ const bundle = await fetchBundle(base, slug, token);
1671
+ if (bundle?.length) return { files: bundle, commit_sha: skill.commit_sha ?? void 0 };
1672
+ if (skill.type === "pack") {
1673
+ const pack = await resolvePackFiles(skill);
1674
+ if (pack) return { files: pack.files, commit_sha: skill.commit_sha ?? void 0 };
1675
+ }
1676
+ return { files: [{ path: "SKILL.md", contents: skillMdFor(skill) }], commit_sha: skill.commit_sha ?? void 0 };
1677
+ }
1678
+ if (source.startsWith("github:")) {
1679
+ const spec = parseSource(source);
1680
+ if (spec.kind !== "github") return null;
1681
+ const files = await tree(gigetInput(spec));
1682
+ const picked = pickSkillTree(files, spec.skill ?? name);
1683
+ return picked ? { files: picked } : null;
1684
+ }
1685
+ if (source.startsWith("gist:")) {
1686
+ const [user, id] = source.slice("gist:".length).split("/");
1687
+ const rs = await one(`https://gist.github.com/${user}/${id}`);
1688
+ return rs[0] ? { files: [{ path: "SKILL.md", contents: rs[0].raw }] } : null;
1689
+ }
1690
+ return null;
1691
+ }
1692
+ };
1693
+ }
1694
+ var defaultUpdateDeps = makeDefaultUpdateDeps();
1695
+ async function runUpdate(names, flags, deps = defaultUpdateDeps) {
1696
+ const scopes = scopesFor(flags);
1405
1697
  const updated = [];
1698
+ const available = [];
1699
+ const failed = [];
1406
1700
  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`));
1701
+ const seen = /* @__PURE__ */ new Set();
1702
+ for (const global of scopes) {
1703
+ const scope = { global, cwd: flags.cwd, home: flags.home };
1704
+ const lock = readLock(scope);
1705
+ const installed = listInstalled(scope).filter((s) => names.length === 0 || names.includes(s.name));
1706
+ for (const s of installed) {
1707
+ seen.add(s.name);
1708
+ const label2 = `${s.name}${scopes.length > 1 ? pc11.dim(` [${s.scope}]`) : ""}`;
1709
+ const entry = lock.skills[s.name];
1710
+ if (!entry) {
1711
+ lines.push(pc11.dim(`- ${label2} untracked \u2014 reinstall with \`npx get-skill add\` to track updates`));
1419
1712
  continue;
1420
1713
  }
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`));
1714
+ if (entry.source.startsWith("local:")) {
1715
+ lines.push(pc11.dim(`- ${label2} local source \u2014 not auto-updated`));
1424
1716
  continue;
1425
1717
  }
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`));
1718
+ try {
1719
+ const latest = await deps.fetchLatest(entry.source, flags, s.name);
1720
+ if (!latest) {
1721
+ failed.push(s.name);
1722
+ lines.push(pc11.red(`${GLYPH.blocked} ${label2} no longer available upstream (${entry.source})`));
1723
+ continue;
1724
+ }
1725
+ if (entry.source.startsWith("registry:") && latest.commit_sha && entry.commit_sha && latest.commit_sha === entry.commit_sha) {
1726
+ lines.push(pc11.dim(`- ${label2} up to date`));
1727
+ continue;
1728
+ }
1729
+ if (digestOf(latest.files) === entry.digest) {
1730
+ lines.push(pc11.dim(`- ${label2} up to date`));
1731
+ continue;
1732
+ }
1733
+ if (flags.check) {
1734
+ available.push(s.name);
1735
+ lines.push(pc11.yellow(`\u2191 ${label2} update available`));
1736
+ continue;
1737
+ }
1738
+ const md = latest.files.find((f) => f.path === "SKILL.md");
1739
+ const raw = md ? typeof md.contents === "string" ? md.contents : md.contents.toString("utf8") : "";
1740
+ const result = lint3(raw, { slug: s.name });
1741
+ if (!result.ok) {
1742
+ failed.push(s.name);
1743
+ 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`));
1744
+ continue;
1745
+ }
1746
+ 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" });
1747
+ updated.push(s.name);
1748
+ lines.push(pc11.green(`${GLYPH.ok} ${label2} updated`));
1749
+ } catch (e) {
1750
+ failed.push(s.name);
1751
+ lines.push(pc11.red(`${GLYPH.blocked} ${label2} ${e instanceof Error ? e.message : String(e)}`));
1752
+ }
1431
1753
  }
1432
1754
  }
1433
- return { updated, output: lines.join("\n") || "Nothing to update." };
1755
+ for (const n of names) {
1756
+ if (seen.has(n)) continue;
1757
+ failed.push(n);
1758
+ lines.push(pc11.yellow(`- ${n} not installed`));
1759
+ }
1760
+ const exitCode = failed.length ? 1 : 0;
1761
+ 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) };
1762
+ if (lines.length === 0) lines.push(pc11.dim("No installed skills found."));
1763
+ return { updated, available, failed, exitCode, output: lines.join("\n") };
1434
1764
  }
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;
1765
+ function scopeOptions(cmd) {
1766
+ return cmd.option("-g, --global", "only user-level skills").option("-p, --project", "only this project's skills").option("--json", "machine-readable output");
1439
1767
  }
1440
1768
  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);
1769
+ 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) => {
1770
+ const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts });
1771
+ console.log(run2.output);
1772
+ process.exitCode = run2.exitCode;
1773
+ });
1774
+ }
1775
+ function checkCommand() {
1776
+ 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) => {
1777
+ const run2 = await runUpdate(names, { ...cmd.parent?.opts(), ...opts, check: true });
1778
+ console.log(run2.output);
1779
+ process.exitCode = run2.exitCode;
1445
1780
  });
1446
1781
  }
1447
1782
 
1448
- // ../cli/src/commands/publish.ts
1783
+ // src/commands/publish.ts
1449
1784
  import { Command as Command11 } from "commander";
1450
- import pc11 from "picocolors";
1785
+ import pc12 from "picocolors";
1786
+ import { lint as lint4, toText } from "@skillmds/core";
1451
1787
  var defaultDeps2 = {
1452
1788
  resolve: resolveSource,
1453
1789
  async submit(payload, flags) {
1454
1790
  const { api, hasToken } = createClient(flags);
1455
- if (!hasToken) throw new Error("No token. Run `skillmd login` or pass --token.");
1791
+ if (!hasToken) throw new Error("No token. Run `npx get-skill login` or pass --token.");
1456
1792
  return api(`/api/skills`, {
1457
1793
  method: "POST",
1458
1794
  headers: { "content-type": "application/json" },
@@ -1463,78 +1799,114 @@ var defaultDeps2 = {
1463
1799
  async function runPublish(target, flags, deps = defaultDeps2) {
1464
1800
  const sources = await deps.resolve(target);
1465
1801
  if (sources.length !== 1) {
1466
- return { result: lint(""), published: false, exitCode: 1, output: `Expected exactly one SKILL.md, found ${sources.length}.` };
1802
+ return { result: lint4(""), published: false, exitCode: 1, output: `Expected exactly one SKILL.md, found ${sources.length}.` };
1467
1803
  }
1468
1804
  const src = sources[0];
1469
- const result = lint(src.raw, { slug: src.slug });
1805
+ const result = lint4(src.raw, { slug: src.slug });
1470
1806
  const report = toText([{ file: src.path, result }], { decorate });
1471
1807
  if (!result.ok) {
1472
1808
  return { result, published: false, exitCode: 1, output: `${report}
1473
- ${pc11.red("\u2717 Refusing to publish: fix the errors above.")}` };
1809
+ ${pc12.red("\u2717 Refusing to publish: fix the errors above.")}` };
1474
1810
  }
1475
1811
  const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
1476
1812
  if (hasWarn && !flags.force) {
1477
1813
  return { result, published: false, exitCode: 1, output: `${report}
1478
- ${pc11.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
1814
+ ${pc12.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
1479
1815
  }
1480
1816
  if (flags.dryRun) {
1481
1817
  return { result, published: false, exitCode: 0, output: `${report}
1482
- ${pc11.green("\u2713 Dry run: would publish.")}` };
1818
+ ${pc12.green("\u2713 Dry run: would publish.")}` };
1483
1819
  }
1484
1820
  const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
1485
1821
  return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
1486
- ${pc11.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
1822
+ ${pc12.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
1487
1823
  }
1488
1824
  function publishCommand() {
1489
1825
  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
1826
  const merged = { ...cmd.parent?.opts(), ...opts };
1491
- const run = await runPublish(target, merged);
1492
- console.log(run.output);
1493
- process.exitCode = run.exitCode;
1827
+ const run2 = await runPublish(target, merged);
1828
+ console.log(run2.output);
1829
+ process.exitCode = run2.exitCode;
1494
1830
  });
1495
1831
  }
1496
1832
 
1497
- // ../cli/src/commands/login.ts
1833
+ // src/commands/login.ts
1498
1834
  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;
1835
+ import * as p5 from "@clack/prompts";
1836
+ import pc13 from "picocolors";
1837
+ var defaultLoginDeps = (flags) => promptIf(flags, async () => {
1838
+ const answer = await p5.password({ message: "Paste your SkillMD token (from your account page)" });
1839
+ return p5.isCancel(answer) ? null : answer;
1840
+ });
1841
+ function finish4(flags, result, doc) {
1842
+ return flags.json ? { ...result, output: JSON.stringify(doc, null, 2) } : result;
1843
+ }
1844
+ async function runLogin(flags, deps = defaultLoginDeps(flags)) {
1845
+ const env = flags.env ?? process.env;
1846
+ let token = flags.token;
1847
+ if (!token) {
1848
+ if (!deps.prompt) {
1849
+ const error = nonInteractiveHint("--token <token> (or set SKILLMD_TOKEN)");
1850
+ return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
1515
1851
  }
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()}`);
1852
+ const answer = await deps.prompt();
1853
+ if (answer === null) return finish4(flags, { exitCode: 0, output: pc13.dim("Cancelled.") }, { ok: false, cancelled: true });
1854
+ token = answer;
1855
+ }
1856
+ if (!token) {
1857
+ const error = "A token is required.";
1858
+ return finish4(flags, { exitCode: 1, output: pc13.red(error) }, { ok: false, error });
1859
+ }
1860
+ const cfg = readConfig(flags.home);
1861
+ let host;
1862
+ try {
1863
+ host = hostOf(resolveApi({ api: flags.api, insecureHttp: flags.insecureHttp }, { env, config: { ...cfg, api: flags.api ?? cfg.api } }));
1864
+ } catch (e) {
1865
+ const msg2 = e instanceof Error ? e.message : String(e);
1866
+ return { exitCode: 1, output: flags.json ? JSON.stringify({ ok: false, error: msg2 }, null, 2) : pc13.red(msg2) };
1867
+ }
1868
+ cfg.token = token;
1869
+ if (flags.api) cfg.api = flags.api;
1870
+ cfg.tokenHost = host;
1871
+ writeConfig(cfg, flags.home);
1872
+ const file = configPath(flags.home);
1873
+ return {
1874
+ exitCode: 0,
1875
+ output: flags.json ? JSON.stringify({ ok: true, file, host }, null, 2) : `Saved token for ${host} to ${file}`
1876
+ };
1877
+ }
1878
+ function runLogout(flags) {
1879
+ const cfg = readConfig(flags.home);
1880
+ const had = Boolean(cfg.token);
1881
+ delete cfg.token;
1882
+ delete cfg.tokenHost;
1883
+ writeConfig(cfg, flags.home);
1884
+ return {
1885
+ exitCode: 0,
1886
+ output: flags.json ? JSON.stringify({ ok: true, removed: had }, null, 2) : had ? "Removed stored token." : "No stored token."
1887
+ };
1888
+ }
1889
+ function loginCommand() {
1890
+ 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) => {
1891
+ const r = await runLogin({ ...cmd.parent?.opts(), ...opts });
1892
+ console.log(r.output);
1893
+ process.exitCode = r.exitCode;
1521
1894
  });
1522
1895
  }
1523
1896
  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.");
1897
+ return new Command12("logout").description("Remove the stored SkillMD token").option("--json", "machine-readable output").action((opts, cmd) => {
1898
+ const r = runLogout({ ...cmd.parent?.opts(), ...opts });
1899
+ console.log(r.output);
1900
+ process.exitCode = r.exitCode;
1529
1901
  });
1530
1902
  }
1531
1903
 
1532
- // ../cli/src/commands/interactive.ts
1533
- import * as p5 from "@clack/prompts";
1534
- import pc12 from "picocolors";
1904
+ // src/commands/interactive.ts
1905
+ import * as p6 from "@clack/prompts";
1906
+ import pc14 from "picocolors";
1535
1907
  function bail(v) {
1536
- if (p5.isCancel(v)) {
1537
- p5.cancel("Cancelled.");
1908
+ if (p6.isCancel(v)) {
1909
+ p6.cancel("Cancelled.");
1538
1910
  return true;
1539
1911
  }
1540
1912
  return false;
@@ -1542,25 +1914,26 @@ function bail(v) {
1542
1914
  async function browseSearchResults(items) {
1543
1915
  const cols = process.stdout.columns ?? 80;
1544
1916
  for (; ; ) {
1545
- const picked = await p5.select({
1917
+ const picked = await p6.select({
1546
1918
  message: "Search results",
1547
1919
  options: [
1548
1920
  ...items.map((item2) => {
1549
1921
  const starsTxt = item2.repo_stars != null ? ` \u2605${formatStars(item2.repo_stars)}` : "";
1550
1922
  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) : ""}`;
1923
+ const slugTxt = safeText(item2.slug, 200);
1924
+ const plainLabel = `${slugTxt}${starsTxt}${checkTxt}`;
1925
+ const label2 = `${pc14.bold(slugTxt)}${starsTxt ? pc14.dim(starsTxt) : ""}${checkTxt ? pc14.green(checkTxt) : ""}`;
1553
1926
  const hintWidth = Math.max(10, cols - plainLabel.length - 6);
1554
1927
  return {
1555
1928
  value: item2.slug,
1556
1929
  label: label2,
1557
- hint: truncate(item2.description ?? "", hintWidth)
1930
+ hint: truncate(safeText(item2.description ?? ""), hintWidth)
1558
1931
  };
1559
1932
  }),
1560
1933
  { value: "__back", label: "\u2190 Back to menu" }
1561
1934
  ]
1562
1935
  });
1563
- if (p5.isCancel(picked) || picked === "__back") return;
1936
+ if (p6.isCancel(picked) || picked === "__back") return;
1564
1937
  const item = items.find((i) => i.slug === picked);
1565
1938
  if (!item) continue;
1566
1939
  await browseOneResult(item);
@@ -1568,14 +1941,15 @@ async function browseSearchResults(items) {
1568
1941
  }
1569
1942
  async function browseOneResult(item) {
1570
1943
  const starsTxt = item.repo_stars != null ? `\u2605 ${formatStars(item.repo_stars)} stars` : "";
1944
+ const slugTxt = safeText(item.slug, 200);
1571
1945
  const meta = [
1572
1946
  starsTxt,
1573
- item.verified ? pc12.green("\u2713 verified") : "unverified",
1947
+ item.verified ? pc14.green("\u2713 verified") : "unverified",
1574
1948
  item.type === "pack" ? "pack" : ""
1575
1949
  ].filter(Boolean).join(" \xB7 ");
1576
- p5.note([item.description ?? "", pc12.dim(meta)].join("\n"), item.slug);
1950
+ p6.note([safeText(item.description ?? ""), pc14.dim(meta)].join("\n"), slugTxt);
1577
1951
  for (; ; ) {
1578
- const action = await p5.select({
1952
+ const action = await p6.select({
1579
1953
  message: "What next?",
1580
1954
  options: [
1581
1955
  { value: "project", label: "Install to this project" },
@@ -1584,33 +1958,36 @@ async function browseOneResult(item) {
1584
1958
  { value: "back", label: "\u2190 Back to results" }
1585
1959
  ]
1586
1960
  });
1587
- if (p5.isCancel(action) || action === "back") return;
1961
+ if (p6.isCancel(action) || action === "back") return;
1588
1962
  if (action === "url") {
1589
- p5.log.message(pc12.dim(`https://skillmd.com/skills/${item.slug}`));
1963
+ p6.log.message(pc14.dim(`https://skillmd.com/skills/${slugTxt}`));
1590
1964
  continue;
1591
1965
  }
1592
- p5.log.step(`Installing ${item.slug}\u2026`);
1966
+ p6.log.step(`Installing ${slugTxt}\u2026`);
1593
1967
  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.");
1968
+ const run2 = await runAdd(item.slug, action === "global" ? { global: true } : { project: true });
1969
+ console.log(run2.output);
1970
+ if (run2.exitCode === 0) p6.log.success("Installed.");
1971
+ else p6.log.warn("Not installed.");
1598
1972
  } catch (err) {
1599
- p5.log.error(err instanceof Error ? err.message : String(err));
1973
+ p6.log.error(err instanceof Error ? err.message : String(err));
1600
1974
  }
1601
1975
  }
1602
1976
  }
1603
1977
  async function runInteractive() {
1604
- p5.intro(pc12.bgCyan(pc12.black(" skillmd ")));
1978
+ p6.intro(banner());
1605
1979
  for (; ; ) {
1606
- const action = await p5.select({
1980
+ const action = await p6.select({
1607
1981
  message: "What would you like to do?",
1608
1982
  options: [
1609
1983
  { value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
1610
1984
  { value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
1611
1985
  { value: "search", label: "Search the registry" },
1986
+ { value: "install", label: "Install a skill", hint: "registry slug, GitHub URL or local path" },
1612
1987
  { value: "init", label: "Create a new skill" },
1613
1988
  { value: "list", label: "List installed skills" },
1989
+ { value: "update", label: "Update installed skills" },
1990
+ { value: "remove", label: "Remove skills" },
1614
1991
  { value: "help", label: "Show all commands" },
1615
1992
  { value: "quit", label: "Quit" }
1616
1993
  ]
@@ -1619,85 +1996,108 @@ async function runInteractive() {
1619
1996
  switch (action) {
1620
1997
  case "lint":
1621
1998
  case "scan": {
1622
- const path = await p5.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1999
+ const path = await p6.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
1623
2000
  if (bail(path)) break;
1624
2001
  const target = String(path || ".");
1625
- const s = p5.spinner();
2002
+ const s = p6.spinner();
1626
2003
  s.start(action === "lint" ? "Linting" : "Scanning");
1627
2004
  try {
1628
- const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
2005
+ const run2 = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
1629
2006
  s.stop("Done");
1630
- console.log(run.output);
1631
- process.exitCode = run.exitCode;
2007
+ console.log(run2.output);
2008
+ process.exitCode = run2.exitCode;
1632
2009
  } catch (err) {
1633
- s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
2010
+ s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
1634
2011
  process.exitCode = 1;
1635
2012
  }
1636
2013
  break;
1637
2014
  }
1638
2015
  case "search": {
1639
- const query = await p5.text({ message: "Search the registry for" });
2016
+ const query = await p6.text({ message: "Search the registry for" });
1640
2017
  if (bail(query)) break;
1641
- const verified = await p5.confirm({ message: "Verified skills only?", initialValue: false });
2018
+ const verified = await p6.confirm({ message: "Verified skills only?", initialValue: false });
1642
2019
  if (bail(verified)) break;
1643
- const s = p5.spinner();
2020
+ const s = p6.spinner();
1644
2021
  s.start("Searching");
1645
2022
  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.");
2023
+ const run2 = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
2024
+ s.stop(`${run2.items.length} result${run2.items.length === 1 ? "" : "s"}`);
2025
+ if (run2.items.length === 0) {
2026
+ p6.log.info("No results.");
1650
2027
  break;
1651
2028
  }
1652
- await browseSearchResults(run.items);
2029
+ await browseSearchResults(run2.items);
1653
2030
  } catch (err) {
1654
- s.stop(pc12.red(err instanceof Error ? err.message : String(err)));
2031
+ s.stop(pc14.red(err instanceof Error ? err.message : String(err)));
1655
2032
  process.exitCode = 1;
1656
2033
  }
1657
2034
  break;
1658
2035
  }
1659
2036
  case "init": {
1660
- const name = await p5.text({ message: "Skill name", placeholder: "my-skill" });
2037
+ const name = await p6.text({ message: "Skill name", placeholder: "my-skill" });
1661
2038
  if (bail(name)) break;
1662
- const description = await p5.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
2039
+ const description = await p6.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
1663
2040
  if (bail(description)) break;
1664
2041
  try {
1665
2042
  const file = initSkill(String(name), { description: String(description || "") });
1666
- p5.log.success(`Created ${pc12.bold(file)}`);
2043
+ p6.log.success(`Created ${pc14.bold(file)}`);
1667
2044
  } catch (err) {
1668
- p5.log.error(err instanceof Error ? err.message : String(err));
2045
+ p6.log.error(err instanceof Error ? err.message : String(err));
2046
+ process.exitCode = 1;
2047
+ }
2048
+ break;
2049
+ }
2050
+ case "install": {
2051
+ const src = await p6.text({ message: "Skill source", placeholder: "owner/name, https://github.com/\u2026, or ./path" });
2052
+ if (bail(src)) break;
2053
+ try {
2054
+ const run2 = await runAdd(String(src), {});
2055
+ console.log(run2.output);
2056
+ process.exitCode = run2.exitCode;
2057
+ } catch (err) {
2058
+ p6.log.error(err instanceof Error ? err.message : String(err));
1669
2059
  process.exitCode = 1;
1670
2060
  }
1671
2061
  break;
1672
2062
  }
1673
2063
  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
- );
2064
+ console.log(runList({}).output);
2065
+ break;
2066
+ }
2067
+ case "update": {
2068
+ try {
2069
+ const run2 = await runUpdate([], {});
2070
+ console.log(run2.output);
2071
+ process.exitCode = run2.exitCode;
2072
+ } catch (err) {
2073
+ p6.log.error(err instanceof Error ? err.message : String(err));
2074
+ process.exitCode = 1;
2075
+ }
2076
+ break;
2077
+ }
2078
+ case "remove": {
2079
+ try {
2080
+ const run2 = await runRemove([], {});
2081
+ console.log(run2.output);
2082
+ process.exitCode = run2.exitCode;
2083
+ } catch (err) {
2084
+ p6.log.error(err instanceof Error ? err.message : String(err));
2085
+ process.exitCode = 1;
1686
2086
  }
1687
2087
  break;
1688
2088
  }
1689
2089
  case "help": {
1690
- p5.note(
2090
+ p6.note(
1691
2091
  [
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")}`
2092
+ `${pc14.bold("lint")} [path] validate SKILL.md + quality score`,
2093
+ `${pc14.bold("scan")} [path] security scan`,
2094
+ `${pc14.bold("search")} <query> search the registry`,
2095
+ `${pc14.bold("add")} <source> install a skill (lints first)`,
2096
+ `${pc14.bold("info")} <slug> registry details for a skill`,
2097
+ `${pc14.bold("init")} [name] scaffold a new skill`,
2098
+ `${pc14.bold("list / remove / update")} manage installed skills`,
2099
+ `${pc14.bold("publish")} [path] publish (fails on lint errors)`,
2100
+ `${pc14.bold("rules / login")}`
1701
2101
  ].join("\n"),
1702
2102
  "Commands (run: skillmd <command>)"
1703
2103
  );
@@ -1705,16 +2105,39 @@ async function runInteractive() {
1705
2105
  }
1706
2106
  }
1707
2107
  }
1708
- p5.outro(pc12.dim("skillmd.com"));
2108
+ p6.outro(pc14.dim("skillmd.com"));
1709
2109
  }
1710
2110
 
1711
- // ../cli/src/index.ts
2111
+ // src/index.ts
1712
2112
  var version = "0.0.0";
1713
2113
  try {
1714
- version = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
2114
+ version = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
1715
2115
  } catch {
1716
2116
  }
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");
2117
+ 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)");
2118
+ program.configureHelp({ sortSubcommands: false });
2119
+ program.addHelpText("after", `
2120
+ Examples:
2121
+ $ npx get-skill add anthropic/pdf install from the registry (asks project vs global)
2122
+ $ npx get-skill add owner/repo/skills/x#main -g a GitHub subfolder at a ref, into your user dirs
2123
+ $ npx get-skill add ./my-skill -a claude-code -p a local skill into this project for one agent
2124
+ $ npx get-skill add anthropic/pdf -y --json non-interactive, machine-readable
2125
+ $ npx get-skill list everything installed, project and global
2126
+ $ npx get-skill check which installed skills have updates
2127
+ $ npx get-skill update -g update your user-level skills
2128
+ $ npx get-skill remove pdf remove a skill from every agent
2129
+
2130
+ Quality: lint scan rules init
2131
+ Registry: search info add list remove update check
2132
+ Publish: publish login logout
2133
+
2134
+ Docs: https://skillmd.com/docs/cli`);
2135
+ program.hook("preAction", (thisCmd) => {
2136
+ const opts = thisCmd.opts();
2137
+ if (opts.token && process.stderr.isTTY) {
2138
+ console.error(pc15.yellow("\u26A0 --token is visible in your shell history and process list; prefer `npx get-skill login` or SKILLMD_TOKEN."));
2139
+ }
2140
+ });
1718
2141
  program.addCommand(lintCommand());
1719
2142
  program.addCommand(scanCommand());
1720
2143
  program.addCommand(rulesCommand());
@@ -1725,6 +2148,7 @@ program.addCommand(addCommand());
1725
2148
  program.addCommand(listCommand());
1726
2149
  program.addCommand(removeCommand());
1727
2150
  program.addCommand(updateCommand());
2151
+ program.addCommand(checkCommand());
1728
2152
  program.addCommand(publishCommand());
1729
2153
  program.addCommand(loginCommand());
1730
2154
  program.addCommand(logoutCommand());