cargo-cooldown 0.1.0

Cargo wrapper that enforces a cooldown window for freshly published crates on crates.io for improved supply chain security.
mod allowlist;
mod cache;
mod config;
mod executor;
mod metadata;
mod registry;
mod resolver;

use std::process::Command;

use anyhow::Result;
use tracing::warn;
use tracing_subscriber::EnvFilter;

use crate::config::{Config, Mode};

fn sanitize_args(mut args: Vec<String>) -> Vec<String> {
    if matches!(args.first(), Some(first) if first == "cooldown") {
        args.remove(0);
    }
    args
}

fn collect_cargo_args() -> Vec<String> {
    let raw: Vec<String> = std::env::args().skip(1).collect();
    sanitize_args(raw)
}

fn init_logging(verbose: bool) {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
        if verbose {
            EnvFilter::new("cargo_cooldown=debug,cargo_cooldown::executor=debug,info")
        } else {
            EnvFilter::new("info")
        }
    });
    let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init();
}

#[tokio::main]
async fn main() -> Result<()> {
    let config = Config::from_env();
    init_logging(config.verbose);

    let args = collect_cargo_args();
    if args.is_empty() {
        eprintln!("Usage: cargo cooldown <cargo-command> [args...]");
        std::process::exit(2);
    }

    if matches!(args.first().map(String::as_str), Some("update")) {
        eprintln!(
            "cargo-cooldown is designed for commands like build, check, test, or run.\n\
             Running it with `cargo update` would replace the lockfile you just cooled down.\n\
             Invoke `cargo update` directly instead if you truly intend to refresh dependency versions."
        );
        std::process::exit(2);
    }

    if config.mode != Mode::Off && config.cooldown_minutes > 0 {
        match executor::run_pinning_flow(&config).await {
            Ok(_) => {}
            Err(err) => match config.mode {
                Mode::Warn => {
                    warn!(error = %err, "cooldown guard failed; continuing due to warn mode");
                }
                Mode::Enforce => {
                    return Err(err);
                }
                Mode::Off => {}
            },
        }
    }

    let status = Command::new("cargo").args(&args).status()?;
    std::process::exit(status.code().unwrap_or(1));
}

#[cfg(test)]
mod tests {
    use super::sanitize_args;

    #[test]
    fn strips_leading_cooldown_subcommand() {
        let args = vec!["cooldown".to_string(), "update".to_string()];
        let sanitized = sanitize_args(args);
        assert_eq!(sanitized, vec!["update".to_string()]);
    }

    #[test]
    fn keeps_regular_arguments() {
        let args = vec!["build".to_string(), "--release".to_string()];
        let sanitized = sanitize_args(args.clone());
        assert_eq!(sanitized, args);
    }
}