diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 93bc9343b4..ea90e05ec0 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -273,11 +273,12 @@ def detect_install_method(project_root: Optional[Path] = None) -> str: ``hermes update`` refuse to run. A legacy ``docker`` value is therefore ignored unless we are really inside a container, and being in a container alone never implies 'docker'. - The supported installs self-identify via the code-scoped stamp: - the curl installer - (scripts/install.sh, the README/website install command) git-clones the repo and stamps ``git`` next to - the code; - the published ``nousresearch/hermes-agent`` image bakes a ``docker`` stamp into - ``/opt/hermes`` at build time. An unsupported manual install dropped into a container (no stamp) falls - through to the ``.git`` checks and behaves like any off-path install. See issue #34397. + Source installers clone a git checkout and publish ``install-stamp.json``; + the ``.git`` fallback identifies it as a source install. Older installations + may carry ``.install_method``, which remains authoritative for compatibility. + The published image bakes a ``docker`` marker into ``/opt/hermes``. A manual + clone in a container still resolves via ``.git``, not container presence alone. + See issue #34397. """ # The stamp is a property of the running code tree (parent of hermes_cli/), NOT of $HERMES_HOME, # so it survives two installs sharing a home. diff --git a/scripts/install.sh b/scripts/install.sh index 4517264ef6..611e376a99 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -31,12 +31,21 @@ INCLUDE_DESKTOP=false while [ $# -gt 0 ]; do case "$1" in - --branch|-Branch) BRANCH="${2:?--branch needs a value}"; shift 2 ;; - --commit|-Commit) INSTALL_COMMIT="$2"; shift 2 ;; - --dir) INSTALL_DIR="$2"; shift 2 ;; - --hermes-home|-HermesHome) HERMES_HOME="$2"; shift 2 ;; + --branch|-Branch|--commit|-Commit|--dir|--hermes-home|-HermesHome|--stage|-Stage) + option="$1" + if [ $# -lt 2 ] || [ -z "$2" ] || [[ "$2" == -* ]]; then + printf '%s needs a value\n' "$option" >&2 + exit 2 + fi + case "$option" in + --branch|-Branch) BRANCH="$2" ;; + --commit|-Commit) INSTALL_COMMIT="$2" ;; + --dir) INSTALL_DIR="$2" ;; + --hermes-home|-HermesHome) HERMES_HOME="$2" ;; + --stage|-Stage) STAGE="$2" ;; + esac + shift 2 ;; --manifest|-Manifest) WANT_MANIFEST=true; shift ;; - --stage|-Stage) STAGE="$2"; shift 2 ;; --json|-Json) JSON=true; shift ;; --non-interactive|-NonInteractive) NON_INTERACTIVE=true; shift ;; --skip-setup) NON_INTERACTIVE=true; shift ;; diff --git a/tests/scripts/install/test_install_sh_reuse_supported_python.py b/tests/scripts/install/test_install_sh_reuse_supported_python.py index 69b474758e..c063695ba0 100644 --- a/tests/scripts/install/test_install_sh_reuse_supported_python.py +++ b/tests/scripts/install/test_install_sh_reuse_supported_python.py @@ -21,6 +21,26 @@ INSTALL_SH = ROOT / "scripts/install.sh" pytestmark = pytest.mark.platforms("posix") +@pytest.mark.parametrize("option", ["--branch", "--commit", "--dir", "--hermes-home", "--stage"]) +@pytest.mark.parametrize("suffix", [[], ["--manifest"], [""]]) +def test_missing_value_fails_before_any_install_work(option: str, suffix: list[str]) -> None: + result = subprocess.run(["bash", str(INSTALL_SH), option, *suffix], + capture_output=True, text=True, timeout=10) + assert result.returncode == 2 + assert result.stderr.strip() == f"{option} needs a value" + assert not result.stdout + + +@pytest.mark.parametrize("flag", ["--skip-browser", "--no-playwright"]) +def test_removed_browser_skip_never_becomes_noninteractive(flag: str) -> None: + result = subprocess.run(["bash", str(INSTALL_SH), flag, "--manifest"], + capture_output=True, text=True, timeout=10) + assert result.returncode == 1 + assert "no longer skips the browser install" in result.stderr + assert "--non-interactive" in result.stderr + assert not result.stdout + + def _environment(tmp_path: Path) -> tuple[Path, Path, dict[str, str]]: core = tmp_path / "checkout" (core / "pm").mkdir(parents=True)