#![warn(missing_docs)]
use clap::{crate_version, Parser, Subcommand};
pub use holochain_cli_bundle as hc_bundle;
use holochain_cli_client as hc_client;
use holochain_cli_sandbox as hc_sandbox;
use lazy_static::lazy_static;
use std::process::Command;
mod external_subcommands;
lazy_static! {
static ref HELP: &'static str = {
let extensions = external_subcommands::list_external_subcommands()
.into_iter()
.map(|s| format!(" {s}\t Run \"hc {s} help\" to see its help"))
.collect::<Vec<String>>()
.join("\n");
let extensions_str = match extensions.len() {
0 => String::from(""),
_ => format!(
r#"
Extensions:
{extensions}"#
),
};
let s = format!(
r#"Holochain CLI
Work with DNA, hApp and web-hApp bundle files, set up sandbox environments for testing and development purposes, make direct admin calls to running conductors, and more.
{extensions_str}"#
);
Box::leak(s.into_boxed_str())
};
}
fn builtin_commands() -> Vec<String> {
["hc-web-app", "hc-dna", "hc-app", "hc-sandbox", "hc-client"]
.iter()
.map(|s| s.to_string())
.collect()
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Parser)]
#[command(about = *HELP, infer_subcommands = true, allow_external_subcommands = true, version = crate_version!())]
pub struct Cli {
#[command(subcommand)]
pub subcommand: CliSubcommand,
}
#[derive(Debug, Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum CliSubcommand {
Dna(hc_bundle::HcDnaBundle),
App(hc_bundle::HcAppBundle),
WebApp(hc_bundle::HcWebAppBundle),
Sandbox(hc_sandbox::HcSandbox),
Client(hc_client::HcClient),
#[command(external_subcommand)]
External(Vec<String>),
}
impl CliSubcommand {
pub async fn run(self) -> anyhow::Result<()> {
match self {
CliSubcommand::App(cmd) => cmd.run().await?,
CliSubcommand::Dna(cmd) => cmd.run().await?,
CliSubcommand::WebApp(cmd) => cmd.run().await?,
CliSubcommand::Sandbox(cmd) => cmd.run().await?,
CliSubcommand::Client(cmd) => cmd.run().await?,
CliSubcommand::External(args) => {
let command_suffix = args.first().expect("Missing subcommand name");
let exe_name = format!("hc-{command_suffix}");
match Command::new(&exe_name).args(&args[1..]).status() {
Ok(status) => {
std::process::exit(status.code().unwrap_or(1));
}
Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!(
"error: `{command_suffix}' is not a recognized internal hc subcommand, nor is '{exe_name}' an external command on your PATH."
);
std::process::exit(1);
}
Err(other_err) => {
eprintln!("error: Failed to execute '{exe_name}': {other_err}");
std::process::exit(1);
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use assert_cmd::Command;
use predicates::prelude::*;
#[test]
fn test_help_flag() {
let mut cmd = Command::cargo_bin("hc").unwrap();
cmd.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Usage:"));
}
#[test]
fn test_no_subcommand() {
let mut cmd = Command::cargo_bin("hc").unwrap();
cmd.assert()
.failure()
.stderr(predicate::str::contains("Usage:"));
}
#[test]
fn test_predefined_subcommand() {
let mut cmd = Command::cargo_bin("hc").unwrap();
cmd.arg("sandbox")
.assert()
.failure()
.stderr(predicate::str::contains("Work with sandboxed environments"));
}
#[test]
fn test_undefined_subcommand() {
let mut cmd = Command::cargo_bin("hc").unwrap();
cmd.arg("blah")
.assert()
.failure()
.stderr(predicate::str::contains(
"not a recognized internal hc subcommand",
));
}
}