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}