Skip to main content

cargo_mate/captain/
version.rs

1use anyhow::Result;
2use colored::*;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6use std::io::IsTerminal;
7#[derive(Debug, Serialize, Deserialize, Clone)]
8pub struct VersionConfig {
9    pub auto_increment: bool,
10    pub version_file: String,
11    pub current_version: String,
12    pub increment_policy: IncrementPolicy,
13    pub version_format: VersionFormat,
14}
15#[derive(Debug, Serialize, Deserialize, Clone)]
16pub enum IncrementPolicy {
17    Patch,
18    Minor,
19    Major,
20    Custom(String),
21}
22#[derive(Debug, Serialize, Deserialize, Clone)]
23pub enum VersionFormat {
24    Semantic,
25    Date,
26    BuildNumber,
27    Custom(String),
28}
29impl Default for VersionConfig {
30    fn default() -> Self {
31        Self {
32            auto_increment: true,
33            version_file: ".v".to_string(),
34            current_version: "1.0.0".to_string(),
35            increment_policy: IncrementPolicy::Patch,
36            version_format: VersionFormat::Semantic,
37        }
38    }
39}
40pub struct VersionManager {
41    pub config: VersionConfig,
42    project_root: PathBuf,
43}
44impl VersionManager {
45    pub fn new(project_root: Option<PathBuf>) -> Result<Self> {
46        let project_root = project_root
47            .unwrap_or_else(|| {
48                std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
49            });
50        let version_file = project_root.join(".v");
51        let config = if version_file.exists() {
52            let content = fs::read_to_string(&version_file)?;
53            toml::from_str(&content)?
54        } else {
55            VersionConfig::default()
56        };
57        Ok(Self { config, project_root })
58    }
59    pub fn init(&mut self, initial_version: Option<String>) -> Result<()> {
60        let version = initial_version.clone().unwrap_or_else(|| "1.0.0".to_string());
61        let is_interactive = std::io::stdin().is_terminal()
62            && std::io::stderr().is_terminal();
63        if initial_version.is_none() {
64            if is_interactive {
65                println!("🚢 Setting up auto-versioning for your project");
66                println!("Enter initial version number (default: 1.0.0):");
67                let mut input = String::new();
68                std::io::stdin().read_line(&mut input)?;
69                let input = input.trim();
70                if !input.is_empty() {
71                    self.config.current_version = input.to_string();
72                } else {
73                    self.config.current_version = version;
74                }
75            } else {
76                println!(
77                    "🚢 Setting up auto-versioning for your project (non-interactive mode)"
78                );
79                self.config.current_version = version;
80            }
81        } else {
82            self.config.current_version = version;
83        }
84        if is_interactive {
85            println!("Enable auto-increment on build/check operations? (Y/n):");
86            let mut input = String::new();
87            std::io::stdin().read_line(&mut input)?;
88            let input = input.trim().to_lowercase();
89            self.config.auto_increment = input.is_empty() || input == "y"
90                || input == "yes";
91        } else {
92            println!("🚢 Auto-increment disabled (non-interactive mode)");
93            self.config.auto_increment = false;
94        }
95        if is_interactive {
96            println!("Choose increment policy:");
97            println!("1. Patch (1.0.0 -> 1.0.1) - default");
98            println!("2. Minor (1.0.0 -> 1.1.0)");
99            println!("3. Major (1.0.0 -> 2.0.0)");
100            println!("4. Custom");
101            let mut input = String::new();
102            std::io::stdin().read_line(&mut input)?;
103            let input = input.trim();
104            self.config.increment_policy = match input {
105                "2" => IncrementPolicy::Minor,
106                "3" => IncrementPolicy::Major,
107                "4" => {
108                    println!(
109                        "Enter custom increment command (e.g., 'echo $((patch + 1))'):"
110                    );
111                    let mut custom = String::new();
112                    std::io::stdin().read_line(&mut custom)?;
113                    IncrementPolicy::Custom(custom.trim().to_string())
114                }
115                _ => IncrementPolicy::Patch,
116            };
117        } else {
118            println!("🚢 Using patch increment policy (non-interactive mode)");
119            self.config.increment_policy = IncrementPolicy::Patch;
120        }
121        self.save_config()?;
122        println!("āœ… Versioning initialized: {}", self.config.current_version.cyan());
123        Ok(())
124    }
125    pub fn current_version(&self) -> &str {
126        &self.config.current_version
127    }
128    pub fn increment(&mut self) -> Result<String> {
129        let new_version = match &self.config.increment_policy {
130            IncrementPolicy::Patch => self.increment_patch()?,
131            IncrementPolicy::Minor => self.increment_minor()?,
132            IncrementPolicy::Major => self.increment_major()?,
133            IncrementPolicy::Custom(cmd) => self.execute_custom_increment(cmd)?,
134        };
135        self.config.current_version = new_version.clone();
136        self.save_config()?;
137        Ok(new_version)
138    }
139    pub fn auto_increment(&mut self) -> Result<Option<String>> {
140        if !self.config.auto_increment {
141            return Ok(None);
142        }
143        let new_version = self.increment()?;
144        Ok(Some(new_version))
145    }
146    pub fn set_version(&mut self, version: &str) -> Result<()> {
147        self.config.current_version = version.to_string();
148        self.save_config()?;
149        println!("āœ… Version set to: {}", version.cyan());
150        Ok(())
151    }
152    pub fn show_info(&self) {
153        println!("🚢 Project Version Information");
154        println!("Current version: {}", self.config.current_version.cyan());
155        println!(
156            "Auto-increment: {}", if self.config.auto_increment { "enabled".green() }
157            else { "disabled".red() }
158        );
159        println!("Increment policy: {:?}", self.config.increment_policy);
160        println!("Version file: {}", self.config.version_file.cyan());
161    }
162    pub fn show_history(&self) -> Result<()> {
163        println!("šŸ“š Version History");
164        let history_file = self.project_root.join("VERSION_HISTORY.md");
165        if history_file.exists() {
166            let content = fs::read_to_string(&history_file)?;
167            println!("{}", content);
168        } else {
169            println!("No version history file found.");
170            println!("To create version history, run: {}", "cm version init".cyan());
171        }
172        println!("\nšŸ“Œ Current Status:");
173        println!("Version: {}", self.config.current_version.cyan());
174        println!(
175            "Auto-increment: {}", if self.config.auto_increment { "enabled".green() }
176            else { "disabled".red() }
177        );
178        let version_file = self.project_root.join(&self.config.version_file);
179        if version_file.exists() {
180            println!("\nšŸ“ Version Configuration:");
181            let content = fs::read_to_string(&version_file)?;
182            println!("{}", content);
183        }
184        Ok(())
185    }
186    pub fn update_cargo_toml(&self) -> Result<()> {
187        let cargo_toml = self.project_root.join("Cargo.toml");
188        if !cargo_toml.exists() {
189            return Ok(());
190        }
191        let content = fs::read_to_string(&cargo_toml)?;
192        let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
193        let mut updated = false;
194        let mut in_package_section = false;
195        for line in &mut lines {
196            if line.trim().starts_with("[package]") {
197                in_package_section = true;
198            } else if line.trim().starts_with("[")
199                && !line.trim().starts_with("[package]")
200            {
201                in_package_section = false;
202            }
203            if in_package_section && line.trim().starts_with("version =") {
204                let trimmed = line.trim();
205                if trimmed.contains("\"") {
206                    let start = trimmed.find("\"").unwrap();
207                    let end = trimmed.rfind("\"").unwrap();
208                    *line = format!(
209                        "{}{}{}", & trimmed[..= start], self.config.current_version, &
210                        trimmed[end..]
211                    );
212                } else {
213                    *line = format!("version = \"{}\"", self.config.current_version);
214                }
215                updated = true;
216                break;
217            }
218        }
219        if updated {
220            fs::write(cargo_toml, lines.join("\n"))?;
221            println!(
222                "āœ… Updated Cargo.toml version to {}", self.config.current_version
223                .cyan()
224            );
225        } else {
226            println!(
227                "āš ļø  Could not find version field in [package] section of Cargo.toml"
228            );
229        }
230        Ok(())
231    }
232    pub fn get_display_version(&self) -> String {
233        format!("v{}", self.config.current_version)
234    }
235    pub fn save_config(&self) -> Result<()> {
236        let version_file = self.project_root.join(&self.config.version_file);
237        let content = toml::to_string_pretty(&self.config)?;
238        fs::write(version_file, content)?;
239        Ok(())
240    }
241    fn increment_patch(&self) -> Result<String> {
242        let parts: Vec<&str> = self.config.current_version.split('.').collect();
243        if parts.len() < 3 {
244            return Err(
245                anyhow::anyhow!(
246                    "Invalid version format: {}", self.config.current_version
247                ),
248            );
249        }
250        let major: u32 = parts[0].parse()?;
251        let minor: u32 = parts[1].parse()?;
252        let patch: u32 = parts[2].parse()?;
253        if patch >= 99 {
254            let new_minor = minor + 1;
255            let new_version = format!("{}.{}.0", major, new_minor);
256            Ok(new_version)
257        } else {
258            let new_patch = patch + 1;
259            let new_version = format!("{}.{}.{}", major, minor, new_patch);
260            Ok(new_version)
261        }
262    }
263    fn increment_minor(&self) -> Result<String> {
264        let parts: Vec<&str> = self.config.current_version.split('.').collect();
265        if parts.len() < 3 {
266            return Err(
267                anyhow::anyhow!(
268                    "Invalid version format: {}", self.config.current_version
269                ),
270            );
271        }
272        let major: u32 = parts[0].parse()?;
273        let minor: u32 = parts[1].parse()?;
274        if minor >= 99 {
275            let new_major = major + 1;
276            let new_version = format!("{}.0.0", new_major);
277            Ok(new_version)
278        } else {
279            let new_minor = minor + 1;
280            let new_version = format!("{}.{}.0", major, new_minor);
281            Ok(new_version)
282        }
283    }
284    fn increment_major(&self) -> Result<String> {
285        let parts: Vec<&str> = self.config.current_version.split('.').collect();
286        if parts.len() < 1 {
287            return Err(
288                anyhow::anyhow!(
289                    "Invalid version format: {}", self.config.current_version
290                ),
291            );
292        }
293        let major: u32 = parts[0].parse()?;
294        if major >= 99 {
295            let new_version = "1.0.0".to_string();
296            Ok(new_version)
297        } else {
298            let new_major = major + 1;
299            let new_version = format!("{}.0.0", new_major);
300            Ok(new_version)
301        }
302    }
303    fn execute_custom_increment(&self, command: &str) -> Result<String> {
304        use std::process::Command;
305        let output = Command::new("sh")
306            .arg("-c")
307            .arg(command)
308            .current_dir(&self.project_root)
309            .output()?;
310        if !output.status.success() {
311            return Err(
312                anyhow::anyhow!(
313                    "Custom increment command failed: {}", String::from_utf8_lossy(&
314                    output.stderr)
315                ),
316            );
317        }
318        let new_version = String::from_utf8_lossy(&output.stdout).trim().to_string();
319        if new_version.is_empty() {
320            return Err(
321                anyhow::anyhow!("Custom increment command returned empty result"),
322            );
323        }
324        Ok(new_version)
325    }
326}
327pub fn pre_operation_hook(project_root: Option<PathBuf>) -> Result<()> {
328    let mut version_manager = VersionManager::new(project_root)?;
329    if let Some(new_version) = version_manager.auto_increment()? {
330        println!("🚢 Auto-incremented version to: {}", new_version.cyan());
331        version_manager.update_cargo_toml()?;
332    }
333    Ok(())
334}
335pub fn post_operation_hook(project_root: Option<PathBuf>, success: bool) -> Result<()> {
336    if success {
337        let version_manager = VersionManager::new(project_root)?;
338        println!(
339            "🚢 Current version: {}", version_manager.get_display_version().cyan()
340        );
341    }
342    Ok(())
343}
344pub fn check_sea_legs(command: &str) -> Result<bool> {
345    println!(
346        "🦵 Testing sea legs for command '{}' - checking stability", command.cyan()
347    );
348    let license_manager = crate::license::LicenseManager::new()?;
349    match license_manager.enforce_license(command) {
350        Ok(_) => {
351            println!(
352                "āœ… Steady as she goes! Command '{}' has good sea legs!", command
353                .green()
354            );
355            println!("   🦵 This command is seaworthy and ready to sail!");
356            Ok(true)
357        }
358        Err(e) => {
359            if e.to_string().contains("limit") {
360                println!(
361                    "āš ļø  Seasick! Command quota exceeded - need more practice!"
362                );
363                println!("   🦵 Steady yourself: https://cargo.do/checkout");
364                println!("   🦵 Get your sea legs with Pro unlimited commands");
365            } else if e.to_string().contains("License not found") {
366                println!("āŒ Landlubber detected! No sailing papers found!");
367                println!("   🦵 Get your sea legs with 'cm register <key>'");
368            } else {
369                println!(
370                    "āŒ Rough seas! Stability check failed: {}", e.to_string().red()
371                );
372                println!("   🦵 Batten down the hatches and contact support");
373            }
374            Ok(false)
375        }
376    }
377}