skiller 0.7.15 → 0.7.17

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
@@ -59,11 +59,21 @@ A Claude-centric fork of [ruler](https://github.com/intellectronica/ruler) with
59
59
  - Reads `.claude/settings.json` `enabledPlugins`
60
60
  - Syncs enabled plugin `skills/` into agent skills directories on `skiller apply`
61
61
  - Syncs enabled plugin `commands/*.md` as skills (`SKILL.md`) into agent skills directories
62
- - Uses the skill/command name by default (matches existing Codex skill names)
63
- - If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginId>__<name>`
64
- - Tracks plugin-managed skills in a single `.skiller-plugins.json` file per agent skills directory
62
+ - Syncs enabled plugin `agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
63
+ - Uses the skill/command/agent name by default (matches existing Codex skill names)
64
+ - If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginId>-<name>`
65
+ - Tracks plugin-managed skills in a single `.skiller.json` file per agent skills directory
65
66
  - Removes stale plugin skills when plugins are disabled
66
67
 
68
+ ## 10. Claude Commands/Agents → Skills
69
+
70
+ - Syncs `.claude/commands/**/*.md` as skills (`SKILL.md`) into agent skills directories
71
+ - Syncs `.claude/agents/**/*.md` as skills (`SKILL.md`) into agent skills directories
72
+ - Uses the command/agent name by default
73
+ - If a name conflicts, existing local/manual skills win and the project item is namespaced as `claude-<name>`
74
+ - Project items win over plugin skills/commands/agents on name conflicts
75
+ - Tracks project-managed items in a single `.skiller.json` file per agent skills directory
76
+
67
77
  ---
68
78
 
69
79
  # Skiller: Centralise Your AI Coding Assistant Instructions
@@ -596,8 +606,8 @@ If your project enables Claude Code plugins in `.claude/settings.json`, Skiller
596
606
  - Plugin `skills/` are copied as skills
597
607
  - Plugin `commands/*.md` are converted into skills (`SKILL.md`)
598
608
  - Plugin skills use their original skill/command name by default
599
- - If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginId>__<name>`
600
- - Plugin-managed skills are tracked via `.skiller-plugins.json` in each agent skills directory
609
+ - If a name conflicts, local skills win and the plugin skill is namespaced as `<pluginId>-<name>`
610
+ - Plugin-managed skills are tracked via `.skiller.json` in each agent skills directory
601
611
 
602
612
  ### Skills Directory Structure
603
613
 
@@ -38,6 +38,7 @@ exports.readInstalledPluginsIndex = readInstalledPluginsIndex;
38
38
  exports.resolvePluginInstall = resolvePluginInstall;
39
39
  exports.discoverPluginSkillDirs = discoverPluginSkillDirs;
40
40
  exports.discoverPluginCommandFiles = discoverPluginCommandFiles;
41
+ exports.discoverPluginAgentFiles = discoverPluginAgentFiles;
41
42
  exports.syncClaudePluginsToSkillsDirs = syncClaudePluginsToSkillsDirs;
42
43
  const fs = __importStar(require("fs/promises"));
43
44
  const os = __importStar(require("os"));
@@ -46,9 +47,8 @@ const yaml = __importStar(require("js-yaml"));
46
47
  const constants_1 = require("../constants");
47
48
  const FrontmatterParser_1 = require("./FrontmatterParser");
48
49
  const SkillsUtils_1 = require("./SkillsUtils");
50
+ const SkillsManifest_1 = require("./SkillsManifest");
49
51
  const LEGACY_MARKER_FILENAME = '.skiller-plugin.json';
50
- const MANIFEST_FILENAME = '.skiller-plugins.json';
51
- const MANIFEST_VERSION = 1;
52
52
  function getUserHomeDir() {
53
53
  // Prefer env vars so tests (and users) can override deterministically.
54
54
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -219,6 +219,49 @@ async function discoverPluginCommandFiles(installPath) {
219
219
  file: path.join(commandsRoot, e.name),
220
220
  }));
221
221
  }
222
+ async function discoverPluginAgentFiles(installPath) {
223
+ const agentsRoot = path.join(installPath, 'agents');
224
+ if (!(await fileExists(agentsRoot)))
225
+ return [];
226
+ const results = [];
227
+ async function walk(current, depth) {
228
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
229
+ return;
230
+ let entries;
231
+ try {
232
+ entries = await fs.readdir(current, { withFileTypes: true });
233
+ }
234
+ catch {
235
+ return;
236
+ }
237
+ for (const entry of entries) {
238
+ const full = path.join(current, entry.name);
239
+ if (entry.isDirectory()) {
240
+ await walk(full, depth + 1);
241
+ continue;
242
+ }
243
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
244
+ continue;
245
+ let content;
246
+ try {
247
+ content = await fs.readFile(full, 'utf8');
248
+ }
249
+ catch {
250
+ continue;
251
+ }
252
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(content);
253
+ const fmName = parsed.rawFrontmatter && typeof parsed.rawFrontmatter.name === 'string'
254
+ ? parsed.rawFrontmatter.name
255
+ : parsed.frontmatter?.name;
256
+ if (typeof fmName !== 'string' || fmName.trim() === '')
257
+ continue;
258
+ const rel = path.relative(agentsRoot, full).replace(/\\/g, '/');
259
+ results.push({ name: fmName.trim(), file: full, rel });
260
+ }
261
+ }
262
+ await walk(agentsRoot, 0);
263
+ return results;
264
+ }
222
265
  function generateBaseNameFromRelId(relId) {
223
266
  const normalized = relId.replace(/\\/g, '/');
224
267
  const segments = normalized.split('/').filter(Boolean);
@@ -228,76 +271,7 @@ function generateBaseNameFromCommand(commandName) {
228
271
  return sanitizeId(commandName);
229
272
  }
230
273
  function generateNamespacedName(pluginId, baseName) {
231
- return `${sanitizeId(pluginId)}__${baseName}`;
232
- }
233
- async function readManifestFile(targetSkillsDir) {
234
- const manifestPath = path.join(targetSkillsDir, MANIFEST_FILENAME);
235
- try {
236
- const raw = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
237
- if (!raw || typeof raw !== 'object')
238
- return null;
239
- const obj = raw;
240
- if (typeof obj.version !== 'number')
241
- return null;
242
- if (!Array.isArray(obj.entries))
243
- return null;
244
- const entries = [];
245
- for (const entry of obj.entries) {
246
- if (!entry || typeof entry !== 'object')
247
- continue;
248
- const e = entry;
249
- if (typeof e.pluginId !== 'string')
250
- continue;
251
- const sourceKind = e.sourceKind;
252
- if (sourceKind !== 'skill' && sourceKind !== 'command')
253
- continue;
254
- if (typeof e.sourceRelPath !== 'string')
255
- continue;
256
- if (typeof e.destRelPath !== 'string')
257
- continue;
258
- const pluginVersion = typeof e.pluginVersion === 'string' ? e.pluginVersion : undefined;
259
- entries.push({
260
- pluginId: e.pluginId,
261
- pluginVersion,
262
- sourceKind,
263
- sourceRelPath: e.sourceRelPath,
264
- destRelPath: e.destRelPath,
265
- });
266
- }
267
- return {
268
- version: obj.version,
269
- entries,
270
- };
271
- }
272
- catch {
273
- return null;
274
- }
275
- }
276
- async function writeManifestFile(targetSkillsDir, entries, dryRun) {
277
- const manifestPath = path.join(targetSkillsDir, MANIFEST_FILENAME);
278
- if (entries.length === 0) {
279
- if (dryRun)
280
- return;
281
- try {
282
- await fs.unlink(manifestPath);
283
- }
284
- catch {
285
- // ignore
286
- }
287
- return;
288
- }
289
- const manifest = {
290
- version: MANIFEST_VERSION,
291
- entries: [...entries].sort((a, b) => {
292
- // Stable ordering for diffs.
293
- const ak = `${a.destRelPath}::${a.pluginId}::${a.sourceKind}::${a.sourceRelPath}`;
294
- const bk = `${b.destRelPath}::${b.pluginId}::${b.sourceKind}::${b.sourceRelPath}`;
295
- return ak.localeCompare(bk);
296
- }),
297
- };
298
- if (dryRun)
299
- return;
300
- await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
274
+ return `${sanitizeId(pluginId)}-${baseName}`;
301
275
  }
302
276
  async function readLegacyMarkerFile(dir) {
303
277
  const markerPath = path.join(dir, LEGACY_MARKER_FILENAME);
@@ -310,7 +284,9 @@ async function readLegacyMarkerFile(dir) {
310
284
  return null;
311
285
  if (typeof obj.generatedName !== 'string')
312
286
  return null;
313
- if (obj.sourceKind !== 'skill' && obj.sourceKind !== 'command')
287
+ if (obj.sourceKind !== 'skill' &&
288
+ obj.sourceKind !== 'command' &&
289
+ obj.sourceKind !== 'agent')
314
290
  return null;
315
291
  if (typeof obj.sourceRelPath !== 'string')
316
292
  return null;
@@ -339,10 +315,17 @@ async function removeLegacyMarkerFile(dir, dryRun) {
339
315
  }
340
316
  }
341
317
  async function loadManagedEntries(targetSkillsDir, dryRun) {
342
- const manifest = await readManifestFile(targetSkillsDir);
343
- const entries = manifest?.entries
344
- ? [...manifest.entries]
345
- : [];
318
+ const allEntries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(targetSkillsDir);
319
+ const pluginEntries = [];
320
+ const otherEntries = [];
321
+ for (const entry of allEntries) {
322
+ if ((0, SkillsManifest_1.isPluginManifestEntry)(entry)) {
323
+ pluginEntries.push(entry);
324
+ }
325
+ else {
326
+ otherEntries.push(entry);
327
+ }
328
+ }
346
329
  // Migration: absorb legacy per-skill markers and remove them so they don't
347
330
  // show up in every skill folder.
348
331
  let dirents;
@@ -350,7 +333,7 @@ async function loadManagedEntries(targetSkillsDir, dryRun) {
350
333
  dirents = await fs.readdir(targetSkillsDir, { withFileTypes: true });
351
334
  }
352
335
  catch {
353
- return entries;
336
+ return { pluginEntries, otherEntries };
354
337
  }
355
338
  for (const dirent of dirents) {
356
339
  if (!dirent.isDirectory())
@@ -360,22 +343,23 @@ async function loadManagedEntries(targetSkillsDir, dryRun) {
360
343
  if (!legacy)
361
344
  continue;
362
345
  const managed = {
346
+ sourceType: 'plugin',
363
347
  pluginId: legacy.pluginId,
364
348
  pluginVersion: legacy.pluginVersion,
365
349
  sourceKind: legacy.sourceKind,
366
350
  sourceRelPath: legacy.sourceRelPath,
367
351
  destRelPath: dirent.name,
368
352
  };
369
- const already = entries.some((e) => e.destRelPath === managed.destRelPath &&
353
+ const already = pluginEntries.some((e) => e.destRelPath === managed.destRelPath &&
370
354
  e.pluginId === managed.pluginId &&
371
355
  e.sourceKind === managed.sourceKind &&
372
356
  e.sourceRelPath === managed.sourceRelPath);
373
357
  if (!already) {
374
- entries.push(managed);
358
+ pluginEntries.push(managed);
375
359
  }
376
360
  await removeLegacyMarkerFile(folder, dryRun);
377
361
  }
378
- return entries;
362
+ return { pluginEntries, otherEntries };
379
363
  }
380
364
  async function discoverLocalSkillNames(projectRoot) {
381
365
  const localSkillsDir = path.join(projectRoot, '.claude', 'skills');
@@ -406,6 +390,77 @@ async function discoverLocalSkillNames(projectRoot) {
406
390
  await walk(localSkillsDir, 0);
407
391
  return names;
408
392
  }
393
+ async function discoverLocalCommandNames(projectRoot) {
394
+ const localCommandsDir = path.join(projectRoot, '.claude', 'commands');
395
+ if (!(await fileExists(localCommandsDir)))
396
+ return new Set();
397
+ const names = new Set();
398
+ async function walk(current, depth) {
399
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
400
+ return;
401
+ let entries;
402
+ try {
403
+ entries = await fs.readdir(current, { withFileTypes: true });
404
+ }
405
+ catch {
406
+ return;
407
+ }
408
+ for (const entry of entries) {
409
+ const full = path.join(current, entry.name);
410
+ if (entry.isDirectory()) {
411
+ await walk(full, depth + 1);
412
+ continue;
413
+ }
414
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
415
+ continue;
416
+ names.add(sanitizeId(path.basename(entry.name, '.md')));
417
+ }
418
+ }
419
+ await walk(localCommandsDir, 0);
420
+ return names;
421
+ }
422
+ async function discoverLocalAgentNames(projectRoot) {
423
+ const localAgentsDir = path.join(projectRoot, '.claude', 'agents');
424
+ if (!(await fileExists(localAgentsDir)))
425
+ return new Set();
426
+ const names = new Set();
427
+ async function walk(current, depth) {
428
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
429
+ return;
430
+ let entries;
431
+ try {
432
+ entries = await fs.readdir(current, { withFileTypes: true });
433
+ }
434
+ catch {
435
+ return;
436
+ }
437
+ for (const entry of entries) {
438
+ const full = path.join(current, entry.name);
439
+ if (entry.isDirectory()) {
440
+ await walk(full, depth + 1);
441
+ continue;
442
+ }
443
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
444
+ continue;
445
+ let content;
446
+ try {
447
+ content = await fs.readFile(full, 'utf8');
448
+ }
449
+ catch {
450
+ continue;
451
+ }
452
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(content);
453
+ const fmName = parsed.rawFrontmatter && typeof parsed.rawFrontmatter.name === 'string'
454
+ ? parsed.rawFrontmatter.name
455
+ : parsed.frontmatter?.name;
456
+ if (typeof fmName !== 'string' || fmName.trim() === '')
457
+ continue;
458
+ names.add(sanitizeId(fmName.trim()));
459
+ }
460
+ }
461
+ await walk(localAgentsDir, 0);
462
+ return names;
463
+ }
409
464
  async function ensureDir(dir, dryRun) {
410
465
  if (dryRun)
411
466
  return;
@@ -439,15 +494,15 @@ async function rewriteSkillMdName(skillMdPath, name, pluginId, dryRun) {
439
494
  return;
440
495
  await fs.writeFile(skillMdPath, next, 'utf8');
441
496
  }
442
- async function writeCommandAsSkill(srcCommandPath, destDir, generatedName, pluginId, dryRun) {
443
- const content = await fs.readFile(srcCommandPath, 'utf8');
497
+ async function writeMarkdownAsSkill(srcMarkdownPath, destDir, generatedName, pluginId, kindLabel, dryRun) {
498
+ const content = await fs.readFile(srcMarkdownPath, 'utf8');
444
499
  const { rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(content);
445
500
  const fm = rawFrontmatter
446
501
  ? { ...rawFrontmatter }
447
502
  : {};
448
503
  fm.name = generatedName;
449
504
  if (typeof fm.description !== 'string' || fm.description.trim() === '') {
450
- fm.description = `Command from ${pluginId}: ${path.basename(srcCommandPath, '.md')}`;
505
+ fm.description = `${kindLabel} from ${pluginId}: ${path.basename(srcMarkdownPath, '.md')}`;
451
506
  }
452
507
  const next = `---\n${yaml
453
508
  .dump(fm, { lineWidth: -1, noRefs: true })
@@ -466,6 +521,13 @@ async function syncClaudePluginsToSkillsDirs(args) {
466
521
  const claudeDir = path.join(getUserHomeDir(), '.claude');
467
522
  const index = await readInstalledPluginsIndex(claudeDir);
468
523
  const localSkillNames = await discoverLocalSkillNames(projectRoot);
524
+ const localCommandNames = await discoverLocalCommandNames(projectRoot);
525
+ const localAgentNames = await discoverLocalAgentNames(projectRoot);
526
+ const localReservedNames = new Set([
527
+ ...localSkillNames,
528
+ ...localCommandNames,
529
+ ...localAgentNames,
530
+ ]);
469
531
  // If we can't read the installed plugins index, we can't install/update
470
532
  // anything, but we can still clean up managed folders for plugins that
471
533
  // are no longer enabled.
@@ -473,10 +535,10 @@ async function syncClaudePluginsToSkillsDirs(args) {
473
535
  for (const targetSkillsDir of targetSkillsDirs) {
474
536
  if (!(await fileExists(targetSkillsDir)))
475
537
  continue;
476
- const managedEntries = await loadManagedEntries(targetSkillsDir, dryRun);
538
+ const { pluginEntries: managedEntries, otherEntries } = await loadManagedEntries(targetSkillsDir, dryRun);
477
539
  const nextEntries = [];
478
540
  // Build reserved set (local skills always win).
479
- const reserved = new Set(localSkillNames);
541
+ const reserved = new Set(localReservedNames);
480
542
  // Also reserve any existing non-managed directories.
481
543
  const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
482
544
  let dirents = [];
@@ -508,7 +570,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
508
570
  }
509
571
  nextEntries.push(entry);
510
572
  }
511
- await writeManifestFile(targetSkillsDir, nextEntries, dryRun);
573
+ await (0, SkillsManifest_1.writeSkillsManifestEntries)(targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
512
574
  }
513
575
  return;
514
576
  }
@@ -553,6 +615,20 @@ async function syncClaudePluginsToSkillsDirs(args) {
553
615
  baseName,
554
616
  });
555
617
  }
618
+ const agentFiles = await discoverPluginAgentFiles(plugin.installPath);
619
+ for (const a of agentFiles) {
620
+ const baseName = sanitizeId(a.name);
621
+ const sourceRelPath = `agents/${a.rel}`;
622
+ expectedItems.push({
623
+ itemKey: makeItemKey(plugin.pluginId, 'agent', sourceRelPath),
624
+ pluginId: plugin.pluginId,
625
+ pluginVersion: plugin.version,
626
+ kind: 'agent',
627
+ sourcePath: a.file,
628
+ sourceRelPath,
629
+ baseName,
630
+ });
631
+ }
556
632
  }
557
633
  const sortedItems = [...expectedItems].sort((a, b) => {
558
634
  const ak = `${a.baseName}::${a.pluginId}::${a.kind}::${a.sourceRelPath}`;
@@ -564,9 +640,13 @@ async function syncClaudePluginsToSkillsDirs(args) {
564
640
  const targetExists = await fileExists(targetSkillsDir);
565
641
  if (!targetExists && sortedItems.length === 0)
566
642
  continue;
567
- const managedEntries = targetExists
568
- ? await loadManagedEntries(targetSkillsDir, dryRun)
569
- : [];
643
+ let managedEntries = [];
644
+ let otherEntries = [];
645
+ if (targetExists) {
646
+ const loaded = await loadManagedEntries(targetSkillsDir, dryRun);
647
+ managedEntries = loaded.pluginEntries;
648
+ otherEntries = loaded.otherEntries;
649
+ }
570
650
  // Map previous destinations by itemKey for stability.
571
651
  const prevDestByItemKey = new Map();
572
652
  for (const entry of managedEntries) {
@@ -574,7 +654,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
574
654
  }
575
655
  const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
576
656
  // Reserve: local skills always win. Also reserve any existing non-managed folders.
577
- const reserved = new Set(localSkillNames);
657
+ const reserved = new Set(localReservedNames);
578
658
  if (targetExists) {
579
659
  let dirents = [];
580
660
  try {
@@ -599,6 +679,11 @@ async function syncClaudePluginsToSkillsDirs(args) {
599
679
  const prev = prevDestByItemKey.get(item.itemKey);
600
680
  if (!prev)
601
681
  continue;
682
+ // Migration: previous versions used `${pluginId}__${name}`.
683
+ // Don't preserve legacy namespaced destinations so we can rename to the
684
+ // new `${pluginId}-${name}` format.
685
+ if (prev.startsWith(`${sanitizeId(item.pluginId)}__`))
686
+ continue;
602
687
  if (taken.has(prev))
603
688
  continue;
604
689
  assignedDestByItemKey.set(item.itemKey, prev);
@@ -618,7 +703,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
618
703
  let candidate = namespacedBase;
619
704
  let i = 2;
620
705
  while (taken.has(candidate)) {
621
- candidate = `${namespacedBase}__${i++}`;
706
+ candidate = `${namespacedBase}-${i++}`;
622
707
  }
623
708
  assignedDestByItemKey.set(item.itemKey, candidate);
624
709
  taken.add(candidate);
@@ -658,12 +743,13 @@ async function syncClaudePluginsToSkillsDirs(args) {
658
743
  await removeLegacyMarkerFile(destDir, dryRun);
659
744
  }
660
745
  else {
746
+ const kindLabel = item.kind === 'command' ? 'command' : 'agent';
661
747
  (0, constants_1.logVerboseInfo)(dryRun
662
- ? `DRY RUN: Would install plugin command '${destRelPath}' as skill to ${targetSkillsDir}`
663
- : `Installing plugin command '${destRelPath}' as skill to ${targetSkillsDir}`, verbose, dryRun);
748
+ ? `DRY RUN: Would install plugin ${kindLabel} '${destRelPath}' as skill to ${targetSkillsDir}`
749
+ : `Installing plugin ${kindLabel} '${destRelPath}' as skill to ${targetSkillsDir}`, verbose, dryRun);
664
750
  await ensureDir(destDir, dryRun);
665
751
  if (!dryRun) {
666
- await writeCommandAsSkill(item.sourcePath, destDir, destRelPath, item.pluginId, dryRun);
752
+ await writeMarkdownAsSkill(item.sourcePath, destDir, destRelPath, item.pluginId, item.kind === 'command' ? 'Command' : 'Agent', dryRun);
667
753
  }
668
754
  await removeLegacyMarkerFile(destDir, dryRun);
669
755
  }
@@ -706,6 +792,7 @@ async function syncClaudePluginsToSkillsDirs(args) {
706
792
  // Add expected entries for installed items
707
793
  for (const item of assignedItems) {
708
794
  nextEntries.push({
795
+ sourceType: 'plugin',
709
796
  pluginId: item.pluginId,
710
797
  pluginVersion: item.pluginVersion,
711
798
  sourceKind: item.kind,
@@ -713,6 +800,6 @@ async function syncClaudePluginsToSkillsDirs(args) {
713
800
  destRelPath: item.destRelPath,
714
801
  });
715
802
  }
716
- await writeManifestFile(targetSkillsDir, nextEntries, dryRun);
803
+ await (0, SkillsManifest_1.writeSkillsManifestEntries)(targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
717
804
  }
718
805
  }
@@ -0,0 +1,393 @@
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.syncClaudeProjectCommandsAndAgentsToSkillsDirs = syncClaudeProjectCommandsAndAgentsToSkillsDirs;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ const yaml = __importStar(require("js-yaml"));
40
+ const constants_1 = require("../constants");
41
+ const FrontmatterParser_1 = require("./FrontmatterParser");
42
+ const SkillsManifest_1 = require("./SkillsManifest");
43
+ const LEGACY_PLUGIN_MARKER_FILENAME = '.skiller-plugin.json';
44
+ function sanitizeId(value) {
45
+ return value.replace(/[^A-Za-z0-9._-]+/g, '_');
46
+ }
47
+ async function fileExists(p) {
48
+ try {
49
+ await fs.access(p);
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ function makeItemKey(sourceKind, sourceRelPath) {
57
+ return `${sourceKind}::${sourceRelPath}`;
58
+ }
59
+ function getCommandsRoot(projectRoot) {
60
+ return path.join(projectRoot, '.claude', 'commands');
61
+ }
62
+ function getAgentsRoot(projectRoot) {
63
+ return path.join(projectRoot, '.claude', 'agents');
64
+ }
65
+ async function discoverCommandFiles(projectRoot) {
66
+ const commandsRoot = getCommandsRoot(projectRoot);
67
+ if (!(await fileExists(commandsRoot)))
68
+ return [];
69
+ const results = [];
70
+ async function walk(current, depth) {
71
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
72
+ return;
73
+ let entries;
74
+ try {
75
+ entries = await fs.readdir(current, { withFileTypes: true });
76
+ }
77
+ catch {
78
+ return;
79
+ }
80
+ for (const entry of entries) {
81
+ const full = path.join(current, entry.name);
82
+ if (entry.isDirectory()) {
83
+ await walk(full, depth + 1);
84
+ continue;
85
+ }
86
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
87
+ continue;
88
+ const name = sanitizeId(path.basename(entry.name, '.md'));
89
+ const rel = path.relative(projectRoot, full).replace(/\\/g, '/');
90
+ results.push({ name, file: full, rel });
91
+ }
92
+ }
93
+ await walk(commandsRoot, 0);
94
+ return results;
95
+ }
96
+ async function discoverAgentFiles(projectRoot) {
97
+ const agentsRoot = getAgentsRoot(projectRoot);
98
+ if (!(await fileExists(agentsRoot)))
99
+ return [];
100
+ const results = [];
101
+ async function walk(current, depth) {
102
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
103
+ return;
104
+ let entries;
105
+ try {
106
+ entries = await fs.readdir(current, { withFileTypes: true });
107
+ }
108
+ catch {
109
+ return;
110
+ }
111
+ for (const entry of entries) {
112
+ const full = path.join(current, entry.name);
113
+ if (entry.isDirectory()) {
114
+ await walk(full, depth + 1);
115
+ continue;
116
+ }
117
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
118
+ continue;
119
+ let content;
120
+ try {
121
+ content = await fs.readFile(full, 'utf8');
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ const parsed = (0, FrontmatterParser_1.parseFrontmatter)(content);
127
+ const fmName = parsed.rawFrontmatter && typeof parsed.rawFrontmatter.name === 'string'
128
+ ? parsed.rawFrontmatter.name
129
+ : parsed.frontmatter?.name;
130
+ if (typeof fmName !== 'string' || fmName.trim() === '')
131
+ continue;
132
+ const name = sanitizeId(fmName.trim());
133
+ const rel = path.relative(projectRoot, full).replace(/\\/g, '/');
134
+ results.push({ name, file: full, rel });
135
+ }
136
+ }
137
+ await walk(agentsRoot, 0);
138
+ return results;
139
+ }
140
+ async function ensureDir(dir, dryRun) {
141
+ if (dryRun)
142
+ return;
143
+ await fs.mkdir(dir, { recursive: true });
144
+ }
145
+ async function removeDir(dir, dryRun) {
146
+ if (dryRun)
147
+ return;
148
+ await fs.rm(dir, { recursive: true, force: true });
149
+ }
150
+ async function writeMarkdownAsSkill(srcPath, destDir, generatedName, kindLabel, dryRun) {
151
+ const content = await fs.readFile(srcPath, 'utf8');
152
+ const { rawFrontmatter, body } = (0, FrontmatterParser_1.parseFrontmatter)(content);
153
+ const fm = rawFrontmatter
154
+ ? { ...rawFrontmatter }
155
+ : {};
156
+ fm.name = generatedName;
157
+ if (typeof fm.description !== 'string' || fm.description.trim() === '') {
158
+ fm.description = `${kindLabel}: ${path.basename(srcPath, '.md')}`;
159
+ }
160
+ const next = `---\n${yaml
161
+ .dump(fm, { lineWidth: -1, noRefs: true })
162
+ .trim()}\n---\n\n${body}\n`;
163
+ if (dryRun)
164
+ return;
165
+ await fs.writeFile(path.join(destDir, 'SKILL.md'), next, 'utf8');
166
+ }
167
+ async function readPluginManagedDestNames(targetSkillsDir) {
168
+ const names = new Set();
169
+ for (const entry of await (0, SkillsManifest_1.loadSkillsManifestEntries)(targetSkillsDir)) {
170
+ if ((0, SkillsManifest_1.isPluginManifestEntry)(entry)) {
171
+ names.add(entry.destRelPath);
172
+ }
173
+ }
174
+ // Legacy: prior versions wrote per-skill plugin marker files. Treat any
175
+ // folder containing one as plugin-managed so project items can take over.
176
+ try {
177
+ const dirents = await fs.readdir(targetSkillsDir, { withFileTypes: true });
178
+ for (const d of dirents) {
179
+ if (!d.isDirectory())
180
+ continue;
181
+ try {
182
+ await fs.access(path.join(targetSkillsDir, d.name, LEGACY_PLUGIN_MARKER_FILENAME));
183
+ names.add(d.name);
184
+ }
185
+ catch {
186
+ // ignore
187
+ }
188
+ }
189
+ }
190
+ catch {
191
+ // ignore
192
+ }
193
+ return names;
194
+ }
195
+ async function discoverLocalSkillNames(projectRoot) {
196
+ const localSkillsDir = path.join(projectRoot, '.claude', 'skills');
197
+ if (!(await fileExists(localSkillsDir)))
198
+ return new Set();
199
+ const names = new Set();
200
+ async function walk(current, depth) {
201
+ if (depth >= constants_1.MAX_RECURSION_DEPTH)
202
+ return;
203
+ let entries;
204
+ try {
205
+ entries = await fs.readdir(current, { withFileTypes: true });
206
+ }
207
+ catch {
208
+ return;
209
+ }
210
+ const hasSkillMd = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
211
+ if (hasSkillMd) {
212
+ names.add(path.basename(current));
213
+ return;
214
+ }
215
+ for (const entry of entries) {
216
+ if (!entry.isDirectory())
217
+ continue;
218
+ await walk(path.join(current, entry.name), depth + 1);
219
+ }
220
+ }
221
+ await walk(localSkillsDir, 0);
222
+ return names;
223
+ }
224
+ async function syncClaudeProjectCommandsAndAgentsToSkillsDirs(args) {
225
+ const { projectRoot, targetSkillsDirs, verbose, dryRun } = args;
226
+ const localSkillNames = await discoverLocalSkillNames(projectRoot);
227
+ const commands = await discoverCommandFiles(projectRoot);
228
+ const agents = await discoverAgentFiles(projectRoot);
229
+ const expectedItems = [];
230
+ for (const cmd of commands) {
231
+ expectedItems.push({
232
+ itemKey: makeItemKey('command', cmd.rel),
233
+ sourceKind: 'command',
234
+ sourcePath: cmd.file,
235
+ sourceRelPath: cmd.rel,
236
+ baseName: cmd.name,
237
+ });
238
+ }
239
+ for (const agent of agents) {
240
+ expectedItems.push({
241
+ itemKey: makeItemKey('agent', agent.rel),
242
+ sourceKind: 'agent',
243
+ sourcePath: agent.file,
244
+ sourceRelPath: agent.rel,
245
+ baseName: agent.name,
246
+ });
247
+ }
248
+ const sortedItems = [...expectedItems].sort((a, b) => {
249
+ const ak = `${a.baseName}::${a.sourceKind}::${a.sourceRelPath}`;
250
+ const bk = `${b.baseName}::${b.sourceKind}::${b.sourceRelPath}`;
251
+ return ak.localeCompare(bk);
252
+ });
253
+ for (const targetSkillsDir of targetSkillsDirs) {
254
+ const targetExists = await fileExists(targetSkillsDir);
255
+ const managedEntries = [];
256
+ const otherEntries = [];
257
+ if (targetExists) {
258
+ const allEntries = await (0, SkillsManifest_1.loadSkillsManifestEntries)(targetSkillsDir);
259
+ for (const entry of allEntries) {
260
+ if ((0, SkillsManifest_1.isClaudeManifestEntry)(entry)) {
261
+ managedEntries.push(entry);
262
+ }
263
+ else {
264
+ otherEntries.push(entry);
265
+ }
266
+ }
267
+ }
268
+ const prevDestByItemKey = new Map();
269
+ for (const entry of managedEntries) {
270
+ prevDestByItemKey.set(makeItemKey(entry.sourceKind, entry.sourceRelPath), entry.destRelPath);
271
+ }
272
+ const managedDest = new Set(managedEntries.map((e) => e.destRelPath));
273
+ const pluginManagedDest = targetExists
274
+ ? await readPluginManagedDestNames(targetSkillsDir)
275
+ : new Set();
276
+ const reserved = new Set(localSkillNames);
277
+ if (targetExists) {
278
+ let dirents = [];
279
+ try {
280
+ dirents = await fs.readdir(targetSkillsDir, { withFileTypes: true });
281
+ }
282
+ catch {
283
+ dirents = [];
284
+ }
285
+ for (const d of dirents) {
286
+ if (!d.isDirectory())
287
+ continue;
288
+ // Reserve any existing folder we do not manage, except plugin-managed
289
+ // folders (project should be able to take those over).
290
+ if (!managedDest.has(d.name) && !pluginManagedDest.has(d.name)) {
291
+ reserved.add(d.name);
292
+ }
293
+ }
294
+ }
295
+ const taken = new Set(reserved);
296
+ const assignedDestByItemKey = new Map();
297
+ // Preserve previous destinations when they are still available.
298
+ for (const item of sortedItems) {
299
+ const prev = prevDestByItemKey.get(item.itemKey);
300
+ if (!prev)
301
+ continue;
302
+ // Migration: previous versions used `claude__<name>`.
303
+ // Don't preserve legacy namespaced destinations so we can rename to the
304
+ // new `claude-<name>` format.
305
+ if (prev.startsWith('claude__'))
306
+ continue;
307
+ if (taken.has(prev))
308
+ continue;
309
+ assignedDestByItemKey.set(item.itemKey, prev);
310
+ taken.add(prev);
311
+ }
312
+ // Assign baseName, otherwise namespace with "claude-".
313
+ for (const item of sortedItems) {
314
+ if (assignedDestByItemKey.has(item.itemKey))
315
+ continue;
316
+ const base = item.baseName;
317
+ if (!taken.has(base)) {
318
+ assignedDestByItemKey.set(item.itemKey, base);
319
+ taken.add(base);
320
+ continue;
321
+ }
322
+ const namespacedBase = `claude-${base}`;
323
+ let candidate = namespacedBase;
324
+ let i = 2;
325
+ while (taken.has(candidate)) {
326
+ candidate = `${namespacedBase}-${i++}`;
327
+ }
328
+ assignedDestByItemKey.set(item.itemKey, candidate);
329
+ taken.add(candidate);
330
+ }
331
+ const assignedItems = sortedItems.map((item) => ({
332
+ ...item,
333
+ destRelPath: assignedDestByItemKey.get(item.itemKey),
334
+ }));
335
+ if (assignedItems.length > 0) {
336
+ await ensureDir(targetSkillsDir, dryRun);
337
+ }
338
+ else if (!targetExists && managedEntries.length === 0) {
339
+ // Nothing to do.
340
+ continue;
341
+ }
342
+ // Install/update expected items
343
+ for (const item of assignedItems) {
344
+ const destRelPath = item.destRelPath;
345
+ const destDir = path.join(targetSkillsDir, destRelPath);
346
+ if (await fileExists(destDir)) {
347
+ const isManagedByProject = managedDest.has(destRelPath);
348
+ const isManagedByPlugin = pluginManagedDest.has(destRelPath);
349
+ if (!isManagedByProject && !isManagedByPlugin) {
350
+ (0, constants_1.logWarn)(`[claude] Destination exists but is not skiller-managed, skipping: ${destDir}`, dryRun);
351
+ continue;
352
+ }
353
+ (0, constants_1.logVerboseInfo)(dryRun
354
+ ? `DRY RUN: Would update claude ${item.sourceKind} '${destRelPath}' in ${targetSkillsDir}`
355
+ : `Updating claude ${item.sourceKind} '${destRelPath}' in ${targetSkillsDir}`, verbose, dryRun);
356
+ await removeDir(destDir, dryRun);
357
+ }
358
+ (0, constants_1.logVerboseInfo)(dryRun
359
+ ? `DRY RUN: Would install claude ${item.sourceKind} '${destRelPath}' to ${targetSkillsDir}`
360
+ : `Installing claude ${item.sourceKind} '${destRelPath}' to ${targetSkillsDir}`, verbose, dryRun);
361
+ await ensureDir(destDir, dryRun);
362
+ if (!dryRun) {
363
+ await writeMarkdownAsSkill(item.sourcePath, destDir, destRelPath, item.sourceKind === 'command' ? 'Command' : 'Agent', dryRun);
364
+ }
365
+ }
366
+ // Cleanup stale managed folders
367
+ const expectedDest = new Set(assignedItems.map((i) => i.destRelPath));
368
+ const nextEntries = [];
369
+ for (const entry of managedEntries) {
370
+ if (expectedDest.has(entry.destRelPath)) {
371
+ // Still expected; re-add below.
372
+ continue;
373
+ }
374
+ if (reserved.has(entry.destRelPath)) {
375
+ // User/local took over the folder name; stop managing it but don't delete.
376
+ continue;
377
+ }
378
+ (0, constants_1.logVerboseInfo)(dryRun
379
+ ? `DRY RUN: Would remove stale claude skill '${entry.destRelPath}' from ${targetSkillsDir}`
380
+ : `Removing stale claude skill '${entry.destRelPath}' from ${targetSkillsDir}`, verbose, dryRun);
381
+ await removeDir(path.join(targetSkillsDir, entry.destRelPath), dryRun);
382
+ }
383
+ for (const item of assignedItems) {
384
+ nextEntries.push({
385
+ sourceType: 'claude',
386
+ sourceKind: item.sourceKind,
387
+ sourceRelPath: item.sourceRelPath,
388
+ destRelPath: item.destRelPath,
389
+ });
390
+ }
391
+ await (0, SkillsManifest_1.writeSkillsManifestEntries)(targetSkillsDir, [...otherEntries, ...nextEntries], dryRun);
392
+ }
393
+ }
@@ -0,0 +1,285 @@
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.SKILLS_MANIFEST_VERSION = exports.LEGACY_CLAUDE_MANIFEST_FILENAME = exports.LEGACY_PLUGIN_MANIFEST_FILENAME = exports.LEGACY_UNIFIED_SKILLS_MANIFEST_FILENAME = exports.SKILLS_MANIFEST_FILENAME = void 0;
37
+ exports.isPluginManifestEntry = isPluginManifestEntry;
38
+ exports.isClaudeManifestEntry = isClaudeManifestEntry;
39
+ exports.loadSkillsManifestEntries = loadSkillsManifestEntries;
40
+ exports.writeSkillsManifestEntries = writeSkillsManifestEntries;
41
+ exports.listSkillDirectories = listSkillDirectories;
42
+ const fs = __importStar(require("fs/promises"));
43
+ const path = __importStar(require("path"));
44
+ exports.SKILLS_MANIFEST_FILENAME = '.skiller.json';
45
+ exports.LEGACY_UNIFIED_SKILLS_MANIFEST_FILENAME = '.skiller-skills.json';
46
+ exports.LEGACY_PLUGIN_MANIFEST_FILENAME = '.skiller-plugins.json';
47
+ exports.LEGACY_CLAUDE_MANIFEST_FILENAME = '.skiller-claude.json';
48
+ exports.SKILLS_MANIFEST_VERSION = 1;
49
+ async function fileExists(p) {
50
+ try {
51
+ await fs.access(p);
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ function isPluginManifestEntry(entry) {
59
+ return entry.sourceType === 'plugin';
60
+ }
61
+ function isClaudeManifestEntry(entry) {
62
+ return entry.sourceType === 'claude';
63
+ }
64
+ function normalizeEntries(entries) {
65
+ // Dedupe by a stable identity that doesn't depend on JSON key order.
66
+ const seen = new Set();
67
+ const out = [];
68
+ for (const e of entries) {
69
+ const key = e.sourceType === 'plugin'
70
+ ? `plugin::${e.destRelPath}::${e.pluginId}::${e.sourceKind}::${e.sourceRelPath}`
71
+ : `claude::${e.destRelPath}::${e.sourceKind}::${e.sourceRelPath}`;
72
+ if (seen.has(key))
73
+ continue;
74
+ seen.add(key);
75
+ out.push(e);
76
+ }
77
+ out.sort((a, b) => {
78
+ const ak = a.sourceType === 'plugin'
79
+ ? `plugin::${a.destRelPath}::${a.pluginId}::${a.sourceKind}::${a.sourceRelPath}`
80
+ : `claude::${a.destRelPath}::${a.sourceKind}::${a.sourceRelPath}`;
81
+ const bk = b.sourceType === 'plugin'
82
+ ? `plugin::${b.destRelPath}::${b.pluginId}::${b.sourceKind}::${b.sourceRelPath}`
83
+ : `claude::${b.destRelPath}::${b.sourceKind}::${b.sourceRelPath}`;
84
+ return ak.localeCompare(bk);
85
+ });
86
+ return out;
87
+ }
88
+ function parseUnifiedEntries(raw) {
89
+ if (!raw || typeof raw !== 'object')
90
+ return [];
91
+ const obj = raw;
92
+ if (!Array.isArray(obj.entries))
93
+ return [];
94
+ const entries = [];
95
+ for (const entry of obj.entries) {
96
+ if (!entry || typeof entry !== 'object')
97
+ continue;
98
+ const e = entry;
99
+ const sourceType = e.sourceType;
100
+ const sourceKind = e.sourceKind;
101
+ if (sourceType === 'plugin') {
102
+ if (typeof e.pluginId !== 'string')
103
+ continue;
104
+ if (sourceKind !== 'skill' &&
105
+ sourceKind !== 'command' &&
106
+ sourceKind !== 'agent')
107
+ continue;
108
+ if (typeof e.sourceRelPath !== 'string')
109
+ continue;
110
+ if (typeof e.destRelPath !== 'string')
111
+ continue;
112
+ entries.push({
113
+ sourceType: 'plugin',
114
+ pluginId: e.pluginId,
115
+ pluginVersion: typeof e.pluginVersion === 'string' ? e.pluginVersion : undefined,
116
+ sourceKind,
117
+ sourceRelPath: e.sourceRelPath,
118
+ destRelPath: e.destRelPath,
119
+ });
120
+ continue;
121
+ }
122
+ if (sourceType === 'claude') {
123
+ if (sourceKind !== 'command' && sourceKind !== 'agent')
124
+ continue;
125
+ if (typeof e.sourceRelPath !== 'string')
126
+ continue;
127
+ if (typeof e.destRelPath !== 'string')
128
+ continue;
129
+ entries.push({
130
+ sourceType: 'claude',
131
+ sourceKind,
132
+ sourceRelPath: e.sourceRelPath,
133
+ destRelPath: e.destRelPath,
134
+ });
135
+ continue;
136
+ }
137
+ }
138
+ return entries;
139
+ }
140
+ function parseLegacyPluginEntries(raw) {
141
+ if (!raw || typeof raw !== 'object')
142
+ return [];
143
+ const obj = raw;
144
+ if (!Array.isArray(obj.entries))
145
+ return [];
146
+ const entries = [];
147
+ for (const entry of obj.entries) {
148
+ if (!entry || typeof entry !== 'object')
149
+ continue;
150
+ const e = entry;
151
+ if (typeof e.pluginId !== 'string')
152
+ continue;
153
+ const sourceKind = e.sourceKind;
154
+ if (sourceKind !== 'skill' &&
155
+ sourceKind !== 'command' &&
156
+ sourceKind !== 'agent')
157
+ continue;
158
+ if (typeof e.sourceRelPath !== 'string')
159
+ continue;
160
+ if (typeof e.destRelPath !== 'string')
161
+ continue;
162
+ entries.push({
163
+ sourceType: 'plugin',
164
+ pluginId: e.pluginId,
165
+ pluginVersion: typeof e.pluginVersion === 'string' ? e.pluginVersion : undefined,
166
+ sourceKind,
167
+ sourceRelPath: e.sourceRelPath,
168
+ destRelPath: e.destRelPath,
169
+ });
170
+ }
171
+ return entries;
172
+ }
173
+ function parseLegacyClaudeEntries(raw) {
174
+ if (!raw || typeof raw !== 'object')
175
+ return [];
176
+ const obj = raw;
177
+ if (!Array.isArray(obj.entries))
178
+ return [];
179
+ const entries = [];
180
+ for (const entry of obj.entries) {
181
+ if (!entry || typeof entry !== 'object')
182
+ continue;
183
+ const e = entry;
184
+ const sourceKind = e.sourceKind;
185
+ if (sourceKind !== 'command' && sourceKind !== 'agent')
186
+ continue;
187
+ if (typeof e.sourceRelPath !== 'string')
188
+ continue;
189
+ if (typeof e.destRelPath !== 'string')
190
+ continue;
191
+ entries.push({
192
+ sourceType: 'claude',
193
+ sourceKind,
194
+ sourceRelPath: e.sourceRelPath,
195
+ destRelPath: e.destRelPath,
196
+ });
197
+ }
198
+ return entries;
199
+ }
200
+ async function loadSkillsManifestEntries(targetSkillsDir) {
201
+ const manifestPath = path.join(targetSkillsDir, exports.SKILLS_MANIFEST_FILENAME);
202
+ if (await fileExists(manifestPath)) {
203
+ try {
204
+ const raw = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
205
+ return normalizeEntries(parseUnifiedEntries(raw));
206
+ }
207
+ catch {
208
+ return [];
209
+ }
210
+ }
211
+ // Migration: previous versions used a different unified filename.
212
+ const legacyUnifiedPath = path.join(targetSkillsDir, exports.LEGACY_UNIFIED_SKILLS_MANIFEST_FILENAME);
213
+ if (await fileExists(legacyUnifiedPath)) {
214
+ try {
215
+ const raw = JSON.parse(await fs.readFile(legacyUnifiedPath, 'utf8'));
216
+ return normalizeEntries(parseUnifiedEntries(raw));
217
+ }
218
+ catch {
219
+ return [];
220
+ }
221
+ }
222
+ // No unified manifest yet: merge legacy manifests if present.
223
+ const merged = [];
224
+ const legacyPluginPath = path.join(targetSkillsDir, exports.LEGACY_PLUGIN_MANIFEST_FILENAME);
225
+ if (await fileExists(legacyPluginPath)) {
226
+ try {
227
+ const raw = JSON.parse(await fs.readFile(legacyPluginPath, 'utf8'));
228
+ merged.push(...parseLegacyPluginEntries(raw));
229
+ }
230
+ catch {
231
+ // ignore
232
+ }
233
+ }
234
+ const legacyClaudePath = path.join(targetSkillsDir, exports.LEGACY_CLAUDE_MANIFEST_FILENAME);
235
+ if (await fileExists(legacyClaudePath)) {
236
+ try {
237
+ const raw = JSON.parse(await fs.readFile(legacyClaudePath, 'utf8'));
238
+ merged.push(...parseLegacyClaudeEntries(raw));
239
+ }
240
+ catch {
241
+ // ignore
242
+ }
243
+ }
244
+ return normalizeEntries(merged);
245
+ }
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);
251
+ const normalized = normalizeEntries(entries);
252
+ 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;
262
+ }
263
+ const manifest = {
264
+ version: exports.SKILLS_MANIFEST_VERSION,
265
+ entries: normalized,
266
+ };
267
+ if (dryRun)
268
+ return;
269
+ await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
270
+ // Clean up legacy manifests once unified is written.
271
+ await Promise.allSettled([
272
+ fs.unlink(legacyUnifiedPath),
273
+ fs.unlink(legacyPluginPath),
274
+ fs.unlink(legacyClaudePath),
275
+ ]);
276
+ }
277
+ async function listSkillDirectories(targetSkillsDir) {
278
+ try {
279
+ const dirents = await fs.readdir(targetSkillsDir, { withFileTypes: true });
280
+ return dirents.filter((d) => d.isDirectory());
281
+ }
282
+ catch {
283
+ return [];
284
+ }
285
+ }
@@ -589,6 +589,17 @@ async function propagateSkills(projectRoot, agents, skillsEnabled, verbose, dryR
589
589
  }
590
590
  }
591
591
  }
592
+ // Sync project Claude commands + agents as skills into agent skills dirs.
593
+ // This intentionally does NOT write into the committed .claude/skills source-of-truth.
594
+ if (destinationPaths.size > 0) {
595
+ const { syncClaudeProjectCommandsAndAgentsToSkillsDirs } = await Promise.resolve().then(() => __importStar(require('./ClaudeProjectSync')));
596
+ await syncClaudeProjectCommandsAndAgentsToSkillsDirs({
597
+ projectRoot,
598
+ targetSkillsDirs: [...destinationPaths],
599
+ verbose,
600
+ dryRun,
601
+ });
602
+ }
592
603
  // Sync Claude plugins (skills + commands converted to skills) into agent skills dirs.
593
604
  // This intentionally does NOT write into the committed .claude/skills source-of-truth.
594
605
  if (destinationPaths.size > 0) {
package/package.json CHANGED
@@ -1,79 +1,79 @@
1
1
  {
2
- "name": "skiller",
3
- "version": "0.7.15",
4
- "description": "Skiller — apply the same rules to all coding agents",
5
- "main": "dist/lib.js",
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
- "scripts": {
10
- "postinstall": "[ -n \"$CI\" ] || npx skiller@latest apply",
11
- "lint": "eslint \"src/**/*.{ts,tsx}\"",
12
- "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
13
- "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
14
- "test": "jest",
15
- "test:watch": "jest --watch",
16
- "test:coverage": "jest --coverage",
17
- "test:integration": "jest tests/e2e/skiller.integration.test.ts --verbose",
18
- "build": "tsc",
19
- "release": "pnpm build && pnpm changeset publish"
20
- },
21
- "repository": {
22
- "type": "git",
23
- "url": "git+https://github.com/udecode/skiller.git"
24
- },
25
- "keywords": [
26
- "ai",
27
- "developer-tools",
28
- "copilot",
29
- "codex",
30
- "claude",
31
- "cursor",
32
- "aider",
33
- "config",
34
- "rules",
35
- "automation"
36
- ],
37
- "author": "Eleanor Berger",
38
- "license": "MIT",
39
- "bugs": {
40
- "url": "https://github.com/udecode/skiller/issues"
41
- },
42
- "engines": {
43
- "node": ">=18"
44
- },
45
- "files": [
46
- "dist",
47
- "README.md",
48
- "LICENSE"
49
- ],
50
- "bin": {
51
- "skiller": "dist/cli/index.js"
52
- },
53
- "devDependencies": {
54
- "@changesets/changelog-github": "^0.5.2",
55
- "@changesets/cli": "2.29.8",
56
- "@eslint/js": "^9.39.1",
57
- "@types/iarna__toml": "^2.0.5",
58
- "@types/jest": "^29.5.14",
59
- "@types/js-yaml": "^4.0.9",
60
- "@types/node": "^24.9.2",
61
- "@types/yargs": "^17.0.34",
62
- "@typescript-eslint/eslint-plugin": "^8.46.2",
63
- "@typescript-eslint/parser": "^8.46.2",
64
- "eslint": "^9.38.0",
65
- "eslint-config-prettier": "^10.1.8",
66
- "eslint-plugin-prettier": "^5.5.4",
67
- "jest": "^29.7.0",
68
- "prettier": "^3.6.2",
69
- "ts-jest": "^29.4.5",
70
- "typescript": "^5.9.3",
71
- "typescript-eslint": "^8.46.2"
72
- },
73
- "dependencies": {
74
- "@iarna/toml": "^2.2.5",
75
- "js-yaml": "^4.1.0",
76
- "yargs": "^18.0.0",
77
- "zod": "^4.1.12"
78
- }
2
+ "name": "skiller",
3
+ "version": "0.7.17",
4
+ "description": "Skiller — apply the same rules to all coding agents",
5
+ "main": "dist/lib.js",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "[ -n \"$CI\" ] || npx skiller@latest apply",
11
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
12
+ "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
13
+ "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
14
+ "test": "jest",
15
+ "test:watch": "jest --watch",
16
+ "test:coverage": "jest --coverage",
17
+ "test:integration": "jest tests/e2e/skiller.integration.test.ts --verbose",
18
+ "build": "tsc",
19
+ "release": "pnpm build && pnpm changeset publish"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/udecode/skiller.git"
24
+ },
25
+ "keywords": [
26
+ "ai",
27
+ "developer-tools",
28
+ "copilot",
29
+ "codex",
30
+ "claude",
31
+ "cursor",
32
+ "aider",
33
+ "config",
34
+ "rules",
35
+ "automation"
36
+ ],
37
+ "author": "Eleanor Berger",
38
+ "license": "MIT",
39
+ "bugs": {
40
+ "url": "https://github.com/udecode/skiller/issues"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "bin": {
51
+ "skiller": "dist/cli/index.js"
52
+ },
53
+ "devDependencies": {
54
+ "@changesets/changelog-github": "^0.5.2",
55
+ "@changesets/cli": "2.29.8",
56
+ "@eslint/js": "^9.39.1",
57
+ "@types/iarna__toml": "^2.0.5",
58
+ "@types/jest": "^29.5.14",
59
+ "@types/js-yaml": "^4.0.9",
60
+ "@types/node": "^24.9.2",
61
+ "@types/yargs": "^17.0.34",
62
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
63
+ "@typescript-eslint/parser": "^8.46.2",
64
+ "eslint": "^9.38.0",
65
+ "eslint-config-prettier": "^10.1.8",
66
+ "eslint-plugin-prettier": "^5.5.4",
67
+ "jest": "^29.7.0",
68
+ "prettier": "^3.6.2",
69
+ "ts-jest": "^29.4.5",
70
+ "typescript": "^5.9.3",
71
+ "typescript-eslint": "^8.46.2"
72
+ },
73
+ "dependencies": {
74
+ "@iarna/toml": "^2.2.5",
75
+ "js-yaml": "^4.1.0",
76
+ "yargs": "^18.0.0",
77
+ "zod": "^4.1.12"
78
+ }
79
79
  }