@@ -1626,6 +1626,7 @@ def extension_add(
16261626 extension : str = typer .Argument (help = "Extension name or path" ),
16271627 dev : bool = typer .Option (False , "--dev" , help = "Install from local directory" ),
16281628 from_url : Optional [str ] = typer .Option (None , "--from" , help = "Install from custom URL" ),
1629+ force : bool = typer .Option (False , "--force" , help = "Overwrite if already installed" ),
16291630 priority : int = typer .Option (10 , "--priority" , help = "Resolution priority (lower = higher precedence, default 10)" ),
16301631):
16311632 """Install an extension."""
@@ -1640,6 +1641,9 @@ def extension_add(
16401641 manager = ExtensionManager (project_root )
16411642 speckit_version = get_speckit_version ()
16421643
1644+ if force :
1645+ console .print ("[yellow]--force:[/yellow] Will overwrite if already installed" )
1646+
16431647 # Prompt for URL-based installs BEFORE the spinner so the user can
16441648 # actually see and respond to the confirmation (the Rich status
16451649 # spinner overwrites the typer.confirm prompt line, making it appear
@@ -1690,11 +1694,15 @@ def extension_add(
16901694 console .print (f"[red]Error:[/red] No extension.yml found in { source_path } " )
16911695 raise typer .Exit (1 )
16921696
1697+ if force :
1698+ console .print (f"[yellow]--force:[/yellow] Installing from [cyan]{ source_path } [/cyan] (will overwrite if already installed)..." )
1699+
16931700 manifest = manager .install_from_directory (
16941701 source_path ,
16951702 speckit_version ,
16961703 priority = priority ,
16971704 link_commands = True ,
1705+ force = force
16981706 )
16991707
17001708 elif from_url :
@@ -1724,7 +1732,7 @@ def extension_add(
17241732 zip_path .write_bytes (zip_data )
17251733
17261734 # Install from downloaded ZIP
1727- manifest = manager .install_from_zip (zip_path , speckit_version , priority = priority )
1735+ manifest = manager .install_from_zip (zip_path , speckit_version , priority = priority , force = force )
17281736 # ExtensionError covers an oversized body (via error_type) and the
17291737 # ValidationError/ExtensionError raised by install_from_zip; URL
17301738 # scheme is validated above. Catching these instead of a blanket
@@ -1741,7 +1749,9 @@ def extension_add(
17411749 # Try bundled extensions first (shipped with spec-kit)
17421750 bundled_path = _locate_bundled_extension (extension )
17431751 if bundled_path is not None :
1744- manifest = manager .install_from_directory (bundled_path , speckit_version , priority = priority )
1752+ manifest = manager .install_from_directory (
1753+ bundled_path , speckit_version , priority = priority , force = force
1754+ )
17451755 else :
17461756 # Install from catalog (also resolves display names to IDs)
17471757 catalog = ExtensionCatalog (project_root )
@@ -1762,7 +1772,9 @@ def extension_add(
17621772 if resolved_id != extension :
17631773 bundled_path = _locate_bundled_extension (resolved_id )
17641774 if bundled_path is not None :
1765- manifest = manager .install_from_directory (bundled_path , speckit_version , priority = priority )
1775+ manifest = manager .install_from_directory (
1776+ bundled_path , speckit_version , priority = priority , force = force
1777+ )
17661778
17671779 if bundled_path is None :
17681780 # Bundled extensions without a download URL must come from the local package
@@ -1798,7 +1810,7 @@ def extension_add(
17981810
17991811 try :
18001812 # Install from downloaded ZIP
1801- manifest = manager .install_from_zip (zip_path , speckit_version , priority = priority )
1813+ manifest = manager .install_from_zip (zip_path , speckit_version , priority = priority , force = force )
18021814 finally :
18031815 # Clean up downloaded ZIP
18041816 if zip_path .exists ():
0 commit comments