MCPcopy
hub / github.com/github/spec-kit / PresetResolver

Class PresetResolver

src/specify_cli/presets/__init__.py:2540–3308  ·  view source on GitHub ↗

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

Source from the content-addressed store, hash-verified

2538
2539
2540class 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

Calls

no outgoing calls