mirror of
https://github.com/github/spec-kit.git
synced 2026-03-21 12:53:08 +00:00
Compare commits
11 Commits
main
...
copilot/ag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
978addc390 | ||
|
|
9b580a536b | ||
|
|
d6016ab9db | ||
|
|
c2227a7ffd | ||
|
|
c3efd1fb71 | ||
|
|
e190116d13 | ||
|
|
a63c248c80 | ||
|
|
b5a5e3fc35 | ||
|
|
ec5471af61 | ||
|
|
3212309e7c | ||
|
|
8b20d0b336 |
@@ -43,6 +43,8 @@ packages = ["src/specify_cli"]
|
||||
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
||||
".github/workflows/scripts/create-release-packages.sh" = "specify_cli/core_pack/release_scripts/create-release-packages.sh"
|
||||
".github/workflows/scripts/create-release-packages.ps1" = "specify_cli/core_pack/release_scripts/create-release-packages.ps1"
|
||||
# Official agent packs (embedded in wheel for zero-config offline operation)
|
||||
"src/specify_cli/core_pack/agents" = "specify_cli/core_pack/agents"
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
|
||||
@@ -36,7 +36,7 @@ import json5
|
||||
import stat
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Tuple
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import typer
|
||||
import httpx
|
||||
@@ -1715,6 +1715,7 @@ def _handle_agent_skills_migration(console: Console, agent_key: str) -> None:
|
||||
def init(
|
||||
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
|
||||
ai_assistant: str = typer.Option(None, "--ai", help=AI_ASSISTANT_HELP),
|
||||
agent: str = typer.Option(None, "--agent", help="AI agent to use (enables file tracking for clean teardown when switching agents). Accepts the same agent IDs as --ai."),
|
||||
ai_commands_dir: str = typer.Option(None, "--ai-commands-dir", help="Directory for agent command files (required with --ai generic, e.g. .myagent/commands/)"),
|
||||
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
|
||||
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
|
||||
@@ -1753,6 +1754,7 @@ def init(
|
||||
Examples:
|
||||
specify init my-project
|
||||
specify init my-project --ai claude
|
||||
specify init my-project --agent claude # Pack-based flow (with file tracking)
|
||||
specify init my-project --ai copilot --no-git
|
||||
specify init --ignore-agent-tools my-project
|
||||
specify init . --ai claude # Initialize in current directory
|
||||
@@ -1765,6 +1767,7 @@ def init(
|
||||
specify init --here --force # Skip confirmation when current directory not empty
|
||||
specify init my-project --ai claude --ai-skills # Install agent skills
|
||||
specify init --here --ai gemini --ai-skills
|
||||
specify init my-project --agent claude --ai-skills # Pack-based flow with skills
|
||||
specify init my-project --ai generic --ai-commands-dir .myagent/commands/ # Unsupported agent
|
||||
specify init my-project --offline # Use bundled assets (no network access)
|
||||
specify init my-project --ai claude --preset healthcare-compliance # With preset
|
||||
@@ -1772,6 +1775,17 @@ def init(
|
||||
|
||||
show_banner()
|
||||
|
||||
# --agent and --ai are interchangeable for agent selection, but --agent
|
||||
# additionally opts into the pack-based flow (file tracking via
|
||||
# finalize_setup for tracked teardown/switch).
|
||||
use_agent_pack = False
|
||||
if agent:
|
||||
if ai_assistant:
|
||||
console.print("[red]Error:[/red] --agent and --ai cannot both be specified. Use one or the other.")
|
||||
raise typer.Exit(1)
|
||||
ai_assistant = agent
|
||||
use_agent_pack = True
|
||||
|
||||
# Detect when option values are likely misinterpreted flags (parameter ordering issue)
|
||||
if ai_assistant and ai_assistant.startswith("--"):
|
||||
console.print(f"[red]Error:[/red] Invalid value for --ai: '{ai_assistant}'")
|
||||
@@ -1802,7 +1816,7 @@ def init(
|
||||
raise typer.Exit(1)
|
||||
|
||||
if ai_skills and not ai_assistant:
|
||||
console.print("[red]Error:[/red] --ai-skills requires --ai to be specified")
|
||||
console.print("[red]Error:[/red] --ai-skills requires --ai or --agent to be specified")
|
||||
console.print("[yellow]Usage:[/yellow] specify init <project> --ai <agent> --ai-skills")
|
||||
raise typer.Exit(1)
|
||||
|
||||
@@ -1854,6 +1868,19 @@ def init(
|
||||
"copilot"
|
||||
)
|
||||
|
||||
# When --agent is used, validate that the agent resolves through the pack
|
||||
# system and prepare the bootstrap for post-init file tracking.
|
||||
agent_bootstrap = None
|
||||
if use_agent_pack:
|
||||
from .agent_pack import resolve_agent_pack, load_bootstrap, PackResolutionError, AgentPackError
|
||||
try:
|
||||
resolved = resolve_agent_pack(selected_ai)
|
||||
agent_bootstrap = load_bootstrap(resolved.path, resolved.manifest)
|
||||
console.print(f"[dim]Pack-based flow: {resolved.manifest.name} ({resolved.source})[/dim]")
|
||||
except (PackResolutionError, AgentPackError) as exc:
|
||||
console.print(f"[red]Error resolving agent pack:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Agents that have moved from explicit commands/prompts to agent skills.
|
||||
if selected_ai in AGENT_SKILLS_MIGRATIONS and not ai_skills:
|
||||
# If selected interactively (no --ai provided), automatically enable
|
||||
@@ -1957,7 +1984,10 @@ def init(
|
||||
"This will become the default in v0.6.0."
|
||||
)
|
||||
|
||||
if use_github:
|
||||
if use_agent_pack:
|
||||
# Pack-based flow: setup() owns scaffolding, always uses bundled assets.
|
||||
tracker.add("scaffold", "Apply bundled assets")
|
||||
elif use_github:
|
||||
for key, label in [
|
||||
("fetch", "Fetch latest release"),
|
||||
("download", "Download template"),
|
||||
@@ -1992,7 +2022,26 @@ def init(
|
||||
verify = not skip_tls
|
||||
local_ssl_context = ssl_context if verify else False
|
||||
|
||||
if use_github:
|
||||
# -- scaffolding ------------------------------------------------
|
||||
# Pack-based flow (--agent): setup() owns scaffolding and
|
||||
# returns every file it created. Legacy flow (--ai): scaffold
|
||||
# directly or download from GitHub.
|
||||
agent_setup_files: list[Path] = []
|
||||
|
||||
if use_agent_pack and agent_bootstrap is not None:
|
||||
tracker.start("scaffold")
|
||||
try:
|
||||
agent_setup_files = agent_bootstrap.setup(
|
||||
project_path, selected_script, {"here": here})
|
||||
tracker.complete(
|
||||
"scaffold",
|
||||
f"{selected_ai} ({len(agent_setup_files)} files)")
|
||||
except Exception as exc:
|
||||
tracker.error("scaffold", str(exc))
|
||||
if not here and project_path.exists():
|
||||
shutil.rmtree(project_path)
|
||||
raise typer.Exit(1)
|
||||
elif use_github:
|
||||
with httpx.Client(verify=local_ssl_context) as local_client:
|
||||
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
|
||||
else:
|
||||
@@ -2090,6 +2139,7 @@ def init(
|
||||
"ai": selected_ai,
|
||||
"ai_skills": ai_skills,
|
||||
"ai_commands_dir": ai_commands_dir,
|
||||
"agent_pack": use_agent_pack,
|
||||
"branch_numbering": branch_numbering or "sequential",
|
||||
"here": here,
|
||||
"preset": preset,
|
||||
@@ -2133,6 +2183,16 @@ def init(
|
||||
if not use_github:
|
||||
tracker.skip("cleanup", "not needed (no download)")
|
||||
|
||||
# When --agent is used, record all installed agent files for
|
||||
# tracked teardown. setup() already returned the files it
|
||||
# created; pass them to finalize_setup so the manifest is
|
||||
# accurate. finalize_setup also scans the agent directory
|
||||
# to catch any additional files created by later pipeline
|
||||
# steps (skills, extensions, presets).
|
||||
if use_agent_pack and agent_bootstrap is not None:
|
||||
agent_bootstrap.finalize_setup(
|
||||
project_path, agent_files=agent_setup_files)
|
||||
|
||||
tracker.complete("final", "project ready")
|
||||
except (typer.Exit, SystemExit):
|
||||
raise
|
||||
@@ -2366,6 +2426,513 @@ def version():
|
||||
console.print()
|
||||
|
||||
|
||||
# ===== Agent Commands =====
|
||||
|
||||
agent_app = typer.Typer(
|
||||
name="agent",
|
||||
help="Manage agent packs for AI assistants",
|
||||
add_completion=False,
|
||||
)
|
||||
app.add_typer(agent_app, name="agent")
|
||||
|
||||
|
||||
@agent_app.command("list")
|
||||
def agent_list(
|
||||
installed: bool = typer.Option(False, "--installed", help="Only show agents with local presence in the current project"),
|
||||
):
|
||||
"""List available agent packs."""
|
||||
from .agent_pack import list_all_agents, list_embedded_agents
|
||||
|
||||
show_banner()
|
||||
|
||||
project_path = Path.cwd()
|
||||
agents = list_all_agents(project_path=project_path if installed else None)
|
||||
if not agents and not installed:
|
||||
agents_from_embedded = list_embedded_agents()
|
||||
if not agents_from_embedded:
|
||||
console.print("[yellow]No agent packs found.[/yellow]")
|
||||
console.print("[dim]Agent packs are embedded in the specify-cli wheel.[/dim]")
|
||||
raise typer.Exit(0)
|
||||
|
||||
table = Table(title="Available Agent Packs", show_lines=False)
|
||||
table.add_column("ID", style="cyan", no_wrap=True)
|
||||
table.add_column("Name", style="white")
|
||||
table.add_column("Version", style="dim")
|
||||
table.add_column("Source", style="green")
|
||||
table.add_column("CLI Required", style="yellow", justify="center")
|
||||
|
||||
for resolved in agents:
|
||||
m = resolved.manifest
|
||||
cli_marker = "✓" if m.requires_cli else "—"
|
||||
source_display = resolved.source
|
||||
if resolved.overrides:
|
||||
source_display += f" (overrides {resolved.overrides})"
|
||||
table.add_row(m.id, m.name, m.version, source_display, cli_marker)
|
||||
|
||||
console.print(table)
|
||||
console.print(f"\n[dim]{len(agents)} agent(s) available[/dim]")
|
||||
|
||||
|
||||
@agent_app.command("info")
|
||||
def agent_info(
|
||||
agent_id: str = typer.Argument(..., help="Agent pack ID (e.g. 'claude', 'gemini')"),
|
||||
):
|
||||
"""Show detailed information about an agent pack."""
|
||||
from .agent_pack import resolve_agent_pack, PackResolutionError
|
||||
|
||||
show_banner()
|
||||
|
||||
try:
|
||||
resolved = resolve_agent_pack(agent_id, project_path=Path.cwd())
|
||||
except PackResolutionError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
m = resolved.manifest
|
||||
|
||||
info_table = Table(show_header=False, box=None, padding=(0, 2))
|
||||
info_table.add_column("Key", style="cyan", justify="right")
|
||||
info_table.add_column("Value", style="white")
|
||||
|
||||
info_table.add_row("Agent", f"{m.name} ({m.id})")
|
||||
info_table.add_row("Version", m.version)
|
||||
info_table.add_row("Description", m.description or "—")
|
||||
info_table.add_row("Author", m.author or "—")
|
||||
info_table.add_row("License", m.license or "—")
|
||||
info_table.add_row("", "")
|
||||
|
||||
source_display = resolved.source
|
||||
if resolved.source == "catalog":
|
||||
source_display = f"catalog — {resolved.path}"
|
||||
elif resolved.source == "embedded":
|
||||
source_display = f"embedded (bundled in specify-cli wheel)"
|
||||
|
||||
info_table.add_row("Source", source_display)
|
||||
if resolved.overrides:
|
||||
info_table.add_row("Overrides", resolved.overrides)
|
||||
info_table.add_row("Pack Path", str(resolved.path))
|
||||
info_table.add_row("", "")
|
||||
|
||||
info_table.add_row("Requires CLI", "Yes" if m.requires_cli else "No")
|
||||
if m.install_url:
|
||||
info_table.add_row("Install URL", m.install_url)
|
||||
if m.cli_tool:
|
||||
info_table.add_row("CLI Tool", m.cli_tool)
|
||||
info_table.add_row("", "")
|
||||
|
||||
info_table.add_row("Commands Dir", m.commands_dir or "—")
|
||||
info_table.add_row("Format", m.command_format)
|
||||
info_table.add_row("Arg Placeholder", m.arg_placeholder)
|
||||
info_table.add_row("File Extension", m.file_extension)
|
||||
info_table.add_row("", "")
|
||||
|
||||
info_table.add_row("Tags", ", ".join(m.tags) if m.tags else "—")
|
||||
info_table.add_row("Speckit Version", m.speckit_version)
|
||||
|
||||
panel = Panel(
|
||||
info_table,
|
||||
title=f"[bold cyan]Agent: {m.name}[/bold cyan]",
|
||||
border_style="cyan",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
|
||||
@agent_app.command("validate")
|
||||
def agent_validate(
|
||||
pack_path: str = typer.Argument(..., help="Path to the agent pack directory to validate"),
|
||||
):
|
||||
"""Validate an agent pack's structure and manifest."""
|
||||
from .agent_pack import validate_pack, ManifestValidationError, AgentManifest, MANIFEST_FILENAME
|
||||
|
||||
show_banner()
|
||||
|
||||
path = Path(pack_path).resolve()
|
||||
if not path.is_dir():
|
||||
console.print(f"[red]Error:[/red] Not a directory: {path}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
warnings = validate_pack(path)
|
||||
except ManifestValidationError as exc:
|
||||
console.print(f"[red]Validation failed:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
manifest = AgentManifest.from_yaml(path / MANIFEST_FILENAME)
|
||||
console.print(f"[green]✓[/green] Pack '{manifest.id}' ({manifest.name}) is valid")
|
||||
|
||||
if warnings:
|
||||
console.print(f"\n[yellow]Warnings ({len(warnings)}):[/yellow]")
|
||||
for w in warnings:
|
||||
console.print(f" [yellow]⚠[/yellow] {w}")
|
||||
else:
|
||||
console.print("[green]No warnings.[/green]")
|
||||
|
||||
|
||||
@agent_app.command("export")
|
||||
def agent_export(
|
||||
agent_id: str = typer.Argument(..., help="Agent pack ID to export"),
|
||||
to: str = typer.Option(..., "--to", help="Destination directory for the exported pack"),
|
||||
):
|
||||
"""Export the active agent pack to a directory for editing."""
|
||||
from .agent_pack import export_pack, PackResolutionError
|
||||
|
||||
show_banner()
|
||||
|
||||
dest = Path(to).resolve()
|
||||
try:
|
||||
result = export_pack(agent_id, dest, project_path=Path.cwd())
|
||||
except PackResolutionError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
console.print(f"[green]✓[/green] Exported '{agent_id}' pack to {result}")
|
||||
console.print(f"[dim]Edit files in {result} and use as a project-level override.[/dim]")
|
||||
|
||||
|
||||
@agent_app.command("switch")
|
||||
def agent_switch(
|
||||
agent_id: str = typer.Argument(..., help="Agent pack ID to switch to"),
|
||||
force: bool = typer.Option(False, "--force", help="Remove agent files even if they were modified since installation"),
|
||||
):
|
||||
"""Switch the active AI agent in the current project.
|
||||
|
||||
Tears down the current agent and sets up the new one.
|
||||
Preserves specs, plans, tasks, constitution, memory, templates, and scripts.
|
||||
"""
|
||||
from .agent_pack import (
|
||||
resolve_agent_pack,
|
||||
load_bootstrap,
|
||||
check_modified_files,
|
||||
get_tracked_files,
|
||||
PackResolutionError,
|
||||
AgentPackError,
|
||||
)
|
||||
|
||||
show_banner()
|
||||
|
||||
project_path = Path.cwd()
|
||||
init_options_file = project_path / ".specify" / "init-options.json"
|
||||
|
||||
if not init_options_file.exists():
|
||||
console.print("[red]Error:[/red] Not a Specify project (missing .specify/init-options.json)")
|
||||
console.print("[yellow]Hint:[/yellow] Run 'specify init --here' first.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Load current project options
|
||||
try:
|
||||
options = json.loads(init_options_file.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
console.print(f"[red]Error reading init options:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
current_agent = options.get("ai")
|
||||
script_type = options.get("script", "sh")
|
||||
|
||||
# Resolve the new agent pack
|
||||
try:
|
||||
resolved = resolve_agent_pack(agent_id, project_path=project_path)
|
||||
except PackResolutionError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
console.print(f"[bold]Switching agent: {current_agent or '(none)'} → {agent_id}[/bold]")
|
||||
|
||||
# Teardown current agent (best effort — may have been set up with old system)
|
||||
if current_agent:
|
||||
try:
|
||||
current_resolved = resolve_agent_pack(current_agent, project_path=project_path)
|
||||
current_bootstrap = load_bootstrap(current_resolved.path, current_resolved.manifest)
|
||||
|
||||
# Check for modified files BEFORE teardown and prompt for confirmation
|
||||
modified = check_modified_files(project_path, current_agent)
|
||||
if modified and not force:
|
||||
console.print("[yellow]The following files have been modified since installation:[/yellow]")
|
||||
for f in modified:
|
||||
console.print(f" {f}")
|
||||
if not typer.confirm("Remove these modified files?"):
|
||||
console.print("[dim]Aborted. Use --force to skip this check.[/dim]")
|
||||
raise typer.Exit(0)
|
||||
|
||||
# Retrieve tracked file lists and feed them into teardown
|
||||
agent_files, extension_files = get_tracked_files(project_path, current_agent)
|
||||
all_files = {**agent_files, **extension_files}
|
||||
|
||||
console.print(f" [dim]Tearing down {current_agent}...[/dim]")
|
||||
current_bootstrap.teardown(
|
||||
project_path,
|
||||
force=True, # already confirmed above
|
||||
files=all_files if all_files else None,
|
||||
)
|
||||
console.print(f" [green]✓[/green] {current_agent} removed")
|
||||
except AgentPackError:
|
||||
# If pack-based teardown fails, try legacy cleanup via AGENT_CONFIG
|
||||
agent_config = AGENT_CONFIG.get(current_agent, {})
|
||||
agent_folder = agent_config.get("folder")
|
||||
if agent_folder:
|
||||
agent_dir = project_path / agent_folder.rstrip("/")
|
||||
if agent_dir.is_dir():
|
||||
shutil.rmtree(agent_dir)
|
||||
console.print(f" [green]✓[/green] {current_agent} directory removed (legacy)")
|
||||
|
||||
# Setup new agent
|
||||
try:
|
||||
new_bootstrap = load_bootstrap(resolved.path, resolved.manifest)
|
||||
console.print(f" [dim]Setting up {agent_id}...[/dim]")
|
||||
agent_files = new_bootstrap.setup(project_path, script_type, options)
|
||||
console.print(f" [green]✓[/green] {agent_id} installed")
|
||||
except AgentPackError as exc:
|
||||
console.print(f"[red]Error setting up {agent_id}:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Update init options
|
||||
options["ai"] = agent_id
|
||||
init_options_file.write_text(json.dumps(options, indent=2), encoding="utf-8")
|
||||
|
||||
# Re-register extension commands for the new agent
|
||||
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) -> List[Path]:
|
||||
"""Re-register all installed extension commands for a new agent after switching.
|
||||
|
||||
Returns:
|
||||
List of absolute file paths created by extension registration.
|
||||
"""
|
||||
created_files: List[Path] = []
|
||||
registry_file = project_path / ".specify" / "extensions" / ".registry"
|
||||
if not registry_file.is_file():
|
||||
return created_files
|
||||
|
||||
try:
|
||||
registry_data = json.loads(registry_file.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return created_files
|
||||
|
||||
extensions = registry_data.get("extensions", {})
|
||||
if not extensions:
|
||||
return created_files
|
||||
|
||||
try:
|
||||
from .agents import CommandRegistrar
|
||||
registrar = CommandRegistrar()
|
||||
except ImportError:
|
||||
return created_files
|
||||
|
||||
# Snapshot the commands directory before registration so we can
|
||||
# detect which files were created by extension commands.
|
||||
agent_config = registrar.AGENT_CONFIGS.get(agent_id)
|
||||
if agent_config:
|
||||
commands_dir = project_path / agent_config["dir"]
|
||||
pre_existing = set(commands_dir.rglob("*")) if commands_dir.is_dir() else set()
|
||||
else:
|
||||
pre_existing = set()
|
||||
|
||||
reregistered = 0
|
||||
for ext_id, ext_data in extensions.items():
|
||||
commands = ext_data.get("registered_commands", {})
|
||||
if not commands:
|
||||
continue
|
||||
|
||||
ext_dir = project_path / ".specify" / "extensions" / ext_id
|
||||
if not ext_dir.is_dir():
|
||||
continue
|
||||
|
||||
# Get the command list from the manifest
|
||||
manifest_file = ext_dir / "extension.yml"
|
||||
if not manifest_file.is_file():
|
||||
continue
|
||||
|
||||
try:
|
||||
from .extensions import ExtensionManifest
|
||||
manifest = ExtensionManifest(manifest_file)
|
||||
if manifest.commands:
|
||||
registered = registrar.register_commands(
|
||||
agent_id, manifest.commands, ext_id, ext_dir / "commands", project_path
|
||||
)
|
||||
if registered:
|
||||
reregistered += len(registered)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Collect files created by extension registration
|
||||
if agent_config:
|
||||
commands_dir = project_path / agent_config["dir"]
|
||||
if commands_dir.is_dir():
|
||||
for p in commands_dir.rglob("*"):
|
||||
if p.is_file() and p not in pre_existing:
|
||||
created_files.append(p)
|
||||
|
||||
if reregistered:
|
||||
console.print(
|
||||
f" [green]✓[/green] Re-registered {reregistered} extension command(s) ({len(created_files)} file(s))"
|
||||
)
|
||||
|
||||
return created_files
|
||||
|
||||
|
||||
@agent_app.command("search")
|
||||
def agent_search(
|
||||
query: str = typer.Argument(None, help="Search query (matches agent ID, name, or tags)"),
|
||||
tag: str = typer.Option(None, "--tag", help="Filter by tag"),
|
||||
):
|
||||
"""Search for agent packs across embedded and catalog sources."""
|
||||
from .agent_pack import list_all_agents
|
||||
|
||||
show_banner()
|
||||
|
||||
all_agents = list_all_agents(project_path=Path.cwd())
|
||||
|
||||
if query:
|
||||
query_lower = query.lower()
|
||||
all_agents = [
|
||||
a for a in all_agents
|
||||
if query_lower in a.manifest.id.lower()
|
||||
or query_lower in a.manifest.name.lower()
|
||||
or query_lower in a.manifest.description.lower()
|
||||
or any(query_lower in t.lower() for t in a.manifest.tags)
|
||||
]
|
||||
|
||||
if tag:
|
||||
tag_lower = tag.lower()
|
||||
all_agents = [
|
||||
a for a in all_agents
|
||||
if any(tag_lower == t.lower() for t in a.manifest.tags)
|
||||
]
|
||||
|
||||
if not all_agents:
|
||||
console.print("[yellow]No agents found matching your search.[/yellow]")
|
||||
raise typer.Exit(0)
|
||||
|
||||
table = Table(title="Search Results", show_lines=False)
|
||||
table.add_column("ID", style="cyan", no_wrap=True)
|
||||
table.add_column("Name", style="white")
|
||||
table.add_column("Description", style="dim")
|
||||
table.add_column("Tags", style="green")
|
||||
table.add_column("Source", style="yellow")
|
||||
|
||||
for resolved in all_agents:
|
||||
m = resolved.manifest
|
||||
table.add_row(
|
||||
m.id, m.name,
|
||||
(m.description[:50] + "...") if len(m.description) > 53 else m.description,
|
||||
", ".join(m.tags),
|
||||
resolved.source,
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print(f"\n[dim]{len(all_agents)} result(s)[/dim]")
|
||||
|
||||
|
||||
@agent_app.command("add")
|
||||
def agent_add(
|
||||
agent_id: str = typer.Argument(..., help="Agent pack ID to install"),
|
||||
from_path: str = typer.Option(None, "--from", help="Install from a local path instead of a catalog"),
|
||||
):
|
||||
"""Install an agent pack from a catalog or local path."""
|
||||
from .agent_pack import (
|
||||
_catalog_agents_dir,
|
||||
AgentManifest,
|
||||
ManifestValidationError,
|
||||
MANIFEST_FILENAME,
|
||||
)
|
||||
|
||||
show_banner()
|
||||
|
||||
if from_path:
|
||||
source = Path(from_path).resolve()
|
||||
if not source.is_dir():
|
||||
console.print(f"[red]Error:[/red] Not a directory: {source}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
manifest_file = source / MANIFEST_FILENAME
|
||||
if not manifest_file.is_file():
|
||||
console.print(f"[red]Error:[/red] No {MANIFEST_FILENAME} found in {source}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
manifest = AgentManifest.from_yaml(manifest_file)
|
||||
except ManifestValidationError as exc:
|
||||
console.print(f"[red]Validation failed:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
dest = _catalog_agents_dir() / manifest.id
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(source, dest, dirs_exist_ok=True)
|
||||
console.print(f"[green]✓[/green] Installed '{manifest.id}' ({manifest.name}) from {source}")
|
||||
else:
|
||||
# Catalog fetch — placeholder for future catalog integration
|
||||
console.print(f"[yellow]Catalog fetch not yet implemented.[/yellow]")
|
||||
console.print(f"[dim]Use --from <path> to install from a local directory.[/dim]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@agent_app.command("remove")
|
||||
def agent_remove(
|
||||
agent_id: str = typer.Argument(..., help="Agent pack ID to remove"),
|
||||
):
|
||||
"""Remove a cached/override agent pack.
|
||||
|
||||
If the agent is an official embedded agent, removing the override
|
||||
falls back to the embedded version.
|
||||
"""
|
||||
from .agent_pack import (
|
||||
_catalog_agents_dir,
|
||||
_user_agents_dir,
|
||||
_embedded_agents_dir,
|
||||
MANIFEST_FILENAME,
|
||||
)
|
||||
|
||||
show_banner()
|
||||
|
||||
removed = False
|
||||
|
||||
# Check user-level
|
||||
user_pack = _user_agents_dir() / agent_id
|
||||
if user_pack.is_dir():
|
||||
shutil.rmtree(user_pack)
|
||||
console.print(f"[green]✓[/green] Removed user-level override for '{agent_id}'")
|
||||
removed = True
|
||||
|
||||
# Check project-level
|
||||
project_pack = Path.cwd() / ".specify" / "agents" / agent_id
|
||||
if project_pack.is_dir():
|
||||
shutil.rmtree(project_pack)
|
||||
console.print(f"[green]✓[/green] Removed project-level override for '{agent_id}'")
|
||||
removed = True
|
||||
|
||||
# Check catalog cache
|
||||
catalog_pack = _catalog_agents_dir() / agent_id
|
||||
if catalog_pack.is_dir():
|
||||
shutil.rmtree(catalog_pack)
|
||||
console.print(f"[green]✓[/green] Removed catalog-cached version of '{agent_id}'")
|
||||
removed = True
|
||||
|
||||
if not removed:
|
||||
# Check if it's an embedded agent
|
||||
embedded_pack = _embedded_agents_dir() / agent_id / MANIFEST_FILENAME
|
||||
if embedded_pack.is_file():
|
||||
console.print(f"[yellow]'{agent_id}' is an embedded official agent and cannot be removed.[/yellow]")
|
||||
console.print("[dim]It has no overrides to remove.[/dim]")
|
||||
else:
|
||||
console.print(f"[red]Error:[/red] Agent '{agent_id}' not found.")
|
||||
raise typer.Exit(1)
|
||||
else:
|
||||
# Check for embedded fallback
|
||||
embedded_pack = _embedded_agents_dir() / agent_id / MANIFEST_FILENAME
|
||||
if embedded_pack.is_file():
|
||||
console.print(f"[dim]Embedded version of '{agent_id}' is now active.[/dim]")
|
||||
|
||||
|
||||
# ===== Extension Commands =====
|
||||
|
||||
extension_app = typer.Typer(
|
||||
|
||||
849
src/specify_cli/agent_pack.py
Normal file
849
src/specify_cli/agent_pack.py
Normal file
@@ -0,0 +1,849 @@
|
||||
"""
|
||||
Agent Pack Manager for Spec Kit
|
||||
|
||||
Implements self-bootstrapping agent packs with declarative manifests
|
||||
(speckit-agent.yml) and Python bootstrap modules (bootstrap.py).
|
||||
|
||||
Agent packs resolve by priority:
|
||||
1. User-level (~/.specify/agents/<id>/)
|
||||
2. Project-level (.specify/agents/<id>/)
|
||||
3. Catalog-installed (downloaded via `specify agent add`)
|
||||
4. Embedded in wheel (official packs under core_pack/agents/)
|
||||
|
||||
The embedded packs ship inside the pip wheel so that
|
||||
`pip install specify-cli && specify init --ai claude` works offline.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
from platformdirs import user_data_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Manifest schema
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MANIFEST_FILENAME = "speckit-agent.yml"
|
||||
BOOTSTRAP_FILENAME = "bootstrap.py"
|
||||
|
||||
MANIFEST_SCHEMA_VERSION = "1.0"
|
||||
|
||||
# Required top-level keys
|
||||
_REQUIRED_TOP_KEYS = {"schema_version", "agent"}
|
||||
|
||||
# Required keys within the ``agent`` block
|
||||
_REQUIRED_AGENT_KEYS = {"id", "name", "version"}
|
||||
|
||||
|
||||
class AgentPackError(Exception):
|
||||
"""Base exception for agent-pack operations."""
|
||||
|
||||
|
||||
class ManifestValidationError(AgentPackError):
|
||||
"""Raised when a speckit-agent.yml file is invalid."""
|
||||
|
||||
|
||||
class PackResolutionError(AgentPackError):
|
||||
"""Raised when no pack can be found for the requested agent id."""
|
||||
|
||||
|
||||
class AgentFileModifiedError(AgentPackError):
|
||||
"""Raised when teardown finds user-modified files and ``--force`` is not set."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Manifest
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class AgentManifest:
|
||||
"""Parsed and validated representation of a speckit-agent.yml file."""
|
||||
|
||||
# identity
|
||||
id: str
|
||||
name: str
|
||||
version: str
|
||||
description: str = ""
|
||||
author: str = ""
|
||||
license: str = ""
|
||||
|
||||
# runtime
|
||||
requires_cli: bool = False
|
||||
install_url: Optional[str] = None
|
||||
cli_tool: Optional[str] = None
|
||||
|
||||
# compatibility
|
||||
speckit_version: str = ">=0.1.0"
|
||||
|
||||
# discovery
|
||||
tags: List[str] = field(default_factory=list)
|
||||
|
||||
# command registration metadata (used by CommandRegistrar / extensions)
|
||||
commands_dir: str = ""
|
||||
command_format: str = "markdown"
|
||||
arg_placeholder: str = "$ARGUMENTS"
|
||||
file_extension: str = ".md"
|
||||
|
||||
# raw data for anything else
|
||||
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
||||
|
||||
# filesystem path to the pack directory that produced this manifest
|
||||
pack_path: Optional[Path] = field(default=None, repr=False)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: Path) -> "AgentManifest":
|
||||
"""Load and validate a manifest from *path*.
|
||||
|
||||
Raises ``ManifestValidationError`` on structural problems.
|
||||
"""
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
data = yaml.safe_load(text) or {}
|
||||
except yaml.YAMLError as exc:
|
||||
raise ManifestValidationError(f"Invalid YAML in {path}: {exc}")
|
||||
except FileNotFoundError:
|
||||
raise ManifestValidationError(f"Manifest not found: {path}")
|
||||
|
||||
return cls.from_dict(data, pack_path=path.parent)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict, *, pack_path: Optional[Path] = None) -> "AgentManifest":
|
||||
"""Build a manifest from a raw dictionary."""
|
||||
if not isinstance(data, dict):
|
||||
raise ManifestValidationError("Manifest must be a YAML mapping")
|
||||
|
||||
missing_top = _REQUIRED_TOP_KEYS - set(data)
|
||||
if missing_top:
|
||||
raise ManifestValidationError(
|
||||
f"Missing required top-level key(s): {', '.join(sorted(missing_top))}"
|
||||
)
|
||||
|
||||
if data.get("schema_version") != MANIFEST_SCHEMA_VERSION:
|
||||
raise ManifestValidationError(
|
||||
f"Unsupported schema_version: {data.get('schema_version')!r} "
|
||||
f"(expected {MANIFEST_SCHEMA_VERSION!r})"
|
||||
)
|
||||
|
||||
agent_block = data.get("agent")
|
||||
if not isinstance(agent_block, dict):
|
||||
raise ManifestValidationError("'agent' must be a mapping")
|
||||
|
||||
missing_agent = _REQUIRED_AGENT_KEYS - set(agent_block)
|
||||
if missing_agent:
|
||||
raise ManifestValidationError(
|
||||
f"Missing required agent key(s): {', '.join(sorted(missing_agent))}"
|
||||
)
|
||||
|
||||
runtime = data.get("runtime") or {}
|
||||
requires = data.get("requires") or {}
|
||||
tags = data.get("tags") or []
|
||||
cmd_reg = data.get("command_registration") or {}
|
||||
|
||||
return cls(
|
||||
id=str(agent_block["id"]),
|
||||
name=str(agent_block["name"]),
|
||||
version=str(agent_block["version"]),
|
||||
description=str(agent_block.get("description", "")),
|
||||
author=str(agent_block.get("author", "")),
|
||||
license=str(agent_block.get("license", "")),
|
||||
requires_cli=bool(runtime.get("requires_cli", False)),
|
||||
install_url=runtime.get("install_url"),
|
||||
cli_tool=runtime.get("cli_tool"),
|
||||
speckit_version=str(requires.get("speckit_version", ">=0.1.0")),
|
||||
tags=[str(t) for t in tags] if isinstance(tags, list) else [],
|
||||
commands_dir=str(cmd_reg.get("commands_dir", "")),
|
||||
command_format=str(cmd_reg.get("format", "markdown")),
|
||||
arg_placeholder=str(cmd_reg.get("arg_placeholder", "$ARGUMENTS")),
|
||||
file_extension=str(cmd_reg.get("file_extension", ".md")),
|
||||
raw=data,
|
||||
pack_path=pack_path,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bootstrap base class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class AgentBootstrap:
|
||||
"""Base class that every agent pack's ``bootstrap.py`` must subclass.
|
||||
|
||||
Subclasses override :meth:`setup` and :meth:`teardown` to define
|
||||
agent-specific lifecycle operations.
|
||||
"""
|
||||
|
||||
def __init__(self, manifest: AgentManifest):
|
||||
self.manifest = manifest
|
||||
self.pack_path = manifest.pack_path
|
||||
|
||||
# -- lifecycle -----------------------------------------------------------
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install agent files into *project_path*.
|
||||
|
||||
This is invoked by ``specify init --ai <agent>`` and
|
||||
``specify agent switch <agent>``.
|
||||
|
||||
Implementations **must** return every file they create so that the
|
||||
CLI can record both agent-installed files and extension-installed
|
||||
files in a single install manifest.
|
||||
|
||||
Args:
|
||||
project_path: Target project directory.
|
||||
script_type: ``"sh"`` or ``"ps"``.
|
||||
options: Arbitrary key/value options forwarded from the CLI.
|
||||
|
||||
Returns:
|
||||
List of absolute paths of files created during setup.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def teardown(
|
||||
self,
|
||||
project_path: Path,
|
||||
*,
|
||||
force: bool = False,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
) -> List[str]:
|
||||
"""Remove agent-specific files from *project_path*.
|
||||
|
||||
Invoked by ``specify agent switch`` (for the *old* agent) and
|
||||
``specify agent remove`` when the user explicitly uninstalls.
|
||||
Must preserve shared infrastructure (specs, plans, tasks, etc.).
|
||||
|
||||
Only individual files are removed — directories are **never**
|
||||
deleted.
|
||||
|
||||
The caller (CLI) is expected to check for user-modified files
|
||||
**before** invoking teardown and prompt for confirmation. If
|
||||
*files* is provided, exactly those files are removed (values are
|
||||
ignored but kept for forward compatibility). Otherwise the
|
||||
install manifest is read.
|
||||
|
||||
Args:
|
||||
project_path: Project directory to clean up.
|
||||
force: When ``True``, remove files even if they were modified
|
||||
after installation.
|
||||
files: Mapping of project-relative path → SHA-256 hash.
|
||||
When supplied, only these files are removed and the
|
||||
install manifest is not consulted.
|
||||
|
||||
Returns:
|
||||
List of project-relative paths that were actually deleted.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# -- helpers available to subclasses ------------------------------------
|
||||
|
||||
def agent_dir(self, project_path: Path) -> Path:
|
||||
"""Return the agent's top-level directory inside the project."""
|
||||
return project_path / self.manifest.commands_dir.split("/")[0]
|
||||
|
||||
def collect_installed_files(self, project_path: Path) -> List[Path]:
|
||||
"""Return every file under the agent's directory tree.
|
||||
|
||||
Subclasses should call this at the end of :meth:`setup` to build
|
||||
the return list. Any files present in the agent directory at
|
||||
that point — whether created by ``setup()`` itself, by the
|
||||
scaffold pipeline, or by a preceding step — are reported.
|
||||
"""
|
||||
root = self.agent_dir(project_path)
|
||||
if not root.is_dir():
|
||||
return []
|
||||
return sorted(p for p in root.rglob("*") if p.is_file())
|
||||
|
||||
def _scaffold_project(
|
||||
self,
|
||||
project_path: Path,
|
||||
script_type: str,
|
||||
is_current_dir: bool = False,
|
||||
) -> List[Path]:
|
||||
"""Run the shared scaffolding pipeline and return new files.
|
||||
|
||||
Calls ``scaffold_from_core_pack`` for this agent and then
|
||||
collects every file that was created. Subclasses should call
|
||||
this from :meth:`setup` when they want to use the shared
|
||||
scaffolding rather than creating files manually.
|
||||
|
||||
Returns:
|
||||
List of absolute paths of **all** files created by the
|
||||
scaffold (agent-specific commands, shared scripts,
|
||||
templates, etc.).
|
||||
"""
|
||||
# Lazy import to avoid circular dependency (agent_pack is
|
||||
# imported by specify_cli.__init__).
|
||||
from specify_cli import scaffold_from_core_pack
|
||||
|
||||
# Snapshot existing files
|
||||
before: set[Path] = set()
|
||||
if project_path.exists():
|
||||
before = {p for p in project_path.rglob("*") if p.is_file()}
|
||||
|
||||
ok = scaffold_from_core_pack(
|
||||
project_path, self.manifest.id, script_type, is_current_dir,
|
||||
)
|
||||
if not ok:
|
||||
raise AgentPackError(
|
||||
f"Scaffolding failed for agent '{self.manifest.id}'")
|
||||
|
||||
# Collect every new file
|
||||
after = {p for p in project_path.rglob("*") if p.is_file()}
|
||||
return sorted(after - before)
|
||||
|
||||
def finalize_setup(
|
||||
self,
|
||||
project_path: Path,
|
||||
agent_files: Optional[List[Path]] = None,
|
||||
extension_files: Optional[List[Path]] = None,
|
||||
) -> None:
|
||||
"""Record installed files for tracked teardown.
|
||||
|
||||
This must be called **after** the full init pipeline has finished
|
||||
writing files (commands, context files, extensions) into the
|
||||
project. It combines the files reported by :meth:`setup` with
|
||||
any extra files (e.g. from extension registration), scans the
|
||||
agent's directory tree for anything additional, and writes the
|
||||
install manifest.
|
||||
|
||||
``setup()`` may return *all* files created by the shared
|
||||
scaffolding (including shared project files in ``.specify/``).
|
||||
Only files under the agent's own directory tree are recorded as
|
||||
``agent_files`` — shared project infrastructure is not tracked
|
||||
per-agent and will not be removed during teardown.
|
||||
|
||||
Args:
|
||||
agent_files: Files reported by :meth:`setup`.
|
||||
extension_files: Files created by extension registration.
|
||||
"""
|
||||
all_extension = list(extension_files or [])
|
||||
|
||||
# Filter agent_files: only keep files under the agent's directory
|
||||
# tree. setup() returns *all* scaffolded files (including shared
|
||||
# project infrastructure in .specify/) but only agent-owned files
|
||||
# should be tracked per-agent — shared files are not removed
|
||||
# during teardown/switch.
|
||||
agent_root = self.agent_dir(project_path)
|
||||
agent_root_resolved = agent_root.resolve()
|
||||
all_agent: List[Path] = []
|
||||
for p in (agent_files or []):
|
||||
try:
|
||||
p.resolve().relative_to(agent_root_resolved)
|
||||
all_agent.append(p)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Scan the agent's directory tree for files created by later
|
||||
# init pipeline steps (skills, presets, extensions) that
|
||||
# setup() did not report. We scan the agent root directory
|
||||
# (e.g. .claude/) so we catch both commands and skills
|
||||
# directories (skills-migrated agents replace the commands
|
||||
# directory with a sibling skills directory during init).
|
||||
if self.manifest.commands_dir:
|
||||
agent_root = self.agent_dir(project_path)
|
||||
if agent_root.is_dir():
|
||||
agent_set = {p.resolve() for p in all_agent}
|
||||
for p in agent_root.rglob("*"):
|
||||
if p.is_file() and p.resolve() not in agent_set:
|
||||
all_agent.append(p)
|
||||
agent_set.add(p.resolve())
|
||||
|
||||
record_installed_files(
|
||||
project_path,
|
||||
self.manifest.id,
|
||||
agent_files=all_agent,
|
||||
extension_files=all_extension,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Installed-file tracking
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _manifest_path(project_path: Path, agent_id: str) -> Path:
|
||||
"""Return the path to the install manifest for *agent_id*."""
|
||||
return project_path / ".specify" / f"agent-manifest-{agent_id}.json"
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
"""Return the hex SHA-256 of a file."""
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _hash_file_list(
|
||||
project_path: Path,
|
||||
files: List[Path],
|
||||
) -> Dict[str, str]:
|
||||
"""Build a {relative_path: sha256} dict from a list of file paths."""
|
||||
entries: Dict[str, str] = {}
|
||||
for file_path in files:
|
||||
abs_path = project_path / file_path if not file_path.is_absolute() else file_path
|
||||
if abs_path.is_file():
|
||||
rel = str(abs_path.relative_to(project_path))
|
||||
entries[rel] = _sha256(abs_path)
|
||||
return entries
|
||||
|
||||
|
||||
def record_installed_files(
|
||||
project_path: Path,
|
||||
agent_id: str,
|
||||
agent_files: Optional[List[Path]] = None,
|
||||
extension_files: Optional[List[Path]] = None,
|
||||
) -> Path:
|
||||
"""Record the installed files and their SHA-256 hashes.
|
||||
|
||||
Writes ``.specify/agent-manifest-<agent_id>.json`` containing
|
||||
categorised mappings of project-relative paths to SHA-256 digests.
|
||||
|
||||
Args:
|
||||
project_path: Project root directory.
|
||||
agent_id: Agent identifier.
|
||||
agent_files: Files created by the agent's ``setup()`` and the
|
||||
init pipeline (core commands / templates).
|
||||
extension_files: Files created by extension registration.
|
||||
|
||||
Returns:
|
||||
Path to the written manifest file.
|
||||
"""
|
||||
agent_entries = _hash_file_list(project_path, agent_files or [])
|
||||
extension_entries = _hash_file_list(project_path, extension_files or [])
|
||||
|
||||
manifest_file = _manifest_path(project_path, agent_id)
|
||||
manifest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
manifest_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_files": agent_entries,
|
||||
"extension_files": extension_entries,
|
||||
},
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest_file
|
||||
|
||||
|
||||
def _all_tracked_entries(data: dict) -> Dict[str, str]:
|
||||
"""Return the combined file → hash mapping from a manifest dict.
|
||||
|
||||
Supports both the new categorised layout (``agent_files`` +
|
||||
``extension_files``) and the legacy flat ``files`` key.
|
||||
"""
|
||||
combined: Dict[str, str] = {}
|
||||
# Legacy flat format
|
||||
if "files" in data and isinstance(data["files"], dict):
|
||||
combined.update(data["files"])
|
||||
# New categorised format
|
||||
if "agent_files" in data and isinstance(data["agent_files"], dict):
|
||||
combined.update(data["agent_files"])
|
||||
if "extension_files" in data and isinstance(data["extension_files"], dict):
|
||||
combined.update(data["extension_files"])
|
||||
return combined
|
||||
|
||||
|
||||
def get_tracked_files(
|
||||
project_path: Path,
|
||||
agent_id: str,
|
||||
) -> tuple[Dict[str, str], Dict[str, str]]:
|
||||
"""Return the tracked file hashes split by source.
|
||||
|
||||
Returns:
|
||||
A tuple ``(agent_files, extension_files)`` where each is a
|
||||
``{relative_path: sha256}`` dict. Returns two empty dicts
|
||||
when no install manifest exists.
|
||||
"""
|
||||
manifest_file = _manifest_path(project_path, agent_id)
|
||||
if not manifest_file.is_file():
|
||||
return {}, {}
|
||||
|
||||
try:
|
||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {}, {}
|
||||
|
||||
# Support legacy flat format
|
||||
if "files" in data and "agent_files" not in data:
|
||||
return dict(data["files"]), {}
|
||||
|
||||
agent_entries = data.get("agent_files", {})
|
||||
ext_entries = data.get("extension_files", {})
|
||||
return dict(agent_entries), dict(ext_entries)
|
||||
|
||||
|
||||
def check_modified_files(
|
||||
project_path: Path,
|
||||
agent_id: str,
|
||||
) -> List[str]:
|
||||
"""Return project-relative paths of files modified since installation.
|
||||
|
||||
Returns an empty list when no install manifest exists or when every
|
||||
tracked file still has its original hash.
|
||||
"""
|
||||
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 []
|
||||
|
||||
entries = _all_tracked_entries(data)
|
||||
|
||||
modified: List[str] = []
|
||||
for rel_path, original_hash in entries.items():
|
||||
abs_path = project_path / rel_path
|
||||
if abs_path.is_file():
|
||||
if _sha256(abs_path) != original_hash:
|
||||
modified.append(rel_path)
|
||||
# If the file was deleted by the user, treat it as not needing
|
||||
# removal — skip rather than flag as modified.
|
||||
|
||||
return modified
|
||||
|
||||
|
||||
def remove_tracked_files(
|
||||
project_path: Path,
|
||||
agent_id: str,
|
||||
*,
|
||||
force: bool = False,
|
||||
files: Optional[Dict[str, str]] = None,
|
||||
) -> List[str]:
|
||||
"""Remove individual tracked files.
|
||||
|
||||
If *files* is provided, exactly those files are removed (the values
|
||||
are ignored but accepted for forward compatibility). Otherwise the
|
||||
install manifest for *agent_id* is read.
|
||||
|
||||
Raises :class:`AgentFileModifiedError` if any tracked file was
|
||||
modified and *force* is ``False`` (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.
|
||||
|
||||
Args:
|
||||
project_path: Project root directory.
|
||||
agent_id: Agent identifier.
|
||||
force: When ``True``, delete even modified files.
|
||||
files: Explicit mapping of project-relative path → hash. When
|
||||
supplied, the install manifest is not consulted.
|
||||
|
||||
Returns:
|
||||
List of project-relative paths that were removed.
|
||||
"""
|
||||
manifest_file = _manifest_path(project_path, agent_id)
|
||||
|
||||
if files is not None:
|
||||
entries = files
|
||||
else:
|
||||
if not manifest_file.is_file():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return []
|
||||
|
||||
entries = _all_tracked_entries(data)
|
||||
if not entries:
|
||||
manifest_file.unlink(missing_ok=True)
|
||||
return []
|
||||
|
||||
if not force:
|
||||
modified = check_modified_files(project_path, agent_id)
|
||||
if modified:
|
||||
raise AgentFileModifiedError(
|
||||
f"The following agent files have been modified since installation:\n"
|
||||
+ "\n".join(f" {p}" for p in modified)
|
||||
+ "\nUse --force to remove them anyway."
|
||||
)
|
||||
|
||||
removed: List[str] = []
|
||||
for rel_path in entries:
|
||||
abs_path = project_path / rel_path
|
||||
if abs_path.is_file():
|
||||
abs_path.unlink()
|
||||
removed.append(rel_path)
|
||||
|
||||
# Clean up the install manifest itself
|
||||
if manifest_file.is_file():
|
||||
manifest_file.unlink(missing_ok=True)
|
||||
return removed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pack resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _embedded_agents_dir() -> Path:
|
||||
"""Return the path to the embedded agent packs inside the wheel."""
|
||||
return Path(__file__).parent / "core_pack" / "agents"
|
||||
|
||||
|
||||
def _user_agents_dir() -> Path:
|
||||
"""Return the user-level agent overrides directory."""
|
||||
return user_data_path("specify", "github") / "agents"
|
||||
|
||||
|
||||
def _project_agents_dir(project_path: Path) -> Path:
|
||||
"""Return the project-level agent overrides directory."""
|
||||
return project_path / ".specify" / "agents"
|
||||
|
||||
|
||||
def _catalog_agents_dir() -> Path:
|
||||
"""Return the catalog-installed agent cache directory."""
|
||||
return user_data_path("specify", "github") / "agent-cache"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResolvedPack:
|
||||
"""Result of resolving an agent pack through the priority stack."""
|
||||
manifest: AgentManifest
|
||||
source: str # "user", "project", "catalog", "embedded"
|
||||
path: Path
|
||||
overrides: Optional[str] = None # version of the pack being overridden
|
||||
|
||||
|
||||
def resolve_agent_pack(
|
||||
agent_id: str,
|
||||
project_path: Optional[Path] = None,
|
||||
) -> ResolvedPack:
|
||||
"""Resolve an agent pack through the priority stack.
|
||||
|
||||
Priority (highest first):
|
||||
1. User-level ``~/.specify/agents/<id>/``
|
||||
2. Project-level ``.specify/agents/<id>/``
|
||||
3. Catalog-installed cache
|
||||
4. Embedded in wheel
|
||||
|
||||
Raises ``PackResolutionError`` when no pack is found at any level.
|
||||
"""
|
||||
candidates: List[tuple[str, Path]] = []
|
||||
|
||||
# Priority 1 — user level
|
||||
user_dir = _user_agents_dir() / agent_id
|
||||
candidates.append(("user", user_dir))
|
||||
|
||||
# Priority 2 — project level
|
||||
if project_path is not None:
|
||||
proj_dir = _project_agents_dir(project_path) / agent_id
|
||||
candidates.append(("project", proj_dir))
|
||||
|
||||
# Priority 3 — catalog cache
|
||||
catalog_dir = _catalog_agents_dir() / agent_id
|
||||
candidates.append(("catalog", catalog_dir))
|
||||
|
||||
# Priority 4 — embedded
|
||||
embedded_dir = _embedded_agents_dir() / agent_id
|
||||
candidates.append(("embedded", embedded_dir))
|
||||
|
||||
embedded_manifest: Optional[AgentManifest] = None
|
||||
|
||||
for source, pack_dir in candidates:
|
||||
manifest_file = pack_dir / MANIFEST_FILENAME
|
||||
if manifest_file.is_file():
|
||||
manifest = AgentManifest.from_yaml(manifest_file)
|
||||
if source == "embedded":
|
||||
embedded_manifest = manifest
|
||||
|
||||
overrides = None
|
||||
if source != "embedded" and embedded_manifest is None:
|
||||
# Try loading embedded to record what it overrides
|
||||
emb_file = _embedded_agents_dir() / agent_id / MANIFEST_FILENAME
|
||||
if emb_file.is_file():
|
||||
try:
|
||||
emb = AgentManifest.from_yaml(emb_file)
|
||||
overrides = f"embedded v{emb.version}"
|
||||
except AgentPackError:
|
||||
pass
|
||||
|
||||
return ResolvedPack(
|
||||
manifest=manifest,
|
||||
source=source,
|
||||
path=pack_dir,
|
||||
overrides=overrides,
|
||||
)
|
||||
|
||||
raise PackResolutionError(
|
||||
f"Agent '{agent_id}' not found locally or in any active catalog.\n"
|
||||
f"Run 'specify agent search' to browse available agents, or\n"
|
||||
f"'specify agent add {agent_id} --from <path>' for offline install."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pack discovery helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def list_embedded_agents() -> List[AgentManifest]:
|
||||
"""Return manifests for all agent packs embedded in the wheel."""
|
||||
agents_dir = _embedded_agents_dir()
|
||||
if not agents_dir.is_dir():
|
||||
return []
|
||||
|
||||
manifests: List[AgentManifest] = []
|
||||
for child in sorted(agents_dir.iterdir()):
|
||||
manifest_file = child / MANIFEST_FILENAME
|
||||
if child.is_dir() and manifest_file.is_file():
|
||||
try:
|
||||
manifests.append(AgentManifest.from_yaml(manifest_file))
|
||||
except AgentPackError:
|
||||
continue
|
||||
return manifests
|
||||
|
||||
|
||||
def list_all_agents(project_path: Optional[Path] = None) -> List[ResolvedPack]:
|
||||
"""List all available agents, resolved through the priority stack.
|
||||
|
||||
Each agent id appears at most once, at its highest-priority source.
|
||||
"""
|
||||
seen: dict[str, ResolvedPack] = {}
|
||||
|
||||
# Start from lowest priority (embedded) so higher priorities overwrite
|
||||
for manifest in list_embedded_agents():
|
||||
seen[manifest.id] = ResolvedPack(
|
||||
manifest=manifest,
|
||||
source="embedded",
|
||||
path=manifest.pack_path or _embedded_agents_dir() / manifest.id,
|
||||
)
|
||||
|
||||
# Catalog cache
|
||||
catalog_dir = _catalog_agents_dir()
|
||||
if catalog_dir.is_dir():
|
||||
for child in sorted(catalog_dir.iterdir()):
|
||||
mf = child / MANIFEST_FILENAME
|
||||
if child.is_dir() and mf.is_file():
|
||||
try:
|
||||
m = AgentManifest.from_yaml(mf)
|
||||
overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None
|
||||
seen[m.id] = ResolvedPack(manifest=m, source="catalog", path=child, overrides=overrides)
|
||||
except AgentPackError:
|
||||
continue
|
||||
|
||||
# Project-level
|
||||
if project_path is not None:
|
||||
proj_dir = _project_agents_dir(project_path)
|
||||
if proj_dir.is_dir():
|
||||
for child in sorted(proj_dir.iterdir()):
|
||||
mf = child / MANIFEST_FILENAME
|
||||
if child.is_dir() and mf.is_file():
|
||||
try:
|
||||
m = AgentManifest.from_yaml(mf)
|
||||
overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None
|
||||
seen[m.id] = ResolvedPack(manifest=m, source="project", path=child, overrides=overrides)
|
||||
except AgentPackError:
|
||||
continue
|
||||
|
||||
# User-level
|
||||
user_dir = _user_agents_dir()
|
||||
if user_dir.is_dir():
|
||||
for child in sorted(user_dir.iterdir()):
|
||||
mf = child / MANIFEST_FILENAME
|
||||
if child.is_dir() and mf.is_file():
|
||||
try:
|
||||
m = AgentManifest.from_yaml(mf)
|
||||
overrides = f"embedded v{seen[m.id].manifest.version}" if m.id in seen else None
|
||||
seen[m.id] = ResolvedPack(manifest=m, source="user", path=child, overrides=overrides)
|
||||
except AgentPackError:
|
||||
continue
|
||||
|
||||
return sorted(seen.values(), key=lambda r: r.manifest.id)
|
||||
|
||||
|
||||
def load_bootstrap(pack_path: Path, manifest: AgentManifest) -> AgentBootstrap:
|
||||
"""Import ``bootstrap.py`` from *pack_path* and return the bootstrap instance.
|
||||
|
||||
The bootstrap module must define exactly one public subclass of
|
||||
``AgentBootstrap``. That class is instantiated with *manifest* and
|
||||
returned.
|
||||
"""
|
||||
bootstrap_file = pack_path / BOOTSTRAP_FILENAME
|
||||
if not bootstrap_file.is_file():
|
||||
raise AgentPackError(
|
||||
f"Bootstrap module not found: {bootstrap_file}"
|
||||
)
|
||||
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"speckit_agent_{manifest.id}_bootstrap", bootstrap_file
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise AgentPackError(f"Cannot load bootstrap module: {bootstrap_file}")
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Find the AgentBootstrap subclass
|
||||
candidates = [
|
||||
obj
|
||||
for name, obj in vars(module).items()
|
||||
if (
|
||||
isinstance(obj, type)
|
||||
and issubclass(obj, AgentBootstrap)
|
||||
and obj is not AgentBootstrap
|
||||
and not name.startswith("_")
|
||||
)
|
||||
]
|
||||
if not candidates:
|
||||
raise AgentPackError(
|
||||
f"No AgentBootstrap subclass found in {bootstrap_file}"
|
||||
)
|
||||
if len(candidates) > 1:
|
||||
raise AgentPackError(
|
||||
f"Multiple AgentBootstrap subclasses in {bootstrap_file}: "
|
||||
f"{[c.__name__ for c in candidates]}"
|
||||
)
|
||||
|
||||
return candidates[0](manifest)
|
||||
|
||||
|
||||
def validate_pack(pack_path: Path) -> List[str]:
|
||||
"""Validate a pack directory structure and return a list of warnings.
|
||||
|
||||
Returns an empty list when the pack is fully valid.
|
||||
Raises ``ManifestValidationError`` on hard errors.
|
||||
"""
|
||||
warnings: List[str] = []
|
||||
manifest_file = pack_path / MANIFEST_FILENAME
|
||||
|
||||
if not manifest_file.is_file():
|
||||
raise ManifestValidationError(
|
||||
f"Missing {MANIFEST_FILENAME} in {pack_path}"
|
||||
)
|
||||
|
||||
manifest = AgentManifest.from_yaml(manifest_file)
|
||||
|
||||
bootstrap_file = pack_path / BOOTSTRAP_FILENAME
|
||||
if not bootstrap_file.is_file():
|
||||
warnings.append(f"Missing {BOOTSTRAP_FILENAME} (pack cannot be bootstrapped)")
|
||||
|
||||
if not manifest.commands_dir:
|
||||
warnings.append("command_registration.commands_dir not set in manifest")
|
||||
|
||||
if not manifest.description:
|
||||
warnings.append("agent.description is empty")
|
||||
|
||||
if not manifest.tags:
|
||||
warnings.append("No tags specified (reduces discoverability)")
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def export_pack(agent_id: str, dest: Path, project_path: Optional[Path] = None) -> Path:
|
||||
"""Export the active pack for *agent_id* to *dest*.
|
||||
|
||||
Returns the path to the exported pack directory.
|
||||
"""
|
||||
resolved = resolve_agent_pack(agent_id, project_path=project_path)
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(resolved.path, dest, dirs_exist_ok=True)
|
||||
return dest
|
||||
0
src/specify_cli/core_pack/agents/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/agy/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/agy/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/agy/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/agy/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Antigravity agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Agy(AgentBootstrap):
|
||||
"""Bootstrap for Antigravity."""
|
||||
|
||||
AGENT_DIR = ".agent"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Antigravity agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Antigravity agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
23
src/specify_cli/core_pack/agents/agy/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/agy/speckit-agent.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "agy"
|
||||
name: "Antigravity"
|
||||
version: "1.0.0"
|
||||
description: "Antigravity IDE for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: false
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['ide', 'antigravity']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".agent/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/amp/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/amp/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/amp/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/amp/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Amp agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Amp(AgentBootstrap):
|
||||
"""Bootstrap for Amp."""
|
||||
|
||||
AGENT_DIR = ".agents"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Amp agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Amp agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/amp/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/amp/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "amp"
|
||||
name: "Amp"
|
||||
version: "1.0.0"
|
||||
description: "Amp CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://ampcode.com/manual#install"
|
||||
cli_tool: "amp"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'amp']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".agents/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/auggie/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/auggie/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/auggie/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/auggie/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Auggie CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Auggie(AgentBootstrap):
|
||||
"""Bootstrap for Auggie CLI."""
|
||||
|
||||
AGENT_DIR = ".augment"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Auggie CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Auggie CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/auggie/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/auggie/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "auggie"
|
||||
name: "Auggie CLI"
|
||||
version: "1.0.0"
|
||||
description: "Auggie CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli"
|
||||
cli_tool: "auggie"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'augment', 'auggie']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".augment/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/bob/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/bob/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/bob/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/bob/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for IBM Bob agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Bob(AgentBootstrap):
|
||||
"""Bootstrap for IBM Bob."""
|
||||
|
||||
AGENT_DIR = ".bob"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install IBM Bob agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove IBM Bob agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
23
src/specify_cli/core_pack/agents/bob/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/bob/speckit-agent.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "bob"
|
||||
name: "IBM Bob"
|
||||
version: "1.0.0"
|
||||
description: "IBM Bob IDE for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: false
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['ide', 'ibm', 'bob']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".bob/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/claude/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/claude/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/claude/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/claude/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Claude Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Claude(AgentBootstrap):
|
||||
"""Bootstrap for Claude Code."""
|
||||
|
||||
AGENT_DIR = ".claude"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Claude Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Claude Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/claude/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/claude/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "claude"
|
||||
name: "Claude Code"
|
||||
version: "1.0.0"
|
||||
description: "Anthropic's Claude Code CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://docs.anthropic.com/en/docs/claude-code/setup"
|
||||
cli_tool: "claude"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'anthropic', 'claude']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".claude/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
30
src/specify_cli/core_pack/agents/codebuddy/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/codebuddy/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for CodeBuddy agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Codebuddy(AgentBootstrap):
|
||||
"""Bootstrap for CodeBuddy."""
|
||||
|
||||
AGENT_DIR = ".codebuddy"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install CodeBuddy agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove CodeBuddy agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/codebuddy/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "codebuddy"
|
||||
name: "CodeBuddy"
|
||||
version: "1.0.0"
|
||||
description: "CodeBuddy CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://www.codebuddy.ai/cli"
|
||||
cli_tool: "codebuddy"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'codebuddy']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".codebuddy/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/codex/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/codex/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/codex/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/codex/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Codex CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Codex(AgentBootstrap):
|
||||
"""Bootstrap for Codex CLI."""
|
||||
|
||||
AGENT_DIR = ".agents"
|
||||
COMMANDS_SUBDIR = "skills"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Codex CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Codex CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/codex/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/codex/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "codex"
|
||||
name: "Codex CLI"
|
||||
version: "1.0.0"
|
||||
description: "OpenAI Codex CLI with project skills support"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://github.com/openai/codex"
|
||||
cli_tool: "codex"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'openai', 'codex', 'skills']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".agents/skills"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: "/SKILL.md"
|
||||
30
src/specify_cli/core_pack/agents/copilot/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/copilot/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for GitHub Copilot agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Copilot(AgentBootstrap):
|
||||
"""Bootstrap for GitHub Copilot."""
|
||||
|
||||
AGENT_DIR = ".github"
|
||||
COMMANDS_SUBDIR = "agents"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install GitHub Copilot agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove GitHub Copilot agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
23
src/specify_cli/core_pack/agents/copilot/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/copilot/speckit-agent.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "copilot"
|
||||
name: "GitHub Copilot"
|
||||
version: "1.0.0"
|
||||
description: "GitHub Copilot for AI-assisted development in VS Code"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: false
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['ide', 'github', 'copilot']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".github/agents"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".agent.md"
|
||||
30
src/specify_cli/core_pack/agents/cursor-agent/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/cursor-agent/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Cursor agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class CursorAgent(AgentBootstrap):
|
||||
"""Bootstrap for Cursor."""
|
||||
|
||||
AGENT_DIR = ".cursor"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Cursor agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Cursor agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
@@ -0,0 +1,23 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "cursor-agent"
|
||||
name: "Cursor"
|
||||
version: "1.0.0"
|
||||
description: "Cursor IDE for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: false
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['ide', 'cursor']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".cursor/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/gemini/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/gemini/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/gemini/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/gemini/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Gemini CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Gemini(AgentBootstrap):
|
||||
"""Bootstrap for Gemini CLI."""
|
||||
|
||||
AGENT_DIR = ".gemini"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Gemini CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Gemini CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/gemini/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/gemini/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "gemini"
|
||||
name: "Gemini CLI"
|
||||
version: "1.0.0"
|
||||
description: "Google's Gemini CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://github.com/google-gemini/gemini-cli"
|
||||
cli_tool: "gemini"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'google', 'gemini']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".gemini/commands"
|
||||
format: "toml"
|
||||
arg_placeholder: "{{args}}"
|
||||
file_extension: ".toml"
|
||||
0
src/specify_cli/core_pack/agents/iflow/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/iflow/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/iflow/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/iflow/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for iFlow CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Iflow(AgentBootstrap):
|
||||
"""Bootstrap for iFlow CLI."""
|
||||
|
||||
AGENT_DIR = ".iflow"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install iFlow CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove iFlow CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/iflow/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/iflow/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "iflow"
|
||||
name: "iFlow CLI"
|
||||
version: "1.0.0"
|
||||
description: "iFlow CLI by iflow-ai for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://docs.iflow.cn/en/cli/quickstart"
|
||||
cli_tool: "iflow"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'iflow']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".iflow/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/junie/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/junie/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/junie/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/junie/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Junie agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Junie(AgentBootstrap):
|
||||
"""Bootstrap for Junie."""
|
||||
|
||||
AGENT_DIR = ".junie"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Junie agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Junie agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/junie/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/junie/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "junie"
|
||||
name: "Junie"
|
||||
version: "1.0.0"
|
||||
description: "Junie by JetBrains for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://junie.jetbrains.com/"
|
||||
cli_tool: "junie"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'jetbrains', 'junie']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".junie/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
30
src/specify_cli/core_pack/agents/kilocode/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/kilocode/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Kilo Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Kilocode(AgentBootstrap):
|
||||
"""Bootstrap for Kilo Code."""
|
||||
|
||||
AGENT_DIR = ".kilocode"
|
||||
COMMANDS_SUBDIR = "workflows"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Kilo Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Kilo Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
23
src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/kilocode/speckit-agent.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "kilocode"
|
||||
name: "Kilo Code"
|
||||
version: "1.0.0"
|
||||
description: "Kilo Code IDE for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: false
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['ide', 'kilocode']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".kilocode/workflows"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/kimi/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/kimi/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/kimi/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/kimi/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Kimi Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Kimi(AgentBootstrap):
|
||||
"""Bootstrap for Kimi Code."""
|
||||
|
||||
AGENT_DIR = ".kimi"
|
||||
COMMANDS_SUBDIR = "skills"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Kimi Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Kimi Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/kimi/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/kimi/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "kimi"
|
||||
name: "Kimi Code"
|
||||
version: "1.0.0"
|
||||
description: "Kimi Code CLI by Moonshot AI with skills support"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://code.kimi.com/"
|
||||
cli_tool: "kimi"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'moonshot', 'kimi', 'skills']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".kimi/skills"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: "/SKILL.md"
|
||||
30
src/specify_cli/core_pack/agents/kiro-cli/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/kiro-cli/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Kiro CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class KiroCli(AgentBootstrap):
|
||||
"""Bootstrap for Kiro CLI."""
|
||||
|
||||
AGENT_DIR = ".kiro"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Kiro CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Kiro CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/kiro-cli/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "kiro-cli"
|
||||
name: "Kiro CLI"
|
||||
version: "1.0.0"
|
||||
description: "Kiro CLI by Amazon for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://kiro.dev/docs/cli/"
|
||||
cli_tool: "kiro-cli"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'amazon', 'kiro']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".kiro/prompts"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
30
src/specify_cli/core_pack/agents/opencode/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/opencode/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for opencode agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Opencode(AgentBootstrap):
|
||||
"""Bootstrap for opencode."""
|
||||
|
||||
AGENT_DIR = ".opencode"
|
||||
COMMANDS_SUBDIR = "command"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install opencode agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove opencode agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/opencode/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/opencode/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "opencode"
|
||||
name: "opencode"
|
||||
version: "1.0.0"
|
||||
description: "opencode CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://opencode.ai"
|
||||
cli_tool: "opencode"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'opencode']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".opencode/command"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/pi/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/pi/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/pi/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/pi/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Pi Coding Agent agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Pi(AgentBootstrap):
|
||||
"""Bootstrap for Pi Coding Agent."""
|
||||
|
||||
AGENT_DIR = ".pi"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Pi Coding Agent agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Pi Coding Agent agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/pi/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/pi/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "pi"
|
||||
name: "Pi Coding Agent"
|
||||
version: "1.0.0"
|
||||
description: "Pi terminal coding agent for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://www.npmjs.com/package/@mariozechner/pi-coding-agent"
|
||||
cli_tool: "pi"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'pi']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".pi/prompts"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
30
src/specify_cli/core_pack/agents/qodercli/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/qodercli/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Qoder CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Qodercli(AgentBootstrap):
|
||||
"""Bootstrap for Qoder CLI."""
|
||||
|
||||
AGENT_DIR = ".qoder"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Qoder CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Qoder CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/qodercli/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "qodercli"
|
||||
name: "Qoder CLI"
|
||||
version: "1.0.0"
|
||||
description: "Qoder CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://qoder.com/cli"
|
||||
cli_tool: "qodercli"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'qoder']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".qoder/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/qwen/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/qwen/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/qwen/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/qwen/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Qwen Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Qwen(AgentBootstrap):
|
||||
"""Bootstrap for Qwen Code."""
|
||||
|
||||
AGENT_DIR = ".qwen"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Qwen Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Qwen Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/qwen/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/qwen/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "qwen"
|
||||
name: "Qwen Code"
|
||||
version: "1.0.0"
|
||||
description: "Alibaba's Qwen Code CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://github.com/QwenLM/qwen-code"
|
||||
cli_tool: "qwen"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'alibaba', 'qwen']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".qwen/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/roo/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/roo/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/roo/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/roo/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Roo Code agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Roo(AgentBootstrap):
|
||||
"""Bootstrap for Roo Code."""
|
||||
|
||||
AGENT_DIR = ".roo"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Roo Code agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Roo Code agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
23
src/specify_cli/core_pack/agents/roo/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/roo/speckit-agent.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "roo"
|
||||
name: "Roo Code"
|
||||
version: "1.0.0"
|
||||
description: "Roo Code IDE for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: false
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['ide', 'roo']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".roo/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/shai/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/shai/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/shai/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/shai/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for SHAI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Shai(AgentBootstrap):
|
||||
"""Bootstrap for SHAI."""
|
||||
|
||||
AGENT_DIR = ".shai"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install SHAI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove SHAI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/shai/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/shai/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "shai"
|
||||
name: "SHAI"
|
||||
version: "1.0.0"
|
||||
description: "SHAI CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://github.com/ovh/shai"
|
||||
cli_tool: "shai"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'ovh', 'shai']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".shai/commands"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
30
src/specify_cli/core_pack/agents/tabnine/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/tabnine/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Tabnine CLI agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Tabnine(AgentBootstrap):
|
||||
"""Bootstrap for Tabnine CLI."""
|
||||
|
||||
AGENT_DIR = ".tabnine/agent"
|
||||
COMMANDS_SUBDIR = "commands"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Tabnine CLI agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Tabnine CLI agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/tabnine/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "tabnine"
|
||||
name: "Tabnine CLI"
|
||||
version: "1.0.0"
|
||||
description: "Tabnine CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://docs.tabnine.com/main/getting-started/tabnine-cli"
|
||||
cli_tool: "tabnine"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'tabnine']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".tabnine/agent/commands"
|
||||
format: "toml"
|
||||
arg_placeholder: "{{args}}"
|
||||
file_extension: ".toml"
|
||||
0
src/specify_cli/core_pack/agents/trae/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/trae/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/trae/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/trae/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Trae agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Trae(AgentBootstrap):
|
||||
"""Bootstrap for Trae."""
|
||||
|
||||
AGENT_DIR = ".trae"
|
||||
COMMANDS_SUBDIR = "rules"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Trae agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Trae agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
23
src/specify_cli/core_pack/agents/trae/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/trae/speckit-agent.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "trae"
|
||||
name: "Trae"
|
||||
version: "1.0.0"
|
||||
description: "Trae IDE for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: false
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['ide', 'trae']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".trae/rules"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
0
src/specify_cli/core_pack/agents/vibe/__init__.py
Normal file
0
src/specify_cli/core_pack/agents/vibe/__init__.py
Normal file
30
src/specify_cli/core_pack/agents/vibe/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/vibe/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Mistral Vibe agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Vibe(AgentBootstrap):
|
||||
"""Bootstrap for Mistral Vibe."""
|
||||
|
||||
AGENT_DIR = ".vibe"
|
||||
COMMANDS_SUBDIR = "prompts"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Mistral Vibe agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Mistral Vibe agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
25
src/specify_cli/core_pack/agents/vibe/speckit-agent.yml
Normal file
25
src/specify_cli/core_pack/agents/vibe/speckit-agent.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "vibe"
|
||||
name: "Mistral Vibe"
|
||||
version: "1.0.0"
|
||||
description: "Mistral Vibe CLI for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: true
|
||||
install_url: "https://github.com/mistralai/mistral-vibe"
|
||||
cli_tool: "vibe"
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['cli', 'mistral', 'vibe']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".vibe/prompts"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
30
src/specify_cli/core_pack/agents/windsurf/bootstrap.py
Normal file
30
src/specify_cli/core_pack/agents/windsurf/bootstrap.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Bootstrap module for Windsurf agent pack."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from specify_cli.agent_pack import AgentBootstrap, remove_tracked_files
|
||||
|
||||
|
||||
class Windsurf(AgentBootstrap):
|
||||
"""Bootstrap for Windsurf."""
|
||||
|
||||
AGENT_DIR = ".windsurf"
|
||||
COMMANDS_SUBDIR = "workflows"
|
||||
|
||||
def setup(self, project_path: Path, script_type: str, options: Dict[str, Any]) -> List[Path]:
|
||||
"""Install Windsurf agent files into the project."""
|
||||
commands_dir = project_path / self.AGENT_DIR / self.COMMANDS_SUBDIR
|
||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
||||
return self._scaffold_project(project_path, script_type)
|
||||
|
||||
def teardown(self, project_path: Path, *, force: bool = False, files: Optional[Dict[str, str]] = None) -> List[str]:
|
||||
"""Remove Windsurf agent files from the project.
|
||||
|
||||
Only removes individual tracked files — directories are never
|
||||
deleted. When *files* is provided, exactly those files are
|
||||
removed. Otherwise the install manifest is consulted and
|
||||
``AgentFileModifiedError`` is raised if any tracked file was
|
||||
modified and *force* is ``False``.
|
||||
"""
|
||||
return remove_tracked_files(project_path, self.manifest.id, force=force, files=files)
|
||||
23
src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml
Normal file
23
src/specify_cli/core_pack/agents/windsurf/speckit-agent.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
agent:
|
||||
id: "windsurf"
|
||||
name: "Windsurf"
|
||||
version: "1.0.0"
|
||||
description: "Windsurf IDE for AI-assisted development"
|
||||
author: "github"
|
||||
license: "MIT"
|
||||
|
||||
runtime:
|
||||
requires_cli: false
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.1.0"
|
||||
|
||||
tags: ['ide', 'windsurf']
|
||||
|
||||
command_registration:
|
||||
commands_dir: ".windsurf/workflows"
|
||||
format: "markdown"
|
||||
arg_placeholder: "$ARGUMENTS"
|
||||
file_extension: ".md"
|
||||
1285
tests/test_agent_pack.py
Normal file
1285
tests/test_agent_pack.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user