skiller 0.9.6 → 0.9.8

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