@@ -969,7 +969,10 @@ def register_commands_for_claude(
969969class ExtensionCatalog :
970970 """Manages extension catalog fetching, caching, and searching."""
971971
972- DEFAULT_CATALOG_URL = "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
972+ DEFAULT_CATALOG_URLS = [
973+ "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json" ,
974+ "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
975+ ]
973976 CACHE_DURATION = 3600 # 1 hour in seconds
974977
975978 def __init__ (self , project_root : Path ):
@@ -984,15 +987,15 @@ def __init__(self, project_root: Path):
984987 self .cache_file = self .cache_dir / "catalog.json"
985988 self .cache_metadata_file = self .cache_dir / "catalog-metadata.json"
986989
987- def get_catalog_url (self ) -> str :
988- """Get catalog URL from config or use default.
990+ def get_catalog_urls (self ) -> List [ str ] :
991+ """Get catalog URLs from config or use default.
989992
990993 Checks in order:
991- 1. SPECKIT_CATALOG_URL environment variable
992- 2. Default catalog URL
994+ 1. SPECKIT_CATALOG_URL environment variable (comma-separated for multiple)
995+ 2. Default catalog URLs
993996
994997 Returns:
995- URL to fetch catalog from
998+ List of URLs to fetch catalogs from
996999
9971000 Raises:
9981001 ValidationError: If custom URL is invalid (non-HTTPS)
@@ -1021,7 +1024,7 @@ def get_catalog_url(self) -> str:
10211024 )
10221025
10231026 # Warn users when using a non-default catalog (once per instance)
1024- if catalog_url != self .DEFAULT_CATALOG_URL :
1027+ if catalog_url not in self .DEFAULT_CATALOG_URLS :
10251028 if not getattr (self , "_non_default_catalog_warning_shown" , False ):
10261029 print (
10271030 "Warning: Using non-default extension catalog. "
@@ -1030,39 +1033,50 @@ def get_catalog_url(self) -> str:
10301033 )
10311034 self ._non_default_catalog_warning_shown = True
10321035
1033- return catalog_url
1036+ return [ catalog_url ]
10341037
10351038 # TODO: Support custom catalogs from .specify/extension-catalogs.yml
1036- return self .DEFAULT_CATALOG_URL
1039+ return self .DEFAULT_CATALOG_URLS
10371040
10381041 def is_cache_valid (self ) -> bool :
1039- """Check if cached catalog is still valid.
1042+ """Check if cached catalog is still valid and matches current URL settings .
10401043
10411044 Returns:
1042- True if cache exists and is within cache duration
1045+ True if cache exists, is within cache duration, and matches current URLs
10431046 """
10441047 if not self .cache_file .exists () or not self .cache_metadata_file .exists ():
10451048 return False
10461049
10471050 try :
10481051 metadata = json .loads (self .cache_metadata_file .read_text ())
1052+
1053+ # Check for schema mismatch (older caches used 'catalog_url' string instead of 'catalog_urls' list)
1054+ cached_urls = metadata .get ("catalog_urls" )
1055+ if not cached_urls or not isinstance (cached_urls , list ):
1056+ return False
1057+
1058+ # Check if the currently requested URLs match the cached configuration
1059+ current_urls = self .get_catalog_urls ()
1060+ if cached_urls != current_urls :
1061+ return False
1062+
10491063 cached_at = datetime .fromisoformat (metadata .get ("cached_at" , "" ))
10501064 age_seconds = (datetime .now (timezone .utc ) - cached_at ).total_seconds ()
10511065 return age_seconds < self .CACHE_DURATION
10521066 except (json .JSONDecodeError , ValueError , KeyError ):
10531067 return False
10541068
10551069 def fetch_catalog (self , force_refresh : bool = False ) -> Dict [str , Any ]:
1056- """Fetch extension catalog from URL or cache.
1070+ """Fetch extension catalogs from URLs or cache and merge them .
10571071
10581072 Args:
10591073 force_refresh: If True, bypass cache and fetch from network
10601074
10611075 Returns:
1062- Catalog data dictionary
1076+ Merged catalog data dictionary
10631077
10641078 Raises:
1065- ExtensionError: If catalog cannot be fetched
1079+ ExtensionError: If catalogs cannot be fetched
10661080 """
10671081 # Check cache first unless force refresh
10681082 if not force_refresh and self .is_cache_valid ():
@@ -1072,36 +1086,48 @@ def fetch_catalog(self, force_refresh: bool = False) -> Dict[str, Any]:
10721086 pass # Fall through to network fetch
10731087
10741088 # Fetch from network
1075- catalog_url = self .get_catalog_url ()
1089+ catalog_urls = self .get_catalog_urls ()
1090+
1091+ merged_catalog = {
1092+ "schema_version" : "1.0" ,
1093+ "extensions" : {}
1094+ }
10761095
10771096 try :
10781097 import urllib .request
10791098 import urllib .error
1080-
1081- with urllib .request .urlopen (catalog_url , timeout = 10 ) as response :
1082- catalog_data = json .loads (response .read ())
1083-
1084- # Validate catalog structure
1085- if "schema_version" not in catalog_data or "extensions" not in catalog_data :
1086- raise ExtensionError ("Invalid catalog format" )
1099+
1100+ for catalog_url in catalog_urls :
1101+ with urllib .request .urlopen (catalog_url , timeout = 10 ) as response :
1102+ catalog_data = json .loads (response .read ())
1103+
1104+ # Validate catalog structure
1105+ if "schema_version" not in catalog_data or "extensions" not in catalog_data :
1106+ raise ExtensionError (f"Invalid catalog format from { catalog_url } " )
1107+
1108+ # Merge extensions into the aggregated catalog, preserving precedence for the first catalog URL
1109+ # that defines a given extension ID.
1110+ for ext_id , ext_data in catalog_data .get ("extensions" , {}).items ():
1111+ if ext_id not in merged_catalog ["extensions" ]:
1112+ merged_catalog ["extensions" ][ext_id ] = ext_data
10871113
10881114 # Save to cache
10891115 self .cache_dir .mkdir (parents = True , exist_ok = True )
1090- self .cache_file .write_text (json .dumps (catalog_data , indent = 2 ))
1116+ self .cache_file .write_text (json .dumps (merged_catalog , indent = 2 ))
10911117
10921118 # Save cache metadata
10931119 metadata = {
10941120 "cached_at" : datetime .now (timezone .utc ).isoformat (),
1095- "catalog_url " : catalog_url ,
1121+ "catalog_urls " : catalog_urls ,
10961122 }
10971123 self .cache_metadata_file .write_text (json .dumps (metadata , indent = 2 ))
10981124
1099- return catalog_data
1125+ return merged_catalog
11001126
11011127 except urllib .error .URLError as e :
1102- raise ExtensionError (f"Failed to fetch catalog from { catalog_url } : { e } " )
1128+ raise ExtensionError (f"Failed to fetch catalog from network : { e } " )
11031129 except json .JSONDecodeError as e :
1104- raise ExtensionError (f"Invalid JSON in catalog: { e } " )
1130+ raise ExtensionError (f"Invalid JSON in catalog payload : { e } " )
11051131
11061132 def search (
11071133 self ,
0 commit comments