#![deny(unsafe_code)]
use asimov_cli::{
BoxError,
commands::{self, ExternalSubcommand, Help, HelpCmd},
};
use clientele::{
ColorChoiceExt, StandardOptions, SubcommandsProvider,
SysexitsError::{self, *},
crates::clap::{CommandFactory, FromArgMatches, Parser, Subcommand},
strip_ansi,
};
use color_print::ceprintln;
use std::ffi::OsString;
#[cfg(feature = "module")]
use crate::commands::module::ModuleCommand;
#[cfg(feature = "proxy")]
use crate::commands::proxy::ProxyCommand;
#[cfg(feature = "source")]
use crate::commands::source::SourceCommand;
#[derive(Debug, Parser)]
#[command(name = "asimov", long_about)]
#[command(allow_external_subcommands = true)]
#[command(arg_required_else_help = true)]
#[command(styles = clientele::HELP_STYLES)]
struct Options {
#[clap(flatten)]
flags: StandardOptions,
#[clap(subcommand)]
command: Option<Command>,
}
#[derive(Debug, Subcommand)]
enum Command {
#[cfg(feature = "module")]
#[clap(subcommand)]
Module(ModuleCommand),
#[cfg(feature = "proxy")]
Proxy {
#[clap(subcommand)]
command: Option<ProxyCommand>,
#[clap(flatten)]
args: commands::proxy::ProxyServeArgs,
},
#[cfg(feature = "source")]
Source {
#[clap(subcommand)]
command: Option<SourceCommand>,
#[clap(flatten)]
args: commands::source::SourceFetchArgs,
},
#[cfg(feature = "unstable")]
#[clap(flatten)]
Unstable(commands::unstable::UnstableCommand),
#[clap(external_subcommand)]
External(Vec<String>),
}
#[tokio::main]
pub async fn main() -> SysexitsError {
clientele::dotenv().ok();
let Ok(mut args) = clientele::args_os() else {
return EX_USAGE;
};
asimov_cli::aliases::resolve(&mut args);
let color = clientele::color_choice(&args);
let use_color = color.to_bool();
let options = Options::command()
.color(color)
.help_template(help_template(use_color))
.after_help(after_help(use_color))
.after_long_help(after_long_help(use_color))
.try_get_matches_from(&args)
.and_then(|mut matches| {
Options::from_arg_matches_mut(&mut matches)
.map_err(|err| err.format(&mut Options::command().color(color)))
});
let options = match options {
Ok(options) => options,
Err(err)
if err.kind() == clap::error::ErrorKind::DisplayHelp
|| err.kind()
== clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand =>
{
err.exit()
},
Err(err)
if err.kind() == clap::error::ErrorKind::InvalidSubcommand
&& args
.get(1)
.and_then(|arg| arg.to_str())
.is_some_and(|arg| arg == "help") =>
{
let debug =
args.contains(&OsString::from("-d")) || args.contains(&OsString::from("--debug"));
let cmd = HelpCmd { is_debug: debug };
let Ok(args) = args
.into_iter()
.map(OsString::into_string)
.collect::<Result<Vec<_>, _>>()
else {
return EX_USAGE;
};
let mut args = args
.into_iter()
.skip(2)
.skip_while(|arg| arg.starts_with("-"));
let Some(cmd_name) = args.next() else {
err.exit();
};
let args: Vec<String> = args.collect();
let result = cmd.execute(&cmd_name, &args);
if let Ok(result) = &result {
if result.success {
let mut stdout = std::io::stdout().lock();
if std::io::copy(&mut result.output.as_slice(), &mut stdout).is_err() {
return EX_IOERR;
}
} else {
eprintln!("asimov: {} doesn't provide help", cmd_name);
if debug {
eprintln!("asimov: status code - {}", result.code);
let mut stdout = std::io::stdout().lock();
if std::io::copy(&mut result.output.as_slice(), &mut stdout).is_err() {
return EX_IOERR;
}
}
}
}
return result.map(|result| result.code).unwrap_or(EX_UNAVAILABLE);
},
Err(err) => err.exit(),
};
let flags = &options.flags;
asimov_module::init_tracing_subscriber(flags).expect("failed to initialize logging");
if flags.version {
println!("ASIMOV {}", env!("CARGO_PKG_VERSION"));
return EX_OK;
}
if flags.license {
print!("{}", include_str!("../UNLICENSE"));
return EX_OK;
}
if flags.debug {
}
let Some(command) = options.command else {
Options::command()
.color(color)
.help_template(help_template(use_color))
.after_help(after_help(use_color))
.print_help()
.ok();
return EX_USAGE;
};
asimov_registry::Registry::default()
.create_file_tree()
.await
.inspect_err(|e| {
tracing::debug!("failed to create module file tree: {e}");
})
.ok();
if let Err(err) = std::fs::create_dir_all(asimov_env::paths::asimov_root().join("snapshots"))
.map_err(|e| {
ceprintln!("<s,r>error:</> failed to create snapshot directory: {e}");
EX_IOERR
})
{
return err;
}
use Command::*;
let result = match command {
#[cfg(feature = "module")]
Module(command) => command.run(flags).await.map_err(sysexits).map(|_| EX_OK),
#[cfg(feature = "proxy")]
Proxy { command, args } => command
.unwrap_or(ProxyCommand::Serve { args })
.run(flags)
.await
.map_err(sysexits)
.map(|_| EX_OK),
#[cfg(feature = "source")]
Source { command, args } => command
.unwrap_or(SourceCommand::Fetch { args })
.run(flags)
.await
.map_err(sysexits)
.map(|_| EX_OK),
External(args) => {
let cmd = ExternalSubcommand {
is_debug: flags.debug,
pipe_output: false,
};
cmd.execute(&args[0], &args[1..]).map(|result| result.code)
},
};
result.unwrap_or_else(|e| e)
}
fn help_template(color: bool) -> String {
let aliases = if color {
aliases_help()
} else {
strip_ansi(&aliases_help())
};
let commands_heading = color_print::cstr!("<y>Commands:</y>");
let options_heading = color_print::cstr!("<y>Options:</y>");
let (commands_heading, options_heading) = if color {
(commands_heading.into(), options_heading.into())
} else {
(strip_ansi(commands_heading), strip_ansi(options_heading))
};
format!(
"{{before-help}}{{about-with-newline}}\n{{usage-heading}} {{usage}}\n\n{commands_heading}\n{{subcommands}}\n\n{aliases}\n{options_heading}\n{{options}}{{after-help}}"
)
}
fn aliases_help() -> String {
let mut help = String::new();
help.push_str(color_print::cstr!("<y>Aliases:</y>\n"));
let width = asimov_cli::aliases::ALIASES
.iter()
.map(|(name, _)| name.len())
.max()
.unwrap_or(0);
for (name, expansion) in asimov_cli::aliases::ALIASES {
help.push_str(&color_print::cformat!(
" <s>{:width$}</s> asimov {}\n",
name,
expansion.join(" "),
));
}
help
}
fn after_long_help(color: bool) -> String {
let mut help = String::new();
let cmds = Help.execute();
for (i, cmd) in cmds.iter().enumerate() {
if i == 0 {
help.push_str(color_print::cstr!("<s><u>Commands:</u></s>\n"));
}
if i > 0 {
help.push_str("\n\n")
}
let predicted_usage = format!("Usage: asimov-{} ", cmd.name);
let description = cmd.description.replace('\n', "\n\t");
if let Some(usage) = cmd
.usage
.as_ref()
.and_then(|usage| usage.strip_prefix(&predicted_usage))
{
help.push_str(&color_print::cformat!(
"\t<dim>$</dim> <s>asimov {}</s> {}\n\t{}",
cmd.name,
usage,
description,
));
} else {
help.push_str(&color_print::cformat!(
"\t<dim>$</dim> <s>asimov {}</s> [OPTIONS] [COMMAND]\n\t{}",
cmd.name,
description
));
}
}
if color { help } else { strip_ansi(&help) }
}
pub fn after_help(color: bool) -> String {
let mut help = String::new();
let commands = SubcommandsProvider::collect("asimov-", 1);
for (i, cmd) in commands.iter().enumerate() {
if i == 0 {
help.push_str(color_print::cstr!("<s><u>Commands:</u></s>\n"));
}
if i > 0 {
help.push('\n');
}
help.push_str(&color_print::cformat!(
"\t<dim>$</dim> <s>asimov {}</s> [OPTIONS] [COMMAND]",
cmd.name,
));
}
if color { help } else { strip_ansi(&help) }
}
fn sysexits(err: BoxError) -> SysexitsError {
err.downcast_ref::<SysexitsError>()
.copied()
.unwrap_or(EX_SOFTWARE)
}