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 Init {
19 #[arg(short, long)]
21 name: Option<String>,
22 #[arg(short, long)]
24 file: Option<PathBuf>,
25 },
26 Apply {
28 #[arg(short, long)]
30 force: bool,
31 #[arg(short, long)]
33 file: Option<PathBuf>,
34 },
35 Check {
37 #[arg(short, long)]
39 file: Option<PathBuf>,
40 },
41 Edit {
43 #[arg(short, long)]
45 file: Option<PathBuf>,
46 },
47 Info {
49 #[arg(short, long)]
51 file: Option<PathBuf>,
52 },
53 Run {
55 script: String,
57 #[arg(short, long)]
59 file: Option<PathBuf>,
60 },
61 Require {
63 name: String,
65 #[arg(short, long)]
67 description: Option<String>,
68 #[arg(short, long)]
70 pattern: Option<String>,
71 #[arg(short, long)]
73 example: Option<String>,
74 #[arg(short, long)]
76 file: Option<PathBuf>,
77 },
78}
79
80#[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 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 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 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}