feat(daemon): wire up all 7 daemon CLI commands

start/run/stop/status/restart/install/uninstall now call the real daemon
modules instead of printing stub messages. Includes a Rich status table
for `pyra daemon status` and friendly error messages when config is missing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-19 15:38:50 +02:00
parent 3d3ce694b9
commit c41ad0afc6
+112 -11
View File
@@ -266,43 +266,144 @@ def daemon() -> None:
_bootstrap_or_exit()
@daemon.command("run", hidden=True)
def daemon_run() -> None:
"""Run daemon in foreground (used by service manager)."""
from pyra.daemon.core import run_foreground
run_foreground()
@daemon.command("start")
def daemon_start() -> None:
"""Start the Pyra daemon in the background."""
console.print("[yellow]Daemon (Stage 6) is not yet implemented.[/yellow]")
from pyra.daemon.core import start_background
try:
start_background()
except FileNotFoundError:
console.print("[red]Error:[/red] Run [dim]pyra setup[/dim] first.")
@daemon.command("stop")
def daemon_stop() -> None:
"""Stop the running Pyra daemon."""
console.print("[yellow]Daemon (Stage 6) is not yet implemented.[/yellow]")
_daemon_ipc("stop", success_msg="Daemon stopped.")
@daemon.command("status")
def daemon_status() -> None:
"""Show daemon status."""
console.print("[yellow]Daemon (Stage 6) is not yet implemented.[/yellow]")
_daemon_ipc("status")
@daemon.command("restart")
def daemon_restart() -> None:
"""Restart the Pyra daemon."""
console.print("[yellow]Daemon (Stage 6) is not yet implemented.[/yellow]")
import time
from pyra.daemon.core import start_background
_daemon_ipc("stop", success_msg=None)
time.sleep(1.5)
try:
start_background()
except FileNotFoundError:
console.print("[red]Error:[/red] Run [dim]pyra setup[/dim] first.")
@daemon.command("install")
def daemon_install() -> None:
"""Install Pyra as a system service (launchd/systemd)."""
console.print("[yellow]Daemon service install (Stage 6) is not yet implemented.[/yellow]")
"""Install Pyra as a system service (launchd/systemd/schtasks)."""
from pyra.daemon.service import detect_platform, install_service
try:
install_service()
console.print(f"[green]Service installed[/green] ({detect_platform()}).")
except Exception as exc:
console.print(f"[red]Install failed:[/red] {exc}")
@daemon.command("uninstall")
def daemon_uninstall() -> None:
"""Remove the Pyra system service."""
console.print("[yellow]Daemon service uninstall (Stage 6) is not yet implemented.[/yellow]")
from pyra.daemon.service import uninstall_service
try:
uninstall_service()
console.print("[green]Service removed.[/green]")
except Exception as exc:
console.print(f"[red]Uninstall failed:[/red] {exc}")
@daemon.command("run", hidden=True)
def daemon_run() -> None:
"""Run daemon in foreground (used by service manager)."""
console.print("[yellow]Daemon (Stage 6) is not yet implemented.[/yellow]")
def _daemon_ipc(cmd: str, *, success_msg: str | None = None) -> None:
"""Send a command to the running daemon via IPC and render the response."""
from pyra.config.manager import load_config
from pyra.daemon.ipc import (
get_socket_path,
is_unix_socket,
get_port_file_path,
send_command,
)
try:
cfg = load_config()
except FileNotFoundError:
console.print("[red]Error:[/red] Run [dim]pyra setup[/dim] first.")
return
if is_unix_socket():
address = get_socket_path(cfg.daemon.socket_path)
else:
port = _read_windows_port()
if port is None:
console.print("[yellow]Daemon is not running.[/yellow]")
return
address = ("127.0.0.1", port)
try:
resp = send_command(address, {"cmd": cmd})
except (ConnectionRefusedError, FileNotFoundError, OSError):
console.print("[yellow]Daemon is not running.[/yellow]")
return
except ConnectionResetError:
console.print("[red]Permission denied:[/red] daemon rejected connection.")
return
except TimeoutError:
console.print("[red]Daemon did not respond in time.[/red]")
return
if not resp.get("ok"):
console.print(f"[red]Error:[/red] {resp.get('data', {}).get('error', 'unknown')}")
return
if cmd == "status":
_render_daemon_status(resp["data"])
elif success_msg:
console.print(f"[green]{success_msg}[/green]")
def _read_windows_port() -> int | None:
from pyra.daemon.ipc import get_port_file_path
try:
return int(get_port_file_path().read_text().strip())
except (FileNotFoundError, ValueError):
return None
def _render_daemon_status(data: dict) -> None:
from rich.table import Table
uptime = data.get("uptime", 0.0)
pid = data.get("pid", "?")
tasks = data.get("tasks", [])
hours, rem = divmod(int(uptime), 3600)
mins, secs = divmod(rem, 60)
uptime_str = f"{hours}h {mins}m {secs}s" if hours else f"{mins}m {secs}s"
console.print(f"[bold green]Daemon running[/bold green] — PID {pid}, uptime {uptime_str}")
if tasks:
table = Table("Task", "Alive", "Restarts", "Last error", show_header=True)
for t in tasks:
alive = "[green]yes[/green]" if t.get("alive") else "[red]no[/red]"
error = t.get("last_error") or ""
table.add_row(t.get("name", "?"), alive, str(t.get("restart_count", 0)), error)
console.print(table)
else:
console.print("[dim]No plugin tasks registered.[/dim]")