From 6155bcd59c7f88909d901c2997471016a2e35755 Mon Sep 17 00:00:00 2001 From: Pavan More Date: Fri, 23 Jan 2026 19:03:39 +0530 Subject: [PATCH 1/2] feat(cli): add --with-server flag to CLI mode --- rogue/__main__.py | 41 ++++++++++++++++++++++++++++++++++++++++- rogue/run_server.py | 3 +++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/rogue/__main__.py b/rogue/__main__.py index 4c0e870c..06396cbf 100644 --- a/rogue/__main__.py +++ b/rogue/__main__.py @@ -95,6 +95,13 @@ def parse_args() -> Namespace: help="Run in non-interactive CLI mode", parents=[common_parser()], ) + cli_parser.add_argument( + "--with-server", + action="store_true", + default=False, + help="Start the rogue server alongside the CLI", + ) + set_server_args(cli_parser) set_cli_args(cli_parser) # TUI mode @@ -263,7 +270,39 @@ def main() -> None: if args.mode == "server": run_server(args, background=False) elif args.mode == "cli": - exit_code = asyncio.run(run_cli(args)) + server_process = None + if args.with_server: + try: + server_process = run_server( + args, + background=True, + ) + except Exception as e: + logger.error(f"Failed to start rogue server: {e}") + sys.exit(1) + if not server_process: + logger.error("Failed to start rogue server. Exiting.") + sys.exit(1) + client_host = ( + "127.0.0.1" + if args.host in {"0.0.0.0", "::"} # nosec B104 + else args.host + ) + args.rogue_server_url = f"http://{client_host}:{args.port}" + + exit_code = 1 + try: + exit_code = asyncio.run(run_cli(args)) + except KeyboardInterrupt: + logger.info("Keyboard interrupt received. Exiting.") + exit_code = 0 + except Exception as e: + logger.error(f"CLI execution failed: {e}") + exit_code = 1 + finally: + if server_process: + server_process.terminate() + server_process.join() sys.exit(exit_code) elif args.mode == "tui": if not RogueTuiInstaller().install_rogue_tui(): diff --git a/rogue/run_server.py b/rogue/run_server.py index cd831e69..38df2e84 100644 --- a/rogue/run_server.py +++ b/rogue/run_server.py @@ -145,6 +145,9 @@ def run_server( ) if background_wait_for_ready: if not wait_until_server_ready(process, host, port): + # Terminate the orphan process before raising + process.terminate() + process.join(timeout=5) raise Exception("Server failed to start") logger.info("Rogue server ready", extra={"host": host, "port": port}) return process From 768d5c2327580082d168501dc4c04c3ccaf82acd Mon Sep 17 00:00:00 2001 From: Pavan More Date: Sat, 24 Jan 2026 16:16:12 +0530 Subject: [PATCH 2/2] refactor: shared network logic & orphan process fix - Extract get_host_for_url utility - Fix orphan server process on startup failure - Auto-configure MCP transport for examples --- rogue/__main__.py | 22 +++++++++++++++++----- rogue/common/network.py | 21 +++++++++++++++++++++ rogue/run_server.py | 13 +++---------- rogue/tests/common/test_network.py | 26 ++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 rogue/common/network.py create mode 100644 rogue/tests/common/test_network.py diff --git a/rogue/__main__.py b/rogue/__main__.py index 06396cbf..91713282 100644 --- a/rogue/__main__.py +++ b/rogue/__main__.py @@ -21,6 +21,7 @@ from rogue import __version__ # noqa: F401 from .common.logging.config import configure_logger +from .common.network import get_host_for_url from .common.tui_installer import RogueTuiInstaller from .common.update_checker import check_for_updates from .run_cli import run_cli, set_cli_args @@ -283,13 +284,24 @@ def main() -> None: if not server_process: logger.error("Failed to start rogue server. Exiting.") sys.exit(1) - client_host = ( - "127.0.0.1" - if args.host in {"0.0.0.0", "::"} # nosec B104 - else args.host - ) + client_host = get_host_for_url(args.host) args.rogue_server_url = f"http://{client_host}:{args.port}" + # Auto-configure CLI args for examples if not specified + if args.example: + if not args.evaluated_agent_url: + args.evaluated_agent_url = ( + f"http://{args.example_host}:{args.example_port}" + ) + + if args.example == "tshirt_store_langgraph_mcp": + from rogue_sdk.types import Protocol, Transport + + if args.protocol == Protocol.A2A: + args.protocol = Protocol.MCP + if args.transport is None: + args.transport = Transport.STREAMABLE_HTTP + exit_code = 1 try: exit_code = asyncio.run(run_cli(args)) diff --git a/rogue/common/network.py b/rogue/common/network.py new file mode 100644 index 00000000..961ce3e5 --- /dev/null +++ b/rogue/common/network.py @@ -0,0 +1,21 @@ +from ipaddress import ip_address + + +def get_host_for_url(host: str) -> str: + """ + Normalize a host string for use in a URL. + + - Converts 0.0.0.0/:: to 127.0.0.1 (for client connection) + - Wraps IPv6 addresses in brackets: [::1] + - Leaves hostnames unchanged + """ + if host in ("0.0.0.0", "::"): # nosec B104 + return "127.0.0.1" + + try: + if ip_address(host).version == 6: + return f"[{host}]" + except ValueError: + pass # hostname + + return host diff --git a/rogue/run_server.py b/rogue/run_server.py index 38df2e84..5300e526 100644 --- a/rogue/run_server.py +++ b/rogue/run_server.py @@ -2,13 +2,14 @@ import os import time from argparse import ArgumentParser, Namespace -from ipaddress import ip_address + from pathlib import Path import psutil import requests from loguru import logger +from .common.network import get_host_for_url from .server.main import start_server @@ -94,15 +95,7 @@ def wait_until_server_ready( # Double-check with HTTP request to ensure the server is fully ready try: # Normalize host for HTTP requests (handle 0.0.0.0/:: and IPv6) - http_host = host - if host in ("0.0.0.0", "::"): # nosec B104 - http_host = "127.0.0.1" - else: - try: - if ip_address(host).version == 6: - http_host = f"[{host}]" - except ValueError: - pass # hostname, leave as-is + http_host = get_host_for_url(host) response = requests.get( f"http://{http_host}:{port}/api/v1/health", diff --git a/rogue/tests/common/test_network.py b/rogue/tests/common/test_network.py new file mode 100644 index 00000000..abbcd8ac --- /dev/null +++ b/rogue/tests/common/test_network.py @@ -0,0 +1,26 @@ +from rogue.common.network import get_host_for_url + + +def test_get_host_for_url_ipv4(): + assert get_host_for_url("127.0.0.1") == "127.0.0.1" + assert get_host_for_url("192.168.1.1") == "192.168.1.1" + + +def test_get_host_for_url_hostname(): + assert get_host_for_url("localhost") == "localhost" + assert get_host_for_url("example.com") == "example.com" + + +def test_get_host_for_url_ipv6(): + assert get_host_for_url("::1") == "[::1]" + assert get_host_for_url("2001:db8::1") == "[2001:db8::1]" + assert ( + get_host_for_url("[::1]") == "[::1]" + ) # Already bracketed? No, ip_address might fail or return IPv6. + # ip_address("[::1]") raises ValueError. So it returns input as is (hostname). + # That is acceptable behavior for already bracketed input if considered a hostname. + + +def test_get_host_for_url_wildcard(): + assert get_host_for_url("0.0.0.0") == "127.0.0.1" + assert get_host_for_url("::") == "127.0.0.1"