metis_docs_cli/commands/
config.rs

1use crate::workspace;
2use anyhow::Result;
3use clap::{Args, Subcommand};
4use metis_core::{domain::configuration::FlightLevelConfig, Database};
5
6#[derive(Args)]
7pub struct ConfigCommand {
8    #[command(subcommand)]
9    pub action: ConfigAction,
10}
11
12#[derive(Subcommand)]
13pub enum ConfigAction {
14    /// Show current configuration
15    Show,
16    /// Set configuration using preset or custom values
17    Set {
18        /// Configuration preset (full, streamlined, direct)
19        #[arg(short, long)]
20        preset: Option<String>,
21        /// Enable/disable strategies (true/false)
22        #[arg(long)]
23        strategies: Option<bool>,
24        /// Enable/disable initiatives (true/false)
25        #[arg(long)]
26        initiatives: Option<bool>,
27    },
28    /// Get a specific configuration value
29    Get {
30        /// Configuration key to retrieve
31        key: String,
32    },
33}
34
35impl ConfigCommand {
36    pub async fn execute(&self) -> Result<()> {
37        // 1. Validate we're in a metis workspace
38        let (workspace_exists, metis_dir) = workspace::has_metis_vault();
39        if !workspace_exists {
40            anyhow::bail!("Not in a Metis workspace. Run 'metis init' to create one.");
41        }
42        let metis_dir = metis_dir.unwrap();
43
44        // 2. Connect to database
45        let db_path = metis_dir.join("metis.db");
46        let db = Database::new(db_path.to_str().unwrap())
47            .map_err(|e| anyhow::anyhow!("Database connection failed: {}", e))?;
48        let mut config_repo = db
49            .configuration_repository()
50            .map_err(|e| anyhow::anyhow!("Failed to create configuration repository: {}", e))?;
51
52        // 3. Execute the requested action
53        match &self.action {
54            ConfigAction::Show => self.show_config(&mut config_repo).await,
55            ConfigAction::Set {
56                preset,
57                strategies,
58                initiatives,
59            } => {
60                self.set_config(&mut config_repo, preset, *strategies, *initiatives)
61                    .await
62            }
63            ConfigAction::Get { key } => self.get_config(&mut config_repo, key).await,
64        }
65    }
66
67    async fn show_config(
68        &self,
69        config_repo: &mut metis_core::dal::database::configuration_repository::ConfigurationRepository,
70    ) -> Result<()> {
71        let flight_config = config_repo
72            .get_flight_level_config()
73            .map_err(|e| anyhow::anyhow!("Failed to get flight level configuration: {}", e))?;
74
75        println!("Current Flight Level Configuration:");
76        println!("  Preset: {}", flight_config.preset_name());
77        println!("  Strategies enabled: {}", flight_config.strategies_enabled);
78        println!(
79            "  Initiatives enabled: {}",
80            flight_config.initiatives_enabled
81        );
82        println!();
83        println!("Hierarchy: {}", flight_config.hierarchy_display());
84        println!();
85        println!("Available document types:");
86        for doc_type in flight_config.enabled_document_types() {
87            println!("  - {}", doc_type);
88        }
89
90        Ok(())
91    }
92
93    async fn set_config(
94        &self,
95        config_repo: &mut metis_core::dal::database::configuration_repository::ConfigurationRepository,
96        preset: &Option<String>,
97        strategies: Option<bool>,
98        initiatives: Option<bool>,
99    ) -> Result<()> {
100        let new_config = if let Some(preset_name) = preset {
101            // Use preset configuration
102            match preset_name.as_str() {
103                "full" => FlightLevelConfig::full(),
104                "streamlined" => FlightLevelConfig::streamlined(),
105                "direct" => FlightLevelConfig::direct(),
106                _ => {
107                    anyhow::bail!(
108                        "Invalid preset '{}'. Valid presets are: full, streamlined, direct",
109                        preset_name
110                    );
111                }
112            }
113        } else if strategies.is_some() || initiatives.is_some() {
114            // Use custom configuration
115            let current_config = config_repo
116                .get_flight_level_config()
117                .map_err(|e| anyhow::anyhow!("Failed to get current configuration: {}", e))?;
118
119            let strategies_enabled = strategies.unwrap_or(current_config.strategies_enabled);
120            let initiatives_enabled = initiatives.unwrap_or(current_config.initiatives_enabled);
121
122            FlightLevelConfig::new(strategies_enabled, initiatives_enabled)
123                .map_err(|e| anyhow::anyhow!("Invalid configuration: {}", e))?
124        } else {
125            anyhow::bail!(
126                "Must specify either --preset or at least one of --strategies/--initiatives"
127            );
128        };
129
130        // Save the new configuration
131        config_repo
132            .set_flight_level_config(&new_config)
133            .map_err(|e| anyhow::anyhow!("Failed to save configuration: {}", e))?;
134
135        println!("Configuration updated successfully!");
136        println!("New configuration:");
137        println!("  Preset: {}", new_config.preset_name());
138        println!("  Strategies enabled: {}", new_config.strategies_enabled);
139        println!("  Initiatives enabled: {}", new_config.initiatives_enabled);
140        println!("  Hierarchy: {}", new_config.hierarchy_display());
141
142        Ok(())
143    }
144
145    async fn get_config(
146        &self,
147        config_repo: &mut metis_core::dal::database::configuration_repository::ConfigurationRepository,
148        key: &str,
149    ) -> Result<()> {
150        let value = config_repo
151            .get(key)
152            .map_err(|e| anyhow::anyhow!("Failed to get configuration value: {}", e))?;
153
154        match value {
155            Some(v) => {
156                println!("{}", v);
157                Ok(())
158            }
159            None => {
160                anyhow::bail!("Configuration key '{}' not found", key);
161            }
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::commands::InitCommand;
170    use tempfile::tempdir;
171
172    #[tokio::test]
173    async fn test_config_show_default() {
174        let temp_dir = tempdir().unwrap();
175        let original_dir = std::env::current_dir().ok();
176
177        // Change to temp directory
178        std::env::set_current_dir(temp_dir.path()).unwrap();
179
180        // Initialize a new project first
181        let init_cmd = InitCommand {
182            name: Some("Test Project".to_string()),
183            preset: None,
184            strategies: None,
185            initiatives: None,
186            prefix: None,
187        };
188        init_cmd.execute().await.unwrap();
189
190        // Test config show
191        let config_cmd = ConfigCommand {
192            action: ConfigAction::Show,
193        };
194
195        let result = config_cmd.execute().await;
196        assert!(result.is_ok());
197
198        // Restore original directory
199        if let Some(dir) = original_dir {
200            std::env::set_current_dir(dir).unwrap();
201        }
202    }
203
204    #[tokio::test]
205    async fn test_config_set_streamlined_preset() {
206        let temp_dir = tempdir().unwrap();
207        let original_dir = std::env::current_dir().ok();
208
209        // Change to temp directory
210        std::env::set_current_dir(temp_dir.path()).unwrap();
211
212        // Initialize a new project first
213        let init_cmd = InitCommand {
214            name: Some("Test Project".to_string()),
215            preset: None,
216            strategies: None,
217            initiatives: None,
218            prefix: None,
219        };
220        init_cmd.execute().await.unwrap();
221
222        // Test setting streamlined preset
223        let config_cmd = ConfigCommand {
224            action: ConfigAction::Set {
225                preset: Some("streamlined".to_string()),
226                strategies: None,
227                initiatives: None,
228            },
229        };
230
231        let result = config_cmd.execute().await;
232        assert!(result.is_ok());
233
234        // Restore original directory
235        if let Some(dir) = original_dir {
236            std::env::set_current_dir(dir).unwrap();
237        }
238    }
239
240    #[tokio::test]
241    async fn test_config_set_invalid_preset() {
242        let temp_dir = tempdir().unwrap();
243        let original_dir = std::env::current_dir().ok();
244
245        // Change to temp directory
246        std::env::set_current_dir(temp_dir.path()).unwrap();
247
248        // Initialize a new project first
249        let init_cmd = InitCommand {
250            name: Some("Test Project".to_string()),
251            preset: None,
252            strategies: None,
253            initiatives: None,
254            prefix: None,
255        };
256        init_cmd.execute().await.unwrap();
257
258        // Test setting invalid preset
259        let config_cmd = ConfigCommand {
260            action: ConfigAction::Set {
261                preset: Some("invalid".to_string()),
262                strategies: None,
263                initiatives: None,
264            },
265        };
266
267        let result = config_cmd.execute().await;
268        assert!(result.is_err());
269        assert!(result.unwrap_err().to_string().contains("Invalid preset"));
270
271        // Restore original directory
272        if let Some(dir) = original_dir {
273            std::env::set_current_dir(dir).unwrap();
274        }
275    }
276
277    #[tokio::test]
278    async fn test_config_without_workspace() {
279        let temp_dir = tempdir().unwrap();
280        let original_dir = std::env::current_dir().ok();
281
282        // Change to temp directory without initializing a workspace
283        std::env::set_current_dir(temp_dir.path()).unwrap();
284
285        // Test config show without workspace
286        let config_cmd = ConfigCommand {
287            action: ConfigAction::Show,
288        };
289
290        let result = config_cmd.execute().await;
291        assert!(result.is_err());
292        assert!(result
293            .unwrap_err()
294            .to_string()
295            .contains("Not in a Metis workspace"));
296
297        // Restore original directory
298        if let Some(dir) = original_dir {
299            std::env::set_current_dir(dir).unwrap();
300        }
301    }
302}