use crate::cli::commands::build::BuildArgs;
use crate::cli::commands::config::ConfigCommand;
use crate::cli::commands::create::CreateArgs;
use crate::cli::commands::init::InitArgs;
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, init, 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};
use tari_ootle_common_types::Network;
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 Some((key, value)) = config_override.split_once('=') else {
return Err(String::from("Invalid override! Expected KEY=VALUE."));
};
if !Config::is_override_key_valid(key) {
return Err(format!("Override key invalid: {key}"));
}
Ok(ConfigOverride {
key: key.to_string(),
value: value.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn override_parser_accepts_nested_network_keys() {
let ov = config_override_parser("networks.esmeralda.wallet-daemon-url=http://localhost:5100/")
.expect("nested network key should parse");
assert_eq!(ov.key, "networks.esmeralda.wallet-daemon-url");
assert_eq!(ov.value, "http://localhost:5100/");
}
#[test]
fn override_parser_rejects_unknown_network() {
assert!(config_override_parser("networks.bogus.wallet-daemon-url=http://x").is_err());
}
#[test]
fn override_parser_rejects_unknown_field() {
assert!(config_override_parser("networks.esmeralda.bogus=http://x").is_err());
}
#[test]
fn override_parser_keeps_value_with_equals() {
let ov = config_override_parser("default_account=acc=ount").expect("split_once should keep tail intact");
assert_eq!(ov.value, "acc=ount");
}
}
pub fn project_name_parser(project_name: &str) -> Result<String, String> {
Ok(project_name.to_case(Case::Snake))
}
fn parse_network(s: &str) -> Result<Network, String> {
s.parse()
.map_err(|e: tari_ootle_common_types::NetworkParseError| e.to_string())
}
#[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>,
#[arg(short = 'n', long, value_name = "NETWORK", value_parser = parse_network, global = true)]
network: Option<Network>,
}
#[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 {
Init {
#[clap(flatten)]
args: InitArgs,
},
#[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::Init { args } = command {
return init::handle(args).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 { .. } => {
let network_override = self.args.network;
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, network_override, args).await
},
},
Command::Publish { args } => publish::handle(config, network_override, args).await,
Command::Metadata { command } => match command {
MetadataCommand::Publish { args } => {
metadata::publish::handle(config, network_override, 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!(),
}
}
}