envx_cli/
project.rs

1use std::path::PathBuf;
2
3use clap::command;
4use clap::{Args, Subcommand};
5use color_eyre::Result;
6use comfy_table::Table;
7use envx_core::{EnvVarManager, ProfileManager, ProjectConfig, ProjectManager, RequiredVar, ValidationReport};
8
9#[derive(Args)]
10pub struct ProjectArgs {
11    #[command(subcommand)]
12    pub command: ProjectCommands,
13}
14
15#[derive(Subcommand)]
16pub enum ProjectCommands {
17    /// Initialize a new project configuration
18    Init {
19        /// Project name
20        #[arg(short, long)]
21        name: Option<String>,
22        /// Custom configuration file path
23        #[arg(short, long)]
24        file: Option<PathBuf>,
25    },
26    /// Apply project configuration
27    Apply {
28        /// Force apply even with validation errors
29        #[arg(long)]
30        force: bool,
31        /// Custom configuration file path
32        #[arg(long)]
33        file: Option<PathBuf>,
34    },
35    /// Validate project configuration
36    Check {
37        /// Custom configuration file path
38        #[arg(long)]
39        file: Option<PathBuf>,
40    },
41    /// Edit project configuration
42    Edit {
43        /// Custom configuration file path
44        #[arg(short, long)]
45        file: Option<PathBuf>,
46    },
47    /// Show project information
48    Info {
49        /// Custom configuration file path
50        #[arg(short, long)]
51        file: Option<PathBuf>,
52    },
53    /// Run a project script
54    Run {
55        /// Script name
56        script: String,
57        /// Custom configuration file path
58        #[arg(short, long)]
59        file: Option<PathBuf>,
60    },
61    /// Add a required variable
62    Require {
63        /// Variable name
64        name: String,
65        /// Description
66        #[arg(short, long)]
67        description: Option<String>,
68        /// Validation pattern (regex)
69        #[arg(short, long)]
70        pattern: Option<String>,
71        /// Example value
72        #[arg(short, long)]
73        example: Option<String>,
74        /// Custom configuration file path
75        #[arg(short, long)]
76        file: Option<PathBuf>,
77    },
78}
79
80/// Handle project-related commands.
81///
82/// # Errors
83///
84/// This function will return an error if:
85/// - Project manager initialization fails
86/// - Environment variable manager operations fail
87/// - Project configuration file cannot be found, read, or written
88/// - Project validation fails (when not using --force)
89/// - Profile manager operations fail
90/// - Script execution fails
91/// - Required variable configuration cannot be updated
92/// - File I/O operations fail during project operations
93#[allow(clippy::too_many_lines)]
94pub fn handle_project(args: ProjectArgs) -> Result<()> {
95    match args.command {
96        ProjectCommands::Init { name, file } => {
97            let manager = ProjectManager::new()?;
98
99            if let Some(custom_file) = file {
100                manager.init_with_file(name, &custom_file)?;
101                println!("āœ… Created project configuration at: {}", custom_file.display());
102            } else {
103                manager.init(name)?;
104            }
105        }
106
107        ProjectCommands::Apply { force, file } => {
108            let mut project = ProjectManager::new()?;
109            let mut env_manager = EnvVarManager::new();
110            let mut profile_manager = ProfileManager::new()?;
111
112            let loaded = if let Some(custom_file) = file {
113                project.load_from_file(&custom_file)?;
114                Some(custom_file.parent().unwrap_or(&PathBuf::from(".")).to_path_buf())
115            } else {
116                project.find_and_load()?
117            };
118
119            if let Some(project_dir) = loaded {
120                println!("šŸ“ Found project at: {}", project_dir.display());
121
122                // Validate first
123                let report = project.validate(&env_manager)?;
124
125                if !report.success && !force {
126                    print_validation_report(&report);
127                    return Err(color_eyre::eyre::eyre!(
128                        "Validation failed. Use --force to apply anyway."
129                    ));
130                }
131
132                // Apply configuration
133                project.apply(&mut env_manager, &mut profile_manager)?;
134                println!("āœ… Applied project configuration");
135
136                if !report.warnings.is_empty() {
137                    println!("\nāš ļø  Warnings:");
138                    for warning in &report.warnings {
139                        println!("  - {}: {}", warning.var_name, warning.message);
140                    }
141                }
142            } else {
143                return Err(color_eyre::eyre::eyre!("No configuration file found"));
144            }
145        }
146
147        ProjectCommands::Check { file } => {
148            let mut project = ProjectManager::new()?;
149            let env_manager = EnvVarManager::new();
150
151            let loaded = if let Some(custom_file) = file {
152                project.load_from_file(&custom_file)?;
153                true
154            } else {
155                project.find_and_load()?.is_some()
156            };
157
158            if loaded {
159                let report = project.validate(&env_manager)?;
160                print_validation_report(&report);
161
162                if !report.success {
163                    std::process::exit(1);
164                }
165            } else {
166                return Err(color_eyre::eyre::eyre!("No project configuration found"));
167            }
168        }
169
170        ProjectCommands::Edit { file } => {
171            let config_path = if let Some(custom_file) = file {
172                custom_file
173            } else {
174                std::env::current_dir()?.join(".envx").join("config.yaml")
175            };
176
177            if !config_path.exists() {
178                return Err(color_eyre::eyre::eyre!(
179                    "Configuration file not found: {}. Run 'envx project init' first.",
180                    config_path.display()
181                ));
182            }
183
184            #[cfg(windows)]
185            {
186                std::process::Command::new("notepad").arg(&config_path).spawn()?;
187            }
188
189            #[cfg(unix)]
190            {
191                let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
192                std::process::Command::new(editor).arg(&config_path).spawn()?;
193            }
194
195            println!("šŸ“ Opening config in editor...");
196        }
197
198        ProjectCommands::Info { file } => {
199            let mut project = ProjectManager::new()?;
200
201            let (project_dir, config_path) = if let Some(custom_file) = file {
202                project.load_from_file(&custom_file)?;
203                (
204                    custom_file.parent().unwrap_or(&PathBuf::from(".")).to_path_buf(),
205                    custom_file,
206                )
207            } else if let Some(project_dir) = project.find_and_load()? {
208                let config_path = project_dir.join(".envx").join("config.yaml");
209                (project_dir, config_path)
210            } else {
211                return Err(color_eyre::eyre::eyre!("No project configuration found"));
212            };
213
214            let content = std::fs::read_to_string(&config_path)?;
215
216            println!("šŸ“ Project Directory: {}", project_dir.display());
217            println!("šŸ“„ Configuration File: {}", config_path.display());
218            println!("\nšŸ“„ Configuration:");
219            println!("{content}");
220        }
221
222        ProjectCommands::Run { script, file } => {
223            let mut project = ProjectManager::new()?;
224            let mut env_manager = EnvVarManager::new();
225
226            let loaded = if let Some(custom_file) = file {
227                project.load_from_file(&custom_file)?;
228                true
229            } else {
230                project.find_and_load()?.is_some()
231            };
232
233            if loaded {
234                project.run_script(&script, &mut env_manager)?;
235                println!("āœ… Script '{script}' completed");
236            } else {
237                return Err(color_eyre::eyre::eyre!("No project configuration found"));
238            }
239        }
240
241        ProjectCommands::Require {
242            name,
243            description,
244            pattern,
245            example,
246            file,
247        } => {
248            let config_path = if let Some(custom_file) = file {
249                custom_file
250            } else {
251                std::env::current_dir()?.join(".envx").join("config.yaml")
252            };
253
254            if !config_path.exists() {
255                return Err(color_eyre::eyre::eyre!(
256                    "Configuration file not found: {}. Run 'envx project init' first.",
257                    config_path.display()
258                ));
259            }
260
261            // Load, modify, and save config
262            let mut config = ProjectConfig::load(&config_path)?;
263            config.required.push(RequiredVar {
264                name: name.clone(),
265                description,
266                pattern,
267                example,
268            });
269            config.save(&config_path)?;
270
271            println!("āœ… Added required variable: {name}");
272            println!("šŸ“„ Updated file: {}", config_path.display());
273        }
274    }
275
276    Ok(())
277}
278
279fn print_validation_report(report: &ValidationReport) {
280    if report.success {
281        println!("āœ… All required variables are set!");
282        return;
283    }
284
285    if !report.missing.is_empty() {
286        println!("āŒ Missing required variables:");
287        let mut table = Table::new();
288        table.set_header(vec!["Variable", "Description", "Example"]);
289
290        for var in &report.missing {
291            table.add_row(vec![
292                var.name.clone(),
293                var.description.clone().unwrap_or_default(),
294                var.example.clone().unwrap_or_default(),
295            ]);
296        }
297
298        println!("{table}");
299    }
300
301    if !report.errors.is_empty() {
302        println!("\nāŒ Validation errors:");
303        for error in &report.errors {
304            println!("  - {}: {}", error.var_name, error.message);
305        }
306    }
307}