2026-03-07 16:08:38 +00:00
|
|
|
"""Configuration management — load/save ~/.sunbeam.json for production host and infra directory."""
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CONFIG_PATH = Path.home() / ".sunbeam.json"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SunbeamConfig:
|
|
|
|
|
"""Sunbeam configuration with production host and infrastructure directory."""
|
|
|
|
|
|
2026-03-10 19:37:02 +00:00
|
|
|
def __init__(self, production_host: str = "", infra_directory: str = "",
|
|
|
|
|
acme_email: str = ""):
|
2026-03-07 16:08:38 +00:00
|
|
|
self.production_host = production_host
|
|
|
|
|
self.infra_directory = infra_directory
|
2026-03-10 19:37:02 +00:00
|
|
|
self.acme_email = acme_email
|
2026-03-07 16:08:38 +00:00
|
|
|
|
|
|
|
|
def to_dict(self) -> dict:
|
|
|
|
|
"""Convert configuration to dictionary for JSON serialization."""
|
|
|
|
|
return {
|
|
|
|
|
"production_host": self.production_host,
|
|
|
|
|
"infra_directory": self.infra_directory,
|
2026-03-10 19:37:02 +00:00
|
|
|
"acme_email": self.acme_email,
|
2026-03-07 16:08:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_dict(cls, data: dict) -> 'SunbeamConfig':
|
|
|
|
|
"""Create configuration from dictionary."""
|
|
|
|
|
return cls(
|
|
|
|
|
production_host=data.get("production_host", ""),
|
|
|
|
|
infra_directory=data.get("infra_directory", ""),
|
2026-03-10 19:37:02 +00:00
|
|
|
acme_email=data.get("acme_email", ""),
|
2026-03-07 16:08:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_config() -> SunbeamConfig:
|
|
|
|
|
"""Load configuration from ~/.sunbeam.json, return empty config if not found."""
|
|
|
|
|
if not CONFIG_PATH.exists():
|
|
|
|
|
return SunbeamConfig()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
with open(CONFIG_PATH, 'r') as f:
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
return SunbeamConfig.from_dict(data)
|
|
|
|
|
except (json.JSONDecodeError, IOError, KeyError) as e:
|
|
|
|
|
from sunbeam.output import warn
|
|
|
|
|
warn(f"Failed to load config from {CONFIG_PATH}: {e}")
|
|
|
|
|
return SunbeamConfig()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_config(config: SunbeamConfig) -> None:
|
|
|
|
|
"""Save configuration to ~/.sunbeam.json."""
|
|
|
|
|
try:
|
|
|
|
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
with open(CONFIG_PATH, 'w') as f:
|
|
|
|
|
json.dump(config.to_dict(), f, indent=2)
|
|
|
|
|
from sunbeam.output import ok
|
|
|
|
|
ok(f"Configuration saved to {CONFIG_PATH}")
|
|
|
|
|
except IOError as e:
|
|
|
|
|
from sunbeam.output import die
|
|
|
|
|
die(f"Failed to save config to {CONFIG_PATH}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_production_host() -> str:
|
|
|
|
|
"""Get production host from config or SUNBEAM_SSH_HOST environment variable."""
|
|
|
|
|
config = load_config()
|
|
|
|
|
if config.production_host:
|
|
|
|
|
return config.production_host
|
|
|
|
|
return os.environ.get("SUNBEAM_SSH_HOST", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_infra_directory() -> str:
|
|
|
|
|
"""Get infrastructure directory from config."""
|
|
|
|
|
config = load_config()
|
|
|
|
|
return config.infra_directory
|
2026-03-10 19:37:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_infra_dir() -> "Path":
|
|
|
|
|
"""Infrastructure manifests directory as a Path.
|
|
|
|
|
|
|
|
|
|
Prefers the configured infra_directory; falls back to the package-relative
|
|
|
|
|
path (works when running from the development checkout).
|
|
|
|
|
"""
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
configured = load_config().infra_directory
|
|
|
|
|
if configured:
|
|
|
|
|
return Path(configured)
|
|
|
|
|
# Dev fallback: cli/sunbeam/config.py → parents[0]=cli/sunbeam, [1]=cli, [2]=monorepo root
|
|
|
|
|
return Path(__file__).resolve().parents[2] / "infrastructure"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_repo_root() -> "Path":
|
|
|
|
|
"""Monorepo root directory (parent of the infrastructure directory)."""
|
|
|
|
|
return get_infra_dir().parent
|