use std::io::Write;
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity};
use lazy_static::lazy_static;
use log::{debug, error, info};
use regex::Regex;
mod commands;
mod constants;
mod core;
mod files;
mod github;
mod models;
mod utils;
use crate::constants::*;
use crate::core::platform_info::{long_version, short_description};
use crate::core::selector::is_env_compatible;
use github::client::{get_asset, get_release};
use utils::semver::SemverStringConversion;
lazy_static! {
static ref REPO_REGEX: Regex = Regex::new(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$").unwrap();
}
fn validate_repo_format(s: &str) -> Result<String, String> {
if REPO_REGEX.is_match(s) {
Ok(s.to_string())
} else {
Err(format!(
"Repository must be in the format USERNAME/REPO, got: {}",
s
))
}
}
#[derive(Parser, Clone)]
struct UseArgs {
#[arg(required = true, value_parser = validate_repo_format)]
repo: String,
#[arg(required = true)]
version: String,
}
#[derive(Parser, Clone)]
struct CmdArgs {
#[arg(required = true, value_parser = validate_repo_format)]
repo: String,
#[arg(long, short)]
tag: Option<String>,
}
#[derive(Parser, Clone)]
struct UpdateArgs {
#[arg(value_parser = validate_repo_format, required_unless_present_any = ["all", "update_self"])]
repo: Option<String>,
#[arg(long, conflicts_with = "repo")]
all: bool,
#[arg(long = "self", conflicts_with = "repo")]
update_self: bool,
}
#[derive(Subcommand, Clone)]
enum Cmd {
Download(CmdArgs),
Install(CmdArgs),
List,
Use(UseArgs),
Update(UpdateArgs),
Enable,
Check,
Clean,
Info,
Version,
#[command(hide = true)]
Debug,
}
#[derive(Parser)]
#[command(
name = APP_NAME,
author = AUTHOR,
version = long_version(),
about = short_description(),
long_version = long_version(),
help_template = "\n\n{name} - {about}\n\n\
{usage-heading} {usage}\n\n\
{all-args}{after-help}",
after_help = format!("For more information, visit: {}\n\n\
If you encounter any issues, please report them at:\n{}/issues\n",
THIS_REPO_URL, THIS_REPO_URL),
)]
struct Cli {
#[command(subcommand)]
command: Cmd,
#[command(flatten)]
verbose: Verbosity<InfoLevel>, }
fn is_supported_os() -> bool {
cfg!(any(target_os = "linux", target_os = "macos"))
}
fn run() -> Result<()> {
if !is_supported_os() {
bail!(
"Sorry, {} is currently unsupported. Please open an issue at {}/issues to ask for support.",
std::env::consts::OS,
THIS_REPO_URL
);
}
let cli = Cli::parse();
env_logger::Builder::new()
.filter_level(cli.verbose.log_level_filter())
.format_timestamp(None)
.format_module_path(false)
.format_target(false)
.init();
match &cli.command {
Cmd::Download(args) => {
info!(
"Downloading {} {} to current dir",
&args.repo,
args.tag.as_deref().unwrap_or("(latest)")
);
let current_dir =
std::env::current_dir().context("Failed to determine current directory")?;
debug!("Working directory: {}", current_dir.display());
let release = get_release(&args.repo, args.tag.as_deref())
.with_context(|| format!("Failed to get release info for {}", args.repo))?;
let binary = get_asset(&release, is_env_compatible).with_context(|| {
format!(
"Failed to find compatible asset for release {}",
release.tag_name()
)
})?;
commands::download::download_binary(
binary.name(),
binary.browser_download_url(),
¤t_dir,
)?;
}
Cmd::Install(args) => {
info!(
"Installing {} {}",
&args.repo,
args.tag.as_deref().unwrap_or("(latest)")
);
commands::install::process_install(&args.repo, args.tag.as_deref())?;
}
Cmd::Use(args) => {
let version = &args.version;
info!(
"Setting version '{}' as default for {}",
version, &args.repo
);
if let Err(e) = commands::make_default::set_default(&args.repo, version) {
error!("Failed to set default version: {}", e);
std::process::exit(110);
}
info!("Version '{}' set as default.", version);
}
Cmd::List => {
let list = commands::list::list_installed_assets();
if list.is_empty() {
info!("No installed binaries found.");
} else {
let mut stdout = std::io::stdout().lock();
writeln!(stdout).unwrap();
writeln!(stdout, "{:<40} {:<15}", "Repository", "Versions").unwrap();
writeln!(stdout, "{:<40} {:<15}", "----------", "--------").unwrap();
for asset in list {
writeln!(
stdout,
"{:<40} {:?}",
asset.get_name(),
asset.get_versions().to_string_vec()
)
.unwrap();
}
writeln!(stdout).unwrap();
drop(stdout); }
}
Cmd::Update(args) => {
commands::update::process_update(args)?; }
Cmd::Check => {
commands::check::check_if_bin_in_path();
}
Cmd::Version => {
println!("{}", crate::core::platform_info::long_version());
}
Cmd::Info => {
commands::info::show_info();
}
Cmd::Debug => {
commands::info::show_info();
}
Cmd::Enable => {
commands::enable::run();
}
Cmd::Clean => {
commands::clean::run_clean()?;
}
}
Ok(())
}
fn main() -> Result<()> {
let result = run();
if let Err(e) = &result {
error!("Execution failed: {:?}", e);
}
result
}