File size: 4,492 Bytes
8f53c4c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""HearthNet mesh launcher — start a local node, optionally join the internet mesh.

Local-first by design:

    python scripts/start_mesh_node.py
        → starts a pure local node (mDNS/UDP discovery + local HTTP server).
          Makes NO outbound internet calls.

    python scripts/start_mesh_node.py --connect hf
        → also joins the public HF Space relay hub, so this node meshes
          all-to-all with every other connected node over NAT.

    python scripts/start_mesh_node.py --connect <invite-or-relay-url>
        → joins the relay hub embedded in an invite (hn1:...) or given directly.

The node stays running (Ctrl-C to stop). While connected, inbound bus calls from
other mesh members are served locally and your calls route to them transparently.
"""

from __future__ import annotations

import argparse
import asyncio
import contextlib
import secrets

HF_SPACE_URL = "https://build-small-hackathon-hearthnet.hf.space"


def _resolve_relay(connect: str) -> str:
    """Map a --connect value to a relay base URL.

    Accepts ``hf`` (the public Space), an ``hn1:`` invite (relay extracted), or a
    raw http(s) relay URL.
    """
    if connect in ("hf", "space"):
        return HF_SPACE_URL
    if connect.startswith("hn1:"):
        from hearthnet.ui.onboarding import decode_invite

        blob = decode_invite(connect)
        if not blob.relay_url:
            raise SystemExit("invite has no relay_url embedded")
        return blob.relay_url
    if connect.startswith(("http://", "https://")):
        return connect.rstrip("/")
    raise SystemExit(f"unrecognised --connect value: {connect!r}")


async def _run(args: argparse.Namespace) -> int:
    from hearthnet.node import HearthNode

    node_id = args.node_id or f"ed25519:mesh-{secrets.token_hex(4)}"
    node = HearthNode(node_id, args.name, args.community)

    if args.demo_services:
        node.install_demo_services(corpus="demo")
    else:
        with contextlib.suppress(Exception):
            node.install_services(corpus="community")

    local_caps = sorted({e.descriptor.name for e in node.bus.registry.all_local()})
    print(f"[node] {node.display_name} ({node_id})")
    print(f"[node] local capabilities: {local_caps}")

    await node.start(host=args.host, port=args.port)
    print(f"[node] local HTTP server on {args.host}:{args.port} (local-first)")

    if args.connect:
        relay_url = _resolve_relay(args.connect)
        print(f"[mesh] joining relay hub {relay_url} ...")
        try:
            result = await node.join_relay(relay_url, token=args.token or None)
        except Exception as exc:  # surface a clear startup error, keep node local
            print(f"[mesh] x relay join failed: {exc}")
            print("       The Space may be asleep/building. Open its UI once and retry.")
        else:
            members = [m.get("node_id", "")[:24] for m in result.get("roster", [])]
            print(f"[mesh] + joined. {len(members)} other member(s): {members}")
            print("[mesh] all-to-all: your bus calls now route to mesh peers over NAT.")
    else:
        print("[mesh] running local-only (no relay). Use --connect hf to mesh.")

    print("[node] up. Press Ctrl-C to stop.")
    stop = asyncio.Event()
    try:
        await stop.wait()
    except (KeyboardInterrupt, asyncio.CancelledError):
        pass
    finally:
        print("\n[node] shutting down ...")
        await node.stop()
    return 0


def main() -> None:
    parser = argparse.ArgumentParser(description="Start a HearthNet mesh node")
    parser.add_argument("--name", default="Mesh Node", help="display name")
    parser.add_argument("--node-id", default="", help="full node id (default: random)")
    parser.add_argument("--community", default="ed25519:community", help="community id")
    parser.add_argument("--host", default="127.0.0.1", help="local HTTP bind host")
    parser.add_argument("--port", type=int, default=7080, help="local HTTP port")
    parser.add_argument(
        "--connect",
        default="",
        help="'hf', an hn1: invite, or a relay URL to join the internet mesh",
    )
    parser.add_argument("--token", default="", help="optional relay join token")
    parser.add_argument(
        "--demo-services",
        action="store_true",
        help="install fast echo/demo services instead of real local backends",
    )
    args = parser.parse_args()
    raise SystemExit(asyncio.run(_run(args)))


if __name__ == "__main__":
    main()