feat: add CLI entry point (muxplex serve, muxplex install-service) and __main__.py
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
"""Allow running muxplex as: python -m muxplex"""
|
||||||
|
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""muxplex CLI — web-based tmux session dashboard."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Module-level path constants (overridable in tests via monkeypatch)
|
||||||
|
_system_service_path = Path("/etc/systemd/system/muxplex.service")
|
||||||
|
|
||||||
|
|
||||||
|
def serve(host: str = "0.0.0.0", port: int = 8088) -> None:
|
||||||
|
"""Start the muxplex server."""
|
||||||
|
import uvicorn # noqa: PLC0415
|
||||||
|
|
||||||
|
os.environ.setdefault("MUXPLEX_PORT", str(port))
|
||||||
|
|
||||||
|
from muxplex.main import app # noqa: PLC0415
|
||||||
|
|
||||||
|
print(f" muxplex → http://{host}:{port}")
|
||||||
|
uvicorn.run(app, host=host, port=port, log_level="warning")
|
||||||
|
|
||||||
|
|
||||||
|
def install_service(*, system: bool = False) -> None:
|
||||||
|
"""Install muxplex as a systemd service."""
|
||||||
|
executable = sys.executable
|
||||||
|
|
||||||
|
unit = f"""\
|
||||||
|
[Unit]
|
||||||
|
Description=muxplex — web-based tmux session dashboard
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart={executable} -m muxplex
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
Environment=PATH={os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy={"multi-user.target" if system else "default.target"}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if system:
|
||||||
|
path = _system_service_path
|
||||||
|
reload_cmd = (
|
||||||
|
"sudo systemctl daemon-reload && sudo systemctl enable --now muxplex"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
path = Path.home() / ".config" / "systemd" / "user" / "muxplex.service"
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
reload_cmd = (
|
||||||
|
"systemctl --user daemon-reload && systemctl --user enable --now muxplex"
|
||||||
|
)
|
||||||
|
|
||||||
|
path.write_text(unit)
|
||||||
|
print(f"Service file written to {path}")
|
||||||
|
print(f"Enable with:\n {reload_cmd}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""CLI entry point."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="muxplex",
|
||||||
|
description="muxplex — web-based tmux session dashboard",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)"
|
||||||
|
)
|
||||||
|
parser.add_argument("--port", type=int, default=8088, help="Port (default: 8088)")
|
||||||
|
|
||||||
|
sub = parser.add_subparsers(dest="command")
|
||||||
|
sub.add_parser("serve", help="Start the server (default)")
|
||||||
|
|
||||||
|
svc = sub.add_parser("install-service", help="Install systemd service unit")
|
||||||
|
svc.add_argument(
|
||||||
|
"--system", action="store_true", help="System-wide (requires sudo)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "install-service":
|
||||||
|
install_service(system=args.system)
|
||||||
|
else:
|
||||||
|
serve(host=args.host, port=args.port)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""Tests for muxplex/cli.py — CLI entry point."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_module_importable():
|
||||||
|
"""muxplex.cli must be importable."""
|
||||||
|
from muxplex.cli import main # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_calls_serve_by_default():
|
||||||
|
"""Calling main() with no args must invoke serve()."""
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
|
with patch("sys.argv", ["muxplex"]):
|
||||||
|
main()
|
||||||
|
mock_serve.assert_called_once_with(host="0.0.0.0", port=8088)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_passes_custom_host_and_port():
|
||||||
|
"""main() with --host/--port must forward them to serve()."""
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
with patch("muxplex.cli.serve") as mock_serve:
|
||||||
|
with patch("sys.argv", ["muxplex", "--host", "127.0.0.1", "--port", "9000"]):
|
||||||
|
main()
|
||||||
|
mock_serve.assert_called_once_with(host="127.0.0.1", port=9000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_install_service_subcommand():
|
||||||
|
"""main() with 'install-service' must invoke install_service()."""
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
with patch("muxplex.cli.install_service") as mock_install:
|
||||||
|
with patch("sys.argv", ["muxplex", "install-service"]):
|
||||||
|
main()
|
||||||
|
mock_install.assert_called_once_with(system=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_install_service_system_flag():
|
||||||
|
"""main() with 'install-service --system' passes system=True."""
|
||||||
|
from muxplex.cli import main
|
||||||
|
|
||||||
|
with patch("muxplex.cli.install_service") as mock_install:
|
||||||
|
with patch("sys.argv", ["muxplex", "install-service", "--system"]):
|
||||||
|
main()
|
||||||
|
mock_install.assert_called_once_with(system=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_service_user_mode_writes_unit_file(tmp_path, monkeypatch):
|
||||||
|
"""install_service(system=False) writes a unit file to ~/.config/systemd/user/."""
|
||||||
|
from muxplex.cli import install_service
|
||||||
|
|
||||||
|
fake_home = tmp_path / "home"
|
||||||
|
fake_home.mkdir()
|
||||||
|
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
||||||
|
|
||||||
|
install_service(system=False)
|
||||||
|
|
||||||
|
unit_path = fake_home / ".config" / "systemd" / "user" / "muxplex.service"
|
||||||
|
assert unit_path.exists()
|
||||||
|
content = unit_path.read_text()
|
||||||
|
assert "[Unit]" in content
|
||||||
|
assert "[Service]" in content
|
||||||
|
assert "[Install]" in content
|
||||||
|
assert "muxplex" in content
|
||||||
|
assert "default.target" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_service_system_mode_target(tmp_path, monkeypatch):
|
||||||
|
"""install_service(system=True) targets multi-user.target in the unit file."""
|
||||||
|
from muxplex.cli import install_service
|
||||||
|
|
||||||
|
# Redirect the system path to tmp so we don't write to /etc
|
||||||
|
unit_path = tmp_path / "muxplex.service"
|
||||||
|
monkeypatch.setattr("muxplex.cli._system_service_path", unit_path)
|
||||||
|
|
||||||
|
install_service(system=True)
|
||||||
|
|
||||||
|
assert unit_path.exists()
|
||||||
|
content = unit_path.read_text()
|
||||||
|
assert "multi-user.target" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_dunder_main_calls_main():
|
||||||
|
"""python -m muxplex must call cli.main()."""
|
||||||
|
with patch("muxplex.cli.main") as mock_main:
|
||||||
|
# Simulate `python -m muxplex` by exec'ing __main__.py
|
||||||
|
import muxplex.__main__ # noqa: F401
|
||||||
|
|
||||||
|
# The import itself calls main() at module level
|
||||||
|
# Re-exec to test:
|
||||||
|
mock_main.reset_mock()
|
||||||
|
exec(Path("muxplex/__main__.py").read_text())
|
||||||
|
mock_main.assert_called_once()
|
||||||
Reference in New Issue
Block a user