docs: tell agents how to drive the installer

llms.txt now gives the package commands, the plan as JSON events from
--dry-run --events, the exit codes and the plan, consent, act pattern
with --yes. Tests hold each fact to install.py and to the exit code table
of the installer page.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 14:23:35 +01:00
1 parent 92546794e2
commit 4c0c480bdd
2 files changed
+84

No files matched your search

+35
View File
@@ -19,6 +19,8 @@ import json
from collections.abc import Callable
from pathlib import Path
from siteguides import PACKAGE_COMMAND
SITE = "https://abdess.github.io/retrobios/"
PUBLISHER = "abdess.github.io"
SH = "curl -fsSL https://abdess.github.io/retrobios/i | sh -s --"
@@ -31,6 +33,22 @@ QUOTING = (f"A folder goes in single quotes, attached to its option. One that ho
"command. On a page of the site, the `device_setup_command` WebMCP tool writes the "
"command and refuses such a folder.")
UVX = PACKAGE_COMMAND
# EVENT_SCHEMA of install.py, and the schema generate_event_schema.py writes
# from EVENT_KEYS, as the site publishes it. tests/test_site_agents.py holds
# both to install.py.
EVENT_SCHEMA = 1
EVENTS_SCHEMA_PATH = "api/v1/schemas/install-events.schema.json"
# Every code of the Exit status table of wiki/installer.md, in short.
EXIT_CODES = (
(0, "everything asked for is installed or already in place"),
(1, "a failure: a download, a file list or a folder that cannot be used"),
(2, "nothing was done: an option refused, every file failed, or the user quit"),
(3, "`--check` found files missing or wrong, nothing having failed"),
(130, "stopped with Ctrl+C"),
(141, "the output went to a reader that closed early"),
)
START_PAGES = (
("Which pack is needed", "which-pack/", "pick a device or setup, get the folder its BIOS files go in, "
"one command that installs and checks them, or a ZIP pack"),
@@ -108,6 +126,23 @@ def llms_txt(platforms: list[tuple[str, str]]) -> str:
"- A game the installer cannot identify gets every BIOS of its system, and the run names it.",
f"- The same steps as an Agent Skill: [SKILL.md]({SITE}{SKILL_PATH}).",
"",
"## Driving the installer for a user",
"",
f"With Python 3.8 or newer, the installer is also a package: `{UVX} --for-games "
f"--required-only --dry-run`, or `pipx run retrobios` with the same options.",
"",
"1. Plan: add `--dry-run --events`. Nothing is written, and standard output carries "
f"one JSON object per line, event schema {EVENT_SCHEMA} "
f"([JSON Schema]({SITE}{EVENTS_SCHEMA_PATH})): a `file` event per file with its "
"`state`, `check` with the totals and `bytes_needed`, then `summary` with `pending`.",
"2. Consent: show the user what will be installed, where, and the size, and ask.",
"3. Act, only on a yes: the same command with `--yes` in place of `--dry-run`.",
"",
"- `--yes` asks nothing: use it in a run without a terminal. A run that needs an "
"answer (two setups found, none found) stops and names the option to give.",
"- The exit code, also the `code` of the last `end` event:",
*(f" - `{code}`: {meaning}" for code, meaning in EXIT_CODES),
"",
"## Start",
"",
*(f"- [{title}]({SITE}{path}): {what}" for title, path, what in START_PAGES),
+49
View File
@@ -50,6 +50,55 @@ class TheGuideLeadsToTheFewestFiles(unittest.TestCase):
self.assertTrue(target.is_file() or (target / "index.html").is_file(), url)
class TheGuideDrivesTheInstaller(unittest.TestCase):
"""What an agent needs to run the installer for a user: the package
command, the plan as JSON, the exit codes, and plan, consent, act. Each
fact is held to install.py, so the guide cannot drift from it."""
def setUp(self) -> None:
import generate_installer_doc # noqa: PLC0415
self.doc = generate_installer_doc
self.text = siteagents.llms_txt([("retroarch", "RetroArch")])
self.section = self.text.split("\n## Driving the installer for a user\n", 1)[1].split("\n## ", 1)[0]
def test_the_exit_codes_are_the_installer_page_table(self) -> None:
page = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
table = page.split("<!-- generated: exit-codes -->", 1)[1].split("<!-- /generated: exit-codes -->")[0]
in_table = [int(code) for code in re.findall(r"^\| `(\d+)` \|", table, re.M)]
self.assertEqual(in_table, self.doc.exit_codes())
self.assertEqual([code for code, _ in siteagents.EXIT_CODES], in_table)
listed = [int(code) for code in re.findall(r"^ - `(\d+)`: ", self.section, re.M)]
self.assertEqual(listed, in_table)
def test_the_event_schema_is_the_installers(self) -> None:
import json # noqa: PLC0415
self.assertEqual(siteagents.EVENT_SCHEMA, self.doc._install.EVENT_SCHEMA)
schema = json.loads((REPO_ROOT / "schemas" / "install-events.schema.json").read_text(encoding="utf-8"))
self.assertEqual(schema["$id"], siteagents.SITE + siteagents.EVENTS_SCHEMA_PATH)
self.assertIn(f"event schema {siteagents.EVENT_SCHEMA} ", self.section)
def test_every_option_and_event_named_exists(self) -> None:
options = {flag for action in self.doc._install.build_parser()._actions
for flag in action.option_strings}
for flag in set(re.findall(r"(?<![\w-])(--[a-z][a-z-]*)", self.section)):
with self.subTest(flag=flag):
self.assertIn(flag, options)
keys = self.doc._install.EVENT_KEYS
self.assertIn("state", keys["file"])
self.assertIn("bytes_needed", keys["check"])
self.assertIn("pending", keys["summary"])
self.assertEqual(tuple(keys["end"]), ("code",))
def test_the_package_commands_and_the_pattern(self) -> None:
self.assertIn("`uvx retrobios --for-games --required-only --dry-run`", self.section)
self.assertIn("`pipx run retrobios`", self.section)
self.assertIn("`--dry-run --events`", self.section)
steps = re.findall(r"^\d\. (\w+)", self.section, re.M)
self.assertEqual(steps, ["Plan", "Consent", "Act"])
class TheSkillIsAnAgentSkill(unittest.TestCase):
"""ARD reserves text/markdown for agent skills (Lighthouse warns on a
plain Markdown entry): the setup steps are published as one."""