use std::fs;
use std::process::ExitCode;
use std::sync::OnceLock;
use clap::{CommandFactory, Parser};
use directories::ProjectDirs;
use log::{debug, error, info, warn};
use cotis_cli::install::{install, normalize_cdylib_stem, parse_install_spec};
use cotis_cli::plugin::LoadedPlugin;
use cotis_cli::routines::{
find_latest_installed_version, host_target_triple, installed_routine_names,
plugin_library_path, routine_root,
};
use crate::cli::{Commands, CotisCli, InfoArgs, InstallArgs, RemoveArgs, RunArgs};
pub(crate) static PROJ_DIRS: OnceLock<ProjectDirs> = OnceLock::new();
mod cli;
mod interactive;
fn main() -> ExitCode {
let cli = CotisCli::parse();
let log_level = if cli.verbose {
log::LevelFilter::Debug
} else {
log::LevelFilter::Info
};
env_logger::Builder::new().filter_level(log_level).init();
if let Some(proj_dirs) = ProjectDirs::from("", "Cotis Layout", "cotis-cli") {
PROJ_DIRS.get_or_init(|| proj_dirs);
} else {
error!("Could not resolve cotis-cli application directories");
return ExitCode::FAILURE;
}
match &cli.command {
Commands::Completions { shell } => {
let mut cmd = CotisCli::command();
clap_complete::generate(*shell, &mut cmd, "cotis-cli", &mut std::io::stdout());
ExitCode::SUCCESS
}
Commands::List => {
list_routines();
ExitCode::SUCCESS
}
Commands::Install(args) | Commands::Update(args) => {
let proj_dirs = proj_dirs_or_panic();
let force = matches!(&cli.command, Commands::Update(..)) || cli.overwrite;
exit_result(cmd_install(proj_dirs, args, force))
}
Commands::Run(args) | Commands::Build(args) => {
exit_result(cmd_run(proj_dirs_or_panic(), args))
}
Commands::Remove(args) => exit_result(cmd_remove(proj_dirs_or_panic(), args)),
Commands::Info(args) => exit_result(cmd_info(proj_dirs_or_panic(), args)),
Commands::HelpRoutine(args) => exit_result(cmd_help_routine(proj_dirs_or_panic(), args)),
}
}
fn proj_dirs_or_panic() -> &'static ProjectDirs {
PROJ_DIRS
.get()
.expect("project dirs initialized before command dispatch")
}
fn exit_result(r: Result<(), ()>) -> ExitCode {
if r.is_ok() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
fn cmd_install(
proj_dirs: &ProjectDirs,
args: &InstallArgs,
base_overwrite: bool,
) -> Result<(), ()> {
let spec_from_tty;
let spec_str = match &args.spec {
Some(s) => {
spec_from_tty = false;
s.clone()
}
None => {
spec_from_tty = true;
match crate::interactive::prompt_install_spec() {
Ok(s) => s,
Err(e) => {
error!("{e}");
return Err(());
}
}
}
};
let alias_override: Option<String> = if let Some(a) = &args.alias {
Some(a.clone())
} else if spec_from_tty {
match crate::interactive::prompt_optional_alias() {
Ok(a) => a,
Err(e) => {
error!("{e}");
return Err(());
}
}
} else {
None
};
let overwrite = base_overwrite || spec_from_tty;
let spec = parse_install_spec(&spec_str).map_err(|e| {
error!("{e}");
})?;
let cdylib_override = match args.cdylib_name.as_deref() {
Some(s) => Some(normalize_cdylib_stem(s).map_err(|e| {
error!("{e}");
})?),
None => None,
};
install(
proj_dirs,
spec,
alias_override.as_deref(),
cdylib_override.as_deref(),
overwrite,
)
.map_err(|e| {
error!("{e}");
})?;
info!("Successfully installed routine");
Ok(())
}
fn cmd_run(proj_dirs: &ProjectDirs, args: &RunArgs) -> Result<(), ()> {
let routine = match &args.routine {
Some(r) => r.clone(),
None => {
match crate::interactive::prompt_routine(proj_dirs, "Which routine do you want to run?")
{
Ok(r) => r,
Err(e) => {
error!("{e}");
return Err(());
}
}
}
};
let target = host_target_triple().map_err(|e| {
error!("{e}");
})?;
let (name, version) = parse_routine_spec(&routine);
let version = resolve_version(name, version.as_deref())?;
run_routine(proj_dirs, name, &version, &target, &args.passthrough)
}
fn run_routine(
proj_dirs: &ProjectDirs,
routine: &str,
version: &str,
target: &str,
passthrough: &[String],
) -> Result<(), ()> {
let lib_path = plugin_library_path(proj_dirs, routine, version, target);
debug!("Loading routine plugin {}", lib_path.display());
unsafe {
std::env::set_var(
"COTIS_CLI_CACHE_DIR",
proj_dirs.cache_dir().to_string_lossy().into_owned(),
);
}
let plugin = unsafe {
LoadedPlugin::load(&lib_path).map_err(|e| {
error!("{e}");
})?
};
plugin.ensure_compatible().map_err(|e| {
error!("{e}");
})?;
let mut all_args = vec![routine.to_string()];
all_args.extend_from_slice(passthrough);
let rc = plugin.run_with_args(&all_args).map_err(|e| {
error!("{e}");
})?;
if rc == 0 { Ok(()) } else { Err(()) }
}
fn cmd_help_routine(proj_dirs: &ProjectDirs, args: &InfoArgs) -> Result<(), ()> {
let spec = match &args.spec {
Some(s) => s.clone(),
None => match crate::interactive::prompt_routine_spec(proj_dirs, "Which routine's help?") {
Ok(s) => s,
Err(e) => {
error!("{e}");
return Err(());
}
},
};
let (routine, version) = parse_routine_spec(&spec);
let version = resolve_version(routine, version.as_deref())?;
let target = host_target_triple().map_err(|e| {
error!("{e}");
})?;
let lib_path = plugin_library_path(proj_dirs, routine, &version, &target);
let plugin = unsafe {
LoadedPlugin::load(&lib_path).map_err(|e| {
error!("{e}");
})?
};
plugin.ensure_compatible().map_err(|e| {
error!("{e}");
})?;
let help = plugin.help().map_err(|e| {
error!("{e}");
})?;
println!("{help}");
Ok(())
}
fn list_routines() {
let proj_dirs = match PROJ_DIRS.get() {
Some(p) => p,
None => {
error!("Project directories not initialized");
return;
}
};
let names = installed_routine_names(proj_dirs);
if names.is_empty() {
println!("No routines installed.");
return;
}
for name in names {
println!("{name}");
}
}
fn cmd_remove(proj_dirs: &ProjectDirs, args: &RemoveArgs) -> Result<(), ()> {
let routine = match &args.routine {
Some(r) => r.clone(),
None => {
match crate::interactive::prompt_routine(proj_dirs, "Which routine should be removed?")
{
Ok(r) => r,
Err(e) => {
error!("{e}");
return Err(());
}
}
}
};
let root = routine_root(proj_dirs, &routine);
if !root.exists() {
warn!("{routine} not installed");
return Ok(());
}
fs::remove_dir_all(&root).map_err(|e| {
error!("Failed to remove {}: {e}", root.display());
})?;
info!("Removed routine `{routine}`");
Ok(())
}
fn cmd_info(proj_dirs: &ProjectDirs, args: &InfoArgs) -> Result<(), ()> {
let spec = match &args.spec {
Some(s) => s.clone(),
None => match crate::interactive::prompt_routine_spec(proj_dirs, "Which routine's info?") {
Ok(s) => s,
Err(e) => {
error!("{e}");
return Err(());
}
},
};
let (routine, version) = parse_routine_spec(&spec);
let version = resolve_version(routine, version.as_deref())?;
let target = host_target_triple().map_err(|e| {
error!("{e}");
})?;
let lib_path = plugin_library_path(proj_dirs, routine, &version, &target);
let plugin = unsafe {
LoadedPlugin::load(&lib_path).map_err(|e| {
error!("{e}");
})?
};
plugin.ensure_compatible().map_err(|e| {
error!("{e}");
})?;
let descriptor = plugin.descriptor_json().map_err(|e| {
error!("{e}");
})?;
println!("{descriptor}");
Ok(())
}
fn parse_routine_spec(spec: &str) -> (&str, Option<String>) {
match spec.split_once('@') {
Some((name, version)) => (name, Some(version.to_string())),
None => (spec, None),
}
}
fn resolve_version(routine: &str, requested: Option<&str>) -> Result<String, ()> {
if let Some(v) = requested {
return Ok(v.to_string());
}
find_latest_installed_version(PROJ_DIRS.get().unwrap(), routine).ok_or_else(|| {
error!("No installed versions found for {routine}");
})
}