//! Shell I/O — the ONLY place in `service::shell/` that may perform side effects. //! //! Side effects here include: filesystem existence and canonicalization checks, //! process spawning via `std::process::Command`, and reading pipe output. //! All pure logic (pattern matching, output truncation, count parsing) lives in //! `path_guard.rs`. use super::Error; use std::path::{Path, PathBuf}; /// Validate that `working_dir` is an absolute path that exists on disk and /// lies inside the project's `.huskies/worktrees/` or `.huskies/merge_workspace/` /// directory. Returns the canonicalized path on success. /// /// # Errors /// - [`Error::Validation`] if the path is relative or does not exist. /// - [`Error::PathNotAllowed`] if the path is outside the allowed roots. /// - [`Error::Io`] if canonicalization fails. pub fn validate_working_dir(working_dir: &str, project_root: &Path) -> Result { let wd = PathBuf::from(working_dir); if !wd.is_absolute() { return Err(Error::Validation( "working_dir must be an absolute path".to_string(), )); } if !wd.exists() { return Err(Error::Validation(format!( "working_dir does not exist: {working_dir}" ))); } let worktrees_root = project_root.join(".huskies").join("worktrees"); let canonical_wd = wd .canonicalize() .map_err(|e| Error::Io(format!("Cannot canonicalize working_dir: {e}")))?; let canonical_wt = if worktrees_root.exists() { worktrees_root .canonicalize() .map_err(|e| Error::Io(format!("Cannot canonicalize worktrees root: {e}")))? } else { return Err(Error::PathNotAllowed( "No worktrees directory found in project".to_string(), )); }; // Also allow the merge workspace so mergemaster can fix conflicts. let merge_workspace = project_root.join(".huskies").join("merge_workspace"); let canonical_mw = merge_workspace.canonicalize().unwrap_or_default(); let in_worktrees = canonical_wd.starts_with(&canonical_wt); let in_merge_ws = !canonical_mw.as_os_str().is_empty() && canonical_wd.starts_with(&canonical_mw); if !in_worktrees && !in_merge_ws { return Err(Error::PathNotAllowed(format!( "working_dir must be inside .huskies/worktrees/ or .huskies/merge_workspace/. Got: {working_dir}" ))); } Ok(canonical_wd) }