Skip to main content

cargo_mate/captain/
config.rs

1use anyhow::{Context, Result};
2use colored::*;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::PathBuf;
7use crate::log::Log;
8use super::license;
9#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct ProjectConfig {
11    pub project: ProjectSettings,
12    pub shortcuts: HashMap<String, String>,
13    pub auto_fix: AutoFixSettings,
14    pub journey: JourneySettings,
15    pub build: BuildSettings,
16    pub version_control: VersionControlSettings,
17    pub hooks: HookSettings,
18}
19#[derive(Debug, Serialize, Deserialize, Clone)]
20pub struct ProjectSettings {
21    pub name: Option<String>,
22    pub default_journey: Option<String>,
23    pub theme: String,
24    pub auto_checklist: bool,
25    pub track_performance: bool,
26}
27#[derive(Debug, Serialize, Deserialize, Clone)]
28pub struct AutoFixSettings {
29    pub format_on_save: bool,
30    pub clippy_on_build: bool,
31    pub auto_deps_update: bool,
32    pub fix_warnings: bool,
33    pub suggest_fixes: bool,
34}
35#[derive(Debug, Serialize, Deserialize, Clone)]
36pub struct JourneySettings {
37    pub auto_record: bool,
38    pub share_templates: bool,
39    pub max_recordings: usize,
40    pub interactive_by_default: bool,
41}
42#[derive(Debug, Serialize, Deserialize, Clone)]
43pub struct BuildSettings {
44    pub default_profile: String,
45    pub parallel_jobs: Option<usize>,
46    pub target_dir: Option<PathBuf>,
47    pub incremental: bool,
48    pub cache_artifacts: bool,
49}
50#[derive(Debug, Serialize, Deserialize, Clone)]
51pub struct HookSettings {
52    pub pre_build: Vec<String>,
53    pub post_build: Vec<String>,
54    pub on_error: Vec<String>,
55    pub on_success: Vec<String>,
56}
57
58#[derive(Debug, Serialize, Deserialize, Clone)]
59pub struct VersionControlSettings {
60    pub auto_git_commit: bool,
61    pub auto_anchor_git: bool,
62}
63pub struct ConfigManager {
64    global_config: ProjectConfig,
65    local_config: Option<ProjectConfig>,
66    global_path: PathBuf,
67    local_path: PathBuf,
68}
69impl ConfigManager {
70    pub fn new() -> Result<Self> {
71        let global_path = dirs::home_dir()
72            .context("Could not find home directory")?
73            .join(".shipwreck")
74            .join("config.toml");
75        let local_path = PathBuf::from(".cg");
76        let global_config = if global_path.exists() {
77            let content = fs::read_to_string(&global_path)?;
78            toml::from_str(&content)?
79        } else {
80            ProjectConfig::default()
81        };
82        let local_config = if local_path.exists() {
83            let content = fs::read_to_string(&local_path)?;
84            Some(toml::from_str(&content)?)
85        } else {
86            None
87        };
88        Ok(Self {
89            global_config,
90            local_config,
91            global_path,
92            local_path,
93        })
94    }
95    pub fn get(&self, key: &str) -> Option<String> {
96        let parts: Vec<&str> = key.split('.').collect();
97        if let Some(ref local) = self.local_config {
98            if let Some(value) = self.get_from_config(local, &parts) {
99                return Some(value);
100            }
101        }
102        self.get_from_config(&self.global_config, &parts)
103    }
104    fn get_from_config(&self, config: &ProjectConfig, parts: &[&str]) -> Option<String> {
105        match parts {
106            ["project", field] => {
107                match *field {
108                    "name" => config.project.name.clone(),
109                    "default_journey" => config.project.default_journey.clone(),
110                    "theme" => Some(config.project.theme.clone()),
111                    "auto_checklist" => Some(config.project.auto_checklist.to_string()),
112                    "track_performance" => {
113                        Some(config.project.track_performance.to_string())
114                    }
115                    _ => None,
116                }
117            }
118            ["shortcuts", key] => config.shortcuts.get(*key).cloned(),
119            ["auto_fix", field] => {
120                match *field {
121                    "format_on_save" => Some(config.auto_fix.format_on_save.to_string()),
122                    "clippy_on_build" => {
123                        Some(config.auto_fix.clippy_on_build.to_string())
124                    }
125                    "auto_deps_update" => {
126                        Some(config.auto_fix.auto_deps_update.to_string())
127                    }
128                    "fix_warnings" => Some(config.auto_fix.fix_warnings.to_string()),
129                    "suggest_fixes" => Some(config.auto_fix.suggest_fixes.to_string()),
130                    _ => None,
131                }
132            }
133            ["build", field] => {
134                match *field {
135                    "default_profile" => Some(config.build.default_profile.clone()),
136                    "parallel_jobs" => config.build.parallel_jobs.map(|j| j.to_string()),
137                    "incremental" => Some(config.build.incremental.to_string()),
138                    "cache_artifacts" => Some(config.build.cache_artifacts.to_string()),
139                    _ => None,
140                }
141            }
142            _ => None,
143        }
144    }
145    pub fn set(&mut self, key: &str, value: &str, local: bool) -> Result<()> {
146        let config = if local {
147            self.local_config.get_or_insert_with(ProjectConfig::default)
148        } else {
149            &mut self.global_config
150        };
151        let parts: Vec<&str> = key.split('.').collect();
152        match parts.as_slice() {
153            ["project", field] => {
154                match *field {
155                    "name" => config.project.name = Some(value.to_string()),
156                    "default_journey" => {
157                        config.project.default_journey = Some(value.to_string());
158                    }
159                    "theme" => config.project.theme = value.to_string(),
160                    "auto_checklist" => config.project.auto_checklist = value.parse()?,
161                    "track_performance" => {
162                        config.project.track_performance = value.parse()?;
163                    }
164                    _ => return Err(anyhow::anyhow!("Unknown project field: {}", field)),
165                }
166            }
167            ["shortcuts", key] => {
168                config.shortcuts.insert(key.to_string(), value.to_string());
169            }
170            ["auto_fix", field] => {
171                match *field {
172                    "format_on_save" => config.auto_fix.format_on_save = value.parse()?,
173                    "clippy_on_build" => config.auto_fix.clippy_on_build = value.parse()?,
174                    "auto_deps_update" => {
175                        config.auto_fix.auto_deps_update = value.parse()?;
176                    }
177                    "fix_warnings" => config.auto_fix.fix_warnings = value.parse()?,
178                    "suggest_fixes" => config.auto_fix.suggest_fixes = value.parse()?,
179                    _ => return Err(anyhow::anyhow!("Unknown auto_fix field: {}", field)),
180                }
181            }
182            ["build", field] => {
183                match *field {
184                    "default_profile" => config.build.default_profile = value.to_string(),
185                    "parallel_jobs" => config.build.parallel_jobs = Some(value.parse()?),
186                    "incremental" => config.build.incremental = value.parse()?,
187                    "cache_artifacts" => config.build.cache_artifacts = value.parse()?,
188                    _ => return Err(anyhow::anyhow!("Unknown build field: {}", field)),
189                }
190            }
191            _ => return Err(anyhow::anyhow!("Unknown config key: {}", key)),
192        }
193        self.save(local)?;
194        println!("✅ Config set: {} = {}", key.cyan(), value.green());
195        Ok(())
196    }
197    pub fn add_shortcut(
198        &mut self,
199        name: &str,
200        command: &str,
201        local: bool,
202    ) -> Result<()> {
203        let config = if local {
204            self.local_config.get_or_insert_with(ProjectConfig::default)
205        } else {
206            &mut self.global_config
207        };
208        config.shortcuts.insert(name.to_string(), command.to_string());
209        self.save(local)?;
210        println!("✅ Shortcut added: {} → {}", name.cyan(), command.green());
211        Ok(())
212    }
213    pub fn get_shortcut(&self, name: &str) -> Option<String> {
214        if let Some(ref local) = self.local_config {
215            if let Some(cmd) = local.shortcuts.get(name) {
216                return Some(cmd.clone());
217            }
218        }
219        self.global_config.shortcuts.get(name).cloned()
220    }
221    pub fn list_shortcuts(&self) {
222        println!("{}", "=== Shortcuts ===".blue().bold());
223        let mut all_shortcuts = self.global_config.shortcuts.clone();
224        if let Some(ref local) = self.local_config {
225            for (name, cmd) in &local.shortcuts {
226                all_shortcuts.insert(name.clone(), cmd.clone());
227            }
228        }
229        if all_shortcuts.is_empty() {
230            println!("No shortcuts defined");
231        } else {
232            for (name, cmd) in all_shortcuts {
233                println!("  {} → {}", name.cyan(), cmd.green());
234            }
235        }
236    }
237    pub fn add_hook(
238        &mut self,
239        hook_type: &str,
240        command: &str,
241        local: bool,
242    ) -> Result<()> {
243        let config = if local {
244            self.local_config.get_or_insert_with(ProjectConfig::default)
245        } else {
246            &mut self.global_config
247        };
248        match hook_type {
249            "pre_build" => config.hooks.pre_build.push(command.to_string()),
250            "post_build" => config.hooks.post_build.push(command.to_string()),
251            "on_error" => config.hooks.on_error.push(command.to_string()),
252            "on_success" => config.hooks.on_success.push(command.to_string()),
253            _ => return Err(anyhow::anyhow!("Unknown hook type: {}", hook_type)),
254        }
255        self.save(local)?;
256        println!("✅ Hook added: {} → {}", hook_type.cyan(), command.green());
257        Ok(())
258    }
259    pub fn run_hooks(&self, hook_type: &str) -> Result<()> {
260        let mut hooks = Vec::new();
261        match hook_type {
262            "pre_build" => {
263                hooks.extend(self.global_config.hooks.pre_build.clone());
264                if let Some(ref local) = self.local_config {
265                    hooks.extend(local.hooks.pre_build.clone());
266                }
267            }
268            "post_build" => {
269                hooks.extend(self.global_config.hooks.post_build.clone());
270                if let Some(ref local) = self.local_config {
271                    hooks.extend(local.hooks.post_build.clone());
272                }
273            }
274            "on_error" => {
275                hooks.extend(self.global_config.hooks.on_error.clone());
276                if let Some(ref local) = self.local_config {
277                    hooks.extend(local.hooks.on_error.clone());
278                }
279            }
280            "on_success" => {
281                hooks.extend(self.global_config.hooks.on_success.clone());
282                if let Some(ref local) = self.local_config {
283                    hooks.extend(local.hooks.on_success.clone());
284                }
285            }
286            _ => {}
287        }
288        for hook in hooks {
289            println!("🎣 Running {} hook: {}", hook_type, hook.dimmed());
290            std::process::Command::new("sh").arg("-c").arg(&hook).status()?;
291        }
292        Ok(())
293    }
294    pub fn init_local(&mut self) -> Result<()> {
295        if self.local_path.exists() {
296            println!("⚠️  Local config already exists");
297            return Ok(());
298        }
299        let config = ProjectConfig::default();
300        let toml = toml::to_string_pretty(&config)?;
301        fs::write(&self.local_path, toml)?;
302        self.local_config = Some(config);
303        println!("✅ Created local config file: .cg");
304        println!("   Edit this file to customize project-specific settings");
305        Ok(())
306    }
307    pub fn show(&self) {
308        println!("{}", "=== Configuration ===".blue().bold());
309        if let Some(ref local) = self.local_config {
310            println!("\n📁 Local Config (.cg):");
311            self.display_config(local, "  ");
312        }
313        println!("\n🌍 Global Config:");
314        self.display_config(&self.global_config, "  ");
315    }
316    fn display_config(&self, config: &ProjectConfig, prefix: &str) {
317        println!("{}Project:", prefix);
318        if let Some(ref name) = config.project.name {
319            println!("{}  name: {}", prefix, name.green());
320        }
321        if let Some(ref journey) = config.project.default_journey {
322            println!("{}  default_journey: {}", prefix, journey.green());
323        }
324        println!("{}  theme: {}", prefix, config.project.theme.green());
325        println!("{}  auto_checklist: {}", prefix, config.project.auto_checklist);
326        if !config.shortcuts.is_empty() {
327            println!("{}Shortcuts:", prefix);
328            for (name, cmd) in &config.shortcuts {
329                println!("{}  {} → {}", prefix, name.cyan(), cmd);
330            }
331        }
332        println!("{}Auto Fix:", prefix);
333        println!("{}  format_on_save: {}", prefix, config.auto_fix.format_on_save);
334        println!("{}  clippy_on_build: {}", prefix, config.auto_fix.clippy_on_build);
335        println!("{}Build:", prefix);
336        println!("{}  default_profile: {}", prefix, config.build.default_profile);
337        println!("{}  incremental: {}", prefix, config.build.incremental);
338    }
339    pub fn save(&self, local: bool) -> Result<()> {
340        if local {
341            if let Some(ref config) = self.local_config {
342                let toml = toml::to_string_pretty(config)?;
343                fs::write(&self.local_path, toml)?;
344            }
345        } else {
346            let toml = toml::to_string_pretty(&self.global_config)?;
347            fs::create_dir_all(self.global_path.parent().unwrap())?;
348            fs::write(&self.global_path, toml)?;
349        }
350        Ok(())
351    }
352    pub fn merge_with_env(&mut self) {
353        if let Ok(val) = std::env::var("CM_DEFAULT_PROFILE") {
354            self.global_config.build.default_profile = val;
355        }
356        if let Ok(val) = std::env::var("CM_PARALLEL_JOBS") {
357            if let Ok(jobs) = val.parse() {
358                self.global_config.build.parallel_jobs = Some(jobs);
359            }
360        }
361        if let Ok(val) = std::env::var("CM_AUTO_FIX") {
362            if let Ok(enabled) = val.parse() {
363                self.global_config.auto_fix.format_on_save = enabled;
364                self.global_config.auto_fix.clippy_on_build = enabled;
365            }
366        }
367    }
368    pub fn reset(&mut self) -> Result<()> {
369        self.local_config = Some(ProjectConfig::default());
370        self.save(true)?;
371        println!("✅ Local configuration reset to defaults");
372        Ok(())
373    }
374}
375impl Default for ProjectConfig {
376    fn default() -> Self {
377        Self {
378            project: ProjectSettings {
379                name: None,
380                default_journey: None,
381                theme: "nautical".to_string(),
382                auto_checklist: true,
383                track_performance: true,
384            },
385            shortcuts: HashMap::new(),
386            auto_fix: AutoFixSettings {
387                format_on_save: false,
388                clippy_on_build: false,
389                auto_deps_update: false,
390                fix_warnings: false,
391                suggest_fixes: true,
392            },
393            journey: JourneySettings {
394                auto_record: false,
395                share_templates: false,
396                max_recordings: 100,
397                interactive_by_default: true,
398            },
399            build: BuildSettings {
400                default_profile: "dev".to_string(),
401                parallel_jobs: None,
402                target_dir: None,
403                incremental: true,
404                cache_artifacts: true,
405            },
406            version_control: VersionControlSettings {
407                auto_git_commit: true,
408                auto_anchor_git: true,
409            },
410            hooks: HookSettings {
411                pre_build: Vec::new(),
412                post_build: Vec::new(),
413                on_error: Vec::new(),
414                on_success: Vec::new(),
415            },
416        }
417    }
418}
419pub fn check_captain_authority(command: &str) -> Result<bool> {
420    let log = Log::new();
421    log.log(
422        &format!(
423            "Captain's authority check for command '{}' - all hands on deck!", command
424        ),
425        vec!["captain".to_string(), "authority".to_string()],
426    )?;
427    let license_manager = license::LicenseManager::new()?;
428    match license_manager.enforce_license(command) {
429        Ok(_) => Ok(true),
430        Err(e) => {
431            if e.to_string().contains("limit") {
432                log.log(
433                    "Command quota exceeded",
434                    vec!["captain".to_string(), "authority".to_string()],
435                )?;
436            } else if e.to_string().contains("License not found") {
437                log.log(
438                    "Unauthorized vessel! No captain's papers found!",
439                    vec!["captain".to_string(), "authority".to_string()],
440                )?;
441            } else {
442                log.log(
443                    "Mutiny alert! Authority check failed",
444                    vec!["captain".to_string(), "authority".to_string()],
445                )?;
446            }
447            Ok(false)
448        }
449    }
450}