skiller 0.7.17 → 0.7.19
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 +5 -3
- package/dist/core/ClaudePluginSync.js +121 -62
- package/dist/core/ClaudeProjectSync.js +8 -1
- package/dist/core/SkillsProcessor.js +48 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,15 +53,17 @@ 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/`)
|
|
62
|
+
- Syncs enabled plugin `skills/` into agent skills directories on `skiller apply` (recursive, flattened names)
|
|
61
63
|
- Syncs enabled plugin `commands/*.md` as skills (`SKILL.md`) into agent skills directories
|
|
62
64
|
- Syncs enabled plugin `agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
|
|
63
65
|
- Uses the skill/command/agent name by default (matches existing Codex skill names)
|
|
64
|
-
- If a name conflicts, local skills win and the plugin skill is namespaced as `<
|
|
66
|
+
- If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginName>-<name>` (marketplace only when needed)
|
|
65
67
|
- Tracks plugin-managed skills in a single `.skiller.json` file per agent skills directory
|
|
66
68
|
- Removes stale plugin skills when plugins are disabled
|
|
67
69
|
|
|
@@ -606,7 +608,7 @@ If your project enables Claude Code plugins in `.claude/settings.json`, Skiller
|
|
|
606
608
|
- Plugin `skills/` are copied as skills
|
|
607
609
|
- Plugin `commands/*.md` are converted into skills (`SKILL.md`)
|
|
608
610
|
- Plugin skills use their original skill/command name by default
|
|
609
|
-
- If a name conflicts, local skills win and the plugin skill is namespaced as `<
|
|
611
|
+
- If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginName>-<name>` (marketplace only when needed)
|
|
610
612
|
- Plugin-managed skills are tracked via `.skiller.json` in each agent skills directory
|
|
611
613
|
|
|
612
614
|
### Skills Directory Structure
|
|
@@ -71,6 +71,14 @@ async function fileExists(p) {
|
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
async function dirExists(p) {
|
|
75
|
+
try {
|
|
76
|
+
return (await fs.stat(p)).isDirectory();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
74
82
|
function makeItemKey(pluginId, sourceKind, sourceRelPath) {
|
|
75
83
|
return `${pluginId}::${sourceKind}::${sourceRelPath}`;
|
|
76
84
|
}
|
|
@@ -107,6 +115,59 @@ async function readInstalledPluginsIndex(claudeDir) {
|
|
|
107
115
|
return null;
|
|
108
116
|
}
|
|
109
117
|
}
|
|
118
|
+
function parsePluginId(pluginId) {
|
|
119
|
+
// `pluginId` format is typically `<pluginName>@<marketplaceId>`.
|
|
120
|
+
// Split by the last `@` so scoped names like `@org/foo@market` work.
|
|
121
|
+
const at = pluginId.lastIndexOf('@');
|
|
122
|
+
if (at <= 0 || at === pluginId.length - 1)
|
|
123
|
+
return null;
|
|
124
|
+
return {
|
|
125
|
+
pluginName: pluginId.slice(0, at),
|
|
126
|
+
marketplaceId: pluginId.slice(at + 1),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function readKnownMarketplaceInstallLocations(claudeDir) {
|
|
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) {
|
|
154
|
+
const parsed = parsePluginId(pluginId);
|
|
155
|
+
if (!parsed)
|
|
156
|
+
return null;
|
|
157
|
+
const marketplaceRoot = knownMarketplaceInstallLocations[parsed.marketplaceId] ??
|
|
158
|
+
path.join(claudeDir, 'plugins', 'marketplaces', parsed.marketplaceId);
|
|
159
|
+
const candidates = [
|
|
160
|
+
path.join(marketplaceRoot, 'plugins', parsed.pluginName),
|
|
161
|
+
path.join(marketplaceRoot, 'external_plugins', parsed.pluginName),
|
|
162
|
+
path.join(marketplaceRoot, '.claude-plugin', 'plugins', parsed.pluginName),
|
|
163
|
+
path.join(marketplaceRoot, '.claude-plugin', 'external_plugins', parsed.pluginName),
|
|
164
|
+
];
|
|
165
|
+
for (const candidate of candidates) {
|
|
166
|
+
if (await dirExists(candidate))
|
|
167
|
+
return candidate;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
110
171
|
function resolvePluginInstall(pluginId, projectRoot, index) {
|
|
111
172
|
const entries = index.plugins?.[pluginId];
|
|
112
173
|
if (!entries || !Array.isArray(entries) || entries.length === 0)
|
|
@@ -265,13 +326,33 @@ async function discoverPluginAgentFiles(installPath) {
|
|
|
265
326
|
function generateBaseNameFromRelId(relId) {
|
|
266
327
|
const normalized = relId.replace(/\\/g, '/');
|
|
267
328
|
const segments = normalized.split('/').filter(Boolean);
|
|
268
|
-
return segments.map(sanitizeId).join('
|
|
329
|
+
return segments.map(sanitizeId).join('-');
|
|
269
330
|
}
|
|
270
331
|
function generateBaseNameFromCommand(commandName) {
|
|
271
332
|
return sanitizeId(commandName);
|
|
272
333
|
}
|
|
273
|
-
function
|
|
274
|
-
|
|
334
|
+
function pluginBaseNamespacePrefix(pluginId) {
|
|
335
|
+
const parsed = parsePluginId(pluginId);
|
|
336
|
+
if (parsed)
|
|
337
|
+
return sanitizeId(parsed.pluginName);
|
|
338
|
+
return sanitizeId(pluginId);
|
|
339
|
+
}
|
|
340
|
+
function computePluginNamespacePrefixes(pluginIds) {
|
|
341
|
+
const basePrefixByPluginId = new Map();
|
|
342
|
+
const counts = new Map();
|
|
343
|
+
for (const pluginId of pluginIds) {
|
|
344
|
+
const base = pluginBaseNamespacePrefix(pluginId);
|
|
345
|
+
basePrefixByPluginId.set(pluginId, base);
|
|
346
|
+
counts.set(base, (counts.get(base) ?? 0) + 1);
|
|
347
|
+
}
|
|
348
|
+
const out = new Map();
|
|
349
|
+
for (const pluginId of pluginIds) {
|
|
350
|
+
const base = basePrefixByPluginId.get(pluginId) ?? sanitizeId(pluginId);
|
|
351
|
+
// If multiple enabled plugins share the same pluginName, fall back to the
|
|
352
|
+
// full pluginId for uniqueness (includes marketplace).
|
|
353
|
+
out.set(pluginId, (counts.get(base) ?? 0) > 1 ? sanitizeId(pluginId) : base);
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
275
356
|
}
|
|
276
357
|
async function readLegacyMarkerFile(dir) {
|
|
277
358
|
const markerPath = path.join(dir, LEGACY_MARKER_FILENAME);
|
|
@@ -378,7 +459,14 @@ async function discoverLocalSkillNames(projectRoot) {
|
|
|
378
459
|
}
|
|
379
460
|
const hasSkillMd = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
|
|
380
461
|
if (hasSkillMd) {
|
|
381
|
-
|
|
462
|
+
const rel = path.relative(localSkillsDir, current).replace(/\\/g, '/');
|
|
463
|
+
const segments = rel.split('/').filter(Boolean);
|
|
464
|
+
if (segments.length > 0) {
|
|
465
|
+
names.add(segments.map(sanitizeId).join('-'));
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
names.add(sanitizeId(path.basename(current)));
|
|
469
|
+
}
|
|
382
470
|
return;
|
|
383
471
|
}
|
|
384
472
|
for (const entry of entries) {
|
|
@@ -520,6 +608,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
520
608
|
}
|
|
521
609
|
const claudeDir = path.join(getUserHomeDir(), '.claude');
|
|
522
610
|
const index = await readInstalledPluginsIndex(claudeDir);
|
|
611
|
+
const knownMarketplaceInstallLocations = await readKnownMarketplaceInstallLocations(claudeDir);
|
|
523
612
|
const localSkillNames = await discoverLocalSkillNames(projectRoot);
|
|
524
613
|
const localCommandNames = await discoverLocalCommandNames(projectRoot);
|
|
525
614
|
const localAgentNames = await discoverLocalAgentNames(projectRoot);
|
|
@@ -528,66 +617,27 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
528
617
|
...localCommandNames,
|
|
529
618
|
...localAgentNames,
|
|
530
619
|
]);
|
|
531
|
-
|
|
532
|
-
// anything, but we can still clean up managed folders for plugins that
|
|
533
|
-
// are no longer enabled.
|
|
534
|
-
if (!index) {
|
|
535
|
-
for (const targetSkillsDir of targetSkillsDirs) {
|
|
536
|
-
if (!(await fileExists(targetSkillsDir)))
|
|
537
|
-
continue;
|
|
538
|
-
const { pluginEntries: managedEntries, otherEntries } = await loadManagedEntries(targetSkillsDir, dryRun);
|
|
539
|
-
const nextEntries = [];
|
|
540
|
-
// Build reserved set (local skills always win).
|
|
541
|
-
const reserved = new Set(localReservedNames);
|
|
542
|
-
// Also reserve any existing non-managed directories.
|
|
543
|
-
const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
|
|
544
|
-
let dirents = [];
|
|
545
|
-
try {
|
|
546
|
-
dirents = await fs.readdir(targetSkillsDir, { withFileTypes: true });
|
|
547
|
-
}
|
|
548
|
-
catch {
|
|
549
|
-
// ignore
|
|
550
|
-
}
|
|
551
|
-
for (const d of dirents) {
|
|
552
|
-
if (!d.isDirectory())
|
|
553
|
-
continue;
|
|
554
|
-
if (!managedDest.has(d.name)) {
|
|
555
|
-
reserved.add(d.name);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
for (const entry of managedEntries) {
|
|
559
|
-
const isEnabled = enabledPlugins.includes(entry.pluginId);
|
|
560
|
-
if (!isEnabled) {
|
|
561
|
-
if (reserved.has(entry.destRelPath)) {
|
|
562
|
-
// Local took over the folder name; stop managing it but don't delete.
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
(0, constants_1.logVerboseInfo)(dryRun
|
|
566
|
-
? `DRY RUN: Would remove stale plugin skill '${entry.destRelPath}' from ${targetSkillsDir}`
|
|
567
|
-
: `Removing stale plugin skill '${entry.destRelPath}' from ${targetSkillsDir}`, verbose, dryRun);
|
|
568
|
-
await removeDir(path.join(targetSkillsDir, entry.destRelPath), dryRun);
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
nextEntries.push(entry);
|
|
572
|
-
}
|
|
573
|
-
await (0, SkillsManifest_1.writeSkillsManifestEntries)(targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
574
|
-
}
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
const resolvedInstalls = [];
|
|
620
|
+
const resolvedSources = [];
|
|
578
621
|
const unresolvedEnabled = new Set();
|
|
579
622
|
for (const pluginId of enabledPlugins) {
|
|
580
|
-
const
|
|
581
|
-
if (!
|
|
623
|
+
const pluginRoot = await resolvePluginMarketplaceRoot(pluginId, claudeDir, knownMarketplaceInstallLocations);
|
|
624
|
+
if (!pluginRoot) {
|
|
582
625
|
unresolvedEnabled.add(pluginId);
|
|
583
626
|
(0, constants_1.logWarn)(`[plugins] Enabled plugin not installed: ${pluginId}`, dryRun);
|
|
584
627
|
continue;
|
|
585
628
|
}
|
|
586
|
-
|
|
629
|
+
const resolved = index
|
|
630
|
+
? resolvePluginInstall(pluginId, projectRoot, index)
|
|
631
|
+
: null;
|
|
632
|
+
resolvedSources.push({
|
|
633
|
+
pluginId,
|
|
634
|
+
pluginRoot,
|
|
635
|
+
version: resolved?.version,
|
|
636
|
+
});
|
|
587
637
|
}
|
|
588
638
|
const expectedItems = [];
|
|
589
|
-
for (const plugin of
|
|
590
|
-
const skillDirs = await discoverPluginSkillDirs(plugin.
|
|
639
|
+
for (const plugin of resolvedSources) {
|
|
640
|
+
const skillDirs = await discoverPluginSkillDirs(plugin.pluginRoot);
|
|
591
641
|
for (const s of skillDirs) {
|
|
592
642
|
const baseName = generateBaseNameFromRelId(s.relId);
|
|
593
643
|
const sourceRelPath = `skills/${s.relId}`;
|
|
@@ -601,7 +651,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
601
651
|
baseName,
|
|
602
652
|
});
|
|
603
653
|
}
|
|
604
|
-
const commandFiles = await discoverPluginCommandFiles(plugin.
|
|
654
|
+
const commandFiles = await discoverPluginCommandFiles(plugin.pluginRoot);
|
|
605
655
|
for (const c of commandFiles) {
|
|
606
656
|
const baseName = generateBaseNameFromCommand(c.name);
|
|
607
657
|
const sourceRelPath = `commands/${path.basename(c.file)}`;
|
|
@@ -615,7 +665,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
615
665
|
baseName,
|
|
616
666
|
});
|
|
617
667
|
}
|
|
618
|
-
const agentFiles = await discoverPluginAgentFiles(plugin.
|
|
668
|
+
const agentFiles = await discoverPluginAgentFiles(plugin.pluginRoot);
|
|
619
669
|
for (const a of agentFiles) {
|
|
620
670
|
const baseName = sanitizeId(a.name);
|
|
621
671
|
const sourceRelPath = `agents/${a.rel}`;
|
|
@@ -635,6 +685,9 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
635
685
|
const bk = `${b.baseName}::${b.pluginId}::${b.kind}::${b.sourceRelPath}`;
|
|
636
686
|
return ak.localeCompare(bk);
|
|
637
687
|
});
|
|
688
|
+
const pluginNamespacePrefixByPluginId = computePluginNamespacePrefixes([
|
|
689
|
+
...new Set(resolvedSources.map((p) => p.pluginId)),
|
|
690
|
+
]);
|
|
638
691
|
// Sync into each target skills dir.
|
|
639
692
|
for (const targetSkillsDir of targetSkillsDirs) {
|
|
640
693
|
const targetExists = await fileExists(targetSkillsDir);
|
|
@@ -679,11 +732,17 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
679
732
|
const prev = prevDestByItemKey.get(item.itemKey);
|
|
680
733
|
if (!prev)
|
|
681
734
|
continue;
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
//
|
|
735
|
+
const desiredPrefix = pluginNamespacePrefixByPluginId.get(item.pluginId) ??
|
|
736
|
+
sanitizeId(item.pluginId);
|
|
737
|
+
// Migration: previous versions used `${pluginId}__${name}`, then
|
|
738
|
+
// `${pluginId}-${name}`. If we changed the namespace prefix (for example
|
|
739
|
+
// to omit marketplace), don't preserve the old destination so the item
|
|
740
|
+
// can be renamed.
|
|
685
741
|
if (prev.startsWith(`${sanitizeId(item.pluginId)}__`))
|
|
686
742
|
continue;
|
|
743
|
+
if (prev.startsWith(`${sanitizeId(item.pluginId)}-`) &&
|
|
744
|
+
desiredPrefix !== sanitizeId(item.pluginId))
|
|
745
|
+
continue;
|
|
687
746
|
if (taken.has(prev))
|
|
688
747
|
continue;
|
|
689
748
|
assignedDestByItemKey.set(item.itemKey, prev);
|
|
@@ -699,7 +758,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
699
758
|
taken.add(base);
|
|
700
759
|
continue;
|
|
701
760
|
}
|
|
702
|
-
const namespacedBase =
|
|
761
|
+
const namespacedBase = `${pluginNamespacePrefixByPluginId.get(item.pluginId) ?? sanitizeId(item.pluginId)}-${base}`;
|
|
703
762
|
let candidate = namespacedBase;
|
|
704
763
|
let i = 2;
|
|
705
764
|
while (taken.has(candidate)) {
|
|
@@ -209,7 +209,14 @@ async function discoverLocalSkillNames(projectRoot) {
|
|
|
209
209
|
}
|
|
210
210
|
const hasSkillMd = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
|
|
211
211
|
if (hasSkillMd) {
|
|
212
|
-
|
|
212
|
+
const rel = path.relative(localSkillsDir, current).replace(/\\/g, '/');
|
|
213
|
+
const segments = rel.split('/').filter(Boolean);
|
|
214
|
+
if (segments.length > 0) {
|
|
215
|
+
names.add(segments.map(sanitizeId).join('-'));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
names.add(sanitizeId(path.basename(current)));
|
|
219
|
+
}
|
|
213
220
|
return;
|
|
214
221
|
}
|
|
215
222
|
for (const entry of entries) {
|
|
@@ -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}`
|