mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop) (#57225)
* 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). * fix(nix): make `hermes` in developement environment actually work install modules as editable overlay with uv * feat: print install method when running --version * fix: correct detect install method when running from a subtree
This commit is contained in:
parent
9de9c25f62
commit
4d7f8ade3e
23 changed files with 340 additions and 95 deletions
2
.envrc
2
.envrc
|
|
@ -1,4 +1,4 @@
|
|||
watch_file pyproject.toml uv.lock
|
||||
watch_file pyproject.toml uv.lock hermes
|
||||
watch_file package-lock.json package.json web/package.json ui-tui/package.json website/package.json apps/shared/package.json apps/desktop/package.json ui-tui/packages/hermes-ink/package.json
|
||||
watch_file flake.nix flake.lock nix/devShell.nix nix/tui.nix nix/package.nix nix/python.nix nix/hermes-agent.nix nix/desktop.nix
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -119,6 +119,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.`,
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export const ja = defineLocale({
|
|||
backendOutOfDateTitle: 'バックエンドが古いです',
|
||||
backendOutOfDateMessage:
|
||||
'Hermes バックエンドがこのデスクトップビルドより古く、正常に動作しない場合があります。更新して揃えてください。',
|
||||
installMethodUnsupportedTitle: 'サポート対象外のインストール方法',
|
||||
updateHermes: 'Hermes を更新',
|
||||
updateReadyTitle: '更新の準備ができました',
|
||||
updateReadyMessage: count => `${count} 件の新しい変更が利用可能です。`,
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ export interface Translations {
|
|||
copyDetailFailed: string
|
||||
backendOutOfDateTitle: string
|
||||
backendOutOfDateMessage: string
|
||||
installMethodUnsupportedTitle: string
|
||||
updateHermes: string
|
||||
updateReadyTitle: string
|
||||
updateReadyMessage: (count: number) => string
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export const zhHant = defineLocale({
|
|||
copyDetailFailed: '無法複製通知詳情',
|
||||
backendOutOfDateTitle: '後端版本過舊',
|
||||
backendOutOfDateMessage: '您的 Hermes 後端早於目前的桌面版本,可能無法正常運作。請更新以保持一致。',
|
||||
installMethodUnsupportedTitle: '不受支援的安裝方式',
|
||||
updateHermes: '更新 Hermes',
|
||||
updateReadyTitle: '有可用更新',
|
||||
updateReadyMessage: count => `有 ${count} 項新變更可用。`,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export const zh: Translations = {
|
|||
copyDetailFailed: '无法复制通知详情',
|
||||
backendOutOfDateTitle: '后端版本过旧',
|
||||
backendOutOfDateMessage: '你的 Hermes 后端早于当前桌面构建,可能无法正常工作。请更新以保持一致。',
|
||||
installMethodUnsupportedTitle: '不受支持的安装方式',
|
||||
updateHermes: '更新 Hermes',
|
||||
updateReadyTitle: '有可用更新',
|
||||
updateReadyMessage: count => `有 ${count} 项新更改可用。`,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,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,8 +321,8 @@ 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
|
||||
if detect_install_method() == "docker":
|
||||
from hermes_cli.config import detect_install_method, get_project_root
|
||||
if detect_install_method(get_project_root()) == "docker":
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -889,17 +888,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(get_project_root())
|
||||
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
|
||||
|
|
|
|||
|
|
@ -441,8 +441,20 @@ def detect_install_method(project_root: Optional[Path] = None) -> str:
|
|||
managed = get_managed_system()
|
||||
if managed:
|
||||
return managed.lower().replace(" ", "-")
|
||||
if (root / ".git").is_dir():
|
||||
|
||||
# detect git repo installs (normal installer, development env)
|
||||
git_path = root / ".git"
|
||||
if git_path.is_dir():
|
||||
return "git"
|
||||
|
||||
# detect git repo installs from worktrees
|
||||
if git_path.is_file():
|
||||
try:
|
||||
content = git_path.read_text(encoding="utf-8").strip()
|
||||
if content.startswith("gitdir:"):
|
||||
return "git"
|
||||
except OSError:
|
||||
pass
|
||||
return "pip"
|
||||
|
||||
|
||||
|
|
@ -528,10 +540,54 @@ def recommended_update_command() -> str:
|
|||
managed_cmd = get_managed_update_command()
|
||||
if managed_cmd:
|
||||
return managed_cmd
|
||||
method = detect_install_method()
|
||||
method = detect_install_method(get_project_root())
|
||||
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
|
||||
|
|
|
|||
|
|
@ -229,9 +229,9 @@ def _read_openai_version_fast() -> str | None:
|
|||
def _print_fast_version_info() -> None:
|
||||
from hermes_cli import __release_date__, __version__
|
||||
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
print(f"Hermes Agent v{__version__} ({__release_date__})")
|
||||
print(f"Project: {project_root}")
|
||||
print(f"Install directory: {PROJECT_ROOT}")
|
||||
|
||||
print(f"Python: {sys.version.split()[0]}")
|
||||
|
||||
openai_version = _read_openai_version_fast()
|
||||
|
|
@ -4329,10 +4329,12 @@ def cmd_import(args):
|
|||
|
||||
|
||||
def _print_version_info(*, check_updates: bool = True) -> None:
|
||||
from hermes_cli.config import detect_install_method
|
||||
from hermes_cli.banner import format_banner_version_label
|
||||
|
||||
print(format_banner_version_label())
|
||||
print(f"Project: {PROJECT_ROOT}")
|
||||
print(f"Install directory: {PROJECT_ROOT}")
|
||||
print(f"Install method: {detect_install_method(PROJECT_ROOT)}")
|
||||
|
||||
# Show Python version
|
||||
print(f"Python: {sys.version.split()[0]}")
|
||||
|
|
@ -8371,8 +8373,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
|
||||
|
|
@ -9255,10 +9263,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
|
||||
|
|
|
|||
|
|
@ -28,13 +28,20 @@
|
|||
packages =
|
||||
with pkgs;
|
||||
[
|
||||
(pkgs.runCommand "hermes" { } ''
|
||||
mkdir -p $out/bin
|
||||
install -Dm755 ${../hermes} $out/bin/hermes
|
||||
'')
|
||||
uv
|
||||
]
|
||||
++ self'.packages.default.passthru.devDeps;
|
||||
shellHook = ''
|
||||
echo "Hermes Agent dev shell"
|
||||
${combinedNonNpm}
|
||||
${hermesNpmLib.mkNpmDevShellHook npmPackageJsonPaths}
|
||||
|
||||
# for the devshell to pick up the src
|
||||
export HERMES_PYTHON_SRC_ROOT=$(git rev-parse --show-toplevel)
|
||||
echo "Hermes Agent dev shell in $HERMES_PYTHON_SRC_ROOT"
|
||||
echo "Ready. Run 'hermes' to start."
|
||||
'';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ let
|
|||
dependency-groups = [ "all" ] ++ extraDependencyGroups;
|
||||
};
|
||||
|
||||
hermesVenv = mkHermesVenv extraDependencyGroups;
|
||||
hermesVenv = (mkHermesVenv extraDependencyGroups).venv;
|
||||
|
||||
hermesNpmLib = callPackage ./lib.nix {
|
||||
inherit npm-lockfile-fix nodejs;
|
||||
|
|
@ -200,33 +200,37 @@ stdenv.mkDerivation (finalAttrs: {
|
|||
runHook postInstall
|
||||
'';
|
||||
|
||||
passthru = {
|
||||
inherit
|
||||
hermesTui
|
||||
hermesWeb
|
||||
hermesNpmLib
|
||||
hermesVenv
|
||||
;
|
||||
passthru =
|
||||
let
|
||||
devPython = (mkHermesVenv (extraDependencyGroups ++ [ "dev" ])).editableVenv;
|
||||
in
|
||||
{
|
||||
inherit
|
||||
hermesTui
|
||||
hermesWeb
|
||||
hermesNpmLib
|
||||
hermesVenv
|
||||
;
|
||||
|
||||
# `hermesDesktop` references `finalAttrs.finalPackage` (this whole
|
||||
# derivation, after all overrides are applied) so the desktop wrapper
|
||||
# can prepend its `/bin` to PATH. The desktop's resolver step 4
|
||||
# ("existing hermes on PATH") then picks up the fully wrapped
|
||||
# `hermes` binary — venv with all deps, bundled skills/plugins,
|
||||
# runtime PATH (ripgrep/git/ffmpeg/etc). No re-implementation
|
||||
# of the agent resolution in the desktop wrapper.
|
||||
hermesDesktop = callPackage ./desktop.nix {
|
||||
inherit hermesNpmLib electron;
|
||||
hermesAgent = finalAttrs.finalPackage;
|
||||
# `hermesDesktop` references `finalAttrs.finalPackage` (this whole
|
||||
# derivation, after all overrides are applied) so the desktop wrapper
|
||||
# can prepend its `/bin` to PATH. The desktop's resolver step 4
|
||||
# ("existing hermes on PATH") then picks up the fully wrapped
|
||||
# `hermes` binary — venv with all deps, bundled skills/plugins,
|
||||
# runtime PATH (ripgrep/git/ffmpeg/etc). No re-implementation
|
||||
# of the agent resolution in the desktop wrapper.
|
||||
hermesDesktop = callPackage ./desktop.nix {
|
||||
inherit hermesNpmLib electron;
|
||||
hermesAgent = finalAttrs.finalPackage;
|
||||
};
|
||||
|
||||
devShellHook = ''
|
||||
export HERMES_PYTHON=${devPython}/bin/python3
|
||||
'';
|
||||
|
||||
devDeps = runtimeDeps ++ [ devPython ];
|
||||
};
|
||||
|
||||
devShellHook = ''
|
||||
export HERMES_PYTHON=${hermesVenv}/bin/python3
|
||||
'';
|
||||
|
||||
devDeps = runtimeDeps ++ [ (mkHermesVenv (extraDependencyGroups ++ [ "dev" ])) ];
|
||||
};
|
||||
|
||||
meta = with lib; {
|
||||
description = "AI agent with advanced tool-calling capabilities";
|
||||
homepage = "https://github.com/NousResearch/hermes-agent";
|
||||
|
|
|
|||
131
nix/python.nix
131
nix/python.nix
|
|
@ -27,7 +27,8 @@ let
|
|||
dependency-groups = { };
|
||||
};
|
||||
|
||||
mkPrebuiltOverride = final: from: dependencies:
|
||||
mkPrebuiltOverride =
|
||||
final: from: dependencies:
|
||||
hacks.nixpkgsPrebuilt {
|
||||
inherit from;
|
||||
prev = {
|
||||
|
|
@ -38,64 +39,100 @@ let
|
|||
|
||||
# Legacy alibabacloud packages ship only sdists with setup.py/setup.cfg
|
||||
# and no pyproject.toml, so setuptools isn't declared as a build dep.
|
||||
buildSystemOverrides = final: prev: builtins.mapAttrs
|
||||
(name: _: prev.${name}.overrideAttrs (old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final.setuptools ];
|
||||
}))
|
||||
(lib.genAttrs [
|
||||
"alibabacloud-credentials-api"
|
||||
"alibabacloud-endpoint-util"
|
||||
"alibabacloud-gateway-dingtalk"
|
||||
"alibabacloud-gateway-spi"
|
||||
"alibabacloud-tea"
|
||||
] (_: null));
|
||||
buildSystemOverrides =
|
||||
final: prev:
|
||||
builtins.mapAttrs
|
||||
(
|
||||
name: _:
|
||||
prev.${name}.overrideAttrs (old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final.setuptools ];
|
||||
})
|
||||
)
|
||||
(
|
||||
lib.genAttrs [
|
||||
"alibabacloud-credentials-api"
|
||||
"alibabacloud-endpoint-util"
|
||||
"alibabacloud-gateway-dingtalk"
|
||||
"alibabacloud-gateway-spi"
|
||||
"alibabacloud-tea"
|
||||
] (_: null)
|
||||
);
|
||||
|
||||
pythonPackageOverrides = final: _prev:
|
||||
if isAarch64Darwin then {
|
||||
numpy = mkPrebuiltOverride final python312.pkgs.numpy { };
|
||||
pythonPackageOverrides =
|
||||
final: _prev:
|
||||
if isAarch64Darwin then
|
||||
{
|
||||
numpy = mkPrebuiltOverride final python312.pkgs.numpy { };
|
||||
|
||||
pyarrow = mkPrebuiltOverride final python312.pkgs.pyarrow { };
|
||||
pyarrow = mkPrebuiltOverride final python312.pkgs.pyarrow { };
|
||||
|
||||
av = mkPrebuiltOverride final python312.pkgs.av { };
|
||||
av = mkPrebuiltOverride final python312.pkgs.av { };
|
||||
|
||||
humanfriendly = mkPrebuiltOverride final python312.pkgs.humanfriendly { };
|
||||
humanfriendly = mkPrebuiltOverride final python312.pkgs.humanfriendly { };
|
||||
|
||||
coloredlogs = mkPrebuiltOverride final python312.pkgs.coloredlogs {
|
||||
humanfriendly = [ ];
|
||||
};
|
||||
coloredlogs = mkPrebuiltOverride final python312.pkgs.coloredlogs {
|
||||
humanfriendly = [ ];
|
||||
};
|
||||
|
||||
onnxruntime = mkPrebuiltOverride final python312.pkgs.onnxruntime {
|
||||
coloredlogs = [ ];
|
||||
numpy = [ ];
|
||||
packaging = [ ];
|
||||
};
|
||||
onnxruntime = mkPrebuiltOverride final python312.pkgs.onnxruntime {
|
||||
coloredlogs = [ ];
|
||||
numpy = [ ];
|
||||
packaging = [ ];
|
||||
};
|
||||
|
||||
ctranslate2 = mkPrebuiltOverride final python312.pkgs.ctranslate2 {
|
||||
numpy = [ ];
|
||||
pyyaml = [ ];
|
||||
};
|
||||
ctranslate2 = mkPrebuiltOverride final python312.pkgs.ctranslate2 {
|
||||
numpy = [ ];
|
||||
pyyaml = [ ];
|
||||
};
|
||||
|
||||
faster-whisper = mkPrebuiltOverride final python312.pkgs.faster-whisper {
|
||||
av = [ ];
|
||||
ctranslate2 = [ ];
|
||||
huggingface-hub = [ ];
|
||||
onnxruntime = [ ];
|
||||
tokenizers = [ ];
|
||||
tqdm = [ ];
|
||||
};
|
||||
} else {};
|
||||
faster-whisper = mkPrebuiltOverride final python312.pkgs.faster-whisper {
|
||||
av = [ ];
|
||||
ctranslate2 = [ ];
|
||||
huggingface-hub = [ ];
|
||||
onnxruntime = [ ];
|
||||
tokenizers = [ ];
|
||||
tqdm = [ ];
|
||||
};
|
||||
}
|
||||
else
|
||||
{ };
|
||||
|
||||
pythonSet =
|
||||
(callPackage pyproject-nix.build.packages {
|
||||
python = python312;
|
||||
}).overrideScope
|
||||
(lib.composeManyExtensions [
|
||||
pyproject-build-systems.overlays.default
|
||||
overlay
|
||||
buildSystemOverrides
|
||||
pythonPackageOverrides
|
||||
]);
|
||||
(
|
||||
lib.composeManyExtensions [
|
||||
pyproject-build-systems.overlays.default
|
||||
overlay
|
||||
buildSystemOverrides
|
||||
pythonPackageOverrides
|
||||
]
|
||||
);
|
||||
|
||||
editableOverlay = workspace.mkEditablePyprojectOverlay {
|
||||
root = "$HERMES_PYTHON_SRC_ROOT"; # resolved at shellHook time
|
||||
};
|
||||
|
||||
workspaceRoot = ./..;
|
||||
editableSet = pythonSet.overrideScope (
|
||||
lib.composeManyExtensions [
|
||||
editableOverlay
|
||||
(final: prev: {
|
||||
hermes-agent = prev.hermes-agent.overrideAttrs (old: {
|
||||
# point straight at the real source instead of the filtered nix store copy
|
||||
src = workspaceRoot;
|
||||
nativeBuildInputs = old.nativeBuildInputs ++ final.resolveBuildSystem { editables = [ ]; };
|
||||
});
|
||||
})
|
||||
]
|
||||
);
|
||||
in
|
||||
pythonSet.mkVirtualEnv "hermes-agent-env" {
|
||||
hermes-agent = dependency-groups;
|
||||
{
|
||||
venv = pythonSet.mkVirtualEnv "hermes-agent-env" {
|
||||
hermes-agent = dependency-groups;
|
||||
};
|
||||
editableVenv = editableSet.mkVirtualEnv "hermes-agent-editable-env" {
|
||||
hermes-agent = dependency-groups;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@ def test_termux_ultrafast_version_runs_before_heavy_startup(
|
|||
|
||||
out = capsys.readouterr().out
|
||||
assert "Hermes Agent v" in out
|
||||
assert "Project:" in out
|
||||
assert "Install directory:" in out
|
||||
assert "Python:" in out
|
||||
assert "OpenAI SDK:" in out
|
||||
|
||||
|
|
|
|||
|
|
@ -4727,6 +4727,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
|
||||
|
|
|
|||
|
|
@ -3238,6 +3238,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