skiller 0.9.4 → 0.9.6

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.
@@ -71,6 +71,15 @@ async function executeSkillsWrapper(projectRoot, args) {
71
71
  function buildClaudePluginMigrationArgs(source) {
72
72
  return ['add', source, '--agent', 'universal', '--skill', '*', '-y'];
73
73
  }
74
+ function resolveRegistryMatchSource(match) {
75
+ if (match.source)
76
+ return match.source;
77
+ const parts = match.slug.split('/').filter(Boolean);
78
+ if (parts.length >= 2) {
79
+ return `${parts[0]}/${parts[1]}`;
80
+ }
81
+ return match.slug;
82
+ }
74
83
  function formatInstalls(count) {
75
84
  if (!count || count <= 0)
76
85
  return '0 installs';
@@ -86,6 +95,116 @@ function formatMatch(match) {
86
95
  const source = match.source || match.slug;
87
96
  return `${source}@${match.name} (${formatInstalls(match.installs)})`;
88
97
  }
98
+ async function readJsonObject(filePath) {
99
+ try {
100
+ const raw = JSON.parse(await fs.readFile(filePath, 'utf8'));
101
+ return raw && typeof raw === 'object'
102
+ ? raw
103
+ : null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ async function cleanupLegacyClaudePluginState(projectRoot, pluginIds) {
110
+ const pluginIdSet = new Set(pluginIds);
111
+ if (pluginIdSet.size === 0)
112
+ return;
113
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
114
+ const settings = await readJsonObject(settingsPath);
115
+ if (settings) {
116
+ const enabledPlugins = settings.enabledPlugins;
117
+ if (enabledPlugins && typeof enabledPlugins === 'object') {
118
+ const nextEnabledPlugins = Object.fromEntries(Object.entries(enabledPlugins).filter(([pluginId]) => !pluginIdSet.has(pluginId)));
119
+ if (Object.keys(nextEnabledPlugins).length !==
120
+ Object.keys(enabledPlugins).length) {
121
+ if (Object.keys(nextEnabledPlugins).length === 0) {
122
+ delete settings.enabledPlugins;
123
+ }
124
+ else {
125
+ settings.enabledPlugins = nextEnabledPlugins;
126
+ }
127
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
128
+ }
129
+ }
130
+ }
131
+ for (const manifestPath of [
132
+ path.join(projectRoot, '.agents', '.skiller.json'),
133
+ path.join(projectRoot, '.claude', '.skiller.json'),
134
+ ]) {
135
+ const manifest = await readJsonObject(manifestPath);
136
+ if (!manifest)
137
+ continue;
138
+ const targets = manifest.targets;
139
+ if (!targets || typeof targets !== 'object')
140
+ continue;
141
+ let changed = false;
142
+ const nextTargets = {};
143
+ for (const [targetKey, rawEntries] of Object.entries(targets)) {
144
+ if (!Array.isArray(rawEntries)) {
145
+ nextTargets[targetKey] = rawEntries;
146
+ continue;
147
+ }
148
+ const filteredEntries = rawEntries.filter((entry) => {
149
+ if (!entry || typeof entry !== 'object')
150
+ return true;
151
+ const sourceType = entry.sourceType;
152
+ const pluginId = entry.pluginId;
153
+ if (sourceType !== 'plugin' || typeof pluginId !== 'string') {
154
+ return true;
155
+ }
156
+ return !pluginIdSet.has(pluginId);
157
+ });
158
+ if (filteredEntries.length !== rawEntries.length) {
159
+ changed = true;
160
+ }
161
+ if (filteredEntries.length > 0) {
162
+ nextTargets[targetKey] = filteredEntries;
163
+ }
164
+ else {
165
+ changed = true;
166
+ }
167
+ }
168
+ if (!changed)
169
+ continue;
170
+ 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) {
174
+ await fs.rm(manifestPath, { force: true });
175
+ continue;
176
+ }
177
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
178
+ }
179
+ }
180
+ async function cleanupMigratedPluginAuxiliaryRules(projectRoot, sources) {
181
+ const candidateRuleNames = await (0, ClaudePluginMigration_1.listClaudePluginAuxiliaryRuleNames)(sources);
182
+ if (candidateRuleNames.length === 0)
183
+ return [];
184
+ const sourceSet = new Set(sources);
185
+ const plan = await (0, RulesToSkillsMigration_1.planRulesToSkillsMigration)(projectRoot, candidateRuleNames);
186
+ const removable = new Set(plan.unmatched);
187
+ for (const candidate of plan.candidates) {
188
+ const exactMatchSources = candidate.matches.map(resolveRegistryMatchSource);
189
+ if (exactMatchSources.length === 0 ||
190
+ exactMatchSources.every((source) => sourceSet.has(source))) {
191
+ removable.add(candidate.ruleName);
192
+ }
193
+ }
194
+ const removed = [...removable].sort((a, b) => a.localeCompare(b));
195
+ for (const ruleName of removed) {
196
+ await (0, RulesToSkillsMigration_1.removeLocalRuleReplacementState)(projectRoot, ruleName, false);
197
+ await fs.rm(path.join(projectRoot, '.agents', 'skills', ruleName), {
198
+ force: true,
199
+ recursive: true,
200
+ });
201
+ await fs.rm(path.join(projectRoot, '.claude', 'skills', ruleName), {
202
+ force: true,
203
+ recursive: true,
204
+ });
205
+ }
206
+ return removed;
207
+ }
89
208
  async function promptLine(message) {
90
209
  const rl = readline.createInterface({
91
210
  input: process.stdin,
@@ -329,10 +448,18 @@ async function migrateClaudePluginsHandler(argv) {
329
448
  for (const install of plan.installs) {
330
449
  await (0, skills_cli_1.runSkillsCli)(projectRoot, buildClaudePluginMigrationArgs(install.source));
331
450
  }
451
+ await cleanupLegacyClaudePluginState(projectRoot, [
452
+ ...plan.installs.flatMap((install) => install.pluginIds),
453
+ ...plan.unresolved.map((entry) => entry.pluginId),
454
+ ]);
455
+ const removedAuxiliaryRules = await cleanupMigratedPluginAuxiliaryRules(projectRoot, plan.installs.map((install) => install.source));
456
+ if (removedAuxiliaryRules.length > 0) {
457
+ console.log(`[skiller] Removed stale plugin-derived local rules:\n${removedAuxiliaryRules.map((name) => `- ${name}`).join('\n')}`);
458
+ }
332
459
  if (plan.unresolved.length > 0) {
333
460
  console.log(`[skiller] Skipped unresolved plugins:\n${plan.unresolved.map((entry) => `- ${entry.pluginId}: ${entry.reason}`).join('\n')}`);
334
461
  }
335
- console.log('[skiller] Claude plugin repo migration completed. Remove the plugin entries from .claude/settings.json, then rerun skiller apply.');
462
+ console.log('[skiller] Claude plugin repo migration completed. Legacy Claude plugin entries were removed from settings/manifests; rerun skiller apply.');
336
463
  }
337
464
  catch (err) {
338
465
  const message = err instanceof Error ? err.message : String(err);
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.listClaudePluginAuxiliaryRuleNames = listClaudePluginAuxiliaryRuleNames;
36
37
  exports.planClaudePluginSkillsMigration = planClaudePluginSkillsMigration;
37
38
  const fs = __importStar(require("fs/promises"));
38
39
  const os = __importStar(require("os"));
@@ -86,6 +87,20 @@ function normalizeCloneSource(source) {
86
87
  }
87
88
  return source;
88
89
  }
90
+ function normalizeSkillNameForDir(value) {
91
+ return value.replace(/:/g, '-').trim();
92
+ }
93
+ function extractAuxiliarySkillNames(skillMd) {
94
+ const names = new Set();
95
+ const pattern = /\b[a-z0-9-]+:[a-z0-9-]+:([a-z0-9-]+)\b/g;
96
+ for (const match of skillMd.toLowerCase().matchAll(pattern)) {
97
+ const candidate = match[1]?.trim();
98
+ if (!candidate || !/[a-z]/.test(candidate))
99
+ continue;
100
+ names.add(candidate);
101
+ }
102
+ return [...names];
103
+ }
89
104
  function formatInspectionError(source, err) {
90
105
  if (typeof err === 'object' && err !== null) {
91
106
  const stderr = 'stderr' in err ? err.stderr : '';
@@ -112,18 +127,30 @@ async function inspectSkillsInstallSource(source) {
112
127
  repoDir,
113
128
  ]);
114
129
  const { skills } = await (0, SkillsUtils_1.walkSkillsTree)(repoDir);
130
+ const publishedSkillNames = new Set();
131
+ const auxiliarySkillNames = new Set();
115
132
  for (const skill of skills) {
116
133
  try {
117
134
  const skillMd = await fs.readFile(path.join(skill.path, 'SKILL.md'), 'utf8');
118
135
  const { frontmatter } = (0, FrontmatterParser_1.parseFrontmatter)(skillMd);
119
136
  if (frontmatter?.name && frontmatter.description) {
120
- return { installable: true };
137
+ publishedSkillNames.add(normalizeSkillNameForDir(String(frontmatter.name)));
138
+ for (const auxiliaryName of extractAuxiliarySkillNames(skillMd)) {
139
+ auxiliarySkillNames.add(auxiliaryName);
140
+ }
121
141
  }
122
142
  }
123
143
  catch {
124
144
  // Keep scanning. One malformed skill should not hide valid siblings.
125
145
  }
126
146
  }
147
+ if (publishedSkillNames.size > 0) {
148
+ return {
149
+ installable: true,
150
+ auxiliarySkillNames: [...auxiliarySkillNames].sort((a, b) => a.localeCompare(b)),
151
+ publishedSkillNames: [...publishedSkillNames].sort((a, b) => a.localeCompare(b)),
152
+ };
153
+ }
127
154
  return {
128
155
  installable: false,
129
156
  reason: `Resolved source ${source} has no valid SKILL.md files with name and description`,
@@ -139,6 +166,23 @@ async function inspectSkillsInstallSource(source) {
139
166
  await fs.rm(tmpDir, { recursive: true, force: true });
140
167
  }
141
168
  }
169
+ async function listClaudePluginAuxiliaryRuleNames(sources, options = {}) {
170
+ const inspectSource = options.inspectSource ?? inspectSkillsInstallSource;
171
+ const auxiliarySkillNames = new Set();
172
+ for (const source of [...new Set(sources)].sort((a, b) => a.localeCompare(b))) {
173
+ const inspection = await inspectSource(source);
174
+ if (!inspection.installable)
175
+ continue;
176
+ const published = new Set((inspection.publishedSkillNames ?? []).map(normalizeSkillNameForDir));
177
+ for (const auxiliaryName of inspection.auxiliarySkillNames ?? []) {
178
+ const normalized = normalizeSkillNameForDir(auxiliaryName);
179
+ if (!normalized || published.has(normalized))
180
+ continue;
181
+ auxiliarySkillNames.add(normalized);
182
+ }
183
+ }
184
+ return [...auxiliarySkillNames].sort((a, b) => a.localeCompare(b));
185
+ }
142
186
  async function readEnabledPluginIds(projectRoot) {
143
187
  const settingsPath = path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR, 'settings.json');
144
188
  const raw = await readJsonFile(settingsPath);
@@ -218,6 +218,27 @@ async function planDirectoryMigration(sourceDir, destinationDir, plannedWrites,
218
218
  deletePaths.add(sourceDir);
219
219
  }
220
220
  }
221
+ async function planLegacySkillsMigration(legacySkillsDir, canonicalSkillsDir, plannedWrites, deletePaths, conflicts) {
222
+ if (!(await pathExists(legacySkillsDir)))
223
+ return;
224
+ const entries = await fs.readdir(legacySkillsDir, { withFileTypes: true });
225
+ for (const entry of entries) {
226
+ if (!entry.isDirectory())
227
+ continue;
228
+ const sourceDir = path.join(legacySkillsDir, entry.name);
229
+ const destinationDir = path.join(canonicalSkillsDir, entry.name);
230
+ if (await pathExists(destinationDir)) {
231
+ // Canonical .agents/skills already owns this name now. Legacy
232
+ // .claude/skills duplicates are stale migration debris.
233
+ deletePaths.add(sourceDir);
234
+ continue;
235
+ }
236
+ await planDirectoryMigration(sourceDir, destinationDir, plannedWrites, deletePaths, conflicts);
237
+ }
238
+ if (conflicts.length === 0) {
239
+ deletePaths.add(legacySkillsDir);
240
+ }
241
+ }
221
242
  async function findRuleSkillFolders(dir) {
222
243
  const folders = [];
223
244
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -395,7 +416,7 @@ async function migrateLegacyProjectState(projectRoot, dryRun) {
395
416
  }
396
417
  }
397
418
  await planFileMigration(path.join(legacyDir, project_paths_1.PROJECT_AGENTS_FILE), path.join(canonicalDir, project_paths_1.PROJECT_AGENTS_FILE), plannedWrites, deletePaths, conflicts);
398
- await planDirectoryMigration(path.join(legacyDir, 'skills'), canonicalSkillsDir, plannedWrites, deletePaths, conflicts);
419
+ await planLegacySkillsMigration(path.join(legacyDir, 'skills'), canonicalSkillsDir, plannedWrites, deletePaths, conflicts);
399
420
  await planLegacyRulesMigration(path.join(legacyDir, 'rules'), canonicalRulesDir, canonicalSkillsDir, plannedWrites, deletePaths, conflicts);
400
421
  if (conflicts.length > 0) {
401
422
  throw new Error(`Legacy .claude migration conflicts:\n- ${conflicts.join('\n- ')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skiller",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Skiller — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "publishConfig": {