@ichintansoni/skills-master 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -4,15 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // package.json
7
- var version = "0.1.6";
8
-
9
- // src/types.ts
10
- var ALL_TARGETS = ["claude", "cursor", "copilot", "agents"];
11
- var RESOURCE_FILES = {
12
- reference: "reference.md",
13
- examples: "examples.md",
14
- checklist: "checklist.md"
15
- };
7
+ var version = "0.1.7";
16
8
 
17
9
  // src/schema/projectConfig.ts
18
10
  import { z } from "zod";
@@ -38,7 +30,8 @@ var ProjectConfigSchema = z.object({
38
30
  copilot: z.string(),
39
31
  agents: z.string()
40
32
  }).partial().default({}),
41
- scope: z.enum(["project"]).default("project"),
33
+ /** @deprecated accepted for configs written by older versions; never written. */
34
+ scope: z.enum(["project"]).optional(),
42
35
  /** true = commit generated files; false = add them to .gitignore. */
43
36
  commit: z.boolean().default(true)
44
37
  }).strict();
@@ -48,6 +41,32 @@ function resolvePaths(cfg) {
48
41
  return { ...DEFAULT_PATHS, ...cfg.paths };
49
42
  }
50
43
 
44
+ // src/types.ts
45
+ var ALL_TARGETS = ["claude", "cursor", "copilot", "agents"];
46
+ var RESOURCE_FILES = {
47
+ reference: "reference.md",
48
+ examples: "examples.md",
49
+ checklist: "checklist.md"
50
+ };
51
+
52
+ // src/core/stability-note.ts
53
+ function stabilityNote(stability, snapshotDate) {
54
+ switch (stability) {
55
+ case "stable":
56
+ return null;
57
+ case "emerging":
58
+ return `> **Emerging** \u2014 this covers an API that was pre-1.0, newly shipped, or still moving as of ${snapshotDate}. Treat the specifics as provisional and confirm against current documentation before relying on them.`;
59
+ case "contested":
60
+ return "> **Contested** \u2014 practitioners disagree here and the vendor does not prescribe an answer. Weigh the tradeoffs for the project at hand rather than adopting one option as the default.";
61
+ }
62
+ }
63
+ function withStabilityNote(body, note) {
64
+ if (!note) return body;
65
+ return `${note}
66
+
67
+ ${body.replace(/^\n+/, "")}`;
68
+ }
69
+
51
70
  // src/core/yaml.ts
52
71
  import YAML from "yaml";
53
72
  function toYaml(obj) {
@@ -71,10 +90,11 @@ import { join } from "path";
71
90
  function existsRel(root, rel) {
72
91
  return existsSync(join(root, rel));
73
92
  }
74
- var ACRONYMS = {
93
+ var CASED_TOKENS = {
75
94
  hig: "HIG",
76
95
  ui: "UI",
77
96
  ml: "ML",
97
+ ai: "AI",
78
98
  ar: "AR",
79
99
  av: "AV",
80
100
  os: "OS",
@@ -84,15 +104,48 @@ var ACRONYMS = {
84
104
  api: "API",
85
105
  sf: "SF",
86
106
  spm: "SPM",
107
+ m3: "M3",
108
+ nfc: "NFC",
109
+ http: "HTTP",
110
+ sqlite: "SQLite",
87
111
  ios: "iOS",
88
112
  ipados: "iPadOS",
89
113
  macos: "macOS",
90
114
  tvos: "tvOS",
91
115
  visionos: "visionOS",
92
- watchos: "watchOS"
116
+ watchos: "watchOS",
117
+ chromeos: "ChromeOS",
118
+ swiftui: "SwiftUI",
119
+ swiftdata: "SwiftData",
120
+ uikit: "UIKit",
121
+ appkit: "AppKit",
122
+ xcode: "Xcode",
123
+ xctest: "XCTest",
124
+ viewmodel: "ViewModel",
125
+ workmanager: "WorkManager",
126
+ activitykit: "ActivityKit",
127
+ arkit: "ARKit",
128
+ cloudkit: "CloudKit",
129
+ cryptokit: "CryptoKit",
130
+ eventkit: "EventKit",
131
+ gamekit: "GameKit",
132
+ healthkit: "HealthKit",
133
+ mapkit: "MapKit",
134
+ musickit: "MusicKit",
135
+ passkit: "PassKit",
136
+ pencilkit: "PencilKit",
137
+ photokit: "PhotoKit",
138
+ realitykit: "RealityKit",
139
+ scenekit: "SceneKit",
140
+ screencapturekit: "ScreenCaptureKit",
141
+ spritekit: "SpriteKit",
142
+ storekit: "StoreKit",
143
+ tipkit: "TipKit",
144
+ weatherkit: "WeatherKit",
145
+ widgetkit: "WidgetKit"
93
146
  };
94
147
  function titleFromName(name) {
95
- return name.split("-").filter(Boolean).map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
148
+ return name.split("-").filter(Boolean).map((w) => CASED_TOKENS[w] ?? w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
96
149
  }
97
150
  function globsToString(fm) {
98
151
  const g = fm.globs;
@@ -110,6 +163,8 @@ var claudeEmitter = {
110
163
  detect: (root) => existsRel(root, ".claude"),
111
164
  emit(skill, ctx) {
112
165
  const dir = `${ctx.paths.claude}/${skill.name}`;
166
+ const xm = skill.frontmatter["x-skills-master"];
167
+ const note = stabilityNote(xm.stability, xm.snapshot_date);
113
168
  const fm = {
114
169
  name: skill.frontmatter.name,
115
170
  description: skill.frontmatter.description
@@ -117,7 +172,7 @@ var claudeEmitter = {
117
172
  const files = [
118
173
  {
119
174
  path: `${dir}/SKILL.md`,
120
- contents: withFrontmatter(fm, skill.body),
175
+ contents: withFrontmatter(fm, withStabilityNote(skill.body, note)),
121
176
  mode: "whole"
122
177
  }
123
178
  ];
@@ -152,12 +207,12 @@ function condenseBody(body, opts = {}) {
152
207
  if (opts.hadResources || strippedLink) {
153
208
  out += `
154
209
 
155
- > ${opts.fullSkillNote ?? DEFAULT_NOTE}`;
210
+ > ${DEFAULT_NOTE}`;
156
211
  }
157
212
  return out + "\n";
158
213
  }
159
214
  function summarizeOpenQuestion(body) {
160
- const re = /^## Open question[ \t]*\n([\s\S]*?)(?=\n## |\s*$)/m;
215
+ const re = /^## Open question[ \t]*\n([\s\S]*?)(?=\n## |(?![\s\S]))/m;
161
216
  return body.replace(re, (_m, section) => {
162
217
  const firstPara = section.trim().split(/\n\s*\n/)[0]?.replace(/\s+/g, " ").trim() ?? "";
163
218
  return `## Open question
@@ -166,6 +221,43 @@ Tradeoff: ${firstPara}
166
221
  `;
167
222
  });
168
223
  }
224
+ function sectionHighlights(body, section, max) {
225
+ const re = new RegExp(`^## ${section}\\n([\\s\\S]*?)(?=\\n## |(?![\\s\\S]))`, "m");
226
+ const m = re.exec(body);
227
+ if (!m) return [];
228
+ const text = m[1];
229
+ const bullets = [];
230
+ let inFence = false;
231
+ for (const line of text.split("\n")) {
232
+ if (/^(```|~~~)/.test(line.trimStart())) {
233
+ inFence = !inFence;
234
+ continue;
235
+ }
236
+ if (!inFence && /^- /.test(line.trim())) bullets.push(line.trim());
237
+ if (bullets.length === max) break;
238
+ }
239
+ if (bullets.length > 0) return bullets;
240
+ const firstPara = text.trim().split(/\n\s*\n/)[0]?.replace(/\s+/g, " ").trim();
241
+ return firstPara ? [firstPara] : [];
242
+ }
243
+ function digestBody(body, opts) {
244
+ const guidance = sectionHighlights(body, "Core guidance", 6);
245
+ const pitfalls = sectionHighlights(body, "Pitfalls", 3);
246
+ const flatten = (lines) => lines.map((l) => l.replace(L3_LINK_RE, (_m, text) => text));
247
+ const parts = [];
248
+ if (opts.stabilityNote) parts.push(opts.stabilityNote);
249
+ parts.push(opts.description.trim());
250
+ if (guidance.length > 0) parts.push(`#### Core guidance
251
+
252
+ ${flatten(guidance).join("\n")}`);
253
+ if (pitfalls.length > 0) parts.push(`#### Pitfalls
254
+
255
+ ${flatten(pitfalls).join("\n")}`);
256
+ parts.push(
257
+ `> Digest only \u2014 the complete skill (full guidance, examples, references) ships with the Claude Code, Cursor, and Copilot projections, or via \`skills-master view ${opts.name}\`.`
258
+ );
259
+ return parts.join("\n\n");
260
+ }
169
261
 
170
262
  // src/emitters/cursor.ts
171
263
  var cursorEmitter = {
@@ -179,10 +271,14 @@ var cursorEmitter = {
179
271
  };
180
272
  if (globs) fm.globs = globs;
181
273
  fm.alwaysApply = false;
182
- const body = condenseBody(skill.body, {
183
- openQuestion: "keep",
184
- hadResources: hasResources(skill.resources)
185
- });
274
+ const xm = skill.frontmatter["x-skills-master"];
275
+ const body = withStabilityNote(
276
+ condenseBody(skill.body, {
277
+ openQuestion: "keep",
278
+ hadResources: hasResources(skill.resources)
279
+ }),
280
+ stabilityNote(xm.stability, xm.snapshot_date)
281
+ );
186
282
  return [
187
283
  {
188
284
  path: `${ctx.paths.cursor}/${skill.name}.mdc`,
@@ -197,19 +293,25 @@ var cursorEmitter = {
197
293
  var copilotEmitter = {
198
294
  id: "copilot",
199
295
  label: "GitHub Copilot",
200
- detect: (root) => existsRel(root, ".github"),
296
+ // A bare .github/ (workflows, templates) says nothing about Copilot use —
297
+ // detect only on Copilot's own customization files.
298
+ detect: (root) => existsRel(root, ".github/copilot-instructions.md") || existsRel(root, ".github/instructions"),
201
299
  emit(skill, ctx) {
202
300
  const base = ctx.paths.copilot;
203
301
  const instructionsPath = `${base}/instructions/${skill.name}.instructions.md`;
204
- const applyTo = globsToString(skill.frontmatter) ?? "**";
302
+ const applyTo = globsToString(skill.frontmatter);
205
303
  const fm = {
206
- applyTo,
304
+ ...applyTo ? { applyTo } : {},
207
305
  description: skill.frontmatter.description
208
306
  };
209
- const body = condenseBody(skill.body, {
210
- openQuestion: "keep",
211
- hadResources: hasResources(skill.resources)
212
- });
307
+ const xm = skill.frontmatter["x-skills-master"];
308
+ const body = withStabilityNote(
309
+ condenseBody(skill.body, {
310
+ openQuestion: "keep",
311
+ hadResources: hasResources(skill.resources)
312
+ }),
313
+ stabilityNote(xm.stability, xm.snapshot_date)
314
+ );
213
315
  const pointer = `For ${titleFromName(skill.name)} guidance, see \`${instructionsPath}\`.`;
214
316
  return [
215
317
  {
@@ -234,9 +336,11 @@ var agentsEmitter = {
234
336
  label: "AGENTS.md",
235
337
  detect: (root) => existsRel(root, "AGENTS.md"),
236
338
  emit(skill, ctx) {
237
- const body = condenseBody(skill.body, {
238
- openQuestion: "summarize",
239
- hadResources: hasResources(skill.resources)
339
+ const xm = skill.frontmatter["x-skills-master"];
340
+ const body = digestBody(skill.body, {
341
+ name: skill.name,
342
+ description: skill.frontmatter.description,
343
+ stabilityNote: stabilityNote(xm.stability, xm.snapshot_date)
240
344
  });
241
345
  const section = `### ${titleFromName(skill.name)}
242
346
 
@@ -254,12 +358,7 @@ ${body.trim()}`;
254
358
  };
255
359
 
256
360
  // src/emitters/index.ts
257
- var EMITTERS = [
258
- claudeEmitter,
259
- cursorEmitter,
260
- copilotEmitter,
261
- agentsEmitter
262
- ];
361
+ var EMITTERS = [claudeEmitter, cursorEmitter, copilotEmitter, agentsEmitter];
263
362
  var BY_ID = new Map(EMITTERS.map((e) => [e.id, e]));
264
363
  function getEmitter(id) {
265
364
  return BY_ID.get(id);
@@ -271,6 +370,7 @@ function detectTargets(projectRoot) {
271
370
  // src/core/project.ts
272
371
  import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
273
372
  import { join as join2 } from "path";
373
+ import { z as z3 } from "zod";
274
374
 
275
375
  // src/schema/lockfile.ts
276
376
  import { z as z2 } from "zod";
@@ -300,6 +400,25 @@ function emptyLockfile(contentRef = "main") {
300
400
  }
301
401
 
302
402
  // src/core/project.ts
403
+ function parseJsonFile(p, schema) {
404
+ let data;
405
+ try {
406
+ data = JSON.parse(readFileSync(p, "utf8"));
407
+ } catch (err) {
408
+ const msg = err instanceof Error ? err.message : String(err);
409
+ throw new Error(`${p} is not valid JSON: ${msg}`);
410
+ }
411
+ try {
412
+ return schema.parse(data);
413
+ } catch (err) {
414
+ if (err instanceof z3.ZodError) {
415
+ const first = err.issues[0];
416
+ const at = first?.path.length ? ` at "${first.path.join(".")}"` : "";
417
+ throw new Error(`${p} is invalid${at}: ${first?.message ?? "schema mismatch"}`);
418
+ }
419
+ throw err;
420
+ }
421
+ }
303
422
  function configPath(root) {
304
423
  return join2(root, CONFIG_FILENAME);
305
424
  }
@@ -309,7 +428,7 @@ function lockfilePath(root) {
309
428
  function loadConfig(root) {
310
429
  const p = configPath(root);
311
430
  if (!existsSync2(p)) return null;
312
- return ProjectConfigSchema.parse(JSON.parse(readFileSync(p, "utf8")));
431
+ return parseJsonFile(p, ProjectConfigSchema);
313
432
  }
314
433
  function loadConfigOrDefault(root) {
315
434
  return loadConfig(root) ?? ProjectConfigSchema.parse({});
@@ -321,14 +440,13 @@ function saveConfig(root, cfg) {
321
440
  function loadLockfile(root) {
322
441
  const p = lockfilePath(root);
323
442
  if (!existsSync2(p)) return emptyLockfile();
324
- return LockfileSchema.parse(JSON.parse(readFileSync(p, "utf8")));
443
+ return parseJsonFile(p, LockfileSchema);
325
444
  }
326
445
  function saveLockfile(root, lock) {
327
446
  writeFileSync(lockfilePath(root), JSON.stringify(lock, null, 2) + "\n", "utf8");
328
447
  }
329
448
 
330
449
  // src/util/log.ts
331
- var quiet = false;
332
450
  var sym = {
333
451
  info: "\u2022",
334
452
  ok: "\u2713",
@@ -337,16 +455,16 @@ var sym = {
337
455
  };
338
456
  var log = {
339
457
  plain(msg) {
340
- if (!quiet) console.log(msg);
458
+ console.log(msg);
341
459
  },
342
460
  info(msg) {
343
- if (!quiet) console.log(`${sym.info} ${msg}`);
461
+ console.log(`${sym.info} ${msg}`);
344
462
  },
345
463
  success(msg) {
346
- if (!quiet) console.log(`${sym.ok} ${msg}`);
464
+ console.log(`${sym.ok} ${msg}`);
347
465
  },
348
466
  warn(msg) {
349
- if (!quiet) console.warn(`${sym.warn} ${msg}`);
467
+ console.warn(`${sym.warn} ${msg}`);
350
468
  },
351
469
  error(msg) {
352
470
  console.error(`${sym.err} ${msg}`);
@@ -357,7 +475,9 @@ var log = {
357
475
  function initCommand(opts) {
358
476
  const existing = loadConfig(opts.cwd);
359
477
  if (existing && !opts.force) {
360
- log.warn(`skills-master.json already exists \u2014 leaving it untouched (use --force to overwrite).`);
478
+ log.warn(
479
+ `skills-master.json already exists \u2014 leaving it untouched (use --force to overwrite).`
480
+ );
361
481
  return existing;
362
482
  }
363
483
  let targets = opts.targets;
@@ -427,45 +547,40 @@ import { basename, join as join4 } from "path";
427
547
  import matter from "gray-matter";
428
548
 
429
549
  // src/schema/frontmatter.ts
430
- import { z as z3 } from "zod";
550
+ import { z as z4 } from "zod";
431
551
  import semver from "semver";
432
552
  var NAME_RE = /^[a-z0-9-]{1,64}$/;
433
553
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
434
- var StabilitySchema = z3.enum(["stable", "emerging", "contested"]);
435
- var SkillClassSchema = z3.enum([
436
- "code",
437
- "design",
438
- "lang-tooling",
439
- "overview"
440
- ]);
554
+ var StabilitySchema = z4.enum(["stable", "emerging", "contested"]);
555
+ var SkillClassSchema = z4.enum(["code", "design", "lang-tooling", "overview"]);
441
556
  var CLASS_DIR = {
442
557
  code: "code",
443
558
  design: "design",
444
559
  "lang-tooling": "lang-tooling",
445
560
  overview: "overviews"
446
561
  };
447
- var XSkillsMasterSchema = z3.object({
448
- domain: z3.string().min(1),
562
+ var XSkillsMasterSchema = z4.object({
563
+ domain: z4.string().min(1),
449
564
  class: SkillClassSchema,
450
- category: z3.string().min(1),
451
- platforms: z3.array(z3.string().min(1)).min(1),
565
+ category: z4.string().min(1),
566
+ platforms: z4.array(z4.string().min(1)).min(1),
452
567
  /** domain-defined version requirements, e.g. { ios: "17", swift: "6.0" }. */
453
- requires: z3.record(z3.string(), z3.string()).optional(),
454
- pairs_with: z3.array(z3.string().regex(NAME_RE)).default([]),
568
+ requires: z4.record(z4.string(), z4.string()).optional(),
569
+ pairs_with: z4.array(z4.string().regex(NAME_RE)).default([]),
455
570
  /** citation URLs to canonical docs — never verbatim content. */
456
- sources: z3.array(z3.string().url()).default([]),
457
- snapshot_date: z3.string().regex(ISO_DATE_RE, "must be an ISO date (YYYY-MM-DD)"),
571
+ sources: z4.array(z4.url()).default([]),
572
+ snapshot_date: z4.string().regex(ISO_DATE_RE, "must be an ISO date (YYYY-MM-DD)"),
458
573
  stability: StabilitySchema,
459
- version: z3.string().refine((v) => semver.valid(v) != null, "must be a valid semver version")
574
+ version: z4.string().refine((v) => semver.valid(v) != null, "must be a valid semver version")
460
575
  }).strict();
461
- var GlobsSchema = z3.union([z3.string(), z3.array(z3.string())]).transform((g) => Array.isArray(g) ? g : [g]).optional();
462
- var FrontmatterSchema = z3.object({
463
- name: z3.string().regex(NAME_RE, "must be kebab-case ([a-z0-9-], <=64 chars)"),
464
- description: z3.string().min(1, "description is required").max(1024, "description must be <= 1024 characters"),
576
+ var GlobsSchema = z4.union([z4.string(), z4.array(z4.string())]).transform((g) => Array.isArray(g) ? g : [g]).optional();
577
+ var FrontmatterSchema = z4.looseObject({
578
+ name: z4.string().regex(NAME_RE, "must be kebab-case ([a-z0-9-], <=64 chars)"),
579
+ description: z4.string().min(1, "description is required").max(1024, "description must be <= 1024 characters"),
465
580
  globs: GlobsSchema,
466
- tags: z3.array(z3.string()).default([]),
581
+ tags: z4.array(z4.string()).default([]),
467
582
  "x-skills-master": XSkillsMasterSchema
468
- }).passthrough();
583
+ });
469
584
 
470
585
  // src/core/parse.ts
471
586
  function loadRawSkill(dir, skillsRoot) {
@@ -555,16 +670,57 @@ function buildRegistry(skillsRoot, version2 = "0.1.0") {
555
670
  };
556
671
  }
557
672
 
673
+ // src/schema/registry.ts
674
+ import { z as z5 } from "zod";
675
+ var RegistryEntrySchema = z5.object({
676
+ name: z5.string(),
677
+ domain: z5.string(),
678
+ class: SkillClassSchema,
679
+ category: z5.string(),
680
+ description: z5.string(),
681
+ platforms: z5.array(z5.string()),
682
+ stability: StabilitySchema,
683
+ version: z5.string(),
684
+ tags: z5.array(z5.string()).default([]),
685
+ pairs_with: z5.array(z5.string()).default([]),
686
+ /** path relative to the skills root. */
687
+ path: z5.string(),
688
+ /** which on-demand resource files exist. */
689
+ resources: z5.object({
690
+ reference: z5.boolean(),
691
+ examples: z5.boolean(),
692
+ checklist: z5.boolean()
693
+ })
694
+ });
695
+ var RegistrySchema = z5.object({
696
+ $schema: z5.string().optional(),
697
+ /** aggregate library version (bumped on release). */
698
+ version: z5.string().default("0.1.0"),
699
+ skills: z5.array(RegistryEntrySchema).default([])
700
+ });
701
+ var REGISTRY_FILENAME = "registry.json";
702
+
558
703
  // src/content/source.ts
559
704
  var DEFAULT_REPO = "github:iChintanSoni/skills-master";
560
705
  var CONTENT_REPO_PACKAGE = "skills-master-monorepo";
706
+ var SkillNotFoundError = class extends Error {
707
+ constructor(skillName, root) {
708
+ super(`Skill "${skillName}" not found in content at ${root}`);
709
+ this.skillName = skillName;
710
+ this.name = "SkillNotFoundError";
711
+ }
712
+ skillName;
713
+ };
561
714
  var ContentSource = class {
562
715
  constructor(root) {
563
716
  this.root = root;
564
717
  }
565
718
  root;
719
+ #dirs;
720
+ #registry;
566
721
  skillDirs() {
567
- return findSkillDirs(this.root);
722
+ this.#dirs ??= findSkillDirs(this.root);
723
+ return this.#dirs;
568
724
  }
569
725
  findDir(name) {
570
726
  const dirs = this.skillDirs();
@@ -572,11 +728,27 @@ var ContentSource = class {
572
728
  }
573
729
  loadSkill(name) {
574
730
  const dir = this.findDir(name);
575
- if (!dir) throw new Error(`Skill "${name}" not found in content at ${this.root}`);
731
+ if (!dir) throw new SkillNotFoundError(name, this.root);
576
732
  return loadSkill(dir, this.root);
577
733
  }
734
+ /**
735
+ * The catalog used by list/search/add. Reads the committed registry.json
736
+ * when the content ships one (it is generated and CI-gated for drift);
737
+ * otherwise — or if the committed file is unreadable — falls back to
738
+ * scanning and parsing every skill, which is always ground truth.
739
+ */
578
740
  registry() {
579
- return buildRegistry(this.root);
741
+ this.#registry ??= this.readCommittedRegistry() ?? buildRegistry(this.root);
742
+ return this.#registry;
743
+ }
744
+ readCommittedRegistry() {
745
+ const p = join5(this.root, "registry.json");
746
+ if (!existsSync5(p)) return null;
747
+ try {
748
+ return RegistrySchema.parse(JSON.parse(readFileSync3(p, "utf8")));
749
+ } catch {
750
+ return null;
751
+ }
580
752
  }
581
753
  };
582
754
  function safeName(dir, root) {
@@ -589,10 +761,15 @@ function safeName(dir, root) {
589
761
  async function resolveContent(opts = {}) {
590
762
  if (opts.content) {
591
763
  const root = isAbsolute(opts.content) ? opts.content : resolve(process.cwd(), opts.content);
764
+ if (!existsSync5(root)) throw new Error(`--content directory not found: ${root}`);
592
765
  return new ContentSource(root);
593
766
  }
594
767
  const env = process.env.SKILLS_MASTER_CONTENT;
595
- if (env) return new ContentSource(resolve(env));
768
+ if (env) {
769
+ const root = resolve(env);
770
+ if (!existsSync5(root)) throw new Error(`SKILLS_MASTER_CONTENT directory not found: ${root}`);
771
+ return new ContentSource(root);
772
+ }
596
773
  const local = findLocalSkillsDir(opts.cwd ?? process.cwd());
597
774
  if (local) return new ContentSource(local);
598
775
  return new ContentSource(await fetchRemote(opts.ref ?? "main"));
@@ -618,14 +795,24 @@ function isContentRepoRoot(dir) {
618
795
  }
619
796
  async function fetchRemote(ref) {
620
797
  const repo = process.env.SKILLS_MASTER_REPO ?? DEFAULT_REPO;
621
- const cacheDir = join5(homedir(), ".skills-master-cache", ref.replace(/[^\w.-]/g, "_"));
798
+ const safeRef = ref.trim().replace(/[^\w.-]/g, "_");
799
+ if (!safeRef) throw new Error(`Invalid content ref "${ref}".`);
800
+ const cacheDir = join5(homedir(), ".skills-master-cache", safeRef);
622
801
  const { downloadTemplate } = await import("giget");
623
- const { dir } = await downloadTemplate(`${repo}/skills#${ref}`, {
624
- dir: cacheDir,
625
- force: true,
626
- forceClean: true
627
- });
628
- return dir;
802
+ try {
803
+ const { dir } = await downloadTemplate(`${repo}/skills#${ref.trim()}`, {
804
+ dir: cacheDir,
805
+ force: true,
806
+ forceClean: true
807
+ });
808
+ return dir;
809
+ } catch (err) {
810
+ const msg = err instanceof Error ? err.message : String(err);
811
+ throw new Error(
812
+ `Failed to fetch skills content from ${repo}#${ref.trim()}: ${msg}
813
+ Check the ref, or point at a local checkout with --content <dir> or SKILLS_MASTER_CONTENT.`
814
+ );
815
+ }
629
816
  }
630
817
 
631
818
  // src/core/install.ts
@@ -644,7 +831,15 @@ function compileSkill(skill, targets, ctx) {
644
831
  }
645
832
 
646
833
  // src/core/writer.ts
647
- import { existsSync as existsSync6, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
834
+ import {
835
+ existsSync as existsSync6,
836
+ mkdirSync,
837
+ readdirSync as readdirSync2,
838
+ readFileSync as readFileSync4,
839
+ rmdirSync,
840
+ rmSync,
841
+ writeFileSync as writeFileSync2
842
+ } from "fs";
648
843
  import { dirname, join as join6 } from "path";
649
844
 
650
845
  // src/core/markers.ts
@@ -704,21 +899,17 @@ function applyWhole(projectRoot, file, opts) {
704
899
  ensureDir(abs);
705
900
  writeFileSync2(abs, next, "utf8");
706
901
  }
707
- return { path: file.path, mode: "whole", action: "created", after: next };
902
+ return { path: file.path, mode: "whole", action: "created" };
708
903
  }
709
904
  const current = readFileSync4(abs, "utf8");
710
905
  if (current === next) {
711
906
  return { path: file.path, mode: "whole", action: "unchanged" };
712
907
  }
713
- let choice = "overwrite";
714
- if (!opts.overwrite) {
715
- choice = opts.onConflict ? opts.onConflict(file.path) : opts.dryRun ? "overwrite" : "skip";
716
- }
717
- if (choice === "skip") {
718
- return { path: file.path, mode: "whole", action: "skipped", before: current, after: next };
908
+ if (!opts.overwrite && !opts.dryRun) {
909
+ return { path: file.path, mode: "whole", action: "skipped" };
719
910
  }
720
911
  if (!opts.dryRun) writeFileSync2(abs, next, "utf8");
721
- return { path: file.path, mode: "whole", action: "updated", before: current, after: next };
912
+ return { path: file.path, mode: "whole", action: "updated" };
722
913
  }
723
914
  function applyBlock(projectRoot, file, opts) {
724
915
  const abs = join6(projectRoot, file.path);
@@ -871,7 +1062,7 @@ async function addCommand(opts) {
871
1062
  const byClass = registry2.skills.filter((s) => s.class === token);
872
1063
  const group = byCategory.length ? byCategory : byClass;
873
1064
  if (group.length) {
874
- group.forEach((s) => selected.add(s.name));
1065
+ for (const s of group) selected.add(s.name);
875
1066
  } else {
876
1067
  log.warn(`No skill, category, or class matches "${token}".`);
877
1068
  skipped.push(token);
@@ -893,16 +1084,22 @@ async function addCommand(opts) {
893
1084
  const lock = loadLockfile(opts.cwd);
894
1085
  lock.contentRef = opts.ref ?? cfg.contentRef;
895
1086
  const installed = [];
1087
+ const ownedFiles = /* @__PURE__ */ new Set();
1088
+ const sharedBlockFiles = /* @__PURE__ */ new Set();
896
1089
  const prefix = opts.dryRun ? "[dry-run] " : "";
897
1090
  for (const name of [...selected].sort()) {
898
1091
  const skill = content.loadSkill(name);
899
1092
  const result = installSkill(opts.cwd, skill, targets, paths, {
900
1093
  dryRun: opts.dryRun,
901
- overwrite: opts.overwrite,
902
- onConflict: opts.onConflict
1094
+ overwrite: opts.overwrite
903
1095
  });
904
1096
  if (!opts.dryRun) lock.skills[name] = result.locked;
905
1097
  installed.push({ name, version: result.version });
1098
+ for (const e of Object.values(result.locked.emitted)) {
1099
+ if (!e) continue;
1100
+ for (const f of e.files) ownedFiles.add(f);
1101
+ if (e.block) sharedBlockFiles.add(e.block);
1102
+ }
906
1103
  for (const r of result.results) {
907
1104
  const tag = r.mode === "block" ? `${r.path} [${r.blockId}]` : r.path;
908
1105
  log.info(`${prefix}${r.action.padEnd(9)} ${tag}`);
@@ -915,8 +1112,15 @@ async function addCommand(opts) {
915
1112
  log.info("Wrote skills-master.json.");
916
1113
  }
917
1114
  if (!cfg.commit) {
918
- const outs = targets.map((t) => paths[t]);
919
- ensureGitignored(opts.cwd, outs);
1115
+ ensureGitignored(
1116
+ opts.cwd,
1117
+ [...ownedFiles].sort((a, b) => a.localeCompare(b)).map((f) => `/${f}`)
1118
+ );
1119
+ if (sharedBlockFiles.size > 0) {
1120
+ log.warn(
1121
+ `Not gitignoring shared file(s) with managed blocks: ${[...sharedBlockFiles].sort().join(", ")}.`
1122
+ );
1123
+ }
920
1124
  }
921
1125
  }
922
1126
  log.success(
@@ -951,8 +1155,12 @@ async function updateCommand(opts) {
951
1155
  let skill;
952
1156
  try {
953
1157
  skill = content.loadSkill(name);
954
- } catch {
955
- log.warn(`"${name}" no longer exists in the content library.`);
1158
+ } catch (err) {
1159
+ if (err instanceof SkillNotFoundError) {
1160
+ log.warn(`"${name}" no longer exists in the content library.`);
1161
+ } else {
1162
+ log.error(`Failed to load "${name}": ${err instanceof Error ? err.message : String(err)}`);
1163
+ }
956
1164
  skipped.push(name);
957
1165
  continue;
958
1166
  }
@@ -967,15 +1175,14 @@ async function updateCommand(opts) {
967
1175
  const e = locked.emitted[t];
968
1176
  return e && diskHash(opts.cwd, e.files) !== e.hash;
969
1177
  });
970
- if (userEdited && !opts.overwrite && !opts.onConflict) {
1178
+ if (userEdited && !opts.overwrite) {
971
1179
  log.warn(`${prefix}"${name}" has local edits \u2014 skipping (use --overwrite to replace).`);
972
1180
  skipped.push(name);
973
1181
  continue;
974
1182
  }
975
1183
  const result = installSkill(opts.cwd, skill, targets, paths, {
976
1184
  dryRun: opts.dryRun,
977
- overwrite: opts.overwrite || !userEdited,
978
- onConflict: opts.onConflict
1185
+ overwrite: opts.overwrite || !userEdited
979
1186
  });
980
1187
  if (!opts.dryRun) lock.skills[name] = result.locked;
981
1188
  updated.push(name);
@@ -1009,6 +1216,11 @@ function removeCommand(opts) {
1009
1216
  continue;
1010
1217
  }
1011
1218
  const targets = (opts.targets?.length ? opts.targets : Object.keys(locked.emitted)).filter((t) => locked.emitted[t]);
1219
+ if (targets.length === 0) {
1220
+ log.warn(`"${name}" is not installed to ${opts.targets?.join(", ")} \u2014 nothing to remove.`);
1221
+ missing.push(name);
1222
+ continue;
1223
+ }
1012
1224
  const wholeRemoved = [];
1013
1225
  for (const t of targets) {
1014
1226
  const e = locked.emitted[t];
@@ -1034,9 +1246,44 @@ function removeCommand(opts) {
1034
1246
  return { removed, missing };
1035
1247
  }
1036
1248
 
1037
- // src/commands/doctor.ts
1249
+ // src/core/installed-state.ts
1038
1250
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1039
1251
  import { join as join9 } from "path";
1252
+ var STATES = ["ok", "edited", "missing"];
1253
+ function worst(states) {
1254
+ return states.reduce(
1255
+ (acc, s) => STATES.indexOf(s) > STATES.indexOf(acc) ? s : acc,
1256
+ "ok"
1257
+ );
1258
+ }
1259
+ function diagnoseInstalled(cwd, lock) {
1260
+ const out = [];
1261
+ for (const name of Object.keys(lock.skills).sort()) {
1262
+ const locked = lock.skills[name];
1263
+ const targets = [];
1264
+ for (const [target, e] of Object.entries(locked.emitted)) {
1265
+ const missingFiles = e.files.filter((f) => !existsSync9(join9(cwd, f)));
1266
+ const edited = missingFiles.length === 0 && diskHash(cwd, e.files) !== e.hash;
1267
+ let missingBlock;
1268
+ if (e.block) {
1269
+ const abs = join9(cwd, e.block);
1270
+ if (!existsSync9(abs) || !hasBlock(readFileSync7(abs, "utf8"), name)) missingBlock = e.block;
1271
+ }
1272
+ const state = missingFiles.length > 0 || missingBlock ? "missing" : edited ? "edited" : "ok";
1273
+ targets.push({ target, missingFiles, edited, missingBlock, state });
1274
+ }
1275
+ targets.sort((a, b) => a.target.localeCompare(b.target));
1276
+ out.push({
1277
+ name,
1278
+ version: locked.version,
1279
+ targets,
1280
+ state: worst(targets.map((t) => t.state))
1281
+ });
1282
+ }
1283
+ return out;
1284
+ }
1285
+
1286
+ // src/commands/doctor.ts
1040
1287
  function doctorCommand(opts) {
1041
1288
  const problems = [];
1042
1289
  const note = (msg) => problems.push(msg);
@@ -1047,32 +1294,28 @@ function doctorCommand(opts) {
1047
1294
  log.info(`Config targets: ${cfg.targets.length ? cfg.targets.join(", ") : "(auto-detect)"}`);
1048
1295
  }
1049
1296
  const lock = loadLockfile(opts.cwd);
1050
- const names = Object.keys(lock.skills);
1051
- if (names.length === 0) {
1297
+ const diagnoses = diagnoseInstalled(opts.cwd, lock);
1298
+ if (diagnoses.length === 0) {
1052
1299
  log.info("No skills installed.");
1053
1300
  return { problems, ok: true };
1054
1301
  }
1055
- for (const name of names) {
1056
- const locked = lock.skills[name];
1057
- for (const [target, e] of Object.entries(locked.emitted)) {
1058
- for (const file of e.files) {
1059
- if (!existsSync9(join9(opts.cwd, file))) note(`${name}: missing ${target} file ${file}`);
1302
+ for (const skill of diagnoses) {
1303
+ for (const t of skill.targets) {
1304
+ for (const file of t.missingFiles) {
1305
+ note(`${skill.name}: missing ${t.target} file ${file}`);
1060
1306
  }
1061
- if (e.files.every((f) => existsSync9(join9(opts.cwd, f)))) {
1062
- if (diskHash(opts.cwd, e.files) !== e.hash) {
1063
- note(`${name}: local edits to ${target} output(s) (run \`update --overwrite\` to reset)`);
1064
- }
1307
+ if (t.edited) {
1308
+ note(
1309
+ `${skill.name}: local edits to ${t.target} output(s) (run \`update --overwrite\` to reset)`
1310
+ );
1065
1311
  }
1066
- if (e.block) {
1067
- const abs = join9(opts.cwd, e.block);
1068
- if (!existsSync9(abs) || !hasBlock(readFileSync7(abs, "utf8"), name)) {
1069
- note(`${name}: missing managed block in ${e.block}`);
1070
- }
1312
+ if (t.missingBlock) {
1313
+ note(`${skill.name}: missing managed block in ${t.missingBlock}`);
1071
1314
  }
1072
1315
  }
1073
1316
  }
1074
1317
  if (problems.length === 0) {
1075
- log.success(`All ${names.length} installed skill(s) look healthy.`);
1318
+ log.success(`All ${diagnoses.length} installed skill(s) look healthy.`);
1076
1319
  } else {
1077
1320
  for (const p of problems) log.warn(p);
1078
1321
  log.plain(`
@@ -1081,6 +1324,70 @@ ${problems.length} problem(s) found.`);
1081
1324
  return { problems, ok: problems.length === 0 };
1082
1325
  }
1083
1326
 
1327
+ // src/commands/status.ts
1328
+ var MARK = { ok: "ok", edited: "edited", missing: "missing" };
1329
+ function statusCommand(opts) {
1330
+ const cfg = loadConfig(opts.cwd);
1331
+ const lock = loadLockfile(opts.cwd);
1332
+ let skills = diagnoseInstalled(opts.cwd, lock);
1333
+ if (opts.names?.length) {
1334
+ const want = new Set(opts.names);
1335
+ skills = skills.filter((s) => want.has(s.name));
1336
+ }
1337
+ if (opts.problemsOnly) skills = skills.filter((s) => s.state !== "ok");
1338
+ const counts = { ok: 0, edited: 0, missing: 0 };
1339
+ for (const s of skills) counts[s.state]++;
1340
+ const report = {
1341
+ contentRef: lock.contentRef,
1342
+ configuredTargets: cfg?.targets ?? [],
1343
+ skills,
1344
+ counts
1345
+ };
1346
+ if (opts.json) {
1347
+ log.plain(JSON.stringify(report, null, 2));
1348
+ return report;
1349
+ }
1350
+ if (Object.keys(lock.skills).length === 0) {
1351
+ log.info("No skills installed \u2014 run `skills-master add <name>`.");
1352
+ return report;
1353
+ }
1354
+ if (skills.length === 0) {
1355
+ log.info(opts.problemsOnly ? "Nothing needs attention." : "No installed skills match.");
1356
+ return report;
1357
+ }
1358
+ log.info(
1359
+ `${skills.length} skill(s) from ref ${report.contentRef}` + (report.configuredTargets.length ? ` \xB7 config targets: ${report.configuredTargets.join(", ")}` : "")
1360
+ );
1361
+ log.plain("");
1362
+ const nameWidth = Math.max(...skills.map((s) => s.name.length));
1363
+ const verWidth = Math.max(...skills.map((s) => s.version.length + 1));
1364
+ for (const s of skills) {
1365
+ const targets = s.targets.map((t) => t.state === "ok" ? t.target : `${t.target} (${MARK[t.state]})`).join(", ");
1366
+ log.plain(
1367
+ ` ${s.name.padEnd(nameWidth)} ${`v${s.version}`.padEnd(verWidth)} ${MARK[s.state].padEnd(7)} ${targets}`
1368
+ );
1369
+ }
1370
+ log.plain("");
1371
+ const parts = [`${counts.ok} ok`];
1372
+ if (counts.edited) parts.push(`${counts.edited} edited`);
1373
+ if (counts.missing) parts.push(`${counts.missing} missing`);
1374
+ log.plain(parts.join(", ") + ".");
1375
+ if (counts.edited || counts.missing) {
1376
+ log.plain("Run `skills-master doctor` for detail, or `update --overwrite` to reset.");
1377
+ }
1378
+ return report;
1379
+ }
1380
+
1381
+ // src/core/search-text.ts
1382
+ function searchNormalize(value) {
1383
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
1384
+ }
1385
+ function searchableText(parts) {
1386
+ return searchNormalize(
1387
+ [parts.name, parts.description, parts.domain, parts.category, parts.class].join(" ")
1388
+ );
1389
+ }
1390
+
1084
1391
  // src/commands/catalog.ts
1085
1392
  async function registryOf(q) {
1086
1393
  const content = await resolveContent({ content: q.content, ref: q.ref, cwd: q.cwd });
@@ -1124,9 +1431,11 @@ ${skills.length} skill(s).`);
1124
1431
  }
1125
1432
  async function searchCommand(opts) {
1126
1433
  const reg = await registryOf(opts);
1127
- const q = opts.query.toLowerCase();
1434
+ const q = searchNormalize(opts.query);
1128
1435
  const hits = reg.skills.filter(
1129
- (s) => [s.name, s.description, s.domain, s.category, s.class, ...s.tags].join(" ").toLowerCase().includes(q)
1436
+ (s) => searchNormalize(
1437
+ [s.name, s.description, s.domain, s.category, s.class, ...s.tags].join(" ")
1438
+ ).includes(q)
1130
1439
  );
1131
1440
  if (hits.length === 0) {
1132
1441
  log.info(`No matches for "${opts.query}".`);
@@ -1148,10 +1457,14 @@ async function viewCommand(opts) {
1148
1457
  log.plain(skill.body);
1149
1458
  return;
1150
1459
  }
1151
- log.plain(`${skill.name} v${xm.version} [${xm.domain}/${xm.class}/${xm.category}] ${xm.stability}`);
1460
+ log.plain(
1461
+ `${skill.name} v${xm.version} [${xm.domain}/${xm.class}/${xm.category}] ${xm.stability}`
1462
+ );
1152
1463
  log.plain(`platforms: ${xm.platforms.join(", ")}`);
1153
1464
  if (xm.requires) {
1154
- log.plain(`requires: ${Object.entries(xm.requires).map(([k, v]) => `${k} ${v}`).join(", ")}`);
1465
+ log.plain(
1466
+ `requires: ${Object.entries(xm.requires).map(([k, v]) => `${k} ${v}`).join(", ")}`
1467
+ );
1155
1468
  }
1156
1469
  if (xm.pairs_with.length) log.plain(`pairs with: ${xm.pairs_with.join(", ")}`);
1157
1470
  log.plain(`
@@ -1212,10 +1525,21 @@ function lintSkills(skillsRoot) {
1212
1525
  for (const issue of v.issues) {
1213
1526
  diagnostics.push({ relPath: raw.relPath, level: "error", message: issue });
1214
1527
  }
1215
- loaded.push({ relPath: raw.relPath, folderName: raw.folderName, body: raw.body });
1528
+ loaded.push({
1529
+ relPath: raw.relPath,
1530
+ folderName: raw.folderName,
1531
+ body: raw.body,
1532
+ resources: raw.resources
1533
+ });
1216
1534
  continue;
1217
1535
  }
1218
- loaded.push({ relPath: raw.relPath, folderName: raw.folderName, fm: v.value, body: raw.body });
1536
+ loaded.push({
1537
+ relPath: raw.relPath,
1538
+ folderName: raw.folderName,
1539
+ fm: v.value,
1540
+ body: raw.body,
1541
+ resources: raw.resources
1542
+ });
1219
1543
  byName.set(v.value.name, v.value);
1220
1544
  }
1221
1545
  const nameDirs = /* @__PURE__ */ new Map();
@@ -1226,7 +1550,11 @@ function lintSkills(skillsRoot) {
1226
1550
  for (const [name, paths] of nameDirs) {
1227
1551
  if (paths.length > 1) {
1228
1552
  for (const p of paths) {
1229
- diagnostics.push({ relPath: p, level: "error", message: `duplicate skill name "${name}" (also at ${paths.filter((x) => x !== p).join(", ")})` });
1553
+ diagnostics.push({
1554
+ relPath: p,
1555
+ level: "error",
1556
+ message: `duplicate skill name "${name}" (also at ${paths.filter((x) => x !== p).join(", ")})`
1557
+ });
1230
1558
  }
1231
1559
  }
1232
1560
  }
@@ -1245,17 +1573,58 @@ function lintSkills(skillsRoot) {
1245
1573
  if (xm.snapshot_date > todayStr) {
1246
1574
  push("error", `snapshot_date ${xm.snapshot_date} is in the future`);
1247
1575
  }
1248
- if (xm.stability === "contested" && !/^## Open question\b/m.test(s.body)) {
1576
+ const hasOpenQuestion = /^## Open question\b/m.test(s.body);
1577
+ if (xm.stability === "contested" && !hasOpenQuestion) {
1249
1578
  push("error", `stability is "contested" but no "## Open question" section is present`);
1579
+ } else if (xm.stability !== "contested" && hasOpenQuestion) {
1580
+ push(
1581
+ "error",
1582
+ `"## Open question" is reserved for stability: contested skills \u2014 retitle the section or mark the skill contested`
1583
+ );
1584
+ }
1585
+ if (xm.sources.length > 3) {
1586
+ push("warn", `${xm.sources.length} sources \u2014 keep at most 3 canonical citation URLs`);
1587
+ }
1588
+ const findable = searchableText({
1589
+ name: fm.name,
1590
+ description: fm.description,
1591
+ domain: xm.domain,
1592
+ category: xm.category,
1593
+ class: xm.class
1594
+ });
1595
+ for (const tag of fm.tags ?? []) {
1596
+ if (findable.includes(searchNormalize(tag))) {
1597
+ push("warn", `tag "${tag}" is already in the name or description \u2014 it adds no search term`);
1598
+ }
1599
+ }
1600
+ const l3 = [
1601
+ ["reference", "reference.md"],
1602
+ ["examples", "examples.md"],
1603
+ ["checklist", "checklist.md"]
1604
+ ];
1605
+ for (const [key, file] of l3) {
1606
+ if (s.resources?.[key] != null && !s.body.includes(`(${file}`) && !s.body.includes(`(./${file}`)) {
1607
+ push("warn", `${file} exists but is never linked from the SKILL.md body`);
1608
+ }
1250
1609
  }
1251
1610
  const lineCount = s.body.split("\n").length;
1252
1611
  if (lineCount > MAX_BODY_LINES) {
1253
- push("error", `SKILL.md body is ${lineCount} lines (max ${MAX_BODY_LINES}); move depth into reference.md/examples.md`);
1612
+ push(
1613
+ "error",
1614
+ `SKILL.md body is ${lineCount} lines (max ${MAX_BODY_LINES}); move depth into reference.md/examples.md`
1615
+ );
1254
1616
  } else if (lineCount > WARN_BODY_LINES) {
1255
- push("warn", `SKILL.md body is ${lineCount} lines (approaching the ${MAX_BODY_LINES}-line cap)`);
1617
+ push(
1618
+ "warn",
1619
+ `SKILL.md body is ${lineCount} lines (approaching the ${MAX_BODY_LINES}-line cap)`
1620
+ );
1256
1621
  }
1257
- if (!s.relPath.startsWith(`${xm.domain}/`)) {
1258
- push("warn", `domain "${xm.domain}" does not match the top folder of "${s.relPath}"`);
1622
+ const expectedPath = `${xm.domain}/${CLASS_DIR[xm.class]}/${xm.category}/${fm.name}`;
1623
+ if (s.relPath !== expectedPath) {
1624
+ push(
1625
+ "error",
1626
+ `on-disk path "${s.relPath}" must be "${expectedPath}" (domain/class dir/category/name from frontmatter)`
1627
+ );
1259
1628
  }
1260
1629
  if (xm.sources.length === 0) {
1261
1630
  push("warn", `no sources \u2014 add at least one canonical documentation URL`);
@@ -1270,6 +1639,12 @@ function lintSkills(skillsRoot) {
1270
1639
  push("warn", `missing recommended section "${heading}"`);
1271
1640
  }
1272
1641
  }
1642
+ if (xm.pairs_with.length > 4) {
1643
+ push(
1644
+ "warn",
1645
+ `${xm.pairs_with.length} pairs_with entries \u2014 keep at most 4; put wider cross-references in "## See also"`
1646
+ );
1647
+ }
1273
1648
  for (const partner of xm.pairs_with) {
1274
1649
  const partnerFm = byName.get(partner);
1275
1650
  if (!partnerFm) {
@@ -1277,7 +1652,10 @@ function lintSkills(skillsRoot) {
1277
1652
  continue;
1278
1653
  }
1279
1654
  if (!partnerFm["x-skills-master"].pairs_with.includes(fm.name)) {
1280
- push("error", `pairs_with "${partner}" is not reciprocated (add "${fm.name}" to its pairs_with)`);
1655
+ push(
1656
+ "error",
1657
+ `pairs_with "${partner}" is not reciprocated (add "${fm.name}" to its pairs_with)`
1658
+ );
1281
1659
  }
1282
1660
  }
1283
1661
  }
@@ -1305,39 +1683,6 @@ Linted ${result.skillCount} skill(s): ${result.errorCount} error(s), ${result.wa
1305
1683
  // src/commands/registry.ts
1306
1684
  import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1307
1685
  import { join as join10 } from "path";
1308
-
1309
- // src/schema/registry.ts
1310
- import { z as z4 } from "zod";
1311
- var RegistryEntrySchema = z4.object({
1312
- name: z4.string(),
1313
- domain: z4.string(),
1314
- class: SkillClassSchema,
1315
- category: z4.string(),
1316
- description: z4.string(),
1317
- platforms: z4.array(z4.string()),
1318
- stability: StabilitySchema,
1319
- version: z4.string(),
1320
- tags: z4.array(z4.string()).default([]),
1321
- pairs_with: z4.array(z4.string()).default([]),
1322
- /** path relative to the skills root. */
1323
- path: z4.string(),
1324
- /** which on-demand resource files exist. */
1325
- resources: z4.object({
1326
- reference: z4.boolean(),
1327
- examples: z4.boolean(),
1328
- checklist: z4.boolean()
1329
- })
1330
- });
1331
- var RegistrySchema = z4.object({
1332
- $schema: z4.string().optional(),
1333
- /** aggregate library version (bumped on release). */
1334
- version: z4.string().default("0.1.0"),
1335
- generatedAt: z4.string().optional(),
1336
- skills: z4.array(RegistryEntrySchema).default([])
1337
- });
1338
- var REGISTRY_FILENAME = "registry.json";
1339
-
1340
- // src/commands/registry.ts
1341
1686
  async function registryBuildCommand(opts) {
1342
1687
  const content = await resolveContent({ content: opts.content, cwd: opts.cwd });
1343
1688
  const registry2 = buildRegistry(content.root, opts.version ?? "0.1.0");
@@ -1433,7 +1778,13 @@ function buildOutputs(content, out, version2) {
1433
1778
  `${PLUGINS_DIR}/${name}/.claude-plugin/plugin.json`,
1434
1779
  JSON.stringify(manifest, null, 2) + "\n"
1435
1780
  );
1436
- plugins.push({ name, source: `./${PLUGINS_DIR}/${name}`, description, version: version2, category: CLASS_CATEGORY[cls] });
1781
+ plugins.push({
1782
+ name,
1783
+ source: `./${PLUGINS_DIR}/${name}`,
1784
+ description,
1785
+ version: version2,
1786
+ category: CLASS_CATEGORY[cls]
1787
+ });
1437
1788
  counts.push({ name, count });
1438
1789
  }
1439
1790
  plugins.sort((a, b) => a.name.localeCompare(b.name));
@@ -1480,7 +1831,11 @@ async function marketplaceBuildCommand(opts) {
1480
1831
  const stale = [...onDisk].filter((rel) => !files.has(rel)).sort();
1481
1832
  for (const rel of stale) rmSync2(join11(out, rel), { force: true });
1482
1833
  pruneEmptyDirsUnder(join11(out, PLUGINS_DIR));
1483
- const emitted = [...files].map(([path, contents]) => ({ path, contents, mode: "whole" }));
1834
+ const emitted = [...files].map(([path, contents]) => ({
1835
+ path,
1836
+ contents,
1837
+ mode: "whole"
1838
+ }));
1484
1839
  applyFiles(out, emitted, { overwrite: true });
1485
1840
  for (const { name, count } of counts) log.info(`Built ${name} (${count} skills).`);
1486
1841
  if (stale.length) log.info(`Removed ${stale.length} stale file(s).`);
@@ -1567,7 +1922,7 @@ async function newSkillCommand(opts) {
1567
1922
  return skillMd;
1568
1923
  }
1569
1924
 
1570
- // src/bin.ts
1925
+ // src/util/targets.ts
1571
1926
  var VALID = new Set(ALL_TARGETS);
1572
1927
  function parseTargets(value) {
1573
1928
  if (!value) return void 0;
@@ -1580,6 +1935,8 @@ function parseTargets(value) {
1580
1935
  }
1581
1936
  return ids;
1582
1937
  }
1938
+
1939
+ // src/bin.ts
1583
1940
  async function run(fn, exitOnFalse = false) {
1584
1941
  try {
1585
1942
  const result = await fn();
@@ -1602,7 +1959,7 @@ program.command("init").description("Detect tools and write skills-master.json")
1602
1959
  })
1603
1960
  )
1604
1961
  );
1605
- program.command("list").description("List available skills").option("--domain <domain>", "e.g. apple, android").option("--class <class>").option("--category <category>").option("--platform <platform>").option("--json").option("--content <dir>", "local skills directory").option("--ref <ref>").action(
1962
+ program.command("list").description("List available skills").option("--domain <domain>", "e.g. apple, android").option("--class <class>", "e.g. code, design, lang-tooling, overview").option("--category <category>", "e.g. app-frameworks, compose-ui").option("--platform <platform>", "e.g. ios, watchos, android").option("--json", "machine-readable JSON output").option("--content <dir>", "local skills directory").option("--ref <ref>", "content git ref (tag/branch/sha)").action(
1606
1963
  (opts) => run(
1607
1964
  () => listCommand({
1608
1965
  cwd: process.cwd(),
@@ -1616,15 +1973,21 @@ program.command("list").description("List available skills").option("--domain <d
1616
1973
  })
1617
1974
  )
1618
1975
  );
1619
- program.command("search <query>").description("Search skills by name, description, tags").option("--content <dir>").option("--ref <ref>").action(
1976
+ program.command("search <query>").description("Search skills by name, description, tags").option("--content <dir>", "local skills directory").option("--ref <ref>", "content git ref (tag/branch/sha)").action(
1620
1977
  (query, opts) => run(() => searchCommand({ cwd: process.cwd(), query, content: opts.content, ref: opts.ref }))
1621
1978
  );
1622
- program.command("view <name>").description("Show a skill's metadata and body").option("--raw", "print the raw SKILL.md body").option("--content <dir>").option("--ref <ref>").action(
1979
+ program.command("view <name>").description("Show a skill's metadata and body").option("--raw", "print the raw SKILL.md body").option("--content <dir>", "local skills directory").option("--ref <ref>", "content git ref (tag/branch/sha)").action(
1623
1980
  (name, opts) => run(
1624
- () => viewCommand({ cwd: process.cwd(), name, raw: opts.raw, content: opts.content, ref: opts.ref })
1981
+ () => viewCommand({
1982
+ cwd: process.cwd(),
1983
+ name,
1984
+ raw: opts.raw,
1985
+ content: opts.content,
1986
+ ref: opts.ref
1987
+ })
1625
1988
  )
1626
1989
  );
1627
- program.command("add <names...>").description("Install skills (by name, category, or class) into your tools").option("--target <list>", "comma list or 'all'").option("--with-pairs", "also install paired (code<->design) skills").option("--dry-run", "preview without writing").option("--overwrite", "overwrite changed files without asking").option("--content <dir>").option("--ref <ref>").action(
1990
+ program.command("add <names...>").description("Install skills (by name, category, or class) into your tools").option("--target <list>", "comma list or 'all'").option("--with-pairs", "also install paired (code<->design) skills").option("--dry-run", "preview without writing").option("--overwrite", "overwrite changed files without asking").option("--content <dir>", "local skills directory").option("--ref <ref>", "content git ref (tag/branch/sha)").action(
1628
1991
  (names, opts) => run(
1629
1992
  () => addCommand({
1630
1993
  cwd: process.cwd(),
@@ -1638,7 +2001,7 @@ program.command("add <names...>").description("Install skills (by name, category
1638
2001
  })
1639
2002
  )
1640
2003
  );
1641
- program.command("update [names...]").description("Re-install skills whose content changed").option("--dry-run").option("--overwrite", "force re-install, replacing local edits").option("--content <dir>").option("--ref <ref>").action(
2004
+ program.command("update [names...]").description("Re-install skills whose content changed").option("--dry-run", "preview without writing").option("--overwrite", "force re-install, replacing local edits").option("--content <dir>", "local skills directory").option("--ref <ref>", "content git ref (tag/branch/sha)").action(
1642
2005
  (names, opts) => run(
1643
2006
  () => updateCommand({
1644
2007
  cwd: process.cwd(),
@@ -1650,35 +2013,59 @@ program.command("update [names...]").description("Re-install skills whose conten
1650
2013
  })
1651
2014
  )
1652
2015
  );
1653
- program.command("remove <names...>").description("Remove installed skills").option("--target <list>", "comma list or 'all'").option("--dry-run").action(
2016
+ program.command("remove <names...>").description("Remove installed skills").option("--target <list>", "comma list or 'all'").option("--dry-run", "preview without writing").action(
2017
+ (names, opts) => run(
2018
+ () => removeCommand({
2019
+ cwd: process.cwd(),
2020
+ names,
2021
+ targets: parseTargets(opts.target),
2022
+ dryRun: opts.dryRun
2023
+ })
2024
+ )
2025
+ );
2026
+ program.command("status").description("Show installed skills: versions, targets, and local-edit state").argument("[names...]", "limit the report to these skills").option("--problems", "show only skills that are edited or missing files").option("--json", "machine-readable output").action(
1654
2027
  (names, opts) => run(
1655
- () => removeCommand({ cwd: process.cwd(), names, targets: parseTargets(opts.target), dryRun: opts.dryRun })
2028
+ () => statusCommand({
2029
+ cwd: process.cwd(),
2030
+ names,
2031
+ problemsOnly: opts.problems,
2032
+ json: opts.json
2033
+ })
1656
2034
  )
1657
2035
  );
1658
- program.command("doctor").description("Check installed skills for drift and missing files").action(() => run(() => doctorCommand({ cwd: process.cwd() })));
1659
- program.command("lint").description("Validate the skill library (maintainer command)").option("--content <dir>").action((opts) => run(() => lintCommand({ cwd: process.cwd(), content: opts.content }), true));
1660
- program.command("new <spec>").description("Scaffold a new skill: class/category/name (maintainer command)").option("--content <dir>").option("--force").action(
1661
- (spec, opts) => run(() => newSkillCommand({ cwd: process.cwd(), spec, content: opts.content, force: opts.force }))
2036
+ program.command("doctor").description("Check installed skills for drift and missing files").action(() => run(() => doctorCommand({ cwd: process.cwd() }).ok, true));
2037
+ program.command("lint").description("Validate the skill library (maintainer command)").option("--content <dir>", "local skills directory").action((opts) => run(() => lintCommand({ cwd: process.cwd(), content: opts.content }), true));
2038
+ program.command("new <spec>").description("Scaffold a new skill: domain/class/category/name (maintainer command)").option("--content <dir>", "local skills directory").option("--force", "overwrite an existing skill directory").action(
2039
+ (spec, opts) => run(
2040
+ () => newSkillCommand({ cwd: process.cwd(), spec, content: opts.content, force: opts.force })
2041
+ )
1662
2042
  );
1663
2043
  var registry = program.command("registry").description("Registry maintenance");
1664
- registry.command("build").description("Generate registry.json from the skill tree").option("--content <dir>").option("--check", "verify the committed registry.json is current (CI)").option("--version <v>").action(
2044
+ registry.command("build").description("Generate registry.json from the skill tree").option("--content <dir>", "local skills directory").option("--check", "verify the committed registry.json is current (CI)").option("--set-version <v>", "schema version to stamp into registry.json").action(
1665
2045
  (opts) => run(
1666
- () => registryBuildCommand({ cwd: process.cwd(), content: opts.content, check: opts.check, version: opts.version }),
2046
+ () => registryBuildCommand({
2047
+ cwd: process.cwd(),
2048
+ content: opts.content,
2049
+ check: opts.check,
2050
+ version: opts.setVersion
2051
+ }),
1667
2052
  true
1668
2053
  )
1669
2054
  );
1670
2055
  var marketplace = program.command("marketplace").description("Claude marketplace maintenance");
1671
- marketplace.command("build").description("Generate .claude-plugin/marketplace.json and per-class plugins").option("--content <dir>").option("--out <dir>", "output root").option("--check", "verify the committed marketplace output is current (CI)").option("--version <v>").action(
2056
+ marketplace.command("build").description("Generate .claude-plugin/marketplace.json and per-class plugins").option("--content <dir>", "local skills directory").option("--out <dir>", "output root").option("--check", "verify the committed marketplace output is current (CI)").option("--set-version <v>", "version to stamp into plugin manifests").action(
1672
2057
  (opts) => run(
1673
2058
  () => marketplaceBuildCommand({
1674
2059
  cwd: process.cwd(),
1675
2060
  content: opts.content,
1676
2061
  out: opts.out,
1677
2062
  check: opts.check,
1678
- version: opts.version
2063
+ version: opts.setVersion
1679
2064
  }),
1680
2065
  true
1681
2066
  )
1682
2067
  );
1683
- program.parseAsync(process.argv);
1684
- //# sourceMappingURL=bin.js.map
2068
+ program.parseAsync(process.argv).catch((err) => {
2069
+ log.error(err instanceof Error ? err.message : String(err));
2070
+ process.exitCode = 1;
2071
+ });