From fc5ff9931f496e097610cb6e2b08a5eff0e7247c Mon Sep 17 00:00:00 2001 From: Troy Larson <1931732+troylar@users.noreply.github.com> Date: Fri, 13 Feb 2026 19:00:23 -0500 Subject: [PATCH] feat(ai_tools): add Roo Code IDE support Closes #32 Add support for the Roo Code VS Code extension (rooveterinaryinc.roo-cline). Instructions are installed to .roo/rules/*.md with recursive directory reading. Roo Code supports MCP servers (.roo/mcp.json), slash commands (.roo/commands/), mode-specific rules, and both global and project scope. - New RooTool class with global and project-level instruction support - Cross-platform detection (macOS, Linux, Windows) - IDE capability registry entry (instructions, MCP, commands, resources) - Component detector recognizes .roo/rules/ directories - Full test coverage for Roo Code tool integration - Updated documentation and version bump to 0.5.3 --- CHANGELOG.md | 13 ++ CLAUDE.md | 3 + README.md | 24 +-- aiconfigkit/ai_tools/base.py | 2 +- aiconfigkit/ai_tools/capability_registry.py | 24 +++ aiconfigkit/ai_tools/detector.py | 3 + aiconfigkit/ai_tools/roo.py | 110 ++++++++++++++ aiconfigkit/core/component_detector.py | 1 + aiconfigkit/core/models.py | 3 +- aiconfigkit/utils/paths.py | 23 +++ pyproject.toml | 4 +- .../unit/packages/test_capability_registry.py | 44 +++++- tests/unit/test_ai_tools_detector.py | 26 +++- tests/unit/test_ai_tools_roo.py | 141 ++++++++++++++++++ 14 files changed, 393 insertions(+), 28 deletions(-) create mode 100644 aiconfigkit/ai_tools/roo.py create mode 100644 tests/unit/test_ai_tools_roo.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ea228..17f60d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.3] - 2026-02-13 + +### Added +- **Roo Code IDE Support** - Added support for the Roo Code VS Code extension (#32) + - Instructions installed to `.roo/rules/*.md` + - Cross-platform detection via VS Code globalStorage (`rooveterinaryinc.roo-cline`) + - Package system support for instructions, MCP servers, commands, and resources + - Project-level MCP config at `.roo/mcp.json` + - Slash commands at `.roo/commands/` + - Global and project scope support + - IDE capability registry entry for Roo Code + - Component detector recognizes `.roo/rules/` directories + ## [0.5.2] - 2026-02-13 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index a230e5e..e5a3984 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,7 @@ ai-config-kit/ │ ├── cursor.py # Cursor (.cursor/rules/*.mdc) │ ├── cline.py # Cline (.clinerules/*.md) │ ├── kiro.py # Kiro (.kiro/steering/*.md) +│ ├── roo.py # Roo Code (.roo/rules/*.md) │ ├── winsurf.py # Windsurf (.windsurf/rules/*.md) │ ├── copilot.py # GitHub Copilot (.github/instructions/*.md) │ └── detector.py # Tool detection logic @@ -396,6 +397,7 @@ Different IDEs support different component types: - **Cline**: Instructions and resources only - **Cursor**: Instructions and resources only - **Kiro**: Instructions and resources only +- **Roo Code**: Instructions, MCP, commands, and resources - **Windsurf**: Instructions and resources only - **GitHub Copilot**: Instructions only @@ -406,6 +408,7 @@ Components are translated to IDE-specific formats: - **Claude Code**: `.md` files in `.claude/rules/`, `.claude/hooks/`, `.claude/commands/` - **Cline**: `.md` files in `.clinerules/` - **Cursor**: `.mdc` files in `.cursor/rules/` +- **Roo Code**: `.md` files in `.roo/rules/`, `.roo/commands/` - **Kiro**: `.md` files in `.kiro/steering/` - **Windsurf**: `.md` files in `.windsurf/rules/` - **GitHub Copilot**: `.md` files in `.github/instructions/` diff --git a/README.md b/README.md index 802b634..b0d7138 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -**Works with:** Claude Code • Claude Desktop • Cline • Cursor • GitHub Copilot • Kiro • Windsurf +**Works with:** Claude Code • Claude Desktop • Cline • Cursor • GitHub Copilot • Kiro • Roo Code • Windsurf @@ -258,16 +258,16 @@ Any IDE-specific content from Git repositories: Complete configuration bundles with multiple component types: -| Component | Claude | Cline | Cursor | Kiro | Windsurf | Copilot | -|-----------|--------|-------|--------|------|----------|---------| -| Instructions | `.claude/rules/` | `.clinerules/` | `.cursor/rules/` | `.kiro/steering/` | `.windsurf/rules/` | `.github/instructions/` | -| MCP Servers | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | -| Hooks | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| Commands | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| Skills | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| Workflows | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | -| Memory Files | ✅ (CLAUDE.md) | ❌ | ❌ | ❌ | ❌ | ❌ | -| Resources | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| Component | Claude | Cline | Cursor | Kiro | Roo Code | Windsurf | Copilot | +|-----------|--------|-------|--------|------|----------|----------|---------| +| Instructions | `.claude/rules/` | `.clinerules/` | `.cursor/rules/` | `.kiro/steering/` | `.roo/rules/` | `.windsurf/rules/` | `.github/instructions/` | +| MCP Servers | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | +| Hooks | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Commands | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| Skills | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Workflows | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | +| Memory Files | ✅ (CLAUDE.md) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Resources | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ### MCP Server Configurations @@ -287,7 +287,7 @@ Model Context Protocol server setups for enhanced AI capabilities: - Python 3.10 or higher - Git (for cloning template repositories) -- One of: Claude Code, Claude Desktop, Cline, Cursor, GitHub Copilot, Kiro, or Windsurf +- One of: Claude Code, Claude Desktop, Cline, Cursor, GitHub Copilot, Kiro, Roo Code, or Windsurf ### Install AI Config Kit diff --git a/aiconfigkit/ai_tools/base.py b/aiconfigkit/ai_tools/base.py index d8ed2e4..0d16f44 100644 --- a/aiconfigkit/ai_tools/base.py +++ b/aiconfigkit/ai_tools/base.py @@ -12,7 +12,7 @@ class AITool(ABC): """ Abstract base class for AI coding tool integrations. - Each AI tool (Cline, Cursor, Copilot, Kiro, Winsurf, Claude) implements this interface + Each AI tool (Cline, Cursor, Copilot, Kiro, Roo Code, Winsurf, Claude) implements this interface to provide tool-specific installation logic. """ diff --git a/aiconfigkit/ai_tools/capability_registry.py b/aiconfigkit/ai_tools/capability_registry.py index dad0a99..52cb2ec 100644 --- a/aiconfigkit/ai_tools/capability_registry.py +++ b/aiconfigkit/ai_tools/capability_registry.py @@ -163,6 +163,30 @@ def supports_component(self, component_type: ComponentType) -> bool: "MCP configured globally via cline_mcp_settings.json, no project-level MCP support." ), ), + AIToolType.ROO: IDECapability( + tool_type=AIToolType.ROO, + tool_name="Roo Code", + supported_components={ + ComponentType.INSTRUCTION, + ComponentType.MCP_SERVER, + ComponentType.COMMAND, + ComponentType.RESOURCE, + }, + instructions_directory=".roo/rules/", + instruction_file_extension=".md", + supports_project_scope=True, + supports_global_scope=True, + mcp_config_path=None, # Global MCP via globalStorage cline_mcp_settings.json + mcp_project_config_path=".roo/mcp.json", + hooks_directory=None, # Hooks not supported + commands_directory=".roo/commands/", + notes=( + "Roo Code uses .md files in .roo/rules/ directory (recursive). " + "Mode-specific rules in .roo/rules-{mode-slug}/. " + "MCP configured via .roo/mcp.json (project) or globalStorage (global). " + "Slash commands in .roo/commands/. Global rules at ~/.roo/rules/." + ), + ), AIToolType.COPILOT: IDECapability( tool_type=AIToolType.COPILOT, tool_name="GitHub Copilot", diff --git a/aiconfigkit/ai_tools/detector.py b/aiconfigkit/ai_tools/detector.py index 5decf4e..29a8a61 100644 --- a/aiconfigkit/ai_tools/detector.py +++ b/aiconfigkit/ai_tools/detector.py @@ -8,6 +8,7 @@ from aiconfigkit.ai_tools.copilot import CopilotTool from aiconfigkit.ai_tools.cursor import CursorTool from aiconfigkit.ai_tools.kiro import KiroTool +from aiconfigkit.ai_tools.roo import RooTool from aiconfigkit.ai_tools.winsurf import WinsurfTool from aiconfigkit.core.models import AIToolType @@ -24,6 +25,7 @@ def __init__(self) -> None: AIToolType.CLAUDE: ClaudeTool(), AIToolType.KIRO: KiroTool(), AIToolType.CLINE: ClineTool(), + AIToolType.ROO: RooTool(), } def detect_installed_tools(self) -> list[AITool]: @@ -84,6 +86,7 @@ def get_primary_tool(self) -> Optional[AITool]: AIToolType.CLAUDE, AIToolType.KIRO, AIToolType.CLINE, + AIToolType.ROO, ] for tool_type in priority: diff --git a/aiconfigkit/ai_tools/roo.py b/aiconfigkit/ai_tools/roo.py new file mode 100644 index 0000000..fb1280d --- /dev/null +++ b/aiconfigkit/ai_tools/roo.py @@ -0,0 +1,110 @@ +"""Roo Code AI tool integration.""" + +from pathlib import Path + +from aiconfigkit.ai_tools.base import AITool +from aiconfigkit.core.models import AIToolType +from aiconfigkit.utils.paths import get_roo_config_dir + + +class RooTool(AITool): + """Integration for Roo Code AI coding tool (VS Code extension). + + Roo Code (formerly Roo Cline) uses .roo/rules/ directory at the project root + for AI instructions. Files are .md (Markdown) and are read recursively in + alphabetical order by filename. Numeric prefixes control load order. + + Mode-specific rules can be placed in .roo/rules-{mode-slug}/ directories. + Project-level MCP config is at .roo/mcp.json. + + Detection is based on the VS Code extension rooveterinaryinc.roo-cline + globalStorage directory. + """ + + @property + def tool_type(self) -> AIToolType: + """Return the AI tool type identifier.""" + return AIToolType.ROO + + @property + def tool_name(self) -> str: + """Return human-readable tool name.""" + return "Roo Code" + + def is_installed(self) -> bool: + """ + Check if Roo Code is installed on the system. + + Checks for existence of the Roo Code VS Code extension globalStorage directory + (rooveterinaryinc.roo-cline). + + Returns: + True if Roo Code is detected + """ + try: + config_dir = get_roo_config_dir() + return config_dir.exists() + except Exception: + return False + + def get_instructions_directory(self) -> Path: + """ + Get the directory where Roo Code global instructions should be installed. + + Roo Code supports global rules at ~/.roo/rules/. + + Returns: + Path to Roo Code global instructions directory + + Raises: + FileNotFoundError: If Roo Code is not installed + """ + if not self.is_installed(): + raise FileNotFoundError(f"{self.tool_name} is not installed") + from aiconfigkit.utils.paths import get_home_directory + + global_dir = get_home_directory() / ".roo" / "rules" + global_dir.mkdir(parents=True, exist_ok=True) + return global_dir + + def get_instruction_file_extension(self) -> str: + """ + Get the file extension for Roo Code instructions. + + Roo Code uses markdown (.md) files in the .roo/rules/ directory. + + Returns: + File extension including the dot + """ + return ".md" + + def get_project_instructions_directory(self, project_root: Path) -> Path: + """ + Get the directory for project-specific Roo Code instructions. + + Roo Code stores project-specific rules in .roo/rules/ directory at the + project root. It reads .md files recursively from this directory. + Numeric prefixes (e.g., 01-coding-standards.md) control load order. + + Args: + project_root: Path to the project root directory + + Returns: + Path to project instructions directory (.roo/rules/) + """ + instructions_dir = project_root / ".roo" / "rules" + instructions_dir.mkdir(parents=True, exist_ok=True) + return instructions_dir + + def get_mcp_config_path(self) -> Path: + """ + Get the path to the MCP configuration file for Roo Code. + + Roo Code stores project-level MCP config at .roo/mcp.json. + Global MCP config is at the globalStorage settings directory. + + Returns: + Path to global MCP configuration file + """ + config_dir = get_roo_config_dir() + return config_dir / "settings" / "cline_mcp_settings.json" diff --git a/aiconfigkit/core/component_detector.py b/aiconfigkit/core/component_detector.py index 7754b6e..a794b8c 100644 --- a/aiconfigkit/core/component_detector.py +++ b/aiconfigkit/core/component_detector.py @@ -234,6 +234,7 @@ class ComponentDetector: ".windsurf/rules": "windsurf", ".kiro/steering": "kiro", ".clinerules": "cline", + ".roo/rules": "roo", ".github/instructions": "copilot", } diff --git a/aiconfigkit/core/models.py b/aiconfigkit/core/models.py index 6c111d3..fe01c43 100644 --- a/aiconfigkit/core/models.py +++ b/aiconfigkit/core/models.py @@ -15,6 +15,7 @@ class AIToolType(Enum): CLAUDE = "claude" KIRO = "kiro" CLINE = "cline" + ROO = "roo" class ConflictResolution(Enum): @@ -415,7 +416,7 @@ def __post_init__(self) -> None: """Validate template file data.""" if not self.path: raise ValueError("Template file path cannot be empty") - valid_ides = ["all", "cursor", "claude", "windsurf", "copilot", "kiro", "cline"] + valid_ides = ["all", "cursor", "claude", "windsurf", "copilot", "kiro", "cline", "roo"] if self.ide not in valid_ides: raise ValueError(f"Invalid IDE type: {self.ide}. Must be one of {valid_ides}") diff --git a/aiconfigkit/utils/paths.py b/aiconfigkit/utils/paths.py index 945d3ad..d41a634 100644 --- a/aiconfigkit/utils/paths.py +++ b/aiconfigkit/utils/paths.py @@ -87,6 +87,29 @@ def get_cline_config_dir() -> Path: raise OSError(f"Unsupported operating system: {os.name}") +def get_roo_config_dir() -> Path: + """Get Roo Code (VS Code extension) configuration directory based on platform.""" + home = get_home_directory() + + if os.name == "nt": # Windows + return home / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline" + elif os.name == "posix": + if "darwin" in os.uname().sysname.lower(): # macOS + return ( + home + / "Library" + / "Application Support" + / "Code" + / "User" + / "globalStorage" + / "rooveterinaryinc.roo-cline" + ) + else: # Linux + return home / ".config" / "Code" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline" + + raise OSError(f"Unsupported operating system: {os.name}") + + def get_claude_config_dir() -> Path: """Get Claude Code configuration directory based on platform.""" home = get_home_directory() diff --git a/pyproject.toml b/pyproject.toml index 7544f03..d62ce1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "ai-config-kit" -version = "0.5.2" +version = "0.5.3" description = "Distribute and sync AI coding assistant configurations across teams" readme = "README.md" authors = [{ name = "Troy Larson", email = "troy@calvinware.com" }] requires-python = ">=3.10" license = { text = "MIT License" } -keywords = ["cli", "ai", "config", "mcp", "cursor", "copilot", "claude", "cline", "kiro", "windsurf"] +keywords = ["cli", "ai", "config", "mcp", "cursor", "copilot", "claude", "cline", "kiro", "roo", "windsurf"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", diff --git a/tests/unit/packages/test_capability_registry.py b/tests/unit/packages/test_capability_registry.py index 91fc047..d869015 100644 --- a/tests/unit/packages/test_capability_registry.py +++ b/tests/unit/packages/test_capability_registry.py @@ -69,6 +69,7 @@ def test_registry_contains_all_tools(self) -> None: assert AIToolType.COPILOT in CAPABILITY_REGISTRY assert AIToolType.KIRO in CAPABILITY_REGISTRY assert AIToolType.CLINE in CAPABILITY_REGISTRY + assert AIToolType.ROO in CAPABILITY_REGISTRY def test_cursor_capabilities(self) -> None: """Test Cursor IDE capabilities.""" @@ -219,29 +220,54 @@ def test_cline_capabilities(self) -> None: assert not cline.supports_component(ComponentType.MCP_SERVER) assert not cline.supports_component(ComponentType.SKILL) + def test_roo_code_capabilities(self) -> None: + """Test Roo Code IDE capabilities.""" + roo = CAPABILITY_REGISTRY[AIToolType.ROO] + + assert roo.tool_type == AIToolType.ROO + assert roo.tool_name == "Roo Code" + assert roo.instructions_directory == ".roo/rules/" + assert roo.instruction_file_extension == ".md" + assert roo.supports_project_scope + assert roo.supports_global_scope + assert roo.mcp_project_config_path == ".roo/mcp.json" + assert roo.commands_directory == ".roo/commands/" + + # Roo Code supports instructions, MCP, commands, and resources + assert roo.supports_component(ComponentType.INSTRUCTION) + assert roo.supports_component(ComponentType.MCP_SERVER) + assert roo.supports_component(ComponentType.COMMAND) + assert roo.supports_component(ComponentType.RESOURCE) + + # Roo Code does not support hooks or skills + assert not roo.supports_component(ComponentType.HOOK) + assert not roo.supports_component(ComponentType.SKILL) + def test_get_supported_tools_for_instruction(self) -> None: """Test getting tools that support instructions.""" tools = get_supported_tools_for_component(ComponentType.INSTRUCTION) # All tools support instructions - assert len(tools) == 6 + assert len(tools) == 7 assert AIToolType.CURSOR in tools assert AIToolType.CLAUDE in tools assert AIToolType.WINSURF in tools assert AIToolType.COPILOT in tools assert AIToolType.KIRO in tools assert AIToolType.CLINE in tools + assert AIToolType.ROO in tools def test_get_supported_tools_for_mcp_server(self) -> None: """Test getting tools that support MCP servers.""" tools = get_supported_tools_for_component(ComponentType.MCP_SERVER) - # All IDEs now support MCP (Claude, Cursor, Windsurf, Copilot) - assert len(tools) == 4 + # Claude, Cursor, Windsurf, Copilot, and Roo Code support MCP + assert len(tools) == 5 assert AIToolType.CLAUDE in tools assert AIToolType.CURSOR in tools assert AIToolType.WINSURF in tools - assert AIToolType.COPILOT in tools # Now supported via VS Code + assert AIToolType.COPILOT in tools + assert AIToolType.ROO in tools def test_get_supported_tools_for_hook(self) -> None: """Test getting tools that support hooks.""" @@ -255,21 +281,23 @@ def test_get_supported_tools_for_command(self) -> None: """Test getting tools that support commands.""" tools = get_supported_tools_for_component(ComponentType.COMMAND) - # Only Claude Code supports commands - assert len(tools) == 1 + # Claude Code and Roo Code support commands + assert len(tools) == 2 assert AIToolType.CLAUDE in tools + assert AIToolType.ROO in tools def test_get_supported_tools_for_resource(self) -> None: """Test getting tools that support resources.""" tools = get_supported_tools_for_component(ComponentType.RESOURCE) - # Cursor, Claude, Windsurf, Kiro, and Cline support resources (not Copilot) - assert len(tools) == 5 + # Cursor, Claude, Windsurf, Kiro, Cline, and Roo Code support resources (not Copilot) + assert len(tools) == 6 assert AIToolType.CURSOR in tools assert AIToolType.CLAUDE in tools assert AIToolType.WINSURF in tools assert AIToolType.KIRO in tools assert AIToolType.CLINE in tools + assert AIToolType.ROO in tools assert AIToolType.COPILOT not in tools # Instructions only def test_validate_component_support_true(self) -> None: diff --git a/tests/unit/test_ai_tools_detector.py b/tests/unit/test_ai_tools_detector.py index e46a34a..94fea71 100644 --- a/tests/unit/test_ai_tools_detector.py +++ b/tests/unit/test_ai_tools_detector.py @@ -29,6 +29,9 @@ def mock_all_tools_installed(monkeypatch, temp_dir): (home_dir / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev").mkdir( parents=True ) + (home_dir / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline").mkdir( + parents=True + ) elif os.name == "posix": if "darwin" in os.uname().sysname.lower(): # macOS (home_dir / "Library" / "Application Support" / "Cursor" / "User" / "globalStorage").mkdir(parents=True) @@ -47,12 +50,25 @@ def mock_all_tools_installed(monkeypatch, temp_dir): / "saoudrizwan.claude-dev" ) cline_dir.mkdir(parents=True) + roo_dir = ( + home_dir + / "Library" + / "Application Support" + / "Code" + / "User" + / "globalStorage" + / "rooveterinaryinc.roo-cline" + ) + roo_dir.mkdir(parents=True) else: # Linux (home_dir / ".config" / "Cursor" / "User" / "globalStorage").mkdir(parents=True) (home_dir / ".config" / "Code" / "User" / "globalStorage" / "github.copilot").mkdir(parents=True) (home_dir / ".config" / "Windsurf" / "User" / "globalStorage").mkdir(parents=True) (home_dir / ".config" / "Kiro" / "User" / "globalStorage").mkdir(parents=True) (home_dir / ".config" / "Code" / "User" / "globalStorage" / "saoudrizwan.claude-dev").mkdir(parents=True) + (home_dir / ".config" / "Code" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline").mkdir( + parents=True + ) else: raise OSError(f"Unsupported operating system: {os.name}") @@ -67,13 +83,14 @@ class TestAIToolDetector: def test_init_creates_all_tools(self, detector): """Test that detector initializes with all supported tools.""" - assert len(detector.tools) == 6 + assert len(detector.tools) == 7 assert AIToolType.CURSOR in detector.tools assert AIToolType.COPILOT in detector.tools assert AIToolType.WINSURF in detector.tools assert AIToolType.CLAUDE in detector.tools assert AIToolType.KIRO in detector.tools assert AIToolType.CLINE in detector.tools + assert AIToolType.ROO in detector.tools def test_detect_installed_tools_none(self, temp_dir, monkeypatch): """Test detect_installed_tools when no tools are installed.""" @@ -91,7 +108,7 @@ def test_detect_installed_tools_all(self, mock_all_tools_installed): # Create fresh detector with mocked paths detector = AIToolDetector() installed = detector.detect_installed_tools() - assert len(installed) == 6 + assert len(installed) == 7 def test_get_tool_by_name_valid(self, detector): """Test get_tool_by_name with valid tool name.""" @@ -180,13 +197,14 @@ def test_is_any_tool_installed_false(self, temp_dir, monkeypatch): def test_get_tool_names(self, detector): """Test get_tool_names returns all tool names.""" names = detector.get_tool_names() - assert len(names) == 6 + assert len(names) == 7 assert "cursor" in names assert "copilot" in names assert "winsurf" in names assert "claude" in names assert "kiro" in names assert "cline" in names + assert "roo" in names def test_validate_tool_name_valid(self, detector): """Test validate_tool_name with valid name.""" @@ -201,7 +219,7 @@ def test_get_detection_summary(self, mock_all_tools_installed): """Test get_detection_summary.""" detector = AIToolDetector() summary = detector.get_detection_summary() - assert len(summary) == 6 + assert len(summary) == 7 assert all(isinstance(v, bool) for v in summary.values()) def test_format_detection_summary(self, mock_all_tools_installed): diff --git a/tests/unit/test_ai_tools_roo.py b/tests/unit/test_ai_tools_roo.py new file mode 100644 index 0000000..e25e33f --- /dev/null +++ b/tests/unit/test_ai_tools_roo.py @@ -0,0 +1,141 @@ +"""Tests for Roo Code AI tool integration.""" + +import pytest + +from aiconfigkit.ai_tools.roo import RooTool +from aiconfigkit.core.models import AIToolType, InstallationScope, Instruction + + +@pytest.fixture +def roo_tool(): + """Create a Roo Code tool instance.""" + return RooTool() + + +@pytest.fixture +def mock_roo_installed(monkeypatch, temp_dir): + """Mock Roo Code as installed.""" + import os + + home_dir = temp_dir / "home" + home_dir.mkdir(parents=True) + + # Roo Code is a VS Code extension — detection uses globalStorage path + if os.name == "nt": # Windows + roo_dir = home_dir / "AppData" / "Roaming" / "Code" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline" + elif os.name == "posix": + if "darwin" in os.uname().sysname.lower(): # macOS + roo_dir = ( + home_dir + / "Library" + / "Application Support" + / "Code" + / "User" + / "globalStorage" + / "rooveterinaryinc.roo-cline" + ) + else: # Linux + roo_dir = home_dir / ".config" / "Code" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline" + else: + raise OSError(f"Unsupported operating system: {os.name}") + + roo_dir.mkdir(parents=True) + + monkeypatch.setattr("aiconfigkit.utils.paths.get_home_directory", lambda: home_dir) + return roo_dir + + +class TestRooTool: + """Test suite for RooTool.""" + + def test_tool_type(self, roo_tool): + """Test tool type property.""" + assert roo_tool.tool_type == AIToolType.ROO + + def test_tool_name(self, roo_tool): + """Test tool name property.""" + assert roo_tool.tool_name == "Roo Code" + + def test_is_installed_when_present(self, roo_tool, mock_roo_installed): + """Test is_installed returns True when Roo Code is present.""" + assert roo_tool.is_installed() is True + + def test_is_installed_when_absent(self, temp_dir, monkeypatch): + """Test is_installed returns False when Roo Code is not present.""" + home_dir = temp_dir / "empty_home" + home_dir.mkdir() + monkeypatch.setattr("aiconfigkit.utils.paths.get_home_directory", lambda: home_dir) + roo_tool = RooTool() + assert roo_tool.is_installed() is False + + def test_is_installed_handles_exception(self, monkeypatch): + """Test is_installed handles exceptions gracefully.""" + + def raise_error(): + raise RuntimeError("Test error") + + monkeypatch.setattr("aiconfigkit.utils.paths.get_home_directory", raise_error) + roo_tool = RooTool() + assert roo_tool.is_installed() is False + + def test_get_instructions_directory(self, roo_tool, mock_roo_installed): + """Test get_instructions_directory returns global rules directory.""" + instructions_dir = roo_tool.get_instructions_directory() + assert instructions_dir.name == "rules" + assert ".roo" in str(instructions_dir) + + def test_get_instructions_directory_not_installed(self, temp_dir, monkeypatch): + """Test get_instructions_directory raises when not installed.""" + home_dir = temp_dir / "empty_home" + home_dir.mkdir() + monkeypatch.setattr("aiconfigkit.utils.paths.get_home_directory", lambda: home_dir) + roo_tool = RooTool() + with pytest.raises(FileNotFoundError): + roo_tool.get_instructions_directory() + + def test_get_instruction_file_extension(self, roo_tool): + """Test instruction file extension.""" + assert roo_tool.get_instruction_file_extension() == ".md" + + def test_get_project_instructions_directory(self, roo_tool, temp_dir): + """Test project instructions directory uses .roo/rules/.""" + project_root = temp_dir / "project" + project_root.mkdir() + + instructions_dir = roo_tool.get_project_instructions_directory(project_root) + + assert instructions_dir == project_root / ".roo" / "rules" + assert instructions_dir.exists() + + def test_install_instruction(self, roo_tool, temp_dir): + """Test install_instruction creates .md file in .roo/rules/.""" + project_root = temp_dir / "project" + project_root.mkdir() + + instruction = Instruction( + name="test-instruction", + description="Test", + content="Test content", + file_path="test.md", + tags=[], + ) + + path = roo_tool.install_instruction(instruction, scope=InstallationScope.PROJECT, project_root=project_root) + + assert path.exists() + assert path.read_text() == "Test content" + assert path.suffix == ".md" + assert ".roo" in str(path) + assert "rules" in str(path) + + def test_get_mcp_config_path(self, roo_tool, mock_roo_installed): + """Test MCP config path returns globalStorage settings path.""" + mcp_path = roo_tool.get_mcp_config_path() + assert "cline_mcp_settings.json" in str(mcp_path) + assert "settings" in str(mcp_path) + + def test_repr(self, roo_tool): + """Test string representation.""" + repr_str = repr(roo_tool) + assert "RooTool" in repr_str + assert AIToolType.ROO.value in repr_str