Skip to main content

cargo_tangle/command/create/
mod.rs

1pub use crate::command::create::error::Error;
2pub use crate::command::create::source::Source;
3pub use crate::command::create::types::BlueprintType;
4use crate::foundry::FoundryToolchain;
5use clap::Args;
6use std::collections::HashMap;
7use types::{BlueprintVariant, EigenlayerVariant};
8
9pub mod error;
10pub mod source;
11pub mod types;
12
13/// Required template keys for Tangle templates
14const TANGLE_REQUIRED_KEYS: [&str; 2] = ["project-description", "project-authors"];
15
16/// Required template keys for EigenLayer templates (more complex)
17const EIGENLAYER_REQUIRED_KEYS: [&str; 6] = [
18    "gh-username",
19    "gh-repo",
20    "gh-organization",
21    "project-description",
22    "project-homepage",
23    "container",
24];
25
26const CONTAINER_TEMPLATE_KEYS: [&str; 2] = ["base-image", "container-registry"];
27
28#[derive(Debug, Clone, Default, Args)]
29pub struct TemplateVariables {
30    /// GitHub username associated with the blueprint repository
31    #[arg(long = "gh-username", value_name = "USERNAME")]
32    pub gh_username: Option<String>,
33
34    /// GitHub repository name for this blueprint
35    #[arg(long = "gh-repo", value_name = "REPO")]
36    pub gh_repo: Option<String>,
37
38    /// GitHub organization or user that owns the repository
39    #[arg(long = "gh-organization", value_name = "ORG")]
40    pub gh_organization: Option<String>,
41
42    /// Short description of the project
43    #[arg(long = "project-description", value_name = "TEXT")]
44    pub project_description: Option<String>,
45
46    /// Authors of the project (comma-separated list)
47    #[arg(long = "project-authors", value_name = "AUTHORS")]
48    pub project_authors: Option<String>,
49
50    /// Homepage or documentation URL
51    #[arg(long = "project-homepage", value_name = "URL")]
52    pub project_homepage: Option<String>,
53
54    /// Enable Nix flakes support
55    #[arg(long = "flakes", value_name = "BOOL")]
56    pub flakes: Option<bool>,
57
58    /// Generate container assets
59    #[arg(long = "container", value_name = "BOOL")]
60    pub container: Option<bool>,
61
62    /// Base image to use when generating containers
63    #[arg(long = "base-image", value_name = "IMAGE")]
64    pub base_image: Option<String>,
65
66    /// Container registry for pushing images
67    #[arg(long = "container-registry", value_name = "REGISTRY")]
68    pub container_registry: Option<String>,
69
70    /// Enable CI workflows
71    #[arg(long = "ci", value_name = "BOOL")]
72    pub ci: Option<bool>,
73
74    /// Enable Rust-specific CI workflows
75    #[arg(long = "rust-ci", value_name = "BOOL")]
76    pub rust_ci: Option<bool>,
77
78    /// Enable release CI workflows
79    #[arg(long = "release-ci", value_name = "BOOL")]
80    pub release_ci: Option<bool>,
81}
82
83impl TemplateVariables {
84    pub fn merge_into(self, define: &mut Vec<String>) {
85        Self::push("gh-username", self.gh_username, define);
86        Self::push("gh-repo", self.gh_repo, define);
87        Self::push("gh-organization", self.gh_organization, define);
88        Self::push("project-description", self.project_description, define);
89        Self::push("project-authors", self.project_authors, define);
90        Self::push("project-homepage", self.project_homepage, define);
91        Self::push("flakes", self.flakes, define);
92        Self::push("container", self.container, define);
93        Self::push("base-image", self.base_image, define);
94        Self::push("container-registry", self.container_registry, define);
95        Self::push("ci", self.ci, define);
96        Self::push("rust-ci", self.rust_ci, define);
97        Self::push("release-ci", self.release_ci, define);
98    }
99
100    fn push<T: ToString>(key: &str, value: Option<T>, define: &mut Vec<String>) {
101        if let Some(value) = value {
102            define.push(format!("{key}={}", value.to_string()));
103        }
104    }
105}
106
107fn ensure_default_bool(define: &mut Vec<String>, key: &str, default: bool) {
108    let key_eq = format!("{key}=");
109    if define.iter().any(|entry| entry.starts_with(&key_eq)) {
110        return;
111    }
112
113    define.push(format!("{key}={}", default));
114}
115
116fn missing_required_template_variables(define: &[String], is_tangle: bool) -> Vec<&'static str> {
117    let provided = build_define_map(define);
118    let mut missing = Vec::new();
119
120    let required_keys: &[&str] = if is_tangle {
121        &TANGLE_REQUIRED_KEYS
122    } else {
123        &EIGENLAYER_REQUIRED_KEYS
124    };
125
126    for key in required_keys {
127        if !provided.contains_key(*key) {
128            missing.push(*key);
129        }
130    }
131
132    // Container fields only required for EigenLayer templates
133    if !is_tangle && container_fields_required(&provided) {
134        for key in CONTAINER_TEMPLATE_KEYS {
135            if !provided.contains_key(key) {
136                missing.push(key);
137            }
138        }
139    }
140
141    missing
142}
143
144fn build_define_map(define: &[String]) -> HashMap<String, String> {
145    let mut map = HashMap::new();
146    for entry in define {
147        if let Some((key, value)) = entry.split_once('=') {
148            map.insert(key.to_string(), value.to_string());
149        }
150    }
151    map
152}
153
154fn container_fields_required(provided: &HashMap<String, String>) -> bool {
155    provided.get("container").is_none_or(|value| {
156        matches!(
157            value.trim().to_ascii_lowercase().as_str(),
158            "true" | "1" | "yes"
159        )
160    })
161}
162
163/// Generate a new blueprint from a template
164///
165/// # Errors
166///
167/// See [`cargo_generate::generate()`]
168///
169/// # Parameters
170///
171/// * `name` - The name of the blueprint
172/// * `source` - Optional source information (repo, branch, path)
173/// * `blueprint_type` - Optional blueprint type (Tangle or Eigenlayer)
174/// * `define` - Template variable definitions (key=value pairs)
175/// * `template_variables` - Typed template variable overrides supplied via CLI flags
176/// * `template_values_file` - Optional path to a file containing template values
177/// * `skip_prompts` - Whether to skip all interactive prompts, using defaults for unspecified values
178pub fn new_blueprint(
179    name: &str,
180    source: Option<Source>,
181    blueprint_type: Option<BlueprintType>,
182    mut define: Vec<String>,
183    template_variables: TemplateVariables,
184    template_values_file: &Option<String>,
185    skip_prompts: bool,
186) -> Result<(), Error> {
187    println!("Generating blueprint with name: {}", name);
188
189    let source = source.unwrap_or_default();
190    let blueprint_variant = blueprint_type.map(|t| t.get_type()).unwrap_or_default();
191    let template_path_opt: Option<cargo_generate::TemplatePath> = source.into();
192
193    let template_path = template_path_opt.unwrap_or_else(|| {
194        // TODO: Interactive selection (#352)
195        let template_repo: String = match blueprint_variant {
196            Some(BlueprintVariant::Tangle) | None => {
197                "https://github.com/tangle-network/blueprint-template".into()
198            }
199            Some(BlueprintVariant::Eigenlayer(EigenlayerVariant::BLS)) => {
200                "https://github.com/tangle-network/eigenlayer-bls-template".into()
201            }
202            Some(BlueprintVariant::Eigenlayer(EigenlayerVariant::ECDSA)) => {
203                "https://github.com/tangle-network/eigenlayer-ecdsa-template".into()
204            }
205        };
206
207        cargo_generate::TemplatePath {
208            git: Some(template_repo),
209            branch: Some(String::from("main")),
210            ..Default::default()
211        }
212    });
213
214    // Determine if this is a Tangle template (simpler requirements)
215    let is_tangle = matches!(blueprint_variant, None | Some(BlueprintVariant::Tangle));
216
217    template_variables.merge_into(&mut define);
218    ensure_default_bool(&mut define, "flakes", true);
219    ensure_default_bool(&mut define, "ci", true);
220    ensure_default_bool(&mut define, "rust-ci", true);
221    ensure_default_bool(&mut define, "release-ci", true);
222
223    if skip_prompts {
224        println!(
225            "Skipping prompts; all template variables must be provided via CLI flags when using --skip-prompts."
226        );
227        let missing = missing_required_template_variables(&define, is_tangle);
228        if !missing.is_empty() {
229            let missing_list = missing.join(", ");
230            return Err(Error::MissingTemplateVariables(missing_list));
231        }
232    } else {
233        println!("Running in interactive mode - will prompt for template variables as needed");
234    }
235
236    if !define.is_empty() {
237        println!("Using template variables: {:?}", define);
238    }
239    let (silent, template_values_file) = if let Some(file) = &template_values_file {
240        println!("Using template values file: {}", file);
241        (true, Some(file.clone()))
242    } else {
243        (false, None)
244    };
245
246    let path = cargo_generate::generate(cargo_generate::GenerateArgs {
247        template_path,
248        list_favorites: false,
249        name: Some(name.to_string()),
250        force: false,
251        verbose: false,
252        template_values_file,
253        silent,
254        config: None,
255        vcs: Some(cargo_generate::Vcs::Git),
256        lib: false,
257        bin: false,
258        ssh_identity: None,
259        gitconfig: None,
260        define,
261        init: false,
262        destination: None,
263        force_git_init: false,
264        allow_commands: false,
265        overwrite: false,
266        skip_submodules: false,
267        other_args: Option::default(),
268        continue_on_error: false,
269        quiet: false,
270        no_workspace: false,
271    })
272    .map_err(Error::GenerationFailed)?;
273
274    println!("Blueprint generated at: {}", path.display());
275
276    let foundry = FoundryToolchain::new();
277    if !foundry.forge.is_installed() {
278        blueprint_core::warn!("Forge not installed, skipping dependencies");
279        blueprint_core::warn!("NOTE: See <https://getfoundry.sh>");
280        blueprint_core::warn!(
281            "NOTE: After installing Forge, you can run `forge soldeer update -d` to install dependencies"
282        );
283        return Ok(());
284    }
285
286    std::env::set_current_dir(path)?;
287    if let Err(e) = foundry.forge.install_dependencies() {
288        blueprint_core::error!("{e}");
289    }
290
291    Ok(())
292}