openra-rl / openra_env /cli /commands.py
github-actions[bot]
Sync from GitHub ac82c3e
02f4a63
"""Subcommand implementations for the openra-rl CLI."""
import shutil
import subprocess
import sys
import webbrowser
from pathlib import Path
from typing import Optional
from openra_env.cli.console import dim, error, header, info, step, success, warn
from openra_env.cli import docker_manager as docker
from openra_env.cli.wizard import (
CONFIG_PATH,
has_saved_config,
load_saved_config,
merge_cli_into_config,
run_wizard,
)
def cmd_play(
provider: Optional[str] = None,
model: Optional[str] = None,
api_key: Optional[str] = None,
difficulty: str = "normal",
verbose: bool = False,
port: int = 8000,
server_url: Optional[str] = None,
local: bool = False,
image_version: Optional[str] = None,
) -> None:
"""Run the LLM agent against the game server."""
use_docker = server_url is None and not local
# 1. Check Docker (unless --local or --server-url)
if use_docker and not docker.check_docker():
sys.exit(1)
# 1b. Version selection — let user pick if multiple versions exist locally
if use_docker and image_version is None:
versions = docker.list_local_versions()
# Filter out "latest" for display — only show concrete version tags
concrete = [v for v in versions if v != "latest"]
if len(concrete) > 1:
info(f"Multiple engine versions available: {', '.join(concrete)}")
try:
choice = input(f" Version to use [{concrete[0]}]: ").strip()
except (EOFError, KeyboardInterrupt):
choice = ""
if choice:
image_version = choice
else:
image_version = concrete[0]
# 2. Load or create config
has_cli_overrides = any([provider, model, api_key])
if has_cli_overrides:
config = load_saved_config() or {}
config = merge_cli_into_config(config, provider=provider, model=model, api_key=api_key)
elif has_saved_config():
config = load_saved_config() or {}
else:
config = run_wizard()
# Validate we have enough config to proceed
llm_cfg = config.get("llm", {})
base_url = llm_cfg.get("base_url", "")
is_local_llm = any(h in base_url for h in ("localhost", "127.0.0.1", "0.0.0.0"))
if not llm_cfg.get("api_key") and not is_local_llm:
error("No API key configured. Run `openra-rl config` or pass --api-key.")
sys.exit(1)
if not llm_cfg.get("model"):
error("No model configured. Run `openra-rl config` or pass --model.")
sys.exit(1)
# 3. Start/reuse server
actual_url = server_url or f"http://localhost:{port}"
we_started_server = False
local_server_proc = None
if local:
# Run the server locally (for developers with local OpenRA build)
header("Starting local server...")
local_server_proc = subprocess.Popen(
[sys.executable, "-m", "openra_env.server.app"],
stdout=sys.stdout,
stderr=sys.stderr,
)
we_started_server = True
# Wait for it to be ready
import time
import urllib.request
import urllib.error
step(f"Waiting for local server on port {port}...")
start = time.time()
while time.time() - start < 60:
try:
req = urllib.request.urlopen(f"{actual_url}/health", timeout=3)
if req.status == 200:
success("Local server is ready!")
break
except (urllib.error.URLError, OSError):
pass
time.sleep(2)
else:
error("Local server did not become ready within 60s.")
local_server_proc.terminate()
sys.exit(1)
elif use_docker:
if docker.is_running():
info(f"Server already running on port {port}.")
else:
if not docker.start_server(port=port, difficulty=difficulty, version=image_version):
sys.exit(1)
we_started_server = True
if not docker.wait_for_health(port=port):
sys.exit(1)
# 4. Run the LLM agent
header("Starting LLM agent...")
provider_name = config.get("provider", "custom")
info(f"Model: {llm_cfg.get('model', '?')} via {provider_name}")
print()
try:
_run_llm_agent(config, actual_url, verbose)
except KeyboardInterrupt:
print("\nInterrupted.")
except ConnectionRefusedError:
error(f"Could not connect to {actual_url}.")
info("Try: openra-rl server start")
info("Check: openra-rl doctor")
except Exception as e:
error(f"Agent error: {e}")
info("Run with --verbose for full details, or check: openra-rl doctor")
# 5. Auto-copy replays from Docker
if use_docker and docker.is_running():
new_replays = docker.copy_replays()
if new_replays:
print()
for f in new_replays:
success(f"Replay saved: {docker.LOCAL_REPLAY_DIR / f}")
info("Watch with: openra-rl replay watch")
# 6. Cleanup
if we_started_server:
print()
if local_server_proc:
try:
answer = input(" Stop local server? [Y/n] ").strip().lower()
except (EOFError, KeyboardInterrupt):
answer = "y"
if answer in ("", "y", "yes"):
local_server_proc.terminate()
local_server_proc.wait(timeout=10)
success("Local server stopped.")
elif use_docker:
try:
answer = input(" Stop game server? [Y/n] ").strip().lower()
except (EOFError, KeyboardInterrupt):
answer = "y"
if answer in ("", "y", "yes"):
docker.stop_server()
def _run_llm_agent(config: dict, server_url: str, verbose: bool) -> None:
"""Import and run the LLM agent with the given config."""
import asyncio
from openra_env.config import load_config
# Build overrides from saved config
cli_overrides: dict = {}
llm_cfg = config.get("llm", {})
if llm_cfg:
cli_overrides["llm"] = llm_cfg
cli_overrides.setdefault("agent", {})["server_url"] = server_url
if verbose:
cli_overrides.setdefault("agent", {})["verbose"] = True
app_config = load_config(cli_overrides=cli_overrides)
from openra_env.agent import run_agent
asyncio.run(run_agent(app_config, verbose))
def cmd_config() -> None:
"""Re-run the setup wizard."""
run_wizard()
def cmd_server_start(port: int = 8000, difficulty: str = "normal", detach: bool = True) -> None:
"""Start the game server."""
if not docker.check_docker():
sys.exit(1)
if not docker.start_server(port=port, difficulty=difficulty, detach=detach):
sys.exit(1)
if detach:
docker.wait_for_health(port=port)
def cmd_server_stop() -> None:
"""Stop the game server."""
docker.stop_server()
def cmd_server_status() -> None:
"""Show game server status."""
status = docker.server_status()
if status:
success(f"Server is running: {status['status']}")
if status.get("ports"):
dim(f" Ports: {status['ports']}")
else:
info("Server is not running.")
def cmd_server_logs(follow: bool = False) -> None:
"""Show game server logs."""
docker.get_logs(follow=follow)
def cmd_doctor() -> None:
"""Check system prerequisites."""
header("OpenRA-RL Doctor")
ok = True
# Docker
if shutil.which("docker"):
success("Docker CLI: installed")
from openra_env.cli.docker_manager import _run
result = _run(["docker", "info"])
if result.returncode == 0:
success("Docker daemon: running")
else:
warn("Docker daemon: not running")
ok = False
else:
error("Docker CLI: not found")
dim(" Install from https://docs.docker.com/get-docker/")
ok = False
# Image
if docker.image_exists():
success(f"Game image: available ({docker.IMAGE})")
else:
warn("Game image: not pulled yet (will be pulled on first `openra-rl play`)")
# Server
if docker.is_running():
success("Game server: running")
else:
dim("Game server: not running")
# Python
py_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
if sys.version_info >= (3, 10):
success(f"Python: {py_version}")
else:
error(f"Python: {py_version} (requires 3.10+)")
ok = False
# Saved config
if has_saved_config():
cfg = load_saved_config() or {}
provider = cfg.get("provider", "unknown")
model = cfg.get("llm", {}).get("model", "unknown")
success(f"Config: {CONFIG_PATH}")
dim(f" Provider: {provider}, Model: {model}")
else:
dim("Config: not yet configured (run `openra-rl play` or `openra-rl config`)")
print()
if ok:
success("All checks passed!")
else:
warn("Some checks failed. Fix the issues above and try again.")
def cmd_version() -> None:
"""Print version."""
try:
from importlib.metadata import version
v = version("openra-rl")
except Exception:
v = "dev"
print(f"openra-rl {v}")
def cmd_mcp_server(server_url: Optional[str] = None, port: int = 8000) -> None:
"""Start the MCP stdio server."""
from openra_env.mcp_server import main as mcp_main
mcp_main(server_url=server_url or f"http://localhost:{port}")
# ── Replay commands ──────────────────────────────────────────────────
def cmd_replay_watch(
file: Optional[str] = None,
port: int = 6080,
resolution: Optional[str] = None,
render_mode: Optional[str] = None,
vnc_quality: Optional[int] = None,
vnc_compression: Optional[int] = None,
cpu_cores: Optional[int] = None,
) -> None:
"""Watch a replay in the browser via VNC-in-Docker."""
if not docker.check_docker():
sys.exit(1)
try:
viewer_settings = docker.load_replay_viewer_settings(
resolution=resolution,
render_mode=render_mode,
vnc_quality=vnc_quality,
vnc_compression=vnc_compression,
cpu_cores=cpu_cores,
)
except ValueError as exc:
error(f"Invalid replay viewer setting: {exc}")
sys.exit(1)
replay_path = file
if replay_path is None:
# Check local replays first (most reliable — file is mounted directly)
local_replays = sorted(docker.LOCAL_REPLAY_DIR.glob("*.orarep"))
if local_replays:
replay_path = str(local_replays[-1])
info(f"Latest local replay: {local_replays[-1].name}")
elif docker.is_running():
# Fall back to container path (uses --volumes-from, less reliable)
replay_path = docker.get_latest_replay()
if replay_path:
info(f"Latest container replay: {Path(replay_path).name}")
if replay_path is None:
error("No replays found. Play a game first with: openra-rl play")
sys.exit(1)
header("Starting replay viewer...")
info(
f"Settings: {viewer_settings.width}x{viewer_settings.height}, "
f"render={viewer_settings.render_mode}, "
f"vnc q/c={viewer_settings.vnc_quality}/{viewer_settings.vnc_compression}"
)
if not docker.start_replay_viewer(replay_path, port=port, settings=viewer_settings):
sys.exit(1)
import time
import urllib.error
import urllib.request
url = (
f"http://localhost:{port}/vnc.html?autoconnect=1&resize=scale"
f"&quality={viewer_settings.vnc_quality}"
f"&compression={viewer_settings.vnc_compression}"
)
step("Waiting for viewer to be ready...")
ready = False
start_time = time.time()
timeout = 30
while time.time() - start_time < timeout:
if not docker.is_replay_viewer_running():
error("Replay viewer exited before it became ready.")
logs = docker.get_replay_viewer_logs()
if logs:
print()
info("Replay viewer logs:")
print(logs)
sys.exit(1)
try:
req = urllib.request.urlopen(f"http://localhost:{port}/vnc.html", timeout=2)
if 200 <= req.status < 500:
ready = True
break
except (urllib.error.URLError, OSError):
pass
time.sleep(1)
if not ready:
error(f"Viewer did not become ready within {timeout}s.")
logs = docker.get_replay_viewer_logs()
if logs:
print()
info("Replay viewer logs:")
print(logs)
sys.exit(1)
info(f"Opening {url}")
webbrowser.open(url)
print()
info("Tip: press F12 in the viewer for maximum replay speed.")
info("Tip: tune with --resolution, --render, --vnc-quality, --vnc-compression.")
info("Press Ctrl+C to stop the replay viewer")
print()
try:
# Wait until container exits or user presses Ctrl+C
while docker.is_replay_viewer_running():
time.sleep(2)
info("Replay viewer has stopped.")
except KeyboardInterrupt:
print()
docker.stop_replay_viewer()
def cmd_replay_list() -> None:
"""List available replays from Docker and local."""
header("Game Replays")
# Docker replays
if docker.is_running():
docker_replays = docker.list_replays()
if docker_replays:
info(f"In Docker container ({len(docker_replays)}):")
for r in docker_replays:
dim(f" {Path(r).name}")
else:
dim(" No replays in Docker container.")
else:
dim(" Docker server not running — cannot list container replays.")
# Local replays
print()
local_dir = docker.LOCAL_REPLAY_DIR
if local_dir.exists():
local_replays = sorted(local_dir.glob("*.orarep"))
if local_replays:
info(f"Local ({len(local_replays)}) — {local_dir}:")
for r in local_replays:
dim(f" {r.name}")
else:
dim(f" No local replays in {local_dir}")
else:
dim(f" No local replay directory ({local_dir})")
def cmd_replay_copy() -> None:
"""Copy replays from Docker container to local directory."""
if not docker.check_docker():
sys.exit(1)
if not docker.is_running():
error("Game server is not running. Start it first or use: openra-rl server start")
sys.exit(1)
header("Copying replays from Docker...")
new_files = docker.copy_replays()
if new_files:
for f in new_files:
success(f" Copied: {f}")
success(f"Copied {len(new_files)} new replay(s) to {docker.LOCAL_REPLAY_DIR}")
else:
info(f"No new replays to copy. Replays are in {docker.LOCAL_REPLAY_DIR}")
def cmd_replay_stop() -> None:
"""Stop the replay viewer."""
docker.stop_replay_viewer()