monofold 1.0.1

A simple scaffolding helper for monorepos
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};

// Constants for the templates
const TEMPLATES: [&str; 6] = [
    "React (npx)",
    "Next.js (npx)",
    "TypeScript Internal Package",
    "TypeScript Vite Package",
    "ESLint Package",
    "TSConfig Package",
];

// Mapping of templates to their directory names
const TEMPLATE_MAP: [(&str, &str); 4] = [
    (TEMPLATES[2], "typescript-internal-pkg"),
    (TEMPLATES[3], "typescript-vite-pkg"),
    (TEMPLATES[4], "eslint-pkg"),
    (TEMPLATES[5], "tsconfig-pkg"),
];

// Function to print the banner
fn print_banner() {
    println!(
        "{}\n\n{}{}",
        r#"
                              __      _     _
                             / _|    | |   | |
 _ __ ___   ___  _ __   ___ | |_ ___ | | __| |
| '_ ` _ \ / _ \| '_ \ / _ \|  _/ _ \| |/ _` |
| | | | | | (_) | | | | (_) | || (_) | | (_| |
|_| |_| |_|\___/|_| |_|\___/|_| \___/|_|\__,_|"#
            .magenta(),
        "A simple scaffolding helper for monorepos\n".magenta(),
        format!("Version: {}\n", env!("CARGO_PKG_VERSION")).magenta(),
    );
}

// Function to get the user's selection
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()
}

// Function to get the location from the user
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.");
    }
}

// Function to handle the git logic
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 the directory does not exist, clone the repository
    if !monofold_dir.exists() {
        Repository::clone(repo_url, &monofold_dir)?;
    }

    // Open the repository
    let repo = Repository::open(&monofold_dir)?;

    // Fetch the latest changes from the remote repository
    let mut remote = repo.find_remote("origin")?;
    let fetch_result = remote.fetch(&["master"], None, None);

    match fetch_result {
        Ok(_) => {
            // Check if FETCH_HEAD exists
            if let Ok(fetch_head) = repo.find_reference("FETCH_HEAD") {
                // Get the commit from the FETCH_HEAD
                let fetch_commit = repo.find_commit(fetch_head.target().unwrap())?;

                // Checkout the fetched commit
                repo.checkout_tree(&fetch_commit.into_object(), None)?;

                // Set the HEAD to the fetched commit
                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(())
}

// Function to print the success message
fn print_success_message(full_location: &PathBuf) {
    println!(
        "{}{}{}",
        "\n🎉 Scaffolding complete\n".magenta().bold(),
        "Location -> ".magenta(),
        full_location.display().to_string().magenta()
    );
}

// Function to handle the npx templates
fn handle_npx_template(template: &str, location: &str, full_location: &PathBuf) {
    // Check if npx is installed
    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;
    }

    // If npx is installed, proceed with the command
    Command::new("npx")
        .arg(if template == TEMPLATES[0] {
            "create-react-app"
        } else {
            "create-next-app"
        })
        .arg(location)
        .stdin(Stdio::inherit()) // Inherit stdin from the parent process
        .stdout(Stdio::inherit()) // Inherit stdout from the parent process
        .stderr(Stdio::inherit()) // Inherit stderr from the parent process
        .output()
        .expect(&"Failed to execute command".red());

    print_success_message(full_location);
}

// Function to handle the non-npx templates
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());
    }

    // Copy the template directory to the destination
    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(&current_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),
    }
}