diff --git a/hermes_constants.py b/hermes_constants.py index 13df867f5c..a988fc5fda 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 0000000000..8487f76e86 --- /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 0c65b6281c..3c2baef076 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]: