use std::process::Command;
use anyhow::{bail, Context, Result};
use colored::Colorize;
use serde::Deserialize;
use verbosio::{set_verbosity, verbose};
use frate::installer::{install_package, install_packages, uninstall_package, uninstall_packages};
use frate::lock::FrateLock;
use frate::registry::fetch_registry;
use frate::{clean_cache, fetch_description, filter_versions, is_cached, remove_cached_archive};
use frate::shims::{run_shell_with_frate_path};
#[cfg(windows)]
use frate::shims::{write_windows_activate};
#[cfg(unix)]
use frate::shims::{write_unix_activate};
use frate::toml::FrateToml;
use frate::util::{ensure_frate_dirs, find_installed_paths, get_frate_toml, get_locked, is_installed, sort_versions};
use crate::cli::{FrateCommand, Cli};
pub fn execute(cli: Cli) -> Result<()> {
match &cli.command {
FrateCommand::Search { .. } |
FrateCommand::Shell |
FrateCommand::Clean { .. } |
FrateCommand::Init => {},
_ => {
let toml_path = get_frate_toml()?;
if !toml_path.exists() {
bail!("frate.toml not found. Run `frate init` to create one.")
}
}
}
match cli.command {
FrateCommand::List { verbose } => {
if verbose {
set_verbosity!()
}
execute_list()
},
FrateCommand::Shell => {
set_verbosity!();
execute_shell()
}
FrateCommand::Init => {
execute_init()
},
FrateCommand::Sync => {
execute_sync()
}
FrateCommand::Install { name } => {
execute_install(name)
}
FrateCommand::Uninstall { name } => {
execute_uninstall(name)
}
FrateCommand::Which { name, verbose } => {
if verbose {
set_verbosity!();
}
execute_which(&name)
}
FrateCommand::Run { command } => {
execute_run(&command)
}
FrateCommand::Add { name_at_version } => {
execute_add(name_at_version)
}
FrateCommand::Search { name, versions, verbose } => {
if verbose {
set_verbosity!();
}
execute_search(name, versions)
}
FrateCommand::Clean { name } => {
execute_clean(name)
}
FrateCommand::Registry { verbose } => {
if verbose {
set_verbosity!();
}
execute_registry()
}
_ => {
Ok(())
}
}
}
pub fn execute_list() -> Result<()> {
let toml_path = get_frate_toml()?;
let toml_str = std::fs::read_to_string(toml_path)?;
let toml: FrateToml = toml::from_str(&toml_str)?;
let lock_path = std::env::current_dir()?.join("frate.lock");
let lock: Option<FrateLock> = if lock_path.exists() {
let lock_str = std::fs::read_to_string(lock_path)?;
Some(toml::from_str(&lock_str)?)
}
else {
None
};
if toml.dependencies.is_empty() {
println!("{}", "No dependencies".yellow());
return Ok(());
}
for (name, version) in &toml.dependencies {
println!("{}: {}", name.bold(), version.bold());
if let Some(lock) = &lock {
let locked = get_locked(name, lock);
match locked {
Some(locked) => {
print!(" {}", " locked".green());
verbose!(@lvl 1, " {} {}", "at:".green(), locked.version.green());
verbose!(@lvl 1, " {} {}", " hash:".green(), locked.hash.green());
verbose!(@lvl 1, " {} {}", " source:".cyan(), locked.source.cyan());
match is_cached(format!("{}-{}", locked.name, locked.version ).as_str()) {
Ok(true) => {
println!(" {}", " cached".green());
}
_ => {}
}
},
None => {
println!(" {}", " unlocked".yellow());
}
}
match is_installed(name) {
true => {
print!(" {}", " installed".green());
},
false => {
print!(" {}", " not installed".red());
},
}
}
println!();
}
Ok(())
}
pub fn execute_init() -> Result<()> {
let cwd = std::env::current_dir()?;
let name = cwd.file_name().ok_or(anyhow::anyhow!("Could not get file name"))?
.to_str().ok_or(anyhow::anyhow!("Invalid directory name"))?;
let _ = ensure_frate_dirs(&cwd)
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
println!(" {} {}", "Initialized".green().bold(), ".frate/");
let toml = FrateToml::default(name);
toml.save(cwd.join("frate.toml")).map_err(|e| anyhow::anyhow!("{:?}", e))?;
println!(" {} {}", "Initialized".green().bold(), "frate.toml");
#[cfg(windows)]
write_windows_activate()?;
#[cfg(not(windows))]
write_unix_activate()?;
Ok(())
}
pub fn execute_sync() -> Result<()> {
let cwd = std::env::current_dir()?;
let toml_str = std::fs::read_to_string(cwd.join("frate.toml"))?;
let toml: FrateToml = toml::from_str(&toml_str)?;
let mut lock = FrateLock::load_or_default(cwd.join("frate.lock"));
let added = lock.sync(&toml)?;
if added.is_empty() {
println!(" {} {}", "Synced".bold().green(), "None".bold().yellow());
}
else {
println!(
" {} {}",
"Synced".bold().green(),
added.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
);
}
lock.save(cwd.join("frate.lock"))?;
println!(" {} {}", "Saved".bold().green(), "frate.lock");
Ok(())
}
pub fn execute_install(name: Option<String>) -> Result<()> {
let cwd = std::env::current_dir()?;
let lock = FrateLock::load_or_default(cwd.join("frate.lock"));
match name {
Some(name) => {
let package = get_locked(&name, &lock)
.ok_or(anyhow::anyhow!(" Package not found: {}", name))?;
install_package(&package, &cwd.join(".frate"))
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
}
None => {
install_packages(&lock, &cwd)
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
}
}
Ok(())
}
pub fn execute_uninstall(name: Option<String>) -> Result<()> {
match name {
Some(name) => {
uninstall_package(&name)
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
}
None => {
uninstall_packages()
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
}
}
Ok(())
}
pub fn execute_which(name: &str) -> Result<()> {
let (exe_path, shim_path) = find_installed_paths(name)?;
if exe_path.is_none() && shim_path.is_none() {
println!("{}", "No installed paths found".yellow());
return Ok(());
}
if let Some(exe_path) = exe_path {
println!(" {} {}", "Binary".green().bold(), exe_path.iter().next_back().unwrap().to_string_lossy());
}
if let Some(shim_path) = shim_path {
println!(" {} {}", "Shim".green().bold(), shim_path.iter().next_back().unwrap().to_string_lossy());
}
Ok(())
}
pub fn execute_run(command: &str) -> Result<()> {
let (name, args) = match command.split_once(' ') {
Some((name, args)) => {
(name, args.split_whitespace().into_iter().collect::<Vec<_>>())
},
None => {
bail!("Invalid command: {}", command);
}
};
let (exe_path, _) = find_installed_paths(name)?;
let exe_path = match exe_path {
Some(exe_path) => {
exe_path
}
None => {
bail!("Executable not found: {}", name);
}
};
println!(
" {} {} {}",
"Running".green().bold(),
exe_path.display(),
args.iter().map(|arg| arg.to_string()).collect::<Vec<_>>().join(" ")
);
let output = Command::new(exe_path)
.args(args).output()?;
if !output.status.success() {
bail!("{}", String::from_utf8(output.stderr)?.red());
}
print!("{}", String::from_utf8(output.stdout)?);
Ok(())
}
fn extract_name_at_version(name_at_version: &str) -> Result<(String, String)> {
let mut split = name_at_version.split('@');
let name = split.next().ok_or(anyhow::anyhow!("Invalid name@version"))?;
let version = split.next().ok_or(anyhow::anyhow!("Invalid name@version"))?;
Ok((name.to_string(), version.to_string()))
}
pub fn execute_add(name_at_version: String) -> Result<()> {
let (name, version) = extract_name_at_version(&name_at_version)?;
let mut toml = FrateToml::load(std::env::current_dir()?.join("frate.toml"))
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
toml.add(&name, &version)?;
toml.save(std::env::current_dir()?.join("frate.toml"))
.map_err(|e| anyhow::anyhow!("{:?}", e))?;
println!(" {} {}", "Added".green().bold(), name_at_version);
Ok(())
}
pub fn execute_search(name: String, versions: usize) -> Result<()> {
let tool = fetch_registry(&name)?;
let sorted = sort_versions(tool.releases);
let filtered = filter_versions(sorted);
println!("{}", name.bold());
if let Some(desc) = fetch_description(tool.repo.as_str())? {
println!(" {}", desc.dimmed());
}
if filtered.is_empty() {
println!(" {}", "No versions found for:".yellow());
println!(" {}", std::env::consts::OS.yellow());
println!(" {}", std::env::consts::ARCH.yellow());
return Ok(());
}
let (latest_version, latest_info) = filtered.last().unwrap();
println!(" {}", "latest:".bold());
println!(" {}", latest_version.split('-').next().unwrap_or(&latest_version).bold().green());
verbose!(" {}", latest_info.url.cyan());
verbose!(" {}", latest_info.hash.cyan());
if versions <= 1 {
return Ok(());
}
println!(" {}", "other versions:".bold());
for (version, info) in filtered[1..versions].iter() {
println!(" {}", version.split('-').next().unwrap_or(&version).bold());
verbose!(" {}", &info.url.cyan());
verbose!(" {}", &info.hash.cyan());
}
Ok(())
}
pub fn execute_shell() -> Result<()> {
run_shell_with_frate_path().with_context(|| "Failed to run shell")
}
pub fn execute_clean(name: Option<String>) -> Result<()> {
if let Some(name) = name {
remove_cached_archive(&name)?;
}
else {
clean_cache()?;
}
Ok(())
}
#[derive(Debug, Deserialize)]
struct ToolInfo {
name: String,
repo: String,
}
#[derive(Debug, Deserialize)]
struct RegistryIndex {
registered: Vec<ToolInfo>,
}
pub fn execute_registry() -> Result<()> {
let url = "https://raw.githubusercontent.com/konni332/frate-registry/refs/heads/master/registry.json";
let resp = reqwest::blocking::get(url)?;
let registry: RegistryIndex = serde_json::from_reader(resp)?;
println!("{}", "Available tools:".bold());
for tool in ®istry.registered {
println!(" {}", tool.name.bold().green());
verbose!(" {}", tool.repo.cyan());
}
Ok(())
}