From 6e9afe3e54ab6fb0162393150b7892655b4561d4 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 24 Sep 2026 18:19:13 +0200 Subject: refactor(node): move the CLI out of daemon.py into cli/ Each `if args.command == ...` branch of main() becomes a function in cli/ (one module per family of verbs); the parser, the process setup and a VERBS table go to cli/parser.py and cli/dispatch.py. daemon.main runs the verb or starts the daemon. Tests patch the CLI through conftest.patch_cli; the CLI golden is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-node/src/meshbay_node/cli/status.py | 127 +++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 packages/meshbay-node/src/meshbay_node/cli/status.py (limited to 'packages/meshbay-node/src/meshbay_node/cli/status.py') diff --git a/packages/meshbay-node/src/meshbay_node/cli/status.py b/packages/meshbay-node/src/meshbay_node/cli/status.py new file mode 100644 index 0000000..162bd46 --- /dev/null +++ b/packages/meshbay-node/src/meshbay_node/cli/status.py @@ -0,0 +1,127 @@ +"""`meshbay-node status`: what the node is, read even while it is stopped.""" + +from pathlib import Path + +from meshbay_node.config import DEFAULT_CONFIG_PATH, load_config +from meshbay_node.keystore import load_keystore + + +def status(args) -> None: + import json as _json + import urllib.request + + cfg = load_config(args.config or DEFAULT_CONFIG_PATH) + print(f"hub {cfg.hub.url} (user {cfg.hub.username or '—'})") + + try: + keys = load_keystore( + path=cfg.keystore.path, unlock_file=cfg.keystore.unlock_file) + print(f"node key {keys.pk_ed25519_b64}") + except FileNotFoundError: + print("node key ") + except Exception as e: + print(f"node key ") + + token_file = cfg.data_dir / "ui-token" + live = None + if token_file.exists(): + try: + url = (f"http://127.0.0.1:{cfg.node.ui_port}" + f"/api/status?t={token_file.read_text(encoding="utf-8").strip()}") + with urllib.request.urlopen(url, timeout=3) as r: + live = _json.loads(r.read()) + except Exception: + live = None + + if live: + print(f"daemon running — {live.get('status')}") + print(f"node_id {live.get('endpoint_hint') or '—'}") + print(f"groups {live.get('group_count', 0)}" + f" files {live.get('total_files', 0)}" + f" peers {live.get('webrtc_peers', 0)}") + + needs = live.get("needs", []) + if needs: + _GUIDANCE = { + "node_key_link": ( + "Link node key", + f"Copy the node key above and paste it in " + f"Settings → Link Node on {cfg.hub.url}"), + "group_add": ( + "Add a group", + "meshbay-node group add --dir /path/to/files"), + "operator_pair": ( + "Pair as operator", + "meshbay-node operator pair"), + } + print() + print("action needed:") + for need in needs: + if need.startswith("gek_init:"): + name = need.split(":", 1)[1] + print(f" → Initialize group key for {name}") + print(f" meshbay-node gek init --group \"{name}\"") + elif need in _GUIDANCE: + label, hint = _GUIDANCE[need] + print(f" → {label}") + print(f" {hint}") + else: + print(f" → {need}") + else: + print("daemon not running") + + print(f"config {DEFAULT_CONFIG_PATH}") + if not cfg.groups: + print("groups none configured — create a group on the hub, then add") + print(" a [[groups]] entry with its id and a directory") + else: + for g in cfg.groups: + print(f" group {g.name} [{g.visibility}] {g.id or ''}") + if not g.roots: + print(" ") + for r in g.roots: + label = r.name or Path(r.path).name + flags = [] + if getattr(r, 'writable', False) or getattr(r, 'upload', False): + flags.append("rw") + else: + flags.append("ro") + if getattr(r, 'removable', False): + flags.append("removable") + flag_str = f" ({', '.join(flags)})" if flags else "" + live = "" if Path(r.path).expanduser().is_dir() else " [UNAVAILABLE]" + print(f" {label} → {r.path}{flag_str}{live}") + # Node authority: the roster is the source of truth, node.toml the legacy + # form. Read the DB directly so this reports correctly while the daemon is + # stopped — the state an operator is most often in when checking. + import asyncio as _asyncio + + from meshbay_node.roster import Roster as _Roster + + async def _read_roster() -> tuple[list, int]: + r = _Roster(db_path=cfg.data_dir / "roster.db") + await r.open() + try: + return (await r.list_members()), len(await r.list_invites()) + finally: + await r.close() + + try: + members, pending = _asyncio.run(_read_roster()) + except Exception as e: + members, pending = [], 0 + print(f"roster ") + + operators = [m for m in members if m["role"] == "operator" + and m["status"] == "active"] + if operators: + for op in operators: + print(f"operator {op.get('username') or op['user_id'][:8]}" + f" key {(op.get('pk_ed25519') or '')[:16]}…" + f" paired {op.get('pinned_at', '?')}") + else: + print("operator NONE PAIRED — file deletion and member invites are") + print(" refused. Run: meshbay-node operator pair") + if pending: + print(f"invites {pending} pending code(s)") + return -- cgit v1.2.3