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)"),
|
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"),
|
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)"),
|
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."),
|
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)"),
|
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)"),
|
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.
|
the agent's directory tree for anything additional.
|
||||||
|
|
||||||
All files returned by ``setup()`` are tracked — including shared
|
All files returned by ``setup()`` are tracked — including shared
|
||||||
project infrastructure — so that teardown/switch can precisely
|
project infrastructure — so that teardown/switch can detect
|
||||||
remove everything the agent installed. This is intentional:
|
modifications. ``remove_tracked_files()`` compares SHA-256
|
||||||
``remove_tracked_files()`` only deletes files whose SHA-256
|
hashes before deleting and will only remove files whose hash
|
||||||
hash still matches the original, so user-modified files are
|
still matches, preserving any user-modified files (unless
|
||||||
always preserved (unless ``--force`` is used).
|
``--force`` is used).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
agent_files: Files reported by :meth:`setup`.
|
agent_files: Files reported by :meth:`setup`.
|
||||||
extension_files: Files created by extension registration.
|
extension_files: Files created by extension registration.
|
||||||
"""
|
"""
|
||||||
all_extension = list(extension_files or [])
|
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 [])
|
all_agent: List[Path] = list(agent_files or [])
|
||||||
|
|
||||||
# Scan the agent's directory tree for files created by later
|
# Scan the agent's directory tree for files created by later
|
||||||
@@ -641,9 +637,13 @@ def remove_tracked_files(
|
|||||||
)
|
)
|
||||||
|
|
||||||
removed: List[str] = []
|
removed: List[str] = []
|
||||||
for rel_path in entries:
|
for rel_path, original_hash in entries.items():
|
||||||
abs_path = project_path / rel_path
|
abs_path = project_path / rel_path
|
||||||
if abs_path.is_file():
|
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()
|
abs_path.unlink()
|
||||||
removed.append(rel_path)
|
removed.append(rel_path)
|
||||||
|
|
||||||
@@ -792,6 +792,11 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
|||||||
"""
|
"""
|
||||||
seen: dict[str, 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
|
# Start from lowest priority (embedded) so higher priorities overwrite
|
||||||
for manifest in list_embedded_agents():
|
for manifest in list_embedded_agents():
|
||||||
seen[manifest.id] = ResolvedPack(
|
seen[manifest.id] = ResolvedPack(
|
||||||
@@ -799,6 +804,7 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
|||||||
source="embedded",
|
source="embedded",
|
||||||
path=manifest.pack_path or _embedded_agents_dir() / manifest.id,
|
path=manifest.pack_path or _embedded_agents_dir() / manifest.id,
|
||||||
)
|
)
|
||||||
|
embedded_versions[manifest.id] = manifest.version
|
||||||
|
|
||||||
# Catalog cache
|
# Catalog cache
|
||||||
catalog_dir = _catalog_agents_dir()
|
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():
|
if child.is_dir() and mf.is_file():
|
||||||
try:
|
try:
|
||||||
m = AgentManifest.from_yaml(mf)
|
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)
|
seen[m.id] = ResolvedPack(manifest=m, source="catalog", path=child, overrides=overrides)
|
||||||
except AgentPackError:
|
except AgentPackError:
|
||||||
continue
|
continue
|
||||||
@@ -822,7 +828,7 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
|||||||
if child.is_dir() and mf.is_file():
|
if child.is_dir() and mf.is_file():
|
||||||
try:
|
try:
|
||||||
m = AgentManifest.from_yaml(mf)
|
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)
|
seen[m.id] = ResolvedPack(manifest=m, source="project", path=child, overrides=overrides)
|
||||||
except AgentPackError:
|
except AgentPackError:
|
||||||
continue
|
continue
|
||||||
@@ -835,7 +841,7 @@ def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
|||||||
if child.is_dir() and mf.is_file():
|
if child.is_dir() and mf.is_file():
|
||||||
try:
|
try:
|
||||||
m = AgentManifest.from_yaml(mf)
|
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)
|
seen[m.id] = ResolvedPack(manifest=m, source="user", path=child, overrides=overrides)
|
||||||
except AgentPackError:
|
except AgentPackError:
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user