diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-24 18:19:13 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-24 18:19:13 +0200 |
| commit | 6e9afe3e54ab6fb0162393150b7892655b4561d4 (patch) | |
| tree | f1cf20d891bf0d75dd206953bcec781f47ab7b15 /packages/meshbay-node/src/meshbay_node/cli/setup.py | |
| parent | a408a25af7b91abf729ec5fac7e5506437a86049 (diff) | |
| download | meshbay-6e9afe3e54ab6fb0162393150b7892655b4561d4.tar.gz | |
refactor(node): move the CLI out of daemon.py into cli/
Each `if args.command == ...` branch of main() becomes a function in
cli/ (one module per family of verbs); the parser, the process setup
and a VERBS table go to cli/parser.py and cli/dispatch.py. daemon.main
runs the verb or starts the daemon. Tests patch the CLI through
conftest.patch_cli; the CLI golden is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/cli/setup.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/cli/setup.py | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/cli/setup.py b/packages/meshbay-node/src/meshbay_node/cli/setup.py new file mode 100644 index 0000000..b2a8e83 --- /dev/null +++ b/packages/meshbay-node/src/meshbay_node/cli/setup.py @@ -0,0 +1,195 @@ +"""Provisioning a node and taking it back down: init, reset, calibrate-argon2.""" + +import sys + +from meshbay_node.config import DEFAULT_CONFIG_PATH, Config, load_config +from meshbay_node.keystore import create_keystore, load_keystore +from meshbay_node.platform import chmod_private, config_dir, data_dir, state_dir + + +def calibrate_argon2(target_ms: int = 500) -> None: + """Benchmark Argon2id and suggest parameters targeting ~target_ms.""" + import os + import time + + print(f"Calibrating Argon2id (target: {target_ms}ms) ...") + salt = os.urandom(16) + for mem in [65536, 131072, 262144, 524288]: + from cryptography.hazmat.primitives.kdf.argon2 import Argon2id + t0 = time.perf_counter() + Argon2id(salt=salt, length=32, iterations=3, + lanes=4, memory_cost=mem).derive(b"benchmark") + elapsed_ms = (time.perf_counter() - t0) * 1000 + print(f" memory_cost={mem:>7} ({mem//1024:>4}MB): {elapsed_ms:.0f}ms", end="") + if abs(elapsed_ms - target_ms) < target_ms * 0.3: + print(" ← recommended") + else: + print() + print("Set memory_cost in meshbay_common/crypto.py: ARGON2_MEMORY_COST") + + +def init(args) -> None: + cfg_path = args.config or DEFAULT_CONFIG_PATH + cfg_dir = cfg_path.parent + cfg_dir.mkdir(parents=True, exist_ok=True) + + from meshbay_node.platform import install_node_env + env_written = install_node_env(cfg_dir) + if env_written: + print(f"Wrote {env_written} (packaged defaults).") + + hub_url = args.hub_url + username = args.username + + if not hub_url: + hub_url = input("Hub URL [https://meshbay.org]: ").strip() or "https://meshbay.org" + if not username: + username = input("Hub username: ").strip() + if not username: + print("Username is required.") + sys.exit(1) + + if cfg_path.exists(): + existing = cfg_path.read_text(encoding="utf-8") + import re as _re + m = _re.search(r'username\s*=\s*"([^"]*)"', existing) + existing_user = m.group(1) if m else "" + if existing_user and existing_user != "myusername" and existing_user != username: + print(f"Config already exists with username {existing_user!r}.") + print("This node belongs to another operator. Use 'meshbay-node reset' first.") + sys.exit(1) + if existing_user in ("", "myusername"): + updated = _re.sub( + r'(username\s*=\s*)"[^"]*"', rf'\1"{username}"', existing) + updated = _re.sub( + r'(url\s*=\s*)"[^"]*"', rf'\1"{hub_url}"', updated, count=1) + cfg_path.write_text(updated, encoding="utf-8", newline="\n") + print(f"Config updated: username={username}, hub={hub_url}") + else: + print(f"Config already exists: {cfg_path}") + else: + unlock_file = cfg_dir / "unlock.key" + toml_lines = [ + "[hub]", + f'url = "{hub_url}"', + f'username = "{username}"', + "", + "[node]", + "quic_enabled = false # QUIC direct path; no client uses it yet", + "quic_port = 19010", + "ui_port = 18000", + "", + "[keystore]", + # Forward slashes: a Windows path in a TOML basic string is a + # parse error (`\U`, `\a`, ... are escape sequences). + f'unlock_file = "{unlock_file.as_posix()}"', + "", + ] + cfg_path.write_text("\n".join(toml_lines) + "\n", encoding="utf-8", newline="\n") + chmod_private(cfg_path) + print(f"Config written to {cfg_path}") + + unlock_file = cfg_dir / "unlock.key" + if not unlock_file.exists(): + import secrets + key = secrets.token_urlsafe(32) + unlock_file.write_text(key + "\n", encoding="utf-8", newline="\n") + chmod_private(unlock_file) + print(f"Unlock key created: {unlock_file}") + + cfg = load_config(cfg_path) + if cfg.keystore.path.exists(): + print(f"Keystore already exists: {cfg.keystore.path}") + keys = load_keystore( + path=cfg.keystore.path, unlock_file=cfg.keystore.unlock_file) + else: + keys = create_keystore( + path=cfg.keystore.path, unlock_file=cfg.keystore.unlock_file) + print(f"Keystore created: {cfg.keystore.path}") + + print(f"Node key: {keys.pk_ed25519_b64}") + print() + print("Next steps:") + print(f" 1. Link this node key on {hub_url} → Settings → Link Node") + if sys.platform == "win32": + print(" 2. meshbay-node autostart install (run at each sign-in)") + print(" — or just: meshbay-node (start it now, this session)") + else: + print(" 2. systemctl --user enable --now meshbay-node") + print(" 3. meshbay-node group add <name> --dir /path/to/files") + print(" 4. meshbay-node gek init") + print(" 5. meshbay-node operator pair") + return + + +def reset(args) -> None: + import shutil + + config_dir_ = config_dir() + data_dir_ = data_dir() + state_dir_ = state_dir() + + items = [] + for d in (config_dir_, data_dir_): + if d.exists(): + for child in sorted(d.iterdir()): + items.append(child) + + if not items: + print("Nothing to reset — no node state found.") + return + + print("This will permanently erase all node state:") + for p in items: + print(f" {p}") + print() + print("WARNING: a new keystore means a new identity. All group") + print("memberships, operator pairings, and invitations are lost.") + + if not args.yes: + answer = input("\nProceed? [y/N] ").strip().lower() + if answer != "y": + print("Aborted.") + return + + import json as _json + import subprocess as _sp + import urllib.error + import urllib.request + + token_file = data_dir_ / "ui-token" + if token_file.exists(): + try: + cfg = Config(config_dir_ / "node.toml") + tok = token_file.read_text(encoding="utf-8").strip() + url = (f"http://127.0.0.1:{cfg.node.ui_port}" + f"/api/unlink?t={tok}") + req = urllib.request.Request(url, method="DELETE") + with urllib.request.urlopen(req, timeout=5) as r: + _json.loads(r.read()) + print("Unlinked node key from hub.") + except Exception: + print("Could not unlink from hub (daemon not reachable).") + + if sys.platform == "win32": + from meshbay_node.platform import autostart_remove, service_remove + autostart_remove() + service_remove() # no-op, silently, if not elevated or not installed + else: + _sp.run(["systemctl", "--user", "disable", "--now", "meshbay-node"], + capture_output=True) + + for d in (config_dir_, data_dir_): + if d.exists(): + shutil.rmtree(d) + print(f"Removed {d}") + if state_dir_.is_dir(): + shutil.rmtree(state_dir_) + print(f"Removed {state_dir_}") + print("Node state erased. Run 'meshbay-node init' to start over.") + return + + +def calibrate(args) -> None: + calibrate_argon2() + return |