/ plugins / google_meet / node / cli.py
cli.py
  1  """`hermes meet node ...` subcommand tree.
  2  
  3  Wired into the existing ``hermes meet`` parser by the plugin's top-level
  4  CLI. This module only defines the subparsers and their dispatch — it
  5  does not mutate the existing cli.py.
  6  """
  7  
  8  from __future__ import annotations
  9  
 10  import argparse
 11  import asyncio
 12  import json
 13  import sys
 14  from typing import Any
 15  
 16  from plugins.google_meet.node.client import NodeClient
 17  from plugins.google_meet.node.registry import NodeRegistry
 18  from plugins.google_meet.node.server import NodeServer
 19  
 20  
 21  def register_cli(subparser: argparse.ArgumentParser) -> None:
 22      """Add ``run / list / approve / remove / status / ping`` subparsers.
 23  
 24      *subparser* is the ``hermes meet node`` argparse object — typically
 25      the result of ``meet_parser.add_parser('node', ...)``.
 26      """
 27      sp = subparser.add_subparsers(dest="node_cmd", required=True)
 28  
 29      run = sp.add_parser("run", help="Start a node server on this machine.")
 30      run.add_argument("--host", default="0.0.0.0")
 31      run.add_argument("--port", type=int, default=18789)
 32      run.add_argument("--display-name", default="hermes-meet-node")
 33      run.set_defaults(func=node_command)
 34  
 35      lst = sp.add_parser("list", help="List approved remote nodes.")
 36      lst.set_defaults(func=node_command)
 37  
 38      app = sp.add_parser("approve", help="Register a remote node on the gateway.")
 39      app.add_argument("name")
 40      app.add_argument("url")
 41      app.add_argument("token")
 42      app.set_defaults(func=node_command)
 43  
 44      rm = sp.add_parser("remove", help="Forget a registered node.")
 45      rm.add_argument("name")
 46      rm.set_defaults(func=node_command)
 47  
 48      st = sp.add_parser("status", help="Ping a registered node.")
 49      st.add_argument("name")
 50      st.set_defaults(func=node_command)
 51  
 52      pg = sp.add_parser("ping", help="Alias for status.")
 53      pg.add_argument("name")
 54      pg.set_defaults(func=node_command)
 55  
 56  
 57  def node_command(args: argparse.Namespace) -> int:
 58      """Dispatch for ``hermes meet node ...``.
 59  
 60      Returns a process exit code. Side-effects print to stdout/stderr.
 61      """
 62      cmd = getattr(args, "node_cmd", None)
 63  
 64      if cmd == "run":
 65          server = NodeServer(
 66              host=args.host,
 67              port=args.port,
 68              display_name=args.display_name,
 69          )
 70          token = server.ensure_token()
 71          print(f"[meet-node] display_name={server.display_name}")
 72          print(f"[meet-node] listening on ws://{args.host}:{args.port}")
 73          print(f"[meet-node] token (copy to gateway): {token}")
 74          print(f"[meet-node] approve with:")
 75          print(f"             hermes meet node approve <name> ws://<host>:{args.port} {token}")
 76          try:
 77              asyncio.run(server.serve())
 78          except KeyboardInterrupt:
 79              return 0
 80          except RuntimeError as exc:
 81              print(f"[meet-node] error: {exc}", file=sys.stderr)
 82              return 2
 83          return 0
 84  
 85      reg = NodeRegistry()
 86  
 87      if cmd == "list":
 88          nodes = reg.list_all()
 89          if not nodes:
 90              print("no nodes registered")
 91              return 0
 92          for n in nodes:
 93              print(f"{n['name']}\t{n['url']}\ttoken={n['token'][:6]}…")
 94          return 0
 95  
 96      if cmd == "approve":
 97          reg.add(args.name, args.url, args.token)
 98          print(f"approved node {args.name!r} at {args.url}")
 99          return 0
100  
101      if cmd == "remove":
102          ok = reg.remove(args.name)
103          print(f"removed {args.name!r}" if ok else f"no such node: {args.name!r}")
104          return 0 if ok else 1
105  
106      if cmd in ("status", "ping"):
107          entry = reg.get(args.name)
108          if entry is None:
109              print(f"no such node: {args.name!r}", file=sys.stderr)
110              return 1
111          client = NodeClient(entry["url"], entry["token"])
112          try:
113              result = client.ping()
114          except Exception as exc:  # noqa: BLE001 — surface any connection error
115              print(json.dumps({"ok": False, "error": str(exc)}))
116              return 1
117          print(json.dumps({"ok": True, "node": args.name, **_coerce_dict(result)}))
118          return 0
119  
120      print(f"unknown node command: {cmd!r}", file=sys.stderr)
121      return 2
122  
123  
124  def _coerce_dict(value: Any) -> dict:
125      return value if isinstance(value, dict) else {"result": value}