From 4c0c480bdd51fdb508d8f95ebf9c6d99a2840995 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:23:35 +0100 Subject: [PATCH] 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. --- scripts/siteagents.py | 35 ++++++++++++++++++++++++++++ tests/test_site_agents.py | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/scripts/siteagents.py b/scripts/siteagents.py index 60b366943..f05d0bcfa 100644 --- a/scripts/siteagents.py +++ b/scripts/siteagents.py @@ -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), diff --git a/tests/test_site_agents.py b/tests/test_site_agents.py index 93af6f9d1..dbbbf4eb2 100644 --- a/tests/test_site_agents.py +++ b/tests/test_site_agents.py @@ -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("", 1)[1].split("")[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"(? 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."""