143 lines
4.7 KiB
Python
143 lines
4.7 KiB
Python
"""
|
|
Project detection and build/test tools.
|
|
|
|
These mirror Loki's .shared/utils.sh detect_project() heuristic and the
|
|
sisyphus/coder tools.sh run_build / run_tests / verify_build commands.
|
|
|
|
Loki Concept Mapping:
|
|
Loki uses a heuristic cascade: check for Cargo.toml → go.mod → package.json
|
|
etc., then falls back to an LLM call for unknown projects. We replicate the
|
|
heuristic portion here. The LLM fallback is omitted since the agents
|
|
themselves can reason about unknown project types.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
|
|
from langchain_core.tools import tool
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Project detection (mirrors _detect_heuristic in utils.sh)
|
|
# ---------------------------------------------------------------------------
|
|
_HEURISTICS: list[tuple[str, dict[str, str]]] = [
|
|
("Cargo.toml", {"type": "rust", "build": "cargo build", "test": "cargo test", "check": "cargo check"}),
|
|
("go.mod", {"type": "go", "build": "go build ./...", "test": "go test ./...", "check": "go vet ./..."}),
|
|
("package.json", {"type": "nodejs", "build": "npm run build", "test": "npm test", "check": "npm run lint"}),
|
|
("pyproject.toml", {"type": "python", "build": "", "test": "pytest", "check": "ruff check ."}),
|
|
("pom.xml", {"type": "java", "build": "mvn compile", "test": "mvn test", "check": "mvn verify"}),
|
|
("Makefile", {"type": "make", "build": "make build", "test": "make test", "check": "make lint"}),
|
|
]
|
|
|
|
|
|
def detect_project(directory: str) -> dict[str, str]:
|
|
"""Detect project type and return build/test commands."""
|
|
for marker, info in _HEURISTICS:
|
|
if os.path.exists(os.path.join(directory, marker)):
|
|
return info
|
|
return {"type": "unknown", "build": "", "test": "", "check": ""}
|
|
|
|
|
|
@tool
|
|
def get_project_info(directory: str = ".") -> str:
|
|
"""Detect the project type and show structure overview.
|
|
|
|
Args:
|
|
directory: Project root directory.
|
|
"""
|
|
directory = os.path.expanduser(directory)
|
|
info = detect_project(directory)
|
|
result = f"Project: {os.path.abspath(directory)}\n"
|
|
result += f"Type: {info['type']}\n"
|
|
result += f"Build: {info['build'] or '(none)'}\n"
|
|
result += f"Test: {info['test'] or '(none)'}\n"
|
|
result += f"Check: {info['check'] or '(none)'}\n"
|
|
return result
|
|
|
|
|
|
def _run_project_command(directory: str, command_key: str) -> str:
|
|
"""Run a detected project command (build/test/check)."""
|
|
directory = os.path.expanduser(directory)
|
|
info = detect_project(directory)
|
|
cmd = info.get(command_key, "")
|
|
|
|
if not cmd:
|
|
return f"No {command_key} command detected for this project."
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
cmd,
|
|
shell=True,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=directory,
|
|
timeout=300,
|
|
)
|
|
output = result.stdout + result.stderr
|
|
status = "SUCCESS" if result.returncode == 0 else f"FAILED (exit {result.returncode})"
|
|
return f"Running: {cmd}\n\n{output}\n\n{command_key.upper()}: {status}"
|
|
except subprocess.TimeoutExpired:
|
|
return f"{command_key.upper()}: TIMEOUT after 300s"
|
|
except Exception as e:
|
|
return f"{command_key.upper()}: ERROR — {e}"
|
|
|
|
|
|
@tool
|
|
def run_build(directory: str = ".") -> str:
|
|
"""Run the project's build command.
|
|
|
|
Args:
|
|
directory: Project root directory.
|
|
"""
|
|
return _run_project_command(directory, "build")
|
|
|
|
|
|
@tool
|
|
def run_tests(directory: str = ".") -> str:
|
|
"""Run the project's test suite.
|
|
|
|
Args:
|
|
directory: Project root directory.
|
|
"""
|
|
return _run_project_command(directory, "test")
|
|
|
|
|
|
@tool
|
|
def verify_build(directory: str = ".") -> str:
|
|
"""Run the project's check/lint command to verify correctness.
|
|
|
|
Args:
|
|
directory: Project root directory.
|
|
"""
|
|
return _run_project_command(directory, "check")
|
|
|
|
|
|
@tool
|
|
def execute_command(command: str, directory: str = ".") -> str:
|
|
"""Execute a shell command and return its output.
|
|
|
|
Args:
|
|
command: Shell command to execute.
|
|
directory: Working directory.
|
|
"""
|
|
directory = os.path.expanduser(directory)
|
|
try:
|
|
result = subprocess.run(
|
|
command,
|
|
shell=True,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=directory,
|
|
timeout=120,
|
|
)
|
|
output = (result.stdout + result.stderr).strip()
|
|
if result.returncode != 0:
|
|
return f"Command failed (exit {result.returncode}):\n{output}"
|
|
return output or "(no output)"
|
|
except subprocess.TimeoutExpired:
|
|
return "Command timed out after 120s."
|
|
except Exception as e:
|
|
return f"Error: {e}"
|