bat-cli 0.12.1

Blockchain Auditor Toolkit (BAT)
#[macro_use]
extern crate log;

extern crate confy;

use clap::{Parser, Subcommand};
use colored::Colorize;
use inflector::Inflector;

use crate::batbelt::metadata::BatMetadata;
use crate::batbelt::path::BatFile;
use crate::commands::miro_commands::MiroCommand;
use crate::commands::sonar_commands::SonarCommand;
use crate::commands::{BatCommandEnumerator, BatPackageJsonCommand, CommandResult};

use crate::batbelt::BatEnumerator;
use batbelt::git::git_action::GitAction;

use commands::co_commands::CodeOverhaulCommand;
use commands::CommandError;
use error_stack::fmt::{Charset, ColorMode};
use error_stack::{IntoReport, Result};
use error_stack::{Report, ResultExt};

use crate::commands::project_commands::ProjectCommands;
use crate::commands::tools_commands::ToolCommand;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;

// use crate::commands::analytics_commands::AnalyticsCommand;
use log4rs::Config;
use package::PackageCommand;
use regex::Regex;

pub mod batbelt;
pub mod commands;
pub mod config;
pub mod package;

// pub type BatDerive = #[derive(Debug, PartialEq, Copy, strum_macros::Display, strum_macros::EnumIter)];

#[derive(Parser, Debug, Clone)]
#[command(author, version, about = "Blockchain Auditor Toolkit (BAT) CLI")]
struct Cli {
    #[clap(flatten)]
    verbose: clap_verbosity_flag::Verbosity,
    #[command(subcommand)]
    command: BatCommands,
}

#[derive(
    Default, strum_macros::Display, Subcommand, Debug, PartialEq, Clone, strum_macros::EnumIter,
)]
enum BatCommands {
    /// Initialize a Bat project
    #[default]
    Init,
    /// Reload the Bat project files (ideal to resume work from git clone)
    Reload,
    /// code-overhaul files management
    #[command(subcommand)]
    CodeOverhaul(CodeOverhaulCommand),
    /// Execute the BatSonar to create metadata files for all Sonar result types
    Sonar,
    /// utils tools
    #[command(subcommand)]
    Tool(ToolCommand),
    /// Miro integration
    #[command(subcommand)]
    Miro(MiroCommand),
    /// Cargo publish operations, available only for dev
    #[command(subcommand)]
    Package(PackageCommand),
}

impl BatEnumerator for BatCommands {}

impl BatCommands {
    pub async fn execute(&self) -> Result<(), CommandError> {
        self.validate_command()?;
        match self {
            BatCommands::Init => ProjectCommands::Init.init_bat_project().await,
            BatCommands::Reload => ProjectCommands::Reload.execute_command(),
            BatCommands::CodeOverhaul(command) => command.execute_command().await,
            BatCommands::Sonar => SonarCommand::Run.execute_command(),
            BatCommands::Miro(command) => command.execute_command().await,
            BatCommands::Tool(command) => command.execute_command(),
            // only for dev
            #[cfg(debug_assertions)]
            BatCommands::Package(PackageCommand::Format) => {
                package::format().change_context(CommandError)
            }
            #[cfg(debug_assertions)]
            BatCommands::Package(PackageCommand::Release) => {
                package::release().change_context(CommandError)
            }
            #[cfg(not(debug_assertions))]
            BatCommands::Package(_) => {
                unimplemented!("Command only implemented for dev operations")
            }
        }
    }

    fn validate_command(&self) -> CommandResult<()> {
        let (check_metadata, check_branch) = match self {
            BatCommands::Init => {
                return Ok(());
            }
            BatCommands::Reload => {
                return Ok(());
            }
            BatCommands::Package(_) => {
                return Ok(());
            }
            BatCommands::Sonar => (
                SonarCommand::Run.check_metadata_is_initialized(),
                SonarCommand::Run.check_correct_branch(),
            ),
            BatCommands::Tool(command) => (
                command.check_metadata_is_initialized(),
                command.check_correct_branch(),
            ),
            BatCommands::CodeOverhaul(command) => (
                command.check_metadata_is_initialized(),
                command.check_correct_branch(),
            ),
            // BatCommands::Finding(command) => (
            //     command.check_metadata_is_initialized(),
            //     command.check_correct_branch(),
            // ),
            BatCommands::Miro(command) => (
                command.check_metadata_is_initialized(),
                command.check_correct_branch(),
            ),
            // BatCommands::Repository(command) => (
            //     command.check_metadata_is_initialized(),
            //     command.check_correct_branch(),
            // ),
            // BatCommands::Analytics(command) => (
            //     command.check_metadata_is_initialized(),
            //     command.check_correct_branch(),
            // ),
        };
        if check_metadata {
            BatMetadata::read_metadata()
                .change_context(CommandError)?
                .check_metadata_is_initialized()
                .change_context(CommandError)?;
        }

        if check_branch {
            GitAction::CheckCorrectBranch
                .execute_action()
                .change_context(CommandError)?;
        }
        Ok(())
    }

    pub fn get_bat_package_json_commands(
        project_type: &crate::config::ProjectType,
    ) -> Vec<BatPackageJsonCommand> {
        use crate::config::ProjectType;
        let _is_anchor = *project_type == ProjectType::Anchor;

        BatCommands::get_type_vec()
            .into_iter()
            .filter_map(|command| match command {
                // Anchor and Pinocchio commands
                BatCommands::CodeOverhaul(_)
                    if *project_type == ProjectType::Anchor
                        || *project_type == ProjectType::Pinocchio =>
                {
                    Some(CodeOverhaulCommand::get_bat_package_json_commands(
                        command.to_string().to_kebab_case(),
                    ))
                }
                // BatCommands::Analytics(_) if is_anchor => {
                //     Some(AnalyticsCommand::get_bat_package_json_commands(
                //         command.to_string().to_kebab_case(),
                //     ))
                // }
                // Universal commands
                // BatCommands::Finding(_) => Some(FindingCommand::get_bat_package_json_commands(
                //     command.to_string().to_kebab_case(),
                // )),
                BatCommands::Tool(_) => Some(ToolCommand::get_bat_package_json_commands(
                    command.to_string().to_kebab_case(),
                )),
                BatCommands::Miro(_) => Some(MiroCommand::get_bat_package_json_commands(
                    command.to_string().to_kebab_case(),
                )),
                // BatCommands::Repository(_) => {
                //     Some(RepositoryCommand::get_bat_package_json_commands(
                //         command.to_string().to_kebab_case(),
                //     ))
                // }
                BatCommands::Sonar => Some(SonarCommand::get_bat_package_json_commands(
                    command.to_string().to_kebab_case(),
                )),
                BatCommands::Reload => Some(BatPackageJsonCommand {
                    command_name: command.to_string().to_kebab_case(),
                    command_options: vec![],
                }),
                _ => None,
            })
            .collect::<Vec<_>>()
    }

    pub fn get_pretty_command(&self) -> CommandResult<String> {
        let multi_line_command_regex = Regex::new(r#"[\w]+(\([\w\s,]+\))+"#)
            .into_report()
            .change_context(CommandError)?;
        let command_string = format!("{self:#?}");
        if multi_line_command_regex.is_match(&command_string) {
            let mut command_string_lines = command_string.lines();
            let command_name = command_string_lines.next().unwrap().to_kebab_case();
            let command_option = command_string_lines.next().unwrap().trim().to_kebab_case();
            return Ok(format!("{} {}", command_name, command_option));
        }
        Ok(self.to_string().to_kebab_case())
    }
}

fn init_log(cli: Cli) -> CommandResult<()> {
    let bat_log_file = BatFile::Batlog;
    let logfile = FileAppender::builder()
        .encoder(Box::new(PatternEncoder::new(
            "{d(%Y-%m-%d %H:%M:%S)} [{f}:{L}] {h({l})} {M}{n}{m}{n}",
        )))
        .build(bat_log_file.get_path(false).change_context(CommandError)?)
        .into_report()
        .change_context(CommandError)?;

    let config = Config::builder()
        .appender(Appender::builder().build("logfile", Box::new(logfile)))
        .build(
            Root::builder()
                .appender("logfile")
                .build(cli.verbose.log_level_filter()),
        )
        .into_report()
        .change_context(CommandError)?;

    log4rs::init_config(config)
        .into_report()
        .change_context(CommandError)?;
    Ok(())
}

pub struct Suggestion(String);

impl Suggestion {
    pub fn set_report() {
        Report::set_charset(Charset::Utf8);
        Report::set_color_mode(ColorMode::Color);
        Report::install_debug_hook::<Self>(|Self(value), context| {
            context.push_body(format!("{}: {value}", "suggestion".yellow()))
        });
    }
}

/// If `Bat.toml` is not in the current directory but exists inside `bat-audit/`,
/// automatically change the working directory so all relative paths resolve correctly.
fn auto_detect_bat_audit_dir() {
    use std::path::Path;
    if Path::new("Bat.toml").exists() {
        return; // Already in the right directory
    }
    if Path::new("bat-audit/Bat.toml").exists() && std::env::set_current_dir("bat-audit").is_ok() {
        println!(
            "{} auto-detected bat-audit/ directory, changing working directory",
            "bat-cli".blue()
        );
    }
}

async fn run() -> CommandResult<()> {
    let cli: Cli = Cli::parse();

    Suggestion::set_report();
    // env_logger selectively
    match cli.command {
        BatCommands::Package(..) | BatCommands::Init => {
            env_logger::init();
            Ok(())
        }
        _ => {
            auto_detect_bat_audit_dir();
            init_log(cli.clone())
        }
    }?;

    cli.command.execute().await
}

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

    match run().await {
        Ok(_) => {
            println!(
                "{} {} script successfully executed!",
                "bat-cli".green(),
                cli.command.get_pretty_command()?.green()
            );
            Ok(())
        }
        Err(error) => {
            eprintln!(
                "{} {} script finished with error",
                "bat-cli".red(),
                cli.command.get_pretty_command()?.red()
            );
            Err(error)
        }
    }
}