mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop)
pip and Homebrew are now Unsupported install methods per website/docs/getting-started/platform-support.md. Surface a warn-don't-block deprecation notice everywhere the install method is already shown, pointing at the platform-support docs and noting these installs will not receive further updates. NixOS (Tier 2) is untouched. - hermes_cli/config.py: shared is_unsupported_install_method() / format_unsupported_install_warning() helpers so the wording and docs link stay consistent across every surface. - hermes_cli/banner.py: generalize the existing pip-only banner warning to also cover Homebrew. - hermes_cli/main.py: hermes update and hermes update --check print the warning before proceeding (still update; warn, don't block). - tui_gateway/server.py: session.info gains install_warning. - ui-tui: SessionPanel renders install_warning alongside the existing 'N commits behind' notice. - apps/desktop: SessionRuntimeInfo/GatewayEventPayload gain install_warning; applyRuntimeInfo + the live session.info event fire a snoozable warning toast via a new reportInstallMethodWarning(), mirroring the existing backend-contract-skew toast pattern. i18n strings added for en/zh/zh-hant/ja. - Tests: updated pip banner assertions for the new wording, added a Homebrew banner test, and two tui_gateway session_info tests (install_warning present for pip, absent for git).
This commit is contained in:
parent
528159f7aa
commit
de45b9529d
18 changed files with 197 additions and 14 deletions
|
|
@ -39,6 +39,7 @@ import {
|
|||
import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents'
|
||||
import { clearActiveSessionTodos } from '@/store/todos'
|
||||
import { recordToolDiff } from '@/store/tool-diffs'
|
||||
import { reportInstallMethodWarning } from '@/store/updates'
|
||||
import { notifyWorkspaceChanged, toolMayMutateFiles } from '@/store/workspace-events'
|
||||
import type { RpcEvent } from '@/types/hermes'
|
||||
|
||||
|
|
@ -216,6 +217,10 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
requestDesktopOnboarding(payload.credential_warning)
|
||||
}
|
||||
|
||||
if (apply) {
|
||||
reportInstallMethodWarning(payload?.install_warning)
|
||||
}
|
||||
|
||||
void refreshHermesConfig()
|
||||
|
||||
if (modelChanged || providerChanged) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
setSessions,
|
||||
setYoloActive
|
||||
} from '@/store/session'
|
||||
import { reportBackendContract } from '@/store/updates'
|
||||
import { reportBackendContract, reportInstallMethodWarning } from '@/store/updates'
|
||||
import type { SessionCreateResponse, SessionInfo, SessionRuntimeInfo } from '@/types/hermes'
|
||||
|
||||
import type { ClientSessionState } from '../../../types'
|
||||
|
|
@ -270,6 +270,8 @@ export function applyRuntimeInfo(info: SessionRuntimeInfo | undefined): SessionR
|
|||
requestDesktopOnboarding(info.credential_warning)
|
||||
}
|
||||
|
||||
reportInstallMethodWarning(info.install_warning)
|
||||
|
||||
if (typeof info.model === 'string') {
|
||||
setCurrentModel(info.model)
|
||||
sessionState.model = info.model
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export const en: Translations = {
|
|||
backendOutOfDateTitle: 'Backend out of date',
|
||||
backendOutOfDateMessage:
|
||||
'Your Hermes backend is older than this desktop build and may not work correctly. Update to align them.',
|
||||
installMethodUnsupportedTitle: 'Unsupported install method',
|
||||
updateHermes: 'Update Hermes',
|
||||
updateReadyTitle: 'Update ready',
|
||||
updateReadyMessage: count => `${count} new change${count === 1 ? '' : 's'} available.`,
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ export const ja = defineLocale({
|
|||
backendOutOfDateTitle: 'バックエンドが古いです',
|
||||
backendOutOfDateMessage:
|
||||
'Hermes バックエンドがこのデスクトップビルドより古く、正常に動作しない場合があります。更新して揃えてください。',
|
||||
installMethodUnsupportedTitle: 'サポート対象外のインストール方法',
|
||||
updateHermes: 'Hermes を更新',
|
||||
updateReadyTitle: '更新の準備ができました',
|
||||
updateReadyMessage: count => `${count} 件の新しい変更が利用可能です。`,
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ export interface Translations {
|
|||
copyDetailFailed: string
|
||||
backendOutOfDateTitle: string
|
||||
backendOutOfDateMessage: string
|
||||
installMethodUnsupportedTitle: string
|
||||
updateHermes: string
|
||||
updateReadyTitle: string
|
||||
updateReadyMessage: (count: number) => string
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export const zhHant = defineLocale({
|
|||
copyDetailFailed: '無法複製通知詳情',
|
||||
backendOutOfDateTitle: '後端版本過舊',
|
||||
backendOutOfDateMessage: '您的 Hermes 後端早於目前的桌面版本,可能無法正常運作。請更新以保持一致。',
|
||||
installMethodUnsupportedTitle: '不受支援的安裝方式',
|
||||
updateHermes: '更新 Hermes',
|
||||
updateReadyTitle: '有可用更新',
|
||||
updateReadyMessage: count => `有 ${count} 項新變更可用。`,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export const zh: Translations = {
|
|||
copyDetailFailed: '无法复制通知详情',
|
||||
backendOutOfDateTitle: '后端版本过旧',
|
||||
backendOutOfDateMessage: '你的 Hermes 后端早于当前桌面构建,可能无法正常工作。请更新以保持一致。',
|
||||
installMethodUnsupportedTitle: '不受支持的安装方式',
|
||||
updateHermes: '更新 Hermes',
|
||||
updateReadyTitle: '有可用更新',
|
||||
updateReadyMessage: count => `有 ${count} 项新更改可用。`,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export type GatewayEventPayload = {
|
|||
cwd?: string
|
||||
branch?: string
|
||||
credential_warning?: string
|
||||
install_warning?: string
|
||||
personality?: string
|
||||
usage?: Partial<UsageStats>
|
||||
// agent.terminal.output — live chunk for a read-only agent terminal tab
|
||||
|
|
|
|||
|
|
@ -111,6 +111,24 @@ function isSkewToastSnoozed(): boolean {
|
|||
return Number.isFinite(until) && Date.now() < until
|
||||
}
|
||||
|
||||
const INSTALL_METHOD_TOAST_ID = 'install-method-not-supported'
|
||||
// Same time-based snooze pattern as the update/skew toasts: the warning is
|
||||
// re-derived from every session.info (session.create/resume/activate all
|
||||
// route through applyRuntimeInfo), so without a snooze it would re-pop on
|
||||
// every session switch even right after the user dismissed it.
|
||||
const INSTALL_METHOD_TOAST_SNOOZE_KEY = 'hermes:install-method-toast-snooze-until'
|
||||
const INSTALL_METHOD_TOAST_COOLDOWN_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
function snoozeInstallMethodToast(): void {
|
||||
persistString(INSTALL_METHOD_TOAST_SNOOZE_KEY, String(Date.now() + INSTALL_METHOD_TOAST_COOLDOWN_MS))
|
||||
}
|
||||
|
||||
function isInstallMethodToastSnoozed(): boolean {
|
||||
const until = Number(storedString(INSTALL_METHOD_TOAST_SNOOZE_KEY) || 0)
|
||||
|
||||
return Number.isFinite(until) && Date.now() < until
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard against a desktop GUI talking to a backend that predates its contract
|
||||
* (e.g. a bb/gui-built app pointed at a `main` checkout). Rather than failing
|
||||
|
|
@ -151,6 +169,27 @@ export function reportBackendContract(contract: number | undefined): void {
|
|||
})
|
||||
}
|
||||
|
||||
export function reportInstallMethodWarning(message: string | undefined): void {
|
||||
if (!message) {
|
||||
dismissNotification(INSTALL_METHOD_TOAST_ID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (isInstallMethodToastSnoozed()) {
|
||||
return
|
||||
}
|
||||
|
||||
notify({
|
||||
durationMs: 0,
|
||||
id: INSTALL_METHOD_TOAST_ID,
|
||||
kind: 'warning',
|
||||
message,
|
||||
onDismiss: () => snoozeInstallMethodToast(),
|
||||
title: translateNow('notifications.installMethodUnsupportedTitle')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a toast when an update is available, at most once per cooldown window.
|
||||
* Closing the toast — dismissing it or opening the updates window from it —
|
||||
|
|
|
|||
|
|
@ -399,6 +399,7 @@ export interface SessionRuntimeInfo {
|
|||
cwd?: string
|
||||
desktop_contract?: number
|
||||
fast?: boolean
|
||||
install_warning?: string
|
||||
model?: string
|
||||
personality?: string
|
||||
provider?: string
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
Pure display functions with no HermesCLI state dependency.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -322,7 +321,7 @@ def check_for_updates() -> Optional[int]:
|
|||
# both the Rich banner (build_welcome_banner) and the Ink badge
|
||||
# (branding.tsx, guarded on `typeof === 'number' && > 0`) show nothing.
|
||||
try:
|
||||
from hermes_cli.config import detect_install_method
|
||||
from hermes_cli.config import detect_install_method, get_project_root
|
||||
if detect_install_method() == "docker":
|
||||
return None
|
||||
except Exception:
|
||||
|
|
@ -873,17 +872,23 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
|
|||
except Exception:
|
||||
pass # Never break the banner over an update check
|
||||
|
||||
# Pip-install warning — `pip install hermes-agent` is not the supported
|
||||
# install path (it exists on PyPI for internal/CI reasons, not end users).
|
||||
# Such installs miss the git checkout + installer-managed deps, so updates,
|
||||
# self-update, and issue triage don't behave correctly. Warn, don't block.
|
||||
# Unsupported install-method warning — pip/PyPI and Homebrew are no
|
||||
# longer an officially supported distribution method (see
|
||||
# website/docs/getting-started/platform-support.md). Such installs miss
|
||||
# the git checkout + installer-managed deps, so updates, self-update, and
|
||||
# issue triage don't behave correctly. Warn, don't block. NixOS is fully
|
||||
# supported and never hits this.
|
||||
try:
|
||||
from hermes_cli.config import detect_install_method
|
||||
if detect_install_method() == "pip":
|
||||
from hermes_cli.config import (
|
||||
detect_install_method,
|
||||
format_unsupported_install_warning,
|
||||
is_unsupported_install_method,
|
||||
get_project_root
|
||||
)
|
||||
_install_method = detect_install_method()
|
||||
if is_unsupported_install_method(_install_method):
|
||||
right_lines.append(
|
||||
"[bold yellow]⚠ pip install not officially supported[/]"
|
||||
"[dim yellow] — exists for reasons other than user install; "
|
||||
"expect instability and an inability to support issues[/]"
|
||||
f"[bold yellow]⚠ {format_unsupported_install_warning(_install_method)}[/]"
|
||||
)
|
||||
except Exception:
|
||||
pass # Never break the banner over the install-method check
|
||||
|
|
|
|||
|
|
@ -530,6 +530,50 @@ def recommended_update_command() -> str:
|
|||
return recommended_update_command_for_method(method)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Unsupported install methods (pip, Homebrew) — deprecation notice
|
||||
# =============================================================================
|
||||
#
|
||||
# pip/PyPI and Homebrew are NOT an officially supported distribution method
|
||||
# (see website/docs/getting-started/platform-support.md, "Unsupported"
|
||||
# section). pip exists on PyPI for internal/CI reasons, not end-user installs;
|
||||
# Homebrew is a legacy packaging path. Unlike NixOS/Homebrew "managed mode"
|
||||
# (which hard-blocks config writes), this is a warn-don't-block deprecation
|
||||
# notice surfaced everywhere the user might see install-method state: the CLI
|
||||
# banner, the TUI/desktop session info panel, and ``hermes update``. NixOS
|
||||
# stays fully supported (Tier 2) and must never hit this path.
|
||||
|
||||
PLATFORM_SUPPORT_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/getting-started/platform-support"
|
||||
|
||||
_UNSUPPORTED_INSTALL_METHODS = frozenset({"pip", "homebrew"})
|
||||
|
||||
|
||||
def is_unsupported_install_method(method: str) -> bool:
|
||||
"""Whether ``method`` (from ``detect_install_method()``) is deprecated."""
|
||||
return method in _UNSUPPORTED_INSTALL_METHODS
|
||||
|
||||
|
||||
def unsupported_install_method_label(method: str) -> str:
|
||||
"""Human-readable name for an unsupported install method."""
|
||||
return "pip" if method == "pip" else "Homebrew"
|
||||
|
||||
|
||||
def format_unsupported_install_warning(method: str) -> str:
|
||||
"""Plain-text (no markup) deprecation notice for pip/Homebrew installs.
|
||||
|
||||
Shared verbatim across the CLI banner, TUI/desktop ``session.info``, and
|
||||
``hermes update`` / ``hermes update --check`` so the wording — and the
|
||||
docs link — stays consistent across every surface instead of drifting
|
||||
into three slightly different warnings.
|
||||
"""
|
||||
label = unsupported_install_method_label(method)
|
||||
return (
|
||||
f"{label} installs are no longer an officially supported platform and "
|
||||
f"will not receive further updates. See {PLATFORM_SUPPORT_DOCS_URL} "
|
||||
"for supported install methods."
|
||||
)
|
||||
|
||||
|
||||
# Long-form text for ``hermes update`` / ``--check`` when running inside the
|
||||
# Docker image. Surfaced by ``cmd_update`` and ``_cmd_update_check`` in
|
||||
# hermes_cli/main.py; lives here so the wording stays consistent and we
|
||||
|
|
|
|||
|
|
@ -8375,8 +8375,14 @@ def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
|
|||
on a PyPI install we surface a one-line notice instead of silently
|
||||
dropping the flag.
|
||||
"""
|
||||
from hermes_cli.config import detect_install_method
|
||||
from hermes_cli.config import (
|
||||
detect_install_method,
|
||||
format_unsupported_install_warning,
|
||||
is_unsupported_install_method,
|
||||
)
|
||||
method = detect_install_method(PROJECT_ROOT)
|
||||
if is_unsupported_install_method(method):
|
||||
print(f"⚠ {format_unsupported_install_warning(method)}")
|
||||
if method == "docker":
|
||||
# Docker can't ``git fetch`` from within the container. Surface the
|
||||
# same long-form ``docker pull`` guidance ``hermes update`` (apply
|
||||
|
|
@ -9259,10 +9265,21 @@ def cmd_update(args):
|
|||
from hermes_cli.config import (
|
||||
detect_install_method,
|
||||
format_docker_update_message,
|
||||
format_unsupported_install_warning,
|
||||
is_managed,
|
||||
is_unsupported_install_method,
|
||||
managed_error,
|
||||
)
|
||||
|
||||
# Deprecation notice for pip/Homebrew installs — printed before the
|
||||
# managed-mode early-return below so Homebrew users (who are blocked from
|
||||
# applying the update here) still see it. Warn, don't block: the update
|
||||
# itself still proceeds (except Homebrew, which is managed-mode blocked
|
||||
# for an unrelated reason — brew owns its own upgrade path).
|
||||
_install_method_for_warning = detect_install_method(PROJECT_ROOT)
|
||||
if is_unsupported_install_method(_install_method_for_warning):
|
||||
print(f"⚠ {format_unsupported_install_warning(_install_method_for_warning)}")
|
||||
|
||||
if is_managed():
|
||||
managed_error("update Hermes Agent")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -195,7 +195,33 @@ def test_banner_warns_on_pip_install(tmp_path):
|
|||
out = buf.getvalue()
|
||||
|
||||
assert "officially" in out
|
||||
assert "instability" in out
|
||||
assert "platform-support" in out
|
||||
|
||||
|
||||
def test_banner_warns_on_homebrew_install(tmp_path):
|
||||
"""The welcome banner surfaces a warning when the install method is homebrew."""
|
||||
import io
|
||||
from rich.console import Console
|
||||
from hermes_cli import banner
|
||||
|
||||
hh = tmp_path / ".hermes"
|
||||
hh.mkdir()
|
||||
(hh / ".install_method").write_text("homebrew\n")
|
||||
|
||||
with patch("hermes_cli.config.get_hermes_home", return_value=hh), \
|
||||
patch("hermes_constants.get_hermes_home", return_value=hh):
|
||||
buf = io.StringIO()
|
||||
console = Console(file=buf, width=400, force_terminal=False, color_system=None)
|
||||
banner.build_welcome_banner(
|
||||
console, model="m", cwd="/tmp",
|
||||
tools=[{"function": {"name": "terminal"}}],
|
||||
enabled_toolsets=["terminal"],
|
||||
)
|
||||
out = buf.getvalue()
|
||||
|
||||
assert "officially" in out
|
||||
assert "Homebrew" in out
|
||||
assert "platform-support" in out
|
||||
|
||||
|
||||
def test_banner_no_pip_warning_on_git_install(tmp_path):
|
||||
|
|
|
|||
|
|
@ -4610,6 +4610,25 @@ def test_session_info_includes_session_title(monkeypatch):
|
|||
assert info["title"] == "Dashboard title"
|
||||
|
||||
|
||||
def test_session_info_includes_install_warning_for_pip(monkeypatch):
|
||||
"""pip installs surface install_warning; git installs don't (issue: pip/brew deprecation)."""
|
||||
monkeypatch.setattr("hermes_cli.config.detect_install_method", lambda: "pip")
|
||||
|
||||
info = server._session_info(types.SimpleNamespace(tools=[], model="", provider=""))
|
||||
|
||||
assert "install_warning" in info
|
||||
assert "pip" in info["install_warning"]
|
||||
assert "platform-support" in info["install_warning"]
|
||||
|
||||
|
||||
def test_session_info_omits_install_warning_for_git(monkeypatch):
|
||||
monkeypatch.setattr("hermes_cli.config.detect_install_method", lambda: "git")
|
||||
|
||||
info = server._session_info(types.SimpleNamespace(tools=[], model="", provider=""))
|
||||
|
||||
assert "install_warning" not in info
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# History-mutating commands must reject while session.running is True.
|
||||
# Without these guards, prompt.submit's post-run history write either
|
||||
|
|
|
|||
|
|
@ -3159,6 +3159,18 @@ def _session_info(agent, session: dict | None = None) -> dict:
|
|||
"usage": _get_usage(agent),
|
||||
"profile_name": _current_profile_name(),
|
||||
}
|
||||
try:
|
||||
from hermes_cli.config import (
|
||||
detect_install_method,
|
||||
format_unsupported_install_warning,
|
||||
is_unsupported_install_method,
|
||||
)
|
||||
|
||||
_install_method = detect_install_method()
|
||||
if is_unsupported_install_method(_install_method):
|
||||
info["install_warning"] = format_unsupported_install_warning(_install_method)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from hermes_cli import __version__, __release_date__
|
||||
|
||||
|
|
|
|||
|
|
@ -412,6 +412,12 @@ export function SessionPanel({ info, maxWidth, sid, t }: SessionPanelProps) {
|
|||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{info.install_warning && (
|
||||
<Text bold color={t.color.warn} wrap="wrap">
|
||||
! {info.install_warning}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export interface McpServerStatus {
|
|||
export interface SessionInfo {
|
||||
cwd?: string
|
||||
fast?: boolean
|
||||
install_warning?: string
|
||||
lazy?: boolean
|
||||
mcp_servers?: McpServerStatus[]
|
||||
model: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue