diff --git a/README.md b/README.md index f4ffe21..3ee0290 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,30 @@ The global environment variables: | `KULEUVEN_TOTP` | `session start` | 6-digit TOTP code for two-factor sign-in. | | `KULEUVEN_AUTH_DEVICE` | `session start` | KU Leuven Authenticator device to push to: a name, 1-based index, or `most-recent`. | +### Docs + +#### `kuleuven docs` + +Prints Markdown documentation for the whole command tree — every command and subcommand, with usage, arguments, and options — so an agent can read the full surface in one call instead of walking `--help` group by group. The Markdown is generated from the live Typer app, so it never drifts from the real CLI. Needs no session and makes no network calls. + +```text +kuleuven docs +``` + +The Markdown is returned as a string under the `docs` key (the command still emits a single JSON object, per the contract). `format` is always `markdown`. Exits `0`. + +```sh +kuleuven docs | jq -r .docs +``` + +```json +{ + "status": "ok", + "format": "markdown", + "docs": "# `kuleuven`\n\nKU Leuven CLI\n..." +} +``` + ### Session #### `kuleuven session start` diff --git a/src/kuleuven/cli/__init__.py b/src/kuleuven/cli/__init__.py index 6663af9..0637231 100644 --- a/src/kuleuven/cli/__init__.py +++ b/src/kuleuven/cli/__init__.py @@ -6,6 +6,7 @@ from kuleuven.cli.content import content_app from kuleuven.cli.courses import courses_app from kuleuven.cli.discussions import discussions_app +from kuleuven.cli.docs import docs from kuleuven.cli.files import files_app from kuleuven.cli.kurt import kurt_app from kuleuven.cli.mcp import mcp_app @@ -46,6 +47,7 @@ def main(ctx: typer.Context) -> None: session_app.command(name="raw")(raw) +app.command(name="docs")(docs) app.add_typer(session_app, name="session") app.add_typer(toledo_app, name="toledo") app.add_typer(kurt_app, name="kurt") diff --git a/src/kuleuven/cli/docs.py b/src/kuleuven/cli/docs.py new file mode 100644 index 0000000..431c49a --- /dev/null +++ b/src/kuleuven/cli/docs.py @@ -0,0 +1,21 @@ +import click +import typer +from typer.cli import get_docs_for_click + +from kuleuven.cli.output import emit + + +def docs() -> None: + """Print Markdown docs for every command and subcommand as JSON.""" + # Imported lazily: kuleuven.cli builds `app` by importing this module. + from kuleuven.cli import app + + # get_docs_for_click is Typer's own recursive Markdown generator (the one + # behind `typer utils docs`). It renders usage, arguments, and options + # through the same code path as `--help`, so the output never drifts from + # the real CLI. A throwaway context is enough; the callback that builds the + # session never runs because we aren't executing a command. + root = typer.main.get_command(app) + context = click.Context(root, info_name="kuleuven") + markdown = get_docs_for_click(obj=root, ctx=context, name="kuleuven") + emit({"status": "ok", "format": "markdown", "docs": markdown.strip()}) diff --git a/tests/test_cli.py b/tests/test_cli.py index a7e2bbb..b149f3b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -113,3 +113,34 @@ def test_emit_raises_typeerror_for_unsupported_types(self): with pytest.raises(TypeError, match="not JSON serializable"): _json_default(object()) + + +class TestDocs: + def test_docs_emits_markdown_for_the_whole_tree(self, runner): + # No network: docs renders the in-process command tree, so it must work + # without a session. + result = runner.invoke(app, ["docs"]) + + assert result.exit_code == 0 + payload = parse_stdout(result) + assert payload["status"] == "ok" + assert payload["format"] == "markdown" + + markdown = payload["docs"] + assert markdown.startswith("# `kuleuven`") + # Nested commands at every depth are present. + for heading in ( + "## `kuleuven session`", + "### `kuleuven session start`", + "#### `kuleuven kurt resources book`", + ): + assert heading in markdown + + def test_docs_includes_arguments_options_and_env_vars(self, runner): + result = runner.invoke(app, ["docs"]) + markdown = parse_stdout(result)["docs"] + + # Help text, env-var bindings, and positional arguments all survive. + assert "[env var: KULEUVEN_USERNAME]" in markdown + assert "RESERVATION_ID" in markdown + assert "Cancel a reservation." in markdown