@@ -2575,20 +2575,21 @@ def extension_update(
25752575 # This ensures we don't remove the old version if download fails
25762576 zip_path = catalog .download_extension (extension_id )
25772577
2578- # Backup extension directory and registry metadata before removing
2578+ # Backup registry metadata and extensions.yml before removing
2579+ # (always backup these even if directory is missing/corrupted)
25792580 extension_dir = manager .extensions_dir / extension_id
25802581 backup_dir = None
2581- backup_metadata = None
2582+ backup_metadata = manager . registry . get ( extension_id )
25822583 backup_command_files = {} # {path: content}
25832584 backup_extensions_yml = None
25842585
2585- if extension_dir .exists ():
2586- backup_dir = Path (tempfile .mkdtemp (prefix = f"speckit-update-{ extension_id } -" ))
2587- shutil .copytree (extension_dir , backup_dir / extension_id )
2588- # Save registry metadata for restoration on failure
2589- backup_metadata = manager .registry .get (extension_id )
2586+ # Backup extensions.yml (remove() will modify hooks)
2587+ extensions_yml = project_root / ".specify" / "extensions.yml"
2588+ if extensions_yml .exists ():
2589+ backup_extensions_yml = extensions_yml .read_text ()
25902590
2591- # Backup registered command files (remove() will delete these)
2591+ # Backup registered command files (remove() will delete these)
2592+ if backup_metadata :
25922593 from .extensions import CommandRegistrar
25932594 registered_commands = backup_metadata .get ("registered_commands" , {})
25942595 registrar = CommandRegistrar ()
@@ -2607,17 +2608,22 @@ def extension_update(
26072608 if prompt_file .exists ():
26082609 backup_command_files [prompt_file ] = prompt_file .read_text ()
26092610
2610- # Backup extensions.yml (remove() will modify hooks)
2611- extensions_yml = project_root / ".specify" / "extensions.yml"
2612- if extensions_yml . exists ():
2613- backup_extensions_yml = extensions_yml . read_text ( )
2611+ # Backup extension directory if it exists
2612+ if extension_dir . exists ():
2613+ backup_dir = Path ( tempfile . mkdtemp ( prefix = f"speckit-update- { extension_id } -" ))
2614+ shutil . copytree ( extension_dir , backup_dir / extension_id )
26142615
26152616 try :
26162617 # Remove old version (keep config files)
26172618 manager .remove (extension_id , keep_config = True )
26182619
2619- # Install new version
2620- manager .install_from_zip (zip_path , speckit_version )
2620+ # Install new version and verify ID matches
2621+ installed_manifest = manager .install_from_zip (zip_path , speckit_version )
2622+ if installed_manifest .id != extension_id :
2623+ raise ValueError (
2624+ f"Extension ID mismatch: expected '{ extension_id } ', "
2625+ f"got '{ installed_manifest .id } '"
2626+ )
26212627 console .print (f" [green]✓[/green] Updated to v{ update ['available' ]} " )
26222628 updated_count += 1
26232629
@@ -2626,25 +2632,26 @@ def extension_update(
26262632 shutil .rmtree (backup_dir )
26272633 except Exception as install_error :
26282634 # Restore from backup if install fails
2635+ # Restore directory if we have a backup
26292636 if backup_dir and (backup_dir / extension_id ).exists ():
26302637 # Remove any partial install
26312638 if extension_dir .exists ():
26322639 shutil .rmtree (extension_dir )
26332640 # Restore backup files
26342641 shutil .copytree (backup_dir / extension_id , extension_dir )
26352642 shutil .rmtree (backup_dir )
2636- # Restore registry entry (use data dict directly to preserve installed_at )
2637- if backup_metadata :
2638- manager .registry .data ["extensions" ][extension_id ] = backup_metadata
2639- manager .registry ._save ()
2640- # Restore command files
2641- for cmd_path , cmd_content in backup_command_files .items ():
2642- cmd_path .parent .mkdir (parents = True , exist_ok = True )
2643- cmd_path .write_text (cmd_content )
2644- # Restore extensions.yml (hooks)
2645- if backup_extensions_yml is not None :
2646- extensions_yml = project_root / ".specify" / "extensions.yml"
2647- extensions_yml .write_text (backup_extensions_yml )
2643+ # Always restore registry if we have backup (even without directory )
2644+ if backup_metadata :
2645+ manager .registry .data ["extensions" ][extension_id ] = backup_metadata
2646+ manager .registry ._save ()
2647+ # Restore command files
2648+ for cmd_path , cmd_content in backup_command_files .items ():
2649+ cmd_path .parent .mkdir (parents = True , exist_ok = True )
2650+ cmd_path .write_text (cmd_content )
2651+ # Restore extensions.yml (hooks)
2652+ if backup_extensions_yml is not None :
2653+ extensions_yml = project_root / ".specify" / "extensions.yml"
2654+ extensions_yml .write_text (backup_extensions_yml )
26482655 raise install_error
26492656 finally :
26502657 # Clean up downloaded ZIP
0 commit comments