@@ -419,6 +419,217 @@ def _unregister_commands(self, registered_commands: Dict[str, List[str]]) -> Non
419419 registrar = CommandRegistrar ()
420420 registrar .unregister_commands (registered_commands , self .project_root )
421421
422+ def _get_skills_dir (self ) -> Optional [Path ]:
423+ """Return the skills directory if ``--ai-skills`` was used during init.
424+
425+ Reads ``.specify/init-options.json`` to determine whether skills
426+ are enabled and which agent was selected, then delegates to
427+ ``_get_skills_dir()`` for the concrete path.
428+
429+ Returns:
430+ The skills directory ``Path``, or ``None`` if skills were not
431+ enabled or the init-options file is missing.
432+ """
433+ from . import load_init_options , _get_skills_dir
434+
435+ opts = load_init_options (self .project_root )
436+ if not opts .get ("ai_skills" ):
437+ return None
438+
439+ agent = opts .get ("ai" )
440+ if not agent :
441+ return None
442+
443+ skills_dir = _get_skills_dir (self .project_root , agent )
444+ if not skills_dir .is_dir ():
445+ return None
446+
447+ return skills_dir
448+
449+ def _register_skills (
450+ self ,
451+ manifest : "PresetManifest" ,
452+ preset_dir : Path ,
453+ ) -> List [str ]:
454+ """Generate SKILL.md files for preset command overrides.
455+
456+ For every command template in the preset, checks whether a
457+ corresponding skill already exists in any detected skills
458+ directory. If so, the skill is overwritten with content derived
459+ from the preset's command file. This ensures that presets that
460+ override commands also propagate to the agentskills.io skill
461+ layer when ``--ai-skills`` was used during project initialisation.
462+
463+ Args:
464+ manifest: Preset manifest.
465+ preset_dir: Installed preset directory.
466+
467+ Returns:
468+ List of skill names that were written (for registry storage).
469+ """
470+ command_templates = [
471+ t for t in manifest .templates if t .get ("type" ) == "command"
472+ ]
473+ if not command_templates :
474+ return []
475+
476+ skills_dir = self ._get_skills_dir ()
477+ if not skills_dir :
478+ return []
479+
480+ from . import SKILL_DESCRIPTIONS
481+
482+ written : List [str ] = []
483+
484+ for cmd_tmpl in command_templates :
485+ cmd_name = cmd_tmpl ["name" ]
486+ cmd_file_rel = cmd_tmpl ["file" ]
487+ source_file = preset_dir / cmd_file_rel
488+ if not source_file .exists ():
489+ continue
490+
491+ # Derive the short command name (e.g. "specify" from "speckit.specify")
492+ short_name = cmd_name
493+ if short_name .startswith ("speckit." ):
494+ short_name = short_name [len ("speckit." ):]
495+ skill_name = f"speckit-{ short_name } "
496+
497+ # Only overwrite if the skill already exists (i.e. --ai-skills was used)
498+ skill_subdir = skills_dir / skill_name
499+ if not skill_subdir .exists ():
500+ continue
501+
502+ # Parse the command file
503+ content = source_file .read_text (encoding = "utf-8" )
504+ if content .startswith ("---" ):
505+ parts = content .split ("---" , 2 )
506+ if len (parts ) >= 3 :
507+ frontmatter = yaml .safe_load (parts [1 ])
508+ if not isinstance (frontmatter , dict ):
509+ frontmatter = {}
510+ body = parts [2 ].strip ()
511+ else :
512+ frontmatter = {}
513+ body = content
514+ else :
515+ frontmatter = {}
516+ body = content
517+
518+ original_desc = frontmatter .get ("description" , "" )
519+ enhanced_desc = SKILL_DESCRIPTIONS .get (
520+ short_name ,
521+ original_desc or f"Spec-kit workflow command: { short_name } " ,
522+ )
523+
524+ frontmatter_data = {
525+ "name" : skill_name ,
526+ "description" : enhanced_desc ,
527+ "compatibility" : "Requires spec-kit project structure with .specify/ directory" ,
528+ "metadata" : {
529+ "author" : "github-spec-kit" ,
530+ "source" : f"preset:{ manifest .id } " ,
531+ },
532+ }
533+ frontmatter_text = yaml .safe_dump (frontmatter_data , sort_keys = False ).strip ()
534+ skill_content = (
535+ f"---\n "
536+ f"{ frontmatter_text } \n "
537+ f"---\n \n "
538+ f"# Speckit { short_name .title ()} Skill\n \n "
539+ f"{ body } \n "
540+ )
541+
542+ skill_file = skill_subdir / "SKILL.md"
543+ skill_file .write_text (skill_content , encoding = "utf-8" )
544+ written .append (skill_name )
545+
546+ return written
547+
548+ def _unregister_skills (self , skill_names : List [str ], preset_dir : Path ) -> None :
549+ """Restore original SKILL.md files after a preset is removed.
550+
551+ For each skill that was overridden by the preset, attempts to
552+ regenerate the skill from the core command template. If no core
553+ template exists, the skill directory is removed.
554+
555+ Args:
556+ skill_names: List of skill names written by the preset.
557+ preset_dir: The preset's installed directory (may already be deleted).
558+ """
559+ if not skill_names :
560+ return
561+
562+ skills_dir = self ._get_skills_dir ()
563+ if not skills_dir :
564+ return
565+
566+ from . import SKILL_DESCRIPTIONS
567+
568+ # Locate core command templates
569+ script_dir = Path (__file__ ).parent .parent .parent # up from src/specify_cli/
570+ core_templates_dir = script_dir / "templates" / "commands"
571+
572+ for skill_name in skill_names :
573+ # Derive command name from skill name (speckit-specify -> specify)
574+ short_name = skill_name
575+ if short_name .startswith ("speckit-" ):
576+ short_name = short_name [len ("speckit-" ):]
577+
578+ skill_subdir = skills_dir / skill_name
579+ skill_file = skill_subdir / "SKILL.md"
580+ if not skill_file .exists ():
581+ continue
582+
583+ # Try to find the core command template
584+ core_file = core_templates_dir / f"{ short_name } .md" if core_templates_dir .exists () else None
585+ if core_file and not core_file .exists ():
586+ core_file = None
587+
588+ if core_file :
589+ # Restore from core template
590+ content = core_file .read_text (encoding = "utf-8" )
591+ if content .startswith ("---" ):
592+ parts = content .split ("---" , 2 )
593+ if len (parts ) >= 3 :
594+ frontmatter = yaml .safe_load (parts [1 ])
595+ if not isinstance (frontmatter , dict ):
596+ frontmatter = {}
597+ body = parts [2 ].strip ()
598+ else :
599+ frontmatter = {}
600+ body = content
601+ else :
602+ frontmatter = {}
603+ body = content
604+
605+ original_desc = frontmatter .get ("description" , "" )
606+ enhanced_desc = SKILL_DESCRIPTIONS .get (
607+ short_name ,
608+ original_desc or f"Spec-kit workflow command: { short_name } " ,
609+ )
610+
611+ frontmatter_data = {
612+ "name" : skill_name ,
613+ "description" : enhanced_desc ,
614+ "compatibility" : "Requires spec-kit project structure with .specify/ directory" ,
615+ "metadata" : {
616+ "author" : "github-spec-kit" ,
617+ "source" : f"templates/commands/{ short_name } .md" ,
618+ },
619+ }
620+ frontmatter_text = yaml .safe_dump (frontmatter_data , sort_keys = False ).strip ()
621+ skill_content = (
622+ f"---\n "
623+ f"{ frontmatter_text } \n "
624+ f"---\n \n "
625+ f"# Speckit { short_name .title ()} Skill\n \n "
626+ f"{ body } \n "
627+ )
628+ skill_file .write_text (skill_content , encoding = "utf-8" )
629+ else :
630+ # No core template — remove the skill entirely
631+ shutil .rmtree (skill_subdir )
632+
422633 def install_from_directory (
423634 self ,
424635 source_dir : Path ,
@@ -459,13 +670,17 @@ def install_from_directory(
459670 # Register command overrides with AI agents
460671 registered_commands = self ._register_commands (manifest , dest_dir )
461672
673+ # Update corresponding skills when --ai-skills was previously used
674+ registered_skills = self ._register_skills (manifest , dest_dir )
675+
462676 self .registry .add (manifest .id , {
463677 "version" : manifest .version ,
464678 "source" : "local" ,
465679 "manifest_hash" : manifest .get_hash (),
466680 "enabled" : True ,
467681 "priority" : priority ,
468682 "registered_commands" : registered_commands ,
683+ "registered_skills" : registered_skills ,
469684 })
470685
471686 return manifest
@@ -539,7 +754,12 @@ def remove(self, pack_id: str) -> bool:
539754 if registered_commands :
540755 self ._unregister_commands (registered_commands )
541756
757+ # Restore original skills when preset is removed
758+ registered_skills = metadata .get ("registered_skills" , []) if metadata else []
542759 pack_dir = self .presets_dir / pack_id
760+ if registered_skills :
761+ self ._unregister_skills (registered_skills , pack_dir )
762+
543763 if pack_dir .exists ():
544764 shutil .rmtree (pack_dir )
545765
0 commit comments