Skip to main content

cargo_mate/
mutiny.rs

1use anyhow::{Context, Result};
2use colored::*;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::PathBuf;
7use std::process::{Command, Stdio};
8use crate::captain::license;
9#[derive(Debug, Serialize, Deserialize, Clone)]
10pub struct MutinyConfig {
11    pub overrides: HashMap<String, Override>,
12    pub force_flags: Vec<String>,
13    pub skip_checks: Vec<String>,
14    pub custom_env: HashMap<String, String>,
15    pub allow_dirty: bool,
16    pub ignore_lockfile: bool,
17}
18#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct Override {
20    pub enabled: bool,
21    pub reason: String,
22    pub expires: Option<chrono::DateTime<chrono::Utc>>,
23    pub commands: Vec<String>,
24}
25pub struct MutinyMode {
26    config: MutinyConfig,
27    config_file: PathBuf,
28    active: bool,
29}
30impl MutinyMode {
31    pub fn new() -> Result<Self> {
32        let config_file = dirs::home_dir()
33            .context("Could not find home directory")?
34            .join(".shipwreck")
35            .join("mutiny.toml");
36        let config = if config_file.exists() {
37            let content = fs::read_to_string(&config_file)?;
38            toml::from_str(&content)?
39        } else {
40            MutinyConfig::default()
41        };
42        Ok(Self {
43            config,
44            config_file,
45            active: false,
46        })
47    }
48    pub fn activate(&mut self, reason: &str) -> Result<()> {
49        self.active = true;
50        println!("๐Ÿดโ€โ˜ ๏ธ {} activated!", "MUTINY MODE".red().bold());
51        println!("โš ๏ธ  Reason: {}", reason.yellow());
52        println!("๐Ÿ”ฅ Cargo's opinions have been overridden!");
53        println!();
54        self.log_activation(reason)?;
55        Ok(())
56    }
57    pub fn deactivate(&mut self) -> Result<()> {
58        self.active = false;
59        println!("โœ… Mutiny Mode deactivated");
60        println!("๐Ÿšข Normal cargo operations restored");
61        Ok(())
62    }
63    pub fn allow_warnings(&mut self) -> Result<()> {
64        let override_config = Override {
65            enabled: true,
66            reason: "Temporarily allowing warnings".to_string(),
67            expires: Some(chrono::Utc::now() + chrono::Duration::hours(1)),
68            commands: vec!["build".to_string(), "test".to_string()],
69        };
70        self.config.overrides.insert("allow_warnings".to_string(), override_config);
71        self.config.force_flags.push("--cap-lints=warn".to_string());
72        self.save_config()?;
73        println!("โš ๏ธ  Warnings will be allowed for the next hour");
74        Ok(())
75    }
76    pub fn skip_tests(&mut self) -> Result<()> {
77        self.config.skip_checks.push("test".to_string());
78        self.save_config()?;
79        println!("๐Ÿƒ Tests will be skipped");
80        Ok(())
81    }
82    pub fn force_build(&mut self) -> Result<()> {
83        self.config.allow_dirty = true;
84        self.config.ignore_lockfile = true;
85        self.save_config()?;
86        println!("๐Ÿ’ช Force build enabled - ignoring dirty state and lockfile");
87        Ok(())
88    }
89    pub fn yolo_mode(&mut self) -> Result<()> {
90        println!(
91            "๐Ÿ’€ {} - Disabling ALL safety checks!", "YOLO MODE ACTIVATED".red().bold()
92            .blink()
93        );
94        println!("โš ๏ธ  This is extremely dangerous!");
95        self.config
96            .overrides
97            .insert(
98                "yolo".to_string(),
99                Override {
100                    enabled: true,
101                    reason: "YOLO - living dangerously".to_string(),
102                    expires: Some(chrono::Utc::now() + chrono::Duration::minutes(30)),
103                    commands: vec!["*".to_string()],
104                },
105            );
106        self.config.force_flags = vec![
107            "--cap-lints=allow".to_string(), "-Z unstable-options".to_string(),
108        ];
109        self.config.skip_checks = vec![
110            "test".to_string(), "clippy".to_string(), "fmt".to_string(), "audit"
111            .to_string(),
112        ];
113        self.config.allow_dirty = true;
114        self.config.ignore_lockfile = true;
115        self.save_config()?;
116        println!("๐ŸŽฒ All bets are off for 30 minutes!");
117        Ok(())
118    }
119    pub fn wrap_cargo_command(&self, args: &[&str]) -> Result<std::process::Output> {
120        let mut cmd = Command::new("cargo");
121        if self.active {
122            for (key, value) in &self.config.custom_env {
123                cmd.env(key, value);
124            }
125            if self.should_skip_command(args.get(0).unwrap_or(&"")) {
126                println!("โญ๏ธ  Skipping {} due to mutiny override", args[0].yellow());
127                #[cfg(unix)]
128                {
129                    use std::os::unix::process::ExitStatusExt;
130                    return Ok(std::process::Output {
131                        status: std::process::ExitStatus::from_raw(0),
132                        stdout: b"Skipped by Mutiny Mode".to_vec(),
133                        stderr: Vec::new(),
134                    });
135                }
136                #[cfg(not(unix))]
137                {
138                    return Ok(std::process::Output {
139                        status: std::process::ExitStatus::from(
140                            std::process::ExitStatus::default(),
141                        ),
142                        stdout: b"Skipped by Mutiny Mode".to_vec(),
143                        stderr: Vec::new(),
144                    });
145                }
146            }
147            cmd.args(args);
148            for flag in &self.config.force_flags {
149                if !args.contains(&flag.as_str()) {
150                    cmd.arg(flag);
151                }
152            }
153            if self.config.allow_dirty && args.get(0) == Some(&"publish") {
154                cmd.arg("--allow-dirty");
155            }
156            if self.config.ignore_lockfile {
157                cmd.env("CARGO_IGNORE_LOCKFILE", "1");
158            }
159        } else {
160            cmd.args(args);
161        }
162        println!("๐Ÿดโ€โ˜ ๏ธ Running: {:?}", cmd);
163        cmd.output().context("Failed to execute cargo command")
164    }
165    fn should_skip_command(&self, command: &str) -> bool {
166        self.config.skip_checks.contains(&command.to_string())
167    }
168    pub fn add_custom_flag(&mut self, flag: &str, reason: &str) -> Result<()> {
169        self.config.force_flags.push(flag.to_string());
170        let override_config = Override {
171            enabled: true,
172            reason: reason.to_string(),
173            expires: None,
174            commands: vec!["*".to_string()],
175        };
176        self.config
177            .overrides
178            .insert(format!("custom_flag_{}", flag.replace("-", "_")), override_config);
179        self.save_config()?;
180        println!("โž• Added custom flag: {}", flag.green());
181        Ok(())
182    }
183    pub fn set_env(&mut self, key: &str, value: &str) -> Result<()> {
184        self.config.custom_env.insert(key.to_string(), value.to_string());
185        self.save_config()?;
186        println!("๐Ÿ”ง Set environment variable: {}={}", key.cyan(), value);
187        Ok(())
188    }
189    pub fn status(&self) {
190        println!("{}", "=== Mutiny Mode Status ===".red().bold());
191        if self.active {
192            println!("Status: {} ACTIVE", "๐Ÿดโ€โ˜ ๏ธ".red());
193        } else {
194            println!("Status: {} Inactive", "๐Ÿšข".green());
195        }
196        if !self.config.overrides.is_empty() {
197            println!("\n๐Ÿ“‹ Active Overrides:");
198            for (name, override_config) in &self.config.overrides {
199                if override_config.enabled {
200                    println!("   {} - {}", name.yellow(), override_config.reason);
201                    if let Some(expires) = override_config.expires {
202                        let remaining = expires - chrono::Utc::now();
203                        println!(
204                            "      Expires in: {} minutes", remaining.num_minutes()
205                        );
206                    }
207                }
208            }
209        }
210        if !self.config.force_flags.is_empty() {
211            println!("\n๐Ÿšฉ Forced Flags:");
212            for flag in &self.config.force_flags {
213                println!("   {}", flag.cyan());
214            }
215        }
216        if !self.config.skip_checks.is_empty() {
217            println!("\nโญ๏ธ  Skipped Checks:");
218            for check in &self.config.skip_checks {
219                println!("   {}", check.yellow());
220            }
221        }
222        if !self.config.custom_env.is_empty() {
223            println!("\n๐Ÿ”ง Custom Environment:");
224            for (key, value) in &self.config.custom_env {
225                println!("   {}={}", key.cyan(), value);
226            }
227        }
228        if self.config.allow_dirty {
229            println!("\nโš ๏ธ  Allowing dirty repository");
230        }
231        if self.config.ignore_lockfile {
232            println!("โš ๏ธ  Ignoring Cargo.lock");
233        }
234    }
235    pub fn clean_expired(&mut self) -> Result<()> {
236        let now = chrono::Utc::now();
237        let mut expired = Vec::new();
238        for (name, override_config) in &self.config.overrides {
239            if let Some(expires) = override_config.expires {
240                if expires < now {
241                    expired.push(name.clone());
242                }
243            }
244        }
245        for name in expired {
246            self.config.overrides.remove(&name);
247            println!("๐Ÿงน Cleaned expired override: {}", name);
248        }
249        self.save_config()?;
250        Ok(())
251    }
252    pub fn reset(&mut self) -> Result<()> {
253        self.config = MutinyConfig::default();
254        self.active = false;
255        self.save_config()?;
256        println!("๐Ÿ”„ Mutiny Mode configuration reset to defaults");
257        Ok(())
258    }
259    fn save_config(&self) -> Result<()> {
260        let toml = toml::to_string_pretty(&self.config)?;
261        fs::write(&self.config_file, toml)?;
262        Ok(())
263    }
264    fn log_activation(&self, reason: &str) -> Result<()> {
265        let log_file = dirs::home_dir()
266            .context("Could not find home directory")?
267            .join(".shipwreck")
268            .join("mutiny.log");
269        let entry = format!(
270            "[{}] Activated: {}\n", chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
271            reason
272        );
273        let mut file = fs::OpenOptions::new().create(true).append(true).open(log_file)?;
274        use std::io::Write;
275        file.write_all(entry.as_bytes())?;
276        Ok(())
277    }
278}
279impl Default for MutinyConfig {
280    fn default() -> Self {
281        Self {
282            overrides: HashMap::new(),
283            force_flags: Vec::new(),
284            skip_checks: Vec::new(),
285            custom_env: HashMap::new(),
286            allow_dirty: false,
287            ignore_lockfile: false,
288        }
289    }
290}
291pub struct MutinyGuard {
292    mode: MutinyMode,
293}
294impl MutinyGuard {
295    pub fn new(reason: &str) -> Result<Self> {
296        let mut mode = MutinyMode::new()?;
297        mode.activate(reason)?;
298        Ok(Self { mode })
299    }
300}
301impl Drop for MutinyGuard {
302    fn drop(&mut self) {
303        let _ = self.mode.deactivate();
304    }
305}
306pub fn check_helmsman_direction(command: &str) -> Result<bool> {
307    println!(
308        "๐Ÿงญ Helmsman checking course for command '{}' - steady as she goes!", command
309        .cyan()
310    );
311    let license_manager = license::LicenseManager::new();
312    match license_manager?.enforce_license(command) {
313        Ok(_) => {
314            println!(
315                "โœ… Helmsman reports: Command '{}' on correct heading!", command.green()
316            );
317            println!("   ๐Ÿงญ All systems aligned - ready to steer!");
318            Ok(true)
319        }
320        Err(e) => {
321            if e.to_string().contains("limit") {
322                println!("โš ๏ธ  Helmsman warning: Course deviation detected!");
323                println!("   ๐Ÿงญ Correct heading: https://cargo.do/checkout");
324                println!("   ๐Ÿงญ Adjust course for unlimited navigation");
325            } else if e.to_string().contains("License not found") {
326                println!("โŒ Helmsman emergency: No navigation coordinates!");
327                println!("   ๐Ÿงญ Plot course with 'cm register <key>'");
328            } else {
329                println!(
330                    "โŒ Helmsman distress: Course check failed: {}", e.to_string().red()
331                );
332                println!("   ๐Ÿงญ Man your stations - prepare to heave to!");
333            }
334            Ok(false)
335        }
336    }
337}