"""Central configuration for the recipe-maintainer-3 project. Resolves the workspace root, provides paths to key directories, and loads instance/deployment configuration from TOML files. All paths are resolved lazily on first access and cached. """ import os import tomllib from pathlib import Path # --------------------------------------------------------------------------- # Workspace root detection # --------------------------------------------------------------------------- _workspace: Path | None = None def get_workspace() -> Path: """Auto-detect the workspace root by walking up to find AGENTS.md.""" global _workspace if _workspace is not None: return _workspace # Start from this file's location (lib/config.py → lib/ → workspace root) candidate = Path(__file__).resolve().parent.parent if (candidate / "AGENTS.md").exists(): _workspace = candidate return _workspace # Fallback: walk up from cwd candidate = Path.cwd() for _ in range(10): if (candidate / "AGENTS.md").exists(): _workspace = candidate return _workspace parent = candidate.parent if parent == candidate: break candidate = parent raise RuntimeError( "Could not find workspace root (no AGENTS.md found). " "Are you running from within the recipe-maintainer-3 directory?" ) # --------------------------------------------------------------------------- # Path constants (lazy) # --------------------------------------------------------------------------- def _ws() -> Path: return get_workspace() class _Paths: """Lazy path accessors for key project directories.""" @property def WORKSPACE(self) -> Path: return _ws() @property def LIB_DIR(self) -> Path: return _ws() / "lib" @property def SCRIPTS_DIR(self) -> Path: return _ws() / "scripts" @property def RECIPE_INFO_DIR(self) -> Path: return _ws() / "recipe-info" @property def TESTSECRETS_DIR(self) -> Path: return _ws() / "recipe-info" / "testsecrets" @property def LOGS_DIR(self) -> Path: return _ws() / "logs" @property def PLANS_DIR(self) -> Path: return _ws() / "plans" @property def PLANNED_UPDATES_DIR(self) -> Path: return _ws() / "planned-updates" @property def TERRAFORM_DIR(self) -> Path: return _ws() / "terraform" @property def ABRA_DIR(self) -> Path: env = os.environ.get("ABRA_DIR") if env: return Path(env) return Path.home() / ".abra" @property def ABRA_RECIPES_DIR(self) -> Path: return self.ABRA_DIR / "recipes" @property def ABRA_SERVERS_DIR(self) -> Path: return self.ABRA_DIR / "servers" paths = _Paths() # --------------------------------------------------------------------------- # Settings # --------------------------------------------------------------------------- def load_settings() -> dict: """Load settings.toml from the workspace root.""" p = paths.WORKSPACE / "settings.toml" if not p.exists(): return {} with open(p, "rb") as f: return tomllib.load(f) # --------------------------------------------------------------------------- # Instance loading (from settings.toml [instances.*]) # --------------------------------------------------------------------------- def _load_instances_from_settings() -> dict: """Load the [instances] section from settings.toml.""" settings = load_settings() return settings.get("instances", {}) def get_instance_names() -> list[str]: """Return all instance names from settings.toml [instances.*].""" return list(_load_instances_from_settings().keys()) def get_default_instance_name() -> str: """Return the default instance from settings.toml.""" settings = load_settings() default = settings.get("default_instance") if not default: raise RuntimeError("No default_instance set in settings.toml") if default not in get_instance_names(): raise RuntimeError( f"default_instance '{default}' from settings.toml " f"not found in settings.toml [instances]" ) return default def load_instance_toml(name: str) -> dict: """Load raw TOML data for a specific instance from settings.toml.""" instances = _load_instances_from_settings() if name not in instances: raise ValueError(f"Instance '{name}' not found in settings.toml [instances]") data = dict(instances[name]) data["name"] = name return data def load_recipe_toml(recipe_name: str) -> dict: """Load recipe-info//recipe.toml.""" p = paths.RECIPE_INFO_DIR / recipe_name / "recipe.toml" if not p.exists(): return {"name": recipe_name} with open(p, "rb") as f: data = tomllib.load(f) data.setdefault("name", recipe_name) return data