From 3a26d4909d726530f3518fe7e36d728dfd583994 Mon Sep 17 00:00:00 2001 From: "Derek Palmer (Creative)" Date: Fri, 29 May 2026 16:11:56 -0400 Subject: [PATCH] refactor(distribution): add Distribution Inventory as single source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralize distribution artifact identity and install policy — canonical skill path, distributed copy paths, marketplace manifest path, managed-region markers, and default install-destination templates — in codeforerunner.distribution. installer, doctor, and the skill-copy validator now consult the inventory instead of re-declaring constants, so a packaging change is one edit. Install behavior is unchanged. Closes #50 --- scripts/validate_skill_copies.py | 17 ++++++--- src/codeforerunner/distribution.py | 56 ++++++++++++++++++++++++++++++ src/codeforerunner/doctor.py | 22 ++++++------ src/codeforerunner/installer.py | 23 ++++++------ tests/test_distribution.py | 54 ++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 28 deletions(-) create mode 100644 src/codeforerunner/distribution.py create mode 100644 tests/test_distribution.py diff --git a/scripts/validate_skill_copies.py b/scripts/validate_skill_copies.py index 6592f4d..e6d2f64 100755 --- a/scripts/validate_skill_copies.py +++ b/scripts/validate_skill_copies.py @@ -7,11 +7,18 @@ import sys ROOT = Path(__file__).resolve().parents[1] -CANONICAL = Path("agent/codeforerunner.skill.md") -COPIES = [ - Path("plugins/codeforerunner/skills/codeforerunner/SKILL.md"), - Path("skills/codeforerunner/SKILL.md"), -] + +# Source the canonical/copy paths from the Distribution Inventory so packaging +# changes are a single edit. Add src/ to the path for standalone runs (no +# installed package required). +sys.path.insert(0, str(ROOT / "src")) +from codeforerunner.distribution import ( # noqa: E402 + CANONICAL_SKILL_REL, + DISTRIBUTED_SKILL_COPIES_REL, +) + +CANONICAL = CANONICAL_SKILL_REL +COPIES = list(DISTRIBUTED_SKILL_COPIES_REL) def strip_frontmatter(text: str) -> str: diff --git a/src/codeforerunner/distribution.py b/src/codeforerunner/distribution.py new file mode 100644 index 0000000..2630947 --- /dev/null +++ b/src/codeforerunner/distribution.py @@ -0,0 +1,56 @@ +"""Distribution Inventory — single source of truth for distribution artifact +identity and install policy. + +Owns the packaging facts that were previously duplicated across the installer, +doctor, and validation scripts: the canonical skill path, its distributed copy +paths, the Codex marketplace manifest path, the managed-region markers, and the +default install-destination templates. Consumers consult this module instead of +re-declaring constants, so a packaging change is one edit here. + +Mirrors the Task Registry (``tasks.py``) and Release Surface Manifest +(``release_surfaces.py``) single-source pattern. +""" + +from __future__ import annotations + +from pathlib import Path + +# --- artifact identity (repo-relative) ------------------------------------ + +# Source of truth for the codeforerunner skill body; copies derive from it. +CANONICAL_SKILL_REL = Path("agent/codeforerunner.skill.md") + +# Distributed copies whose bodies must match the canonical (V10 body parity). +DISTRIBUTED_SKILL_COPIES_REL: tuple[Path, ...] = ( + Path("plugins/codeforerunner/skills/codeforerunner/SKILL.md"), + Path("skills/codeforerunner/SKILL.md"), +) + +# Codex marketplace manifest shipped as a release asset. +MARKETPLACE_MANIFEST_REL = Path("plugins/codex/marketplace.json") + +# --- managed-region markers ----------------------------------------------- + +# Delimit the installer-owned region in a destination file so re-runs are +# idempotent and unmanaged content is never clobbered. +MARKER_BEGIN = "" +MARKER_END = "" + +# --- install-destination templates ---------------------------------------- + +# Agents whose default skill destination the inventory can resolve. +SKILL_DEST_AGENTS: tuple[str, ...] = ("codex", "claude") + + +def skill_destination(agent: str, slug: str, home: Path) -> Path: + """Default install path for skill ``slug`` under ``agent``, relative to ``home``.""" + if agent == "codex": + return home / f".codex/skills/{slug}/SKILL.md" + if agent == "claude": + return home / f".claude/plugins/codeforerunner/skills/{slug}/SKILL.md" + raise ValueError(f"no default skill destination for agent {agent!r} (expected: {', '.join(SKILL_DEST_AGENTS)})") + + +def marketplace_destination(home: Path) -> Path: + """Default install path for the Codex marketplace manifest, relative to ``home``.""" + return home / ".codex/marketplaces/codeforerunner.json" diff --git a/src/codeforerunner/doctor.py b/src/codeforerunner/doctor.py index 160d3f5..9aeafd1 100644 --- a/src/codeforerunner/doctor.py +++ b/src/codeforerunner/doctor.py @@ -11,17 +11,17 @@ from pathlib import Path from typing import Callable +from codeforerunner import distribution as _dist from codeforerunner.config import CONFIG_FILENAME, ConfigError, load_from_repo -CANONICAL_REL = Path("agent/codeforerunner.skill.md") -SKILL_COPIES_REL: tuple[Path, ...] = ( - Path("plugins/codeforerunner/skills/codeforerunner/SKILL.md"), - Path("skills/codeforerunner/SKILL.md"), -) -MARKETPLACE_REL = Path("plugins/codex/marketplace.json") +# Distribution artifact identity and markers come from the Distribution +# Inventory; re-exported here for callers/tests that import them off doctor. +CANONICAL_REL = _dist.CANONICAL_SKILL_REL +SKILL_COPIES_REL: tuple[Path, ...] = _dist.DISTRIBUTED_SKILL_COPIES_REL +MARKETPLACE_REL = _dist.MARKETPLACE_MANIFEST_REL -MARKER_BEGIN = "" -MARKER_END = "" +MARKER_BEGIN = _dist.MARKER_BEGIN +MARKER_END = _dist.MARKER_END @dataclass(frozen=True) @@ -37,14 +37,14 @@ def _installed_skill_destinations() -> list[Path]: """Return default install paths for the codeforerunner skill across supported agents.""" home = Path(os.path.expanduser("~")) return [ - home / ".codex/skills/codeforerunner/SKILL.md", - home / ".claude/plugins/codeforerunner/skills/codeforerunner/SKILL.md", + _dist.skill_destination(agent, "codeforerunner", home) + for agent in _dist.SKILL_DEST_AGENTS ] def _installed_marketplace_destination() -> Path: """Return default install path for the Codex marketplace manifest.""" - return Path(os.path.expanduser("~")) / ".codex/marketplaces/codeforerunner.json" + return _dist.marketplace_destination(Path(os.path.expanduser("~"))) def _load_script_module(repo: Path, relpath: str, module_name: str): diff --git a/src/codeforerunner/installer.py b/src/codeforerunner/installer.py index f1714b3..f7a764d 100644 --- a/src/codeforerunner/installer.py +++ b/src/codeforerunner/installer.py @@ -10,13 +10,16 @@ from pathlib import Path from typing import Iterable, Literal +from codeforerunner import distribution as _dist from codeforerunner.tasks import installable_slugs as _installable_slugs -MARKER_BEGIN = "" -MARKER_END = "" +# Distribution artifact identity and markers come from the Distribution +# Inventory; re-exported here for callers/tests that import them off installer. +MARKER_BEGIN = _dist.MARKER_BEGIN +MARKER_END = _dist.MARKER_END -CANONICAL_REL = Path("agent/codeforerunner.skill.md") -MARKETPLACE_REL = Path("plugins/codex/marketplace.json") +CANONICAL_REL = _dist.CANONICAL_SKILL_REL +MARKETPLACE_REL = _dist.MARKETPLACE_MANIFEST_REL EXIT_OK = 0 EXIT_USAGE = 2 @@ -48,10 +51,8 @@ def resolve_target(agent: str, override: Path | None) -> Target: if override is not None: return Target(agent, override.expanduser().resolve()) home = _home() - if agent == "codex": - return Target(agent, home / ".codex/skills/codeforerunner/SKILL.md") - if agent == "claude": - return Target(agent, home / ".claude/plugins/codeforerunner/skills/codeforerunner/SKILL.md") + if agent in _dist.SKILL_DEST_AGENTS: + return Target(agent, _dist.skill_destination(agent, "codeforerunner", home)) if agent == "gemini": raise ValueError( "gemini install is handled via `gemini extensions install`; " @@ -63,10 +64,8 @@ def resolve_target(agent: str, override: Path | None) -> Target: def resolve_skill_target(agent: str, slug: str) -> Target: """Return install target for a per-task skill slug.""" home = _home() - if agent == "codex": - return Target(agent, home / f".codex/skills/{slug}/SKILL.md") - if agent == "claude": - return Target(agent, home / f".claude/plugins/codeforerunner/skills/{slug}/SKILL.md") + if agent in _dist.SKILL_DEST_AGENTS: + return Target(agent, _dist.skill_destination(agent, slug, home)) raise ValueError(f"install_all not supported for agent '{agent}' (expected: codex, claude)") diff --git a/tests/test_distribution.py b/tests/test_distribution.py new file mode 100644 index 0000000..f21e47d --- /dev/null +++ b/tests/test_distribution.py @@ -0,0 +1,54 @@ +"""Behavior tests for the Distribution Inventory (distribution.py). + +The inventory owns codeforerunner's distribution artifact identity — canonical +skill path, distributed copy paths, marketplace manifest path, managed-region +markers — and the default install-destination templates. Installer, doctor, and +validators consult it instead of repeating these constants. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from codeforerunner import distribution as dist + + +def test_canonical_and_copies_are_declared(): + assert dist.CANONICAL_SKILL_REL == Path("agent/codeforerunner.skill.md") + assert dist.DISTRIBUTED_SKILL_COPIES_REL # non-empty + # Canonical is the source of truth, never one of the distributed copies. + assert dist.CANONICAL_SKILL_REL not in dist.DISTRIBUTED_SKILL_COPIES_REL + + +def test_marketplace_manifest_path_is_declared(): + assert dist.MARKETPLACE_MANIFEST_REL == Path("plugins/codex/marketplace.json") + + +def test_managed_region_markers_are_declared(): + assert dist.MARKER_BEGIN.startswith("" + + +def test_skill_destination_resolves_per_agent_and_slug(): + home = Path("/home/u") + assert ( + dist.skill_destination("codex", "codeforerunner", home) + == home / ".codex/skills/codeforerunner/SKILL.md" + ) + assert ( + dist.skill_destination("claude", "forerunner-scan", home) + == home / ".claude/plugins/codeforerunner/skills/forerunner-scan/SKILL.md" + ) + + +def test_skill_destination_rejects_unknown_agent(): + with pytest.raises(ValueError): + dist.skill_destination("emacs", "codeforerunner", Path("/home/u")) + + +def test_marketplace_destination_resolves_under_home(): + home = Path("/home/u") + assert dist.marketplace_destination(home) == home / ".codex/marketplaces/codeforerunner.json"