diff --git a/src/codedecorum/cli.py b/src/codedecorum/cli.py index 8b973fa..6c8b618 100644 --- a/src/codedecorum/cli.py +++ b/src/codedecorum/cli.py @@ -3,7 +3,7 @@ import sys from dataclasses import replace from datetime import timedelta -from importlib.metadata import version +from importlib.metadata import metadata, version from pathlib import Path from codedecorum.api import RuleUnit, Scope @@ -39,9 +39,17 @@ def main(argv: list[str] | None = None) -> int: metavar=("RULE", "PATH"), ) parser.add_argument("--approval-timeout", type=_approval_timeout, metavar="DURATION") + parser.add_argument( + "--plugin-help", + action="store_true", + help="show the complete plugin setup and rule-authoring guide, then exit", + ) parser.add_argument("--version", action="version", version=version("codedecorum")) args = parser.parse_args(argv) + if args.plugin_help: + print(_plugin_help()) + return 0 if args.approval_timeout is not None and args.unlocking_with_explicit_human_approval is None: parser.error("--approval-timeout requires --unlocking-with-explicit-human-approval") if args.unlocking_with_explicit_human_approval is not None and args.paths: @@ -125,6 +133,16 @@ def main(argv: list[str] | None = None) -> int: return int(bool(diagnostics)) +def _plugin_help() -> str: + readme = metadata("codedecorum").get_payload() + heading = "## How to write a plugin" + _, separator, remainder = readme.partition(f"{heading}\n") + if not separator: + raise RuntimeError(f"packaged README does not contain {heading!r}") + body, _, _ = remainder.partition("\n## ") + return f"{heading}\n{body.rstrip()}" + + def _approval_timeout(value: str) -> timedelta: units = {"s": 1, "m": 60, "h": 3600} try: diff --git a/tests/test_contracts.py b/tests/test_contracts.py index 4f21d12..a892552 100644 --- a/tests/test_contracts.py +++ b/tests/test_contracts.py @@ -1,6 +1,7 @@ import subprocess from collections.abc import Iterable, Mapping from datetime import UTC, datetime, timedelta +from importlib.metadata import metadata from pathlib import Path, PurePosixPath import pytest @@ -270,6 +271,24 @@ def test_parameterized_guidance_contract() -> None: configure_rules((invalid,), Config()) +def test_plugin_help_prints_packaged_readme_section( + capsys: pytest.CaptureFixture[str], +) -> None: + readme = metadata("codedecorum").get_payload() + expected = readme[readme.index("## How to write a plugin") :].split("\n## ", 1)[0].rstrip() + + assert main(["--plugin-help"]) == 0 + assert capsys.readouterr().out.rstrip() == expected + + with pytest.raises(SystemExit) as exit_info: + main(["--help"]) + assert exit_info.value.code == 0 + help_output = capsys.readouterr().out + assert "--plugin-help" in help_output + assert "show the complete plugin setup and rule-authoring" in help_output + assert "guide, then exit" in help_output + + def test_complete_plugin_pipeline(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: root = tmp_path / "repo" subprocess.run(["git", "init", "-q", "-b", "main", root], check=True)