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>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user