Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions scripts/validate_skill_copies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
56 changes: 56 additions & 0 deletions src/codeforerunner/distribution.py
Original file line number Diff line number Diff line change
@@ -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 = "<!-- forerunner:begin managed=codeforerunner.skill -->"
MARKER_END = "<!-- forerunner: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"
22 changes: 11 additions & 11 deletions src/codeforerunner/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<!-- forerunner:begin managed=codeforerunner.skill -->"
MARKER_END = "<!-- forerunner:end -->"
MARKER_BEGIN = _dist.MARKER_BEGIN
MARKER_END = _dist.MARKER_END


@dataclass(frozen=True)
Expand All @@ -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):
Expand Down
23 changes: 11 additions & 12 deletions src/codeforerunner/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<!-- forerunner:begin managed=codeforerunner.skill -->"
MARKER_END = "<!-- forerunner: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
Expand Down Expand Up @@ -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`; "
Expand All @@ -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)")


Expand Down
54 changes: 54 additions & 0 deletions tests/test_distribution.py
Original file line number Diff line number Diff line change
@@ -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("<!-- forerunner:begin")
assert "managed=codeforerunner.skill" in dist.MARKER_BEGIN
assert dist.MARKER_END == "<!-- forerunner:end -->"


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"