feat(us-016): mining-style node startup CLI + live dashboard

- `meshnet-node` with no args runs interactive setup wizard on first run,
  then starts directly on subsequent runs using saved config
- Wizard auto-detects all GPUs/VRAM, shows curated model list with per-quant
  VRAM requirements, marks models that exceed available VRAM as incompatible,
  offers HuggingFace Hub browse as escape hatch
- Persistent config saved to ~/.config/meshnet/config.json (0o600)
- Live rich dashboard (tokens/sec EMA, VRAM, requests, peers, uptime) with
  automatic plain-text fallback when stdout is not a TTY (WSL2/SSH/CI)
- All wizard values overridable via CLI flags; --reset-config re-runs wizard
- `meshnet-node models` lists curated models; `--browse` fetches HF Hub top-20
- `meshnet-node config` prints saved config
- `meshnet-node start ...` preserved for backward compatibility
- 19 new tests; 97 passed, 1 skipped (no regressions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dobromir Popov
2026-06-29 17:45:38 +03:00
parent 65dee3d6d1
commit 65f3ee6a85
7 changed files with 1284 additions and 64 deletions

View File

@@ -0,0 +1,72 @@
"""Persistent node configuration — stored in ~/.config/meshnet/config.json."""
from __future__ import annotations
import json
import os
import stat
from pathlib import Path
_DEFAULT_CONFIG_DIR = Path.home() / ".config" / "meshnet"
_DEFAULT_CONFIG_FILE = _DEFAULT_CONFIG_DIR / "config.json"
_DEFAULT_DOWNLOAD_DIR = Path.home() / ".meshnet" / "models"
_DEFAULT_TRACKER_URL = "http://localhost:8080"
_DEFAULT_WALLET_PATH = str(Path.home() / ".config" / "meshnet" / "wallet.json")
_DEFAULT_QUANTIZATION = "nf4"
DEFAULTS = {
"model_hf_repo": "",
"model_name": "",
"quantization": _DEFAULT_QUANTIZATION,
"download_dir": str(_DEFAULT_DOWNLOAD_DIR),
"tracker_url": _DEFAULT_TRACKER_URL,
"wallet_path": _DEFAULT_WALLET_PATH,
"shard_start": None,
"shard_end": None,
"port": 7000,
"host": "0.0.0.0",
}
def config_path(override: Path | None = None) -> Path:
return override or _DEFAULT_CONFIG_FILE
def load_config(path: Path | None = None) -> dict | None:
"""Return parsed config dict, or None if no config file exists."""
p = config_path(path)
if not p.exists():
return None
try:
cfg = json.loads(p.read_text())
if not isinstance(cfg, dict):
return None
return cfg
except (json.JSONDecodeError, OSError):
return None
def save_config(cfg: dict, path: Path | None = None) -> None:
"""Write config to disk with restricted permissions (0o600)."""
p = config_path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(json.dumps(cfg, indent=2))
try:
os.chmod(p, stat.S_IRUSR | stat.S_IWUSR)
except OSError:
pass # Windows / some filesystems don't support chmod
def delete_config(path: Path | None = None) -> None:
p = config_path(path)
if p.exists():
p.unlink()
def merge_cli_overrides(cfg: dict, **cli_kwargs) -> dict:
"""Return a copy of cfg with any non-None CLI values applied on top."""
result = dict(cfg)
for key, val in cli_kwargs.items():
if val is not None:
result[key] = val
return result