mirror of
https://github.com/github/spec-kit.git
synced 2026-03-24 14:23:09 +00:00
fix: hash-check before deletion, track all files, fix overrides bug, update help text
- remove_tracked_files: always compare SHA-256 hash before deleting, even when called with explicit files dict; skip modified files unless --force is set (was unconditionally deleting all tracked files) - finalize_setup: track ALL files from setup() (no agent-root filter); safe because removal now checks hashes - list_all_agents: track embedded versions in separate dict so overrides always reference the correct embedded version, not a catalog/project pack that overwrote the seen dict - --ai-skills help text: updated to say 'requires --ai or --agent'
This commit is contained in:
@@ -1725,7 +1725,7 @@ def init(
|
||||
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
|
||||
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
|
||||
github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
|
||||
ai_skills: bool = typer.Option(False, "--ai-skills", help="Install Prompt.MD templates as agent skills (requires --ai)"),
|
||||
ai_skills: bool = typer.Option(False, "--ai-skills", help="Install Prompt.MD templates as agent skills (requires --ai or --agent)"),
|
||||
offline: bool = typer.Option(False, "--offline", help="Use assets bundled in the specify-cli package instead of downloading from GitHub (no network access required). Bundled assets will become the default in v0.6.0 and this flag will be removed."),
|
||||
preset: str = typer.Option(None, "--preset", help="Install a preset during initialization (by preset ID)"),
|
||||
branch_numbering: str = typer.Option(None, "--branch-numbering", help="Branch numbering strategy: 'sequential' (001, 002, ...) or 'timestamp' (YYYYMMDD-HHMMSS)"),
|
||||
|
||||
@@ -346,21 +346,17 @@ class AgentBootstrap:
|
||||
the agent's directory tree for anything additional.
|
||||
|
||||
All files returned by ``setup()`` are tracked — including shared
|
||||
project infrastructure — so that teardown/switch can precisely
|
||||
remove everything the agent installed. This is intentional:
|
||||
``remove_tracked_files()`` only deletes files whose SHA-256
|
||||
hash still matches the original, so user-modified files are
|
||||
always preserved (unless ``--force`` is used).
|
||||
project infrastructure — so that teardown/switch can detect
|
||||
modifications. ``remove_tracked_files()`` compares SHA-256
|
||||
hashes before deleting and will only remove files whose hash
|
||||
still matches, preserving any user-modified files (unless
|
||||
``--force`` is used).
|
||||
|
||||
Args:
|
||||
agent_files: Files reported by :meth:`setup`.
|
||||
extension_files: Files created by extension registration.
|
||||
"""
|
||||
all_extension = list(extension_files or [])
|
||||
# Track ALL files returned by setup(), not just those under the
|
||||
# agent's directory tree. This is safe because teardown only
|
||||
# removes files that are unmodified (hash check) and prompts
|
||||
# for confirmation on modified files.
|
||||
all_agent: List[Path] = list(agent_files or [])
|
||||
|
||||
# Scan the agent's directory tree for files created by later
|
||||
@@ -641,9 +637,13 @@ def remove_tracked_files(
|
||||
)
|
||||
|
||||
removed: List[str] = []
|
||||
for rel_path in entries:
|
||||
for rel_path, original_hash in entries.items():
|
||||
abs_path = project_path / rel_path
|
||||
if abs_path.is_file():
|
||||
if original_hash and _sha256(abs_path) != original_hash:
|
||||
# File was modified since installation — skip unless forced
|
||||
if not force:
|
||||
continue
|
||||
abs_path.unlink()
|
||||
removed.append(rel_path)
|
||||
|
||||
@@ -792,6 +792,11 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
||||
"""
|
||||
seen: dict[str, ResolvedPack] = {}
|
||||
|
||||
# Track embedded versions separately so overrides can accurately
|
||||
# reference what they replace, even after catalog/project/user
|
||||
# packs have overwritten the seen dict entry.
|
||||
embedded_versions: dict[str, str] = {}
|
||||
|
||||
# Start from lowest priority (embedded) so higher priorities overwrite
|
||||
for manifest in list_embedded_agents():
|
||||
seen[manifest.id] = ResolvedPack(
|
||||
@@ -799,6 +804,7 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
||||
source="embedded",
|
||||
path=manifest.pack_path or _embedded_agents_dir() / manifest.id,
|
||||
)
|
||||
embedded_versions[manifest.id] = manifest.version
|
||||
|
||||
# Catalog cache
|
||||
catalog_dir = _catalog_agents_dir()
|
||||
@@ -808,7 +814,7 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
||||
if child.is_dir() and mf.is_file():
|
||||
try:
|
||||
m = AgentManifest.from_yaml(mf)
|
||||
overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None
|
||||
overrides = f"embedded v{embedded_versions[m.id]}" if m.id in embedded_versions else None
|
||||
seen[m.id] = ResolvedPack(manifest=m, source="catalog", path=child, overrides=overrides)
|
||||
except AgentPackError:
|
||||
continue
|
||||
@@ -822,7 +828,7 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
||||
if child.is_dir() and mf.is_file():
|
||||
try:
|
||||
m = AgentManifest.from_yaml(mf)
|
||||
overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None
|
||||
overrides = f"embedded v{embedded_versions[m.id]}" if m.id in embedded_versions else None
|
||||
seen[m.id] = ResolvedPack(manifest=m, source="project", path=child, overrides=overrides)
|
||||
except AgentPackError:
|
||||
continue
|
||||
@@ -835,7 +841,7 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
||||
if child.is_dir() and mf.is_file():
|
||||
try:
|
||||
m = AgentManifest.from_yaml(mf)
|
||||
overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None
|
||||
overrides = f"embedded v{embedded_versions[m.id]}" if m.id in embedded_versions else None
|
||||
seen[m.id] = ResolvedPack(manifest=m, source="user", path=child, overrides=overrides)
|
||||
except AgentPackError:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user