morph-cli 0.1.0

AST-based codebase migration and codemod tool for JavaScript and TypeScript projects.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};

use super::schema::MorphCliSchema;

pub struct ConfigLoader {
    #[allow(dead_code)]
    search_dirs: Vec<PathBuf>,
}

impl ConfigLoader {
    #[allow(dead_code)]
    pub fn new() -> Self {
        Self {
            search_dirs: Vec::new(),
        }
    }

    #[allow(dead_code)]
    pub fn add_search_dir(&mut self, dir: impl AsRef<Path>) {
        self.search_dirs.push(dir.as_ref().to_path_buf());
    }

    #[allow(dead_code)]
    pub fn load(&self) -> Result<MorphCliSchema> {
        if let Some(config_path) = self.find_config()? {
            let content = fs::read_to_string(&config_path)
                .with_context(|| format!("Failed to read {}", config_path.display()))?;

            toml::from_str(&content)
                .with_context(|| format!("Failed to parse {}", config_path.display()))
        } else {
            Ok(MorphCliSchema::default())
        }
    }

    #[allow(dead_code)]
    pub fn find_config(&self) -> Result<Option<PathBuf>> {
        if self.search_dirs.is_empty() {
            let current = std::env::current_dir().context("Failed to get current directory")?;
            return self.find_config_upward(&current);
        }

        for dir in &self.search_dirs {
            if let Some(path) = self.find_config_upward(dir)? {
                return Ok(Some(path));
            }
        }

        Ok(None)
    }

    #[allow(dead_code)]
    fn find_config_upward(&self, start: &Path) -> Result<Option<PathBuf>> {
        let mut current = start.to_path_buf();

        loop {
            let config_path = current.join("morph-cli.toml");
            if config_path.exists() {
                return Ok(Some(config_path));
            }

            if !current.pop() {
                return Ok(None);
            }
        }
    }

    pub fn generate_config(path: &Path) -> Result<PathBuf> {
        let config_path = path.join("morph-cli.toml");

        if config_path.exists() {
            anyhow::bail!("morph-cli.toml already exists at {}", path.display());
        }

        let content = Self::default_config_content();
        fs::write(&config_path, content)
            .with_context(|| format!("Failed to write {}", config_path.display()))?;

        Ok(config_path)
    }

    fn default_config_content() -> String {
        r#"# morph-cli Configuration
# https://github.com/s0r0j/morph-cli

# Enabled recipes for transformation
enabled_recipes = ["commonjs_to_esm"]

# Paths to exclude from scanning
# excluded_paths = ["node_modules", "dist", "build", ".git"]

# Maximum file size in KB (files exceeding this are skipped)
# max_file_size_kb = 500

# Default to dry-run mode
# dry_run_default = true

# Enable backup before transformations
# backup_enabled = true

# Number of lines to show in preview (0 = unlimited)
# preview_lines = 100

# Allow risky transformations (requires manual review)
# allow_risky_transforms = false
"#
        .to_string()
    }
}

impl Default for ConfigLoader {
    fn default() -> Self {
        Self::new()
    }
}

#[allow(dead_code)]
pub fn load_config_for_path(path: &Path) -> Result<MorphCliSchema> {
    let mut loader = ConfigLoader::new();
    loader.add_search_dir(path);
    loader.load()
}

#[allow(dead_code)]
pub fn find_config_upward(start: &Path) -> Result<Option<PathBuf>> {
    let mut current = start.to_path_buf();

    loop {
        let config_path = current.join("morph-cli.toml");
        if config_path.exists() {
            return Ok(Some(config_path));
        }

        if !current.pop() {
            return Ok(None);
        }
    }
}