Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand All @@ -35,4 +35,3 @@ target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]

55 changes: 27 additions & 28 deletions src/secdevai_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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."""
Expand All @@ -186,4 +186,3 @@ def _make_executable(self, file_path: Path):

if __name__ == "__main__":
main()

10 changes: 5 additions & 5 deletions tests/test_module_deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from secdevai_cli import ModuleDeployer


# ---------------------------------------------------------------------------
# detect_platforms
# ---------------------------------------------------------------------------
Expand All @@ -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)


# ---------------------------------------------------------------------------
Expand Down