vapor_cli/
shell.rs

1use std::io::{self};
2use std::process::Command;
3use std::env;
4use std::path::Path;
5use rustyline::error::ReadlineError;
6use rustyline::{Editor, Helper};
7use rustyline::completion::{Completer, FilenameCompleter};
8use rustyline::highlight::Highlighter;
9use rustyline::hint::Hinter;
10use rustyline::validate::Validator;
11use ctrlc;
12use anyhow::Result;
13
14const BUILTIN_COMMANDS: &[&str] = &["cd", "pwd", "history", "help", "exit", ".vrepl", ".dbinfo"];
15
16struct ShellHelper {
17    filename_completer: FilenameCompleter,
18}
19
20impl Helper for ShellHelper {}
21
22#[derive(Debug, PartialEq, Eq)]
23pub enum ShellAction {
24    Exit,
25    SwitchToRepl,
26}
27
28impl Completer for ShellHelper {
29    type Candidate = String;
30
31    fn complete(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> rustyline::Result<(usize, Vec<String>)> {
32        let line = &line[..pos];
33        let words: Vec<&str> = line.split_whitespace().collect();
34        
35        if words.is_empty() {
36            // Complete with built-in commands
37            return Ok((0, BUILTIN_COMMANDS.iter().map(|&s| s.to_string()).collect()));
38        }
39
40        if words.len() == 1 {
41            // First word - complete with commands
42            let prefix = words[0];
43            let candidates: Vec<String> = BUILTIN_COMMANDS
44                .iter()
45                .filter(|&&cmd| cmd.starts_with(prefix))
46                .map(|&s| s.to_string())
47                .collect();
48            
49            if !candidates.is_empty() {
50                return Ok((0, candidates));
51            }
52        }
53
54        // For other cases, use filename completion
55        if line.trim_start().starts_with("cd ") || line.trim_start().starts_with("ls ") {
56            let (start, pairs) = self.filename_completer.complete(line, pos, _ctx)?;
57            let candidates: Vec<String> = pairs.into_iter().map(|p| p.replacement).collect();
58            return Ok((start, candidates));
59        }
60
61        Ok((pos, Vec::new()))
62    }
63}
64
65impl Highlighter for ShellHelper {}
66
67impl Hinter for ShellHelper {
68    type Hint = String;
69}
70
71impl Validator for ShellHelper {}
72
73pub struct Shell {
74    history: Vec<String>,
75    editor: Editor<ShellHelper, rustyline::history::FileHistory>,
76    original_dir: std::path::PathBuf,
77    history_path: std::path::PathBuf,
78    db_path: String, // To store the database path
79}
80
81impl Shell {
82    pub fn new(db_path: &str) -> Self {
83        let helper = ShellHelper {
84            filename_completer: FilenameCompleter::new(),
85        };
86        
87        let mut editor = Editor::new().unwrap();
88        editor.set_helper(Some(helper));
89        
90        let original_dir = env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
91        
92        // Set up history path in home directory
93        let history_path = match env::var("HOME") {
94            Ok(home) => Path::new(&home).join(".vapor_shell_history"),
95            Err(_) => Path::new(".vapor_shell_history").to_path_buf(),
96        };
97
98        // Load history if available
99        if history_path.exists() {
100            if let Err(e) = editor.load_history(&history_path) {
101                eprintln!("Warning: Could not load shell history: {}", e);
102            }
103        }
104
105        // Set up Ctrl+C handler
106        if let Err(e) = ctrlc::set_handler(move || {
107            println!("\nUse 'exit' to return to the REPL");
108        }) {
109            eprintln!("Warning: Could not set up Ctrl+C handler: {}", e);
110        }
111
112        Shell {
113            history: Vec::new(),
114            editor,
115            original_dir,
116            history_path,
117            db_path: db_path.to_string(),
118        }
119    }
120
121    fn get_prompt(&self) -> String {
122        let cwd = env::current_dir().unwrap_or_default();
123        let home = env::var("HOME").map(std::path::PathBuf::from).unwrap_or_default();
124
125        let display_path = if cwd == home {
126            "~".to_string()
127        } else if let Ok(stripped) = cwd.strip_prefix(&home) {
128            format!("~/{}", stripped.display())
129        } else {
130            cwd.display().to_string()
131        };
132
133        format!("[vapor-shell {}]$ ", display_path)
134    }
135
136    pub fn run(&mut self) -> ShellAction {
137        println!("Welcome to Vapor Shell! Type 'exit' to return to the REPL.");
138        println!("Type 'help' for available commands.");
139        
140        loop {
141            let prompt = self.get_prompt();
142            let readline = self.editor.readline(&prompt);
143            match readline {
144                Ok(line) => {
145                    let line = line.trim();
146                    if line.is_empty() {
147                        continue;
148                    }
149
150                    // Add to history
151                    self.history.push(line.to_string());
152                    if let Err(e) = self.editor.add_history_entry(line) {
153                        eprintln!("Warning: Could not add to history: {}", e);
154                    }
155
156                    // Handle exit command first
157                    if line == "exit" {
158                        println!("Returning to REPL...");
159                        // Save history before exiting
160                        if let Err(e) = self.editor.save_history(&self.history_path) {
161                            eprintln!("Warning: Could not save shell history: {}", e);
162                        }
163                        // Restore original directory
164                        if let Err(e) = env::set_current_dir(&self.original_dir) {
165                            eprintln!("Warning: Could not restore original directory: {}", e);
166                        }
167                        return ShellAction::Exit;
168                    }
169
170                    if line == ".vrepl" {
171                        println!("Switching to SQL REPL...");
172                        // Save history before switching
173                        if let Err(e) = self.editor.save_history(&self.history_path) {
174                            eprintln!("Warning: Could not save shell history: {}", e);
175                        }
176                        // Restore original directory
177                        if let Err(e) = env::set_current_dir(&self.original_dir) {
178                            eprintln!("Warning: Could not restore original directory: {}", e);
179                        }
180                        return ShellAction::SwitchToRepl;
181                    }
182
183                    if line == "help" {
184                        self.show_help();
185                        continue;
186                    }
187
188                    self.execute_command(line);
189                }
190                Err(ReadlineError::Interrupted) => {
191                    println!("^C");
192                    continue;
193                }
194                Err(ReadlineError::Eof) => {
195                    println!("EOF");
196                    // Save history and restore dir before breaking
197                    if let Err(e) = self.editor.save_history(&self.history_path) {
198                        eprintln!("Warning: Could not save shell history: {}", e);
199                    }
200                    if let Err(e) = env::set_current_dir(&self.original_dir) {
201                        eprintln!("Warning: Could not restore original directory: {}", e);
202                    }
203                    return ShellAction::Exit; // Treat EOF as a normal exit
204                }
205                Err(err) => {
206                    eprintln!("Input error: {}", err);
207                    continue;
208                }
209            }
210        }
211
212        // All paths that exit this loop (exit command, .vrepl command, EOF) 
213        // now explicitly return a ShellAction. Therefore, the loop itself will not terminate
214        // in a way that would cause execution to reach code after the loop.
215        // The function is guaranteed to return a ShellAction via one of those paths.
216    }
217
218    fn execute_command(&mut self, command: &str) {
219        let parts: Vec<&str> = command.split_whitespace().collect();
220        if parts.is_empty() {
221            return;
222        }
223
224        match parts[0] {
225            "cd" => {
226                let path = if parts.len() > 1 {
227                    let p = parts[1];
228                    if p == "~" {
229                        env::var("HOME").unwrap_or_else(|_| ".".to_string())
230                    } else if p.starts_with("~/") {
231                        env::var("HOME").map(|home| format!("{}/{}", home, &p[2..])).unwrap_or_else(|_| p.to_string())
232                    } else {
233                        p.to_string()
234                    }
235                } else {
236                    env::var("HOME").unwrap_or_else(|_| ".".to_string())
237                };
238
239                if let Err(e) = env::set_current_dir(Path::new(&path)) {
240                    eprintln!("cd: {}: {}", path, e);
241                }
242            }
243            "pwd" => {
244                if let Ok(current_dir) = env::current_dir() {
245                    println!("{}", current_dir.display());
246                }
247            }
248            "history" => {
249                for (i, cmd) in self.history.iter().enumerate() {
250                    println!("{}: {}", i + 1, cmd);
251                }
252            }
253            "help" => self.show_help(),
254            _ => {
255                let status = Command::new(parts[0])
256                    .args(&parts[1..])
257                    .status();
258
259                match status {
260                    Ok(status) => {
261                        if !status.success() {
262                            if let Some(code) = status.code() {
263                                eprintln!("Command failed with exit code: {}", code);
264                            } else {
265                                eprintln!("Command terminated by signal");
266                            }
267                        }
268                    }
269                    Err(e) => {
270                        eprintln!("Error executing command: {}", e);
271                        if e.kind() == io::ErrorKind::NotFound {
272                            eprintln!("Command not found: {}", parts[0]);
273                        }
274                    }
275                }
276            }
277        }
278    }
279
280    fn show_help(&self) {
281        println!("Vapor Shell - Available Commands:");
282        println!("\nBuilt-in Commands:");
283        println!("  cd [dir]     Change directory (defaults to home if no dir specified)");
284        println!("  pwd          Print working directory");
285        println!("  history      Show command history");
286        println!("  help         Show this help message");
287        println!("  .vrepl       Exit shell and switch to SQL REPL for the current database");
288        println!("  exit         Exit shell and return to the main CLI prompt (or OS shell if launched directly)");
289        println!("\nSystem Commands:");
290        println!("  All standard Unix/Linux commands are available");
291        println!("  Command completion is available (press TAB)");
292        println!("  File/directory completion is available (press TAB)");
293        println!("\nFeatures:");
294        println!("  • Command history with arrow keys");
295        println!("  • Tab completion for commands and files");
296        println!("  • Command history persistence");
297        println!("  • Error handling and reporting");
298    }
299}
300
301/// Start shell mode with database context
302/// Start shell mode with database context
303pub fn shell_mode(db_path: &str) -> Result<ShellAction> {
304    println!("Starting shell mode for database: {}", db_path);
305    
306    let mut shell = Shell::new(db_path);
307    let action = shell.run();
308    
309    Ok(action)
310}