1use crate::cli::ConfigCommands;
4use crate::storage::packages::{PackageManager, ResourceKind};
5use crate::store::settings::Settings;
6use anyhow::Result;
7
8pub fn handle_config(action: &ConfigCommands) -> Result<()> {
10 match action {
11 ConfigCommands::Show => config_show(),
12 ConfigCommands::List { resource_type } => config_list(resource_type.as_ref()),
13 ConfigCommands::Enable {
14 resource_type,
15 name,
16 } => config_toggle_resource(resource_type, name, true),
17 ConfigCommands::Disable {
18 resource_type,
19 name,
20 } => config_toggle_resource(resource_type, name, false),
21 ConfigCommands::Set { key, value } => config_set(key, value),
22 ConfigCommands::Get { key } => config_get(key),
23 ConfigCommands::AddProvider {
24 name,
25 base_url,
26 api_key_env,
27 api,
28 } => config_add_provider(name, base_url, api_key_env, api),
29 ConfigCommands::RemoveProvider { name } => config_remove_provider(name),
30 ConfigCommands::Reset { all } => handle_config_reset(*all),
31 ConfigCommands::Path => handle_config_path_command(),
32 }
33}
34
35fn config_show() -> Result<()> {
37 let settings = Settings::load()?;
38 println!("oxicode configuration:");
39 println!(" Settings file: {}", Settings::settings_path()?.display());
40 println!();
41 println!(
42 " Model: {}",
43 settings
44 .effective_model(None)
45 .unwrap_or_else(|| "(not set)".to_string())
46 );
47 println!(
48 " Provider: {}",
49 settings
50 .effective_provider(None)
51 .unwrap_or_else(|| "(not set)".to_string())
52 );
53 println!(" Theme: {}", settings.get_theme_name());
54 println!(" Glyph set: {}", settings.glyph_set.label());
55 println!(" Thinking: {:?}", settings.thinking_level);
56 println!(" Extensions enabled: {}", settings.extensions_enabled);
57 println!(" Auto-compaction: {}", settings.auto_compaction);
58 println!(" Tool timeout: {}s", settings.tool_timeout_seconds);
59
60 let resource_types = [
61 ("Extensions", &settings.extensions),
62 ("Skills", &settings.skills),
63 ("Prompts", &settings.prompts),
64 ("Themes", &settings.themes),
65 ];
66
67 for (label, list) in &resource_types {
68 if list.is_empty() {
69 println!(" {}: (none)", label);
70 } else {
71 println!(" {}:", label);
72 for item in list.iter() {
73 println!(" - {}", item);
74 }
75 }
76 }
77
78 if settings.custom_providers.is_empty() {
79 println!(" Custom providers: (none)");
80 } else {
81 println!(" Custom providers:");
82 for cp in &settings.custom_providers {
83 println!(" - {} ({} @ {})", cp.name, cp.api, cp.base_url);
84 }
85 }
86 Ok(())
87}
88
89fn config_list(resource_type: Option<&String>) -> Result<()> {
91 let settings = Settings::load()?;
92
93 let resource_types: Vec<(&str, &Vec<String>, ResourceKind)> = vec![
94 ("extensions", &settings.extensions, ResourceKind::Extension),
95 ("skills", &settings.skills, ResourceKind::Skill),
96 ("prompts", &settings.prompts, ResourceKind::Prompt),
97 ("themes", &settings.themes, ResourceKind::Theme),
98 ];
99
100 let filtered: Vec<_> = if let Some(rt) = resource_type {
101 let kind = parse_resource_type(rt).ok_or_else(|| {
102 anyhow::anyhow!(
103 "Unknown resource type '{}'. Valid: extension, skill, prompt, theme",
104 rt
105 )
106 })?;
107 resource_types
108 .into_iter()
109 .filter(|(_, _, k)| *k == kind)
110 .collect()
111 } else {
112 resource_types
113 };
114
115 for (label, list, _) in &filtered {
116 if list.is_empty() {
117 println!("No {} configured.", label);
118 } else {
119 println!("{}:", label);
120 for (i, item) in list.iter().enumerate() {
121 println!(" {}. {}", i + 1, item);
122 }
123 }
124 println!();
125 }
126
127 let mgr = PackageManager::new()?;
129 let packages = mgr.list();
130 if !packages.is_empty() {
131 println!("Package resources:");
132 for pkg in packages {
133 if let Ok(resources) = mgr.discover_resources(&pkg.name) {
134 for r in &resources {
135 if let Some(rt) = resource_type
136 && let Some(kind) = parse_resource_type(rt)
137 && r.kind != kind
138 {
139 continue;
140 }
141 println!(" {} [{}] {}", pkg.name, r.kind, r.relative_path);
142 }
143 }
144 }
145 }
146 Ok(())
147}
148
149fn config_toggle_resource(resource_type: &str, name: &str, enable: bool) -> Result<()> {
151 let kind = parse_resource_type(resource_type).ok_or_else(|| {
152 anyhow::anyhow!(
153 "Unknown resource type '{}'. Valid: extension, skill, prompt, theme",
154 resource_type
155 )
156 })?;
157
158 let mut settings = Settings::load()?;
159
160 let list = match kind {
161 ResourceKind::Extension => &mut settings.extensions,
162 ResourceKind::Skill => &mut settings.skills,
163 ResourceKind::Prompt => &mut settings.prompts,
164 ResourceKind::Theme => &mut settings.themes,
165 };
166
167 if enable {
168 if list.iter().any(|item| item == name) {
169 println!("{} '{}' is already enabled.", kind, name);
170 return Ok(());
171 }
172 list.push(name.to_string());
173 settings.save()?;
174 println!("Enabled {} '{}'", kind, name);
175 } else {
176 let original_len = list.len();
177 list.retain(|item| item != name);
178 if list.len() == original_len {
179 println!("{} '{}' was not enabled.", kind, name);
180 return Ok(());
181 }
182 settings.save()?;
183 println!("Disabled {} '{}'", kind, name);
184 }
185 Ok(())
186}
187
188fn config_set(key: &str, value: &str) -> Result<()> {
190 let mut settings = Settings::load()?;
191
192 match key {
193 "theme" => {
194 settings.theme = value.to_string();
195 }
196 "model" => {
197 settings.last_used_model = Some(value.to_string());
198 }
199 "provider" => {
200 settings.last_used_provider = Some(value.to_string());
201 }
202 "thinking_level" | "thinking" => {
203 let level = crate::store::settings::parse_thinking_level(value).ok_or_else(|| {
204 anyhow::anyhow!(
205 "Invalid thinking level: '{}'. Valid: off, minimal, low, medium, high, xhigh",
206 value
207 )
208 })?;
209 settings.thinking_level = level;
210 }
211 "extensions_enabled" | "extensions" => {
212 settings.extensions_enabled = parse_config_bool(value)?;
213 }
214 "auto_compaction" => {
215 settings.auto_compaction = parse_config_bool(value)?;
216 }
217 "tool_timeout" | "tool_timeout_seconds" => {
218 settings.tool_timeout_seconds = value
219 .parse()
220 .map_err(|_| anyhow::anyhow!("Invalid timeout: '{}'", value))?;
221 }
222 "max_tokens" => {
223 settings.max_tokens = Some(
224 value
225 .parse()
226 .map_err(|_| anyhow::anyhow!("Invalid max_tokens: '{}'", value))?,
227 );
228 }
229 "temperature" => {
230 settings.default_temperature = Some(
231 value
232 .parse()
233 .map_err(|_| anyhow::anyhow!("Invalid temperature: '{}'", value))?,
234 );
235 }
236 "session_history_size" => {
237 settings.session_history_size = value
238 .parse()
239 .map_err(|_| anyhow::anyhow!("Invalid session_history_size: '{}'", value))?;
240 }
241 "glyph" | "glyph_set" => {
242 settings.glyph_set = value.parse().map_err(|e| anyhow::anyhow!("{e}"))?;
243 }
244 _ => {
245 anyhow::bail!(
246 "Unknown setting: '{}'. Valid keys: theme, model, provider,\
247 thinking_level, extensions_enabled, auto_compaction,\
248 glyph, tool_timeout,\
249 max_tokens, temperature, session_history_size",
250 key
251 );
252 }
253 }
254
255 settings.save()?;
256 println!("Set {} = {}", key, value);
257 Ok(())
258}
259
260fn config_get(key: &str) -> Result<()> {
262 let settings = Settings::load()?;
263
264 let value = match key {
265 "theme" => settings.theme.clone(),
266 "model" => settings
267 .last_used_model
268 .clone()
269 .unwrap_or_else(|| "(not set)".to_string()),
270 "provider" => settings
271 .last_used_provider
272 .clone()
273 .unwrap_or_else(|| "(not set)".to_string()),
274 "thinking_level" | "thinking" => format!("{:?}", settings.thinking_level).to_lowercase(),
275 "extensions_enabled" => settings.extensions_enabled.to_string(),
276 "auto_compaction" => settings.auto_compaction.to_string(),
277 "tool_timeout" | "tool_timeout_seconds" => {
278 format!("{}s", settings.tool_timeout_seconds)
279 }
280 "max_tokens" => settings
281 .max_tokens
282 .map(|t| t.to_string())
283 .unwrap_or_else(|| "(not set)".to_string()),
284 "temperature" => settings
285 .effective_temperature()
286 .map(|t| t.to_string())
287 .unwrap_or_else(|| "(not set)".to_string()),
288 "session_history_size" => settings.session_history_size.to_string(),
289 "extensions" => format!("{:?}", settings.extensions),
290 "skills" => format!("{:?}", settings.skills),
291 "prompts" => format!("{:?}", settings.prompts),
292 "themes" => format!("{:?}", settings.themes),
293 "custom_providers" => {
294 let items: Vec<String> = settings
295 .custom_providers
296 .iter()
297 .map(|cp| format!("{} ({} @ {})", cp.name, cp.api, cp.base_url))
298 .collect();
299 if items.is_empty() {
300 "(none)".to_string()
301 } else {
302 items.join(", ")
303 }
304 }
305 "glyph" | "glyph_set" => settings.glyph_set.label().to_string(),
306 _ => {
307 anyhow::bail!(
308 "Unknown setting: '{}'. Valid keys: theme, model, provider,\
309 thinking_level, extensions_enabled, auto_compaction,\
310 glyph, tool_timeout,\
311 max_tokens, temperature, session_history_size,\
312 extensions, skills, prompts, themes, custom_providers",
313 key
314 );
315 }
316 };
317
318 println!("{} = {}", key, value);
319 Ok(())
320}
321
322fn config_add_provider(name: &str, base_url: &str, api_key_env: &str, api: &str) -> Result<()> {
324 use crate::store::settings::CustomProvider;
325
326 let mut settings = Settings::load()?;
327
328 if let Some(cp) = settings
330 .custom_providers
331 .iter_mut()
332 .find(|cp| cp.name == name)
333 {
334 cp.base_url = base_url.to_string();
335 cp.api_key_env = api_key_env.to_string();
336 cp.api = api.to_string();
337 settings.save()?;
338 println!(
339 "Updated custom provider '{}' -> {} ({})",
340 name, base_url, api
341 );
342 } else {
343 settings.custom_providers.push(CustomProvider {
344 name: name.to_string(),
345 base_url: base_url.to_string(),
346 api_key_env: api_key_env.to_string(),
347 api: api.to_string(),
348 });
349 settings.save()?;
350 println!("Added custom provider '{}' -> {} ({})", name, base_url, api);
351 }
352 Ok(())
353}
354
355fn config_remove_provider(name: &str) -> Result<()> {
357 let mut settings = Settings::load()?;
358 let original_len = settings.custom_providers.len();
359 settings.custom_providers.retain(|cp| cp.name != name);
360
361 if settings.custom_providers.len() == original_len {
362 println!("Custom provider '{}' not found.", name);
363 return Ok(());
364 }
365
366 settings.save()?;
367 println!("Removed custom provider '{}'", name);
368 Ok(())
369}
370
371pub fn handle_config_reset(all: bool) -> Result<()> {
375 let auth_path = dirs::config_dir()
377 .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".config"))
378 .join("oxicode")
379 .join("auth.json");
380 if auth_path.exists() {
381 std::fs::remove_file(&auth_path)?;
382 println!("Removed credentials: {}", auth_path.display());
383 } else {
384 println!("No credentials file found at {}", auth_path.display());
385 }
386
387 if all {
388 if let Ok(settings_path) = crate::store::settings::Settings::settings_path()
390 && settings_path.exists()
391 {
392 std::fs::remove_file(&settings_path)?;
393 println!("Removed settings: {}", settings_path.display());
394 }
395 println!("Full reset complete. Run 'oxicode setup' to reconfigure.");
396 } else {
397 println!("Credentials reset. Run 'oxicode setup' to reconfigure API keys.");
398 }
399
400 Ok(())
401}
402
403fn handle_config_path_command() -> Result<()> {
405 let path = crate::store::settings::Settings::settings_path()?;
406 println!("{}", path.display());
407 Ok(())
408}
409
410pub fn parse_resource_type(s: &str) -> Option<ResourceKind> {
412 match s.to_lowercase().as_str() {
413 "extension" | "extensions" | "ext" => Some(ResourceKind::Extension),
414 "skill" | "skills" => Some(ResourceKind::Skill),
415 "prompt" | "prompts" => Some(ResourceKind::Prompt),
416 "theme" | "themes" => Some(ResourceKind::Theme),
417 _ => None,
418 }
419}
420
421pub fn parse_config_bool(s: &str) -> Result<bool> {
423 match s.to_lowercase().as_str() {
424 "true" | "1" | "yes" | "on" => Ok(true),
425 "false" | "0" | "no" | "off" => Ok(false),
426 _ => anyhow::bail!(
427 "Invalid boolean value: '{}'. Use true/false, yes/no, on/off, or 1/0",
428 s
429 ),
430 }
431}