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}