feat(config): add install-method stamping + Docker detection (#27843)
* feat(config): add install-method stamping + Docker detection Dockerfile stamps "docker", install.sh stamps "git", and cmd_postinstall stamps "pip" into ~/.hermes/.install_method. detect_install_method() reads the stamp first, then falls back to managed-system / container / .git heuristics. Adds Docker upgrade guidance. Tracking: #27826 * fix(stamp): move Docker stamp to entrypoint, install.sh stamp after print_success The Dockerfile stamp was overwritten by the VOLUME overlay at container start. Moving it to entrypoint.sh ensures it persists. The install.sh stamp now writes after print_success so it only lands on full success.
This commit is contained in:
@@ -115,5 +115,6 @@ RUN uv pip install --no-cache-dir --no-deps -e "."
|
|||||||
ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist
|
ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist
|
||||||
ENV HERMES_HOME=/opt/data
|
ENV HERMES_HOME=/opt/data
|
||||||
ENV PATH="/opt/data/.local/bin:${PATH}"
|
ENV PATH="/opt/data/.local/bin:${PATH}"
|
||||||
|
RUN mkdir -p /opt/data
|
||||||
VOLUME [ "/opt/data" ]
|
VOLUME [ "/opt/data" ]
|
||||||
ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ]
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ fi
|
|||||||
# --- Running as hermes from here ---
|
# --- Running as hermes from here ---
|
||||||
source "${INSTALL_DIR}/.venv/bin/activate"
|
source "${INSTALL_DIR}/.venv/bin/activate"
|
||||||
|
|
||||||
|
# Stamp install method for detect_install_method()
|
||||||
|
echo "docker" > "${HERMES_HOME:=/opt/data}/.install_method" 2>/dev/null || true
|
||||||
|
|
||||||
# Create essential directory structure. Cache and platform directories
|
# Create essential directory structure. Cache and platform directories
|
||||||
# (cache/images, cache/audio, platforms/whatsapp, etc.) are created on
|
# (cache/images, cache/audio, platforms/whatsapp, etc.) are created on
|
||||||
# demand by the application — don't pre-create them here so new installs
|
# demand by the application — don't pre-create them here so new installs
|
||||||
|
|||||||
@@ -188,21 +188,42 @@ def is_managed() -> bool:
|
|||||||
return get_managed_system() is not None
|
return get_managed_system() is not None
|
||||||
|
|
||||||
|
|
||||||
|
_NIX_UPDATE_MSG = "Update your Nix flake input and rebuild (e.g. nix flake update, nixos-rebuild, or home-manager switch)"
|
||||||
|
|
||||||
|
|
||||||
def get_managed_update_command() -> Optional[str]:
|
def get_managed_update_command() -> Optional[str]:
|
||||||
"""Return the preferred upgrade command for a managed install."""
|
"""Return the preferred upgrade command for a managed install."""
|
||||||
managed_system = get_managed_system()
|
managed_system = get_managed_system()
|
||||||
if managed_system == "Homebrew":
|
if managed_system == "Homebrew":
|
||||||
return "brew upgrade hermes-agent"
|
return "brew upgrade hermes-agent"
|
||||||
if managed_system == "NixOS":
|
if managed_system == "NixOS":
|
||||||
return "sudo nixos-rebuild switch"
|
return _NIX_UPDATE_MSG
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def detect_install_method(project_root: Optional[Path] = None) -> str:
|
def detect_install_method(project_root: Optional[Path] = None) -> str:
|
||||||
"""Detect how Hermes was installed: 'nixos', 'homebrew', 'git', or 'pip'."""
|
"""Detect how Hermes was installed: 'docker', 'nixos', 'homebrew', 'git', or 'pip'.
|
||||||
|
|
||||||
|
Resolution order:
|
||||||
|
1. Stamped ``~/.hermes/.install_method`` file (written by installers)
|
||||||
|
2. HERMES_MANAGED env / .managed marker (NixOS, Homebrew)
|
||||||
|
3. Container detection (/.dockerenv, /run/.containerenv, cgroup)
|
||||||
|
4. .git directory presence -> 'git'
|
||||||
|
5. Fallback -> 'pip'
|
||||||
|
"""
|
||||||
|
stamp = get_hermes_home() / ".install_method"
|
||||||
|
try:
|
||||||
|
method = stamp.read_text(encoding="utf-8").strip().lower()
|
||||||
|
if method:
|
||||||
|
return method
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
managed = get_managed_system()
|
managed = get_managed_system()
|
||||||
if managed:
|
if managed:
|
||||||
return managed.lower().replace(" ", "-")
|
return managed.lower().replace(" ", "-")
|
||||||
|
from hermes_constants import is_container
|
||||||
|
if is_container():
|
||||||
|
return "docker"
|
||||||
if project_root is None:
|
if project_root is None:
|
||||||
project_root = Path(__file__).parent.parent.resolve()
|
project_root = Path(__file__).parent.parent.resolve()
|
||||||
if (project_root / ".git").is_dir():
|
if (project_root / ".git").is_dir():
|
||||||
@@ -210,12 +231,24 @@ def detect_install_method(project_root: Optional[Path] = None) -> str:
|
|||||||
return "pip"
|
return "pip"
|
||||||
|
|
||||||
|
|
||||||
|
def stamp_install_method(method: str) -> None:
|
||||||
|
"""Write the install method to ~/.hermes/.install_method."""
|
||||||
|
stamp = get_hermes_home() / ".install_method"
|
||||||
|
try:
|
||||||
|
stamp.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
stamp.write_text(method + "\n", encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def recommended_update_command_for_method(method: str) -> str:
|
def recommended_update_command_for_method(method: str) -> str:
|
||||||
"""Return the update command for a given install method."""
|
"""Return the update command or guidance for a given install method."""
|
||||||
if method == "nixos":
|
if method == "nixos":
|
||||||
return "sudo nixos-rebuild switch"
|
return _NIX_UPDATE_MSG
|
||||||
if method == "homebrew":
|
if method == "homebrew":
|
||||||
return "brew upgrade hermes-agent"
|
return "brew upgrade hermes-agent"
|
||||||
|
if method == "docker":
|
||||||
|
return "docker pull nousresearch/hermes-agent:latest"
|
||||||
if method == "pip":
|
if method == "pip":
|
||||||
import shutil
|
import shutil
|
||||||
uv = shutil.which("uv")
|
uv = shutil.which("uv")
|
||||||
|
|||||||
@@ -1735,8 +1735,11 @@ def cmd_setup(args):
|
|||||||
|
|
||||||
def cmd_postinstall(args):
|
def cmd_postinstall(args):
|
||||||
"""One-shot bootstrap for pip users: install non-Python deps + run setup."""
|
"""One-shot bootstrap for pip users: install non-Python deps + run setup."""
|
||||||
|
from hermes_cli.config import stamp_install_method
|
||||||
from hermes_cli.dep_ensure import ensure_dependency
|
from hermes_cli.dep_ensure import ensure_dependency
|
||||||
|
|
||||||
|
stamp_install_method("pip")
|
||||||
|
|
||||||
print("⚕ Hermes post-install bootstrap")
|
print("⚕ Hermes post-install bootstrap")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|||||||
@@ -1996,6 +1996,8 @@ main() {
|
|||||||
maybe_start_gateway
|
maybe_start_gateway
|
||||||
|
|
||||||
print_success
|
print_success
|
||||||
|
|
||||||
|
echo "git" > "$HERMES_HOME/.install_method"
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ -n "$ENSURE_DEPS" ]; then
|
if [ -n "$ENSURE_DEPS" ]; then
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
def test_pip_install_detected_when_no_git_dir(tmp_path):
|
def test_pip_install_detected_when_no_git_dir(tmp_path):
|
||||||
"""When PROJECT_ROOT has no .git, detect as pip install."""
|
"""When PROJECT_ROOT has no .git, detect as pip install."""
|
||||||
with patch("hermes_cli.config.get_managed_system", return_value=None):
|
with patch("hermes_cli.config.get_managed_system", return_value=None), \
|
||||||
|
patch("hermes_cli.config.get_hermes_home", return_value=tmp_path):
|
||||||
from hermes_cli.config import detect_install_method
|
from hermes_cli.config import detect_install_method
|
||||||
method = detect_install_method(project_root=tmp_path)
|
method = detect_install_method(project_root=tmp_path)
|
||||||
assert method == "pip"
|
assert method == "pip"
|
||||||
@@ -13,7 +14,8 @@ def test_pip_install_detected_when_no_git_dir(tmp_path):
|
|||||||
def test_git_install_detected_when_git_dir_exists(tmp_path):
|
def test_git_install_detected_when_git_dir_exists(tmp_path):
|
||||||
"""When PROJECT_ROOT has .git, detect as git install."""
|
"""When PROJECT_ROOT has .git, detect as git install."""
|
||||||
(tmp_path / ".git").mkdir()
|
(tmp_path / ".git").mkdir()
|
||||||
with patch("hermes_cli.config.get_managed_system", return_value=None):
|
with patch("hermes_cli.config.get_managed_system", return_value=None), \
|
||||||
|
patch("hermes_cli.config.get_hermes_home", return_value=tmp_path):
|
||||||
from hermes_cli.config import detect_install_method
|
from hermes_cli.config import detect_install_method
|
||||||
method = detect_install_method(project_root=tmp_path)
|
method = detect_install_method(project_root=tmp_path)
|
||||||
assert method == "git"
|
assert method == "git"
|
||||||
@@ -22,7 +24,8 @@ def test_git_install_detected_when_git_dir_exists(tmp_path):
|
|||||||
def test_managed_install_takes_precedence(tmp_path):
|
def test_managed_install_takes_precedence(tmp_path):
|
||||||
"""When HERMES_MANAGED is set, that takes precedence over git detection."""
|
"""When HERMES_MANAGED is set, that takes precedence over git detection."""
|
||||||
(tmp_path / ".git").mkdir()
|
(tmp_path / ".git").mkdir()
|
||||||
with patch("hermes_cli.config.get_managed_system", return_value="NixOS"):
|
with patch("hermes_cli.config.get_managed_system", return_value="NixOS"), \
|
||||||
|
patch("hermes_cli.config.get_hermes_home", return_value=tmp_path):
|
||||||
from hermes_cli.config import detect_install_method
|
from hermes_cli.config import detect_install_method
|
||||||
method = detect_install_method(project_root=tmp_path)
|
method = detect_install_method(project_root=tmp_path)
|
||||||
assert method == "nixos"
|
assert method == "nixos"
|
||||||
@@ -35,3 +38,25 @@ def test_recommended_update_command_pip():
|
|||||||
assert "pip install" in cmd or "uv pip install" in cmd
|
assert "pip install" in cmd or "uv pip install" in cmd
|
||||||
assert "--upgrade" in cmd
|
assert "--upgrade" in cmd
|
||||||
assert "hermes-agent" in cmd
|
assert "hermes-agent" in cmd
|
||||||
|
|
||||||
|
|
||||||
|
def test_stamp_file_takes_precedence(tmp_path):
|
||||||
|
(tmp_path / ".git").mkdir()
|
||||||
|
(tmp_path / ".install_method").write_text("docker\n")
|
||||||
|
with patch("hermes_cli.config.get_managed_system", return_value=None), \
|
||||||
|
patch("hermes_cli.config.get_hermes_home", return_value=tmp_path):
|
||||||
|
from hermes_cli.config import detect_install_method
|
||||||
|
assert detect_install_method(project_root=tmp_path) == "docker"
|
||||||
|
|
||||||
|
|
||||||
|
def test_docker_detected_via_dockerenv(tmp_path):
|
||||||
|
with patch("hermes_cli.config.get_managed_system", return_value=None), \
|
||||||
|
patch("hermes_cli.config.get_hermes_home", return_value=tmp_path), \
|
||||||
|
patch("hermes_constants.is_container", return_value=True):
|
||||||
|
from hermes_cli.config import detect_install_method
|
||||||
|
assert detect_install_method(project_root=tmp_path) == "docker"
|
||||||
|
|
||||||
|
|
||||||
|
def test_recommended_update_command_docker():
|
||||||
|
from hermes_cli.config import recommended_update_command_for_method
|
||||||
|
assert "docker pull" in recommended_update_command_for_method("docker")
|
||||||
|
|||||||
Reference in New Issue
Block a user