789 lines
24 KiB
Python
789 lines
24 KiB
Python
"""Command-line interface for Radicle-Reticulum adapter."""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import signal
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
DEFAULT_IDENTITY_PATH = Path.home() / ".radicle-rns" / "identity"
|
|
|
|
import RNS
|
|
|
|
from radicle_reticulum.adapter import RNSTransportAdapter, PeerInfo
|
|
from radicle_reticulum.identity import RadicleIdentity
|
|
from radicle_reticulum.bridge import RadicleBridge
|
|
from radicle_reticulum.gossip import GossipRelay
|
|
from radicle_reticulum.seed import SeedNode, DEFAULT_SEED_HOME, DEFAULT_SEED_PORT
|
|
|
|
|
|
def detect_radicle_nid() -> Optional[str]:
|
|
"""Try to detect the local radicle NID by running 'rad self'.
|
|
|
|
Returns the NID string (e.g. 'z6Mk...') or None if rad is not available
|
|
or the NID cannot be parsed.
|
|
"""
|
|
try:
|
|
result = subprocess.run(
|
|
["rad", "self"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
if result.returncode != 0:
|
|
return None
|
|
for line in result.stdout.splitlines():
|
|
parts = line.split()
|
|
if len(parts) >= 2 and parts[0] == "NID":
|
|
return parts[1]
|
|
# Older versions may print DID format
|
|
if len(parts) >= 2 and parts[0] == "DID":
|
|
did = parts[1]
|
|
if did.startswith("did:key:"):
|
|
return did[len("did:key:"):]
|
|
return None
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
|
return None
|
|
|
|
|
|
def on_peer_discovered(peer: PeerInfo):
|
|
"""Callback when a new peer is discovered."""
|
|
print(f"[+] Discovered peer: {peer.identity.did}")
|
|
print(f" RNS hash: {peer.destination_hash.hex()}")
|
|
|
|
|
|
def cmd_node(args):
|
|
"""Run a Radicle-RNS node."""
|
|
print("Starting Radicle-RNS node...")
|
|
|
|
identity = RadicleIdentity.load_or_generate(args.identity)
|
|
_print_identity_info(args.identity)
|
|
|
|
# Create adapter
|
|
adapter = RNSTransportAdapter(identity=identity)
|
|
adapter.set_on_peer_discovered(on_peer_discovered)
|
|
|
|
print(f"Node ID: {identity.did}")
|
|
print(f"RNS Hash: {adapter.node_hash_hex}")
|
|
print()
|
|
|
|
# Start adapter
|
|
adapter.start()
|
|
|
|
print("Node running. Press Ctrl+C to stop.")
|
|
print("Announcing every 60 seconds...")
|
|
print()
|
|
|
|
# Handle graceful shutdown
|
|
running = True
|
|
def signal_handler(sig, frame):
|
|
nonlocal running
|
|
print("\nShutting down...")
|
|
running = False
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
# Main loop
|
|
last_announce = 0
|
|
try:
|
|
while running:
|
|
now = time.time()
|
|
|
|
# Periodic announce
|
|
if now - last_announce > 60:
|
|
adapter.announce()
|
|
last_announce = now
|
|
|
|
time.sleep(0.5)
|
|
finally:
|
|
adapter.stop()
|
|
print("Node stopped.")
|
|
|
|
|
|
def _print_identity_info(identity_path: Path):
|
|
"""Print identity file location (new or loaded)."""
|
|
path = Path(identity_path)
|
|
status = "loaded" if path.exists() else "generated"
|
|
print(f"Identity {status}: {path}")
|
|
|
|
|
|
def cmd_identity(args):
|
|
"""Generate or display identity information."""
|
|
if args.action == "generate":
|
|
path = Path(args.identity)
|
|
if path.exists() and not args.force:
|
|
print(f"Identity already exists: {path}")
|
|
print("Use --force to overwrite, or --identity <path> for a different path.")
|
|
sys.exit(1)
|
|
identity = RadicleIdentity.generate()
|
|
identity.save(path)
|
|
print(f"Generated new identity: {path}")
|
|
print(f" DID: {identity.did}")
|
|
print(f" RNS Hash: {identity.rns_identity_hash_hex}")
|
|
elif args.action == "info":
|
|
path = Path(args.identity)
|
|
if path.exists():
|
|
try:
|
|
identity = RadicleIdentity.load(path)
|
|
print(f"Identity: {path}")
|
|
print(f" DID: {identity.did}")
|
|
print(f" RNS Hash: {identity.rns_identity_hash_hex}")
|
|
print(f" Public key (hex): {identity.public_key_bytes.hex()}")
|
|
except Exception as e:
|
|
print(f"Error loading identity: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
elif args.did:
|
|
try:
|
|
identity = RadicleIdentity.from_did(args.did)
|
|
print(f"DID: {identity.did}")
|
|
print(f"Public key (hex): {identity.public_key_bytes.hex()}")
|
|
except Exception as e:
|
|
print(f"Error parsing DID: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
else:
|
|
print(f"No identity file found at {path} and no --did provided.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def cmd_ping(args):
|
|
"""Ping a peer by RNS hash."""
|
|
print(f"Connecting to {args.destination}...")
|
|
|
|
identity = RadicleIdentity.load_or_generate(args.identity)
|
|
adapter = RNSTransportAdapter(identity=identity)
|
|
adapter.start()
|
|
|
|
try:
|
|
dest_hash = bytes.fromhex(args.destination)
|
|
except ValueError:
|
|
print("Error: Invalid destination hash (must be hex)", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
link = adapter.connect(dest_hash, timeout=args.timeout)
|
|
if link is None:
|
|
print("Failed to connect")
|
|
sys.exit(1)
|
|
|
|
print(f"Connected! RTT: {link.rtt:.3f}s" if link.rtt else "Connected!")
|
|
|
|
# Send ping
|
|
from radicle_reticulum.messages import Ping, Pong, decode_message, MessageType
|
|
import struct
|
|
|
|
ping = Ping()
|
|
ping_time = time.time()
|
|
link.send(ping.to_message())
|
|
print("Ping sent, waiting for pong...")
|
|
|
|
response = link.recv(timeout=10.0)
|
|
if response:
|
|
header, msg = decode_message(response)
|
|
if header.msg_type == MessageType.PONG:
|
|
rtt = (time.time() - ping_time) * 1000
|
|
print(f"Pong received! RTT: {rtt:.1f}ms")
|
|
else:
|
|
print(f"Unexpected response: {header.msg_type}")
|
|
else:
|
|
print("No response (timeout)")
|
|
|
|
link.close()
|
|
adapter.stop()
|
|
|
|
|
|
def cmd_peers(args):
|
|
"""List discovered peers."""
|
|
identity = RadicleIdentity.load_or_generate(args.identity)
|
|
adapter = RNSTransportAdapter(identity=identity)
|
|
adapter.set_on_peer_discovered(on_peer_discovered)
|
|
adapter.start()
|
|
|
|
print(f"Listening for peers for {args.timeout} seconds...")
|
|
print()
|
|
|
|
time.sleep(args.timeout)
|
|
|
|
peers = adapter.get_peers()
|
|
if peers:
|
|
print(f"\nDiscovered {len(peers)} peer(s):")
|
|
for peer in peers:
|
|
print(f" {peer.identity.did}")
|
|
print(f" Hash: {peer.destination_hash.hex()}")
|
|
print(f" Age: {peer.age:.1f}s")
|
|
else:
|
|
print("\nNo peers discovered.")
|
|
|
|
adapter.stop()
|
|
|
|
|
|
def _detect_rid(repo_path: Path) -> Optional[str]:
|
|
"""Detect the Radicle RID for the repo at repo_path via 'rad inspect'."""
|
|
try:
|
|
result = subprocess.run(
|
|
["rad", "inspect", "--rid"],
|
|
cwd=repo_path,
|
|
capture_output=True, text=True, timeout=5,
|
|
)
|
|
if result.returncode == 0:
|
|
rid = result.stdout.strip()
|
|
if rid.startswith("rad:"):
|
|
return rid
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def cmd_gossip(args):
|
|
"""Run the gossip relay daemon."""
|
|
identity = RadicleIdentity.load_or_generate(args.identity)
|
|
_print_identity_info(args.identity)
|
|
|
|
# Collect RIDs: explicit args + auto-detect from CWD
|
|
rids = list(args.rids)
|
|
if not rids:
|
|
rid = _detect_rid(Path.cwd())
|
|
if rid:
|
|
print(f"Auto-detected RID: {rid}")
|
|
rids.append(rid)
|
|
else:
|
|
print("Error: no RIDs given and could not auto-detect from current directory.", file=sys.stderr)
|
|
print("Pass one or more RIDs as arguments, or run from inside a radicle repo.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
nid = args.nid or detect_radicle_nid()
|
|
if nid:
|
|
print(f"Local NID: {nid}")
|
|
|
|
try:
|
|
announce_retry_delays = tuple(
|
|
int(x.strip()) for x in args.announce_retry_delays.split(",") if x.strip()
|
|
)
|
|
except ValueError:
|
|
print("Error: --announce-retry-delays must be comma-separated integers.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
relay = GossipRelay(
|
|
identity=identity,
|
|
rids=rids,
|
|
radicle_nid=nid,
|
|
bridge_port=args.bridge_port,
|
|
poll_interval=args.poll_interval,
|
|
announce_retry_delays=announce_retry_delays,
|
|
)
|
|
|
|
def on_sync(rid, peer_nid):
|
|
print(f"[sync] {rid[:24]}... from {peer_nid[:24] if peer_nid else 'peer'}")
|
|
|
|
relay.set_on_sync_triggered(on_sync)
|
|
relay.start()
|
|
|
|
print()
|
|
print(f"Gossip relay running:")
|
|
print(f" RNS address: {relay.destination.hexhash}")
|
|
print(f" Repos: {', '.join(r[:28] for r in rids)}")
|
|
print(f" Poll: every {args.poll_interval}s")
|
|
print()
|
|
print("Press Ctrl+C to stop.")
|
|
print()
|
|
|
|
running = True
|
|
|
|
def signal_handler(sig, frame):
|
|
nonlocal running
|
|
print("\nShutting down...")
|
|
running = False
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
last_known_peers = -1
|
|
try:
|
|
while running:
|
|
stats = relay.get_stats()
|
|
if stats["known_peers"] != last_known_peers:
|
|
last_known_peers = stats["known_peers"]
|
|
print(f"[Status] Peers: {stats['known_peers']}, "
|
|
f"Repos: {stats['watched_repos']}, "
|
|
f"Refs: {stats['refs_per_repo']}")
|
|
time.sleep(5)
|
|
finally:
|
|
relay.stop()
|
|
print("Gossip relay stopped.")
|
|
|
|
|
|
def cmd_seed(args):
|
|
"""Start a dedicated seed radicle-node, bridge, and gossip relay."""
|
|
seed_home = Path(args.seed_home)
|
|
|
|
seed = SeedNode(seed_home=seed_home, port=args.seed_port)
|
|
|
|
# First-time setup: guide the user
|
|
if not seed.is_initialized():
|
|
seed.write_config()
|
|
print(f"Seed home: {seed_home}")
|
|
print()
|
|
print("Seed identity not found. Initialize it with:")
|
|
print(f" RAD_HOME={seed_home} rad auth")
|
|
print()
|
|
print("Then run 'radicle-rns seed' again.")
|
|
sys.exit(1)
|
|
|
|
# Start seed radicle-node
|
|
print(f"Starting seed radicle-node (port {args.seed_port})...")
|
|
try:
|
|
seed.start()
|
|
except RuntimeError as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
nid = seed.get_nid()
|
|
if not nid:
|
|
print("Error: could not get seed NID.", file=sys.stderr)
|
|
seed.stop()
|
|
sys.exit(1)
|
|
|
|
# Start bridge pointing at the seed
|
|
identity = RadicleIdentity.load_or_generate(args.identity)
|
|
_print_identity_info(args.identity)
|
|
|
|
try:
|
|
announce_retry_delays = tuple(
|
|
int(x.strip()) for x in args.announce_retry_delays.split(",") if x.strip()
|
|
)
|
|
except ValueError:
|
|
print("Error: --announce-retry-delays must be comma-separated integers.", file=sys.stderr)
|
|
seed.stop()
|
|
sys.exit(1)
|
|
|
|
bridge = RadicleBridge(
|
|
identity=identity,
|
|
listen_port=args.bridge_port,
|
|
radicle_host="127.0.0.1",
|
|
radicle_port=args.seed_port,
|
|
auto_connect=True,
|
|
auto_seed=True,
|
|
announce_retry_delays=announce_retry_delays,
|
|
rad_home=str(seed_home),
|
|
)
|
|
bridge.set_local_radicle_nid(nid)
|
|
|
|
def on_peer_seed_discovered(dest_hash, remote_nid=None):
|
|
nid_info = f" (NID: {remote_nid[:32]})" if remote_nid else ""
|
|
print(f"[+] Discovered remote seed: {dest_hash.hex()[:16]}{nid_info}")
|
|
|
|
bridge.set_on_bridge_discovered(on_peer_seed_discovered)
|
|
|
|
# Gossip relay: watches seed's storage, notifies remote seeds of ref changes.
|
|
# bridge_port=None: bridge's auto_seed already registered NIDs on correct ports.
|
|
gossip = GossipRelay(
|
|
identity=identity,
|
|
rids=[],
|
|
storage=seed_home / "storage",
|
|
radicle_nid=nid,
|
|
bridge_port=None,
|
|
poll_interval=args.poll_interval,
|
|
announce_retry_delays=announce_retry_delays,
|
|
auto_discover=True,
|
|
rad_home=str(seed_home),
|
|
)
|
|
|
|
running = True
|
|
|
|
def signal_handler(sig, frame):
|
|
nonlocal running
|
|
print("\nShutting down...")
|
|
running = False
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
try:
|
|
bridge.start()
|
|
gossip.start()
|
|
|
|
print()
|
|
print("Seed node running.")
|
|
print(f" Seed NID: {nid}")
|
|
print(f" Seed port: {args.seed_port}")
|
|
print(f" Bridge hash: {bridge.destination.hexhash}")
|
|
print(f" Gossip hash: {gossip.destination.hexhash}")
|
|
print()
|
|
print("Add this seed to your radicle node (one-time setup):")
|
|
print(f" rad node connect {nid}@127.0.0.1:{args.seed_port}")
|
|
print()
|
|
print("Other machines running 'radicle-rns seed' will discover this")
|
|
print("seed automatically and sync over the mesh.")
|
|
print()
|
|
print("Press Ctrl+C to stop.")
|
|
print()
|
|
|
|
last_known_bridges = -1
|
|
while running:
|
|
if not seed.is_running():
|
|
print("Seed radicle-node exited unexpectedly.", file=sys.stderr)
|
|
break
|
|
|
|
stats = bridge.get_stats()
|
|
if stats["known_bridges"] != last_known_bridges:
|
|
last_known_bridges = stats["known_bridges"]
|
|
print(f"[Status] Remote seeds: {stats['known_bridges']}, "
|
|
f"Tunnels: {stats['active_tunnels']}, "
|
|
f"Gossip peers: {gossip.get_stats()['known_peers']}")
|
|
|
|
time.sleep(5)
|
|
finally:
|
|
gossip.stop()
|
|
bridge.stop()
|
|
seed.stop()
|
|
print("Seed stopped.")
|
|
|
|
|
|
def cmd_bridge(args):
|
|
"""Run Radicle-Reticulum bridge."""
|
|
print("Starting Radicle-Reticulum bridge...")
|
|
|
|
identity = RadicleIdentity.load_or_generate(args.identity)
|
|
_print_identity_info(args.identity)
|
|
|
|
# Determine auto modes
|
|
auto_connect = not args.no_auto_connect and not args.connect
|
|
auto_seed = not args.no_auto_seed
|
|
|
|
try:
|
|
announce_retry_delays = tuple(
|
|
int(x.strip()) for x in args.announce_retry_delays.split(",") if x.strip()
|
|
)
|
|
except ValueError:
|
|
print("Error: --announce-retry-delays must be comma-separated integers, e.g. 5,15,30", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
bridge = RadicleBridge(
|
|
identity=identity,
|
|
listen_port=args.listen_port,
|
|
radicle_host=args.radicle_host,
|
|
radicle_port=args.radicle_port,
|
|
auto_connect=auto_connect,
|
|
auto_seed=auto_seed,
|
|
announce_retry_delays=announce_retry_delays,
|
|
)
|
|
|
|
# Resolve local radicle NID: explicit flag > auto-detect from 'rad self'
|
|
nid = args.nid
|
|
if not nid:
|
|
nid = detect_radicle_nid()
|
|
if nid:
|
|
print(f"Auto-detected radicle NID: {nid}")
|
|
else:
|
|
print("Could not auto-detect NID (is rad installed and initialized?)")
|
|
print("Run with --nid <YOUR_NID> to set it manually.")
|
|
if nid:
|
|
bridge.set_local_radicle_nid(nid)
|
|
|
|
# Set up discovery callback
|
|
def on_bridge_discovered(dest_hash: bytes, radicle_nid: str = None):
|
|
nid_info = ""
|
|
if radicle_nid:
|
|
nid_info = f"\n Radicle NID: {radicle_nid}"
|
|
print(f"[+] Discovered bridge: {dest_hash.hex()}{nid_info}")
|
|
if auto_connect:
|
|
print(f" Auto-connecting...")
|
|
if auto_seed and radicle_nid:
|
|
print(f" Auto-registering seed with radicle-node...")
|
|
|
|
bridge.set_on_bridge_discovered(on_bridge_discovered)
|
|
|
|
bridge.start()
|
|
|
|
print()
|
|
print(f"Bridge running:")
|
|
print(f" RNS address: {bridge.destination.hexhash}")
|
|
print(f" TCP listen: 127.0.0.1:{args.listen_port}")
|
|
print(f" Radicle: {args.radicle_host}:{args.radicle_port}")
|
|
print(f" Auto-connect: {'enabled' if auto_connect else 'disabled'}")
|
|
print(f" Auto-seed: {'enabled' if auto_seed else 'disabled'}")
|
|
if args.nid:
|
|
print(f" Local NID: {args.nid}")
|
|
print()
|
|
|
|
# Connect to remote bridge if specified
|
|
if args.connect:
|
|
try:
|
|
remote_hash = bytes.fromhex(args.connect)
|
|
print(f"Connecting to remote bridge: {args.connect}")
|
|
if bridge.connect_to_bridge(remote_hash):
|
|
print("Connected!")
|
|
else:
|
|
print("Failed to connect to remote bridge")
|
|
except ValueError:
|
|
print(f"Invalid remote hash: {args.connect}", file=sys.stderr)
|
|
elif auto_connect:
|
|
print("Waiting for bridge announcements (auto-discovery enabled)...")
|
|
|
|
if nid and auto_seed:
|
|
print()
|
|
print("When remote bridges are discovered, their NIDs will be")
|
|
print("automatically registered with radicle-node. You can then")
|
|
print("use radicle normally (clone, push, pull, etc.)")
|
|
|
|
print()
|
|
print("Press Ctrl+C to stop.")
|
|
print()
|
|
|
|
# Handle shutdown
|
|
running = True
|
|
def signal_handler(sig, frame):
|
|
nonlocal running
|
|
print("\nShutting down...")
|
|
running = False
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
# Status loop
|
|
last_announce = 0
|
|
last_stats = None
|
|
try:
|
|
while running:
|
|
now = time.time()
|
|
|
|
# Periodic announce
|
|
if now - last_announce > 120:
|
|
bridge.announce()
|
|
last_announce = now
|
|
|
|
# Show status changes
|
|
stats = bridge.get_stats()
|
|
if stats != last_stats:
|
|
print(f"[Status] Tunnels: {stats['active_tunnels']}, "
|
|
f"Remote bridges: {stats['known_bridges']}, "
|
|
f"TX: {stats['bytes_sent']}, RX: {stats['bytes_received']}")
|
|
last_stats = stats.copy()
|
|
|
|
time.sleep(1.0)
|
|
finally:
|
|
bridge.stop()
|
|
print("Bridge stopped.")
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Radicle-Reticulum transport adapter"
|
|
)
|
|
parser.add_argument(
|
|
"-v", "--verbose",
|
|
action="store_true",
|
|
help="Enable verbose logging"
|
|
)
|
|
|
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
|
|
def add_identity_arg(p):
|
|
p.add_argument(
|
|
"--identity",
|
|
type=Path,
|
|
default=DEFAULT_IDENTITY_PATH,
|
|
metavar="PATH",
|
|
help=f"Identity file (default: {DEFAULT_IDENTITY_PATH})",
|
|
)
|
|
|
|
# node command
|
|
node_parser = subparsers.add_parser("node", help="Run a Radicle-RNS node")
|
|
add_identity_arg(node_parser)
|
|
|
|
# identity command
|
|
id_parser = subparsers.add_parser("identity", help="Identity operations")
|
|
id_parser.add_argument(
|
|
"action",
|
|
choices=["generate", "info"],
|
|
help="Action to perform"
|
|
)
|
|
add_identity_arg(id_parser)
|
|
id_parser.add_argument("--did", help="DID string for info action (if no identity file)")
|
|
id_parser.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="Overwrite existing identity file (for generate)"
|
|
)
|
|
|
|
# ping command
|
|
ping_parser = subparsers.add_parser("ping", help="Ping a peer")
|
|
ping_parser.add_argument("destination", help="RNS destination hash (hex)")
|
|
ping_parser.add_argument(
|
|
"-t", "--timeout",
|
|
type=float,
|
|
default=30.0,
|
|
help="Connection timeout (seconds)"
|
|
)
|
|
add_identity_arg(ping_parser)
|
|
|
|
# peers command
|
|
peers_parser = subparsers.add_parser("peers", help="Discover peers")
|
|
peers_parser.add_argument(
|
|
"-t", "--timeout",
|
|
type=float,
|
|
default=10.0,
|
|
help="Discovery timeout (seconds)"
|
|
)
|
|
add_identity_arg(peers_parser)
|
|
|
|
# gossip command
|
|
gossip_parser = subparsers.add_parser(
|
|
"gossip",
|
|
help="Run gossip relay: watch refs, notify peers, trigger rad sync",
|
|
)
|
|
gossip_parser.add_argument(
|
|
"rids",
|
|
nargs="*",
|
|
metavar="RID",
|
|
help="Radicle repo IDs to watch (auto-detected from CWD if omitted)",
|
|
)
|
|
gossip_parser.add_argument(
|
|
"--nid",
|
|
help="Local radicle NID to advertise (auto-detected if omitted)",
|
|
)
|
|
gossip_parser.add_argument(
|
|
"--bridge-port",
|
|
type=int,
|
|
default=8777,
|
|
metavar="PORT",
|
|
help="TCP port of the local bridge (default: 8777)",
|
|
)
|
|
gossip_parser.add_argument(
|
|
"--poll-interval",
|
|
type=int,
|
|
default=30,
|
|
metavar="SECONDS",
|
|
help="Seconds between ref polls (default: 30)",
|
|
)
|
|
gossip_parser.add_argument(
|
|
"--announce-retry-delays",
|
|
default="5,15,30",
|
|
metavar="SECONDS",
|
|
help="Startup re-announce delays, comma-separated (default: 5,15,30). "
|
|
"Use longer values on LoRa, e.g. 60,300,900",
|
|
)
|
|
add_identity_arg(gossip_parser)
|
|
|
|
# seed command
|
|
seed_parser = subparsers.add_parser(
|
|
"seed",
|
|
help="Start a Radicle seed node and bridge it to the mesh over Reticulum",
|
|
)
|
|
seed_parser.add_argument(
|
|
"--seed-home",
|
|
default=str(DEFAULT_SEED_HOME),
|
|
metavar="PATH",
|
|
help=f"RAD_HOME for the seed node (default: {DEFAULT_SEED_HOME})",
|
|
)
|
|
seed_parser.add_argument(
|
|
"--seed-port",
|
|
type=int,
|
|
default=DEFAULT_SEED_PORT,
|
|
metavar="PORT",
|
|
help=f"TCP port for the seed radicle-node (default: {DEFAULT_SEED_PORT})",
|
|
)
|
|
seed_parser.add_argument(
|
|
"--bridge-port",
|
|
type=int,
|
|
default=8778,
|
|
metavar="PORT",
|
|
help="TCP listen port for the seed bridge (default: 8778)",
|
|
)
|
|
seed_parser.add_argument(
|
|
"--poll-interval",
|
|
type=int,
|
|
default=30,
|
|
metavar="SECONDS",
|
|
help="Seconds between gossip ref polls (default: 30)",
|
|
)
|
|
seed_parser.add_argument(
|
|
"--announce-retry-delays",
|
|
default="5,15,30",
|
|
metavar="SECONDS",
|
|
help="Startup re-announce delays, comma-separated (default: 5,15,30). "
|
|
"Use longer values on LoRa, e.g. 60,300,900",
|
|
)
|
|
add_identity_arg(seed_parser)
|
|
|
|
bridge_parser = subparsers.add_parser("bridge", help="Run Radicle-Reticulum bridge")
|
|
bridge_parser.add_argument(
|
|
"-l", "--listen-port",
|
|
type=int,
|
|
default=8777,
|
|
help="TCP port to listen on (default: 8777)"
|
|
)
|
|
bridge_parser.add_argument(
|
|
"--radicle-host",
|
|
default="127.0.0.1",
|
|
help="Radicle node host (default: 127.0.0.1)"
|
|
)
|
|
bridge_parser.add_argument(
|
|
"--radicle-port",
|
|
type=int,
|
|
default=8776,
|
|
help="Radicle node port (default: 8776)"
|
|
)
|
|
bridge_parser.add_argument(
|
|
"-c", "--connect",
|
|
help="Connect to remote bridge (RNS hash)"
|
|
)
|
|
bridge_parser.add_argument(
|
|
"--no-auto-connect",
|
|
action="store_true",
|
|
help="Disable auto-discovery and auto-connect to bridges"
|
|
)
|
|
bridge_parser.add_argument(
|
|
"--no-auto-seed",
|
|
action="store_true",
|
|
help="Disable auto-registering discovered NIDs with radicle-node"
|
|
)
|
|
bridge_parser.add_argument(
|
|
"--nid",
|
|
help="Local radicle node NID to announce (from 'rad self')"
|
|
)
|
|
bridge_parser.add_argument(
|
|
"--announce-retry-delays",
|
|
default="5,15,30",
|
|
metavar="SECONDS",
|
|
help=(
|
|
"Comma-separated delays for startup re-announces (default: 5,15,30). "
|
|
"On LoRa use longer values to respect duty cycle, e.g. 60,300,900"
|
|
),
|
|
)
|
|
add_identity_arg(bridge_parser)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Configure logging
|
|
if args.verbose:
|
|
RNS.loglevel = RNS.LOG_VERBOSE
|
|
else:
|
|
RNS.loglevel = RNS.LOG_INFO
|
|
|
|
# Dispatch command
|
|
if args.command == "node":
|
|
cmd_node(args)
|
|
elif args.command == "identity":
|
|
cmd_identity(args)
|
|
elif args.command == "ping":
|
|
cmd_ping(args)
|
|
elif args.command == "peers":
|
|
cmd_peers(args)
|
|
elif args.command == "gossip":
|
|
cmd_gossip(args)
|
|
elif args.command == "seed":
|
|
cmd_seed(args)
|
|
elif args.command == "bridge":
|
|
cmd_bridge(args)
|
|
else:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|