Skip to main content

holochain_cli/
lib.rs

1#![warn(missing_docs)]
2
3//! A library and CLI to help create, run, and interact with Holochain conductor setups.
4//! **Warning this is still WIP and subject to change**
5//! There's probably a few bugs. If you find one please open an [issue](https://github.com/holochain/holochain/issues)
6//! or make a PR.
7//!
8//! ## CLI
9//!
10//! The `hc` CLI makes it easy to create, modify, and run hApps that
11//! you are working on or someone has sent you.
12//! It has been designed to use sensible defaults but still give you
13//! the configurability when that's required.
14//!
15//! Setups are stored in tmp directories by default and the paths are
16//! persisted in a `.hc` file which is created wherever you are using
17//! the CLI.
18
19// Useful to have this public when using this as a library.
20use 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
29// TODO: change this so it inherits clap's formatting.
30// Clap 3 and 4 format helptext using colours and bold/underline respectively.
31// https://github.com/clap-rs/clap/pull/4765 introduces the ability to style your own help text
32// using a library like `color_print`.
33// https://github.com/clap-rs/clap/issues/4786 requests that the styler's built-in helper methods
34// be exposed to consumers, thereby allowing us to durably make our styling consistent
35// with whatever clap's happens to be at the moment.
36// I'd prefer the latter approach, if it lands.
37lazy_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/// The main entry-point for the command.
72#[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    /// The `hc` subcommand to run.
77    #[command(subcommand)]
78    pub subcommand: CliSubcommand,
79}
80
81/// Describes all the possible CLI arguments for `hc`, including external subcommands like `hc-scaffold`.
82#[derive(Debug, Subcommand)]
83#[allow(clippy::large_enum_variant)]
84pub enum CliSubcommand {
85    /// Work with DNA bundles.
86    Dna(hc_bundle::HcDnaBundle),
87    /// Work with hApp bundles.
88    App(hc_bundle::HcAppBundle),
89    /// Work with web-hApp bundles.
90    WebApp(hc_bundle::HcWebAppBundle),
91    /// Work with sandboxed environments for testing and development.
92    Sandbox(hc_sandbox::HcSandbox),
93    /// Connect to and interact with running Holochain conductors.
94    Client(hc_client::HcClient),
95    /// Allow redirect of external subcommands (like `hc-scaffold` and `hc-launch`).
96    #[command(external_subcommand)]
97    External(Vec<String>),
98}
99
100impl CliSubcommand {
101    /// Run this command.
102    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}