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()
|
_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")
|
@daemon.command("start")
|
||||||
def daemon_start() -> None:
|
def daemon_start() -> None:
|
||||||
"""Start the Pyra daemon in the background."""
|
"""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")
|
@daemon.command("stop")
|
||||||
def daemon_stop() -> None:
|
def daemon_stop() -> None:
|
||||||
"""Stop the running Pyra daemon."""
|
"""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")
|
@daemon.command("status")
|
||||||
def daemon_status() -> None:
|
def daemon_status() -> None:
|
||||||
"""Show daemon status."""
|
"""Show daemon status."""
|
||||||
console.print("[yellow]Daemon (Stage 6) is not yet implemented.[/yellow]")
|
_daemon_ipc("status")
|
||||||
|
|
||||||
|
|
||||||
@daemon.command("restart")
|
@daemon.command("restart")
|
||||||
def daemon_restart() -> None:
|
def daemon_restart() -> None:
|
||||||
"""Restart the Pyra daemon."""
|
"""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")
|
@daemon.command("install")
|
||||||
def daemon_install() -> None:
|
def daemon_install() -> None:
|
||||||
"""Install Pyra as a system service (launchd/systemd)."""
|
"""Install Pyra as a system service (launchd/systemd/schtasks)."""
|
||||||
console.print("[yellow]Daemon service install (Stage 6) is not yet implemented.[/yellow]")
|
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")
|
@daemon.command("uninstall")
|
||||||
def daemon_uninstall() -> None:
|
def daemon_uninstall() -> None:
|
||||||
"""Remove the Pyra system service."""
|
"""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_ipc(cmd: str, *, success_msg: str | None = None) -> None:
|
||||||
def daemon_run() -> None:
|
"""Send a command to the running daemon via IPC and render the response."""
|
||||||
"""Run daemon in foreground (used by service manager)."""
|
from pyra.config.manager import load_config
|
||||||
console.print("[yellow]Daemon (Stage 6) is not yet implemented.[/yellow]")
|
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