152 lines
4.4 KiB
Python
152 lines
4.4 KiB
Python
"""Configuration loading (INI config + YAML manifest) and FlowContext."""
|
|
|
|
import configparser
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import yaml
|
|
|
|
from flow.core.console import ConsoleLogger
|
|
from flow.core import paths
|
|
from flow.core.platform import PlatformInfo
|
|
|
|
|
|
@dataclass
|
|
class TargetConfig:
|
|
namespace: str
|
|
platform: str
|
|
ssh_host: str
|
|
ssh_identity: Optional[str] = None
|
|
|
|
|
|
@dataclass
|
|
class AppConfig:
|
|
dotfiles_url: str = ""
|
|
dotfiles_branch: str = "main"
|
|
projects_dir: str = "~/projects"
|
|
container_registry: str = "registry.tomastm.com"
|
|
container_tag: str = "latest"
|
|
tmux_session: str = "default"
|
|
targets: List[TargetConfig] = field(default_factory=list)
|
|
|
|
|
|
def _parse_target_config(key: str, value: str) -> Optional[TargetConfig]:
|
|
"""Parse a target line from config.
|
|
|
|
Supported formats:
|
|
1) namespace = platform ssh_host [ssh_identity]
|
|
2) namespace@platform = ssh_host [ssh_identity]
|
|
"""
|
|
parts = value.split()
|
|
if not parts:
|
|
return None
|
|
|
|
if "@" in key:
|
|
namespace, platform = key.split("@", 1)
|
|
ssh_host = parts[0]
|
|
ssh_identity = parts[1] if len(parts) > 1 else None
|
|
if not namespace or not platform:
|
|
return None
|
|
return TargetConfig(
|
|
namespace=namespace,
|
|
platform=platform,
|
|
ssh_host=ssh_host,
|
|
ssh_identity=ssh_identity,
|
|
)
|
|
|
|
if len(parts) < 2:
|
|
return None
|
|
|
|
return TargetConfig(
|
|
namespace=key,
|
|
platform=parts[0],
|
|
ssh_host=parts[1],
|
|
ssh_identity=parts[2] if len(parts) > 2 else None,
|
|
)
|
|
|
|
|
|
def load_config(path: Optional[Path] = None) -> AppConfig:
|
|
"""Load INI config file into AppConfig with cascading priority.
|
|
|
|
Priority:
|
|
1. Dotfiles repo (self-hosted): ~/.local/share/devflow/dotfiles/flow/.config/flow/config
|
|
2. Local override: ~/.config/devflow/config
|
|
3. Empty fallback
|
|
"""
|
|
cfg = AppConfig()
|
|
|
|
if path is None:
|
|
# Priority 1: Check dotfiles repo for self-hosted config
|
|
if paths.DOTFILES_CONFIG.exists():
|
|
path = paths.DOTFILES_CONFIG
|
|
# Priority 2: Fall back to local config
|
|
else:
|
|
path = paths.CONFIG_FILE
|
|
|
|
assert path is not None
|
|
|
|
if not path.exists():
|
|
return cfg
|
|
|
|
parser = configparser.ConfigParser()
|
|
parser.read(path)
|
|
|
|
if parser.has_section("repository"):
|
|
cfg.dotfiles_url = parser.get("repository", "dotfiles_url", fallback=cfg.dotfiles_url)
|
|
cfg.dotfiles_branch = parser.get("repository", "dotfiles_branch", fallback=cfg.dotfiles_branch)
|
|
|
|
if parser.has_section("paths"):
|
|
cfg.projects_dir = parser.get("paths", "projects_dir", fallback=cfg.projects_dir)
|
|
|
|
if parser.has_section("defaults"):
|
|
cfg.container_registry = parser.get("defaults", "container_registry", fallback=cfg.container_registry)
|
|
cfg.container_tag = parser.get("defaults", "container_tag", fallback=cfg.container_tag)
|
|
cfg.tmux_session = parser.get("defaults", "tmux_session", fallback=cfg.tmux_session)
|
|
|
|
if parser.has_section("targets"):
|
|
for key in parser.options("targets"):
|
|
raw_value = parser.get("targets", key)
|
|
tc = _parse_target_config(key, raw_value)
|
|
if tc is not None:
|
|
cfg.targets.append(tc)
|
|
|
|
return cfg
|
|
|
|
|
|
def load_manifest(path: Optional[Path] = None) -> Dict[str, Any]:
|
|
"""Load YAML manifest file with cascading priority.
|
|
|
|
Priority:
|
|
1. Dotfiles repo (self-hosted): ~/.local/share/devflow/dotfiles/flow/.config/flow/manifest.yaml
|
|
2. Local override: ~/.config/devflow/manifest.yaml
|
|
3. Empty fallback
|
|
"""
|
|
if path is None:
|
|
# Priority 1: Check dotfiles repo for self-hosted manifest
|
|
if paths.DOTFILES_MANIFEST.exists():
|
|
path = paths.DOTFILES_MANIFEST
|
|
# Priority 2: Fall back to local manifest
|
|
else:
|
|
path = paths.MANIFEST_FILE
|
|
|
|
assert path is not None
|
|
|
|
if not path.exists():
|
|
return {}
|
|
|
|
try:
|
|
with open(path, "r") as f:
|
|
data = yaml.safe_load(f)
|
|
except yaml.YAMLError as e:
|
|
raise RuntimeError(f"Invalid YAML in {path}: {e}") from e
|
|
return data if isinstance(data, dict) else {}
|
|
|
|
|
|
@dataclass
|
|
class FlowContext:
|
|
config: AppConfig
|
|
manifest: Dict[str, Any]
|
|
platform: PlatformInfo
|
|
console: ConsoleLogger
|