cargo_tangle/command/create/
mod.rs1pub 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
13const TANGLE_REQUIRED_KEYS: [&str; 2] = ["project-description", "project-authors"];
15
16const 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 #[arg(long = "gh-username", value_name = "USERNAME")]
32 pub gh_username: Option<String>,
33
34 #[arg(long = "gh-repo", value_name = "REPO")]
36 pub gh_repo: Option<String>,
37
38 #[arg(long = "gh-organization", value_name = "ORG")]
40 pub gh_organization: Option<String>,
41
42 #[arg(long = "project-description", value_name = "TEXT")]
44 pub project_description: Option<String>,
45
46 #[arg(long = "project-authors", value_name = "AUTHORS")]
48 pub project_authors: Option<String>,
49
50 #[arg(long = "project-homepage", value_name = "URL")]
52 pub project_homepage: Option<String>,
53
54 #[arg(long = "flakes", value_name = "BOOL")]
56 pub flakes: Option<bool>,
57
58 #[arg(long = "container", value_name = "BOOL")]
60 pub container: Option<bool>,
61
62 #[arg(long = "base-image", value_name = "IMAGE")]
64 pub base_image: Option<String>,
65
66 #[arg(long = "container-registry", value_name = "REGISTRY")]
68 pub container_registry: Option<String>,
69
70 #[arg(long = "ci", value_name = "BOOL")]
72 pub ci: Option<bool>,
73
74 #[arg(long = "rust-ci", value_name = "BOOL")]
76 pub rust_ci: Option<bool>,
77
78 #[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 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
163pub 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 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 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}