From 255371d36741241df92090602765851259e82914 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:17:21 -0500 Subject: [PATCH] =?UTF-8?q?Stage=203:=20Standard=20markdown=20integrations?= =?UTF-8?q?=20=E2=80=94=2019=20agents=20migrated=20to=20plugin=20architect?= =?UTF-8?q?ure=20(#2038)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Stage 3: Standard markdown integrations — 19 agents migrated to plugin architecture Migrate all standard markdown integrations to self-contained subpackages under integrations/. Each subclasses MarkdownIntegration with config-only overrides (~10 lines per __init__.py). Integrations migrated (19): claude, qwen, opencode, junie, kilocode, auggie, roo, codebuddy, qodercli, amp, shai, bob, trae, pi, iflow, kiro-cli, windsurf, vibe, cursor-agent Changes: - Create integrations// subpackage with __init__.py and scripts/ (update-context.sh, update-context.ps1) for each integration - Register all 19 in INTEGRATION_REGISTRY (20 total with copilot) - MarkdownIntegration.setup() processes templates (replaces {SCRIPT}, {ARGS}, __AGENT__; strips frontmatter blocks; rewrites paths) - Extract install_scripts() to IntegrationBase; refactor copilot to use it - Generalize --ai auto-promote from copilot-only to registry-driven: any integration registered in INTEGRATION_REGISTRY auto-promotes. Unregistered agents (gemini, tabnine, codex, kimi, agy, generic) continue through the legacy --ai path unchanged. - Fix cursor/cursor-agent key mismatch in CommandRegistrar.AGENT_CONFIGS - Add missing vibe entry to CommandRegistrar.AGENT_CONFIGS - Update kiro alias test to reflect auto-promote behavior Testing: - Per-agent test files (test_integration_.py) with shared mixin - 1316 tests passing, 0 failures - Complete file inventory tests for both sh and ps variants - Byte-for-byte validated against v0.4.3 release packages (684 files) * Address PR review: fix repo root detection and no-op test - Fix repo root fallback in all 20 update-context.sh scripts: walk up from script location to find .specify/ instead of falling back to pwd - Fix repo root fallback in all 20 update-context.ps1 scripts: walk up from script location to find .specify/ instead of falling back to $PWD - Add assertions to test_setup_writes_to_correct_directory: verify expected_dir exists and all command files reside under it * Fix REPO_ROOT priority: prefer .specify walk-up over git root In monorepos the git toplevel may differ from the project root that contains .specify/. The previous fix still preferred git rev-parse over the walk-up result. Bash scripts (20): prefer the discovered _root when it contains .specify/; only accept git root if it also contains .specify/. PowerShell scripts (20): validate git root contains .specify/ before using it; fall back to walking up from script directory otherwise. * Guard git call with try/catch in PowerShell scripts With $ErrorActionPreference = 'Stop', an unguarded git rev-parse throws a terminating CommandNotFoundException when git is not installed, preventing the .specify walk-up fallback from running. Wrap the git call in try/catch across all 20 update-context.ps1 scripts so the fallback works reliably without git. * Rename hyphenated package dirs to valid Python identifiers Rename kiro-cli → kiro_cli and cursor-agent → cursor_agent so the packages can be imported with normal Python syntax instead of importlib. The user-facing integration key (IntegrationBase.key) stays hyphenated to match the actual CLI tool / binary name. Also reorganize _register_builtins(): imports and registrations are now grouped alphabetically with clear section comments. * Reuse CommandRegistrar path rewriting in process_template() Replace the duplicated regex-based path rewriting in MarkdownIntegration.process_template() with a call to the shared CommandRegistrar._rewrite_project_relative_paths() implementation. This ensures extension-local paths are preserved and boundary rules stay consistent across the codebase. * Promote _rewrite_project_relative_paths to public API Rename CommandRegistrar._rewrite_project_relative_paths() to rewrite_project_relative_paths() (drop leading underscore) so integrations can call it without reaching into a private method across subsystem boundaries. Addresses PR review feedback: https://github.com/github/spec-kit/pull/2038#discussion_r3022105627 * Broaden TestRegistrarKeyAlignment to cover all integration keys Parametrize across ALL_INTEGRATION_KEYS instead of only checking cursor-agent and vibe. Keeps a separate negative test for the stale 'cursor' shorthand. Addresses PR review feedback: https://github.com/github/spec-kit/pull/2038#discussion_r3022269032 --- src/specify_cli/__init__.py | 10 +- src/specify_cli/agents.py | 14 +- src/specify_cli/integrations/__init__.py | 51 ++- src/specify_cli/integrations/amp/__init__.py | 21 ++ .../amp/scripts/update-context.ps1 | 23 ++ .../amp/scripts/update-context.sh | 28 ++ .../integrations/auggie/__init__.py | 21 ++ .../auggie/scripts/update-context.ps1 | 23 ++ .../auggie/scripts/update-context.sh | 28 ++ src/specify_cli/integrations/base.py | 111 ++++++- src/specify_cli/integrations/bob/__init__.py | 21 ++ .../bob/scripts/update-context.ps1 | 23 ++ .../bob/scripts/update-context.sh | 28 ++ .../integrations/claude/__init__.py | 21 ++ .../claude/scripts/update-context.ps1 | 23 ++ .../claude/scripts/update-context.sh | 28 ++ .../integrations/codebuddy/__init__.py | 21 ++ .../codebuddy/scripts/update-context.ps1 | 23 ++ .../codebuddy/scripts/update-context.sh | 28 ++ .../integrations/copilot/__init__.py | 14 +- .../copilot/scripts/update-context.ps1 | 14 +- .../copilot/scripts/update-context.sh | 17 +- .../integrations/cursor_agent/__init__.py | 21 ++ .../cursor_agent/scripts/update-context.ps1 | 23 ++ .../cursor_agent/scripts/update-context.sh | 28 ++ .../integrations/iflow/__init__.py | 21 ++ .../iflow/scripts/update-context.ps1 | 23 ++ .../iflow/scripts/update-context.sh | 28 ++ .../integrations/junie/__init__.py | 21 ++ .../junie/scripts/update-context.ps1 | 23 ++ .../junie/scripts/update-context.sh | 28 ++ .../integrations/kilocode/__init__.py | 21 ++ .../kilocode/scripts/update-context.ps1 | 23 ++ .../kilocode/scripts/update-context.sh | 28 ++ .../integrations/kiro_cli/__init__.py | 21 ++ .../kiro_cli/scripts/update-context.ps1 | 23 ++ .../kiro_cli/scripts/update-context.sh | 28 ++ .../integrations/opencode/__init__.py | 21 ++ .../opencode/scripts/update-context.ps1 | 23 ++ .../opencode/scripts/update-context.sh | 28 ++ src/specify_cli/integrations/pi/__init__.py | 21 ++ .../pi/scripts/update-context.ps1 | 23 ++ .../integrations/pi/scripts/update-context.sh | 28 ++ .../integrations/qodercli/__init__.py | 21 ++ .../qodercli/scripts/update-context.ps1 | 23 ++ .../qodercli/scripts/update-context.sh | 28 ++ src/specify_cli/integrations/qwen/__init__.py | 21 ++ .../qwen/scripts/update-context.ps1 | 23 ++ .../qwen/scripts/update-context.sh | 28 ++ src/specify_cli/integrations/roo/__init__.py | 21 ++ .../roo/scripts/update-context.ps1 | 23 ++ .../roo/scripts/update-context.sh | 28 ++ src/specify_cli/integrations/shai/__init__.py | 21 ++ .../shai/scripts/update-context.ps1 | 23 ++ .../shai/scripts/update-context.sh | 28 ++ src/specify_cli/integrations/trae/__init__.py | 21 ++ .../trae/scripts/update-context.ps1 | 23 ++ .../trae/scripts/update-context.sh | 28 ++ src/specify_cli/integrations/vibe/__init__.py | 21 ++ .../vibe/scripts/update-context.ps1 | 23 ++ .../vibe/scripts/update-context.sh | 28 ++ .../integrations/windsurf/__init__.py | 21 ++ .../windsurf/scripts/update-context.ps1 | 23 ++ .../windsurf/scripts/update-context.sh | 28 ++ tests/integrations/test_integration_amp.py | 11 + tests/integrations/test_integration_auggie.py | 11 + .../test_integration_base_markdown.py | 296 ++++++++++++++++++ tests/integrations/test_integration_bob.py | 11 + tests/integrations/test_integration_claude.py | 11 + .../test_integration_codebuddy.py | 11 + ...copilot.py => test_integration_copilot.py} | 0 .../test_integration_cursor_agent.py | 11 + tests/integrations/test_integration_iflow.py | 11 + tests/integrations/test_integration_junie.py | 11 + .../integrations/test_integration_kilocode.py | 11 + .../integrations/test_integration_kiro_cli.py | 11 + .../integrations/test_integration_opencode.py | 11 + tests/integrations/test_integration_pi.py | 11 + .../integrations/test_integration_qodercli.py | 11 + tests/integrations/test_integration_qwen.py | 11 + tests/integrations/test_integration_roo.py | 11 + tests/integrations/test_integration_shai.py | 11 + tests/integrations/test_integration_trae.py | 11 + tests/integrations/test_integration_vibe.py | 11 + .../integrations/test_integration_windsurf.py | 11 + tests/integrations/test_registry.py | 37 ++- tests/test_ai_skills.py | 31 +- tests/test_extensions.py | 2 +- 88 files changed, 2113 insertions(+), 61 deletions(-) create mode 100644 src/specify_cli/integrations/amp/__init__.py create mode 100644 src/specify_cli/integrations/amp/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/amp/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/auggie/__init__.py create mode 100644 src/specify_cli/integrations/auggie/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/auggie/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/bob/__init__.py create mode 100644 src/specify_cli/integrations/bob/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/bob/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/claude/__init__.py create mode 100644 src/specify_cli/integrations/claude/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/claude/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/codebuddy/__init__.py create mode 100644 src/specify_cli/integrations/codebuddy/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/codebuddy/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/cursor_agent/__init__.py create mode 100644 src/specify_cli/integrations/cursor_agent/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/cursor_agent/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/iflow/__init__.py create mode 100644 src/specify_cli/integrations/iflow/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/iflow/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/junie/__init__.py create mode 100644 src/specify_cli/integrations/junie/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/junie/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/kilocode/__init__.py create mode 100644 src/specify_cli/integrations/kilocode/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/kilocode/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/kiro_cli/__init__.py create mode 100644 src/specify_cli/integrations/kiro_cli/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/kiro_cli/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/opencode/__init__.py create mode 100644 src/specify_cli/integrations/opencode/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/opencode/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/pi/__init__.py create mode 100644 src/specify_cli/integrations/pi/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/pi/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/qodercli/__init__.py create mode 100644 src/specify_cli/integrations/qodercli/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/qodercli/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/qwen/__init__.py create mode 100644 src/specify_cli/integrations/qwen/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/qwen/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/roo/__init__.py create mode 100644 src/specify_cli/integrations/roo/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/roo/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/shai/__init__.py create mode 100644 src/specify_cli/integrations/shai/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/shai/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/trae/__init__.py create mode 100644 src/specify_cli/integrations/trae/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/trae/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/vibe/__init__.py create mode 100644 src/specify_cli/integrations/vibe/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/vibe/scripts/update-context.sh create mode 100644 src/specify_cli/integrations/windsurf/__init__.py create mode 100644 src/specify_cli/integrations/windsurf/scripts/update-context.ps1 create mode 100755 src/specify_cli/integrations/windsurf/scripts/update-context.sh create mode 100644 tests/integrations/test_integration_amp.py create mode 100644 tests/integrations/test_integration_auggie.py create mode 100644 tests/integrations/test_integration_base_markdown.py create mode 100644 tests/integrations/test_integration_bob.py create mode 100644 tests/integrations/test_integration_claude.py create mode 100644 tests/integrations/test_integration_codebuddy.py rename tests/integrations/{test_copilot.py => test_integration_copilot.py} (100%) create mode 100644 tests/integrations/test_integration_cursor_agent.py create mode 100644 tests/integrations/test_integration_iflow.py create mode 100644 tests/integrations/test_integration_junie.py create mode 100644 tests/integrations/test_integration_kilocode.py create mode 100644 tests/integrations/test_integration_kiro_cli.py create mode 100644 tests/integrations/test_integration_opencode.py create mode 100644 tests/integrations/test_integration_pi.py create mode 100644 tests/integrations/test_integration_qodercli.py create mode 100644 tests/integrations/test_integration_qwen.py create mode 100644 tests/integrations/test_integration_roo.py create mode 100644 tests/integrations/test_integration_shai.py create mode 100644 tests/integrations/test_integration_trae.py create mode 100644 tests/integrations/test_integration_vibe.py create mode 100644 tests/integrations/test_integration_windsurf.py diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index e53d7f18e..698b672da 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1974,7 +1974,7 @@ def init( console.print("[yellow]Use:[/yellow] --integration for the new integration system, or --ai for the legacy path") raise typer.Exit(1) - # Auto-promote: --ai copilot → integration path with a nudge + # Auto-promote: --ai → integration path with a nudge (if registered) use_integration = False if integration: from .integrations import INTEGRATION_REGISTRY, get_integration @@ -1987,14 +1987,14 @@ def init( use_integration = True # Map integration key to the ai_assistant variable for downstream compatibility ai_assistant = integration - elif ai_assistant == "copilot": + elif ai_assistant: from .integrations import get_integration - resolved_integration = get_integration("copilot") + resolved_integration = get_integration(ai_assistant) if resolved_integration: use_integration = True console.print( - "[dim]Tip: Use [bold]--integration copilot[/bold] instead of " - "--ai copilot. The --ai flag will be deprecated in a future release.[/dim]" + f"[dim]Tip: Use [bold]--integration {ai_assistant}[/bold] instead of " + f"--ai {ai_assistant}. The --ai flag will be deprecated in a future release.[/dim]" ) if project_name == ".": diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 64617e843..4a8c2d1b2 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -43,7 +43,7 @@ class CommandRegistrar: "args": "$ARGUMENTS", "extension": ".agent.md" }, - "cursor": { + "cursor-agent": { "dir": ".cursor/commands", "format": "markdown", "args": "$ARGUMENTS", @@ -162,6 +162,12 @@ class CommandRegistrar: "format": "markdown", "args": "$ARGUMENTS", "extension": ".md" + }, + "vibe": { + "dir": ".vibe/prompts", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md" } } @@ -235,11 +241,11 @@ class CommandRegistrar: for key, script_path in scripts.items(): if isinstance(script_path, str): - scripts[key] = self._rewrite_project_relative_paths(script_path) + scripts[key] = self.rewrite_project_relative_paths(script_path) return frontmatter @staticmethod - def _rewrite_project_relative_paths(text: str) -> str: + def rewrite_project_relative_paths(text: str) -> str: """Rewrite repo-relative paths to their generated project locations.""" if not isinstance(text, str) or not text: return text @@ -422,7 +428,7 @@ class CommandRegistrar: body = body.replace("{AGENT_SCRIPT}", agent_script_command) body = body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", agent_name) - return CommandRegistrar._rewrite_project_relative_paths(body) + return CommandRegistrar.rewrite_project_relative_paths(body) def _convert_argument_placeholder(self, content: str, from_placeholder: str, to_placeholder: str) -> str: """Convert argument placeholder format. diff --git a/src/specify_cli/integrations/__init__.py b/src/specify_cli/integrations/__init__.py index e5ddc5c5a..0d7a71242 100644 --- a/src/specify_cli/integrations/__init__.py +++ b/src/specify_cli/integrations/__init__.py @@ -37,10 +37,57 @@ def get_integration(key: str) -> IntegrationBase | None: # -- Register built-in integrations -------------------------------------- def _register_builtins() -> None: - """Register all built-in integrations.""" - from .copilot import CopilotIntegration + """Register all built-in integrations. + Package directories use Python-safe identifiers (e.g. ``kiro_cli``, + ``cursor_agent``). The user-facing integration key stored in + ``IntegrationBase.key`` stays hyphenated (``"kiro-cli"``, + ``"cursor-agent"``) to match the actual CLI tool / binary name that + users install and invoke. + """ + # -- Imports (alphabetical) ------------------------------------------- + from .amp import AmpIntegration + from .auggie import AuggieIntegration + from .bob import BobIntegration + from .claude import ClaudeIntegration + from .codebuddy import CodebuddyIntegration + from .copilot import CopilotIntegration + from .cursor_agent import CursorAgentIntegration + from .iflow import IflowIntegration + from .junie import JunieIntegration + from .kilocode import KilocodeIntegration + from .kiro_cli import KiroCliIntegration + from .opencode import OpencodeIntegration + from .pi import PiIntegration + from .qodercli import QodercliIntegration + from .qwen import QwenIntegration + from .roo import RooIntegration + from .shai import ShaiIntegration + from .trae import TraeIntegration + from .vibe import VibeIntegration + from .windsurf import WindsurfIntegration + + # -- Registration (alphabetical) -------------------------------------- + _register(AmpIntegration()) + _register(AuggieIntegration()) + _register(BobIntegration()) + _register(ClaudeIntegration()) + _register(CodebuddyIntegration()) _register(CopilotIntegration()) + _register(CursorAgentIntegration()) + _register(IflowIntegration()) + _register(JunieIntegration()) + _register(KilocodeIntegration()) + _register(KiroCliIntegration()) + _register(OpencodeIntegration()) + _register(PiIntegration()) + _register(QodercliIntegration()) + _register(QwenIntegration()) + _register(RooIntegration()) + _register(ShaiIntegration()) + _register(TraeIntegration()) + _register(VibeIntegration()) + _register(WindsurfIntegration()) _register_builtins() diff --git a/src/specify_cli/integrations/amp/__init__.py b/src/specify_cli/integrations/amp/__init__.py new file mode 100644 index 000000000..39df0a9bb --- /dev/null +++ b/src/specify_cli/integrations/amp/__init__.py @@ -0,0 +1,21 @@ +"""Amp CLI integration.""" + +from ..base import MarkdownIntegration + + +class AmpIntegration(MarkdownIntegration): + key = "amp" + config = { + "name": "Amp", + "folder": ".agents/", + "commands_subdir": "commands", + "install_url": "https://ampcode.com/manual#install", + "requires_cli": True, + } + registrar_config = { + "dir": ".agents/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "AGENTS.md" diff --git a/src/specify_cli/integrations/amp/scripts/update-context.ps1 b/src/specify_cli/integrations/amp/scripts/update-context.ps1 new file mode 100644 index 000000000..c217b99f9 --- /dev/null +++ b/src/specify_cli/integrations/amp/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Amp integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType amp diff --git a/src/specify_cli/integrations/amp/scripts/update-context.sh b/src/specify_cli/integrations/amp/scripts/update-context.sh new file mode 100755 index 000000000..56cbf6e78 --- /dev/null +++ b/src/specify_cli/integrations/amp/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Amp integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" amp diff --git a/src/specify_cli/integrations/auggie/__init__.py b/src/specify_cli/integrations/auggie/__init__.py new file mode 100644 index 000000000..9715e936e --- /dev/null +++ b/src/specify_cli/integrations/auggie/__init__.py @@ -0,0 +1,21 @@ +"""Auggie CLI integration.""" + +from ..base import MarkdownIntegration + + +class AuggieIntegration(MarkdownIntegration): + key = "auggie" + config = { + "name": "Auggie CLI", + "folder": ".augment/", + "commands_subdir": "commands", + "install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli", + "requires_cli": True, + } + registrar_config = { + "dir": ".augment/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = ".augment/rules/specify-rules.md" diff --git a/src/specify_cli/integrations/auggie/scripts/update-context.ps1 b/src/specify_cli/integrations/auggie/scripts/update-context.ps1 new file mode 100644 index 000000000..49e7e6b5f --- /dev/null +++ b/src/specify_cli/integrations/auggie/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Auggie CLI integration: create/update .augment/rules/specify-rules.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType auggie diff --git a/src/specify_cli/integrations/auggie/scripts/update-context.sh b/src/specify_cli/integrations/auggie/scripts/update-context.sh new file mode 100755 index 000000000..4cf80bba2 --- /dev/null +++ b/src/specify_cli/integrations/auggie/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Auggie CLI integration: create/update .augment/rules/specify-rules.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" auggie diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 012b45c4c..0320d7f7a 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -206,6 +206,53 @@ class IntegrationBase(ABC): manifest.record_existing(rel) return dest + def integration_scripts_dir(self) -> Path | None: + """Return path to this integration's bundled ``scripts/`` directory. + + Looks for a ``scripts/`` sibling of the module that defines the + concrete subclass (not ``IntegrationBase`` itself). + Returns ``None`` if the directory doesn't exist. + """ + import inspect + + cls_file = inspect.getfile(type(self)) + scripts = Path(cls_file).resolve().parent / "scripts" + return scripts if scripts.is_dir() else None + + def install_scripts( + self, + project_root: Path, + manifest: IntegrationManifest, + ) -> list[Path]: + """Copy integration-specific scripts into the project. + + Copies files from this integration's ``scripts/`` directory to + ``.specify/integrations//scripts/`` in the project. Shell + scripts are made executable. All copied files are recorded in + *manifest*. + + Returns the list of files created. + """ + scripts_src = self.integration_scripts_dir() + if not scripts_src: + return [] + + created: list[Path] = [] + scripts_dest = project_root / ".specify" / "integrations" / self.key / "scripts" + scripts_dest.mkdir(parents=True, exist_ok=True) + + for src_script in sorted(scripts_src.iterdir()): + if not src_script.is_file(): + continue + dst_script = scripts_dest / src_script.name + shutil.copy2(src_script, dst_script) + if dst_script.suffix == ".sh": + dst_script.chmod(dst_script.stat().st_mode | 0o111) + self.record_file_in_manifest(dst_script, project_root, manifest) + created.append(dst_script) + + return created + @staticmethod def process_template( content: str, @@ -299,13 +346,11 @@ class IntegrationBase(ABC): # 6. Replace __AGENT__ content = content.replace("__AGENT__", agent_name) - # 7. Rewrite paths (matches release script's rewrite_paths()) - content = re.sub(r"(/?)memory/", r".specify/memory/", content) - content = re.sub(r"(/?)scripts/", r".specify/scripts/", content) - content = re.sub(r"(/?)templates/", r".specify/templates/", content) - # Fix double-prefix (same as release script's .specify.specify/ fix) - content = content.replace(".specify.specify/", ".specify/") - content = content.replace(".specify/.specify/", ".specify/") + # 7. Rewrite paths — delegate to the shared implementation in + # CommandRegistrar so extension-local paths are preserved and + # boundary rules stay consistent across the codebase. + from specify_cli.agents import CommandRegistrar + content = CommandRegistrar.rewrite_project_relative_paths(content) return content @@ -405,11 +450,51 @@ class MarkdownIntegration(IntegrationBase): Subclasses only need to set ``key``, ``config``, ``registrar_config`` (and optionally ``context_file``). Everything else is inherited. - The default ``setup()`` from ``IntegrationBase`` copies templates - into the agent's commands directory — which is correct for the - standard Markdown case. + ``setup()`` processes command templates (replacing ``{SCRIPT}``, + ``{ARGS}``, ``__AGENT__``, rewriting paths) and installs + integration-specific scripts (``update-context.sh`` / ``.ps1``). """ - # MarkdownIntegration inherits IntegrationBase.setup() as-is. - # Future stages may add markdown-specific path rewriting here. - pass + def setup( + self, + project_root: Path, + manifest: IntegrationManifest, + parsed_options: dict[str, Any] | None = None, + **opts: Any, + ) -> list[Path]: + templates = self.list_command_templates() + if not templates: + return [] + + project_root_resolved = project_root.resolve() + if manifest.project_root != project_root_resolved: + raise ValueError( + f"manifest.project_root ({manifest.project_root}) does not match " + f"project_root ({project_root_resolved})" + ) + + dest = self.commands_dest(project_root).resolve() + try: + dest.relative_to(project_root_resolved) + except ValueError as exc: + raise ValueError( + f"Integration destination {dest} escapes " + f"project root {project_root_resolved}" + ) from exc + dest.mkdir(parents=True, exist_ok=True) + + script_type = opts.get("script_type", "sh") + arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS") if self.registrar_config else "$ARGUMENTS" + created: list[Path] = [] + + for src_file in templates: + raw = src_file.read_text(encoding="utf-8") + processed = self.process_template(raw, self.key, script_type, arg_placeholder) + dst_name = self.command_filename(src_file.stem) + dst_file = self.write_file_and_record( + processed, dest / dst_name, project_root, manifest + ) + created.append(dst_file) + + created.extend(self.install_scripts(project_root, manifest)) + return created diff --git a/src/specify_cli/integrations/bob/__init__.py b/src/specify_cli/integrations/bob/__init__.py new file mode 100644 index 000000000..78f2df037 --- /dev/null +++ b/src/specify_cli/integrations/bob/__init__.py @@ -0,0 +1,21 @@ +"""IBM Bob integration.""" + +from ..base import MarkdownIntegration + + +class BobIntegration(MarkdownIntegration): + key = "bob" + config = { + "name": "IBM Bob", + "folder": ".bob/", + "commands_subdir": "commands", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".bob/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "AGENTS.md" diff --git a/src/specify_cli/integrations/bob/scripts/update-context.ps1 b/src/specify_cli/integrations/bob/scripts/update-context.ps1 new file mode 100644 index 000000000..188860899 --- /dev/null +++ b/src/specify_cli/integrations/bob/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — IBM Bob integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType bob diff --git a/src/specify_cli/integrations/bob/scripts/update-context.sh b/src/specify_cli/integrations/bob/scripts/update-context.sh new file mode 100755 index 000000000..0228603fe --- /dev/null +++ b/src/specify_cli/integrations/bob/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — IBM Bob integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" bob diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py new file mode 100644 index 000000000..00375ead5 --- /dev/null +++ b/src/specify_cli/integrations/claude/__init__.py @@ -0,0 +1,21 @@ +"""Claude Code integration.""" + +from ..base import MarkdownIntegration + + +class ClaudeIntegration(MarkdownIntegration): + key = "claude" + config = { + "name": "Claude Code", + "folder": ".claude/", + "commands_subdir": "commands", + "install_url": "https://docs.anthropic.com/en/docs/claude-code/setup", + "requires_cli": True, + } + registrar_config = { + "dir": ".claude/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "CLAUDE.md" diff --git a/src/specify_cli/integrations/claude/scripts/update-context.ps1 b/src/specify_cli/integrations/claude/scripts/update-context.ps1 new file mode 100644 index 000000000..837974d47 --- /dev/null +++ b/src/specify_cli/integrations/claude/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Claude Code integration: create/update CLAUDE.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType claude diff --git a/src/specify_cli/integrations/claude/scripts/update-context.sh b/src/specify_cli/integrations/claude/scripts/update-context.sh new file mode 100755 index 000000000..4b83855a2 --- /dev/null +++ b/src/specify_cli/integrations/claude/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Claude Code integration: create/update CLAUDE.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" claude diff --git a/src/specify_cli/integrations/codebuddy/__init__.py b/src/specify_cli/integrations/codebuddy/__init__.py new file mode 100644 index 000000000..061ac7641 --- /dev/null +++ b/src/specify_cli/integrations/codebuddy/__init__.py @@ -0,0 +1,21 @@ +"""CodeBuddy CLI integration.""" + +from ..base import MarkdownIntegration + + +class CodebuddyIntegration(MarkdownIntegration): + key = "codebuddy" + config = { + "name": "CodeBuddy", + "folder": ".codebuddy/", + "commands_subdir": "commands", + "install_url": "https://www.codebuddy.ai/cli", + "requires_cli": True, + } + registrar_config = { + "dir": ".codebuddy/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "CODEBUDDY.md" diff --git a/src/specify_cli/integrations/codebuddy/scripts/update-context.ps1 b/src/specify_cli/integrations/codebuddy/scripts/update-context.ps1 new file mode 100644 index 000000000..0269392c0 --- /dev/null +++ b/src/specify_cli/integrations/codebuddy/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — CodeBuddy integration: create/update CODEBUDDY.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codebuddy diff --git a/src/specify_cli/integrations/codebuddy/scripts/update-context.sh b/src/specify_cli/integrations/codebuddy/scripts/update-context.sh new file mode 100755 index 000000000..d57ddc356 --- /dev/null +++ b/src/specify_cli/integrations/codebuddy/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — CodeBuddy integration: create/update CODEBUDDY.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codebuddy diff --git a/src/specify_cli/integrations/copilot/__init__.py b/src/specify_cli/integrations/copilot/__init__.py index 0c5354d53..036f2e1db 100644 --- a/src/specify_cli/integrations/copilot/__init__.py +++ b/src/specify_cli/integrations/copilot/__init__.py @@ -118,19 +118,7 @@ class CopilotIntegration(IntegrationBase): created.append(dst_settings) # 4. Install integration-specific update-context scripts - scripts_src = Path(__file__).resolve().parent / "scripts" - if scripts_src.is_dir(): - scripts_dest = project_root / ".specify" / "integrations" / "copilot" / "scripts" - scripts_dest.mkdir(parents=True, exist_ok=True) - for src_script in sorted(scripts_src.iterdir()): - if src_script.is_file(): - dst_script = scripts_dest / src_script.name - shutil.copy2(src_script, dst_script) - # Make shell scripts executable - if dst_script.suffix == ".sh": - dst_script.chmod(dst_script.stat().st_mode | 0o111) - self.record_file_in_manifest(dst_script, project_root, manifest) - created.append(dst_script) + created.extend(self.install_scripts(project_root, manifest)) return created diff --git a/src/specify_cli/integrations/copilot/scripts/update-context.ps1 b/src/specify_cli/integrations/copilot/scripts/update-context.ps1 index c6f9845a3..26e746a78 100644 --- a/src/specify_cli/integrations/copilot/scripts/update-context.ps1 +++ b/src/specify_cli/integrations/copilot/scripts/update-context.ps1 @@ -14,8 +14,18 @@ $ErrorActionPreference = 'Stop' -$repoRoot = git rev-parse --show-toplevel 2>$null -if (-not $repoRoot) { $repoRoot = $PWD.Path } +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} # Invoke shared update-agent-context script as a separate process. # Dot-sourcing is unsafe until that script guards its Main call. diff --git a/src/specify_cli/integrations/copilot/scripts/update-context.sh b/src/specify_cli/integrations/copilot/scripts/update-context.sh index 84c86422e..c7f3bc60b 100644 --- a/src/specify_cli/integrations/copilot/scripts/update-context.sh +++ b/src/specify_cli/integrations/copilot/scripts/update-context.sh @@ -15,7 +15,22 @@ set -euo pipefail -REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi # Invoke shared update-agent-context script as a separate process. # Sourcing is unsafe until that script guards its main logic. diff --git a/src/specify_cli/integrations/cursor_agent/__init__.py b/src/specify_cli/integrations/cursor_agent/__init__.py new file mode 100644 index 000000000..c244a7c01 --- /dev/null +++ b/src/specify_cli/integrations/cursor_agent/__init__.py @@ -0,0 +1,21 @@ +"""Cursor IDE integration.""" + +from ..base import MarkdownIntegration + + +class CursorAgentIntegration(MarkdownIntegration): + key = "cursor-agent" + config = { + "name": "Cursor", + "folder": ".cursor/", + "commands_subdir": "commands", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".cursor/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = ".cursor/rules/specify-rules.mdc" diff --git a/src/specify_cli/integrations/cursor_agent/scripts/update-context.ps1 b/src/specify_cli/integrations/cursor_agent/scripts/update-context.ps1 new file mode 100644 index 000000000..4ce50a487 --- /dev/null +++ b/src/specify_cli/integrations/cursor_agent/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Cursor integration: create/update .cursor/rules/specify-rules.mdc +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType cursor-agent diff --git a/src/specify_cli/integrations/cursor_agent/scripts/update-context.sh b/src/specify_cli/integrations/cursor_agent/scripts/update-context.sh new file mode 100755 index 000000000..597ca2289 --- /dev/null +++ b/src/specify_cli/integrations/cursor_agent/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Cursor integration: create/update .cursor/rules/specify-rules.mdc +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" cursor-agent diff --git a/src/specify_cli/integrations/iflow/__init__.py b/src/specify_cli/integrations/iflow/__init__.py new file mode 100644 index 000000000..4acc2cf37 --- /dev/null +++ b/src/specify_cli/integrations/iflow/__init__.py @@ -0,0 +1,21 @@ +"""iFlow CLI integration.""" + +from ..base import MarkdownIntegration + + +class IflowIntegration(MarkdownIntegration): + key = "iflow" + config = { + "name": "iFlow CLI", + "folder": ".iflow/", + "commands_subdir": "commands", + "install_url": "https://docs.iflow.cn/en/cli/quickstart", + "requires_cli": True, + } + registrar_config = { + "dir": ".iflow/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "IFLOW.md" diff --git a/src/specify_cli/integrations/iflow/scripts/update-context.ps1 b/src/specify_cli/integrations/iflow/scripts/update-context.ps1 new file mode 100644 index 000000000..b502d4182 --- /dev/null +++ b/src/specify_cli/integrations/iflow/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — iFlow CLI integration: create/update IFLOW.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType iflow diff --git a/src/specify_cli/integrations/iflow/scripts/update-context.sh b/src/specify_cli/integrations/iflow/scripts/update-context.sh new file mode 100755 index 000000000..508040207 --- /dev/null +++ b/src/specify_cli/integrations/iflow/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — iFlow CLI integration: create/update IFLOW.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" iflow diff --git a/src/specify_cli/integrations/junie/__init__.py b/src/specify_cli/integrations/junie/__init__.py new file mode 100644 index 000000000..0cc3b3f0f --- /dev/null +++ b/src/specify_cli/integrations/junie/__init__.py @@ -0,0 +1,21 @@ +"""Junie integration (JetBrains).""" + +from ..base import MarkdownIntegration + + +class JunieIntegration(MarkdownIntegration): + key = "junie" + config = { + "name": "Junie", + "folder": ".junie/", + "commands_subdir": "commands", + "install_url": "https://junie.jetbrains.com/", + "requires_cli": True, + } + registrar_config = { + "dir": ".junie/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = ".junie/AGENTS.md" diff --git a/src/specify_cli/integrations/junie/scripts/update-context.ps1 b/src/specify_cli/integrations/junie/scripts/update-context.ps1 new file mode 100644 index 000000000..5a3243213 --- /dev/null +++ b/src/specify_cli/integrations/junie/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Junie integration: create/update .junie/AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType junie diff --git a/src/specify_cli/integrations/junie/scripts/update-context.sh b/src/specify_cli/integrations/junie/scripts/update-context.sh new file mode 100755 index 000000000..f4c8ba6c0 --- /dev/null +++ b/src/specify_cli/integrations/junie/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Junie integration: create/update .junie/AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" junie diff --git a/src/specify_cli/integrations/kilocode/__init__.py b/src/specify_cli/integrations/kilocode/__init__.py new file mode 100644 index 000000000..ffd38f741 --- /dev/null +++ b/src/specify_cli/integrations/kilocode/__init__.py @@ -0,0 +1,21 @@ +"""Kilo Code integration.""" + +from ..base import MarkdownIntegration + + +class KilocodeIntegration(MarkdownIntegration): + key = "kilocode" + config = { + "name": "Kilo Code", + "folder": ".kilocode/", + "commands_subdir": "workflows", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".kilocode/workflows", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = ".kilocode/rules/specify-rules.md" diff --git a/src/specify_cli/integrations/kilocode/scripts/update-context.ps1 b/src/specify_cli/integrations/kilocode/scripts/update-context.ps1 new file mode 100644 index 000000000..d87e7ef59 --- /dev/null +++ b/src/specify_cli/integrations/kilocode/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Kilo Code integration: create/update .kilocode/rules/specify-rules.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kilocode diff --git a/src/specify_cli/integrations/kilocode/scripts/update-context.sh b/src/specify_cli/integrations/kilocode/scripts/update-context.sh new file mode 100755 index 000000000..132c0403f --- /dev/null +++ b/src/specify_cli/integrations/kilocode/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Kilo Code integration: create/update .kilocode/rules/specify-rules.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kilocode diff --git a/src/specify_cli/integrations/kiro_cli/__init__.py b/src/specify_cli/integrations/kiro_cli/__init__.py new file mode 100644 index 000000000..b316cb4bd --- /dev/null +++ b/src/specify_cli/integrations/kiro_cli/__init__.py @@ -0,0 +1,21 @@ +"""Kiro CLI integration.""" + +from ..base import MarkdownIntegration + + +class KiroCliIntegration(MarkdownIntegration): + key = "kiro-cli" + config = { + "name": "Kiro CLI", + "folder": ".kiro/", + "commands_subdir": "prompts", + "install_url": "https://kiro.dev/docs/cli/", + "requires_cli": True, + } + registrar_config = { + "dir": ".kiro/prompts", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "AGENTS.md" diff --git a/src/specify_cli/integrations/kiro_cli/scripts/update-context.ps1 b/src/specify_cli/integrations/kiro_cli/scripts/update-context.ps1 new file mode 100644 index 000000000..7dd2b35fb --- /dev/null +++ b/src/specify_cli/integrations/kiro_cli/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Kiro CLI integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType kiro-cli diff --git a/src/specify_cli/integrations/kiro_cli/scripts/update-context.sh b/src/specify_cli/integrations/kiro_cli/scripts/update-context.sh new file mode 100755 index 000000000..fa258edc7 --- /dev/null +++ b/src/specify_cli/integrations/kiro_cli/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Kiro CLI integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" kiro-cli diff --git a/src/specify_cli/integrations/opencode/__init__.py b/src/specify_cli/integrations/opencode/__init__.py new file mode 100644 index 000000000..be4dcc309 --- /dev/null +++ b/src/specify_cli/integrations/opencode/__init__.py @@ -0,0 +1,21 @@ +"""opencode integration.""" + +from ..base import MarkdownIntegration + + +class OpencodeIntegration(MarkdownIntegration): + key = "opencode" + config = { + "name": "opencode", + "folder": ".opencode/", + "commands_subdir": "command", + "install_url": "https://opencode.ai", + "requires_cli": True, + } + registrar_config = { + "dir": ".opencode/command", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "AGENTS.md" diff --git a/src/specify_cli/integrations/opencode/scripts/update-context.ps1 b/src/specify_cli/integrations/opencode/scripts/update-context.ps1 new file mode 100644 index 000000000..4bba02b45 --- /dev/null +++ b/src/specify_cli/integrations/opencode/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — opencode integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType opencode diff --git a/src/specify_cli/integrations/opencode/scripts/update-context.sh b/src/specify_cli/integrations/opencode/scripts/update-context.sh new file mode 100755 index 000000000..24c7e6025 --- /dev/null +++ b/src/specify_cli/integrations/opencode/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — opencode integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" opencode diff --git a/src/specify_cli/integrations/pi/__init__.py b/src/specify_cli/integrations/pi/__init__.py new file mode 100644 index 000000000..8a25f326b --- /dev/null +++ b/src/specify_cli/integrations/pi/__init__.py @@ -0,0 +1,21 @@ +"""Pi Coding Agent integration.""" + +from ..base import MarkdownIntegration + + +class PiIntegration(MarkdownIntegration): + key = "pi" + config = { + "name": "Pi Coding Agent", + "folder": ".pi/", + "commands_subdir": "prompts", + "install_url": "https://www.npmjs.com/package/@mariozechner/pi-coding-agent", + "requires_cli": True, + } + registrar_config = { + "dir": ".pi/prompts", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "AGENTS.md" diff --git a/src/specify_cli/integrations/pi/scripts/update-context.ps1 b/src/specify_cli/integrations/pi/scripts/update-context.ps1 new file mode 100644 index 000000000..6362118a5 --- /dev/null +++ b/src/specify_cli/integrations/pi/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Pi Coding Agent integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType pi diff --git a/src/specify_cli/integrations/pi/scripts/update-context.sh b/src/specify_cli/integrations/pi/scripts/update-context.sh new file mode 100755 index 000000000..1ad84c95a --- /dev/null +++ b/src/specify_cli/integrations/pi/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Pi Coding Agent integration: create/update AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" pi diff --git a/src/specify_cli/integrations/qodercli/__init__.py b/src/specify_cli/integrations/qodercli/__init__.py new file mode 100644 index 000000000..541001be1 --- /dev/null +++ b/src/specify_cli/integrations/qodercli/__init__.py @@ -0,0 +1,21 @@ +"""Qoder CLI integration.""" + +from ..base import MarkdownIntegration + + +class QodercliIntegration(MarkdownIntegration): + key = "qodercli" + config = { + "name": "Qoder CLI", + "folder": ".qoder/", + "commands_subdir": "commands", + "install_url": "https://qoder.com/cli", + "requires_cli": True, + } + registrar_config = { + "dir": ".qoder/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "QODER.md" diff --git a/src/specify_cli/integrations/qodercli/scripts/update-context.ps1 b/src/specify_cli/integrations/qodercli/scripts/update-context.ps1 new file mode 100644 index 000000000..1fa007a16 --- /dev/null +++ b/src/specify_cli/integrations/qodercli/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Qoder CLI integration: create/update QODER.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType qodercli diff --git a/src/specify_cli/integrations/qodercli/scripts/update-context.sh b/src/specify_cli/integrations/qodercli/scripts/update-context.sh new file mode 100755 index 000000000..d371ad795 --- /dev/null +++ b/src/specify_cli/integrations/qodercli/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Qoder CLI integration: create/update QODER.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" qodercli diff --git a/src/specify_cli/integrations/qwen/__init__.py b/src/specify_cli/integrations/qwen/__init__.py new file mode 100644 index 000000000..d9d930152 --- /dev/null +++ b/src/specify_cli/integrations/qwen/__init__.py @@ -0,0 +1,21 @@ +"""Qwen Code integration.""" + +from ..base import MarkdownIntegration + + +class QwenIntegration(MarkdownIntegration): + key = "qwen" + config = { + "name": "Qwen Code", + "folder": ".qwen/", + "commands_subdir": "commands", + "install_url": "https://github.com/QwenLM/qwen-code", + "requires_cli": True, + } + registrar_config = { + "dir": ".qwen/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "QWEN.md" diff --git a/src/specify_cli/integrations/qwen/scripts/update-context.ps1 b/src/specify_cli/integrations/qwen/scripts/update-context.ps1 new file mode 100644 index 000000000..24e4c90fa --- /dev/null +++ b/src/specify_cli/integrations/qwen/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Qwen Code integration: create/update QWEN.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType qwen diff --git a/src/specify_cli/integrations/qwen/scripts/update-context.sh b/src/specify_cli/integrations/qwen/scripts/update-context.sh new file mode 100755 index 000000000..d1c62eb16 --- /dev/null +++ b/src/specify_cli/integrations/qwen/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Qwen Code integration: create/update QWEN.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" qwen diff --git a/src/specify_cli/integrations/roo/__init__.py b/src/specify_cli/integrations/roo/__init__.py new file mode 100644 index 000000000..3c680e7e3 --- /dev/null +++ b/src/specify_cli/integrations/roo/__init__.py @@ -0,0 +1,21 @@ +"""Roo Code integration.""" + +from ..base import MarkdownIntegration + + +class RooIntegration(MarkdownIntegration): + key = "roo" + config = { + "name": "Roo Code", + "folder": ".roo/", + "commands_subdir": "commands", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".roo/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = ".roo/rules/specify-rules.md" diff --git a/src/specify_cli/integrations/roo/scripts/update-context.ps1 b/src/specify_cli/integrations/roo/scripts/update-context.ps1 new file mode 100644 index 000000000..d1dec923e --- /dev/null +++ b/src/specify_cli/integrations/roo/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Roo Code integration: create/update .roo/rules/specify-rules.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType roo diff --git a/src/specify_cli/integrations/roo/scripts/update-context.sh b/src/specify_cli/integrations/roo/scripts/update-context.sh new file mode 100755 index 000000000..8fe255cb1 --- /dev/null +++ b/src/specify_cli/integrations/roo/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Roo Code integration: create/update .roo/rules/specify-rules.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" roo diff --git a/src/specify_cli/integrations/shai/__init__.py b/src/specify_cli/integrations/shai/__init__.py new file mode 100644 index 000000000..7a9d1deb0 --- /dev/null +++ b/src/specify_cli/integrations/shai/__init__.py @@ -0,0 +1,21 @@ +"""SHAI CLI integration.""" + +from ..base import MarkdownIntegration + + +class ShaiIntegration(MarkdownIntegration): + key = "shai" + config = { + "name": "SHAI", + "folder": ".shai/", + "commands_subdir": "commands", + "install_url": "https://github.com/ovh/shai", + "requires_cli": True, + } + registrar_config = { + "dir": ".shai/commands", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = "SHAI.md" diff --git a/src/specify_cli/integrations/shai/scripts/update-context.ps1 b/src/specify_cli/integrations/shai/scripts/update-context.ps1 new file mode 100644 index 000000000..2c621c76a --- /dev/null +++ b/src/specify_cli/integrations/shai/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — SHAI integration: create/update SHAI.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType shai diff --git a/src/specify_cli/integrations/shai/scripts/update-context.sh b/src/specify_cli/integrations/shai/scripts/update-context.sh new file mode 100755 index 000000000..093b9d1f7 --- /dev/null +++ b/src/specify_cli/integrations/shai/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — SHAI integration: create/update SHAI.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" shai diff --git a/src/specify_cli/integrations/trae/__init__.py b/src/specify_cli/integrations/trae/__init__.py new file mode 100644 index 000000000..7037eecb8 --- /dev/null +++ b/src/specify_cli/integrations/trae/__init__.py @@ -0,0 +1,21 @@ +"""Trae IDE integration.""" + +from ..base import MarkdownIntegration + + +class TraeIntegration(MarkdownIntegration): + key = "trae" + config = { + "name": "Trae", + "folder": ".trae/", + "commands_subdir": "rules", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".trae/rules", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = ".trae/rules/AGENTS.md" diff --git a/src/specify_cli/integrations/trae/scripts/update-context.ps1 b/src/specify_cli/integrations/trae/scripts/update-context.ps1 new file mode 100644 index 000000000..f72d96318 --- /dev/null +++ b/src/specify_cli/integrations/trae/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Trae integration: create/update .trae/rules/AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType trae diff --git a/src/specify_cli/integrations/trae/scripts/update-context.sh b/src/specify_cli/integrations/trae/scripts/update-context.sh new file mode 100755 index 000000000..b868a7c98 --- /dev/null +++ b/src/specify_cli/integrations/trae/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Trae integration: create/update .trae/rules/AGENTS.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" trae diff --git a/src/specify_cli/integrations/vibe/__init__.py b/src/specify_cli/integrations/vibe/__init__.py new file mode 100644 index 000000000..dcc4a60dd --- /dev/null +++ b/src/specify_cli/integrations/vibe/__init__.py @@ -0,0 +1,21 @@ +"""Mistral Vibe CLI integration.""" + +from ..base import MarkdownIntegration + + +class VibeIntegration(MarkdownIntegration): + key = "vibe" + config = { + "name": "Mistral Vibe", + "folder": ".vibe/", + "commands_subdir": "prompts", + "install_url": "https://github.com/mistralai/mistral-vibe", + "requires_cli": True, + } + registrar_config = { + "dir": ".vibe/prompts", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = ".vibe/agents/specify-agents.md" diff --git a/src/specify_cli/integrations/vibe/scripts/update-context.ps1 b/src/specify_cli/integrations/vibe/scripts/update-context.ps1 new file mode 100644 index 000000000..d82ce3389 --- /dev/null +++ b/src/specify_cli/integrations/vibe/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Mistral Vibe integration: create/update .vibe/agents/specify-agents.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType vibe diff --git a/src/specify_cli/integrations/vibe/scripts/update-context.sh b/src/specify_cli/integrations/vibe/scripts/update-context.sh new file mode 100755 index 000000000..f924cdb89 --- /dev/null +++ b/src/specify_cli/integrations/vibe/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Mistral Vibe integration: create/update .vibe/agents/specify-agents.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" vibe diff --git a/src/specify_cli/integrations/windsurf/__init__.py b/src/specify_cli/integrations/windsurf/__init__.py new file mode 100644 index 000000000..f0f77d318 --- /dev/null +++ b/src/specify_cli/integrations/windsurf/__init__.py @@ -0,0 +1,21 @@ +"""Windsurf IDE integration.""" + +from ..base import MarkdownIntegration + + +class WindsurfIntegration(MarkdownIntegration): + key = "windsurf" + config = { + "name": "Windsurf", + "folder": ".windsurf/", + "commands_subdir": "workflows", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".windsurf/workflows", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": ".md", + } + context_file = ".windsurf/rules/specify-rules.md" diff --git a/src/specify_cli/integrations/windsurf/scripts/update-context.ps1 b/src/specify_cli/integrations/windsurf/scripts/update-context.ps1 new file mode 100644 index 000000000..b5fe1d0c0 --- /dev/null +++ b/src/specify_cli/integrations/windsurf/scripts/update-context.ps1 @@ -0,0 +1,23 @@ +# update-context.ps1 — Windsurf integration: create/update .windsurf/rules/specify-rules.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +$ErrorActionPreference = 'Stop' + +# Derive repo root from script location (walks up to find .specify/) +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null } +# If git did not return a repo root, or the git root does not contain .specify, +# fall back to walking up from the script directory to find the initialized project root. +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = $scriptDir + $fsRoot = [System.IO.Path]::GetPathRoot($repoRoot) + while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) { + $repoRoot = Split-Path -Parent $repoRoot + } +} + +& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType windsurf diff --git a/src/specify_cli/integrations/windsurf/scripts/update-context.sh b/src/specify_cli/integrations/windsurf/scripts/update-context.sh new file mode 100755 index 000000000..b9a78d320 --- /dev/null +++ b/src/specify_cli/integrations/windsurf/scripts/update-context.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# update-context.sh — Windsurf integration: create/update .windsurf/rules/specify-rules.md +# +# Thin wrapper that delegates to the shared update-agent-context script. +# Activated in Stage 7 when the shared script uses integration.json dispatch. +# +# Until then, this delegates to the shared script as a subprocess. + +set -euo pipefail + +# Derive repo root from script location (walks up to find .specify/) +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_root="$_script_dir" +while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done +if [ -z "${REPO_ROOT:-}" ]; then + if [ -d "$_root/.specify" ]; then + REPO_ROOT="$_root" + else + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then + REPO_ROOT="$git_root" + else + REPO_ROOT="$_root" + fi + fi +fi + +exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" windsurf diff --git a/tests/integrations/test_integration_amp.py b/tests/integrations/test_integration_amp.py new file mode 100644 index 000000000..a36dd4713 --- /dev/null +++ b/tests/integrations/test_integration_amp.py @@ -0,0 +1,11 @@ +"""Tests for AmpIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestAmpIntegration(MarkdownIntegrationTests): + KEY = "amp" + FOLDER = ".agents/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".agents/commands" + CONTEXT_FILE = "AGENTS.md" diff --git a/tests/integrations/test_integration_auggie.py b/tests/integrations/test_integration_auggie.py new file mode 100644 index 000000000..e4033a23e --- /dev/null +++ b/tests/integrations/test_integration_auggie.py @@ -0,0 +1,11 @@ +"""Tests for AuggieIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestAuggieIntegration(MarkdownIntegrationTests): + KEY = "auggie" + FOLDER = ".augment/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".augment/commands" + CONTEXT_FILE = ".augment/rules/specify-rules.md" diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py new file mode 100644 index 000000000..75319eb94 --- /dev/null +++ b/tests/integrations/test_integration_base_markdown.py @@ -0,0 +1,296 @@ +"""Reusable test mixin for standard MarkdownIntegration subclasses. + +Each per-agent test file sets ``KEY``, ``FOLDER``, ``COMMANDS_SUBDIR``, +``REGISTRAR_DIR``, and ``CONTEXT_FILE``, then inherits all verification +logic from ``MarkdownIntegrationTests``. +""" + +import os + +from specify_cli.integrations import INTEGRATION_REGISTRY, get_integration +from specify_cli.integrations.base import MarkdownIntegration +from specify_cli.integrations.manifest import IntegrationManifest + + +class MarkdownIntegrationTests: + """Mixin — set class-level constants and inherit these tests. + + Required class attrs on subclass:: + + KEY: str — integration registry key + FOLDER: str — e.g. ".claude/" + COMMANDS_SUBDIR: str — e.g. "commands" + REGISTRAR_DIR: str — e.g. ".claude/commands" + CONTEXT_FILE: str — e.g. "CLAUDE.md" + """ + + KEY: str + FOLDER: str + COMMANDS_SUBDIR: str + REGISTRAR_DIR: str + CONTEXT_FILE: str + + # -- Registration ----------------------------------------------------- + + def test_registered(self): + assert self.KEY in INTEGRATION_REGISTRY + assert get_integration(self.KEY) is not None + + def test_is_markdown_integration(self): + assert isinstance(get_integration(self.KEY), MarkdownIntegration) + + # -- Config ----------------------------------------------------------- + + def test_config_folder(self): + i = get_integration(self.KEY) + assert i.config["folder"] == self.FOLDER + + def test_config_commands_subdir(self): + i = get_integration(self.KEY) + assert i.config["commands_subdir"] == self.COMMANDS_SUBDIR + + def test_registrar_config(self): + i = get_integration(self.KEY) + assert i.registrar_config["dir"] == self.REGISTRAR_DIR + assert i.registrar_config["format"] == "markdown" + assert i.registrar_config["args"] == "$ARGUMENTS" + assert i.registrar_config["extension"] == ".md" + + def test_context_file(self): + i = get_integration(self.KEY) + assert i.context_file == self.CONTEXT_FILE + + # -- Setup / teardown ------------------------------------------------- + + def test_setup_creates_files(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + created = i.setup(tmp_path, m) + assert len(created) > 0 + cmd_files = [f for f in created if "scripts" not in f.parts] + for f in cmd_files: + assert f.exists() + assert f.name.startswith("speckit.") + assert f.name.endswith(".md") + + def test_setup_writes_to_correct_directory(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + created = i.setup(tmp_path, m) + expected_dir = i.commands_dest(tmp_path) + assert expected_dir.exists(), f"Expected directory {expected_dir} was not created" + cmd_files = [f for f in created if "scripts" not in f.parts] + assert len(cmd_files) > 0, "No command files were created" + for f in cmd_files: + assert f.resolve().parent == expected_dir.resolve(), ( + f"{f} is not under {expected_dir}" + ) + + def test_templates_are_processed(self, tmp_path): + """Command files must have placeholders replaced, not raw templates.""" + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + created = i.setup(tmp_path, m) + cmd_files = [f for f in created if "scripts" not in f.parts] + assert len(cmd_files) > 0 + for f in cmd_files: + content = f.read_text(encoding="utf-8") + assert "{SCRIPT}" not in content, f"{f.name} has unprocessed {{SCRIPT}}" + assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__" + assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}" + assert "\nscripts:\n" not in content, f"{f.name} has unstripped scripts: block" + assert "\nagent_scripts:\n" not in content, f"{f.name} has unstripped agent_scripts: block" + + def test_all_files_tracked_in_manifest(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + created = i.setup(tmp_path, m) + for f in created: + rel = f.resolve().relative_to(tmp_path.resolve()).as_posix() + assert rel in m.files, f"{rel} not tracked in manifest" + + def test_install_uninstall_roundtrip(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + created = i.install(tmp_path, m) + assert len(created) > 0 + m.save() + for f in created: + assert f.exists() + removed, skipped = i.uninstall(tmp_path, m) + assert len(removed) == len(created) + assert skipped == [] + + def test_modified_file_survives_uninstall(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + created = i.install(tmp_path, m) + m.save() + modified_file = created[0] + modified_file.write_text("user modified this", encoding="utf-8") + removed, skipped = i.uninstall(tmp_path, m) + assert modified_file.exists() + assert modified_file in skipped + + # -- Scripts ---------------------------------------------------------- + + def test_setup_installs_update_context_scripts(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + created = i.setup(tmp_path, m) + scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" + assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" + assert (scripts_dir / "update-context.sh").exists() + assert (scripts_dir / "update-context.ps1").exists() + + def test_scripts_tracked_in_manifest(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m) + script_rels = [k for k in m.files if "update-context" in k] + assert len(script_rels) >= 2 + + def test_sh_script_is_executable(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m) + sh = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" / "update-context.sh" + assert os.access(sh, os.X_OK) + + # -- CLI auto-promote ------------------------------------------------- + + def test_ai_flag_auto_promotes(self, tmp_path): + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / f"promote-{self.KEY}" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + runner = CliRunner() + result = runner.invoke(app, [ + "init", "--here", "--ai", self.KEY, "--script", "sh", "--no-git", + "--ignore-agent-tools", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0, f"init --ai {self.KEY} failed: {result.output}" + assert f"--integration {self.KEY}" in result.output + + def test_integration_flag_creates_files(self, tmp_path): + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / f"int-{self.KEY}" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + runner = CliRunner() + result = runner.invoke(app, [ + "init", "--here", "--integration", self.KEY, "--script", "sh", "--no-git", + "--ignore-agent-tools", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0, f"init --integration {self.KEY} failed: {result.output}" + i = get_integration(self.KEY) + cmd_dir = i.commands_dest(project) + assert cmd_dir.is_dir(), f"Commands directory {cmd_dir} not created" + commands = sorted(cmd_dir.glob("speckit.*")) + assert len(commands) > 0, f"No command files in {cmd_dir}" + + # -- Complete file inventory ------------------------------------------ + + COMMAND_STEMS = [ + "analyze", "checklist", "clarify", "constitution", + "implement", "plan", "specify", "tasks", "taskstoissues", + ] + + def _expected_files(self, script_variant: str) -> list[str]: + """Build the expected file list for this integration + script variant.""" + i = get_integration(self.KEY) + cmd_dir = i.registrar_config["dir"] + files = [] + + # Command files + for stem in self.COMMAND_STEMS: + files.append(f"{cmd_dir}/speckit.{stem}.md") + + # Integration scripts + files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") + files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") + + # Framework files + files.append(f".specify/integration.json") + files.append(f".specify/init-options.json") + files.append(f".specify/integrations/{self.KEY}.manifest.json") + files.append(f".specify/integrations/speckit.manifest.json") + + if script_variant == "sh": + for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.sh", + "setup-plan.sh", "update-agent-context.sh"]: + files.append(f".specify/scripts/bash/{name}") + else: + for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1", + "setup-plan.ps1", "update-agent-context.ps1"]: + files.append(f".specify/scripts/powershell/{name}") + + for name in ["agent-file-template.md", "checklist-template.md", + "constitution-template.md", "plan-template.md", + "spec-template.md", "tasks-template.md"]: + files.append(f".specify/templates/{name}") + + files.append(".specify/memory/constitution.md") + return sorted(files) + + def test_complete_file_inventory_sh(self, tmp_path): + """Every file produced by specify init --integration --script sh.""" + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / f"inventory-sh-{self.KEY}" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + result = CliRunner().invoke(app, [ + "init", "--here", "--integration", self.KEY, "--script", "sh", + "--no-git", "--ignore-agent-tools", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0, f"init failed: {result.output}" + actual = sorted(p.relative_to(project).as_posix() + for p in project.rglob("*") if p.is_file()) + expected = self._expected_files("sh") + assert actual == expected, ( + f"Missing: {sorted(set(expected) - set(actual))}\n" + f"Extra: {sorted(set(actual) - set(expected))}" + ) + + def test_complete_file_inventory_ps(self, tmp_path): + """Every file produced by specify init --integration --script ps.""" + from typer.testing import CliRunner + from specify_cli import app + + project = tmp_path / f"inventory-ps-{self.KEY}" + project.mkdir() + old_cwd = os.getcwd() + try: + os.chdir(project) + result = CliRunner().invoke(app, [ + "init", "--here", "--integration", self.KEY, "--script", "ps", + "--no-git", "--ignore-agent-tools", + ], catch_exceptions=False) + finally: + os.chdir(old_cwd) + assert result.exit_code == 0, f"init failed: {result.output}" + actual = sorted(p.relative_to(project).as_posix() + for p in project.rglob("*") if p.is_file()) + expected = self._expected_files("ps") + assert actual == expected, ( + f"Missing: {sorted(set(expected) - set(actual))}\n" + f"Extra: {sorted(set(actual) - set(expected))}" + ) diff --git a/tests/integrations/test_integration_bob.py b/tests/integrations/test_integration_bob.py new file mode 100644 index 000000000..1562f0100 --- /dev/null +++ b/tests/integrations/test_integration_bob.py @@ -0,0 +1,11 @@ +"""Tests for BobIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestBobIntegration(MarkdownIntegrationTests): + KEY = "bob" + FOLDER = ".bob/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".bob/commands" + CONTEXT_FILE = "AGENTS.md" diff --git a/tests/integrations/test_integration_claude.py b/tests/integrations/test_integration_claude.py new file mode 100644 index 000000000..6867a295e --- /dev/null +++ b/tests/integrations/test_integration_claude.py @@ -0,0 +1,11 @@ +"""Tests for ClaudeIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestClaudeIntegration(MarkdownIntegrationTests): + KEY = "claude" + FOLDER = ".claude/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".claude/commands" + CONTEXT_FILE = "CLAUDE.md" diff --git a/tests/integrations/test_integration_codebuddy.py b/tests/integrations/test_integration_codebuddy.py new file mode 100644 index 000000000..dcc2153a7 --- /dev/null +++ b/tests/integrations/test_integration_codebuddy.py @@ -0,0 +1,11 @@ +"""Tests for CodebuddyIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestCodebuddyIntegration(MarkdownIntegrationTests): + KEY = "codebuddy" + FOLDER = ".codebuddy/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".codebuddy/commands" + CONTEXT_FILE = "CODEBUDDY.md" diff --git a/tests/integrations/test_copilot.py b/tests/integrations/test_integration_copilot.py similarity index 100% rename from tests/integrations/test_copilot.py rename to tests/integrations/test_integration_copilot.py diff --git a/tests/integrations/test_integration_cursor_agent.py b/tests/integrations/test_integration_cursor_agent.py new file mode 100644 index 000000000..71b7db1c9 --- /dev/null +++ b/tests/integrations/test_integration_cursor_agent.py @@ -0,0 +1,11 @@ +"""Tests for CursorAgentIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestCursorAgentIntegration(MarkdownIntegrationTests): + KEY = "cursor-agent" + FOLDER = ".cursor/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".cursor/commands" + CONTEXT_FILE = ".cursor/rules/specify-rules.mdc" diff --git a/tests/integrations/test_integration_iflow.py b/tests/integrations/test_integration_iflow.py new file mode 100644 index 000000000..ea2f5ef97 --- /dev/null +++ b/tests/integrations/test_integration_iflow.py @@ -0,0 +1,11 @@ +"""Tests for IflowIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestIflowIntegration(MarkdownIntegrationTests): + KEY = "iflow" + FOLDER = ".iflow/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".iflow/commands" + CONTEXT_FILE = "IFLOW.md" diff --git a/tests/integrations/test_integration_junie.py b/tests/integrations/test_integration_junie.py new file mode 100644 index 000000000..2b924ce43 --- /dev/null +++ b/tests/integrations/test_integration_junie.py @@ -0,0 +1,11 @@ +"""Tests for JunieIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestJunieIntegration(MarkdownIntegrationTests): + KEY = "junie" + FOLDER = ".junie/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".junie/commands" + CONTEXT_FILE = ".junie/AGENTS.md" diff --git a/tests/integrations/test_integration_kilocode.py b/tests/integrations/test_integration_kilocode.py new file mode 100644 index 000000000..8e441c083 --- /dev/null +++ b/tests/integrations/test_integration_kilocode.py @@ -0,0 +1,11 @@ +"""Tests for KilocodeIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestKilocodeIntegration(MarkdownIntegrationTests): + KEY = "kilocode" + FOLDER = ".kilocode/" + COMMANDS_SUBDIR = "workflows" + REGISTRAR_DIR = ".kilocode/workflows" + CONTEXT_FILE = ".kilocode/rules/specify-rules.md" diff --git a/tests/integrations/test_integration_kiro_cli.py b/tests/integrations/test_integration_kiro_cli.py new file mode 100644 index 000000000..d6ae7afce --- /dev/null +++ b/tests/integrations/test_integration_kiro_cli.py @@ -0,0 +1,11 @@ +"""Tests for KiroCliIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestKiroCliIntegration(MarkdownIntegrationTests): + KEY = "kiro-cli" + FOLDER = ".kiro/" + COMMANDS_SUBDIR = "prompts" + REGISTRAR_DIR = ".kiro/prompts" + CONTEXT_FILE = "AGENTS.md" diff --git a/tests/integrations/test_integration_opencode.py b/tests/integrations/test_integration_opencode.py new file mode 100644 index 000000000..4f3aee5d9 --- /dev/null +++ b/tests/integrations/test_integration_opencode.py @@ -0,0 +1,11 @@ +"""Tests for OpencodeIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestOpencodeIntegration(MarkdownIntegrationTests): + KEY = "opencode" + FOLDER = ".opencode/" + COMMANDS_SUBDIR = "command" + REGISTRAR_DIR = ".opencode/command" + CONTEXT_FILE = "AGENTS.md" diff --git a/tests/integrations/test_integration_pi.py b/tests/integrations/test_integration_pi.py new file mode 100644 index 000000000..5ac567650 --- /dev/null +++ b/tests/integrations/test_integration_pi.py @@ -0,0 +1,11 @@ +"""Tests for PiIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestPiIntegration(MarkdownIntegrationTests): + KEY = "pi" + FOLDER = ".pi/" + COMMANDS_SUBDIR = "prompts" + REGISTRAR_DIR = ".pi/prompts" + CONTEXT_FILE = "AGENTS.md" diff --git a/tests/integrations/test_integration_qodercli.py b/tests/integrations/test_integration_qodercli.py new file mode 100644 index 000000000..1dbee480a --- /dev/null +++ b/tests/integrations/test_integration_qodercli.py @@ -0,0 +1,11 @@ +"""Tests for QodercliIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestQodercliIntegration(MarkdownIntegrationTests): + KEY = "qodercli" + FOLDER = ".qoder/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".qoder/commands" + CONTEXT_FILE = "QODER.md" diff --git a/tests/integrations/test_integration_qwen.py b/tests/integrations/test_integration_qwen.py new file mode 100644 index 000000000..10a3c083f --- /dev/null +++ b/tests/integrations/test_integration_qwen.py @@ -0,0 +1,11 @@ +"""Tests for QwenIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestQwenIntegration(MarkdownIntegrationTests): + KEY = "qwen" + FOLDER = ".qwen/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".qwen/commands" + CONTEXT_FILE = "QWEN.md" diff --git a/tests/integrations/test_integration_roo.py b/tests/integrations/test_integration_roo.py new file mode 100644 index 000000000..69d859c42 --- /dev/null +++ b/tests/integrations/test_integration_roo.py @@ -0,0 +1,11 @@ +"""Tests for RooIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestRooIntegration(MarkdownIntegrationTests): + KEY = "roo" + FOLDER = ".roo/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".roo/commands" + CONTEXT_FILE = ".roo/rules/specify-rules.md" diff --git a/tests/integrations/test_integration_shai.py b/tests/integrations/test_integration_shai.py new file mode 100644 index 000000000..74f93396b --- /dev/null +++ b/tests/integrations/test_integration_shai.py @@ -0,0 +1,11 @@ +"""Tests for ShaiIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestShaiIntegration(MarkdownIntegrationTests): + KEY = "shai" + FOLDER = ".shai/" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".shai/commands" + CONTEXT_FILE = "SHAI.md" diff --git a/tests/integrations/test_integration_trae.py b/tests/integrations/test_integration_trae.py new file mode 100644 index 000000000..307c3481d --- /dev/null +++ b/tests/integrations/test_integration_trae.py @@ -0,0 +1,11 @@ +"""Tests for TraeIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestTraeIntegration(MarkdownIntegrationTests): + KEY = "trae" + FOLDER = ".trae/" + COMMANDS_SUBDIR = "rules" + REGISTRAR_DIR = ".trae/rules" + CONTEXT_FILE = ".trae/rules/AGENTS.md" diff --git a/tests/integrations/test_integration_vibe.py b/tests/integrations/test_integration_vibe.py new file mode 100644 index 000000000..ea6dc85a8 --- /dev/null +++ b/tests/integrations/test_integration_vibe.py @@ -0,0 +1,11 @@ +"""Tests for VibeIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestVibeIntegration(MarkdownIntegrationTests): + KEY = "vibe" + FOLDER = ".vibe/" + COMMANDS_SUBDIR = "prompts" + REGISTRAR_DIR = ".vibe/prompts" + CONTEXT_FILE = ".vibe/agents/specify-agents.md" diff --git a/tests/integrations/test_integration_windsurf.py b/tests/integrations/test_integration_windsurf.py new file mode 100644 index 000000000..fa8d1e622 --- /dev/null +++ b/tests/integrations/test_integration_windsurf.py @@ -0,0 +1,11 @@ +"""Tests for WindsurfIntegration.""" + +from .test_integration_base_markdown import MarkdownIntegrationTests + + +class TestWindsurfIntegration(MarkdownIntegrationTests): + KEY = "windsurf" + FOLDER = ".windsurf/" + COMMANDS_SUBDIR = "workflows" + REGISTRAR_DIR = ".windsurf/workflows" + CONTEXT_FILE = ".windsurf/rules/specify-rules.md" diff --git a/tests/integrations/test_registry.py b/tests/integrations/test_registry.py index 8fb5ef066..e70f3006a 100644 --- a/tests/integrations/test_registry.py +++ b/tests/integrations/test_registry.py @@ -1,4 +1,4 @@ -"""Tests for INTEGRATION_REGISTRY.""" +"""Tests for INTEGRATION_REGISTRY — mechanics, completeness, and registrar alignment.""" import pytest @@ -11,6 +11,16 @@ from specify_cli.integrations.base import MarkdownIntegration from .conftest import StubIntegration +# Every integration key that must be registered (Stage 2 + Stage 3). +ALL_INTEGRATION_KEYS = [ + "copilot", + # Stage 3 — standard markdown integrations + "claude", "qwen", "opencode", "junie", "kilocode", "auggie", + "roo", "codebuddy", "qodercli", "amp", "shai", "bob", "trae", + "pi", "iflow", "kiro-cli", "windsurf", "vibe", "cursor-agent", +] + + class TestRegistry: def test_registry_is_dict(self): assert isinstance(INTEGRATION_REGISTRY, dict) @@ -41,5 +51,26 @@ class TestRegistry: finally: INTEGRATION_REGISTRY.pop("stub", None) - def test_copilot_registered(self): - assert "copilot" in INTEGRATION_REGISTRY + +class TestRegistryCompleteness: + """Every expected integration must be registered.""" + + @pytest.mark.parametrize("key", ALL_INTEGRATION_KEYS) + def test_key_registered(self, key): + assert key in INTEGRATION_REGISTRY, f"{key} missing from registry" + + +class TestRegistrarKeyAlignment: + """Every integration key must have a matching AGENT_CONFIGS entry.""" + + @pytest.mark.parametrize("key", ALL_INTEGRATION_KEYS) + def test_integration_key_in_registrar(self, key): + from specify_cli.agents import CommandRegistrar + assert key in CommandRegistrar.AGENT_CONFIGS, ( + f"Integration '{key}' is registered but has no AGENT_CONFIGS entry" + ) + + def test_no_stale_cursor_shorthand(self): + """The old 'cursor' shorthand must not appear in AGENT_CONFIGS.""" + from specify_cli.agents import CommandRegistrar + assert "cursor" not in CommandRegistrar.AGENT_CONFIGS diff --git a/tests/test_ai_skills.py b/tests/test_ai_skills.py index f0e220e26..7f9ecf66a 100644 --- a/tests/test_ai_skills.py +++ b/tests/test_ai_skills.py @@ -1237,24 +1237,22 @@ class TestCliValidation: assert "agent skills" in plain.lower() def test_kiro_alias_normalized_to_kiro_cli(self, tmp_path): - """--ai kiro should normalize to canonical kiro-cli agent key.""" + """--ai kiro should normalize to canonical kiro-cli and auto-promote to integration path.""" + import os from typer.testing import CliRunner runner = CliRunner() target = tmp_path / "kiro-alias-proj" + target.mkdir() - with patch("specify_cli.download_and_extract_template") as mock_download, \ - patch("specify_cli.scaffold_from_core_pack", create=True) as mock_scaffold, \ - patch("specify_cli.ensure_executable_scripts"), \ - patch("specify_cli.ensure_constitution_from_template"), \ - patch("specify_cli.is_git_repo", return_value=False), \ - patch("specify_cli.shutil.which", return_value="/usr/bin/git"): - mock_scaffold.return_value = True + old_cwd = os.getcwd() + try: + os.chdir(target) result = runner.invoke( app, [ "init", - str(target), + "--here", "--ai", "kiro", "--ignore-agent-tools", @@ -1262,17 +1260,16 @@ class TestCliValidation: "sh", "--no-git", ], + catch_exceptions=False, ) + finally: + os.chdir(old_cwd) assert result.exit_code == 0 - # Without --offline, the download path should be taken. - assert mock_download.called, ( - "Expected download_and_extract_template to be called (default non-offline path)" - ) - assert mock_download.call_args.args[1] == "kiro-cli" - assert not mock_scaffold.called, ( - "scaffold_from_core_pack should not be called without --offline" - ) + # kiro alias should auto-promote to integration path with nudge + assert "--integration kiro-cli" in result.output + # Command files should be created via integration path + assert (target / ".kiro" / "prompts" / "speckit.plan.md").exists() def test_q_removed_from_agent_config(self): """Amazon Q legacy key should not remain in AGENT_CONFIG.""" diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 64b38547d..a5ee4e03a 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -981,7 +981,7 @@ $ARGUMENTS "Run scripts/bash/setup-plan.sh\n" ) - rewritten = AgentCommandRegistrar._rewrite_project_relative_paths(body) + rewritten = AgentCommandRegistrar.rewrite_project_relative_paths(body) assert ".specify/extensions/test-ext/templates/spec.md" in rewritten assert ".specify/scripts/bash/setup-plan.sh" in rewritten