Skip to main content

reach/
cli.rs

1use anyhow::Result;
2use dirs;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::PathBuf;
7
8use crate::{Config, Identity, SessionManager, UserCredentials};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct UserProfile {
12    pub email: String,
13    pub name: String,
14    pub created_at: String,
15    pub last_used: String,
16    pub config_path: PathBuf,
17}
18
19pub struct UserManager {
20    users_file: PathBuf,
21    current_user_file: PathBuf,
22}
23
24impl UserManager {
25    pub fn new() -> Result<Self> {
26        let config_dir = dirs::config_dir()
27            .ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?
28            .join("rustalk");
29
30        fs::create_dir_all(&config_dir)?;
31
32        Ok(UserManager {
33            users_file: config_dir.join("users.json"),
34            current_user_file: config_dir.join("current_user"),
35        })
36    }
37
38    pub fn register_user(&self, credentials: &UserCredentials, config_path: PathBuf) -> Result<()> {
39        let mut users = self.load_users()?;
40
41        let profile = UserProfile {
42            email: credentials.email.clone(),
43            name: credentials.name.clone(),
44            created_at: chrono::Utc::now().to_rfc3339(),
45            last_used: chrono::Utc::now().to_rfc3339(),
46            config_path,
47        };
48
49        users.insert(credentials.email.clone(), profile);
50        self.save_users(&users)?;
51
52        // Set as current user
53        fs::write(&self.current_user_file, &credentials.email)?;
54
55        Ok(())
56    }
57
58    pub fn list_users(&self) -> Result<Vec<UserProfile>> {
59        let users = self.load_users()?;
60        Ok(users.values().cloned().collect())
61    }
62
63    pub fn switch_user(&self, email: &str) -> Result<()> {
64        let users = self.load_users()?;
65
66        if users.contains_key(email) {
67            fs::write(&self.current_user_file, email)?;
68            Ok(())
69        } else {
70            Err(anyhow::anyhow!("User '{}' not found", email))
71        }
72    }
73
74    pub fn remove_user(&self, email: &str) -> Result<()> {
75        let mut users = self.load_users()?;
76
77        if let Some(user) = users.remove(email) {
78            // Remove config file if it exists
79            if user.config_path.exists() {
80                fs::remove_file(&user.config_path)?;
81            }
82
83            self.save_users(&users)?;
84
85            // If this was the current user, clear it
86            if let Ok(current) = self.get_current_user() {
87                if current == email {
88                    let _ = fs::remove_file(&self.current_user_file);
89                }
90            }
91
92            Ok(())
93        } else {
94            Err(anyhow::anyhow!("User '{}' not found", email))
95        }
96    }
97
98    pub fn get_current_user(&self) -> Result<String> {
99        fs::read_to_string(&self.current_user_file)
100            .map(|s| s.trim().to_string())
101            .map_err(|_| anyhow::anyhow!("No current user set"))
102    }
103
104    pub fn get_user_profile(&self, email: &str) -> Result<UserProfile> {
105        let users = self.load_users()?;
106        users
107            .get(email)
108            .cloned()
109            .ok_or_else(|| anyhow::anyhow!("User '{}' not found", email))
110    }
111
112    fn load_users(&self) -> Result<HashMap<String, UserProfile>> {
113        if !self.users_file.exists() {
114            return Ok(HashMap::new());
115        }
116
117        let contents = fs::read_to_string(&self.users_file)?;
118        let users: HashMap<String, UserProfile> =
119            serde_json::from_str(&contents).unwrap_or_else(|_| HashMap::new());
120
121        Ok(users)
122    }
123
124    fn save_users(&self, users: &HashMap<String, UserProfile>) -> Result<()> {
125        let contents = serde_json::to_string_pretty(users)?;
126        fs::write(&self.users_file, contents)?;
127        Ok(())
128    }
129}
130
131pub struct PathManager;
132
133impl PathManager {
134    pub fn add_to_path() -> Result<String> {
135        let exe_path = std::env::current_exe()?;
136        let exe_dir = exe_path
137            .parent()
138            .ok_or_else(|| anyhow::anyhow!("Could not get executable directory"))?;
139
140        #[cfg(target_os = "windows")]
141        {
142            use std::process::Command;
143            let path_str = exe_dir.to_string_lossy();
144
145            let output = Command::new("powershell")
146                .args(&[
147                    "-Command",
148                    &format!(
149                        "[Environment]::SetEnvironmentVariable('PATH', [Environment]::GetEnvironmentVariable('PATH', 'User') + ';{}', 'User')",
150                        path_str
151                    )
152                ])
153                .output()?;
154
155            if output.status.success() {
156                Ok(format!("Added {} to PATH", path_str))
157            } else {
158                Err(anyhow::anyhow!(
159                    "Failed to add to PATH: {}",
160                    String::from_utf8_lossy(&output.stderr)
161                ))
162            }
163        }
164
165        #[cfg(not(target_os = "windows"))]
166        {
167            // For Unix-like systems, we'll add to ~/.bashrc and ~/.zshrc
168            let home =
169                dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
170
171            let path_str = exe_dir.to_string_lossy();
172            let export_line = format!("export PATH=\"$PATH:{}\"", path_str);
173
174            for shell_config in &[".bashrc", ".zshrc"] {
175                let config_file = home.join(shell_config);
176                if config_file.exists() {
177                    let contents = fs::read_to_string(&config_file)?;
178                    if !contents.contains(&path_str) {
179                        fs::write(&config_file, format!("{}\n{}", contents, export_line))?;
180                    }
181                }
182            }
183
184            Ok(format!("Added {} to shell configurations", path_str))
185        }
186    }
187
188    pub fn remove_from_path() -> Result<String> {
189        let exe_path = std::env::current_exe()?;
190        let exe_dir = exe_path
191            .parent()
192            .ok_or_else(|| anyhow::anyhow!("Could not get executable directory"))?;
193
194        #[cfg(target_os = "windows")]
195        {
196            use std::process::Command;
197            let path_str = exe_dir.to_string_lossy();
198
199            let output = Command::new("powershell")
200                .args(&[
201                    "-Command",
202                    &format!(
203                        "[Environment]::SetEnvironmentVariable('PATH', ([Environment]::GetEnvironmentVariable('PATH', 'User') -replace ';?{}', ''), 'User')",
204                        regex::escape(&path_str)
205                    )
206                ])
207                .output()?;
208
209            if output.status.success() {
210                Ok(format!("Removed {} from PATH", path_str))
211            } else {
212                Err(anyhow::anyhow!(
213                    "Failed to remove from PATH: {}",
214                    String::from_utf8_lossy(&output.stderr)
215                ))
216            }
217        }
218
219        #[cfg(not(target_os = "windows"))]
220        {
221            let home =
222                dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
223
224            let path_str = exe_dir.to_string_lossy();
225
226            for shell_config in &[".bashrc", ".zshrc"] {
227                let config_file = home.join(shell_config);
228                if config_file.exists() {
229                    let contents = fs::read_to_string(&config_file)?;
230                    let new_contents = contents
231                        .lines()
232                        .filter(|line| !line.contains(&path_str))
233                        .collect::<Vec<_>>()
234                        .join("\n");
235                    fs::write(&config_file, new_contents)?;
236                }
237            }
238
239            Ok(format!("Removed {} from shell configurations", path_str))
240        }
241    }
242
243    pub fn check_in_path() -> Result<String> {
244        let exe_path = std::env::current_exe()?;
245        let exe_dir = exe_path
246            .parent()
247            .ok_or_else(|| anyhow::anyhow!("Could not get executable directory"))?;
248
249        let path_env = std::env::var("PATH")?;
250        let path_str = exe_dir.to_string_lossy();
251
252        if path_env.contains(path_str.as_ref()) {
253            Ok(format!("āœ“ {} is in PATH", path_str))
254        } else {
255            Ok(format!("āœ— {} is NOT in PATH", path_str))
256        }
257    }
258}
259
260pub struct CliOperations;
261
262impl CliOperations {
263    pub async fn setup_user(
264        email: Option<String>,
265        name: Option<String>,
266        password: Option<String>,
267    ) -> Result<UserCredentials> {
268        use std::io::{self, Write};
269
270        let email = match email {
271            Some(e) => e,
272            None => {
273                print!("Enter your email: ");
274                io::stdout().flush()?;
275                let mut input = String::new();
276                io::stdin().read_line(&mut input)?;
277                input.trim().to_string()
278            }
279        };
280
281        let name = match name {
282            Some(n) => n,
283            None => {
284                print!("Enter your display name: ");
285                io::stdout().flush()?;
286                let mut input = String::new();
287                io::stdin().read_line(&mut input)?;
288                input.trim().to_string()
289            }
290        };
291
292        let password = match password {
293            Some(p) => p,
294            None => {
295                print!("Enter your password: ");
296                io::stdout().flush()?;
297                // For now, just read plain text. In production, use rpassword crate
298                let mut input = String::new();
299                io::stdin().read_line(&mut input)?;
300                input.trim().to_string()
301            }
302        };
303
304        let credentials = UserCredentials {
305            email,
306            name,
307            password,
308        };
309
310        // Create identity and save config
311        let identity = Identity::new(credentials.clone())?;
312        let config = Config::new(identity);
313
314        let config_file = crate::config::get_config_file()?;
315        crate::config::save_config(&config)?;
316
317        // Register user
318        let user_manager = UserManager::new()?;
319        user_manager.register_user(&credentials, config_file)?;
320
321        Ok(credentials)
322    }
323
324    pub async fn start_chat_session(port: u16) -> Result<SessionManager> {
325        // Load current user
326        let user_manager = UserManager::new()?;
327        let current_email = user_manager.get_current_user()?;
328        let _profile = user_manager.get_user_profile(&current_email)?;
329
330        // Load config
331        let config = crate::config::load_config()?;
332
333        let session_manager = SessionManager::new(config.identity).await?;
334        session_manager.start_session(port).await?;
335
336        Ok(session_manager)
337    }
338
339    pub async fn get_user_info() -> Result<String> {
340        let user_manager = UserManager::new()?;
341        let current_email = user_manager.get_current_user()?;
342        let profile = user_manager.get_user_profile(&current_email)?;
343
344        Ok(format!(
345            "šŸ“§ Email: {}\nšŸ‘¤ Name: {}\nšŸ“… Created: {}\nšŸ•’ Last used: {}",
346            profile.email, profile.name, profile.created_at, profile.last_used
347        ))
348    }
349
350    pub async fn reset_config() -> Result<String> {
351        let config_file = crate::config::get_config_file()?;
352        if config_file.exists() {
353            fs::remove_file(&config_file)?;
354            Ok("Configuration reset successfully".to_string())
355        } else {
356            Ok("No configuration to reset".to_string())
357        }
358    }
359}