Skip to main content

oxicode/cli/commands/
config.rs

1//! Config subcommand handler and helpers.
2
3use crate::cli::ConfigCommands;
4use crate::storage::packages::{PackageManager, ResourceKind};
5use crate::store::settings::Settings;
6use anyhow::Result;
7
8/// Handle `oxicode config …` subcommands.
9pub 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
35/// Handle `oxicode config show`
36fn 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
89/// Handle `oxicode config list [resource_type]`
90fn 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    // Also show resources from installed packages
128    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
149/// Handle `oxicode config enable/disable <type> <name>`
150fn 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
188/// Handle `oxicode config set <key> <value>`
189fn 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
260/// Handle `oxicode config get <key>`
261fn 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
322/// Handle `oxicode config add-provider`
323fn 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    // Update existing or add new
329    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
355/// Handle `oxicode config remove-provider`
356fn 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
371/// Handle `oxicode config reset [--all]`
372///
373/// Resets auth.json (credentials). With `--all`, also resets settings.
374pub fn handle_config_reset(all: bool) -> Result<()> {
375    // Always reset auth.json
376    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        // Also reset settings
389        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
403/// Handle `oxicode config path` — print config file path.
404fn handle_config_path_command() -> Result<()> {
405    let path = crate::store::settings::Settings::settings_path()?;
406    println!("{}", path.display());
407    Ok(())
408}
409
410/// Parse a resource type string into a ResourceKind
411pub 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
421/// Parse a boolean value from a config string
422pub 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}