Files
jpmschweitzerandClaude Opus 4.5 b87e61248e feat: add config file support to CLI
- Add ~/.webber/config.toml for persistent settings
- Support api.url, api.key, cli.mode, cli.stream, history.file
- Environment variables override config file values
- Add 'config' command to show settings and init config file
- Update all commands to use config defaults

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 12:06:13 +01:00

153 lines
3.5 KiB
Python

"""
Configuration management for Webber CLI.
Loads settings from ~/.webber/config.toml with environment variable overrides.
"""
import os
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
# Config directory and file paths
CONFIG_DIR = Path.home() / ".webber"
CONFIG_FILE = CONFIG_DIR / "config.toml"
# Default configuration template
DEFAULT_CONFIG = """\
# Webber CLI Configuration
# https://github.com/jpmschweitzer/webber
[api]
# API server URL (dev: 8095, prod: 8086)
url = "http://localhost:8095"
# API key for authentication (optional for dev mode)
# key = "your-api-key"
[cli]
# Default permission mode: default, plan, auto_accept
mode = "default"
# Enable streaming by default
stream = true
[history]
# Command history file location
file = "~/.webber_history"
"""
@dataclass
class ApiConfig:
"""API connection settings."""
url: str = "http://localhost:8095"
key: str | None = None
@dataclass
class CliConfig:
"""CLI behavior settings."""
mode: str = "default"
stream: bool = True
@dataclass
class HistoryConfig:
"""History settings."""
file: str = "~/.webber_history"
@dataclass
class Config:
"""Complete configuration."""
api: ApiConfig = field(default_factory=ApiConfig)
cli: CliConfig = field(default_factory=CliConfig)
history: HistoryConfig = field(default_factory=HistoryConfig)
def load_config() -> Config:
"""
Load configuration from file with environment variable overrides.
Priority (highest to lowest):
1. Environment variables (WEBBER_API_URL, WEBBER_API_KEY, WEBBER_MODE)
2. Config file (~/.webber/config.toml)
3. Built-in defaults
Returns:
Config object with merged settings
"""
config = Config()
# Load from file if exists
if CONFIG_FILE.exists():
try:
with open(CONFIG_FILE, "rb") as f:
data = tomllib.load(f)
config = _parse_config(data)
except Exception:
# If config file is invalid, use defaults
pass
# Apply environment variable overrides
if url := os.environ.get("WEBBER_API_URL"):
config.api.url = url
if key := os.environ.get("WEBBER_API_KEY"):
config.api.key = key
if mode := os.environ.get("WEBBER_MODE"):
config.cli.mode = mode
return config
def _parse_config(data: dict[str, Any]) -> Config:
"""Parse config dict into Config object."""
config = Config()
if api := data.get("api"):
config.api.url = api.get("url", config.api.url)
config.api.key = api.get("key", config.api.key)
if cli := data.get("cli"):
config.cli.mode = cli.get("mode", config.cli.mode)
config.cli.stream = cli.get("stream", config.cli.stream)
if history := data.get("history"):
config.history.file = history.get("file", config.history.file)
return config
def init_config() -> Path:
"""
Initialize config directory and file with defaults.
Returns:
Path to the created config file
"""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
if not CONFIG_FILE.exists():
CONFIG_FILE.write_text(DEFAULT_CONFIG)
return CONFIG_FILE
def get_config_path() -> Path | None:
"""Get path to config file if it exists."""
return CONFIG_FILE if CONFIG_FILE.exists() else None
# Module-level cached config
_config: Config | None = None
def get_config() -> Config:
"""Get cached config, loading if necessary."""
global _config
if _config is None:
_config = load_config()
return _config