From 466abe66ee6293ba97ac2c5d6a8aaf241389d5c5 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Thu, 23 Apr 2026 01:01:14 +0200 Subject: [PATCH] docs(l4d2): finalize v1 CLI contracts and web-facing read APIs --- components/l4d2-host-lib/README.md | 30 +++++++++++++ components/l4d2-host-lib/src/l4d2host/cli.py | 44 ++++++++++++++------ components/l4d2-host-lib/tests/test_cli.py | 16 +++++++ 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/components/l4d2-host-lib/README.md b/components/l4d2-host-lib/README.md index ac7f61f..9ac4dac 100644 --- a/components/l4d2-host-lib/README.md +++ b/components/l4d2-host-lib/README.md @@ -1 +1,31 @@ # l4d2-host-lib + +Python host library and CLI for managing L4D2 instances. + +## CLI + +`l4d2ctl` exposes exactly these commands in v1: + +- `install` +- `initialize -f ` +- `start ` +- `stop ` +- `delete ` + +Subprocess failures are fail-fast. Raw stderr is written to stderr and the command exits with the same subprocess return code. + +## Runtime Paths + +The host library uses hard-coded runtime paths under `/opt/l4d2`: + +- `/opt/l4d2/installation` +- `/opt/l4d2/overlays/` +- `/opt/l4d2/instances/` +- `/opt/l4d2/runtime//{upper,work,merged}` + +## Web App Read APIs + +These read APIs are provided for web app integration: + +- `get_instance_status(name)` +- `stream_instance_logs(name, lines=200, follow=True)` diff --git a/components/l4d2-host-lib/src/l4d2host/cli.py b/components/l4d2-host-lib/src/l4d2host/cli.py index dd8dddd..b8539b1 100644 --- a/components/l4d2-host-lib/src/l4d2host/cli.py +++ b/components/l4d2-host-lib/src/l4d2host/cli.py @@ -1,38 +1,56 @@ +from pathlib import Path +import subprocess + import typer +from l4d2host.instances import delete_instance, initialize_instance, start_instance, stop_instance +from l4d2host.steam_install import SteamInstaller + app = typer.Typer(no_args_is_help=True) -def _todo() -> None: - raise typer.Exit(code=1) +def _exit_from_subprocess_error(exc: subprocess.CalledProcessError) -> None: + if exc.stderr: + typer.echo(exc.stderr, err=True) + raise typer.Exit(code=exc.returncode) @app.command() def install() -> None: - _todo() + try: + SteamInstaller().install_or_update(passthrough=True) + except subprocess.CalledProcessError as exc: + _exit_from_subprocess_error(exc) @app.command() -def initialize(name: str, spec: str = typer.Option(..., "--spec", "-f")) -> None: - del name - del spec - _todo() +def initialize(name: str, spec: Path = typer.Option(..., "-f")) -> None: + try: + initialize_instance(name, spec, passthrough=True) + except subprocess.CalledProcessError as exc: + _exit_from_subprocess_error(exc) @app.command() def start(name: str) -> None: - del name - _todo() + try: + start_instance(name, passthrough=True) + except subprocess.CalledProcessError as exc: + _exit_from_subprocess_error(exc) @app.command() def stop(name: str) -> None: - del name - _todo() + try: + stop_instance(name, passthrough=True) + except subprocess.CalledProcessError as exc: + _exit_from_subprocess_error(exc) @app.command() def delete(name: str) -> None: - del name - _todo() + try: + delete_instance(name, passthrough=True) + except subprocess.CalledProcessError as exc: + _exit_from_subprocess_error(exc) diff --git a/components/l4d2-host-lib/tests/test_cli.py b/components/l4d2-host-lib/tests/test_cli.py index 10fb86d..6ae4d2a 100644 --- a/components/l4d2-host-lib/tests/test_cli.py +++ b/components/l4d2-host-lib/tests/test_cli.py @@ -1,3 +1,5 @@ +import subprocess + from typer.testing import CliRunner from l4d2host.cli import app @@ -8,3 +10,17 @@ def test_help_lists_v1_commands() -> None: assert result.exit_code == 0 for command in ["install", "initialize", "start", "stop", "delete"]: assert command in result.output + + +def test_cli_propagates_subprocess_return_code(monkeypatch) -> None: + def fail(*args, **kwargs): + del args + del kwargs + raise subprocess.CalledProcessError(returncode=9, cmd=["x"], stderr="boom") + + monkeypatch.setattr("l4d2host.cli.start_instance", fail) + + result = CliRunner().invoke(app, ["start", "alpha"]) + + assert result.exit_code == 9 + assert "boom" in result.stderr