diff --git a/src/specify_cli/integrations/opencode/__init__.py b/src/specify_cli/integrations/opencode/__init__.py index 17db2bd11b..646c5702aa 100644 --- a/src/specify_cli/integrations/opencode/__init__.py +++ b/src/specify_cli/integrations/opencode/__init__.py @@ -1,6 +1,29 @@ """opencode integration.""" +from __future__ import annotations + +import shutil +from pathlib import Path +from typing import Any + from ..base import MarkdownIntegration +from ..manifest import IntegrationManifest + + +def _migrate_legacy_command_dir(project_root: Path) -> int: + """Remove the legacy `.opencode/command` directory. + + Called after setup() has already written canonical files to + `.opencode/commands/`. The legacy directory only ever contained + spec-kit-managed files, so it is safe to remove. + Returns the number of entries that were in the legacy directory. + """ + legacy = project_root / ".opencode" / "command" + if not legacy.is_dir(): + return 0 + count = sum(1 for _ in legacy.iterdir()) + shutil.rmtree(legacy) + return count class OpencodeIntegration(MarkdownIntegration): @@ -8,18 +31,30 @@ class OpencodeIntegration(MarkdownIntegration): config = { "name": "opencode", "folder": ".opencode/", - "commands_subdir": "command", + "commands_subdir": "commands", "install_url": "https://opencode.ai", "requires_cli": True, } registrar_config = { - "dir": ".opencode/command", + "dir": ".opencode/commands", "format": "markdown", "args": "$ARGUMENTS", "extension": ".md", } context_file = "AGENTS.md" + def setup( + self, + project_root: Path, + manifest: IntegrationManifest, + parsed_options: dict[str, Any] | None = None, + **opts: Any, + ) -> list[Path]: + """Install commands and remove any legacy `.opencode/command` directory.""" + created = super().setup(project_root, manifest, parsed_options=parsed_options, **opts) + _migrate_legacy_command_dir(project_root) + return created + def build_exec_args( self, prompt: str, diff --git a/tests/integrations/test_integration_opencode.py b/tests/integrations/test_integration_opencode.py index 427fd15167..3013187c0f 100644 --- a/tests/integrations/test_integration_opencode.py +++ b/tests/integrations/test_integration_opencode.py @@ -1,6 +1,8 @@ """Tests for OpencodeIntegration.""" from specify_cli.integrations import get_integration +from specify_cli.integrations.manifest import IntegrationManifest +from specify_cli.integrations.opencode import _migrate_legacy_command_dir from .test_integration_base_markdown import MarkdownIntegrationTests @@ -8,8 +10,8 @@ class TestOpencodeIntegration(MarkdownIntegrationTests): KEY = "opencode" FOLDER = ".opencode/" - COMMANDS_SUBDIR = "command" - REGISTRAR_DIR = ".opencode/command" + COMMANDS_SUBDIR = "commands" + REGISTRAR_DIR = ".opencode/commands" CONTEXT_FILE = "AGENTS.md" def test_build_exec_args_uses_run_command_dispatch(self): @@ -57,3 +59,34 @@ def test_build_exec_args_keeps_plain_prompt_dispatch(self): args = integration.build_exec_args("explain this repository", output_json=False) assert args == ["opencode", "run", "explain this repository"] + + +class TestOpencodeCommandMigration: + """Test legacy .opencode/command → .opencode/commands migration.""" + + def test_removes_legacy_command_dir(self, tmp_path): + legacy = tmp_path / ".opencode" / "command" + legacy.mkdir(parents=True) + (legacy / "speckit.specify.md").write_text("old content") + + removed = _migrate_legacy_command_dir(tmp_path) + + assert removed == 1 + assert not legacy.exists() + + def test_no_op_when_no_legacy_dir(self, tmp_path): + removed = _migrate_legacy_command_dir(tmp_path) + assert removed == 0 + + def test_setup_removes_legacy_dir(self, tmp_path): + """OpencodeIntegration.setup() cleans up legacy .opencode/command/.""" + legacy = tmp_path / ".opencode" / "command" + legacy.mkdir(parents=True) + (legacy / "speckit.specify.md").write_text("old content") + + i = get_integration("opencode") + m = IntegrationManifest("opencode", tmp_path) + i.setup(tmp_path, m) + + assert not legacy.exists() + assert (tmp_path / ".opencode" / "commands").is_dir()