summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/cli/status.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/cli/status.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/cli/status.py127
1 files changed, 127 insertions, 0 deletions
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 <no keystore — run: meshbay-node init>")
+ except Exception as e:
+ print(f"node key <keystore locked: {e}>")
+
+ 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 <name> --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 '<no id>'}")
+ if not g.roots:
+ print(" <no directory configured>")
+ 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 <unreadable: {e}>")
+
+ 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