""" Config Manager ============== Reads and writes ~/.securelens/config.yaml. Falls back to environment variables so the CLI works in CI/CD without a config file. """ import os import yaml from pathlib import Path from dataclasses import dataclass, field CONFIG_DIR = Path.home() / ".securelens" CONFIG_FILE = CONFIG_DIR / "config.yaml" @dataclass class CLIConfig: # AI backend default_model: str = "gemini/gemini-2.0-flash" api_key: str = "" # Backend Integration (for sync / auth) backend_url: str = "http://localhost:8000" token: str = "" # Scan behaviour output_format: str = "terminal" # terminal | json | markdown | all max_files_to_scan: int = 20 max_file_size_kb: int = 200 scan_timeout: int = 10 # seconds — for web scans # File exclusions (gitignore-style globs) ignore_patterns: list = field(default_factory=lambda: [ "*.lock", "node_modules/**", ".git/**", "venv/**", ".venv/**", "__pycache__/**", "*.pyc", "dist/**", "build/**", ".next/**", "*.min.js", "*.min.css", "*.map", ]) def load_config() -> CLIConfig: """ Load config from ~/.securelens/config.yaml, then overlay any env-var overrides. """ cfg = CLIConfig() if CONFIG_FILE.exists(): with open(CONFIG_FILE) as f: data = yaml.safe_load(f) or {} cfg.default_model = data.get("default_model", cfg.default_model) cfg.api_key = data.get("api_key", cfg.api_key) cfg.backend_url = data.get("backend_url", cfg.backend_url) cfg.token = data.get("token", cfg.token) cfg.output_format = data.get("output_format", cfg.output_format) cfg.max_files_to_scan = data.get("max_files_to_scan", cfg.max_files_to_scan) cfg.max_file_size_kb = data.get("max_file_size_kb", cfg.max_file_size_kb) cfg.scan_timeout = data.get("scan_timeout", cfg.scan_timeout) cfg.ignore_patterns = data.get("ignore_patterns", cfg.ignore_patterns) # Env-var overrides (for CI/CD) cfg.api_key = ( os.environ.get("SECURELENS_API_KEY") or os.environ.get("AI_API_KEY") or os.environ.get("GEMINI_API_KEY") or os.environ.get("OPENAI_API_KEY") or cfg.api_key ) cfg.default_model = ( os.environ.get("SECURELENS_MODEL") or os.environ.get("AI_MODEL") or cfg.default_model ) return cfg def save_config(cfg: CLIConfig) -> None: """Persist the config object to ~/.securelens/config.yaml.""" CONFIG_DIR.mkdir(parents=True, exist_ok=True) data = { "default_model": cfg.default_model, "api_key": cfg.api_key, "backend_url": cfg.backend_url, "token": cfg.token, "output_format": cfg.output_format, "max_files_to_scan": cfg.max_files_to_scan, "max_file_size_kb": cfg.max_file_size_kb, "scan_timeout": cfg.scan_timeout, "ignore_patterns": cfg.ignore_patterns, } with open(CONFIG_FILE, "w") as f: yaml.safe_dump(data, f, default_flow_style=False) def config_exists() -> bool: return CONFIG_FILE.exists() and bool(load_config().api_key)