@ichintansoni/skills-master 0.1.6 → 0.1.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/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.8";
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);
@@ -846,13 +1037,31 @@ function ensureGitignored(root, entries) {
846
1037
  writeFileSync3(p, next, "utf8");
847
1038
  }
848
1039
 
1040
+ // src/util/targets.ts
1041
+ var VALID = new Set(ALL_TARGETS);
1042
+ function resolveTargets(cwd, configured, explicit) {
1043
+ if (explicit?.length) return explicit;
1044
+ if (configured.length) return configured;
1045
+ const detected = detectTargets(cwd);
1046
+ return detected.length ? detected : ALL_TARGETS;
1047
+ }
1048
+ function parseTargets(value) {
1049
+ if (!value) return void 0;
1050
+ if (value === "all") return ALL_TARGETS;
1051
+ const ids = value.split(",").map((s) => s.trim()).filter(Boolean);
1052
+ for (const id of ids) {
1053
+ if (!VALID.has(id)) {
1054
+ throw new Error(`Unknown target "${id}". Valid: ${[...VALID, "all"].join(", ")}.`);
1055
+ }
1056
+ }
1057
+ return ids;
1058
+ }
1059
+
849
1060
  // src/commands/add.ts
850
1061
  async function addCommand(opts) {
851
1062
  const cfg = loadConfigOrDefault(opts.cwd);
852
1063
  const hadConfig = loadConfig(opts.cwd) != null;
853
- let targets = opts.targets?.length ? opts.targets : cfg.targets;
854
- if (!targets.length) targets = detectTargets(opts.cwd);
855
- if (!targets.length) targets = ALL_TARGETS;
1064
+ const targets = resolveTargets(opts.cwd, cfg.targets, opts.targets);
856
1065
  const content = await resolveContent({
857
1066
  content: opts.content,
858
1067
  ref: opts.ref ?? cfg.contentRef,
@@ -871,7 +1080,7 @@ async function addCommand(opts) {
871
1080
  const byClass = registry2.skills.filter((s) => s.class === token);
872
1081
  const group = byCategory.length ? byCategory : byClass;
873
1082
  if (group.length) {
874
- group.forEach((s) => selected.add(s.name));
1083
+ for (const s of group) selected.add(s.name);
875
1084
  } else {
876
1085
  log.warn(`No skill, category, or class matches "${token}".`);
877
1086
  skipped.push(token);
@@ -893,16 +1102,22 @@ async function addCommand(opts) {
893
1102
  const lock = loadLockfile(opts.cwd);
894
1103
  lock.contentRef = opts.ref ?? cfg.contentRef;
895
1104
  const installed = [];
1105
+ const ownedFiles = /* @__PURE__ */ new Set();
1106
+ const sharedBlockFiles = /* @__PURE__ */ new Set();
896
1107
  const prefix = opts.dryRun ? "[dry-run] " : "";
897
1108
  for (const name of [...selected].sort()) {
898
1109
  const skill = content.loadSkill(name);
899
1110
  const result = installSkill(opts.cwd, skill, targets, paths, {
900
1111
  dryRun: opts.dryRun,
901
- overwrite: opts.overwrite,
902
- onConflict: opts.onConflict
1112
+ overwrite: opts.overwrite
903
1113
  });
904
1114
  if (!opts.dryRun) lock.skills[name] = result.locked;
905
1115
  installed.push({ name, version: result.version });
1116
+ for (const e of Object.values(result.locked.emitted)) {
1117
+ if (!e) continue;
1118
+ for (const f of e.files) ownedFiles.add(f);
1119
+ if (e.block) sharedBlockFiles.add(e.block);
1120
+ }
906
1121
  for (const r of result.results) {
907
1122
  const tag = r.mode === "block" ? `${r.path} [${r.blockId}]` : r.path;
908
1123
  log.info(`${prefix}${r.action.padEnd(9)} ${tag}`);
@@ -915,8 +1130,15 @@ async function addCommand(opts) {
915
1130
  log.info("Wrote skills-master.json.");
916
1131
  }
917
1132
  if (!cfg.commit) {
918
- const outs = targets.map((t) => paths[t]);
919
- ensureGitignored(opts.cwd, outs);
1133
+ ensureGitignored(
1134
+ opts.cwd,
1135
+ [...ownedFiles].sort((a, b) => a.localeCompare(b)).map((f) => `/${f}`)
1136
+ );
1137
+ if (sharedBlockFiles.size > 0) {
1138
+ log.warn(
1139
+ `Not gitignoring shared file(s) with managed blocks: ${[...sharedBlockFiles].sort().join(", ")}.`
1140
+ );
1141
+ }
920
1142
  }
921
1143
  }
922
1144
  log.success(
@@ -951,8 +1173,12 @@ async function updateCommand(opts) {
951
1173
  let skill;
952
1174
  try {
953
1175
  skill = content.loadSkill(name);
954
- } catch {
955
- log.warn(`"${name}" no longer exists in the content library.`);
1176
+ } catch (err) {
1177
+ if (err instanceof SkillNotFoundError) {
1178
+ log.warn(`"${name}" no longer exists in the content library.`);
1179
+ } else {
1180
+ log.error(`Failed to load "${name}": ${err instanceof Error ? err.message : String(err)}`);
1181
+ }
956
1182
  skipped.push(name);
957
1183
  continue;
958
1184
  }
@@ -967,15 +1193,14 @@ async function updateCommand(opts) {
967
1193
  const e = locked.emitted[t];
968
1194
  return e && diskHash(opts.cwd, e.files) !== e.hash;
969
1195
  });
970
- if (userEdited && !opts.overwrite && !opts.onConflict) {
1196
+ if (userEdited && !opts.overwrite) {
971
1197
  log.warn(`${prefix}"${name}" has local edits \u2014 skipping (use --overwrite to replace).`);
972
1198
  skipped.push(name);
973
1199
  continue;
974
1200
  }
975
1201
  const result = installSkill(opts.cwd, skill, targets, paths, {
976
1202
  dryRun: opts.dryRun,
977
- overwrite: opts.overwrite || !userEdited,
978
- onConflict: opts.onConflict
1203
+ overwrite: opts.overwrite || !userEdited
979
1204
  });
980
1205
  if (!opts.dryRun) lock.skills[name] = result.locked;
981
1206
  updated.push(name);
@@ -1009,6 +1234,11 @@ function removeCommand(opts) {
1009
1234
  continue;
1010
1235
  }
1011
1236
  const targets = (opts.targets?.length ? opts.targets : Object.keys(locked.emitted)).filter((t) => locked.emitted[t]);
1237
+ if (targets.length === 0) {
1238
+ log.warn(`"${name}" is not installed to ${opts.targets?.join(", ")} \u2014 nothing to remove.`);
1239
+ missing.push(name);
1240
+ continue;
1241
+ }
1012
1242
  const wholeRemoved = [];
1013
1243
  for (const t of targets) {
1014
1244
  const e = locked.emitted[t];
@@ -1034,9 +1264,44 @@ function removeCommand(opts) {
1034
1264
  return { removed, missing };
1035
1265
  }
1036
1266
 
1037
- // src/commands/doctor.ts
1267
+ // src/core/installed-state.ts
1038
1268
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
1039
1269
  import { join as join9 } from "path";
1270
+ var STATES = ["ok", "edited", "missing"];
1271
+ function worst(states) {
1272
+ return states.reduce(
1273
+ (acc, s) => STATES.indexOf(s) > STATES.indexOf(acc) ? s : acc,
1274
+ "ok"
1275
+ );
1276
+ }
1277
+ function diagnoseInstalled(cwd, lock) {
1278
+ const out = [];
1279
+ for (const name of Object.keys(lock.skills).sort()) {
1280
+ const locked = lock.skills[name];
1281
+ const targets = [];
1282
+ for (const [target, e] of Object.entries(locked.emitted)) {
1283
+ const missingFiles = e.files.filter((f) => !existsSync9(join9(cwd, f)));
1284
+ const edited = missingFiles.length === 0 && diskHash(cwd, e.files) !== e.hash;
1285
+ let missingBlock;
1286
+ if (e.block) {
1287
+ const abs = join9(cwd, e.block);
1288
+ if (!existsSync9(abs) || !hasBlock(readFileSync7(abs, "utf8"), name)) missingBlock = e.block;
1289
+ }
1290
+ const state = missingFiles.length > 0 || missingBlock ? "missing" : edited ? "edited" : "ok";
1291
+ targets.push({ target, missingFiles, edited, missingBlock, state });
1292
+ }
1293
+ targets.sort((a, b) => a.target.localeCompare(b.target));
1294
+ out.push({
1295
+ name,
1296
+ version: locked.version,
1297
+ targets,
1298
+ state: worst(targets.map((t) => t.state))
1299
+ });
1300
+ }
1301
+ return out;
1302
+ }
1303
+
1304
+ // src/commands/doctor.ts
1040
1305
  function doctorCommand(opts) {
1041
1306
  const problems = [];
1042
1307
  const note = (msg) => problems.push(msg);
@@ -1047,32 +1312,28 @@ function doctorCommand(opts) {
1047
1312
  log.info(`Config targets: ${cfg.targets.length ? cfg.targets.join(", ") : "(auto-detect)"}`);
1048
1313
  }
1049
1314
  const lock = loadLockfile(opts.cwd);
1050
- const names = Object.keys(lock.skills);
1051
- if (names.length === 0) {
1315
+ const diagnoses = diagnoseInstalled(opts.cwd, lock);
1316
+ if (diagnoses.length === 0) {
1052
1317
  log.info("No skills installed.");
1053
1318
  return { problems, ok: true };
1054
1319
  }
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}`);
1320
+ for (const skill of diagnoses) {
1321
+ for (const t of skill.targets) {
1322
+ for (const file of t.missingFiles) {
1323
+ note(`${skill.name}: missing ${t.target} file ${file}`);
1060
1324
  }
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
- }
1325
+ if (t.edited) {
1326
+ note(
1327
+ `${skill.name}: local edits to ${t.target} output(s) (run \`update --overwrite\` to reset)`
1328
+ );
1065
1329
  }
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
- }
1330
+ if (t.missingBlock) {
1331
+ note(`${skill.name}: missing managed block in ${t.missingBlock}`);
1071
1332
  }
1072
1333
  }
1073
1334
  }
1074
1335
  if (problems.length === 0) {
1075
- log.success(`All ${names.length} installed skill(s) look healthy.`);
1336
+ log.success(`All ${diagnoses.length} installed skill(s) look healthy.`);
1076
1337
  } else {
1077
1338
  for (const p of problems) log.warn(p);
1078
1339
  log.plain(`
@@ -1081,6 +1342,211 @@ ${problems.length} problem(s) found.`);
1081
1342
  return { problems, ok: problems.length === 0 };
1082
1343
  }
1083
1344
 
1345
+ // src/commands/status.ts
1346
+ var MARK = { ok: "ok", edited: "edited", missing: "missing" };
1347
+ function statusCommand(opts) {
1348
+ const cfg = loadConfig(opts.cwd);
1349
+ const lock = loadLockfile(opts.cwd);
1350
+ let skills = diagnoseInstalled(opts.cwd, lock);
1351
+ if (opts.names?.length) {
1352
+ const want = new Set(opts.names);
1353
+ skills = skills.filter((s) => want.has(s.name));
1354
+ }
1355
+ if (opts.problemsOnly) skills = skills.filter((s) => s.state !== "ok");
1356
+ const counts = { ok: 0, edited: 0, missing: 0 };
1357
+ for (const s of skills) counts[s.state]++;
1358
+ const report = {
1359
+ contentRef: lock.contentRef,
1360
+ configuredTargets: cfg?.targets ?? [],
1361
+ skills,
1362
+ counts
1363
+ };
1364
+ if (opts.json) {
1365
+ log.plain(JSON.stringify(report, null, 2));
1366
+ return report;
1367
+ }
1368
+ if (Object.keys(lock.skills).length === 0) {
1369
+ log.info("No skills installed \u2014 run `skills-master add <name>`.");
1370
+ return report;
1371
+ }
1372
+ if (skills.length === 0) {
1373
+ log.info(opts.problemsOnly ? "Nothing needs attention." : "No installed skills match.");
1374
+ return report;
1375
+ }
1376
+ log.info(
1377
+ `${skills.length} skill(s) from ref ${report.contentRef}` + (report.configuredTargets.length ? ` \xB7 config targets: ${report.configuredTargets.join(", ")}` : "")
1378
+ );
1379
+ log.plain("");
1380
+ const nameWidth = Math.max(...skills.map((s) => s.name.length));
1381
+ const verWidth = Math.max(...skills.map((s) => s.version.length + 1));
1382
+ for (const s of skills) {
1383
+ const targets = s.targets.map((t) => t.state === "ok" ? t.target : `${t.target} (${MARK[t.state]})`).join(", ");
1384
+ log.plain(
1385
+ ` ${s.name.padEnd(nameWidth)} ${`v${s.version}`.padEnd(verWidth)} ${MARK[s.state].padEnd(7)} ${targets}`
1386
+ );
1387
+ }
1388
+ log.plain("");
1389
+ const parts = [`${counts.ok} ok`];
1390
+ if (counts.edited) parts.push(`${counts.edited} edited`);
1391
+ if (counts.missing) parts.push(`${counts.missing} missing`);
1392
+ log.plain(parts.join(", ") + ".");
1393
+ if (counts.edited || counts.missing) {
1394
+ log.plain("Run `skills-master doctor` for detail, or `update --overwrite` to reset.");
1395
+ }
1396
+ return report;
1397
+ }
1398
+
1399
+ // src/commands/sync.ts
1400
+ async function syncCommand(opts) {
1401
+ const cfg = loadConfigOrDefault(opts.cwd);
1402
+ const lock = loadLockfile(opts.cwd);
1403
+ const targets = resolveTargets(opts.cwd, cfg.targets);
1404
+ const paths = resolvePaths(cfg);
1405
+ const prefix = opts.dryRun ? "[dry-run] " : "";
1406
+ const all = Object.keys(lock.skills).sort();
1407
+ const names = opts.names?.length ? all.filter((n) => opts.names.includes(n)) : all;
1408
+ const result = {
1409
+ synced: [],
1410
+ skipped: [],
1411
+ addedTargets: [],
1412
+ orphaned: [],
1413
+ stale: [],
1414
+ pruned: false
1415
+ };
1416
+ if (all.length === 0) {
1417
+ log.info("No skills installed \u2014 run `skills-master add <name>`.");
1418
+ return result;
1419
+ }
1420
+ if (names.length === 0) {
1421
+ log.warn("No installed skills match.");
1422
+ return result;
1423
+ }
1424
+ const edited = new Map(
1425
+ diagnoseInstalled(opts.cwd, lock).map((d) => [d.name, d.targets.some((t) => t.edited)])
1426
+ );
1427
+ const content = await resolveContent({
1428
+ content: opts.content,
1429
+ ref: opts.ref ?? cfg.contentRef,
1430
+ cwd: opts.cwd
1431
+ });
1432
+ log.info(`Syncing ${names.length} skill(s) to targets: ${targets.join(", ")}`);
1433
+ const addedTargets = /* @__PURE__ */ new Set();
1434
+ const pruneNames = [];
1435
+ const pruneTargets = /* @__PURE__ */ new Set();
1436
+ for (const name of names) {
1437
+ const locked = lock.skills[name];
1438
+ const installedTo = Object.keys(locked.emitted);
1439
+ const orphans = installedTo.filter((t) => !targets.includes(t));
1440
+ if (orphans.length) {
1441
+ result.orphaned.push({ name, targets: orphans });
1442
+ pruneNames.push(name);
1443
+ for (const t of orphans) pruneTargets.add(t);
1444
+ }
1445
+ if (edited.get(name) && !opts.overwrite) {
1446
+ log.warn(`${prefix}"${name}" has local edits \u2014 skipping (use --overwrite to replace).`);
1447
+ result.skipped.push(name);
1448
+ continue;
1449
+ }
1450
+ let skill;
1451
+ try {
1452
+ skill = content.loadSkill(name);
1453
+ } catch (err) {
1454
+ if (err instanceof SkillNotFoundError) {
1455
+ log.warn(`"${name}" no longer exists in the content library \u2014 leaving it in place.`);
1456
+ } else {
1457
+ log.error(`Failed to load "${name}": ${err instanceof Error ? err.message : String(err)}`);
1458
+ }
1459
+ result.skipped.push(name);
1460
+ continue;
1461
+ }
1462
+ for (const t of targets) if (!locked.emitted[t]) addedTargets.add(t);
1463
+ const prevFiles = /* @__PURE__ */ new Set();
1464
+ const prevBlocks = /* @__PURE__ */ new Map();
1465
+ for (const t of targets) {
1466
+ const e = locked.emitted[t];
1467
+ if (!e) continue;
1468
+ for (const f of e.files) prevFiles.add(f);
1469
+ if (e.block) prevBlocks.set(t, e.block);
1470
+ }
1471
+ const emitted = installSkill(opts.cwd, skill, targets, paths, {
1472
+ dryRun: opts.dryRun,
1473
+ overwrite: true
1474
+ // edits were already checked above
1475
+ });
1476
+ const nowFiles = /* @__PURE__ */ new Set();
1477
+ for (const e of Object.values(emitted.locked.emitted)) {
1478
+ for (const f of e.files) nowFiles.add(f);
1479
+ }
1480
+ const staleFiles = [...prevFiles].filter((f) => !nowFiles.has(f));
1481
+ const staleBlocks = [...prevBlocks].filter(([t, b]) => emitted.locked.emitted[t]?.block !== b).map(([, b]) => b);
1482
+ if (staleFiles.length || staleBlocks.length) {
1483
+ result.stale.push({ name, files: staleFiles, blocks: staleBlocks });
1484
+ }
1485
+ if (!opts.dryRun) {
1486
+ lock.skills[name] = {
1487
+ ...emitted.locked,
1488
+ emitted: { ...locked.emitted, ...emitted.locked.emitted }
1489
+ };
1490
+ }
1491
+ result.synced.push(name);
1492
+ for (const r of emitted.results) {
1493
+ if (r.action === "unchanged") continue;
1494
+ const tag = r.mode === "block" ? `${r.path} [${r.blockId}]` : r.path;
1495
+ log.info(`${prefix}${r.action.padEnd(9)} ${tag}`);
1496
+ }
1497
+ }
1498
+ result.addedTargets = [...addedTargets].sort();
1499
+ if (!opts.dryRun) saveLockfile(opts.cwd, lock);
1500
+ if (result.orphaned.length) {
1501
+ const list = [...pruneTargets].sort().join(", ");
1502
+ if (opts.prune) {
1503
+ removeCommand({
1504
+ cwd: opts.cwd,
1505
+ names: pruneNames,
1506
+ targets: [...pruneTargets],
1507
+ dryRun: opts.dryRun
1508
+ });
1509
+ result.pruned = true;
1510
+ } else {
1511
+ log.warn(
1512
+ `${result.orphaned.length} skill(s) still have output for target(s) the config no longer lists: ${list}. Re-run with --prune to remove.`
1513
+ );
1514
+ }
1515
+ }
1516
+ if (result.stale.length) {
1517
+ const gone = [];
1518
+ for (const f of result.stale.flatMap((s) => s.files)) {
1519
+ if (removeWholeFile(opts.cwd, f, opts.dryRun)) {
1520
+ gone.push(f);
1521
+ log.info(`${prefix}moved-from ${f}`);
1522
+ }
1523
+ }
1524
+ pruneEmptyDirs(opts.cwd, gone, opts.dryRun);
1525
+ for (const s of result.stale) {
1526
+ for (const b of s.blocks) {
1527
+ if (removeBlockFromFile(opts.cwd, b, s.name, opts.dryRun)) {
1528
+ log.info(`${prefix}unblocked ${b} [${s.name}]`);
1529
+ }
1530
+ }
1531
+ }
1532
+ }
1533
+ const bits = [`${result.synced.length} synced`];
1534
+ if (result.addedTargets.length) bits.push(`new target(s): ${result.addedTargets.join(", ")}`);
1535
+ if (result.skipped.length) bits.push(`${result.skipped.length} skipped`);
1536
+ log.success(`${prefix}${bits.join(", ")}.`);
1537
+ return result;
1538
+ }
1539
+
1540
+ // src/core/search-text.ts
1541
+ function searchNormalize(value) {
1542
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
1543
+ }
1544
+ function searchableText(parts) {
1545
+ return searchNormalize(
1546
+ [parts.name, parts.description, parts.domain, parts.category, parts.class].join(" ")
1547
+ );
1548
+ }
1549
+
1084
1550
  // src/commands/catalog.ts
1085
1551
  async function registryOf(q) {
1086
1552
  const content = await resolveContent({ content: q.content, ref: q.ref, cwd: q.cwd });
@@ -1124,9 +1590,11 @@ ${skills.length} skill(s).`);
1124
1590
  }
1125
1591
  async function searchCommand(opts) {
1126
1592
  const reg = await registryOf(opts);
1127
- const q = opts.query.toLowerCase();
1593
+ const q = searchNormalize(opts.query);
1128
1594
  const hits = reg.skills.filter(
1129
- (s) => [s.name, s.description, s.domain, s.category, s.class, ...s.tags].join(" ").toLowerCase().includes(q)
1595
+ (s) => searchNormalize(
1596
+ [s.name, s.description, s.domain, s.category, s.class, ...s.tags].join(" ")
1597
+ ).includes(q)
1130
1598
  );
1131
1599
  if (hits.length === 0) {
1132
1600
  log.info(`No matches for "${opts.query}".`);
@@ -1148,10 +1616,14 @@ async function viewCommand(opts) {
1148
1616
  log.plain(skill.body);
1149
1617
  return;
1150
1618
  }
1151
- log.plain(`${skill.name} v${xm.version} [${xm.domain}/${xm.class}/${xm.category}] ${xm.stability}`);
1619
+ log.plain(
1620
+ `${skill.name} v${xm.version} [${xm.domain}/${xm.class}/${xm.category}] ${xm.stability}`
1621
+ );
1152
1622
  log.plain(`platforms: ${xm.platforms.join(", ")}`);
1153
1623
  if (xm.requires) {
1154
- log.plain(`requires: ${Object.entries(xm.requires).map(([k, v]) => `${k} ${v}`).join(", ")}`);
1624
+ log.plain(
1625
+ `requires: ${Object.entries(xm.requires).map(([k, v]) => `${k} ${v}`).join(", ")}`
1626
+ );
1155
1627
  }
1156
1628
  if (xm.pairs_with.length) log.plain(`pairs with: ${xm.pairs_with.join(", ")}`);
1157
1629
  log.plain(`
@@ -1212,10 +1684,21 @@ function lintSkills(skillsRoot) {
1212
1684
  for (const issue of v.issues) {
1213
1685
  diagnostics.push({ relPath: raw.relPath, level: "error", message: issue });
1214
1686
  }
1215
- loaded.push({ relPath: raw.relPath, folderName: raw.folderName, body: raw.body });
1687
+ loaded.push({
1688
+ relPath: raw.relPath,
1689
+ folderName: raw.folderName,
1690
+ body: raw.body,
1691
+ resources: raw.resources
1692
+ });
1216
1693
  continue;
1217
1694
  }
1218
- loaded.push({ relPath: raw.relPath, folderName: raw.folderName, fm: v.value, body: raw.body });
1695
+ loaded.push({
1696
+ relPath: raw.relPath,
1697
+ folderName: raw.folderName,
1698
+ fm: v.value,
1699
+ body: raw.body,
1700
+ resources: raw.resources
1701
+ });
1219
1702
  byName.set(v.value.name, v.value);
1220
1703
  }
1221
1704
  const nameDirs = /* @__PURE__ */ new Map();
@@ -1226,7 +1709,11 @@ function lintSkills(skillsRoot) {
1226
1709
  for (const [name, paths] of nameDirs) {
1227
1710
  if (paths.length > 1) {
1228
1711
  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(", ")})` });
1712
+ diagnostics.push({
1713
+ relPath: p,
1714
+ level: "error",
1715
+ message: `duplicate skill name "${name}" (also at ${paths.filter((x) => x !== p).join(", ")})`
1716
+ });
1230
1717
  }
1231
1718
  }
1232
1719
  }
@@ -1245,17 +1732,58 @@ function lintSkills(skillsRoot) {
1245
1732
  if (xm.snapshot_date > todayStr) {
1246
1733
  push("error", `snapshot_date ${xm.snapshot_date} is in the future`);
1247
1734
  }
1248
- if (xm.stability === "contested" && !/^## Open question\b/m.test(s.body)) {
1735
+ const hasOpenQuestion = /^## Open question\b/m.test(s.body);
1736
+ if (xm.stability === "contested" && !hasOpenQuestion) {
1249
1737
  push("error", `stability is "contested" but no "## Open question" section is present`);
1738
+ } else if (xm.stability !== "contested" && hasOpenQuestion) {
1739
+ push(
1740
+ "error",
1741
+ `"## Open question" is reserved for stability: contested skills \u2014 retitle the section or mark the skill contested`
1742
+ );
1743
+ }
1744
+ if (xm.sources.length > 3) {
1745
+ push("warn", `${xm.sources.length} sources \u2014 keep at most 3 canonical citation URLs`);
1746
+ }
1747
+ const findable = searchableText({
1748
+ name: fm.name,
1749
+ description: fm.description,
1750
+ domain: xm.domain,
1751
+ category: xm.category,
1752
+ class: xm.class
1753
+ });
1754
+ for (const tag of fm.tags ?? []) {
1755
+ if (findable.includes(searchNormalize(tag))) {
1756
+ push("warn", `tag "${tag}" is already in the name or description \u2014 it adds no search term`);
1757
+ }
1758
+ }
1759
+ const l3 = [
1760
+ ["reference", "reference.md"],
1761
+ ["examples", "examples.md"],
1762
+ ["checklist", "checklist.md"]
1763
+ ];
1764
+ for (const [key, file] of l3) {
1765
+ if (s.resources?.[key] != null && !s.body.includes(`(${file}`) && !s.body.includes(`(./${file}`)) {
1766
+ push("warn", `${file} exists but is never linked from the SKILL.md body`);
1767
+ }
1250
1768
  }
1251
1769
  const lineCount = s.body.split("\n").length;
1252
1770
  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`);
1771
+ push(
1772
+ "error",
1773
+ `SKILL.md body is ${lineCount} lines (max ${MAX_BODY_LINES}); move depth into reference.md/examples.md`
1774
+ );
1254
1775
  } else if (lineCount > WARN_BODY_LINES) {
1255
- push("warn", `SKILL.md body is ${lineCount} lines (approaching the ${MAX_BODY_LINES}-line cap)`);
1776
+ push(
1777
+ "warn",
1778
+ `SKILL.md body is ${lineCount} lines (approaching the ${MAX_BODY_LINES}-line cap)`
1779
+ );
1256
1780
  }
1257
- if (!s.relPath.startsWith(`${xm.domain}/`)) {
1258
- push("warn", `domain "${xm.domain}" does not match the top folder of "${s.relPath}"`);
1781
+ const expectedPath = `${xm.domain}/${CLASS_DIR[xm.class]}/${xm.category}/${fm.name}`;
1782
+ if (s.relPath !== expectedPath) {
1783
+ push(
1784
+ "error",
1785
+ `on-disk path "${s.relPath}" must be "${expectedPath}" (domain/class dir/category/name from frontmatter)`
1786
+ );
1259
1787
  }
1260
1788
  if (xm.sources.length === 0) {
1261
1789
  push("warn", `no sources \u2014 add at least one canonical documentation URL`);
@@ -1270,6 +1798,12 @@ function lintSkills(skillsRoot) {
1270
1798
  push("warn", `missing recommended section "${heading}"`);
1271
1799
  }
1272
1800
  }
1801
+ if (xm.pairs_with.length > 4) {
1802
+ push(
1803
+ "warn",
1804
+ `${xm.pairs_with.length} pairs_with entries \u2014 keep at most 4; put wider cross-references in "## See also"`
1805
+ );
1806
+ }
1273
1807
  for (const partner of xm.pairs_with) {
1274
1808
  const partnerFm = byName.get(partner);
1275
1809
  if (!partnerFm) {
@@ -1277,7 +1811,10 @@ function lintSkills(skillsRoot) {
1277
1811
  continue;
1278
1812
  }
1279
1813
  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)`);
1814
+ push(
1815
+ "error",
1816
+ `pairs_with "${partner}" is not reciprocated (add "${fm.name}" to its pairs_with)`
1817
+ );
1281
1818
  }
1282
1819
  }
1283
1820
  }
@@ -1305,39 +1842,6 @@ Linted ${result.skillCount} skill(s): ${result.errorCount} error(s), ${result.wa
1305
1842
  // src/commands/registry.ts
1306
1843
  import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
1307
1844
  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
1845
  async function registryBuildCommand(opts) {
1342
1846
  const content = await resolveContent({ content: opts.content, cwd: opts.cwd });
1343
1847
  const registry2 = buildRegistry(content.root, opts.version ?? "0.1.0");
@@ -1433,7 +1937,13 @@ function buildOutputs(content, out, version2) {
1433
1937
  `${PLUGINS_DIR}/${name}/.claude-plugin/plugin.json`,
1434
1938
  JSON.stringify(manifest, null, 2) + "\n"
1435
1939
  );
1436
- plugins.push({ name, source: `./${PLUGINS_DIR}/${name}`, description, version: version2, category: CLASS_CATEGORY[cls] });
1940
+ plugins.push({
1941
+ name,
1942
+ source: `./${PLUGINS_DIR}/${name}`,
1943
+ description,
1944
+ version: version2,
1945
+ category: CLASS_CATEGORY[cls]
1946
+ });
1437
1947
  counts.push({ name, count });
1438
1948
  }
1439
1949
  plugins.sort((a, b) => a.name.localeCompare(b.name));
@@ -1480,7 +1990,11 @@ async function marketplaceBuildCommand(opts) {
1480
1990
  const stale = [...onDisk].filter((rel) => !files.has(rel)).sort();
1481
1991
  for (const rel of stale) rmSync2(join11(out, rel), { force: true });
1482
1992
  pruneEmptyDirsUnder(join11(out, PLUGINS_DIR));
1483
- const emitted = [...files].map(([path, contents]) => ({ path, contents, mode: "whole" }));
1993
+ const emitted = [...files].map(([path, contents]) => ({
1994
+ path,
1995
+ contents,
1996
+ mode: "whole"
1997
+ }));
1484
1998
  applyFiles(out, emitted, { overwrite: true });
1485
1999
  for (const { name, count } of counts) log.info(`Built ${name} (${count} skills).`);
1486
2000
  if (stale.length) log.info(`Removed ${stale.length} stale file(s).`);
@@ -1568,18 +2082,6 @@ async function newSkillCommand(opts) {
1568
2082
  }
1569
2083
 
1570
2084
  // src/bin.ts
1571
- var VALID = new Set(ALL_TARGETS);
1572
- function parseTargets(value) {
1573
- if (!value) return void 0;
1574
- if (value === "all") return ALL_TARGETS;
1575
- const ids = value.split(",").map((s) => s.trim()).filter(Boolean);
1576
- for (const id of ids) {
1577
- if (!VALID.has(id)) {
1578
- throw new Error(`Unknown target "${id}". Valid: ${[...VALID, "all"].join(", ")}.`);
1579
- }
1580
- }
1581
- return ids;
1582
- }
1583
2085
  async function run(fn, exitOnFalse = false) {
1584
2086
  try {
1585
2087
  const result = await fn();
@@ -1602,7 +2104,7 @@ program.command("init").description("Detect tools and write skills-master.json")
1602
2104
  })
1603
2105
  )
1604
2106
  );
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(
2107
+ 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
2108
  (opts) => run(
1607
2109
  () => listCommand({
1608
2110
  cwd: process.cwd(),
@@ -1616,15 +2118,21 @@ program.command("list").description("List available skills").option("--domain <d
1616
2118
  })
1617
2119
  )
1618
2120
  );
1619
- program.command("search <query>").description("Search skills by name, description, tags").option("--content <dir>").option("--ref <ref>").action(
2121
+ 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
2122
  (query, opts) => run(() => searchCommand({ cwd: process.cwd(), query, content: opts.content, ref: opts.ref }))
1621
2123
  );
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(
2124
+ 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
2125
  (name, opts) => run(
1624
- () => viewCommand({ cwd: process.cwd(), name, raw: opts.raw, content: opts.content, ref: opts.ref })
2126
+ () => viewCommand({
2127
+ cwd: process.cwd(),
2128
+ name,
2129
+ raw: opts.raw,
2130
+ content: opts.content,
2131
+ ref: opts.ref
2132
+ })
1625
2133
  )
1626
2134
  );
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(
2135
+ 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
2136
  (names, opts) => run(
1629
2137
  () => addCommand({
1630
2138
  cwd: process.cwd(),
@@ -1638,7 +2146,7 @@ program.command("add <names...>").description("Install skills (by name, category
1638
2146
  })
1639
2147
  )
1640
2148
  );
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(
2149
+ 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
2150
  (names, opts) => run(
1643
2151
  () => updateCommand({
1644
2152
  cwd: process.cwd(),
@@ -1650,35 +2158,72 @@ program.command("update [names...]").description("Re-install skills whose conten
1650
2158
  })
1651
2159
  )
1652
2160
  );
1653
- program.command("remove <names...>").description("Remove installed skills").option("--target <list>", "comma list or 'all'").option("--dry-run").action(
2161
+ program.command("remove <names...>").description("Remove installed skills").option("--target <list>", "comma list or 'all'").option("--dry-run", "preview without writing").action(
1654
2162
  (names, opts) => run(
1655
- () => removeCommand({ cwd: process.cwd(), names, targets: parseTargets(opts.target), dryRun: opts.dryRun })
2163
+ () => removeCommand({
2164
+ cwd: process.cwd(),
2165
+ names,
2166
+ targets: parseTargets(opts.target),
2167
+ dryRun: opts.dryRun
2168
+ })
1656
2169
  )
1657
2170
  );
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 }))
2171
+ program.command("sync").description("Re-emit installed skills to match the current config (new targets, moved paths)").argument("[names...]", "limit the sync to these installed skills").option("--content <dir>", "local skills directory").option("--ref <git-ref>", "content ref to read from").option("--overwrite", "replace locally edited outputs").option("--prune", "delete outputs for targets the config no longer lists").option("--dry-run", "show what would change without writing").action(
2172
+ (names, opts) => run(
2173
+ () => syncCommand({
2174
+ cwd: process.cwd(),
2175
+ names,
2176
+ content: opts.content,
2177
+ ref: opts.ref,
2178
+ overwrite: opts.overwrite,
2179
+ prune: opts.prune,
2180
+ dryRun: opts.dryRun
2181
+ })
2182
+ )
2183
+ );
2184
+ 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(
2185
+ (names, opts) => run(
2186
+ () => statusCommand({
2187
+ cwd: process.cwd(),
2188
+ names,
2189
+ problemsOnly: opts.problems,
2190
+ json: opts.json
2191
+ })
2192
+ )
2193
+ );
2194
+ program.command("doctor").description("Check installed skills for drift and missing files").action(() => run(() => doctorCommand({ cwd: process.cwd() }).ok, true));
2195
+ 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));
2196
+ 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(
2197
+ (spec, opts) => run(
2198
+ () => newSkillCommand({ cwd: process.cwd(), spec, content: opts.content, force: opts.force })
2199
+ )
1662
2200
  );
1663
2201
  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(
2202
+ 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
2203
  (opts) => run(
1666
- () => registryBuildCommand({ cwd: process.cwd(), content: opts.content, check: opts.check, version: opts.version }),
2204
+ () => registryBuildCommand({
2205
+ cwd: process.cwd(),
2206
+ content: opts.content,
2207
+ check: opts.check,
2208
+ version: opts.setVersion
2209
+ }),
1667
2210
  true
1668
2211
  )
1669
2212
  );
1670
2213
  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(
2214
+ 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
2215
  (opts) => run(
1673
2216
  () => marketplaceBuildCommand({
1674
2217
  cwd: process.cwd(),
1675
2218
  content: opts.content,
1676
2219
  out: opts.out,
1677
2220
  check: opts.check,
1678
- version: opts.version
2221
+ version: opts.setVersion
1679
2222
  }),
1680
2223
  true
1681
2224
  )
1682
2225
  );
1683
- program.parseAsync(process.argv);
1684
- //# sourceMappingURL=bin.js.map
2226
+ program.parseAsync(process.argv).catch((err) => {
2227
+ log.error(err instanceof Error ? err.message : String(err));
2228
+ process.exitCode = 1;
2229
+ });