1#![warn(missing_docs)]
2
3use clap::{crate_version, Parser, Subcommand};
21pub use holochain_cli_bundle as hc_bundle;
22use holochain_cli_client as hc_client;
23use holochain_cli_sandbox as hc_sandbox;
24use lazy_static::lazy_static;
25use std::process::Command;
26
27mod external_subcommands;
28
29lazy_static! {
38 static ref HELP: &'static str = {
39 let extensions = external_subcommands::list_external_subcommands()
40 .into_iter()
41 .map(|s| format!(" {s}\t Run \"hc {s} help\" to see its help"))
42 .collect::<Vec<String>>()
43 .join("\n");
44
45 let extensions_str = match extensions.len() {
46 0 => String::from(""),
47 _ => format!(
48 r#"
49Extensions:
50{extensions}"#
51 ),
52 };
53
54 let s = format!(
55 r#"Holochain CLI
56
57Work 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.
58{extensions_str}"#
59 );
60 Box::leak(s.into_boxed_str())
61 };
62}
63
64fn builtin_commands() -> Vec<String> {
65 ["hc-web-app", "hc-dna", "hc-app", "hc-sandbox", "hc-client"]
66 .iter()
67 .map(|s| s.to_string())
68 .collect()
69}
70
71#[allow(clippy::large_enum_variant)]
73#[derive(Debug, Parser)]
74#[command(about = *HELP, infer_subcommands = true, allow_external_subcommands = true, version = crate_version!())]
75pub struct Cli {
76 #[command(subcommand)]
78 pub subcommand: CliSubcommand,
79}
80
81#[derive(Debug, Subcommand)]
83#[allow(clippy::large_enum_variant)]
84pub enum CliSubcommand {
85 Dna(hc_bundle::HcDnaBundle),
87 App(hc_bundle::HcAppBundle),
89 WebApp(hc_bundle::HcWebAppBundle),
91 Sandbox(hc_sandbox::HcSandbox),
93 Client(hc_client::HcClient),
95 #[command(external_subcommand)]
97 External(Vec<String>),
98}
99
100impl CliSubcommand {
101 pub async fn run(self) -> anyhow::Result<()> {
103 match self {
104 CliSubcommand::App(cmd) => cmd.run().await?,
105 CliSubcommand::Dna(cmd) => cmd.run().await?,
106 CliSubcommand::WebApp(cmd) => cmd.run().await?,
107 CliSubcommand::Sandbox(cmd) => cmd.run().await?,
108 CliSubcommand::Client(cmd) => cmd.run().await?,
109 CliSubcommand::External(args) => {
110 let command_suffix = args.first().expect("Missing subcommand name");
111 let exe_name = format!("hc-{command_suffix}");
112
113 match Command::new(&exe_name).args(&args[1..]).status() {
114 Ok(status) => {
115 std::process::exit(status.code().unwrap_or(1));
116 }
117 Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => {
118 eprintln!(
119 "error: `{command_suffix}' is not a recognized internal hc subcommand, nor is '{exe_name}' an external command on your PATH."
120 );
121
122 std::process::exit(1);
123 }
124 Err(other_err) => {
125 eprintln!("error: Failed to execute '{exe_name}': {other_err}");
126 std::process::exit(1);
127 }
128 }
129 }
130 }
131 Ok(())
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use assert_cmd::Command;
138 use predicates::prelude::*;
139
140 #[test]
141 fn test_help_flag() {
142 let mut cmd = Command::cargo_bin("hc").unwrap();
143
144 cmd.arg("--help")
145 .assert()
146 .success()
147 .stdout(predicate::str::contains("Usage:"));
148 }
149
150 #[test]
151 fn test_no_subcommand() {
152 let mut cmd = Command::cargo_bin("hc").unwrap();
153
154 cmd.assert()
155 .failure()
156 .stderr(predicate::str::contains("Usage:"));
157 }
158
159 #[test]
160 fn test_predefined_subcommand() {
161 let mut cmd = Command::cargo_bin("hc").unwrap();
162
163 cmd.arg("sandbox")
164 .assert()
165 .failure()
166 .stderr(predicate::str::contains("Work with sandboxed environments"));
167 }
168
169 #[test]
170 fn test_undefined_subcommand() {
171 let mut cmd = Command::cargo_bin("hc").unwrap();
172
173 cmd.arg("blah")
174 .assert()
175 .failure()
176 .stderr(predicate::str::contains(
177 "not a recognized internal hc subcommand",
178 ));
179 }
180}