use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command as StdCommand;
use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use kickstart::cli::file_input::load_json_input;
use kickstart::cli::prompt::{ask_bool, ask_choices, ask_integer, ask_string};
use kickstart::cli::terminal;
use kickstart::{HookFile, Template, TemplateDefinition, Value};
#[derive(Parser)]
#[clap(version, author, about, subcommand_negates_reqs = true)]
pub struct Cli {
#[clap(required = true)]
pub template: Option<String>,
#[clap(short = 'o', long, default_value_os_t = PathBuf::from("."))]
pub output_dir: PathBuf,
#[clap(short = 'd', long)]
pub directory: Option<String>,
#[clap(long, default_value_t = false)]
pub no_input: bool,
#[clap(short = 'i', long = "input-file", value_name = "PATH")]
pub input_file: Option<PathBuf>,
#[clap(long, default_value_t = true)]
pub run_hooks: bool,
#[clap(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Validate {
path: PathBuf,
},
}
fn ask_questions(
template: &Template,
no_input: bool,
provided_values: &HashMap<String, Value>,
) -> Result<HashMap<String, Value>> {
let mut vals = HashMap::new();
for var in &template.definition.variables {
if var.derived.unwrap_or(false) {
let default = template.get_default_for(&var.name, &vals)?;
vals.insert(var.name.clone(), default);
continue;
}
if !template.should_ask_variable(&var.name, &vals)? {
continue;
}
let default = template.get_default_for(&var.name, &vals)?;
if let Some(provided) = provided_values.get(&var.name) {
vals.insert(var.name.clone(), provided.clone());
continue;
}
if no_input {
vals.insert(var.name.clone(), default);
continue;
}
let prompt_text = var.prompt.as_deref().unwrap_or("");
if let Some(ref choices) = var.choices {
let res = ask_choices(prompt_text, &default, choices)?;
vals.insert(var.name.clone(), res);
continue;
}
match default {
Value::Boolean(b) => {
let res = ask_bool(prompt_text, b)?;
vals.insert(var.name.clone(), Value::Boolean(res));
}
Value::String(s) => {
let res = ask_string(prompt_text, &s, &var.validation)?;
vals.insert(var.name.clone(), Value::String(res));
}
Value::Integer(i) => {
let res = ask_integer(prompt_text, i)?;
vals.insert(var.name.clone(), Value::Integer(res));
}
}
}
Ok(vals)
}
fn execute_hook(hook: &HookFile, output_dir: &PathBuf) -> Result<()> {
terminal::bold(&format!(" - {}\n", hook.name()));
let mut command = StdCommand::new(hook.path());
if output_dir.exists() {
command.current_dir(output_dir);
}
let code = command.status()?;
if code.success() { Ok(()) } else { bail!("Hook `{}` exited with a non 0 code\n", hook.name()) }
}
fn try_main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Command::Validate { path }) => {
let errs = TemplateDefinition::validate_file(path)?;
if !errs.is_empty() {
let err = format!(
"The template.toml is invalid:\n{}",
errs.into_iter().map(|e| format!("- {}\n", e)).collect::<Vec<_>>().join("\n"),
);
bail!(err);
} else {
terminal::success("The template.toml file is valid!\n");
}
}
None => {
let mut template =
Template::from_input(&cli.template.unwrap(), cli.directory.as_deref())?;
let (no_input, provided_values) = if let Some(ref input_path) = cli.input_file {
(true, load_json_input(input_path, &template)?)
} else {
(cli.no_input, HashMap::new())
};
let vals = ask_questions(&template, no_input, &provided_values)?;
template.set_variables(vals)?;
let pre_gen_hooks = template.get_pre_gen_hooks()?;
if cli.run_hooks && !pre_gen_hooks.is_empty() {
terminal::bold("Running pre-gen hooks...\n");
for hook in &pre_gen_hooks {
execute_hook(hook, &cli.output_dir)?;
}
println!();
}
template.generate(&cli.output_dir)?;
let post_gen_hooks = template.get_post_gen_hooks()?;
if cli.run_hooks && !post_gen_hooks.is_empty() {
terminal::bold("Running post-gen hooks...\n");
for hook in &post_gen_hooks {
execute_hook(hook, &cli.output_dir)?;
}
println!();
}
terminal::success("\nEverything done, ready to go!\n");
}
}
Ok(())
}
fn main() {
if let Err(e) = try_main() {
terminal::error(&format!("Error: {}", e));
let mut cause = e.source();
while let Some(e) = cause {
terminal::error(&format!("\nReason: {}", e));
cause = e.source();
}
::std::process::exit(1)
}
}