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}