skiller 0.8.2 → 0.9.0

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.
@@ -485,40 +485,7 @@ async function loadManagedEntries(projectRoot, targetSkillsDir, dryRun) {
485
485
  return { pluginEntries, otherEntries };
486
486
  }
487
487
  async function discoverLocalSkillNames(projectRoot) {
488
- const localSkillsDir = path.join(projectRoot, '.claude', 'skills');
489
- if (!(await fileExists(localSkillsDir)))
490
- return new Set();
491
- const names = new Set();
492
- async function walk(current, depth) {
493
- if (depth >= constants_1.MAX_RECURSION_DEPTH)
494
- return;
495
- let entries;
496
- try {
497
- entries = await fs.readdir(current, { withFileTypes: true });
498
- }
499
- catch {
500
- return;
501
- }
502
- const hasSkillMd = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
503
- if (hasSkillMd) {
504
- const rel = path.relative(localSkillsDir, current).replace(/\\/g, '/');
505
- const segments = rel.split('/').filter(Boolean);
506
- if (segments.length > 0) {
507
- names.add(segments.map(sanitizeId).join('-'));
508
- }
509
- else {
510
- names.add(sanitizeId(path.basename(current)));
511
- }
512
- return;
513
- }
514
- for (const entry of entries) {
515
- if (!entry.isDirectory())
516
- continue;
517
- await walk(path.join(current, entry.name), depth + 1);
518
- }
519
- }
520
- await walk(localSkillsDir, 0);
521
- return names;
488
+ return new Set(await (0, SkillsManifest_1.loadLocalSkillNames)(projectRoot));
522
489
  }
523
490
  async function discoverLocalCommandNames(projectRoot) {
524
491
  const localCommandsDir = path.join(projectRoot, '.claude', 'commands');
@@ -40,7 +40,7 @@ const yaml = __importStar(require("js-yaml"));
40
40
  const constants_1 = require("../constants");
41
41
  const FrontmatterParser_1 = require("./FrontmatterParser");
42
42
  const SkillsManifest_1 = require("./SkillsManifest");
43
- const LEGACY_PLUGIN_MARKER_FILENAME = '.skiller-plugin.json';
43
+ const project_paths_1 = require("./project-paths");
44
44
  function sanitizeId(value) {
45
45
  return value.replace(/[^A-Za-z0-9._-]+/g, '_');
46
46
  }
@@ -171,42 +171,21 @@ async function writeMarkdownAsSkill(srcPath, destDir, generatedName, kindLabel,
171
171
  return;
172
172
  await fs.writeFile(path.join(destDir, 'SKILL.md'), next, 'utf8');
173
173
  }
174
- async function readPluginManagedDestNames(projectRoot, targetSkillsDir) {
175
- const names = new Set();
176
- for (const entry of await (0, SkillsManifest_1.loadSkillsManifestEntries)(projectRoot, targetSkillsDir)) {
177
- if ((0, SkillsManifest_1.isPluginManifestEntry)(entry)) {
178
- names.add(entry.destRelPath);
179
- }
180
- }
181
- // Legacy: prior versions wrote per-skill plugin marker files. Treat any
182
- // folder containing one as plugin-managed so project items can take over.
183
- try {
184
- const dirents = await fs.readdir(targetSkillsDir, { withFileTypes: true });
185
- for (const d of dirents) {
186
- if (!d.isDirectory())
187
- continue;
188
- try {
189
- await fs.access(path.join(targetSkillsDir, d.name, LEGACY_PLUGIN_MARKER_FILENAME));
190
- names.add(d.name);
191
- }
192
- catch {
193
- // ignore
194
- }
195
- }
196
- }
197
- catch {
198
- // ignore
199
- }
200
- return names;
201
- }
202
174
  async function discoverLocalSkillNames(projectRoot) {
203
- const localSkillsDir = path.join(projectRoot, '.claude', 'skills');
204
- if (!(await fileExists(localSkillsDir)))
175
+ return new Set(await (0, SkillsManifest_1.loadLocalSkillNames)(projectRoot));
176
+ }
177
+ async function discoverCanonicalSkillNames(projectRoot) {
178
+ const skillsRoot = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, 'skills');
179
+ if (!(await fileExists(skillsRoot)))
205
180
  return new Set();
206
181
  const names = new Set();
207
- async function walk(current, depth) {
182
+ async function walk(current, rel, depth) {
208
183
  if (depth >= constants_1.MAX_RECURSION_DEPTH)
209
184
  return;
185
+ if (rel && (await fileExists(path.join(current, 'SKILL.md')))) {
186
+ names.add(rel.split('/').filter(Boolean).map(sanitizeId).join('-'));
187
+ return;
188
+ }
210
189
  let entries;
211
190
  try {
212
191
  entries = await fs.readdir(current, { withFileTypes: true });
@@ -214,30 +193,20 @@ async function discoverLocalSkillNames(projectRoot) {
214
193
  catch {
215
194
  return;
216
195
  }
217
- const hasSkillMd = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
218
- if (hasSkillMd) {
219
- const rel = path.relative(localSkillsDir, current).replace(/\\/g, '/');
220
- const segments = rel.split('/').filter(Boolean);
221
- if (segments.length > 0) {
222
- names.add(segments.map(sanitizeId).join('-'));
223
- }
224
- else {
225
- names.add(sanitizeId(path.basename(current)));
226
- }
227
- return;
228
- }
229
196
  for (const entry of entries) {
230
197
  if (!entry.isDirectory())
231
198
  continue;
232
- await walk(path.join(current, entry.name), depth + 1);
199
+ const nextRel = rel ? `${rel}/${entry.name}` : entry.name;
200
+ await walk(path.join(current, entry.name), nextRel, depth + 1);
233
201
  }
234
202
  }
235
- await walk(localSkillsDir, 0);
203
+ await walk(skillsRoot, '', 0);
236
204
  return names;
237
205
  }
238
206
  async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
239
207
  const { projectRoot, targetSkillsDirs, verbose, dryRun } = args;
240
208
  const localSkillNames = await discoverLocalSkillNames(projectRoot);
209
+ const canonicalSkillNames = await discoverCanonicalSkillNames(projectRoot);
241
210
  const commands = await discoverCommandFiles(projectRoot);
242
211
  const agents = await discoverAgentFiles(projectRoot);
243
212
  const expectedItems = [];
@@ -284,9 +253,23 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
284
253
  prevDestByItemKey.set(makeItemKey(entry.sourceKind, entry.sourceRelPath), entry.destRelPath);
285
254
  }
286
255
  const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
287
- const pluginManagedDest = targetExists
288
- ? await readPluginManagedDestNames(projectRoot, targetSkillsDir)
289
- : new Set();
256
+ const reservedCanonicalNames = new Set([
257
+ ...canonicalSkillNames,
258
+ ...localSkillNames,
259
+ ]);
260
+ const canonicalSkillsDir = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, 'skills');
261
+ if (path.resolve(targetSkillsDir) === path.resolve(canonicalSkillsDir)) {
262
+ for (const entry of managedEntries) {
263
+ reservedCanonicalNames.delete(entry.destRelPath);
264
+ }
265
+ }
266
+ const activeItems = sortedItems.filter((item) => {
267
+ if (!reservedCanonicalNames.has(item.baseName)) {
268
+ return true;
269
+ }
270
+ (0, constants_1.logVerboseInfo)(`Skipping claude ${item.sourceKind} '${item.baseName}' because canonical skills already own that name`, verbose, dryRun);
271
+ return false;
272
+ });
290
273
  const reserved = new Set(localSkillNames);
291
274
  if (targetExists) {
292
275
  let dirents = [];
@@ -299,9 +282,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
299
282
  for (const d of dirents) {
300
283
  if (!d.isDirectory())
301
284
  continue;
302
- // Reserve any existing folder we do not manage, except plugin-managed
303
- // folders (project should be able to take those over).
304
- if (!managedDest.has(d.name) && !pluginManagedDest.has(d.name)) {
285
+ if (!managedDest.has(d.name)) {
305
286
  reserved.add(d.name);
306
287
  }
307
288
  }
@@ -309,7 +290,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
309
290
  const taken = new Set(reserved);
310
291
  const assignedDestByItemKey = new Map();
311
292
  // Preserve previous destinations when they are still available.
312
- for (const item of sortedItems) {
293
+ for (const item of activeItems) {
313
294
  const prev = prevDestByItemKey.get(item.itemKey);
314
295
  if (!prev)
315
296
  continue;
@@ -324,7 +305,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
324
305
  taken.add(prev);
325
306
  }
326
307
  // Assign baseName, otherwise namespace with "claude-".
327
- for (const item of sortedItems) {
308
+ for (const item of activeItems) {
328
309
  if (assignedDestByItemKey.has(item.itemKey))
329
310
  continue;
330
311
  const base = item.baseName;
@@ -342,7 +323,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
342
323
  assignedDestByItemKey.set(item.itemKey, candidate);
343
324
  taken.add(candidate);
344
325
  }
345
- const assignedItems = sortedItems.map((item) => ({
326
+ const assignedItems = activeItems.map((item) => ({
346
327
  ...item,
347
328
  destRelPath: assignedDestByItemKey.get(item.itemKey),
348
329
  }));
@@ -358,9 +339,7 @@ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
358
339
  const destRelPath = item.destRelPath;
359
340
  const destDir = path.join(targetSkillsDir, destRelPath);
360
341
  if (await fileExists(destDir)) {
361
- const isManagedByProject = managedDest.has(destRelPath);
362
- const isManagedByPlugin = pluginManagedDest.has(destRelPath);
363
- if (!isManagedByProject && !isManagedByPlugin) {
342
+ if (!managedDest.has(destRelPath)) {
364
343
  (0, constants_1.logWarn)(`[claude] Destination exists but is not skiller-managed, skipping: ${destDir}`, dryRun);
365
344
  continue;
366
345
  }
@@ -40,6 +40,7 @@ const os = __importStar(require("os"));
40
40
  const toml_1 = require("@iarna/toml");
41
41
  const zod_1 = require("zod");
42
42
  const constants_1 = require("../constants");
43
+ const project_paths_1 = require("./project-paths");
43
44
  const mcpConfigSchema = zod_1.z
44
45
  .object({
45
46
  enabled: zod_1.z.boolean().optional(),
@@ -121,8 +122,7 @@ async function loadConfig(options) {
121
122
  configFile = path.resolve(configPath);
122
123
  }
123
124
  else {
124
- // Try local .claude/skiller.toml first
125
- const localConfigFile = path.join(projectRoot, '.claude', 'skiller.toml');
125
+ const localConfigFile = path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, project_paths_1.SKILLER_CONFIG_FILE);
126
126
  try {
127
127
  await fs_1.promises.access(localConfigFile);
128
128
  configFile = localConfigFile;
@@ -236,10 +236,10 @@ async function loadConfig(options) {
236
236
  }
237
237
  // Deprecation warnings for removed config options
238
238
  if ('generate_from_rules' in rawSkillsSection) {
239
- console.warn(`[skiller] Warning: skills.generate_from_rules is deprecated and has no effect. Skills are now edited directly in .claude/skills/`);
239
+ console.warn(`[skiller] Warning: skills.generate_from_rules is deprecated and has no effect. Local rule sources in .agents/rules/ compile automatically into .agents/skills/.`);
240
240
  }
241
241
  if ('prune' in rawSkillsSection) {
242
- console.warn(`[skiller] Warning: skills.prune is deprecated and has no effect. Skills in .claude/skills/ are never auto-deleted.`);
242
+ console.warn(`[skiller] Warning: skills.prune is deprecated and has no effect. Skills in .agents/skills/ are never auto-deleted.`);
243
243
  }
244
244
  const rawRulesSection = raw.rules && typeof raw.rules === 'object' && !Array.isArray(raw.rules)
245
245
  ? raw.rules
@@ -48,6 +48,7 @@ const path = __importStar(require("path"));
48
48
  const os = __importStar(require("os"));
49
49
  const FrontmatterParser_1 = require("./FrontmatterParser");
50
50
  const constants_1 = require("../constants");
51
+ const project_paths_1 = require("./project-paths");
51
52
  /**
52
53
  * Gets the XDG config directory path, falling back to ~/.config if XDG_CONFIG_HOME is not set.
53
54
  */
@@ -55,31 +56,30 @@ function getXdgConfigDir() {
55
56
  return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
56
57
  }
57
58
  /**
58
- * Searches upwards from startPath to find a .claude directory with skiller.toml.
59
+ * Searches upwards from startPath to find a .agents directory with skiller.toml.
59
60
  * If not found locally and checkGlobal is true, checks for global config at XDG_CONFIG_HOME/skiller.
60
61
  * Returns the path to the found directory, or null if not found.
61
62
  */
62
63
  async function findSkillerDir(startPath, checkGlobal = true) {
63
- // Search upwards from startPath for local .claude directory with skiller.toml
64
+ // Search upwards from startPath for local .agents directory with skiller.toml
64
65
  let current = startPath;
65
66
  while (current) {
66
- const claudeCandidate = path.join(current, '.claude');
67
+ const skillerCandidate = path.join(current, project_paths_1.CANONICAL_SKILLER_DIR);
67
68
  try {
68
- const stat = await fs_1.promises.stat(claudeCandidate);
69
+ const stat = await fs_1.promises.stat(skillerCandidate);
69
70
  if (stat.isDirectory()) {
70
- // Check if this .claude directory has skiller.toml
71
- const tomlPath = path.join(claudeCandidate, 'skiller.toml');
71
+ const tomlPath = path.join(skillerCandidate, project_paths_1.SKILLER_CONFIG_FILE);
72
72
  try {
73
73
  await fs_1.promises.stat(tomlPath);
74
- return claudeCandidate;
74
+ return skillerCandidate;
75
75
  }
76
76
  catch {
77
- // .claude exists but no skiller.toml, continue searching
77
+ // .agents exists but no skiller.toml, continue searching
78
78
  }
79
79
  }
80
80
  }
81
81
  catch {
82
- // ignore errors when checking for .claude directory
82
+ // ignore errors when checking for .agents directory
83
83
  }
84
84
  const parent = path.dirname(current);
85
85
  if (parent === current) {
@@ -87,7 +87,7 @@ async function findSkillerDir(startPath, checkGlobal = true) {
87
87
  }
88
88
  current = parent;
89
89
  }
90
- // If no local .claude found and checkGlobal is true, check global config directory
90
+ // If no local .agents found and checkGlobal is true, check global config directory
91
91
  if (checkGlobal) {
92
92
  const globalConfigDir = path.join(getXdgConfigDir(), 'skiller');
93
93
  try {
@@ -96,8 +96,8 @@ async function findSkillerDir(startPath, checkGlobal = true) {
96
96
  return globalConfigDir;
97
97
  }
98
98
  }
99
- catch (err) {
100
- console.error(`[skiller] Error checking global config directory ${globalConfigDir}:`, err);
99
+ catch {
100
+ // ignore if global config doesn't exist
101
101
  }
102
102
  }
103
103
  return null;
@@ -259,8 +259,8 @@ async function readMarkdownFiles(skillerDir, options) {
259
259
  // 2. If AGENTS.md absent but legacy instructions.md present, use it (no longer emits a warning; legacy accepted silently).
260
260
  // 3. Include any remaining .md files (excluding whichever of the above was used if present) in
261
261
  // sorted order AFTER the preferred primary file so that new concatenation priority starts with AGENTS.md.
262
- const topLevelAgents = path.join(skillerDir, 'AGENTS.md');
263
- const topLevelLegacy = path.join(skillerDir, 'instructions.md');
262
+ const topLevelAgents = path.join(skillerDir, project_paths_1.PROJECT_AGENTS_FILE);
263
+ const topLevelLegacy = path.join(skillerDir, project_paths_1.LEGACY_INSTRUCTIONS_FILE);
264
264
  // Separate primary candidates from others
265
265
  let primaryFile = null;
266
266
  const others = [];
@@ -284,35 +284,7 @@ async function readMarkdownFiles(skillerDir, options) {
284
284
  }
285
285
  // Sort the remaining others for stable deterministic concatenation order.
286
286
  others.sort((a, b) => a.path.localeCompare(b.path));
287
- let ordered = primaryFile ? [primaryFile, ...others] : others;
288
- // NEW: Prepend repository root AGENTS.md (outside .claude) if it exists and is not identical path.
289
- try {
290
- const repoRoot = path.dirname(skillerDir); // .claude parent
291
- const rootAgentsPath = path.join(repoRoot, 'AGENTS.md');
292
- if (path.resolve(rootAgentsPath) !== path.resolve(topLevelAgents)) {
293
- const stat = await fs_1.promises.stat(rootAgentsPath);
294
- if (stat.isFile()) {
295
- const content = await fs_1.promises.readFile(rootAgentsPath, 'utf8');
296
- // Check if this is a generated file and we have other .claude files
297
- const isGenerated = content.startsWith('<!-- Generated by Skiller -->');
298
- const hasSkillerFiles = others.length > 0 || primaryFile !== null;
299
- // Additional check: if AGENTS.md contains skiller source comments and we have skiller files,
300
- // it's likely a corrupted generated file that should be skipped
301
- const containsSkillerSources = content.includes('<!-- Source: .claude/') ||
302
- content.includes('<!-- Source: claude/');
303
- const isProbablyGenerated = isGenerated || (containsSkillerSources && hasSkillerFiles);
304
- // Skip generated AGENTS.md if we have other files in .claude
305
- if (!isProbablyGenerated || !hasSkillerFiles) {
306
- // Prepend so it has highest precedence
307
- ordered = [{ path: rootAgentsPath, content }, ...ordered];
308
- }
309
- }
310
- }
311
- }
312
- catch {
313
- // ignore if root AGENTS.md not present
314
- }
315
- return ordered;
287
+ return primaryFile ? [primaryFile, ...others] : others;
316
288
  }
317
289
  /**
318
290
  * Writes content to filePath, creating parent directories if necessary.
@@ -359,7 +331,7 @@ async function findGlobalSkillerDir() {
359
331
  // Alias for backward compatibility
360
332
  exports.findGlobalRulerDir = findGlobalSkillerDir;
361
333
  /**
362
- * Searches the entire directory tree from startPath to find all .claude directories with skiller.toml.
334
+ * Searches the entire directory tree from startPath to find all .agents directories with skiller.toml.
363
335
  * Returns an array of directory paths from most specific to least specific.
364
336
  */
365
337
  async function findAllSkillerDirs(startPath) {
@@ -375,15 +347,14 @@ async function findAllSkillerDirs(startPath) {
375
347
  for (const entry of entries) {
376
348
  const fullPath = path.join(dir, entry.name);
377
349
  if (entry.isDirectory()) {
378
- if (entry.name === '.claude') {
379
- // Check if .claude has skiller.toml
380
- const tomlPath = path.join(fullPath, 'skiller.toml');
350
+ if (entry.name === project_paths_1.CANONICAL_SKILLER_DIR) {
351
+ const tomlPath = path.join(fullPath, project_paths_1.SKILLER_CONFIG_FILE);
381
352
  try {
382
353
  await fs_1.promises.stat(tomlPath);
383
354
  skillerDirs.push(fullPath);
384
355
  }
385
356
  catch {
386
- // .claude exists but no skiller.toml
357
+ // .agents exists but no skiller.toml
387
358
  }
388
359
  }
389
360
  else {
@@ -79,7 +79,8 @@ function parseFrontmatter(content) {
79
79
  try {
80
80
  // Fix common issue: globs as comma-separated unquoted strings
81
81
  // Pattern: globs: *.tsx,**/path -> globs: ["*.tsx", "**/path"]
82
- const fixedYaml = yamlContent.replace(/^(\s*globs\s*:\s*)([^\n[{]+)$/gm, (match, prefix, value) => {
82
+ const fixedYaml = yamlContent
83
+ .replace(/^(\s*globs\s*:\s*)([^\n[{]+)$/gm, (match, prefix, value) => {
83
84
  // Check if value looks like comma-separated patterns (contains * or commas)
84
85
  if (value.includes('*') || value.includes(',')) {
85
86
  // Split by comma and quote each part
@@ -92,6 +93,15 @@ function parseFrontmatter(content) {
92
93
  return `${prefix}[${patterns}]`;
93
94
  }
94
95
  return match;
96
+ })
97
+ .replace(/^(\s*[\w.-]+\s*:\s*)([^\n"'[{>|].*:\s.*)$/gm, (match, prefix, value) => {
98
+ const trimmed = value.trim();
99
+ if (trimmed.length === 0 ||
100
+ /^(true|false|null)$/i.test(trimmed) ||
101
+ /^-?\d+(\.\d+)?$/.test(trimmed)) {
102
+ return match;
103
+ }
104
+ return `${prefix}${JSON.stringify(trimmed)}`;
95
105
  });
96
106
  // Try parsing again with fixed YAML
97
107
  if (fixedYaml !== yamlContent) {
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.assertNoLegacyClaudePluginState = assertNoLegacyClaudePluginState;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ const project_paths_1 = require("./project-paths");
40
+ const SkillsManifest_1 = require("./SkillsManifest");
41
+ async function readEnabledPluginIds(projectRoot) {
42
+ const settingsPath = path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR, 'settings.json');
43
+ try {
44
+ const raw = JSON.parse(await fs.readFile(settingsPath, 'utf8'));
45
+ if (!raw || typeof raw !== 'object')
46
+ return [];
47
+ const enabledPlugins = raw.enabledPlugins;
48
+ if (!enabledPlugins || typeof enabledPlugins !== 'object')
49
+ return [];
50
+ return Object.entries(enabledPlugins)
51
+ .filter(([, enabled]) => enabled === true)
52
+ .map(([pluginId]) => pluginId)
53
+ .sort((a, b) => a.localeCompare(b));
54
+ }
55
+ catch {
56
+ return [];
57
+ }
58
+ }
59
+ function readPluginIdsFromManifestRaw(raw) {
60
+ if (!raw || typeof raw !== 'object')
61
+ return [];
62
+ const targets = raw.targets;
63
+ if (!targets || typeof targets !== 'object')
64
+ return [];
65
+ const pluginIds = new Set();
66
+ for (const rawEntries of Object.values(targets)) {
67
+ if (!Array.isArray(rawEntries))
68
+ continue;
69
+ for (const entry of rawEntries) {
70
+ if (!entry || typeof entry !== 'object')
71
+ continue;
72
+ const sourceType = entry.sourceType;
73
+ const pluginId = entry.pluginId;
74
+ if (sourceType !== 'plugin' || typeof pluginId !== 'string')
75
+ continue;
76
+ pluginIds.add(pluginId);
77
+ }
78
+ }
79
+ return [...pluginIds].sort((a, b) => a.localeCompare(b));
80
+ }
81
+ async function readPluginManifestLocations(projectRoot) {
82
+ const manifestPaths = [
83
+ path.join(projectRoot, project_paths_1.CANONICAL_SKILLER_DIR, SkillsManifest_1.SKILLS_MANIFEST_FILENAME),
84
+ path.join(projectRoot, project_paths_1.LEGACY_SKILLER_DIR, SkillsManifest_1.SKILLS_MANIFEST_FILENAME),
85
+ ];
86
+ const locations = [];
87
+ for (const manifestPath of manifestPaths) {
88
+ try {
89
+ const raw = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
90
+ const pluginIds = readPluginIdsFromManifestRaw(raw);
91
+ if (pluginIds.length === 0)
92
+ continue;
93
+ locations.push({
94
+ manifestPath,
95
+ pluginIds,
96
+ });
97
+ }
98
+ catch {
99
+ // Ignore missing or invalid manifests here. Validation lives elsewhere.
100
+ }
101
+ }
102
+ return locations;
103
+ }
104
+ async function assertNoLegacyClaudePluginState(projectRoot) {
105
+ const enabledPluginIds = await readEnabledPluginIds(projectRoot);
106
+ const manifestLocations = await readPluginManifestLocations(projectRoot);
107
+ if (enabledPluginIds.length === 0 && manifestLocations.length === 0) {
108
+ return;
109
+ }
110
+ const lines = [
111
+ 'Claude plugin sync is no longer supported.',
112
+ '',
113
+ 'Found legacy Claude plugin state:',
114
+ ];
115
+ if (enabledPluginIds.length > 0) {
116
+ lines.push(`- enabled plugins in .claude/settings.json: ${enabledPluginIds.join(', ')}`);
117
+ }
118
+ for (const location of manifestLocations) {
119
+ lines.push(`- plugin manifest entries in ${path.relative(projectRoot, location.manifestPath)}: ${location.pluginIds.join(', ')}`);
120
+ }
121
+ lines.push('', 'Migrate manually:', '1. Run `skiller migrate claude-plugins` to preview the repo installs, then rerun it with `--execute`', '2. Remove the plugin from .claude/settings.json', '3. Rerun `skiller apply`');
122
+ throw new Error(lines.join('\n'));
123
+ }