From ff2abb5f77d6387c511d25dc9f869d9f96909a67 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Tue, 31 Mar 2026 16:58:08 -0700 Subject: [PATCH] feat: wire service subcommand group into CLI (install, uninstall, start, stop, restart, status, logs) --- muxplex/cli.py | 40 +++++++++++++++++ muxplex/tests/test_cli.py | 93 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/muxplex/cli.py b/muxplex/cli.py index 9bfc4d3..160cbe8 100644 --- a/muxplex/cli.py +++ b/muxplex/cli.py @@ -707,6 +707,18 @@ def main() -> None: "--system", action="store_true", help="System-wide (requires sudo)" ) + service_parser = sub.add_parser( + "service", help="Manage the muxplex background service" + ) + service_sub = service_parser.add_subparsers(dest="service_command") + service_sub.add_parser("install", help="Install + enable + start the service") + service_sub.add_parser("uninstall", help="Stop + disable + remove the service") + service_sub.add_parser("start", help="Start the service") + service_sub.add_parser("stop", help="Stop the service") + service_sub.add_parser("restart", help="Stop + start the service") + service_sub.add_parser("status", help="Show service status") + service_sub.add_parser("logs", help="Tail service logs") + sub.add_parser("show-password", help="Show the current muxplex password") sub.add_parser( @@ -743,6 +755,34 @@ def main() -> None: doctor() elif args.command in ("upgrade", "update"): upgrade(force=getattr(args, "force", False)) + elif args.command == "service": + from muxplex.service import ( # noqa: PLC0415 + service_install, + service_logs, + service_restart, + service_start, + service_status, + service_stop, + service_uninstall, + ) + + cmd = getattr(args, "service_command", None) + if cmd == "install": + service_install() + elif cmd == "uninstall": + service_uninstall() + elif cmd == "start": + service_start() + elif cmd == "stop": + service_stop() + elif cmd == "restart": + service_restart() + elif cmd == "status": + service_status() + elif cmd == "logs": + service_logs() + else: + service_parser.print_help() else: _check_dependencies() serve( diff --git a/muxplex/tests/test_cli.py b/muxplex/tests/test_cli.py index e6ddf22..b29d566 100644 --- a/muxplex/tests/test_cli.py +++ b/muxplex/tests/test_cli.py @@ -1025,3 +1025,96 @@ def test_doctor_shows_serve_config(tmp_path, monkeypatch, capsys): assert "0.0.0.0" in out assert "9999" in out assert "password" in out + + +# --------------------------------------------------------------------------- +# service subcommand dispatch tests +# --------------------------------------------------------------------------- + + +def test_service_install_dispatches(): + """muxplex service install must call service_install().""" + from muxplex.cli import main + + with patch("muxplex.service.service_install") as mock_fn: + with patch("sys.argv", ["muxplex", "service", "install"]): + main() + mock_fn.assert_called_once() + + +def test_service_uninstall_dispatches(): + """muxplex service uninstall must call service_uninstall().""" + from muxplex.cli import main + + with patch("muxplex.service.service_uninstall") as mock_fn: + with patch("sys.argv", ["muxplex", "service", "uninstall"]): + main() + mock_fn.assert_called_once() + + +def test_service_start_dispatches(): + """muxplex service start must call service_start().""" + from muxplex.cli import main + + with patch("muxplex.service.service_start") as mock_fn: + with patch("sys.argv", ["muxplex", "service", "start"]): + main() + mock_fn.assert_called_once() + + +def test_service_stop_dispatches(): + """muxplex service stop must call service_stop().""" + from muxplex.cli import main + + with patch("muxplex.service.service_stop") as mock_fn: + with patch("sys.argv", ["muxplex", "service", "stop"]): + main() + mock_fn.assert_called_once() + + +def test_service_restart_dispatches(): + """muxplex service restart must call service_restart().""" + from muxplex.cli import main + + with patch("muxplex.service.service_restart") as mock_fn: + with patch("sys.argv", ["muxplex", "service", "restart"]): + main() + mock_fn.assert_called_once() + + +def test_service_status_dispatches(): + """muxplex service status must call service_status().""" + from muxplex.cli import main + + with patch("muxplex.service.service_status") as mock_fn: + with patch("sys.argv", ["muxplex", "service", "status"]): + main() + mock_fn.assert_called_once() + + +def test_service_logs_dispatches(): + """muxplex service logs must call service_logs().""" + from muxplex.cli import main + + with patch("muxplex.service.service_logs") as mock_fn: + with patch("sys.argv", ["muxplex", "service", "logs"]): + main() + mock_fn.assert_called_once() + + +def test_service_subcommand_in_help(): + """'service' must appear in muxplex --help output.""" + import io + + from muxplex.cli import main + + buf = io.StringIO() + with patch("sys.argv", ["muxplex", "--help"]): + try: + with patch("sys.stdout", buf): + main() + except SystemExit: + pass + + help_text = buf.getvalue().lower() + assert "service" in help_text