Files
Auditor/theauditor/tools.py

153 lines
5.6 KiB
Python

"""Tool version detection and reporting."""
import json
import os
import subprocess
import tempfile
from pathlib import Path
from typing import Any
def detect_tool_version(cmd: list[str]) -> str:
"""
Detect tool version by running command.
Returns version string or "missing" if not found.
"""
try:
# Use temp files to avoid buffer overflow
with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='_stdout.txt', encoding='utf-8') as stdout_fp, \
tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='_stderr.txt', encoding='utf-8') as stderr_fp:
stdout_path = stdout_fp.name
stderr_path = stderr_fp.name
result = subprocess.run(cmd, stdout=stdout_fp, stderr=stderr_fp, text=True, timeout=5, check=False)
# Read the outputs back
with open(stdout_path, 'r', encoding='utf-8') as f:
result.stdout = f.read()
with open(stderr_path, 'r', encoding='utf-8') as f:
result.stderr = f.read()
# Clean up temp files
os.unlink(stdout_path)
os.unlink(stderr_path)
if result.returncode == 0:
output = result.stdout.strip()
# Extract version from common patterns
# Handle formats like "ruff 0.1.0", "black, 23.1.0", "mypy 1.0.0", "Version 5.0.0"
import re
# Look for version patterns like x.y.z
version_pattern = r"(\d+\.\d+(?:\.\d+)?)"
match = re.search(version_pattern, output)
if match:
return match.group(1)
# Fallback to simple version extraction
parts = output.split()
for part in parts:
if any(c.isdigit() for c in part):
# Found version-like string
version = part.strip(",").strip()
# Clean up common prefixes
if version.startswith("v"):
version = version[1:]
return version
return output.split()[0] if output else "unknown"
return "missing"
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return "missing"
def write_tools_report(out_dir: str) -> dict[str, Any]:
"""
Detect all tool versions and write reports.
Writes:
- {out_dir}/TOOLS.md - Human-readable markdown
- {out_dir}/tools.json - Machine-readable JSON
Returns the tools dictionary.
"""
# Ensure output directory exists
Path(out_dir).mkdir(parents=True, exist_ok=True)
# Detect Python tools
python_tools = {
"ruff": detect_tool_version(["ruff", "--version"]),
"black": detect_tool_version(["black", "--version"]),
"pytest": detect_tool_version(["pytest", "--version"]),
"mypy": detect_tool_version(["mypy", "--version"]),
}
# Detect Node tools from TheAuditor's sandboxed environment
# Check in .auditor_venv/.theauditor_tools/ for our bundled tools
sandbox_base = Path(".auditor_venv/.theauditor_tools")
# Try sandboxed tools first, fallback to system
if sandbox_base.exists():
# Use our sandboxed Node.js and tools
node_exe = sandbox_base / "node-runtime" / ("node.exe" if os.name == "nt" else "bin/node")
npx_exe = sandbox_base / "node-runtime" / ("npx.cmd" if os.name == "nt" else "bin/npx")
if node_exe.exists() and npx_exe.exists():
# Run npx from sandbox with proper paths
node_tools = {
"eslint": detect_tool_version([str(npx_exe), "--prefix", str(sandbox_base), "eslint", "--version"]),
"typescript": detect_tool_version([str(npx_exe), "--prefix", str(sandbox_base), "tsc", "--version"]),
"prettier": detect_tool_version([str(npx_exe), "--prefix", str(sandbox_base), "prettier", "--version"]),
}
else:
# Sandbox exists but Node not found
node_tools = {
"eslint": "missing (sandbox incomplete)",
"typescript": "missing (sandbox incomplete)",
"prettier": "missing (sandbox incomplete)",
}
else:
# No sandbox, check system (fallback)
node_tools = {
"eslint": detect_tool_version(["npx", "eslint", "--version"]),
"typescript": detect_tool_version(["npx", "tsc", "--version"]),
"prettier": detect_tool_version(["npx", "prettier", "--version"]),
}
# Build report structure
tools = {
"python": python_tools,
"node": node_tools,
}
# Write JSON (machine-readable)
json_path = Path(out_dir) / "tools.json"
with open(json_path, "w") as f:
json.dump(tools, f, indent=2, sort_keys=True)
# Write Markdown (human-readable)
md_path = Path(out_dir) / "TOOLS.md"
with open(md_path, "w") as f:
f.write("# Tool Versions Report\n\n")
f.write("## Python Tools\n\n")
f.write("| Tool | Version |\n")
f.write("|------|--------|\n")
for tool, version in sorted(python_tools.items()):
status = "[OK]" if version != "missing" else "[MISSING]"
f.write(f"| {tool} | {status} {version} |\n")
f.write("\n## Node Tools\n\n")
f.write("| Tool | Version |\n")
f.write("|------|--------|\n")
for tool, version in sorted(node_tools.items()):
status = "[OK]" if version != "missing" else "[MISSING]"
f.write(f"| {tool} | {status} {version} |\n")
f.write("\n---\n")
f.write("*Generated by TheAuditor*\n")
return tools