Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions hud/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ def debug(

from .utils.environment import (
build_environment,
find_dockerfile,
get_image_name,
image_exists,
is_environment_directory,
)

hud_console = HUDConsole()
Expand Down Expand Up @@ -224,8 +224,9 @@ def debug(
first_param = params[0]
docker_args = params[1:] if len(params) > 1 else []

# Check if it's a directory
if Path(first_param).exists() and is_environment_directory(first_param):
# Check if it's a directory with a Dockerfile
p = Path(first_param)
if p.exists() and p.is_dir() and find_dockerfile(p) is not None:
# Directory mode - like hud dev
directory = first_param

Expand Down
33 changes: 33 additions & 0 deletions hud/cli/tests/test_debug_directory_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from pathlib import Path


def test_hud_debug_directory_mode_accepts_dockerfile_hud_without_pyproject(
tmp_path: Path, monkeypatch
) -> None:
"""Test that hud debug . works with Dockerfile.hud and no pyproject.toml."""
(tmp_path / "Dockerfile.hud").write_text("FROM python:3.11\n", encoding="utf-8")
monkeypatch.chdir(tmp_path)

import hud.cli.__init__ as cli
from hud.cli.utils import environment as env_utils

monkeypatch.setattr(env_utils, "image_exists", lambda _image: True)

captured: dict[str, object] = {}

async def _fake_debug_mcp_stdio(command, logger, max_phase: int = 5) -> int: # type: ignore[no-untyped-def]
captured["command"] = command
return max_phase

monkeypatch.setattr(cli, "debug_mcp_stdio", _fake_debug_mcp_stdio)
cli.debug(params=["."], config=None, cursor=None, build=False, max_phase=1)

command = captured["command"]
assert isinstance(command, list)
expected_name = tmp_path.name.replace("_", "-")
assert command[-1] == f"{expected_name}:dev"
29 changes: 26 additions & 3 deletions hud/cli/utils/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,22 @@ def build_environment(directory: str | Path, image_name: str, no_cache: bool = F
Returns:
True if build succeeded, False otherwise
"""
dir_path = Path(directory)
dockerfile_path = find_dockerfile(dir_path)

build_cmd = ["docker", "build", "-t", image_name]

# Specify the Dockerfile path if using Dockerfile.hud
if dockerfile_path is not None and dockerfile_path.name != "Dockerfile":
build_cmd.extend(["-f", str(dockerfile_path)])

if no_cache:
build_cmd.append("--no-cache")
build_cmd.append(str(directory))

hud_console.info(f"🔨 Building image: {image_name}{' (no cache)' if no_cache else ''}")
if dockerfile_path is not None and dockerfile_path.name != "Dockerfile":
hud_console.info(f"Using {dockerfile_path.name}")
hud_console.info("") # Empty line before Docker output

# Just run Docker build directly - it has its own nice live display
Expand All @@ -110,20 +120,33 @@ def image_exists(image_name: str) -> bool:
return result.returncode == 0


def find_dockerfile(directory: Path) -> Path | None:
"""Find Dockerfile in a directory, preferring Dockerfile.hud over Dockerfile."""
hud_dockerfile = directory / "Dockerfile.hud"
if hud_dockerfile.exists():
return hud_dockerfile

standard_dockerfile = directory / "Dockerfile"
if standard_dockerfile.exists():
return standard_dockerfile

return None


def is_environment_directory(path: str | Path) -> bool:
"""Check if a path looks like an environment directory.

An environment directory should have:
- A Dockerfile
- A Dockerfile (Dockerfile.hud or Dockerfile)
- A pyproject.toml file
- Optionally a src directory
"""
dir_path = Path(path)
if not dir_path.is_dir():
return False

# Must have Dockerfile
if not (dir_path / "Dockerfile").exists():
# Must have Dockerfile.hud or Dockerfile
if find_dockerfile(dir_path) is None:
return False

# Must have pyproject.toml
Expand Down
41 changes: 40 additions & 1 deletion hud/cli/utils/tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

from hud.cli.utils.environment import get_image_name, image_exists, is_environment_directory
from hud.cli.utils.environment import (
find_dockerfile,
get_image_name,
image_exists,
is_environment_directory,
)

if TYPE_CHECKING:
from pathlib import Path
Expand Down Expand Up @@ -36,6 +41,40 @@ def test_is_environment_directory(tmp_path: Path):
assert is_environment_directory(d) is True


def test_is_environment_directory_with_dockerfile_hud(tmp_path: Path):
"""Test that Dockerfile.hud is recognized as a valid environment directory."""
d = tmp_path / "env"
d.mkdir()
assert is_environment_directory(d) is False
# Use Dockerfile.hud instead of Dockerfile
(d / "Dockerfile.hud").write_text("FROM python:3.11")
assert is_environment_directory(d) is False
(d / "pyproject.toml").write_text("[tool.hud]")
assert is_environment_directory(d) is True


def test_find_dockerfile_prefers_dockerfile_hud(tmp_path: Path):
"""Test that Dockerfile.hud is preferred over Dockerfile."""
d = tmp_path / "env"
d.mkdir()
# No Dockerfile
assert find_dockerfile(d) is None
# Add Dockerfile
(d / "Dockerfile").write_text("FROM python:3.11")
assert find_dockerfile(d) == d / "Dockerfile"
# Add Dockerfile.hud - should now be preferred
(d / "Dockerfile.hud").write_text("FROM python:3.12")
assert find_dockerfile(d) == d / "Dockerfile.hud"


def test_find_dockerfile_only_dockerfile_hud(tmp_path: Path):
"""Test that Dockerfile.hud alone is found."""
d = tmp_path / "env"
d.mkdir()
(d / "Dockerfile.hud").write_text("FROM python:3.11")
assert find_dockerfile(d) == d / "Dockerfile.hud"


@patch("subprocess.run")
def test_image_exists_true(mock_run):
mock_run.return_value = MagicMock(returncode=0)
Expand Down
Loading