skiller 0.7.19 → 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 +9 -7
- package/dist/core/ClaudePluginSync.js +70 -62
- package/dist/core/ClaudeProjectSync.js +13 -6
- package/dist/core/SkillsManifest.js +103 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,21 +60,22 @@ A Claude-centric fork of [ruler](https://github.com/intellectronica/ruler) with
|
|
|
60
60
|
- Reads `.claude/settings.json` `enabledPlugins`
|
|
61
61
|
- Reads plugin content from `~/.claude/plugins/marketplaces` (never from `cache/`)
|
|
62
62
|
- Syncs enabled plugin `skills/` into agent skills directories on `skiller apply` (recursive, flattened names)
|
|
63
|
-
- Syncs enabled plugin `commands
|
|
63
|
+
- Syncs enabled plugin `commands/**/*.md` as skills (`SKILL.md`) into agent skills directories (recursive, flattened names)
|
|
64
64
|
- Syncs enabled plugin `agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
|
|
65
65
|
- Uses the skill/command/agent name by default (matches existing Codex skill names)
|
|
66
|
-
- If a name conflicts, local skills win and the plugin
|
|
67
|
-
- 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)
|
|
68
68
|
- Removes stale plugin skills when plugins are disabled
|
|
69
69
|
|
|
70
70
|
## 10. Claude Commands/Agents → Skills
|
|
71
71
|
|
|
72
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`)
|
|
73
74
|
- Syncs `.claude/agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
|
|
74
75
|
- Uses the command/agent name by default
|
|
75
76
|
- If a name conflicts, existing local/manual skills win and the project item is namespaced as `claude-<name>`
|
|
76
77
|
- Project items win over plugin skills/commands/agents on name conflicts
|
|
77
|
-
- Tracks project-managed items in
|
|
78
|
+
- Tracks project-managed items in `.claude/.skiller.json` (per project, grouped by agent skills dir)
|
|
78
79
|
|
|
79
80
|
---
|
|
80
81
|
|
|
@@ -606,10 +607,11 @@ Shared paths are deduplicated — agents sharing the same directory only trigger
|
|
|
606
607
|
If your project enables Claude Code plugins in `.claude/settings.json`, Skiller also syncs plugin content into agent skills directories on `skiller apply`:
|
|
607
608
|
|
|
608
609
|
- Plugin `skills/` are copied as skills
|
|
609
|
-
- Plugin `commands
|
|
610
|
+
- Plugin `commands/**/*.md` are converted into skills (`SKILL.md`)
|
|
611
|
+
- Plugin `agents/**/*.md` are converted into skills (`SKILL.md`)
|
|
610
612
|
- Plugin skills use their original skill/command name by default
|
|
611
|
-
- If a name conflicts, local skills win and the plugin
|
|
612
|
-
- 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)
|
|
613
615
|
|
|
614
616
|
### Skills Directory Structure
|
|
615
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');
|
|
@@ -328,8 +315,13 @@ function generateBaseNameFromRelId(relId) {
|
|
|
328
315
|
const segments = normalized.split('/').filter(Boolean);
|
|
329
316
|
return segments.map(sanitizeId).join('-');
|
|
330
317
|
}
|
|
331
|
-
function
|
|
332
|
-
|
|
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('-');
|
|
333
325
|
}
|
|
334
326
|
function pluginBaseNamespacePrefix(pluginId) {
|
|
335
327
|
const parsed = parsePluginId(pluginId);
|
|
@@ -338,19 +330,25 @@ function pluginBaseNamespacePrefix(pluginId) {
|
|
|
338
330
|
return sanitizeId(pluginId);
|
|
339
331
|
}
|
|
340
332
|
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
333
|
const out = new Map();
|
|
334
|
+
const pluginIdsByBase = new Map();
|
|
349
335
|
for (const pluginId of pluginIds) {
|
|
350
|
-
const base =
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
+
});
|
|
354
352
|
}
|
|
355
353
|
return out;
|
|
356
354
|
}
|
|
@@ -395,8 +393,8 @@ async function removeLegacyMarkerFile(dir, dryRun) {
|
|
|
395
393
|
// ignore
|
|
396
394
|
}
|
|
397
395
|
}
|
|
398
|
-
async function loadManagedEntries(targetSkillsDir, dryRun) {
|
|
399
|
-
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);
|
|
400
398
|
const pluginEntries = [];
|
|
401
399
|
const otherEntries = [];
|
|
402
400
|
for (const entry of allEntries) {
|
|
@@ -501,7 +499,12 @@ async function discoverLocalCommandNames(projectRoot) {
|
|
|
501
499
|
}
|
|
502
500
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
503
501
|
continue;
|
|
504
|
-
|
|
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('-'));
|
|
505
508
|
}
|
|
506
509
|
}
|
|
507
510
|
await walk(localCommandsDir, 0);
|
|
@@ -608,7 +611,6 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
608
611
|
}
|
|
609
612
|
const claudeDir = path.join(getUserHomeDir(), '.claude');
|
|
610
613
|
const index = await readInstalledPluginsIndex(claudeDir);
|
|
611
|
-
const knownMarketplaceInstallLocations = await readKnownMarketplaceInstallLocations(claudeDir);
|
|
612
614
|
const localSkillNames = await discoverLocalSkillNames(projectRoot);
|
|
613
615
|
const localCommandNames = await discoverLocalCommandNames(projectRoot);
|
|
614
616
|
const localAgentNames = await discoverLocalAgentNames(projectRoot);
|
|
@@ -620,10 +622,16 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
620
622
|
const resolvedSources = [];
|
|
621
623
|
const unresolvedEnabled = new Set();
|
|
622
624
|
for (const pluginId of enabledPlugins) {
|
|
623
|
-
const pluginRoot = await resolvePluginMarketplaceRoot(pluginId, claudeDir
|
|
625
|
+
const pluginRoot = await resolvePluginMarketplaceRoot(pluginId, claudeDir);
|
|
624
626
|
if (!pluginRoot) {
|
|
625
627
|
unresolvedEnabled.add(pluginId);
|
|
626
|
-
|
|
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
|
+
}
|
|
627
635
|
continue;
|
|
628
636
|
}
|
|
629
637
|
const resolved = index
|
|
@@ -653,8 +661,8 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
653
661
|
}
|
|
654
662
|
const commandFiles = await discoverPluginCommandFiles(plugin.pluginRoot);
|
|
655
663
|
for (const c of commandFiles) {
|
|
656
|
-
const baseName =
|
|
657
|
-
const sourceRelPath = `commands/${
|
|
664
|
+
const baseName = generateBaseNameFromCommandRelPath(c.rel);
|
|
665
|
+
const sourceRelPath = `commands/${c.rel}`;
|
|
658
666
|
expectedItems.push({
|
|
659
667
|
itemKey: makeItemKey(plugin.pluginId, 'command', sourceRelPath),
|
|
660
668
|
pluginId: plugin.pluginId,
|
|
@@ -696,7 +704,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
696
704
|
let managedEntries = [];
|
|
697
705
|
let otherEntries = [];
|
|
698
706
|
if (targetExists) {
|
|
699
|
-
const loaded = await loadManagedEntries(targetSkillsDir, dryRun);
|
|
707
|
+
const loaded = await loadManagedEntries(projectRoot, targetSkillsDir, dryRun);
|
|
700
708
|
managedEntries = loaded.pluginEntries;
|
|
701
709
|
otherEntries = loaded.otherEntries;
|
|
702
710
|
}
|
|
@@ -859,6 +867,6 @@ async function syncClaudePluginsToSkillsDirs(args) {
|
|
|
859
867
|
destRelPath: item.destRelPath,
|
|
860
868
|
});
|
|
861
869
|
}
|
|
862
|
-
await (0, SkillsManifest_1.writeSkillsManifestEntries)(targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
870
|
+
await (0, SkillsManifest_1.writeSkillsManifestEntries)(projectRoot, targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
863
871
|
}
|
|
864
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
|
}
|
|
@@ -262,7 +269,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
|
|
|
262
269
|
const managedEntries = [];
|
|
263
270
|
const otherEntries = [];
|
|
264
271
|
if (targetExists) {
|
|
265
|
-
const allEntries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(targetSkillsDir);
|
|
272
|
+
const allEntries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(projectRoot, targetSkillsDir);
|
|
266
273
|
for (const entry of allEntries) {
|
|
267
274
|
if ((0, SkillsManifest_1.isClaudeManifestEntry)(entry)) {
|
|
268
275
|
managedEntries.push(entry);
|
|
@@ -278,7 +285,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
|
|
|
278
285
|
}
|
|
279
286
|
const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
|
|
280
287
|
const pluginManagedDest = targetExists
|
|
281
|
-
? await readPluginManagedDestNames(targetSkillsDir)
|
|
288
|
+
? await readPluginManagedDestNames(projectRoot, targetSkillsDir)
|
|
282
289
|
: new Set();
|
|
283
290
|
const reserved = new Set(localSkillNames);
|
|
284
291
|
if (targetExists) {
|
|
@@ -395,6 +402,6 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
|
|
|
395
402
|
destRelPath: item.destRelPath,
|
|
396
403
|
});
|
|
397
404
|
}
|
|
398
|
-
await (0, SkillsManifest_1.writeSkillsManifestEntries)(targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
405
|
+
await (0, SkillsManifest_1.writeSkillsManifestEntries)(projectRoot, targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
|
|
399
406
|
}
|
|
400
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),
|