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:
72
packages/node/meshnet_node/config.py
Normal file
72
packages/node/meshnet_node/config.py
Normal 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
|
||||
Reference in New Issue
Block a user