This commit is contained in:
2026-02-12 09:42:59 +02:00
commit 906adb539d
87 changed files with 5288 additions and 0 deletions

151
core/config.py Normal file
View File

@@ -0,0 +1,151 @@
"""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