mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 13:26:40 +00:00
install.sh --skip-browser / --no-playwright and install.ps1 -SkipBrowser now hand PM `--without agent-browser` again instead of `--without chromium`. The downloaded installer is the newest script, but by default it installs the stable tag, whose PM (v0.22.5) rejects `--without chromium` with "--without accepts only optional default packages (agent-browser, cua-driver)" and exits 1, failing the whole install. The new PM already reads `agent-browser` as `chromium` (pm.defaults.RETIRED_DEFAULTS), so the legacy spelling works against both the stable tag and main. Direct `hermes pm install --without chromium` stays valid on the new PM.
101 lines
4.2 KiB
Python
101 lines
4.2 KiB
Python
"""Optional packages the default install carries, and the user's opt-outs.
|
|
|
|
A ``default`` package (see ``Package.default``) joins the installers' PM stage,
|
|
a bare ``hermes pm install`` and ``hermes update`` on every target it builds
|
|
for. The user can decline one (``install.sh --skip-browser`` /
|
|
``--skip-computer-use``, ``install.ps1 -SkipBrowser`` / ``-SkipComputerUse``,
|
|
``hermes pm install --without NAME``). The choice
|
|
is recorded per installation beside PM's other install state, so a later
|
|
update or bare install never re-adds it. An explicit
|
|
``hermes pm install NAME`` clears it.
|
|
|
|
Stdlib-only apart from PM's own modules: the installers run this before any
|
|
application dependency exists.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Iterable
|
|
from pathlib import Path
|
|
|
|
DECLINED_FILENAME = "declined-packages.json"
|
|
|
|
# A retired default whose opt-out carries over to the package that took its
|
|
# place: `--without agent-browser` declined the browser, which is Chromium now.
|
|
# Read through on every load, so a record written by an older installer keeps
|
|
# skipping the browser. The installers still pass the old name on purpose: the
|
|
# stable PM they install by default accepts only `agent-browser`.
|
|
RETIRED_DEFAULTS = {"agent-browser": "chromium"}
|
|
|
|
|
|
def canonical_default_name(name: str) -> str:
|
|
return RETIRED_DEFAULTS.get(name, name)
|
|
|
|
|
|
def declined_path(project_root: Path | None = None) -> Path:
|
|
from pm.environments import install_state_dir
|
|
from pm.paths import repo_root
|
|
|
|
return install_state_dir(repo_root() if project_root is None else Path(project_root)) / DECLINED_FILENAME
|
|
|
|
|
|
def declined(project_root: Path | None = None) -> frozenset[str]:
|
|
try:
|
|
data = json.loads(declined_path(project_root).read_text(encoding="utf-8-sig"))
|
|
except FileNotFoundError:
|
|
return frozenset()
|
|
except (OSError, ValueError) as exc:
|
|
# Silently treating a damaged record as "nothing declined" would
|
|
# re-add a package the user explicitly refused.
|
|
from pm.package import InstallError
|
|
|
|
raise InstallError("defaults", f"cannot read {declined_path(project_root)}: {exc}",
|
|
"fix or delete the file, then retry") from exc
|
|
names = data.get("declined") if isinstance(data, dict) else None
|
|
return frozenset(canonical_default_name(name) for name in names or () if isinstance(name, str))
|
|
|
|
|
|
def record_declined(*, add: Iterable[str] = (), remove: Iterable[str] = (),
|
|
project_root: Path | None = None) -> frozenset[str]:
|
|
"""Update the recorded opt-outs; returns the new set. Writes only on change."""
|
|
from pm.filesystem import durable_write_bytes
|
|
|
|
current = declined(project_root)
|
|
updated = (current | {canonical_default_name(name) for name in add}) - set(remove)
|
|
if updated != current:
|
|
body = json.dumps({"schema": 1, "declined": sorted(updated)}, indent=2) + "\n"
|
|
durable_write_bytes(declined_path(project_root), body.encode("utf-8"))
|
|
return frozenset(updated)
|
|
|
|
|
|
def default_package_names() -> list[str]:
|
|
"""Every package that may be declined: the ``default`` ones, any target."""
|
|
from pm.registry import all_packages, get_package
|
|
|
|
return [name for name in all_packages() if get_package(name).default and not get_package(name).internal]
|
|
|
|
|
|
def default_packages(names: list[str], *, target: str | None = None,
|
|
declined_names: frozenset[str] | None = None) -> list[str]:
|
|
"""The optional defaults among ``names`` this install should carry.
|
|
|
|
Excludes targets the package has no build for (its ``gaps``) and packages
|
|
the user declined. ``names`` is the lockfile's package list.
|
|
"""
|
|
from pm.registry import get_package
|
|
from pm.store import current_target
|
|
|
|
target = current_target() if target is None else target
|
|
refused = declined() if declined_names is None else declined_names
|
|
selected = []
|
|
for name in names:
|
|
try:
|
|
package = get_package(name)
|
|
except KeyError:
|
|
continue # Lockfile/definition skew mid-update; drift() skips these too.
|
|
if package.default and not package.internal and name not in refused \
|
|
and package.missing_reason(target) is None:
|
|
selected.append(name)
|
|
return selected
|