diff --git a/.gitignore b/.gitignore index f51980b..d0dd023 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,9 @@ dist/ # Ralph local runtime state .ralph-tui/ + + +.env +.env.* +!.env.example +!.env.testnet diff --git a/packages/node/meshnet_node/cli.py b/packages/node/meshnet_node/cli.py index 187792e..c48800c 100644 --- a/packages/node/meshnet_node/cli.py +++ b/packages/node/meshnet_node/cli.py @@ -3,12 +3,45 @@ from __future__ import annotations import argparse +import os import socket import sys import time from pathlib import Path +def _load_env_file(path: Path) -> None: + """Load simple KEY=VALUE pairs from an env file without overriding env vars.""" + if not path.exists(): + return + try: + lines = path.read_text().splitlines() + except OSError: + return + for line in lines: + text = line.strip() + if not text or text.startswith("#"): + continue + if text.startswith("export "): + text = text[len("export "):].strip() + if "=" not in text: + continue + key, value = text.split("=", 1) + key = key.strip() + if not key or key in os.environ: + continue + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: + value = value[1:-1] + os.environ[key] = value + + +def _load_env_defaults() -> None: + """Load local and user-level node env defaults before config defaults are imported.""" + _load_env_file(Path.cwd() / ".env") + _load_env_file(Path.home() / ".config" / "meshnet" / "secrets.env") + + def _run_node(cfg: dict) -> None: """Start the node and hand off to the live dashboard. Blocks until Ctrl-C.""" from .startup import run_startup @@ -224,6 +257,8 @@ def _cmd_start(args) -> int: def main() -> None: + _load_env_defaults() + parser = argparse.ArgumentParser( prog="meshnet-node", description="Distributed AI Inference — Node Client", diff --git a/tests/test_node_startup.py b/tests/test_node_startup.py index 443bdd9..ad38474 100644 --- a/tests/test_node_startup.py +++ b/tests/test_node_startup.py @@ -2,6 +2,7 @@ import json import io +import os import sys import types import urllib.request @@ -1416,14 +1417,35 @@ def test_layers_from_config_get_text_config_and_variants(): assert layers_from_config(types.SimpleNamespace()) is None -def test_download_dir_env_override(monkeypatch): +def test_download_dir_env_override(tmp_path, monkeypatch): import importlib from meshnet_node import config as config_mod + monkeypatch.chdir(tmp_path) monkeypatch.setenv("MESHNET_DOWNLOAD_DIR", "/tmp/llm-store") importlib.reload(config_mod) assert config_mod.DEFAULTS["download_dir"] == "/tmp/llm-store" monkeypatch.delenv("MESHNET_DOWNLOAD_DIR") importlib.reload(config_mod) assert config_mod.DEFAULTS["download_dir"].endswith("models") + + +def test_cli_loads_local_env_before_config_defaults(tmp_path, monkeypatch): + import importlib + + from meshnet_node import cli as cli_mod + from meshnet_node import config as config_mod + + monkeypatch.delenv("MESHNET_DOWNLOAD_DIR", raising=False) + monkeypatch.chdir(tmp_path) + (tmp_path / ".env").write_text( + "MESHNET_DOWNLOAD_DIR=/run/media/popov/DATA/llm/safetensor/models\n" + "HF_TOKEN=hf_test_token\n" + ) + + cli_mod._load_env_defaults() + importlib.reload(config_mod) + + assert config_mod.DEFAULTS["download_dir"] == "/run/media/popov/DATA/llm/safetensor/models" + assert os.environ["HF_TOKEN"] == "hf_test_token"