use crate::cli::commands::build::BuildArgs;
use crate::cli::commands::config::ConfigCommand;
use crate::cli::commands::create::CreateArgs;
use crate::cli::commands::metadata::MetadataCommand;
use crate::cli::commands::publish;
use crate::cli::commands::publish::PublishArgs;
use crate::cli::commands::template::TemplateCommand;
use crate::{
cli::{
commands::{build, config as config_cmd, create, metadata, template, wizard},
config::{Config, TemplateRepository},
util,
},
git::repository::GitRepository,
loading,
};
use anyhow::anyhow;
use clap::{
Parser, Subcommand,
builder::{Styles, styling::AnsiColor},
};
use convert_case::{Case, Casing};
use std::{env, path::PathBuf};
const DEFAULT_DATA_FOLDER_NAME: &str = "tari_cli";
const TEMPLATE_REPOS_FOLDER_NAME: &str = "template_repositories";
const DEFAULT_CONFIG_FILE_NAME: &str = "tari.config.toml";
pub fn cli_styles() -> Styles {
Styles::styled()
.header(AnsiColor::BrightMagenta.on_default())
.usage(AnsiColor::BrightMagenta.on_default())
.literal(AnsiColor::BrightGreen.on_default())
.placeholder(AnsiColor::BrightMagenta.on_default())
.error(AnsiColor::BrightRed.on_default())
.invalid(AnsiColor::BrightRed.on_default())
.valid(AnsiColor::BrightGreen.on_default())
}
pub fn default_base_dir() -> PathBuf {
dirs_next::data_dir()
.unwrap_or_else(|| env::current_dir().unwrap())
.join(DEFAULT_DATA_FOLDER_NAME)
}
pub fn default_output_dir() -> PathBuf {
env::current_dir().unwrap()
}
pub fn default_config_file() -> PathBuf {
dirs_next::config_dir()
.unwrap_or_else(|| env::current_dir().unwrap())
.join(DEFAULT_DATA_FOLDER_NAME)
.join(DEFAULT_CONFIG_FILE_NAME)
}
pub fn config_override_parser(config_override: &str) -> Result<ConfigOverride, String> {
if config_override.is_empty() {
return Err(String::from("Override cannot be empty!"));
}
let split: Vec<&str> = config_override.split("=").collect();
if split.len() != 2 {
return Err(String::from("Invalid override!"));
}
let (key, value) = (split.first().unwrap(), split.get(1).unwrap());
if !Config::is_override_key_valid(key) {
return Err(format!("Override key invalid: {key}"));
}
Ok(ConfigOverride {
key: key.to_string(),
value: value.to_string(),
})
}
pub fn project_name_parser(project_name: &str) -> Result<String, String> {
Ok(project_name.to_case(Case::Snake))
}
#[derive(Clone, Debug)]
pub struct ConfigOverride {
pub key: String,
pub value: String,
}
#[derive(Clone, Parser, Debug)]
pub struct CommonArguments {
#[arg(short = 'b', long, value_name = "PATH", default_value = default_base_dir().into_os_string())]
base_dir: PathBuf,
#[arg(short = 'c', long, value_name = "PATH", default_value = default_config_file().into_os_string())]
config_file_path: PathBuf,
#[arg(short = 'e', long, value_name = "KEY=VALUE", value_parser = config_override_parser)]
config_overrides: Vec<ConfigOverride>,
}
#[derive(Clone, Parser)]
#[command(styles = cli_styles())]
#[command(
version,
about = "🚀 Tari CLI 🚀",
long_about = "🚀 Tari Ootle CLI 🚀\nDevelop and publish Tari templates."
)]
pub struct Cli {
#[clap(flatten)]
args: CommonArguments,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Clone, Subcommand)]
pub enum Command {
#[clap(alias = "new")]
Create {
#[clap(flatten)]
args: CreateArgs,
},
Build {
#[clap(flatten)]
args: BuildArgs,
},
#[clap(alias = "deploy")]
Publish {
#[clap(flatten)]
args: PublishArgs,
},
Template {
#[command(subcommand)]
command: TemplateCommand,
},
Metadata {
#[command(subcommand)]
command: MetadataCommand,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
}
impl Cli {
async fn init_base_dir_and_config(&self) -> anyhow::Result<Config> {
util::create_dir(&self.args.base_dir).await?;
util::create_dir(
&self
.args
.config_file_path
.parent()
.ok_or(anyhow!("Can't find folder of configuration file!"))?
.to_path_buf(),
)
.await?;
let path = &self.args.config_file_path;
let mut config = if !util::file_exists(path).await? {
println!("Existing config not found. Creating a new config at {}", path.display());
let cfg = Config::default();
cfg.write_to_file(path).await?;
cfg
} else {
match Config::open(path).await {
Ok(cfg) => cfg,
Err(error) => {
println!("Failed to open config file: {error:?}, creating default...");
let cfg = Config::default();
cfg.write_to_file(path).await?;
cfg
},
}
};
for config_override in &self.args.config_overrides {
config.override_data(config_override.key.as_str(), config_override.value.as_str())?;
}
Ok(config)
}
async fn refresh_template_repository(&self, template_repo: &TemplateRepository) -> anyhow::Result<GitRepository> {
util::create_dir(&self.args.base_dir.join(TEMPLATE_REPOS_FOLDER_NAME)).await?;
let repo_url_splitted: Vec<&str> = template_repo.url.split("/").collect();
let repo_name = repo_url_splitted
.last()
.ok_or(anyhow!("Failed to get repository name from URL!"))?;
let repo_user = repo_url_splitted
.get(repo_url_splitted.len() - 2)
.ok_or(anyhow!("Failed to get repository owner from URL!"))?;
let repo_folder_path = self
.args
.base_dir
.join(TEMPLATE_REPOS_FOLDER_NAME)
.join(repo_user)
.join(repo_name);
let mut repo = GitRepository::new(repo_folder_path.clone());
if util::dir_exists(&repo_folder_path).await? {
repo.load()?;
let current_branch = repo.current_branch_name()?;
if current_branch != template_repo.branch {
repo.pull_changes(Some(template_repo.branch.clone()))?;
} else {
repo.pull_changes(None)?;
}
} else {
repo.clone_and_checkout(template_repo.url.as_str(), template_repo.branch.as_str())?;
}
Ok(repo)
}
pub async fn handle_command(mut self) -> anyhow::Result<()> {
let Some(command) = self.command.take() else {
return wizard::handle().await;
};
if let Command::Config { command } = command {
return config_cmd::handle(command).await;
}
if let Command::Build { args } = command {
return build::handle(args).await;
}
if let Command::Metadata {
command: MetadataCommand::Inspect { args },
} = command
{
return template::inspect_metadata::handle(args).await;
}
let config = loading!(
"Init configuration and directories",
self.init_base_dir_and_config().await
)?;
match &command {
Command::Template { .. } | Command::Publish { .. } | Command::Metadata { .. } => {
return match command {
Command::Template { command } => match command {
TemplateCommand::Init { args } => template::init_metadata::handle(args).await,
TemplateCommand::Inspect { args } => template::inspect_metadata::handle(args).await,
TemplateCommand::Publish { args } => template::publish::handle(config, args).await,
},
Command::Publish { args } => publish::handle(config, args).await,
Command::Metadata { command } => match command {
MetadataCommand::Publish { args } => metadata::publish::handle(config, args).await,
MetadataCommand::Inspect { .. } => unreachable!(),
},
_ => unreachable!(),
};
},
_ => {},
}
let template_repo = loading!(
"Refresh templates repository",
self.refresh_template_repository(&config.template_repository).await
)?;
match command {
Command::Create { args } => create::handle(config, template_repo.local_folder().clone(), args).await,
_ => unreachable!(),
}
}
}