Manages workflow catalog fetching, caching, and searching. Resolution order for catalog sources: 1. ``SPECKIT_WORKFLOW_CATALOG_URL`` env var (overrides all) 2. Project-level ``.specify/workflow-catalogs.yml`` 3. User-level ``~/.specify/workflow-catalogs.yml`` 4. Built-in default
| 127 | |
| 128 | |
| 129 | class WorkflowCatalog: |
| 130 | """Manages workflow catalog fetching, caching, and searching. |
| 131 | |
| 132 | Resolution order for catalog sources: |
| 133 | 1. ``SPECKIT_WORKFLOW_CATALOG_URL`` env var (overrides all) |
| 134 | 2. Project-level ``.specify/workflow-catalogs.yml`` |
| 135 | 3. User-level ``~/.specify/workflow-catalogs.yml`` |
| 136 | 4. Built-in defaults (official + community) |
| 137 | """ |
| 138 | |
| 139 | DEFAULT_CATALOG_URL = ( |
| 140 | "https://raw.githubusercontent.com/github/spec-kit/main/" |
| 141 | "workflows/catalog.json" |
| 142 | ) |
| 143 | COMMUNITY_CATALOG_URL = ( |
| 144 | "https://raw.githubusercontent.com/github/spec-kit/main/" |
| 145 | "workflows/catalog.community.json" |
| 146 | ) |
| 147 | CACHE_DURATION = 3600 # 1 hour |
| 148 | |
| 149 | def __init__(self, project_root: Path) -> None: |
| 150 | self.project_root = project_root |
| 151 | self.workflows_dir = project_root / ".specify" / "workflows" |
| 152 | self.cache_dir = self.workflows_dir / ".cache" |
| 153 | |
| 154 | # -- Catalog resolution ----------------------------------------------- |
| 155 | |
| 156 | def _validate_catalog_url(self, url: str) -> None: |
| 157 | """Validate that a catalog URL uses HTTPS (localhost HTTP allowed).""" |
| 158 | from urllib.parse import urlparse |
| 159 | |
| 160 | parsed = urlparse(url) |
| 161 | is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1") |
| 162 | if parsed.scheme != "https" and not ( |
| 163 | parsed.scheme == "http" and is_localhost |
| 164 | ): |
| 165 | raise WorkflowValidationError( |
| 166 | f"Catalog URL must use HTTPS (got {parsed.scheme}://). " |
| 167 | "HTTP is only allowed for localhost." |
| 168 | ) |
| 169 | if not parsed.hostname: |
| 170 | raise WorkflowValidationError( |
| 171 | "Catalog URL must be a valid URL with a host." |
| 172 | ) |
| 173 | |
| 174 | def _load_catalog_config( |
| 175 | self, config_path: Path |
| 176 | ) -> list[WorkflowCatalogEntry] | None: |
| 177 | """Load catalog stack configuration from a YAML file.""" |
| 178 | if not config_path.exists(): |
| 179 | return None |
| 180 | try: |
| 181 | data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} |
| 182 | except (yaml.YAMLError, OSError, UnicodeError) as exc: |
| 183 | raise WorkflowValidationError( |
| 184 | f"Failed to read catalog config {config_path}: {exc}" |
| 185 | ) from exc |
| 186 | if not isinstance(data, dict): |
no outgoing calls