Skip to content

Commit b57b0f9

Browse files
ycode-linclaude
andcommitted
feat: add interactive configuration for environment variables
Add ConfigManager that detects missing environment variables and provides an interactive setup prompt. Configuration is saved to ~/.ycode/config.env for persistence. Users no longer need to manually set YCODE_AUTH_TOKEN, YCODE_API_BASE_URI, and YCODE_MODEL before running the CLI. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent df0744c commit b57b0f9

File tree

2 files changed

+270
-3
lines changed

2 files changed

+270
-3
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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+
}

YCode.CLI/ServiceRegistration.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,24 @@ internal static class ServiceRegistration
66
{
77
public static IServiceProvider Register(this IServiceCollection services)
88
{
9-
var key = Environment.GetEnvironmentVariable("YCODE_AUTH_TOKEN")!;
10-
var uri = Environment.GetEnvironmentVariable("YCODE_API_BASE_URI")!;
11-
var model = Environment.GetEnvironmentVariable("YCODE_MODEL")!;
9+
// Register ConfigManager first to ensure it's available
10+
services.AddSingleton<ConfigManager>();
11+
12+
// Build a temporary provider to get ConfigManager
13+
var tempProvider = services.BuildServiceProvider();
14+
var configManager = tempProvider.GetRequiredService<ConfigManager>();
15+
16+
// Ensure configuration is set up
17+
configManager.EnsureConfiguration();
18+
19+
// Get configuration values from ConfigManager
20+
var key = configManager.GetEnvironmentVariable("YCODE_AUTH_TOKEN")
21+
?? throw new InvalidOperationException("YCODE_AUTH_TOKEN is required but not configured");
22+
var uri = configManager.GetEnvironmentVariable("YCODE_API_BASE_URI")
23+
?? throw new InvalidOperationException("YCODE_API_BASE_URI is required but not configured");
24+
var model = configManager.GetEnvironmentVariable("YCODE_MODEL")
25+
?? throw new InvalidOperationException("YCODE_MODEL is required but not configured");
26+
1227
var workDir = Directory.GetCurrentDirectory();
1328
var osDescription = System.Runtime.InteropServices.RuntimeInformation.OSDescription;
1429
var osPlatform = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)

0 commit comments

Comments
 (0)