- `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>
73 lines
2.1 KiB
Python
73 lines
2.1 KiB
Python
"""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
|