skiller 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.js +60 -1
- package/dist/cli/handlers.js +240 -5
- package/dist/cli/skills-cli.js +72 -0
- package/dist/constants.js +2 -1
- package/dist/core/ClaudePluginMigration.js +229 -0
- package/dist/core/ClaudePluginSync.js +1 -34
- package/dist/core/ClaudeProjectSync.js +37 -58
- package/dist/core/ConfigLoader.js +4 -4
- package/dist/core/FileSystemUtils.js +19 -48
- package/dist/core/FrontmatterParser.js +11 -1
- package/dist/core/LegacyClaudePluginState.js +123 -0
- package/dist/core/RulesToSkillsMigration.js +173 -0
- package/dist/core/SkillOwnership.js +397 -0
- package/dist/core/SkillsManifest.js +79 -23
- package/dist/core/SkillsProcessor.js +565 -317
- package/dist/core/SkillsUtils.js +6 -11
- package/dist/core/UnifiedConfigLoader.js +6 -5
- package/dist/core/project-paths.js +8 -0
- package/dist/lib.js +10 -16
- package/package.json +8 -8
|
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.isReferenceBody = isReferenceBody;
|
|
37
|
+
exports.extractLocalRulesFromCanonicalSkills = extractLocalRulesFromCanonicalSkills;
|
|
38
|
+
exports.compileRulesToSkills = compileRulesToSkills;
|
|
39
|
+
exports.normalizeCanonicalSkills = normalizeCanonicalSkills;
|
|
37
40
|
exports.syncMdcToSkillMd = syncMdcToSkillMd;
|
|
38
41
|
exports.discoverSkills = discoverSkills;
|
|
39
42
|
exports.copySkillsToAgent = copySkillsToAgent;
|
|
@@ -49,8 +52,74 @@ const yaml = __importStar(require("js-yaml"));
|
|
|
49
52
|
const constants_1 = require("../constants");
|
|
50
53
|
const SkillsUtils_1 = require("./SkillsUtils");
|
|
51
54
|
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
55
|
+
const SkillOwnership_1 = require("./SkillOwnership");
|
|
52
56
|
const LEGACY_CODEX_SKILLS_PATH = path.join('.codex', 'skills');
|
|
53
57
|
const UNIVERSAL_AGENTS_SKILLS_PATH = path.join('.agents', 'skills');
|
|
58
|
+
async function pathExists(targetPath) {
|
|
59
|
+
try {
|
|
60
|
+
await fs.access(targetPath);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function resolveProjectSkillsDir(projectRoot, skillerDir) {
|
|
68
|
+
if (skillerDir)
|
|
69
|
+
return path.join(skillerDir, 'skills');
|
|
70
|
+
const canonicalSkillsDir = path.join(projectRoot, '.agents', 'skills');
|
|
71
|
+
if (await pathExists(canonicalSkillsDir)) {
|
|
72
|
+
return canonicalSkillsDir;
|
|
73
|
+
}
|
|
74
|
+
const legacySkillsDir = path.join(projectRoot, '.claude', 'skills');
|
|
75
|
+
if (await pathExists(legacySkillsDir)) {
|
|
76
|
+
return legacySkillsDir;
|
|
77
|
+
}
|
|
78
|
+
return canonicalSkillsDir;
|
|
79
|
+
}
|
|
80
|
+
async function skillFolderContainsMdc(dir, depth = 0) {
|
|
81
|
+
if (depth >= constants_1.MAX_RECURSION_DEPTH)
|
|
82
|
+
return false;
|
|
83
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const fullPath = path.join(dir, entry.name);
|
|
86
|
+
if (entry.isFile() && entry.name.endsWith('.mdc')) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
const nestedHasMdc = await skillFolderContainsMdc(fullPath, depth + 1);
|
|
91
|
+
if (nestedHasMdc)
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
async function skillCanBeSymlinked(skillPath) {
|
|
98
|
+
const skillMdPath = path.join(skillPath, constants_1.SKILL_MD_FILENAME);
|
|
99
|
+
let skillMdContent;
|
|
100
|
+
try {
|
|
101
|
+
skillMdContent = await fs.readFile(skillMdPath, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (isReferenceBody((0, FrontmatterParser_1.parseFrontmatter)(skillMdContent).body).isReference) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return !(await skillFolderContainsMdc(skillPath));
|
|
110
|
+
}
|
|
111
|
+
async function createRelativeDirectorySymlink(sourceDir, targetDir) {
|
|
112
|
+
try {
|
|
113
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
114
|
+
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
115
|
+
const relativeTarget = path.relative(path.dirname(targetDir), sourceDir);
|
|
116
|
+
await fs.symlink(relativeTarget, targetDir, 'junction');
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
54
123
|
/**
|
|
55
124
|
* For non-Claude agents, compile a wrapper SKILL.md (body is a single @reference)
|
|
56
125
|
* into a standalone SKILL.md with the referenced file's body inlined.
|
|
@@ -59,30 +128,14 @@ const UNIVERSAL_AGENTS_SKILLS_PATH = path.join('.agents', 'skills');
|
|
|
59
128
|
* an @reference line, to avoid accidentally treating email addresses or
|
|
60
129
|
* "@mentions" inside real content as file references.
|
|
61
130
|
*/
|
|
62
|
-
async function compileSkillMdForNonClaudeAgents(skillMdContent, projectRoot, skillFolderPath) {
|
|
131
|
+
async function compileSkillMdForNonClaudeAgents(skillMdContent, projectRoot, skillFolderPath, options = {}) {
|
|
63
132
|
const { frontmatter, rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(skillMdContent);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const referencePath = refCheck.referencePath;
|
|
69
|
-
const absoluteRefPath = referencePath.startsWith('./') || referencePath.startsWith('../')
|
|
70
|
-
? path.resolve(skillFolderPath, referencePath)
|
|
71
|
-
: path.resolve(projectRoot, referencePath);
|
|
72
|
-
// Security: only inline references within the project root.
|
|
73
|
-
const normalizedProjectRoot = path.resolve(projectRoot);
|
|
74
|
-
const normalizedAbsoluteRefPath = path.resolve(absoluteRefPath);
|
|
75
|
-
if (!normalizedAbsoluteRefPath.startsWith(normalizedProjectRoot + path.sep)) {
|
|
76
|
-
return skillMdContent;
|
|
77
|
-
}
|
|
78
|
-
let referencedContent;
|
|
79
|
-
try {
|
|
80
|
-
referencedContent = await fs.readFile(normalizedAbsoluteRefPath, 'utf8');
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
133
|
+
const compiledBodyResult = await inlineReferenceDirectives(body, projectRoot, options.baseFilePath ?? path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME), {
|
|
134
|
+
fallbackReferenceDir: options.fallbackReferenceDir,
|
|
135
|
+
});
|
|
136
|
+
if (!compiledBodyResult.changed) {
|
|
83
137
|
return skillMdContent;
|
|
84
138
|
}
|
|
85
|
-
const { body: referencedBody } = (0, FrontmatterParser_1.parseFrontmatter)(referencedContent);
|
|
86
139
|
const fmData = rawFrontmatter && Object.keys(rawFrontmatter).length > 0
|
|
87
140
|
? rawFrontmatter
|
|
88
141
|
: frontmatter && Object.keys(frontmatter).length > 0
|
|
@@ -93,10 +146,10 @@ async function compileSkillMdForNonClaudeAgents(skillMdContent, projectRoot, ski
|
|
|
93
146
|
${yaml.dump(fmData, { lineWidth: -1, noRefs: true }).trim()}
|
|
94
147
|
---
|
|
95
148
|
|
|
96
|
-
${
|
|
149
|
+
${compiledBodyResult.body}
|
|
97
150
|
`;
|
|
98
151
|
}
|
|
99
|
-
return `${
|
|
152
|
+
return `${compiledBodyResult.body}\n`;
|
|
100
153
|
}
|
|
101
154
|
/**
|
|
102
155
|
* Copies a single skill directory to an agent skill directory:
|
|
@@ -151,301 +204,493 @@ function isReferenceBody(body) {
|
|
|
151
204
|
}
|
|
152
205
|
return { isReference: false };
|
|
153
206
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
207
|
+
function parseReferenceDirectiveLine(line) {
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
if (!trimmed.startsWith('@'))
|
|
210
|
+
return null;
|
|
211
|
+
const raw = trimmed.slice(1);
|
|
212
|
+
const hashIndex = raw.indexOf('#');
|
|
213
|
+
const pathPart = (hashIndex === -1 ? raw : raw.slice(0, hashIndex)).trim();
|
|
214
|
+
const fragment = hashIndex === -1 ? undefined : raw.slice(hashIndex + 1).trim() || undefined;
|
|
215
|
+
const looksLikePath = pathPart.startsWith('./') ||
|
|
216
|
+
pathPart.startsWith('../') ||
|
|
217
|
+
pathPart.startsWith('.agents/') ||
|
|
218
|
+
pathPart.startsWith('.claude/') ||
|
|
219
|
+
pathPart.includes('/') ||
|
|
220
|
+
/\.[A-Za-z0-9_-]+$/.test(pathPart);
|
|
221
|
+
if (!looksLikePath)
|
|
222
|
+
return null;
|
|
223
|
+
return { pathPart, fragment };
|
|
224
|
+
}
|
|
225
|
+
function slugifyHeading(value) {
|
|
226
|
+
return value
|
|
227
|
+
.trim()
|
|
228
|
+
.toLowerCase()
|
|
229
|
+
.replace(/[`*_~]/g, '')
|
|
230
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
|
231
|
+
.replace(/\s+/g, '-')
|
|
232
|
+
.replace(/-+/g, '-');
|
|
233
|
+
}
|
|
234
|
+
function extractMarkdownFragment(body, fragment) {
|
|
235
|
+
if (!fragment)
|
|
236
|
+
return body;
|
|
237
|
+
const lines = body.split('\n');
|
|
238
|
+
const target = fragment.trim();
|
|
239
|
+
const targetSlug = slugifyHeading(target);
|
|
240
|
+
let startIndex = -1;
|
|
241
|
+
let headingLevel = 0;
|
|
242
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
243
|
+
const match = /^(#{1,6})\s+(.*)$/.exec(lines[index].trim());
|
|
244
|
+
if (!match)
|
|
245
|
+
continue;
|
|
246
|
+
const headingText = match[2].trim();
|
|
247
|
+
if (headingText === target || slugifyHeading(headingText) === targetSlug) {
|
|
248
|
+
startIndex = index + 1;
|
|
249
|
+
headingLevel = match[1].length;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (startIndex === -1)
|
|
254
|
+
return body;
|
|
255
|
+
let endIndex = lines.length;
|
|
256
|
+
for (let index = startIndex; index < lines.length; index += 1) {
|
|
257
|
+
const match = /^(#{1,6})\s+/.exec(lines[index].trim());
|
|
258
|
+
if (match && match[1].length <= headingLevel) {
|
|
259
|
+
endIndex = index;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return lines.slice(startIndex, endIndex).join('\n').trim();
|
|
264
|
+
}
|
|
265
|
+
async function readReferenceDirectiveContent(projectRoot, baseFilePath, directive, options) {
|
|
266
|
+
const candidates = [];
|
|
267
|
+
const { pathPart, fragment } = directive;
|
|
268
|
+
if (pathPart.startsWith('./') || pathPart.startsWith('../')) {
|
|
269
|
+
candidates.push(path.resolve(path.dirname(baseFilePath), pathPart));
|
|
270
|
+
if (options.fallbackReferenceDir) {
|
|
271
|
+
candidates.push(path.resolve(options.fallbackReferenceDir, pathPart));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
candidates.push(path.resolve(projectRoot, pathPart));
|
|
276
|
+
}
|
|
277
|
+
let resolvedPath = null;
|
|
278
|
+
for (const candidate of candidates) {
|
|
279
|
+
try {
|
|
280
|
+
await fs.access(candidate);
|
|
281
|
+
resolvedPath = candidate;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Try next candidate.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!resolvedPath)
|
|
289
|
+
return null;
|
|
290
|
+
const visitKey = `${resolvedPath}#${fragment ?? ''}`;
|
|
291
|
+
if (options.visited.has(visitKey)) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
options.visited.add(visitKey);
|
|
295
|
+
try {
|
|
296
|
+
const rawContent = await fs.readFile(resolvedPath, 'utf8');
|
|
297
|
+
const extension = path.extname(resolvedPath).toLowerCase();
|
|
298
|
+
if (extension === '.md' || extension === '.mdc') {
|
|
299
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(rawContent);
|
|
300
|
+
const nested = await inlineReferenceDirectives(parsed.body, projectRoot, resolvedPath, {
|
|
301
|
+
fallbackReferenceDir: path.dirname(resolvedPath),
|
|
302
|
+
visited: options.visited,
|
|
303
|
+
depth: options.depth + 1,
|
|
304
|
+
});
|
|
305
|
+
return extractMarkdownFragment(nested.body, fragment);
|
|
306
|
+
}
|
|
307
|
+
return rawContent.trim();
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
options.visited.delete(visitKey);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function inlineReferenceDirectives(body, projectRoot, baseFilePath, options = {}) {
|
|
314
|
+
const depth = options.depth ?? 0;
|
|
315
|
+
if (depth >= constants_1.MAX_RECURSION_DEPTH) {
|
|
316
|
+
return { body, changed: false };
|
|
317
|
+
}
|
|
318
|
+
const visited = options.visited ?? new Set();
|
|
319
|
+
const lines = body.split('\n');
|
|
320
|
+
const output = [];
|
|
321
|
+
let changed = false;
|
|
322
|
+
for (const line of lines) {
|
|
323
|
+
const directive = parseReferenceDirectiveLine(line);
|
|
324
|
+
if (!directive) {
|
|
325
|
+
output.push(line);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const referencedContent = await readReferenceDirectiveContent(projectRoot, baseFilePath, directive, {
|
|
329
|
+
fallbackReferenceDir: options.fallbackReferenceDir,
|
|
330
|
+
visited,
|
|
331
|
+
depth,
|
|
332
|
+
});
|
|
333
|
+
if (referencedContent === null) {
|
|
334
|
+
output.push(line);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
changed = true;
|
|
338
|
+
output.push(referencedContent.trimEnd());
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
body: output.join('\n'),
|
|
342
|
+
changed,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function toProjectRelative(projectRoot, targetPath) {
|
|
346
|
+
return path.relative(projectRoot, targetPath).replace(/\\/g, '/');
|
|
347
|
+
}
|
|
348
|
+
function cloneRecord(value) {
|
|
349
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
350
|
+
? { ...value }
|
|
351
|
+
: {};
|
|
352
|
+
}
|
|
353
|
+
function buildCanonicalSkillFrontmatter(skillName, rawFrontmatter, options = {}) {
|
|
354
|
+
const next = rawFrontmatter ? { ...rawFrontmatter } : {};
|
|
355
|
+
delete next.globs;
|
|
356
|
+
delete next.alwaysApply;
|
|
357
|
+
next.name = skillName;
|
|
358
|
+
if (typeof next.description !== 'string' ||
|
|
359
|
+
next.description.trim().length === 0) {
|
|
360
|
+
next.description = `Skill: ${skillName}`;
|
|
361
|
+
}
|
|
362
|
+
const metadata = cloneRecord(next.metadata);
|
|
363
|
+
const skillerMeta = cloneRecord(metadata.skiller);
|
|
364
|
+
if (options.sourceRelPath) {
|
|
365
|
+
skillerMeta.source = options.sourceRelPath;
|
|
366
|
+
}
|
|
367
|
+
if (options.alwaysApply === true) {
|
|
368
|
+
skillerMeta.alwaysApply = true;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
delete skillerMeta.alwaysApply;
|
|
372
|
+
}
|
|
373
|
+
if (Object.keys(skillerMeta).length > 0) {
|
|
374
|
+
metadata.skiller = skillerMeta;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
delete metadata.skiller;
|
|
378
|
+
}
|
|
379
|
+
if (Object.keys(metadata).length > 0) {
|
|
380
|
+
next.metadata = metadata;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
delete next.metadata;
|
|
384
|
+
}
|
|
385
|
+
return next;
|
|
386
|
+
}
|
|
387
|
+
function buildCanonicalSkillContent(skillName, rawFrontmatter, body, options = {}) {
|
|
388
|
+
const frontmatter = buildCanonicalSkillFrontmatter(skillName, rawFrontmatter, options);
|
|
389
|
+
return `---
|
|
390
|
+
${yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).trim()}
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
${body.trim()}
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
396
|
+
function buildRuleSourceContent(rawFrontmatter, body) {
|
|
397
|
+
const next = rawFrontmatter ? { ...rawFrontmatter } : {};
|
|
398
|
+
delete next.name;
|
|
399
|
+
delete next.globs;
|
|
400
|
+
const metadata = cloneRecord(next.metadata);
|
|
401
|
+
const skillerMeta = cloneRecord(metadata.skiller);
|
|
402
|
+
const alwaysApply = skillerMeta.alwaysApply === true;
|
|
403
|
+
delete skillerMeta.source;
|
|
404
|
+
delete skillerMeta.alwaysApply;
|
|
405
|
+
if (Object.keys(skillerMeta).length > 0) {
|
|
406
|
+
metadata.skiller = skillerMeta;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
delete metadata.skiller;
|
|
410
|
+
}
|
|
411
|
+
if (Object.keys(metadata).length > 0) {
|
|
412
|
+
next.metadata = metadata;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
delete next.metadata;
|
|
416
|
+
}
|
|
417
|
+
if (alwaysApply) {
|
|
418
|
+
next.alwaysApply = true;
|
|
419
|
+
}
|
|
420
|
+
if (Object.keys(next).length === 0) {
|
|
421
|
+
return `${body.trim()}\n`;
|
|
422
|
+
}
|
|
423
|
+
return `---
|
|
424
|
+
${yaml.dump(next, { lineWidth: -1, noRefs: true }).trim()}
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
${body.trim()}
|
|
428
|
+
`;
|
|
429
|
+
}
|
|
430
|
+
function flattenNestedFrontmatter(rawFrontmatter, body) {
|
|
431
|
+
const nested = (0, FrontmatterParser_1.parseFrontmatter)(body);
|
|
432
|
+
if (!nested.rawFrontmatter) {
|
|
433
|
+
return { rawFrontmatter, body };
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
rawFrontmatter: {
|
|
437
|
+
...(rawFrontmatter ?? {}),
|
|
438
|
+
...nested.rawFrontmatter,
|
|
439
|
+
},
|
|
440
|
+
body: nested.body,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function normalizeRuleSourceContent(content) {
|
|
444
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(content);
|
|
445
|
+
const flattened = flattenNestedFrontmatter(parsed.rawFrontmatter, parsed.body);
|
|
446
|
+
return buildRuleSourceContent(flattened.rawFrontmatter, flattened.body);
|
|
447
|
+
}
|
|
448
|
+
function resolveSkillReferencePath(projectRoot, skillFolderPath, referencePath) {
|
|
449
|
+
return referencePath.startsWith('./') || referencePath.startsWith('../')
|
|
450
|
+
? path.resolve(skillFolderPath, referencePath)
|
|
451
|
+
: path.resolve(projectRoot, referencePath);
|
|
452
|
+
}
|
|
453
|
+
async function readLegacyLocalRuleSource(projectRoot, skillFolderPath, skillMdContent) {
|
|
454
|
+
const { rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(skillMdContent);
|
|
455
|
+
const refCheck = isReferenceBody(body);
|
|
456
|
+
if (refCheck.isReference && refCheck.referencePath) {
|
|
457
|
+
const referencedPath = resolveSkillReferencePath(projectRoot, skillFolderPath, refCheck.referencePath);
|
|
458
|
+
return fs.readFile(referencedPath, 'utf8');
|
|
459
|
+
}
|
|
460
|
+
const flattened = flattenNestedFrontmatter(rawFrontmatter, body);
|
|
461
|
+
return buildRuleSourceContent(flattened.rawFrontmatter, flattened.body);
|
|
462
|
+
}
|
|
463
|
+
async function writeFileIfChanged(targetPath, content, dryRun) {
|
|
464
|
+
try {
|
|
465
|
+
const existing = await fs.readFile(targetPath, 'utf8');
|
|
466
|
+
if (existing === content) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// Write below.
|
|
472
|
+
}
|
|
473
|
+
if (!dryRun) {
|
|
474
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
475
|
+
await fs.writeFile(targetPath, content, 'utf8');
|
|
476
|
+
}
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
async function extractLocalRulesFromCanonicalSkills(projectRoot, verbose, dryRun) {
|
|
171
480
|
const warnings = [];
|
|
481
|
+
const extracted = [];
|
|
482
|
+
const skillsDir = path.join(projectRoot, '.agents', 'skills');
|
|
483
|
+
const rulesDir = path.join(projectRoot, '.agents', 'rules');
|
|
484
|
+
const ownership = await (0, SkillOwnership_1.resolveSkillOwnership)(projectRoot);
|
|
172
485
|
try {
|
|
173
486
|
await fs.access(skillsDir);
|
|
174
487
|
}
|
|
175
488
|
catch {
|
|
176
|
-
|
|
177
|
-
return { synced, warnings };
|
|
489
|
+
return { extracted, warnings };
|
|
178
490
|
}
|
|
179
491
|
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const skillName =
|
|
185
|
-
|
|
492
|
+
const adoptedNames = [];
|
|
493
|
+
for (const entry of entries) {
|
|
494
|
+
if (!entry.isDirectory())
|
|
495
|
+
continue;
|
|
496
|
+
const skillName = entry.name;
|
|
497
|
+
if (ownership.upstreamOwned.has(skillName)) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const rulePath = path.join(rulesDir, `${skillName}.mdc`);
|
|
501
|
+
try {
|
|
502
|
+
await fs.access(rulePath);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// No explicit local rule yet; safe to extract below.
|
|
507
|
+
}
|
|
186
508
|
const skillFolderPath = path.join(skillsDir, skillName);
|
|
187
|
-
const
|
|
509
|
+
const skillMdPath = path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME);
|
|
510
|
+
let skillMdContent;
|
|
188
511
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
(0, constants_1.logVerboseInfo)(`Migrated ${mdcFile.name} to ${skillName}/${mdcFile.name}`, verbose, dryRun);
|
|
512
|
+
skillMdContent = await fs.readFile(skillMdPath, 'utf8');
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const ruleContent = await readLegacyLocalRuleSource(projectRoot, skillFolderPath, skillMdContent);
|
|
519
|
+
const changed = await writeFileIfChanged(rulePath, ruleContent, dryRun);
|
|
520
|
+
if (changed) {
|
|
521
|
+
(0, constants_1.logVerboseInfo)(dryRun
|
|
522
|
+
? `DRY RUN: Would extract local skill source ${skillName} to ${toProjectRelative(projectRoot, rulePath)}`
|
|
523
|
+
: `Extracted local skill source ${skillName} to ${toProjectRelative(projectRoot, rulePath)}`, verbose, dryRun);
|
|
202
524
|
}
|
|
525
|
+
extracted.push(skillName);
|
|
526
|
+
adoptedNames.push(skillName);
|
|
203
527
|
}
|
|
204
528
|
catch (err) {
|
|
205
|
-
warnings.push(`Failed to
|
|
529
|
+
warnings.push(`Failed to extract local rule source for ${skillName}: ${err.message}`);
|
|
206
530
|
}
|
|
207
531
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
532
|
+
if (adoptedNames.length > 0) {
|
|
533
|
+
await (0, SkillOwnership_1.adoptSkillerOwnedSkillNames)(projectRoot, adoptedNames, dryRun);
|
|
534
|
+
}
|
|
535
|
+
return { extracted, warnings };
|
|
536
|
+
}
|
|
537
|
+
async function compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun) {
|
|
538
|
+
const warnings = [];
|
|
539
|
+
const compiled = [];
|
|
540
|
+
const rulesDir = path.join(skillerDir, 'rules');
|
|
541
|
+
const skillsDir = path.join(skillerDir, 'skills');
|
|
542
|
+
const ownership = await (0, SkillOwnership_1.resolveSkillOwnership)(projectRoot);
|
|
543
|
+
try {
|
|
544
|
+
await fs.access(rulesDir);
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
return { compiled, warnings };
|
|
548
|
+
}
|
|
549
|
+
const entries = await fs.readdir(rulesDir, { withFileTypes: true });
|
|
550
|
+
const ruleFiles = entries.filter((entry) => {
|
|
551
|
+
return entry.isFile() && entry.name.endsWith('.mdc');
|
|
552
|
+
});
|
|
553
|
+
const adoptedNames = [];
|
|
554
|
+
for (const ruleFile of ruleFiles) {
|
|
555
|
+
const skillName = path.basename(ruleFile.name, '.mdc');
|
|
556
|
+
if (ownership.upstreamOwned.has(skillName)) {
|
|
557
|
+
throw new Error(`Local rule '${skillName}' conflicts with upstream-managed skill '${skillName}' in skills-lock.json`);
|
|
558
|
+
}
|
|
559
|
+
const sourcePath = path.join(rulesDir, ruleFile.name);
|
|
560
|
+
const sourceContent = await fs.readFile(sourcePath, 'utf8');
|
|
561
|
+
const normalizedSourceContent = normalizeRuleSourceContent(sourceContent);
|
|
562
|
+
if (normalizedSourceContent !== sourceContent) {
|
|
563
|
+
await writeFileIfChanged(sourcePath, normalizedSourceContent, dryRun);
|
|
564
|
+
}
|
|
565
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(normalizedSourceContent);
|
|
214
566
|
const skillFolderPath = path.join(skillsDir, skillName);
|
|
567
|
+
const compiledBodyResult = await inlineReferenceDirectives(parsed.body, projectRoot, sourcePath, {
|
|
568
|
+
fallbackReferenceDir: skillFolderPath,
|
|
569
|
+
});
|
|
570
|
+
const compiledContent = buildCanonicalSkillContent(skillName, parsed.rawFrontmatter, compiledBodyResult.body, {
|
|
571
|
+
sourceRelPath: toProjectRelative(projectRoot, sourcePath),
|
|
572
|
+
alwaysApply: parsed.frontmatter?.alwaysApply === true,
|
|
573
|
+
});
|
|
215
574
|
const skillMdPath = path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME);
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
575
|
+
const changed = await writeFileIfChanged(skillMdPath, compiledContent, dryRun);
|
|
576
|
+
if (!dryRun) {
|
|
577
|
+
await fs.mkdir(skillFolderPath, { recursive: true });
|
|
578
|
+
const skillEntries = await fs.readdir(skillFolderPath, {
|
|
579
|
+
withFileTypes: true,
|
|
580
|
+
});
|
|
581
|
+
for (const entry of skillEntries) {
|
|
582
|
+
if (entry.isFile() && entry.name.endsWith('.mdc')) {
|
|
583
|
+
await fs.rm(path.join(skillFolderPath, entry.name), {
|
|
584
|
+
force: true,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
221
588
|
}
|
|
222
|
-
|
|
223
|
-
|
|
589
|
+
if (changed) {
|
|
590
|
+
(0, constants_1.logVerboseInfo)(dryRun
|
|
591
|
+
? `DRY RUN: Would compile ${toProjectRelative(projectRoot, sourcePath)} to ${toProjectRelative(projectRoot, skillMdPath)}`
|
|
592
|
+
: `Compiled ${toProjectRelative(projectRoot, sourcePath)} to ${toProjectRelative(projectRoot, skillMdPath)}`, verbose, dryRun);
|
|
224
593
|
}
|
|
225
|
-
|
|
594
|
+
compiled.push(skillName);
|
|
595
|
+
adoptedNames.push(skillName);
|
|
596
|
+
}
|
|
597
|
+
if (adoptedNames.length > 0) {
|
|
598
|
+
await (0, SkillOwnership_1.adoptSkillerOwnedSkillNames)(projectRoot, adoptedNames, dryRun);
|
|
599
|
+
}
|
|
600
|
+
return { compiled, warnings };
|
|
601
|
+
}
|
|
602
|
+
async function normalizeCanonicalSkills(projectRoot, skillsDir, verbose, dryRun) {
|
|
603
|
+
const normalized = [];
|
|
604
|
+
const warnings = [];
|
|
605
|
+
try {
|
|
606
|
+
await fs.access(skillsDir);
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
return { normalized, warnings };
|
|
610
|
+
}
|
|
611
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
612
|
+
for (const entry of entries) {
|
|
613
|
+
if (!entry.isDirectory())
|
|
614
|
+
continue;
|
|
615
|
+
const skillName = entry.name;
|
|
616
|
+
const skillFolderPath = path.join(skillsDir, skillName);
|
|
617
|
+
const skillMdPath = path.join(skillFolderPath, constants_1.SKILL_MD_FILENAME);
|
|
226
618
|
let skillMdContent = null;
|
|
227
619
|
try {
|
|
228
620
|
skillMdContent = await fs.readFile(skillMdPath, 'utf8');
|
|
229
621
|
}
|
|
230
622
|
catch {
|
|
231
|
-
|
|
623
|
+
skillMdContent = null;
|
|
232
624
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
// Generate SKILL.md with @reference (absolute path)
|
|
244
|
-
// Keep all frontmatter from .mdc except globs and alwaysApply
|
|
245
|
-
const skillFrontmatter = {
|
|
246
|
-
name: skillName,
|
|
247
|
-
...Object.fromEntries(Object.entries(mdcFrontmatter || {}).filter(([key]) => key !== 'globs' && key !== 'alwaysApply')),
|
|
248
|
-
};
|
|
249
|
-
// Ensure description has a default
|
|
250
|
-
if (!skillFrontmatter.description) {
|
|
251
|
-
skillFrontmatter.description = `Skill: ${skillName}`;
|
|
252
|
-
}
|
|
253
|
-
const newSkillMd = `---
|
|
254
|
-
${yaml.dump(skillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
@.claude/skills/${skillName}/${skillName}.mdc
|
|
258
|
-
`;
|
|
259
|
-
if (dryRun) {
|
|
260
|
-
(0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/SKILL.md with @reference`, verbose, dryRun);
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
|
|
264
|
-
(0, constants_1.logVerboseInfo)(`Generated ${skillName}/SKILL.md with @.claude/skills/${skillName}/${skillName}.mdc reference`, verbose, dryRun);
|
|
625
|
+
let changed = false;
|
|
626
|
+
if (skillMdContent) {
|
|
627
|
+
const compiled = await compileSkillMdForNonClaudeAgents(skillMdContent, projectRoot, skillFolderPath, {
|
|
628
|
+
baseFilePath: skillMdPath,
|
|
629
|
+
fallbackReferenceDir: skillFolderPath,
|
|
630
|
+
});
|
|
631
|
+
if (compiled !== skillMdContent) {
|
|
632
|
+
if (!dryRun) {
|
|
633
|
+
await fs.writeFile(skillMdPath, compiled, 'utf8');
|
|
265
634
|
}
|
|
266
|
-
|
|
635
|
+
changed = true;
|
|
267
636
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
---
|
|
301
|
-
|
|
302
|
-
@.claude/skills/${skillName}/${skillName}.mdc
|
|
303
|
-
`;
|
|
304
|
-
if (dryRun) {
|
|
305
|
-
(0, constants_1.logVerboseInfo)(`DRY RUN: Would migrate ${skillName}/SKILL.md to absolute path`, verbose, dryRun);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
|
|
309
|
-
(0, constants_1.logVerboseInfo)(`Migrated ${skillName}/SKILL.md to absolute path`, verbose, dryRun);
|
|
310
|
-
}
|
|
311
|
-
synced.push(skillName);
|
|
312
|
-
}
|
|
313
|
-
// If already absolute path, nothing to do - SKILL.md is source of truth for frontmatter
|
|
314
|
-
}
|
|
315
|
-
else if (refCheck.referencePath) {
|
|
316
|
-
// Pre-0.7 pattern or other external reference - migrate to sibling pattern
|
|
317
|
-
// Determine base path for resolution:
|
|
318
|
-
// - Paths starting with .claude/ are relative to project root
|
|
319
|
-
// - Other paths are relative to the skill folder
|
|
320
|
-
let referencedPath;
|
|
321
|
-
if (refCheck.referencePath.startsWith('.claude/')) {
|
|
322
|
-
// Project root is parent of .claude directory (skillsDir is .claude/skills)
|
|
323
|
-
const projectRoot = path.dirname(path.dirname(skillsDir));
|
|
324
|
-
referencedPath = path.join(projectRoot, refCheck.referencePath);
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
referencedPath = path.resolve(skillFolderPath, refCheck.referencePath);
|
|
328
|
-
}
|
|
329
|
-
// One-time migration: for old @.claude/rules/ references, prefer the
|
|
330
|
-
// original rules path if it exists, then fall back to migrated skills.
|
|
331
|
-
const candidatePaths = [referencedPath];
|
|
332
|
-
if (refCheck.referencePath?.includes('/rules/')) {
|
|
333
|
-
const refFileName = path.basename(refCheck.referencePath);
|
|
334
|
-
const refBaseName = path.basename(refFileName, '.mdc');
|
|
335
|
-
candidatePaths.push(path.join(skillsDir, refBaseName, refFileName));
|
|
336
|
-
}
|
|
337
|
-
let referencedContent = null;
|
|
338
|
-
let actualPath = referencedPath;
|
|
339
|
-
for (const candidatePath of candidatePaths) {
|
|
340
|
-
try {
|
|
341
|
-
referencedContent = await fs.readFile(candidatePath, 'utf8');
|
|
342
|
-
actualPath = candidatePath;
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
// Try next candidate
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (referencedContent === null) {
|
|
350
|
-
warnings.push(`Cannot migrate ${skillName}: referenced file not found at ${actualPath}`);
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
// Parse the referenced file for frontmatter
|
|
354
|
-
const { frontmatter: refFrontmatter, body: refBody } = (0, FrontmatterParser_1.parseFrontmatter)(referencedContent);
|
|
355
|
-
// Create sibling .mdc - only keep frontmatter for alwaysApply rules
|
|
356
|
-
let mdcContent;
|
|
357
|
-
if (refFrontmatter?.alwaysApply === true) {
|
|
358
|
-
// alwaysApply rules keep frontmatter (description since no SKILL.md)
|
|
359
|
-
const mdcFrontmatterData = {
|
|
360
|
-
alwaysApply: true,
|
|
361
|
-
};
|
|
362
|
-
if (refFrontmatter.description) {
|
|
363
|
-
mdcFrontmatterData.description = refFrontmatter.description;
|
|
364
|
-
}
|
|
365
|
-
mdcContent = `---
|
|
366
|
-
${yaml.dump(mdcFrontmatterData, { lineWidth: -1, noRefs: true }).trim()}
|
|
367
|
-
---
|
|
368
|
-
|
|
369
|
-
${refBody}
|
|
370
|
-
`;
|
|
371
|
-
}
|
|
372
|
-
else {
|
|
373
|
-
// Regular skills: body only (description goes in SKILL.md)
|
|
374
|
-
mdcContent = refBody;
|
|
375
|
-
}
|
|
376
|
-
// Update SKILL.md to point to sibling .mdc (absolute path)
|
|
377
|
-
const newFrontmatter = {
|
|
378
|
-
name: skillFrontmatter?.name || skillName,
|
|
379
|
-
description: refFrontmatter?.description ||
|
|
380
|
-
skillFrontmatter?.description ||
|
|
381
|
-
`Skill: ${skillName}`,
|
|
382
|
-
};
|
|
383
|
-
const newSkillMd = `---
|
|
384
|
-
${yaml.dump(newFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
|
|
385
|
-
---
|
|
386
|
-
|
|
387
|
-
@.claude/skills/${skillName}/${skillName}.mdc
|
|
388
|
-
`;
|
|
389
|
-
if (dryRun) {
|
|
390
|
-
(0, constants_1.logVerboseInfo)(`DRY RUN: Would migrate ${skillName} from ${refCheck.referencePath} to sibling pattern`, verbose, dryRun);
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
await fs.writeFile(siblingMdcPath, mdcContent, 'utf8');
|
|
394
|
-
await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
|
|
395
|
-
(0, constants_1.logVerboseInfo)(`Migrated ${skillName} from ${actualPath} to sibling pattern`, verbose, dryRun);
|
|
396
|
-
}
|
|
397
|
-
synced.push(skillName);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
// Case 3: SKILL.md has full content → generate sibling .mdc, update to @reference
|
|
403
|
-
// Generate .mdc from SKILL.md body (no frontmatter needed - description is in SKILL.md)
|
|
404
|
-
const mdcContent = skillBody;
|
|
405
|
-
// Update SKILL.md to @reference (absolute path)
|
|
406
|
-
// Preserve ALL existing frontmatter (use rawFrontmatter to keep custom fields like user-invocable)
|
|
407
|
-
// Only add defaults for missing name/description
|
|
408
|
-
const newSkillFrontmatter = skillRawFrontmatter ? { ...skillRawFrontmatter } : {};
|
|
409
|
-
if (!newSkillFrontmatter.name) {
|
|
410
|
-
newSkillFrontmatter.name = skillName;
|
|
411
|
-
}
|
|
412
|
-
if (!newSkillFrontmatter.description) {
|
|
413
|
-
newSkillFrontmatter.description = `Skill: ${skillName}`;
|
|
414
|
-
}
|
|
415
|
-
const newSkillMd = `---
|
|
416
|
-
${yaml.dump(newSkillFrontmatter, { lineWidth: -1, noRefs: true }).trim()}
|
|
417
|
-
---
|
|
418
|
-
|
|
419
|
-
@.claude/skills/${skillName}/${skillName}.mdc
|
|
420
|
-
`;
|
|
421
|
-
if (dryRun) {
|
|
422
|
-
(0, constants_1.logVerboseInfo)(`DRY RUN: Would generate ${skillName}/${skillName}.mdc and update SKILL.md`, verbose, dryRun);
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
await fs.writeFile(siblingMdcPath, mdcContent, 'utf8');
|
|
426
|
-
await fs.writeFile(skillMdPath, newSkillMd, 'utf8');
|
|
427
|
-
(0, constants_1.logVerboseInfo)(`Generated ${skillName}/${skillName}.mdc and updated SKILL.md to @reference`, verbose, dryRun);
|
|
428
|
-
}
|
|
429
|
-
synced.push(skillName);
|
|
637
|
+
}
|
|
638
|
+
const folderEntries = await fs.readdir(skillFolderPath, {
|
|
639
|
+
withFileTypes: true,
|
|
640
|
+
});
|
|
641
|
+
const mdcEntries = folderEntries.filter((folderEntry) => folderEntry.isFile() && folderEntry.name.endsWith('.mdc'));
|
|
642
|
+
const legacySingleMdcSourcePath = skillMdContent === null && mdcEntries.length === 1
|
|
643
|
+
? path.join(skillFolderPath, mdcEntries[0].name)
|
|
644
|
+
: null;
|
|
645
|
+
if (legacySingleMdcSourcePath) {
|
|
646
|
+
const sourceContent = await fs.readFile(legacySingleMdcSourcePath, 'utf8');
|
|
647
|
+
const parsed = (0, FrontmatterParser_1.parseFrontmatter)(sourceContent);
|
|
648
|
+
const compiledBodyResult = await inlineReferenceDirectives(parsed.body, projectRoot, legacySingleMdcSourcePath, {
|
|
649
|
+
fallbackReferenceDir: skillFolderPath,
|
|
650
|
+
});
|
|
651
|
+
const compiledContent = buildCanonicalSkillContent(skillName, parsed.rawFrontmatter, compiledBodyResult.body, {
|
|
652
|
+
sourceRelPath: toProjectRelative(projectRoot, legacySingleMdcSourcePath),
|
|
653
|
+
alwaysApply: parsed.frontmatter?.alwaysApply === true,
|
|
654
|
+
});
|
|
655
|
+
if (!dryRun) {
|
|
656
|
+
await fs.writeFile(skillMdPath, compiledContent, 'utf8');
|
|
657
|
+
}
|
|
658
|
+
changed = true;
|
|
659
|
+
}
|
|
660
|
+
else if (skillMdContent === null && mdcEntries.length > 1) {
|
|
661
|
+
warnings.push(`Canonical skill '${skillName}' has multiple legacy .mdc files and no SKILL.md`);
|
|
662
|
+
}
|
|
663
|
+
if (mdcEntries.length > 0) {
|
|
664
|
+
if (!dryRun) {
|
|
665
|
+
for (const mdcEntry of mdcEntries) {
|
|
666
|
+
await fs.rm(path.join(skillFolderPath, mdcEntry.name), {
|
|
667
|
+
force: true,
|
|
668
|
+
});
|
|
430
669
|
}
|
|
431
670
|
}
|
|
432
|
-
|
|
671
|
+
changed = true;
|
|
433
672
|
}
|
|
434
|
-
|
|
435
|
-
|
|
673
|
+
if (changed) {
|
|
674
|
+
normalized.push(skillName);
|
|
675
|
+
(0, constants_1.logVerboseInfo)(dryRun
|
|
676
|
+
? `DRY RUN: Would normalize canonical skill '${skillName}'`
|
|
677
|
+
: `Normalized canonical skill '${skillName}'`, verbose, dryRun);
|
|
436
678
|
}
|
|
437
679
|
}
|
|
438
|
-
return {
|
|
680
|
+
return { normalized, warnings };
|
|
681
|
+
}
|
|
682
|
+
// Deprecated compatibility shim. Canonical skills are now plain SKILL.md only.
|
|
683
|
+
async function syncMdcToSkillMd(skillsDir, verbose, dryRun) {
|
|
684
|
+
const projectRoot = path.resolve(skillsDir, '..', '..');
|
|
685
|
+
const { normalized, warnings } = await normalizeCanonicalSkills(projectRoot, skillsDir, verbose, dryRun);
|
|
686
|
+
return { synced: normalized, warnings };
|
|
439
687
|
}
|
|
440
688
|
/**
|
|
441
|
-
* Discovers skills in the project's skills directory
|
|
689
|
+
* Discovers skills in the project's canonical skills directory.
|
|
442
690
|
* Returns discovered skills, validation warnings, and deleted empty folders.
|
|
443
691
|
*/
|
|
444
692
|
async function discoverSkills(projectRoot, skillerDir) {
|
|
445
|
-
|
|
446
|
-
const skillsPath = skillerDir
|
|
447
|
-
? path.join(skillerDir, 'skills')
|
|
448
|
-
: path.join(projectRoot, constants_1.CLAUDE_SKILLS_PATH);
|
|
693
|
+
const skillsPath = await resolveProjectSkillsDir(projectRoot, skillerDir);
|
|
449
694
|
// Check if skills directory exists
|
|
450
695
|
try {
|
|
451
696
|
await fs.access(skillsPath);
|
|
@@ -530,10 +775,15 @@ async function copySkillsToAgent(sourceSkillsDir, targetSkillsDir, projectRoot,
|
|
|
530
775
|
}
|
|
531
776
|
taken.add(destName);
|
|
532
777
|
const targetSkillPath = path.join(targetSkillsDir, destName);
|
|
778
|
+
const sourceLeafName = path.basename(skillPath);
|
|
533
779
|
if (!dryRun) {
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
780
|
+
const symlinkSafe = destName === sourceLeafName && (await skillCanBeSymlinked(skillPath));
|
|
781
|
+
const symlinkCreated = symlinkSafe &&
|
|
782
|
+
(await createRelativeDirectorySymlink(skillPath, targetSkillPath));
|
|
783
|
+
if (!symlinkCreated) {
|
|
784
|
+
await copySkillDirectoryForNonClaudeAgents(skillPath, targetSkillPath, projectRoot, skillPath);
|
|
785
|
+
}
|
|
786
|
+
if (!symlinkCreated && destName !== sourceLeafName) {
|
|
537
787
|
await rewriteSkillMdName(path.join(targetSkillPath, constants_1.SKILL_MD_FILENAME), destName);
|
|
538
788
|
}
|
|
539
789
|
}
|
|
@@ -546,11 +796,11 @@ async function copySkillsToAgent(sourceSkillsDir, targetSkillsDir, projectRoot,
|
|
|
546
796
|
}
|
|
547
797
|
/**
|
|
548
798
|
* Gets the paths that skills will generate, for gitignore purposes.
|
|
549
|
-
* Collects paths from all agents with native skills support, excluding the source
|
|
799
|
+
* Collects paths from all agents with native skills support, excluding the canonical source.
|
|
550
800
|
*/
|
|
551
801
|
function getSkillsGitignorePaths(projectRoot, agents) {
|
|
552
802
|
const paths = [];
|
|
553
|
-
const sourceSkillsPath = path.join(projectRoot, constants_1.
|
|
803
|
+
const sourceSkillsPath = path.join(projectRoot, constants_1.CANONICAL_SKILLS_PATH);
|
|
554
804
|
for (const agent of agents) {
|
|
555
805
|
if (agent.supportsNativeSkills?.() && agent.getSkillsPath) {
|
|
556
806
|
const skillsPath = agent.getSkillsPath(projectRoot);
|
|
@@ -568,15 +818,16 @@ function getSkillsGitignorePaths(projectRoot, agents) {
|
|
|
568
818
|
}
|
|
569
819
|
/**
|
|
570
820
|
* Propagates skills for agents that need them.
|
|
571
|
-
*
|
|
572
|
-
* This function now only discovers and validates skills.
|
|
821
|
+
* Canonical skills live in .agents/skills, and local .mdc authoring lives in .agents/rules.
|
|
573
822
|
*/
|
|
574
823
|
async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryRun, skillerDir) {
|
|
575
|
-
async function migrateLegacyCodexSkillsDir(destinationPaths) {
|
|
824
|
+
async function migrateLegacyCodexSkillsDir(currentSourceSkillsDir, destinationPaths) {
|
|
576
825
|
const universalSkillsDir = path.join(projectRoot, UNIVERSAL_AGENTS_SKILLS_PATH);
|
|
577
826
|
const legacyCodexSkillsDir = path.join(projectRoot, LEGACY_CODEX_SKILLS_PATH);
|
|
578
|
-
if (
|
|
827
|
+
if (currentSourceSkillsDir !== universalSkillsDir &&
|
|
828
|
+
!destinationPaths.has(universalSkillsDir)) {
|
|
579
829
|
return;
|
|
830
|
+
}
|
|
580
831
|
try {
|
|
581
832
|
await fs.access(legacyCodexSkillsDir);
|
|
582
833
|
}
|
|
@@ -615,11 +866,19 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
615
866
|
(0, constants_1.logVerboseInfo)('Skills support disabled', verbose, dryRun);
|
|
616
867
|
return;
|
|
617
868
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
869
|
+
if (skillerDir) {
|
|
870
|
+
const extractedResult = await extractLocalRulesFromCanonicalSkills(projectRoot, verbose, dryRun);
|
|
871
|
+
for (const warning of extractedResult.warnings) {
|
|
872
|
+
(0, constants_1.logWarn)(warning, dryRun);
|
|
873
|
+
}
|
|
874
|
+
const compileResult = await compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun);
|
|
875
|
+
for (const warning of compileResult.warnings) {
|
|
876
|
+
(0, constants_1.logWarn)(warning, dryRun);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Determine canonical skills directory, with legacy fallback for migration.
|
|
880
|
+
const skillsDir = await resolveProjectSkillsDir(projectRoot, skillerDir);
|
|
881
|
+
// Compute destinations up-front so legacy codex migration can de-duplicate targets.
|
|
623
882
|
const destinationPaths = new Set();
|
|
624
883
|
for (const agent of agents) {
|
|
625
884
|
if (agent.supportsNativeSkills?.() && agent.getSkillsPath) {
|
|
@@ -630,7 +889,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
630
889
|
}
|
|
631
890
|
}
|
|
632
891
|
}
|
|
633
|
-
await migrateLegacyCodexSkillsDir(destinationPaths);
|
|
892
|
+
await migrateLegacyCodexSkillsDir(skillsDir, destinationPaths);
|
|
634
893
|
// Check if skills directory exists
|
|
635
894
|
let skillsDirExists = true;
|
|
636
895
|
try {
|
|
@@ -638,15 +897,15 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
638
897
|
}
|
|
639
898
|
catch {
|
|
640
899
|
skillsDirExists = false;
|
|
641
|
-
(0, constants_1.logVerboseInfo)(`No
|
|
900
|
+
(0, constants_1.logVerboseInfo)(`No skills directory found`, verbose, dryRun);
|
|
901
|
+
}
|
|
902
|
+
const ownership = await (0, SkillOwnership_1.resolveSkillOwnership)(projectRoot);
|
|
903
|
+
for (const warning of ownership.warnings) {
|
|
904
|
+
(0, constants_1.logWarn)(warning, dryRun);
|
|
642
905
|
}
|
|
643
906
|
if (skillsDirExists) {
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
if (syncResult.synced.length > 0) {
|
|
647
|
-
(0, constants_1.logVerboseInfo)(`Synced ${syncResult.synced.length} .mdc file(s) to SKILL.md`, verbose, dryRun);
|
|
648
|
-
}
|
|
649
|
-
for (const warning of syncResult.warnings) {
|
|
907
|
+
const normalizeResult = await normalizeCanonicalSkills(projectRoot, skillsDir, verbose, dryRun);
|
|
908
|
+
for (const warning of normalizeResult.warnings) {
|
|
650
909
|
(0, constants_1.logWarn)(warning, dryRun);
|
|
651
910
|
}
|
|
652
911
|
// Discover and validate skills
|
|
@@ -660,7 +919,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
660
919
|
}
|
|
661
920
|
}
|
|
662
921
|
if (skills.length === 0) {
|
|
663
|
-
(0, constants_1.logVerboseInfo)('No valid skills found in
|
|
922
|
+
(0, constants_1.logVerboseInfo)('No valid skills found in project skills directory', verbose, dryRun);
|
|
664
923
|
}
|
|
665
924
|
else {
|
|
666
925
|
(0, constants_1.logVerboseInfo)(`Discovered ${skills.length} skill(s)`, verbose, dryRun);
|
|
@@ -677,7 +936,7 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
677
936
|
}
|
|
678
937
|
}
|
|
679
938
|
// Sync project Claude commands + agents as skills into agent skills dirs.
|
|
680
|
-
// This intentionally does NOT write into the
|
|
939
|
+
// This intentionally does NOT write into the canonical .agents/skills source-of-truth.
|
|
681
940
|
if (destinationPaths.size > 0) {
|
|
682
941
|
const { syncClaudeProjectCommandsAndAgentsToSkillsDirs } = await Promise.resolve().then(() => __importStar(require('./ClaudeProjectSync')));
|
|
683
942
|
await syncClaudeProjectCommandsAndAgentsToSkillsDirs({
|
|
@@ -687,17 +946,6 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
687
946
|
dryRun,
|
|
688
947
|
});
|
|
689
948
|
}
|
|
690
|
-
// Sync Claude plugins (skills + commands converted to skills) into agent skills dirs.
|
|
691
|
-
// This intentionally does NOT write into the committed .claude/skills source-of-truth.
|
|
692
|
-
if (destinationPaths.size > 0) {
|
|
693
|
-
const { syncClaudePluginsToSkillsDirs } = await Promise.resolve().then(() => __importStar(require('./ClaudePluginSync')));
|
|
694
|
-
await syncClaudePluginsToSkillsDirs({
|
|
695
|
-
projectRoot,
|
|
696
|
-
targetSkillsDirs: [...destinationPaths],
|
|
697
|
-
verbose,
|
|
698
|
-
dryRun,
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
949
|
}
|
|
702
950
|
/**
|
|
703
951
|
* Recursively finds all folders containing SKILL.md in a directory.
|