summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/cli/status.py
blob: 162bd46583fad10102bdffb273809c3d4d6eb4cd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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