Manages extension lifecycle: installation, removal, updates.
| 687 | |
| 688 | |
| 689 | class ExtensionManager: |
| 690 | """Manages extension lifecycle: installation, removal, updates.""" |
| 691 | |
| 692 | def __init__(self, project_root: Path): |
| 693 | """Initialize extension manager. |
| 694 | |
| 695 | Args: |
| 696 | project_root: Path to project root directory |
| 697 | """ |
| 698 | self.project_root = project_root |
| 699 | self.extensions_dir = project_root / ".specify" / "extensions" |
| 700 | self.registry = ExtensionRegistry(self.extensions_dir) |
| 701 | |
| 702 | @staticmethod |
| 703 | def _collect_manifest_command_names(manifest: ExtensionManifest) -> Dict[str, str]: |
| 704 | """Collect command and alias names declared by a manifest. |
| 705 | |
| 706 | Performs install-time validation for extension-specific constraints: |
| 707 | - primary commands must use the canonical `speckit.{extension}.{command}` shape |
| 708 | - primary commands must use this extension's namespace |
| 709 | - command namespaces must not shadow core commands |
| 710 | - duplicate command/alias names inside one manifest are rejected |
| 711 | - aliases are validated for type and uniqueness only (no pattern enforcement) |
| 712 | |
| 713 | Args: |
| 714 | manifest: Parsed extension manifest |
| 715 | |
| 716 | Returns: |
| 717 | Mapping of declared command/alias name -> kind ("command"/"alias") |
| 718 | |
| 719 | Raises: |
| 720 | ValidationError: If any declared name is invalid |
| 721 | """ |
| 722 | if manifest.id in CORE_COMMAND_NAMES: |
| 723 | raise ValidationError( |
| 724 | f"Extension ID '{manifest.id}' conflicts with core command namespace '{manifest.id}'" |
| 725 | ) |
| 726 | |
| 727 | declared_names: Dict[str, str] = {} |
| 728 | |
| 729 | for cmd in manifest.commands: |
| 730 | primary_name = cmd["name"] |
| 731 | aliases = cmd.get("aliases", []) |
| 732 | |
| 733 | if aliases is None: |
| 734 | aliases = [] |
| 735 | if not isinstance(aliases, list): |
| 736 | raise ValidationError( |
| 737 | f"Aliases for command '{primary_name}' must be a list" |
| 738 | ) |
| 739 | |
| 740 | for kind, name in [("command", primary_name)] + [ |
| 741 | ("alias", alias) for alias in aliases |
| 742 | ]: |
| 743 | if not isinstance(name, str): |
| 744 | raise ValidationError( |
| 745 | f"{kind.capitalize()} for command '{primary_name}' must be a string" |
| 746 | ) |
no outgoing calls