...
This commit is contained in:
		
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -14,3 +14,8 @@ Cargo.lock | ||||
| # MSVC Windows builds of rustc generate these, which store debugging information | ||||
| *.pdb | ||||
|  | ||||
|  | ||||
|  | ||||
| # Added by cargo | ||||
|  | ||||
| /target | ||||
|   | ||||
							
								
								
									
										28
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| [package] | ||||
| name = "sal" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
| authors = ["Your Name <your.email@example.com>"] | ||||
| description = "System Abstraction Layer - A library for easy interaction with operating system features" | ||||
| repository = "https://github.com/yourusername/sal" | ||||
| license = "MIT OR Apache-2.0" | ||||
| keywords = ["system", "os", "abstraction", "platform", "filesystem"] | ||||
| categories = ["os", "filesystem", "api-bindings"] | ||||
| readme = "README.md" | ||||
|  | ||||
| [dependencies] | ||||
| # Cross-platform functionality | ||||
| libc = "0.2" | ||||
| cfg-if = "1.0" | ||||
| thiserror = "1.0"  # For error handling | ||||
| log = "0.4"        # Logging facade | ||||
|  | ||||
| # Optional features for specific OS functionality | ||||
| [target.'cfg(unix)'.dependencies] | ||||
| nix = "0.26"       # Unix-specific functionality | ||||
|  | ||||
| [target.'cfg(windows)'.dependencies] | ||||
| windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Storage_FileSystem"] } | ||||
|  | ||||
| [dev-dependencies] | ||||
| tempfile = "3.5"   # For tests that need temporary files/directories | ||||
							
								
								
									
										597
									
								
								src/git/git.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										597
									
								
								src/git/git.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,597 @@ | ||||
| use std::process::Command; | ||||
| use std::path::Path; | ||||
| use std::fs; | ||||
| use std::env; | ||||
| use regex::Regex; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
|  | ||||
| // Define a custom error type for git operations | ||||
| #[derive(Debug)] | ||||
| pub enum GitError { | ||||
|     GitNotInstalled(std::io::Error), | ||||
|     InvalidUrl(String), | ||||
|     HomeDirectoryNotFound(std::env::VarError), | ||||
|     FileSystemError(std::io::Error), | ||||
|     GitCommandFailed(String), | ||||
|     CommandExecutionError(std::io::Error), | ||||
|     NoRepositoriesFound, | ||||
|     RepositoryNotFound(String), | ||||
|     MultipleRepositoriesFound(String, usize), | ||||
|     NotAGitRepository(String), | ||||
|     LocalChangesExist(String), | ||||
| } | ||||
|  | ||||
| // Implement Display for GitError | ||||
| impl fmt::Display for GitError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e), | ||||
|             GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url), | ||||
|             GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e), | ||||
|             GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e), | ||||
|             GitError::GitCommandFailed(e) => write!(f, "{}", e), | ||||
|             GitError::CommandExecutionError(e) => write!(f, "Error executing command: {}", e), | ||||
|             GitError::NoRepositoriesFound => write!(f, "No repositories found"), | ||||
|             GitError::RepositoryNotFound(pattern) => write!(f, "No repositories found matching '{}'", pattern), | ||||
|             GitError::MultipleRepositoriesFound(pattern, count) =>  | ||||
|                 write!(f, "Multiple repositories ({}) found matching '{}'. Use '*' suffix for multiple matches.", count, pattern), | ||||
|             GitError::NotAGitRepository(path) => write!(f, "Not a git repository at {}", path), | ||||
|             GitError::LocalChangesExist(path) => write!(f, "Repository at {} has local changes", path), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Error trait for GitError | ||||
| impl Error for GitError { | ||||
|     fn source(&self) -> Option<&(dyn Error + 'static)> { | ||||
|         match self { | ||||
|             GitError::GitNotInstalled(e) => Some(e), | ||||
|             GitError::HomeDirectoryNotFound(e) => Some(e), | ||||
|             GitError::FileSystemError(e) => Some(e), | ||||
|             GitError::CommandExecutionError(e) => Some(e), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Git utility functions | ||||
|  | ||||
| /** | ||||
|  * Clones a git repository to a standardized location in the user's home directory. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `url` - The URL of the git repository to clone. Can be in HTTPS format  | ||||
|  *   (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git). | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - The path where the repository was cloned, formatted as  | ||||
|  *   ~/code/server/account/repo (e.g., ~/code/github.com/username/repo). | ||||
|  * * `Err(GitError)` - An error if the clone operation failed. | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let repo_path = git_clone("https://github.com/username/repo.git")?; | ||||
|  * println!("Repository cloned to: {}", repo_path); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn git_clone(url: &str) -> Result<String, GitError> { | ||||
|     // Check if git is installed | ||||
|     let git_check = Command::new("git") | ||||
|         .arg("--version") | ||||
|         .output() | ||||
|         .map_err(GitError::GitNotInstalled)?; | ||||
|      | ||||
|     // Parse the URL to determine the clone path | ||||
|     let (server, account, repo) = parse_git_url(url); | ||||
|     if server.is_empty() || account.is_empty() || repo.is_empty() { | ||||
|         return Err(GitError::InvalidUrl(url.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Create the target directory | ||||
|     let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?; | ||||
|      | ||||
|     let clone_path = format!("{}/code/{}/{}/{}", home_dir, server, account, repo); | ||||
|     let clone_dir = Path::new(&clone_path); | ||||
|      | ||||
|     // Check if repo already exists | ||||
|     if clone_dir.exists() { | ||||
|         return Ok(format!("Repository already exists at {}", clone_path)); | ||||
|     } | ||||
|      | ||||
|     // Create parent directory | ||||
|     if let Some(parent) = clone_dir.parent() { | ||||
|         fs::create_dir_all(parent).map_err(GitError::FileSystemError)?; | ||||
|     } | ||||
|      | ||||
|     // Clone the repository | ||||
|     let output = Command::new("git") | ||||
|         .args(&["clone", "--depth", "1", url, &clone_path]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|          | ||||
|     if output.status.success() { | ||||
|         Ok(clone_path) | ||||
|     } else { | ||||
|         let error = String::from_utf8_lossy(&output.stderr); | ||||
|         Err(GitError::GitCommandFailed(format!("Git clone error: {}", error))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Parses a git URL to extract the server, account, and repository name. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `url` - The URL of the git repository to parse. Can be in HTTPS format  | ||||
|  *   (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git). | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * A tuple containing: | ||||
|  * * `server` - The server name (e.g., "github.com") | ||||
|  * * `account` - The account or organization name (e.g., "username") | ||||
|  * * `repo` - The repository name (e.g., "repo") | ||||
|  *  | ||||
|  * If the URL cannot be parsed, all three values will be empty strings. | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let (server, account, repo) = parse_git_url("https://github.com/username/repo.git"); | ||||
|  * assert_eq!(server, "github.com"); | ||||
|  * assert_eq!(account, "username"); | ||||
|  * assert_eq!(repo, "repo"); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn parse_git_url(url: &str) -> (String, String, String) { | ||||
|     // HTTP(S) URL format: https://github.com/username/repo.git | ||||
|     let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap(); | ||||
|      | ||||
|     // SSH URL format: git@github.com:username/repo.git | ||||
|     let ssh_re = Regex::new(r"git@([^:]+):([^/]+)/([^/\.]+)(?:\.git)?").unwrap(); | ||||
|      | ||||
|     if let Some(caps) = https_re.captures(url) { | ||||
|         let server = caps.get(1).map_or("", |m| m.as_str()).to_string(); | ||||
|         let account = caps.get(2).map_or("", |m| m.as_str()).to_string(); | ||||
|         let repo = caps.get(3).map_or("", |m| m.as_str()).to_string(); | ||||
|          | ||||
|         return (server, account, repo); | ||||
|     } else if let Some(caps) = ssh_re.captures(url) { | ||||
|         let server = caps.get(1).map_or("", |m| m.as_str()).to_string(); | ||||
|         let account = caps.get(2).map_or("", |m| m.as_str()).to_string(); | ||||
|         let repo = caps.get(3).map_or("", |m| m.as_str()).to_string(); | ||||
|          | ||||
|         return (server, account, repo); | ||||
|     } | ||||
|      | ||||
|     (String::new(), String::new(), String::new()) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Lists all git repositories found in the user's ~/code directory. | ||||
|  *  | ||||
|  * This function searches for directories containing a .git subdirectory, | ||||
|  * which indicates a git repository. | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(Vec<String>)` - A vector of paths to git repositories | ||||
|  * * `Err(GitError)` - An error if the operation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let repos = git_list()?; | ||||
|  * for repo in repos { | ||||
|  *     println!("Found repository: {}", repo); | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn git_list() -> Result<Vec<String>, GitError> { | ||||
|     // Get home directory | ||||
|     let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?; | ||||
|      | ||||
|     let code_dir = format!("{}/code", home_dir); | ||||
|     let code_path = Path::new(&code_dir); | ||||
|      | ||||
|     if !code_path.exists() || !code_path.is_dir() { | ||||
|         return Ok(Vec::new()); | ||||
|     } | ||||
|      | ||||
|     let mut repos = Vec::new(); | ||||
|      | ||||
|     // Find all directories with .git subdirectories | ||||
|     let output = Command::new("find") | ||||
|         .args(&[&code_dir, "-type", "d", "-name", ".git"]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|          | ||||
|     if output.status.success() { | ||||
|         let stdout = String::from_utf8_lossy(&output.stdout); | ||||
|         for line in stdout.lines() { | ||||
|             // Get the parent directory of .git which is the repo root | ||||
|             if let Some(parent) = Path::new(line).parent() { | ||||
|                 if let Some(path_str) = parent.to_str() { | ||||
|                     repos.push(path_str.to_string()); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         let error = String::from_utf8_lossy(&output.stderr); | ||||
|         return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error))); | ||||
|     } | ||||
|      | ||||
|     Ok(repos) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Checks if a git repository has uncommitted changes. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `repo_path` - The path to the git repository | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise | ||||
|  * * `Err(GitError)` - An error if the operation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * if has_git_changes("/path/to/repo")? { | ||||
|  *     println!("Repository has uncommitted changes"); | ||||
|  * } else { | ||||
|  *     println!("Repository is clean"); | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn has_git_changes(repo_path: &str) -> Result<bool, GitError> { | ||||
|     let output = Command::new("git") | ||||
|         .args(&["-C", repo_path, "status", "--porcelain"]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|          | ||||
|     Ok(!output.stdout.is_empty()) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Finds repositories matching a pattern or partial path. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `pattern` - The pattern to match against repository paths | ||||
|  *   - If the pattern ends with '*', all matching repositories are returned | ||||
|  *   - Otherwise, exactly one matching repository must be found | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(Vec<String>)` - A vector of paths to matching repositories | ||||
|  * * `Err(GitError)` - An error if no matching repositories are found, | ||||
|  *   or if multiple repositories match a non-wildcard pattern | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // Find all repositories containing "project" | ||||
|  * let repos = find_matching_repos("project*")?; | ||||
|  *  | ||||
|  * // Find exactly one repository containing "unique-project" | ||||
|  * let repo = find_matching_repos("unique-project")?[0]; | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn find_matching_repos(pattern: &str) -> Result<Vec<String>, GitError> { | ||||
|     // Get all repos | ||||
|     let repos = git_list()?; | ||||
|      | ||||
|     if repos.is_empty() { | ||||
|         return Err(GitError::NoRepositoriesFound); | ||||
|     } | ||||
|      | ||||
|     // Check if pattern ends with wildcard | ||||
|     if pattern.ends_with('*') { | ||||
|         let search_pattern = &pattern[0..pattern.len()-1]; // Remove the * | ||||
|         let matching: Vec<String> = repos.iter() | ||||
|             .filter(|repo| repo.contains(search_pattern)) | ||||
|             .cloned() | ||||
|             .collect(); | ||||
|          | ||||
|         if matching.is_empty() { | ||||
|             return Err(GitError::RepositoryNotFound(pattern.to_string())); | ||||
|         } | ||||
|          | ||||
|         Ok(matching) | ||||
|     } else { | ||||
|         // No wildcard, need to find exactly one match | ||||
|         let matching: Vec<String> = repos.iter() | ||||
|             .filter(|repo| repo.contains(pattern)) | ||||
|             .cloned() | ||||
|             .collect(); | ||||
|          | ||||
|         match matching.len() { | ||||
|             0 => Err(GitError::RepositoryNotFound(pattern.to_string())), | ||||
|             1 => Ok(matching), | ||||
|             _ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Updates a git repository by pulling the latest changes. | ||||
|  *  | ||||
|  * This function will fail if there are uncommitted changes in the repository. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating the repository was updated | ||||
|  * * `Err(GitError)` - An error if the update failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let result = git_update("my-project")?; | ||||
|  * println!("{}", result); // "Successfully updated repository at /home/user/code/github.com/user/my-project" | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn git_update(repo_path: &str) -> Result<String, GitError> { | ||||
|     // If repo_path may be a partial path, find the matching repository | ||||
|     let repos = find_matching_repos(repo_path)?; | ||||
|      | ||||
|     // Should only be one repository at this point | ||||
|     let actual_path = &repos[0]; | ||||
|      | ||||
|     // Check if repository exists and is a git repository | ||||
|     let git_dir = Path::new(actual_path).join(".git"); | ||||
|     if !git_dir.exists() || !git_dir.is_dir() { | ||||
|         return Err(GitError::NotAGitRepository(actual_path.clone())); | ||||
|     } | ||||
|      | ||||
|     // Check for local changes | ||||
|     if has_git_changes(actual_path)? { | ||||
|         return Err(GitError::LocalChangesExist(actual_path.clone())); | ||||
|     } | ||||
|  | ||||
|     // Pull the latest changes | ||||
|     let output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "pull"]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if output.status.success() { | ||||
|         let stdout = String::from_utf8_lossy(&output.stdout); | ||||
|         if stdout.contains("Already up to date") { | ||||
|             Ok(format!("Repository already up to date at {}", actual_path)) | ||||
|         } else { | ||||
|             Ok(format!("Successfully updated repository at {}", actual_path)) | ||||
|         } | ||||
|     } else { | ||||
|         let error = String::from_utf8_lossy(&output.stderr); | ||||
|         Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Force updates a git repository by discarding local changes and pulling the latest changes. | ||||
|  *  | ||||
|  * This function will reset any uncommitted changes and clean untracked files before pulling. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating the repository was force-updated | ||||
|  * * `Err(GitError)` - An error if the update failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let result = git_update_force("my-project")?; | ||||
|  * println!("{}", result); // "Successfully force-updated repository at /home/user/code/github.com/user/my-project" | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn git_update_force(repo_path: &str) -> Result<String, GitError> { | ||||
|     // If repo_path may be a partial path, find the matching repository | ||||
|     let repos = find_matching_repos(repo_path)?; | ||||
|      | ||||
|     // Should only be one repository at this point | ||||
|     let actual_path = &repos[0]; | ||||
|      | ||||
|     // Check if repository exists and is a git repository | ||||
|     let git_dir = Path::new(actual_path).join(".git"); | ||||
|     if !git_dir.exists() || !git_dir.is_dir() { | ||||
|         return Err(GitError::NotAGitRepository(actual_path.clone())); | ||||
|     } | ||||
|      | ||||
|     // Reset any local changes | ||||
|     let reset_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "reset", "--hard", "HEAD"]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if !reset_output.status.success() { | ||||
|         let error = String::from_utf8_lossy(&reset_output.stderr); | ||||
|         return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error))); | ||||
|     } | ||||
|  | ||||
|     // Clean untracked files | ||||
|     let clean_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "clean", "-fd"]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if !clean_output.status.success() { | ||||
|         let error = String::from_utf8_lossy(&clean_output.stderr); | ||||
|         return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error))); | ||||
|     } | ||||
|  | ||||
|     // Pull the latest changes | ||||
|     let pull_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "pull"]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if pull_output.status.success() { | ||||
|         Ok(format!("Successfully force-updated repository at {}", actual_path)) | ||||
|     } else { | ||||
|         let error = String::from_utf8_lossy(&pull_output.stderr); | ||||
|         Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Commits changes in a git repository and then updates it by pulling the latest changes. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository | ||||
|  * * `message` - The commit message | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating the repository was committed and updated | ||||
|  * * `Err(GitError)` - An error if the operation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let result = git_update_commit("my-project", "Fix bug in login form")?; | ||||
|  * println!("{}", result); // "Successfully committed and updated repository at /home/user/code/github.com/user/my-project" | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn git_update_commit(repo_path: &str, message: &str) -> Result<String, GitError> { | ||||
|     // If repo_path may be a partial path, find the matching repository | ||||
|     let repos = find_matching_repos(repo_path)?; | ||||
|      | ||||
|     // Should only be one repository at this point | ||||
|     let actual_path = &repos[0]; | ||||
|      | ||||
|     // Check if repository exists and is a git repository | ||||
|     let git_dir = Path::new(actual_path).join(".git"); | ||||
|     if !git_dir.exists() || !git_dir.is_dir() { | ||||
|         return Err(GitError::NotAGitRepository(actual_path.clone())); | ||||
|     } | ||||
|  | ||||
|     // Check for local changes | ||||
|     if !has_git_changes(actual_path)? { | ||||
|         return Ok(format!("No changes to commit in repository at {}", actual_path)); | ||||
|     } | ||||
|  | ||||
|     // Add all changes | ||||
|     let add_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "add", "."]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if !add_output.status.success() { | ||||
|         let error = String::from_utf8_lossy(&add_output.stderr); | ||||
|         return Err(GitError::GitCommandFailed(format!("Git add error: {}", error))); | ||||
|     } | ||||
|  | ||||
|     // Commit the changes | ||||
|     let commit_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "commit", "-m", message]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if !commit_output.status.success() { | ||||
|         let error = String::from_utf8_lossy(&commit_output.stderr); | ||||
|         return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error))); | ||||
|     } | ||||
|  | ||||
|     // Pull the latest changes | ||||
|     let pull_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "pull"]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if pull_output.status.success() { | ||||
|         Ok(format!("Successfully committed and updated repository at {}", actual_path)) | ||||
|     } else { | ||||
|         let error = String::from_utf8_lossy(&pull_output.stderr); | ||||
|         Err(GitError::GitCommandFailed(format!("Git pull error: {}", error))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Commits changes in a git repository and pushes them to the remote. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository | ||||
|  * * `message` - The commit message | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating the repository was committed and pushed | ||||
|  * * `Err(GitError)` - An error if the operation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let result = git_update_commit_push("my-project", "Add new feature")?; | ||||
|  * println!("{}", result); // "Successfully committed and pushed repository at /home/user/code/github.com/user/my-project" | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result<String, GitError> { | ||||
|     // If repo_path may be a partial path, find the matching repository | ||||
|     let repos = find_matching_repos(repo_path)?; | ||||
|      | ||||
|     // Should only be one repository at this point | ||||
|     let actual_path = &repos[0]; | ||||
|      | ||||
|     // Check if repository exists and is a git repository | ||||
|     let git_dir = Path::new(actual_path).join(".git"); | ||||
|     if !git_dir.exists() || !git_dir.is_dir() { | ||||
|         return Err(GitError::NotAGitRepository(actual_path.clone())); | ||||
|     } | ||||
|  | ||||
|     // Check for local changes | ||||
|     if !has_git_changes(actual_path)? { | ||||
|         return Ok(format!("No changes to commit in repository at {}", actual_path)); | ||||
|     } | ||||
|  | ||||
|     // Add all changes | ||||
|     let add_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "add", "."]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if !add_output.status.success() { | ||||
|         let error = String::from_utf8_lossy(&add_output.stderr); | ||||
|         return Err(GitError::GitCommandFailed(format!("Git add error: {}", error))); | ||||
|     } | ||||
|  | ||||
|     // Commit the changes | ||||
|     let commit_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "commit", "-m", message]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if !commit_output.status.success() { | ||||
|         let error = String::from_utf8_lossy(&commit_output.stderr); | ||||
|         return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error))); | ||||
|     } | ||||
|  | ||||
|     // Push the changes | ||||
|     let push_output = Command::new("git") | ||||
|         .args(&["-C", actual_path, "push"]) | ||||
|         .output() | ||||
|         .map_err(GitError::CommandExecutionError)?; | ||||
|  | ||||
|     if push_output.status.success() { | ||||
|         Ok(format!("Successfully committed and pushed repository at {}", actual_path)) | ||||
|     } else { | ||||
|         let error = String::from_utf8_lossy(&push_output.stderr); | ||||
|         Err(GitError::GitCommandFailed(format!("Git push error: {}", error))) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/git/git_executor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/git/git_executor.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| use std::process::Command; | ||||
| use std::path::Path; | ||||
| use std::fs; | ||||
| use std::env; | ||||
| use regex::Regex; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
							
								
								
									
										12
									
								
								src/git/instructions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/git/instructions.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| in @/sal/git/git_executor.rs  | ||||
| create a GitExecutor which is the one executing git commands | ||||
| and also checking if ssh-agent is loaded | ||||
| or if there is an authentication mechanism defined in redis | ||||
|  | ||||
| check if there is a redis on ~/hero/var/redis.sock | ||||
|  | ||||
| over unix domani socket | ||||
|  | ||||
| if yes then check there is an entry on | ||||
|  | ||||
| sal::git:: | ||||
							
								
								
									
										2
									
								
								src/git/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/git/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| use git::*; | ||||
| use git_executor::*; | ||||
							
								
								
									
										60
									
								
								src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| //! # System Abstraction Layer (SAL) | ||||
| //! | ||||
| //! `sal` is a library that provides a unified interface for interacting with | ||||
| //! operating system features across different platforms. It abstracts away the | ||||
| //! platform-specific details, allowing developers to write cross-platform code | ||||
| //! with ease. | ||||
| //! | ||||
| //! ## Features | ||||
| //! | ||||
| //! - File system operations | ||||
| //! - Process management | ||||
| //! - System information | ||||
| //! - Network operations | ||||
| //! - Environment variables | ||||
|  | ||||
| use std::io; | ||||
| use thiserror::Error; | ||||
|  | ||||
| /// Error types for the SAL library | ||||
| #[derive(Error, Debug)] | ||||
| pub enum Error { | ||||
|     /// An error occurred during an I/O operation | ||||
|     #[error("I/O error: {0}")] | ||||
|     Io(#[from] io::Error), | ||||
|  | ||||
|     /// An error specific to the SAL library | ||||
|     #[error("SAL error: {0}")] | ||||
|     Sal(String), | ||||
|  | ||||
|     /// An error that occurred in a platform-specific operation | ||||
|     #[error("Platform-specific error: {0}")] | ||||
|     Platform(String), | ||||
| } | ||||
|  | ||||
| /// Result type for SAL operations | ||||
| pub type Result<T> = std::result::Result<T, Error>; | ||||
|  | ||||
| // Re-export modules | ||||
| pub mod process; | ||||
| pub mod git; | ||||
| pub mod os; | ||||
| pub mod network; | ||||
| pub mod env; | ||||
| pub mod text; | ||||
|  | ||||
| // Version information | ||||
| /// Returns the version of the SAL library | ||||
| pub fn version() -> &'static str { | ||||
|     env!("CARGO_PKG_VERSION") | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use super::*; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_version() { | ||||
|         assert!(!version().is_empty()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| pub mod os; | ||||
| pub mod text; | ||||
| pub mod git; | ||||
							
								
								
									
										289
									
								
								src/os/download.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								src/os/download.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,289 @@ | ||||
| use std::process::Command; | ||||
| use std::path::Path; | ||||
| use std::fs; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
| use std::io; | ||||
|  | ||||
| // Define a custom error type for download operations | ||||
| #[derive(Debug)] | ||||
| pub enum DownloadError { | ||||
|     CreateDirectoryFailed(io::Error), | ||||
|     CurlExecutionFailed(io::Error), | ||||
|     DownloadFailed(String), | ||||
|     FileMetadataError(io::Error), | ||||
|     FileTooSmall(i64, i64), | ||||
|     RemoveFileFailed(io::Error), | ||||
|     ExtractionFailed(String), | ||||
|     CommandExecutionFailed(io::Error), | ||||
|     InvalidUrl(String), | ||||
|     NotAFile(String), | ||||
|     PlatformNotSupported(String), | ||||
|     InstallationFailed(String), | ||||
| } | ||||
|  | ||||
| // Implement Display for DownloadError | ||||
| impl fmt::Display for DownloadError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             DownloadError::CreateDirectoryFailed(e) => write!(f, "Error creating directories: {}", e), | ||||
|             DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e), | ||||
|             DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url), | ||||
|             DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e), | ||||
|             DownloadError::FileTooSmall(size, min) => write!(f, "Error: Downloaded file is too small ({}KB < {}KB)", size, min), | ||||
|             DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e), | ||||
|             DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e), | ||||
|             DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e), | ||||
|             DownloadError::InvalidUrl(url) => write!(f, "Invalid URL: {}", url), | ||||
|             DownloadError::NotAFile(path) => write!(f, "Not a file: {}", path), | ||||
|             DownloadError::PlatformNotSupported(msg) => write!(f, "{}", msg), | ||||
|             DownloadError::InstallationFailed(msg) => write!(f, "{}", msg), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Error trait for DownloadError | ||||
| impl Error for DownloadError { | ||||
|     fn source(&self) -> Option<&(dyn Error + 'static)> { | ||||
|         match self { | ||||
|             DownloadError::CreateDirectoryFailed(e) => Some(e), | ||||
|             DownloadError::CurlExecutionFailed(e) => Some(e), | ||||
|             DownloadError::FileMetadataError(e) => Some(e), | ||||
|             DownloadError::RemoveFileFailed(e) => Some(e), | ||||
|             DownloadError::CommandExecutionFailed(e) => Some(e), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Download a file from URL to destination using the curl command. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `url` - The URL to download from | ||||
|  * * `dest` - The destination path where the file will be saved | ||||
|  * * `min_size_kb` - Minimum required file size in KB (0 for no minimum) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - The path where the file was saved or extracted | ||||
|  * * `Err(DownloadError)` - An error if the download failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // Download a file with no minimum size requirement | ||||
|  * let path = download("https://example.com/file.txt", "/tmp/file.txt", 0)?; | ||||
|  *  | ||||
|  * // Download a file with minimum size requirement of 100KB | ||||
|  * let path = download("https://example.com/file.zip", "/tmp/file.zip", 100)?; | ||||
|  * ``` | ||||
|  *  | ||||
|  * # Notes | ||||
|  *  | ||||
|  * If the URL ends with .tar.gz, .tgz, .tar, or .zip, the file will be automatically | ||||
|  * extracted to the destination directory. | ||||
|  */ | ||||
| pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> { | ||||
|     // Create parent directories if they don't exist | ||||
|     let dest_path = Path::new(dest); | ||||
|     if let Some(parent) = dest_path.parent() { | ||||
|         fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?; | ||||
|     } | ||||
|      | ||||
|     // Create a temporary path for downloading | ||||
|     let temp_path = format!("{}.download", dest); | ||||
|      | ||||
|     // Use curl to download the file with progress bar | ||||
|     println!("Downloading {} to {}", url, dest); | ||||
|     let output = Command::new("curl") | ||||
|         .args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) | ||||
|         .status() | ||||
|         .map_err(DownloadError::CurlExecutionFailed)?; | ||||
|          | ||||
|     if !output.success() { | ||||
|         return Err(DownloadError::DownloadFailed(url.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Show file size after download | ||||
|     match fs::metadata(&temp_path) { | ||||
|         Ok(metadata) => { | ||||
|             let size_bytes = metadata.len(); | ||||
|             let size_kb = size_bytes / 1024; | ||||
|             let size_mb = size_kb / 1024; | ||||
|             if size_mb > 1 { | ||||
|                 println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0)); | ||||
|             } else { | ||||
|                 println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0); | ||||
|             } | ||||
|         }, | ||||
|         Err(_) => println!("Download complete!"), | ||||
|     } | ||||
|      | ||||
|     // Check file size if minimum size is specified | ||||
|     if min_size_kb > 0 { | ||||
|         let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; | ||||
|         let size_kb = metadata.len() as i64 / 1024; | ||||
|         if size_kb < min_size_kb { | ||||
|             fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; | ||||
|             return Err(DownloadError::FileTooSmall(size_kb, min_size_kb)); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Check if it's a compressed file that needs extraction | ||||
|     let lower_url = url.to_lowercase(); | ||||
|     let is_archive = lower_url.ends_with(".tar.gz") ||  | ||||
|                      lower_url.ends_with(".tgz") ||  | ||||
|                      lower_url.ends_with(".tar") || | ||||
|                      lower_url.ends_with(".zip"); | ||||
|      | ||||
|     if is_archive { | ||||
|         // Create the destination directory | ||||
|         fs::create_dir_all(dest).map_err(DownloadError::CreateDirectoryFailed)?; | ||||
|          | ||||
|         // Extract the file using the appropriate command with progress indication | ||||
|         println!("Extracting {} to {}", temp_path, dest); | ||||
|         let output = if lower_url.ends_with(".zip") { | ||||
|             Command::new("unzip") | ||||
|                 .args(&["-o", &temp_path, "-d", dest])  // Removed -q for verbosity | ||||
|                 .status() | ||||
|         } else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") { | ||||
|             Command::new("tar") | ||||
|                 .args(&["-xzvf", &temp_path, "-C", dest])  // Added v for verbosity | ||||
|                 .status() | ||||
|         } else { | ||||
|             Command::new("tar") | ||||
|                 .args(&["-xvf", &temp_path, "-C", dest])  // Added v for verbosity | ||||
|                 .status() | ||||
|         }; | ||||
|          | ||||
|         match output { | ||||
|             Ok(status) => { | ||||
|                 if !status.success() { | ||||
|                     return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string())); | ||||
|                 } | ||||
|             }, | ||||
|             Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), | ||||
|         } | ||||
|          | ||||
|         // Show number of extracted files | ||||
|         match fs::read_dir(dest) { | ||||
|             Ok(entries) => { | ||||
|                 let count = entries.count(); | ||||
|                 println!("Extraction complete! Extracted {} files/directories", count); | ||||
|             }, | ||||
|             Err(_) => println!("Extraction complete!"), | ||||
|         } | ||||
|          | ||||
|         // Remove the temporary file | ||||
|         fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; | ||||
|          | ||||
|         Ok(dest.to_string()) | ||||
|     } else { | ||||
|         // Just rename the temporary file to the final destination | ||||
|         fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; | ||||
|          | ||||
|         Ok(dest.to_string()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Download a file and install it if it's a supported package format. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `url` - The URL to download from | ||||
|  * * `min_size_kb` - Minimum required file size in KB (0 for no minimum) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - The path where the file was saved or extracted | ||||
|  * * `Err(DownloadError)` - An error if the download or installation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // Download and install a .deb package | ||||
|  * let result = download_install("https://example.com/package.deb", 100)?; | ||||
|  * ``` | ||||
|  *  | ||||
|  * # Notes | ||||
|  *  | ||||
|  * Currently only supports .deb packages on Debian-based systems. | ||||
|  * For other file types, it behaves the same as the download function. | ||||
|  */ | ||||
| pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadError> { | ||||
|     // Extract filename from URL | ||||
|     let filename = match url.split('/').last() { | ||||
|         Some(name) => name, | ||||
|         None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string())) | ||||
|     }; | ||||
|      | ||||
|     // Create a proper destination path | ||||
|     let dest_path = format!("/tmp/{}", filename); | ||||
|  | ||||
|     let download_result = download(url, &dest_path, min_size_kb)?; | ||||
|      | ||||
|     // Check if the downloaded result is a file | ||||
|     let path = Path::new(&dest_path); | ||||
|     if !path.is_file() {    | ||||
|         return Ok(download_result); // Not a file, might be an extracted directory | ||||
|     } | ||||
|      | ||||
|     // Check if it's a .deb package | ||||
|     if dest_path.to_lowercase().ends_with(".deb") { | ||||
|         // Check if we're on a Debian-based platform | ||||
|         let platform_check = Command::new("sh") | ||||
|             .arg("-c") | ||||
|             .arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version") | ||||
|             .status(); | ||||
|              | ||||
|         match platform_check { | ||||
|             Ok(status) => { | ||||
|                 if !status.success() { | ||||
|                     return Err(DownloadError::PlatformNotSupported( | ||||
|                         "Cannot install .deb package: not on a Debian-based system".to_string() | ||||
|                     )); | ||||
|                 } | ||||
|             }, | ||||
|             Err(_) => return Err(DownloadError::PlatformNotSupported( | ||||
|                 "Failed to check system compatibility for .deb installation".to_string() | ||||
|             )), | ||||
|         } | ||||
|          | ||||
|         // Install the .deb package non-interactively | ||||
|         println!("Installing package: {}", dest_path); | ||||
|         let install_result = Command::new("sudo") | ||||
|             .args(&["dpkg", "--install", &dest_path]) | ||||
|             .status(); | ||||
|              | ||||
|         match install_result { | ||||
|             Ok(status) => { | ||||
|                 if !status.success() { | ||||
|                     // If dpkg fails, try to fix dependencies and retry | ||||
|                     println!("Attempting to resolve dependencies..."); | ||||
|                     let fix_deps = Command::new("sudo") | ||||
|                         .args(&["apt-get", "install", "-f", "-y"]) | ||||
|                         .status(); | ||||
|                          | ||||
|                     if let Ok(fix_status) = fix_deps { | ||||
|                         if !fix_status.success() { | ||||
|                             return Err(DownloadError::InstallationFailed( | ||||
|                                 "Failed to resolve package dependencies".to_string() | ||||
|                             )); | ||||
|                         } | ||||
|                     } else { | ||||
|                         return Err(DownloadError::InstallationFailed( | ||||
|                             "Failed to resolve package dependencies".to_string() | ||||
|                         )); | ||||
|                     } | ||||
|                 } | ||||
|                 println!("Package installation completed successfully"); | ||||
|             }, | ||||
|             Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     Ok(download_result) | ||||
| } | ||||
							
								
								
									
										580
									
								
								src/os/fs.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										580
									
								
								src/os/fs.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,580 @@ | ||||
| use std::fs; | ||||
| use std::path::Path; | ||||
| use std::process::Command; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
| use std::io; | ||||
|  | ||||
| // Define a custom error type for file system operations | ||||
| #[derive(Debug)] | ||||
| pub enum FsError { | ||||
|     DirectoryNotFound(String), | ||||
|     FileNotFound(String), | ||||
|     CreateDirectoryFailed(io::Error), | ||||
|     CopyFailed(io::Error), | ||||
|     DeleteFailed(io::Error), | ||||
|     CommandFailed(String), | ||||
|     CommandExecutionError(io::Error), | ||||
|     InvalidGlobPattern(glob::PatternError), | ||||
|     NotADirectory(String), | ||||
|     NotAFile(String), | ||||
|     UnknownFileType(String), | ||||
|     MetadataError(io::Error), | ||||
| } | ||||
|  | ||||
| // Implement Display for FsError | ||||
| impl fmt::Display for FsError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             FsError::DirectoryNotFound(dir) => write!(f, "Directory '{}' does not exist", dir), | ||||
|             FsError::FileNotFound(pattern) => write!(f, "No files found matching '{}'", pattern), | ||||
|             FsError::CreateDirectoryFailed(e) => write!(f, "Failed to create parent directories: {}", e), | ||||
|             FsError::CopyFailed(e) => write!(f, "Failed to copy file: {}", e), | ||||
|             FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e), | ||||
|             FsError::CommandFailed(e) => write!(f, "{}", e), | ||||
|             FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e), | ||||
|             FsError::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", e), | ||||
|             FsError::NotADirectory(path) => write!(f, "Path '{}' exists but is not a directory", path), | ||||
|             FsError::NotAFile(path) => write!(f, "Path '{}' is not a regular file", path), | ||||
|             FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path), | ||||
|             FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Error trait for FsError | ||||
| impl Error for FsError { | ||||
|     fn source(&self) -> Option<&(dyn Error + 'static)> { | ||||
|         match self { | ||||
|             FsError::CreateDirectoryFailed(e) => Some(e), | ||||
|             FsError::CopyFailed(e) => Some(e), | ||||
|             FsError::DeleteFailed(e) => Some(e), | ||||
|             FsError::CommandExecutionError(e) => Some(e), | ||||
|             FsError::InvalidGlobPattern(e) => Some(e), | ||||
|             FsError::MetadataError(e) => Some(e), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Recursively copy a file or directory from source to destination. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `src` - The source path, which can include wildcards | ||||
|  * * `dest` - The destination path | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating what was copied | ||||
|  * * `Err(FsError)` - An error if the copy operation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // Copy a single file | ||||
|  * let result = copy("file.txt", "backup/file.txt")?; | ||||
|  *  | ||||
|  * // Copy multiple files using wildcards | ||||
|  * let result = copy("*.txt", "backup/")?; | ||||
|  *  | ||||
|  * // Copy a directory recursively | ||||
|  * let result = copy("src_dir", "dest_dir")?; | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn copy(src: &str, dest: &str) -> Result<String, FsError> { | ||||
|     let dest_path = Path::new(dest); | ||||
|      | ||||
|     // Check if source path contains wildcards | ||||
|     if src.contains('*') || src.contains('?') || src.contains('[') { | ||||
|         // Create parent directories for destination if needed | ||||
|         if let Some(parent) = dest_path.parent() { | ||||
|             fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; | ||||
|         } | ||||
|          | ||||
|         // Use glob to expand wildcards | ||||
|         let entries = glob::glob(src).map_err(FsError::InvalidGlobPattern)?; | ||||
|          | ||||
|         let paths: Vec<_> = entries | ||||
|             .filter_map(Result::ok) | ||||
|             .collect(); | ||||
|          | ||||
|         if paths.is_empty() { | ||||
|             return Err(FsError::FileNotFound(src.to_string())); | ||||
|         } | ||||
|          | ||||
|         let mut success_count = 0; | ||||
|         let dest_is_dir = dest_path.exists() && dest_path.is_dir(); | ||||
|          | ||||
|         for path in paths { | ||||
|             let target_path = if dest_is_dir { | ||||
|                 // If destination is a directory, copy the file into it | ||||
|                 dest_path.join(path.file_name().unwrap_or_default()) | ||||
|             } else { | ||||
|                 // Otherwise use the destination as is (only makes sense for single file) | ||||
|                 dest_path.to_path_buf() | ||||
|             }; | ||||
|              | ||||
|             if path.is_file() { | ||||
|                 // Copy file | ||||
|                 if let Err(e) = fs::copy(&path, &target_path) { | ||||
|                     println!("Warning: Failed to copy {}: {}", path.display(), e); | ||||
|                 } else { | ||||
|                     success_count += 1; | ||||
|                 } | ||||
|             } else if path.is_dir() { | ||||
|                 // For directories, use platform-specific command | ||||
|                 #[cfg(target_os = "windows")] | ||||
|                 let output = Command::new("xcopy") | ||||
|                     .args(&["/E", "/I", "/H", "/Y",  | ||||
|                            &path.to_string_lossy(),  | ||||
|                            &target_path.to_string_lossy()]) | ||||
|                     .status(); | ||||
|                      | ||||
|                 #[cfg(not(target_os = "windows"))] | ||||
|                 let output = Command::new("cp") | ||||
|                     .args(&["-R",  | ||||
|                            &path.to_string_lossy(),  | ||||
|                            &target_path.to_string_lossy()]) | ||||
|                     .status(); | ||||
|                      | ||||
|                 match output { | ||||
|                     Ok(status) => { | ||||
|                         if status.success() { | ||||
|                             success_count += 1; | ||||
|                         } | ||||
|                     }, | ||||
|                     Err(e) => println!("Warning: Failed to copy directory {}: {}", path.display(), e), | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if success_count > 0 { | ||||
|             Ok(format!("Successfully copied {} items from '{}' to '{}'",  | ||||
|                       success_count, src, dest)) | ||||
|         } else { | ||||
|             Err(FsError::CommandFailed(format!("Failed to copy any files from '{}' to '{}'", src, dest))) | ||||
|         } | ||||
|     } else { | ||||
|         // Handle non-wildcard paths normally | ||||
|         let src_path = Path::new(src); | ||||
|          | ||||
|         // Check if source exists | ||||
|         if !src_path.exists() { | ||||
|             return Err(FsError::FileNotFound(src.to_string())); | ||||
|         } | ||||
|          | ||||
|         // Create parent directories if they don't exist | ||||
|         if let Some(parent) = dest_path.parent() { | ||||
|             fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; | ||||
|         } | ||||
|          | ||||
|         // Copy based on source type | ||||
|         if src_path.is_file() { | ||||
|             // Copy file | ||||
|             fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?; | ||||
|             Ok(format!("Successfully copied file '{}' to '{}'", src, dest)) | ||||
|         } else if src_path.is_dir() { | ||||
|             // For directories, use platform-specific command | ||||
|             #[cfg(target_os = "windows")] | ||||
|             let output = Command::new("xcopy") | ||||
|                 .args(&["/E", "/I", "/H", "/Y", src, dest]) | ||||
|                 .output(); | ||||
|                  | ||||
|             #[cfg(not(target_os = "windows"))] | ||||
|             let output = Command::new("cp") | ||||
|                 .args(&["-R", src, dest]) | ||||
|                 .output(); | ||||
|              | ||||
|             match output { | ||||
|                 Ok(out) => { | ||||
|                     if out.status.success() { | ||||
|                         Ok(format!("Successfully copied directory '{}' to '{}'", src, dest)) | ||||
|                     } else { | ||||
|                         let error = String::from_utf8_lossy(&out.stderr); | ||||
|                         Err(FsError::CommandFailed(format!("Failed to copy directory: {}", error))) | ||||
|                     } | ||||
|                 }, | ||||
|                 Err(e) => Err(FsError::CommandExecutionError(e)), | ||||
|             } | ||||
|         } else { | ||||
|             Err(FsError::UnknownFileType(src.to_string())) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if a file or directory exists. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `path` - The path to check | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `bool` - True if the path exists, false otherwise | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * if exist("file.txt") { | ||||
|  *     println!("File exists"); | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn exist(path: &str) -> bool { | ||||
|     Path::new(path).exists() | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Find a file in a directory (with support for wildcards). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `dir` - The directory to search in | ||||
|  * * `filename` - The filename pattern to search for (can include wildcards) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - The path to the found file | ||||
|  * * `Err(FsError)` - An error if no file is found or multiple files are found | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let file_path = find_file("/path/to/dir", "*.txt")?; | ||||
|  * println!("Found file: {}", file_path); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn find_file(dir: &str, filename: &str) -> Result<String, FsError> { | ||||
|     let dir_path = Path::new(dir); | ||||
|      | ||||
|     // Check if directory exists | ||||
|     if !dir_path.exists() || !dir_path.is_dir() { | ||||
|         return Err(FsError::DirectoryNotFound(dir.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Use glob to find files - use recursive pattern to find in subdirectories too | ||||
|     let pattern = format!("{}/**/{}", dir, filename); | ||||
|     let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; | ||||
|      | ||||
|     let files: Vec<_> = entries | ||||
|         .filter_map(Result::ok) | ||||
|         .filter(|path| path.is_file()) | ||||
|         .collect(); | ||||
|              | ||||
|     match files.len() { | ||||
|         0 => Err(FsError::FileNotFound(filename.to_string())), | ||||
|         1 => Ok(files[0].to_string_lossy().to_string()), | ||||
|         _ => { | ||||
|             // If multiple matches, just return the first one instead of erroring | ||||
|             // This makes wildcard searches more practical | ||||
|             println!("Note: Multiple files found matching '{}', returning first match", filename); | ||||
|             Ok(files[0].to_string_lossy().to_string()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Find multiple files in a directory (recursive, with support for wildcards). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `dir` - The directory to search in | ||||
|  * * `filename` - The filename pattern to search for (can include wildcards) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(Vec<String>)` - A vector of paths to the found files | ||||
|  * * `Err(FsError)` - An error if the directory doesn't exist or the pattern is invalid | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let files = find_files("/path/to/dir", "*.txt")?; | ||||
|  * for file in files { | ||||
|  *     println!("Found file: {}", file); | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn find_files(dir: &str, filename: &str) -> Result<Vec<String>, FsError> { | ||||
|     let dir_path = Path::new(dir); | ||||
|      | ||||
|     // Check if directory exists | ||||
|     if !dir_path.exists() || !dir_path.is_dir() { | ||||
|         return Err(FsError::DirectoryNotFound(dir.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Use glob to find files | ||||
|     let pattern = format!("{}/**/{}", dir, filename); | ||||
|     let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; | ||||
|      | ||||
|     let files: Vec<String> = entries | ||||
|         .filter_map(Result::ok) | ||||
|         .filter(|path| path.is_file()) | ||||
|         .map(|path| path.to_string_lossy().to_string()) | ||||
|         .collect(); | ||||
|              | ||||
|     Ok(files) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Find a directory in a parent directory (with support for wildcards). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `dir` - The parent directory to search in | ||||
|  * * `dirname` - The directory name pattern to search for (can include wildcards) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - The path to the found directory | ||||
|  * * `Err(FsError)` - An error if no directory is found or multiple directories are found | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let dir_path = find_dir("/path/to/parent", "sub*")?; | ||||
|  * println!("Found directory: {}", dir_path); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn find_dir(dir: &str, dirname: &str) -> Result<String, FsError> { | ||||
|     let dir_path = Path::new(dir); | ||||
|      | ||||
|     // Check if directory exists | ||||
|     if !dir_path.exists() || !dir_path.is_dir() { | ||||
|         return Err(FsError::DirectoryNotFound(dir.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Use glob to find directories | ||||
|     let pattern = format!("{}/{}", dir, dirname); | ||||
|     let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; | ||||
|      | ||||
|     let dirs: Vec<_> = entries | ||||
|         .filter_map(Result::ok) | ||||
|         .filter(|path| path.is_dir()) | ||||
|         .collect(); | ||||
|              | ||||
|     match dirs.len() { | ||||
|         0 => Err(FsError::DirectoryNotFound(dirname.to_string())), | ||||
|         1 => Ok(dirs[0].to_string_lossy().to_string()), | ||||
|         _ => Err(FsError::CommandFailed(format!("Multiple directories found matching '{}', expected only one", dirname))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Find multiple directories in a parent directory (recursive, with support for wildcards). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `dir` - The parent directory to search in | ||||
|  * * `dirname` - The directory name pattern to search for (can include wildcards) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(Vec<String>)` - A vector of paths to the found directories | ||||
|  * * `Err(FsError)` - An error if the parent directory doesn't exist or the pattern is invalid | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let dirs = find_dirs("/path/to/parent", "sub*")?; | ||||
|  * for dir in dirs { | ||||
|  *     println!("Found directory: {}", dir); | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn find_dirs(dir: &str, dirname: &str) -> Result<Vec<String>, FsError> { | ||||
|     let dir_path = Path::new(dir); | ||||
|      | ||||
|     // Check if directory exists | ||||
|     if !dir_path.exists() || !dir_path.is_dir() { | ||||
|         return Err(FsError::DirectoryNotFound(dir.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Use glob to find directories | ||||
|     let pattern = format!("{}/**/{}", dir, dirname); | ||||
|     let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?; | ||||
|      | ||||
|     let dirs: Vec<String> = entries | ||||
|         .filter_map(Result::ok) | ||||
|         .filter(|path| path.is_dir()) | ||||
|         .map(|path| path.to_string_lossy().to_string()) | ||||
|         .collect(); | ||||
|              | ||||
|     Ok(dirs) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Delete a file or directory (defensive - doesn't error if file doesn't exist). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `path` - The path to delete | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating what was deleted | ||||
|  * * `Err(FsError)` - An error if the deletion failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // Delete a file | ||||
|  * let result = delete("file.txt")?; | ||||
|  *  | ||||
|  * // Delete a directory and all its contents | ||||
|  * let result = delete("directory/")?; | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn delete(path: &str) -> Result<String, FsError> { | ||||
|     let path_obj = Path::new(path); | ||||
|      | ||||
|     // Check if path exists | ||||
|     if !path_obj.exists() { | ||||
|         return Ok(format!("Nothing to delete at '{}'", path)); | ||||
|     } | ||||
|      | ||||
|     // Delete based on path type | ||||
|     if path_obj.is_file() || path_obj.is_symlink() { | ||||
|         fs::remove_file(path_obj).map_err(FsError::DeleteFailed)?; | ||||
|         Ok(format!("Successfully deleted file '{}'", path)) | ||||
|     } else if path_obj.is_dir() { | ||||
|         fs::remove_dir_all(path_obj).map_err(FsError::DeleteFailed)?; | ||||
|         Ok(format!("Successfully deleted directory '{}'", path)) | ||||
|     } else { | ||||
|         Err(FsError::UnknownFileType(path.to_string())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a directory and all parent directories (defensive - doesn't error if directory exists). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `path` - The path of the directory to create | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating the directory was created | ||||
|  * * `Err(FsError)` - An error if the creation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let result = mkdir("path/to/new/directory")?; | ||||
|  * println!("{}", result); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn mkdir(path: &str) -> Result<String, FsError> { | ||||
|     let path_obj = Path::new(path); | ||||
|      | ||||
|     // Check if path already exists | ||||
|     if path_obj.exists() { | ||||
|         if path_obj.is_dir() { | ||||
|             return Ok(format!("Directory '{}' already exists", path)); | ||||
|         } else { | ||||
|             return Err(FsError::NotADirectory(path.to_string())); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Create directory and parents | ||||
|     fs::create_dir_all(path_obj).map_err(FsError::CreateDirectoryFailed)?; | ||||
|     Ok(format!("Successfully created directory '{}'", path)) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the size of a file in bytes. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `path` - The path of the file | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(i64)` - The size of the file in bytes | ||||
|  * * `Err(FsError)` - An error if the file doesn't exist or isn't a regular file | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let size = file_size("file.txt")?; | ||||
|  * println!("File size: {} bytes", size); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn file_size(path: &str) -> Result<i64, FsError> { | ||||
|     let path_obj = Path::new(path); | ||||
|      | ||||
|     // Check if file exists | ||||
|     if !path_obj.exists() { | ||||
|         return Err(FsError::FileNotFound(path.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Check if it's a regular file | ||||
|     if !path_obj.is_file() { | ||||
|         return Err(FsError::NotAFile(path.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Get file metadata | ||||
|     let metadata = fs::metadata(path_obj).map_err(FsError::MetadataError)?; | ||||
|     Ok(metadata.len() as i64) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Sync directories using rsync (or platform equivalent). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `src` - The source directory | ||||
|  * * `dest` - The destination directory | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating the directories were synced | ||||
|  * * `Err(FsError)` - An error if the sync failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let result = rsync("source_dir/", "backup_dir/")?; | ||||
|  * println!("{}", result); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> { | ||||
|     let src_path = Path::new(src); | ||||
|     let dest_path = Path::new(dest); | ||||
|      | ||||
|     // Check if source exists | ||||
|     if !src_path.exists() { | ||||
|         return Err(FsError::FileNotFound(src.to_string())); | ||||
|     } | ||||
|      | ||||
|     // Create parent directories if they don't exist | ||||
|     if let Some(parent) = dest_path.parent() { | ||||
|         fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?; | ||||
|     } | ||||
|      | ||||
|     // Use platform-specific command for syncing | ||||
|     #[cfg(target_os = "windows")] | ||||
|     let output = Command::new("robocopy") | ||||
|         .args(&[src, dest, "/MIR", "/NFL", "/NDL"]) | ||||
|         .output(); | ||||
|          | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     let output = Command::new("rsync") | ||||
|         .args(&["-a", "--delete", src, dest]) | ||||
|         .output(); | ||||
|          | ||||
|     match output { | ||||
|         Ok(out) => { | ||||
|             if out.status.success() || out.status.code() == Some(1) { // rsync and robocopy return 1 for some non-error cases | ||||
|                 Ok(format!("Successfully synced '{}' to '{}'", src, dest)) | ||||
|             } else { | ||||
|                 let error = String::from_utf8_lossy(&out.stderr); | ||||
|                 Err(FsError::CommandFailed(format!("Failed to sync directories: {}", error))) | ||||
|             } | ||||
|         }, | ||||
|         Err(e) => Err(FsError::CommandExecutionError(e)), | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2
									
								
								src/os/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/os/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| use fs::*; | ||||
| use download::*; | ||||
							
								
								
									
										303
									
								
								src/process/mgmt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								src/process/mgmt.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,303 @@ | ||||
| use std::process::Command; | ||||
| use std::collections::HashMap; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
| use std::io; | ||||
|  | ||||
| // Define a custom error type for process operations | ||||
| #[derive(Debug)] | ||||
| pub enum ProcessError { | ||||
|     CommandExecutionFailed(io::Error), | ||||
|     CommandFailed(String), | ||||
|     NoProcessFound(String), | ||||
|     MultipleProcessesFound(String, usize), | ||||
| } | ||||
|  | ||||
| // Implement Display for ProcessError | ||||
| impl fmt::Display for ProcessError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             ProcessError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e), | ||||
|             ProcessError::CommandFailed(e) => write!(f, "{}", e), | ||||
|             ProcessError::NoProcessFound(pattern) => write!(f, "No processes found matching '{}'", pattern), | ||||
|             ProcessError::MultipleProcessesFound(pattern, count) =>  | ||||
|                 write!(f, "Multiple processes ({}) found matching '{}'", count, pattern), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Error trait for ProcessError | ||||
| impl Error for ProcessError { | ||||
|     fn source(&self) -> Option<&(dyn Error + 'static)> { | ||||
|         match self { | ||||
|             ProcessError::CommandExecutionFailed(e) => Some(e), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Define a struct to represent process information | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct ProcessInfo { | ||||
|     pub pid: i64, | ||||
|     pub name: String, | ||||
|     pub memory: f64, | ||||
|     pub cpu: f64, | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if a command exists in PATH. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `cmd` - The command to check | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Option<String>` - The full path to the command if found, None otherwise | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * match which("git") { | ||||
|  *     Some(path) => println!("Git is installed at: {}", path), | ||||
|  *     None => println!("Git is not installed"), | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn which(cmd: &str) -> Option<String> { | ||||
|     #[cfg(target_os = "windows")] | ||||
|     let which_cmd = "where"; | ||||
|      | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     let which_cmd = "which"; | ||||
|      | ||||
|     let output = Command::new(which_cmd) | ||||
|         .arg(cmd) | ||||
|         .output(); | ||||
|      | ||||
|     match output { | ||||
|         Ok(out) => { | ||||
|             if out.status.success() { | ||||
|                 let path = String::from_utf8_lossy(&out.stdout).trim().to_string(); | ||||
|                 Some(path) | ||||
|             } else { | ||||
|                 None | ||||
|             } | ||||
|         }, | ||||
|         Err(_) => None | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Kill processes matching a pattern. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `pattern` - The pattern to match against process names | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(String)` - A success message indicating processes were killed or none were found | ||||
|  * * `Err(ProcessError)` - An error if the kill operation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // Kill all processes with "server" in their name | ||||
|  * let result = kill("server")?; | ||||
|  * println!("{}", result); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn kill(pattern: &str) -> Result<String, ProcessError> { | ||||
|     // Platform specific implementation | ||||
|     #[cfg(target_os = "windows")] | ||||
|     { | ||||
|         // On Windows, use taskkill with wildcard support | ||||
|         let mut args = vec!["/F"]; // Force kill | ||||
|          | ||||
|         if pattern.contains('*') { | ||||
|             // If it contains wildcards, use filter | ||||
|             args.extend(&["/FI", &format!("IMAGENAME eq {}", pattern)]); | ||||
|         } else { | ||||
|             // Otherwise use image name directly | ||||
|             args.extend(&["/IM", pattern]); | ||||
|         } | ||||
|          | ||||
|         let output = Command::new("taskkill") | ||||
|             .args(&args) | ||||
|             .output() | ||||
|             .map_err(ProcessError::CommandExecutionFailed)?; | ||||
|              | ||||
|         if output.status.success() { | ||||
|             Ok("Successfully killed processes".to_string()) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             if error.is_empty() { | ||||
|                 let stdout = String::from_utf8_lossy(&output.stdout); | ||||
|                 if stdout.contains("No tasks") { | ||||
|                     Ok("No matching processes found".to_string()) | ||||
|                 } else { | ||||
|                     Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", stdout))) | ||||
|                 } | ||||
|             } else { | ||||
|                 Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error))) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     { | ||||
|         // On Unix-like systems, use pkill which has built-in pattern matching | ||||
|         let output = Command::new("pkill") | ||||
|             .arg("-f") // Match against full process name/args | ||||
|             .arg(pattern) | ||||
|             .output() | ||||
|             .map_err(ProcessError::CommandExecutionFailed)?; | ||||
|              | ||||
|         // pkill returns 0 if processes were killed, 1 if none matched | ||||
|         if output.status.success() { | ||||
|             Ok("Successfully killed processes".to_string()) | ||||
|         } else if output.status.code() == Some(1) { | ||||
|             Ok("No matching processes found".to_string()) | ||||
|         } else { | ||||
|             let error = String::from_utf8_lossy(&output.stderr); | ||||
|             Err(ProcessError::CommandFailed(format!("Failed to kill processes: {}", error))) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * List processes matching a pattern (or all if pattern is empty). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `pattern` - The pattern to match against process names (empty string for all processes) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(Vec<ProcessInfo>)` - A vector of process information for matching processes | ||||
|  * * `Err(ProcessError)` - An error if the list operation failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // List all processes | ||||
|  * let processes = process_list("")?; | ||||
|  *  | ||||
|  * // List processes with "server" in their name | ||||
|  * let processes = process_list("server")?; | ||||
|  * for proc in processes { | ||||
|  *     println!("PID: {}, Name: {}", proc.pid, proc.name); | ||||
|  * } | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError> { | ||||
|     let mut processes = Vec::new(); | ||||
|      | ||||
|     // Platform specific implementations | ||||
|     #[cfg(target_os = "windows")] | ||||
|     { | ||||
|         // Windows implementation using wmic | ||||
|         let output = Command::new("wmic") | ||||
|             .args(&["process", "list", "brief"]) | ||||
|             .output() | ||||
|             .map_err(ProcessError::CommandExecutionFailed)?; | ||||
|              | ||||
|         if output.status.success() { | ||||
|             let stdout = String::from_utf8_lossy(&output.stdout).to_string(); | ||||
|              | ||||
|             // Parse output (assuming format: Handle Name Priority) | ||||
|             for line in stdout.lines().skip(1) { // Skip header | ||||
|                 let parts: Vec<&str> = line.trim().split_whitespace().collect(); | ||||
|                 if parts.len() >= 2 { | ||||
|                     let pid = parts[0].parse::<i64>().unwrap_or(0); | ||||
|                     let name = parts[1].to_string(); | ||||
|                      | ||||
|                     // Filter by pattern if provided | ||||
|                     if !pattern.is_empty() && !name.contains(pattern) { | ||||
|                         continue; | ||||
|                     } | ||||
|                      | ||||
|                     processes.push(ProcessInfo { | ||||
|                         pid, | ||||
|                         name, | ||||
|                         memory: 0.0, // Placeholder | ||||
|                         cpu: 0.0,    // Placeholder | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             let stderr = String::from_utf8_lossy(&output.stderr).to_string(); | ||||
|             return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr))); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     { | ||||
|         // Unix implementation using ps | ||||
|         let output = Command::new("ps") | ||||
|             .args(&["-eo", "pid,comm"]) | ||||
|             .output() | ||||
|             .map_err(ProcessError::CommandExecutionFailed)?; | ||||
|              | ||||
|         if output.status.success() { | ||||
|             let stdout = String::from_utf8_lossy(&output.stdout).to_string(); | ||||
|              | ||||
|             // Parse output (assuming format: PID COMMAND) | ||||
|             for line in stdout.lines().skip(1) { // Skip header | ||||
|                 let parts: Vec<&str> = line.trim().split_whitespace().collect(); | ||||
|                 if parts.len() >= 2 { | ||||
|                     let pid = parts[0].parse::<i64>().unwrap_or(0); | ||||
|                     let name = parts[1].to_string(); | ||||
|                      | ||||
|                     // Filter by pattern if provided | ||||
|                     if !pattern.is_empty() && !name.contains(pattern) { | ||||
|                         continue; | ||||
|                     } | ||||
|                      | ||||
|                     processes.push(ProcessInfo { | ||||
|                         pid, | ||||
|                         name, | ||||
|                         memory: 0.0, // Placeholder | ||||
|                         cpu: 0.0,    // Placeholder | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             let stderr = String::from_utf8_lossy(&output.stderr).to_string(); | ||||
|             return Err(ProcessError::CommandFailed(format!("Failed to list processes: {}", stderr))); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     Ok(processes) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get a single process matching the pattern (error if 0 or more than 1 match). | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `pattern` - The pattern to match against process names | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(ProcessInfo)` - Information about the matching process | ||||
|  * * `Err(ProcessError)` - An error if no process or multiple processes match | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let process = process_get("unique-server-name")?; | ||||
|  * println!("Found process: {} (PID: {})", process.name, process.pid); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn process_get(pattern: &str) -> Result<ProcessInfo, ProcessError> { | ||||
|     let processes = process_list(pattern)?; | ||||
|      | ||||
|     match processes.len() { | ||||
|         0 => Err(ProcessError::NoProcessFound(pattern.to_string())), | ||||
|         1 => Ok(processes[0].clone()), | ||||
|         _ => Err(ProcessError::MultipleProcessesFound(pattern.to_string(), processes.len())), | ||||
|     } | ||||
| } | ||||
							
								
								
									
										2
									
								
								src/process/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/process/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| use run::*; | ||||
| use mgmt::*; | ||||
							
								
								
									
										493
									
								
								src/process/run.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										493
									
								
								src/process/run.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,493 @@ | ||||
| use std::io::{BufRead, BufReader, Write}; | ||||
| use std::fs::{self, File}; | ||||
| use std::path::{Path, PathBuf}; | ||||
| use std::process::{Child, Command, Output, Stdio}; | ||||
| use std::collections::HashMap; | ||||
| use std::fmt; | ||||
| use std::error::Error; | ||||
| use std::io; | ||||
|  | ||||
| use super::text; | ||||
|  | ||||
| // Define a custom error type for run operations | ||||
| #[derive(Debug)] | ||||
| pub enum RunError { | ||||
|     EmptyCommand, | ||||
|     CommandExecutionFailed(io::Error), | ||||
|     CommandFailed(String), | ||||
|     ScriptPreparationFailed(String), | ||||
|     ChildProcessError(String), | ||||
|     TempDirCreationFailed(io::Error), | ||||
|     FileCreationFailed(io::Error), | ||||
|     FileWriteFailed(io::Error), | ||||
|     PermissionError(io::Error), | ||||
| } | ||||
|  | ||||
| // Implement Display for RunError | ||||
| impl fmt::Display for RunError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             RunError::EmptyCommand => write!(f, "Empty command"), | ||||
|             RunError::CommandExecutionFailed(e) => write!(f, "Failed to execute command: {}", e), | ||||
|             RunError::CommandFailed(e) => write!(f, "{}", e), | ||||
|             RunError::ScriptPreparationFailed(e) => write!(f, "{}", e), | ||||
|             RunError::ChildProcessError(e) => write!(f, "{}", e), | ||||
|             RunError::TempDirCreationFailed(e) => write!(f, "Failed to create temporary directory: {}", e), | ||||
|             RunError::FileCreationFailed(e) => write!(f, "Failed to create script file: {}", e), | ||||
|             RunError::FileWriteFailed(e) => write!(f, "Failed to write to script file: {}", e), | ||||
|             RunError::PermissionError(e) => write!(f, "Failed to set file permissions: {}", e), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Implement Error trait for RunError | ||||
| impl Error for RunError { | ||||
|     fn source(&self) -> Option<&(dyn Error + 'static)> { | ||||
|         match self { | ||||
|             RunError::CommandExecutionFailed(e) => Some(e), | ||||
|             RunError::TempDirCreationFailed(e) => Some(e), | ||||
|             RunError::FileCreationFailed(e) => Some(e), | ||||
|             RunError::FileWriteFailed(e) => Some(e), | ||||
|             RunError::PermissionError(e) => Some(e), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A structure to hold command execution results | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct CommandResult { | ||||
|     pub stdout: String, | ||||
|     pub stderr: String, | ||||
|     pub success: bool, | ||||
|     pub code: i32, | ||||
| } | ||||
|  | ||||
| impl CommandResult { | ||||
|     /// Create a default failed result with an error message | ||||
|     fn error(message: &str) -> Self { | ||||
|         Self { | ||||
|             stdout: String::new(), | ||||
|             stderr: message.to_string(), | ||||
|             success: false, | ||||
|             code: -1, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Prepare a script file and return the path and interpreter | ||||
| fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfile::TempDir), RunError> { | ||||
|     // Dedent the script | ||||
|     let dedented = text::dedent(script_content); | ||||
|      | ||||
|     // Create a temporary directory | ||||
|     let temp_dir = tempfile::tempdir() | ||||
|         .map_err(RunError::TempDirCreationFailed)?; | ||||
|      | ||||
|     // Determine script extension and interpreter | ||||
|     #[cfg(target_os = "windows")] | ||||
|     let (ext, interpreter) = (".bat", "cmd.exe".to_string()); | ||||
|      | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     let (ext, interpreter) = (".sh", "/bin/sh".to_string()); | ||||
|      | ||||
|     // Create the script file | ||||
|     let script_path = temp_dir.path().join(format!("script{}", ext)); | ||||
|     let mut file = File::create(&script_path) | ||||
|         .map_err(RunError::FileCreationFailed)?; | ||||
|      | ||||
|     // Write the script content | ||||
|     file.write_all(dedented.as_bytes()) | ||||
|         .map_err(RunError::FileWriteFailed)?; | ||||
|      | ||||
|     // Make the script executable (Unix only) | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     { | ||||
|         use std::os::unix::fs::PermissionsExt; | ||||
|         let mut perms = fs::metadata(&script_path) | ||||
|             .map_err(|e| RunError::PermissionError(e))? | ||||
|             .permissions(); | ||||
|         perms.set_mode(0o755); // rwxr-xr-x | ||||
|         fs::set_permissions(&script_path, perms) | ||||
|             .map_err(RunError::PermissionError)?; | ||||
|     } | ||||
|      | ||||
|     Ok((script_path, interpreter, temp_dir)) | ||||
| } | ||||
|  | ||||
| /// Capture output from Child's stdio streams with optional printing | ||||
| fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult, RunError> { | ||||
|     // Prepare to read stdout & stderr line-by-line | ||||
|     let stdout = child.stdout.take(); | ||||
|     let stderr = child.stderr.take(); | ||||
|      | ||||
|     // Buffers for captured output | ||||
|     let mut captured_stdout = String::new(); | ||||
|     let mut captured_stderr = String::new(); | ||||
|      | ||||
|     // Process stdout | ||||
|     let stdout_handle = if let Some(out) = stdout { | ||||
|         let reader = BufReader::new(out); | ||||
|         let silent_clone = silent; | ||||
|         // Spawn a thread to capture and optionally print stdout | ||||
|         Some(std::thread::spawn(move || { | ||||
|             let mut local_buffer = String::new(); | ||||
|             for line in reader.lines() { | ||||
|                 if let Ok(l) = line { | ||||
|                     // Print the line if not silent and flush immediately | ||||
|                     if !silent_clone { | ||||
|                         println!("{}", l); | ||||
|                         std::io::stdout().flush().unwrap_or(()); | ||||
|                     } | ||||
|                     // Store it in our captured buffer | ||||
|                     local_buffer.push_str(&l); | ||||
|                     local_buffer.push('\n'); | ||||
|                 } | ||||
|             } | ||||
|             local_buffer | ||||
|         })) | ||||
|     } else { | ||||
|         None | ||||
|     }; | ||||
|      | ||||
|     // Process stderr | ||||
|     let stderr_handle = if let Some(err) = stderr { | ||||
|         let reader = BufReader::new(err); | ||||
|         let silent_clone = silent; | ||||
|         // Spawn a thread to capture and optionally print stderr | ||||
|         Some(std::thread::spawn(move || { | ||||
|             let mut local_buffer = String::new(); | ||||
|             for line in reader.lines() { | ||||
|                 if let Ok(l) = line { | ||||
|                     // Print the line if not silent and flush immediately | ||||
|                     if !silent_clone { | ||||
|                         eprintln!("{}", l); | ||||
|                         std::io::stderr().flush().unwrap_or(()); | ||||
|                     } | ||||
|                     // Store it in our captured buffer | ||||
|                     local_buffer.push_str(&l); | ||||
|                     local_buffer.push('\n'); | ||||
|                 } | ||||
|             } | ||||
|             local_buffer | ||||
|         })) | ||||
|     } else { | ||||
|         None | ||||
|     }; | ||||
|      | ||||
|     // Wait for the child process to exit | ||||
|     let status = child.wait() | ||||
|         .map_err(|e| RunError::ChildProcessError(format!("Failed to wait on child process: {}", e)))?; | ||||
|      | ||||
|     // Join our stdout thread if it exists | ||||
|     if let Some(handle) = stdout_handle { | ||||
|         captured_stdout = handle.join().unwrap_or_default(); | ||||
|     } else { | ||||
|         captured_stdout = "Failed to capture stdout".to_string(); | ||||
|     } | ||||
|      | ||||
|     // Join our stderr thread if it exists | ||||
|     if let Some(handle) = stderr_handle { | ||||
|         captured_stderr = handle.join().unwrap_or_default(); | ||||
|     } else { | ||||
|         captured_stderr = "Failed to capture stderr".to_string(); | ||||
|     } | ||||
|      | ||||
|     // Return the command result | ||||
|     Ok(CommandResult { | ||||
|         stdout: captured_stdout, | ||||
|         stderr: captured_stderr, | ||||
|         success: status.success(), | ||||
|         code: status.code().unwrap_or(-1), | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /// Processes Output structure from Command::output() into CommandResult | ||||
| fn process_command_output(output: Result<Output, std::io::Error>) -> Result<CommandResult, RunError> { | ||||
|     match output { | ||||
|         Ok(out) => { | ||||
|             let stdout = String::from_utf8_lossy(&out.stdout).to_string(); | ||||
|             let stderr = String::from_utf8_lossy(&out.stderr).to_string(); | ||||
|              | ||||
|             Ok(CommandResult { | ||||
|                 stdout, | ||||
|                 stderr, | ||||
|                 success: out.status.success(), | ||||
|                 code: out.status.code().unwrap_or(-1), | ||||
|             }) | ||||
|         }, | ||||
|         Err(e) => Err(RunError::CommandExecutionFailed(e)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Common logic for running a command with optional silent mode. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `command` - The command + args as a single string (e.g., "ls -la") | ||||
|  * * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the command execution | ||||
|  * * `Err(RunError)` - An error if the command execution failed | ||||
|  */ | ||||
| fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> { | ||||
|     let mut parts = command.split_whitespace(); | ||||
|     let cmd = match parts.next() { | ||||
|         Some(c) => c, | ||||
|         None => return Err(RunError::EmptyCommand), | ||||
|     }; | ||||
|  | ||||
|     let args: Vec<&str> = parts.collect(); | ||||
|  | ||||
|     // Spawn the child process with piped stdout & stderr | ||||
|     let child = Command::new(cmd) | ||||
|         .args(&args) | ||||
|         .stdout(Stdio::piped()) | ||||
|         .stderr(Stdio::piped()) | ||||
|         .spawn() | ||||
|         .map_err(RunError::CommandExecutionFailed)?; | ||||
|          | ||||
|     handle_child_output(child, silent) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Execute a script with the given interpreter and path. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `interpreter` - The interpreter to use (e.g., "/bin/sh") | ||||
|  * * `script_path` - The path to the script file | ||||
|  * * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the script execution | ||||
|  * * `Err(RunError)` - An error if the script execution failed | ||||
|  */ | ||||
| fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) -> Result<CommandResult, RunError> { | ||||
|     #[cfg(target_os = "windows")] | ||||
|     let command_args = vec!["/c", script_path.to_str().unwrap_or("")]; | ||||
|      | ||||
|     #[cfg(any(target_os = "macos", target_os = "linux"))] | ||||
|     let command_args = vec![script_path.to_str().unwrap_or("")]; | ||||
|      | ||||
|     if silent { | ||||
|         // For silent execution, use output() which captures but doesn't display | ||||
|         let output = Command::new(interpreter) | ||||
|             .args(&command_args) | ||||
|             .output(); | ||||
|              | ||||
|         process_command_output(output) | ||||
|     } else { | ||||
|         // For normal execution, spawn and handle the output streams | ||||
|         let child = Command::new(interpreter) | ||||
|             .args(&command_args) | ||||
|             .stdout(Stdio::piped()) | ||||
|             .stderr(Stdio::piped()) | ||||
|             .spawn() | ||||
|             .map_err(RunError::CommandExecutionFailed)?; | ||||
|              | ||||
|         handle_child_output(child, false) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Run a single command with arguments, showing live stdout and stderr. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `command` - The command + args as a single string (e.g., "ls -la") | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the command execution | ||||
|  * * `Err(RunError)` - An error if the command execution failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let result = run_command("ls -la")?; | ||||
|  * println!("Command exited with code: {}", result.code); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn run_command(command: &str) -> Result<CommandResult, RunError> { | ||||
|     run_command_internal(command, /* silent = */ false) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Run a single command with arguments silently. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `command` - The command + args as a single string (e.g., "ls -la") | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the command execution | ||||
|  * * `Err(RunError)` - An error if the command execution failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let result = run_command_silent("ls -la")?; | ||||
|  * println!("Command output: {}", result.stdout); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn run_command_silent(command: &str) -> Result<CommandResult, RunError> { | ||||
|     run_command_internal(command, /* silent = */ true) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Run a multiline script with optional silent mode. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `script` - The script content as a string | ||||
|  * * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only) | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the script execution | ||||
|  * * `Err(RunError)` - An error if the script execution failed | ||||
|  */ | ||||
| fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> { | ||||
|     let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?; | ||||
|     // _temp_dir is kept in scope until the end of this function to ensure | ||||
|     // it's not dropped prematurely, which would clean up the directory | ||||
|     execute_script_internal(&interpreter, &script_path, silent) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Run a multiline script by saving it to a temporary file and executing. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `script` - The script content as a string | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the script execution | ||||
|  * * `Err(RunError)` - An error if the script execution failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let script = r#" | ||||
|  *     echo "Hello, world!" | ||||
|  *     ls -la | ||||
|  * "#; | ||||
|  * let result = run_script(script)?; | ||||
|  * println!("Script exited with code: {}", result.code); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn run_script(script: &str) -> Result<CommandResult, RunError> { | ||||
|     run_script_internal(script, false) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Run a multiline script silently by saving it to a temporary file and executing. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `script` - The script content as a string | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the script execution | ||||
|  * * `Err(RunError)` - An error if the script execution failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let script = r#" | ||||
|  *     echo "Hello, world!" | ||||
|  *     ls -la | ||||
|  * "#; | ||||
|  * let result = run_script_silent(script)?; | ||||
|  * println!("Script output: {}", result.stdout); | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn run_script_silent(script: &str) -> Result<CommandResult, RunError> { | ||||
|     run_script_internal(script, true) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Run a command or multiline script with arguments. | ||||
|  * Shows stdout/stderr as it arrives. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `command` - The command or script to run | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the execution | ||||
|  * * `Err(RunError)` - An error if the execution failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // Run a single command | ||||
|  * let result = run("ls -la")?; | ||||
|  *  | ||||
|  * // Run a multiline script | ||||
|  * let result = run(r#" | ||||
|  *     echo "Hello, world!" | ||||
|  *     ls -la | ||||
|  * "#)?; | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn run(command: &str) -> Result<CommandResult, RunError> { | ||||
|     let trimmed = command.trim(); | ||||
|      | ||||
|     // Check if this is a multiline script | ||||
|     if trimmed.contains('\n') { | ||||
|         // This is a multiline script, write to a temporary file and execute | ||||
|         run_script(trimmed) | ||||
|     } else { | ||||
|         // This is a single command with arguments | ||||
|         run_command(trimmed) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Run a command or multiline script with arguments silently. | ||||
|  * Doesn't show stdout/stderr as it arrives. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `command` - The command or script to run | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `Ok(CommandResult)` - The result of the execution | ||||
|  * * `Err(RunError)` - An error if the execution failed | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * // Run a single command silently | ||||
|  * let result = run_silent("ls -la")?; | ||||
|  *  | ||||
|  * // Run a multiline script silently | ||||
|  * let result = run_silent(r#" | ||||
|  *     echo "Hello, world!" | ||||
|  *     ls -la | ||||
|  * "#)?; | ||||
|  * ``` | ||||
|  */ | ||||
| pub fn run_silent(command: &str) -> Result<CommandResult, RunError> { | ||||
|     let trimmed = command.trim(); | ||||
|      | ||||
|     // Check if this is a multiline script | ||||
|     if trimmed.contains('\n') { | ||||
|         // This is a multiline script, write to a temporary file and execute | ||||
|         run_script_silent(trimmed) | ||||
|     } else { | ||||
|         // This is a single command with arguments | ||||
|         run_command_silent(trimmed) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/text/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/text/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| use text::*; | ||||
							
								
								
									
										83
									
								
								src/text/text.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/text/text.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| /** | ||||
|  * Dedent a multiline string by removing common leading whitespace. | ||||
|  *  | ||||
|  * This function analyzes all non-empty lines in the input text to determine | ||||
|  * the minimum indentation level, then removes that amount of whitespace | ||||
|  * from the beginning of each line. This is useful for working with | ||||
|  * multi-line strings in code that have been indented to match the | ||||
|  * surrounding code structure. | ||||
|  *  | ||||
|  * # Arguments | ||||
|  *  | ||||
|  * * `text` - The multiline string to dedent | ||||
|  *  | ||||
|  * # Returns | ||||
|  *  | ||||
|  * * `String` - The dedented string | ||||
|  *  | ||||
|  * # Examples | ||||
|  *  | ||||
|  * ``` | ||||
|  * let indented = "    line 1\n    line 2\n        line 3"; | ||||
|  * let dedented = dedent(indented); | ||||
|  * assert_eq!(dedented, "line 1\nline 2\n    line 3"); | ||||
|  * ``` | ||||
|  *  | ||||
|  * # Notes | ||||
|  *  | ||||
|  * - Empty lines are preserved but have all leading whitespace removed | ||||
|  * - Tabs are counted as 4 spaces for indentation purposes | ||||
|  */ | ||||
| pub fn dedent(text: &str) -> String { | ||||
|     let lines: Vec<&str> = text.lines().collect(); | ||||
|  | ||||
|     // Find the minimum indentation level (ignore empty lines) | ||||
|     let min_indent = lines.iter() | ||||
|         .filter(|line| !line.trim().is_empty()) | ||||
|         .map(|line| { | ||||
|             let mut spaces = 0; | ||||
|             for c in line.chars() { | ||||
|                 if c == ' ' { | ||||
|                     spaces += 1; | ||||
|                 } else if c == '\t' { | ||||
|                     spaces += 4; // Count tabs as 4 spaces | ||||
|                 } else { | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             spaces | ||||
|         }) | ||||
|         .min() | ||||
|         .unwrap_or(0); | ||||
|  | ||||
|     // Remove that many spaces from the beginning of each line | ||||
|     lines.iter() | ||||
|         .map(|line| { | ||||
|             if line.trim().is_empty() { | ||||
|                 return String::new(); | ||||
|             } | ||||
|  | ||||
|             let mut count = 0; | ||||
|             let mut chars = line.chars().peekable(); | ||||
|              | ||||
|             // Skip initial spaces up to min_indent | ||||
|             while count < min_indent && chars.peek().is_some() { | ||||
|                 match chars.peek() { | ||||
|                     Some(' ') => { | ||||
|                         chars.next(); | ||||
|                         count += 1; | ||||
|                     }, | ||||
|                     Some('\t') => { | ||||
|                         chars.next(); | ||||
|                         count += 4; | ||||
|                     }, | ||||
|                     _ => break, | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             // Return the remaining characters | ||||
|             chars.collect::<String>() | ||||
|         }) | ||||
|         .collect::<Vec<String>>() | ||||
|         .join("\n") | ||||
| } | ||||
		Reference in New Issue
	
	Block a user