quick-arch 0.1.0

powerful CLI tool built in Rust that automates project scaffolding using JSON templates. Generate complete project structures with conditional features in seconds!
use crate::config::{DirectoryItem, FileItem, GenerationStats, ProjectConfig};
use crate::evaluator::{build_context, evaluate_condition};
use crate::ui::{print_step, Status};
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

pub struct ProjectGenerator {
    config: ProjectConfig,
    context: HashMap<String, String>,
    output_dir: PathBuf,
}

impl ProjectGenerator {
    pub fn new(config_path: &str) -> Result<Self> {
        let content = fs::read_to_string(config_path)
            .with_context(|| format!("Could not read config file: {}", config_path))?;

        let config: ProjectConfig =
            serde_json::from_str(&content).with_context(|| "Failed to parse JSON config")?;

        let context = build_context(&config.features)?;
        let output_dir = PathBuf::from(&config.project.name);

        Ok(Self {
            config,
            context,
            output_dir,
        })
    }

    pub fn generate(&mut self, output_arg: Option<&str>) -> Result<GenerationStats> {
        if let Some(out) = output_arg {
            self.output_dir = PathBuf::from(out);
        }

        let project_name = self.config.project.name.clone();
        let output_str = self.output_dir.display().to_string();

        if !self.output_dir.exists() {
            fs::create_dir_all(&self.output_dir)?;
        }

        let total_items = self.config.directories.len() + self.config.files.len();
        let mut current_count = 0;

        crate::ui::print_section("Creating Directories");
        let mut created_dirs = 0;
        let mut skipped_dirs = 0;

        for item in &self.config.directories {
            current_count += 1;
            let (path, condition) = match item {
                DirectoryItem::Simple(p) => (p.clone(), None),
                DirectoryItem::Complex(c) => (c.path.clone(), c.condition.clone()),
            };

            let skip = if let Some(cond) = &condition {
                !evaluate_condition(cond, &self.context)?
            } else {
                false
            };

            if skip {
                print_step(
                    current_count,
                    total_items as u32,
                    "Skip Dir",
                    &path,
                    Status::Skip,
                );
                skipped_dirs += 1;
                continue;
            }

            let full_path = self.output_dir.join(&path);
            if !full_path.exists() {
                fs::create_dir_all(&full_path)?;
                print_step(
                    current_count,
                    total_items as u32,
                    "Create Dir",
                    &path,
                    Status::Success,
                );
                created_dirs += 1;
            } else {
                print_step(
                    current_count,
                    total_items as u32,
                    "Exists Dir",
                    &path,
                    Status::Skip,
                );
                skipped_dirs += 1;
            }
        }

        crate::ui::print_section("Creating Files");
        let mut created_files = 0;
        let mut skipped_files = 0;

        for item in &self.config.files {
            current_count += 1;
            let (path, content, condition) = match item {
                FileItem::Simple(p) => (p.clone(), None, None),
                FileItem::Complex(c) => (c.path.clone(), c.content.clone(), c.condition.clone()),
            };

            let skip = if let Some(cond) = &condition {
                !evaluate_condition(cond, &self.context)?
            } else {
                false
            };

            if skip {
                print_step(
                    current_count,
                    total_items as u32,
                    "Skip File",
                    &path,
                    Status::Skip,
                );
                skipped_files += 1;
                continue;
            }

            let full_path = self.output_dir.join(&path);

            if let Some(parent) = full_path.parent() {
                if !parent.exists() {
                    fs::create_dir_all(parent)?;
                }
            }

            if !full_path.exists() {
                if let Some(text) = content {
                    fs::write(&full_path, text)?;
                } else {
                    fs::File::create(&full_path)?;
                }
                print_step(
                    current_count,
                    total_items as u32,
                    "Create File",
                    &path,
                    Status::Success,
                );
                created_files += 1;
            } else {
                print_step(
                    current_count,
                    total_items as u32,
                    "Exists File",
                    &path,
                    Status::Skip,
                );
                skipped_files += 1;
            }
        }

        let scripts = if self.config.custom_scripts.post_create.is_empty() {
            None
        } else {
            Some(self.config.custom_scripts.post_create.clone())
        };

        Ok(GenerationStats {
            project_name,
            output_dir: output_str,
            dirs_count: created_dirs,
            files_count: created_files,
            skipped_count: skipped_dirs + skipped_files,
            scripts_to_run: scripts,
        })
    }

    pub fn execute_scripts(&self, scripts: &[String]) -> Result<()> {
        for script in scripts {
            crate::ui::print_info(&format!("Running: {}", script));

            let result = if cfg!(target_os = "windows") {
                Command::new("cmd")
                    .args(["/C", script])
                    .current_dir(&self.output_dir)
                    .output()
            } else {
                Command::new("sh")
                    .args(["-c", script])
                    .current_dir(&self.output_dir)
                    .output()
            };

            match result {
                Ok(output) => {
                    if !output.status.success() {
                        crate::ui::print_error(&format!(
                            "Script failed: {}",
                            String::from_utf8_lossy(&output.stderr)
                        ));
                    }
                }
                Err(e) => {
                    crate::ui::print_error(&format!("Failed to execute script: {}", e));
                }
            }
        }
        Ok(())
    }
}