#[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 log4rs::Config;
use package::PackageCommand;
use regex::Regex;
pub mod batbelt;
pub mod commands;
pub mod config;
pub mod package;
#[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 {
#[default]
Init,
Reload,
#[command(subcommand)]
CodeOverhaul(CodeOverhaulCommand),
Sonar,
#[command(subcommand)]
Tool(ToolCommand),
#[command(subcommand)]
Miro(MiroCommand),
#[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(),
#[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::Miro(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 {
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::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::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()))
});
}
}
fn auto_detect_bat_audit_dir() {
use std::path::Path;
if Path::new("Bat.toml").exists() {
return; }
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();
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)
}
}
}