mirror of
https://github.com/github/spec-kit.git
synced 2026-03-21 12:53:08 +00:00
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
This commit is contained in:
committed by
GitHub
parent
a63c248c80
commit
e190116d13
@@ -36,7 +36,7 @@ import json5
|
||||
import stat
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Tuple
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import typer
|
||||
import httpx
|
||||
@@ -2543,9 +2543,10 @@ def agent_switch(
|
||||
from .agent_pack import (
|
||||
resolve_agent_pack,
|
||||
load_bootstrap,
|
||||
check_modified_files,
|
||||
get_tracked_files,
|
||||
PackResolutionError,
|
||||
AgentPackError,
|
||||
AgentFileModifiedError,
|
||||
)
|
||||
|
||||
show_banner()
|
||||
@@ -2582,13 +2583,28 @@ def agent_switch(
|
||||
try:
|
||||
current_resolved = resolve_agent_pack(current_agent, project_path=project_path)
|
||||
current_bootstrap = load_bootstrap(current_resolved.path, current_resolved.manifest)
|
||||
|
||||
# Check for modified files BEFORE teardown and prompt for confirmation
|
||||
modified = check_modified_files(project_path, current_agent)
|
||||
if modified and not force:
|
||||
console.print("[yellow]The following files have been modified since installation:[/yellow]")
|
||||
for f in modified:
|
||||
console.print(f" {f}")
|
||||
if not typer.confirm("Remove these modified files?"):
|
||||
console.print("[dim]Aborted. Use --force to skip this check.[/dim]")
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Retrieve tracked file lists and feed them into teardown
|
||||
agent_files, extension_files = get_tracked_files(project_path, current_agent)
|
||||
all_files = {**agent_files, **extension_files}
|
||||
|
||||
console.print(f" [dim]Tearing down {current_agent}...[/dim]")
|
||||
current_bootstrap.teardown(project_path, force=force)
|
||||
current_bootstrap.teardown(
|
||||
project_path,
|
||||
force=True, # already confirmed above
|
||||
files=all_files if all_files else None,
|
||||
)
|
||||
console.print(f" [green]✓[/green] {current_agent} removed")
|
||||
except AgentFileModifiedError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc}")
|
||||
console.print("[yellow]Hint:[/yellow] Use --force to remove modified files.")
|
||||
raise typer.Exit(1)
|
||||
except AgentPackError:
|
||||
# If pack-based teardown fails, try legacy cleanup via AGENT_CONFIG
|
||||
agent_config = AGENT_CONFIG.get(current_agent, {})
|
||||
@@ -2603,9 +2619,7 @@ def agent_switch(
|
||||
try:
|
||||
new_bootstrap = load_bootstrap(resolved.path, resolved.manifest)
|
||||
console.print(f" [dim]Setting up {agent_id}...[/dim]")
|
||||
new_bootstrap.setup(project_path, script_type, options)
|
||||
# Record all installed files for tracked teardown
|
||||
new_bootstrap.finalize_setup(project_path)
|
||||
agent_files = new_bootstrap.setup(project_path, script_type, options)
|
||||
console.print(f" [green]✓[/green] {agent_id} installed")
|
||||
except AgentPackError as exc:
|
||||
console.print(f"[red]Error setting up {agent_id}:[/red] {exc}")
|
||||
@@ -2614,32 +2628,54 @@ def agent_switch(
|
||||
# Update init options
|
||||
options["ai"] = agent_id
|
||||
init_options_file.write_text(json.dumps(options, indent=2), encoding="utf-8")
|
||||
console.print(f"\n[bold green]Successfully switched to {resolved.manifest.name}[/bold green]")
|
||||
|
||||
# Re-register extension commands for the new agent
|
||||
_reregister_extension_commands(project_path, agent_id)
|
||||
extension_files = _reregister_extension_commands(project_path, agent_id)
|
||||
|
||||
# Record all installed files (agent + extensions) for tracked teardown
|
||||
new_bootstrap.finalize_setup(
|
||||
project_path,
|
||||
agent_files=agent_files,
|
||||
extension_files=extension_files,
|
||||
)
|
||||
|
||||
console.print(f"\n[bold green]Successfully switched to {resolved.manifest.name}[/bold green]")
|
||||
|
||||
|
||||
def _reregister_extension_commands(project_path: Path, agent_id: str) -> None:
|
||||
"""Re-register all installed extension commands for a new agent after switching."""
|
||||
def _reregister_extension_commands(project_path: Path, agent_id: str) -> List[Path]:
|
||||
"""Re-register all installed extension commands for a new agent after switching.
|
||||
|
||||
Returns:
|
||||
List of absolute file paths created by extension registration.
|
||||
"""
|
||||
created_files: List[Path] = []
|
||||
registry_file = project_path / ".specify" / "extensions" / ".registry"
|
||||
if not registry_file.is_file():
|
||||
return
|
||||
return created_files
|
||||
|
||||
try:
|
||||
registry_data = json.loads(registry_file.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return
|
||||
return created_files
|
||||
|
||||
extensions = registry_data.get("extensions", {})
|
||||
if not extensions:
|
||||
return
|
||||
return created_files
|
||||
|
||||
try:
|
||||
from .agents import CommandRegistrar
|
||||
registrar = CommandRegistrar()
|
||||
except ImportError:
|
||||
return
|
||||
return created_files
|
||||
|
||||
# Snapshot the commands directory before registration so we can
|
||||
# detect which files were created by extension commands.
|
||||
agent_config = registrar.AGENT_CONFIGS.get(agent_id)
|
||||
if agent_config:
|
||||
commands_dir = project_path / agent_config["dir"]
|
||||
pre_existing = set(commands_dir.rglob("*")) if commands_dir.is_dir() else set()
|
||||
else:
|
||||
pre_existing = set()
|
||||
|
||||
reregistered = 0
|
||||
for ext_id, ext_data in extensions.items():
|
||||
@@ -2668,8 +2704,19 @@ def _reregister_extension_commands(project_path: Path, agent_id: str) -> None:
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Collect files created by extension registration
|
||||
if agent_config:
|
||||
commands_dir = project_path / agent_config["dir"]
|
||||
if commands_dir.is_dir():
|
||||
for p in commands_dir.rglob("*"):
|
||||
if p.is_file() and p not in pre_existing:
|
||||
created_files.append(p)
|
||||
|
||||
if reregistered:
|
||||
console.print(f" [green]✓[/green] Re-registered {reregistered} extension command(s)")
|
||||
console.print(f" [green]✓[/green] Re-registered {reregistered} extension command(s)"
|
||||
f" ({len(created_files)} file(s))")
|
||||
|
||||
return created_files
|
||||
|
||||
|
||||
@agent_app.command("search")
|
||||
|
||||
@@ -184,35 +184,58 @@ class AgentBootstrap:
|
||||
|
||||
# -- lifecycle -----------------------------------------------------------
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install agent files into *project_path*.
|
||||
|
||||
This is invoked by ``specify init --ai <agent>`` and
|
||||
``specify agent switch <agent>``.
|
||||
|
||||
Implementations **must** return every file they create so that the
|
||||
CLI can record both agent-installed files and extension-installed
|
||||
files in a single install manifest.
|
||||
|
||||
Args:
|
||||
project_path: Target project directory.
|
||||
script_type: ``"sh"`` or ``"ps"``.
|
||||
options: Arbitrary key/value options forwarded from the CLI.
|
||||
|
||||
Returns:
|
||||
List of absolute paths of files created during setup.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(
|
||||
self,
|
||||
project_path: Path,
|
||||
*,
|
||||
force: bool = False,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
) -> List[str]:
|
||||
"""Remove agent-specific files from *project_path*.
|
||||
|
||||
Invoked by ``specify agent switch`` (for the *old* agent) and
|
||||
``specify agent remove`` when the user explicitly uninstalls.
|
||||
Must preserve shared infrastructure (specs, plans, tasks, etc.).
|
||||
|
||||
Only individual files recorded in the install manifest are removed
|
||||
— directories are never deleted. If any tracked file has been
|
||||
modified since installation and *force* is ``False``, raises
|
||||
:class:`AgentFileModifiedError`.
|
||||
Only individual files are removed — directories are **never**
|
||||
deleted.
|
||||
|
||||
The caller (CLI) is expected to check for user-modified files
|
||||
**before** invoking teardown and prompt for confirmation. If
|
||||
*files* is provided, exactly those files are removed (values are
|
||||
ignored but kept for forward compatibility). Otherwise the
|
||||
install manifest is read.
|
||||
|
||||
Args:
|
||||
project_path: Project directory to clean up.
|
||||
force: When ``True``, remove files even if they were modified
|
||||
after installation.
|
||||
files: Mapping of project-relative path → SHA-256 hash.
|
||||
When supplied, only these files are removed and the
|
||||
install manifest is not consulted.
|
||||
|
||||
Returns:
|
||||
List of project-relative paths that were actually deleted.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -222,21 +245,44 @@ class AgentBootstrap:
|
||||
"""Return the agent's top-level directory inside the project."""
|
||||
return project_path / self.manifest.commands_dir.split("/")[0]
|
||||
|
||||
def finalize_setup(self, project_path: Path) -> None:
|
||||
"""Record all files in the agent directory for tracked teardown.
|
||||
def finalize_setup(
|
||||
self,
|
||||
project_path: Path,
|
||||
agent_files: Optional[List[Path]] = None,
|
||||
extension_files: Optional[List[Path]] = None,
|
||||
) -> None:
|
||||
"""Record installed files for tracked teardown.
|
||||
|
||||
This must be called **after** the full init pipeline has finished
|
||||
writing files (commands, context files, etc.) into the agent
|
||||
directory. It scans ``self.manifest.commands_dir`` and records
|
||||
every file with its SHA-256 hash so that :meth:`teardown` can
|
||||
detect user modifications.
|
||||
writing files (commands, context files, extensions) into the
|
||||
project. It combines the files reported by :meth:`setup` with
|
||||
any extra files (e.g. from extension registration), scans the
|
||||
agent's ``commands_dir`` for anything additional, and writes the
|
||||
install manifest.
|
||||
|
||||
Args:
|
||||
agent_files: Files reported by :meth:`setup`.
|
||||
extension_files: Files created by extension registration.
|
||||
"""
|
||||
if not self.manifest.commands_dir:
|
||||
return
|
||||
commands_dir = project_path / self.manifest.commands_dir
|
||||
if commands_dir.is_dir():
|
||||
installed = [p for p in commands_dir.rglob("*") if p.is_file()]
|
||||
record_installed_files(project_path, self.manifest.id, installed)
|
||||
all_agent = list(agent_files or [])
|
||||
all_extension = list(extension_files or [])
|
||||
|
||||
# Also scan the commands directory for files created by the
|
||||
# init pipeline that setup() did not report directly.
|
||||
if self.manifest.commands_dir:
|
||||
commands_dir = project_path / self.manifest.commands_dir
|
||||
if commands_dir.is_dir():
|
||||
agent_set = {p.resolve() for p in all_agent}
|
||||
for p in commands_dir.rglob("*"):
|
||||
if p.is_file() and p.resolve() not in agent_set:
|
||||
all_agent.append(p)
|
||||
|
||||
record_installed_files(
|
||||
project_path,
|
||||
self.manifest.id,
|
||||
agent_files=all_agent,
|
||||
extension_files=all_extension,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -257,41 +303,107 @@ def _sha256(path: Path) -> str:
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def record_installed_files(
|
||||
def _hash_file_list(
|
||||
project_path: Path,
|
||||
agent_id: str,
|
||||
files: List[Path],
|
||||
) -> Path:
|
||||
"""Record the installed files and their SHA-256 hashes.
|
||||
|
||||
Writes ``.specify/agent-manifest-<agent_id>.json`` containing a
|
||||
mapping of project-relative paths to their SHA-256 digests.
|
||||
|
||||
Args:
|
||||
project_path: Project root directory.
|
||||
agent_id: Agent identifier.
|
||||
files: Absolute or project-relative paths of the files that
|
||||
were created during ``setup()``.
|
||||
|
||||
Returns:
|
||||
Path to the written manifest file.
|
||||
"""
|
||||
) -> Dict[str, str]:
|
||||
"""Build a {relative_path: sha256} dict from a list of file paths."""
|
||||
entries: Dict[str, str] = {}
|
||||
for file_path in files:
|
||||
abs_path = project_path / file_path if not file_path.is_absolute() else file_path
|
||||
if abs_path.is_file():
|
||||
rel = str(abs_path.relative_to(project_path))
|
||||
entries[rel] = _sha256(abs_path)
|
||||
return entries
|
||||
|
||||
|
||||
def record_installed_files(
|
||||
project_path: Path,
|
||||
agent_id: str,
|
||||
agent_files: Optional[List[Path]] = None,
|
||||
extension_files: Optional[List[Path]] = None,
|
||||
) -> Path:
|
||||
"""Record the installed files and their SHA-256 hashes.
|
||||
|
||||
Writes ``.specify/agent-manifest-<agent_id>.json`` containing
|
||||
categorised mappings of project-relative paths to SHA-256 digests.
|
||||
|
||||
Args:
|
||||
project_path: Project root directory.
|
||||
agent_id: Agent identifier.
|
||||
agent_files: Files created by the agent's ``setup()`` and the
|
||||
init pipeline (core commands / templates).
|
||||
extension_files: Files created by extension registration.
|
||||
|
||||
Returns:
|
||||
Path to the written manifest file.
|
||||
"""
|
||||
agent_entries = _hash_file_list(project_path, agent_files or [])
|
||||
extension_entries = _hash_file_list(project_path, extension_files or [])
|
||||
|
||||
manifest_file = _manifest_path(project_path, agent_id)
|
||||
manifest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
manifest_file.write_text(
|
||||
json.dumps({"agent_id": agent_id, "files": entries}, indent=2),
|
||||
json.dumps(
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_files": agent_entries,
|
||||
"extension_files": extension_entries,
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest_file
|
||||
|
||||
|
||||
def _all_tracked_entries(data: dict) -> Dict[str, str]:
|
||||
"""Return the combined file → hash mapping from a manifest dict.
|
||||
|
||||
Supports both the new categorised layout (``agent_files`` +
|
||||
``extension_files``) and the legacy flat ``files`` key.
|
||||
"""
|
||||
combined: Dict[str, str] = {}
|
||||
# Legacy flat format
|
||||
if "files" in data and isinstance(data["files"], dict):
|
||||
combined.update(data["files"])
|
||||
# New categorised format
|
||||
if "agent_files" in data and isinstance(data["agent_files"], dict):
|
||||
combined.update(data["agent_files"])
|
||||
if "extension_files" in data and isinstance(data["extension_files"], dict):
|
||||
combined.update(data["extension_files"])
|
||||
return combined
|
||||
|
||||
|
||||
def get_tracked_files(
|
||||
project_path: Path,
|
||||
agent_id: str,
|
||||
) -> tuple[Dict[str, str], Dict[str, str]]:
|
||||
"""Return the tracked file hashes split by source.
|
||||
|
||||
Returns:
|
||||
A tuple ``(agent_files, extension_files)`` where each is a
|
||||
``{relative_path: sha256}`` dict. Returns two empty dicts
|
||||
when no install manifest exists.
|
||||
"""
|
||||
manifest_file = _manifest_path(project_path, agent_id)
|
||||
if not manifest_file.is_file():
|
||||
return {}, {}
|
||||
|
||||
try:
|
||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {}, {}
|
||||
|
||||
# Support legacy flat format
|
||||
if "files" in data and "agent_files" not in data:
|
||||
return dict(data["files"]), {}
|
||||
|
||||
agent_entries = data.get("agent_files", {})
|
||||
ext_entries = data.get("extension_files", {})
|
||||
return dict(agent_entries), dict(ext_entries)
|
||||
|
||||
|
||||
def check_modified_files(
|
||||
project_path: Path,
|
||||
agent_id: str,
|
||||
@@ -310,8 +422,10 @@ def check_modified_files(
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return []
|
||||
|
||||
entries = _all_tracked_entries(data)
|
||||
|
||||
modified: List[str] = []
|
||||
for rel_path, original_hash in data.get("files", {}).items():
|
||||
for rel_path, original_hash in entries.items():
|
||||
abs_path = project_path / rel_path
|
||||
if abs_path.is_file():
|
||||
if _sha256(abs_path) != original_hash:
|
||||
@@ -327,11 +441,18 @@ def remove_tracked_files(
|
||||
agent_id: str,
|
||||
*,
|
||||
force: bool = False,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
) -> List[str]:
|
||||
"""Remove the individual files recorded in the install manifest.
|
||||
"""Remove individual tracked files.
|
||||
|
||||
If *files* is provided, exactly those files are removed (the values
|
||||
are ignored but accepted for forward compatibility). Otherwise the
|
||||
install manifest for *agent_id* is read.
|
||||
|
||||
Raises :class:`AgentFileModifiedError` if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
modified and *force* is ``False`` (only when reading from the
|
||||
manifest — callers that pass *files* are expected to have already
|
||||
prompted the user).
|
||||
|
||||
Directories are **never** deleted — only individual files.
|
||||
|
||||
@@ -339,32 +460,37 @@ def remove_tracked_files(
|
||||
project_path: Project root directory.
|
||||
agent_id: Agent identifier.
|
||||
force: When ``True``, delete even modified files.
|
||||
files: Explicit mapping of project-relative path → hash. When
|
||||
supplied, the install manifest is not consulted.
|
||||
|
||||
Returns:
|
||||
List of project-relative paths that were removed.
|
||||
"""
|
||||
manifest_file = _manifest_path(project_path, agent_id)
|
||||
if not manifest_file.is_file():
|
||||
return []
|
||||
|
||||
try:
|
||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return []
|
||||
if files is not None:
|
||||
entries = files
|
||||
else:
|
||||
if not manifest_file.is_file():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return []
|
||||
|
||||
entries: Dict[str, str] = data.get("files", {})
|
||||
if not entries:
|
||||
manifest_file.unlink(missing_ok=True)
|
||||
return []
|
||||
entries = _all_tracked_entries(data)
|
||||
if not entries:
|
||||
manifest_file.unlink(missing_ok=True)
|
||||
return []
|
||||
|
||||
if not force:
|
||||
modified = check_modified_files(project_path, agent_id)
|
||||
if modified:
|
||||
raise AgentFileModifiedError(
|
||||
f"The following agent files have been modified since installation:\n"
|
||||
+ "\n".join(f" {p}" for p in modified)
|
||||
+ "\nUse --force to remove them anyway."
|
||||
)
|
||||
if not force:
|
||||
modified = check_modified_files(project_path, agent_id)
|
||||
if modified:
|
||||
raise AgentFileModifiedError(
|
||||
f"The following agent files have been modified since installation:\n"
|
||||
+ "\n".join(f" {p}" for p in modified)
|
||||
+ "\nUse --force to remove them anyway."
|
||||
)
|
||||
|
||||
removed: List[str] = []
|
||||
for rel_path in entries:
|
||||
@@ -374,7 +500,8 @@ def remove_tracked_files(
|
||||
removed.append(rel_path)
|
||||
|
||||
# Clean up the install manifest itself
|
||||
manifest_file.unlink(missing_ok=True)
|
||||
if manifest_file.is_file():
|
||||
manifest_file.unlink(missing_ok=True)
|
||||
return removed
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Antigravity agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Agy(AgentBootstrap):
|
||||
AGENT_DIR = ".agent"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Antigravity agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Antigravity agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Amp agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Amp(AgentBootstrap):
|
||||
AGENT_DIR = ".agents"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Amp agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Amp agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Auggie CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Auggie(AgentBootstrap):
|
||||
AGENT_DIR = ".augment"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Auggie CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Auggie CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for IBM Bob agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Bob(AgentBootstrap):
|
||||
AGENT_DIR = ".bob"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install IBM Bob agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove IBM Bob agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Claude Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Claude(AgentBootstrap):
|
||||
AGENT_DIR = ".claude"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Claude Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Claude Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for CodeBuddy agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Codebuddy(AgentBootstrap):
|
||||
AGENT_DIR = ".codebuddy"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install CodeBuddy agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove CodeBuddy agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Codex CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Codex(AgentBootstrap):
|
||||
AGENT_DIR = ".agents"
|
||||
COMMANDS_SUBDIR = "skills"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Codex CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Codex CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for GitHub Copilot agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Copilot(AgentBootstrap):
|
||||
AGENT_DIR = ".github"
|
||||
COMMANDS_SUBDIR = "agents"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install GitHub Copilot agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove GitHub Copilot agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Cursor agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class CursorAgent(AgentBootstrap):
|
||||
AGENT_DIR = ".cursor"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Cursor agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Cursor agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Gemini CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Gemini(AgentBootstrap):
|
||||
AGENT_DIR = ".gemini"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Gemini CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Gemini CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for iFlow CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Iflow(AgentBootstrap):
|
||||
AGENT_DIR = ".iflow"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install iFlow CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove iFlow CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Junie agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Junie(AgentBootstrap):
|
||||
AGENT_DIR = ".junie"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Junie agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Junie agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Kilo Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Kilocode(AgentBootstrap):
|
||||
AGENT_DIR = ".kilocode"
|
||||
COMMANDS_SUBDIR = "workflows"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Kilo Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Kilo Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Kimi Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Kimi(AgentBootstrap):
|
||||
AGENT_DIR = ".kimi"
|
||||
COMMANDS_SUBDIR = "skills"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Kimi Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Kimi Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Kiro CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class KiroCli(AgentBootstrap):
|
||||
AGENT_DIR = ".kiro"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Kiro CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Kiro CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for opencode agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Opencode(AgentBootstrap):
|
||||
AGENT_DIR = ".opencode"
|
||||
COMMANDS_SUBDIR = "command"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install opencode agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove opencode agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Pi Coding Agent agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Pi(AgentBootstrap):
|
||||
AGENT_DIR = ".pi"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Pi Coding Agent agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Pi Coding Agent agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Qoder CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Qodercli(AgentBootstrap):
|
||||
AGENT_DIR = ".qoder"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Qoder CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Qoder CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Qwen Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Qwen(AgentBootstrap):
|
||||
AGENT_DIR = ".qwen"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Qwen Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Qwen Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Roo Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Roo(AgentBootstrap):
|
||||
AGENT_DIR = ".roo"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Roo Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Roo Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for SHAI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Shai(AgentBootstrap):
|
||||
AGENT_DIR = ".shai"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install SHAI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove SHAI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Tabnine CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Tabnine(AgentBootstrap):
|
||||
AGENT_DIR = ".tabnine/agent"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Tabnine CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Tabnine CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Trae agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Trae(AgentBootstrap):
|
||||
AGENT_DIR = ".trae"
|
||||
COMMANDS_SUBDIR = "rules"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Trae agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Trae agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Mistral Vibe agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Vibe(AgentBootstrap):
|
||||
AGENT_DIR = ".vibe"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Mistral Vibe agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Mistral Vibe agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Bootstrap module for Windsurf agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
@@ -12,16 +12,19 @@ class Windsurf(AgentBootstrap):
|
||||
AGENT_DIR = ".windsurf"
|
||||
COMMANDS_SUBDIR = "workflows"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Windsurf agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return [] # directories only — actual files are created by the init pipeline
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Windsurf agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
||||
was modified and *force* is ``False``.
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
|
||||
@@ -27,6 +27,7 @@ from specify_cli.agent_pack import (
|
||||
_sha256,
|
||||
check_modified_files,
|
||||
export_pack,
|
||||
get_tracked_files,
|
||||
list_all_agents,
|
||||
list_embedded_agents,
|
||||
load_bootstrap,
|
||||
@@ -79,17 +80,18 @@ def _write_bootstrap(pack_dir: Path, class_name: str = "TestAgent", agent_dir: s
|
||||
bootstrap_file = pack_dir / BOOTSTRAP_FILENAME
|
||||
bootstrap_file.write_text(textwrap.dedent(f"""\
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, List, Optional
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
class {class_name}(AgentBootstrap):
|
||||
AGENT_DIR = "{agent_dir}"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> None:
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
(project_path / self.AGENT_DIR / "commands").mkdir(parents=True, exist_ok=True)
|
||||
return []
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False) -> None:
|
||||
remove_tracked_files(project_path, self.manifest.id, force=force)
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
"""), encoding="utf-8")
|
||||
return bootstrap_file
|
||||
|
||||
@@ -273,20 +275,31 @@ class TestBootstrapContract:
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
|
||||
b.setup(project, "sh", {})
|
||||
agent_files = b.setup(project, "sh", {})
|
||||
assert isinstance(agent_files, list)
|
||||
assert (project / ".test-agent" / "commands").is_dir()
|
||||
|
||||
# Simulate the init pipeline writing a file
|
||||
cmd_file = project / ".test-agent" / "commands" / "hello.md"
|
||||
cmd_file.write_text("hello", encoding="utf-8")
|
||||
|
||||
# finalize_setup records files for tracking
|
||||
b.finalize_setup(project)
|
||||
# Simulate extension registration writing a file
|
||||
ext_file = project / ".test-agent" / "commands" / "ext-cmd.md"
|
||||
ext_file.write_text("ext", encoding="utf-8")
|
||||
|
||||
# finalize_setup records both agent and extension files
|
||||
b.finalize_setup(project, agent_files=agent_files, extension_files=[ext_file])
|
||||
assert _manifest_path(project, "test-agent").is_file()
|
||||
|
||||
# Verify the manifest separates agent and extension files
|
||||
manifest_data = json.loads(_manifest_path(project, "test-agent").read_text())
|
||||
assert "agent_files" in manifest_data
|
||||
assert "extension_files" in manifest_data
|
||||
|
||||
b.teardown(project)
|
||||
# The tracked file should be removed
|
||||
# The tracked files should be removed
|
||||
assert not cmd_file.exists()
|
||||
assert not ext_file.exists()
|
||||
# Install manifest itself should be cleaned up
|
||||
assert not _manifest_path(project, "test-agent").is_file()
|
||||
# Directories are preserved (only files are removed)
|
||||
@@ -557,7 +570,7 @@ class TestFileTracking:
|
||||
f.parent.mkdir(parents=True)
|
||||
f.write_text("hello world", encoding="utf-8")
|
||||
|
||||
record_installed_files(project, "myagent", [f])
|
||||
record_installed_files(project, "myagent", agent_files=[f])
|
||||
|
||||
# No modifications yet
|
||||
assert check_modified_files(project, "myagent") == []
|
||||
@@ -571,7 +584,7 @@ class TestFileTracking:
|
||||
f.parent.mkdir(parents=True)
|
||||
f.write_text("original", encoding="utf-8")
|
||||
|
||||
record_installed_files(project, "myagent", [f])
|
||||
record_installed_files(project, "myagent", agent_files=[f])
|
||||
|
||||
# Now modify the file
|
||||
f.write_text("modified content", encoding="utf-8")
|
||||
@@ -595,7 +608,7 @@ class TestFileTracking:
|
||||
f1.write_text("aaa", encoding="utf-8")
|
||||
f2.write_text("bbb", encoding="utf-8")
|
||||
|
||||
record_installed_files(project, "ag", [f1, f2])
|
||||
record_installed_files(project, "ag", agent_files=[f1, f2])
|
||||
|
||||
removed = remove_tracked_files(project, "ag")
|
||||
assert len(removed) == 2
|
||||
@@ -615,7 +628,7 @@ class TestFileTracking:
|
||||
f.parent.mkdir(parents=True)
|
||||
f.write_text("original", encoding="utf-8")
|
||||
|
||||
record_installed_files(project, "ag", [f])
|
||||
record_installed_files(project, "ag", agent_files=[f])
|
||||
f.write_text("user-edited", encoding="utf-8")
|
||||
|
||||
with pytest.raises(AgentFileModifiedError, match="modified"):
|
||||
@@ -633,7 +646,7 @@ class TestFileTracking:
|
||||
f.parent.mkdir(parents=True)
|
||||
f.write_text("original", encoding="utf-8")
|
||||
|
||||
record_installed_files(project, "ag", [f])
|
||||
record_installed_files(project, "ag", agent_files=[f])
|
||||
f.write_text("user-edited", encoding="utf-8")
|
||||
|
||||
removed = remove_tracked_files(project, "ag", force=True)
|
||||
@@ -655,7 +668,7 @@ class TestFileTracking:
|
||||
f = d / "deep.md"
|
||||
f.write_text("deep", encoding="utf-8")
|
||||
|
||||
record_installed_files(project, "myagent", [f])
|
||||
record_installed_files(project, "myagent", agent_files=[f])
|
||||
remove_tracked_files(project, "myagent")
|
||||
|
||||
assert not f.exists()
|
||||
@@ -672,7 +685,7 @@ class TestFileTracking:
|
||||
f.parent.mkdir(parents=True)
|
||||
f.write_text("data", encoding="utf-8")
|
||||
|
||||
record_installed_files(project, "ag", [f])
|
||||
record_installed_files(project, "ag", agent_files=[f])
|
||||
|
||||
# User deletes the file before teardown
|
||||
f.unlink()
|
||||
@@ -700,10 +713,98 @@ class TestFileTracking:
|
||||
f.parent.mkdir(parents=True)
|
||||
f.write_text("content", encoding="utf-8")
|
||||
|
||||
manifest_file = record_installed_files(project, "ag", [f])
|
||||
manifest_file = record_installed_files(project, "ag", agent_files=[f])
|
||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
|
||||
assert data["agent_id"] == "ag"
|
||||
assert isinstance(data["files"], dict)
|
||||
assert ".ag/x.md" in data["files"]
|
||||
assert len(data["files"][".ag/x.md"]) == 64
|
||||
assert isinstance(data["agent_files"], dict)
|
||||
assert ".ag/x.md" in data["agent_files"]
|
||||
assert len(data["agent_files"][".ag/x.md"]) == 64
|
||||
|
||||
# -- New: categorised manifest & explicit file teardown --
|
||||
|
||||
def test_manifest_categorises_agent_and_extension_files(self, tmp_path):
|
||||
"""record_installed_files stores agent and extension files separately."""
|
||||
project = tmp_path / "project"
|
||||
(project / ".specify").mkdir(parents=True)
|
||||
|
||||
agent_f = project / ".ag" / "core.md"
|
||||
ext_f = project / ".ag" / "ext-cmd.md"
|
||||
agent_f.parent.mkdir(parents=True)
|
||||
agent_f.write_text("core", encoding="utf-8")
|
||||
ext_f.write_text("ext", encoding="utf-8")
|
||||
|
||||
manifest_file = record_installed_files(
|
||||
project, "ag", agent_files=[agent_f], extension_files=[ext_f]
|
||||
)
|
||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
|
||||
assert ".ag/core.md" in data["agent_files"]
|
||||
assert ".ag/ext-cmd.md" in data["extension_files"]
|
||||
assert ".ag/core.md" not in data.get("extension_files", {})
|
||||
assert ".ag/ext-cmd.md" not in data.get("agent_files", {})
|
||||
|
||||
def test_get_tracked_files_returns_both_categories(self, tmp_path):
|
||||
"""get_tracked_files splits agent and extension files."""
|
||||
project = tmp_path / "project"
|
||||
(project / ".specify").mkdir(parents=True)
|
||||
|
||||
agent_f = project / ".ag" / "a.md"
|
||||
ext_f = project / ".ag" / "e.md"
|
||||
agent_f.parent.mkdir(parents=True)
|
||||
agent_f.write_text("a", encoding="utf-8")
|
||||
ext_f.write_text("e", encoding="utf-8")
|
||||
|
||||
record_installed_files(
|
||||
project, "ag", agent_files=[agent_f], extension_files=[ext_f]
|
||||
)
|
||||
|
||||
agent_files, extension_files = get_tracked_files(project, "ag")
|
||||
assert ".ag/a.md" in agent_files
|
||||
assert ".ag/e.md" in extension_files
|
||||
|
||||
def test_get_tracked_files_no_manifest(self, tmp_path):
|
||||
"""get_tracked_files returns ({}, {}) when no manifest exists."""
|
||||
agent_files, extension_files = get_tracked_files(tmp_path, "nope")
|
||||
assert agent_files == {}
|
||||
assert extension_files == {}
|
||||
|
||||
def test_teardown_with_explicit_files(self, tmp_path):
|
||||
"""teardown accepts explicit files dict (CLI-driven teardown)."""
|
||||
project = tmp_path / "project"
|
||||
(project / ".specify").mkdir(parents=True)
|
||||
|
||||
f1 = project / ".ag" / "a.md"
|
||||
f2 = project / ".ag" / "b.md"
|
||||
f1.parent.mkdir(parents=True)
|
||||
f1.write_text("aaa", encoding="utf-8")
|
||||
f2.write_text("bbb", encoding="utf-8")
|
||||
|
||||
# Record the files
|
||||
record_installed_files(project, "ag", agent_files=[f1, f2])
|
||||
|
||||
# Get the tracked entries
|
||||
agent_entries, _ = get_tracked_files(project, "ag")
|
||||
|
||||
# Pass explicit files to remove_tracked_files
|
||||
removed = remove_tracked_files(project, "ag", files=agent_entries)
|
||||
assert len(removed) == 2
|
||||
assert not f1.exists()
|
||||
assert not f2.exists()
|
||||
|
||||
def test_check_detects_extension_file_modification(self, tmp_path):
|
||||
"""Modified extension files are also detected by check_modified_files."""
|
||||
project = tmp_path / "project"
|
||||
(project / ".specify").mkdir(parents=True)
|
||||
|
||||
ext_f = project / ".ag" / "ext.md"
|
||||
ext_f.parent.mkdir(parents=True)
|
||||
ext_f.write_text("original", encoding="utf-8")
|
||||
|
||||
record_installed_files(project, "ag", extension_files=[ext_f])
|
||||
|
||||
ext_f.write_text("user-edited", encoding="utf-8")
|
||||
|
||||
modified = check_modified_files(project, "ag")
|
||||
assert len(modified) == 1
|
||||
assert ".ag/ext.md" in modified[0]
|
||||
|
||||
Reference in New Issue
Block a user