mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 18:07:24 +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
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
"""The AgentReady half of the installer: a website gets the agent-ready gate.
|
|
|
|
Why it rides on install_repo_contract_gate.sh rather than a separate script:
|
|
that installer is already the one step every new repo gets after `git init`,
|
|
so a new Vite site cannot come out of scaffolding without the gate. Pieces,
|
|
mirroring the repo-contract and dependency-freshness gates: the
|
|
`[agent_ready]` table in repo-contract.toml (proposed from detection, never
|
|
overwritten), the scripts/check_agent_ready.sh delegate, the pre-commit hook
|
|
and the agent-ready CI workflow (skipped when the repo has no Actions).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from dep_freshness.install import NO_WORKFLOW_MARKER
|
|
from repo_contract.precommit import (
|
|
PRECOMMIT,
|
|
hook_line,
|
|
needs_hook,
|
|
set_hook_line,
|
|
write_hook,
|
|
)
|
|
from repo_contract.web import (
|
|
CONFIG,
|
|
HOOK_ID,
|
|
SHIM,
|
|
TABLE,
|
|
WORKFLOW,
|
|
AgentReady,
|
|
ConfigError,
|
|
detect,
|
|
hook_files,
|
|
load,
|
|
render,
|
|
)
|
|
|
|
TEMPLATES = Path(__file__).resolve().parent / "templates"
|
|
HEADER = (
|
|
"# Repo-contract settings, read by ~/src/utils/repo_contract.\n"
|
|
"# [agent_ready]: this repo is a website and must pass the AgentReady\n"
|
|
"# checker (~/src/utils/agent_ready). See repo_contract/web.py for keys.\n\n"
|
|
)
|
|
CONFIG_PIECE = f"{CONFIG} ([{TABLE}])"
|
|
HOOK_PIECE = f"{PRECOMMIT} ({HOOK_ID} hook)"
|
|
|
|
|
|
def _template(name: str) -> str:
|
|
return (TEMPLATES / name).read_text(encoding="utf-8")
|
|
|
|
|
|
def _drifted(repo: Path, rel: Path, template: str) -> bool:
|
|
target = repo / rel
|
|
return not target.is_file() or target.read_text(encoding="utf-8") != _template(
|
|
template
|
|
)
|
|
|
|
|
|
def _files_line(cfg: AgentReady) -> str:
|
|
"""The hook's `files:` line: the regex as a single-quoted YAML scalar."""
|
|
return " files: '" + hook_files(cfg).replace("'", "''") + "'"
|
|
|
|
|
|
def _settings(repo: Path) -> tuple[AgentReady | None, bool]:
|
|
"""(settings to gate with, whether the config must be written)."""
|
|
try:
|
|
declared = load(repo)
|
|
except ConfigError:
|
|
return None, False # check.py reports it; never guess over a typo
|
|
if declared is not None:
|
|
return (None if declared.mode == "skip" else declared), False
|
|
proposed = detect(repo)
|
|
return proposed, proposed is not None
|
|
|
|
|
|
def _pieces(repo: Path) -> tuple[AgentReady | None, list[str]]:
|
|
cfg, write_config = _settings(repo)
|
|
if cfg is None:
|
|
return None, []
|
|
todo = [CONFIG_PIECE] if write_config else []
|
|
if _drifted(repo, SHIM, "check_agent_ready.shim.sh"):
|
|
todo.append(str(SHIM))
|
|
if not (repo / NO_WORKFLOW_MARKER).exists() and _drifted(
|
|
repo, WORKFLOW, "agent-ready.yml"
|
|
):
|
|
todo.append(str(WORKFLOW))
|
|
if needs_hook(repo, HOOK_ID) or hook_line(repo, HOOK_ID, "files") != _files_line(
|
|
cfg
|
|
):
|
|
todo.append(HOOK_PIECE)
|
|
return cfg, todo
|
|
|
|
|
|
def plan_web(repo: Path) -> list[str]:
|
|
"""Which AgentReady gate pieces are missing or drifted."""
|
|
return _pieces(repo)[1]
|
|
|
|
|
|
def _write_config(repo: Path, cfg: AgentReady) -> None:
|
|
target = repo / CONFIG
|
|
if target.is_file():
|
|
body = target.read_text(encoding="utf-8")
|
|
body = (body if body.endswith("\n") else body + "\n") + "\n"
|
|
else:
|
|
body = HEADER
|
|
target.write_text(body + render(cfg), encoding="utf-8")
|
|
|
|
|
|
def install_web(repo: Path) -> list[str]:
|
|
"""Write every missing or drifted AgentReady gate piece. Returns them."""
|
|
cfg, todo = _pieces(repo)
|
|
if cfg is not None and CONFIG_PIECE in todo:
|
|
_write_config(repo, cfg)
|
|
if str(SHIM) in todo:
|
|
target = repo / SHIM
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(_template("check_agent_ready.shim.sh"), encoding="utf-8")
|
|
target.chmod(0o755)
|
|
if str(WORKFLOW) in todo:
|
|
target = repo / WORKFLOW
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(_template("agent-ready.yml"), encoding="utf-8")
|
|
if cfg is not None and HOOK_PIECE in todo:
|
|
if needs_hook(repo, HOOK_ID):
|
|
write_hook(repo, _template("agent-ready-hook.yaml"))
|
|
set_hook_line(repo, HOOK_ID, "files", _files_line(cfg))
|
|
return todo
|