skills-master 0.1.0
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/README.md +42 -0
- package/dist/bin.js +1604 -0
- package/dist/bin.js.map +1 -0
- package/package.json +66 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1604 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/types.ts
|
|
7
|
+
var ALL_TARGETS = ["claude", "cursor", "copilot", "agents"];
|
|
8
|
+
var RESOURCE_FILES = {
|
|
9
|
+
reference: "reference.md",
|
|
10
|
+
examples: "examples.md",
|
|
11
|
+
checklist: "checklist.md"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/schema/projectConfig.ts
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var TargetIdSchema = z.enum(["claude", "cursor", "copilot", "agents"]);
|
|
17
|
+
var DEFAULT_PATHS = {
|
|
18
|
+
claude: ".claude/skills",
|
|
19
|
+
cursor: ".cursor/rules",
|
|
20
|
+
copilot: ".github",
|
|
21
|
+
agents: "AGENTS.md"
|
|
22
|
+
};
|
|
23
|
+
var ProjectConfigSchema = z.object({
|
|
24
|
+
$schema: z.string().optional(),
|
|
25
|
+
/** Git ref of the content repo to install from (tag/branch/sha). */
|
|
26
|
+
contentRef: z.string().default("main"),
|
|
27
|
+
/**
|
|
28
|
+
* Targets to emit. Empty array means "auto-detect on every run".
|
|
29
|
+
*/
|
|
30
|
+
targets: z.array(TargetIdSchema).default([]),
|
|
31
|
+
/** Per-target output path overrides (merged over DEFAULT_PATHS). */
|
|
32
|
+
paths: z.object({
|
|
33
|
+
claude: z.string(),
|
|
34
|
+
cursor: z.string(),
|
|
35
|
+
copilot: z.string(),
|
|
36
|
+
agents: z.string()
|
|
37
|
+
}).partial().default({}),
|
|
38
|
+
scope: z.enum(["project"]).default("project"),
|
|
39
|
+
/** true = commit generated files; false = add them to .gitignore. */
|
|
40
|
+
commit: z.boolean().default(true)
|
|
41
|
+
}).strict();
|
|
42
|
+
var CONFIG_FILENAME = "skills-master.json";
|
|
43
|
+
var LOCKFILE_NAME = "skills-master.lock.json";
|
|
44
|
+
function resolvePaths(cfg) {
|
|
45
|
+
return { ...DEFAULT_PATHS, ...cfg.paths };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/core/yaml.ts
|
|
49
|
+
import YAML from "yaml";
|
|
50
|
+
function toYaml(obj) {
|
|
51
|
+
return YAML.stringify(obj, { lineWidth: 0 }).trimEnd();
|
|
52
|
+
}
|
|
53
|
+
function frontmatterBlock(obj) {
|
|
54
|
+
return `---
|
|
55
|
+
${toYaml(obj)}
|
|
56
|
+
---`;
|
|
57
|
+
}
|
|
58
|
+
function withFrontmatter(obj, body) {
|
|
59
|
+
return `${frontmatterBlock(obj)}
|
|
60
|
+
|
|
61
|
+
${body.trim()}
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/emitters/util.ts
|
|
66
|
+
import { existsSync } from "fs";
|
|
67
|
+
import { join } from "path";
|
|
68
|
+
function existsRel(root, rel) {
|
|
69
|
+
return existsSync(join(root, rel));
|
|
70
|
+
}
|
|
71
|
+
var ACRONYMS = {
|
|
72
|
+
hig: "HIG",
|
|
73
|
+
ui: "UI",
|
|
74
|
+
ml: "ML",
|
|
75
|
+
ar: "AR",
|
|
76
|
+
av: "AV",
|
|
77
|
+
os: "OS",
|
|
78
|
+
ci: "CI",
|
|
79
|
+
cd: "CD",
|
|
80
|
+
url: "URL",
|
|
81
|
+
api: "API",
|
|
82
|
+
sf: "SF",
|
|
83
|
+
spm: "SPM",
|
|
84
|
+
ios: "iOS",
|
|
85
|
+
ipados: "iPadOS",
|
|
86
|
+
macos: "macOS",
|
|
87
|
+
tvos: "tvOS",
|
|
88
|
+
visionos: "visionOS",
|
|
89
|
+
watchos: "watchOS"
|
|
90
|
+
};
|
|
91
|
+
function titleFromName(name) {
|
|
92
|
+
return name.split("-").filter(Boolean).map((w) => ACRONYMS[w] ?? w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
93
|
+
}
|
|
94
|
+
function globsToString(fm) {
|
|
95
|
+
const g = fm.globs;
|
|
96
|
+
if (!g || g.length === 0) return void 0;
|
|
97
|
+
return g.join(",");
|
|
98
|
+
}
|
|
99
|
+
function hasResources(resources) {
|
|
100
|
+
return Boolean(resources.reference || resources.examples || resources.checklist);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/emitters/claude.ts
|
|
104
|
+
var claudeEmitter = {
|
|
105
|
+
id: "claude",
|
|
106
|
+
label: "Claude Code",
|
|
107
|
+
detect: (root) => existsRel(root, ".claude"),
|
|
108
|
+
emit(skill, ctx) {
|
|
109
|
+
const dir = `${ctx.paths.claude}/${skill.name}`;
|
|
110
|
+
const fm = {
|
|
111
|
+
name: skill.frontmatter.name,
|
|
112
|
+
description: skill.frontmatter.description
|
|
113
|
+
};
|
|
114
|
+
const files = [
|
|
115
|
+
{
|
|
116
|
+
path: `${dir}/SKILL.md`,
|
|
117
|
+
contents: withFrontmatter(fm, skill.body),
|
|
118
|
+
mode: "whole"
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
for (const key of Object.keys(RESOURCE_FILES)) {
|
|
122
|
+
const text = skill.resources[key];
|
|
123
|
+
if (text) {
|
|
124
|
+
files.push({
|
|
125
|
+
path: `${dir}/${RESOURCE_FILES[key]}`,
|
|
126
|
+
contents: text.trimEnd() + "\n",
|
|
127
|
+
mode: "whole"
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return files;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// src/core/condense.ts
|
|
136
|
+
var L3_LINK_RE = /\[([^\]]+)\]\((?:\.\/)?(reference|examples|checklist)\.md(?:#[^)]*)?\)/g;
|
|
137
|
+
var DEFAULT_NOTE = "Extended reference and worked examples are available in the full Claude Code skill for this topic.";
|
|
138
|
+
function condenseBody(body, opts = {}) {
|
|
139
|
+
let out = body;
|
|
140
|
+
let strippedLink = false;
|
|
141
|
+
out = out.replace(L3_LINK_RE, (_m, text) => {
|
|
142
|
+
strippedLink = true;
|
|
143
|
+
return text;
|
|
144
|
+
});
|
|
145
|
+
if (opts.openQuestion === "summarize") {
|
|
146
|
+
out = summarizeOpenQuestion(out);
|
|
147
|
+
}
|
|
148
|
+
out = out.replace(/\n{3,}/g, "\n\n").trim();
|
|
149
|
+
if (opts.hadResources || strippedLink) {
|
|
150
|
+
out += `
|
|
151
|
+
|
|
152
|
+
> ${opts.fullSkillNote ?? DEFAULT_NOTE}`;
|
|
153
|
+
}
|
|
154
|
+
return out + "\n";
|
|
155
|
+
}
|
|
156
|
+
function summarizeOpenQuestion(body) {
|
|
157
|
+
const re = /^## Open question[ \t]*\n([\s\S]*?)(?=\n## |\s*$)/m;
|
|
158
|
+
return body.replace(re, (_m, section) => {
|
|
159
|
+
const firstPara = section.trim().split(/\n\s*\n/)[0]?.replace(/\s+/g, " ").trim() ?? "";
|
|
160
|
+
return `## Open question
|
|
161
|
+
|
|
162
|
+
Tradeoff: ${firstPara}
|
|
163
|
+
`;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/emitters/cursor.ts
|
|
168
|
+
var cursorEmitter = {
|
|
169
|
+
id: "cursor",
|
|
170
|
+
label: "Cursor",
|
|
171
|
+
detect: (root) => existsRel(root, ".cursor"),
|
|
172
|
+
emit(skill, ctx) {
|
|
173
|
+
const globs = globsToString(skill.frontmatter);
|
|
174
|
+
const fm = {
|
|
175
|
+
description: skill.frontmatter.description
|
|
176
|
+
};
|
|
177
|
+
if (globs) fm.globs = globs;
|
|
178
|
+
fm.alwaysApply = false;
|
|
179
|
+
const body = condenseBody(skill.body, {
|
|
180
|
+
openQuestion: "keep",
|
|
181
|
+
hadResources: hasResources(skill.resources)
|
|
182
|
+
});
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
path: `${ctx.paths.cursor}/${skill.name}.mdc`,
|
|
186
|
+
contents: withFrontmatter(fm, body),
|
|
187
|
+
mode: "whole"
|
|
188
|
+
}
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/emitters/copilot.ts
|
|
194
|
+
var copilotEmitter = {
|
|
195
|
+
id: "copilot",
|
|
196
|
+
label: "GitHub Copilot",
|
|
197
|
+
detect: (root) => existsRel(root, ".github"),
|
|
198
|
+
emit(skill, ctx) {
|
|
199
|
+
const base = ctx.paths.copilot;
|
|
200
|
+
const instructionsPath = `${base}/instructions/${skill.name}.instructions.md`;
|
|
201
|
+
const applyTo = globsToString(skill.frontmatter) ?? "**";
|
|
202
|
+
const fm = {
|
|
203
|
+
applyTo,
|
|
204
|
+
description: skill.frontmatter.description
|
|
205
|
+
};
|
|
206
|
+
const body = condenseBody(skill.body, {
|
|
207
|
+
openQuestion: "keep",
|
|
208
|
+
hadResources: hasResources(skill.resources)
|
|
209
|
+
});
|
|
210
|
+
const pointer = `For ${titleFromName(skill.name)} guidance, see \`${instructionsPath}\`.`;
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
path: instructionsPath,
|
|
214
|
+
contents: withFrontmatter(fm, body),
|
|
215
|
+
mode: "whole"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
path: `${base}/copilot-instructions.md`,
|
|
219
|
+
contents: pointer,
|
|
220
|
+
mode: "block",
|
|
221
|
+
blockId: skill.name,
|
|
222
|
+
blockVersion: skill.frontmatter["x-skills-master"].version
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/emitters/agents.ts
|
|
229
|
+
var agentsEmitter = {
|
|
230
|
+
id: "agents",
|
|
231
|
+
label: "AGENTS.md",
|
|
232
|
+
detect: (root) => existsRel(root, "AGENTS.md"),
|
|
233
|
+
emit(skill, ctx) {
|
|
234
|
+
const body = condenseBody(skill.body, {
|
|
235
|
+
openQuestion: "summarize",
|
|
236
|
+
hadResources: hasResources(skill.resources)
|
|
237
|
+
});
|
|
238
|
+
const section = `### ${titleFromName(skill.name)}
|
|
239
|
+
|
|
240
|
+
${body.trim()}`;
|
|
241
|
+
return [
|
|
242
|
+
{
|
|
243
|
+
path: ctx.paths.agents,
|
|
244
|
+
contents: section,
|
|
245
|
+
mode: "block",
|
|
246
|
+
blockId: skill.name,
|
|
247
|
+
blockVersion: skill.frontmatter["x-skills-master"].version
|
|
248
|
+
}
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/emitters/index.ts
|
|
254
|
+
var EMITTERS = [
|
|
255
|
+
claudeEmitter,
|
|
256
|
+
cursorEmitter,
|
|
257
|
+
copilotEmitter,
|
|
258
|
+
agentsEmitter
|
|
259
|
+
];
|
|
260
|
+
var BY_ID = new Map(EMITTERS.map((e) => [e.id, e]));
|
|
261
|
+
function getEmitter(id) {
|
|
262
|
+
return BY_ID.get(id);
|
|
263
|
+
}
|
|
264
|
+
function detectTargets(projectRoot) {
|
|
265
|
+
return EMITTERS.filter((e) => e.detect(projectRoot)).map((e) => e.id);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/core/project.ts
|
|
269
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
|
|
270
|
+
import { join as join2 } from "path";
|
|
271
|
+
|
|
272
|
+
// src/schema/lockfile.ts
|
|
273
|
+
import { z as z2 } from "zod";
|
|
274
|
+
var EmittedTargetSchema = z2.object({
|
|
275
|
+
/** whole-file outputs owned by this target for this skill. */
|
|
276
|
+
files: z2.array(z2.string()).default([]),
|
|
277
|
+
/** shared file this skill writes a managed block into (block-mode targets). */
|
|
278
|
+
block: z2.string().optional(),
|
|
279
|
+
/** sha256 of the concatenated emitted contents, for drift detection. */
|
|
280
|
+
hash: z2.string()
|
|
281
|
+
});
|
|
282
|
+
var LockedSkillSchema = z2.object({
|
|
283
|
+
/** resolved skill version at install time. */
|
|
284
|
+
version: z2.string(),
|
|
285
|
+
/** sha256 of the canonical SKILL.md + resource files at install time. */
|
|
286
|
+
sourceHash: z2.string(),
|
|
287
|
+
/** targets this skill was emitted to. */
|
|
288
|
+
emitted: z2.record(z2.string(), EmittedTargetSchema)
|
|
289
|
+
});
|
|
290
|
+
var LockfileSchema = z2.object({
|
|
291
|
+
lockfileVersion: z2.literal(1).default(1),
|
|
292
|
+
contentRef: z2.string().default("main"),
|
|
293
|
+
skills: z2.record(z2.string(), LockedSkillSchema).default({})
|
|
294
|
+
});
|
|
295
|
+
function emptyLockfile(contentRef = "main") {
|
|
296
|
+
return { lockfileVersion: 1, contentRef, skills: {} };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/core/project.ts
|
|
300
|
+
function configPath(root) {
|
|
301
|
+
return join2(root, CONFIG_FILENAME);
|
|
302
|
+
}
|
|
303
|
+
function lockfilePath(root) {
|
|
304
|
+
return join2(root, LOCKFILE_NAME);
|
|
305
|
+
}
|
|
306
|
+
function loadConfig(root) {
|
|
307
|
+
const p = configPath(root);
|
|
308
|
+
if (!existsSync2(p)) return null;
|
|
309
|
+
return ProjectConfigSchema.parse(JSON.parse(readFileSync(p, "utf8")));
|
|
310
|
+
}
|
|
311
|
+
function loadConfigOrDefault(root) {
|
|
312
|
+
return loadConfig(root) ?? ProjectConfigSchema.parse({});
|
|
313
|
+
}
|
|
314
|
+
function saveConfig(root, cfg) {
|
|
315
|
+
const withSchema = { $schema: "https://skills-master.dev/schema/config.json", ...cfg };
|
|
316
|
+
writeFileSync(configPath(root), JSON.stringify(withSchema, null, 2) + "\n", "utf8");
|
|
317
|
+
}
|
|
318
|
+
function loadLockfile(root) {
|
|
319
|
+
const p = lockfilePath(root);
|
|
320
|
+
if (!existsSync2(p)) return emptyLockfile();
|
|
321
|
+
return LockfileSchema.parse(JSON.parse(readFileSync(p, "utf8")));
|
|
322
|
+
}
|
|
323
|
+
function saveLockfile(root, lock) {
|
|
324
|
+
writeFileSync(lockfilePath(root), JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/util/log.ts
|
|
328
|
+
var quiet = false;
|
|
329
|
+
var sym = {
|
|
330
|
+
info: "\u2022",
|
|
331
|
+
ok: "\u2713",
|
|
332
|
+
warn: "!",
|
|
333
|
+
err: "\u2717"
|
|
334
|
+
};
|
|
335
|
+
var log = {
|
|
336
|
+
plain(msg) {
|
|
337
|
+
if (!quiet) console.log(msg);
|
|
338
|
+
},
|
|
339
|
+
info(msg) {
|
|
340
|
+
if (!quiet) console.log(`${sym.info} ${msg}`);
|
|
341
|
+
},
|
|
342
|
+
success(msg) {
|
|
343
|
+
if (!quiet) console.log(`${sym.ok} ${msg}`);
|
|
344
|
+
},
|
|
345
|
+
warn(msg) {
|
|
346
|
+
if (!quiet) console.warn(`${sym.warn} ${msg}`);
|
|
347
|
+
},
|
|
348
|
+
error(msg) {
|
|
349
|
+
console.error(`${sym.err} ${msg}`);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/commands/init.ts
|
|
354
|
+
function initCommand(opts) {
|
|
355
|
+
const existing = loadConfig(opts.cwd);
|
|
356
|
+
if (existing && !opts.force) {
|
|
357
|
+
log.warn(`skills-master.json already exists \u2014 leaving it untouched (use --force to overwrite).`);
|
|
358
|
+
return existing;
|
|
359
|
+
}
|
|
360
|
+
let targets = opts.targets;
|
|
361
|
+
if (!targets || targets.length === 0) {
|
|
362
|
+
const detected = detectTargets(opts.cwd);
|
|
363
|
+
if (detected.length > 0) {
|
|
364
|
+
targets = detected;
|
|
365
|
+
log.info(`Detected tools: ${detected.join(", ")}`);
|
|
366
|
+
} else {
|
|
367
|
+
targets = ALL_TARGETS;
|
|
368
|
+
log.info(`No tools detected \u2014 defaulting to all targets (${ALL_TARGETS.join(", ")}).`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const cfg = ProjectConfigSchema.parse({
|
|
372
|
+
contentRef: opts.contentRef ?? "main",
|
|
373
|
+
targets,
|
|
374
|
+
commit: opts.commit ?? true
|
|
375
|
+
});
|
|
376
|
+
saveConfig(opts.cwd, cfg);
|
|
377
|
+
log.success(`Wrote skills-master.json (targets: ${targets.join(", ")}, commit: ${cfg.commit}).`);
|
|
378
|
+
return cfg;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/content/source.ts
|
|
382
|
+
import { existsSync as existsSync5 } from "fs";
|
|
383
|
+
import { homedir } from "os";
|
|
384
|
+
import { basename as basename2, isAbsolute, join as join5, resolve } from "path";
|
|
385
|
+
|
|
386
|
+
// src/core/discover.ts
|
|
387
|
+
import { existsSync as existsSync3, readdirSync, statSync } from "fs";
|
|
388
|
+
import { join as join3, relative } from "path";
|
|
389
|
+
var SKILL_FILE = "SKILL.md";
|
|
390
|
+
var IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".cache"]);
|
|
391
|
+
function findSkillDirs(skillsRoot) {
|
|
392
|
+
const found = [];
|
|
393
|
+
if (!existsSync3(skillsRoot)) return found;
|
|
394
|
+
const walk = (dir) => {
|
|
395
|
+
let entries;
|
|
396
|
+
try {
|
|
397
|
+
entries = readdirSync(dir);
|
|
398
|
+
} catch {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (entries.includes(SKILL_FILE)) found.push(dir);
|
|
402
|
+
for (const entry of entries) {
|
|
403
|
+
if (IGNORE.has(entry) || entry.startsWith(".")) continue;
|
|
404
|
+
const full = join3(dir, entry);
|
|
405
|
+
let isDir = false;
|
|
406
|
+
try {
|
|
407
|
+
isDir = statSync(full).isDirectory();
|
|
408
|
+
} catch {
|
|
409
|
+
isDir = false;
|
|
410
|
+
}
|
|
411
|
+
if (isDir) walk(full);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
walk(skillsRoot);
|
|
415
|
+
return found.sort();
|
|
416
|
+
}
|
|
417
|
+
function relPathOf(skillsRoot, dir) {
|
|
418
|
+
return relative(skillsRoot, dir).split(/[\\/]/).join("/");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/core/parse.ts
|
|
422
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
423
|
+
import { basename, join as join4 } from "path";
|
|
424
|
+
import matter from "gray-matter";
|
|
425
|
+
|
|
426
|
+
// src/schema/frontmatter.ts
|
|
427
|
+
import { z as z3 } from "zod";
|
|
428
|
+
import semver from "semver";
|
|
429
|
+
var NAME_RE = /^[a-z0-9-]{1,64}$/;
|
|
430
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
431
|
+
var StabilitySchema = z3.enum(["stable", "emerging", "contested"]);
|
|
432
|
+
var SkillClassSchema = z3.enum([
|
|
433
|
+
"code",
|
|
434
|
+
"design",
|
|
435
|
+
"lang-tooling",
|
|
436
|
+
"overview"
|
|
437
|
+
]);
|
|
438
|
+
var CLASS_DIR = {
|
|
439
|
+
code: "code",
|
|
440
|
+
design: "design",
|
|
441
|
+
"lang-tooling": "lang-tooling",
|
|
442
|
+
overview: "overviews"
|
|
443
|
+
};
|
|
444
|
+
var XSkillsMasterSchema = z3.object({
|
|
445
|
+
domain: z3.string().min(1),
|
|
446
|
+
class: SkillClassSchema,
|
|
447
|
+
category: z3.string().min(1),
|
|
448
|
+
platforms: z3.array(z3.string().min(1)).min(1),
|
|
449
|
+
/** domain-defined version requirements, e.g. { ios: "17", swift: "6.0" }. */
|
|
450
|
+
requires: z3.record(z3.string(), z3.string()).optional(),
|
|
451
|
+
pairs_with: z3.array(z3.string().regex(NAME_RE)).default([]),
|
|
452
|
+
/** citation URLs to canonical docs — never verbatim content. */
|
|
453
|
+
sources: z3.array(z3.string().url()).default([]),
|
|
454
|
+
snapshot_date: z3.string().regex(ISO_DATE_RE, "must be an ISO date (YYYY-MM-DD)"),
|
|
455
|
+
stability: StabilitySchema,
|
|
456
|
+
version: z3.string().refine((v) => semver.valid(v) != null, "must be a valid semver version")
|
|
457
|
+
}).strict();
|
|
458
|
+
var GlobsSchema = z3.union([z3.string(), z3.array(z3.string())]).transform((g) => Array.isArray(g) ? g : [g]).optional();
|
|
459
|
+
var FrontmatterSchema = z3.object({
|
|
460
|
+
name: z3.string().regex(NAME_RE, "must be kebab-case ([a-z0-9-], <=64 chars)"),
|
|
461
|
+
description: z3.string().min(1, "description is required").max(1024, "description must be <= 1024 characters"),
|
|
462
|
+
globs: GlobsSchema,
|
|
463
|
+
tags: z3.array(z3.string()).default([]),
|
|
464
|
+
"x-skills-master": XSkillsMasterSchema
|
|
465
|
+
}).passthrough();
|
|
466
|
+
|
|
467
|
+
// src/core/parse.ts
|
|
468
|
+
function loadRawSkill(dir, skillsRoot) {
|
|
469
|
+
const skillMdPath = join4(dir, "SKILL.md");
|
|
470
|
+
const rawText = readFileSync2(skillMdPath, "utf8");
|
|
471
|
+
const parsed = matter(rawText);
|
|
472
|
+
const resources = {};
|
|
473
|
+
for (const key of Object.keys(RESOURCE_FILES)) {
|
|
474
|
+
const p = join4(dir, RESOURCE_FILES[key]);
|
|
475
|
+
if (existsSync4(p)) resources[key] = readFileSync2(p, "utf8");
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
dir,
|
|
479
|
+
relPath: relPathOf(skillsRoot, dir),
|
|
480
|
+
folderName: basename(dir),
|
|
481
|
+
data: parsed.data,
|
|
482
|
+
body: parsed.content.replace(/^\n+/, "").trimEnd(),
|
|
483
|
+
resources,
|
|
484
|
+
skillMdPath,
|
|
485
|
+
rawText
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
var SkillValidationError = class extends Error {
|
|
489
|
+
constructor(relPath, issues) {
|
|
490
|
+
super(`Invalid skill "${relPath}":
|
|
491
|
+
- ${issues.join("\n - ")}`);
|
|
492
|
+
this.relPath = relPath;
|
|
493
|
+
this.issues = issues;
|
|
494
|
+
this.name = "SkillValidationError";
|
|
495
|
+
}
|
|
496
|
+
relPath;
|
|
497
|
+
issues;
|
|
498
|
+
};
|
|
499
|
+
function validateFrontmatter(data) {
|
|
500
|
+
const result = FrontmatterSchema.safeParse(data);
|
|
501
|
+
if (result.success) return { ok: true, value: result.data };
|
|
502
|
+
const issues = result.error.issues.map((i) => {
|
|
503
|
+
const path = i.path.join(".");
|
|
504
|
+
return path ? `${path}: ${i.message}` : i.message;
|
|
505
|
+
});
|
|
506
|
+
return { ok: false, issues };
|
|
507
|
+
}
|
|
508
|
+
function loadSkill(dir, skillsRoot) {
|
|
509
|
+
const raw = loadRawSkill(dir, skillsRoot);
|
|
510
|
+
const validated = validateFrontmatter(raw.data);
|
|
511
|
+
if (!validated.ok) throw new SkillValidationError(raw.relPath, validated.issues);
|
|
512
|
+
return {
|
|
513
|
+
name: validated.value.name,
|
|
514
|
+
dir: raw.dir,
|
|
515
|
+
relPath: raw.relPath,
|
|
516
|
+
frontmatter: validated.value,
|
|
517
|
+
body: raw.body,
|
|
518
|
+
resources: raw.resources
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/core/registry-build.ts
|
|
523
|
+
function buildRegistry(skillsRoot, version = "0.1.0") {
|
|
524
|
+
const dirs = findSkillDirs(skillsRoot);
|
|
525
|
+
const skills = dirs.map((dir) => {
|
|
526
|
+
const s = loadSkill(dir, skillsRoot);
|
|
527
|
+
const xm = s.frontmatter["x-skills-master"];
|
|
528
|
+
return {
|
|
529
|
+
name: s.name,
|
|
530
|
+
domain: xm.domain,
|
|
531
|
+
class: xm.class,
|
|
532
|
+
category: xm.category,
|
|
533
|
+
description: s.frontmatter.description,
|
|
534
|
+
platforms: xm.platforms,
|
|
535
|
+
stability: xm.stability,
|
|
536
|
+
version: xm.version,
|
|
537
|
+
tags: s.frontmatter.tags ?? [],
|
|
538
|
+
pairs_with: xm.pairs_with,
|
|
539
|
+
path: s.relPath,
|
|
540
|
+
resources: {
|
|
541
|
+
reference: Boolean(s.resources.reference),
|
|
542
|
+
examples: Boolean(s.resources.examples),
|
|
543
|
+
checklist: Boolean(s.resources.checklist)
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
548
|
+
return {
|
|
549
|
+
$schema: "https://skills-master.dev/schema/registry.json",
|
|
550
|
+
version,
|
|
551
|
+
skills
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/content/source.ts
|
|
556
|
+
var DEFAULT_REPO = "github:iChintanSoni/skills-master";
|
|
557
|
+
var ContentSource = class {
|
|
558
|
+
constructor(root) {
|
|
559
|
+
this.root = root;
|
|
560
|
+
}
|
|
561
|
+
root;
|
|
562
|
+
skillDirs() {
|
|
563
|
+
return findSkillDirs(this.root);
|
|
564
|
+
}
|
|
565
|
+
findDir(name) {
|
|
566
|
+
const dirs = this.skillDirs();
|
|
567
|
+
return dirs.find((d) => basename2(d) === name) ?? dirs.find((d) => safeName(d, this.root) === name);
|
|
568
|
+
}
|
|
569
|
+
loadSkill(name) {
|
|
570
|
+
const dir = this.findDir(name);
|
|
571
|
+
if (!dir) throw new Error(`Skill "${name}" not found in content at ${this.root}`);
|
|
572
|
+
return loadSkill(dir, this.root);
|
|
573
|
+
}
|
|
574
|
+
registry() {
|
|
575
|
+
return buildRegistry(this.root);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
function safeName(dir, root) {
|
|
579
|
+
try {
|
|
580
|
+
return loadSkill(dir, root).name;
|
|
581
|
+
} catch {
|
|
582
|
+
return "";
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function resolveContent(opts = {}) {
|
|
586
|
+
if (opts.content) {
|
|
587
|
+
const root = isAbsolute(opts.content) ? opts.content : resolve(process.cwd(), opts.content);
|
|
588
|
+
return new ContentSource(root);
|
|
589
|
+
}
|
|
590
|
+
const env = process.env.SKILLS_MASTER_CONTENT;
|
|
591
|
+
if (env) return new ContentSource(resolve(env));
|
|
592
|
+
const local = findLocalSkillsDir(opts.cwd ?? process.cwd());
|
|
593
|
+
if (local) return new ContentSource(local);
|
|
594
|
+
return new ContentSource(await fetchRemote(opts.ref ?? "main"));
|
|
595
|
+
}
|
|
596
|
+
function findLocalSkillsDir(start) {
|
|
597
|
+
let dir = resolve(start);
|
|
598
|
+
for (let i = 0; i < 8; i++) {
|
|
599
|
+
const candidate = join5(dir, "skills");
|
|
600
|
+
if (existsSync5(join5(dir, "pnpm-workspace.yaml")) && existsSync5(candidate)) return candidate;
|
|
601
|
+
const parent = resolve(dir, "..");
|
|
602
|
+
if (parent === dir) break;
|
|
603
|
+
dir = parent;
|
|
604
|
+
}
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
async function fetchRemote(ref) {
|
|
608
|
+
const repo = process.env.SKILLS_MASTER_REPO ?? DEFAULT_REPO;
|
|
609
|
+
const cacheDir = join5(homedir(), ".skills-master-cache", ref.replace(/[^\w.-]/g, "_"));
|
|
610
|
+
const { downloadTemplate } = await import("giget");
|
|
611
|
+
const { dir } = await downloadTemplate(`${repo}/skills#${ref}`, {
|
|
612
|
+
dir: cacheDir,
|
|
613
|
+
force: true,
|
|
614
|
+
forceClean: true
|
|
615
|
+
});
|
|
616
|
+
return dir;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/core/install.ts
|
|
620
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
621
|
+
import { join as join7 } from "path";
|
|
622
|
+
|
|
623
|
+
// src/core/compile.ts
|
|
624
|
+
function compileSkill(skill, targets, ctx) {
|
|
625
|
+
const out = [];
|
|
626
|
+
for (const target of targets) {
|
|
627
|
+
const emitter = getEmitter(target);
|
|
628
|
+
if (!emitter) throw new Error(`Unknown target: ${target}`);
|
|
629
|
+
out.push({ target, files: emitter.emit(skill, ctx) });
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/core/writer.ts
|
|
635
|
+
import { existsSync as existsSync6, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, rmdirSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
636
|
+
import { dirname, join as join6 } from "path";
|
|
637
|
+
|
|
638
|
+
// src/core/markers.ts
|
|
639
|
+
var PREFIX = "skills-master";
|
|
640
|
+
function escapeRe(s) {
|
|
641
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
642
|
+
}
|
|
643
|
+
function beginMarker(id, version) {
|
|
644
|
+
return version ? `<!-- BEGIN ${PREFIX}:${id} v${version} -->` : `<!-- BEGIN ${PREFIX}:${id} -->`;
|
|
645
|
+
}
|
|
646
|
+
function endMarker(id) {
|
|
647
|
+
return `<!-- END ${PREFIX}:${id} -->`;
|
|
648
|
+
}
|
|
649
|
+
function blockRegex(id) {
|
|
650
|
+
const esc = escapeRe(id);
|
|
651
|
+
return new RegExp(
|
|
652
|
+
`[ \\t]*<!-- BEGIN ${PREFIX}:${esc}(?: v[^>]*)? -->[\\s\\S]*?<!-- END ${PREFIX}:${esc} -->`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
function renderBlock(id, body, version) {
|
|
656
|
+
return `${beginMarker(id, version)}
|
|
657
|
+
${body.trim()}
|
|
658
|
+
${endMarker(id)}`;
|
|
659
|
+
}
|
|
660
|
+
function hasBlock(file, id) {
|
|
661
|
+
return blockRegex(id).test(file);
|
|
662
|
+
}
|
|
663
|
+
function upsertBlock(file, id, body, version) {
|
|
664
|
+
const block = renderBlock(id, body, version);
|
|
665
|
+
const re = blockRegex(id);
|
|
666
|
+
if (re.test(file)) {
|
|
667
|
+
return file.replace(re, block);
|
|
668
|
+
}
|
|
669
|
+
const base = file.replace(/\s+$/, "");
|
|
670
|
+
if (base.length === 0) return block + "\n";
|
|
671
|
+
return `${base}
|
|
672
|
+
|
|
673
|
+
${block}
|
|
674
|
+
`;
|
|
675
|
+
}
|
|
676
|
+
function removeBlock(file, id) {
|
|
677
|
+
const re = new RegExp(blockRegex(id).source + `\\n?\\n?`);
|
|
678
|
+
const next = file.replace(re, "");
|
|
679
|
+
return next.trim().length === 0 ? "" : next.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/core/writer.ts
|
|
683
|
+
function ensureDir(absPath) {
|
|
684
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
685
|
+
}
|
|
686
|
+
function applyWhole(projectRoot, file, opts) {
|
|
687
|
+
const abs = join6(projectRoot, file.path);
|
|
688
|
+
const exists = existsSync6(abs);
|
|
689
|
+
const next = file.contents;
|
|
690
|
+
if (!exists) {
|
|
691
|
+
if (!opts.dryRun) {
|
|
692
|
+
ensureDir(abs);
|
|
693
|
+
writeFileSync2(abs, next, "utf8");
|
|
694
|
+
}
|
|
695
|
+
return { path: file.path, mode: "whole", action: "created", after: next };
|
|
696
|
+
}
|
|
697
|
+
const current = readFileSync3(abs, "utf8");
|
|
698
|
+
if (current === next) {
|
|
699
|
+
return { path: file.path, mode: "whole", action: "unchanged" };
|
|
700
|
+
}
|
|
701
|
+
let choice = "overwrite";
|
|
702
|
+
if (!opts.overwrite) {
|
|
703
|
+
choice = opts.onConflict ? opts.onConflict(file.path) : opts.dryRun ? "overwrite" : "skip";
|
|
704
|
+
}
|
|
705
|
+
if (choice === "skip") {
|
|
706
|
+
return { path: file.path, mode: "whole", action: "skipped", before: current, after: next };
|
|
707
|
+
}
|
|
708
|
+
if (!opts.dryRun) writeFileSync2(abs, next, "utf8");
|
|
709
|
+
return { path: file.path, mode: "whole", action: "updated", before: current, after: next };
|
|
710
|
+
}
|
|
711
|
+
function applyBlock(projectRoot, file, opts) {
|
|
712
|
+
const abs = join6(projectRoot, file.path);
|
|
713
|
+
const exists = existsSync6(abs);
|
|
714
|
+
const current = exists ? readFileSync3(abs, "utf8") : "";
|
|
715
|
+
const blockId = file.blockId;
|
|
716
|
+
const next = upsertBlock(current, blockId, file.contents, file.blockVersion);
|
|
717
|
+
let action;
|
|
718
|
+
if (current === next) action = "unchanged";
|
|
719
|
+
else if (!exists || !hasBlock(current, blockId)) action = "created";
|
|
720
|
+
else action = "updated";
|
|
721
|
+
if (action !== "unchanged" && !opts.dryRun) {
|
|
722
|
+
ensureDir(abs);
|
|
723
|
+
writeFileSync2(abs, next, "utf8");
|
|
724
|
+
}
|
|
725
|
+
return { path: file.path, mode: "block", blockId, action };
|
|
726
|
+
}
|
|
727
|
+
function applyFiles(projectRoot, files, opts = {}) {
|
|
728
|
+
return files.map(
|
|
729
|
+
(f) => f.mode === "block" ? applyBlock(projectRoot, f, opts) : applyWhole(projectRoot, f, opts)
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
function pruneEmptyDirs(projectRoot, relFilePaths, dryRun = false) {
|
|
733
|
+
if (dryRun) return;
|
|
734
|
+
const seen = /* @__PURE__ */ new Set();
|
|
735
|
+
for (const rel of relFilePaths) {
|
|
736
|
+
let dir = dirname(join6(projectRoot, rel));
|
|
737
|
+
while (dir.startsWith(projectRoot) && dir !== projectRoot && !seen.has(dir)) {
|
|
738
|
+
seen.add(dir);
|
|
739
|
+
try {
|
|
740
|
+
if (existsSync6(dir) && readdirSync2(dir).length === 0) {
|
|
741
|
+
rmdirSync(dir);
|
|
742
|
+
dir = dirname(dir);
|
|
743
|
+
} else break;
|
|
744
|
+
} catch {
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function removeWholeFile(projectRoot, relPath, dryRun = false) {
|
|
751
|
+
const abs = join6(projectRoot, relPath);
|
|
752
|
+
if (!existsSync6(abs)) return false;
|
|
753
|
+
if (!dryRun) rmSync(abs);
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
function removeBlockFromFile(projectRoot, relPath, blockId, dryRun = false) {
|
|
757
|
+
const abs = join6(projectRoot, relPath);
|
|
758
|
+
if (!existsSync6(abs)) return false;
|
|
759
|
+
const current = readFileSync3(abs, "utf8");
|
|
760
|
+
if (!hasBlock(current, blockId)) return false;
|
|
761
|
+
const next = removeBlock(current, blockId);
|
|
762
|
+
if (!dryRun) {
|
|
763
|
+
if (next.trim().length === 0) rmSync(abs);
|
|
764
|
+
else writeFileSync2(abs, next, "utf8");
|
|
765
|
+
}
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/core/hash.ts
|
|
770
|
+
import { createHash } from "crypto";
|
|
771
|
+
function sha256(...parts) {
|
|
772
|
+
const h = createHash("sha256");
|
|
773
|
+
for (const p of parts) {
|
|
774
|
+
h.update(p, "utf8");
|
|
775
|
+
h.update("\0");
|
|
776
|
+
}
|
|
777
|
+
return "sha256:" + h.digest("hex");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/core/install.ts
|
|
781
|
+
function sourceHashOf(skill) {
|
|
782
|
+
const xm = skill.frontmatter["x-skills-master"];
|
|
783
|
+
return sha256(
|
|
784
|
+
skill.relPath,
|
|
785
|
+
xm.version,
|
|
786
|
+
skill.frontmatter.description,
|
|
787
|
+
skill.body,
|
|
788
|
+
skill.resources.reference ?? "",
|
|
789
|
+
skill.resources.examples ?? "",
|
|
790
|
+
skill.resources.checklist ?? ""
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
function diskHash(projectRoot, relPaths) {
|
|
794
|
+
const sorted = [...relPaths].sort((a, b) => a.localeCompare(b));
|
|
795
|
+
const parts = [];
|
|
796
|
+
for (const p of sorted) {
|
|
797
|
+
const abs = join7(projectRoot, p);
|
|
798
|
+
const contents = existsSync7(abs) ? readFileSync4(abs, "utf8") : "<<missing>>";
|
|
799
|
+
parts.push(p, contents);
|
|
800
|
+
}
|
|
801
|
+
return sha256(...parts);
|
|
802
|
+
}
|
|
803
|
+
function installSkill(projectRoot, skill, targets, paths, opts = {}) {
|
|
804
|
+
const compiled = compileSkill(skill, targets, { projectRoot, paths });
|
|
805
|
+
const allFiles = compiled.flatMap((c) => c.files);
|
|
806
|
+
const results = applyFiles(projectRoot, allFiles, opts);
|
|
807
|
+
const emitted = {};
|
|
808
|
+
for (const c of compiled) {
|
|
809
|
+
const whole = c.files.filter((f) => f.mode === "whole").map((f) => f.path);
|
|
810
|
+
const block = c.files.find((f) => f.mode === "block")?.path;
|
|
811
|
+
const hash = opts.dryRun ? sha256("dry-run", ...whole) : diskHash(projectRoot, whole);
|
|
812
|
+
emitted[c.target] = { files: whole, ...block ? { block } : {}, hash };
|
|
813
|
+
}
|
|
814
|
+
const version = skill.frontmatter["x-skills-master"].version;
|
|
815
|
+
return {
|
|
816
|
+
name: skill.name,
|
|
817
|
+
version,
|
|
818
|
+
results,
|
|
819
|
+
locked: { version, sourceHash: sourceHashOf(skill), emitted }
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/core/gitignore.ts
|
|
824
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
825
|
+
import { join as join8 } from "path";
|
|
826
|
+
function ensureGitignored(root, entries) {
|
|
827
|
+
const p = join8(root, ".gitignore");
|
|
828
|
+
const current = existsSync8(p) ? readFileSync5(p, "utf8") : "";
|
|
829
|
+
const lines = new Set(current.split("\n").map((l) => l.trim()));
|
|
830
|
+
const missing = entries.filter((e) => !lines.has(e));
|
|
831
|
+
if (missing.length === 0) return;
|
|
832
|
+
const header = current.includes("# skills-master") ? "" : "\n# skills-master generated outputs\n";
|
|
833
|
+
const next = current.replace(/\s*$/, "") + header + missing.join("\n") + "\n";
|
|
834
|
+
writeFileSync3(p, next, "utf8");
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/commands/add.ts
|
|
838
|
+
async function addCommand(opts) {
|
|
839
|
+
const cfg = loadConfigOrDefault(opts.cwd);
|
|
840
|
+
const hadConfig = loadConfig(opts.cwd) != null;
|
|
841
|
+
let targets = opts.targets?.length ? opts.targets : cfg.targets;
|
|
842
|
+
if (!targets.length) targets = detectTargets(opts.cwd);
|
|
843
|
+
if (!targets.length) targets = ALL_TARGETS;
|
|
844
|
+
const content = await resolveContent({
|
|
845
|
+
content: opts.content,
|
|
846
|
+
ref: opts.ref ?? cfg.contentRef,
|
|
847
|
+
cwd: opts.cwd
|
|
848
|
+
});
|
|
849
|
+
const registry2 = content.registry();
|
|
850
|
+
const byName = new Map(registry2.skills.map((s) => [s.name, s]));
|
|
851
|
+
const selected = /* @__PURE__ */ new Set();
|
|
852
|
+
const skipped = [];
|
|
853
|
+
for (const token of opts.names) {
|
|
854
|
+
if (byName.has(token)) {
|
|
855
|
+
selected.add(token);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const byCategory = registry2.skills.filter((s) => s.category === token);
|
|
859
|
+
const byClass = registry2.skills.filter((s) => s.class === token);
|
|
860
|
+
const group = byCategory.length ? byCategory : byClass;
|
|
861
|
+
if (group.length) {
|
|
862
|
+
group.forEach((s) => selected.add(s.name));
|
|
863
|
+
} else {
|
|
864
|
+
log.warn(`No skill, category, or class matches "${token}".`);
|
|
865
|
+
skipped.push(token);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (opts.withPairs) {
|
|
869
|
+
for (const name of [...selected]) {
|
|
870
|
+
for (const pair of byName.get(name)?.pairs_with ?? []) {
|
|
871
|
+
if (byName.has(pair)) selected.add(pair);
|
|
872
|
+
else log.warn(`Paired skill "${pair}" (from "${name}") is not in the registry.`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (selected.size === 0) {
|
|
877
|
+
log.error("Nothing to install.");
|
|
878
|
+
return { targets, installed: [], skipped };
|
|
879
|
+
}
|
|
880
|
+
const paths = resolvePaths(cfg);
|
|
881
|
+
const lock = loadLockfile(opts.cwd);
|
|
882
|
+
lock.contentRef = opts.ref ?? cfg.contentRef;
|
|
883
|
+
const installed = [];
|
|
884
|
+
const prefix = opts.dryRun ? "[dry-run] " : "";
|
|
885
|
+
for (const name of [...selected].sort()) {
|
|
886
|
+
const skill = content.loadSkill(name);
|
|
887
|
+
const result = installSkill(opts.cwd, skill, targets, paths, {
|
|
888
|
+
dryRun: opts.dryRun,
|
|
889
|
+
overwrite: opts.overwrite,
|
|
890
|
+
onConflict: opts.onConflict
|
|
891
|
+
});
|
|
892
|
+
if (!opts.dryRun) lock.skills[name] = result.locked;
|
|
893
|
+
installed.push({ name, version: result.version });
|
|
894
|
+
for (const r of result.results) {
|
|
895
|
+
const tag = r.mode === "block" ? `${r.path} [${r.blockId}]` : r.path;
|
|
896
|
+
log.info(`${prefix}${r.action.padEnd(9)} ${tag}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (!opts.dryRun) {
|
|
900
|
+
saveLockfile(opts.cwd, lock);
|
|
901
|
+
if (!hadConfig) {
|
|
902
|
+
saveConfig(opts.cwd, { ...cfg, targets });
|
|
903
|
+
log.info("Wrote skills-master.json.");
|
|
904
|
+
}
|
|
905
|
+
if (!cfg.commit) {
|
|
906
|
+
const outs = targets.map((t) => paths[t]);
|
|
907
|
+
ensureGitignored(opts.cwd, outs);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
log.success(
|
|
911
|
+
`${prefix}Installed ${installed.length} skill(s) into ${targets.length} target(s): ${targets.join(", ")}.`
|
|
912
|
+
);
|
|
913
|
+
return { targets, installed, skipped };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// src/commands/update.ts
|
|
917
|
+
async function updateCommand(opts) {
|
|
918
|
+
const cfg = loadConfigOrDefault(opts.cwd);
|
|
919
|
+
const lock = loadLockfile(opts.cwd);
|
|
920
|
+
const ref = opts.ref ?? cfg.contentRef;
|
|
921
|
+
const updated = [];
|
|
922
|
+
const upToDate = [];
|
|
923
|
+
const skipped = [];
|
|
924
|
+
const names = opts.names?.length ? opts.names : Object.keys(lock.skills);
|
|
925
|
+
if (names.length === 0) {
|
|
926
|
+
log.info("No installed skills to update.");
|
|
927
|
+
return { updated, upToDate, skipped };
|
|
928
|
+
}
|
|
929
|
+
const content = await resolveContent({ content: opts.content, ref, cwd: opts.cwd });
|
|
930
|
+
const paths = resolvePaths(cfg);
|
|
931
|
+
const prefix = opts.dryRun ? "[dry-run] " : "";
|
|
932
|
+
for (const name of names.sort()) {
|
|
933
|
+
const locked = lock.skills[name];
|
|
934
|
+
if (!locked) {
|
|
935
|
+
log.warn(`"${name}" is not installed \u2014 run \`add\` first.`);
|
|
936
|
+
skipped.push(name);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
let skill;
|
|
940
|
+
try {
|
|
941
|
+
skill = content.loadSkill(name);
|
|
942
|
+
} catch {
|
|
943
|
+
log.warn(`"${name}" no longer exists in the content library.`);
|
|
944
|
+
skipped.push(name);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const refChanged = lock.contentRef !== ref;
|
|
948
|
+
const sourceChanged = locked.sourceHash !== sourceHashOf(skill);
|
|
949
|
+
if (!opts.overwrite && !sourceChanged && !refChanged) {
|
|
950
|
+
upToDate.push(name);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const targets = Object.keys(locked.emitted);
|
|
954
|
+
const userEdited = targets.some((t) => {
|
|
955
|
+
const e = locked.emitted[t];
|
|
956
|
+
return e && diskHash(opts.cwd, e.files) !== e.hash;
|
|
957
|
+
});
|
|
958
|
+
if (userEdited && !opts.overwrite && !opts.onConflict) {
|
|
959
|
+
log.warn(`${prefix}"${name}" has local edits \u2014 skipping (use --overwrite to replace).`);
|
|
960
|
+
skipped.push(name);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const result = installSkill(opts.cwd, skill, targets, paths, {
|
|
964
|
+
dryRun: opts.dryRun,
|
|
965
|
+
overwrite: opts.overwrite || !userEdited,
|
|
966
|
+
onConflict: opts.onConflict
|
|
967
|
+
});
|
|
968
|
+
if (!opts.dryRun) lock.skills[name] = result.locked;
|
|
969
|
+
updated.push(name);
|
|
970
|
+
for (const r of result.results) {
|
|
971
|
+
if (r.action === "unchanged") continue;
|
|
972
|
+
const tag = r.mode === "block" ? `${r.path} [${r.blockId}]` : r.path;
|
|
973
|
+
log.info(`${prefix}${r.action.padEnd(9)} ${tag}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (!opts.dryRun) {
|
|
977
|
+
lock.contentRef = ref;
|
|
978
|
+
saveLockfile(opts.cwd, lock);
|
|
979
|
+
}
|
|
980
|
+
log.success(
|
|
981
|
+
`${prefix}Updated ${updated.length}, up-to-date ${upToDate.length}, skipped ${skipped.length}.`
|
|
982
|
+
);
|
|
983
|
+
return { updated, upToDate, skipped };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/commands/remove.ts
|
|
987
|
+
function removeCommand(opts) {
|
|
988
|
+
const lock = loadLockfile(opts.cwd);
|
|
989
|
+
const removed = [];
|
|
990
|
+
const missing = [];
|
|
991
|
+
const prefix = opts.dryRun ? "[dry-run] " : "";
|
|
992
|
+
for (const name of opts.names) {
|
|
993
|
+
const locked = lock.skills[name];
|
|
994
|
+
if (!locked) {
|
|
995
|
+
log.warn(`"${name}" is not installed.`);
|
|
996
|
+
missing.push(name);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const targets = (opts.targets?.length ? opts.targets : Object.keys(locked.emitted)).filter((t) => locked.emitted[t]);
|
|
1000
|
+
const wholeRemoved = [];
|
|
1001
|
+
for (const t of targets) {
|
|
1002
|
+
const e = locked.emitted[t];
|
|
1003
|
+
for (const file of e.files) {
|
|
1004
|
+
if (removeWholeFile(opts.cwd, file, opts.dryRun)) {
|
|
1005
|
+
wholeRemoved.push(file);
|
|
1006
|
+
log.info(`${prefix}removed ${file}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (e.block && removeBlockFromFile(opts.cwd, e.block, name, opts.dryRun)) {
|
|
1010
|
+
log.info(`${prefix}unblocked ${e.block} [${name}]`);
|
|
1011
|
+
}
|
|
1012
|
+
if (!opts.dryRun) delete locked.emitted[t];
|
|
1013
|
+
}
|
|
1014
|
+
pruneEmptyDirs(opts.cwd, wholeRemoved, opts.dryRun);
|
|
1015
|
+
if (!opts.dryRun) {
|
|
1016
|
+
if (Object.keys(locked.emitted).length === 0) delete lock.skills[name];
|
|
1017
|
+
}
|
|
1018
|
+
removed.push(name);
|
|
1019
|
+
}
|
|
1020
|
+
if (!opts.dryRun) saveLockfile(opts.cwd, lock);
|
|
1021
|
+
log.success(`${prefix}Removed ${removed.length} skill(s).`);
|
|
1022
|
+
return { removed, missing };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/commands/doctor.ts
|
|
1026
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
1027
|
+
import { join as join9 } from "path";
|
|
1028
|
+
function doctorCommand(opts) {
|
|
1029
|
+
const problems = [];
|
|
1030
|
+
const note = (msg) => problems.push(msg);
|
|
1031
|
+
const cfg = loadConfig(opts.cwd);
|
|
1032
|
+
if (!cfg) {
|
|
1033
|
+
log.warn("No skills-master.json found \u2014 run `skills-master init`.");
|
|
1034
|
+
} else {
|
|
1035
|
+
log.info(`Config targets: ${cfg.targets.length ? cfg.targets.join(", ") : "(auto-detect)"}`);
|
|
1036
|
+
}
|
|
1037
|
+
const lock = loadLockfile(opts.cwd);
|
|
1038
|
+
const names = Object.keys(lock.skills);
|
|
1039
|
+
if (names.length === 0) {
|
|
1040
|
+
log.info("No skills installed.");
|
|
1041
|
+
return { problems, ok: true };
|
|
1042
|
+
}
|
|
1043
|
+
for (const name of names) {
|
|
1044
|
+
const locked = lock.skills[name];
|
|
1045
|
+
for (const [target, e] of Object.entries(locked.emitted)) {
|
|
1046
|
+
for (const file of e.files) {
|
|
1047
|
+
if (!existsSync9(join9(opts.cwd, file))) note(`${name}: missing ${target} file ${file}`);
|
|
1048
|
+
}
|
|
1049
|
+
if (e.files.every((f) => existsSync9(join9(opts.cwd, f)))) {
|
|
1050
|
+
if (diskHash(opts.cwd, e.files) !== e.hash) {
|
|
1051
|
+
note(`${name}: local edits to ${target} output(s) (run \`update --overwrite\` to reset)`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (e.block) {
|
|
1055
|
+
const abs = join9(opts.cwd, e.block);
|
|
1056
|
+
if (!existsSync9(abs) || !hasBlock(readFileSync6(abs, "utf8"), name)) {
|
|
1057
|
+
note(`${name}: missing managed block in ${e.block}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (problems.length === 0) {
|
|
1063
|
+
log.success(`All ${names.length} installed skill(s) look healthy.`);
|
|
1064
|
+
} else {
|
|
1065
|
+
for (const p of problems) log.warn(p);
|
|
1066
|
+
log.plain(`
|
|
1067
|
+
${problems.length} problem(s) found.`);
|
|
1068
|
+
}
|
|
1069
|
+
return { problems, ok: problems.length === 0 };
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/commands/catalog.ts
|
|
1073
|
+
async function registryOf(q) {
|
|
1074
|
+
const content = await resolveContent({ content: q.content, ref: q.ref, cwd: q.cwd });
|
|
1075
|
+
return content.registry();
|
|
1076
|
+
}
|
|
1077
|
+
async function listCommand(opts) {
|
|
1078
|
+
const reg = await registryOf(opts);
|
|
1079
|
+
let skills = reg.skills;
|
|
1080
|
+
if (opts.domain) skills = skills.filter((s) => s.domain === opts.domain);
|
|
1081
|
+
if (opts.class) skills = skills.filter((s) => s.class === opts.class);
|
|
1082
|
+
if (opts.category) skills = skills.filter((s) => s.category === opts.category);
|
|
1083
|
+
if (opts.platform) {
|
|
1084
|
+
const platform = opts.platform;
|
|
1085
|
+
skills = skills.filter((s) => s.platforms.includes(platform));
|
|
1086
|
+
}
|
|
1087
|
+
if (opts.json) {
|
|
1088
|
+
log.plain(JSON.stringify(skills, null, 2));
|
|
1089
|
+
return skills;
|
|
1090
|
+
}
|
|
1091
|
+
if (skills.length === 0) {
|
|
1092
|
+
log.info("No skills match.");
|
|
1093
|
+
return skills;
|
|
1094
|
+
}
|
|
1095
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1096
|
+
for (const s of skills) {
|
|
1097
|
+
const key = `${s.domain}/${s.class}/${s.category}`;
|
|
1098
|
+
(groups.get(key) ?? groups.set(key, []).get(key)).push(s);
|
|
1099
|
+
}
|
|
1100
|
+
for (const [group, entries] of [...groups].sort()) {
|
|
1101
|
+
log.plain(`
|
|
1102
|
+
${group}`);
|
|
1103
|
+
for (const s of entries) {
|
|
1104
|
+
const flag = s.stability === "stable" ? "" : ` (${s.stability})`;
|
|
1105
|
+
log.plain(` ${s.name} v${s.version}${flag}`);
|
|
1106
|
+
log.plain(` ${truncate(s.description, 96)}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
log.plain(`
|
|
1110
|
+
${skills.length} skill(s).`);
|
|
1111
|
+
return skills;
|
|
1112
|
+
}
|
|
1113
|
+
async function searchCommand(opts) {
|
|
1114
|
+
const reg = await registryOf(opts);
|
|
1115
|
+
const q = opts.query.toLowerCase();
|
|
1116
|
+
const hits = reg.skills.filter(
|
|
1117
|
+
(s) => [s.name, s.description, s.domain, s.category, s.class, ...s.tags].join(" ").toLowerCase().includes(q)
|
|
1118
|
+
);
|
|
1119
|
+
if (hits.length === 0) {
|
|
1120
|
+
log.info(`No matches for "${opts.query}".`);
|
|
1121
|
+
return hits;
|
|
1122
|
+
}
|
|
1123
|
+
for (const s of hits) {
|
|
1124
|
+
log.plain(`${s.name} (${s.class}/${s.category}) v${s.version}`);
|
|
1125
|
+
log.plain(` ${truncate(s.description, 96)}`);
|
|
1126
|
+
}
|
|
1127
|
+
log.plain(`
|
|
1128
|
+
${hits.length} match(es).`);
|
|
1129
|
+
return hits;
|
|
1130
|
+
}
|
|
1131
|
+
async function viewCommand(opts) {
|
|
1132
|
+
const content = await resolveContent({ content: opts.content, ref: opts.ref, cwd: opts.cwd });
|
|
1133
|
+
const skill = content.loadSkill(opts.name);
|
|
1134
|
+
const xm = skill.frontmatter["x-skills-master"];
|
|
1135
|
+
if (opts.raw) {
|
|
1136
|
+
log.plain(skill.body);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
log.plain(`${skill.name} v${xm.version} [${xm.domain}/${xm.class}/${xm.category}] ${xm.stability}`);
|
|
1140
|
+
log.plain(`platforms: ${xm.platforms.join(", ")}`);
|
|
1141
|
+
if (xm.requires) {
|
|
1142
|
+
log.plain(`requires: ${Object.entries(xm.requires).map(([k, v]) => `${k} ${v}`).join(", ")}`);
|
|
1143
|
+
}
|
|
1144
|
+
if (xm.pairs_with.length) log.plain(`pairs with: ${xm.pairs_with.join(", ")}`);
|
|
1145
|
+
log.plain(`
|
|
1146
|
+
${skill.frontmatter.description}
|
|
1147
|
+
`);
|
|
1148
|
+
log.plain(`sources:`);
|
|
1149
|
+
for (const url of xm.sources) log.plain(` ${url}`);
|
|
1150
|
+
const res = Object.entries(skill.resources).filter(([, v]) => v).map(([k]) => k);
|
|
1151
|
+
if (res.length) log.plain(`
|
|
1152
|
+
resource files: ${res.join(", ")}`);
|
|
1153
|
+
}
|
|
1154
|
+
function truncate(s, n) {
|
|
1155
|
+
return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/core/lint.ts
|
|
1159
|
+
var CANONICAL_HEADINGS = [
|
|
1160
|
+
"## When to use",
|
|
1161
|
+
"## Core guidance",
|
|
1162
|
+
"## Pitfalls",
|
|
1163
|
+
"## References",
|
|
1164
|
+
"## See also"
|
|
1165
|
+
];
|
|
1166
|
+
var MAX_BODY_LINES = 500;
|
|
1167
|
+
var WARN_BODY_LINES = 450;
|
|
1168
|
+
function today() {
|
|
1169
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1170
|
+
}
|
|
1171
|
+
function lintSkills(skillsRoot) {
|
|
1172
|
+
const dirs = findSkillDirs(skillsRoot);
|
|
1173
|
+
const diagnostics = [];
|
|
1174
|
+
const loaded = [];
|
|
1175
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1176
|
+
for (const dir of dirs) {
|
|
1177
|
+
let raw;
|
|
1178
|
+
try {
|
|
1179
|
+
raw = loadRawSkill(dir, skillsRoot);
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
diagnostics.push({
|
|
1182
|
+
relPath: relPathOf(skillsRoot, dir),
|
|
1183
|
+
level: "error",
|
|
1184
|
+
message: `failed to parse SKILL.md: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`
|
|
1185
|
+
});
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
for (const line of raw.rawText.split("\n")) {
|
|
1189
|
+
const m = /^(\s*)(name|description|category):\s+(?!["'])(.*\s#.*)$/.exec(line);
|
|
1190
|
+
if (m) {
|
|
1191
|
+
diagnostics.push({
|
|
1192
|
+
relPath: raw.relPath,
|
|
1193
|
+
level: "warn",
|
|
1194
|
+
message: `frontmatter "${m[2]}" contains " #", which YAML reads as a comment \u2014 quote the value`
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
const v = validateFrontmatter(raw.data);
|
|
1199
|
+
if (!v.ok) {
|
|
1200
|
+
for (const issue of v.issues) {
|
|
1201
|
+
diagnostics.push({ relPath: raw.relPath, level: "error", message: issue });
|
|
1202
|
+
}
|
|
1203
|
+
loaded.push({ relPath: raw.relPath, folderName: raw.folderName, body: raw.body });
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
loaded.push({ relPath: raw.relPath, folderName: raw.folderName, fm: v.value, body: raw.body });
|
|
1207
|
+
byName.set(v.value.name, v.value);
|
|
1208
|
+
}
|
|
1209
|
+
const nameDirs = /* @__PURE__ */ new Map();
|
|
1210
|
+
for (const s of loaded) {
|
|
1211
|
+
const n = s.fm?.name ?? s.folderName;
|
|
1212
|
+
(nameDirs.get(n) ?? nameDirs.set(n, []).get(n)).push(s.relPath);
|
|
1213
|
+
}
|
|
1214
|
+
for (const [name, paths] of nameDirs) {
|
|
1215
|
+
if (paths.length > 1) {
|
|
1216
|
+
for (const p of paths) {
|
|
1217
|
+
diagnostics.push({ relPath: p, level: "error", message: `duplicate skill name "${name}" (also at ${paths.filter((x) => x !== p).join(", ")})` });
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const todayStr = today();
|
|
1222
|
+
for (const s of loaded) {
|
|
1223
|
+
const push = (level, message) => diagnostics.push({ relPath: s.relPath, level, message });
|
|
1224
|
+
if (!s.fm) continue;
|
|
1225
|
+
const fm = s.fm;
|
|
1226
|
+
const xm = fm["x-skills-master"];
|
|
1227
|
+
if (fm.name !== s.folderName) {
|
|
1228
|
+
push("error", `name "${fm.name}" must equal the folder name "${s.folderName}"`);
|
|
1229
|
+
}
|
|
1230
|
+
if (!/use when/i.test(fm.description)) {
|
|
1231
|
+
push("warn", `description should include a "Use when ..." trigger clause`);
|
|
1232
|
+
}
|
|
1233
|
+
if (xm.snapshot_date > todayStr) {
|
|
1234
|
+
push("error", `snapshot_date ${xm.snapshot_date} is in the future`);
|
|
1235
|
+
}
|
|
1236
|
+
if (xm.stability === "contested" && !/^## Open question\b/m.test(s.body)) {
|
|
1237
|
+
push("error", `stability is "contested" but no "## Open question" section is present`);
|
|
1238
|
+
}
|
|
1239
|
+
const lineCount = s.body.split("\n").length;
|
|
1240
|
+
if (lineCount > MAX_BODY_LINES) {
|
|
1241
|
+
push("error", `SKILL.md body is ${lineCount} lines (max ${MAX_BODY_LINES}); move depth into reference.md/examples.md`);
|
|
1242
|
+
} else if (lineCount > WARN_BODY_LINES) {
|
|
1243
|
+
push("warn", `SKILL.md body is ${lineCount} lines (approaching the ${MAX_BODY_LINES}-line cap)`);
|
|
1244
|
+
}
|
|
1245
|
+
if (!s.relPath.startsWith(`${xm.domain}/`)) {
|
|
1246
|
+
push("warn", `domain "${xm.domain}" does not match the top folder of "${s.relPath}"`);
|
|
1247
|
+
}
|
|
1248
|
+
if (xm.sources.length === 0) {
|
|
1249
|
+
push("warn", `no sources \u2014 add at least one canonical documentation URL`);
|
|
1250
|
+
}
|
|
1251
|
+
for (const url of xm.sources) {
|
|
1252
|
+
if (!/^https:\/\//.test(url)) {
|
|
1253
|
+
push("warn", `sources entry should be an https URL: ${url}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
for (const heading of CANONICAL_HEADINGS) {
|
|
1257
|
+
if (!s.body.includes(heading)) {
|
|
1258
|
+
push("warn", `missing recommended section "${heading}"`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
for (const partner of xm.pairs_with) {
|
|
1262
|
+
const partnerFm = byName.get(partner);
|
|
1263
|
+
if (!partnerFm) {
|
|
1264
|
+
push("error", `pairs_with references unknown skill "${partner}"`);
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (!partnerFm["x-skills-master"].pairs_with.includes(fm.name)) {
|
|
1268
|
+
push("error", `pairs_with "${partner}" is not reciprocated (add "${fm.name}" to its pairs_with)`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
const errorCount = diagnostics.filter((d) => d.level === "error").length;
|
|
1273
|
+
const warnCount = diagnostics.filter((d) => d.level === "warn").length;
|
|
1274
|
+
return { diagnostics, skillCount: dirs.length, errorCount, warnCount };
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/commands/lint.ts
|
|
1278
|
+
async function lintCommand(opts) {
|
|
1279
|
+
const content = await resolveContent({ content: opts.content, cwd: opts.cwd });
|
|
1280
|
+
const result = lintSkills(content.root);
|
|
1281
|
+
for (const d of result.diagnostics) {
|
|
1282
|
+
const line = `${d.level === "error" ? "\u2717" : "!"} ${d.relPath}: ${d.message}`;
|
|
1283
|
+
if (d.level === "error") log.error(line);
|
|
1284
|
+
else log.warn(line);
|
|
1285
|
+
}
|
|
1286
|
+
log.plain(
|
|
1287
|
+
`
|
|
1288
|
+
Linted ${result.skillCount} skill(s): ${result.errorCount} error(s), ${result.warnCount} warning(s).`
|
|
1289
|
+
);
|
|
1290
|
+
return result.errorCount === 0;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// src/commands/registry.ts
|
|
1294
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
1295
|
+
import { join as join10 } from "path";
|
|
1296
|
+
|
|
1297
|
+
// src/schema/registry.ts
|
|
1298
|
+
import { z as z4 } from "zod";
|
|
1299
|
+
var RegistryEntrySchema = z4.object({
|
|
1300
|
+
name: z4.string(),
|
|
1301
|
+
domain: z4.string(),
|
|
1302
|
+
class: SkillClassSchema,
|
|
1303
|
+
category: z4.string(),
|
|
1304
|
+
description: z4.string(),
|
|
1305
|
+
platforms: z4.array(z4.string()),
|
|
1306
|
+
stability: StabilitySchema,
|
|
1307
|
+
version: z4.string(),
|
|
1308
|
+
tags: z4.array(z4.string()).default([]),
|
|
1309
|
+
pairs_with: z4.array(z4.string()).default([]),
|
|
1310
|
+
/** path relative to the skills root. */
|
|
1311
|
+
path: z4.string(),
|
|
1312
|
+
/** which on-demand resource files exist. */
|
|
1313
|
+
resources: z4.object({
|
|
1314
|
+
reference: z4.boolean(),
|
|
1315
|
+
examples: z4.boolean(),
|
|
1316
|
+
checklist: z4.boolean()
|
|
1317
|
+
})
|
|
1318
|
+
});
|
|
1319
|
+
var RegistrySchema = z4.object({
|
|
1320
|
+
$schema: z4.string().optional(),
|
|
1321
|
+
/** aggregate library version (bumped on release). */
|
|
1322
|
+
version: z4.string().default("0.1.0"),
|
|
1323
|
+
generatedAt: z4.string().optional(),
|
|
1324
|
+
skills: z4.array(RegistryEntrySchema).default([])
|
|
1325
|
+
});
|
|
1326
|
+
var REGISTRY_FILENAME = "registry.json";
|
|
1327
|
+
|
|
1328
|
+
// src/commands/registry.ts
|
|
1329
|
+
async function registryBuildCommand(opts) {
|
|
1330
|
+
const content = await resolveContent({ content: opts.content, cwd: opts.cwd });
|
|
1331
|
+
const registry2 = buildRegistry(content.root, opts.version ?? "0.1.0");
|
|
1332
|
+
const json = JSON.stringify(registry2, null, 2) + "\n";
|
|
1333
|
+
const outPath = join10(content.root, REGISTRY_FILENAME);
|
|
1334
|
+
if (opts.check) {
|
|
1335
|
+
const current = existsSync10(outPath) ? readFileSync7(outPath, "utf8") : "";
|
|
1336
|
+
if (current !== json) {
|
|
1337
|
+
log.error(`registry.json is out of date \u2014 run \`registry build\` and commit the result.`);
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
log.success(`registry.json is up to date (${registry2.skills.length} skills).`);
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
writeFileSync4(outPath, json, "utf8");
|
|
1344
|
+
log.success(`Wrote ${REGISTRY_FILENAME} (${registry2.skills.length} skills).`);
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/commands/marketplace.ts
|
|
1349
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1350
|
+
import { dirname as dirname2, join as join11, resolve as resolve2 } from "path";
|
|
1351
|
+
var CLASS_LABEL = {
|
|
1352
|
+
code: "code skills (frameworks & APIs)",
|
|
1353
|
+
design: "design-review skills",
|
|
1354
|
+
"lang-tooling": "language, build, test & ship skills",
|
|
1355
|
+
overview: "decision-guidance routers"
|
|
1356
|
+
};
|
|
1357
|
+
var CLASS_CATEGORY = {
|
|
1358
|
+
code: "development",
|
|
1359
|
+
design: "design",
|
|
1360
|
+
"lang-tooling": "development",
|
|
1361
|
+
overview: "development"
|
|
1362
|
+
};
|
|
1363
|
+
function pluginName(domain, cls) {
|
|
1364
|
+
const clsSeg = cls === "overview" ? "overviews" : cls;
|
|
1365
|
+
return `skills-master-${domain}-${clsSeg}`;
|
|
1366
|
+
}
|
|
1367
|
+
function titleCase(s) {
|
|
1368
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1369
|
+
}
|
|
1370
|
+
async function marketplaceBuildCommand(opts) {
|
|
1371
|
+
const content = await resolveContent({ content: opts.content, cwd: opts.cwd });
|
|
1372
|
+
const out = resolve2(opts.out ?? dirname2(content.root));
|
|
1373
|
+
const version = opts.version ?? "0.1.0";
|
|
1374
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1375
|
+
for (const dir of content.skillDirs()) {
|
|
1376
|
+
const skill = content.loadSkill(dir.split(/[\\/]/).pop());
|
|
1377
|
+
const xm = skill.frontmatter["x-skills-master"];
|
|
1378
|
+
const key = `${xm.domain}:${xm.class}`;
|
|
1379
|
+
const name = pluginName(xm.domain, xm.class);
|
|
1380
|
+
const ctx = {
|
|
1381
|
+
projectRoot: out,
|
|
1382
|
+
paths: {
|
|
1383
|
+
claude: `plugins/${name}/skills`,
|
|
1384
|
+
cursor: "",
|
|
1385
|
+
copilot: "",
|
|
1386
|
+
agents: ""
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
const files = claudeEmitter.emit(skill, ctx);
|
|
1390
|
+
const g = groups.get(key) ?? { domain: xm.domain, cls: xm.class, files: [], count: 0 };
|
|
1391
|
+
g.files.push(...files);
|
|
1392
|
+
g.count += 1;
|
|
1393
|
+
groups.set(key, g);
|
|
1394
|
+
}
|
|
1395
|
+
const plugins = [];
|
|
1396
|
+
for (const { domain, cls, files, count } of groups.values()) {
|
|
1397
|
+
const name = pluginName(domain, cls);
|
|
1398
|
+
applyFiles(out, files, { overwrite: true });
|
|
1399
|
+
const description = `${titleCase(domain)} ${CLASS_LABEL[cls]}.`;
|
|
1400
|
+
const pluginDir = join11(out, "plugins", name, ".claude-plugin");
|
|
1401
|
+
mkdirSync2(pluginDir, { recursive: true });
|
|
1402
|
+
const manifest = {
|
|
1403
|
+
name,
|
|
1404
|
+
version,
|
|
1405
|
+
description,
|
|
1406
|
+
author: { name: "skills-master contributors" }
|
|
1407
|
+
};
|
|
1408
|
+
writeFileSync5(join11(pluginDir, "plugin.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
1409
|
+
plugins.push({ name, source: `./plugins/${name}`, description, version, category: CLASS_CATEGORY[cls] });
|
|
1410
|
+
log.info(`Built ${name} (${count} skills).`);
|
|
1411
|
+
}
|
|
1412
|
+
plugins.sort((a, b) => a.name.localeCompare(b.name));
|
|
1413
|
+
const marketplace2 = {
|
|
1414
|
+
$schema: "https://www.schemastore.org/claude-code-marketplace.json",
|
|
1415
|
+
name: "skills-master",
|
|
1416
|
+
owner: { name: "skills-master contributors" },
|
|
1417
|
+
plugins
|
|
1418
|
+
};
|
|
1419
|
+
const mpDir = join11(out, ".claude-plugin");
|
|
1420
|
+
mkdirSync2(mpDir, { recursive: true });
|
|
1421
|
+
writeFileSync5(join11(mpDir, "marketplace.json"), JSON.stringify(marketplace2, null, 2) + "\n", "utf8");
|
|
1422
|
+
log.success(`Wrote .claude-plugin/marketplace.json (${plugins.length} plugins).`);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/commands/new.ts
|
|
1426
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
1427
|
+
import { join as join12 } from "path";
|
|
1428
|
+
function template(domain, cls, category, name) {
|
|
1429
|
+
const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1430
|
+
return `---
|
|
1431
|
+
name: ${name}
|
|
1432
|
+
description: TODO \u2014 one-line, third person. Use when <triggers go here>.
|
|
1433
|
+
globs:
|
|
1434
|
+
- "**/*"
|
|
1435
|
+
tags: []
|
|
1436
|
+
x-skills-master:
|
|
1437
|
+
domain: ${domain}
|
|
1438
|
+
class: ${cls}
|
|
1439
|
+
category: ${category}
|
|
1440
|
+
platforms: [${domain}]
|
|
1441
|
+
requires: {}
|
|
1442
|
+
pairs_with: []
|
|
1443
|
+
sources: []
|
|
1444
|
+
snapshot_date: "${today2}"
|
|
1445
|
+
stability: emerging
|
|
1446
|
+
version: 0.1.0
|
|
1447
|
+
---
|
|
1448
|
+
|
|
1449
|
+
## When to use
|
|
1450
|
+
|
|
1451
|
+
TODO \u2014 when should an agent reach for this skill?
|
|
1452
|
+
|
|
1453
|
+
## Core guidance
|
|
1454
|
+
|
|
1455
|
+
TODO \u2014 the do/don't, idioms, and best practices (original prose, no copied docs).
|
|
1456
|
+
|
|
1457
|
+
## Platform notes
|
|
1458
|
+
|
|
1459
|
+
TODO \u2014 platform-specific caveats.
|
|
1460
|
+
|
|
1461
|
+
## Pitfalls
|
|
1462
|
+
|
|
1463
|
+
TODO \u2014 common mistakes.
|
|
1464
|
+
|
|
1465
|
+
## References
|
|
1466
|
+
|
|
1467
|
+
- **Documentation:** [TODO](https://developer.apple.com/documentation/...)
|
|
1468
|
+
- **Human Interface Guidelines:** [TODO](https://developer.apple.com/design/human-interface-guidelines/...)
|
|
1469
|
+
- **WWDC:** [TODO](https://developer.apple.com/videos/...)
|
|
1470
|
+
|
|
1471
|
+
## See also
|
|
1472
|
+
|
|
1473
|
+
TODO \u2014 paired skills referenced by name.
|
|
1474
|
+
`;
|
|
1475
|
+
}
|
|
1476
|
+
async function newSkillCommand(opts) {
|
|
1477
|
+
const parts = opts.spec.split("/").filter(Boolean);
|
|
1478
|
+
if (parts.length < 4) {
|
|
1479
|
+
throw new Error(`Expected "domain/class/category/name", got "${opts.spec}".`);
|
|
1480
|
+
}
|
|
1481
|
+
const domain = parts[0];
|
|
1482
|
+
const cls = SkillClassSchema.parse(parts[1]);
|
|
1483
|
+
const category = parts[2];
|
|
1484
|
+
const name = parts[parts.length - 1];
|
|
1485
|
+
const content = await resolveContent({ content: opts.content, cwd: opts.cwd });
|
|
1486
|
+
const dir = join12(content.root, domain, CLASS_DIR[cls], ...parts.slice(2));
|
|
1487
|
+
const skillMd = join12(dir, "SKILL.md");
|
|
1488
|
+
if (existsSync11(skillMd) && !opts.force) {
|
|
1489
|
+
throw new Error(`Skill already exists at ${skillMd} (use --force to overwrite).`);
|
|
1490
|
+
}
|
|
1491
|
+
mkdirSync3(dir, { recursive: true });
|
|
1492
|
+
writeFileSync6(skillMd, template(domain, cls, category, name), "utf8");
|
|
1493
|
+
log.success(`Created ${skillMd}`);
|
|
1494
|
+
return skillMd;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/bin.ts
|
|
1498
|
+
var VALID = new Set(ALL_TARGETS);
|
|
1499
|
+
function parseTargets(value) {
|
|
1500
|
+
if (!value) return void 0;
|
|
1501
|
+
if (value === "all") return ALL_TARGETS;
|
|
1502
|
+
const ids = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1503
|
+
for (const id of ids) {
|
|
1504
|
+
if (!VALID.has(id)) {
|
|
1505
|
+
throw new Error(`Unknown target "${id}". Valid: ${[...VALID, "all"].join(", ")}.`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
return ids;
|
|
1509
|
+
}
|
|
1510
|
+
async function run(fn, exitOnFalse = false) {
|
|
1511
|
+
try {
|
|
1512
|
+
const result = await fn();
|
|
1513
|
+
if (exitOnFalse && result === false) process.exitCode = 1;
|
|
1514
|
+
} catch (err) {
|
|
1515
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
1516
|
+
process.exitCode = 1;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
var program = new Command();
|
|
1520
|
+
program.name("skills-master").description("Install tool-agnostic Apple development skills into any AI coding tool.").version("0.1.0");
|
|
1521
|
+
program.command("init").description("Detect tools and write skills-master.json").option("--target <list>", "comma list or 'all'").option("--ref <ref>", "content git ref").option("--no-commit", "gitignore generated files instead of committing them").option("--force", "overwrite an existing config").action(
|
|
1522
|
+
(opts) => run(
|
|
1523
|
+
() => initCommand({
|
|
1524
|
+
cwd: process.cwd(),
|
|
1525
|
+
targets: parseTargets(opts.target),
|
|
1526
|
+
commit: opts.commit,
|
|
1527
|
+
contentRef: opts.ref,
|
|
1528
|
+
force: opts.force
|
|
1529
|
+
})
|
|
1530
|
+
)
|
|
1531
|
+
);
|
|
1532
|
+
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(
|
|
1533
|
+
(opts) => run(
|
|
1534
|
+
() => listCommand({
|
|
1535
|
+
cwd: process.cwd(),
|
|
1536
|
+
domain: opts.domain,
|
|
1537
|
+
class: opts.class,
|
|
1538
|
+
category: opts.category,
|
|
1539
|
+
platform: opts.platform,
|
|
1540
|
+
json: opts.json,
|
|
1541
|
+
content: opts.content,
|
|
1542
|
+
ref: opts.ref
|
|
1543
|
+
})
|
|
1544
|
+
)
|
|
1545
|
+
);
|
|
1546
|
+
program.command("search <query>").description("Search skills by name, description, tags").option("--content <dir>").option("--ref <ref>").action(
|
|
1547
|
+
(query, opts) => run(() => searchCommand({ cwd: process.cwd(), query, content: opts.content, ref: opts.ref }))
|
|
1548
|
+
);
|
|
1549
|
+
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(
|
|
1550
|
+
(name, opts) => run(
|
|
1551
|
+
() => viewCommand({ cwd: process.cwd(), name, raw: opts.raw, content: opts.content, ref: opts.ref })
|
|
1552
|
+
)
|
|
1553
|
+
);
|
|
1554
|
+
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(
|
|
1555
|
+
(names, opts) => run(
|
|
1556
|
+
() => addCommand({
|
|
1557
|
+
cwd: process.cwd(),
|
|
1558
|
+
names,
|
|
1559
|
+
targets: parseTargets(opts.target),
|
|
1560
|
+
withPairs: opts.withPairs,
|
|
1561
|
+
dryRun: opts.dryRun,
|
|
1562
|
+
overwrite: opts.overwrite,
|
|
1563
|
+
content: opts.content,
|
|
1564
|
+
ref: opts.ref
|
|
1565
|
+
})
|
|
1566
|
+
)
|
|
1567
|
+
);
|
|
1568
|
+
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(
|
|
1569
|
+
(names, opts) => run(
|
|
1570
|
+
() => updateCommand({
|
|
1571
|
+
cwd: process.cwd(),
|
|
1572
|
+
names,
|
|
1573
|
+
dryRun: opts.dryRun,
|
|
1574
|
+
overwrite: opts.overwrite,
|
|
1575
|
+
content: opts.content,
|
|
1576
|
+
ref: opts.ref
|
|
1577
|
+
})
|
|
1578
|
+
)
|
|
1579
|
+
);
|
|
1580
|
+
program.command("remove <names...>").description("Remove installed skills").option("--target <list>", "comma list or 'all'").option("--dry-run").action(
|
|
1581
|
+
(names, opts) => run(
|
|
1582
|
+
() => removeCommand({ cwd: process.cwd(), names, targets: parseTargets(opts.target), dryRun: opts.dryRun })
|
|
1583
|
+
)
|
|
1584
|
+
);
|
|
1585
|
+
program.command("doctor").description("Check installed skills for drift and missing files").action(() => run(() => doctorCommand({ cwd: process.cwd() })));
|
|
1586
|
+
program.command("lint").description("Validate the skill library (maintainer command)").option("--content <dir>").action((opts) => run(() => lintCommand({ cwd: process.cwd(), content: opts.content }), true));
|
|
1587
|
+
program.command("new <spec>").description("Scaffold a new skill: class/category/name (maintainer command)").option("--content <dir>").option("--force").action(
|
|
1588
|
+
(spec, opts) => run(() => newSkillCommand({ cwd: process.cwd(), spec, content: opts.content, force: opts.force }))
|
|
1589
|
+
);
|
|
1590
|
+
var registry = program.command("registry").description("Registry maintenance");
|
|
1591
|
+
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(
|
|
1592
|
+
(opts) => run(
|
|
1593
|
+
() => registryBuildCommand({ cwd: process.cwd(), content: opts.content, check: opts.check, version: opts.version }),
|
|
1594
|
+
true
|
|
1595
|
+
)
|
|
1596
|
+
);
|
|
1597
|
+
var marketplace = program.command("marketplace").description("Claude marketplace maintenance");
|
|
1598
|
+
marketplace.command("build").description("Generate .claude-plugin/marketplace.json and per-class plugins").option("--content <dir>").option("--out <dir>", "output root").option("--version <v>").action(
|
|
1599
|
+
(opts) => run(
|
|
1600
|
+
() => marketplaceBuildCommand({ cwd: process.cwd(), content: opts.content, out: opts.out, version: opts.version })
|
|
1601
|
+
)
|
|
1602
|
+
);
|
|
1603
|
+
program.parseAsync(process.argv);
|
|
1604
|
+
//# sourceMappingURL=bin.js.map
|