Files
Tianyi ZhouandClaude Opus 5.5 e9f95675dd Share one host table, ship one payload, split optional deps (#208)
* Share one host table, ship one payload, split optional deps

- tools/hosts.json is the single host path table, read by bin/distilly.mjs
  and tools/hosts.py (install_distilly, install_generated_skill,
  install_claude_generated_skill, skill_writer).
- package.json "files" is the single publish list; the npm CLI and the
  Python installer both derive the install payload from it, so the Python
  installer no longer copies docs/, tests/ or .github/ into skill dirs and
  now supports all eight hosts.
- requirements.txt keeps only requests; collector extras move to
  requirements-optional.txt.
- Ruff is pinned with an explicit rule set and blocks CI; fix its findings.
- tests/_paths.py replaces the sys.path boilerplate in 11 test files.
- Rename the cover image to docs/social-preview.png so redesigns stop
  adding versioned files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AQdagxHQtDiYDXHNMsRSK

* Keep reinstalling from an installed copy working

An installed copy has no package.json, so install_distilly.py copies it
whole (minus caches) instead of failing to read the publish list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AQdagxHQtDiYDXHNMsRSK

* Allow running CI manually

A stacked PR retargeted after its base merges gets no CI run until the
next push; workflow_dispatch lets a maintainer start one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AQdagxHQtDiYDXHNMsRSK

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 13:37:01 +08:00

49 lines
1.4 KiB
Python

"""Host install paths, loaded from hosts.json (shared with bin/distilly.mjs)."""
from __future__ import annotations
import json
import os
from collections.abc import Mapping
from pathlib import Path
HOSTS_FILE = Path(__file__).resolve().with_name("hosts.json")
HOSTS: dict[str, dict] = json.loads(HOSTS_FILE.read_text(encoding="utf-8"))["hosts"]
def _resolve(
host: str,
key: str,
home: Path | None,
environ: Mapping[str, str] | None,
) -> Path:
try:
spec = HOSTS[host]
except KeyError as error:
raise ValueError(f"unsupported host: {host}") from error
parts = spec.get(key) or spec["skills_dir"]
environment = os.environ if environ is None else environ
home_env = spec.get("home_env")
if home_env and environment.get(home_env):
return Path(environment[home_env]).expanduser().joinpath(*parts[1:])
return (home or Path.home()).joinpath(*parts)
def skills_dir(
host: str,
home: Path | None = None,
environ: Mapping[str, str] | None = None,
) -> Path:
"""Return the host's skill root where Distilly itself is installed."""
return _resolve(host, "skills_dir", home, environ)
def generated_skills_dir(
host: str,
home: Path | None = None,
environ: Mapping[str, str] | None = None,
) -> Path:
"""Return the host's skill root for generated person skills."""
return _resolve(host, "generated_skills_dir", home, environ)