skiller 0.7.18 → 0.7.20
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 +11 -8
- package/dist/core/ClaudePluginSync.js +103 -59
- package/dist/core/ClaudeProjectSync.js +21 -7
- package/dist/core/SkillsManifest.js +103 -24
- package/dist/core/SkillsProcessor.js +48 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,27 +53,29 @@ A Claude-centric fork of [ruler](https://github.com/intellectronica/ruler) with
|
|
|
53
53
|
- Shared paths are deduplicated (Claude/Copilot/Kilo share `.claude/skills`, Goose/Amp share `.agents/skills`)
|
|
54
54
|
- Agent skills directories are auto-added to `.gitignore` (excluding `.claude/skills`)
|
|
55
55
|
- Validates skill structure — warns on missing `SKILL.md`
|
|
56
|
+
- Flattens nested skills into dash-separated names for agent skills dirs (e.g., `workflows/lfg` → `workflows-lfg`)
|
|
56
57
|
|
|
57
58
|
## 9. Claude Code Plugins → Skills
|
|
58
59
|
|
|
59
60
|
- Reads `.claude/settings.json` `enabledPlugins`
|
|
60
61
|
- Reads plugin content from `~/.claude/plugins/marketplaces` (never from `cache/`)
|
|
61
|
-
- Syncs enabled plugin `skills/` into agent skills directories on `skiller apply`
|
|
62
|
-
- Syncs enabled plugin `commands
|
|
62
|
+
- Syncs enabled plugin `skills/` into agent skills directories on `skiller apply` (recursive, flattened names)
|
|
63
|
+
- Syncs enabled plugin `commands/**/*.md` as skills (`SKILL.md`) into agent skills directories (recursive, flattened names)
|
|
63
64
|
- Syncs enabled plugin `agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
|
|
64
65
|
- Uses the skill/command/agent name by default (matches existing Codex skill names)
|
|
65
|
-
- If a name conflicts, local skills win and the plugin
|
|
66
|
-
- Tracks plugin-managed
|
|
66
|
+
- If a name conflicts, local skills win and the plugin item is namespaced as `<pluginName>-<name>` (numeric suffix if multiple enabled plugins share the same name)
|
|
67
|
+
- Tracks plugin-managed items in `.claude/.skiller.json` (per project, grouped by agent skills dir)
|
|
67
68
|
- Removes stale plugin skills when plugins are disabled
|
|
68
69
|
|
|
69
70
|
## 10. Claude Commands/Agents → Skills
|
|
70
71
|
|
|
71
72
|
- Syncs `.claude/commands/**/*.md` as skills (`SKILL.md`) into agent skills directories
|
|
73
|
+
- Flattens nested commands into dash-separated names (e.g., `workflows/brainstorm.md` → `workflows-brainstorm`)
|
|
72
74
|
- Syncs `.claude/agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
|
|
73
75
|
- Uses the command/agent name by default
|
|
74
76
|
- If a name conflicts, existing local/manual skills win and the project item is namespaced as `claude-<name>`
|
|
75
77
|
- Project items win over plugin skills/commands/agents on name conflicts
|
|
76
|
-
- Tracks project-managed items in
|
|
78
|
+
- Tracks project-managed items in `.claude/.skiller.json` (per project, grouped by agent skills dir)
|
|
77
79
|
|
|
78
80
|
---
|
|
79
81
|
|
|
@@ -605,10 +607,11 @@ Shared paths are deduplicated — agents sharing the same directory only trigger
|
|
|
605
607
|
If your project enables Claude Code plugins in `.claude/settings.json`, Skiller also syncs plugin content into agent skills directories on `skiller apply`:
|
|
606
608
|
|
|
607
609
|
- Plugin `skills/` are copied as skills
|
|
608
|
-
- Plugin `commands
|
|
610
|
+
- Plugin `commands/**/*.md` are converted into skills (`SKILL.md`)
|
|
611
|
+
- Plugin `agents/**/*.md` are converted into skills (`SKILL.md`)
|
|
609
612
|
- Plugin skills use their original skill/command name by default
|
|
610
|
-
- If a name conflicts, local skills win and the plugin
|
|
611
|
-
- Plugin-managed
|
|
613
|
+
- If a name conflicts, local skills win and the plugin item is namespaced as `<pluginName>-<name>` (numeric suffix if multiple enabled plugins share the same name)
|
|
614
|
+
- Plugin-managed items are tracked via `.claude/.skiller.json` (per project, grouped by agent skills dir)
|
|
612
615
|
|
|
613
616
|
### Skills Directory Structure
|
|
614
617
|
|
|
@@ -126,36 +126,11 @@ function parsePluginId(pluginId) {
|
|
|
126
126
|
marketplaceId: pluginId.slice(at + 1),
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
-
async function
|
|
130
|
-
const knownPath = path.join(claudeDir, 'plugins', 'known_marketplaces.json');
|
|
131
|
-
let raw;
|
|
132
|
-
try {
|
|
133
|
-
raw = JSON.parse(await fs.readFile(knownPath, 'utf8'));
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return {};
|
|
137
|
-
}
|
|
138
|
-
if (!raw || typeof raw !== 'object')
|
|
139
|
-
return {};
|
|
140
|
-
const obj = raw;
|
|
141
|
-
const out = {};
|
|
142
|
-
for (const [marketplaceId, marketplace] of Object.entries(obj)) {
|
|
143
|
-
if (!marketplace || typeof marketplace !== 'object')
|
|
144
|
-
continue;
|
|
145
|
-
const installLocation = marketplace
|
|
146
|
-
.installLocation;
|
|
147
|
-
if (typeof installLocation !== 'string' || installLocation.trim() === '')
|
|
148
|
-
continue;
|
|
149
|
-
out[marketplaceId] = installLocation;
|
|
150
|
-
}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
async function resolvePluginMarketplaceRoot(pluginId, claudeDir, knownMarketplaceInstallLocations) {
|
|
129
|
+
async function resolvePluginMarketplaceRoot(pluginId, claudeDir) {
|
|
154
130
|
const parsed = parsePluginId(pluginId);
|
|
155
131
|
if (!parsed)
|
|
156
132
|
return null;
|
|
157
|
-
const marketplaceRoot =
|
|
158
|
-
path.join(claudeDir, 'plugins', 'marketplaces', parsed.marketplaceId);
|
|
133
|
+
const marketplaceRoot = path.join(claudeDir, 'plugins', 'marketplaces', parsed.marketplaceId);
|
|
159
134
|
const candidates = [
|
|
160
135
|
path.join(marketplaceRoot, 'plugins', parsed.pluginName),
|
|
161
136
|
path.join(marketplaceRoot, 'external_plugins', parsed.pluginName),
|
|
@@ -266,19 +241,31 @@ async function discoverPluginCommandFiles(installPath) {
|
|
|
266
241
|
const commandsRoot = path.join(installPath, 'commands');
|
|
267
242
|
if (!(await fileExists(commandsRoot)))
|
|
268
243
|
return [];
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
244
|
+
const results = [];
|
|
245
|
+
async function walk(current, depth) {
|
|
246
|
+
if (depth >= constants_1.MAX_RECURSION_DEPTH)
|
|
247
|
+
return;
|
|
248
|
+
let entries;
|
|
249
|
+
try {
|
|
250
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
const full = path.join(current, entry.name);
|
|
257
|
+
if (entry.isDirectory()) {
|
|
258
|
+
await walk(full, depth + 1);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
262
|
+
continue;
|
|
263
|
+
const rel = path.relative(commandsRoot, full).replace(/\\/g, '/');
|
|
264
|
+
results.push({ rel, file: full });
|
|
265
|
+
}
|
|
275
266
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
.map((e) => ({
|
|
279
|
-
name: path.basename(e.name, '.md'),
|
|
280
|
-
file: path.join(commandsRoot, e.name),
|
|
281
|
-
}));
|
|
267
|
+
await walk(commandsRoot, 0);
|
|
268
|
+
return results;
|
|
282
269
|
}
|
|
283
270
|
async function discoverPluginAgentFiles(installPath) {
|
|
284
271
|
const agentsRoot = path.join(installPath, 'agents');
|
|
@@ -326,13 +313,44 @@ async function discoverPluginAgentFiles(installPath) {
|
|
|
326
313
|
function generateBaseNameFromRelId(relId) {
|
|
327
314
|
const normalized = relId.replace(/\\/g, '/');
|
|
328
315
|
const segments = normalized.split('/').filter(Boolean);
|
|
329
|
-
return segments.map(sanitizeId).join('
|
|
316
|
+
return segments.map(sanitizeId).join('-');
|
|
317
|
+
}
|
|
318
|
+
function generateBaseNameFromCommandRelPath(commandRelPath) {
|
|
319
|
+
const normalized = commandRelPath.replace(/\\/g, '/');
|
|
320
|
+
const withoutExt = normalized.endsWith('.md')
|
|
321
|
+
? normalized.slice(0, -'.md'.length)
|
|
322
|
+
: normalized;
|
|
323
|
+
const segments = withoutExt.split('/').filter(Boolean);
|
|
324
|
+
return segments.map(sanitizeId).join('-');
|
|
330
325
|
}
|
|
331
|
-
function
|
|
332
|
-
|
|
326
|
+
function pluginBaseNamespacePrefix(pluginId) {
|
|
327
|
+
const parsed = parsePluginId(pluginId);
|
|
328
|
+
if (parsed)
|
|
329
|
+
return sanitizeId(parsed.pluginName);
|
|
330
|
+
return sanitizeId(pluginId);
|
|
333
331
|
}
|
|
334
|
-
function
|
|
335
|
-
|
|
332
|
+
function computePluginNamespacePrefixes(pluginIds) {
|
|
333
|
+
const out = new Map();
|
|
334
|
+
const pluginIdsByBase = new Map();
|
|
335
|
+
for (const pluginId of pluginIds) {
|
|
336
|
+
const base = pluginBaseNamespacePrefix(pluginId);
|
|
337
|
+
const bucket = pluginIdsByBase.get(base) ?? [];
|
|
338
|
+
bucket.push(pluginId);
|
|
339
|
+
pluginIdsByBase.set(base, bucket);
|
|
340
|
+
}
|
|
341
|
+
for (const [base, ids] of pluginIdsByBase.entries()) {
|
|
342
|
+
ids.sort((a, b) => a.localeCompare(b));
|
|
343
|
+
if (ids.length === 1) {
|
|
344
|
+
out.set(ids[0], base);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
// If multiple enabled plugins share the same plugin name, disambiguate
|
|
348
|
+
// without including the marketplace in the folder name.
|
|
349
|
+
ids.forEach((id, idx) => {
|
|
350
|
+
out.set(id, idx === 0 ? base : `${base}-${idx + 1}`);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
336
354
|
}
|
|
337
355
|
async function readLegacyMarkerFile(dir) {
|
|
338
356
|
const markerPath = path.join(dir, LEGACY_MARKER_FILENAME);
|
|
@@ -375,8 +393,8 @@ async function removeLegacyMarkerFile(dir, dryRun) {
|
|
|
375
393
|
// ignore
|
|
376
394
|
}
|
|
377
395
|
}
|
|
378
|
-
async function loadManagedEntries(targetSkillsDir, dryRun) {
|
|
379
|
-
const allEntries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(targetSkillsDir);
|
|
396
|
+
async function loadManagedEntries(projectRoot, targetSkillsDir, dryRun) {
|
|
397
|
+
const allEntries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(projectRoot, targetSkillsDir);
|
|
380
398
|
const pluginEntries = [];
|
|
381
399
|
const otherEntries = [];
|
|
382
400
|
for (const entry of allEntries) {
|
|
@@ -439,7 +457,14 @@ async function discoverLocalSkillNames(projectRoot) {
|
|
|
439
457
|
}
|
|
440
458
|
const hasSkillMd = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
|
|
441
459
|
if (hasSkillMd) {
|
|
442
|
-
|
|
460
|
+
const rel = path.relative(localSkillsDir, current).replace(/\\/g, '/');
|
|
461
|
+
const segments = rel.split('/').filter(Boolean);
|
|
462
|
+
if (segments.length > 0) {
|
|
463
|
+
names.add(segments.map(sanitizeId).join('-'));
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
names.add(sanitizeId(path.basename(current)));
|
|
467
|
+
}
|
|
443
468
|
return;
|
|
444
469
|
}
|
|
445
470
|
for (const entry of entries) {
|
|
@@ -474,7 +499,12 @@ async function discoverLocalCommandNames(projectRoot) {
|
|
|
474
499
|
}
|
|
475
500
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
476
501
|
continue;
|
|
477
|
-
|
|
502
|
+
const rel = path.relative(localCommandsDir, full).replace(/\\/g, '/');
|
|
503
|
+
const withoutExt = rel.endsWith('.md')
|
|
504
|
+
? rel.slice(0, -'.md'.length)
|
|
505
|
+
: rel;
|
|
506
|
+
const segments = withoutExt.split('/').filter(Boolean);
|
|
507
|
+
names.add(segments.map(sanitizeId).join('-'));
|
|
478
508
|
}
|
|
479
509
|
}
|
|
480
510
|
await walk(localCommandsDir, 0);
|
|
@@ -581,7 +611,6 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
581
611
|
}
|
|
582
612
|
const claudeDir = path.join(getUserHomeDir(), '.claude');
|
|
583
613
|
const index = await readInstalledPluginsIndex(claudeDir);
|
|
584
|
-
const knownMarketplaceInstallLocations = await readKnownMarketplaceInstallLocations(claudeDir);
|
|
585
614
|
const localSkillNames = await discoverLocalSkillNames(projectRoot);
|
|
586
615
|
const localCommandNames = await discoverLocalCommandNames(projectRoot);
|
|
587
616
|
const localAgentNames = await discoverLocalAgentNames(projectRoot);
|
|
@@ -593,10 +622,16 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
593
622
|
const resolvedSources = [];
|
|
594
623
|
const unresolvedEnabled = new Set();
|
|
595
624
|
for (const pluginId of enabledPlugins) {
|
|
596
|
-
const pluginRoot = await resolvePluginMarketplaceRoot(pluginId, claudeDir
|
|
625
|
+
const pluginRoot = await resolvePluginMarketplaceRoot(pluginId, claudeDir);
|
|
597
626
|
if (!pluginRoot) {
|
|
598
627
|
unresolvedEnabled.add(pluginId);
|
|
599
|
-
|
|
628
|
+
const hasIndexEntry = Boolean(index?.plugins?.[pluginId]?.length);
|
|
629
|
+
if (hasIndexEntry) {
|
|
630
|
+
(0, constants_1.logVerboseInfo)(`[plugins] Enabled plugin has no marketplace content, skipping: ${pluginId}`, verbose, dryRun);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
(0, constants_1.logWarn)(`[plugins] Enabled plugin not installed: ${pluginId}`, dryRun);
|
|
634
|
+
}
|
|
600
635
|
continue;
|
|
601
636
|
}
|
|
602
637
|
const resolved = index
|
|
@@ -626,8 +661,8 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
626
661
|
}
|
|
627
662
|
const commandFiles = await discoverPluginCommandFiles(plugin.pluginRoot);
|
|
628
663
|
for (const c of commandFiles) {
|
|
629
|
-
const baseName =
|
|
630
|
-
const sourceRelPath = `commands/${
|
|
664
|
+
const baseName = generateBaseNameFromCommandRelPath(c.rel);
|
|
665
|
+
const sourceRelPath = `commands/${c.rel}`;
|
|
631
666
|
expectedItems.push({
|
|
632
667
|
itemKey: makeItemKey(plugin.pluginId, 'command', sourceRelPath),
|
|
633
668
|
pluginId: plugin.pluginId,
|
|
@@ -658,6 +693,9 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
658
693
|
const bk = `${b.baseName}::${b.pluginId}::${b.kind}::${b.sourceRelPath}`;
|
|
659
694
|
return ak.localeCompare(bk);
|
|
660
695
|
});
|
|
696
|
+
const pluginNamespacePrefixByPluginId = computePluginNamespacePrefixes([
|
|
697
|
+
...new Set(resolvedSources.map((p) => p.pluginId)),
|
|
698
|
+
]);
|
|
661
699
|
// Sync into each target skills dir.
|
|
662
700
|
for (const targetSkillsDir of targetSkillsDirs) {
|
|
663
701
|
const targetExists = await fileExists(targetSkillsDir);
|
|
@@ -666,7 +704,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
666
704
|
let managedEntries = [];
|
|
667
705
|
let otherEntries = [];
|
|
668
706
|
if (targetExists) {
|
|
669
|
-
const loaded = await loadManagedEntries(targetSkillsDir, dryRun);
|
|
707
|
+
const loaded = await loadManagedEntries(projectRoot, targetSkillsDir, dryRun);
|
|
670
708
|
managedEntries = loaded.pluginEntries;
|
|
671
709
|
otherEntries = loaded.otherEntries;
|
|
672
710
|
}
|
|
@@ -702,11 +740,17 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
702
740
|
const prev = prevDestByItemKey.get(item.itemKey);
|
|
703
741
|
if (!prev)
|
|
704
742
|
continue;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
//
|
|
743
|
+
const desiredPrefix = pluginNamespacePrefixByPluginId.get(item.pluginId) ??
|
|
744
|
+
sanitizeId(item.pluginId);
|
|
745
|
+
// Migration: previous versions used `${pluginId}__${name}`, then
|
|
746
|
+
// `${pluginId}-${name}`. If we changed the namespace prefix (for example
|
|
747
|
+
// to omit marketplace), don't preserve the old destination so the item
|
|
748
|
+
// can be renamed.
|
|
708
749
|
if (prev.startsWith(`${sanitizeId(item.pluginId)}__`))
|
|
709
750
|
continue;
|
|
751
|
+
if (prev.startsWith(`${sanitizeId(item.pluginId)}-`) &&
|
|
752
|
+
desiredPrefix !== sanitizeId(item.pluginId))
|
|
753
|
+
continue;
|
|
710
754
|
if (taken.has(prev))
|
|
711
755
|
continue;
|
|
712
756
|
assignedDestByItemKey.set(item.itemKey, prev);
|
|
@@ -722,7 +766,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
722
766
|
taken.add(base);
|
|
723
767
|
continue;
|
|
724
768
|
}
|
|
725
|
-
const namespacedBase =
|
|
769
|
+
const namespacedBase = `${pluginNamespacePrefixByPluginId.get(item.pluginId) ?? sanitizeId(item.pluginId)}-${base}`;
|
|
726
770
|
let candidate = namespacedBase;
|
|
727
771
|
let i = 2;
|
|
728
772
|
while (taken.has(candidate)) {
|
|
@@ -823,6 +867,6 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
823
867
|
destRelPath: item.destRelPath,
|
|
824
868
|
});
|
|
825
869
|
}
|
|
826
|
-
await (0, SkillsManifest_1.writeSkillsManifestEntries)(targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
870
|
+
await (0, SkillsManifest_1.writeSkillsManifestEntries)(projectRoot, targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
827
871
|
}
|
|
828
872
|
}
|
|
@@ -85,7 +85,14 @@ async function discoverCommandFiles(projectRoot) {
|
|
|
85
85
|
}
|
|
86
86
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
87
87
|
continue;
|
|
88
|
-
const
|
|
88
|
+
const relFromCommands = path
|
|
89
|
+
.relative(commandsRoot, full)
|
|
90
|
+
.replace(/\\/g, '/');
|
|
91
|
+
const withoutExt = relFromCommands.endsWith('.md')
|
|
92
|
+
? relFromCommands.slice(0, -'.md'.length)
|
|
93
|
+
: relFromCommands;
|
|
94
|
+
const segments = withoutExt.split('/').filter(Boolean);
|
|
95
|
+
const name = segments.map(sanitizeId).join('-');
|
|
89
96
|
const rel = path.relative(projectRoot, full).replace(/\\/g, '/');
|
|
90
97
|
results.push({ name, file: full, rel });
|
|
91
98
|
}
|
|
@@ -164,9 +171,9 @@ async function writeMarkdownAsSkill(srcPath, destDir, generatedName, kindLabel,
|
|
|
164
171
|
return;
|
|
165
172
|
await fs.writeFile(path.join(destDir, 'SKILL.md'), next, 'utf8');
|
|
166
173
|
}
|
|
167
|
-
async function readPluginManagedDestNames(targetSkillsDir) {
|
|
174
|
+
async function readPluginManagedDestNames(projectRoot, targetSkillsDir) {
|
|
168
175
|
const names = new Set();
|
|
169
|
-
for (const entry of await (0, SkillsManifest_1.loadSkillsManifestEntries)(targetSkillsDir)) {
|
|
176
|
+
for (const entry of await (0, SkillsManifest_1.loadSkillsManifestEntries)(projectRoot, targetSkillsDir)) {
|
|
170
177
|
if ((0, SkillsManifest_1.isPluginManifestEntry)(entry)) {
|
|
171
178
|
names.add(entry.destRelPath);
|
|
172
179
|
}
|
|
@@ -209,7 +216,14 @@ async function discoverLocalSkillNames(projectRoot) {
|
|
|
209
216
|
}
|
|
210
217
|
const hasSkillMd = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
|
|
211
218
|
if (hasSkillMd) {
|
|
212
|
-
|
|
219
|
+
const rel = path.relative(localSkillsDir, current).replace(/\\/g, '/');
|
|
220
|
+
const segments = rel.split('/').filter(Boolean);
|
|
221
|
+
if (segments.length > 0) {
|
|
222
|
+
names.add(segments.map(sanitizeId).join('-'));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
names.add(sanitizeId(path.basename(current)));
|
|
226
|
+
}
|
|
213
227
|
return;
|
|
214
228
|
}
|
|
215
229
|
for (const entry of entries) {
|
|
@@ -255,7 +269,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
|
|
|
255
269
|
const managedEntries = [];
|
|
256
270
|
const otherEntries = [];
|
|
257
271
|
if (targetExists) {
|
|
258
|
-
const allEntries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(targetSkillsDir);
|
|
272
|
+
const allEntries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(projectRoot, targetSkillsDir);
|
|
259
273
|
for (const entry of allEntries) {
|
|
260
274
|
if ((0, SkillsManifest_1.isClaudeManifestEntry)(entry)) {
|
|
261
275
|
managedEntries.push(entry);
|
|
@@ -271,7 +285,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
|
|
|
271
285
|
}
|
|
272
286
|
const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
|
|
273
287
|
const pluginManagedDest = targetExists
|
|
274
|
-
? await readPluginManagedDestNames(targetSkillsDir)
|
|
288
|
+
? await readPluginManagedDestNames(projectRoot, targetSkillsDir)
|
|
275
289
|
: new Set();
|
|
276
290
|
const reserved = new Set(localSkillNames);
|
|
277
291
|
if (targetExists) {
|
|
@@ -388,6 +402,6 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
|
|
|
388
402
|
destRelPath: item.destRelPath,
|
|
389
403
|
});
|
|
390
404
|
}
|
|
391
|
-
await (0, SkillsManifest_1.writeSkillsManifestEntries)(targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
405
|
+
await (0, SkillsManifest_1.writeSkillsManifestEntries)(projectRoot, targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
392
406
|
}
|
|
393
407
|
}
|
|
@@ -41,6 +41,8 @@ exports.writeSkillsManifestEntries = writeSkillsManifestEntries;
|
|
|
41
41
|
exports.listSkillDirectories = listSkillDirectories;
|
|
42
42
|
const fs = __importStar(require("fs/promises"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
|
+
// Project-level manifest (stored in `.claude/.skiller.json`) that tracks what
|
|
45
|
+
// Skiller installed into each target agent skills directory for this project.
|
|
44
46
|
exports.SKILLS_MANIFEST_FILENAME = '.skiller.json';
|
|
45
47
|
exports.LEGACY_UNIFIED_SKILLS_MANIFEST_FILENAME = '.skiller-skills.json';
|
|
46
48
|
exports.LEGACY_PLUGIN_MANIFEST_FILENAME = '.skiller-plugins.json';
|
|
@@ -137,6 +139,37 @@ function parseUnifiedEntries(raw) {
|
|
|
137
139
|
}
|
|
138
140
|
return entries;
|
|
139
141
|
}
|
|
142
|
+
function normalizePathForKey(p) {
|
|
143
|
+
return path.resolve(p).replace(/\\/g, '/');
|
|
144
|
+
}
|
|
145
|
+
function computeTargetKey(projectRoot, targetSkillsDir) {
|
|
146
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
147
|
+
const resolvedTarget = path.resolve(targetSkillsDir);
|
|
148
|
+
if (resolvedTarget === resolvedProjectRoot)
|
|
149
|
+
return '.';
|
|
150
|
+
if (resolvedTarget.startsWith(resolvedProjectRoot + path.sep)) {
|
|
151
|
+
return path
|
|
152
|
+
.relative(resolvedProjectRoot, resolvedTarget)
|
|
153
|
+
.replace(/\\/g, '/');
|
|
154
|
+
}
|
|
155
|
+
return normalizePathForKey(resolvedTarget);
|
|
156
|
+
}
|
|
157
|
+
function parseProjectTargets(raw) {
|
|
158
|
+
if (!raw || typeof raw !== 'object')
|
|
159
|
+
return {};
|
|
160
|
+
const obj = raw;
|
|
161
|
+
if (!obj.targets || typeof obj.targets !== 'object')
|
|
162
|
+
return {};
|
|
163
|
+
const targetsObj = obj.targets;
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const [targetKey, rawEntries] of Object.entries(targetsObj)) {
|
|
166
|
+
// Stored as an array of entries per target.
|
|
167
|
+
if (!Array.isArray(rawEntries))
|
|
168
|
+
continue;
|
|
169
|
+
out[targetKey] = normalizeEntries(parseUnifiedEntries({ entries: rawEntries }));
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
140
173
|
function parseLegacyPluginEntries(raw) {
|
|
141
174
|
if (!raw || typeof raw !== 'object')
|
|
142
175
|
return [];
|
|
@@ -197,11 +230,11 @@ function parseLegacyClaudeEntries(raw) {
|
|
|
197
230
|
}
|
|
198
231
|
return entries;
|
|
199
232
|
}
|
|
200
|
-
async function
|
|
201
|
-
const
|
|
202
|
-
if (await fileExists(
|
|
233
|
+
async function loadLegacyTargetSkillsManifestEntries(targetSkillsDir) {
|
|
234
|
+
const legacyManifestPath = path.join(targetSkillsDir, exports.SKILLS_MANIFEST_FILENAME);
|
|
235
|
+
if (await fileExists(legacyManifestPath)) {
|
|
203
236
|
try {
|
|
204
|
-
const raw = JSON.parse(await fs.readFile(
|
|
237
|
+
const raw = JSON.parse(await fs.readFile(legacyManifestPath, 'utf8'));
|
|
205
238
|
return normalizeEntries(parseUnifiedEntries(raw));
|
|
206
239
|
}
|
|
207
240
|
catch {
|
|
@@ -243,32 +276,78 @@ async function loadSkillsManifestEntries(targetSkillsDir) {
|
|
|
243
276
|
}
|
|
244
277
|
return normalizeEntries(merged);
|
|
245
278
|
}
|
|
246
|
-
async function
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
279
|
+
async function loadSkillsManifestEntries(projectRoot, targetSkillsDir) {
|
|
280
|
+
const projectClaudeDir = path.join(projectRoot, '.claude');
|
|
281
|
+
const projectManifestPath = path.join(projectClaudeDir, exports.SKILLS_MANIFEST_FILENAME);
|
|
282
|
+
const preferredTargetKey = computeTargetKey(projectRoot, targetSkillsDir);
|
|
283
|
+
const absoluteTargetKey = normalizePathForKey(targetSkillsDir);
|
|
284
|
+
if (await fileExists(projectManifestPath)) {
|
|
285
|
+
try {
|
|
286
|
+
const raw = JSON.parse(await fs.readFile(projectManifestPath, 'utf8'));
|
|
287
|
+
const targets = parseProjectTargets(raw);
|
|
288
|
+
const entries = targets[preferredTargetKey] ?? targets[absoluteTargetKey] ?? [];
|
|
289
|
+
return normalizeEntries(entries);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Legacy migration: prior versions stored manifests in the target skills dir.
|
|
296
|
+
return await loadLegacyTargetSkillsManifestEntries(targetSkillsDir);
|
|
297
|
+
}
|
|
298
|
+
async function writeSkillsManifestEntries(projectRoot, targetSkillsDir, entries, dryRun) {
|
|
251
299
|
const normalized = normalizeEntries(entries);
|
|
300
|
+
const projectClaudeDir = path.join(projectRoot, '.claude');
|
|
301
|
+
const projectManifestPath = path.join(projectClaudeDir, exports.SKILLS_MANIFEST_FILENAME);
|
|
302
|
+
const preferredTargetKey = computeTargetKey(projectRoot, targetSkillsDir);
|
|
303
|
+
const absoluteTargetKey = normalizePathForKey(targetSkillsDir);
|
|
304
|
+
let existingTargets = {};
|
|
305
|
+
if (await fileExists(projectManifestPath)) {
|
|
306
|
+
try {
|
|
307
|
+
const raw = JSON.parse(await fs.readFile(projectManifestPath, 'utf8'));
|
|
308
|
+
existingTargets = parseProjectTargets(raw);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
existingTargets = {};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
252
314
|
if (normalized.length === 0) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
315
|
+
delete existingTargets[preferredTargetKey];
|
|
316
|
+
if (preferredTargetKey !== absoluteTargetKey) {
|
|
317
|
+
delete existingTargets[absoluteTargetKey];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
existingTargets[preferredTargetKey] = normalized;
|
|
322
|
+
if (preferredTargetKey !== absoluteTargetKey) {
|
|
323
|
+
delete existingTargets[absoluteTargetKey];
|
|
324
|
+
}
|
|
262
325
|
}
|
|
263
|
-
const manifest = {
|
|
264
|
-
version: exports.SKILLS_MANIFEST_VERSION,
|
|
265
|
-
entries: normalized,
|
|
266
|
-
};
|
|
267
326
|
if (dryRun)
|
|
268
327
|
return;
|
|
269
|
-
|
|
270
|
-
|
|
328
|
+
// Ensure `.claude` exists since the manifest lives there.
|
|
329
|
+
await fs.mkdir(projectClaudeDir, { recursive: true });
|
|
330
|
+
const targetKeys = Object.keys(existingTargets).sort((a, b) => a.localeCompare(b));
|
|
331
|
+
if (targetKeys.length === 0) {
|
|
332
|
+
await Promise.allSettled([fs.unlink(projectManifestPath)]);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const nextTargets = {};
|
|
336
|
+
for (const key of targetKeys)
|
|
337
|
+
nextTargets[key] = existingTargets[key];
|
|
338
|
+
const manifest = {
|
|
339
|
+
version: exports.SKILLS_MANIFEST_VERSION,
|
|
340
|
+
targets: nextTargets,
|
|
341
|
+
};
|
|
342
|
+
await fs.writeFile(projectManifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
343
|
+
}
|
|
344
|
+
// Remove legacy per-target manifests so users see a single `.claude/.skiller.json`.
|
|
345
|
+
const legacyManifestPath = path.join(targetSkillsDir, exports.SKILLS_MANIFEST_FILENAME);
|
|
346
|
+
const legacyUnifiedPath = path.join(targetSkillsDir, exports.LEGACY_UNIFIED_SKILLS_MANIFEST_FILENAME);
|
|
347
|
+
const legacyPluginPath = path.join(targetSkillsDir, exports.LEGACY_PLUGIN_MANIFEST_FILENAME);
|
|
348
|
+
const legacyClaudePath = path.join(targetSkillsDir, exports.LEGACY_CLAUDE_MANIFEST_FILENAME);
|
|
271
349
|
await Promise.allSettled([
|
|
350
|
+
fs.unlink(legacyManifestPath),
|
|
272
351
|
fs.unlink(legacyUnifiedPath),
|
|
273
352
|
fs.unlink(legacyPluginPath),
|
|
274
353
|
fs.unlink(legacyClaudePath),
|
|
@@ -462,6 +462,32 @@ async function discoverSkills(projectRoot, skillerDir) {
|
|
|
462
462
|
async function copySkillsToAgent(sourceSkillsDir, targetSkillsDir, projectRoot, verbose, dryRun) {
|
|
463
463
|
const warnings = [];
|
|
464
464
|
let copied = 0;
|
|
465
|
+
function sanitizeId(value) {
|
|
466
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, '_');
|
|
467
|
+
}
|
|
468
|
+
function flattenRelativeSkillPath(relativeSkillPath) {
|
|
469
|
+
const normalized = relativeSkillPath.replace(/\\/g, '/');
|
|
470
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
471
|
+
return segments.map(sanitizeId).join('-');
|
|
472
|
+
}
|
|
473
|
+
async function rewriteSkillMdName(skillMdPath, name) {
|
|
474
|
+
let content;
|
|
475
|
+
try {
|
|
476
|
+
content = await fs.readFile(skillMdPath, 'utf8');
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const { rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(content);
|
|
482
|
+
const fm = rawFrontmatter
|
|
483
|
+
? { ...rawFrontmatter }
|
|
484
|
+
: {};
|
|
485
|
+
fm.name = name;
|
|
486
|
+
const next = `---\n${yaml
|
|
487
|
+
.dump(fm, { lineWidth: -1, noRefs: true })
|
|
488
|
+
.trim()}\n---\n\n${body}\n`;
|
|
489
|
+
await fs.writeFile(skillMdPath, next, 'utf8');
|
|
490
|
+
}
|
|
465
491
|
try {
|
|
466
492
|
await fs.access(sourceSkillsDir);
|
|
467
493
|
}
|
|
@@ -471,8 +497,15 @@ async function copySkillsToAgent(sourceSkillsDir, targetSkillsDir, projectRoot,
|
|
|
471
497
|
}
|
|
472
498
|
// Use walkSkillsTree to discover skills
|
|
473
499
|
const skillsTree = await (0, SkillsUtils_1.walkSkillsTree)(sourceSkillsDir);
|
|
500
|
+
// Deterministic order so name collision suffixing is stable.
|
|
501
|
+
const sortedSkills = [...skillsTree.skills].sort((a, b) => {
|
|
502
|
+
const ar = path.relative(sourceSkillsDir, a.path).replace(/\\/g, '/');
|
|
503
|
+
const br = path.relative(sourceSkillsDir, b.path).replace(/\\/g, '/');
|
|
504
|
+
return ar.localeCompare(br);
|
|
505
|
+
});
|
|
506
|
+
const taken = new Set();
|
|
474
507
|
// Validate and copy each skill
|
|
475
|
-
for (const skill of
|
|
508
|
+
for (const skill of sortedSkills) {
|
|
476
509
|
// skill.path is absolute, use it directly
|
|
477
510
|
const skillPath = skill.path;
|
|
478
511
|
const skillMdPath = path.join(skillPath, constants_1.SKILL_MD_FILENAME);
|
|
@@ -484,11 +517,23 @@ async function copySkillsToAgent(sourceSkillsDir, targetSkillsDir, projectRoot,
|
|
|
484
517
|
warnings.push(`Skill '${skill.name}' missing required SKILL.md file, skipping`);
|
|
485
518
|
continue;
|
|
486
519
|
}
|
|
487
|
-
//
|
|
520
|
+
// Flatten nested skills into root-level skill folders for other agents:
|
|
521
|
+
// `category/foo` -> `category-foo`
|
|
488
522
|
const relativeSkillPath = path.relative(sourceSkillsDir, skill.path);
|
|
489
|
-
const
|
|
523
|
+
const baseDestName = flattenRelativeSkillPath(relativeSkillPath);
|
|
524
|
+
let destName = baseDestName;
|
|
525
|
+
let i = 2;
|
|
526
|
+
while (taken.has(destName)) {
|
|
527
|
+
destName = `${baseDestName}-${i++}`;
|
|
528
|
+
}
|
|
529
|
+
taken.add(destName);
|
|
530
|
+
const targetSkillPath = path.join(targetSkillsDir, destName);
|
|
490
531
|
if (!dryRun) {
|
|
491
532
|
await copySkillDirectoryForNonClaudeAgents(skillPath, targetSkillPath, projectRoot, skillPath);
|
|
533
|
+
const sourceLeafName = path.basename(skillPath);
|
|
534
|
+
if (destName !== sourceLeafName) {
|
|
535
|
+
await rewriteSkillMdName(path.join(targetSkillPath, constants_1.SKILL_MD_FILENAME), destName);
|
|
536
|
+
}
|
|
492
537
|
}
|
|
493
538
|
(0, constants_1.logVerboseInfo)(dryRun
|
|
494
539
|
? `DRY RUN: Would copy skill '${skill.name}' to ${targetSkillsDir}`
|