|
| 1 | +namespace YCode.CLI; |
| 2 | + |
| 3 | +[Inject] |
| 4 | +internal sealed class ConfigManager |
| 5 | +{ |
| 6 | + private const string ConfigDirName = ".ycode"; |
| 7 | + private const string ConfigFileName = "config.env"; |
| 8 | + private static readonly string[] RequiredEnvVars = ["YCODE_AUTH_TOKEN", "YCODE_API_BASE_URI", "YCODE_MODEL"]; |
| 9 | + |
| 10 | + private readonly string _configDir; |
| 11 | + private readonly string _configFile; |
| 12 | + private readonly Dictionary<string, string> _config; |
| 13 | + |
| 14 | + public ConfigManager() |
| 15 | + { |
| 16 | + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); |
| 17 | + _configDir = Path.Combine(userProfile, ConfigDirName); |
| 18 | + _configFile = Path.Combine(_configDir, ConfigFileName); |
| 19 | + _config = []; |
| 20 | + |
| 21 | + LoadConfig(); |
| 22 | + } |
| 23 | + |
| 24 | + private void LoadConfig() |
| 25 | + { |
| 26 | + if (!File.Exists(_configFile)) |
| 27 | + { |
| 28 | + return; |
| 29 | + } |
| 30 | + |
| 31 | + var lines = File.ReadAllLines(_configFile); |
| 32 | + foreach (var line in lines) |
| 33 | + { |
| 34 | + var trimmed = line.Trim(); |
| 35 | + if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) |
| 36 | + { |
| 37 | + continue; |
| 38 | + } |
| 39 | + |
| 40 | + // Remove 'export ' prefix if present |
| 41 | + var exportPrefix = "export "; |
| 42 | + if (trimmed.StartsWith(exportPrefix)) |
| 43 | + { |
| 44 | + trimmed = trimmed[exportPrefix.Length..].Trim(); |
| 45 | + } |
| 46 | + |
| 47 | + var parts = trimmed.Split('=', 2); |
| 48 | + if (parts.Length == 2) |
| 49 | + { |
| 50 | + var key = parts[0].Trim(); |
| 51 | + var value = parts[1].Trim().Trim('"', '\''); |
| 52 | + |
| 53 | + if (!string.IsNullOrEmpty(key)) |
| 54 | + { |
| 55 | + _config[key] = value; |
| 56 | + } |
| 57 | + } |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + private void SaveConfig() |
| 62 | + { |
| 63 | + if (!Directory.Exists(_configDir)) |
| 64 | + { |
| 65 | + Directory.CreateDirectory(_configDir); |
| 66 | + } |
| 67 | + |
| 68 | + var lines = new List<string> { "# YCode Configuration File", "# Generated by YCode CLI", "" }; |
| 69 | + |
| 70 | + foreach (var kvp in _config) |
| 71 | + { |
| 72 | + lines.Add($"export {kvp.Key}=\"{kvp.Value}\""); |
| 73 | + } |
| 74 | + |
| 75 | + File.WriteAllLines(_configFile, lines); |
| 76 | + } |
| 77 | + |
| 78 | + public string? GetEnvironmentVariable(string name) |
| 79 | + { |
| 80 | + // First check system environment |
| 81 | + var value = Environment.GetEnvironmentVariable(name); |
| 82 | + if (!string.IsNullOrEmpty(value)) |
| 83 | + { |
| 84 | + return value; |
| 85 | + } |
| 86 | + |
| 87 | + // Fall back to config file |
| 88 | + return _config.TryGetValue(name, out var configValue) ? configValue : null; |
| 89 | + } |
| 90 | + |
| 91 | + public void EnsureConfiguration() |
| 92 | + { |
| 93 | + var missingVars = RequiredEnvVars |
| 94 | + .Where(envVar => string.IsNullOrEmpty(GetEnvironmentVariable(envVar))) |
| 95 | + .ToList(); |
| 96 | + |
| 97 | + if (missingVars.Count == 0) |
| 98 | + { |
| 99 | + return; |
| 100 | + } |
| 101 | + |
| 102 | + Console.WriteLine(); |
| 103 | + AnsiConsole.MarkupLine("[bold yellow]⚠ Configuration Required[/]"); |
| 104 | + AnsiConsole.MarkupLine("[dim]The following environment variables are not set:[/]"); |
| 105 | + |
| 106 | + foreach (var envVar in missingVars) |
| 107 | + { |
| 108 | + var description = envVar switch |
| 109 | + { |
| 110 | + "YCODE_AUTH_TOKEN" => "API authentication token", |
| 111 | + "YCODE_API_BASE_URI" => "API base endpoint URL", |
| 112 | + "YCODE_MODEL" => "Model identifier (e.g., gpt-4o)", |
| 113 | + _ => "Required configuration" |
| 114 | + }; |
| 115 | + AnsiConsole.MarkupLine($" [dim]•[/] [cyan]{envVar}[/] - {description}"); |
| 116 | + } |
| 117 | + |
| 118 | + Console.WriteLine(); |
| 119 | + AnsiConsole.MarkupLine("[dim]Please enter the required values. They will be saved to:[/]"); |
| 120 | + AnsiConsole.MarkupLine($" [cyan]{_configFile}[/]"); |
| 121 | + Console.WriteLine(); |
| 122 | + |
| 123 | + foreach (var envVar in missingVars) |
| 124 | + { |
| 125 | + var prompt = GetPromptForEnvVar(envVar); |
| 126 | + var defaultValue = GetDefaultValueForEnvVar(envVar); |
| 127 | + var input = ReadInput(prompt, defaultValue, !string.IsNullOrEmpty(defaultValue)); |
| 128 | + |
| 129 | + if (!string.IsNullOrEmpty(input)) |
| 130 | + { |
| 131 | + _config[envVar] = input!; |
| 132 | + Environment.SetEnvironmentVariable(envVar, input); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + SaveConfig(); |
| 137 | + |
| 138 | + Console.WriteLine(); |
| 139 | + AnsiConsole.MarkupLine("[bold green]✓ Configuration saved successfully[/]"); |
| 140 | + Console.WriteLine(); |
| 141 | + |
| 142 | + PrintSetupInstructions(); |
| 143 | + } |
| 144 | + |
| 145 | + private static string GetPromptForEnvVar(string envVar) |
| 146 | + { |
| 147 | + return envVar switch |
| 148 | + { |
| 149 | + "YCODE_AUTH_TOKEN" => "Enter your API authentication token", |
| 150 | + "YCODE_API_BASE_URI" => "Enter your API base endpoint URL", |
| 151 | + "YCODE_MODEL" => "Enter the model identifier", |
| 152 | + _ => $"Enter value for {envVar}" |
| 153 | + }; |
| 154 | + } |
| 155 | + |
| 156 | + private static string GetDefaultValueForEnvVar(string envVar) |
| 157 | + { |
| 158 | + return envVar switch |
| 159 | + { |
| 160 | + "YCODE_API_BASE_URI" => "https://api.openai.com/v1", |
| 161 | + "YCODE_MODEL" => "gpt-4o", |
| 162 | + _ => string.Empty |
| 163 | + }; |
| 164 | + } |
| 165 | + |
| 166 | + private static string? ReadInput(string prompt, string? defaultValue = null, bool allowEmpty = false) |
| 167 | + { |
| 168 | + while (true) |
| 169 | + { |
| 170 | + var defaultPrompt = !string.IsNullOrEmpty(defaultValue) |
| 171 | + ? $"[dim]({defaultValue})[/] " |
| 172 | + : string.Empty; |
| 173 | + |
| 174 | + AnsiConsole.Markup($"[bold green]→[/] {prompt}: {defaultPrompt}"); |
| 175 | + var input = Console.ReadLine(); |
| 176 | + |
| 177 | + if (!string.IsNullOrEmpty(input)) |
| 178 | + { |
| 179 | + return input; |
| 180 | + } |
| 181 | + |
| 182 | + if (!string.IsNullOrEmpty(defaultValue)) |
| 183 | + { |
| 184 | + return defaultValue; |
| 185 | + } |
| 186 | + |
| 187 | + if (allowEmpty) |
| 188 | + { |
| 189 | + return null; |
| 190 | + } |
| 191 | + |
| 192 | + AnsiConsole.MarkupLine("[red]✗ This field cannot be empty. Please try again.[/]"); |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + private void PrintSetupInstructions() |
| 197 | + { |
| 198 | + var shellConfig = DetectShellConfigFile(); |
| 199 | + |
| 200 | + if (!string.IsNullOrEmpty(shellConfig)) |
| 201 | + { |
| 202 | + AnsiConsole.MarkupLine("[dim]To make these environment variables permanent, add this line to your shell configuration:[/]"); |
| 203 | + Console.WriteLine(); |
| 204 | + AnsiConsole.MarkupLine($" [cyan]source \"{_configFile}\"[/]"); |
| 205 | + Console.WriteLine(); |
| 206 | + AnsiConsole.MarkupLine($"[dim]Your shell config file appears to be:[/] [cyan]{shellConfig}[/]"); |
| 207 | + } |
| 208 | + else |
| 209 | + { |
| 210 | + AnsiConsole.MarkupLine("[dim]Note: To make these environment variables permanent across sessions,[/]"); |
| 211 | + AnsiConsole.MarkupLine($"[dim]add this line to your shell configuration:[/] [cyan]source \"{_configFile}\"[/]"); |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + private static string? DetectShellConfigFile() |
| 216 | + { |
| 217 | + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); |
| 218 | + |
| 219 | + // Check for common shell config files |
| 220 | + var shellFiles = new[] |
| 221 | + { |
| 222 | + Path.Combine(userProfile, ".zshrc"), |
| 223 | + Path.Combine(userProfile, ".bashrc"), |
| 224 | + Path.Combine(userProfile, ".bash_profile"), |
| 225 | + Path.Combine(userProfile, ".profile") |
| 226 | + }; |
| 227 | + |
| 228 | + var shellEnv = Environment.GetEnvironmentVariable("SHELL"); |
| 229 | + if (!string.IsNullOrEmpty(shellEnv)) |
| 230 | + { |
| 231 | + var shellName = Path.GetFileName(shellEnv); |
| 232 | + var configFile = shellName switch |
| 233 | + { |
| 234 | + "zsh" => ".zshrc", |
| 235 | + "bash" => ".bashrc", |
| 236 | + _ => null |
| 237 | + }; |
| 238 | + |
| 239 | + if (!string.IsNullOrEmpty(configFile)) |
| 240 | + { |
| 241 | + var configPath = Path.Combine(userProfile, configFile); |
| 242 | + if (File.Exists(configPath)) |
| 243 | + { |
| 244 | + return configPath; |
| 245 | + } |
| 246 | + } |
| 247 | + } |
| 248 | + |
| 249 | + // Return first existing file |
| 250 | + return shellFiles.FirstOrDefault(File.Exists); |
| 251 | + } |
| 252 | +} |
0 commit comments