mdkg 0.0.3 → 0.0.5
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 +181 -169
- package/dist/cli.js +1002 -624
- package/dist/commands/checkpoint.js +17 -6
- package/dist/commands/event.js +46 -0
- package/dist/commands/event_support.js +146 -0
- package/dist/commands/format.js +6 -7
- package/dist/commands/index.js +10 -4
- package/dist/commands/init.js +224 -16
- package/dist/commands/list.js +18 -1
- package/dist/commands/new.js +30 -5
- package/dist/commands/pack.js +194 -2
- package/dist/commands/query_output.js +84 -0
- package/dist/commands/search.js +22 -5
- package/dist/commands/show.js +26 -11
- package/dist/commands/skill.js +374 -0
- package/dist/commands/skill_mirror.js +290 -0
- package/dist/commands/skill_support.js +122 -0
- package/dist/commands/task.js +278 -0
- package/dist/commands/validate.js +106 -7
- package/dist/graph/edges.js +2 -2
- package/dist/graph/frontmatter.js +1 -0
- package/dist/graph/indexer.js +21 -0
- package/dist/graph/node.js +20 -4
- package/dist/graph/skills_index_cache.js +94 -0
- package/dist/graph/skills_indexer.js +160 -0
- package/dist/init/AGENTS.md +6 -41
- package/dist/init/AGENT_START.md +46 -0
- package/dist/init/CLAUDE.md +6 -35
- package/dist/init/CLI_COMMAND_MATRIX.md +29 -0
- package/dist/init/README.md +6 -4
- package/dist/init/core/HUMAN.md +36 -0
- package/dist/init/core/SOUL.md +257 -0
- package/dist/init/core/core.md +3 -1
- package/dist/init/core/rule-1-mdkg-conventions.md +9 -2
- package/dist/init/core/rule-3-cli-contract.md +81 -14
- package/dist/init/core/rule-4-repo-safety-and-ignores.md +9 -3
- package/dist/init/core/rule-6-templates-and-schemas.md +6 -2
- package/dist/init/llms.txt +17 -0
- package/dist/init/skills/SKILL.md.example +41 -0
- package/dist/init/templates/default/bug.md +1 -0
- package/dist/init/templates/default/chk.md +1 -0
- package/dist/init/templates/default/epic.md +1 -0
- package/dist/init/templates/default/feat.md +1 -0
- package/dist/init/templates/default/task.md +1 -0
- package/dist/init/templates/default/test.md +1 -0
- package/dist/pack/export_md.js +6 -0
- package/dist/pack/export_xml.js +6 -0
- package/dist/pack/pack.js +35 -0
- package/dist/util/argparse.js +36 -5
- package/dist/util/filter.js +18 -0
- package/dist/util/id.js +23 -0
- package/package.json +5 -2
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runSkillNewCommand = runSkillNewCommand;
|
|
7
|
+
exports.runSkillListCommand = runSkillListCommand;
|
|
8
|
+
exports.runSkillShowCommand = runSkillShowCommand;
|
|
9
|
+
exports.runSkillSearchCommand = runSkillSearchCommand;
|
|
10
|
+
exports.runSkillValidateCommand = runSkillValidateCommand;
|
|
11
|
+
exports.runSkillSyncCommand = runSkillSyncCommand;
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const config_1 = require("../core/config");
|
|
15
|
+
const skills_indexer_1 = require("../graph/skills_indexer");
|
|
16
|
+
const skills_index_cache_1 = require("../graph/skills_index_cache");
|
|
17
|
+
const skills_index_cache_2 = require("../graph/skills_index_cache");
|
|
18
|
+
const errors_1 = require("../util/errors");
|
|
19
|
+
const skill_support_1 = require("./skill_support");
|
|
20
|
+
const query_output_1 = require("./query_output");
|
|
21
|
+
const event_support_1 = require("./event_support");
|
|
22
|
+
const skill_mirror_1 = require("./skill_mirror");
|
|
23
|
+
function parseCsvList(raw) {
|
|
24
|
+
if (!raw) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return raw
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((value) => value.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
function normalizeLowercaseList(raw) {
|
|
33
|
+
return parseCsvList(raw).map((value) => value.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
function normalizeSlug(raw) {
|
|
36
|
+
const slug = raw.trim().toLowerCase();
|
|
37
|
+
if (!skills_indexer_1.SKILL_SLUG_RE.test(slug)) {
|
|
38
|
+
throw new errors_1.UsageError(`skill slug must be kebab-case: ${raw}`);
|
|
39
|
+
}
|
|
40
|
+
return slug;
|
|
41
|
+
}
|
|
42
|
+
function resolveSkillPaths(root, slug) {
|
|
43
|
+
const config = (0, config_1.loadConfig)(root);
|
|
44
|
+
const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(root, config);
|
|
45
|
+
const skillDir = path_1.default.join(skillsRoot, slug);
|
|
46
|
+
return {
|
|
47
|
+
skillDir,
|
|
48
|
+
canonicalPath: path_1.default.join(skillDir, "SKILL.md"),
|
|
49
|
+
compatPath: path_1.default.join(skillDir, "SKILLS.md"),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function validateSingleSkill(root, slug) {
|
|
53
|
+
const { skillDir, canonicalPath, compatPath } = resolveSkillPaths(root, slug);
|
|
54
|
+
const warnings = [];
|
|
55
|
+
const errors = [];
|
|
56
|
+
const hasCanonical = fs_1.default.existsSync(canonicalPath);
|
|
57
|
+
const hasCompat = fs_1.default.existsSync(compatPath);
|
|
58
|
+
if (!fs_1.default.existsSync(skillDir)) {
|
|
59
|
+
throw new errors_1.NotFoundError(`skill not found: ${slug}`);
|
|
60
|
+
}
|
|
61
|
+
if (hasCanonical && hasCompat) {
|
|
62
|
+
errors.push(`${skillDir}: both SKILL.md and SKILLS.md exist`);
|
|
63
|
+
return { warnings, errors };
|
|
64
|
+
}
|
|
65
|
+
if (!hasCanonical && !hasCompat) {
|
|
66
|
+
errors.push(`${skillDir}: missing SKILL.md or SKILLS.md`);
|
|
67
|
+
return { warnings, errors };
|
|
68
|
+
}
|
|
69
|
+
const skillPath = hasCanonical ? canonicalPath : compatPath;
|
|
70
|
+
if (!hasCanonical) {
|
|
71
|
+
warnings.push(`${path_1.default.relative(root, compatPath)}: using legacy SKILLS.md compatibility file`);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
(0, skills_indexer_1.buildSkillIndexEntry)(root, slug, skillPath);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const message = err instanceof Error ? err.message : "unknown skill validation error";
|
|
78
|
+
errors.push(message);
|
|
79
|
+
}
|
|
80
|
+
return { warnings, errors };
|
|
81
|
+
}
|
|
82
|
+
function maybeLine(label, values) {
|
|
83
|
+
if (values.length === 0) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return `${label}: ${values.join(", ")}`;
|
|
87
|
+
}
|
|
88
|
+
function filterSkills(skills, tags, tagsMode = "any") {
|
|
89
|
+
const normalizedTags = tags?.map((value) => value.toLowerCase()).filter(Boolean) ?? [];
|
|
90
|
+
if (normalizedTags.length === 0) {
|
|
91
|
+
return skills;
|
|
92
|
+
}
|
|
93
|
+
return skills.filter((skill) => {
|
|
94
|
+
const skillTags = new Set(skill.tags.map((value) => value.toLowerCase()));
|
|
95
|
+
if (tagsMode === "all") {
|
|
96
|
+
return normalizedTags.every((value) => skillTags.has(value));
|
|
97
|
+
}
|
|
98
|
+
return normalizedTags.some((value) => skillTags.has(value));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function buildSkillSearchText(skill) {
|
|
102
|
+
const ochatrTokens = Object.entries(skill.ochatr).flatMap(([key, value]) => {
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
return [key, ...value];
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === "boolean") {
|
|
107
|
+
return [key, value ? "true" : "false"];
|
|
108
|
+
}
|
|
109
|
+
return [key, value];
|
|
110
|
+
});
|
|
111
|
+
const tokens = [
|
|
112
|
+
skill.slug,
|
|
113
|
+
skill.id,
|
|
114
|
+
skill.qid,
|
|
115
|
+
skill.name,
|
|
116
|
+
skill.description,
|
|
117
|
+
skill.path,
|
|
118
|
+
...skill.tags,
|
|
119
|
+
...skill.authors,
|
|
120
|
+
...skill.links,
|
|
121
|
+
...ochatrTokens,
|
|
122
|
+
];
|
|
123
|
+
return tokens.join(" ").toLowerCase();
|
|
124
|
+
}
|
|
125
|
+
function matchesSkillQuery(skill, terms) {
|
|
126
|
+
const text = buildSkillSearchText(skill);
|
|
127
|
+
for (const term of terms) {
|
|
128
|
+
if (!text.includes(term)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
function runSkillNewCommand(options) {
|
|
135
|
+
const root = options.root;
|
|
136
|
+
const config = (0, config_1.loadConfig)(root);
|
|
137
|
+
const slug = normalizeSlug(options.slug);
|
|
138
|
+
const name = options.name.trim();
|
|
139
|
+
const description = options.description.trim();
|
|
140
|
+
if (!name) {
|
|
141
|
+
throw new errors_1.UsageError("skill name cannot be empty");
|
|
142
|
+
}
|
|
143
|
+
if (!description) {
|
|
144
|
+
throw new errors_1.UsageError("skill description cannot be empty");
|
|
145
|
+
}
|
|
146
|
+
const tags = normalizeLowercaseList(options.tags);
|
|
147
|
+
const authors = parseCsvList(options.authors);
|
|
148
|
+
const links = parseCsvList(options.links);
|
|
149
|
+
const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(root, config);
|
|
150
|
+
const skillDir = path_1.default.join(skillsRoot, slug);
|
|
151
|
+
const canonicalPath = path_1.default.join(skillDir, "SKILL.md");
|
|
152
|
+
const compatPath = path_1.default.join(skillDir, "SKILLS.md");
|
|
153
|
+
const force = Boolean(options.force);
|
|
154
|
+
if (fs_1.default.existsSync(compatPath)) {
|
|
155
|
+
throw new errors_1.UsageError(`legacy compatibility file exists for ${slug}; migrate SKILLS.md before scaffolding`);
|
|
156
|
+
}
|
|
157
|
+
if (fs_1.default.existsSync(canonicalPath) && !force) {
|
|
158
|
+
throw new errors_1.UsageError(`skill already exists: ${path_1.default.relative(root, canonicalPath)} (use --force to overwrite)`);
|
|
159
|
+
}
|
|
160
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
161
|
+
fs_1.default.mkdirSync(path_1.default.join(skillDir, "references"), { recursive: true });
|
|
162
|
+
fs_1.default.mkdirSync(path_1.default.join(skillDir, "assets"), { recursive: true });
|
|
163
|
+
if (options.withScripts) {
|
|
164
|
+
fs_1.default.mkdirSync(path_1.default.join(skillDir, "scripts"), { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
const content = (0, skill_support_1.renderSkillTemplate)({
|
|
167
|
+
name,
|
|
168
|
+
description,
|
|
169
|
+
tags,
|
|
170
|
+
authors,
|
|
171
|
+
links,
|
|
172
|
+
});
|
|
173
|
+
fs_1.default.writeFileSync(canonicalPath, content, "utf8");
|
|
174
|
+
(0, skill_support_1.ensureSkillsRegistry)(root, config);
|
|
175
|
+
(0, skill_support_1.refreshSkillsRegistry)(root, config);
|
|
176
|
+
if ((0, skill_mirror_1.shouldMaintainSkillMirrors)(root)) {
|
|
177
|
+
(0, skill_mirror_1.syncSkillMirrors)({ root, config, createRoots: true, force });
|
|
178
|
+
}
|
|
179
|
+
if (config.index.auto_reindex) {
|
|
180
|
+
const skillsIndex = (0, skills_indexer_1.buildSkillsIndex)(root, config);
|
|
181
|
+
(0, skills_index_cache_2.writeSkillsIndex)((0, skills_indexer_1.resolveSkillsIndexPath)(root), skillsIndex);
|
|
182
|
+
}
|
|
183
|
+
(0, event_support_1.appendAutomaticEvent)({
|
|
184
|
+
root,
|
|
185
|
+
ws: "root",
|
|
186
|
+
kind: "SKILL_CREATED",
|
|
187
|
+
status: "ok",
|
|
188
|
+
refs: [`skill:${slug}`],
|
|
189
|
+
notes: `skill created via mdkg skill new`,
|
|
190
|
+
runId: options.runId,
|
|
191
|
+
skill: slug,
|
|
192
|
+
now: options.now,
|
|
193
|
+
});
|
|
194
|
+
console.log(`skill created: root:skill:${slug} (${path_1.default.relative(root, canonicalPath)})`);
|
|
195
|
+
}
|
|
196
|
+
function runSkillListCommand(options) {
|
|
197
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
198
|
+
const { index, rebuilt, stale } = (0, skills_index_cache_1.loadSkillsIndex)({
|
|
199
|
+
root: options.root,
|
|
200
|
+
config,
|
|
201
|
+
useCache: !options.noCache,
|
|
202
|
+
allowReindex: !options.noReindex,
|
|
203
|
+
});
|
|
204
|
+
if (stale && !rebuilt && !options.noCache) {
|
|
205
|
+
console.error("warning: skills index is stale; run mdkg index to refresh");
|
|
206
|
+
}
|
|
207
|
+
const skills = filterSkills(Object.values(index.skills), options.tags, options.tagsMode ?? "any").sort((a, b) => a.qid.localeCompare(b.qid));
|
|
208
|
+
if (options.json) {
|
|
209
|
+
(0, query_output_1.writeJson)({
|
|
210
|
+
command: "list",
|
|
211
|
+
kind: "skill",
|
|
212
|
+
count: skills.length,
|
|
213
|
+
items: skills.map(query_output_1.toSkillSummaryJson),
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
(0, query_output_1.writeCount)(skills.length, skills.length === 0 ? "no skills matched current filters" : undefined);
|
|
218
|
+
for (const skill of skills) {
|
|
219
|
+
console.log((0, skill_support_1.formatSkillCard)(skill));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function runSkillShowCommand(options) {
|
|
223
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
224
|
+
const slug = normalizeSlug(options.slug);
|
|
225
|
+
const { index, rebuilt, stale } = (0, skills_index_cache_1.loadSkillsIndex)({
|
|
226
|
+
root: options.root,
|
|
227
|
+
config,
|
|
228
|
+
useCache: !options.noCache,
|
|
229
|
+
allowReindex: !options.noReindex,
|
|
230
|
+
});
|
|
231
|
+
if (stale && !rebuilt && !options.noCache) {
|
|
232
|
+
console.error("warning: skills index is stale; run mdkg index to refresh");
|
|
233
|
+
}
|
|
234
|
+
const skill = index.skills[slug];
|
|
235
|
+
if (!skill) {
|
|
236
|
+
throw new errors_1.NotFoundError(`skill not found: ${slug}`);
|
|
237
|
+
}
|
|
238
|
+
const skillPath = path_1.default.resolve(options.root, skill.path);
|
|
239
|
+
let body = "";
|
|
240
|
+
if (!options.metaOnly) {
|
|
241
|
+
if (!fs_1.default.existsSync(skillPath)) {
|
|
242
|
+
throw new errors_1.NotFoundError(`file not found for ${skill.id}: ${skill.path}`);
|
|
243
|
+
}
|
|
244
|
+
body = fs_1.default.readFileSync(skillPath, "utf8").trimEnd();
|
|
245
|
+
}
|
|
246
|
+
if (options.json) {
|
|
247
|
+
(0, query_output_1.writeJson)({
|
|
248
|
+
command: "show",
|
|
249
|
+
kind: "skill",
|
|
250
|
+
item: (0, query_output_1.toSkillDetailJson)(skill, options.metaOnly ? undefined : body),
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (options.metaOnly) {
|
|
255
|
+
const lines = [];
|
|
256
|
+
lines.push((0, skill_support_1.formatSkillCard)(skill));
|
|
257
|
+
lines.push(`description: ${skill.description}`);
|
|
258
|
+
const tagsLine = maybeLine("tags", skill.tags);
|
|
259
|
+
if (tagsLine) {
|
|
260
|
+
lines.push(tagsLine);
|
|
261
|
+
}
|
|
262
|
+
if (skill.version) {
|
|
263
|
+
lines.push(`version: ${skill.version}`);
|
|
264
|
+
}
|
|
265
|
+
const authorsLine = maybeLine("authors", skill.authors);
|
|
266
|
+
if (authorsLine) {
|
|
267
|
+
lines.push(authorsLine);
|
|
268
|
+
}
|
|
269
|
+
const linksLine = maybeLine("links", skill.links);
|
|
270
|
+
if (linksLine) {
|
|
271
|
+
lines.push(linksLine);
|
|
272
|
+
}
|
|
273
|
+
lines.push(`has_scripts: ${skill.has_scripts ? "true" : "false"}`);
|
|
274
|
+
lines.push(`has_references: ${skill.has_references ? "true" : "false"}`);
|
|
275
|
+
for (const [key, value] of Object.entries(skill.ochatr).sort(([a], [b]) => a.localeCompare(b))) {
|
|
276
|
+
if (Array.isArray(value)) {
|
|
277
|
+
lines.push(`${key}: ${value.join(", ")}`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (typeof value === "boolean") {
|
|
281
|
+
lines.push(`${key}: ${value ? "true" : "false"}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
lines.push(`${key}: ${value}`);
|
|
285
|
+
}
|
|
286
|
+
console.log(lines.join("\n"));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
console.log(body);
|
|
290
|
+
}
|
|
291
|
+
function runSkillSearchCommand(options) {
|
|
292
|
+
const query = options.query.trim();
|
|
293
|
+
if (!query) {
|
|
294
|
+
throw new errors_1.UsageError("search query cannot be empty");
|
|
295
|
+
}
|
|
296
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
297
|
+
const { index, rebuilt, stale } = (0, skills_index_cache_1.loadSkillsIndex)({
|
|
298
|
+
root: options.root,
|
|
299
|
+
config,
|
|
300
|
+
useCache: !options.noCache,
|
|
301
|
+
allowReindex: !options.noReindex,
|
|
302
|
+
});
|
|
303
|
+
if (stale && !rebuilt && !options.noCache) {
|
|
304
|
+
console.error("warning: skills index is stale; run mdkg index to refresh");
|
|
305
|
+
}
|
|
306
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
307
|
+
const skills = filterSkills(Object.values(index.skills), options.tags, options.tagsMode ?? "any")
|
|
308
|
+
.filter((skill) => matchesSkillQuery(skill, terms))
|
|
309
|
+
.sort((a, b) => a.qid.localeCompare(b.qid));
|
|
310
|
+
if (options.json) {
|
|
311
|
+
(0, query_output_1.writeJson)({
|
|
312
|
+
command: "search",
|
|
313
|
+
kind: "skill",
|
|
314
|
+
count: skills.length,
|
|
315
|
+
items: skills.map(query_output_1.toSkillSummaryJson),
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
(0, query_output_1.writeCount)(skills.length, skills.length === 0 ? `no skills matched query "${query}"` : undefined);
|
|
320
|
+
for (const skill of skills) {
|
|
321
|
+
console.log((0, skill_support_1.formatSkillCard)(skill));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function runSkillValidateCommand(options) {
|
|
325
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
326
|
+
const targetSlug = options.slug?.trim().toLowerCase();
|
|
327
|
+
const warnings = [];
|
|
328
|
+
const errors = [];
|
|
329
|
+
if (targetSlug) {
|
|
330
|
+
const result = validateSingleSkill(options.root, normalizeSlug(targetSlug));
|
|
331
|
+
warnings.push(...result.warnings);
|
|
332
|
+
errors.push(...result.errors);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const skillsRoot = (0, skills_indexer_1.resolveSkillsRoot)(options.root, config);
|
|
336
|
+
if (fs_1.default.existsSync(skillsRoot)) {
|
|
337
|
+
const entries = fs_1.default.readdirSync(skillsRoot, { withFileTypes: true });
|
|
338
|
+
for (const entry of entries.filter((value) => value.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
339
|
+
const result = validateSingleSkill(options.root, entry.name.toLowerCase());
|
|
340
|
+
warnings.push(...result.warnings);
|
|
341
|
+
errors.push(...result.errors);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
for (const warning of Array.from(new Set(warnings))) {
|
|
346
|
+
console.error(`warning: ${warning}`);
|
|
347
|
+
}
|
|
348
|
+
if (errors.length > 0) {
|
|
349
|
+
for (const error of Array.from(new Set(errors))) {
|
|
350
|
+
console.error(error);
|
|
351
|
+
}
|
|
352
|
+
throw new errors_1.ValidationError(`skill validation failed with ${Array.from(new Set(errors)).length} error(s)`);
|
|
353
|
+
}
|
|
354
|
+
if (targetSlug) {
|
|
355
|
+
console.log(`skill validation ok: ${targetSlug} (1 skill checked)`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const checkedCount = fs_1.default.existsSync((0, skills_indexer_1.resolveSkillsRoot)(options.root, config))
|
|
359
|
+
? fs_1.default
|
|
360
|
+
.readdirSync((0, skills_indexer_1.resolveSkillsRoot)(options.root, config), { withFileTypes: true })
|
|
361
|
+
.filter((value) => value.isDirectory()).length
|
|
362
|
+
: 0;
|
|
363
|
+
console.log(`skill validation ok: ${checkedCount} skill${checkedCount === 1 ? "" : "s"} checked`);
|
|
364
|
+
}
|
|
365
|
+
function runSkillSyncCommand(options) {
|
|
366
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
367
|
+
const result = (0, skill_mirror_1.syncSkillMirrors)({
|
|
368
|
+
root: options.root,
|
|
369
|
+
config,
|
|
370
|
+
createRoots: true,
|
|
371
|
+
force: options.force,
|
|
372
|
+
});
|
|
373
|
+
console.log(`skill mirror sync ok: ${result.synced} synced, ${result.pruned} pruned across ${result.targets} target${result.targets === 1 ? "" : "s"}`);
|
|
374
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.syncSkillMirrors = syncSkillMirrors;
|
|
7
|
+
exports.shouldMaintainSkillMirrors = shouldMaintainSkillMirrors;
|
|
8
|
+
exports.auditSkillMirrors = auditSkillMirrors;
|
|
9
|
+
exports.scaffoldMirrorRoots = scaffoldMirrorRoots;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const skills_indexer_1 = require("../graph/skills_indexer");
|
|
13
|
+
const errors_1 = require("../util/errors");
|
|
14
|
+
const MIRROR_PRODUCTS = ["agents", "claude"];
|
|
15
|
+
const MANIFEST_FILE = ".mdkg-managed.json";
|
|
16
|
+
const MANAGED_ROOT_MARKERS = [
|
|
17
|
+
path_1.default.join(".mdkg", "core", "SOUL.md"),
|
|
18
|
+
path_1.default.join(".mdkg", "core", "HUMAN.md"),
|
|
19
|
+
];
|
|
20
|
+
const ALLOWED_ROOT_ENTRIES = ["SKILL.md", "references", "assets", "scripts"];
|
|
21
|
+
function resolveMirrorTargets(root) {
|
|
22
|
+
return MIRROR_PRODUCTS.map((product) => {
|
|
23
|
+
const rootDir = path_1.default.join(root, `.${product}`);
|
|
24
|
+
const skillsRoot = path_1.default.join(rootDir, "skills");
|
|
25
|
+
return {
|
|
26
|
+
product,
|
|
27
|
+
rootDir,
|
|
28
|
+
skillsRoot,
|
|
29
|
+
manifestPath: path_1.default.join(skillsRoot, MANIFEST_FILE),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function readManifest(target) {
|
|
34
|
+
if (!fs_1.default.existsSync(target.manifestPath)) {
|
|
35
|
+
return new Set();
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(target.manifestPath, "utf8"));
|
|
39
|
+
if (!Array.isArray(parsed.managed_slugs)) {
|
|
40
|
+
return new Set();
|
|
41
|
+
}
|
|
42
|
+
return new Set(parsed.managed_slugs
|
|
43
|
+
.map((value) => String(value).trim().toLowerCase())
|
|
44
|
+
.filter(Boolean));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return new Set();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeManifest(target, managed) {
|
|
51
|
+
const payload = {
|
|
52
|
+
managed_slugs: Array.from(new Set(Array.from(managed).map((value) => value.toLowerCase()))).sort(),
|
|
53
|
+
};
|
|
54
|
+
fs_1.default.mkdirSync(target.skillsRoot, { recursive: true });
|
|
55
|
+
fs_1.default.writeFileSync(target.manifestPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
56
|
+
}
|
|
57
|
+
function shouldCreateMirrorRoots(root) {
|
|
58
|
+
if (MANAGED_ROOT_MARKERS.some((relPath) => fs_1.default.existsSync(path_1.default.join(root, relPath)))) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return resolveMirrorTargets(root).some((target) => fs_1.default.existsSync(target.rootDir) || fs_1.default.existsSync(target.skillsRoot));
|
|
62
|
+
}
|
|
63
|
+
function listAllowedEntries(dirPath) {
|
|
64
|
+
if (!fs_1.default.existsSync(dirPath)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const entries = [];
|
|
68
|
+
const queue = [dirPath];
|
|
69
|
+
while (queue.length > 0) {
|
|
70
|
+
const current = queue.shift();
|
|
71
|
+
const dirEntries = fs_1.default.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
72
|
+
for (const entry of dirEntries) {
|
|
73
|
+
const fullPath = path_1.default.join(current, entry.name);
|
|
74
|
+
const relPath = path_1.default.relative(dirPath, fullPath).replace(/\\/g, "/");
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
entries.push(`${relPath}/`);
|
|
77
|
+
queue.push(fullPath);
|
|
78
|
+
}
|
|
79
|
+
else if (entry.isFile()) {
|
|
80
|
+
entries.push(relPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return entries.sort();
|
|
85
|
+
}
|
|
86
|
+
function resolveSkillDocPath(skillDir) {
|
|
87
|
+
const canonicalPath = path_1.default.join(skillDir, "SKILL.md");
|
|
88
|
+
const compatPath = path_1.default.join(skillDir, "SKILLS.md");
|
|
89
|
+
if (fs_1.default.existsSync(canonicalPath) && fs_1.default.existsSync(compatPath)) {
|
|
90
|
+
throw new errors_1.UsageError(`${skillDir}: both SKILL.md and SKILLS.md exist; fix the canonical skill first`);
|
|
91
|
+
}
|
|
92
|
+
if (fs_1.default.existsSync(canonicalPath)) {
|
|
93
|
+
return canonicalPath;
|
|
94
|
+
}
|
|
95
|
+
if (fs_1.default.existsSync(compatPath)) {
|
|
96
|
+
return compatPath;
|
|
97
|
+
}
|
|
98
|
+
throw new errors_1.UsageError(`${skillDir}: missing SKILL.md or SKILLS.md`);
|
|
99
|
+
}
|
|
100
|
+
function loadCanonicalSources(root, config) {
|
|
101
|
+
const index = (0, skills_indexer_1.buildSkillsIndex)(root, config);
|
|
102
|
+
return Object.values(index.skills)
|
|
103
|
+
.sort((a, b) => a.slug.localeCompare(b.slug))
|
|
104
|
+
.map((entry) => {
|
|
105
|
+
const docPath = path_1.default.resolve(root, entry.path);
|
|
106
|
+
return {
|
|
107
|
+
slug: entry.slug,
|
|
108
|
+
sourceDir: path_1.default.dirname(docPath),
|
|
109
|
+
docPath,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function copyDir(srcDir, destDir) {
|
|
114
|
+
fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
115
|
+
const entries = fs_1.default.readdirSync(srcDir, { withFileTypes: true });
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const srcPath = path_1.default.join(srcDir, entry.name);
|
|
118
|
+
const destPath = path_1.default.join(destDir, entry.name);
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
copyDir(srcPath, destPath);
|
|
121
|
+
}
|
|
122
|
+
else if (entry.isFile()) {
|
|
123
|
+
fs_1.default.mkdirSync(path_1.default.dirname(destPath), { recursive: true });
|
|
124
|
+
fs_1.default.copyFileSync(srcPath, destPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function materializeSkillMirror(source, destDir) {
|
|
129
|
+
fs_1.default.rmSync(destDir, { recursive: true, force: true });
|
|
130
|
+
fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
131
|
+
fs_1.default.copyFileSync(source.docPath, path_1.default.join(destDir, "SKILL.md"));
|
|
132
|
+
for (const entry of ["references", "assets", "scripts"]) {
|
|
133
|
+
const srcPath = path_1.default.join(source.sourceDir, entry);
|
|
134
|
+
if (fs_1.default.existsSync(srcPath) && fs_1.default.statSync(srcPath).isDirectory()) {
|
|
135
|
+
copyDir(srcPath, path_1.default.join(destDir, entry));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function collectExpectedSkillTree(source) {
|
|
140
|
+
const expected = new Map();
|
|
141
|
+
expected.set("SKILL.md", fs_1.default.readFileSync(source.docPath, "utf8"));
|
|
142
|
+
for (const entry of ["references", "assets", "scripts"]) {
|
|
143
|
+
const srcPath = path_1.default.join(source.sourceDir, entry);
|
|
144
|
+
if (!fs_1.default.existsSync(srcPath) || !fs_1.default.statSync(srcPath).isDirectory()) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
expected.set(`${entry}/`, "dir");
|
|
148
|
+
for (const relPath of listAllowedEntries(srcPath)) {
|
|
149
|
+
const absolute = path_1.default.join(srcPath, relPath.replace(/\/$/, ""));
|
|
150
|
+
expected.set(`${entry}/${relPath}`, relPath.endsWith("/") ? "dir" : fs_1.default.readFileSync(absolute, "utf8"));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return expected;
|
|
154
|
+
}
|
|
155
|
+
function collectActualSkillTree(destDir) {
|
|
156
|
+
const entries = new Map();
|
|
157
|
+
if (!fs_1.default.existsSync(destDir)) {
|
|
158
|
+
return { entries, hasUnexpectedRootEntry: false };
|
|
159
|
+
}
|
|
160
|
+
const rootEntries = fs_1.default.readdirSync(destDir, { withFileTypes: true });
|
|
161
|
+
let hasUnexpectedRootEntry = false;
|
|
162
|
+
for (const entry of rootEntries) {
|
|
163
|
+
if (!ALLOWED_ROOT_ENTRIES.includes(entry.name)) {
|
|
164
|
+
hasUnexpectedRootEntry = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const fullPath = path_1.default.join(destDir, entry.name);
|
|
168
|
+
if (entry.isFile()) {
|
|
169
|
+
entries.set(entry.name, fs_1.default.readFileSync(fullPath, "utf8"));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!entry.isDirectory()) {
|
|
173
|
+
hasUnexpectedRootEntry = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
entries.set(`${entry.name}/`, "dir");
|
|
177
|
+
for (const relPath of listAllowedEntries(fullPath)) {
|
|
178
|
+
const absolute = path_1.default.join(fullPath, relPath.replace(/\/$/, ""));
|
|
179
|
+
entries.set(`${entry.name}/${relPath}`, relPath.endsWith("/") ? "dir" : fs_1.default.readFileSync(absolute, "utf8"));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { entries, hasUnexpectedRootEntry };
|
|
183
|
+
}
|
|
184
|
+
function isManagedSkillTreeInSync(source, destDir) {
|
|
185
|
+
const expected = collectExpectedSkillTree(source);
|
|
186
|
+
const actual = collectActualSkillTree(destDir);
|
|
187
|
+
if (actual.hasUnexpectedRootEntry) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (expected.size !== actual.entries.size) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
for (const [relPath, content] of expected.entries()) {
|
|
194
|
+
if (!actual.entries.has(relPath) || actual.entries.get(relPath) !== content) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
function syncSkillMirrors(options) {
|
|
201
|
+
const sources = loadCanonicalSources(options.root, options.config);
|
|
202
|
+
const createRoots = Boolean(options.createRoots);
|
|
203
|
+
const force = Boolean(options.force);
|
|
204
|
+
const targets = resolveMirrorTargets(options.root);
|
|
205
|
+
let synced = 0;
|
|
206
|
+
let pruned = 0;
|
|
207
|
+
let touchedTargets = 0;
|
|
208
|
+
for (const target of targets) {
|
|
209
|
+
const shouldManageTarget = createRoots || fs_1.default.existsSync(target.skillsRoot) || fs_1.default.existsSync(target.rootDir);
|
|
210
|
+
if (!shouldManageTarget) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
touchedTargets += 1;
|
|
214
|
+
fs_1.default.mkdirSync(target.skillsRoot, { recursive: true });
|
|
215
|
+
const managed = readManifest(target);
|
|
216
|
+
for (const source of sources) {
|
|
217
|
+
const destDir = path_1.default.join(target.skillsRoot, source.slug);
|
|
218
|
+
const exists = fs_1.default.existsSync(destDir);
|
|
219
|
+
if (exists && !managed.has(source.slug) && !force) {
|
|
220
|
+
throw new errors_1.UsageError(`${path_1.default.relative(options.root, destDir)} already exists and is not mdkg-managed; rerun \`mdkg skill sync --force\` to replace it`);
|
|
221
|
+
}
|
|
222
|
+
materializeSkillMirror(source, destDir);
|
|
223
|
+
managed.add(source.slug);
|
|
224
|
+
synced += 1;
|
|
225
|
+
}
|
|
226
|
+
for (const slug of Array.from(managed)) {
|
|
227
|
+
if (sources.some((source) => source.slug === slug)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const destDir = path_1.default.join(target.skillsRoot, slug);
|
|
231
|
+
if (fs_1.default.existsSync(destDir)) {
|
|
232
|
+
fs_1.default.rmSync(destDir, { recursive: true, force: true });
|
|
233
|
+
pruned += 1;
|
|
234
|
+
}
|
|
235
|
+
managed.delete(slug);
|
|
236
|
+
}
|
|
237
|
+
writeManifest(target, managed);
|
|
238
|
+
}
|
|
239
|
+
return { synced, pruned, targets: touchedTargets };
|
|
240
|
+
}
|
|
241
|
+
function shouldMaintainSkillMirrors(root) {
|
|
242
|
+
return shouldCreateMirrorRoots(root);
|
|
243
|
+
}
|
|
244
|
+
function auditSkillMirrors(root, config) {
|
|
245
|
+
const shouldAudit = shouldCreateMirrorRoots(root);
|
|
246
|
+
if (!shouldAudit) {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
const warnings = [];
|
|
250
|
+
const sources = loadCanonicalSources(root, config);
|
|
251
|
+
const sourceBySlug = new Map(sources.map((source) => [source.slug, source]));
|
|
252
|
+
for (const target of resolveMirrorTargets(root)) {
|
|
253
|
+
if (!fs_1.default.existsSync(target.skillsRoot)) {
|
|
254
|
+
warnings.push(`${path_1.default.relative(root, target.skillsRoot)}: mirror root missing; run \`mdkg skill sync\``);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const managed = readManifest(target);
|
|
258
|
+
if (!fs_1.default.existsSync(target.manifestPath)) {
|
|
259
|
+
warnings.push(`${path_1.default.relative(root, target.manifestPath)}: mirror manifest missing; run \`mdkg skill sync\``);
|
|
260
|
+
}
|
|
261
|
+
for (const source of sources) {
|
|
262
|
+
const destDir = path_1.default.join(target.skillsRoot, source.slug);
|
|
263
|
+
if (!fs_1.default.existsSync(destDir)) {
|
|
264
|
+
warnings.push(`${path_1.default.relative(root, destDir)}: missing mirrored skill; run \`mdkg skill sync\``);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (!managed.has(source.slug)) {
|
|
268
|
+
warnings.push(`${path_1.default.relative(root, destDir)}: conflicting unmanaged mirror; rerun \`mdkg skill sync --force\` to replace it`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!isManagedSkillTreeInSync(source, destDir)) {
|
|
272
|
+
warnings.push(`${path_1.default.relative(root, destDir)}: mirrored skill drift detected; run \`mdkg skill sync\``);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const slug of managed) {
|
|
276
|
+
if (!sourceBySlug.has(slug)) {
|
|
277
|
+
warnings.push(`${path_1.default.relative(root, path_1.default.join(target.skillsRoot, slug))}: stale mirrored skill; run \`mdkg skill sync\``);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return warnings;
|
|
282
|
+
}
|
|
283
|
+
function scaffoldMirrorRoots(root) {
|
|
284
|
+
for (const target of resolveMirrorTargets(root)) {
|
|
285
|
+
fs_1.default.mkdirSync(target.skillsRoot, { recursive: true });
|
|
286
|
+
if (!fs_1.default.existsSync(target.manifestPath)) {
|
|
287
|
+
writeManifest(target, []);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|