docs(l4d2): finalize v1 CLI contracts and web-facing read APIs

This commit is contained in:
mwiegand 2026-04-23 01:01:14 +02:00
parent a6c4a6c50f
commit 466abe66ee
No known key found for this signature in database
3 changed files with 77 additions and 13 deletions

View file

@ -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 <name> -f <spec.yaml>`
- `start <name>`
- `stop <name>`
- `delete <name>`
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/<overlay>`
- `/opt/l4d2/instances/<name>`
- `/opt/l4d2/runtime/<name>/{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)`

View file

@ -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)

View file

@ -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