Resolves template names to file paths using a priority stack. Resolution order: 1. .specify/templates/overrides/ - Project-local overrides 2. .specify/presets/ / - Installed presets 3. .specify/extensions/ /templates/ - Extension-provided templates
| 2538 | |
| 2539 | |
| 2540 | class PresetResolver: |
| 2541 | """Resolves template names to file paths using a priority stack. |
| 2542 | |
| 2543 | Resolution order: |
| 2544 | 1. .specify/templates/overrides/ - Project-local overrides |
| 2545 | 2. .specify/presets/<preset-id>/ - Installed presets |
| 2546 | 3. .specify/extensions/<ext-id>/templates/ - Extension-provided templates |
| 2547 | 4. .specify/templates/ - Core templates (shipped with Spec Kit) |
| 2548 | """ |
| 2549 | |
| 2550 | def __init__(self, project_root: Path): |
| 2551 | """Initialize preset resolver. |
| 2552 | |
| 2553 | Args: |
| 2554 | project_root: Path to project root directory |
| 2555 | """ |
| 2556 | self.project_root = project_root |
| 2557 | self.templates_dir = project_root / ".specify" / "templates" |
| 2558 | self.presets_dir = project_root / ".specify" / "presets" |
| 2559 | self.overrides_dir = self.templates_dir / "overrides" |
| 2560 | self.extensions_dir = project_root / ".specify" / "extensions" |
| 2561 | self._manifest_cache: Dict[str, Optional["PresetManifest"]] = {} |
| 2562 | |
| 2563 | def _get_manifest(self, pack_dir: Path) -> Optional["PresetManifest"]: |
| 2564 | """Get a cached preset manifest, parsing it on first access.""" |
| 2565 | key = str(pack_dir) |
| 2566 | if key not in self._manifest_cache: |
| 2567 | manifest_path = pack_dir / "preset.yml" |
| 2568 | if manifest_path.exists(): |
| 2569 | try: |
| 2570 | self._manifest_cache[key] = PresetManifest(manifest_path) |
| 2571 | except PresetValidationError: |
| 2572 | self._manifest_cache[key] = None |
| 2573 | else: |
| 2574 | self._manifest_cache[key] = None |
| 2575 | return self._manifest_cache[key] |
| 2576 | |
| 2577 | def _get_all_extensions_by_priority(self) -> list[tuple[int, str, dict | None]]: |
| 2578 | """Build unified list of registered and unregistered extensions sorted by priority. |
| 2579 | |
| 2580 | Registered extensions use their stored priority; unregistered directories |
| 2581 | get implicit priority=10. Results are sorted by (priority, ext_id) for |
| 2582 | deterministic ordering. |
| 2583 | |
| 2584 | Returns: |
| 2585 | List of (priority, ext_id, metadata_or_none) tuples sorted by priority. |
| 2586 | """ |
| 2587 | if not self.extensions_dir.exists(): |
| 2588 | return [] |
| 2589 | |
| 2590 | registry = ExtensionRegistry(self.extensions_dir) |
| 2591 | # Use keys() to track ALL extensions (including corrupted entries) without deep copy |
| 2592 | # This prevents corrupted entries from being picked up as "unregistered" dirs |
| 2593 | registered_extension_ids = registry.keys() |
| 2594 | |
| 2595 | # Get all registered extensions including disabled; we filter disabled manually below |
| 2596 | all_registered = registry.list_by_priority(include_disabled=True) |
| 2597 |
no outgoing calls