""" Shared configuration and utility functions for the Prole installer (root-level). This mirrors `prole.installer.config` but is located under the root `installer/` package per the refactor request. UI code should import from `installer.config`. """ from __future__ import annotations from pathlib import Path import platform import subprocess import sys from typing import Tuple, Optional import os # .properties loader (simple key=value, # comments) def _load_properties(path: Path) -> dict: props: dict[str, str] = {} try: text = path.read_text(encoding="utf-8") except Exception: return props for line in text.splitlines(): s = line.strip() if not s or s.startswith("#"): continue if "=" in s: k, v = s.split("=", 1) k = k.strip() v = v.strip() if k: props[k] = v return props # Repository root: this file lives at /installer/config.py → parent is repo PROJECT_ROOT = Path(__file__).resolve().parents[1] PROLE_APP_DIR = PROJECT_ROOT / "prole-app" PROLE_PROPS_PATH = PROLE_APP_DIR / "prole.properties" _PROLE_PROPS_CACHE: Optional[dict] = None def get_properties() -> dict: """Load and cache prole.properties from the repo (installer context). Order: - Repo default at prole-app/prole.properties - Optional env override: PROLE_PROPERTIES points to a file """ global _PROLE_PROPS_CACHE if _PROLE_PROPS_CACHE is None: props: dict[str, str] = {} # repo default if PROLE_PROPS_PATH.exists(): props.update(_load_properties(PROLE_PROPS_PATH)) # env override (absolute path) env_path = os.environ.get("PROLE_PROPERTIES") if env_path: p = Path(env_path) if p.exists(): props.update(_load_properties(p)) _PROLE_PROPS_CACHE = props return dict(_PROLE_PROPS_CACHE) def get_config_value(key: str, default: Optional[str] = None) -> Optional[str]: return get_properties().get(key, default) def get_ui_icon_image_path() -> Path: """Return absolute path to the UI icon image for the installer. Priority (new → legacy): - `icon` key in prole.properties (requested) - legacy `ui.icon` Defaults to img/proleIcon.png under repo root if not set or missing. """ # Prefer new key `icon`, fall back to old `ui.icon` rel = ( get_config_value("icon") or get_config_value("ui.icon") or "img/proleIcon.png" ) p = (PROJECT_ROOT / rel).resolve() if p.exists(): return p # fallback return (PROJECT_ROOT / "img/proleIcon.png").resolve() def get_ui_background_image_path() -> Path: """Return absolute path to the UI background image. Priority (new → legacy): - `background` key in prole.properties (requested) - legacy `ui.background` Defaults to img/proleLogoSepia.png under repo root if not set or missing. """ rel = ( get_config_value("background") or get_config_value("ui.background") or "img/proleLogoSepia.png" ) p = (PROJECT_ROOT / rel).resolve() if p.exists(): return p # fallback return (PROJECT_ROOT / "img/proleLogoSepia.png").resolve() def is_apple_silicon() -> bool: return platform.machine() == "arm64" and platform.system() == "Darwin" def get_docker_build_platform_args() -> list[str]: if is_apple_silicon(): return ["--platform", "linux/amd64"] return [] def normalize_version(text: str) -> str: if not text: return "" import re m = re.search(r"(\d+(?:[._-]\d+){0,5})", text) if not m: nums = re.findall(r"\d+", text) else: nums = re.findall(r"\d+", m.group(1)) if not nums: return text.strip() parts = (nums + ["0", "0"])[:3] try: parts = [f"{int(p):02d}" for p in parts] except Exception: parts = [p.zfill(2) for p in parts] return ".".join(parts) DEPENDENCIES = [ { "id": "brew", "name": "Homebrew", "description": "Package manager for macOS", "url": "https://brew.sh", "install_cmd": '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', "check_cmd": "brew --version", "bin": "brew", }, { "id": "docker", "name": "Docker", "description": "Container platform for running Prole services", "url": "https://www.docker.com/products/docker-desktop", "install_cmd": None, "check_cmd": "docker --version", "bin": "docker", }, { "id": "k3d", "name": "k3d", "description": "Lightweight wrapper to run k3s in Docker", "url": "https://k3d.io", "install_cmd": "brew install k3d", "check_cmd": "k3d --version", "bin": "k3d", }, { "id": "ollama", "name": "Ollama", "description": "Local LLM runtime used by the deployment agent", "url": "https://ollama.com", "install_cmd": "brew install ollama", "check_cmd": "ollama --version", "bin": "ollama", }, { "id": "tshark", "name": "tshark (Wireshark)", "description": "Network protocol analyzer for network scanning", "url": "https://www.wireshark.org", "install_cmd": "brew install wireshark" if platform.system() == 'Darwin' else "sudo apt-get update && sudo apt-get install -y tshark", "check_cmd": "tshark --version", "bin": "tshark", }, { "id": "pyshark", "name": "pyshark", "description": "Python wrapper for tshark", "url": "https://github.com/KimiNewt/pyshark", "install_cmd": f"'{sys.executable}' -m pip install pyshark", "check_cmd": f"'{sys.executable}' -c 'import pyshark'", "bin": None, }, ] def get_dep_info(dep: dict) -> Tuple[bool, Optional[str], Optional[str]]: bin_name = dep.get("bin") or dep["name"] location = None version = None installed = False try: if bin_name: res = subprocess.run(["bash", "-lc", f"command -v {bin_name}"], capture_output=True, text=True) if res.returncode == 0: location = res.stdout.strip() installed = True check_cmd = dep.get("check_cmd") if check_cmd: res2 = subprocess.run(["bash", "-lc", check_cmd], capture_output=True, text=True) if res2.returncode == 0: version = " ".join(res2.stdout.strip().splitlines()[:1]) # If check_cmd succeeded, consider it installed installed = True if not location: location = "Installed via Python" except Exception: pass return installed, location, version