├── src ├── system │ ├── mod.rs │ ├── installer.rs │ └── login.rs ├── utils │ ├── mod.rs │ ├── performance.rs │ └── path_utils.rs ├── config │ └── mod.rs ├── main.rs ├── shell │ ├── documentation.rs │ ├── suggestions.rs │ ├── signal_handler.rs │ ├── command_processor.rs │ ├── alias.rs │ ├── shell_env.rs │ ├── executor.rs │ ├── job_control.rs │ ├── command_parser.rs │ └── mod.rs ├── llm │ ├── context_manager.rs │ ├── mod.rs │ └── api_client.rs ├── job_control.rs └── terminal │ ├── history.rs │ ├── completion.rs │ └── mod.rs ├── Cargo.toml ├── .gitignore ├── README.md └── Cargo.lock /src/system/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod login; 2 | pub mod installer; -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod performance; 2 | pub mod path_utils; 3 | 4 | pub use performance::*; -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::sync::Arc; 3 | 4 | #[derive(Clone)] 5 | pub struct Config { 6 | pub llm_host: String, 7 | pub llm_model: String, 8 | pub max_context_items: usize, 9 | pub suggestion_count: usize, 10 | pub command_preview: bool, 11 | } 12 | 13 | lazy_static! { 14 | pub static ref CONFIG: Arc = Arc::new(Config { 15 | llm_host: "http://192.168.86.201:11434".to_string(), 16 | llm_model: "qwen2.5:14b".to_string(), 17 | max_context_items: 10, 18 | suggestion_count: 3, 19 | command_preview: true, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "llm-shell" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tokio = { version = "1.28", features = ["full"] } 8 | reqwest = { version = "0.11", features = ["json"] } 9 | serde = { version = "1.0", features = ["derive"] } 10 | serde_json = "1.0" 11 | anyhow = "1.0" 12 | async-trait = "0.1" 13 | dirs = "5.0" 14 | nix = "0.26" 15 | rustyline = "11.0" 16 | log = "0.4" 17 | env_logger = "0.10" 18 | dotenv = "0.15" 19 | shellwords = "1.1" 20 | colored = "2.0" 21 | fuzzy-matcher = "0.3" 22 | async-recursion = "1.0" 23 | regex = "1.5" 24 | lazy_static = "1.4" 25 | libc = "0.2" 26 | hostname = "0.3" -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod shell; 2 | mod llm; 3 | mod terminal; 4 | mod system; 5 | mod utils; 6 | mod config; 7 | 8 | use crate::shell::Shell; 9 | use anyhow::Result; 10 | use std::env; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); 15 | dotenv::dotenv().ok(); 16 | 17 | // Handle installation if --install flag is present 18 | if env::args().any(|arg| arg == "--install") { 19 | let current_exe = env::current_exe()?; 20 | let installer = crate::system::installer::Installer::new(current_exe); 21 | installer.install()?; 22 | println!("LLM Shell installed successfully!"); 23 | return Ok(()); 24 | } 25 | 26 | let mut shell = Shell::new(); 27 | shell.run().await?; 28 | 29 | Ok(()) 30 | } -------------------------------------------------------------------------------- /src/shell/documentation.rs: -------------------------------------------------------------------------------- 1 | use crate::llm::LLMClient; 2 | use anyhow::Result; 3 | use std::collections::HashMap; 4 | 5 | pub struct Documentation { 6 | cache: HashMap, 7 | llm_client: LLMClient, 8 | } 9 | 10 | impl Documentation { 11 | pub fn new(llm_client: LLMClient) -> Self { 12 | Documentation { 13 | cache: HashMap::new(), 14 | llm_client, 15 | } 16 | } 17 | 18 | pub async fn get_command_help(&mut self, command: &str) -> Result { 19 | if let Some(cached) = self.cache.get(command) { 20 | return Ok(cached.clone()); 21 | } 22 | 23 | let explanation = self.llm_client.get_command_explanation(command).await?; 24 | self.cache.insert(command.to_string(), explanation.clone()); 25 | Ok(explanation) 26 | } 27 | 28 | pub fn clear_cache(&mut self) { 29 | self.cache.clear(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/system/installer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::path::PathBuf; 3 | use std::fs; 4 | 5 | pub struct Installer { 6 | binary_path: PathBuf, 7 | } 8 | 9 | impl Installer { 10 | pub fn new(binary_path: PathBuf) -> Self { 11 | Installer { binary_path } 12 | } 13 | 14 | pub fn install(&self) -> Result<()> { 15 | self.copy_binary()?; 16 | self.update_shells_file()?; 17 | Ok(()) 18 | } 19 | 20 | fn copy_binary(&self) -> Result<()> { 21 | fs::copy(&self.binary_path, "/usr/bin/llm-shell")?; 22 | Ok(()) 23 | } 24 | 25 | fn update_shells_file(&self) -> Result<()> { 26 | let shells_path = "/etc/shells"; 27 | let shell_path = "/usr/bin/llm-shell"; 28 | 29 | let content = fs::read_to_string(shells_path)?; 30 | if !content.contains(shell_path) { 31 | fs::write(shells_path, format!("{}\n{}", content, shell_path))?; 32 | } 33 | 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/llm/context_manager.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone)] 2 | pub struct ContextManager { 3 | current_dir: String, 4 | last_commands: Vec, 5 | } 6 | 7 | impl ContextManager { 8 | pub fn new() -> Self { 9 | ContextManager { 10 | current_dir: std::env::current_dir() 11 | .unwrap_or_default() 12 | .to_string_lossy() 13 | .to_string(), 14 | last_commands: Vec::new(), 15 | } 16 | } 17 | 18 | pub fn get_context(&self) -> String { 19 | format!( 20 | "Current directory: {}. Last commands: {}", 21 | self.current_dir, 22 | self.last_commands.join(", ") 23 | ) 24 | } 25 | 26 | pub fn update_directory(&mut self, new_dir: &str) { 27 | self.current_dir = new_dir.to_string(); 28 | } 29 | 30 | pub fn add_command(&mut self, command: &str) { 31 | self.last_commands.push(command.to_string()); 32 | if self.last_commands.len() > 5 { 33 | self.last_commands.remove(0); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/llm/mod.rs: -------------------------------------------------------------------------------- 1 | mod api_client; 2 | pub mod context_manager; 3 | 4 | use anyhow::Result; 5 | 6 | #[derive(Clone)] 7 | pub struct LLMClient { 8 | pub(crate) api_client: api_client::APIClient, 9 | pub(crate) context_manager: context_manager::ContextManager, 10 | } 11 | 12 | impl LLMClient { 13 | pub fn new() -> Self { 14 | LLMClient { 15 | api_client: api_client::APIClient::new(), 16 | context_manager: context_manager::ContextManager::new(), 17 | } 18 | } 19 | 20 | pub async fn translate_command(&self, natural_command: &str) -> Result { 21 | self.api_client.translate_command(natural_command).await 22 | } 23 | 24 | pub async fn get_command_explanation(&self, command: &str) -> Result { 25 | self.api_client.get_command_explanation(command).await 26 | } 27 | 28 | pub async fn suggest_commands(&self, context: &str, command_prefix: Option<&str>) -> Result> { 29 | self.api_client.suggest_commands(context, command_prefix).await 30 | } 31 | 32 | pub async fn chat(&self, question: &str) -> Result { 33 | self.api_client.chat(question).await 34 | } 35 | } -------------------------------------------------------------------------------- /src/shell/suggestions.rs: -------------------------------------------------------------------------------- 1 | use fuzzy_matcher::FuzzyMatcher; 2 | use fuzzy_matcher::skim::SkimMatcherV2; 3 | use std::collections::HashMap; 4 | 5 | pub struct SuggestionEngine { 6 | history: Vec, 7 | frequency_map: HashMap, 8 | matcher: SkimMatcherV2, 9 | } 10 | 11 | impl SuggestionEngine { 12 | pub fn new() -> Self { 13 | SuggestionEngine { 14 | history: Vec::new(), 15 | frequency_map: HashMap::new(), 16 | matcher: SkimMatcherV2::default(), 17 | } 18 | } 19 | 20 | pub fn add_command(&mut self, command: &str) { 21 | self.history.push(command.to_string()); 22 | *self.frequency_map.entry(command.to_string()).or_insert(0) += 1; 23 | } 24 | 25 | pub fn get_suggestions(&self, partial_input: &str) -> Vec { 26 | let mut matches: Vec<(i64, String)> = self.history 27 | .iter() 28 | .filter_map(|cmd| { 29 | self.matcher 30 | .fuzzy_match(cmd, partial_input) 31 | .map(|score| (score, cmd.clone())) 32 | }) 33 | .collect(); 34 | 35 | matches.sort_by(|a, b| b.0.cmp(&a.0)); 36 | matches.into_iter() 37 | .map(|(_, cmd)| cmd) 38 | .take(3) 39 | .collect() 40 | } 41 | } -------------------------------------------------------------------------------- /src/utils/performance.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use std::collections::VecDeque; 3 | use lazy_static::lazy_static; 4 | use std::sync::Mutex; 5 | 6 | lazy_static! { 7 | pub static ref PERFORMANCE_MONITOR: Mutex = Mutex::new(PerformanceMonitor::new(100)); 8 | } 9 | 10 | pub struct PerformanceMonitor { 11 | command_timings: VecDeque<(String, Duration)>, 12 | max_samples: usize, 13 | } 14 | 15 | impl PerformanceMonitor { 16 | pub fn new(max_samples: usize) -> Self { 17 | PerformanceMonitor { 18 | command_timings: VecDeque::new(), 19 | max_samples, 20 | } 21 | } 22 | 23 | pub fn record_execution(&mut self, command: &str, duration: Duration) { 24 | self.command_timings.push_back((command.to_string(), duration)); 25 | if self.command_timings.len() > self.max_samples { 26 | self.command_timings.pop_front(); 27 | } 28 | } 29 | 30 | pub fn get_average_duration(&self) -> Duration { 31 | if self.command_timings.is_empty() { 32 | return Duration::from_secs(0); 33 | } 34 | 35 | let total = self.command_timings 36 | .iter() 37 | .map(|(_, duration)| duration.as_millis()) 38 | .sum::(); 39 | 40 | Duration::from_millis((total / self.command_timings.len() as u128) as u64) 41 | } 42 | } -------------------------------------------------------------------------------- /src/job_control.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::process::{Command, Stdio}; 3 | 4 | pub struct JobControl { 5 | background_jobs: Vec, // PIDs of background processes 6 | } 7 | 8 | impl JobControl { 9 | pub fn new() -> Self { 10 | JobControl { 11 | background_jobs: Vec::new(), 12 | } 13 | } 14 | 15 | pub fn execute(&mut self, command: &str) -> Result<()> { 16 | let parts: Vec<&str> = shellwords::split(command)? 17 | .iter() 18 | .map(|s| s.as_str()) 19 | .collect(); 20 | if parts.is_empty() { 21 | return Ok(()); 22 | } 23 | 24 | let background = command.ends_with('&'); 25 | let command_str = if background { 26 | command[..command.len()-1].trim() 27 | } else { 28 | command 29 | }; 30 | 31 | let mut cmd = Command::new(parts[0]); 32 | cmd.args(&parts[1..]) 33 | .stdin(Stdio::inherit()) 34 | .stdout(Stdio::inherit()) 35 | .stderr(Stdio::inherit()); 36 | 37 | let child = cmd.spawn()?; 38 | 39 | if background { 40 | self.background_jobs.push(child.id()); 41 | println!("[{}] {}", self.background_jobs.len(), child.id()); 42 | } else { 43 | let status = child.wait()?; 44 | if !status.success() { 45 | eprintln!("Command failed with exit code: {}", status.code().unwrap_or(-1)); 46 | } 47 | } 48 | 49 | Ok(()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/system/login.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use anyhow::Result; 3 | 4 | pub struct LoginShell { 5 | home_dir: PathBuf, 6 | } 7 | 8 | impl LoginShell { 9 | pub fn new() -> Result { 10 | Ok(LoginShell { 11 | home_dir: dirs::home_dir().unwrap_or_default(), 12 | }) 13 | } 14 | 15 | pub fn initialize(&self) -> Result<()> { 16 | self.process_profile_files()?; 17 | self.setup_environment()?; 18 | Ok(()) 19 | } 20 | 21 | fn process_profile_files(&self) -> Result<()> { 22 | // Process global profile 23 | if let Ok(contents) = std::fs::read_to_string("/etc/profile") { 24 | self.process_profile_content(&contents)?; 25 | } 26 | 27 | // Process user profile 28 | let profile_path = self.home_dir.join(".profile"); 29 | if let Ok(contents) = std::fs::read_to_string(profile_path) { 30 | self.process_profile_content(&contents)?; 31 | } 32 | 33 | Ok(()) 34 | } 35 | 36 | fn process_profile_content(&self, content: &str) -> Result<()> { 37 | for line in content.lines() { 38 | if line.starts_with("export ") { 39 | let parts: Vec<&str> = line["export ".len()..].splitn(2, '=').collect(); 40 | if parts.len() == 2 { 41 | std::env::set_var(parts[0].trim(), parts[1].trim().trim_matches('"')); 42 | } 43 | } 44 | } 45 | Ok(()) 46 | } 47 | 48 | fn setup_environment(&self) -> Result<()> { 49 | if std::env::var("PATH").is_err() { 50 | std::env::set_var("PATH", "/usr/local/bin:/usr/bin:/bin"); 51 | } 52 | Ok(()) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/path_utils.rs: -------------------------------------------------------------------------------- 1 | // src/utils/path_utils.rs 2 | use std::path::{Path, PathBuf}; 3 | use std::env; 4 | use std::fs; 5 | 6 | pub fn find_executable(command: &str) -> Option { 7 | // If the command contains a path separator, check if it exists directly 8 | if command.contains('/') { 9 | let path = Path::new(command); 10 | if path.exists() && is_executable(path) { 11 | return Some(path.to_path_buf()); 12 | } 13 | return None; 14 | } 15 | 16 | // For common commands, try direct paths first 17 | let common_paths = [ 18 | "/bin", "/usr/bin", "/usr/local/bin", "/sbin", "/usr/sbin" 19 | ]; 20 | 21 | for dir in &common_paths { 22 | let path = Path::new(dir).join(command); 23 | if path.exists() && is_executable(&path) { 24 | return Some(path); 25 | } 26 | } 27 | 28 | // Otherwise, search in PATH 29 | if let Ok(path_var) = env::var("PATH") { 30 | for dir in path_var.split(':') { 31 | let path = Path::new(dir).join(command); 32 | if path.exists() && is_executable(&path) { 33 | return Some(path); 34 | } 35 | } 36 | } 37 | 38 | // If not found in PATH, just return the command itself 39 | // This allows the shell to handle the error more gracefully 40 | Some(PathBuf::from(command)) 41 | } 42 | 43 | #[cfg(unix)] 44 | fn is_executable(path: &Path) -> bool { 45 | use std::os::unix::fs::PermissionsExt; 46 | if let Ok(metadata) = fs::metadata(path) { 47 | let permissions = metadata.permissions(); 48 | return permissions.mode() & 0o111 != 0; 49 | } 50 | false 51 | } 52 | 53 | #[cfg(not(unix))] 54 | fn is_executable(path: &Path) -> bool { 55 | path.exists() 56 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore for LLM Shell 2 | 3 | # Rust build artifacts 4 | /target/ 5 | **/*.rs.bk 6 | Cargo.lock 7 | 8 | # IDE and editor files 9 | .idea/ 10 | .vscode/ 11 | *.swp 12 | *.swo 13 | *~ 14 | .DS_Store 15 | 16 | # Environment files 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | # Log files 24 | *.log 25 | logs/ 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | 42 | # nyc test coverage 43 | .nyc_output 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variable files 65 | .env 66 | 67 | # parcel-bundler cache 68 | .cache 69 | 70 | # Next.js build output 71 | .next 72 | out 73 | 74 | # Nuxt.js build / generate output 75 | .nuxt 76 | dist 77 | 78 | # Gatsby files 79 | .cache/ 80 | public 81 | 82 | # vuepress build output 83 | .vuepress/dist 84 | 85 | # Serverless directories 86 | .serverless/ 87 | 88 | # FuseBox cache 89 | .fusebox/ 90 | 91 | # DynamoDB Local files 92 | .dynamodb/ 93 | 94 | # TernJS port file 95 | .tern-port 96 | 97 | # Stores VSCode versions used for testing VSCode extensions 98 | .vscode-test 99 | 100 | # User-specific files 101 | *.rs.bk 102 | *.swp 103 | *.swo 104 | 105 | # Shell history files 106 | .llm_shell_history 107 | 108 | # Configuration files with potential secrets 109 | config.local.toml 110 | 111 | # Compiled binary 112 | llm-shell 113 | llmsh 114 | 115 | # Test data 116 | test_data/ 117 | -------------------------------------------------------------------------------- /src/shell/signal_handler.rs: -------------------------------------------------------------------------------- 1 | use nix::sys::signal::{self, Signal, SigHandler, SigAction, SigSet, SaFlags}; 2 | use std::sync::atomic::{AtomicBool, Ordering}; 3 | use std::sync::Arc; 4 | use log::debug; 5 | 6 | // Global flag to indicate if Ctrl+C was pressed 7 | lazy_static::lazy_static! { 8 | pub static ref INTERRUPT_RECEIVED: Arc = Arc::new(AtomicBool::new(false)); 9 | } 10 | 11 | pub struct SignalHandler; 12 | 13 | impl SignalHandler { 14 | pub fn initialize() -> Result<(), nix::Error> { 15 | debug!("Initializing signal handlers"); 16 | 17 | // Set up SIGINT (Ctrl+C) handler 18 | let sigint_action = SigAction::new( 19 | SigHandler::Handler(Self::handle_sigint), 20 | SaFlags::empty(), 21 | SigSet::empty(), 22 | ); 23 | unsafe { signal::sigaction(Signal::SIGINT, &sigint_action)? }; 24 | 25 | // Set up SIGTSTP (Ctrl+Z) handler 26 | let sigtstp_action = SigAction::new( 27 | SigHandler::Handler(Self::handle_sigtstp), 28 | SaFlags::empty(), 29 | SigSet::empty(), 30 | ); 31 | unsafe { signal::sigaction(Signal::SIGTSTP, &sigtstp_action)? }; 32 | 33 | // Set up SIGCHLD handler for child process termination 34 | let sigchld_action = SigAction::new( 35 | SigHandler::Handler(Self::handle_sigchld), 36 | SaFlags::empty(), 37 | SigSet::empty(), 38 | ); 39 | unsafe { signal::sigaction(Signal::SIGCHLD, &sigchld_action)? }; 40 | 41 | Ok(()) 42 | } 43 | 44 | extern "C" fn handle_sigint(_: i32) { 45 | INTERRUPT_RECEIVED.store(true, Ordering::SeqCst); 46 | // Print a newline to ensure the next prompt appears on a fresh line 47 | println!(); 48 | } 49 | 50 | extern "C" fn handle_sigtstp(_: i32) { 51 | // Default behavior is fine for now - just let the process be suspended 52 | } 53 | 54 | extern "C" fn handle_sigchld(_: i32) { 55 | // This will be handled by the job control system 56 | // We just need to catch the signal to prevent the default behavior 57 | } 58 | 59 | pub fn was_interrupted() -> bool { 60 | let was_interrupted = INTERRUPT_RECEIVED.load(Ordering::SeqCst); 61 | if was_interrupted { 62 | INTERRUPT_RECEIVED.store(false, Ordering::SeqCst); 63 | } 64 | was_interrupted 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/shell/command_processor.rs: -------------------------------------------------------------------------------- 1 | // src/shell/command_processor.rs 2 | use anyhow::Result; 3 | 4 | #[derive(Debug)] 5 | pub struct Command { 6 | pub command: String, 7 | pub is_natural_language: bool, 8 | } 9 | 10 | pub struct CommandProcessor; 11 | 12 | impl CommandProcessor { 13 | pub fn new() -> Self { 14 | CommandProcessor 15 | } 16 | 17 | pub fn parse(&self, input: &str) -> Result> { 18 | let mut commands = Vec::new(); 19 | 20 | // Split by semicolons to handle multiple commands 21 | for cmd_str in input.split(';') { 22 | let trimmed = cmd_str.trim(); 23 | if trimmed.is_empty() { 24 | continue; 25 | } 26 | 27 | // Check if this looks like natural language 28 | let is_natural_language = self.detect_natural_language(trimmed); 29 | 30 | commands.push(Command { 31 | command: trimmed.to_string(), 32 | is_natural_language, 33 | }); 34 | } 35 | 36 | Ok(commands) 37 | } 38 | 39 | fn detect_natural_language(&self, input: &str) -> bool { 40 | // Simple heuristic: if it has multiple words and doesn't start with a common command 41 | let common_commands = [ 42 | "ls", "cd", "grep", "find", "cat", "echo", "mkdir", "rm", "cp", "mv", 43 | "git", "docker", "ssh", "sudo", "apt", "yum", "dnf", "pacman", "brew", 44 | "python", "node", "npm", "cargo", "rustc", "gcc", "make", "ps", "top", 45 | "kill", "systemctl", "journalctl", "curl", "wget", "tar", "zip", "unzip", 46 | ]; 47 | 48 | let words: Vec<&str> = input.split_whitespace().collect(); 49 | if words.is_empty() { 50 | return false; 51 | } 52 | 53 | // If it starts with a common command, probably not natural language 54 | if common_commands.contains(&words[0]) { 55 | return false; 56 | } 57 | 58 | // If it has 4+ words, likely natural language 59 | if words.len() >= 4 { 60 | return true; 61 | } 62 | 63 | // Check for natural language patterns 64 | let natural_patterns = [ 65 | "show", "find", "list", "get", "display", "create", "make", "tell", 66 | "give", "use", "how", "what", "where", "can", "could", "would", "should", 67 | "explain", "help", "search", "look", "count", "calculate", "summarize", 68 | ]; 69 | 70 | natural_patterns.iter().any(|&pattern| words[0].eq_ignore_ascii_case(pattern)) 71 | } 72 | } -------------------------------------------------------------------------------- /src/terminal/history.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use std::fs::{File, OpenOptions}; 3 | use std::io::{BufRead, BufReader, Write}; 4 | use std::path::PathBuf; 5 | use dirs; 6 | 7 | pub struct History { 8 | history_file: PathBuf, 9 | max_history_size: usize, 10 | entries: Vec, 11 | } 12 | 13 | impl History { 14 | pub fn new() -> Result { 15 | let home_dir = dirs::home_dir().context("Could not determine home directory")?; 16 | let history_file = home_dir.join(".llm_shell_history"); 17 | 18 | let mut history = History { 19 | history_file, 20 | max_history_size: 1000, 21 | entries: Vec::new(), 22 | }; 23 | 24 | history.load()?; 25 | Ok(history) 26 | } 27 | 28 | pub fn load(&mut self) -> Result<()> { 29 | if !self.history_file.exists() { 30 | return Ok(()); 31 | } 32 | 33 | let file = File::open(&self.history_file)?; 34 | let reader = BufReader::new(file); 35 | 36 | self.entries.clear(); 37 | for line in reader.lines() { 38 | if let Ok(entry) = line { 39 | if !entry.trim().is_empty() { 40 | self.entries.push(entry); 41 | } 42 | } 43 | } 44 | 45 | // Trim to max size 46 | if self.entries.len() > self.max_history_size { 47 | self.entries = self.entries[self.entries.len() - self.max_history_size..].to_vec(); 48 | } 49 | 50 | Ok(()) 51 | } 52 | 53 | pub fn save(&self) -> Result<()> { 54 | let mut file = OpenOptions::new() 55 | .write(true) 56 | .create(true) 57 | .truncate(true) 58 | .open(&self.history_file)?; 59 | 60 | for entry in &self.entries { 61 | writeln!(file, "{}", entry)?; 62 | } 63 | 64 | Ok(()) 65 | } 66 | 67 | pub fn add(&mut self, entry: &str) -> Result<()> { 68 | let entry = entry.trim(); 69 | if entry.is_empty() { 70 | return Ok(()); 71 | } 72 | 73 | // Don't add duplicate of the last command 74 | if let Some(last) = self.entries.last() { 75 | if last == entry { 76 | return Ok(()); 77 | } 78 | } 79 | 80 | self.entries.push(entry.to_string()); 81 | 82 | // Trim to max size 83 | if self.entries.len() > self.max_history_size { 84 | self.entries.remove(0); 85 | } 86 | 87 | self.save()?; 88 | Ok(()) 89 | } 90 | 91 | pub fn get_entries(&self) -> &[String] { 92 | &self.entries 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/shell/alias.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::collections::HashMap; 3 | use std::fs; 4 | 5 | pub struct AliasManager { 6 | aliases: HashMap, 7 | } 8 | 9 | impl AliasManager { 10 | pub fn new() -> Self { 11 | AliasManager { 12 | aliases: HashMap::new(), 13 | } 14 | } 15 | 16 | pub fn initialize(&mut self) -> Result<()> { 17 | // Load system aliases 18 | if let Ok(content) = fs::read_to_string("/etc/bash.bashrc") { 19 | self.parse_aliases(&content); 20 | } 21 | 22 | // Load user aliases 23 | if let Some(home) = dirs::home_dir() { 24 | let bashrc = home.join(".bashrc"); 25 | if let Ok(content) = fs::read_to_string(bashrc) { 26 | self.parse_aliases(&content); 27 | } 28 | 29 | // Load custom aliases file if it exists 30 | let aliases_file = home.join(".llm_shell_aliases"); 31 | if aliases_file.exists() { 32 | if let Ok(content) = fs::read_to_string(aliases_file) { 33 | self.parse_aliases(&content); 34 | } 35 | } 36 | } 37 | 38 | // Add some default aliases 39 | self.add_default_aliases(); 40 | 41 | Ok(()) 42 | } 43 | 44 | fn parse_aliases(&mut self, content: &str) { 45 | for line in content.lines() { 46 | let line = line.trim(); 47 | 48 | // Skip comments and empty lines 49 | if line.is_empty() || line.starts_with('#') { 50 | continue; 51 | } 52 | 53 | // Parse alias definitions 54 | if line.starts_with("alias ") { 55 | let alias_def = &line["alias ".len()..]; 56 | if let Some(equals_pos) = alias_def.find('=') { 57 | let name = alias_def[..equals_pos].trim(); 58 | let mut value = alias_def[equals_pos + 1..].trim(); 59 | 60 | // Remove surrounding quotes if present 61 | if (value.starts_with('\'') && value.ends_with('\'')) || 62 | (value.starts_with('"') && value.ends_with('"')) { 63 | value = &value[1..value.len() - 1]; 64 | } 65 | 66 | self.aliases.insert(name.to_string(), value.to_string()); 67 | } 68 | } 69 | } 70 | } 71 | 72 | fn add_default_aliases(&mut self) { 73 | // Add some useful default aliases 74 | self.aliases.insert("ll".to_string(), "ls -la".to_string()); 75 | self.aliases.insert("la".to_string(), "ls -A".to_string()); 76 | self.aliases.insert("l".to_string(), "ls -CF".to_string()); 77 | self.aliases.insert("..".to_string(), "cd ..".to_string()); 78 | self.aliases.insert("...".to_string(), "cd ../..".to_string()); 79 | } 80 | 81 | pub fn expand(&self, command: &str) -> String { 82 | let parts: Vec<&str> = command.split_whitespace().collect(); 83 | if parts.is_empty() { 84 | return command.to_string(); 85 | } 86 | 87 | if let Some(alias) = self.aliases.get(parts[0]) { 88 | if parts.len() > 1 { 89 | format!("{} {}", alias, parts[1..].join(" ")) 90 | } else { 91 | alias.clone() 92 | } 93 | } else { 94 | command.to_string() 95 | } 96 | } 97 | 98 | pub fn add_alias(&mut self, name: &str, value: &str) -> Result<()> { 99 | self.aliases.insert(name.to_string(), value.to_string()); 100 | self.save_aliases()?; 101 | Ok(()) 102 | } 103 | 104 | pub fn remove_alias(&mut self, name: &str) -> Result<()> { 105 | self.aliases.remove(name); 106 | self.save_aliases()?; 107 | Ok(()) 108 | } 109 | 110 | pub fn list_aliases(&self) -> Vec<(String, String)> { 111 | self.aliases 112 | .iter() 113 | .map(|(k, v)| (k.clone(), v.clone())) 114 | .collect() 115 | } 116 | 117 | fn save_aliases(&self) -> Result<()> { 118 | if let Some(home) = dirs::home_dir() { 119 | let aliases_file = home.join(".llm_shell_aliases"); 120 | let mut content = String::new(); 121 | 122 | for (name, value) in &self.aliases { 123 | content.push_str(&format!("alias {}='{}'\n", name, value)); 124 | } 125 | 126 | fs::write(aliases_file, content)?; 127 | } 128 | 129 | Ok(()) 130 | } 131 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LLM Shell 2 | 3 | A powerful shell environment enhanced with Large Language Model capabilities for natural language command processing. 4 | 5 | ## ⚠️ WARNING ⚠️ 6 | 7 | **IMPORTANT SECURITY AND SAFETY NOTICE:** 8 | 9 | - **This shell can execute arbitrary commands on your system based on natural language input.** 10 | - **LLMs can hallucinate or misinterpret your intent, potentially leading to destructive commands.** 11 | - **Always review translated commands before confirming execution, especially for destructive operations.** 12 | - **Never run this shell with elevated privileges (root/sudo) unless absolutely necessary.** 13 | - **Use in production environments at your own risk - this is primarily a development tool.** 14 | - **The shell may transmit command context to external LLM services.** 15 | 16 | By using LLM Shell, you accept full responsibility for any consequences resulting from commands executed through this interface. 17 | 18 | ## Features 19 | 20 | - Natural language command processing 21 | - Command explanations and suggestions 22 | - Environment variable support 23 | - Built-in shell commands 24 | - Command history and completion 25 | - Job control 26 | - LLM-powered assistance 27 | 28 | ## 10 Cool Ways to Use LLM Shell 29 | 30 | 1. **Natural Language Commands** 31 | ``` 32 | find all python files modified in the last week 33 | ``` 34 | LLM Shell translates this to the appropriate `find` command with the correct syntax. 35 | 36 | 2. **Ask Questions Directly** 37 | ``` 38 | ? how do I check disk usage in Linux 39 | ``` 40 | Get answers to technical questions without leaving your terminal. 41 | 42 | 3. **Command Suggestions** 43 | ``` 44 | git ?? 45 | ``` 46 | Append `??` to any command to get contextually relevant suggestions. 47 | 48 | 4. **Complex Command Generation** 49 | ``` 50 | create a backup of my home directory excluding node_modules folders 51 | ``` 52 | Generate complex commands with exclude patterns without memorizing syntax. 53 | 54 | 5. **Command Explanations** 55 | ``` 56 | awk '{print $1}' file.txt ?? 57 | ``` 58 | Get explanations of what complex commands actually do. 59 | 60 | 6. **Data Processing Tasks** 61 | ``` 62 | extract all email addresses from log.txt 63 | ``` 64 | Let the LLM generate the appropriate regex and command. 65 | 66 | 7. **System Administration** 67 | ``` 68 | show me all processes using more than 1GB of memory 69 | ``` 70 | Generate and execute system monitoring commands easily. 71 | 72 | 8. **File Operations** 73 | ``` 74 | find and delete all empty directories under the current path 75 | ``` 76 | Perform complex file operations with simple language. 77 | 78 | 9. **Network Diagnostics** 79 | ``` 80 | check if port 8080 is open and what process is using it 81 | ``` 82 | Simplify network troubleshooting with natural language. 83 | 84 | 10. **Learning Tool** 85 | ``` 86 | ? what's the difference between grep and egrep 87 | ``` 88 | Use the shell as a learning platform to understand command-line tools better. 89 | 90 | ## Installation 91 | 92 | ```bash 93 | # Clone the repository 94 | git clone https://github.com/phildougherty/llmsh.git 95 | 96 | # Build the project 97 | cd llmsh 98 | cargo build --release 99 | 100 | # Install the binary 101 | sudo cp target/release/llm-shell /usr/local/bin/llmsh 102 | 103 | # Run the shell 104 | llmsh 105 | ``` 106 | 107 | ## Configuration 108 | 109 | LLM Shell uses the following environment variables: 110 | 111 | - `RUST_LOG`: Set log level (info, warn, error, debug) 112 | - `LLM_HOST`: URL of the LLM service (default: http://localhost:11434) 113 | - `LLM_MODEL`: Model to use (default: qwen2.5:14b) 114 | 115 | ## Usage 116 | 117 | - Regular shell commands work as expected 118 | - Start with `?` to ask a question 119 | - Type natural language for command translation 120 | - Append `??` to any command for suggestions 121 | - Use `help` to see built-in commands 122 | 123 | ## Built-in Commands 124 | 125 | - `cd [dir]`: Change directory 126 | - `pwd`: Print working directory 127 | - `export VAR=VALUE`: Set environment variable 128 | - `echo [text]`: Display text 129 | - `alias [name[=value]]`: Manage aliases 130 | - `history`: View command history 131 | - `help`: Show help information 132 | - And many more standard shell built-ins 133 | 134 | ## Contributing 135 | 136 | Contributions are welcome! Please feel free to submit a Pull Request. 137 | 138 | ## License 139 | 140 | This project is licensed under the MIT License - see the LICENSE file for details. 141 | 142 | ## Acknowledgments 143 | 144 | - Built with Rust 145 | - Powered by [Ollama](https://ollama.ai/) or other LLM providers 146 | - Inspired by traditional Unix shells and modern AI assistants 147 | -------------------------------------------------------------------------------- /src/shell/shell_env.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use std::env; 3 | use std::fs; 4 | use log::debug; 5 | 6 | pub struct Environment { 7 | env_vars: std::collections::HashMap, 8 | is_login_shell: bool, 9 | } 10 | 11 | impl Environment { 12 | pub fn new(is_login_shell: bool) -> Self { 13 | Environment { 14 | env_vars: std::collections::HashMap::new(), 15 | is_login_shell, 16 | } 17 | } 18 | 19 | pub fn initialize(&mut self) -> Result<()> { 20 | // Set basic environment variables 21 | self.set_default_env_vars(); 22 | 23 | // Process profile files for login shells 24 | if self.is_login_shell { 25 | self.process_login_files()?; 26 | } 27 | 28 | // Process rc files for all shells 29 | self.process_rc_files()?; 30 | 31 | // Apply all environment variables 32 | self.apply_env_vars(); 33 | 34 | Ok(()) 35 | } 36 | 37 | fn set_default_env_vars(&mut self) { 38 | // Set PATH if not already set or append to it 39 | let default_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; 40 | 41 | if let Ok(current_path) = env::var("PATH") { 42 | if !current_path.contains(default_path) { 43 | self.env_vars.insert("PATH".to_string(), format!("{}:{}", current_path, default_path)); 44 | } 45 | } else { 46 | self.env_vars.insert("PATH".to_string(), default_path.to_string()); 47 | } 48 | 49 | // Set HOME if not already set 50 | if env::var("HOME").is_err() { 51 | if let Some(home) = dirs::home_dir() { 52 | self.env_vars.insert("HOME".to_string(), home.to_string_lossy().to_string()); 53 | } 54 | } 55 | 56 | // Set SHELL to point to our shell 57 | if let Ok(exe) = env::current_exe() { 58 | self.env_vars.insert("SHELL".to_string(), exe.to_string_lossy().to_string()); 59 | } 60 | 61 | // Set basic terminal variables 62 | self.env_vars.insert("TERM".to_string(), "xterm-256color".to_string()); 63 | } 64 | 65 | fn process_login_files(&mut self) -> Result<()> { 66 | debug!("Processing login files"); 67 | 68 | // Process /etc/profile 69 | if let Ok(content) = fs::read_to_string("/etc/profile") { 70 | self.parse_env_file(&content); 71 | } 72 | 73 | // Process ~/.profile 74 | let home = dirs::home_dir().context("Could not determine home directory")?; 75 | let profile_path = home.join(".profile"); 76 | if let Ok(content) = fs::read_to_string(profile_path) { 77 | self.parse_env_file(&content); 78 | } 79 | 80 | // Process ~/.bash_profile or ~/.bash_login if they exist 81 | let bash_profile = home.join(".bash_profile"); 82 | let bash_login = home.join(".bash_login"); 83 | 84 | if bash_profile.exists() { 85 | if let Ok(content) = fs::read_to_string(bash_profile) { 86 | self.parse_env_file(&content); 87 | } 88 | } else if bash_login.exists() { 89 | if let Ok(content) = fs::read_to_string(bash_login) { 90 | self.parse_env_file(&content); 91 | } 92 | } 93 | 94 | Ok(()) 95 | } 96 | 97 | fn process_rc_files(&mut self) -> Result<()> { 98 | debug!("Processing rc files"); 99 | 100 | // Process /etc/bashrc 101 | if let Ok(content) = fs::read_to_string("/etc/bashrc") { 102 | self.parse_env_file(&content); 103 | } 104 | 105 | // Process ~/.bashrc 106 | let home = dirs::home_dir().context("Could not determine home directory")?; 107 | let bashrc_path = home.join(".bashrc"); 108 | if let Ok(content) = fs::read_to_string(bashrc_path) { 109 | self.parse_env_file(&content); 110 | } 111 | 112 | // Process ~/.llm_shellrc if it exists 113 | let llm_shellrc = home.join(".llm_shellrc"); 114 | if llm_shellrc.exists() { 115 | if let Ok(content) = fs::read_to_string(llm_shellrc) { 116 | self.parse_env_file(&content); 117 | } 118 | } 119 | 120 | Ok(()) 121 | } 122 | 123 | fn parse_env_file(&mut self, content: &str) { 124 | for line in content.lines() { 125 | let line = line.trim(); 126 | 127 | // Skip comments and empty lines 128 | if line.is_empty() || line.starts_with('#') { 129 | continue; 130 | } 131 | 132 | // Handle export statements 133 | if line.starts_with("export ") { 134 | let parts: Vec<&str> = line["export ".len()..].splitn(2, '=').collect(); 135 | if parts.len() == 2 { 136 | let key = parts[0].trim(); 137 | let value = parts[1].trim().trim_matches('"').trim_matches('\''); 138 | self.env_vars.insert(key.to_string(), value.to_string()); 139 | } 140 | } 141 | } 142 | } 143 | 144 | fn apply_env_vars(&self) { 145 | for (key, value) in &self.env_vars { 146 | env::set_var(key, value); 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /src/terminal/completion.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | use std::collections::HashSet; 5 | 6 | pub struct CompletionEngine { 7 | commands: HashSet, 8 | } 9 | 10 | impl CompletionEngine { 11 | pub fn new() -> Self { 12 | CompletionEngine { 13 | commands: HashSet::new(), 14 | } 15 | } 16 | 17 | pub fn initialize(&mut self) -> Result<()> { 18 | // Load commands from PATH 19 | self.load_commands_from_path()?; 20 | 21 | // Add built-in commands 22 | self.add_builtin_commands(); 23 | 24 | Ok(()) 25 | } 26 | 27 | fn load_commands_from_path(&mut self) -> Result<()> { 28 | if let Ok(path) = std::env::var("PATH") { 29 | for path_entry in path.split(':') { 30 | let path_dir = Path::new(path_entry); 31 | if path_dir.exists() && path_dir.is_dir() { 32 | if let Ok(entries) = fs::read_dir(path_dir) { 33 | for entry in entries.flatten() { 34 | if let Ok(file_type) = entry.file_type() { 35 | if file_type.is_file() { 36 | if let Some(name) = entry.file_name().to_str() { 37 | // Check if the file is executable 38 | if let Ok(metadata) = entry.metadata() { 39 | let permissions = metadata.permissions(); 40 | #[cfg(unix)] 41 | { 42 | use std::os::unix::fs::PermissionsExt; 43 | if permissions.mode() & 0o111 != 0 { 44 | self.commands.insert(name.to_string()); 45 | } 46 | } 47 | #[cfg(not(unix))] 48 | { 49 | self.commands.insert(name.to_string()); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | fn add_builtin_commands(&mut self) { 65 | // Add shell built-ins 66 | let builtins = [ 67 | "cd", "alias", "unalias", "exit", "help", "jobs", "fg", "bg", 68 | "echo", "export", "source", ".", "history", "pwd", "type", 69 | ]; 70 | 71 | for cmd in builtins { 72 | self.commands.insert(cmd.to_string()); 73 | } 74 | } 75 | 76 | pub fn get_commands(&self) -> Vec { 77 | self.commands.iter().cloned().collect() 78 | } 79 | 80 | pub fn complete_command(&self, partial: &str) -> Vec { 81 | self.commands 82 | .iter() 83 | .filter(|cmd| cmd.starts_with(partial)) 84 | .cloned() 85 | .collect() 86 | } 87 | 88 | pub fn complete_path(&self, partial: &str) -> Vec { 89 | let mut results = Vec::new(); 90 | 91 | // Handle home directory expansion 92 | let expanded_partial = if partial.starts_with('~') { 93 | if let Some(home) = dirs::home_dir() { 94 | if partial.len() == 1 { 95 | // Just "~" 96 | home.to_string_lossy().to_string() 97 | } else { 98 | // "~/something" 99 | home.join(&partial[2..]).to_string_lossy().to_string() 100 | } 101 | } else { 102 | partial.to_string() 103 | } 104 | } else { 105 | partial.to_string() 106 | }; 107 | 108 | // Split into directory and file parts 109 | let (dir_part, file_part) = if let Some(last_slash) = expanded_partial.rfind('/') { 110 | let dir = &expanded_partial[..=last_slash]; 111 | let file = &expanded_partial[last_slash + 1..]; 112 | (PathBuf::from(dir), file.to_string()) 113 | } else { 114 | (PathBuf::from("."), expanded_partial) 115 | }; 116 | 117 | // Read directory entries 118 | if dir_part.exists() && dir_part.is_dir() { 119 | if let Ok(entries) = fs::read_dir(&dir_part) { 120 | for entry in entries.flatten() { 121 | if let Some(name) = entry.file_name().to_str() { 122 | if name.starts_with(&file_part) { 123 | let mut full_path = dir_part.join(name); 124 | 125 | // Add trailing slash for directories 126 | if let Ok(metadata) = entry.metadata() { 127 | if metadata.is_dir() { 128 | full_path = full_path.join(""); 129 | } 130 | } 131 | 132 | results.push(full_path.to_string_lossy().to_string()); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | results 140 | } 141 | } -------------------------------------------------------------------------------- /src/shell/executor.rs: -------------------------------------------------------------------------------- 1 | // src/shell/executor.rs 2 | use anyhow::{Result, Context}; 3 | use std::process::{Command, Stdio}; 4 | use std::fs::{File, OpenOptions}; 5 | use std::io::Write; 6 | use crate::shell::command_parser::{Pipeline, SimpleCommand, Redirection}; 7 | use crate::utils::path_utils; 8 | 9 | pub struct Executor; 10 | 11 | impl Executor { 12 | pub fn execute(pipeline: &Pipeline) -> Result { 13 | if pipeline.commands.is_empty() { 14 | return Ok(0); 15 | } 16 | 17 | // Single command without pipes 18 | if pipeline.commands.len() == 1 && !pipeline.commands[0].redirections.contains(&Redirection::Pipe) { 19 | return Self::execute_simple_command(&pipeline.commands[0], pipeline.background); 20 | } 21 | 22 | // Pipeline with multiple commands 23 | let mut children = Vec::new(); 24 | let mut prev_stdout = None; 25 | 26 | for (i, cmd) in pipeline.commands.iter().enumerate() { 27 | let is_last = i == pipeline.commands.len() - 1; 28 | 29 | // Set up stdin from previous command's stdout 30 | let stdin = if let Some(prev_out) = prev_stdout.take() { 31 | Stdio::from(prev_out) 32 | } else { 33 | Stdio::inherit() 34 | }; 35 | 36 | // Set up stdout for piping to next command 37 | let stdout = if is_last { 38 | Stdio::inherit() 39 | } else { 40 | Stdio::piped() 41 | }; 42 | 43 | // Create the command 44 | let mut command = Self::create_command(cmd)?; 45 | command.stdin(stdin); 46 | command.stdout(stdout); 47 | 48 | // Apply redirections 49 | Self::apply_redirections(&mut command, cmd)?; 50 | 51 | // Spawn the command 52 | let mut child = command.spawn() 53 | .with_context(|| format!("Failed to spawn command: {}", cmd.program))?; 54 | 55 | // Save stdout for the next command if not the last command 56 | if !is_last { 57 | prev_stdout = child.stdout.take(); 58 | } 59 | 60 | // Add to list of children 61 | children.push(child); 62 | } 63 | 64 | // Wait for all children to complete 65 | let mut exit_code = 0; 66 | for mut child in children { 67 | let status = child.wait() 68 | .with_context(|| "Failed to wait for child process")?; 69 | if !status.success() { 70 | exit_code = status.code().unwrap_or(1); 71 | } 72 | } 73 | 74 | Ok(exit_code) 75 | } 76 | 77 | fn execute_simple_command(cmd: &SimpleCommand, background: bool) -> Result { 78 | // Create the command 79 | let mut command = Self::create_command(cmd)?; 80 | 81 | // Apply redirections 82 | Self::apply_redirections(&mut command, cmd)?; 83 | 84 | if background { 85 | // Run in background 86 | let child = command.spawn() 87 | .with_context(|| format!("Failed to spawn command: {}", cmd.program))?; 88 | println!("[{}] {}", child.id(), cmd.program); 89 | Ok(0) 90 | } else { 91 | // Run in foreground 92 | let status = command.status() 93 | .with_context(|| format!("Failed to execute command: {}", cmd.program))?; 94 | Ok(status.code().unwrap_or(0)) 95 | } 96 | } 97 | 98 | fn create_command(cmd: &SimpleCommand) -> Result { 99 | // Find the executable 100 | let executable = path_utils::find_executable(&cmd.program) 101 | .with_context(|| format!("Command not found: {}", cmd.program))?; 102 | 103 | // Create the command 104 | let mut command = Command::new(executable); 105 | 106 | // Add arguments 107 | command.args(&cmd.args); 108 | 109 | Ok(command) 110 | } 111 | 112 | fn apply_redirections(command: &mut Command, cmd: &SimpleCommand) -> Result<()> { 113 | for redirection in &cmd.redirections { 114 | match redirection { 115 | Redirection::Input(filename) => { 116 | let file = File::open(filename) 117 | .with_context(|| format!("Failed to open file for input: {}", filename))?; 118 | command.stdin(Stdio::from(file)); 119 | }, 120 | Redirection::Output(filename) => { 121 | let file = File::create(filename) 122 | .with_context(|| format!("Failed to create file for output: {}", filename))?; 123 | command.stdout(Stdio::from(file)); 124 | }, 125 | Redirection::Append(filename) => { 126 | let file = OpenOptions::new() 127 | .write(true) 128 | .append(true) 129 | .create(true) 130 | .open(filename) 131 | .with_context(|| format!("Failed to open file for append: {}", filename))?; 132 | command.stdout(Stdio::from(file)); 133 | }, 134 | Redirection::ErrorOutput(filename) => { 135 | let file = File::create(filename) 136 | .with_context(|| format!("Failed to create file for error output: {}", filename))?; 137 | command.stderr(Stdio::from(file)); 138 | }, 139 | Redirection::ErrorAppend(filename) => { 140 | let file = OpenOptions::new() 141 | .write(true) 142 | .append(true) 143 | .create(true) 144 | .open(filename) 145 | .with_context(|| format!("Failed to open file for error append: {}", filename))?; 146 | command.stderr(Stdio::from(file)); 147 | }, 148 | Redirection::Pipe => { 149 | // Pipes are handled separately 150 | }, 151 | } 152 | } 153 | 154 | Ok(()) 155 | } 156 | } -------------------------------------------------------------------------------- /src/llm/api_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use reqwest::Client; 3 | use serde::{Deserialize, Serialize}; 4 | use crate::config::CONFIG; 5 | use regex::Regex; 6 | use lazy_static::lazy_static; 7 | 8 | #[derive(Clone)] 9 | pub struct APIClient { 10 | client: Client, 11 | } 12 | 13 | #[derive(Debug, Serialize)] 14 | struct OllamaRequest { 15 | model: String, 16 | messages: Vec, 17 | stream: bool, 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | struct Message { 22 | role: String, 23 | content: String, 24 | } 25 | 26 | #[derive(Debug, Deserialize)] 27 | struct OllamaResponse { 28 | choices: Vec, 29 | } 30 | 31 | #[derive(Debug, Deserialize)] 32 | struct Choice { 33 | message: Message, 34 | } 35 | 36 | lazy_static! { 37 | static ref CODE_BLOCK_RE: Regex = Regex::new(r"```(?:shell|bash)?\s*([^`]+)```").unwrap(); 38 | } 39 | 40 | impl APIClient { 41 | pub fn new() -> Self { 42 | APIClient { 43 | client: Client::new(), 44 | } 45 | } 46 | 47 | pub async fn chat(&self, question: &str) -> Result { 48 | let request = OllamaRequest { 49 | model: CONFIG.llm_model.clone(), 50 | messages: vec![ 51 | Message { 52 | role: "system".to_string(), 53 | content: "You are a helpful command-line assistant. Provide clear, concise answers.".to_string(), 54 | }, 55 | Message { 56 | role: "user".to_string(), 57 | content: question.to_string(), 58 | }, 59 | ], 60 | stream: false, 61 | }; 62 | 63 | let response = self.client 64 | .post(format!("{}/v1/chat/completions", CONFIG.llm_host)) 65 | .json(&request) 66 | .send() 67 | .await? 68 | .json::() 69 | .await?; 70 | 71 | Ok(response.choices[0].message.content.trim().to_string()) 72 | } 73 | 74 | pub async fn translate_command(&self, natural_command: &str) -> Result { 75 | let request = OllamaRequest { 76 | model: CONFIG.llm_model.clone(), 77 | messages: vec![ 78 | Message { 79 | role: "system".to_string(), 80 | content: "You are a shell command translator. Convert natural language to shell commands. Respond ONLY with the exact command to execute, nothing else. No markdown, no explanations.".to_string(), 81 | }, 82 | Message { 83 | role: "user".to_string(), 84 | content: natural_command.to_string(), 85 | }, 86 | ], 87 | stream: false, 88 | }; 89 | 90 | let response = self.client 91 | .post(format!("{}/v1/chat/completions", CONFIG.llm_host)) 92 | .json(&request) 93 | .send() 94 | .await? 95 | .json::() 96 | .await?; 97 | 98 | let command = response.choices[0].message.content.clone(); 99 | Ok(self.clean_command_output(&command)) 100 | } 101 | 102 | pub async fn get_command_explanation(&self, command: &str) -> Result { 103 | let request = OllamaRequest { 104 | model: CONFIG.llm_model.clone(), 105 | messages: vec![ 106 | Message { 107 | role: "system".to_string(), 108 | content: "Explain what this shell command does in one brief sentence:".to_string(), 109 | }, 110 | Message { 111 | role: "user".to_string(), 112 | content: command.to_string(), 113 | }, 114 | ], 115 | stream: false, 116 | }; 117 | 118 | let response = self.client 119 | .post(format!("{}/v1/chat/completions", CONFIG.llm_host)) 120 | .json(&request) 121 | .send() 122 | .await? 123 | .json::() 124 | .await?; 125 | 126 | Ok(response.choices[0].message.content.trim().to_string()) 127 | } 128 | 129 | pub async fn suggest_commands(&self, context: &str, command_prefix: Option<&str>) -> Result> { 130 | let system_prompt = if let Some(prefix) = command_prefix { 131 | format!( 132 | "Suggest 3 useful variations or related commands for '{}'. Provide only the commands, one per line, no explanations.", 133 | prefix 134 | ) 135 | } else { 136 | "Suggest 3 useful shell commands based on the current context. Provide only the commands, one per line, no explanations.".to_string() 137 | }; 138 | 139 | let request = OllamaRequest { 140 | model: CONFIG.llm_model.clone(), 141 | messages: vec![ 142 | Message { 143 | role: "system".to_string(), 144 | content: system_prompt, 145 | }, 146 | Message { 147 | role: "user".to_string(), 148 | content: context.to_string(), 149 | }, 150 | ], 151 | stream: false, 152 | }; 153 | 154 | let response = self.client 155 | .post(format!("{}/v1/chat/completions", CONFIG.llm_host)) 156 | .json(&request) 157 | .send() 158 | .await? 159 | .json::() 160 | .await?; 161 | 162 | Ok(response.choices[0].message.content 163 | .lines() 164 | .map(|s| self.clean_command_output(s)) 165 | .filter(|s| !s.is_empty()) 166 | .collect()) 167 | } 168 | 169 | fn clean_command_output(&self, output: &str) -> String { 170 | // First try to extract command from code blocks 171 | if let Some(captures) = CODE_BLOCK_RE.captures(output) { 172 | if let Some(command) = captures.get(1) { 173 | return command.as_str().trim().to_string(); 174 | } 175 | } 176 | 177 | // If no code blocks, clean up the raw output 178 | output 179 | .lines() 180 | .next() 181 | .unwrap_or(output) 182 | .trim() 183 | .trim_matches('`') 184 | .to_string() 185 | } 186 | } -------------------------------------------------------------------------------- /src/terminal/mod.rs: -------------------------------------------------------------------------------- 1 | mod history; 2 | mod completion; 3 | 4 | use anyhow::Result; 5 | use rustyline::{DefaultEditor, Config, EditMode}; 6 | use std::path::PathBuf; 7 | use colored::*; 8 | use std::env; 9 | use std::process::Command; 10 | use self::history::History; 11 | use self::completion::CompletionEngine; 12 | 13 | pub struct Terminal { 14 | editor: DefaultEditor, 15 | history: History, 16 | completion_engine: CompletionEngine, 17 | } 18 | 19 | impl Terminal { 20 | pub fn new() -> Self { 21 | // Configure rustyline 22 | let config = Config::builder() 23 | .edit_mode(EditMode::Emacs) 24 | .auto_add_history(false) 25 | .completion_type(rustyline::CompletionType::List) 26 | .build(); 27 | 28 | let editor = DefaultEditor::with_config(config).unwrap_or_else(|_| DefaultEditor::new().unwrap()); 29 | 30 | // Initialize history 31 | let history = History::new().unwrap_or_else(|e| { 32 | eprintln!("Warning: Failed to initialize history: {}", e); 33 | History::new().unwrap() 34 | }); 35 | 36 | // Initialize completion engine 37 | let mut completion_engine = CompletionEngine::new(); 38 | completion_engine.initialize().unwrap_or_else(|e| { 39 | eprintln!("Warning: Failed to initialize completion engine: {}", e); 40 | }); 41 | 42 | Terminal { 43 | editor, 44 | history, 45 | completion_engine, 46 | } 47 | } 48 | 49 | pub fn read_line(&mut self) -> Result<(String, bool)> { 50 | let prompt = self.create_prompt()?; 51 | 52 | // Read input with tab completion 53 | let line = match self.editor.readline(&prompt) { 54 | Ok(line) => line, 55 | Err(err) => { 56 | // Handle different error types 57 | if err.to_string().contains("interrupted") { 58 | // Ctrl+C was pressed 59 | return Ok(("".to_string(), false)); 60 | } else if err.to_string().contains("eof") { 61 | // Ctrl+D was pressed - exit 62 | return Ok(("exit".to_string(), false)); 63 | } else { 64 | return Err(anyhow::anyhow!("Error reading input: {}", err)); 65 | } 66 | } 67 | }; 68 | 69 | let trimmed = line.trim(); 70 | 71 | // Consider showing suggestions if the line ends with '??' 72 | let show_suggestions = trimmed.ends_with("??"); 73 | let line = trimmed.trim_end_matches('?').to_string(); 74 | 75 | // Add to history if non-empty 76 | if !line.is_empty() { 77 | self.history.add(&line)?; 78 | self.editor.add_history_entry(&line)?; 79 | } 80 | 81 | Ok((line, show_suggestions)) 82 | } 83 | 84 | fn create_prompt(&self) -> Result { 85 | let cwd = env::current_dir()?; 86 | let home = dirs::home_dir().unwrap_or_default(); 87 | let path = self.shorten_path(cwd, &home); 88 | 89 | let username = env::var("USER").unwrap_or_else(|_| "user".to_string()); 90 | let hostname = self.get_hostname(); 91 | let git_info = self.get_git_info()?; 92 | 93 | // Create a fancy multi-line prompt 94 | Ok(format!("\n{}{}{}{}{}", 95 | "┌─[".bright_blue(), 96 | username.bright_green(), 97 | "@".bright_blue(), 98 | hostname.bright_cyan(), 99 | "]".bright_blue(), 100 | ) + &format!("─[{}]", path.bright_yellow()) + &git_info + "\n" + 101 | &format!("└─{} ", "❯".bright_purple())) 102 | } 103 | 104 | fn get_hostname(&self) -> String { 105 | if let Ok(hostname) = Command::new("hostname") 106 | .output() 107 | .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string()) { 108 | hostname 109 | } else { 110 | "unknown".to_string() 111 | } 112 | } 113 | 114 | fn shorten_path(&self, path: PathBuf, home: &PathBuf) -> String { 115 | let path_str = path.to_string_lossy(); 116 | if let Ok(stripped) = path.strip_prefix(home) { 117 | if stripped.as_os_str().is_empty() { 118 | "~".to_string() 119 | } else { 120 | format!("~/{}", stripped.to_string_lossy()) 121 | } 122 | } else { 123 | path_str.to_string() 124 | } 125 | } 126 | 127 | fn get_git_info(&self) -> Result { 128 | // First check if we're in a git repository 129 | let is_git_repo = Command::new("git") 130 | .args(&["rev-parse", "--is-inside-work-tree"]) 131 | .output() 132 | .map(|output| output.status.success()) 133 | .unwrap_or(false); 134 | 135 | if !is_git_repo { 136 | return Ok(String::new()); 137 | } 138 | 139 | // Try to get git branch 140 | let branch = Command::new("git") 141 | .args(&["rev-parse", "--abbrev-ref", "HEAD"]) 142 | .output() 143 | .ok() 144 | .and_then(|output| { 145 | if output.status.success() { 146 | String::from_utf8(output.stdout).ok() 147 | } else { 148 | None 149 | } 150 | }); 151 | 152 | // Try to get git status 153 | let status_clean = Command::new("git") 154 | .args(&["diff", "--quiet"]) 155 | .status() 156 | .map(|status| status.success()) 157 | .unwrap_or(true); 158 | 159 | match branch { 160 | Some(branch) => { 161 | let branch = branch.trim(); 162 | let status_symbol = if status_clean { 163 | "✓".green() 164 | } else { 165 | "✗".red() 166 | }; 167 | 168 | // Get ahead/behind status 169 | let ahead_behind = self.get_git_ahead_behind()?; 170 | 171 | Ok(format!("─[{}{}{}", 172 | branch.bright_purple(), 173 | status_symbol, 174 | ahead_behind 175 | ) + "]") 176 | } 177 | None => Ok(String::new()) 178 | } 179 | } 180 | 181 | fn get_git_ahead_behind(&self) -> Result { 182 | // Get ahead/behind counts 183 | let output = Command::new("git") 184 | .args(&["rev-list", "--count", "--left-right", "@{upstream}...HEAD"]) 185 | .output(); 186 | 187 | match output { 188 | Ok(output) => { 189 | if output.status.success() { 190 | let counts = String::from_utf8_lossy(&output.stdout).trim().to_string(); 191 | let parts: Vec<&str> = counts.split_whitespace().collect(); 192 | 193 | if parts.len() == 2 { 194 | let behind = parts[0].parse::().unwrap_or(0); 195 | let ahead = parts[1].parse::().unwrap_or(0); 196 | 197 | let mut status = String::new(); 198 | if ahead > 0 { 199 | status.push_str(&format!(" ↑{}", ahead).yellow().to_string()); 200 | } 201 | if behind > 0 { 202 | status.push_str(&format!(" ↓{}", behind).red().to_string()); 203 | } 204 | 205 | Ok(status) 206 | } else { 207 | Ok(String::new()) 208 | } 209 | } else { 210 | // Not tracking a remote branch 211 | Ok(String::new()) 212 | } 213 | } 214 | Err(_) => Ok(String::new()) 215 | } 216 | } 217 | 218 | pub fn get_history(&self) -> &History { 219 | &self.history 220 | } 221 | 222 | pub fn add_to_history(&mut self, entry: &str) -> Result<()> { 223 | self.history.add(entry) 224 | } 225 | } 226 | 227 | impl Drop for Terminal { 228 | fn drop(&mut self) { 229 | // Save history when terminal is dropped 230 | if let Err(e) = self.history.save() { 231 | eprintln!("Warning: Failed to save history: {}", e); 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /src/shell/job_control.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, Context}; 2 | use std::process::{Command, Stdio, Child}; 3 | use std::collections::HashMap; 4 | use std::sync::{Arc, Mutex}; 5 | use nix::sys::signal::{self, Signal}; 6 | use nix::unistd::Pid; 7 | use std::time::SystemTime; 8 | use libc; 9 | 10 | #[derive(Debug)] 11 | pub struct Job { 12 | pid: u32, 13 | command: String, 14 | status: JobStatus, 15 | start_time: SystemTime, 16 | } 17 | 18 | #[derive(Debug, Clone, PartialEq)] 19 | pub enum JobStatus { 20 | Running, 21 | Stopped, 22 | Completed(i32), 23 | Failed(i32), 24 | } 25 | 26 | #[derive(Default)] 27 | pub struct JobControl { 28 | jobs: HashMap, 29 | last_job_id: u32, 30 | foreground_job: Option, 31 | job_mutex: Arc>, 32 | } 33 | 34 | impl JobControl { 35 | pub fn new() -> Self { 36 | Self { 37 | jobs: HashMap::new(), 38 | last_job_id: 0, 39 | foreground_job: None, 40 | job_mutex: Arc::new(Mutex::new(())), 41 | } 42 | } 43 | 44 | pub fn execute(&mut self, input_command: &str) -> Result<()> { 45 | // Check if the command contains pipes 46 | if input_command.contains('|') { 47 | // For piped commands, use the shell to execute 48 | let mut cmd = Command::new("sh"); 49 | cmd.arg("-c") 50 | .arg(input_command) 51 | .stdin(Stdio::inherit()) 52 | .stdout(Stdio::inherit()) 53 | .stderr(Stdio::inherit()); 54 | 55 | let status = cmd.status() 56 | .with_context(|| format!("Failed to execute command: {}", input_command))?; 57 | 58 | if !status.success() { 59 | eprintln!("Command failed with exit code: {}", status.code().unwrap_or(-1)); 60 | } 61 | 62 | return Ok(()); 63 | } 64 | 65 | // For non-piped commands, continue with the existing logic 66 | let parts: Vec = shellwords::split(input_command) 67 | .with_context(|| format!("Failed to parse command: {}", input_command))?; 68 | 69 | if parts.is_empty() { 70 | return Ok(()); 71 | } 72 | 73 | // Handle built-in commands 74 | match parts[0].as_str() { 75 | "jobs" => return self.list_jobs(), 76 | "fg" => return self.bring_to_foreground(&parts), 77 | "bg" => return self.continue_in_background(&parts), 78 | _ => {} 79 | } 80 | 81 | let background = input_command.ends_with('&'); 82 | let exec_command = if background { 83 | input_command[..input_command.len()-1].trim() 84 | } else { 85 | input_command 86 | }; 87 | 88 | let mut cmd = Command::new(&parts[0]); 89 | if parts.len() > 1 { 90 | cmd.args(&parts[1..]); 91 | } 92 | 93 | cmd.stdin(Stdio::inherit()) 94 | .stdout(Stdio::inherit()) 95 | .stderr(Stdio::inherit()); 96 | 97 | let child = cmd.spawn() 98 | .with_context(|| format!("Failed to spawn command: {}", exec_command))?; 99 | 100 | let job = Job { 101 | pid: child.id(), 102 | command: exec_command.to_string(), 103 | status: JobStatus::Running, 104 | start_time: SystemTime::now(), 105 | }; 106 | 107 | self.last_job_id += 1; 108 | let job_id = self.last_job_id; 109 | self.jobs.insert(job_id, job); 110 | 111 | if background { 112 | println!("[{}] {} {}", job_id, child.id(), exec_command); 113 | self.monitor_background_job(job_id, child); 114 | } else { 115 | self.foreground_job = Some(job_id); 116 | self.wait_for_foreground_job(child)?; 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | fn monitor_background_job(&self, job_id: u32, mut child: Child) { 123 | let job_mutex = self.job_mutex.clone(); 124 | std::thread::spawn(move || { 125 | let status = child.wait(); 126 | let _lock = job_mutex.lock().unwrap(); 127 | 128 | if let Ok(status) = status { 129 | if let Some(code) = status.code() { 130 | if status.success() { 131 | println!("[{}] Done {}", job_id, code); 132 | } else { 133 | println!("[{}] Exit {}", job_id, code); 134 | } 135 | } 136 | } 137 | }); 138 | } 139 | 140 | fn wait_for_foreground_job(&mut self, mut child: Child) -> Result<()> { 141 | let status = child.wait() 142 | .with_context(|| "Failed to wait for foreground process")?; 143 | 144 | if let Some(job_id) = self.foreground_job.take() { 145 | if let Some(job) = self.jobs.get_mut(&job_id) { 146 | job.status = if let Some(code) = status.code() { 147 | if status.success() { 148 | JobStatus::Completed(code) 149 | } else { 150 | JobStatus::Failed(code) 151 | } 152 | } else { 153 | JobStatus::Failed(-1) 154 | }; 155 | } 156 | } 157 | 158 | Ok(()) 159 | } 160 | 161 | pub fn list_jobs(&self) -> Result<()> { 162 | for (job_id, job) in &self.jobs { 163 | let runtime = job.start_time.elapsed() 164 | .unwrap_or_default() 165 | .as_secs(); 166 | 167 | let status = match job.status { 168 | JobStatus::Running => "Running", 169 | JobStatus::Stopped => "Stopped", 170 | JobStatus::Completed(_) => "Done", 171 | JobStatus::Failed(_) => "Failed", 172 | }; 173 | 174 | println!("[{}] {:?} {} ({} sec) {}", 175 | job_id, 176 | job.pid, 177 | status, 178 | runtime, 179 | job.command 180 | ); 181 | } 182 | Ok(()) 183 | } 184 | 185 | pub fn bring_to_foreground(&mut self, args: &[String]) -> Result<()> { 186 | let job_id = if args.len() > 1 { 187 | args[1].parse::() 188 | .with_context(|| "Invalid job ID")? 189 | } else { 190 | self.last_job_id 191 | }; 192 | 193 | if let Some(job) = self.jobs.get(&job_id) { 194 | let pid = Pid::from_raw(job.pid as i32); 195 | signal::kill(pid, Signal::SIGCONT) 196 | .with_context(|| format!("Failed to send SIGCONT to pid {}", job.pid))?; 197 | 198 | self.foreground_job = Some(job_id); 199 | println!("Brought job {} to foreground: {}", job_id, job.command); 200 | 201 | // Wait for the job to complete or stop 202 | self.wait_for_job(job_id)?; 203 | } else { 204 | println!("No such job: {}", job_id); 205 | } 206 | 207 | Ok(()) 208 | } 209 | 210 | pub fn continue_in_background(&mut self, args: &[String]) -> Result<()> { 211 | let job_id = if args.len() > 1 { 212 | args[1].parse::() 213 | .with_context(|| "Invalid job ID")? 214 | } else { 215 | self.last_job_id 216 | }; 217 | 218 | if let Some(job) = self.jobs.get(&job_id) { 219 | let pid = Pid::from_raw(job.pid as i32); 220 | signal::kill(pid, Signal::SIGCONT) 221 | .with_context(|| format!("Failed to send SIGCONT to pid {}", job.pid))?; 222 | 223 | println!("Continued job {} in background: {}", job_id, job.command); 224 | } else { 225 | println!("No such job: {}", job_id); 226 | } 227 | 228 | Ok(()) 229 | } 230 | 231 | fn wait_for_job(&self, job_id: u32) -> Result<()> { 232 | if let Some(job) = self.jobs.get(&job_id) { 233 | let pid = Pid::from_raw(job.pid as i32); 234 | let mut status = 0; 235 | unsafe { 236 | libc::waitpid(pid.as_raw(), &mut status, 0); 237 | } 238 | } 239 | Ok(()) 240 | } 241 | 242 | pub fn cleanup_completed_jobs(&mut self) { 243 | self.jobs.retain(|_, job| { 244 | matches!(job.status, JobStatus::Running | JobStatus::Stopped) 245 | }); 246 | } 247 | 248 | pub fn handle_sigchld(&mut self) -> Result<()> { 249 | loop { 250 | match unsafe { libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) } { 251 | 0 => break, // No more children have status changes 252 | -1 => break, // Error (probably no children) 253 | pid => { 254 | if let Some(job_id) = self.find_job_by_pid(pid as u32) { 255 | if let Some(job) = self.jobs.get_mut(&job_id) { 256 | job.status = JobStatus::Completed(0); 257 | } 258 | } 259 | } 260 | } 261 | } 262 | Ok(()) 263 | } 264 | 265 | fn find_job_by_pid(&self, pid: u32) -> Option { 266 | self.jobs 267 | .iter() 268 | .find(|(_, job)| job.pid == pid) 269 | .map(|(job_id, _)| *job_id) 270 | } 271 | 272 | pub fn get_job_status(&self, job_id: u32) -> Option { 273 | self.jobs.get(&job_id).map(|job| job.status.clone()) 274 | } 275 | } 276 | 277 | impl Drop for JobControl { 278 | fn drop(&mut self) { 279 | // Attempt to clean up any remaining jobs 280 | for (_, job) in &self.jobs { 281 | if matches!(job.status, JobStatus::Running | JobStatus::Stopped) { 282 | let pid = Pid::from_raw(job.pid as i32); 283 | let _ = signal::kill(pid, Signal::SIGTERM); 284 | } 285 | } 286 | } 287 | } -------------------------------------------------------------------------------- /src/shell/command_parser.rs: -------------------------------------------------------------------------------- 1 | // src/shell/command_parser.rs 2 | use anyhow::{Result, Context}; 3 | use std::path::PathBuf; 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub enum Redirection { 7 | Input(String), // < 8 | Output(String), // > 9 | Append(String), // >> 10 | ErrorOutput(String), // 2> 11 | ErrorAppend(String), // 2>> 12 | Pipe, // | 13 | } 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct SimpleCommand { 17 | pub program: String, 18 | pub args: Vec, 19 | pub redirections: Vec, 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | pub struct Pipeline { 24 | pub commands: Vec, 25 | pub background: bool, 26 | } 27 | 28 | pub struct CommandParser; 29 | 30 | impl CommandParser { 31 | pub fn parse(input: &str) -> Result { 32 | let mut commands = Vec::new(); 33 | let mut current_command = SimpleCommand { 34 | program: String::new(), 35 | args: Vec::new(), 36 | redirections: Vec::new(), 37 | }; 38 | let mut background = false; 39 | let mut in_quotes = false; 40 | let mut quote_char = ' '; 41 | let mut current_token = String::new(); 42 | let mut i = 0; 43 | let chars: Vec = input.chars().collect(); 44 | 45 | while i < chars.len() { 46 | let c = chars[i]; 47 | 48 | // Handle quotes 49 | if (c == '"' || c == '\'') && (!in_quotes || quote_char == c) { 50 | if in_quotes { 51 | in_quotes = false; 52 | } else { 53 | in_quotes = true; 54 | quote_char = c; 55 | } 56 | i += 1; 57 | continue; 58 | } 59 | 60 | // Inside quotes, just add the character 61 | if in_quotes { 62 | current_token.push(c); 63 | i += 1; 64 | continue; 65 | } 66 | 67 | // Handle pipe 68 | if c == '|' { 69 | if !current_token.is_empty() { 70 | if current_command.program.is_empty() { 71 | current_command.program = current_token; 72 | } else { 73 | current_command.args.push(current_token); 74 | } 75 | current_token = String::new(); 76 | } 77 | current_command.redirections.push(Redirection::Pipe); 78 | commands.push(current_command); 79 | current_command = SimpleCommand { 80 | program: String::new(), 81 | args: Vec::new(), 82 | redirections: Vec::new(), 83 | }; 84 | i += 1; 85 | continue; 86 | } 87 | 88 | // Handle redirections 89 | if c == '<' || c == '>' { 90 | if !current_token.is_empty() { 91 | if current_command.program.is_empty() { 92 | current_command.program = current_token; 93 | } else { 94 | current_command.args.push(current_token); 95 | } 96 | current_token = String::new(); 97 | } 98 | 99 | // Check for >> or 2> or 2>> 100 | if c == '>' && i + 1 < chars.len() && chars[i + 1] == '>' { 101 | // >> 102 | i += 2; 103 | // Skip whitespace 104 | while i < chars.len() && chars[i].is_whitespace() { 105 | i += 1; 106 | } 107 | // Read the filename 108 | let mut filename = String::new(); 109 | while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '|' && chars[i] != '<' && chars[i] != '>' { 110 | filename.push(chars[i]); 111 | i += 1; 112 | } 113 | current_command.redirections.push(Redirection::Append(filename)); 114 | } else if i > 0 && chars[i - 1] == '2' && c == '>' && i + 1 < chars.len() && chars[i + 1] == '>' { 115 | // 2>> 116 | i += 2; 117 | // Skip whitespace 118 | while i < chars.len() && chars[i].is_whitespace() { 119 | i += 1; 120 | } 121 | // Read the filename 122 | let mut filename = String::new(); 123 | while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '|' && chars[i] != '<' && chars[i] != '>' { 124 | filename.push(chars[i]); 125 | i += 1; 126 | } 127 | current_command.redirections.push(Redirection::ErrorAppend(filename)); 128 | } else if i > 0 && chars[i - 1] == '2' && c == '>' { 129 | // 2> 130 | i += 1; 131 | // Skip whitespace 132 | while i < chars.len() && chars[i].is_whitespace() { 133 | i += 1; 134 | } 135 | // Read the filename 136 | let mut filename = String::new(); 137 | while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '|' && chars[i] != '<' && chars[i] != '>' { 138 | filename.push(chars[i]); 139 | i += 1; 140 | } 141 | current_command.redirections.push(Redirection::ErrorOutput(filename)); 142 | } else if c == '>' { 143 | // > 144 | i += 1; 145 | // Skip whitespace 146 | while i < chars.len() && chars[i].is_whitespace() { 147 | i += 1; 148 | } 149 | // Read the filename 150 | let mut filename = String::new(); 151 | while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '|' && chars[i] != '<' && chars[i] != '>' { 152 | filename.push(chars[i]); 153 | i += 1; 154 | } 155 | current_command.redirections.push(Redirection::Output(filename)); 156 | } else if c == '<' { 157 | // < 158 | i += 1; 159 | // Skip whitespace 160 | while i < chars.len() && chars[i].is_whitespace() { 161 | i += 1; 162 | } 163 | // Read the filename 164 | let mut filename = String::new(); 165 | while i < chars.len() && !chars[i].is_whitespace() && chars[i] != '|' && chars[i] != '<' && chars[i] != '>' { 166 | filename.push(chars[i]); 167 | i += 1; 168 | } 169 | current_command.redirections.push(Redirection::Input(filename)); 170 | } 171 | continue; 172 | } 173 | 174 | // Handle background 175 | if c == '&' && i == chars.len() - 1 { 176 | background = true; 177 | break; 178 | } 179 | 180 | // Handle whitespace 181 | if c.is_whitespace() { 182 | if !current_token.is_empty() { 183 | if current_command.program.is_empty() { 184 | current_command.program = current_token; 185 | } else { 186 | current_command.args.push(current_token); 187 | } 188 | current_token = String::new(); 189 | } 190 | i += 1; 191 | continue; 192 | } 193 | 194 | // Add character to current token 195 | current_token.push(c); 196 | i += 1; 197 | } 198 | 199 | // Add the last token 200 | if !current_token.is_empty() { 201 | if current_command.program.is_empty() { 202 | current_command.program = current_token; 203 | } else { 204 | current_command.args.push(current_token); 205 | } 206 | } 207 | 208 | // Add the last command 209 | if !current_command.program.is_empty() { 210 | commands.push(current_command); 211 | } 212 | 213 | Ok(Pipeline { 214 | commands, 215 | background, 216 | }) 217 | } 218 | } 219 | 220 | #[cfg(test)] 221 | mod tests { 222 | use super::*; 223 | 224 | #[test] 225 | fn test_simple_command() { 226 | let input = "ls -la"; 227 | let pipeline = CommandParser::parse(input).unwrap(); 228 | assert_eq!(pipeline.commands.len(), 1); 229 | assert_eq!(pipeline.commands[0].program, "ls"); 230 | assert_eq!(pipeline.commands[0].args, vec!["-la"]); 231 | assert_eq!(pipeline.commands[0].redirections.len(), 0); 232 | assert_eq!(pipeline.background, false); 233 | } 234 | 235 | #[test] 236 | fn test_pipe() { 237 | let input = "ls -la | grep Cargo"; 238 | let pipeline = CommandParser::parse(input).unwrap(); 239 | assert_eq!(pipeline.commands.len(), 2); 240 | assert_eq!(pipeline.commands[0].program, "ls"); 241 | assert_eq!(pipeline.commands[0].args, vec!["-la"]); 242 | assert_eq!(pipeline.commands[0].redirections.len(), 1); 243 | assert_eq!(pipeline.commands[0].redirections[0], Redirection::Pipe); 244 | assert_eq!(pipeline.commands[1].program, "grep"); 245 | assert_eq!(pipeline.commands[1].args, vec!["Cargo"]); 246 | assert_eq!(pipeline.commands[1].redirections.len(), 0); 247 | assert_eq!(pipeline.background, false); 248 | } 249 | 250 | #[test] 251 | fn test_redirections() { 252 | let input = "cat < input.txt > output.txt"; 253 | let pipeline = CommandParser::parse(input).unwrap(); 254 | assert_eq!(pipeline.commands.len(), 1); 255 | assert_eq!(pipeline.commands[0].program, "cat"); 256 | assert_eq!(pipeline.commands[0].args.len(), 0); 257 | assert_eq!(pipeline.commands[0].redirections.len(), 2); 258 | match &pipeline.commands[0].redirections[0] { 259 | Redirection::Input(filename) => assert_eq!(filename, "input.txt"), 260 | _ => panic!("Expected Input redirection"), 261 | } 262 | match &pipeline.commands[0].redirections[1] { 263 | Redirection::Output(filename) => assert_eq!(filename, "output.txt"), 264 | _ => panic!("Expected Output redirection"), 265 | } 266 | assert_eq!(pipeline.background, false); 267 | } 268 | 269 | #[test] 270 | fn test_append() { 271 | let input = "echo hello >> output.txt"; 272 | let pipeline = CommandParser::parse(input).unwrap(); 273 | assert_eq!(pipeline.commands.len(), 1); 274 | assert_eq!(pipeline.commands[0].program, "echo"); 275 | assert_eq!(pipeline.commands[0].args, vec!["hello"]); 276 | assert_eq!(pipeline.commands[0].redirections.len(), 1); 277 | match &pipeline.commands[0].redirections[0] { 278 | Redirection::Append(filename) => assert_eq!(filename, "output.txt"), 279 | _ => panic!("Expected Append redirection"), 280 | } 281 | assert_eq!(pipeline.background, false); 282 | } 283 | 284 | #[test] 285 | fn test_error_redirection() { 286 | let input = "gcc program.c 2> errors.txt"; 287 | let pipeline = CommandParser::parse(input).unwrap(); 288 | assert_eq!(pipeline.commands.len(), 1); 289 | assert_eq!(pipeline.commands[0].program, "gcc"); 290 | assert_eq!(pipeline.commands[0].args, vec!["program.c"]); 291 | assert_eq!(pipeline.commands[0].redirections.len(), 1); 292 | match &pipeline.commands[0].redirections[0] { 293 | Redirection::ErrorOutput(filename) => assert_eq!(filename, "errors.txt"), 294 | _ => panic!("Expected ErrorOutput redirection"), 295 | } 296 | assert_eq!(pipeline.background, false); 297 | } 298 | 299 | #[test] 300 | fn test_background() { 301 | let input = "sleep 10 &"; 302 | let pipeline = CommandParser::parse(input).unwrap(); 303 | assert_eq!(pipeline.commands.len(), 1); 304 | assert_eq!(pipeline.commands[0].program, "sleep"); 305 | assert_eq!(pipeline.commands[0].args, vec!["10"]); 306 | assert_eq!(pipeline.commands[0].redirections.len(), 0); 307 | assert_eq!(pipeline.background, true); 308 | } 309 | 310 | #[test] 311 | fn test_complex_command() { 312 | let input = "find . -name \"*.rs\" | xargs grep \"fn main\" > results.txt 2> errors.txt &"; 313 | let pipeline = CommandParser::parse(input).unwrap(); 314 | assert_eq!(pipeline.commands.len(), 2); 315 | assert_eq!(pipeline.commands[0].program, "find"); 316 | assert_eq!(pipeline.commands[0].args, vec![".", "-name", "*.rs"]); 317 | assert_eq!(pipeline.commands[0].redirections.len(), 1); 318 | assert_eq!(pipeline.commands[0].redirections[0], Redirection::Pipe); 319 | assert_eq!(pipeline.commands[1].program, "xargs"); 320 | assert_eq!(pipeline.commands[1].args, vec!["grep", "fn main"]); 321 | assert_eq!(pipeline.commands[1].redirections.len(), 2); 322 | match &pipeline.commands[1].redirections[0] { 323 | Redirection::Output(filename) => assert_eq!(filename, "results.txt"), 324 | _ => panic!("Expected Output redirection"), 325 | } 326 | match &pipeline.commands[1].redirections[1] { 327 | Redirection::ErrorOutput(filename) => assert_eq!(filename, "errors.txt"), 328 | _ => panic!("Expected ErrorOutput redirection"), 329 | } 330 | assert_eq!(pipeline.background, true); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/shell/mod.rs: -------------------------------------------------------------------------------- 1 | mod command_processor; 2 | mod job_control; 3 | mod suggestions; 4 | mod documentation; 5 | mod shell_env; 6 | mod alias; 7 | mod signal_handler; 8 | mod command_parser; 9 | mod executor; 10 | 11 | use std::io::Write; 12 | use std::os::unix::process::CommandExt; 13 | use std::path::PathBuf; 14 | use colored::*; 15 | use anyhow::{Result, Context}; 16 | use crate::llm::LLMClient; 17 | use crate::terminal::Terminal; 18 | use crate::llm::context_manager::ContextManager; 19 | use crate::shell::suggestions::SuggestionEngine; 20 | use crate::shell::documentation::Documentation; 21 | use crate::utils::performance::PERFORMANCE_MONITOR; 22 | use log::debug; 23 | 24 | pub struct Shell { 25 | terminal: Terminal, 26 | command_processor: command_processor::CommandProcessor, 27 | job_control: job_control::JobControl, 28 | llm_client: LLMClient, 29 | working_dir: PathBuf, 30 | suggestion_engine: SuggestionEngine, 31 | documentation: Documentation, 32 | context_manager: ContextManager, 33 | environment: shell_env::Environment, 34 | alias_manager: alias::AliasManager, 35 | } 36 | 37 | impl Shell { 38 | pub fn new() -> Self { 39 | let llm_client = LLMClient::new(); 40 | 41 | // Initialize signal handler 42 | signal_handler::SignalHandler::initialize().unwrap_or_else(|e| { 43 | eprintln!("Warning: Failed to initialize signal handlers: {}", e); 44 | }); 45 | 46 | // Determine if this is a login shell 47 | let is_login_shell = std::env::args() 48 | .next() 49 | .map(|arg| arg.starts_with('-')) 50 | .unwrap_or(false); 51 | 52 | // Create environment manager 53 | let mut environment = shell_env::Environment::new(is_login_shell); 54 | environment.initialize().unwrap_or_else(|e| { 55 | eprintln!("Warning: Failed to initialize environment: {}", e); 56 | }); 57 | 58 | // Create alias manager 59 | let mut alias_manager = alias::AliasManager::new(); 60 | alias_manager.initialize().unwrap_or_else(|e| { 61 | eprintln!("Warning: Failed to initialize aliases: {}", e); 62 | }); 63 | 64 | Shell { 65 | terminal: Terminal::new(), 66 | command_processor: command_processor::CommandProcessor::new(), 67 | job_control: job_control::JobControl::new(), 68 | suggestion_engine: SuggestionEngine::new(), 69 | documentation: Documentation::new(llm_client.clone()), 70 | context_manager: ContextManager::new(), 71 | llm_client, 72 | working_dir: std::env::current_dir().unwrap_or_default(), 73 | environment, 74 | alias_manager, 75 | } 76 | } 77 | 78 | fn expand_env_vars(&self, value: &str) -> String { 79 | let mut result = value.to_string(); 80 | let mut i = 0; 81 | 82 | while i < result.len() { 83 | if result[i..].starts_with('$') { 84 | let var_start = i; 85 | i += 1; // Skip the $ 86 | 87 | // Handle ${VAR} format 88 | if i < result.len() && result[i..].starts_with('{') { 89 | i += 1; // Skip the { 90 | let var_name_start = i; 91 | 92 | // Find closing brace 93 | while i < result.len() && !result[i..].starts_with('}') { 94 | i += 1; 95 | } 96 | 97 | if i < result.len() { 98 | let var_name = &result[var_name_start..i]; 99 | i += 1; // Skip the } 100 | 101 | if let Ok(value) = std::env::var(var_name) { 102 | result.replace_range(var_start..i, &value); 103 | i = var_start + value.len(); 104 | } 105 | } 106 | } 107 | // Handle $VAR format 108 | else { 109 | let var_name_start = i; 110 | 111 | // Find end of variable name (alphanumeric or _) 112 | while i < result.len() && (result[i..].chars().next().unwrap().is_alphanumeric() || result[i..].starts_with('_')) { 113 | i += 1; 114 | } 115 | 116 | if i > var_name_start { 117 | let var_name = &result[var_name_start..i]; 118 | 119 | if let Ok(value) = std::env::var(var_name) { 120 | result.replace_range(var_start..i, &value); 121 | i = var_start + value.len(); 122 | } 123 | } 124 | } 125 | } else { 126 | i += 1; 127 | } 128 | } 129 | 130 | result 131 | } 132 | 133 | pub async fn run(&mut self) -> Result<()> { 134 | self.initialize()?; 135 | 136 | loop { 137 | let (input, show_suggestions) = self.terminal.read_line()?; 138 | let input = input.trim(); 139 | 140 | // Check for interrupt 141 | if signal_handler::SignalHandler::was_interrupted() { 142 | continue; 143 | } 144 | 145 | if input.is_empty() { 146 | continue; 147 | } 148 | 149 | if input == "exit" { 150 | break; 151 | } 152 | 153 | // Handle built-in commands 154 | if let Some(result) = self.handle_builtin_command(input) { 155 | match result { 156 | Ok(should_exit) => { 157 | if should_exit { 158 | break; 159 | } 160 | continue; 161 | } 162 | Err(e) => { 163 | eprintln!("Error: {}", e); 164 | continue; 165 | } 166 | } 167 | } 168 | 169 | // Handle suggestions 170 | if show_suggestions { 171 | let command_prefix = input.split_whitespace().next(); 172 | if let Ok(suggestions) = self.show_suggestions(command_prefix).await { 173 | println!("{}", suggestions); 174 | continue; 175 | } 176 | } 177 | 178 | // Expand aliases 179 | let expanded_input = self.alias_manager.expand(input); 180 | 181 | // Update context 182 | self.context_manager.update_directory(&self.working_dir.to_string_lossy()); 183 | self.context_manager.add_command(&expanded_input); 184 | 185 | let start_time = std::time::Instant::now(); 186 | 187 | // Process the input 188 | self.process_input(&expanded_input).await?; 189 | 190 | // Record execution time 191 | let duration = start_time.elapsed(); 192 | PERFORMANCE_MONITOR.lock().unwrap().record_execution(&expanded_input, duration); 193 | 194 | // Update working directory 195 | if let Ok(dir) = std::env::current_dir() { 196 | self.working_dir = dir; 197 | } 198 | 199 | // Clean up any completed background jobs 200 | self.job_control.cleanup_completed_jobs(); 201 | } 202 | 203 | Ok(()) 204 | } 205 | 206 | fn handle_builtin_command(&mut self, input: &str) -> Option> { 207 | let parts: Vec<&str> = input.split_whitespace().collect(); 208 | if parts.is_empty() { 209 | return None; 210 | } 211 | 212 | match parts[0] { 213 | // Directory navigation 214 | "cd" => { 215 | let dir_to_use = if parts.len() > 1 { 216 | parts[1].to_string() 217 | } else { 218 | // Default to home directory 219 | dirs::home_dir() 220 | .and_then(|p| p.to_str().map(|s| s.to_string())) 221 | .unwrap_or_else(|| ".".to_string()) 222 | }; 223 | 224 | // Handle ~ expansion 225 | let expanded_dir = if dir_to_use.starts_with('~') { 226 | if let Some(home) = dirs::home_dir() { 227 | if dir_to_use.len() == 1 { 228 | home.to_string_lossy().to_string() 229 | } else { 230 | home.join(&dir_to_use[2..]).to_string_lossy().to_string() 231 | } 232 | } else { 233 | dir_to_use 234 | } 235 | } else { 236 | dir_to_use 237 | }; 238 | 239 | match std::env::set_current_dir(&expanded_dir) { 240 | Ok(_) => { 241 | if let Ok(new_dir) = std::env::current_dir() { 242 | self.working_dir = new_dir; 243 | self.context_manager.update_directory(&self.working_dir.to_string_lossy()); 244 | } 245 | Some(Ok(false)) 246 | } 247 | Err(e) => Some(Err(anyhow::anyhow!("cd: {}: {}", expanded_dir, e))), 248 | } 249 | }, 250 | 251 | "pwd" => { 252 | println!("{}", self.working_dir.display()); 253 | Some(Ok(false)) 254 | }, 255 | 256 | // Environment variables 257 | "export" => { 258 | if parts.len() == 1 { 259 | // Just 'export' - list all environment variables 260 | for (key, value) in std::env::vars() { 261 | println!("{}={}", key, value); 262 | } 263 | } else { 264 | // Handle export VAR=VALUE 265 | let export_str = input["export ".len()..].trim(); 266 | if let Some(equals_pos) = export_str.find('=') { 267 | let name = export_str[..equals_pos].trim(); 268 | let value = export_str[equals_pos + 1..].trim(); 269 | 270 | // Remove quotes if present 271 | let clean_value = value.trim_matches('"').trim_matches('\''); 272 | 273 | // Expand variables in the value 274 | let expanded_value = self.expand_env_vars(clean_value); 275 | 276 | // Set the environment variable 277 | std::env::set_var(name, expanded_value); 278 | } else { 279 | eprintln!("Invalid export format. Use: export VAR=VALUE"); 280 | } 281 | } 282 | Some(Ok(false)) 283 | }, 284 | 285 | "unset" => { 286 | if parts.len() > 1 { 287 | for var in &parts[1..] { 288 | std::env::remove_var(var); 289 | } 290 | } else { 291 | eprintln!("unset: missing variable name"); 292 | } 293 | Some(Ok(false)) 294 | }, 295 | 296 | "set" => { 297 | if parts.len() == 1 { 298 | // Just 'set' - list all environment variables 299 | for (key, value) in std::env::vars() { 300 | println!("{}={}", key, value); 301 | } 302 | } else { 303 | // Handle shell options (simplified) 304 | // In a real shell, this would handle options like -e, -x, etc. 305 | eprintln!("Note: shell options not fully implemented"); 306 | } 307 | Some(Ok(false)) 308 | }, 309 | 310 | // Output and redirection 311 | "echo" => { 312 | if parts.len() > 1 { 313 | // Check for -n option (no newline) 314 | let no_newline = parts[1] == "-n"; 315 | let start_idx = if no_newline { 2 } else { 1 }; 316 | 317 | // Join all arguments and expand variables 318 | let echo_str = parts[start_idx..].join(" "); 319 | let expanded = self.expand_env_vars(&echo_str); 320 | 321 | if no_newline { 322 | print!("{}", expanded); 323 | std::io::stdout().flush().unwrap_or(()); 324 | } else { 325 | println!("{}", expanded); 326 | } 327 | } else { 328 | // Just echo a newline 329 | println!(); 330 | } 331 | Some(Ok(false)) 332 | }, 333 | 334 | "printf" => { 335 | if parts.len() > 1 { 336 | // Very simplified printf implementation 337 | let format_str = self.expand_env_vars(parts[1]); 338 | let args: Vec = parts[2..].iter() 339 | .map(|arg| self.expand_env_vars(arg)) 340 | .collect(); 341 | 342 | // Basic % substitution (simplified) 343 | let mut result = format_str.clone(); 344 | for arg in args { 345 | if let Some(pos) = result.find('%') { 346 | let end = pos + 2.min(result.len() - pos); 347 | result.replace_range(pos..end, &arg); 348 | } 349 | } 350 | 351 | print!("{}", result); 352 | std::io::stdout().flush().unwrap_or(()); 353 | } else { 354 | eprintln!("printf: missing format string"); 355 | } 356 | Some(Ok(false)) 357 | }, 358 | 359 | // Job control 360 | "jobs" => { 361 | match self.job_control.list_jobs() { 362 | Ok(_) => {}, 363 | Err(e) => eprintln!("Error listing jobs: {}", e), 364 | } 365 | Some(Ok(false)) 366 | }, 367 | 368 | "fg" => { 369 | let args = parts.iter().map(|s| s.to_string()).collect::>(); 370 | match self.job_control.bring_to_foreground(&args) { 371 | Ok(_) => {}, 372 | Err(e) => eprintln!("Error bringing job to foreground: {}", e), 373 | } 374 | Some(Ok(false)) 375 | }, 376 | 377 | "bg" => { 378 | let args = parts.iter().map(|s| s.to_string()).collect::>(); 379 | match self.job_control.continue_in_background(&args) { 380 | Ok(_) => {}, 381 | Err(e) => eprintln!("Error continuing job in background: {}", e), 382 | } 383 | Some(Ok(false)) 384 | }, 385 | 386 | "kill" => { 387 | if parts.len() < 2 { 388 | eprintln!("kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]"); 389 | return Some(Ok(false)); 390 | } 391 | 392 | // Handle -l option to list signals 393 | if parts[1] == "-l" { 394 | println!("HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS"); 395 | return Some(Ok(false)); 396 | } 397 | 398 | // Parse signal if provided 399 | let mut signal = 15; // Default to SIGTERM 400 | let mut arg_start = 1; 401 | 402 | if parts[1].starts_with('-') { 403 | if let Ok(sig) = parts[1][1..].parse::() { 404 | signal = sig; 405 | arg_start = 2; 406 | } else if parts[1] == "-KILL" || parts[1] == "-9" { 407 | signal = 9; 408 | arg_start = 2; 409 | } else if parts[1] == "-HUP" || parts[1] == "-1" { 410 | signal = 1; 411 | arg_start = 2; 412 | } else if parts[1] == "-INT" || parts[1] == "-2" { 413 | signal = 2; 414 | arg_start = 2; 415 | } else if parts[1] == "-TERM" || parts[1] == "-15" { 416 | signal = 15; 417 | arg_start = 2; 418 | } 419 | } 420 | 421 | // Send signal to each PID 422 | for pid_str in &parts[arg_start..] { 423 | if let Ok(pid) = pid_str.parse::() { 424 | unsafe { 425 | if libc::kill(pid, signal) != 0 { 426 | eprintln!("kill: ({}) - No such process", pid); 427 | } 428 | } 429 | } else { 430 | eprintln!("kill: ({}) - Invalid process id", pid_str); 431 | } 432 | } 433 | 434 | Some(Ok(false)) 435 | }, 436 | 437 | "wait" => { 438 | if parts.len() > 1 { 439 | for pid_str in &parts[1..] { 440 | if let Ok(pid) = pid_str.parse::() { 441 | unsafe { 442 | let mut status = 0; 443 | libc::waitpid(pid, &mut status, 0); 444 | } 445 | } else { 446 | eprintln!("wait: {}: invalid process id", pid_str); 447 | } 448 | } 449 | } else { 450 | // Wait for all children 451 | unsafe { 452 | libc::wait(std::ptr::null_mut()); 453 | } 454 | } 455 | Some(Ok(false)) 456 | }, 457 | 458 | // Aliases 459 | "alias" => { 460 | if parts.len() == 1 { 461 | // List all aliases 462 | for (name, value) in self.alias_manager.list_aliases() { 463 | println!("alias {}='{}'", name, value); 464 | } 465 | } else if parts.len() == 2 && !parts[1].contains('=') { 466 | // Show specific alias 467 | let aliases = self.alias_manager.list_aliases(); 468 | let name = parts[1]; 469 | let found = aliases.iter().find(|(n, _)| n == name); 470 | if let Some((_, value)) = found { 471 | println!("alias {}='{}'", name, value); 472 | } else { 473 | println!("alias: {} not found", name); 474 | } 475 | } else { 476 | // Define new alias 477 | let alias_def = input["alias ".len()..].trim(); 478 | if let Some(equals_pos) = alias_def.find('=') { 479 | let name = alias_def[..equals_pos].trim(); 480 | let mut value = alias_def[equals_pos + 1..].trim(); 481 | // Remove surrounding quotes if present 482 | if (value.starts_with('\'') && value.ends_with('\'')) || 483 | (value.starts_with('"') && value.ends_with('"')) { 484 | value = &value[1..value.len() - 1]; 485 | } 486 | match self.alias_manager.add_alias(name, value) { 487 | Ok(_) => {}, 488 | Err(e) => eprintln!("Error adding alias: {}", e), 489 | } 490 | } else { 491 | eprintln!("Invalid alias format. Use: alias name='value'"); 492 | } 493 | } 494 | Some(Ok(false)) 495 | }, 496 | 497 | "unalias" => { 498 | if parts.len() > 1 { 499 | for name in &parts[1..] { 500 | match self.alias_manager.remove_alias(name) { 501 | Ok(_) => {}, 502 | Err(e) => eprintln!("Error removing alias {}: {}", name, e), 503 | } 504 | } 505 | } else { 506 | eprintln!("unalias: missing alias name"); 507 | } 508 | Some(Ok(false)) 509 | }, 510 | 511 | // History 512 | "history" => { 513 | let entries = self.terminal.get_history().get_entries(); 514 | let count = if parts.len() > 1 { 515 | parts[1].parse::().unwrap_or(entries.len()) 516 | } else { 517 | entries.len() 518 | }; 519 | 520 | for (i, entry) in entries.iter().rev().take(count).rev().enumerate() { 521 | println!("{:5} {}", entries.len() - count + i + 1, entry); 522 | } 523 | Some(Ok(false)) 524 | }, 525 | 526 | // File operations 527 | "touch" => { 528 | if parts.len() > 1 { 529 | for file in &parts[1..] { 530 | let path = std::path::Path::new(file); 531 | if !path.exists() { 532 | if let Err(e) = std::fs::File::create(path) { 533 | eprintln!("touch: cannot touch '{}': {}", file, e); 534 | } 535 | } else { 536 | // Update file times (simplified - just recreates the file) 537 | let content = std::fs::read(path).unwrap_or_default(); 538 | if let Err(e) = std::fs::write(path, content) { 539 | eprintln!("touch: cannot touch '{}': {}", file, e); 540 | } 541 | } 542 | } 543 | } else { 544 | eprintln!("touch: missing file operand"); 545 | } 546 | Some(Ok(false)) 547 | }, 548 | 549 | "mkdir" => { 550 | if parts.len() > 1 { 551 | let mut create_parents = false; 552 | let mut dirs_start = 1; 553 | 554 | if parts[1] == "-p" { 555 | create_parents = true; 556 | dirs_start = 2; 557 | } 558 | 559 | for dir in &parts[dirs_start..] { 560 | let path = std::path::Path::new(dir); 561 | let result = if create_parents { 562 | std::fs::create_dir_all(path) 563 | } else { 564 | std::fs::create_dir(path) 565 | }; 566 | 567 | if let Err(e) = result { 568 | eprintln!("mkdir: cannot create directory '{}': {}", dir, e); 569 | } 570 | } 571 | } else { 572 | eprintln!("mkdir: missing operand"); 573 | } 574 | Some(Ok(false)) 575 | }, 576 | 577 | "rmdir" => { 578 | if parts.len() > 1 { 579 | for dir in &parts[1..] { 580 | if let Err(e) = std::fs::remove_dir(dir) { 581 | eprintln!("rmdir: failed to remove '{}': {}", dir, e); 582 | } 583 | } 584 | } else { 585 | eprintln!("rmdir: missing operand"); 586 | } 587 | Some(Ok(false)) 588 | }, 589 | 590 | // Shell control 591 | "exit" | "logout" | "bye" => { 592 | let exit_code = if parts.len() > 1 { 593 | parts[1].parse::().unwrap_or(0) 594 | } else { 595 | 0 596 | }; 597 | 598 | if exit_code != 0 { 599 | eprintln!("Exit code: {}", exit_code); 600 | } 601 | 602 | Some(Ok(true)) // Signal to exit the shell 603 | }, 604 | 605 | "source" | "." => { 606 | if parts.len() > 1 { 607 | let path = std::path::Path::new(parts[1]); 608 | if let Ok(content) = std::fs::read_to_string(path) { 609 | for line in content.lines() { 610 | let line = line.trim(); 611 | if line.is_empty() || line.starts_with('#') { 612 | continue; 613 | } 614 | 615 | // Process each line as a command 616 | // Note: This will be handled by the caller since process_input is async 617 | return Some(Err(anyhow::anyhow!("source: async operations not supported in built-ins"))); 618 | } 619 | } else { 620 | eprintln!("{}: cannot open {}: No such file or directory", parts[0], parts[1]); 621 | } 622 | } else { 623 | eprintln!("{}: filename argument required", parts[0]); 624 | } 625 | Some(Ok(false)) 626 | }, 627 | 628 | "eval" => { 629 | if parts.len() > 1 { 630 | let cmd = parts[1..].join(" "); 631 | // Note: This will be handled by the caller since process_input is async 632 | return Some(Err(anyhow::anyhow!("eval: async operations not supported in built-ins"))); 633 | } 634 | Some(Ok(false)) 635 | }, 636 | 637 | // Information and help 638 | "type" => { 639 | if parts.len() > 1 { 640 | for cmd in &parts[1..] { 641 | // Check if it's a built-in 642 | let is_builtin = matches!(*cmd, 643 | "cd" | "pwd" | "export" | "unset" | "set" | "echo" | "printf" | 644 | "jobs" | "fg" | "bg" | "kill" | "wait" | "alias" | "unalias" | 645 | "history" | "touch" | "mkdir" | "rmdir" | "exit" | "logout" | 646 | "source" | "." | "eval" | "type" | "help" | "true" | "false" | 647 | "test" | "time" | "umask" | "ulimit" | "read" | "exec" 648 | ); 649 | 650 | if is_builtin { 651 | println!("{} is a shell builtin", cmd); 652 | } else if let Some(path) = crate::utils::path_utils::find_executable(cmd) { 653 | println!("{} is {}", cmd, path.display()); 654 | } else if self.alias_manager.list_aliases().iter().any(|(name, _)| name == cmd) { 655 | println!("{} is an alias", cmd); 656 | } else { 657 | println!("{}: not found", cmd); 658 | } 659 | } 660 | } else { 661 | eprintln!("type: missing argument"); 662 | } 663 | Some(Ok(false)) 664 | }, 665 | 666 | "help" => { 667 | self.show_help(); 668 | Some(Ok(false)) 669 | }, 670 | 671 | // Simple utilities 672 | "true" => { 673 | Some(Ok(false)) 674 | }, 675 | 676 | "false" => { 677 | // In a real shell, this would set the exit status to 1 678 | Some(Ok(false)) 679 | }, 680 | 681 | "test" | "[" => { 682 | // Very simplified test implementation 683 | if parts.len() < 2 { 684 | eprintln!("test: missing argument"); 685 | return Some(Ok(false)); 686 | } 687 | 688 | // Handle the closing bracket for [ command 689 | let test_parts = if parts[0] == "[" { 690 | if parts[parts.len() - 1] != "]" { 691 | eprintln!("[: missing closing ]"); 692 | return Some(Ok(false)); 693 | } 694 | &parts[1..parts.len() - 1] 695 | } else { 696 | &parts[1..] 697 | }; 698 | 699 | if test_parts.is_empty() { 700 | // Empty test is false 701 | eprintln!("Test failed"); 702 | return Some(Ok(false)); 703 | } 704 | 705 | // Handle simple file tests 706 | if test_parts.len() == 2 && test_parts[0] == "-f" { 707 | let path = std::path::Path::new(test_parts[1]); 708 | if !path.is_file() { 709 | eprintln!("Test failed: {} is not a file", test_parts[1]); 710 | } 711 | } else if test_parts.len() == 2 && test_parts[0] == "-d" { 712 | let path = std::path::Path::new(test_parts[1]); 713 | if !path.is_dir() { 714 | eprintln!("Test failed: {} is not a directory", test_parts[1]); 715 | } 716 | } else if test_parts.len() == 3 && test_parts[1] == "=" { 717 | if test_parts[0] != test_parts[2] { 718 | eprintln!("Test failed: {} != {}", test_parts[0], test_parts[2]); 719 | } 720 | } else if test_parts.len() == 3 && test_parts[1] == "!=" { 721 | if test_parts[0] == test_parts[2] { 722 | eprintln!("Test failed: {} == {}", test_parts[0], test_parts[2]); 723 | } 724 | } 725 | 726 | Some(Ok(false)) 727 | }, 728 | 729 | "time" => { 730 | if parts.len() > 1 { 731 | let cmd = parts[1..].join(" "); 732 | // Note: This will be handled by the caller since process_input is async 733 | return Some(Err(anyhow::anyhow!("time: async operations not supported in built-ins"))); 734 | } else { 735 | eprintln!("time: missing command"); 736 | } 737 | Some(Ok(false)) 738 | }, 739 | 740 | // System control 741 | "umask" => { 742 | if parts.len() > 1 { 743 | // Set umask (simplified) 744 | if let Ok(mask) = u32::from_str_radix(parts[1], 8) { 745 | unsafe { 746 | libc::umask(mask); 747 | } 748 | } else { 749 | eprintln!("umask: invalid octal number: {}", parts[1]); 750 | } 751 | } else { 752 | // Get current umask 753 | unsafe { 754 | // Save current umask 755 | let current = libc::umask(0); 756 | // Restore it 757 | libc::umask(current); 758 | println!("{:04o}", current); 759 | } 760 | } 761 | Some(Ok(false)) 762 | }, 763 | 764 | "ulimit" => { 765 | // Simplified ulimit implementation 766 | if parts.len() == 1 { 767 | // Show file size limit 768 | unsafe { 769 | let mut rlim: libc::rlimit = std::mem::zeroed(); 770 | if libc::getrlimit(libc::RLIMIT_FSIZE, &mut rlim) == 0 { 771 | if rlim.rlim_cur == libc::RLIM_INFINITY { 772 | println!("unlimited"); 773 | } else { 774 | println!("{}", rlim.rlim_cur); 775 | } 776 | } else { 777 | eprintln!("ulimit: error getting limit"); 778 | } 779 | } 780 | } else if parts[1] == "-a" { 781 | // Show all limits 782 | println!("core file size (blocks, -c) unlimited"); 783 | println!("data seg size (kbytes, -d) unlimited"); 784 | println!("scheduling priority (-e) 0"); 785 | println!("file size (blocks, -f) unlimited"); 786 | println!("pending signals (-i) 15169"); 787 | println!("max locked memory (kbytes, -l) 65536"); 788 | println!("max memory size (kbytes, -m) unlimited"); 789 | println!("open files (-n) 1024"); 790 | println!("pipe size (512 bytes, -p) 8"); 791 | println!("POSIX message queues (bytes, -q) 819200"); 792 | println!("real-time priority (-r) 0"); 793 | println!("stack size (kbytes, -s) 8192"); 794 | println!("cpu time (seconds, -t) unlimited"); 795 | println!("max user processes (-u) 15169"); 796 | println!("virtual memory (kbytes, -v) unlimited"); 797 | println!("file locks (-x) unlimited"); 798 | } 799 | Some(Ok(false)) 800 | }, 801 | 802 | // Input/output 803 | "read" => { 804 | if parts.len() > 1 { 805 | let mut input = String::new(); 806 | if std::io::stdin().read_line(&mut input).is_ok() { 807 | input = input.trim().to_string(); 808 | 809 | // Handle -p prompt option 810 | let mut var_start = 1; 811 | if parts[1] == "-p" && parts.len() > 3 { 812 | print!("{}", parts[2]); 813 | std::io::stdout().flush().unwrap_or(()); 814 | var_start = 3; 815 | } 816 | 817 | // Assign to variables 818 | if parts.len() > var_start { 819 | let var_name = parts[var_start]; 820 | std::env::set_var(var_name, input); 821 | } 822 | } 823 | } else { 824 | eprintln!("read: missing variable name"); 825 | } 826 | Some(Ok(false)) 827 | }, 828 | 829 | "exec" => { 830 | if parts.len() > 1 { 831 | let cmd = parts[1].to_string(); 832 | let args: Vec = parts[1..].iter().map(|s| s.to_string()).collect(); 833 | 834 | if let Some(path) = crate::utils::path_utils::find_executable(&cmd) { 835 | use std::os::unix::process::CommandExt; 836 | let err = std::process::Command::new(path) 837 | .args(&args[1..]) 838 | .exec(); 839 | 840 | // If we get here, exec failed 841 | eprintln!("exec: failed to execute {}: {}", cmd, err); 842 | } else { 843 | eprintln!("exec: {}: command not found", cmd); 844 | } 845 | } else { 846 | // No command specified, just continue 847 | } 848 | Some(Ok(false)) 849 | }, 850 | 851 | // Not a built-in command 852 | _ => None, 853 | } 854 | } 855 | 856 | fn show_help(&self) { 857 | println!("\n{}", "LLM Shell Help".bright_green()); 858 | println!("{}", "=============".bright_green()); 859 | 860 | println!("\n{}", "Basic Commands:".bright_yellow()); 861 | println!(" cd [dir] - Change directory"); 862 | println!(" alias [name[=value]] - List or set aliases"); 863 | println!(" unalias name - Remove an alias"); 864 | println!(" jobs - List background jobs"); 865 | println!(" fg [job_id] - Bring job to foreground"); 866 | println!(" bg [job_id] - Continue job in background"); 867 | println!(" exit - Exit the shell"); 868 | 869 | println!("\n{}", "Special Features:".bright_yellow()); 870 | println!(" command?? - Show command suggestions"); 871 | println!(" ?query - Ask a question to the LLM"); 872 | println!(" use natural language - Type commands in plain English"); 873 | 874 | println!("\n{}", "Examples:".bright_yellow()); 875 | println!(" ? How do I find large files in Linux?"); 876 | println!(" find all python files modified in the last week"); 877 | println!(" ps ?? - Show suggestions for ps command"); 878 | 879 | println!("\n{}", "For more information, visit: https://github.com/yourusername/llm-shell".bright_blue()); 880 | } 881 | 882 | async fn process_input(&mut self, input: &str) -> Result<()> { 883 | // Expand environment variables 884 | let expanded_input = self.expand_env_vars(input); 885 | // Check for chat prefix 886 | if input.starts_with('?') { 887 | let question = input[1..].trim(); 888 | if !question.is_empty() { 889 | println!("\n{}", "Thinking...".bright_blue()); 890 | match self.llm_client.chat(question).await { 891 | Ok(response) => { 892 | println!("\n{}", "Answer:".bright_green()); 893 | println!("{}\n", response); 894 | } 895 | Err(e) => println!("Error getting response: {}", e), 896 | } 897 | return Ok(()); 898 | } 899 | } 900 | 901 | // Check for natural language patterns 902 | let natural_language_patterns = [ 903 | "show me", "find all", "list all", "get all", "display", "create a", 904 | "make a", "tell me", "give me", "use the", "how do", "what is", "where is", 905 | "can you", "could you", "would you", "should I", "explain", "help me", 906 | "search for", "look for", "find files", "count", "calculate", "summarize", 907 | "who are", "what are", "which", "when", "why", "how many", "how much", 908 | "get the", "list", "show", "find", "tell", "give", "display", "print", 909 | ]; 910 | 911 | let is_natural_language = natural_language_patterns.iter() 912 | .any(|pattern| input.to_lowercase().starts_with(pattern)) || 913 | (input.split_whitespace().count() >= 4); 914 | 915 | if is_natural_language { 916 | debug!("Processing as natural language: {}", input); 917 | println!("Processing as natural language: {}", input.bright_yellow()); 918 | 919 | let shell_command = self.llm_client.translate_command(input).await?; 920 | 921 | println!("\nTranslated command: {}", shell_command.bright_green()); 922 | 923 | if let Ok(explanation) = self.documentation.get_command_help(&shell_command).await { 924 | println!("Explanation: {}", explanation.bright_blue()); 925 | } 926 | 927 | // Only ask for confirmation if it's a destructive command 928 | if self.is_destructive_command(&shell_command) { 929 | println!("\nWarning: This command may modify or delete data."); 930 | print!("Proceed? [y/N] "); 931 | std::io::stdout().flush()?; 932 | 933 | let mut response = String::new(); 934 | std::io::stdin().read_line(&mut response)?; 935 | 936 | if !response.trim().eq_ignore_ascii_case("y") { 937 | println!("Command aborted."); 938 | return Ok(()); 939 | } 940 | } 941 | 942 | return self.execute_command(&shell_command); 943 | } 944 | 945 | // Regular command processing 946 | let commands = self.command_processor.parse(input)?; 947 | 948 | for cmd in commands { 949 | if cmd.is_natural_language { 950 | debug!("Detected natural language: {}", cmd.command); 951 | println!("Detected natural language: {}", cmd.command.bright_yellow()); 952 | 953 | let shell_command = self.llm_client.translate_command(&cmd.command).await?; 954 | 955 | println!("\nTranslated command: {}", shell_command.bright_green()); 956 | 957 | if let Ok(explanation) = self.documentation.get_command_help(&shell_command).await { 958 | println!("Explanation: {}", explanation.bright_blue()); 959 | } 960 | 961 | // Only ask for confirmation if it's a destructive command 962 | if self.is_destructive_command(&shell_command) { 963 | println!("\nWarning: This command may modify or delete data."); 964 | print!("Proceed? [y/N] "); 965 | std::io::stdout().flush()?; 966 | 967 | let mut response = String::new(); 968 | std::io::stdin().read_line(&mut response)?; 969 | 970 | if !response.trim().eq_ignore_ascii_case("y") { 971 | println!("Command aborted."); 972 | continue; 973 | } 974 | } 975 | 976 | self.execute_command(&shell_command)?; 977 | } else { 978 | // Only ask for confirmation if it's a destructive command 979 | if self.is_destructive_command(&cmd.command) { 980 | println!("\nWarning: This command may modify or delete data."); 981 | print!("Proceed? [y/N] "); 982 | std::io::stdout().flush()?; 983 | 984 | let mut response = String::new(); 985 | std::io::stdin().read_line(&mut response)?; 986 | 987 | if !response.trim().eq_ignore_ascii_case("y") { 988 | println!("Command aborted."); 989 | continue; 990 | } 991 | } 992 | self.execute_command(&cmd.command)?; 993 | } 994 | } 995 | 996 | Ok(()) 997 | } 998 | 999 | fn is_destructive_command(&self, command: &str) -> bool { 1000 | let destructive_patterns = [ 1001 | "rm", "rmdir", "dd", "mkfs", 1002 | "format", "fdisk", "mkfs", 1003 | ">", "truncate", "shred", 1004 | "mv", "chmod", "chown", 1005 | "sudo rm", "sudo dd", "sudo mkfs", 1006 | "sudo fdisk", "sudo chown", "sudo chmod", 1007 | "pkill", "kill", "killall", 1008 | ]; 1009 | 1010 | let command_words: Vec<&str> = command.split_whitespace().collect(); 1011 | if command_words.is_empty() { 1012 | return false; 1013 | } 1014 | 1015 | // Check for redirection that would overwrite files 1016 | if command.contains('>') && !command.contains(">>") { 1017 | return true; 1018 | } 1019 | 1020 | // Check for destructive commands 1021 | for pattern in &destructive_patterns { 1022 | if command.starts_with(pattern) { 1023 | return true; 1024 | } 1025 | } 1026 | 1027 | // Special case for rm with -rf flags 1028 | if command_words[0] == "rm" && 1029 | (command.contains(" -rf ") || 1030 | command.contains(" -fr ") || 1031 | command.contains(" -f ") || 1032 | command.contains(" --force")) { 1033 | return true; 1034 | } 1035 | 1036 | false 1037 | } 1038 | 1039 | async fn show_suggestions(&self, command_prefix: Option<&str>) -> Result { 1040 | let suggestions = self.llm_client 1041 | .suggest_commands(&self.context_manager.get_context(), command_prefix) 1042 | .await?; 1043 | 1044 | if suggestions.is_empty() { 1045 | Ok("No suggestions available.".to_string()) 1046 | } else { 1047 | Ok(format!("\nSuggested commands:\n{}", 1048 | suggestions.iter() 1049 | .map(|s| format!(" {}", s.bright_cyan())) 1050 | .collect::>() 1051 | .join("\n") 1052 | )) 1053 | } 1054 | } 1055 | 1056 | fn initialize(&mut self) -> Result<()> { 1057 | // Process login shell initialization if needed 1058 | if self.is_login_shell() { 1059 | self.process_profile_files()?; 1060 | } 1061 | 1062 | // Set up environment 1063 | self.setup_environment()?; 1064 | 1065 | // Handle SIGCHLD for job control 1066 | self.job_control.handle_sigchld()?; 1067 | 1068 | // Print welcome message 1069 | self.print_welcome_message(); 1070 | 1071 | Ok(()) 1072 | } 1073 | 1074 | fn print_welcome_message(&self) { 1075 | println!("{}", "\n╭───────────────────────────────────────────╮".bright_blue()); 1076 | println!("{}", "│ Welcome to LLM Shell │".bright_green()); 1077 | println!("{}", "│ │".bright_blue()); 1078 | println!("{}", "│ • Use natural language for commands │".bright_blue()); 1079 | println!("{}", "│ • Type '??' after a command for help │".bright_blue()); 1080 | println!("{}", "│ • Start with '?' to ask a question │".bright_blue()); 1081 | println!("{}", "│ • Type 'help' for more information │".bright_blue()); 1082 | println!("{}", "╰───────────────────────────────────────────╯".bright_blue()); 1083 | println!(); 1084 | } 1085 | 1086 | fn is_login_shell(&self) -> bool { 1087 | std::env::args() 1088 | .next() 1089 | .map(|arg| arg.starts_with('-')) 1090 | .unwrap_or(false) 1091 | } 1092 | 1093 | fn process_profile_files(&self) -> Result<()> { 1094 | let home = dirs::home_dir().context("Could not determine home directory")?; 1095 | 1096 | // Process global profile 1097 | if let Ok(contents) = std::fs::read_to_string("/etc/profile") { 1098 | self.process_profile_content(&contents)?; 1099 | } 1100 | 1101 | // Process user profile 1102 | let profile_path = home.join(".profile"); 1103 | if let Ok(contents) = std::fs::read_to_string(profile_path) { 1104 | self.process_profile_content(&contents)?; 1105 | } 1106 | 1107 | // Process .bash_profile or .bash_login if they exist 1108 | let bash_profile = home.join(".bash_profile"); 1109 | let bash_login = home.join(".bash_login"); 1110 | 1111 | if bash_profile.exists() { 1112 | if let Ok(contents) = std::fs::read_to_string(bash_profile) { 1113 | self.process_profile_content(&contents)?; 1114 | } 1115 | } else if bash_login.exists() { 1116 | if let Ok(contents) = std::fs::read_to_string(bash_login) { 1117 | self.process_profile_content(&contents)?; 1118 | } 1119 | } 1120 | 1121 | Ok(()) 1122 | } 1123 | 1124 | fn process_profile_content(&self, content: &str) -> Result<()> { 1125 | for line in content.lines() { 1126 | let line = line.trim(); 1127 | 1128 | // Skip comments and empty lines 1129 | if line.is_empty() || line.starts_with('#') { 1130 | continue; 1131 | } 1132 | 1133 | if line.starts_with("export ") { 1134 | let parts: Vec<&str> = line["export ".len()..].splitn(2, '=').collect(); 1135 | if parts.len() == 2 { 1136 | let key = parts[0].trim(); 1137 | let value = parts[1].trim().trim_matches('"').trim_matches('\''); 1138 | 1139 | // Handle variable expansion in values 1140 | let expanded_value = self.expand_env_vars(value); 1141 | std::env::set_var(key, expanded_value); 1142 | } 1143 | } 1144 | } 1145 | Ok(()) 1146 | } 1147 | 1148 | fn setup_environment(&self) -> Result<()> { 1149 | // Set basic environment variables 1150 | if std::env::var("PATH").is_err() { 1151 | std::env::set_var("PATH", "/usr/local/bin:/usr/bin:/bin"); 1152 | } 1153 | 1154 | if std::env::var("HOME").is_err() { 1155 | if let Some(home) = dirs::home_dir() { 1156 | std::env::set_var("HOME", home.to_string_lossy().as_ref()); 1157 | } 1158 | } 1159 | 1160 | // Set SHELL to point to our shell 1161 | if let Ok(exe) = std::env::current_exe() { 1162 | std::env::set_var("SHELL", exe.to_string_lossy().as_ref()); 1163 | } 1164 | 1165 | // Set basic terminal variables 1166 | if std::env::var("TERM").is_err() { 1167 | std::env::set_var("TERM", "xterm-256color"); 1168 | } 1169 | 1170 | Ok(()) 1171 | } 1172 | 1173 | fn execute_command(&mut self, command: &str) -> Result<()> { 1174 | // Parse the command 1175 | let pipeline = crate::shell::command_parser::CommandParser::parse(command)?; 1176 | 1177 | // Execute the pipeline 1178 | let exit_code = crate::shell::executor::Executor::execute(&pipeline)?; 1179 | 1180 | if exit_code != 0 { 1181 | eprintln!("Command failed with exit code: {}", exit_code); 1182 | } 1183 | 1184 | Ok(()) 1185 | } 1186 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.96" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" 34 | 35 | [[package]] 36 | name = "async-recursion" 37 | version = "1.1.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" 40 | dependencies = [ 41 | "proc-macro2", 42 | "quote", 43 | "syn", 44 | ] 45 | 46 | [[package]] 47 | name = "async-trait" 48 | version = "0.1.86" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" 51 | dependencies = [ 52 | "proc-macro2", 53 | "quote", 54 | "syn", 55 | ] 56 | 57 | [[package]] 58 | name = "autocfg" 59 | version = "1.4.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 62 | 63 | [[package]] 64 | name = "backtrace" 65 | version = "0.3.74" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 68 | dependencies = [ 69 | "addr2line", 70 | "cfg-if", 71 | "libc", 72 | "miniz_oxide", 73 | "object", 74 | "rustc-demangle", 75 | "windows-targets 0.52.6", 76 | ] 77 | 78 | [[package]] 79 | name = "base64" 80 | version = "0.21.7" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 83 | 84 | [[package]] 85 | name = "bitflags" 86 | version = "1.3.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 89 | 90 | [[package]] 91 | name = "bitflags" 92 | version = "2.8.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 95 | 96 | [[package]] 97 | name = "bumpalo" 98 | version = "3.17.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 101 | 102 | [[package]] 103 | name = "bytes" 104 | version = "1.10.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" 107 | 108 | [[package]] 109 | name = "cc" 110 | version = "1.2.15" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" 113 | dependencies = [ 114 | "shlex", 115 | ] 116 | 117 | [[package]] 118 | name = "cfg-if" 119 | version = "1.0.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 122 | 123 | [[package]] 124 | name = "clipboard-win" 125 | version = "4.5.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" 128 | dependencies = [ 129 | "error-code", 130 | "str-buf", 131 | "winapi", 132 | ] 133 | 134 | [[package]] 135 | name = "colored" 136 | version = "2.2.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 139 | dependencies = [ 140 | "lazy_static", 141 | "windows-sys 0.59.0", 142 | ] 143 | 144 | [[package]] 145 | name = "core-foundation" 146 | version = "0.9.4" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 149 | dependencies = [ 150 | "core-foundation-sys", 151 | "libc", 152 | ] 153 | 154 | [[package]] 155 | name = "core-foundation-sys" 156 | version = "0.8.7" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 159 | 160 | [[package]] 161 | name = "dirs" 162 | version = "5.0.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 165 | dependencies = [ 166 | "dirs-sys", 167 | ] 168 | 169 | [[package]] 170 | name = "dirs-next" 171 | version = "2.0.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 174 | dependencies = [ 175 | "cfg-if", 176 | "dirs-sys-next", 177 | ] 178 | 179 | [[package]] 180 | name = "dirs-sys" 181 | version = "0.4.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 184 | dependencies = [ 185 | "libc", 186 | "option-ext", 187 | "redox_users", 188 | "windows-sys 0.48.0", 189 | ] 190 | 191 | [[package]] 192 | name = "dirs-sys-next" 193 | version = "0.1.2" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 196 | dependencies = [ 197 | "libc", 198 | "redox_users", 199 | "winapi", 200 | ] 201 | 202 | [[package]] 203 | name = "displaydoc" 204 | version = "0.2.5" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 207 | dependencies = [ 208 | "proc-macro2", 209 | "quote", 210 | "syn", 211 | ] 212 | 213 | [[package]] 214 | name = "dotenv" 215 | version = "0.15.0" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 218 | 219 | [[package]] 220 | name = "encoding_rs" 221 | version = "0.8.35" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 224 | dependencies = [ 225 | "cfg-if", 226 | ] 227 | 228 | [[package]] 229 | name = "endian-type" 230 | version = "0.1.2" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" 233 | 234 | [[package]] 235 | name = "env_logger" 236 | version = "0.10.2" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 239 | dependencies = [ 240 | "humantime", 241 | "is-terminal", 242 | "log", 243 | "regex", 244 | "termcolor", 245 | ] 246 | 247 | [[package]] 248 | name = "equivalent" 249 | version = "1.0.2" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 252 | 253 | [[package]] 254 | name = "errno" 255 | version = "0.3.10" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 258 | dependencies = [ 259 | "libc", 260 | "windows-sys 0.59.0", 261 | ] 262 | 263 | [[package]] 264 | name = "error-code" 265 | version = "2.3.1" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" 268 | dependencies = [ 269 | "libc", 270 | "str-buf", 271 | ] 272 | 273 | [[package]] 274 | name = "fastrand" 275 | version = "2.3.0" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 278 | 279 | [[package]] 280 | name = "fd-lock" 281 | version = "3.0.13" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" 284 | dependencies = [ 285 | "cfg-if", 286 | "rustix", 287 | "windows-sys 0.48.0", 288 | ] 289 | 290 | [[package]] 291 | name = "fnv" 292 | version = "1.0.7" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 295 | 296 | [[package]] 297 | name = "foreign-types" 298 | version = "0.3.2" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 301 | dependencies = [ 302 | "foreign-types-shared", 303 | ] 304 | 305 | [[package]] 306 | name = "foreign-types-shared" 307 | version = "0.1.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 310 | 311 | [[package]] 312 | name = "form_urlencoded" 313 | version = "1.2.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 316 | dependencies = [ 317 | "percent-encoding", 318 | ] 319 | 320 | [[package]] 321 | name = "futures-channel" 322 | version = "0.3.31" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 325 | dependencies = [ 326 | "futures-core", 327 | ] 328 | 329 | [[package]] 330 | name = "futures-core" 331 | version = "0.3.31" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 334 | 335 | [[package]] 336 | name = "futures-sink" 337 | version = "0.3.31" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 340 | 341 | [[package]] 342 | name = "futures-task" 343 | version = "0.3.31" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 346 | 347 | [[package]] 348 | name = "futures-util" 349 | version = "0.3.31" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 352 | dependencies = [ 353 | "futures-core", 354 | "futures-task", 355 | "pin-project-lite", 356 | "pin-utils", 357 | ] 358 | 359 | [[package]] 360 | name = "fuzzy-matcher" 361 | version = "0.3.7" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 364 | dependencies = [ 365 | "thread_local", 366 | ] 367 | 368 | [[package]] 369 | name = "getrandom" 370 | version = "0.2.15" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 373 | dependencies = [ 374 | "cfg-if", 375 | "libc", 376 | "wasi 0.11.0+wasi-snapshot-preview1", 377 | ] 378 | 379 | [[package]] 380 | name = "getrandom" 381 | version = "0.3.1" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 384 | dependencies = [ 385 | "cfg-if", 386 | "libc", 387 | "wasi 0.13.3+wasi-0.2.2", 388 | "windows-targets 0.52.6", 389 | ] 390 | 391 | [[package]] 392 | name = "gimli" 393 | version = "0.31.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 396 | 397 | [[package]] 398 | name = "h2" 399 | version = "0.3.26" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 402 | dependencies = [ 403 | "bytes", 404 | "fnv", 405 | "futures-core", 406 | "futures-sink", 407 | "futures-util", 408 | "http", 409 | "indexmap", 410 | "slab", 411 | "tokio", 412 | "tokio-util", 413 | "tracing", 414 | ] 415 | 416 | [[package]] 417 | name = "hashbrown" 418 | version = "0.15.2" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 421 | 422 | [[package]] 423 | name = "hermit-abi" 424 | version = "0.4.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 427 | 428 | [[package]] 429 | name = "hostname" 430 | version = "0.3.1" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" 433 | dependencies = [ 434 | "libc", 435 | "match_cfg", 436 | "winapi", 437 | ] 438 | 439 | [[package]] 440 | name = "http" 441 | version = "0.2.12" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 444 | dependencies = [ 445 | "bytes", 446 | "fnv", 447 | "itoa", 448 | ] 449 | 450 | [[package]] 451 | name = "http-body" 452 | version = "0.4.6" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 455 | dependencies = [ 456 | "bytes", 457 | "http", 458 | "pin-project-lite", 459 | ] 460 | 461 | [[package]] 462 | name = "httparse" 463 | version = "1.10.0" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" 466 | 467 | [[package]] 468 | name = "httpdate" 469 | version = "1.0.3" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 472 | 473 | [[package]] 474 | name = "humantime" 475 | version = "2.1.0" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 478 | 479 | [[package]] 480 | name = "hyper" 481 | version = "0.14.32" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" 484 | dependencies = [ 485 | "bytes", 486 | "futures-channel", 487 | "futures-core", 488 | "futures-util", 489 | "h2", 490 | "http", 491 | "http-body", 492 | "httparse", 493 | "httpdate", 494 | "itoa", 495 | "pin-project-lite", 496 | "socket2", 497 | "tokio", 498 | "tower-service", 499 | "tracing", 500 | "want", 501 | ] 502 | 503 | [[package]] 504 | name = "hyper-tls" 505 | version = "0.5.0" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 508 | dependencies = [ 509 | "bytes", 510 | "hyper", 511 | "native-tls", 512 | "tokio", 513 | "tokio-native-tls", 514 | ] 515 | 516 | [[package]] 517 | name = "icu_collections" 518 | version = "1.5.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 521 | dependencies = [ 522 | "displaydoc", 523 | "yoke", 524 | "zerofrom", 525 | "zerovec", 526 | ] 527 | 528 | [[package]] 529 | name = "icu_locid" 530 | version = "1.5.0" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 533 | dependencies = [ 534 | "displaydoc", 535 | "litemap", 536 | "tinystr", 537 | "writeable", 538 | "zerovec", 539 | ] 540 | 541 | [[package]] 542 | name = "icu_locid_transform" 543 | version = "1.5.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 546 | dependencies = [ 547 | "displaydoc", 548 | "icu_locid", 549 | "icu_locid_transform_data", 550 | "icu_provider", 551 | "tinystr", 552 | "zerovec", 553 | ] 554 | 555 | [[package]] 556 | name = "icu_locid_transform_data" 557 | version = "1.5.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 560 | 561 | [[package]] 562 | name = "icu_normalizer" 563 | version = "1.5.0" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 566 | dependencies = [ 567 | "displaydoc", 568 | "icu_collections", 569 | "icu_normalizer_data", 570 | "icu_properties", 571 | "icu_provider", 572 | "smallvec", 573 | "utf16_iter", 574 | "utf8_iter", 575 | "write16", 576 | "zerovec", 577 | ] 578 | 579 | [[package]] 580 | name = "icu_normalizer_data" 581 | version = "1.5.0" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 584 | 585 | [[package]] 586 | name = "icu_properties" 587 | version = "1.5.1" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 590 | dependencies = [ 591 | "displaydoc", 592 | "icu_collections", 593 | "icu_locid_transform", 594 | "icu_properties_data", 595 | "icu_provider", 596 | "tinystr", 597 | "zerovec", 598 | ] 599 | 600 | [[package]] 601 | name = "icu_properties_data" 602 | version = "1.5.0" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 605 | 606 | [[package]] 607 | name = "icu_provider" 608 | version = "1.5.0" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 611 | dependencies = [ 612 | "displaydoc", 613 | "icu_locid", 614 | "icu_provider_macros", 615 | "stable_deref_trait", 616 | "tinystr", 617 | "writeable", 618 | "yoke", 619 | "zerofrom", 620 | "zerovec", 621 | ] 622 | 623 | [[package]] 624 | name = "icu_provider_macros" 625 | version = "1.5.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 628 | dependencies = [ 629 | "proc-macro2", 630 | "quote", 631 | "syn", 632 | ] 633 | 634 | [[package]] 635 | name = "idna" 636 | version = "1.0.3" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 639 | dependencies = [ 640 | "idna_adapter", 641 | "smallvec", 642 | "utf8_iter", 643 | ] 644 | 645 | [[package]] 646 | name = "idna_adapter" 647 | version = "1.2.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 650 | dependencies = [ 651 | "icu_normalizer", 652 | "icu_properties", 653 | ] 654 | 655 | [[package]] 656 | name = "indexmap" 657 | version = "2.7.1" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 660 | dependencies = [ 661 | "equivalent", 662 | "hashbrown", 663 | ] 664 | 665 | [[package]] 666 | name = "ipnet" 667 | version = "2.11.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 670 | 671 | [[package]] 672 | name = "is-terminal" 673 | version = "0.4.15" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" 676 | dependencies = [ 677 | "hermit-abi", 678 | "libc", 679 | "windows-sys 0.59.0", 680 | ] 681 | 682 | [[package]] 683 | name = "itoa" 684 | version = "1.0.14" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 687 | 688 | [[package]] 689 | name = "js-sys" 690 | version = "0.3.77" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 693 | dependencies = [ 694 | "once_cell", 695 | "wasm-bindgen", 696 | ] 697 | 698 | [[package]] 699 | name = "lazy_static" 700 | version = "1.5.0" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 703 | 704 | [[package]] 705 | name = "libc" 706 | version = "0.2.170" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 709 | 710 | [[package]] 711 | name = "libredox" 712 | version = "0.1.3" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 715 | dependencies = [ 716 | "bitflags 2.8.0", 717 | "libc", 718 | ] 719 | 720 | [[package]] 721 | name = "linux-raw-sys" 722 | version = "0.4.15" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 725 | 726 | [[package]] 727 | name = "litemap" 728 | version = "0.7.4" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 731 | 732 | [[package]] 733 | name = "llm-shell" 734 | version = "0.3.0" 735 | dependencies = [ 736 | "anyhow", 737 | "async-recursion", 738 | "async-trait", 739 | "colored", 740 | "dirs", 741 | "dotenv", 742 | "env_logger", 743 | "fuzzy-matcher", 744 | "hostname", 745 | "lazy_static", 746 | "libc", 747 | "log", 748 | "nix", 749 | "regex", 750 | "reqwest", 751 | "rustyline", 752 | "serde", 753 | "serde_json", 754 | "shellwords", 755 | "tokio", 756 | ] 757 | 758 | [[package]] 759 | name = "lock_api" 760 | version = "0.4.12" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 763 | dependencies = [ 764 | "autocfg", 765 | "scopeguard", 766 | ] 767 | 768 | [[package]] 769 | name = "log" 770 | version = "0.4.26" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 773 | 774 | [[package]] 775 | name = "match_cfg" 776 | version = "0.1.0" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" 779 | 780 | [[package]] 781 | name = "memchr" 782 | version = "2.7.4" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 785 | 786 | [[package]] 787 | name = "memoffset" 788 | version = "0.7.1" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 791 | dependencies = [ 792 | "autocfg", 793 | ] 794 | 795 | [[package]] 796 | name = "mime" 797 | version = "0.3.17" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 800 | 801 | [[package]] 802 | name = "miniz_oxide" 803 | version = "0.8.5" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 806 | dependencies = [ 807 | "adler2", 808 | ] 809 | 810 | [[package]] 811 | name = "mio" 812 | version = "1.0.3" 813 | source = "registry+https://github.com/rust-lang/crates.io-index" 814 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 815 | dependencies = [ 816 | "libc", 817 | "wasi 0.11.0+wasi-snapshot-preview1", 818 | "windows-sys 0.52.0", 819 | ] 820 | 821 | [[package]] 822 | name = "native-tls" 823 | version = "0.2.14" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 826 | dependencies = [ 827 | "libc", 828 | "log", 829 | "openssl", 830 | "openssl-probe", 831 | "openssl-sys", 832 | "schannel", 833 | "security-framework", 834 | "security-framework-sys", 835 | "tempfile", 836 | ] 837 | 838 | [[package]] 839 | name = "nibble_vec" 840 | version = "0.1.0" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" 843 | dependencies = [ 844 | "smallvec", 845 | ] 846 | 847 | [[package]] 848 | name = "nix" 849 | version = "0.26.4" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 852 | dependencies = [ 853 | "bitflags 1.3.2", 854 | "cfg-if", 855 | "libc", 856 | "memoffset", 857 | "pin-utils", 858 | ] 859 | 860 | [[package]] 861 | name = "object" 862 | version = "0.36.7" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 865 | dependencies = [ 866 | "memchr", 867 | ] 868 | 869 | [[package]] 870 | name = "once_cell" 871 | version = "1.20.3" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 874 | 875 | [[package]] 876 | name = "openssl" 877 | version = "0.10.71" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" 880 | dependencies = [ 881 | "bitflags 2.8.0", 882 | "cfg-if", 883 | "foreign-types", 884 | "libc", 885 | "once_cell", 886 | "openssl-macros", 887 | "openssl-sys", 888 | ] 889 | 890 | [[package]] 891 | name = "openssl-macros" 892 | version = "0.1.1" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 895 | dependencies = [ 896 | "proc-macro2", 897 | "quote", 898 | "syn", 899 | ] 900 | 901 | [[package]] 902 | name = "openssl-probe" 903 | version = "0.1.6" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 906 | 907 | [[package]] 908 | name = "openssl-sys" 909 | version = "0.9.106" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" 912 | dependencies = [ 913 | "cc", 914 | "libc", 915 | "pkg-config", 916 | "vcpkg", 917 | ] 918 | 919 | [[package]] 920 | name = "option-ext" 921 | version = "0.2.0" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 924 | 925 | [[package]] 926 | name = "parking_lot" 927 | version = "0.12.3" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 930 | dependencies = [ 931 | "lock_api", 932 | "parking_lot_core", 933 | ] 934 | 935 | [[package]] 936 | name = "parking_lot_core" 937 | version = "0.9.10" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 940 | dependencies = [ 941 | "cfg-if", 942 | "libc", 943 | "redox_syscall", 944 | "smallvec", 945 | "windows-targets 0.52.6", 946 | ] 947 | 948 | [[package]] 949 | name = "percent-encoding" 950 | version = "2.3.1" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 953 | 954 | [[package]] 955 | name = "pin-project-lite" 956 | version = "0.2.16" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 959 | 960 | [[package]] 961 | name = "pin-utils" 962 | version = "0.1.0" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 965 | 966 | [[package]] 967 | name = "pkg-config" 968 | version = "0.3.31" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 971 | 972 | [[package]] 973 | name = "proc-macro2" 974 | version = "1.0.93" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 977 | dependencies = [ 978 | "unicode-ident", 979 | ] 980 | 981 | [[package]] 982 | name = "quote" 983 | version = "1.0.38" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 986 | dependencies = [ 987 | "proc-macro2", 988 | ] 989 | 990 | [[package]] 991 | name = "radix_trie" 992 | version = "0.2.1" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" 995 | dependencies = [ 996 | "endian-type", 997 | "nibble_vec", 998 | ] 999 | 1000 | [[package]] 1001 | name = "redox_syscall" 1002 | version = "0.5.9" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" 1005 | dependencies = [ 1006 | "bitflags 2.8.0", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "redox_users" 1011 | version = "0.4.6" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 1014 | dependencies = [ 1015 | "getrandom 0.2.15", 1016 | "libredox", 1017 | "thiserror", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "regex" 1022 | version = "1.11.1" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1025 | dependencies = [ 1026 | "aho-corasick", 1027 | "memchr", 1028 | "regex-automata", 1029 | "regex-syntax", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "regex-automata" 1034 | version = "0.4.9" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1037 | dependencies = [ 1038 | "aho-corasick", 1039 | "memchr", 1040 | "regex-syntax", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "regex-syntax" 1045 | version = "0.8.5" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1048 | 1049 | [[package]] 1050 | name = "reqwest" 1051 | version = "0.11.27" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 1054 | dependencies = [ 1055 | "base64", 1056 | "bytes", 1057 | "encoding_rs", 1058 | "futures-core", 1059 | "futures-util", 1060 | "h2", 1061 | "http", 1062 | "http-body", 1063 | "hyper", 1064 | "hyper-tls", 1065 | "ipnet", 1066 | "js-sys", 1067 | "log", 1068 | "mime", 1069 | "native-tls", 1070 | "once_cell", 1071 | "percent-encoding", 1072 | "pin-project-lite", 1073 | "rustls-pemfile", 1074 | "serde", 1075 | "serde_json", 1076 | "serde_urlencoded", 1077 | "sync_wrapper", 1078 | "system-configuration", 1079 | "tokio", 1080 | "tokio-native-tls", 1081 | "tower-service", 1082 | "url", 1083 | "wasm-bindgen", 1084 | "wasm-bindgen-futures", 1085 | "web-sys", 1086 | "winreg", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "rustc-demangle" 1091 | version = "0.1.24" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1094 | 1095 | [[package]] 1096 | name = "rustix" 1097 | version = "0.38.44" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1100 | dependencies = [ 1101 | "bitflags 2.8.0", 1102 | "errno", 1103 | "libc", 1104 | "linux-raw-sys", 1105 | "windows-sys 0.59.0", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "rustls-pemfile" 1110 | version = "1.0.4" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 1113 | dependencies = [ 1114 | "base64", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "rustversion" 1119 | version = "1.0.19" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 1122 | 1123 | [[package]] 1124 | name = "rustyline" 1125 | version = "11.0.0" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "5dfc8644681285d1fb67a467fb3021bfea306b99b4146b166a1fe3ada965eece" 1128 | dependencies = [ 1129 | "bitflags 1.3.2", 1130 | "cfg-if", 1131 | "clipboard-win", 1132 | "dirs-next", 1133 | "fd-lock", 1134 | "libc", 1135 | "log", 1136 | "memchr", 1137 | "nix", 1138 | "radix_trie", 1139 | "scopeguard", 1140 | "unicode-segmentation", 1141 | "unicode-width", 1142 | "utf8parse", 1143 | "winapi", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "ryu" 1148 | version = "1.0.19" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 1151 | 1152 | [[package]] 1153 | name = "schannel" 1154 | version = "0.1.27" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1157 | dependencies = [ 1158 | "windows-sys 0.59.0", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "scopeguard" 1163 | version = "1.2.0" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1166 | 1167 | [[package]] 1168 | name = "security-framework" 1169 | version = "2.11.1" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1172 | dependencies = [ 1173 | "bitflags 2.8.0", 1174 | "core-foundation", 1175 | "core-foundation-sys", 1176 | "libc", 1177 | "security-framework-sys", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "security-framework-sys" 1182 | version = "2.14.0" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1185 | dependencies = [ 1186 | "core-foundation-sys", 1187 | "libc", 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "serde" 1192 | version = "1.0.218" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 1195 | dependencies = [ 1196 | "serde_derive", 1197 | ] 1198 | 1199 | [[package]] 1200 | name = "serde_derive" 1201 | version = "1.0.218" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 1204 | dependencies = [ 1205 | "proc-macro2", 1206 | "quote", 1207 | "syn", 1208 | ] 1209 | 1210 | [[package]] 1211 | name = "serde_json" 1212 | version = "1.0.139" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" 1215 | dependencies = [ 1216 | "itoa", 1217 | "memchr", 1218 | "ryu", 1219 | "serde", 1220 | ] 1221 | 1222 | [[package]] 1223 | name = "serde_urlencoded" 1224 | version = "0.7.1" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1227 | dependencies = [ 1228 | "form_urlencoded", 1229 | "itoa", 1230 | "ryu", 1231 | "serde", 1232 | ] 1233 | 1234 | [[package]] 1235 | name = "shellwords" 1236 | version = "1.1.0" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "89e515aa4699a88148ed5ef96413ceef0048ce95b43fbc955a33bde0a70fcae6" 1239 | dependencies = [ 1240 | "lazy_static", 1241 | "regex", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "shlex" 1246 | version = "1.3.0" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1249 | 1250 | [[package]] 1251 | name = "signal-hook-registry" 1252 | version = "1.4.2" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1255 | dependencies = [ 1256 | "libc", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "slab" 1261 | version = "0.4.9" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1264 | dependencies = [ 1265 | "autocfg", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "smallvec" 1270 | version = "1.14.0" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 1273 | 1274 | [[package]] 1275 | name = "socket2" 1276 | version = "0.5.8" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1279 | dependencies = [ 1280 | "libc", 1281 | "windows-sys 0.52.0", 1282 | ] 1283 | 1284 | [[package]] 1285 | name = "stable_deref_trait" 1286 | version = "1.2.0" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1289 | 1290 | [[package]] 1291 | name = "str-buf" 1292 | version = "1.0.6" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" 1295 | 1296 | [[package]] 1297 | name = "syn" 1298 | version = "2.0.98" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 1301 | dependencies = [ 1302 | "proc-macro2", 1303 | "quote", 1304 | "unicode-ident", 1305 | ] 1306 | 1307 | [[package]] 1308 | name = "sync_wrapper" 1309 | version = "0.1.2" 1310 | source = "registry+https://github.com/rust-lang/crates.io-index" 1311 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1312 | 1313 | [[package]] 1314 | name = "synstructure" 1315 | version = "0.13.1" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1318 | dependencies = [ 1319 | "proc-macro2", 1320 | "quote", 1321 | "syn", 1322 | ] 1323 | 1324 | [[package]] 1325 | name = "system-configuration" 1326 | version = "0.5.1" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1329 | dependencies = [ 1330 | "bitflags 1.3.2", 1331 | "core-foundation", 1332 | "system-configuration-sys", 1333 | ] 1334 | 1335 | [[package]] 1336 | name = "system-configuration-sys" 1337 | version = "0.5.0" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1340 | dependencies = [ 1341 | "core-foundation-sys", 1342 | "libc", 1343 | ] 1344 | 1345 | [[package]] 1346 | name = "tempfile" 1347 | version = "3.17.1" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" 1350 | dependencies = [ 1351 | "cfg-if", 1352 | "fastrand", 1353 | "getrandom 0.3.1", 1354 | "once_cell", 1355 | "rustix", 1356 | "windows-sys 0.59.0", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "termcolor" 1361 | version = "1.4.1" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1364 | dependencies = [ 1365 | "winapi-util", 1366 | ] 1367 | 1368 | [[package]] 1369 | name = "thiserror" 1370 | version = "1.0.69" 1371 | source = "registry+https://github.com/rust-lang/crates.io-index" 1372 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1373 | dependencies = [ 1374 | "thiserror-impl", 1375 | ] 1376 | 1377 | [[package]] 1378 | name = "thiserror-impl" 1379 | version = "1.0.69" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1382 | dependencies = [ 1383 | "proc-macro2", 1384 | "quote", 1385 | "syn", 1386 | ] 1387 | 1388 | [[package]] 1389 | name = "thread_local" 1390 | version = "1.1.8" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1393 | dependencies = [ 1394 | "cfg-if", 1395 | "once_cell", 1396 | ] 1397 | 1398 | [[package]] 1399 | name = "tinystr" 1400 | version = "0.7.6" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1403 | dependencies = [ 1404 | "displaydoc", 1405 | "zerovec", 1406 | ] 1407 | 1408 | [[package]] 1409 | name = "tokio" 1410 | version = "1.43.0" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 1413 | dependencies = [ 1414 | "backtrace", 1415 | "bytes", 1416 | "libc", 1417 | "mio", 1418 | "parking_lot", 1419 | "pin-project-lite", 1420 | "signal-hook-registry", 1421 | "socket2", 1422 | "tokio-macros", 1423 | "windows-sys 0.52.0", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "tokio-macros" 1428 | version = "2.5.0" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1431 | dependencies = [ 1432 | "proc-macro2", 1433 | "quote", 1434 | "syn", 1435 | ] 1436 | 1437 | [[package]] 1438 | name = "tokio-native-tls" 1439 | version = "0.3.1" 1440 | source = "registry+https://github.com/rust-lang/crates.io-index" 1441 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1442 | dependencies = [ 1443 | "native-tls", 1444 | "tokio", 1445 | ] 1446 | 1447 | [[package]] 1448 | name = "tokio-util" 1449 | version = "0.7.13" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 1452 | dependencies = [ 1453 | "bytes", 1454 | "futures-core", 1455 | "futures-sink", 1456 | "pin-project-lite", 1457 | "tokio", 1458 | ] 1459 | 1460 | [[package]] 1461 | name = "tower-service" 1462 | version = "0.3.3" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1465 | 1466 | [[package]] 1467 | name = "tracing" 1468 | version = "0.1.41" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1471 | dependencies = [ 1472 | "pin-project-lite", 1473 | "tracing-core", 1474 | ] 1475 | 1476 | [[package]] 1477 | name = "tracing-core" 1478 | version = "0.1.33" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1481 | dependencies = [ 1482 | "once_cell", 1483 | ] 1484 | 1485 | [[package]] 1486 | name = "try-lock" 1487 | version = "0.2.5" 1488 | source = "registry+https://github.com/rust-lang/crates.io-index" 1489 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1490 | 1491 | [[package]] 1492 | name = "unicode-ident" 1493 | version = "1.0.17" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 1496 | 1497 | [[package]] 1498 | name = "unicode-segmentation" 1499 | version = "1.12.0" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1502 | 1503 | [[package]] 1504 | name = "unicode-width" 1505 | version = "0.1.14" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1508 | 1509 | [[package]] 1510 | name = "url" 1511 | version = "2.5.4" 1512 | source = "registry+https://github.com/rust-lang/crates.io-index" 1513 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1514 | dependencies = [ 1515 | "form_urlencoded", 1516 | "idna", 1517 | "percent-encoding", 1518 | ] 1519 | 1520 | [[package]] 1521 | name = "utf16_iter" 1522 | version = "1.0.5" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1525 | 1526 | [[package]] 1527 | name = "utf8_iter" 1528 | version = "1.0.4" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1531 | 1532 | [[package]] 1533 | name = "utf8parse" 1534 | version = "0.2.2" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1537 | 1538 | [[package]] 1539 | name = "vcpkg" 1540 | version = "0.2.15" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1543 | 1544 | [[package]] 1545 | name = "want" 1546 | version = "0.3.1" 1547 | source = "registry+https://github.com/rust-lang/crates.io-index" 1548 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1549 | dependencies = [ 1550 | "try-lock", 1551 | ] 1552 | 1553 | [[package]] 1554 | name = "wasi" 1555 | version = "0.11.0+wasi-snapshot-preview1" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1558 | 1559 | [[package]] 1560 | name = "wasi" 1561 | version = "0.13.3+wasi-0.2.2" 1562 | source = "registry+https://github.com/rust-lang/crates.io-index" 1563 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 1564 | dependencies = [ 1565 | "wit-bindgen-rt", 1566 | ] 1567 | 1568 | [[package]] 1569 | name = "wasm-bindgen" 1570 | version = "0.2.100" 1571 | source = "registry+https://github.com/rust-lang/crates.io-index" 1572 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1573 | dependencies = [ 1574 | "cfg-if", 1575 | "once_cell", 1576 | "rustversion", 1577 | "wasm-bindgen-macro", 1578 | ] 1579 | 1580 | [[package]] 1581 | name = "wasm-bindgen-backend" 1582 | version = "0.2.100" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1585 | dependencies = [ 1586 | "bumpalo", 1587 | "log", 1588 | "proc-macro2", 1589 | "quote", 1590 | "syn", 1591 | "wasm-bindgen-shared", 1592 | ] 1593 | 1594 | [[package]] 1595 | name = "wasm-bindgen-futures" 1596 | version = "0.4.50" 1597 | source = "registry+https://github.com/rust-lang/crates.io-index" 1598 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 1599 | dependencies = [ 1600 | "cfg-if", 1601 | "js-sys", 1602 | "once_cell", 1603 | "wasm-bindgen", 1604 | "web-sys", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "wasm-bindgen-macro" 1609 | version = "0.2.100" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1612 | dependencies = [ 1613 | "quote", 1614 | "wasm-bindgen-macro-support", 1615 | ] 1616 | 1617 | [[package]] 1618 | name = "wasm-bindgen-macro-support" 1619 | version = "0.2.100" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1622 | dependencies = [ 1623 | "proc-macro2", 1624 | "quote", 1625 | "syn", 1626 | "wasm-bindgen-backend", 1627 | "wasm-bindgen-shared", 1628 | ] 1629 | 1630 | [[package]] 1631 | name = "wasm-bindgen-shared" 1632 | version = "0.2.100" 1633 | source = "registry+https://github.com/rust-lang/crates.io-index" 1634 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1635 | dependencies = [ 1636 | "unicode-ident", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "web-sys" 1641 | version = "0.3.77" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1644 | dependencies = [ 1645 | "js-sys", 1646 | "wasm-bindgen", 1647 | ] 1648 | 1649 | [[package]] 1650 | name = "winapi" 1651 | version = "0.3.9" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1654 | dependencies = [ 1655 | "winapi-i686-pc-windows-gnu", 1656 | "winapi-x86_64-pc-windows-gnu", 1657 | ] 1658 | 1659 | [[package]] 1660 | name = "winapi-i686-pc-windows-gnu" 1661 | version = "0.4.0" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1664 | 1665 | [[package]] 1666 | name = "winapi-util" 1667 | version = "0.1.9" 1668 | source = "registry+https://github.com/rust-lang/crates.io-index" 1669 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1670 | dependencies = [ 1671 | "windows-sys 0.59.0", 1672 | ] 1673 | 1674 | [[package]] 1675 | name = "winapi-x86_64-pc-windows-gnu" 1676 | version = "0.4.0" 1677 | source = "registry+https://github.com/rust-lang/crates.io-index" 1678 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1679 | 1680 | [[package]] 1681 | name = "windows-sys" 1682 | version = "0.48.0" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1685 | dependencies = [ 1686 | "windows-targets 0.48.5", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "windows-sys" 1691 | version = "0.52.0" 1692 | source = "registry+https://github.com/rust-lang/crates.io-index" 1693 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1694 | dependencies = [ 1695 | "windows-targets 0.52.6", 1696 | ] 1697 | 1698 | [[package]] 1699 | name = "windows-sys" 1700 | version = "0.59.0" 1701 | source = "registry+https://github.com/rust-lang/crates.io-index" 1702 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1703 | dependencies = [ 1704 | "windows-targets 0.52.6", 1705 | ] 1706 | 1707 | [[package]] 1708 | name = "windows-targets" 1709 | version = "0.48.5" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1712 | dependencies = [ 1713 | "windows_aarch64_gnullvm 0.48.5", 1714 | "windows_aarch64_msvc 0.48.5", 1715 | "windows_i686_gnu 0.48.5", 1716 | "windows_i686_msvc 0.48.5", 1717 | "windows_x86_64_gnu 0.48.5", 1718 | "windows_x86_64_gnullvm 0.48.5", 1719 | "windows_x86_64_msvc 0.48.5", 1720 | ] 1721 | 1722 | [[package]] 1723 | name = "windows-targets" 1724 | version = "0.52.6" 1725 | source = "registry+https://github.com/rust-lang/crates.io-index" 1726 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1727 | dependencies = [ 1728 | "windows_aarch64_gnullvm 0.52.6", 1729 | "windows_aarch64_msvc 0.52.6", 1730 | "windows_i686_gnu 0.52.6", 1731 | "windows_i686_gnullvm", 1732 | "windows_i686_msvc 0.52.6", 1733 | "windows_x86_64_gnu 0.52.6", 1734 | "windows_x86_64_gnullvm 0.52.6", 1735 | "windows_x86_64_msvc 0.52.6", 1736 | ] 1737 | 1738 | [[package]] 1739 | name = "windows_aarch64_gnullvm" 1740 | version = "0.48.5" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1743 | 1744 | [[package]] 1745 | name = "windows_aarch64_gnullvm" 1746 | version = "0.52.6" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1749 | 1750 | [[package]] 1751 | name = "windows_aarch64_msvc" 1752 | version = "0.48.5" 1753 | source = "registry+https://github.com/rust-lang/crates.io-index" 1754 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1755 | 1756 | [[package]] 1757 | name = "windows_aarch64_msvc" 1758 | version = "0.52.6" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1761 | 1762 | [[package]] 1763 | name = "windows_i686_gnu" 1764 | version = "0.48.5" 1765 | source = "registry+https://github.com/rust-lang/crates.io-index" 1766 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1767 | 1768 | [[package]] 1769 | name = "windows_i686_gnu" 1770 | version = "0.52.6" 1771 | source = "registry+https://github.com/rust-lang/crates.io-index" 1772 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1773 | 1774 | [[package]] 1775 | name = "windows_i686_gnullvm" 1776 | version = "0.52.6" 1777 | source = "registry+https://github.com/rust-lang/crates.io-index" 1778 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1779 | 1780 | [[package]] 1781 | name = "windows_i686_msvc" 1782 | version = "0.48.5" 1783 | source = "registry+https://github.com/rust-lang/crates.io-index" 1784 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1785 | 1786 | [[package]] 1787 | name = "windows_i686_msvc" 1788 | version = "0.52.6" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1791 | 1792 | [[package]] 1793 | name = "windows_x86_64_gnu" 1794 | version = "0.48.5" 1795 | source = "registry+https://github.com/rust-lang/crates.io-index" 1796 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1797 | 1798 | [[package]] 1799 | name = "windows_x86_64_gnu" 1800 | version = "0.52.6" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1803 | 1804 | [[package]] 1805 | name = "windows_x86_64_gnullvm" 1806 | version = "0.48.5" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1809 | 1810 | [[package]] 1811 | name = "windows_x86_64_gnullvm" 1812 | version = "0.52.6" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1815 | 1816 | [[package]] 1817 | name = "windows_x86_64_msvc" 1818 | version = "0.48.5" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1821 | 1822 | [[package]] 1823 | name = "windows_x86_64_msvc" 1824 | version = "0.52.6" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1827 | 1828 | [[package]] 1829 | name = "winreg" 1830 | version = "0.50.0" 1831 | source = "registry+https://github.com/rust-lang/crates.io-index" 1832 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 1833 | dependencies = [ 1834 | "cfg-if", 1835 | "windows-sys 0.48.0", 1836 | ] 1837 | 1838 | [[package]] 1839 | name = "wit-bindgen-rt" 1840 | version = "0.33.0" 1841 | source = "registry+https://github.com/rust-lang/crates.io-index" 1842 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1843 | dependencies = [ 1844 | "bitflags 2.8.0", 1845 | ] 1846 | 1847 | [[package]] 1848 | name = "write16" 1849 | version = "1.0.0" 1850 | source = "registry+https://github.com/rust-lang/crates.io-index" 1851 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1852 | 1853 | [[package]] 1854 | name = "writeable" 1855 | version = "0.5.5" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1858 | 1859 | [[package]] 1860 | name = "yoke" 1861 | version = "0.7.5" 1862 | source = "registry+https://github.com/rust-lang/crates.io-index" 1863 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1864 | dependencies = [ 1865 | "serde", 1866 | "stable_deref_trait", 1867 | "yoke-derive", 1868 | "zerofrom", 1869 | ] 1870 | 1871 | [[package]] 1872 | name = "yoke-derive" 1873 | version = "0.7.5" 1874 | source = "registry+https://github.com/rust-lang/crates.io-index" 1875 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1876 | dependencies = [ 1877 | "proc-macro2", 1878 | "quote", 1879 | "syn", 1880 | "synstructure", 1881 | ] 1882 | 1883 | [[package]] 1884 | name = "zerofrom" 1885 | version = "0.1.5" 1886 | source = "registry+https://github.com/rust-lang/crates.io-index" 1887 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1888 | dependencies = [ 1889 | "zerofrom-derive", 1890 | ] 1891 | 1892 | [[package]] 1893 | name = "zerofrom-derive" 1894 | version = "0.1.5" 1895 | source = "registry+https://github.com/rust-lang/crates.io-index" 1896 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1897 | dependencies = [ 1898 | "proc-macro2", 1899 | "quote", 1900 | "syn", 1901 | "synstructure", 1902 | ] 1903 | 1904 | [[package]] 1905 | name = "zerovec" 1906 | version = "0.10.4" 1907 | source = "registry+https://github.com/rust-lang/crates.io-index" 1908 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1909 | dependencies = [ 1910 | "yoke", 1911 | "zerofrom", 1912 | "zerovec-derive", 1913 | ] 1914 | 1915 | [[package]] 1916 | name = "zerovec-derive" 1917 | version = "0.10.3" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1920 | dependencies = [ 1921 | "proc-macro2", 1922 | "quote", 1923 | "syn", 1924 | ] 1925 | --------------------------------------------------------------------------------