Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions omnigent/update_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
# ``direct_url.json`` in place of a URL's userinfo. See
# ``_unredact_ssh_userinfo`` for why we have to repair it.
_REDACTED_USERINFO = "****"
# Homebrew bottles record ``brew`` in ``INSTALLER``; the tap formula is the
# only upgrade source (pip into the Cellar is clobbered on the next bump).
_BREW_TAP_FORMULA = "omnigent-ai/tap/omnigent"


@dataclass
Expand Down Expand Up @@ -1391,12 +1394,13 @@ class _UpgradeSuggestion:
Always populated.
:param runnable: ``True`` when ``command`` is a real shell
invocation we can execute via ``subprocess.run`` —
i.e. the installer is one of uv / pip / pipx / poetry
i.e. the installer is one of uv / pip / pipx / poetry / brew
(or pip-as-fallback for an unknown installer with a known
VCS URL). ``False`` for the unknown-installer prose
fallbacks (``"reinstall X from ..."``) which exist to be
read, not run; the interactive "run this now?" prompt is
suppressed in that case.
fallbacks (``"reinstall X from ..."``) and for requests the
installer cannot honor (a pinned version or added extras on a
Homebrew install), which exist to be read, not run; the
interactive "run this now?" prompt is suppressed in that case.
"""

command: str
Expand Down Expand Up @@ -1468,7 +1472,7 @@ def _build_upgrade_suggestion(
"""Build the right upgrade command for the user's install shape.

Picks based on ``detected_installer`` (uv / pip / pipx / poetry /
unknown) and whether ``direct_url.json`` recorded a VCS URL.
brew / unknown) and whether ``direct_url.json`` recorded a VCS URL.

:param info: Metadata from ``_read_installed_wheel_info``.
:param allow_prerelease: When ``True`` (``omni upgrade --pre``), append
Expand Down Expand Up @@ -1529,6 +1533,28 @@ def _build_upgrade_suggestion(
)

# Registry install — no VCS URL recorded.
if installer == "brew":
# `brew upgrade` takes neither a version pin nor extras, so refuse
# those rather than silently ignoring them.
if target_version:
return _UpgradeSuggestion(
command=(
f"upgrade {_DIST_NAME} with `brew upgrade {_BREW_TAP_FORMULA}` — Homebrew "
f"installs whatever version the tap currently carries and cannot pin "
f"{target_version}"
),
runnable=False,
)
if extras:
return _UpgradeSuggestion(
command=(
f"upgrade {_DIST_NAME} with `brew upgrade {_BREW_TAP_FORMULA}` — the formula "
f"bundles its own extras and cannot add {_extras_str(extras)}"
),
runnable=False,
)
return _UpgradeSuggestion(command=f"brew upgrade {_BREW_TAP_FORMULA}", runnable=True)

registry_spec = _package_spec(version=target_version, extras=extras)
if installer == "uv":
if target_version or extras:
Expand Down
44 changes: 44 additions & 0 deletions tests/cli/test_update_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,8 @@ def test_read_wheel_info_handles_corrupt_direct_url(
# poetry path — included for completeness; poetry is rare for
# CLI tool installs but the format is documented.
("poetry", None, "poetry update omnigent", True),
# Homebrew bottle — upgrades through the tap, never pip into the keg.
("brew", None, "brew upgrade omnigent-ai/tap/omnigent", True),
# Unknown installer WITH a VCS URL — we know the source but
# not the tool, so the suggestion is prose ("reinstall X from
# <url>"), not a command. Must be runnable=False so the
Expand Down Expand Up @@ -1525,6 +1527,48 @@ def test_build_upgrade_suggestion_preserves_uv_tool_extras() -> None:
)


def test_build_upgrade_suggestion_brew_is_runnable() -> None:
"""A Homebrew bottle upgrades through the tap, not through pip.

The bottle records ``brew`` in ``INSTALLER`` and ships no
``direct_url.json``, so it lands on the registry path. Before this branch
existed it fell through to the unknown-installer prose and ``omni
upgrade`` dead-ended with "No automatic upgrade command is known".
"""
suggestion = _build_upgrade_suggestion(_make_info("brew"))
assert suggestion.command == "brew upgrade omnigent-ai/tap/omnigent"
assert suggestion.runnable is True
assert "pip" not in suggestion.command


def test_build_upgrade_suggestion_brew_prerelease_has_no_flag() -> None:
"""``--pre`` is a no-op for brew: ``brew upgrade`` takes no such flag."""
assert (
_build_upgrade_suggestion(_make_info("brew"), allow_prerelease=True).command
== "brew upgrade omnigent-ai/tap/omnigent"
)


def test_build_upgrade_suggestion_brew_refuses_pinned_version() -> None:
"""A pinned version on brew is refused, not silently dropped.

``brew upgrade`` installs whatever the tap carries, so a runnable command
here would land on a different version than ``--target-version`` asked for.
"""
suggestion = _build_upgrade_suggestion(_make_info("brew"), target_version="0.2.0")
assert suggestion.runnable is False
assert "0.2.0" in suggestion.command
assert "brew upgrade omnigent-ai/tap/omnigent" in suggestion.command


def test_build_upgrade_suggestion_brew_refuses_extras() -> None:
"""Extras on brew are refused: the formula bundles its own set."""
suggestion = _build_upgrade_suggestion(_make_info("brew", extras=("all",)))
assert suggestion.runnable is False
assert "[all]" in suggestion.command
assert "brew upgrade omnigent-ai/tap/omnigent" in suggestion.command


def test_build_upgrade_suggestion_uv_target_version() -> None:
"""A pinned target version produces ``install --reinstall`` with the spec."""
assert (
Expand Down
Loading