@@ -621,6 +621,31 @@ def _locate_bundled_extension(extension_id: str) -> Path | None:
621621 return None
622622
623623
624+ def _locate_bundled_preset (preset_id : str ) -> Path | None :
625+ """Return the path to a bundled preset, or None.
626+
627+ Checks the wheel's core_pack first, then falls back to the
628+ source-checkout ``presets/<id>/`` directory.
629+ """
630+ import re as _re
631+ if not _re .match (r'^[a-z0-9-]+$' , preset_id ):
632+ return None
633+
634+ core = _locate_core_pack ()
635+ if core is not None :
636+ candidate = core / "presets" / preset_id
637+ if (candidate / "preset.yml" ).is_file ():
638+ return candidate
639+
640+ # Source-checkout / editable install: look relative to repo root
641+ repo_root = Path (__file__ ).parent .parent .parent
642+ candidate = repo_root / "presets" / preset_id
643+ if (candidate / "preset.yml" ).is_file ():
644+ return candidate
645+
646+ return None
647+
648+
624649def _install_shared_infra (
625650 project_path : Path ,
626651 script_type : str ,
@@ -1266,27 +1291,41 @@ def init(
12661291 preset_manager = PresetManager (project_path )
12671292 speckit_ver = get_speckit_version ()
12681293
1269- # Try local directory first, then catalog
1294+ # Try local directory first, then bundled, then catalog
12701295 local_path = Path (preset ).resolve ()
12711296 if local_path .is_dir () and (local_path / "preset.yml" ).exists ():
12721297 preset_manager .install_from_directory (local_path , speckit_ver )
12731298 else :
1274- preset_catalog = PresetCatalog (project_path )
1275- pack_info = preset_catalog .get_pack_info (preset )
1276- if not pack_info :
1277- console .print (f"[yellow]Warning:[/yellow] Preset '{ preset } ' not found in catalog. Skipping." )
1299+ bundled_path = _locate_bundled_preset (preset )
1300+ if bundled_path :
1301+ preset_manager .install_from_directory (bundled_path , speckit_ver )
12781302 else :
1279- try :
1280- zip_path = preset_catalog .download_pack (preset )
1281- preset_manager .install_from_zip (zip_path , speckit_ver )
1282- # Clean up downloaded ZIP to avoid cache accumulation
1303+ preset_catalog = PresetCatalog (project_path )
1304+ pack_info = preset_catalog .get_pack_info (preset )
1305+ if not pack_info :
1306+ console .print (f"[yellow]Warning:[/yellow] Preset '{ preset } ' not found in catalog. Skipping." )
1307+ elif pack_info .get ("bundled" ) and not pack_info .get ("download_url" ):
1308+ from .extensions import REINSTALL_COMMAND
1309+ console .print (
1310+ f"[yellow]Warning:[/yellow] Preset '{ preset } ' is bundled with spec-kit "
1311+ f"but could not be found in the installed package."
1312+ )
1313+ console .print (
1314+ "This usually means the spec-kit installation is incomplete or corrupted."
1315+ )
1316+ console .print (f"Try reinstalling: { REINSTALL_COMMAND } " )
1317+ else :
12831318 try :
1284- zip_path .unlink (missing_ok = True )
1285- except OSError :
1286- # Best-effort cleanup; failure to delete is non-fatal
1287- pass
1288- except PresetError as preset_err :
1289- console .print (f"[yellow]Warning:[/yellow] Failed to install preset '{ preset } ': { preset_err } " )
1319+ zip_path = preset_catalog .download_pack (preset )
1320+ preset_manager .install_from_zip (zip_path , speckit_ver )
1321+ # Clean up downloaded ZIP to avoid cache accumulation
1322+ try :
1323+ zip_path .unlink (missing_ok = True )
1324+ except OSError :
1325+ # Best-effort cleanup; failure to delete is non-fatal
1326+ pass
1327+ except PresetError as preset_err :
1328+ console .print (f"[yellow]Warning:[/yellow] Failed to install preset '{ preset } ': { preset_err } " )
12901329 except Exception as preset_err :
12911330 console .print (f"[yellow]Warning:[/yellow] Failed to install preset: { preset_err } " )
12921331
@@ -2140,28 +2179,50 @@ def preset_add(
21402179 console .print (f"[green]✓[/green] Preset '{ manifest .name } ' v{ manifest .version } installed (priority { priority } )" )
21412180
21422181 elif pack_id :
2143- catalog = PresetCatalog (project_root )
2144- pack_info = catalog .get_pack_info (pack_id )
2182+ # Try bundled preset first, then catalog
2183+ bundled_path = _locate_bundled_preset (pack_id )
2184+ if bundled_path :
2185+ console .print (f"Installing bundled preset [cyan]{ pack_id } [/cyan]..." )
2186+ manifest = manager .install_from_directory (bundled_path , speckit_version , priority )
2187+ console .print (f"[green]✓[/green] Preset '{ manifest .name } ' v{ manifest .version } installed (priority { priority } )" )
2188+ else :
2189+ catalog = PresetCatalog (project_root )
2190+ pack_info = catalog .get_pack_info (pack_id )
21452191
2146- if not pack_info :
2147- console .print (f"[red]Error:[/red] Preset '{ pack_id } ' not found in catalog" )
2148- raise typer .Exit (1 )
2192+ if not pack_info :
2193+ console .print (f"[red]Error:[/red] Preset '{ pack_id } ' not found in catalog" )
2194+ raise typer .Exit (1 )
21492195
2150- if not pack_info .get ("_install_allowed" , True ):
2151- catalog_name = pack_info .get ("_catalog_name" , "unknown" )
2152- console .print (f"[red]Error:[/red] Preset '{ pack_id } ' is from the '{ catalog_name } ' catalog which is discovery-only (install not allowed)." )
2153- console .print ("Add the catalog with --install-allowed or install from the preset's repository directly with --from." )
2154- raise typer .Exit (1 )
2196+ # Bundled presets should have been caught above; if we reach
2197+ # here the bundled files are missing from the installation.
2198+ if pack_info .get ("bundled" ) and not pack_info .get ("download_url" ):
2199+ from .extensions import REINSTALL_COMMAND
2200+ console .print (
2201+ f"[red]Error:[/red] Preset '{ pack_id } ' is bundled with spec-kit "
2202+ f"but could not be found in the installed package."
2203+ )
2204+ console .print (
2205+ "\n This usually means the spec-kit installation is incomplete or corrupted."
2206+ )
2207+ console .print ("Try reinstalling spec-kit:" )
2208+ console .print (f" { REINSTALL_COMMAND } " )
2209+ raise typer .Exit (1 )
21552210
2156- console .print (f"Installing preset [cyan]{ pack_info .get ('name' , pack_id )} [/cyan]..." )
2211+ if not pack_info .get ("_install_allowed" , True ):
2212+ catalog_name = pack_info .get ("_catalog_name" , "unknown" )
2213+ console .print (f"[red]Error:[/red] Preset '{ pack_id } ' is from the '{ catalog_name } ' catalog which is discovery-only (install not allowed)." )
2214+ console .print ("Add the catalog with --install-allowed or install from the preset's repository directly with --from." )
2215+ raise typer .Exit (1 )
21572216
2158- try :
2159- zip_path = catalog .download_pack (pack_id )
2160- manifest = manager .install_from_zip (zip_path , speckit_version , priority )
2161- console .print (f"[green]✓[/green] Preset '{ manifest .name } ' v{ manifest .version } installed (priority { priority } )" )
2162- finally :
2163- if 'zip_path' in locals () and zip_path .exists ():
2164- zip_path .unlink (missing_ok = True )
2217+ console .print (f"Installing preset [cyan]{ pack_info .get ('name' , pack_id )} [/cyan]..." )
2218+
2219+ try :
2220+ zip_path = catalog .download_pack (pack_id )
2221+ manifest = manager .install_from_zip (zip_path , speckit_version , priority )
2222+ console .print (f"[green]✓[/green] Preset '{ manifest .name } ' v{ manifest .version } installed (priority { priority } )" )
2223+ finally :
2224+ if 'zip_path' in locals () and zip_path .exists ():
2225+ zip_path .unlink (missing_ok = True )
21652226 else :
21662227 console .print ("[red]Error:[/red] Specify a preset ID, --from URL, or --dev path" )
21672228 raise typer .Exit (1 )
0 commit comments