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 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/*.md` as skills (`SKILL.md`) into agent skills directories
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 skill is namespaced as `<pluginName>-<name>` (marketplace only when needed)
67
- - Tracks plugin-managed skills in a single `.skiller.json` file per agent skills directory
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 a single `.skiller.json` file per agent skills directory
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/*.md` are converted into skills (`SKILL.md`)
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 skill is namespaced as `<pluginName>-<name>` (marketplace only when needed)
612
- - Plugin-managed skills are tracked via `.skiller.json` in each agent skills directory
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 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) {
129
+ async function resolvePluginMarketplaceRoot(pluginId, claudeDir) {
154
130
  const parsed = parsePluginId(pluginId);
155
131
  if (!parsed)
156
132
  return null;
157
- const marketplaceRoot = knownMarketplaceInstallLocations[parsed.marketplaceId] ??
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
- let entries;
270
- try {
271
- entries = await fs.readdir(commandsRoot, { withFileTypes: true });
272
- }
273
- catch {
274
- return [];
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
- return entries
277
- .filter((e) => e.isFile() && e.name.endsWith('.md'))
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 generateBaseNameFromCommand(commandName) {
332
- return sanitizeId(commandName);
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 = 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);
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
- names.add(sanitizeId(path.basename(entry.name, '.md')));
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, knownMarketplaceInstallLocations);
625
+ const pluginRoot = await resolvePluginMarketplaceRoot(pluginId, claudeDir);
624
626
  if (!pluginRoot) {
625
627
  unresolvedEnabled.add(pluginId);
626
- (0, constants_1.logWarn)(`[plugins] Enabled plugin not installed: ${pluginId}`, dryRun);
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 = generateBaseNameFromCommand(c.name);
657
- const sourceRelPath = `commands/${path.basename(c.file)}`;
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 name = sanitizeId(path.basename(entry.name, '.md'));
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 loadSkillsManifestEntries(targetSkillsDir) {
201
- const manifestPath = path.join(targetSkillsDir, exports.SKILLS_MANIFEST_FILENAME);
202
- if (await fileExists(manifestPath)) {
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(manifestPath, 'utf8'));
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 writeSkillsManifestEntries(targetSkillsDir, entries, dryRun) {
247
- const manifestPath = path.join(targetSkillsDir, exports.SKILLS_MANIFEST_FILENAME);
248
- const legacyUnifiedPath = path.join(targetSkillsDir, exports.LEGACY_UNIFIED_SKILLS_MANIFEST_FILENAME);
249
- const legacyPluginPath = path.join(targetSkillsDir, exports.LEGACY_PLUGIN_MANIFEST_FILENAME);
250
- const legacyClaudePath = path.join(targetSkillsDir, exports.LEGACY_CLAUDE_MANIFEST_FILENAME);
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
- if (dryRun)
254
- return;
255
- await Promise.allSettled([
256
- fs.unlink(manifestPath),
257
- fs.unlink(legacyUnifiedPath),
258
- fs.unlink(legacyPluginPath),
259
- fs.unlink(legacyClaudePath),
260
- ]);
261
- return;
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
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
270
- // Clean up legacy manifests once unified is written.
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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skiller",
3
- "version": "0.7.19",
3
+ "version": "0.7.20",
4
4
  "description": "Skiller — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "publishConfig": {