fix(cli): inline the $HERMES_HOME/npmrc lookup, trim tests to two, document it
Fold the helper into _npm_lifecycle_env itself: the whole fix is one is_file() check plus a setdefault, so a separate function, the try/except around get_hermes_home() (it never raises) and the dict-returning indirection were shape-gate violations. Build the path with os.fspath so it is correct on Windows too. Tests: keep the two invariants (file present -> NPM_CONFIG_USERCONFIG points at it; explicit process/caller value wins and a missing file sets nothing), fold the other two into the negative test. Docs: one paragraph in the desktop troubleshooting page next to ELECTRON_MIRROR describing $HERMES_HOME/npmrc. Refs #106373
This commit is contained in:
@@ -420,37 +420,22 @@ def _ensure_tui_workspace(tui_dir: Path) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _persistent_npm_userconfig() -> Optional[dict[str, str]]:
|
||||
"""``NPM_CONFIG_USERCONFIG`` for ``$HERMES_HOME/npmrc`` when it exists.
|
||||
|
||||
The repo-root ``.npmrc`` is git-tracked, so the updater's autostash parks
|
||||
any mirror/proxy line added there and every update reinstalls without it
|
||||
(restricted networks then prune optional native deps like get-windows and
|
||||
the rebuild fails). ``$HERMES_HOME`` lives outside the git tree, survives
|
||||
autostash/reset, and the update hand-off already carries ``HERMES_HOME``
|
||||
down to every npm child. An explicit ``NPM_CONFIG_USERCONFIG`` wins.
|
||||
"""
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
if os.environ.get("NPM_CONFIG_USERCONFIG", "").strip():
|
||||
return None
|
||||
try:
|
||||
candidate = get_hermes_home() / "npmrc"
|
||||
except Exception:
|
||||
return None
|
||||
if not candidate.is_file():
|
||||
return None
|
||||
return {"NPM_CONFIG_USERCONFIG": str(candidate)}
|
||||
|
||||
|
||||
def _npm_lifecycle_env(env: dict[str, str] | None = None) -> dict[str, str]:
|
||||
"""Build a clean environment for the pinned UI toolchain lifecycle."""
|
||||
run_env = {**os.environ, **(env or {}), "CI": "1"}
|
||||
# esbuild treats this as an executable override. If a shell points it at a
|
||||
# different release, the pinned package's postinstall rejects that binary.
|
||||
run_env.pop("ESBUILD_BINARY_PATH", None)
|
||||
for key, value in (_persistent_npm_userconfig() or {}).items():
|
||||
run_env.setdefault(key, value)
|
||||
# The repo-root ``.npmrc`` is git-tracked, so the updater's autostash parks
|
||||
# any mirror/proxy line added there and every update reinstalls without it
|
||||
# (restricted networks then prune optional native deps like get-windows and
|
||||
# the rebuild fails). ``$HERMES_HOME`` lives outside the git tree and the
|
||||
# update hand-off already carries ``HERMES_HOME`` down to every npm child.
|
||||
# An explicit ``NPM_CONFIG_USERCONFIG`` wins (#106373).
|
||||
from hermes_constants import get_hermes_home
|
||||
npmrc = get_hermes_home() / "npmrc"
|
||||
if npmrc.is_file():
|
||||
run_env.setdefault("NPM_CONFIG_USERCONFIG", os.fspath(npmrc))
|
||||
return run_env
|
||||
|
||||
|
||||
|
||||
@@ -781,56 +781,34 @@ def test_make_tui_argv_omits_workspace_and_scrubs_esbuild_override(
|
||||
|
||||
|
||||
class TestPersistentNpmUserconfig:
|
||||
"""$HERMES_HOME/npmrc must reach every npm lifecycle child process."""
|
||||
"""$HERMES_HOME/npmrc must reach every npm lifecycle child process (#106373)."""
|
||||
|
||||
def test_hermes_home_npmrc_sets_userconfig(self, tmp_path, monkeypatch):
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
(home / "npmrc").write_text(
|
||||
"node_get_windows_binary_host_mirror=https://ghproxy.net/https://github.com/sindresorhus/get-windows/releases/download/\n",
|
||||
"node_get_windows_binary_host_mirror=https://mirror.example/get-windows/\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.delenv("NPM_CONFIG_USERCONFIG", raising=False)
|
||||
monkeypatch.delenv("ESBUILD_BINARY_PATH", raising=False)
|
||||
|
||||
env = main_tui_launch._npm_lifecycle_env()
|
||||
|
||||
assert env["NPM_CONFIG_USERCONFIG"] == str(home / "npmrc")
|
||||
assert env["NPM_CONFIG_USERCONFIG"] == os.fspath(home / "npmrc")
|
||||
|
||||
def test_missing_npmrc_leaves_env_untouched(self, tmp_path, monkeypatch):
|
||||
def test_explicit_userconfig_wins_and_missing_file_sets_nothing(self, tmp_path, monkeypatch):
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.delenv("NPM_CONFIG_USERCONFIG", raising=False)
|
||||
|
||||
env = main_tui_launch._npm_lifecycle_env()
|
||||
assert "NPM_CONFIG_USERCONFIG" not in main_tui_launch._npm_lifecycle_env()
|
||||
|
||||
assert "NPM_CONFIG_USERCONFIG" not in env
|
||||
|
||||
def test_explicit_userconfig_wins(self, tmp_path, monkeypatch):
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
(home / "npmrc").write_text(
|
||||
"registry=https://example.invalid\n", encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
(home / "npmrc").write_text("registry=https://example.invalid\n", encoding="utf-8")
|
||||
monkeypatch.setenv("NPM_CONFIG_USERCONFIG", str(tmp_path / "custom-npmrc"))
|
||||
assert main_tui_launch._npm_lifecycle_env()["NPM_CONFIG_USERCONFIG"] == str(tmp_path / "custom-npmrc")
|
||||
|
||||
env = main_tui_launch._npm_lifecycle_env()
|
||||
|
||||
assert env["NPM_CONFIG_USERCONFIG"] == str(tmp_path / "custom-npmrc")
|
||||
|
||||
def test_caller_env_does_not_override_userconfig(self, tmp_path, monkeypatch):
|
||||
env_in = {"NPM_CONFIG_USERCONFIG": "/from-caller/npmrc"}
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
(home / "npmrc").write_text(
|
||||
"registry=https://example.invalid\n", encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.delenv("NPM_CONFIG_USERCONFIG", raising=False)
|
||||
|
||||
env = main_tui_launch._npm_lifecycle_env(env_in)
|
||||
|
||||
monkeypatch.delenv("NPM_CONFIG_USERCONFIG")
|
||||
env = main_tui_launch._npm_lifecycle_env({"NPM_CONFIG_USERCONFIG": "/from-caller/npmrc"})
|
||||
assert env["NPM_CONFIG_USERCONFIG"] == "/from-caller/npmrc"
|
||||
|
||||
@@ -539,6 +539,8 @@ ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ \
|
||||
bash -c 'cd "$HOME/.hermes/hermes-agent/apps/desktop" && CSC_IDENTITY_AUTO_DISCOVERY=false npm run pack'
|
||||
```
|
||||
|
||||
**Other native downloads (e.g. the `get-windows` prebuilt on Windows) that need a mirror:** put the npm keys in `$HERMES_HOME/npmrc` (`%LOCALAPPDATA%\hermes\npmrc` on Windows, `~/.hermes/npmrc` elsewhere) — for example `node_get_windows_binary_host_mirror=https://<mirror>/sindresorhus/get-windows/releases/download/`. Every `npm ci`/`npm run` the updater spawns (desktop, web and TUI builds) points `NPM_CONFIG_USERCONFIG` at that file when it exists, so the config survives `hermes update`; the repo-root `.npmrc` is git-tracked and gets autostashed on every update, and `~/.npmrc` may be missed because the desktop hand-off inherits the GUI's environment. An `NPM_CONFIG_USERCONFIG` you set yourself is never overridden.
|
||||
|
||||
To clear a corrupt cached zip by hand:
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user