From 3a7ed7be081011cd63db46f7c66a06e8f2ced203 Mon Sep 17 00:00:00 2001 From: LeonSGP43 <154585401+LeonSGP43@users.noreply.github.com> Date: Mon, 18 May 2026 20:52:29 -0700 Subject: [PATCH] fix(packaging): ship bundled skills in wheel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salvages #23738 by @LeonSGP43. Wheel installs were missing skills/ and optional-skills/ because pyproject's [tool.setuptools.packages.find] only includes Python packages — the skills directories don't have __init__.py so they were silently dropped from the wheel. Adds setup.py with data_files spec emitting skills/* and optional-skills/* under hermes_agent-.data/data/, and a get_bundled_skills_dir() helper in hermes_constants that discovers the wheel-installed location via sysconfig before falling back to a source-checkout path. tools/skills_sync uses the helper so 'hermes update' works for pip-installed users. --- hermes_constants.py | 41 +++++++++++++++++++++++++++++++++++++++++ setup.py | 28 ++++++++++++++++++++++++++++ tools/skills_sync.py | 10 ++++------ 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 setup.py diff --git a/hermes_constants.py b/hermes_constants.py index 13df867f5ca..a988fc5fda5 100644 --- a/hermes_constants.py +++ b/hermes_constants.py @@ -5,6 +5,7 @@ without risk of circular imports. """ import os +import sysconfig from contextvars import ContextVar, Token from pathlib import Path @@ -139,6 +140,23 @@ def get_default_hermes_root() -> Path: return env_path +def _get_packaged_data_dir(name: str) -> Path | None: + """Return an installed data-files directory if one exists. + + Used to discover bundled skills/optional-skills when Hermes is installed + from a wheel that emitted them via setuptools data_files. + """ + candidates = [] + for scheme in ("data", "purelib", "platlib"): + raw = sysconfig.get_path(scheme) + if raw: + candidates.append(Path(raw) / name) + for candidate in candidates: + if candidate.exists(): + return candidate + return None + + def get_optional_skills_dir(default: Path | None = None) -> Path: """Return the optional-skills directory, honoring package-manager wrappers. @@ -148,11 +166,34 @@ def get_optional_skills_dir(default: Path | None = None) -> Path: override = os.getenv("HERMES_OPTIONAL_SKILLS", "").strip() if override: return Path(override) + packaged = _get_packaged_data_dir("optional-skills") + if packaged is not None: + return packaged if default is not None: return default return get_hermes_home() / "optional-skills" +def get_bundled_skills_dir(default: Path | None = None) -> Path: + """Return the bundled skills directory for source and packaged installs. + + Resolution order: + 1. ``HERMES_BUNDLED_SKILLS`` env var (Nix wrapper / explicit override) + 2. Wheel-installed ``/skills`` (pip install path) + 3. Caller-supplied ``default`` (typically the source-checkout path) + 4. ``/skills`` last-resort + """ + override = os.getenv("HERMES_BUNDLED_SKILLS", "").strip() + if override: + return Path(override) + packaged = _get_packaged_data_dir("skills") + if packaged is not None: + return packaged + if default is not None: + return default + return get_hermes_home() / "skills" + + def get_hermes_dir(new_subpath: str, old_name: str) -> Path: """Resolve a Hermes subdirectory with backward compatibility. diff --git a/setup.py b/setup.py new file mode 100644 index 00000000000..8487f76e86f --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path + +from setuptools import setup + + +REPO_ROOT = Path(__file__).parent.resolve() + + +def _data_file_tree(root_name: str) -> list[tuple[str, list[str]]]: + root = REPO_ROOT / root_name + grouped: defaultdict[str, list[str]] = defaultdict(list) + for path in sorted(root.rglob("*")): + if not path.is_file(): + continue + rel_path = path.relative_to(REPO_ROOT) + grouped[str(rel_path.parent)].append(str(rel_path)) + return sorted(grouped.items()) + + +setup( + data_files=[ + *_data_file_tree("skills"), + *_data_file_tree("optional-skills"), + ] +) diff --git a/tools/skills_sync.py b/tools/skills_sync.py index 0c65b6281c7..3c2baef0765 100644 --- a/tools/skills_sync.py +++ b/tools/skills_sync.py @@ -26,7 +26,7 @@ import logging import os import shutil from pathlib import Path -from hermes_constants import get_hermes_home +from hermes_constants import get_bundled_skills_dir, get_hermes_home from typing import Dict, List, Tuple from utils import atomic_replace @@ -42,12 +42,10 @@ def _get_bundled_dir() -> Path: """Locate the bundled skills/ directory. Checks HERMES_BUNDLED_SKILLS env var first (set by Nix wrapper), - then falls back to the relative path from this source file. + then a wheel-installed data dir, then falls back to the relative + path from this source file. """ - env_override = os.getenv("HERMES_BUNDLED_SKILLS") - if env_override: - return Path(env_override) - return Path(__file__).parent.parent / "skills" + return get_bundled_skills_dir(Path(__file__).parent.parent / "skills") def _read_manifest() -> Dict[str, str]: