1use anyhow::Result;
2use glob::glob;
3use std::collections::BTreeMap;
4use std::io::{self};
5use std::path::PathBuf;
6
7use crate::nvy_config::{does_config_exist, is_target_shell, load_config, save_config, Config, Profile, DEFAULT_TARGET};
8use crate::log::{message, wrap_yellow};
9use crate::{success, warn};
10
11pub fn run_init() -> Result<()> {
12 let mut target = String::from(DEFAULT_TARGET);
13 let mut ignore = vec![".env.example".to_string()];
14
15 if does_config_exist() {
16 if !prompt_reinit()? {
17 warn!("Initialization cancelled.");
18 return Ok(());
19 }
20
21 let config = load_config()?;
22 if !is_target_shell(&config) {
23 target = config.target.clone();
24 ignore.push(target.clone().to_string());
25 }
26 }
27
28 let env_files = discover_env_files(ignore)?;
29 init_config(&target, env_files)?;
30 Ok(())
31}
32
33fn prompt_reinit() -> Result<bool> {
34 let prompt = "Do you want to reinitialize? [Y/n]";
35 let formatted: String = wrap_yellow(prompt);
36
37 message(vec![
38 "An existing nv configuration file was found in the current directory.",
39 &formatted,
40 ]);
41
42 let mut input = String::new();
43 io::stdin().read_line(&mut input)?;
44 let input = input.trim().to_lowercase();
45
46 Ok(input != "n" && input != "no")
47}
48
49fn discover_env_files(ignore: Vec<String>) -> Result<Vec<PathBuf>> {
50 Ok(glob(".env*")?
51 .filter_map(|result| {
52 result.ok().and_then(|path| {
53 let path_str = path.to_string_lossy();
54 if ignore.iter().any(|ignored| path_str.contains(ignored)) {
55 None
56 } else {
57 Some(path)
58 }
59 })
60 })
61 .collect())
62}
63
64fn get_profile_name(file_name: &str) -> Option<String> {
66 if file_name == ".env" {
67 Some("default".to_string())
68 } else if let Some(suffix) = file_name.strip_prefix(".env.") {
69 if !suffix.is_empty() {
70 Some(suffix.to_string())
71 } else {
72 None
73 }
74 } else {
75 None
76 }
77}
78
79fn init_config(target: &str, env_files: Vec<PathBuf>) -> Result<()> {
80 let mut profiles = BTreeMap::new();
81
82 profiles.insert(
83 "default".to_string(),
84 vec![Profile {
85 path: ".env".to_string(),
86 }],
87 );
88
89 for file in env_files {
90 let file_name = file.to_string_lossy();
91 if let Some(profile_name) = get_profile_name(&file_name) {
92 if profile_name != "default" {
93 profiles.insert(
94 profile_name.to_string(),
95 vec![Profile {
96 path: file_name.into_owned(),
97 }],
98 );
99 }
100 }
101 }
102
103 let config = Config { target: target.to_string(), profiles, current_profiles: vec![] };
104 let res = save_config(&config);
105 match res {
106 Ok(()) => {
107 success!("Initialized nvy.yaml in file mode, pointing to {}; run `nvy target set <file>` to change the target.", DEFAULT_TARGET);
108 Ok(())
109 },
110 Err(e) => Err(anyhow::anyhow!(e)),
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use std::fs;
117 use crate::nvy_config::TARGET_SHELL;
118
119 use super::*;
120
121 #[test]
122 fn test_get_profile_name() {
123 assert_eq!(get_profile_name(".env"), Some("default".to_string()));
124 assert_eq!(get_profile_name(".env.local"), Some("local".to_string()));
125 assert_eq!(get_profile_name(".env.prod"), Some("prod".to_string()));
126 assert_eq!(get_profile_name(".env.staging"), Some("staging".to_string()));
127 assert_eq!(get_profile_name(".env."), None);
128 assert_eq!(get_profile_name("env"), None);
129 assert_eq!(get_profile_name(".environment"), None);
130 }
131
132 #[test]
133 fn test_init_config_empty_dir() -> Result<()> {
134 let empty_files = Vec::new();
135 init_config(TARGET_SHELL,empty_files)?;
136
137 let content = fs::read_to_string("nvy.yaml")?;
138 assert!(content.contains("default:"));
139 assert!(content.contains("path: .env"));
140
141 fs::remove_file("nvy.yaml")?;
142 Ok(())
143 }
144}