skiller 0.9.6 → 0.9.7
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 +6 -4
- package/dist/cli/commands.js +16 -2
- package/dist/cli/handlers.js +59 -3
- package/dist/core/RulesToSkillsMigration.js +1 -5
- package/dist/core/SkillOwnership.js +13 -43
- package/dist/core/SkillsManifest.js +28 -29
- package/dist/core/SkillsProcessor.js +195 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,14 +4,16 @@ Apply the same rules (and skills) to multiple AI coding agents.
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx skiller@latest init
|
|
7
|
-
npx skiller@latest
|
|
7
|
+
npx skiller@latest install
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## Skills
|
|
11
11
|
|
|
12
|
-
- `.
|
|
13
|
-
-
|
|
14
|
-
-
|
|
12
|
+
- `.agents/rules/*.mdc` is local rule authoring
|
|
13
|
+
- `.agents/skills/` is the canonical runtime skill tree
|
|
14
|
+
- `skills-lock.json` is the upstream source of truth for installed skills
|
|
15
|
+
- `skiller install` and `skiller update` use the local `skills` CLI, then auto-run `apply`
|
|
16
|
+
- `skiller apply` stays local and non-destructive
|
|
15
17
|
- See [docs/skills.md](docs/skills.md)
|
|
16
18
|
|
|
17
19
|
## MCP
|
package/dist/cli/commands.js
CHANGED
|
@@ -128,12 +128,26 @@ async function run() {
|
|
|
128
128
|
.command('list [args..]', 'Run the local skills CLI list command', skillsArgsBuilder, handlers_1.listHandler)
|
|
129
129
|
.command('find [args..]', 'Run the local skills CLI find command', skillsArgsBuilder, handlers_1.findHandler)
|
|
130
130
|
.command('check [args..]', 'Run the local skills CLI check command', skillsArgsBuilder, handlers_1.checkHandler)
|
|
131
|
-
.command('
|
|
131
|
+
.command('install [args..]', 'Run the local skills CLI install command, then skiller apply', (y) => skillsArgsBuilder(y)
|
|
132
|
+
.option('verbose', {
|
|
133
|
+
type: 'boolean',
|
|
134
|
+
description: 'Enable verbose logging for the follow-up apply step',
|
|
135
|
+
default: false,
|
|
136
|
+
})
|
|
137
|
+
.alias('verbose', 'v'), handlers_1.installHandler)
|
|
138
|
+
.command('update [args..]', 'Run the local skills CLI update command, then skiller apply', (y) => skillsArgsBuilder(y)
|
|
139
|
+
.option('verbose', {
|
|
140
|
+
type: 'boolean',
|
|
141
|
+
description: 'Enable verbose logging for the follow-up apply step',
|
|
142
|
+
default: false,
|
|
143
|
+
})
|
|
144
|
+
.alias('verbose', 'v'), handlers_1.updateHandler)
|
|
145
|
+
.command('outdated [args..]', 'Run the local skills CLI outdated command', skillsArgsBuilder, handlers_1.outdatedHandler)
|
|
132
146
|
.command('skills <subcommand> [args..]', 'Pass through an arbitrary command to the local skills CLI', (y) => skillsArgsBuilder(y).positional('subcommand', {
|
|
133
147
|
type: 'string',
|
|
134
148
|
description: 'The local skills CLI subcommand to run',
|
|
135
149
|
}), handlers_1.skillsHandler)
|
|
136
|
-
.command('init', 'Scaffold a .
|
|
150
|
+
.command('init', 'Scaffold a .agents directory with default files', (y) => {
|
|
137
151
|
return y
|
|
138
152
|
.option('project-root', {
|
|
139
153
|
type: 'string',
|
package/dist/cli/handlers.js
CHANGED
|
@@ -38,11 +38,13 @@ exports.initHandler = initHandler;
|
|
|
38
38
|
exports.migrateClaudePluginsHandler = migrateClaudePluginsHandler;
|
|
39
39
|
exports.migrateRulesToSkillsHandler = migrateRulesToSkillsHandler;
|
|
40
40
|
exports.addHandler = addHandler;
|
|
41
|
+
exports.installHandler = installHandler;
|
|
41
42
|
exports.removeHandler = removeHandler;
|
|
42
43
|
exports.listHandler = listHandler;
|
|
43
44
|
exports.findHandler = findHandler;
|
|
44
45
|
exports.checkHandler = checkHandler;
|
|
45
46
|
exports.updateHandler = updateHandler;
|
|
47
|
+
exports.outdatedHandler = outdatedHandler;
|
|
46
48
|
exports.skillsHandler = skillsHandler;
|
|
47
49
|
exports.revertHandler = revertHandler;
|
|
48
50
|
const lib_1 = require("../lib");
|
|
@@ -68,9 +70,18 @@ async function executeSkillsWrapper(projectRoot, args) {
|
|
|
68
70
|
process.exit(1);
|
|
69
71
|
}
|
|
70
72
|
}
|
|
73
|
+
async function applyAfterSkillsLifecycleStep(projectRoot, verbose) {
|
|
74
|
+
await applyHandler({
|
|
75
|
+
'project-root': projectRoot,
|
|
76
|
+
verbose,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
71
79
|
function buildClaudePluginMigrationArgs(source) {
|
|
72
80
|
return ['add', source, '--agent', 'universal', '--skill', '*', '-y'];
|
|
73
81
|
}
|
|
82
|
+
const LEGACY_EXTERNAL_RULE_REPLACEMENT_SOURCES = new Set([
|
|
83
|
+
'ratacat/claude-skills',
|
|
84
|
+
]);
|
|
74
85
|
function resolveRegistryMatchSource(match) {
|
|
75
86
|
if (match.source)
|
|
76
87
|
return match.source;
|
|
@@ -168,9 +179,8 @@ async function cleanupLegacyClaudePluginState(projectRoot, pluginIds) {
|
|
|
168
179
|
if (!changed)
|
|
169
180
|
continue;
|
|
170
181
|
manifest.targets = nextTargets;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (Object.keys(nextTargets).length === 0 && !hasLocalSkills) {
|
|
182
|
+
delete manifest.localSkills;
|
|
183
|
+
if (Object.keys(nextTargets).length === 0) {
|
|
174
184
|
await fs.rm(manifestPath, { force: true });
|
|
175
185
|
continue;
|
|
176
186
|
}
|
|
@@ -205,6 +215,34 @@ async function cleanupMigratedPluginAuxiliaryRules(projectRoot, sources) {
|
|
|
205
215
|
}
|
|
206
216
|
return removed;
|
|
207
217
|
}
|
|
218
|
+
async function cleanupLegacyExternalRuleMatches(projectRoot) {
|
|
219
|
+
const plan = await (0, RulesToSkillsMigration_1.planRulesToSkillsMigration)(projectRoot);
|
|
220
|
+
const removals = new Map();
|
|
221
|
+
for (const candidate of plan.candidates) {
|
|
222
|
+
if (!candidate.matches.some((match) => LEGACY_EXTERNAL_RULE_REPLACEMENT_SOURCES.has(resolveRegistryMatchSource(match)))) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
removals.set(candidate.ruleName, {
|
|
226
|
+
alreadyInstalled: candidate.alreadyInstalled,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const removed = [...removals.keys()].sort((a, b) => a.localeCompare(b));
|
|
230
|
+
for (const ruleName of removed) {
|
|
231
|
+
const removal = removals.get(ruleName);
|
|
232
|
+
await (0, RulesToSkillsMigration_1.removeLocalRuleReplacementState)(projectRoot, ruleName, false);
|
|
233
|
+
if (!removal?.alreadyInstalled) {
|
|
234
|
+
await fs.rm(path.join(projectRoot, '.agents', 'skills', ruleName), {
|
|
235
|
+
force: true,
|
|
236
|
+
recursive: true,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
await fs.rm(path.join(projectRoot, '.claude', 'skills', ruleName), {
|
|
240
|
+
force: true,
|
|
241
|
+
recursive: true,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return removed;
|
|
245
|
+
}
|
|
208
246
|
async function promptLine(message) {
|
|
209
247
|
const rl = readline.createInterface({
|
|
210
248
|
input: process.stdin,
|
|
@@ -456,6 +494,10 @@ async function migrateClaudePluginsHandler(argv) {
|
|
|
456
494
|
if (removedAuxiliaryRules.length > 0) {
|
|
457
495
|
console.log(`[skiller] Removed stale plugin-derived local rules:\n${removedAuxiliaryRules.map((name) => `- ${name}`).join('\n')}`);
|
|
458
496
|
}
|
|
497
|
+
const removedLegacyExternalRules = await cleanupLegacyExternalRuleMatches(projectRoot);
|
|
498
|
+
if (removedLegacyExternalRules.length > 0) {
|
|
499
|
+
console.log(`[skiller] Removed legacy external rule matches:\n${removedLegacyExternalRules.map((name) => `- ${name}`).join('\n')}`);
|
|
500
|
+
}
|
|
459
501
|
if (plan.unresolved.length > 0) {
|
|
460
502
|
console.log(`[skiller] Skipped unresolved plugins:\n${plan.unresolved.map((entry) => `- ${entry.pluginId}: ${entry.reason}`).join('\n')}`);
|
|
461
503
|
}
|
|
@@ -524,6 +566,13 @@ async function addHandler(argv) {
|
|
|
524
566
|
...(argv.args ?? []),
|
|
525
567
|
]);
|
|
526
568
|
}
|
|
569
|
+
async function installHandler(argv) {
|
|
570
|
+
await executeSkillsWrapper(argv['project-root'], [
|
|
571
|
+
'install',
|
|
572
|
+
...(argv.args ?? []),
|
|
573
|
+
]);
|
|
574
|
+
await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
|
|
575
|
+
}
|
|
527
576
|
async function removeHandler(argv) {
|
|
528
577
|
await executeSkillsWrapper(argv['project-root'], [
|
|
529
578
|
'remove',
|
|
@@ -553,6 +602,13 @@ async function updateHandler(argv) {
|
|
|
553
602
|
'update',
|
|
554
603
|
...(argv.args ?? []),
|
|
555
604
|
]);
|
|
605
|
+
await applyAfterSkillsLifecycleStep(argv['project-root'], argv.verbose ?? false);
|
|
606
|
+
}
|
|
607
|
+
async function outdatedHandler(argv) {
|
|
608
|
+
await executeSkillsWrapper(argv['project-root'], [
|
|
609
|
+
'outdated',
|
|
610
|
+
...(argv.args ?? []),
|
|
611
|
+
]);
|
|
556
612
|
}
|
|
557
613
|
async function skillsHandler(argv) {
|
|
558
614
|
await executeSkillsWrapper(argv['project-root'], [
|
|
@@ -134,11 +134,7 @@ async function removeLocalRuleReplacementState(projectRoot, ruleName, dryRun) {
|
|
|
134
134
|
if (!dryRun) {
|
|
135
135
|
await fs.rm(rulePath, { force: true });
|
|
136
136
|
}
|
|
137
|
-
|
|
138
|
-
if (!localSkillNames.includes(ruleName)) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
await (0, SkillsManifest_1.writeLocalSkillNames)(projectRoot, localSkillNames.filter((name) => name !== ruleName), dryRun);
|
|
137
|
+
await (0, SkillsManifest_1.scrubLegacyLocalSkillsManifest)(projectRoot, dryRun);
|
|
142
138
|
}
|
|
143
139
|
async function planRulesToSkillsMigration(projectRoot, requestedRuleNames) {
|
|
144
140
|
const availableRuleNames = await listRuleNames(projectRoot);
|
|
@@ -37,14 +37,11 @@ exports.getCanonicalSkillsDir = getCanonicalSkillsDir;
|
|
|
37
37
|
exports.getCanonicalRulesDir = getCanonicalRulesDir;
|
|
38
38
|
exports.readUpstreamOwnedSkillNames = readUpstreamOwnedSkillNames;
|
|
39
39
|
exports.resolveSkillOwnership = resolveSkillOwnership;
|
|
40
|
-
exports.adoptSkillerOwnedSkillNames = adoptSkillerOwnedSkillNames;
|
|
41
|
-
exports.syncSkillerOwnedSkillNamesFromRules = syncSkillerOwnedSkillNamesFromRules;
|
|
42
40
|
exports.migrateLegacyProjectState = migrateLegacyProjectState;
|
|
43
41
|
const fs = __importStar(require("fs/promises"));
|
|
44
42
|
const path = __importStar(require("path"));
|
|
45
43
|
const yaml = __importStar(require("js-yaml"));
|
|
46
44
|
const project_paths_1 = require("./project-paths");
|
|
47
|
-
const SkillsManifest_1 = require("./SkillsManifest");
|
|
48
45
|
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
49
46
|
function normalizeSkillNameForFilesystem(name) {
|
|
50
47
|
return name.replace(/:/g, '-');
|
|
@@ -321,10 +318,7 @@ async function readUpstreamOwnedSkillNames(projectRoot) {
|
|
|
321
318
|
}
|
|
322
319
|
async function resolveSkillOwnership(projectRoot) {
|
|
323
320
|
const upstreamOwned = await readUpstreamOwnedSkillNames(projectRoot);
|
|
324
|
-
const localOwned =
|
|
325
|
-
...(await (0, SkillsManifest_1.loadLocalSkillNames)(projectRoot)),
|
|
326
|
-
...(await readLocalRuleSkillNames(projectRoot)),
|
|
327
|
-
]);
|
|
321
|
+
const localOwned = await readLocalRuleSkillNames(projectRoot);
|
|
328
322
|
const canonicalSkillNames = await readCanonicalSkillNames(projectRoot);
|
|
329
323
|
const allExplicitNames = new Set([...upstreamOwned, ...localOwned]);
|
|
330
324
|
const orphaned = new Set([...canonicalSkillNames]
|
|
@@ -346,11 +340,11 @@ async function resolveSkillOwnership(projectRoot) {
|
|
|
346
340
|
if (upstreamOwned.has(name))
|
|
347
341
|
owners.push('skills-lock.json');
|
|
348
342
|
if (localOwned.has(name))
|
|
349
|
-
owners.push('
|
|
343
|
+
owners.push('.agents/rules');
|
|
350
344
|
return `Skill '${name}' has mixed ownership: ${owners.join(', ')}`;
|
|
351
345
|
}),
|
|
352
346
|
...[...orphaned].map((name) => {
|
|
353
|
-
return `Canonical skill '${name}' is unmanaged; leaving it untouched because it is not in skills-lock.json
|
|
347
|
+
return `Canonical skill '${name}' is unmanaged; leaving it untouched because it is not in skills-lock.json or .agents/rules/${name}.mdc.`;
|
|
354
348
|
}),
|
|
355
349
|
];
|
|
356
350
|
return {
|
|
@@ -361,38 +355,6 @@ async function resolveSkillOwnership(projectRoot) {
|
|
|
361
355
|
warnings,
|
|
362
356
|
};
|
|
363
357
|
}
|
|
364
|
-
async function adoptSkillerOwnedSkillNames(projectRoot, skillNames, dryRun) {
|
|
365
|
-
if (skillNames.length === 0)
|
|
366
|
-
return;
|
|
367
|
-
const ownership = await resolveSkillOwnership(projectRoot);
|
|
368
|
-
const next = new Set(ownership.localOwned);
|
|
369
|
-
for (const name of skillNames) {
|
|
370
|
-
if (ownership.upstreamOwned.has(name))
|
|
371
|
-
continue;
|
|
372
|
-
next.add(name);
|
|
373
|
-
}
|
|
374
|
-
await (0, SkillsManifest_1.writeLocalSkillNames)(projectRoot, [...next], dryRun);
|
|
375
|
-
}
|
|
376
|
-
async function syncSkillerOwnedSkillNamesFromRules(projectRoot, dryRun) {
|
|
377
|
-
const rulesDir = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, 'rules');
|
|
378
|
-
let ruleNames = [];
|
|
379
|
-
try {
|
|
380
|
-
const entries = await fs.readdir(rulesDir, { withFileTypes: true });
|
|
381
|
-
ruleNames = entries
|
|
382
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith('.mdc'))
|
|
383
|
-
.map((entry) => path.basename(entry.name, '.mdc'))
|
|
384
|
-
.sort((a, b) => a.localeCompare(b));
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
ruleNames = [];
|
|
388
|
-
}
|
|
389
|
-
const upstreamOwned = await readUpstreamOwnedSkillNames(projectRoot);
|
|
390
|
-
const nextLocalSkillNames = ruleNames.filter((name) => {
|
|
391
|
-
return !upstreamOwned.has(name);
|
|
392
|
-
});
|
|
393
|
-
await (0, SkillsManifest_1.writeLocalSkillNames)(projectRoot, nextLocalSkillNames, dryRun);
|
|
394
|
-
return nextLocalSkillNames;
|
|
395
|
-
}
|
|
396
358
|
async function migrateLegacyProjectState(projectRoot, dryRun) {
|
|
397
359
|
const legacyDir = path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR);
|
|
398
360
|
const canonicalDir = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR);
|
|
@@ -407,7 +369,10 @@ async function migrateLegacyProjectState(projectRoot, dryRun) {
|
|
|
407
369
|
const plannedWrites = new Map();
|
|
408
370
|
const deletePaths = new Set();
|
|
409
371
|
const conflicts = [];
|
|
410
|
-
|
|
372
|
+
const legacyManifestPath = path.join(legacyDir, '.skiller.json');
|
|
373
|
+
if (await pathExists(legacyManifestPath)) {
|
|
374
|
+
deletePaths.add(legacyManifestPath);
|
|
375
|
+
}
|
|
411
376
|
const legacyConfigPath = path.join(legacyDir, project_paths_1.SKILLER_CONFIG_FILE);
|
|
412
377
|
if (await pathExists(legacyConfigPath)) {
|
|
413
378
|
await planBufferWrite(path.join(canonicalDir, project_paths_1.SKILLER_CONFIG_FILE), Buffer.from(rewriteLegacyAgentIdsInToml(await fs.readFile(legacyConfigPath, 'utf8'))), legacyConfigPath, plannedWrites, conflicts);
|
|
@@ -416,11 +381,16 @@ async function migrateLegacyProjectState(projectRoot, dryRun) {
|
|
|
416
381
|
}
|
|
417
382
|
}
|
|
418
383
|
await planFileMigration(path.join(legacyDir, project_paths_1.PROJECT_AGENTS_FILE), path.join(canonicalDir, project_paths_1.PROJECT_AGENTS_FILE), plannedWrites, deletePaths, conflicts);
|
|
419
|
-
|
|
384
|
+
if (!(await pathExists(canonicalSkillsDir))) {
|
|
385
|
+
await planLegacySkillsMigration(path.join(legacyDir, 'skills'), canonicalSkillsDir, plannedWrites, deletePaths, conflicts);
|
|
386
|
+
}
|
|
420
387
|
await planLegacyRulesMigration(path.join(legacyDir, 'rules'), canonicalRulesDir, canonicalSkillsDir, plannedWrites, deletePaths, conflicts);
|
|
421
388
|
if (conflicts.length > 0) {
|
|
422
389
|
throw new Error(`Legacy .claude migration conflicts:\n- ${conflicts.join('\n- ')}`);
|
|
423
390
|
}
|
|
391
|
+
if (plannedWrites.size === 0 && deletePaths.size === 0) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
424
394
|
if (dryRun)
|
|
425
395
|
return;
|
|
426
396
|
for (const [destinationPath, content] of plannedWrites.entries()) {
|
|
@@ -37,8 +37,7 @@ exports.SKILLS_MANIFEST_VERSION = exports.LEGACY_CLAUDE_MANIFEST_FILENAME = expo
|
|
|
37
37
|
exports.isPluginManifestEntry = isPluginManifestEntry;
|
|
38
38
|
exports.isClaudeManifestEntry = isClaudeManifestEntry;
|
|
39
39
|
exports.loadSkillsManifestEntries = loadSkillsManifestEntries;
|
|
40
|
-
exports.
|
|
41
|
-
exports.writeLocalSkillNames = writeLocalSkillNames;
|
|
40
|
+
exports.scrubLegacyLocalSkillsManifest = scrubLegacyLocalSkillsManifest;
|
|
42
41
|
exports.writeSkillsManifestEntries = writeSkillsManifestEntries;
|
|
43
42
|
exports.listSkillDirectories = listSkillDirectories;
|
|
44
43
|
const fs = __importStar(require("fs/promises"));
|
|
@@ -191,16 +190,6 @@ function parseProjectTargets(raw) {
|
|
|
191
190
|
}
|
|
192
191
|
return out;
|
|
193
192
|
}
|
|
194
|
-
function parseLocalSkills(raw) {
|
|
195
|
-
if (!raw || typeof raw !== 'object')
|
|
196
|
-
return [];
|
|
197
|
-
const obj = raw;
|
|
198
|
-
if (!Array.isArray(obj.localSkills))
|
|
199
|
-
return [];
|
|
200
|
-
return [
|
|
201
|
-
...new Set(obj.localSkills.filter((v) => typeof v === 'string')),
|
|
202
|
-
].sort((a, b) => a.localeCompare(b));
|
|
203
|
-
}
|
|
204
193
|
async function readProjectManifestRaw(projectRoot) {
|
|
205
194
|
const canonicalManifestPath = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, exports.SKILLS_MANIFEST_FILENAME);
|
|
206
195
|
const legacyManifestPath = path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR, exports.SKILLS_MANIFEST_FILENAME);
|
|
@@ -336,25 +325,37 @@ async function loadSkillsManifestEntries(projectRoot, targetSkillsDir) {
|
|
|
336
325
|
// Legacy migration: prior versions stored manifests in the target skills dir.
|
|
337
326
|
return await loadLegacyTargetSkillsManifestEntries(targetSkillsDir);
|
|
338
327
|
}
|
|
339
|
-
async function
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
328
|
+
async function normalizeManifestFile(manifestPath, dryRun) {
|
|
329
|
+
if (!(await fileExists(manifestPath)))
|
|
330
|
+
return;
|
|
331
|
+
let raw;
|
|
332
|
+
try {
|
|
333
|
+
raw = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const hasLegacyLocalSkills = !!raw &&
|
|
339
|
+
typeof raw === 'object' &&
|
|
340
|
+
Array.isArray(raw.localSkills);
|
|
341
|
+
if (!hasLegacyLocalSkills)
|
|
342
|
+
return;
|
|
349
343
|
if (dryRun)
|
|
350
344
|
return;
|
|
351
|
-
|
|
345
|
+
const targets = parseProjectTargets(raw);
|
|
346
|
+
if (Object.keys(targets).length === 0) {
|
|
347
|
+
await fs.rm(manifestPath, { force: true });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
352
350
|
const manifest = {
|
|
353
351
|
version: exports.SKILLS_MANIFEST_VERSION,
|
|
354
|
-
targets
|
|
355
|
-
localSkills: nextLocalSkills,
|
|
352
|
+
targets,
|
|
356
353
|
};
|
|
357
|
-
await fs.writeFile(
|
|
354
|
+
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
355
|
+
}
|
|
356
|
+
async function scrubLegacyLocalSkillsManifest(projectRoot, dryRun) {
|
|
357
|
+
await normalizeManifestFile(path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, exports.SKILLS_MANIFEST_FILENAME), dryRun);
|
|
358
|
+
await normalizeManifestFile(path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR, exports.SKILLS_MANIFEST_FILENAME), dryRun);
|
|
358
359
|
}
|
|
359
360
|
async function writeSkillsManifestEntries(projectRoot, targetSkillsDir, entries, dryRun) {
|
|
360
361
|
const normalized = normalizeEntries(entries);
|
|
@@ -365,7 +366,6 @@ async function writeSkillsManifestEntries(projectRoot, targetSkillsDir, entries,
|
|
|
365
366
|
let existingTargets = {};
|
|
366
367
|
const raw = await readProjectManifestRaw(projectRoot);
|
|
367
368
|
existingTargets = parseProjectTargets(raw);
|
|
368
|
-
const existingLocalSkills = parseLocalSkills(raw);
|
|
369
369
|
if (normalized.length === 0) {
|
|
370
370
|
delete existingTargets[preferredTargetKey];
|
|
371
371
|
if (preferredTargetKey !== absoluteTargetKey) {
|
|
@@ -383,7 +383,7 @@ async function writeSkillsManifestEntries(projectRoot, targetSkillsDir, entries,
|
|
|
383
383
|
// Ensure `.claude` exists since the manifest lives there.
|
|
384
384
|
await fs.mkdir(projectClaudeDir, { recursive: true });
|
|
385
385
|
const targetKeys = Object.keys(existingTargets).sort((a, b) => a.localeCompare(b));
|
|
386
|
-
if (targetKeys.length === 0
|
|
386
|
+
if (targetKeys.length === 0) {
|
|
387
387
|
await Promise.allSettled([fs.unlink(projectManifestPath)]);
|
|
388
388
|
}
|
|
389
389
|
else {
|
|
@@ -393,7 +393,6 @@ async function writeSkillsManifestEntries(projectRoot, targetSkillsDir, entries,
|
|
|
393
393
|
const manifest = {
|
|
394
394
|
version: exports.SKILLS_MANIFEST_VERSION,
|
|
395
395
|
targets: nextTargets,
|
|
396
|
-
localSkills: existingLocalSkills,
|
|
397
396
|
};
|
|
398
397
|
await fs.writeFile(projectManifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
399
398
|
}
|
|
@@ -52,6 +52,7 @@ const yaml = __importStar(require("js-yaml"));
|
|
|
52
52
|
const constants_1 = require("../constants");
|
|
53
53
|
const SkillsUtils_1 = require("./SkillsUtils");
|
|
54
54
|
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
55
|
+
const SkillsManifest_1 = require("./SkillsManifest");
|
|
55
56
|
const SkillOwnership_1 = require("./SkillOwnership");
|
|
56
57
|
const LEGACY_CODEX_SKILLS_PATH = path.join('.codex', 'skills');
|
|
57
58
|
const UNIVERSAL_AGENTS_SKILLS_PATH = path.join('.agents', 'skills');
|
|
@@ -476,6 +477,184 @@ async function writeFileIfChanged(targetPath, content, dryRun) {
|
|
|
476
477
|
}
|
|
477
478
|
return true;
|
|
478
479
|
}
|
|
480
|
+
async function readNormalizedRuleSourceContent(rulePath) {
|
|
481
|
+
try {
|
|
482
|
+
return normalizeRuleSourceContent(await fs.readFile(rulePath, 'utf8'));
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function getFrontmatterBlock(content) {
|
|
489
|
+
const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
|
|
490
|
+
return match ? match[1] : null;
|
|
491
|
+
}
|
|
492
|
+
function stripQuotedValue(value) {
|
|
493
|
+
const trimmed = value.trim();
|
|
494
|
+
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
495
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
496
|
+
return trimmed.slice(1, -1);
|
|
497
|
+
}
|
|
498
|
+
return trimmed;
|
|
499
|
+
}
|
|
500
|
+
function extractSkillerSourceRelPathFromFrontmatter(content) {
|
|
501
|
+
const block = getFrontmatterBlock(content);
|
|
502
|
+
if (!block)
|
|
503
|
+
return null;
|
|
504
|
+
let metadataIndent = null;
|
|
505
|
+
let skillerIndent = null;
|
|
506
|
+
for (const line of block.split('\n')) {
|
|
507
|
+
const trimmed = line.trim();
|
|
508
|
+
if (!trimmed)
|
|
509
|
+
continue;
|
|
510
|
+
const indent = line.length - line.trimStart().length;
|
|
511
|
+
if (metadataIndent !== null &&
|
|
512
|
+
indent <= metadataIndent &&
|
|
513
|
+
trimmed !== 'metadata:') {
|
|
514
|
+
metadataIndent = null;
|
|
515
|
+
skillerIndent = null;
|
|
516
|
+
}
|
|
517
|
+
if (skillerIndent !== null &&
|
|
518
|
+
indent <= skillerIndent &&
|
|
519
|
+
trimmed !== 'skiller:') {
|
|
520
|
+
skillerIndent = null;
|
|
521
|
+
}
|
|
522
|
+
if (trimmed === 'metadata:') {
|
|
523
|
+
metadataIndent = indent;
|
|
524
|
+
skillerIndent = null;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (metadataIndent !== null && trimmed === 'skiller:') {
|
|
528
|
+
skillerIndent = indent;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (skillerIndent !== null && trimmed.startsWith('source:')) {
|
|
532
|
+
return stripQuotedValue(trimmed.slice('source:'.length));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
async function pruneDuplicateClaudeAliasRules(projectRoot, targetSkillsDirs, verbose, dryRun) {
|
|
538
|
+
const rulesDir = path.join(projectRoot, '.agents', 'rules');
|
|
539
|
+
const canonicalSkillsDir = path.join(projectRoot, '.agents', 'skills');
|
|
540
|
+
let ruleEntries;
|
|
541
|
+
try {
|
|
542
|
+
ruleEntries = await fs.readdir(rulesDir, { withFileTypes: true });
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
const pruned = [];
|
|
548
|
+
for (const entry of ruleEntries) {
|
|
549
|
+
if (!entry.isFile() || !entry.name.endsWith('.mdc'))
|
|
550
|
+
continue;
|
|
551
|
+
const aliasName = path.basename(entry.name, '.mdc');
|
|
552
|
+
if (!aliasName.startsWith('claude-'))
|
|
553
|
+
continue;
|
|
554
|
+
const baseName = aliasName.slice('claude-'.length);
|
|
555
|
+
if (!baseName)
|
|
556
|
+
continue;
|
|
557
|
+
const aliasRulePath = path.join(rulesDir, `${aliasName}.mdc`);
|
|
558
|
+
const baseRulePath = path.join(rulesDir, `${baseName}.mdc`);
|
|
559
|
+
const [aliasRuleContent, baseRuleContent] = await Promise.all([
|
|
560
|
+
readNormalizedRuleSourceContent(aliasRulePath),
|
|
561
|
+
readNormalizedRuleSourceContent(baseRulePath),
|
|
562
|
+
]);
|
|
563
|
+
if (!aliasRuleContent || !baseRuleContent)
|
|
564
|
+
continue;
|
|
565
|
+
if (aliasRuleContent !== baseRuleContent)
|
|
566
|
+
continue;
|
|
567
|
+
const deletePaths = [
|
|
568
|
+
aliasRulePath,
|
|
569
|
+
path.join(canonicalSkillsDir, aliasName),
|
|
570
|
+
path.join(projectRoot, LEGACY_CODEX_SKILLS_PATH, aliasName),
|
|
571
|
+
...targetSkillsDirs.map((skillsDir) => path.join(skillsDir, aliasName)),
|
|
572
|
+
];
|
|
573
|
+
if (!dryRun) {
|
|
574
|
+
await Promise.all(deletePaths.map((deletePath) => fs.rm(deletePath, { recursive: true, force: true })));
|
|
575
|
+
}
|
|
576
|
+
pruned.push(aliasName);
|
|
577
|
+
(0, constants_1.logVerboseInfo)(dryRun
|
|
578
|
+
? `DRY RUN: Would prune stale claude alias '${aliasName}' because '${baseName}' already exists with identical local rule content`
|
|
579
|
+
: `Pruned stale claude alias '${aliasName}' because '${baseName}' already exists with identical local rule content`, verbose, dryRun);
|
|
580
|
+
}
|
|
581
|
+
return pruned;
|
|
582
|
+
}
|
|
583
|
+
async function pruneCompiledSkillsWithMissingRuleSources(projectRoot, targetSkillsDirs, verbose, dryRun) {
|
|
584
|
+
const canonicalSkillsDir = path.join(projectRoot, '.agents', 'skills');
|
|
585
|
+
let skillEntries;
|
|
586
|
+
try {
|
|
587
|
+
skillEntries = await fs.readdir(canonicalSkillsDir, {
|
|
588
|
+
withFileTypes: true,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
const pruned = [];
|
|
595
|
+
for (const entry of skillEntries) {
|
|
596
|
+
if (!entry.isDirectory())
|
|
597
|
+
continue;
|
|
598
|
+
const skillName = entry.name;
|
|
599
|
+
const skillDir = path.join(canonicalSkillsDir, skillName);
|
|
600
|
+
const skillMdPath = path.join(skillDir, constants_1.SKILL_MD_FILENAME);
|
|
601
|
+
let skillMdContent;
|
|
602
|
+
try {
|
|
603
|
+
skillMdContent = await fs.readFile(skillMdPath, 'utf8');
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const sourceRelPath = extractSkillerSourceRelPathFromFrontmatter(skillMdContent);
|
|
609
|
+
if (!sourceRelPath?.startsWith('.agents/rules/'))
|
|
610
|
+
continue;
|
|
611
|
+
const sourcePath = path.resolve(projectRoot, sourceRelPath);
|
|
612
|
+
if (await pathExists(sourcePath))
|
|
613
|
+
continue;
|
|
614
|
+
const deletePaths = [
|
|
615
|
+
skillDir,
|
|
616
|
+
path.join(projectRoot, LEGACY_CODEX_SKILLS_PATH, skillName),
|
|
617
|
+
...targetSkillsDirs.map((skillsDir) => path.join(skillsDir, skillName)),
|
|
618
|
+
];
|
|
619
|
+
if (!dryRun) {
|
|
620
|
+
await Promise.all(deletePaths.map((deletePath) => fs.rm(deletePath, { recursive: true, force: true })));
|
|
621
|
+
}
|
|
622
|
+
pruned.push(skillName);
|
|
623
|
+
(0, constants_1.logVerboseInfo)(dryRun
|
|
624
|
+
? `DRY RUN: Would prune compiled skill '${skillName}' because its source rule is missing: ${sourceRelPath}`
|
|
625
|
+
: `Pruned compiled skill '${skillName}' because its source rule is missing: ${sourceRelPath}`, verbose, dryRun);
|
|
626
|
+
}
|
|
627
|
+
return pruned;
|
|
628
|
+
}
|
|
629
|
+
async function cleanupLegacyClaudeManagedSkillMirrors(projectRoot, targetSkillsDirs, verbose, dryRun) {
|
|
630
|
+
const cleaned = [];
|
|
631
|
+
for (const targetSkillsDir of targetSkillsDirs) {
|
|
632
|
+
const entries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(projectRoot, targetSkillsDir);
|
|
633
|
+
if (entries.length === 0)
|
|
634
|
+
continue;
|
|
635
|
+
const legacyClaudeEntries = entries.filter(SkillsManifest_1.isClaudeManifestEntry);
|
|
636
|
+
if (legacyClaudeEntries.length === 0)
|
|
637
|
+
continue;
|
|
638
|
+
const nextEntries = entries.filter((entry) => !(0, SkillsManifest_1.isClaudeManifestEntry)(entry));
|
|
639
|
+
const legacyDestPaths = [
|
|
640
|
+
...new Set(legacyClaudeEntries.map((entry) => entry.destRelPath)),
|
|
641
|
+
];
|
|
642
|
+
if (!dryRun) {
|
|
643
|
+
await Promise.all(legacyDestPaths.map((destRelPath) => fs.rm(path.join(targetSkillsDir, destRelPath), {
|
|
644
|
+
recursive: true,
|
|
645
|
+
force: true,
|
|
646
|
+
})));
|
|
647
|
+
}
|
|
648
|
+
await (0, SkillsManifest_1.writeSkillsManifestEntries)(projectRoot, targetSkillsDir, nextEntries, dryRun);
|
|
649
|
+
cleaned.push(...legacyDestPaths.map((destRelPath) => `${targetSkillsDir}:${destRelPath}`));
|
|
650
|
+
for (const destRelPath of legacyDestPaths) {
|
|
651
|
+
(0, constants_1.logVerboseInfo)(dryRun
|
|
652
|
+
? `DRY RUN: Would remove legacy claude-managed skill mirror '${destRelPath}' from ${targetSkillsDir}`
|
|
653
|
+
: `Removed legacy claude-managed skill mirror '${destRelPath}' from ${targetSkillsDir}`, verbose, dryRun);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return cleaned;
|
|
657
|
+
}
|
|
479
658
|
async function extractLocalRulesFromCanonicalSkills(projectRoot, verbose, dryRun) {
|
|
480
659
|
const warnings = [];
|
|
481
660
|
const extracted = [];
|
|
@@ -489,7 +668,6 @@ async function extractLocalRulesFromCanonicalSkills(projectRoot, verbose, dryRun
|
|
|
489
668
|
return { extracted, warnings };
|
|
490
669
|
}
|
|
491
670
|
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
492
|
-
const adoptedNames = [];
|
|
493
671
|
for (const entry of entries) {
|
|
494
672
|
if (!entry.isDirectory())
|
|
495
673
|
continue;
|
|
@@ -523,15 +701,11 @@ async function extractLocalRulesFromCanonicalSkills(projectRoot, verbose, dryRun
|
|
|
523
701
|
: `Extracted local skill source ${skillName} to ${toProjectRelative(projectRoot, rulePath)}`, verbose, dryRun);
|
|
524
702
|
}
|
|
525
703
|
extracted.push(skillName);
|
|
526
|
-
adoptedNames.push(skillName);
|
|
527
704
|
}
|
|
528
705
|
catch (err) {
|
|
529
706
|
warnings.push(`Failed to extract local rule source for ${skillName}: ${err.message}`);
|
|
530
707
|
}
|
|
531
708
|
}
|
|
532
|
-
if (adoptedNames.length > 0) {
|
|
533
|
-
await (0, SkillOwnership_1.adoptSkillerOwnedSkillNames)(projectRoot, adoptedNames, dryRun);
|
|
534
|
-
}
|
|
535
709
|
return { extracted, warnings };
|
|
536
710
|
}
|
|
537
711
|
async function compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun) {
|
|
@@ -550,7 +724,6 @@ async function compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun) {
|
|
|
550
724
|
const ruleFiles = entries.filter((entry) => {
|
|
551
725
|
return entry.isFile() && entry.name.endsWith('.mdc');
|
|
552
726
|
});
|
|
553
|
-
const adoptedNames = [];
|
|
554
727
|
for (const ruleFile of ruleFiles) {
|
|
555
728
|
const skillName = path.basename(ruleFile.name, '.mdc');
|
|
556
729
|
if (ownership.upstreamOwned.has(skillName)) {
|
|
@@ -592,10 +765,6 @@ async function compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun) {
|
|
|
592
765
|
: `Compiled ${toProjectRelative(projectRoot, sourcePath)} to ${toProjectRelative(projectRoot, skillMdPath)}`, verbose, dryRun);
|
|
593
766
|
}
|
|
594
767
|
compiled.push(skillName);
|
|
595
|
-
adoptedNames.push(skillName);
|
|
596
|
-
}
|
|
597
|
-
if (adoptedNames.length > 0) {
|
|
598
|
-
await (0, SkillOwnership_1.adoptSkillerOwnedSkillNames)(projectRoot, adoptedNames, dryRun);
|
|
599
768
|
}
|
|
600
769
|
return { compiled, warnings };
|
|
601
770
|
}
|
|
@@ -866,31 +1035,34 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
866
1035
|
(0, constants_1.logVerboseInfo)('Skills support disabled', verbose, dryRun);
|
|
867
1036
|
return;
|
|
868
1037
|
}
|
|
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
|
-
await (0, SkillOwnership_1.syncSkillerOwnedSkillNamesFromRules)(projectRoot, dryRun);
|
|
879
|
-
}
|
|
880
1038
|
// Determine canonical skills directory, with legacy fallback for migration.
|
|
881
1039
|
const skillsDir = await resolveProjectSkillsDir(projectRoot, skillerDir);
|
|
882
|
-
// Compute destinations up-front so legacy codex migration can de-duplicate targets.
|
|
1040
|
+
// Compute destinations up-front so cleanup + legacy codex migration can de-duplicate targets.
|
|
883
1041
|
const destinationPaths = new Set();
|
|
884
1042
|
for (const agent of agents) {
|
|
885
1043
|
if (agent.supportsNativeSkills?.() && agent.getSkillsPath) {
|
|
886
1044
|
const targetPath = agent.getSkillsPath(projectRoot);
|
|
887
1045
|
if (targetPath && targetPath !== skillsDir) {
|
|
888
|
-
// Deduplicate shared paths
|
|
889
1046
|
destinationPaths.add(targetPath);
|
|
890
1047
|
}
|
|
891
1048
|
}
|
|
892
1049
|
}
|
|
1050
|
+
if (skillerDir) {
|
|
1051
|
+
await (0, SkillsManifest_1.scrubLegacyLocalSkillsManifest)(projectRoot, dryRun);
|
|
1052
|
+
await pruneCompiledSkillsWithMissingRuleSources(projectRoot, [...destinationPaths], verbose, dryRun);
|
|
1053
|
+
await pruneDuplicateClaudeAliasRules(projectRoot, [...destinationPaths], verbose, dryRun);
|
|
1054
|
+
const compileResult = await compileRulesToSkills(skillerDir, projectRoot, verbose, dryRun);
|
|
1055
|
+
for (const warning of compileResult.warnings) {
|
|
1056
|
+
(0, constants_1.logWarn)(warning, dryRun);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
893
1059
|
await migrateLegacyCodexSkillsDir(skillsDir, destinationPaths);
|
|
1060
|
+
if (destinationPaths.size > 0) {
|
|
1061
|
+
await cleanupLegacyClaudeManagedSkillMirrors(projectRoot, [...destinationPaths], verbose, dryRun);
|
|
1062
|
+
}
|
|
1063
|
+
if (skillerDir) {
|
|
1064
|
+
await pruneDuplicateClaudeAliasRules(projectRoot, [...destinationPaths], verbose, dryRun);
|
|
1065
|
+
}
|
|
894
1066
|
// Check if skills directory exists
|
|
895
1067
|
let skillsDirExists = true;
|
|
896
1068
|
try {
|
|
@@ -936,17 +1108,6 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
|
|
|
936
1108
|
}
|
|
937
1109
|
}
|
|
938
1110
|
}
|
|
939
|
-
// Sync project Claude commands + agents as skills into agent skills dirs.
|
|
940
|
-
// This intentionally does NOT write into the canonical .agents/skills source-of-truth.
|
|
941
|
-
if (destinationPaths.size > 0) {
|
|
942
|
-
const { syncClaudeProjectCommandsAndAgentsToSkillsDirs } = await Promise.resolve().then(() => __importStar(require('./ClaudeProjectSync')));
|
|
943
|
-
await syncClaudeProjectCommandsAndAgentsToSkillsDirs({
|
|
944
|
-
projectRoot,
|
|
945
|
-
targetSkillsDirs: [...destinationPaths],
|
|
946
|
-
verbose,
|
|
947
|
-
dryRun,
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
1111
|
}
|
|
951
1112
|
/**
|
|
952
1113
|
* Recursively finds all folders containing SKILL.md in a directory.
|