188 lines
5.3 KiB
Python
188 lines
5.3 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from core.tools.base import BaseTool, ToolContext
|
|
from core.tools.registry import registry
|
|
from core.safety import safety
|
|
from core.events import bus
|
|
|
|
|
|
class VenvTool(BaseTool):
|
|
name = "venv"
|
|
description = "Python virtual environment management (create, install, run, list packages)"
|
|
|
|
# =========================================================
|
|
# EXECUTE ROUTER (ONLY ENTRYPOINT WITH ctx)
|
|
# =========================================================
|
|
|
|
def execute(self, payload: dict[str, Any], ctx: ToolContext):
|
|
action = str(payload.get("action", "")).strip()
|
|
|
|
bus.log(
|
|
"VENV",
|
|
"venv_execute",
|
|
"INFO",
|
|
{"action": action}
|
|
)
|
|
|
|
match action:
|
|
case "create":
|
|
return self.create_venv(payload)
|
|
|
|
case "install":
|
|
return self.install_package(payload)
|
|
|
|
case "run":
|
|
return self.run_python(payload)
|
|
|
|
case "list":
|
|
return self.list_packages(payload)
|
|
|
|
case _:
|
|
raise ValueError(f"Unknown venv action: {action}")
|
|
|
|
# =========================================================
|
|
# PATH HELPERS
|
|
# =========================================================
|
|
|
|
def _venv_path(self, path: str) -> Path:
|
|
return safety.validate_path(path)
|
|
|
|
def _python_bin(self, venv: Path) -> Path:
|
|
"""Cross-platform python binary resolution."""
|
|
if (venv / "bin").exists():
|
|
return venv / "bin" / "python"
|
|
return venv / "Scripts" / "python.exe"
|
|
|
|
def _pip_bin(self, venv: Path) -> Path:
|
|
"""Cross-platform pip binary resolution."""
|
|
if (venv / "bin").exists():
|
|
return venv / "bin" / "pip"
|
|
return venv / "Scripts" / "pip.exe"
|
|
|
|
# =========================================================
|
|
# CREATE VENV
|
|
# =========================================================
|
|
|
|
def create_venv(self, payload: dict[str, Any]):
|
|
path = payload.get("path")
|
|
|
|
if not isinstance(path, str):
|
|
raise ValueError("path must be string")
|
|
|
|
venv_path = self._venv_path(path)
|
|
|
|
if venv_path.exists():
|
|
return {
|
|
"status": "exists",
|
|
"path": str(venv_path)
|
|
}
|
|
|
|
subprocess.run(
|
|
[sys.executable, "-m", "venv", str(venv_path)],
|
|
check=True
|
|
)
|
|
|
|
return {
|
|
"status": "created",
|
|
"path": str(venv_path)
|
|
}
|
|
|
|
# =========================================================
|
|
# INSTALL PACKAGE
|
|
# =========================================================
|
|
|
|
def install_package(self, payload: dict[str, Any]):
|
|
path = payload.get("path")
|
|
package = payload.get("package")
|
|
|
|
if not isinstance(path, str):
|
|
raise ValueError("path must be string")
|
|
|
|
if not isinstance(package, str):
|
|
raise ValueError("package must be string")
|
|
|
|
venv_path = self._venv_path(path)
|
|
pip = self._pip_bin(venv_path)
|
|
|
|
if not pip.exists():
|
|
raise ValueError("pip not found in virtual environment")
|
|
|
|
result = subprocess.run(
|
|
[str(pip), "install", package],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
return {
|
|
"status": "ok" if result.returncode == 0 else "error",
|
|
"stdout": result.stdout,
|
|
"stderr": result.stderr
|
|
}
|
|
|
|
# =========================================================
|
|
# RUN PYTHON CODE
|
|
# =========================================================
|
|
|
|
def run_python(self, payload: dict[str, Any]):
|
|
path = payload.get("path")
|
|
code = payload.get("code")
|
|
|
|
if not isinstance(path, str):
|
|
raise ValueError("path must be string")
|
|
|
|
if not isinstance(code, str):
|
|
raise ValueError("code must be string")
|
|
|
|
venv_path = self._venv_path(path)
|
|
python_bin = self._python_bin(venv_path)
|
|
|
|
if not python_bin.exists():
|
|
raise ValueError("python executable not found in venv")
|
|
|
|
result = subprocess.run(
|
|
[str(python_bin), "-c", code],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
return {
|
|
"status": "ok" if result.returncode == 0 else "error",
|
|
"stdout": result.stdout,
|
|
"stderr": result.stderr
|
|
}
|
|
|
|
# =========================================================
|
|
# LIST PACKAGES
|
|
# =========================================================
|
|
|
|
def list_packages(self, payload: dict[str, Any]):
|
|
path = payload.get("path")
|
|
|
|
if not isinstance(path, str):
|
|
raise ValueError("path must be string")
|
|
|
|
venv_path = self._venv_path(path)
|
|
pip = self._pip_bin(venv_path)
|
|
|
|
result = subprocess.run(
|
|
[str(pip), "list"],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
return {
|
|
"status": "ok",
|
|
"output": result.stdout
|
|
}
|
|
|
|
|
|
# =========================================================
|
|
# REGISTER TOOL
|
|
# =========================================================
|
|
|
|
registry.register(VenvTool()) |
