vampus 0.4.5

A CLI tool for automated semantic version management across configurable file patterns.
use std::{env, str::FromStr};

use clap::{CommandFactory, Parser};
use tracing::{debug, error};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};

mod cli;
mod config;
mod utils;
use cli::{Cli, Commands, VersionArgs};
use config::Config;
use utils::{
    Operation, apply_replacement, calculate_version, get_config_path, get_version_change,
    simulate_replacement, wrap_search_pattern,
};

#[tokio::main]
async fn main() {
    let cli = Cli::parse();

    let log_filter_str = if cli.debug {
        "debug".to_string()
    } else {
        env::var("RUST_LOG").unwrap_or("ERROR".to_string())
    };

    tracing_subscriber::registry()
        .with(
            EnvFilter::from_str(&log_filter_str)
                .unwrap_or_else(|_| EnvFilter::from_str("error").unwrap()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    debug!("log_level: {}", log_filter_str);

    match &cli.command {
        Commands::Upgrade(args) => {
            execute_version_change(args, Operation::Increment).await;
        }
        Commands::Downgrade(args) => {
            execute_version_change(args, Operation::Decrement).await;
        }
        Commands::Preview(args) => {
            let change_type = get_version_change(args);
            let config_path = get_config_path().await;

            match Config::read(&config_path).await {
                Some(config) => {
                    match calculate_version(
                        &config.current_version,
                        change_type,
                        Operation::Increment,
                    ) {
                        Ok(new_version) => {
                            println!("Current version: {}", config.current_version);
                            println!("Preview version (Increment): {}", new_version);
                        }
                        Err(e) => {
                            error!("Error calculating the version: {}", e);
                        }
                    }
                }
                None => error!("Failed to read config file at {}", config_path.display()),
            }
        }
        Commands::Show => {
            let config_path = get_config_path().await;
            match Config::read(&config_path).await {
                Some(config) => {
                    println!("{}", config.current_version);
                }
                None => error!("Failed to read config file at {}", config_path.display()),
            }
        }
        Commands::Init => {
            let config_path = get_config_path().await;

            if config_path.exists() {
                error!(
                    "Configuration file already exists at {}",
                    config_path.display()
                );
                return;
            }

            if let Err(e) = Config::write_default(&config_path).await {
                error!("Failed to write default config: {}", e);
                return;
            }
            println!(
                "✅ Created default configuration file: {}",
                config_path.display()
            );
        }
        Commands::Completions(args) => {
            let mut cmd = Cli::command();
            let name = cmd.get_name().to_string();
            clap_complete::generate(args.shell, &mut cmd, name, &mut std::io::stdout());
        }
    }
}

async fn execute_version_change(args: &VersionArgs, operation: Operation) {
    let change_type = get_version_change(args);
    let config_path = get_config_path().await;

    let Some(mut config) = Config::read(&config_path).await else {
        error!("Failed to read config file at {}", config_path.display());
        return;
    };

    let new_version = match calculate_version(&config.current_version, change_type, operation) {
        Ok(v) => v,
        Err(e) => {
            error!("Error calculating the version: {}", e);
            return;
        }
    };

    println!("Current version: {}", config.current_version);
    println!("New version: {}", new_version);

    let replacement_to = format!("${{1}}{}${{2}}", new_version);

    let mut modified_files = Vec::new();
    let mut all_files_verified = true;

    println!("-- Verifying and simulating changes... --");

    for replace in &config.replaces {
        let wrapped_search = wrap_search_pattern(replace.pattern.as_str());
        debug!("Wrapped search pattern: {}", wrapped_search);

        let pattern_from = format!(
            "(?m){}",
            wrapped_search.replace(
                "{{current_version}}",
                &config.current_version.replace('.', "\\.")
            )
        );
        debug!("Pattern FROM: {}", pattern_from);

        let pattern_to = format!(
            "(?m){}",
            wrapped_search.replace("{{current_version}}", &new_version.replace('.', "\\."))
        );
        debug!("Pattern TO: {}", pattern_to);

        match simulate_replacement(
            replace.file.as_str(),
            &pattern_from,
            &replacement_to,
            &pattern_to,
        )
        .await
        {
            Ok(content) => {
                modified_files.push((replace.file.clone(), content));
            }
            Err(e) => {
                error!(File = %replace.file, "Simulation failure: {}", e);
                all_files_verified = false;
                break;
            }
        }
    }

    if all_files_verified {
        println!("-- Applying changes... --");

        for (file_path, content) in modified_files {
            match apply_replacement(file_path.as_str(), &content).await {
                Ok(_) => {
                    println!("✅ Updated: {}", file_path);
                }
                Err(e) => {
                    error!(File = %file_path, "Write failure: {}", e);
                }
            }
        }

        config.current_version = new_version;
        if let Err(e) = config.write(&config_path).await {
            error!("Failed to write config file: {}", e);
        } else {
            println!(
                "\n🎉 Success: Config version updated to {}",
                config.current_version
            );
        }
    } else {
        error!("Aborted. No changes were written to files.");
    }
}