use std::ffi::OsString; use std::fmt::Display; use std::io; use std::io::Write; use std::{io::ErrorKind, process}; use anyhow::anyhow; use clap::builder::Styles; use clap::builder::styling::AnsiColor; use clap::{CommandFactory as _, Parser, Subcommand}; use radicle::version::Version; use radicle_cli::commands::*; use radicle_cli::terminal as term; pub const NAME: &str = "rad"; pub const GIT_HEAD: &str = env!("GIT_HEAD"); pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const RADICLE_VERSION: &str = env!("RADICLE_VERSION"); pub const RADICLE_VERSION_LONG: &str = concat!(env!("RADICLE_VERSION"), " (", env!("GIT_HEAD"), ")"); pub const DESCRIPTION: &str = "Radicle command line interface"; pub const LONG_DESCRIPTION: &str = " Radicle is a sovereign code forge built on Git. See `rad --help` to learn about a specific command. Do you have feedback? - Chat <\x1b]8;;https://radicle.zulipchat.com\x1b\\radicle.zulipchat.com\x1b]8;;\x1b\\> - Mail <\x1b]8;;mailto:feedback@radicle.xyz\x1b\\feedback@radicle.xyz\x1b]8;;\x1b\\> (Messages are automatically posted to the public #feedback channel on Zulip.)\ "; pub const TIMESTAMP: &str = env!("SOURCE_DATE_EPOCH"); pub const VERSION: Version = Version { name: NAME, version: RADICLE_VERSION, commit: GIT_HEAD, timestamp: TIMESTAMP, }; const STYLES: Styles = Styles::styled() .header(AnsiColor::Magenta.on_default().bold()) .usage(AnsiColor::Magenta.on_default().bold()) .placeholder(AnsiColor::Cyan.on_default()); /// Radicle command line interface #[derive(Parser, Debug)] #[command(name = NAME)] #[command(version = RADICLE_VERSION)] #[command(long_version = RADICLE_VERSION_LONG)] #[command(about = DESCRIPTION)] #[command(long_about = LONG_DESCRIPTION)] #[command(propagate_version = true)] #[command(styles = STYLES)] struct CliArgs { #[command(subcommand)] pub command: Command, } #[derive(Subcommand, Debug)] enum Command { Auth(auth::Args), Block(block::Args), Checkout(checkout::Args), Clean(clean::Args), Clone(clone::Args), #[command(hide = true)] Cob(cob::Args), Config(config::Args), Debug(debug::Args), Follow(follow::Args), #[command(hide = true)] // `rad fork` command is deprecated Fork(fork::Args), Id(id::Args), Inbox(inbox::Args), Init(init::Args), #[command(alias = ".")] Inspect(inspect::Args), Issue(issue::Args), Ls(ls::Args), Node(node::Args), Patch(patch::Args), Path(path::Args), Publish(publish::Args), Remote(remote::Args), Seed(seed::Args), #[command(name = "self")] RadSelf(rad_self::Args), Stats(stats::Args), Sync(sync::Args), Unblock(unblock::Args), Unfollow(unfollow::Args), Unseed(unseed::Args), Watch(watch::Args), /// Print the version information of the CLI Version { /// Print the version information in JSON format #[arg(long)] json: bool, }, /// Print static completion information for a given shell #[command(hide = true)] Completion { /// The type of shell to output a static completion script for. shell: clap_complete::Shell, }, #[command(external_subcommand)] External(Vec), } fn main() { human_panic::setup_panic!(human_panic::Metadata::new( env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") ) .homepage(env!("CARGO_PKG_HOMEPAGE")) .support("Open a support request at https://radicle.zulipchat.com/ or file an issue via Radicle itself, or e-mail to team@radicle.xyz")); if let Some(lvl) = radicle::logger::env_level() { let logger = Box::new(radicle::logger::Logger::new(lvl)); log::set_boxed_logger(logger).expect("no other logger should have been set already"); log::set_max_level(lvl.to_level_filter()); } if let Err(e) = radicle::io::set_file_limit(4096) { log::warn!(target: "cli", "Unable to set open file limit: {e}"); } let CliArgs { command } = CliArgs::parse(); run(command, term::DefaultContext) } fn write_version(as_json: bool) -> anyhow::Result<()> { let mut stdout = io::stdout(); if as_json { VERSION.write_json(&mut stdout)?; writeln!(&mut stdout)?; Ok(()) } else { VERSION.write(&mut stdout)?; Ok(()) } } fn run(command: Command, ctx: impl term::Context) -> ! { match run_command(command, ctx) { Ok(()) => process::exit(0), Err(err) => { term::fail(&err); process::exit(1); } } } fn run_command(command: Command, ctx: impl term::Context) -> Result<(), anyhow::Error> { match command { Command::Auth(args) => auth::run(args, ctx), Command::Block(args) => block::run(args, ctx), Command::Checkout(args) => checkout::run(args, ctx), Command::Clean(args) => clean::run(args, ctx), Command::Clone(args) => clone::run(args, ctx), Command::Cob(args) => cob::run(args, ctx), Command::Config(args) => config::run(args, ctx), Command::Debug(args) => debug::run(args, ctx), Command::Follow(args) => follow::run(args, ctx), Command::Fork(args) => fork::run(args, ctx), Command::Id(args) => id::run(args, ctx), Command::Inbox(args) => inbox::run(args, ctx), Command::Init(args) => init::run(args, ctx), Command::Inspect(args) => inspect::run(args, ctx), Command::Issue(args) => issue::run(args, ctx), Command::Ls(args) => ls::run(args, ctx), Command::Node(args) => node::run(args, ctx), Command::Patch(args) => patch::run(args, ctx), Command::Path(args) => path::run(args, ctx), Command::Publish(args) => publish::run(args, ctx), Command::Remote(args) => remote::run(args, ctx), Command::Seed(args) => seed::run(args, ctx), Command::RadSelf(args) => rad_self::run(args, ctx), Command::Stats(args) => stats::run(args, ctx), Command::Sync(args) => sync::run(args, ctx), Command::Unblock(args) => unblock::run(args, ctx), Command::Unfollow(args) => unfollow::run(args, ctx), Command::Unseed(args) => unseed::run(args, ctx), Command::Watch(args) => watch::run(args, ctx), Command::Version { json } => write_version(json), Command::Completion { shell } => { print_completion(shell, &mut CliArgs::command()); Ok(()) } Command::External(args) => ExternalCommand::new(args).run(), } } fn print_completion(generator: G, cmd: &mut clap::Command) { clap_complete::generate( generator, cmd, cmd.get_name().to_string(), &mut io::stdout(), ); } struct ExternalCommand { command: OsString, args: Vec, } impl ExternalCommand { fn new(mut args: Vec) -> Self { let command = args.remove(0); Self { command, args } } fn is_diff(&self) -> bool { self.command == "diff" } fn exe(&self) -> OsString { let mut exe = OsString::from(NAME); exe.push("-"); exe.push(self.command.clone()); exe } fn display_exe(&self) -> impl Display + use<> { match self.exe().into_string() { Ok(exe) => exe, Err(exe) => format!("{exe:?}"), } } fn run(self) -> anyhow::Result<()> { // This command is deprecated and delegates to `git diff`. // Even before it was deprecated, it was not printed by // `rad -h`. // // Since it is external, `--help` will delegate to `git diff --help`. if self.is_diff() { return diff::run(self.args); } let status = process::Command::new(self.exe()).args(&self.args).status(); match status { Ok(status) => { if !status.success() { return Err(anyhow!("`{}` exited with an error.", self.display_exe())); } Ok(()) } Err(err) => { if let ErrorKind::NotFound = err.kind() { Err(anyhow!( "`{}` is not a known command. See `rad --help` for a list of commands.", self.display_exe(), )) } else { Err(err.into()) } } } } }