guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use anyhow::Result;
use clap::{Parser, Subcommand};
use supercli::clap::create_help_styles;

/// Format version string with git SHA
fn format_version() -> &'static str {
    const VERSION: &str = env!("CARGO_PKG_VERSION");
    const GIT_SHA: &str = env!("GIT_SHA");

    // Use lazy_static or just leak the string since it's used for the entire program lifetime
    Box::leak(format!("{} ({})", VERSION, GIT_SHA).into_boxed_str())
}

pub mod config;
pub mod hooks;
pub mod scan;
pub mod status;
pub mod sync;
pub mod version;

#[derive(Parser)]
#[command(
    name = "guardy",
    version = format_version(),
    about = "Fast, secure git hooks in Rust with secret scanning and file synchronization",
    long_about = "Guardy provides native Rust implementations of git hooks with security scanning \
                  and protected file synchronization across repositories.",
    styles = create_help_styles()
)]
pub struct Cli {
    /// Run as if started in `<DIR>` instead of current working directory
    #[arg(short = 'C', long = "directory", global = true)]
    pub directory: Option<String>,

    /// Increase verbosity (can be repeated)
    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
    pub verbose: u8,

    /// Suppress non-error output
    #[arg(short, long, global = true)]
    pub quiet: bool,

    /// Use custom configuration file
    #[arg(long, global = true)]
    pub config: Option<String>,

    /// Enable/disable recursive config loading from parent directories  
    #[arg(long, global = true)]
    pub recursive_config: Option<bool>,

    #[command(subcommand)]
    pub command: Option<Commands>,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Git hooks management
    Hooks(hooks::HooksArgs),
    /// Scan files or directories for secrets
    Scan(scan::ScanArgs),
    /// Configuration management
    Config(config::ConfigArgs),
    /// Show current installation and configuration status
    Status(status::StatusArgs),
    /// Protected file synchronization
    Sync(sync::SyncArgs),
    /// Show version information
    Version(version::VersionArgs),
}

impl Cli {
    pub async fn run(&self) -> Result<()> {
        // Change directory if specified
        if let Some(dir) = &self.directory {
            std::env::set_current_dir(dir)?;
        }

        // Set up logging early (before any CONFIG access) using CLI args and env vars
        let early_verbose = detect_early_verbosity(self);
        setup_logging(early_verbose, self.quiet);

        match &self.command {
            Some(Commands::Hooks(args)) => hooks::execute(args.clone()).await,
            Some(Commands::Scan(args)) => {
                // Get verbose level from config (includes env var precedence)
                scan::execute(args.clone(), crate::config::CONFIG.general.verbose).await
            }
            Some(Commands::Config(args)) => {
                config::execute(args.clone(), self.config.as_deref(), self.verbose).await
            }
            Some(Commands::Status(args)) => status::execute(args.clone()).await,
            Some(Commands::Sync(args)) => sync::execute(args.clone()).await,
            Some(Commands::Version(args)) => version::execute(args.clone()).await,
            None => {
                // Default behavior - show status if in git repo, otherwise show help
                if crate::git::GitRepo::discover().is_ok() {
                    status::execute(status::StatusArgs::default()).await
                } else {
                    // TODO: Implement proper help display using clap's help system
                    println!("Run 'guardy --help' for usage information");
                    Ok(())
                }
            }
        }
    }
}

/// Detect verbosity level early without accessing CONFIG
/// Checks CLI args first, then GUARDY_VERBOSE env var
fn detect_early_verbosity(cli: &Cli) -> u8 {
    // CLI args take highest priority
    if cli.verbose > 0 {
        return cli.verbose;
    }

    // Check GUARDY_VERBOSE env var as fallback
    if let Ok(env_verbose) = std::env::var("GUARDY_VERBOSE")
        && let Ok(val) = env_verbose.parse::<u8>()
    {
        return val;
    }

    // Default to 0 (warn level)
    0
}

fn setup_logging(verbose: u8, quiet: bool) {
    if quiet {
        return;
    }

    // Create filter that suppresses debug from ignore/globset crates appropriately
    let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
        match verbose {
            0 => tracing_subscriber::EnvFilter::new("warn"),
            1 => tracing_subscriber::EnvFilter::new("info,ignore=warn,globset=warn"),
            2 => tracing_subscriber::EnvFilter::new("debug,ignore=warn,globset=warn"),
            _ => tracing_subscriber::EnvFilter::new("trace"), /* -vvv shows everything including
                                                               * globset */
        }
    });

    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .with_target(false)
        .with_file(true)
        .with_line_number(true)
        .compact()
        .init();
}