use colored::*;
use dialoguer::{theme::ColorfulTheme, Input, Select};
use dirs;
use git2::Repository;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
const TEMPLATES: [&str; 6] = [
"React (npx)",
"Next.js (npx)",
"TypeScript Internal Package",
"TypeScript Vite Package",
"ESLint Package",
"TSConfig Package",
];
const TEMPLATE_MAP: [(&str, &str); 4] = [
(TEMPLATES[2], "typescript-internal-pkg"),
(TEMPLATES[3], "typescript-vite-pkg"),
(TEMPLATES[4], "eslint-pkg"),
(TEMPLATES[5], "tsconfig-pkg"),
];
fn print_banner() {
println!(
"{}\n\n{}{}",
r#"
__ _ _
/ _| | | | |
_ __ ___ ___ _ __ ___ | |_ ___ | | __| |
| '_ ` _ \ / _ \| '_ \ / _ \| _/ _ \| |/ _` |
| | | | | | (_) | | | | (_) | || (_) | | (_| |
|_| |_| |_|\___/|_| |_|\___/|_| \___/|_|\__,_|"#
.magenta(),
"A simple scaffolding helper for monorepos\n".magenta(),
format!("Version: {}\n", env!("CARGO_PKG_VERSION")).magenta(),
);
}
fn get_selection() -> Option<usize> {
let display_templates = TEMPLATES
.iter()
.enumerate()
.map(|(i, template)| format!("{}. 📦 {}", i + 1, template))
.collect::<Vec<_>>();
Select::with_theme(&ColorfulTheme::default())
.with_prompt("Please choose an option")
.default(0)
.items(&display_templates[..])
.interact()
.ok()
}
fn get_location(current_dir: &PathBuf, template_dir: &str) -> Option<PathBuf> {
loop {
let location: String = match Input::new()
.with_prompt(&format!(
"Please enter the location where you want to scaffold (relative to the current directory):\n(Hint: {})",
current_dir.display().to_string().magenta()
))
.default(template_dir.to_string())
.interact()
{
Ok(location) => location,
Err(_) => return None,
};
let full_location = current_dir.join(&location);
if !full_location.exists() {
return Some(full_location);
}
println!("🚨 The location already exists. Please enter a new location.");
}
}
fn handle_git_logic() -> Result<(), git2::Error> {
let home_dir = dirs::home_dir().ok_or(git2::Error::from_str("Could not get home directory"))?;
let monofold_dir = home_dir.join(".config/monofold");
let repo_url = "https://github.com/kschio/monofold-templates.git";
if !monofold_dir.exists() {
Repository::clone(repo_url, &monofold_dir)?;
}
let repo = Repository::open(&monofold_dir)?;
let mut remote = repo.find_remote("origin")?;
let fetch_result = remote.fetch(&["master"], None, None);
match fetch_result {
Ok(_) => {
if let Ok(fetch_head) = repo.find_reference("FETCH_HEAD") {
let fetch_commit = repo.find_commit(fetch_head.target().unwrap())?;
repo.checkout_tree(&fetch_commit.into_object(), None)?;
repo.set_head_detached(fetch_head.target().unwrap())?;
} else {
println!("💡Templates are up to date. No new changes to fetch.");
}
}
Err(e) => {
eprintln!("{}", format!("Failed to fetch from remote: {}", e).red());
}
}
Ok(())
}
fn print_success_message(full_location: &PathBuf) {
println!(
"{}{}{}",
"\n🎉 Scaffolding complete\n".magenta().bold(),
"Location -> ".magenta(),
full_location.display().to_string().magenta()
);
}
fn handle_npx_template(template: &str, location: &str, full_location: &PathBuf) {
let npx_check = Command::new("npx")
.arg("--version")
.output()
.expect(&"Failed to execute command".red());
if !npx_check.status.success() {
eprintln!(
"{}",
"npx is not installed. Please install it from https://nodejs.org/en/download/".red()
);
return;
}
Command::new("npx")
.arg(if template == TEMPLATES[0] {
"create-react-app"
} else {
"create-next-app"
})
.arg(location)
.stdin(Stdio::inherit()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .output()
.expect(&"Failed to execute command".red());
print_success_message(full_location);
}
fn handle_non_npx_template(template: &str, full_location: &PathBuf) {
let home_dir = dirs::home_dir().expect(&"Could not get home directory".red());
let monofold_dir = home_dir.join(".config/monofold");
let template_dir = monofold_dir.join(
TEMPLATE_MAP
.iter()
.find(|(key, _)| key == &template)
.unwrap()
.1,
);
if !full_location.exists() {
fs::create_dir_all(&full_location).expect(&"Failed to create directory".red());
}
fs_extra::dir::copy(
&template_dir,
&full_location,
&fs_extra::dir::CopyOptions::new()
.overwrite(true)
.content_only(true),
)
.expect(&"Failed to copy template".red());
print_success_message(full_location);
}
fn main() {
print_banner();
let selection = match get_selection() {
Some(selection) => selection,
None => return,
};
let current_dir = std::env::current_dir().expect("Failed to get current directory");
let selected_template = TEMPLATES[selection];
let template_dir = TEMPLATE_MAP
.iter()
.find(|(template, _)| template == &selected_template)
.map(|(_, dir)| dir)
.unwrap_or(&"");
let full_location = match get_location(¤t_dir, template_dir) {
Some(location) => location,
None => return,
};
if selection >= 2 && selection <= 5 {
if let Err(e) = handle_git_logic() {
eprintln!("{}", format!("Failed to handle git logic: {}", e).red());
return;
}
}
match TEMPLATES[selection] {
"React (npx)" | "Next.js (npx)" => handle_npx_template(
TEMPLATES[selection],
&full_location.to_string_lossy(),
&full_location,
),
_ => handle_non_npx_template(TEMPLATES[selection], &full_location),
}
}