Skip to main content

morph_cli/core/config/
loader.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5
6use super::schema::MorphCliSchema;
7
8pub struct ConfigLoader {
9    #[allow(dead_code)]
10    search_dirs: Vec<PathBuf>,
11}
12
13impl ConfigLoader {
14    #[allow(dead_code)]
15    pub fn new() -> Self {
16        Self {
17            search_dirs: Vec::new(),
18        }
19    }
20
21    #[allow(dead_code)]
22    pub fn add_search_dir(&mut self, dir: impl AsRef<Path>) {
23        self.search_dirs.push(dir.as_ref().to_path_buf());
24    }
25
26    #[allow(dead_code)]
27    pub fn load(&self) -> Result<MorphCliSchema> {
28        if let Some(config_path) = self.find_config()? {
29            let content = fs::read_to_string(&config_path)
30                .with_context(|| format!("Failed to read {}", config_path.display()))?;
31
32            toml::from_str(&content)
33                .with_context(|| format!("Failed to parse {}", config_path.display()))
34        } else {
35            Ok(MorphCliSchema::default())
36        }
37    }
38
39    #[allow(dead_code)]
40    pub fn find_config(&self) -> Result<Option<PathBuf>> {
41        if self.search_dirs.is_empty() {
42            let current = std::env::current_dir().context("Failed to get current directory")?;
43            return self.find_config_upward(&current);
44        }
45
46        for dir in &self.search_dirs {
47            if let Some(path) = self.find_config_upward(dir)? {
48                return Ok(Some(path));
49            }
50        }
51
52        Ok(None)
53    }
54
55    #[allow(dead_code)]
56    fn find_config_upward(&self, start: &Path) -> Result<Option<PathBuf>> {
57        let mut current = start.to_path_buf();
58
59        loop {
60            let config_path = current.join("morph-cli.toml");
61            if config_path.exists() {
62                return Ok(Some(config_path));
63            }
64
65            if !current.pop() {
66                return Ok(None);
67            }
68        }
69    }
70
71    pub fn generate_config(path: &Path) -> Result<PathBuf> {
72        let config_path = path.join("morph-cli.toml");
73
74        if config_path.exists() {
75            anyhow::bail!("morph-cli.toml already exists at {}", path.display());
76        }
77
78        let content = Self::default_config_content();
79        fs::write(&config_path, content)
80            .with_context(|| format!("Failed to write {}", config_path.display()))?;
81
82        Ok(config_path)
83    }
84
85    fn default_config_content() -> String {
86        r#"# morph-cli Configuration
87# https://github.com/s0r0j/morph-cli
88
89# Enabled recipes for transformation
90enabled_recipes = ["commonjs_to_esm"]
91
92# Paths to exclude from scanning
93# excluded_paths = ["node_modules", "dist", "build", ".git"]
94
95# Maximum file size in KB (files exceeding this are skipped)
96# max_file_size_kb = 500
97
98# Default to dry-run mode
99# dry_run_default = true
100
101# Enable backup before transformations
102# backup_enabled = true
103
104# Number of lines to show in preview (0 = unlimited)
105# preview_lines = 100
106
107# Allow risky transformations (requires manual review)
108# allow_risky_transforms = false
109"#
110        .to_string()
111    }
112}
113
114impl Default for ConfigLoader {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120#[allow(dead_code)]
121pub fn load_config_for_path(path: &Path) -> Result<MorphCliSchema> {
122    let mut loader = ConfigLoader::new();
123    loader.add_search_dir(path);
124    loader.load()
125}
126
127#[allow(dead_code)]
128pub fn find_config_upward(start: &Path) -> Result<Option<PathBuf>> {
129    let mut current = start.to_path_buf();
130
131    loop {
132        let config_path = current.join("morph-cli.toml");
133        if config_path.exists() {
134            return Ok(Some(config_path));
135        }
136
137        if !current.pop() {
138            return Ok(None);
139        }
140    }
141}