Computer use and browser use are meant to work out of the box. The PM rewrite (3d12e86ef1) and the MSIX installer rework (47f4ab3a17) dropped the install-time cua-driver fetch (7060ac7bed) and the Browser Use CLI install (baa6b2e34d). On a fresh install the computer_use check_fn therefore stayed False, so the tool never reached the model and its lazy ensure could not fire, and browser_exec quietly fell back to the built-in tools. - cua-driver is a default PM package, so the installers, a bare `hermes pm install` and `hermes update` carry it on every target it builds for. Adds the missing Android gap (the lock has no bionic artifact). - The shared default-tool step, which the installers (via source completion) and `hermes update` both run, provisions the Browser Use CLI for the default and explicit Browser Use backends. `--skip-browser` declines it along with agent-browser, and `off`/Camofox never use it. - Installers regain --skip-computer-use / -SkipComputerUse (recorded as `--without cua-driver`). - The update message stops calling every default "browser tools".
90 lines
3.6 KiB
Python
90 lines
3.6 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"
|
|
|
|
|
|
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(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 | set(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
|