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:
+112
-11
@@ -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]")
|
||||
|
||||
Reference in New Issue
Block a user