use std::collections::BTreeMap; use std::fs::{create_dir_all, read, read_dir}; use std::io::Write; use std::path::PathBuf; use clap::{ArgMatches, Command}; use walkdir::WalkDir; use crate::cli::CuddleCli; pub fn build_command(root_cmd: Command, _cli: CuddleCli) -> Command { let mut repo_url = clap::Arg::new("repo").long("repo").short('r'); if let Ok(cuddle_template_url) = std::env::var("CUDDLE_TEMPLATE_URL") { repo_url = repo_url.default_value(cuddle_template_url); } else { repo_url = repo_url.required(true); } let execute_cmd = Command::new("init") .about("init bootstraps a repository from a template") .arg(repo_url) .arg(clap::Arg::new("name")) .arg(clap::Arg::new("path")) .arg(clap::Arg::new("value").short('v').long("value")); root_cmd.subcommand(execute_cmd) } pub fn execute_init(exe_submatch: &ArgMatches, _cli: CuddleCli) -> anyhow::Result<()> { let repo = exe_submatch.get_one::("repo").unwrap(); let name = exe_submatch.get_one::("name"); let path = exe_submatch.get_one::("path"); let values = exe_submatch .get_many::("value") .unwrap_or_default() .collect::>(); tracing::info!("Downloading: {}", repo); create_dir_all(std::env::temp_dir())?; let tmpdir = tempfile::tempdir()?; let tmpdir_path = tmpdir.path().canonicalize()?; let output = std::process::Command::new("git") .args(&["clone", repo, "."]) .current_dir(tmpdir_path) .output()?; std::io::stdout().write_all(&output.stdout)?; std::io::stderr().write_all(&output.stderr)?; let templates_path = tmpdir.path().join("cuddle-templates.json"); let template_path = tmpdir.path().join("cuddle-template.json"); let templates = if templates_path.exists() { let templates = read(templates_path)?; let templates: CuddleTemplates = serde_json::from_slice(&templates)?; let mut single_templates = Vec::new(); for template_name in templates.templates.iter() { let template = read( tmpdir .path() .join(template_name) .join("cuddle-template.json"), )?; let template = serde_json::from_slice::(&template)?; single_templates.push((template_name, template)) } single_templates .into_iter() .map(|(name, template)| (name.clone(), tmpdir.path().join(name), template)) .collect::>() } else if template_path.exists() { let template = read(template_path)?; let template = serde_json::from_slice::(&template)?; vec![(template.clone().name, tmpdir.path().to_path_buf(), template)] } else { anyhow::bail!("No cuddle-template.json or cuddle-templates.json found"); }; let template = match name { Some(name) => { let template = read(tmpdir.path().join(name).join("cuddle-template.json"))?; let template = serde_json::from_slice::(&template)?; Ok((name.clone(), tmpdir.path().join(name), template)) } None => { if templates.len() > 1 { let name = inquire::Select::new( "template", templates.iter().map(|t| t.0.clone()).collect(), ) .with_help_message("name of which template to use") .prompt()?; let found_template = templates .iter() .find(|item| item.0 == name) .ok_or(anyhow::anyhow!("could not find an item with that name"))?; Ok(found_template.clone()) } else if templates.len() == 1 { Ok(templates[0].clone()) } else { Err(anyhow::anyhow!("No templates found, with any valid names")) } } }; let (_name, template_dir, mut template) = template?; let path = match path { Some(path) => path.clone(), None => inquire::Text::new("path") .with_help_message("to where it should be placed") .with_default(".") .prompt()?, }; create_dir_all(&path)?; let dir = std::fs::read_dir(&path)?; if dir.count() != 0 { for entry in read_dir(&path)? { let entry = entry?; if entry.file_name() == ".git" { continue; } else { anyhow::bail!("Directory {} is not empty", &path); } } } { if let Some(ref mut prompt) = template.prompt { 'prompt: for (name, prompt) in prompt { for value in &values { if let Some((value_name, value_content)) = value.split_once("=") { if value_name == name { prompt.value = value_content.to_string(); continue 'prompt; } } } let value = inquire::Text::new(&name) .with_help_message(&prompt.description) .prompt()?; prompt.value = value; } } } for entry in WalkDir::new(&template_dir).follow_links(false) { let entry = entry?; let entry_path = entry.path(); let new_path = PathBuf::from(&path).join(entry_path.strip_prefix(&template_dir)?); let new_path = replace_with_variables(&new_path.to_string_lossy().to_string(), &template)?; let new_path = PathBuf::from(new_path); if entry_path.is_dir() { create_dir_all(&new_path)?; } if entry_path.is_file() { let name = entry.file_name(); if let Some(parent) = entry_path.parent() { create_dir_all(parent)?; } if name == "cuddle-template.json" || name == "cuddle-templates.json" { continue; } tracing::info!("writing to: {}", new_path.display()); let new_content = replace_with_variables(&std::fs::read_to_string(entry_path)?, &template)?; std::fs::write(new_path, new_content.as_bytes())?; } } Ok(()) } fn replace_with_variables(content: &str, template: &CuddleTemplate) -> anyhow::Result { let mut content = content.to_string(); if let Some(prompt) = &template.prompt { for (name, value) in prompt { content = content.replace(&format!("%%{}%%", name), &value.value); } } Ok(content) } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct CuddleTemplates { pub templates: Vec, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct CuddleTemplate { pub name: String, pub prompt: Option>, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct CuddleTemplatePrompt { pub description: String, #[serde(skip)] pub value: String, }