diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5523ecd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.15.11 + hooks: + - id: ruff-check + args: [ --fix ] + - id: ruff-format diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb30ab7..fb41eb7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,3 +29,28 @@ For guidance on adding new security context such as rules, tools, or languages, We encourage you to contribute your context extensions back to help the community! AI-generated code is absolutely welcome, however, just make sure to test your changes and provide clear descriptions in your PR. Every contribution, big or small, helps make SecDevAI better. +## Local Development + +### Running tests + +From the `secdevai` repository run: + +```sh +uv run pytest +``` + +### pre-commit hooks + +Install pre-commit hooks: + +```sh +uv run pre-commit install +``` + +### Running + +From another repository run: + +```sh +uvx --no-cache /path/to/secdevai +``` diff --git a/pyproject.toml b/pyproject.toml index f9f3b7e..97af6a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,8 +23,8 @@ packages = ["src/secdevai_cli"] [tool.hatch.build.targets.wheel.shared-data] "lola-module" = "share/secdevai/lola-module" -[tool.uv] -dev-dependencies = [ +[dependency-groups] +dev = [ "pytest>=7.0.0", "ruff>=0.1.0", ] @@ -35,4 +35,3 @@ target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "N", "W"] - diff --git a/src/secdevai_cli/__init__.py b/src/secdevai_cli/__init__.py index 6a995e8..d577941 100644 --- a/src/secdevai_cli/__init__.py +++ b/src/secdevai_cli/__init__.py @@ -48,7 +48,9 @@ def _find_module_dir() -> Path | None: @app.command() def init( - project_path: str = typer.Argument(".", help="Path to project directory (defaults to current directory)"), + project_path: str = typer.Argument( + ".", help="Path to project directory (defaults to current directory)" + ), ): """Initialize SecDevAI in a project directory.""" target_dir = Path(project_path).expanduser().resolve() @@ -83,22 +85,21 @@ class ModuleDeployer: For Gemini CLI, .md files inside commands/ are converted to .toml format. """ + SUPPORTED_PLATFORMS = {"cursor", "claude", "gemini"} + DEFAULT_PLATFORMS = ["cursor", "claude"] + def __init__(self, module_dir: Path): """Initialize module deployer.""" self.module_dir = module_dir def detect_platforms(self, target_dir: Path) -> list[str]: """Detect which AI assistant platforms are present.""" - platforms = [] - for platform in ("cursor", "claude", "gemini"): - if (target_dir / f".{platform}").exists(): - platforms.append(platform) - - # If no platforms detected, default to cursor and claude - if not platforms: - platforms = ["cursor", "claude"] - - return platforms + platforms = [ + platform + for platform in ModuleDeployer.SUPPORTED_PLATFORMS + if (target_dir / f".{platform}").exists() + ] + return platforms or ModuleDeployer.DEFAULT_PLATFORMS def _convert_md_to_toml(self, md_content: str) -> str: """Convert markdown command to Gemini CLI .toml format. @@ -146,37 +147,36 @@ def deploy(self, target_dir: Path): console.print(f"[dim]Detected platforms: {', '.join(platforms)}[/dim]\n") # Collect all files from lola-module/ (preserving relative paths) - source_files = sorted( - f for f in self.module_dir.rglob("*") if f.is_file() - ) + # note: rglob is recursive glob **/* + source_files = sorted(f for f in self.module_dir.rglob("*") if f.is_file()) for platform in platforms: platform_dir = target_dir / f".{platform}" - deployed_count = 0 for source_path in source_files: rel_path = source_path.relative_to(self.module_dir) # For Gemini CLI: convert commands/*.md to .toml - if platform == "gemini" and self._is_commands_dir(rel_path) and source_path.suffix == ".md": - toml_content = self._convert_md_to_toml(source_path.read_text()) + if ( + platform == "gemini" + and self._is_commands_dir(rel_path) + and source_path.suffix == ".md" + ): + toml_content = self._convert_md_to_toml(source_path.read_text(encoding="utf-8")) target_path = platform_dir / rel_path.with_suffix(".toml") target_path.parent.mkdir(parents=True, exist_ok=True) - target_path.write_text(toml_content) - else: - target_path = platform_dir / rel_path - target_path.parent.mkdir(parents=True, exist_ok=True) - target_path.write_bytes(source_path.read_bytes()) + target_path.write_text(toml_content, encoding="utf-8") + continue + + target_path = platform_dir / rel_path + target_path.parent.mkdir(parents=True, exist_ok=True) + target_path.write_bytes(source_path.read_bytes()) # Make shell scripts executable if target_path.suffix == ".sh": self._make_executable(target_path) - deployed_count += 1 - - console.print( - f"[green]✓[/green] Deployed {deployed_count} files to .{platform}/" - ) + console.print(f"[green]✓[/green] Deployed {len(source_files)} files to .{platform}/") def _make_executable(self, file_path: Path): """Make file executable by adding +x permissions.""" @@ -186,4 +186,3 @@ def _make_executable(self, file_path: Path): if __name__ == "__main__": main() - diff --git a/tests/test_module_deployer.py b/tests/test_module_deployer.py index e42d779..1faced4 100644 --- a/tests/test_module_deployer.py +++ b/tests/test_module_deployer.py @@ -7,7 +7,6 @@ from secdevai_cli import ModuleDeployer - # --------------------------------------------------------------------------- # detect_platforms # --------------------------------------------------------------------------- @@ -32,15 +31,16 @@ def test_detects_gemini_directory(self, fake_lola_module, target_project): assert deployer.detect_platforms(target_project) == ["gemini"] def test_detects_multiple_platforms(self, fake_lola_module, target_project): - for name in (".cursor", ".claude", ".gemini"): - (target_project / name).mkdir() + for name in ModuleDeployer.SUPPORTED_PLATFORMS: + (target_project / f".{name}").mkdir() deployer = ModuleDeployer(fake_lola_module) platforms = deployer.detect_platforms(target_project) - assert set(platforms) == {"cursor", "claude", "gemini"} + assert set(platforms) == ModuleDeployer.SUPPORTED_PLATFORMS def test_defaults_to_cursor_and_claude_when_none(self, fake_lola_module, target_project): deployer = ModuleDeployer(fake_lola_module) - assert set(deployer.detect_platforms(target_project)) == {"cursor", "claude"} + platforms = deployer.detect_platforms(target_project) + assert set(platforms) == set(ModuleDeployer.DEFAULT_PLATFORMS) # ---------------------------------------------------------------------------