← refacto-feature-scout
"""Work out how a project is built, tested and put live, so nobody has to be asked.
Every answer here is a suggestion. Setup shows them and the person confirms — this file is
never the last word, because guessing wrong about how to check work is worse than asking.
"""
from __future__ import annotations
import json
import re
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
# npm writes this into every new project; running it fails on purpose.
NPM_PLACEHOLDER = "no test specified"
DOC_NAMES = ("CLAUDE.md", "AGENTS.md", "README.md", "PRD.md", "ARCHITECTURE.md", "CONTRIBUTING.md")
@dataclass
class Found:
test_commands: list[str] = field(default_factory=list) # best guess first
extra_checks: list[str] = field(default_factory=list)
install_hint: str = ""
link_dirs: tuple[str, ...] = ()
context_files: list[str] = field(default_factory=list)
deploy_command: str | None = None
language: str = ""
@property
def test_command(self) -> str:
return self.test_commands[0] if self.test_commands else ""
def _read_json(path: Path) -> dict:
try:
data = json.loads(path.read_text())
return data if isinstance(data, dict) else {}
except (OSError, json.JSONDecodeError):
return {}
def _read_toml(path: Path) -> dict:
try:
return tomllib.loads(path.read_text())
except (OSError, tomllib.TOMLDecodeError):
return {}
def _node(folder: Path, found: Found) -> None:
pkg = _read_json(folder / "package.json")
if not pkg:
return
found.language = "node"
scripts = pkg.get("scripts") or {}
test = str(scripts.get("test") or "")
if test and NPM_PLACEHOLDER not in test:
found.test_commands.append("npm test")
for name in ("typecheck", "lint", "types", "check"):
if scripts.get(name):
found.extra_checks.append(f"npm run {name}")
runner = "npm ci"
for lock, cmd in (("pnpm-lock.yaml", "pnpm install --frozen-lockfile"),
("yarn.lock", "yarn install --frozen-lockfile"),
("bun.lockb", "bun install"),
("package-lock.json", "npm ci")):
if (folder / lock).exists():
runner = cmd
break
found.install_hint = runner
links = ["node_modules"]
for spot in (pkg.get("workspaces") or []):
if isinstance(spot, str) and "*" not in spot and (folder / spot / "package.json").exists():
links.append(f"{spot}/node_modules")
for extra in ("web", "app", "client", "server"):
if (folder / extra / "package.json").exists() and f"{extra}/node_modules" not in links:
links.append(f"{extra}/node_modules")
found.link_dirs = tuple(links)
def _python(folder: Path, found: Found) -> None:
pyproject = _read_toml(folder / "pyproject.toml")
looks_python = bool(pyproject) or (folder / "setup.py").exists() or (folder / "tests").is_dir()
if not looks_python:
return
found.language = found.language or "python"
tool = pyproject.get("tool") or {}
project = pyproject.get("project") or {}
text = json.dumps(pyproject)
uses_pytest = "pytest" in tool or "pytest" in text or (folder / "tests").is_dir()
if not uses_pytest:
return
if (folder / "uv.lock").exists():
found.test_commands.append("uv run pytest -q")
found.install_hint = found.install_hint or "uv sync"
elif (folder / "poetry.lock").exists():
found.test_commands.append("poetry run pytest -q")
found.install_hint = found.install_hint or "poetry install"
elif (folder / ".venv" / "bin" / "python").exists():
found.test_commands.append(".venv/bin/python -m pytest -q")
elif (folder / ".venv" / "Scripts" / "python.exe").exists():
found.test_commands.append(r".venv\Scripts\python -m pytest -q")
else:
found.test_commands.append("python -m pytest -q")
if project.get("dependencies") or (folder / "requirements.txt").exists():
found.install_hint = found.install_hint or "pip install -e ."
def _makefile(folder: Path, found: Found) -> None:
path = folder / "Makefile"
if not path.exists():
return
try:
text = path.read_text()
except OSError:
return
if re.search(r"^test\s*:", text, re.M):
found.test_commands.append("make test")
def _others(folder: Path, found: Found) -> None:
simple = [
("Cargo.toml", "cargo test", "rust", "cargo build"),
("go.mod", "go test ./...", "go", ""),
("mix.exs", "mix test", "elixir", "mix deps.get"),
("composer.json", "composer test", "php", "composer install"),
("pom.xml", "mvn -q test", "java", ""),
("build.gradle", "./gradlew test", "java", ""),
("build.gradle.kts", "./gradlew test", "java", ""),
]
for name, command, language, install in simple:
if (folder / name).exists():
found.test_commands.append(command)
found.language = found.language or language
found.install_hint = found.install_hint or install
if (folder / "Gemfile").exists() and (folder / "spec").is_dir():
found.test_commands.append("bundle exec rspec")
found.language = found.language or "ruby"
found.install_hint = found.install_hint or "bundle install"
if any(folder.glob("*.sln")) or any(folder.glob("*.csproj")):
found.test_commands.append("dotnet test")
found.language = found.language or "dotnet"
if (folder / "Package.swift").exists():
found.test_commands.append("swift test")
found.language = found.language or "swift"
def _deploy(folder: Path, found: Found) -> None:
if (folder / "fly.toml").exists():
found.deploy_command = "fly deploy --remote-only"
elif (folder / "vercel.json").exists():
found.deploy_command = "vercel --prod"
def _docs(folder: Path, found: Found) -> None:
files = [name for name in DOC_NAMES if (folder / name).exists()]
docs = sorted((p for p in (folder / "docs").glob("*.md") if p.is_file()),
key=lambda p: p.stat().st_size, reverse=True)[:2] if (folder / "docs").is_dir() else []
found.context_files = files + [f"docs/{p.name}" for p in docs]
def inspect(folder: Path) -> Found:
"""Everything worth guessing about a project, from what's on disk."""
folder = Path(folder)
found = Found()
for step in (_node, _python, _makefile, _others, _deploy, _docs):
step(folder, found)
seen: set[str] = set()
found.test_commands = [c for c in found.test_commands if not (c in seen or seen.add(c))]
return found
def is_git_project(folder: Path) -> bool:
return (Path(folder) / ".git").exists()