3636import stat
3737import yaml
3838from pathlib import Path
39- from typing import Any , Optional , Tuple
39+ from typing import Any , List , Optional , Tuple
4040
4141import typer
4242import httpx
@@ -2543,9 +2543,10 @@ def agent_switch(
25432543 from .agent_pack import (
25442544 resolve_agent_pack ,
25452545 load_bootstrap ,
2546+ check_modified_files ,
2547+ get_tracked_files ,
25462548 PackResolutionError ,
25472549 AgentPackError ,
2548- AgentFileModifiedError ,
25492550 )
25502551
25512552 show_banner ()
@@ -2582,13 +2583,28 @@ def agent_switch(
25822583 try :
25832584 current_resolved = resolve_agent_pack (current_agent , project_path = project_path )
25842585 current_bootstrap = load_bootstrap (current_resolved .path , current_resolved .manifest )
2586+
2587+ # Check for modified files BEFORE teardown and prompt for confirmation
2588+ modified = check_modified_files (project_path , current_agent )
2589+ if modified and not force :
2590+ console .print ("[yellow]The following files have been modified since installation:[/yellow]" )
2591+ for f in modified :
2592+ console .print (f" { f } " )
2593+ if not typer .confirm ("Remove these modified files?" ):
2594+ console .print ("[dim]Aborted. Use --force to skip this check.[/dim]" )
2595+ raise typer .Exit (0 )
2596+
2597+ # Retrieve tracked file lists and feed them into teardown
2598+ agent_files , extension_files = get_tracked_files (project_path , current_agent )
2599+ all_files = {** agent_files , ** extension_files }
2600+
25852601 console .print (f" [dim]Tearing down { current_agent } ...[/dim]" )
2586- current_bootstrap .teardown (project_path , force = force )
2602+ current_bootstrap .teardown (
2603+ project_path ,
2604+ force = True , # already confirmed above
2605+ files = all_files if all_files else None ,
2606+ )
25872607 console .print (f" [green]✓[/green] { current_agent } removed" )
2588- except AgentFileModifiedError as exc :
2589- console .print (f"[red]Error:[/red] { exc } " )
2590- console .print ("[yellow]Hint:[/yellow] Use --force to remove modified files." )
2591- raise typer .Exit (1 )
25922608 except AgentPackError :
25932609 # If pack-based teardown fails, try legacy cleanup via AGENT_CONFIG
25942610 agent_config = AGENT_CONFIG .get (current_agent , {})
@@ -2603,9 +2619,7 @@ def agent_switch(
26032619 try :
26042620 new_bootstrap = load_bootstrap (resolved .path , resolved .manifest )
26052621 console .print (f" [dim]Setting up { agent_id } ...[/dim]" )
2606- new_bootstrap .setup (project_path , script_type , options )
2607- # Record all installed files for tracked teardown
2608- new_bootstrap .finalize_setup (project_path )
2622+ agent_files = new_bootstrap .setup (project_path , script_type , options )
26092623 console .print (f" [green]✓[/green] { agent_id } installed" )
26102624 except AgentPackError as exc :
26112625 console .print (f"[red]Error setting up { agent_id } :[/red] { exc } " )
@@ -2614,32 +2628,54 @@ def agent_switch(
26142628 # Update init options
26152629 options ["ai" ] = agent_id
26162630 init_options_file .write_text (json .dumps (options , indent = 2 ), encoding = "utf-8" )
2617- console .print (f"\n [bold green]Successfully switched to { resolved .manifest .name } [/bold green]" )
26182631
26192632 # Re-register extension commands for the new agent
2620- _reregister_extension_commands (project_path , agent_id )
2633+ extension_files = _reregister_extension_commands (project_path , agent_id )
2634+
2635+ # Record all installed files (agent + extensions) for tracked teardown
2636+ new_bootstrap .finalize_setup (
2637+ project_path ,
2638+ agent_files = agent_files ,
2639+ extension_files = extension_files ,
2640+ )
2641+
2642+ console .print (f"\n [bold green]Successfully switched to { resolved .manifest .name } [/bold green]" )
2643+
26212644
2645+ def _reregister_extension_commands (project_path : Path , agent_id : str ) -> List [Path ]:
2646+ """Re-register all installed extension commands for a new agent after switching.
26222647
2623- def _reregister_extension_commands (project_path : Path , agent_id : str ) -> None :
2624- """Re-register all installed extension commands for a new agent after switching."""
2648+ Returns:
2649+ List of absolute file paths created by extension registration.
2650+ """
2651+ created_files : List [Path ] = []
26252652 registry_file = project_path / ".specify" / "extensions" / ".registry"
26262653 if not registry_file .is_file ():
2627- return
2654+ return created_files
26282655
26292656 try :
26302657 registry_data = json .loads (registry_file .read_text (encoding = "utf-8" ))
26312658 except (json .JSONDecodeError , OSError ):
2632- return
2659+ return created_files
26332660
26342661 extensions = registry_data .get ("extensions" , {})
26352662 if not extensions :
2636- return
2663+ return created_files
26372664
26382665 try :
26392666 from .agents import CommandRegistrar
26402667 registrar = CommandRegistrar ()
26412668 except ImportError :
2642- return
2669+ return created_files
2670+
2671+ # Snapshot the commands directory before registration so we can
2672+ # detect which files were created by extension commands.
2673+ agent_config = registrar .AGENT_CONFIGS .get (agent_id )
2674+ if agent_config :
2675+ commands_dir = project_path / agent_config ["dir" ]
2676+ pre_existing = set (commands_dir .rglob ("*" )) if commands_dir .is_dir () else set ()
2677+ else :
2678+ pre_existing = set ()
26432679
26442680 reregistered = 0
26452681 for ext_id , ext_data in extensions .items ():
@@ -2668,8 +2704,19 @@ def _reregister_extension_commands(project_path: Path, agent_id: str) -> None:
26682704 except Exception :
26692705 continue
26702706
2707+ # Collect files created by extension registration
2708+ if agent_config :
2709+ commands_dir = project_path / agent_config ["dir" ]
2710+ if commands_dir .is_dir ():
2711+ for p in commands_dir .rglob ("*" ):
2712+ if p .is_file () and p not in pre_existing :
2713+ created_files .append (p )
2714+
26712715 if reregistered :
2672- console .print (f" [green]✓[/green] Re-registered { reregistered } extension command(s)" )
2716+ console .print (f" [green]✓[/green] Re-registered { reregistered } extension command(s)"
2717+ f" ({ len (created_files )} file(s))" )
2718+
2719+ return created_files
26732720
26742721
26752722@agent_app .command ("search" )
0 commit comments