@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/README.md +9 -3
- package/dist/bin.js +741 -196
- package/package.json +7 -4
- package/dist/bin.js.map +0 -1
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.
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
> ${
|
|
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##
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
458
|
+
console.log(msg);
|
|
341
459
|
},
|
|
342
460
|
info(msg) {
|
|
343
|
-
|
|
461
|
+
console.log(`${sym.info} ${msg}`);
|
|
344
462
|
},
|
|
345
463
|
success(msg) {
|
|
346
|
-
|
|
464
|
+
console.log(`${sym.ok} ${msg}`);
|
|
347
465
|
},
|
|
348
466
|
warn(msg) {
|
|
349
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
435
|
-
var SkillClassSchema =
|
|
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 =
|
|
448
|
-
domain:
|
|
562
|
+
var XSkillsMasterSchema = z4.object({
|
|
563
|
+
domain: z4.string().min(1),
|
|
449
564
|
class: SkillClassSchema,
|
|
450
|
-
category:
|
|
451
|
-
platforms:
|
|
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:
|
|
454
|
-
pairs_with:
|
|
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:
|
|
457
|
-
snapshot_date:
|
|
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:
|
|
574
|
+
version: z4.string().refine((v) => semver.valid(v) != null, "must be a valid semver version")
|
|
460
575
|
}).strict();
|
|
461
|
-
var GlobsSchema =
|
|
462
|
-
var FrontmatterSchema =
|
|
463
|
-
name:
|
|
464
|
-
description:
|
|
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:
|
|
581
|
+
tags: z4.array(z4.string()).default([]),
|
|
467
582
|
"x-skills-master": XSkillsMasterSchema
|
|
468
|
-
})
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
624
|
-
dir
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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 {
|
|
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"
|
|
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
|
-
|
|
714
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
919
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
|
1051
|
-
if (
|
|
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
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
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 (
|
|
1062
|
-
|
|
1063
|
-
|
|
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 (
|
|
1067
|
-
|
|
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 ${
|
|
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
|
|
1593
|
+
const q = searchNormalize(opts.query);
|
|
1128
1594
|
const hits = reg.skills.filter(
|
|
1129
|
-
(s) =>
|
|
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(
|
|
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(
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1776
|
+
push(
|
|
1777
|
+
"warn",
|
|
1778
|
+
`SKILL.md body is ${lineCount} lines (approaching the ${MAX_BODY_LINES}-line cap)`
|
|
1779
|
+
);
|
|
1256
1780
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
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(
|
|
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({
|
|
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]) => ({
|
|
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({
|
|
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({
|
|
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("
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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({
|
|
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.
|
|
2221
|
+
version: opts.setVersion
|
|
1679
2222
|
}),
|
|
1680
2223
|
true
|
|
1681
2224
|
)
|
|
1682
2225
|
);
|
|
1683
|
-
program.parseAsync(process.argv)
|
|
1684
|
-
|
|
2226
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
2227
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
2228
|
+
process.exitCode = 1;
|
|
2229
|
+
});
|