"""`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