Skip to main content

nika_cli/
config.rs

1//! Configuration management subcommand handler
2
3use clap::Subcommand;
4use colored::Colorize;
5use std::fs;
6use std::path::PathBuf;
7
8use nika_engine::error::NikaError;
9
10/// Configuration management actions
11#[derive(Subcommand)]
12pub enum ConfigAction {
13    /// List all configuration values
14    List {
15        /// Output as JSON
16        #[arg(long)]
17        json: bool,
18    },
19
20    /// Get a specific config value
21    Get {
22        /// Config key (dot-separated, e.g., editor.theme)
23        key: String,
24    },
25
26    /// Set a config value
27    Set {
28        /// Config key (dot-separated, e.g., editor.theme)
29        key: String,
30        /// Value to set
31        value: String,
32    },
33
34    /// Open config file in $EDITOR
35    Edit,
36
37    /// Show config file path
38    Path,
39
40    /// Reset config to defaults
41    Reset {
42        /// Skip confirmation
43        #[arg(short, long)]
44        force: bool,
45    },
46}
47
48pub fn handle_config_command(action: ConfigAction, quiet: bool) -> Result<(), NikaError> {
49    // Find .nika directory
50    let nika_dir = find_nika_dir()?;
51    let config_path = nika_dir.join("config.toml");
52
53    match action {
54        ConfigAction::Path => {
55            println!("{}", config_path.display());
56            Ok(())
57        }
58
59        ConfigAction::List { json } => {
60            if !config_path.exists() {
61                if json {
62                    println!("{{}}");
63                } else {
64                    println!(
65                        "{} No config file found at {}",
66                        "ℹ".cyan(),
67                        config_path.display()
68                    );
69                    println!("  Run 'nika init' to create one.");
70                }
71                return Ok(());
72            }
73
74            let content = fs::read_to_string(&config_path)?;
75
76            if json {
77                // Parse TOML and convert to JSON
78                let value: toml::Value =
79                    toml::from_str(&content).map_err(|e| NikaError::ValidationError {
80                        reason: format!("Invalid TOML: {e}"),
81                    })?;
82                let json = serde_json::to_string_pretty(&value).map_err(|e| {
83                    NikaError::ValidationError {
84                        reason: format!("JSON conversion failed: {e}"),
85                    }
86                })?;
87                println!("{json}");
88            } else {
89                println!("{}", "Nika Configuration".bold());
90                println!("{}", "─".repeat(40));
91                println!();
92                println!("{content}");
93            }
94            Ok(())
95        }
96
97        ConfigAction::Get { key } => {
98            if !config_path.exists() {
99                return Err(NikaError::ValidationError {
100                    reason: "No config file found. Run 'nika init' first.".to_string(),
101                });
102            }
103
104            let content = fs::read_to_string(&config_path)?;
105            let value: toml::Value =
106                toml::from_str(&content).map_err(|e| NikaError::ValidationError {
107                    reason: format!("Invalid TOML: {e}"),
108                })?;
109
110            // Navigate to the key (dot-separated path)
111            let mut current = &value;
112            for part in key.split('.') {
113                current = current
114                    .get(part)
115                    .ok_or_else(|| NikaError::ValidationError {
116                        reason: format!("Key '{key}' not found"),
117                    })?;
118            }
119
120            // Print the value
121            match current {
122                toml::Value::String(s) => println!("{s}"),
123                toml::Value::Integer(i) => println!("{i}"),
124                toml::Value::Float(f) => println!("{f}"),
125                toml::Value::Boolean(b) => println!("{b}"),
126                _ => println!("{current}"),
127            }
128            Ok(())
129        }
130
131        ConfigAction::Set { key, value } => {
132            if !config_path.exists() {
133                return Err(NikaError::ValidationError {
134                    reason: "No config file found. Run 'nika init' first.".to_string(),
135                });
136            }
137
138            let content = fs::read_to_string(&config_path)?;
139            let mut doc =
140                content
141                    .parse::<toml::Table>()
142                    .map_err(|e| NikaError::ValidationError {
143                        reason: format!("Invalid TOML: {e}"),
144                    })?;
145
146            // Navigate and set the value
147            let parts: Vec<&str> = key.split('.').collect();
148            if parts.is_empty() {
149                return Err(NikaError::ValidationError {
150                    reason: "Empty key".to_string(),
151                });
152            }
153
154            // Build nested structure
155            let mut current = &mut doc;
156            for (i, part) in parts.iter().enumerate() {
157                if i == parts.len() - 1 {
158                    // Last part - set the value
159                    let toml_value = parse_config_value(&value);
160                    current.insert((*part).to_string(), toml_value);
161                } else {
162                    // Navigate or create table
163                    if !current.contains_key(*part) {
164                        current.insert((*part).to_string(), toml::Value::Table(toml::Table::new()));
165                    }
166                    current = current
167                        .get_mut(*part)
168                        .ok_or_else(|| NikaError::ValidationError {
169                            reason: format!("Config key '{part}' not found"),
170                        })?
171                        .as_table_mut()
172                        .ok_or_else(|| NikaError::ValidationError {
173                            reason: format!("'{part}' is not a table"),
174                        })?;
175                }
176            }
177
178            // Write back
179            let new_content =
180                toml::to_string_pretty(&doc).map_err(|e| NikaError::ValidationError {
181                    reason: format!("TOML serialization failed: {e}"),
182                })?;
183            fs::write(&config_path, new_content)?;
184
185            if !quiet {
186                println!("{} {} = {}", "✓".green(), key, value);
187            }
188            Ok(())
189        }
190
191        ConfigAction::Edit => {
192            let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
193
194            if !config_path.exists() {
195                return Err(NikaError::ValidationError {
196                    reason: format!(
197                        "No config file found at {}. Run 'nika init' first.",
198                        config_path.display()
199                    ),
200                });
201            }
202
203            let status = std::process::Command::new(&editor)
204                .arg(&config_path)
205                .status()
206                .map_err(|e| NikaError::ValidationError {
207                    reason: format!("Failed to launch editor '{editor}': {e}"),
208                })?;
209
210            if !status.success() {
211                return Err(NikaError::ValidationError {
212                    reason: format!("Editor '{}' exited with code {:?}", editor, status.code()),
213                });
214            }
215            Ok(())
216        }
217
218        ConfigAction::Reset { force } => {
219            if !force {
220                println!(
221                    "{} This will reset config to defaults. Use --force to confirm.",
222                    "⚠".yellow()
223                );
224                return Ok(());
225            }
226
227            if config_path.exists() {
228                fs::remove_file(&config_path)?;
229            }
230
231            // Create default config
232            let default_config = include_str!("../templates/config.toml");
233            fs::write(&config_path, default_config)?;
234
235            if !quiet {
236                println!("{} Config reset to defaults", "✓".green());
237            }
238            Ok(())
239        }
240    }
241}
242
243fn parse_config_value(value: &str) -> toml::Value {
244    // Try boolean
245    if value == "true" {
246        return toml::Value::Boolean(true);
247    }
248    if value == "false" {
249        return toml::Value::Boolean(false);
250    }
251
252    // Try integer
253    if let Ok(i) = value.parse::<i64>() {
254        return toml::Value::Integer(i);
255    }
256
257    // Try float
258    if let Ok(f) = value.parse::<f64>() {
259        return toml::Value::Float(f);
260    }
261
262    // Default to string
263    toml::Value::String(value.to_string())
264}
265
266pub fn find_nika_dir() -> Result<PathBuf, NikaError> {
267    let current = std::env::current_dir()?;
268
269    let mut dir = current.as_path();
270    loop {
271        let nika_dir = dir.join(".nika");
272        if nika_dir.exists() && nika_dir.is_dir() {
273            return Ok(nika_dir);
274        }
275
276        match dir.parent() {
277            Some(parent) => dir = parent,
278            None => break,
279        }
280    }
281
282    // Default to current directory
283    Ok(current.join(".nika"))
284}