mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:07:18 +01:00
New agent_ready/ package (dist and live-url modes, 100% branch coverage) checks robots, sitemap, llms.txt, ai-catalog, 404, no-JS text, markdown mirrors and JSON-LD. repo_contract gains web-repo detection and installs the agent-ready pre-commit hook, CI workflow and check_agent_ready.sh shim; scripts/check_agent_ready.sh is the shared entry point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBScELfMUiieK9ManFbS3Y
135 lines
5.0 KiB
Python
135 lines
5.0 KiB
Python
"""Install the repo-contract gate into a repo and bootstrap its Commands section.
|
|
|
|
Usage: python3 -m repo_contract.install REPO [--check] [--no-bootstrap]
|
|
(or `scripts/install_repo_contract_gate.sh REPO`).
|
|
|
|
Why this exists: the gate used to be four manual steps in the README. Nothing
|
|
ran them for a repo created after the 2026-10-04 rollout (teatr-pw and
|
|
teatr-pw-pytania were made ad hoc on 2026-10-09 and came out with no
|
|
`## Commands`, no shim, no hook), so the sweep was the first thing to notice.
|
|
One idempotent command now does all of it: delegate shim, pre-commit hook,
|
|
CI workflow (unless the repo opted out of GitHub Actions) and the Commands
|
|
section plus `scripts/test_changed.sh` through bootstrap.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
if __package__ in (None, ""): # pragma: no cover - plain-script import path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from dep_freshness.install import NO_WORKFLOW_MARKER
|
|
from repo_contract import bootstrap
|
|
from repo_contract.check import violations
|
|
from repo_contract.infer import propose
|
|
from repo_contract.install_web import install_web, plan_web
|
|
from repo_contract.precommit import PRECOMMIT, needs_hook, write_hook
|
|
from repo_contract.stacks import detect_stacks
|
|
|
|
TEMPLATES = bootstrap.TEMPLATES
|
|
SHIM = Path("scripts/check_repo_contract.sh")
|
|
WORKFLOW = Path(".github/workflows/repo-contract.yml")
|
|
HOOK_ID = "repo-contract"
|
|
|
|
|
|
def _template(name: str) -> str:
|
|
return (TEMPLATES / name).read_text(encoding="utf-8")
|
|
|
|
|
|
def _needs_shim(repo: Path) -> bool:
|
|
"""False for utils itself, where SHIM is the real checker, not a delegate."""
|
|
if (repo / "repo_contract" / "check.py").is_file():
|
|
return False
|
|
target = repo / SHIM
|
|
return not target.is_file() or target.read_text(encoding="utf-8") != _template(
|
|
"check_repo_contract.shim.sh"
|
|
)
|
|
|
|
|
|
def _needs_workflow(repo: Path) -> bool:
|
|
"""Missing or drifted CI workflow, unless the repo opted out of Actions."""
|
|
if (repo / NO_WORKFLOW_MARKER).exists():
|
|
return False
|
|
target = repo / WORKFLOW
|
|
return not target.is_file() or target.read_text(encoding="utf-8") != _template(
|
|
"repo-contract.yml"
|
|
)
|
|
|
|
|
|
def plan(repo: Path) -> list[str]:
|
|
"""Which gate pieces are missing or drifted (the Commands section excluded).
|
|
|
|
A website (detected or declared in repo-contract.toml) also gets the
|
|
AgentReady gate: config, shim, workflow and hook (see install_web.py).
|
|
"""
|
|
todo = []
|
|
if _needs_shim(repo):
|
|
todo.append(str(SHIM))
|
|
if _needs_workflow(repo):
|
|
todo.append(str(WORKFLOW))
|
|
if needs_hook(repo, HOOK_ID):
|
|
todo.append(f"{PRECOMMIT} ({HOOK_ID} hook)")
|
|
return todo + plan_web(repo)
|
|
|
|
|
|
def install(repo: Path) -> list[str]:
|
|
"""Write every missing or drifted gate piece. Returns what changed."""
|
|
done = []
|
|
if _needs_shim(repo):
|
|
target = repo / SHIM
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(_template("check_repo_contract.shim.sh"), encoding="utf-8")
|
|
target.chmod(0o755)
|
|
done.append(str(SHIM))
|
|
if _needs_workflow(repo):
|
|
target = repo / WORKFLOW
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(_template("repo-contract.yml"), encoding="utf-8")
|
|
done.append(str(WORKFLOW))
|
|
if needs_hook(repo, HOOK_ID):
|
|
write_hook(repo, _template("pre-commit-snippet.yaml"))
|
|
done.append(f"{PRECOMMIT} ({HOOK_ID} hook)")
|
|
return done + install_web(repo)
|
|
|
|
|
|
def bootstrap_commands(repo: Path) -> list[str]:
|
|
"""Write the missing Commands lines and test_changed.sh (never overwrites)."""
|
|
prop = propose(repo, detect_stacks(repo))
|
|
doc = bootstrap.doc_path(repo)
|
|
text = doc.read_text(encoding="utf-8") if doc.is_file() else ""
|
|
new = bootstrap.missing_lines(prop, text)
|
|
return bootstrap.apply(repo, doc, text, new, bootstrap.script_plan(repo, prop))
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""CLI entry point; 0 contract satisfied, 1 violations left, 2 not a git repo."""
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("repo", type=Path)
|
|
parser.add_argument("--check", action="store_true", help="report, change nothing")
|
|
parser.add_argument("--no-bootstrap", action="store_true", help="gate pieces only")
|
|
args = parser.parse_args(argv)
|
|
repo = args.repo.expanduser().resolve()
|
|
if not (repo / ".git").exists():
|
|
print(f"install: {repo} is not a git repo", file=sys.stderr)
|
|
return 2
|
|
if args.check:
|
|
for piece in plan(repo):
|
|
print(f"would write: {piece}")
|
|
else:
|
|
for piece in install(repo):
|
|
print(f"wrote: {piece}")
|
|
if not args.no_bootstrap:
|
|
for action in bootstrap_commands(repo):
|
|
print(f"bootstrap: {action}")
|
|
found, _ = violations(repo)
|
|
for problem in found:
|
|
print(f"repo-contract: {repo.name}: {problem}", file=sys.stderr)
|
|
return 1 if found else 0
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
sys.exit(main())
|