Skip to content

Commit e190116

Browse files
Copilotmnriem
andauthored
refactor: setup reports files, CLI checks modifications before teardown, categorised manifest
- setup() returns List[Path] of installed files so CLI can record them - finalize_setup() accepts agent_files + extension_files for combined tracking - Install manifest categorises files: agent_files and extension_files - get_tracked_files() returns (agent_files, extension_files) split - remove_tracked_files() accepts explicit files dict for CLI-driven teardown - agent_switch checks for modifications BEFORE teardown and prompts user - _reregister_extension_commands() returns List[Path] of created files - teardown() accepts files parameter to receive explicit file lists - All 25 bootstraps updated with new signatures - 5 new tests: categorised manifest, get_tracked_files, explicit file teardown, extension file modification detection Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/spec-kit/sessions/32e470fc-6bf5-453c-bf6c-79a8521efa56
1 parent a63c248 commit e190116

File tree

28 files changed

+592
-242
lines changed

28 files changed

+592
-242
lines changed

src/specify_cli/__init__.py

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import stat
3737
import yaml
3838
from pathlib import Path
39-
from typing import Any, Optional, Tuple
39+
from typing import Any, List, Optional, Tuple
4040

4141
import typer
4242
import 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

Comments
 (0)