@@ -1847,7 +1847,7 @@ def _resolve_installed_extension(
18471847 for ext in name_matches :
18481848 table .add_row (ext .get ("id" , "" ), ext .get ("name" , "" ), str (ext .get ("version" , "" )))
18491849 console .print (table )
1850- console .print (f "\n Please rerun using the extension ID:" )
1850+ console .print ("\n Please rerun using the extension ID:" )
18511851 console .print (f" [bold]specify extension { command_name } <extension-id>[/bold]" )
18521852 raise typer .Exit (1 )
18531853 else :
@@ -1910,7 +1910,7 @@ def _resolve_catalog_extension(
19101910 ext .get ("_catalog_name" , "" ),
19111911 )
19121912 console .print (table )
1913- console .print (f "\n Please rerun using the extension ID:" )
1913+ console .print ("\n Please rerun using the extension ID:" )
19141914 console .print (f" [bold]specify extension { command_name } <extension-id>[/bold]" )
19151915 raise typer .Exit (1 )
19161916
@@ -2426,7 +2426,7 @@ def extension_info(
24262426 extension : str = typer .Argument (help = "Extension ID or name" ),
24272427):
24282428 """Show detailed information about an extension."""
2429- from .extensions import ExtensionCatalog , ExtensionManager , ExtensionError
2429+ from .extensions import ExtensionCatalog , ExtensionManager
24302430
24312431 project_root = Path .cwd ()
24322432
@@ -2488,7 +2488,7 @@ def extension_info(
24882488 console .print (f"[yellow]Catalog unavailable:[/yellow] { catalog_error } " )
24892489 console .print ("[dim]Note: Using locally installed extension; catalog info could not be verified.[/dim]" )
24902490 else :
2491- console .print (f "[yellow]Note:[/yellow] Not found in catalog (custom/local extension)" )
2491+ console .print ("[yellow]Note:[/yellow] Not found in catalog (custom/local extension)" )
24922492
24932493 console .print ()
24942494 console .print ("[green]✓ Installed[/green]" )
@@ -2608,6 +2608,7 @@ def extension_update(
26082608 ExtensionManager ,
26092609 ExtensionCatalog ,
26102610 ExtensionError ,
2611+ ValidationError ,
26112612 CommandRegistrar ,
26122613 HookExecutor ,
26132614 )
@@ -2649,7 +2650,16 @@ def extension_update(
26492650 for ext_id in extensions_to_update :
26502651 # Get installed version
26512652 metadata = manager .registry .get (ext_id )
2652- installed_version = pkg_version .Version (metadata ["version" ])
2653+ if metadata is None or "version" not in metadata :
2654+ console .print (f"⚠ { ext_id } : Registry entry corrupted or missing (skipping)" )
2655+ continue
2656+ try :
2657+ installed_version = pkg_version .Version (metadata ["version" ])
2658+ except pkg_version .InvalidVersion :
2659+ console .print (
2660+ f"⚠ { ext_id } : Invalid installed version '{ metadata .get ('version' )} ' in registry (skipping)"
2661+ )
2662+ continue
26532663
26542664 # Get catalog info
26552665 ext_info = catalog .get_extension_info (ext_id )
@@ -2662,7 +2672,13 @@ def extension_update(
26622672 console .print (f"⚠ { ext_id } : Updates not allowed from '{ ext_info .get ('_catalog_name' , 'catalog' )} ' (skipping)" )
26632673 continue
26642674
2665- catalog_version = pkg_version .Version (ext_info ["version" ])
2675+ try :
2676+ catalog_version = pkg_version .Version (ext_info ["version" ])
2677+ except pkg_version .InvalidVersion :
2678+ console .print (
2679+ f"⚠ { ext_id } : Invalid catalog version '{ ext_info .get ('version' )} ' (skipping)"
2680+ )
2681+ continue
26662682
26672683 if catalog_version > installed_version :
26682684 updates_available .append (
@@ -2710,6 +2726,7 @@ def extension_update(
27102726 backup_base = manager .extensions_dir / ".backup" / f"{ extension_id } -update"
27112727 backup_ext_dir = backup_base / "extension"
27122728 backup_commands_dir = backup_base / "commands"
2729+ backup_config_dir = backup_base / "config"
27132730
27142731 # Store backup state
27152732 backup_registry_entry = None
@@ -2728,6 +2745,15 @@ def extension_update(
27282745 shutil .rmtree (backup_ext_dir )
27292746 shutil .copytree (extension_dir , backup_ext_dir )
27302747
2748+ # Backup config files separately so they can be restored
2749+ # after a successful install (install_from_directory clears dest dir).
2750+ config_files = list (extension_dir .glob ("*-config.yml" )) + list (
2751+ extension_dir .glob ("*-config.local.yml" )
2752+ )
2753+ for cfg_file in config_files :
2754+ backup_config_dir .mkdir (parents = True , exist_ok = True )
2755+ shutil .copy2 (cfg_file , backup_config_dir / cfg_file .name )
2756+
27312757 # 3. Backup command files for all agents
27322758 registered_commands = backup_registry_entry .get ("registered_commands" , {})
27332759 for agent_name , cmd_names in registered_commands .items ():
@@ -2801,9 +2827,23 @@ def extension_update(
28012827 # 8. Install new version
28022828 _ = manager .install_from_zip (zip_path , speckit_version )
28032829
2830+ # Restore user config files from backup after successful install.
2831+ new_extension_dir = manager .extensions_dir / extension_id
2832+ if backup_config_dir .exists () and new_extension_dir .exists ():
2833+ for cfg_file in backup_config_dir .iterdir ():
2834+ if cfg_file .is_file ():
2835+ shutil .copy2 (cfg_file , new_extension_dir / cfg_file .name )
2836+
28042837 # 9. Restore metadata from backup (installed_at, enabled state)
28052838 if backup_registry_entry :
2806- new_metadata = manager .registry .get (extension_id )
2839+ # Copy current registry entry to avoid mutating internal
2840+ # registry state before explicit restore().
2841+ current_metadata = manager .registry .get (extension_id )
2842+ if current_metadata is None :
2843+ raise RuntimeError (
2844+ f"Registry entry for '{ extension_id } ' missing after install — update incomplete"
2845+ )
2846+ new_metadata = dict (current_metadata )
28072847
28082848 # Preserve the original installation timestamp
28092849 if "installed_at" in backup_registry_entry :
@@ -2813,7 +2853,9 @@ def extension_update(
28132853 if not backup_registry_entry .get ("enabled" , True ):
28142854 new_metadata ["enabled" ] = False
28152855
2816- manager .registry .update (extension_id , new_metadata )
2856+ # Use restore() instead of update() because update() always
2857+ # preserves the existing installed_at, ignoring our override
2858+ manager .registry .restore (extension_id , new_metadata )
28172859
28182860 # Also disable hooks in extensions.yml if extension was disabled
28192861 if not backup_registry_entry .get ("enabled" , True ):
@@ -2860,7 +2902,10 @@ def extension_update(
28602902 # (files that weren't in the original backup)
28612903 try :
28622904 new_registry_entry = manager .registry .get (extension_id )
2863- new_registered_commands = new_registry_entry .get ("registered_commands" , {})
2905+ if new_registry_entry is None :
2906+ new_registered_commands = {}
2907+ else :
2908+ new_registered_commands = new_registry_entry .get ("registered_commands" , {})
28642909 for agent_name , cmd_names in new_registered_commands .items ():
28652910 if agent_name not in registrar .AGENT_CONFIGS :
28662911 continue
@@ -2926,7 +2971,7 @@ def extension_update(
29262971 if backup_registry_entry :
29272972 manager .registry .restore (extension_id , backup_registry_entry )
29282973
2929- console .print (f " [green]✓[/green] Rollback successful" )
2974+ console .print (" [green]✓[/green] Rollback successful" )
29302975 # Clean up backup directory only on successful rollback
29312976 if backup_base .exists ():
29322977 shutil .rmtree (backup_base )
@@ -2944,6 +2989,9 @@ def extension_update(
29442989 console .print (f" • { ext_name } : { error } " )
29452990 raise typer .Exit (1 )
29462991
2992+ except ValidationError as e :
2993+ console .print (f"\n [red]Validation Error:[/red] { e } " )
2994+ raise typer .Exit (1 )
29472995 except ExtensionError as e :
29482996 console .print (f"\n [red]Error:[/red] { e } " )
29492997 raise typer .Exit (1 )
@@ -2974,6 +3022,10 @@ def extension_enable(
29743022
29753023 # Update registry
29763024 metadata = manager .registry .get (extension_id )
3025+ if metadata is None :
3026+ console .print (f"[red]Error:[/red] Extension '{ extension_id } ' not found in registry (corrupted state)" )
3027+ raise typer .Exit (1 )
3028+
29773029 if metadata .get ("enabled" , True ):
29783030 console .print (f"[yellow]Extension '{ display_name } ' is already enabled[/yellow]" )
29793031 raise typer .Exit (0 )
@@ -3018,6 +3070,10 @@ def extension_disable(
30183070
30193071 # Update registry
30203072 metadata = manager .registry .get (extension_id )
3073+ if metadata is None :
3074+ console .print (f"[red]Error:[/red] Extension '{ extension_id } ' not found in registry (corrupted state)" )
3075+ raise typer .Exit (1 )
3076+
30213077 if not metadata .get ("enabled" , True ):
30223078 console .print (f"[yellow]Extension '{ display_name } ' is already disabled[/yellow]" )
30233079 raise typer .Exit (0 )
0 commit comments