1#![allow(clippy::format_push_string)]
2use color_eyre::Result;
3use color_eyre::eyre::eyre;
4use dialoguer::{Confirm, Input, MultiSelect, Select, theme::ColorfulTheme};
5use std::{
6    fs,
7    path::{Path, PathBuf},
8};
9
10use ahash::AHashMap as HashMap;
11use colored::Colorize;
12use glob::glob;
13use serde::{Deserialize, Serialize};
14
15use crate::{ProfileManager, ProjectConfig, RequiredVar, ValidationRules as ConfigValidationRules};
16
17#[derive(Debug)]
19struct EscPressed;
20
21impl std::fmt::Display for EscPressed {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "User pressed ESC")
24    }
25}
26
27impl std::error::Error for EscPressed {}
28
29#[derive(Debug, Clone, Serialize, Deserialize, Default)]
30pub struct WizardConfig {
31    pub skip_system_check: bool,
32    pub auto_detect_project: bool,
33    pub default_profiles: Vec<String>,
34    pub template_path: Option<PathBuf>,
35    pub selected_vars: Vec<SelectedVariable>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SelectedVariable {
40    pub name: String,
41    pub value: String,
42    pub description: String,
43    pub required: bool,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ProjectType {
48    pub name: String,
49    pub category: ProjectCategory,
50    pub suggested_vars: Vec<SuggestedVariable>,
51    pub suggested_profiles: Vec<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum ProjectCategory {
56    WebApp,
57    Python,
58    Rust,
59    Docker,
60    Microservices,
61    Custom,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct SuggestedVariable {
66    pub name: String,
67    pub description: String,
68    pub example: String,
69    pub required: bool,
70    pub sensitive: bool,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct TeamConfig {
75    pub config_path: PathBuf,
76    pub git_hooks: bool,
77    pub ci_integration: bool,
78    pub shared_profiles: bool,
79}
80
81#[allow(clippy::struct_excessive_bools)]
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ValidationRules {
84    pub require_all_defined: bool,
85    pub validate_urls: bool,
86    pub validate_numbers: bool,
87    pub warn_missing: bool,
88    pub strict_mode: bool,
89    pub custom_patterns: HashMap<String, String>,
90}
91
92#[allow(clippy::struct_excessive_bools)]
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct Integrations {
95    pub shell_aliases: bool,
96    pub auto_completion: bool,
97    pub vscode_extension: bool,
98    pub git_hooks: bool,
99    pub docker_integration: bool,
100}
101
102#[derive(Debug, Clone)]
103pub struct SystemInfo {
104    pub os: String,
105    pub shell: String,
106    pub terminal: String,
107    pub home_dir: PathBuf,
108    pub config_dir: PathBuf,
109}
110
111impl SystemInfo {
112    pub fn detect() -> Result<Self> {
120        let os = if cfg!(windows) {
121            "Windows".to_string()
122        } else if cfg!(target_os = "macos") {
123            "macOS".to_string()
124        } else {
125            "Linux".to_string()
126        };
127
128        let shell = std::env::var("SHELL").unwrap_or_else(|_| {
129            if cfg!(windows) {
130                "PowerShell".to_string()
131            } else {
132                "bash".to_string()
133            }
134        });
135
136        let terminal = std::env::var("TERM_PROGRAM")
137            .or_else(|_| std::env::var("TERMINAL"))
138            .unwrap_or_else(|_| "Unknown".to_string());
139
140        let home_dir = dirs::home_dir().ok_or_else(|| eyre!("Could not find home directory"))?;
141        let config_dir = dirs::config_dir().ok_or_else(|| eyre!("Could not find config directory"))?;
142
143        Ok(Self {
144            os,
145            shell,
146            terminal,
147            home_dir,
148            config_dir,
149        })
150    }
151}
152
153#[derive(Default)]
154pub struct SetupWizard {
155    theme: ColorfulTheme,
156    config: WizardConfig,
157}
158
159#[derive(Debug, Clone)]
160pub struct SetupResult {
161    pub project_type: ProjectType,
162    pub profiles: Vec<String>,
163    pub profile_configs: HashMap<String, HashMap<String, String>>,
164    pub team_config: Option<TeamConfig>,
165    pub validation_rules: ValidationRules,
166    pub imported_files: Vec<PathBuf>,
167    pub create_env_files: bool,
168    pub selected_vars: Vec<SelectedVariable>,
169}
170
171impl SetupWizard {
172    #[must_use]
173    pub fn new() -> Self {
174        Self::default()
175    }
176
177    pub fn run(&mut self) -> Result<SetupResult> {
188        match self.run_wizard() {
190            Ok(result) => Ok(result),
191            Err(e) => {
192                if e.downcast_ref::<EscPressed>().is_some() {
193                    Self::show_goodbye();
194                    std::process::exit(0);
195                } else {
196                    Err(e)
197                }
198            }
199        }
200    }
201
202    fn run_wizard(&mut self) -> Result<SetupResult> {
203        Self::show_welcome()?;
205
206        let system_info = Self::detect_system()?;
208        Self::show_system_info(&system_info);
209
210        let project_type = self.select_project_type()?;
212
213        let imported_files = if let Some(existing_files) = self.scan_existing_files()? {
215            self.import_existing(existing_files)?
216        } else {
217            Vec::new()
218        };
219
220        let selected_vars = self.configure_variables(&project_type)?;
222
223        let (profiles, profile_configs) = self.create_and_configure_profiles(&project_type, &selected_vars)?;
225
226        let create_env_files = self.ask_create_env_files()?;
228
229        let team_config = if self.ask_team_setup()? {
231            Some(self.configure_team_features()?)
232        } else {
233            None
234        };
235
236        let validation_rules = self.configure_validation(&project_type)?;
238
239        let result = SetupResult {
241            project_type: project_type.clone(),
242            profiles,
243            profile_configs,
244            team_config,
245            validation_rules,
246            imported_files,
247            create_env_files,
248            selected_vars,
249        };
250
251        self.review_and_apply(&result)?;
252
253        Self::check_required_variables(&result);
255
256        Ok(result)
257    }
258
259    fn show_goodbye() {
260        println!("\n{}", "ā".repeat(65).bright_black());
261        println!(
262            "\n{} {}",
263            "š".bright_yellow(),
264            "Setup cancelled. No worries!".bright_cyan().bold()
265        );
266        println!(
267            "\n{}",
268            "You can run 'envx init' anytime to start the setup wizard again.".bright_white()
269        );
270        println!("{}", "Your project files remain unchanged.".bright_white());
271        println!("\n{}", "ā".repeat(65).bright_black());
272        println!("\n{}", "Happy coding! š".bright_magenta());
273    }
274
275    #[allow(clippy::too_many_lines)]
276    fn show_welcome() -> Result<()> {
277        print!("\x1B[2J\x1B[1;1H");
279
280        println!(
282            "{}",
283            "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā®".bright_cyan()
284        );
285        println!(
286            "{}",
287            "ā                                                             ā".bright_cyan()
288        );
289        println!(
290            "{}",
291            "ā  āāāāāāāāāāāā   āāāāāā   āāāāāā  āāā                        ā".bright_cyan()
292        );
293        println!(
294            "{}",
295            "ā  āāāāāāāāāāāāā  āāāāāā   āāāāāāāāāāā                        ā".bright_cyan()
296        );
297        println!(
298            "{}",
299            "ā  āāāāāā  āāāāāā āāāāāā   āāā āāāāāā                         ā".bright_cyan()
300        );
301        println!(
302            "{}",
303            "ā  āāāāāā  āāāāāāāāāāāāāā āāāā āāāāāā                         ā".bright_cyan()
304        );
305        println!(
306            "{}",
307            "ā  āāāāāāāāāāā āāāāāā āāāāāāā  āāāā āāā                       ā".bright_cyan()
308        );
309        println!(
310            "{}",
311            "ā  āāāāāāāāāāā  āāāāā  āāāāā   āāā  āāā                       ā".bright_cyan()
312        );
313        println!(
314            "{}",
315            "ā                                                             ā".bright_cyan()
316        );
317        println!(
318            "{}",
319            format!(
320                "ā           Environment Variable Manager v{:<8}            ā",
321                env!("CARGO_PKG_VERSION")
322            )
323            .bright_cyan()
324        );
325        println!(
326            "{}",
327            "ā°āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāÆ".bright_cyan()
328        );
329
330        println!(
331            "\n{} {} {}",
332            "āØ".bright_yellow(),
333            "Welcome to envx!".bright_white().bold(),
334            "Your intelligent environment variable companion".bright_blue()
335        );
336
337        println!("\n{}", "ā".repeat(65).bright_black());
338
339        println!("\n{}", "This setup wizard will help you:".bright_white());
341
342        let features = vec![
343            (
344                "š",
345                "Define environment variables",
346                "Set up your project's environment",
347            ),
348            (
349                "š",
350                "Create profiles",
351                "Configure dev, test, and production environments",
352            ),
353            ("š¦", "Import existing files", "Seamlessly migrate from .env files"),
354            ("š", "Generate .env files", "Create .env files for each profile"),
355            ("š„", "Enable team features", "Share configurations with your team"),
356        ];
357
358        for (icon, title, desc) in features {
359            println!(
360                "  {} {} {}",
361                icon,
362                format!("{title:<22}").bright_green().bold(),
363                format!("ā {desc}").bright_black()
364            );
365        }
366
367        println!("\n{}", "ā".repeat(65).bright_black());
368
369        println!(
371            "\n{} {} {}",
372            "ā±ļø ".bright_blue(),
373            "Estimated time:".bright_white(),
374            "1-3 minutes".bright_yellow().bold()
375        );
376
377        println!(
379            "\n{} {}",
380            "š”".bright_yellow(),
381            "Tip: Press ESC at any time to exit the wizard".bright_white().italic()
382        );
383
384        println!(
386            "\n{}",
387            "Let's create the perfect setup for your project! šÆ".bright_magenta()
388        );
389
390        let continue_prompt = format!(
391            "\n{} {} {} {} {}",
392            "Type".bright_black(),
393            "[y (yes)]".bright_green().bold(),
394            "to begin your journey,".bright_black(),
395            "[n (no)]".bright_red().bold(),
396            "to skip, or [ESC] to exit".bright_black()
397        );
398
399        print!("\n{}", "Initializing".bright_cyan());
401        for _ in 0..3 {
402            std::thread::sleep(std::time::Duration::from_millis(400));
403            print!("{}", ".".bright_cyan());
404            std::io::Write::flush(&mut std::io::stdout())?;
405        }
406        println!(" {}", "Ready!".bright_green().bold());
407
408        println!("{continue_prompt}");
409
410        let welcome_theme = ColorfulTheme {
412            prompt_style: dialoguer::console::Style::new().cyan().bold(),
413            ..ColorfulTheme::default()
414        };
415
416        let result = Confirm::with_theme(&welcome_theme)
417            .with_prompt("")
418            .default(true)
419            .show_default(false)
420            .wait_for_newline(true)
421            .interact_opt()?;
422
423        match result {
424            Some(true) => {
425                println!(
427                    "\n{} {}",
428                    "š".bright_yellow(),
429                    "Great choice! Let's build something amazing together."
430                        .bright_green()
431                        .bold()
432                );
433                std::thread::sleep(std::time::Duration::from_millis(1000));
434                Ok(())
435            }
436            Some(false) => {
437                println!(
438                    "\n{} {}",
439                    "š".bright_yellow(),
440                    "No problem! You can run 'envx init' anytime to set up.".bright_blue()
441                );
442                std::process::exit(0);
443            }
444            None => Err(EscPressed.into()),
445        }
446    }
447
448    fn detect_system() -> Result<SystemInfo> {
449        println!("\nš Detecting your system...");
450
451        let info = SystemInfo::detect()?;
452        Ok(info)
453    }
454
455    fn show_system_info(info: &SystemInfo) {
456        println!("ā OS: {}", info.os);
457        println!("ā Shell: {}", info.shell);
458        println!("ā Terminal: {}", info.terminal);
459        println!("ā Envx Version: {}\n", env!("CARGO_PKG_VERSION"));
460    }
461
462    fn ask_team_setup(&self) -> Result<bool> {
463        match Confirm::with_theme(&self.theme)
464            .with_prompt("Are you working in a team?")
465            .default(false)
466            .interact_opt()?
467        {
468            Some(value) => Ok(value),
469            None => Err(EscPressed.into()),
470        }
471    }
472
473    fn ask_create_env_files(&self) -> Result<bool> {
474        match Confirm::with_theme(&self.theme)
475            .with_prompt("\nWould you like to create .env files for your profiles?")
476            .default(true)
477            .interact_opt()?
478        {
479            Some(value) => Ok(value),
480            None => Err(EscPressed.into()),
481        }
482    }
483
484    pub fn select_project_type(&self) -> Result<ProjectType> {
493        let options = vec![
494            "Web Application (Node.js, React, etc.)",
495            "Python Application",
496            "Rust Application",
497            "Docker/Container-based",
498            "Multi-service/Microservices",
499            "Other/Custom",
500        ];
501
502        let Some(selection) = Select::with_theme(&self.theme)
503            .with_prompt("What type of project are you working on?")
504            .items(&options)
505            .interact_opt()?
506        else {
507            return Err(EscPressed.into());
508        };
509
510        let project_type = match selection {
511            0 => Self::create_web_app_type(),
512            1 => Self::create_python_type(),
513            2 => Self::create_rust_type(),
514            3 => Self::create_docker_type(),
515            4 => Self::create_microservices_type(),
516            _ => self.create_custom_type()?,
517        };
518
519        Ok(project_type)
520    }
521
522    fn configure_variables(&mut self, project_type: &ProjectType) -> Result<Vec<SelectedVariable>> {
523        let mut selected_vars = Vec::new();
524
525        if !project_type.suggested_vars.is_empty() {
527            println!("\nš Let's configure variables for your {} project:", project_type.name);
528
529            let options: Vec<String> = project_type
530                .suggested_vars
531                .iter()
532                .map(|var| {
533                    let required_marker = if var.required { " (required)" } else { "" };
534                    format!("{} - {}{}", var.name, var.description, required_marker)
535                })
536                .collect();
537
538            let defaults: Vec<bool> = project_type.suggested_vars.iter().map(|var| var.required).collect();
539
540            let Some(selections) = MultiSelect::with_theme(&self.theme)
541                .with_prompt("Select variables to configure (Space to toggle, Enter to continue)")
542                .items(&options)
543                .defaults(&defaults)
544                .interact_opt()?
545            else {
546                return Err(EscPressed.into());
547            };
548
549            if !selections.is_empty() {
551                println!("\nš§ Configure variable values:");
552                for &idx in &selections {
553                    let var = &project_type.suggested_vars[idx];
554
555                    let value = Input::<String>::with_theme(&self.theme)
556                        .with_prompt(format!("{} ({})", var.name, var.description))
557                        .default(var.example.clone())
558                        .interact()?;
559
560                    selected_vars.push(SelectedVariable {
561                        name: var.name.clone(),
562                        value,
563                        description: var.description.clone(),
564                        required: var.required,
565                    });
566                }
567            }
568        }
569
570        println!("\nā Custom Environment Variables");
572        let Some(add_custom) = Confirm::with_theme(&self.theme)
573            .with_prompt("Would you like to add custom environment variables?")
574            .default(true)
575            .interact_opt()?
576        else {
577            return Err(EscPressed.into());
578        };
579
580        if add_custom {
581            loop {
582                println!("\nš Add a custom variable:");
583
584                let var_name = Input::<String>::with_theme(&self.theme)
585                    .with_prompt("Variable name (or press Enter to finish)")
586                    .allow_empty(true)
587                    .interact()?;
588
589                if var_name.is_empty() {
590                    break;
591                }
592
593                let description = Input::<String>::with_theme(&self.theme)
594                    .with_prompt("Description")
595                    .default(format!("{var_name} configuration"))
596                    .interact()?;
597
598                let value = Input::<String>::with_theme(&self.theme)
599                    .with_prompt("Value")
600                    .default("your-value-here".to_string())
601                    .interact()?;
602
603                let Some(required) = Confirm::with_theme(&self.theme)
604                    .with_prompt("Is this variable required?")
605                    .default(false)
606                    .interact_opt()?
607                else {
608                    return Err(EscPressed.into());
609                };
610
611                selected_vars.push(SelectedVariable {
612                    name: var_name,
613                    value,
614                    description,
615                    required,
616                });
617
618                let Some(add_more) = Confirm::with_theme(&self.theme)
619                    .with_prompt("Add another custom variable?")
620                    .default(true)
621                    .interact_opt()?
622                else {
623                    return Err(EscPressed.into());
624                };
625
626                if !add_more {
627                    break;
628                }
629            }
630        }
631
632        self.config.selected_vars.clone_from(&selected_vars);
633        Ok(selected_vars)
634    }
635
636    fn create_and_configure_profiles(
637        &self,
638        project_type: &ProjectType,
639        selected_vars: &[SelectedVariable],
640    ) -> Result<(Vec<String>, HashMap<String, HashMap<String, String>>)> {
641        println!("\nš Let's create environment profiles:");
642
643        let mut profiles = Vec::new();
644        let mut profile_configs = HashMap::new();
645
646        let suggested = &project_type.suggested_profiles;
648        let mut options: Vec<String> = suggested
649            .iter()
650            .map(|p| {
651                format!(
652                    "{} ({})",
653                    p,
654                    match p.as_str() {
655                        "development" => "local development",
656                        "testing" => "running tests",
657                        "staging" => "pre-production",
658                        "production" => "live environment",
659                        _ => "custom",
660                    }
661                )
662            })
663            .collect();
664
665        options.push("Add custom profile".to_string());
666
667        let defaults: Vec<bool> = vec![true, false, false, false]; let Some(selections) = MultiSelect::with_theme(&self.theme)
670            .with_prompt("Select profiles to create")
671            .items(&options)
672            .defaults(&defaults)
673            .interact_opt()?
674        else {
675            return Err(EscPressed.into());
676        };
677
678        for &idx in &selections {
680            if idx < suggested.len() {
681                profiles.push(suggested[idx].clone());
682            } else if idx == options.len() - 1 {
683                let custom_name = Input::<String>::with_theme(&self.theme)
685                    .with_prompt("Enter custom profile name")
686                    .interact()?;
687
688                if !custom_name.is_empty() {
689                    profiles.push(custom_name);
690                }
691            }
692        }
693
694        loop {
696            let Some(add_more) = Confirm::with_theme(&self.theme)
697                .with_prompt("Add another custom profile?")
698                .default(false)
699                .interact_opt()?
700            else {
701                return Err(EscPressed.into());
702            };
703
704            if !add_more {
705                break;
706            }
707
708            let custom_name = Input::<String>::with_theme(&self.theme)
709                .with_prompt("Enter profile name")
710                .interact()?;
711
712            if !custom_name.is_empty() && !profiles.contains(&custom_name) {
713                profiles.push(custom_name);
714            }
715        }
716
717        for profile in &profiles {
719            println!("\nāļø  Configuring '{profile}' profile:");
720            let mut profile_config = HashMap::new();
721
722            for var in selected_vars {
724                let default_value = Self::get_profile_default_value(profile, &var.name, &var.value);
725
726                let value = Input::<String>::with_theme(&self.theme)
727                    .with_prompt(format!("  {}", var.name))
728                    .default(default_value)
729                    .interact()?;
730
731                profile_config.insert(var.name.clone(), value);
732            }
733
734            profile_configs.insert(profile.clone(), profile_config);
735        }
736
737        Ok((profiles, profile_configs))
738    }
739
740    fn get_profile_default_value(profile: &str, var_name: &str, base_value: &str) -> String {
741        match (profile, var_name) {
742            ("development", "NODE_ENV") => "development".to_string(),
743            ("testing", "NODE_ENV") => "test".to_string(),
744            ("staging", "NODE_ENV") => "staging".to_string(),
745            ("production", "NODE_ENV") => "production".to_string(),
746
747            ("development", "DATABASE_URL") => base_value.replace("myapp", "myapp_dev"),
748            ("testing", "DATABASE_URL") => base_value.replace("myapp", "myapp_test"),
749            ("staging", "DATABASE_URL") => base_value.replace("myapp", "myapp_staging"),
750
751            ("development", "LOG_LEVEL") => "debug".to_string(),
752            ("testing", "LOG_LEVEL") => "error".to_string(),
753            ("production", "LOG_LEVEL") => "info".to_string(),
754
755            ("development", "DEBUG") => "true".to_string(),
756            (_, "DEBUG") => "false".to_string(),
757
758            _ => base_value.to_string(),
759        }
760    }
761
762    pub fn scan_existing_files(&self) -> Result<Option<Vec<PathBuf>>> {
771        println!("\nš Scanning for existing environment files...");
772
773        let patterns = vec![".env", ".env.*", "docker-compose.yml", "docker-compose.yaml"];
774        let mut found_files = Vec::new();
775
776        for pattern in patterns {
777            if let Ok(paths) = glob(pattern) {
778                for path in paths.flatten() {
779                    found_files.push(path);
780                }
781            }
782        }
783
784        if found_files.is_empty() {
785            return Ok(None);
786        }
787
788        println!("Found existing environment files:");
789        for (i, file) in found_files.iter().enumerate() {
790            let var_count = Self::count_env_vars(file).unwrap_or(0);
791            println!(
792                "  {} {} ({} variables)",
793                if i == 0 { "ā" } else { " " },
794                file.display(),
795                var_count
796            );
797        }
798
799        let Some(import) = Confirm::with_theme(&self.theme)
800            .with_prompt("\nWould you like to import these?")
801            .default(true)
802            .interact_opt()?
803        else {
804            return Err(EscPressed.into());
805        };
806
807        if import { Ok(Some(found_files)) } else { Ok(None) }
808    }
809
810    fn count_env_vars(path: &Path) -> Result<usize> {
811        let content = fs::read_to_string(path)?;
812        let count = content
813            .lines()
814            .filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#'))
815            .filter(|line| line.contains('='))
816            .count();
817        Ok(count)
818    }
819
820    pub fn import_existing(&self, files: Vec<PathBuf>) -> Result<Vec<PathBuf>> {
828        let options: Vec<&str> = vec!["Import all", "Select files to import", "Skip import"];
829
830        let Some(selection) = Select::with_theme(&self.theme)
831            .with_prompt("Import option")
832            .items(&options)
833            .interact_opt()?
834        else {
835            return Err(EscPressed.into());
836        };
837
838        match selection {
839            0 => Ok(files),
840            1 => {
841                let file_names: Vec<String> = files.iter().map(|p| p.display().to_string()).collect();
842
843                let Some(selections) = MultiSelect::with_theme(&self.theme)
844                    .with_prompt("Select files to import")
845                    .items(&file_names)
846                    .interact_opt()?
847                else {
848                    return Err(EscPressed.into());
849                };
850
851                Ok(selections.into_iter().map(|i| files[i].clone()).collect())
852            }
853            _ => Ok(Vec::new()),
854        }
855    }
856
857    pub fn configure_team_features(&self) -> Result<TeamConfig> {
866        println!("\nš„ Team Collaboration Setup:");
867
868        let Some(create_config) = Confirm::with_theme(&self.theme)
869            .with_prompt("Create .envx/config.yaml for team?")
870            .default(true)
871            .interact_opt()?
872        else {
873            return Err(EscPressed.into());
874        };
875
876        let git_hooks = false;
877
878        let ci_integration = false;
879
880        let Some(shared_profiles) = Confirm::with_theme(&self.theme)
881            .with_prompt("Enable shared profiles?")
882            .default(true)
883            .interact_opt()?
884        else {
885            return Err(EscPressed.into());
886        };
887
888        let config_path = if create_config {
889            let repo_root = Self::find_repo_root().unwrap_or_else(|_| PathBuf::from("."));
890            repo_root.join(".envx").join("config.yaml")
891        } else {
892            PathBuf::from(".envx/config.yaml")
893        };
894
895        Ok(TeamConfig {
896            config_path,
897            git_hooks,
898            ci_integration,
899            shared_profiles,
900        })
901    }
902
903    fn find_repo_root() -> Result<PathBuf> {
904        let current = std::env::current_dir()?;
905        let mut dir = current.as_path();
906
907        loop {
908            if dir.join(".git").exists() {
909                return Ok(dir.to_path_buf());
910            }
911            match dir.parent() {
912                Some(parent) => dir = parent,
913                None => return Err(eyre!("No git repository found")),
914            }
915        }
916    }
917
918    pub fn configure_validation(&self, project_type: &ProjectType) -> Result<ValidationRules> {
927        println!("\nā
 Configure Validation Rules:");
928
929        let options = vec![
930            "Require all variables in .envx/config.yaml",
931            "Validate URLs are properly formatted",
932            "Check numeric values are in valid ranges",
933            "Warn about missing required variables",
934            "Strict mode (fail on any validation error)",
935        ];
936
937        let defaults = vec![true, true, true, true, false]; let Some(selections) = MultiSelect::with_theme(&self.theme)
940            .with_prompt("Select validation rules")
941            .items(&options)
942            .defaults(&defaults)
943            .interact_opt()?
944        else {
945            return Err(EscPressed.into());
946        };
947
948        let rules = ValidationRules {
949            require_all_defined: selections.contains(&0),
950            validate_urls: selections.contains(&1),
951            validate_numbers: selections.contains(&2),
952            warn_missing: selections.contains(&3),
953            strict_mode: selections.contains(&4),
954            custom_patterns: self.get_custom_patterns(project_type)?,
955        };
956
957        Ok(rules)
958    }
959
960    fn get_custom_patterns(&self, project_type: &ProjectType) -> Result<HashMap<String, String>> {
961        let mut patterns = HashMap::new();
962
963        match &project_type.category {
964            ProjectCategory::WebApp => {
965                patterns.insert("*_URL".to_string(), r"^https?://.*".to_string());
966                patterns.insert("*_PORT".to_string(), r"^[0-9]{1,5}$".to_string());
967            }
968            ProjectCategory::Docker => {
969                patterns.insert("*_IMAGE".to_string(), r"^[a-z0-9\-_/:.]+$".to_string());
970            }
971            _ => {}
972        }
973
974        let Some(add_custom) = Confirm::with_theme(&self.theme)
975            .with_prompt("\nAdd custom validation pattern?")
976            .default(false)
977            .interact_opt()?
978        else {
979            return Err(EscPressed.into());
980        };
981
982        if add_custom {
983            let pattern_name = Input::<String>::with_theme(&self.theme)
984                .with_prompt("Pattern name (e.g., *_EMAIL)")
985                .interact()?;
986
987            let pattern_regex = Input::<String>::with_theme(&self.theme)
988                .with_prompt("Regex pattern")
989                .interact()?;
990
991            patterns.insert(pattern_name, pattern_regex);
992        }
993
994        Ok(patterns)
995    }
996
997    pub fn review_and_apply(&self, result: &SetupResult) -> Result<()> {
1007        println!("\nš Setup Summary:");
1008        println!("{}", "ā".repeat(50));
1009        println!("Project Type:     {}", result.project_type.name);
1010        println!("Profiles:         {}", result.profiles.join(", "));
1011        println!("Variables:        {} configured", result.selected_vars.len());
1012        println!(
1013            "Create .env files: {}",
1014            if result.create_env_files { "Yes" } else { "No" }
1015        );
1016        println!(
1017            "Team Setup:       {}",
1018            if result.team_config.is_some() {
1019                "Enabled"
1020            } else {
1021                "Disabled"
1022            }
1023        );
1024
1025        if !result.imported_files.is_empty() {
1026            println!("Imported Files:   {}", result.imported_files.len());
1027        }
1028
1029        println!("{}", "ā".repeat(50));
1030
1031        let Some(confirm) = Confirm::with_theme(&self.theme)
1032            .with_prompt("\nReady to apply configuration?")
1033            .default(true)
1034            .interact_opt()?
1035        else {
1036            return Err(EscPressed.into());
1037        };
1038
1039        if !confirm {
1040            return Err(eyre!("Setup cancelled by user"));
1041        }
1042
1043        self.apply_configuration(result)?;
1045
1046        Ok(())
1047    }
1048
1049    #[allow(clippy::too_many_lines)]
1050    fn apply_configuration(&self, result: &SetupResult) -> Result<()> {
1051        println!("\nš Applying configuration...");
1052
1053        if let Some(team_config) = &result.team_config {
1055            Self::create_project_config(result, &team_config.config_path)?;
1056            println!("ā Created project configuration");
1057        }
1058
1059        for file in &result.imported_files {
1061            println!("ā Imported {}", file.display());
1062            }
1064
1065        let mut profile_manager = ProfileManager::new()?;
1067
1068        let mut profile_mappings: HashMap<String, String> = HashMap::new();
1070        for profile_name in &result.profiles {
1071            profile_mappings.insert(profile_name.clone(), profile_name.clone());
1072        }
1073
1074        for (profile_name, _) in &result.profile_configs {
1075            if profile_manager.get(profile_name).is_some() {
1076                println!("\nā ļø  Profile '{profile_name}' already exists!");
1077
1078                let options = vec![
1079                    format!("Rename new profile (current: {})", profile_name),
1080                    format!("Delete existing '{}' profile and replace", profile_name),
1081                    "Skip this profile".to_string(),
1082                ];
1083
1084                let Some(choice) = Select::with_theme(&self.theme)
1085                    .with_prompt("How would you like to proceed?")
1086                    .items(&options)
1087                    .interact_opt()?
1088                else {
1089                    return Err(EscPressed.into());
1090                };
1091
1092                match choice {
1093                    0 => {
1094                        loop {
1096                            let new_name = Input::<String>::with_theme(&self.theme)
1097                                .with_prompt("Enter new profile name")
1098                                .default(format!("{profile_name}_new"))
1099                                .interact()?;
1100
1101                            if new_name.is_empty() {
1102                                println!("Profile name cannot be empty!");
1103                                continue;
1104                            }
1105
1106                            if profile_manager.get(&new_name).is_none() {
1107                                profile_mappings.insert(profile_name.clone(), new_name);
1108                                break;
1109                            }
1110                            println!("Profile '{new_name}' also exists! Please choose another name.");
1111                        }
1112                    }
1113                    1 => {
1114                        let Some(confirm_delete) = Confirm::with_theme(&self.theme)
1116                            .with_prompt(format!(
1117                                "Are you sure you want to delete the existing '{profile_name}' profile?"
1118                            ))
1119                            .default(false)
1120                            .interact_opt()?
1121                        else {
1122                            return Err(EscPressed.into());
1123                        };
1124
1125                        if confirm_delete {
1126                            profile_manager.delete(profile_name)?;
1127                            println!("ā Deleted existing profile: {profile_name}");
1128                        } else {
1129                            println!("Skipping profile: {profile_name}");
1130                            profile_mappings.remove(profile_name);
1131                        }
1132                    }
1133                    2 => {
1134                        println!("Skipping profile: {profile_name}");
1136                        profile_mappings.remove(profile_name);
1137                    }
1138                    _ => unreachable!(),
1139                }
1140            }
1141        }
1142
1143        for (original_name, actual_name) in &profile_mappings {
1145            if let Some(profile_vars) = result.profile_configs.get(original_name) {
1146                profile_manager.create(actual_name.clone(), Some(format!("{actual_name} environment")))?;
1148
1149                if let Some(profile) = profile_manager.get_mut(actual_name) {
1151                    for (var_name, var_value) in profile_vars {
1152                        profile.add_var(var_name.clone(), var_value.clone(), false);
1153                    }
1154                }
1155
1156                if original_name == actual_name {
1157                    println!("ā Created profile: {actual_name}");
1158                } else {
1159                    println!("ā Created profile: {actual_name} (renamed from {original_name})");
1160                }
1161            }
1162        }
1163
1164        if let Some(first_profile) = result.profiles.first() {
1167            if let Some(actual_name) = profile_mappings.get(first_profile) {
1168                profile_manager.switch(actual_name)?;
1169                println!("ā Set active profile: {actual_name}");
1170            }
1171        }
1172
1173        if result.create_env_files {
1175            Self::create_env_files_with_mappings(result, &profile_mappings)?;
1176        }
1177
1178        for var in &result.selected_vars {
1180            unsafe { std::env::set_var(&var.name, &var.value) };
1181            println!("ā Set {} in current session", var.name);
1182        }
1183
1184        Ok(())
1185    }
1186
1187    fn create_env_files_with_mappings(result: &SetupResult, mappings: &HashMap<String, String>) -> Result<()> {
1188        println!("\nš Creating .env files...");
1189
1190        for (original_name, config) in &result.profile_configs {
1191            if let Some(actual_name) = mappings.get(original_name) {
1192                let filename = if actual_name == "development" {
1193                    ".env".to_string()
1194                } else {
1195                    format!(".env.{actual_name}")
1196                };
1197
1198                let mut content = String::new();
1199                content.push_str(&format!("# Environment variables for {actual_name} profile\n"));
1200                if original_name != actual_name {
1201                    content.push_str(&format!("# (originally configured as {original_name})\n"));
1202                }
1203                content.push_str(&format!(
1204                    "# Generated by envx on {}\n\n",
1205                    chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
1206                ));
1207
1208                for (key, value) in config {
1209                    content.push_str(&format!("{key}={value}\n"));
1210                }
1211
1212                fs::write(&filename, content)?;
1213                println!("ā Created {filename}");
1214            }
1215        }
1216
1217        Ok(())
1218    }
1219
1220    fn check_required_variables(result: &SetupResult) {
1221        println!("\nš Checking required environment variables...");
1222
1223        let mut missing_vars = Vec::new();
1224        let mut set_vars = Vec::new();
1225
1226        for var in &result.selected_vars {
1227            if var.required {
1228                match std::env::var(&var.name) {
1229                    Ok(value) => {
1230                        if value == var.value {
1231                            set_vars.push(&var.name);
1232                        } else {
1233                            missing_vars.push(&var.name);
1234                        }
1235                    }
1236                    Err(_) => missing_vars.push(&var.name),
1237                }
1238            }
1239        }
1240
1241        if !set_vars.is_empty() {
1242            println!("\nā
 Successfully set in current session:");
1243            for var in set_vars {
1244                println!("   ā {var}");
1245            }
1246        }
1247
1248        if !missing_vars.is_empty() {
1249            println!("\nā ļø  The following required variables need a terminal restart to take effect:");
1250            for var in missing_vars {
1251                println!("   ⢠{var}");
1252            }
1253
1254            println!("\nš” To apply these variables:");
1255            println!("   1. Close and restart your terminal");
1256            println!("   2. Run 'envx list' to verify they are set");
1257            println!("   3. Or source the .env file: source .env");
1258        } else if result.selected_vars.iter().any(|v| v.required) {
1259            println!("\nā
 All required variables are set!");
1260        }
1261
1262        println!("\nā
 Setup complete! Here's what to do next:");
1263        println!("\n  1. Run `envx list` to see your environment variables");
1264        println!("  2. Run `envx tui` to launch the interactive interface");
1265        println!("  3. Run `envx profile list` to see available profiles");
1266        println!("  4. Run `envx profile set <name>` to switch profiles");
1267
1268        if result.team_config.is_some() {
1269            println!("  5. Commit .envx/config.yaml to share with your team");
1270        }
1271    }
1272
1273    fn create_project_config(result: &SetupResult, path: &Path) -> Result<()> {
1274        if let Some(parent) = path.parent() {
1276            fs::create_dir_all(parent)?;
1277        }
1278
1279        let config = ProjectConfig {
1280            name: Some(result.project_type.name.to_lowercase().replace(' ', "-")),
1281            description: Some(format!("{} project", result.project_type.name)),
1282            required: result
1283                .selected_vars
1284                .iter()
1285                .filter(|v| v.required)
1286                .map(|v| RequiredVar {
1287                    name: v.name.clone(),
1288                    description: Some(v.description.clone()),
1289                    pattern: None,
1290                    example: Some(v.value.clone()),
1291                })
1292                .collect(),
1293            defaults: result
1294                .selected_vars
1295                .iter()
1296                .map(|v| (v.name.clone(), v.value.clone()))
1297                .collect(),
1298            auto_load: vec![".env".to_string(), ".env.local".to_string()],
1299            profile: result.profiles.first().cloned(),
1300            scripts: HashMap::new(),
1301            validation: ConfigValidationRules {
1302                warn_unused: result.validation_rules.warn_missing,
1303                strict_names: result.validation_rules.strict_mode,
1304                patterns: result.validation_rules.custom_patterns.clone(),
1305            },
1306            inherit: true,
1307        };
1308
1309        let yaml = serde_yaml::to_string(&config)?;
1310        fs::write(path, yaml)?;
1311
1312        Ok(())
1313    }
1314
1315    fn create_web_app_type() -> ProjectType {
1317        ProjectType {
1318            name: "Web Application".to_string(),
1319            category: ProjectCategory::WebApp,
1320            suggested_vars: vec![
1321                SuggestedVariable {
1322                    name: "NODE_ENV".to_string(),
1323                    description: "Application environment".to_string(),
1324                    example: "development".to_string(),
1325                    required: true,
1326                    sensitive: false,
1327                },
1328                SuggestedVariable {
1329                    name: "PORT".to_string(),
1330                    description: "Server port".to_string(),
1331                    example: "3000".to_string(),
1332                    required: true,
1333                    sensitive: false,
1334                },
1335                SuggestedVariable {
1336                    name: "DATABASE_URL".to_string(),
1337                    description: "Database connection string".to_string(),
1338                    example: "postgresql://localhost:5432/myapp".to_string(),
1339                    required: true,
1340                    sensitive: true,
1341                },
1342                SuggestedVariable {
1343                    name: "JWT_SECRET".to_string(),
1344                    description: "JWT signing secret".to_string(),
1345                    example: "your-secret-key".to_string(),
1346                    required: false,
1347                    sensitive: true,
1348                },
1349                SuggestedVariable {
1350                    name: "API_KEY".to_string(),
1351                    description: "External API key".to_string(),
1352                    example: "your-api-key".to_string(),
1353                    required: false,
1354                    sensitive: true,
1355                },
1356            ],
1357            suggested_profiles: vec![
1358                "development".to_string(),
1359                "testing".to_string(),
1360                "production".to_string(),
1361            ],
1362        }
1363    }
1364
1365    fn create_python_type() -> ProjectType {
1366        ProjectType {
1367            name: "Python Application".to_string(),
1368            category: ProjectCategory::Python,
1369            suggested_vars: vec![
1370                SuggestedVariable {
1371                    name: "PYTHONPATH".to_string(),
1372                    description: "Python module search path".to_string(),
1373                    example: "./src".to_string(),
1374                    required: false,
1375                    sensitive: false,
1376                },
1377                SuggestedVariable {
1378                    name: "DATABASE_URL".to_string(),
1379                    description: "Database connection string".to_string(),
1380                    example: "postgresql://localhost:5432/myapp".to_string(),
1381                    required: true,
1382                    sensitive: true,
1383                },
1384                SuggestedVariable {
1385                    name: "SECRET_KEY".to_string(),
1386                    description: "Django/Flask secret key".to_string(),
1387                    example: "your-secret-key".to_string(),
1388                    required: true,
1389                    sensitive: true,
1390                },
1391                SuggestedVariable {
1392                    name: "DEBUG".to_string(),
1393                    description: "Debug mode flag".to_string(),
1394                    example: "True".to_string(),
1395                    required: false,
1396                    sensitive: false,
1397                },
1398            ],
1399            suggested_profiles: vec![
1400                "development".to_string(),
1401                "testing".to_string(),
1402                "production".to_string(),
1403            ],
1404        }
1405    }
1406
1407    fn create_rust_type() -> ProjectType {
1408        ProjectType {
1409            name: "Rust Application".to_string(),
1410            category: ProjectCategory::Rust,
1411            suggested_vars: vec![
1412                SuggestedVariable {
1413                    name: "RUST_LOG".to_string(),
1414                    description: "Rust logging level".to_string(),
1415                    example: "info".to_string(),
1416                    required: false,
1417                    sensitive: false,
1418                },
1419                SuggestedVariable {
1420                    name: "DATABASE_URL".to_string(),
1421                    description: "Database connection string".to_string(),
1422                    example: "postgresql://localhost:5432/myapp".to_string(),
1423                    required: true,
1424                    sensitive: true,
1425                },
1426                SuggestedVariable {
1427                    name: "SERVER_PORT".to_string(),
1428                    description: "Server port".to_string(),
1429                    example: "8080".to_string(),
1430                    required: true,
1431                    sensitive: false,
1432                },
1433            ],
1434            suggested_profiles: vec!["development".to_string(), "release".to_string()],
1435        }
1436    }
1437
1438    fn create_docker_type() -> ProjectType {
1439        ProjectType {
1440            name: "Docker Application".to_string(),
1441            category: ProjectCategory::Docker,
1442            suggested_vars: vec![
1443                SuggestedVariable {
1444                    name: "COMPOSE_PROJECT_NAME".to_string(),
1445                    description: "Docker Compose project name".to_string(),
1446                    example: "myapp".to_string(),
1447                    required: true,
1448                    sensitive: false,
1449                },
1450                SuggestedVariable {
1451                    name: "DOCKER_REGISTRY".to_string(),
1452                    description: "Docker registry URL".to_string(),
1453                    example: "docker.io".to_string(),
1454                    required: false,
1455                    sensitive: false,
1456                },
1457            ],
1458            suggested_profiles: vec!["local".to_string(), "staging".to_string(), "production".to_string()],
1459        }
1460    }
1461
1462    fn create_microservices_type() -> ProjectType {
1463        ProjectType {
1464            name: "Microservices".to_string(),
1465            category: ProjectCategory::Microservices,
1466            suggested_vars: vec![
1467                SuggestedVariable {
1468                    name: "SERVICE_DISCOVERY_URL".to_string(),
1469                    description: "Service discovery endpoint".to_string(),
1470                    example: "http://consul:8500".to_string(),
1471                    required: true,
1472                    sensitive: false,
1473                },
1474                SuggestedVariable {
1475                    name: "KAFKA_BROKERS".to_string(),
1476                    description: "Kafka broker addresses".to_string(),
1477                    example: "kafka1:9092,kafka2:9092".to_string(),
1478                    required: false,
1479                    sensitive: false,
1480                },
1481            ],
1482            suggested_profiles: vec!["local".to_string(), "kubernetes".to_string(), "production".to_string()],
1483        }
1484    }
1485
1486    fn create_custom_type(&self) -> Result<ProjectType> {
1487        let value = Input::<String>::with_theme(&self.theme)
1488            .with_prompt("Enter project type name")
1489            .default("Custom Project".to_string())
1490            .interact()?;
1491        let name = value;
1492
1493        Ok(ProjectType {
1496            name,
1497            category: ProjectCategory::Custom,
1498            suggested_vars: Vec::new(), suggested_profiles: vec!["development".to_string(), "production".to_string()],
1500        })
1501    }
1502}