mirror of
https://github.com/github/spec-kit.git
synced 2026-03-22 13:23: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 stat
|
||||||
import yaml
|
import yaml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional, Tuple
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
import httpx
|
import httpx
|
||||||
@@ -2543,9 +2543,10 @@ def agent_switch(
|
|||||||
from .agent_pack import (
|
from .agent_pack import (
|
||||||
resolve_agent_pack,
|
resolve_agent_pack,
|
||||||
load_bootstrap,
|
load_bootstrap,
|
||||||
|
check_modified_files,
|
||||||
|
get_tracked_files,
|
||||||
PackResolutionError,
|
PackResolutionError,
|
||||||
AgentPackError,
|
AgentPackError,
|
||||||
AgentFileModifiedError,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
show_banner()
|
show_banner()
|
||||||
@@ -2582,13 +2583,28 @@ def agent_switch(
|
|||||||
try:
|
try:
|
||||||
current_resolved = resolve_agent_pack(current_agent, project_path=project_path)
|
current_resolved = resolve_agent_pack(current_agent, project_path=project_path)
|
||||||
current_bootstrap = load_bootstrap(current_resolved.path, current_resolved.manifest)
|
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]")
|
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")
|
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:
|
except AgentPackError:
|
||||||
# If pack-based teardown fails, try legacy cleanup via AGENT_CONFIG
|
# If pack-based teardown fails, try legacy cleanup via AGENT_CONFIG
|
||||||
agent_config = AGENT_CONFIG.get(current_agent, {})
|
agent_config = AGENT_CONFIG.get(current_agent, {})
|
||||||
@@ -2603,9 +2619,7 @@ def agent_switch(
|
|||||||
try:
|
try:
|
||||||
new_bootstrap = load_bootstrap(resolved.path, resolved.manifest)
|
new_bootstrap = load_bootstrap(resolved.path, resolved.manifest)
|
||||||
console.print(f" [dim]Setting up {agent_id}...[/dim]")
|
console.print(f" [dim]Setting up {agent_id}...[/dim]")
|
||||||
new_bootstrap.setup(project_path, script_type, options)
|
agent_files = new_bootstrap.setup(project_path, script_type, options)
|
||||||
# Record all installed files for tracked teardown
|
|
||||||
new_bootstrap.finalize_setup(project_path)
|
|
||||||
console.print(f" [green]✓[/green] {agent_id} installed")
|
console.print(f" [green]✓[/green] {agent_id} installed")
|
||||||
except AgentPackError as exc:
|
except AgentPackError as exc:
|
||||||
console.print(f"[red]Error setting up {agent_id}:[/red] {exc}")
|
console.print(f"[red]Error setting up {agent_id}:[/red] {exc}")
|
||||||
@@ -2614,32 +2628,54 @@ def agent_switch(
|
|||||||
# Update init options
|
# Update init options
|
||||||
options["ai"] = agent_id
|
options["ai"] = agent_id
|
||||||
init_options_file.write_text(json.dumps(options, indent=2), encoding="utf-8")
|
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
|
# 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:
|
def _reregister_extension_commands(project_path: Path, agent_id: str) -> List[Path]:
|
||||||
"""Re-register all installed extension commands for a new agent after switching."""
|
"""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"
|
registry_file = project_path / ".specify" / "extensions" / ".registry"
|
||||||
if not registry_file.is_file():
|
if not registry_file.is_file():
|
||||||
return
|
return created_files
|
||||||
|
|
||||||
try:
|
try:
|
||||||
registry_data = json.loads(registry_file.read_text(encoding="utf-8"))
|
registry_data = json.loads(registry_file.read_text(encoding="utf-8"))
|
||||||
except (json.JSONDecodeError, OSError):
|
except (json.JSONDecodeError, OSError):
|
||||||
return
|
return created_files
|
||||||
|
|
||||||
extensions = registry_data.get("extensions", {})
|
extensions = registry_data.get("extensions", {})
|
||||||
if not extensions:
|
if not extensions:
|
||||||
return
|
return created_files
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .agents import CommandRegistrar
|
from .agents import CommandRegistrar
|
||||||
registrar = CommandRegistrar()
|
registrar = CommandRegistrar()
|
||||||
except ImportError:
|
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
|
reregistered = 0
|
||||||
for ext_id, ext_data in extensions.items():
|
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:
|
except Exception:
|
||||||
continue
|
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:
|
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")
|
@agent_app.command("search")
|
||||||
|
|||||||
@@ -184,35 +184,58 @@ class AgentBootstrap:
|
|||||||
|
|
||||||
# -- lifecycle -----------------------------------------------------------
|
# -- 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*.
|
"""Install agent files into *project_path*.
|
||||||
|
|
||||||
This is invoked by ``specify init --ai <agent>`` and
|
This is invoked by ``specify init --ai <agent>`` and
|
||||||
``specify agent switch <agent>``.
|
``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:
|
Args:
|
||||||
project_path: Target project directory.
|
project_path: Target project directory.
|
||||||
script_type: ``"sh"`` or ``"ps"``.
|
script_type: ``"sh"`` or ``"ps"``.
|
||||||
options: Arbitrary key/value options forwarded from the CLI.
|
options: Arbitrary key/value options forwarded from the CLI.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of absolute paths of files created during setup.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
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*.
|
"""Remove agent-specific files from *project_path*.
|
||||||
|
|
||||||
Invoked by ``specify agent switch`` (for the *old* agent) and
|
Invoked by ``specify agent switch`` (for the *old* agent) and
|
||||||
``specify agent remove`` when the user explicitly uninstalls.
|
``specify agent remove`` when the user explicitly uninstalls.
|
||||||
Must preserve shared infrastructure (specs, plans, tasks, etc.).
|
Must preserve shared infrastructure (specs, plans, tasks, etc.).
|
||||||
|
|
||||||
Only individual files recorded in the install manifest are removed
|
Only individual files are removed — directories are **never**
|
||||||
— directories are never deleted. If any tracked file has been
|
deleted.
|
||||||
modified since installation and *force* is ``False``, raises
|
|
||||||
:class:`AgentFileModifiedError`.
|
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:
|
Args:
|
||||||
project_path: Project directory to clean up.
|
project_path: Project directory to clean up.
|
||||||
force: When ``True``, remove files even if they were modified
|
force: When ``True``, remove files even if they were modified
|
||||||
after installation.
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -222,21 +245,44 @@ class AgentBootstrap:
|
|||||||
"""Return the agent's top-level directory inside the project."""
|
"""Return the agent's top-level directory inside the project."""
|
||||||
return project_path / self.manifest.commands_dir.split("/")[0]
|
return project_path / self.manifest.commands_dir.split("/")[0]
|
||||||
|
|
||||||
def finalize_setup(self, project_path: Path) -> None:
|
def finalize_setup(
|
||||||
"""Record all files in the agent directory for tracked teardown.
|
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
|
This must be called **after** the full init pipeline has finished
|
||||||
writing files (commands, context files, etc.) into the agent
|
writing files (commands, context files, extensions) into the
|
||||||
directory. It scans ``self.manifest.commands_dir`` and records
|
project. It combines the files reported by :meth:`setup` with
|
||||||
every file with its SHA-256 hash so that :meth:`teardown` can
|
any extra files (e.g. from extension registration), scans the
|
||||||
detect user modifications.
|
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:
|
all_agent = list(agent_files or [])
|
||||||
return
|
all_extension = list(extension_files or [])
|
||||||
commands_dir = project_path / self.manifest.commands_dir
|
|
||||||
if commands_dir.is_dir():
|
# Also scan the commands directory for files created by the
|
||||||
installed = [p for p in commands_dir.rglob("*") if p.is_file()]
|
# init pipeline that setup() did not report directly.
|
||||||
record_installed_files(project_path, self.manifest.id, installed)
|
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()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def record_installed_files(
|
def _hash_file_list(
|
||||||
project_path: Path,
|
project_path: Path,
|
||||||
agent_id: str,
|
|
||||||
files: List[Path],
|
files: List[Path],
|
||||||
) -> Path:
|
) -> Dict[str, str]:
|
||||||
"""Record the installed files and their SHA-256 hashes.
|
"""Build a {relative_path: sha256} dict from a list of file paths."""
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
entries: Dict[str, str] = {}
|
entries: Dict[str, str] = {}
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
abs_path = project_path / file_path if not file_path.is_absolute() else file_path
|
abs_path = project_path / file_path if not file_path.is_absolute() else file_path
|
||||||
if abs_path.is_file():
|
if abs_path.is_file():
|
||||||
rel = str(abs_path.relative_to(project_path))
|
rel = str(abs_path.relative_to(project_path))
|
||||||
entries[rel] = _sha256(abs_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 = _manifest_path(project_path, agent_id)
|
||||||
manifest_file.parent.mkdir(parents=True, exist_ok=True)
|
manifest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
manifest_file.write_text(
|
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",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
return manifest_file
|
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(
|
def check_modified_files(
|
||||||
project_path: Path,
|
project_path: Path,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
@@ -310,8 +422,10 @@ def check_modified_files(
|
|||||||
except (json.JSONDecodeError, OSError):
|
except (json.JSONDecodeError, OSError):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
entries = _all_tracked_entries(data)
|
||||||
|
|
||||||
modified: List[str] = []
|
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
|
abs_path = project_path / rel_path
|
||||||
if abs_path.is_file():
|
if abs_path.is_file():
|
||||||
if _sha256(abs_path) != original_hash:
|
if _sha256(abs_path) != original_hash:
|
||||||
@@ -327,11 +441,18 @@ def remove_tracked_files(
|
|||||||
agent_id: str,
|
agent_id: str,
|
||||||
*,
|
*,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
|
files: Optional[Dict[str, str]] = None,
|
||||||
) -> List[str]:
|
) -> 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
|
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.
|
Directories are **never** deleted — only individual files.
|
||||||
|
|
||||||
@@ -339,32 +460,37 @@ def remove_tracked_files(
|
|||||||
project_path: Project root directory.
|
project_path: Project root directory.
|
||||||
agent_id: Agent identifier.
|
agent_id: Agent identifier.
|
||||||
force: When ``True``, delete even modified files.
|
force: When ``True``, delete even modified files.
|
||||||
|
files: Explicit mapping of project-relative path → hash. When
|
||||||
|
supplied, the install manifest is not consulted.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of project-relative paths that were removed.
|
List of project-relative paths that were removed.
|
||||||
"""
|
"""
|
||||||
manifest_file = _manifest_path(project_path, agent_id)
|
manifest_file = _manifest_path(project_path, agent_id)
|
||||||
if not manifest_file.is_file():
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
if files is not None:
|
||||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
entries = files
|
||||||
except (json.JSONDecodeError, OSError):
|
else:
|
||||||
return []
|
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", {})
|
entries = _all_tracked_entries(data)
|
||||||
if not entries:
|
if not entries:
|
||||||
manifest_file.unlink(missing_ok=True)
|
manifest_file.unlink(missing_ok=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if not force:
|
if not force:
|
||||||
modified = check_modified_files(project_path, agent_id)
|
modified = check_modified_files(project_path, agent_id)
|
||||||
if modified:
|
if modified:
|
||||||
raise AgentFileModifiedError(
|
raise AgentFileModifiedError(
|
||||||
f"The following agent files have been modified since installation:\n"
|
f"The following agent files have been modified since installation:\n"
|
||||||
+ "\n".join(f" {p}" for p in modified)
|
+ "\n".join(f" {p}" for p in modified)
|
||||||
+ "\nUse --force to remove them anyway."
|
+ "\nUse --force to remove them anyway."
|
||||||
)
|
)
|
||||||
|
|
||||||
removed: List[str] = []
|
removed: List[str] = []
|
||||||
for rel_path in entries:
|
for rel_path in entries:
|
||||||
@@ -374,7 +500,8 @@ def remove_tracked_files(
|
|||||||
removed.append(rel_path)
|
removed.append(rel_path)
|
||||||
|
|
||||||
# Clean up the install manifest itself
|
# 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
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Bootstrap module for Antigravity agent pack."""
|
"""Bootstrap module for Antigravity agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Agy(AgentBootstrap):
|
|||||||
AGENT_DIR = ".agent"
|
AGENT_DIR = ".agent"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Antigravity agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Antigravity agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Amp agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Amp(AgentBootstrap):
|
|||||||
AGENT_DIR = ".agents"
|
AGENT_DIR = ".agents"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Amp agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Amp agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Auggie CLI agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Auggie(AgentBootstrap):
|
|||||||
AGENT_DIR = ".augment"
|
AGENT_DIR = ".augment"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Auggie CLI agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Auggie CLI agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for IBM Bob agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Bob(AgentBootstrap):
|
|||||||
AGENT_DIR = ".bob"
|
AGENT_DIR = ".bob"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install IBM Bob agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove IBM Bob agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Claude Code agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Claude(AgentBootstrap):
|
|||||||
AGENT_DIR = ".claude"
|
AGENT_DIR = ".claude"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Claude Code agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Claude Code agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for CodeBuddy agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Codebuddy(AgentBootstrap):
|
|||||||
AGENT_DIR = ".codebuddy"
|
AGENT_DIR = ".codebuddy"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install CodeBuddy agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove CodeBuddy agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Codex CLI agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Codex(AgentBootstrap):
|
|||||||
AGENT_DIR = ".agents"
|
AGENT_DIR = ".agents"
|
||||||
COMMANDS_SUBDIR = "skills"
|
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."""
|
"""Install Codex CLI agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Codex CLI agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for GitHub Copilot agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Copilot(AgentBootstrap):
|
|||||||
AGENT_DIR = ".github"
|
AGENT_DIR = ".github"
|
||||||
COMMANDS_SUBDIR = "agents"
|
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."""
|
"""Install GitHub Copilot agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove GitHub Copilot agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Cursor agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class CursorAgent(AgentBootstrap):
|
|||||||
AGENT_DIR = ".cursor"
|
AGENT_DIR = ".cursor"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Cursor agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Cursor agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Gemini CLI agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Gemini(AgentBootstrap):
|
|||||||
AGENT_DIR = ".gemini"
|
AGENT_DIR = ".gemini"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Gemini CLI agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Gemini CLI agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for iFlow CLI agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Iflow(AgentBootstrap):
|
|||||||
AGENT_DIR = ".iflow"
|
AGENT_DIR = ".iflow"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install iFlow CLI agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove iFlow CLI agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Junie agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Junie(AgentBootstrap):
|
|||||||
AGENT_DIR = ".junie"
|
AGENT_DIR = ".junie"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Junie agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Junie agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Kilo Code agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Kilocode(AgentBootstrap):
|
|||||||
AGENT_DIR = ".kilocode"
|
AGENT_DIR = ".kilocode"
|
||||||
COMMANDS_SUBDIR = "workflows"
|
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."""
|
"""Install Kilo Code agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Kilo Code agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Kimi Code agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Kimi(AgentBootstrap):
|
|||||||
AGENT_DIR = ".kimi"
|
AGENT_DIR = ".kimi"
|
||||||
COMMANDS_SUBDIR = "skills"
|
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."""
|
"""Install Kimi Code agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Kimi Code agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Kiro CLI agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class KiroCli(AgentBootstrap):
|
|||||||
AGENT_DIR = ".kiro"
|
AGENT_DIR = ".kiro"
|
||||||
COMMANDS_SUBDIR = "prompts"
|
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."""
|
"""Install Kiro CLI agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Kiro CLI agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for opencode agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Opencode(AgentBootstrap):
|
|||||||
AGENT_DIR = ".opencode"
|
AGENT_DIR = ".opencode"
|
||||||
COMMANDS_SUBDIR = "command"
|
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."""
|
"""Install opencode agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove opencode agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Pi Coding Agent agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Pi(AgentBootstrap):
|
|||||||
AGENT_DIR = ".pi"
|
AGENT_DIR = ".pi"
|
||||||
COMMANDS_SUBDIR = "prompts"
|
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."""
|
"""Install Pi Coding Agent agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Pi Coding Agent agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Qoder CLI agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Qodercli(AgentBootstrap):
|
|||||||
AGENT_DIR = ".qoder"
|
AGENT_DIR = ".qoder"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Qoder CLI agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Qoder CLI agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Qwen Code agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Qwen(AgentBootstrap):
|
|||||||
AGENT_DIR = ".qwen"
|
AGENT_DIR = ".qwen"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Qwen Code agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Qwen Code agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Roo Code agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Roo(AgentBootstrap):
|
|||||||
AGENT_DIR = ".roo"
|
AGENT_DIR = ".roo"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Roo Code agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Roo Code agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for SHAI agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Shai(AgentBootstrap):
|
|||||||
AGENT_DIR = ".shai"
|
AGENT_DIR = ".shai"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install SHAI agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove SHAI agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Tabnine CLI agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Tabnine(AgentBootstrap):
|
|||||||
AGENT_DIR = ".tabnine/agent"
|
AGENT_DIR = ".tabnine/agent"
|
||||||
COMMANDS_SUBDIR = "commands"
|
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."""
|
"""Install Tabnine CLI agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Tabnine CLI agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Trae agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Trae(AgentBootstrap):
|
|||||||
AGENT_DIR = ".trae"
|
AGENT_DIR = ".trae"
|
||||||
COMMANDS_SUBDIR = "rules"
|
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."""
|
"""Install Trae agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Trae agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Mistral Vibe agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Vibe(AgentBootstrap):
|
|||||||
AGENT_DIR = ".vibe"
|
AGENT_DIR = ".vibe"
|
||||||
COMMANDS_SUBDIR = "prompts"
|
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."""
|
"""Install Mistral Vibe agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Mistral Vibe agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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."""
|
"""Bootstrap module for Windsurf agent pack."""
|
||||||
|
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
@@ -12,16 +12,19 @@ class Windsurf(AgentBootstrap):
|
|||||||
AGENT_DIR = ".windsurf"
|
AGENT_DIR = ".windsurf"
|
||||||
COMMANDS_SUBDIR = "workflows"
|
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."""
|
"""Install Windsurf agent files into the project."""
|
||||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
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.
|
"""Remove Windsurf agent files from the project.
|
||||||
|
|
||||||
Only removes individual tracked files — directories are never
|
Only removes individual tracked files — directories are never
|
||||||
deleted. Raises ``AgentFileModifiedError`` if any tracked file
|
deleted. When *files* is provided, exactly those files are
|
||||||
was modified and *force* is ``False``.
|
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,
|
_sha256,
|
||||||
check_modified_files,
|
check_modified_files,
|
||||||
export_pack,
|
export_pack,
|
||||||
|
get_tracked_files,
|
||||||
list_all_agents,
|
list_all_agents,
|
||||||
list_embedded_agents,
|
list_embedded_agents,
|
||||||
load_bootstrap,
|
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 = pack_dir / BOOTSTRAP_FILENAME
|
||||||
bootstrap_file.write_text(textwrap.dedent(f"""\
|
bootstrap_file.write_text(textwrap.dedent(f"""\
|
||||||
from pathlib import Path
|
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
|
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||||
|
|
||||||
class {class_name}(AgentBootstrap):
|
class {class_name}(AgentBootstrap):
|
||||||
AGENT_DIR = "{agent_dir}"
|
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)
|
(project_path / self.AGENT_DIR / "commands").mkdir(parents=True, exist_ok=True)
|
||||||
|
return []
|
||||||
|
|
||||||
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_tracked_files(project_path, self.manifest.id, force=force)
|
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||||
"""), encoding="utf-8")
|
"""), encoding="utf-8")
|
||||||
return bootstrap_file
|
return bootstrap_file
|
||||||
|
|
||||||
@@ -273,20 +275,31 @@ class TestBootstrapContract:
|
|||||||
project = tmp_path / "project"
|
project = tmp_path / "project"
|
||||||
project.mkdir()
|
project.mkdir()
|
||||||
|
|
||||||
b.setup(project, "sh", {})
|
agent_files = b.setup(project, "sh", {})
|
||||||
|
assert isinstance(agent_files, list)
|
||||||
assert (project / ".test-agent" / "commands").is_dir()
|
assert (project / ".test-agent" / "commands").is_dir()
|
||||||
|
|
||||||
# Simulate the init pipeline writing a file
|
# Simulate the init pipeline writing a file
|
||||||
cmd_file = project / ".test-agent" / "commands" / "hello.md"
|
cmd_file = project / ".test-agent" / "commands" / "hello.md"
|
||||||
cmd_file.write_text("hello", encoding="utf-8")
|
cmd_file.write_text("hello", encoding="utf-8")
|
||||||
|
|
||||||
# finalize_setup records files for tracking
|
# Simulate extension registration writing a file
|
||||||
b.finalize_setup(project)
|
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()
|
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)
|
b.teardown(project)
|
||||||
# The tracked file should be removed
|
# The tracked files should be removed
|
||||||
assert not cmd_file.exists()
|
assert not cmd_file.exists()
|
||||||
|
assert not ext_file.exists()
|
||||||
# Install manifest itself should be cleaned up
|
# Install manifest itself should be cleaned up
|
||||||
assert not _manifest_path(project, "test-agent").is_file()
|
assert not _manifest_path(project, "test-agent").is_file()
|
||||||
# Directories are preserved (only files are removed)
|
# Directories are preserved (only files are removed)
|
||||||
@@ -557,7 +570,7 @@ class TestFileTracking:
|
|||||||
f.parent.mkdir(parents=True)
|
f.parent.mkdir(parents=True)
|
||||||
f.write_text("hello world", encoding="utf-8")
|
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
|
# No modifications yet
|
||||||
assert check_modified_files(project, "myagent") == []
|
assert check_modified_files(project, "myagent") == []
|
||||||
@@ -571,7 +584,7 @@ class TestFileTracking:
|
|||||||
f.parent.mkdir(parents=True)
|
f.parent.mkdir(parents=True)
|
||||||
f.write_text("original", encoding="utf-8")
|
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
|
# Now modify the file
|
||||||
f.write_text("modified content", encoding="utf-8")
|
f.write_text("modified content", encoding="utf-8")
|
||||||
@@ -595,7 +608,7 @@ class TestFileTracking:
|
|||||||
f1.write_text("aaa", encoding="utf-8")
|
f1.write_text("aaa", encoding="utf-8")
|
||||||
f2.write_text("bbb", 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")
|
removed = remove_tracked_files(project, "ag")
|
||||||
assert len(removed) == 2
|
assert len(removed) == 2
|
||||||
@@ -615,7 +628,7 @@ class TestFileTracking:
|
|||||||
f.parent.mkdir(parents=True)
|
f.parent.mkdir(parents=True)
|
||||||
f.write_text("original", encoding="utf-8")
|
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")
|
f.write_text("user-edited", encoding="utf-8")
|
||||||
|
|
||||||
with pytest.raises(AgentFileModifiedError, match="modified"):
|
with pytest.raises(AgentFileModifiedError, match="modified"):
|
||||||
@@ -633,7 +646,7 @@ class TestFileTracking:
|
|||||||
f.parent.mkdir(parents=True)
|
f.parent.mkdir(parents=True)
|
||||||
f.write_text("original", encoding="utf-8")
|
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")
|
f.write_text("user-edited", encoding="utf-8")
|
||||||
|
|
||||||
removed = remove_tracked_files(project, "ag", force=True)
|
removed = remove_tracked_files(project, "ag", force=True)
|
||||||
@@ -655,7 +668,7 @@ class TestFileTracking:
|
|||||||
f = d / "deep.md"
|
f = d / "deep.md"
|
||||||
f.write_text("deep", encoding="utf-8")
|
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")
|
remove_tracked_files(project, "myagent")
|
||||||
|
|
||||||
assert not f.exists()
|
assert not f.exists()
|
||||||
@@ -672,7 +685,7 @@ class TestFileTracking:
|
|||||||
f.parent.mkdir(parents=True)
|
f.parent.mkdir(parents=True)
|
||||||
f.write_text("data", encoding="utf-8")
|
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
|
# User deletes the file before teardown
|
||||||
f.unlink()
|
f.unlink()
|
||||||
@@ -700,10 +713,98 @@ class TestFileTracking:
|
|||||||
f.parent.mkdir(parents=True)
|
f.parent.mkdir(parents=True)
|
||||||
f.write_text("content", encoding="utf-8")
|
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"))
|
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
assert data["agent_id"] == "ag"
|
assert data["agent_id"] == "ag"
|
||||||
assert isinstance(data["files"], dict)
|
assert isinstance(data["agent_files"], dict)
|
||||||
assert ".ag/x.md" in data["files"]
|
assert ".ag/x.md" in data["agent_files"]
|
||||||
assert len(data["files"][".ag/x.md"]) == 64
|
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