|
1 | 1 | """ |
2 | | -Configuration management for CLI Code. |
| 2 | +Configuration management for the CLI Code application. |
| 3 | +
|
| 4 | +This module provides a Config class that manages configuration for the CLI Code |
| 5 | +application, including loading environment variables, ensuring the configuration |
| 6 | +file exists, and loading and saving configuration. |
| 7 | +
|
| 8 | +Configuration in CLI Code supports two approaches: |
| 9 | +1. File-based configuration (.yaml): Primary approach for end users who install from pip |
| 10 | +2. Environment variables: Used mainly during development for quick experimentation |
| 11 | +
|
| 12 | +Both approaches are supported simultaneously - there is no migration needed as both |
| 13 | +configuration methods can coexist. Environment variables take precedence over file-based |
| 14 | +configuration when both are present. |
3 | 15 | """ |
4 | 16 |
|
5 | 17 | import logging |
|
12 | 24 |
|
13 | 25 |
|
14 | 26 | class Config: |
15 | | - """Manages configuration for the cli-code application.""" |
| 27 | + """ |
| 28 | + Configuration management for the CLI Code application. |
| 29 | + |
| 30 | + This class manages loading configuration from a YAML file, creating a |
| 31 | + default configuration file if one doesn't exist, and loading environment |
| 32 | + variables. |
| 33 | + |
| 34 | + The configuration is loaded in the following order of precedence: |
| 35 | + 1. Environment variables (highest precedence) |
| 36 | + 2. Configuration file |
| 37 | + 3. Default values (lowest precedence) |
| 38 | + """ |
16 | 39 |
|
17 | 40 | def __init__(self): |
18 | | - self.config_dir = Path.home() / ".config" / "cli-code-agent" |
| 41 | + """ |
| 42 | + Initialize the configuration. |
| 43 | + |
| 44 | + This will load environment variables, ensure the configuration file |
| 45 | + exists, and load the configuration from the file. |
| 46 | + """ |
| 47 | + self.config_dir = Path(os.path.expanduser("~/.config/cli-code")) |
19 | 48 | self.config_file = self.config_dir / "config.yaml" |
20 | | - self.config = {} |
21 | | - |
22 | | - # First load environment variables from .env file if it exists |
| 49 | + |
| 50 | + # Load environment variables |
23 | 51 | self._load_dotenv() |
24 | | - |
25 | | - try: |
26 | | - self._ensure_config_exists() |
27 | | - self.config = self._load_config() |
28 | | - self._migrate_old_keys() |
29 | | - |
30 | | - # Override config with environment variables if they exist |
31 | | - self._apply_env_vars() |
32 | | - except Exception as e: |
33 | | - log.error(f"Error initializing configuration from {self.config_file}: {e}", exc_info=True) |
| 52 | + |
| 53 | + # Ensure config file exists |
| 54 | + self._ensure_config_exists() |
| 55 | + |
| 56 | + # Load config from file |
| 57 | + self.config = self._load_config() |
| 58 | + |
| 59 | + # Apply environment variable overrides |
| 60 | + self._apply_env_vars() |
34 | 61 |
|
35 | 62 | def _load_dotenv(self): |
36 | 63 | """Load environment variables from .env file if it exists.""" |
@@ -77,32 +104,50 @@ def _load_dotenv(self): |
77 | 104 | log.debug("No .env or .env.example file found in current directory") |
78 | 105 |
|
79 | 106 | def _apply_env_vars(self): |
80 | | - """Apply environment variables to override config settings.""" |
81 | | - # Map of environment variable names to config keys |
82 | | - env_var_mapping = { |
83 | | - "CLI_CODE_GOOGLE_API_KEY": "google_api_key", |
84 | | - "CLI_CODE_OLLAMA_API_URL": "ollama_api_url", |
85 | | - "CLI_CODE_DEFAULT_PROVIDER": "default_provider", |
86 | | - "CLI_CODE_DEFAULT_MODEL": "default_model", |
87 | | - "CLI_CODE_OLLAMA_DEFAULT_MODEL": "ollama_default_model", |
| 107 | + """ |
| 108 | + Apply environment variable overrides to the configuration. |
| 109 | + |
| 110 | + Environment variables take precedence over configuration file values. |
| 111 | + Environment variables are formatted as: |
| 112 | + CLI_CODE_SETTING_NAME |
| 113 | + |
| 114 | + For example: |
| 115 | + CLI_CODE_GOOGLE_API_KEY=my-api-key |
| 116 | + CLI_CODE_DEFAULT_PROVIDER=gemini |
| 117 | + CLI_CODE_SETTINGS_MAX_TOKENS=4096 |
| 118 | + """ |
| 119 | + |
| 120 | + # Direct mappings from env to config keys |
| 121 | + env_mappings = { |
| 122 | + 'CLI_CODE_GOOGLE_API_KEY': 'google_api_key', |
| 123 | + 'CLI_CODE_DEFAULT_PROVIDER': 'default_provider', |
| 124 | + 'CLI_CODE_DEFAULT_MODEL': 'default_model', |
| 125 | + 'CLI_CODE_OLLAMA_API_URL': 'ollama_api_url', |
| 126 | + 'CLI_CODE_OLLAMA_DEFAULT_MODEL': 'ollama_default_model', |
88 | 127 | } |
89 | | - |
90 | | - for env_var, config_key in env_var_mapping.items(): |
91 | | - if env_var in os.environ: |
92 | | - value = os.environ[env_var] |
93 | | - # Mask sensitive values in logs |
94 | | - log_value = "****" if "KEY" in env_var or "TOKEN" in env_var else value |
95 | | - log.info(f"Using environment variable {env_var}={log_value} to override config key '{config_key}'") |
96 | | - self.config[config_key] = value |
97 | | - |
98 | | - # Apply and save if environment variables were found |
99 | | - if any(env_var in os.environ for env_var in env_var_mapping): |
100 | | - self._save_config() |
101 | | - # Log all the current config values for debugging |
102 | | - safe_config = self.config.copy() |
103 | | - if "google_api_key" in safe_config and safe_config["google_api_key"]: |
104 | | - safe_config["google_api_key"] = "****" |
105 | | - log.debug(f"Final config after applying environment variables: {safe_config}") |
| 128 | + |
| 129 | + # Apply direct mappings |
| 130 | + for env_key, config_key in env_mappings.items(): |
| 131 | + if env_key in os.environ: |
| 132 | + self.config[config_key] = os.environ[env_key] |
| 133 | + |
| 134 | + # Settings with CLI_CODE_SETTINGS_ prefix go into settings dict |
| 135 | + if 'settings' not in self.config: |
| 136 | + self.config['settings'] = {} |
| 137 | + |
| 138 | + for env_key, env_value in os.environ.items(): |
| 139 | + if env_key.startswith('CLI_CODE_SETTINGS_'): |
| 140 | + setting_name = env_key[len('CLI_CODE_SETTINGS_'):].lower() |
| 141 | + |
| 142 | + # Try to convert to appropriate type (int, float, bool) |
| 143 | + if env_value.isdigit(): |
| 144 | + self.config['settings'][setting_name] = int(env_value) |
| 145 | + elif env_value.replace('.', '', 1).isdigit() and env_value.count('.') <= 1: |
| 146 | + self.config['settings'][setting_name] = float(env_value) |
| 147 | + elif env_value.lower() in ('true', 'false'): |
| 148 | + self.config['settings'][setting_name] = env_value.lower() == 'true' |
| 149 | + else: |
| 150 | + self.config['settings'][setting_name] = env_value |
106 | 151 |
|
107 | 152 | def _ensure_config_exists(self): |
108 | 153 | """Create config directory and file with defaults if they don't exist.""" |
@@ -154,47 +199,6 @@ def _save_config(self): |
154 | 199 | except Exception as e: |
155 | 200 | log.error(f"Error saving config file {self.config_file}: {e}", exc_info=True) |
156 | 201 |
|
157 | | - def _migrate_old_keys(self): |
158 | | - """Migrate from old nested 'api_keys': {'google': ...} structure if present.""" |
159 | | - if "api_keys" in self.config and isinstance(self.config["api_keys"], dict): |
160 | | - log.info("Migrating old 'api_keys' structure in config file.") |
161 | | - if "google" in self.config["api_keys"] and "google_api_key" not in self.config: |
162 | | - self.config["google_api_key"] = self.config["api_keys"]["google"] |
163 | | - del self.config["api_keys"] |
164 | | - self._save_config() |
165 | | - log.info("Finished migrating 'api_keys'.") |
166 | | - |
167 | | - # Check for old config paths and migrate if needed |
168 | | - self._migrate_old_config_paths() |
169 | | - |
170 | | - def _migrate_old_config_paths(self): |
171 | | - """Check for and migrate config from older versions with different path names.""" |
172 | | - old_paths = [ |
173 | | - Path.home() / ".config" / "gemini-code" / "config.yaml", |
174 | | - Path.home() / ".config" / "cli-code" / "config.yaml", |
175 | | - ] |
176 | | - |
177 | | - for old_path in old_paths: |
178 | | - if old_path.exists() and not self.config_file.exists(): |
179 | | - log.info(f"Found old config at {old_path}. Migrating to {self.config_file}...") |
180 | | - try: |
181 | | - # Ensure new directory exists |
182 | | - self.config_dir.mkdir(parents=True, exist_ok=True) |
183 | | - |
184 | | - # Read old config |
185 | | - with open(old_path, "r") as old_file: |
186 | | - old_config = yaml.safe_load(old_file) or {} |
187 | | - |
188 | | - # Update our config with old values |
189 | | - self.config.update(old_config) |
190 | | - |
191 | | - # Save to new location |
192 | | - self._save_config() |
193 | | - log.info(f"Successfully migrated config from {old_path} to {self.config_file}") |
194 | | - except Exception as e: |
195 | | - log.error(f"Error migrating config from {old_path}: {e}", exc_info=True) |
196 | | - # Continue trying other paths on failure |
197 | | - |
198 | 202 | def get_credential(self, provider: str) -> str | None: |
199 | 203 | """Get the credential (API key or URL) for a specific provider.""" |
200 | 204 | if provider == "gemini": |
|
0 commit comments