mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-03-14 05:25:54 +00:00
ci: add GitHub Actions workflows with lint, typecheck and tests
This commit is contained in:
@@ -13,3 +13,49 @@ ignore = [
|
||||
"PLR2004", # allowing comparisons using unamed numerical constants in tests
|
||||
"S101", # allowing 'assert' statements in tests
|
||||
]
|
||||
"src/fuzzforge_cli/tui/**" = [
|
||||
"ARG002", # unused method argument: callback signature
|
||||
"BLE001", # blind exception: broad error handling in UI
|
||||
"C901", # complexity: UI logic
|
||||
"D107", # missing docstring in __init__: simple dataclasses
|
||||
"FBT001", # boolean positional arg
|
||||
"FBT002", # boolean default arg
|
||||
"PLC0415", # import outside top-level: lazy loading
|
||||
"PLR0911", # too many return statements
|
||||
"PLR0912", # too many branches
|
||||
"PLR2004", # magic value comparison
|
||||
"RUF012", # mutable class default: Textual pattern
|
||||
"S603", # subprocess: validated inputs
|
||||
"S607", # subprocess: PATH lookup
|
||||
"SIM108", # ternary: readability preference
|
||||
"TC001", # TYPE_CHECKING: runtime type needs
|
||||
"TC002", # TYPE_CHECKING: runtime type needs
|
||||
"TC003", # TYPE_CHECKING: runtime type needs
|
||||
"TRY300", # try-else: existing pattern
|
||||
]
|
||||
"tui/*.py" = [
|
||||
"D107", # missing docstring in __init__: simple dataclasses
|
||||
"TC001", # TYPE_CHECKING: runtime type needs
|
||||
"TC002", # TYPE_CHECKING: runtime type needs
|
||||
"TC003", # TYPE_CHECKING: runtime type needs
|
||||
]
|
||||
"src/fuzzforge_cli/commands/mcp.py" = [
|
||||
"ARG001", # unused argument: callback signature
|
||||
"B904", # raise from: existing pattern
|
||||
"F841", # unused variable: legacy code
|
||||
"FBT002", # boolean default arg
|
||||
"PLR0912", # too many branches
|
||||
"PLR0915", # too many statements
|
||||
"SIM108", # ternary: readability preference
|
||||
]
|
||||
"src/fuzzforge_cli/application.py" = [
|
||||
"B008", # function call in default: Path.cwd()
|
||||
"PLC0415", # import outside top-level: lazy loading
|
||||
]
|
||||
"src/fuzzforge_cli/commands/projects.py" = [
|
||||
"TC003", # TYPE_CHECKING: runtime type needs
|
||||
]
|
||||
"src/fuzzforge_cli/context.py" = [
|
||||
"TC002", # TYPE_CHECKING: runtime type needs
|
||||
"TC003", # TYPE_CHECKING: runtime type needs
|
||||
]
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from fuzzforge_mcp.storage import LocalStorage # type: ignore[import-untyped]
|
||||
from typer import Context as TyperContext
|
||||
from typer import Option, Typer
|
||||
|
||||
from fuzzforge_cli.commands import mcp, projects
|
||||
from fuzzforge_cli.context import Context
|
||||
from fuzzforge_mcp.storage import LocalStorage
|
||||
|
||||
application: Typer = Typer(
|
||||
name="fuzzforge",
|
||||
|
||||
@@ -12,7 +12,7 @@ import os
|
||||
import sys
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Any
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
@@ -44,10 +44,10 @@ def _get_copilot_mcp_path() -> Path:
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
return Path.home() / "Library" / "Application Support" / "Code" / "User" / "mcp.json"
|
||||
elif sys.platform == "win32":
|
||||
if sys.platform == "win32":
|
||||
return Path(os.environ.get("APPDATA", "")) / "Code" / "User" / "mcp.json"
|
||||
else: # Linux
|
||||
return Path.home() / ".config" / "Code" / "User" / "mcp.json"
|
||||
# Linux
|
||||
return Path.home() / ".config" / "Code" / "User" / "mcp.json"
|
||||
|
||||
|
||||
def _get_claude_desktop_mcp_path() -> Path:
|
||||
@@ -58,10 +58,10 @@ def _get_claude_desktop_mcp_path() -> Path:
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
||||
elif sys.platform == "win32":
|
||||
if sys.platform == "win32":
|
||||
return Path(os.environ.get("APPDATA", "")) / "Claude" / "claude_desktop_config.json"
|
||||
else: # Linux
|
||||
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
||||
# Linux
|
||||
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
||||
|
||||
|
||||
def _get_claude_code_mcp_path(project_path: Path | None = None) -> Path:
|
||||
@@ -114,13 +114,13 @@ def _detect_docker_socket() -> str:
|
||||
:returns: Path to the Docker socket.
|
||||
|
||||
"""
|
||||
socket_paths = [
|
||||
"/var/run/docker.sock",
|
||||
socket_paths: list[Path] = [
|
||||
Path("/var/run/docker.sock"),
|
||||
Path.home() / ".docker" / "run" / "docker.sock",
|
||||
]
|
||||
|
||||
for path in socket_paths:
|
||||
if Path(path).exists():
|
||||
if path.exists():
|
||||
return str(path)
|
||||
|
||||
return "/var/run/docker.sock"
|
||||
@@ -148,7 +148,7 @@ def _generate_mcp_config(
|
||||
fuzzforge_root: Path,
|
||||
engine_type: str,
|
||||
engine_socket: str,
|
||||
) -> dict:
|
||||
) -> dict[str, Any]:
|
||||
"""Generate MCP server configuration.
|
||||
|
||||
:param fuzzforge_root: Path to fuzzforge-oss installation.
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from fuzzforge_mcp.storage import LocalStorage
|
||||
from fuzzforge_mcp.storage import LocalStorage # type: ignore[import-untyped]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typer import Context as TyperContext
|
||||
|
||||
@@ -9,12 +9,14 @@ hub management capabilities.
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from rich.text import Text
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
from textual.widgets import Button, DataTable, Footer, Header, Label
|
||||
from textual.widgets import Button, DataTable, Footer, Header
|
||||
|
||||
from fuzzforge_cli.tui.helpers import (
|
||||
check_agent_status,
|
||||
@@ -24,11 +26,14 @@ from fuzzforge_cli.tui.helpers import (
|
||||
load_hub_config,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fuzzforge_cli.commands.mcp import AIAgent
|
||||
|
||||
# Agent config entries stored alongside their linked status for row mapping
|
||||
_AgentRow = tuple[str, "AIAgent", "Path", str, bool] # noqa: F821
|
||||
_AgentRow = tuple[str, "AIAgent", Path, str, bool]
|
||||
|
||||
|
||||
class FuzzForgeApp(App):
|
||||
class FuzzForgeApp(App[None]):
|
||||
"""FuzzForge AI terminal user interface."""
|
||||
|
||||
TITLE = "FuzzForge AI"
|
||||
@@ -236,7 +241,7 @@ class FuzzForgeApp(App):
|
||||
return
|
||||
|
||||
# Group servers by source hub
|
||||
groups: dict[str, list[dict]] = defaultdict(list)
|
||||
groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for server in servers:
|
||||
source = server.get("source_hub", "manual")
|
||||
groups[source].append(server)
|
||||
@@ -245,7 +250,7 @@ class FuzzForgeApp(App):
|
||||
ready_count = 0
|
||||
total = len(hub_servers)
|
||||
|
||||
statuses: list[tuple[dict, bool, str]] = []
|
||||
statuses: list[tuple[dict[str, Any], bool, str]] = []
|
||||
for server in hub_servers:
|
||||
enabled = server.get("enabled", True)
|
||||
if not enabled:
|
||||
|
||||
@@ -108,7 +108,7 @@ def check_hub_image(image: str) -> tuple[bool, str]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "image", "inspect", image],
|
||||
capture_output=True,
|
||||
check=False, capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
@@ -132,7 +132,8 @@ def load_hub_config(fuzzforge_root: Path) -> dict[str, Any]:
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(config_path.read_text())
|
||||
data: dict[str, Any] = json.loads(config_path.read_text())
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
@@ -264,7 +265,8 @@ def load_hubs_registry() -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {"hubs": []}
|
||||
try:
|
||||
return json.loads(path.read_text())
|
||||
data: dict[str, Any] = json.loads(path.read_text())
|
||||
return data
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {"hubs": []}
|
||||
|
||||
@@ -422,8 +424,7 @@ def clone_hub(
|
||||
"""
|
||||
if name is None:
|
||||
name = git_url.rstrip("/").split("/")[-1]
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
name = name.removesuffix(".git")
|
||||
|
||||
if dest is None:
|
||||
dest = get_default_hubs_dir() / name
|
||||
@@ -433,7 +434,7 @@ def clone_hub(
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(dest), "pull"],
|
||||
capture_output=True,
|
||||
check=False, capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
@@ -451,7 +452,7 @@ def clone_hub(
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "clone", git_url, str(dest)],
|
||||
capture_output=True,
|
||||
check=False, capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
@@ -81,6 +81,7 @@ class HubManagerScreen(ModalScreen[str | None]):
|
||||
is_default = hub.get("is_default", False)
|
||||
|
||||
hub_path = Path(path)
|
||||
count: str | Text
|
||||
if hub_path.is_dir():
|
||||
servers = scan_hub_for_servers(hub_path)
|
||||
count = str(len(servers))
|
||||
@@ -88,10 +89,11 @@ class HubManagerScreen(ModalScreen[str | None]):
|
||||
count = Text("dir missing", style="yellow")
|
||||
|
||||
source = git_url or "local"
|
||||
name_cell: str | Text
|
||||
if is_default:
|
||||
name_cell = Text(f"★ {name}", style="bold")
|
||||
else:
|
||||
name_cell = Text(name)
|
||||
name_cell = name
|
||||
|
||||
table.add_row(name_cell, path, count, source)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user