//! Setup and dependency checking for the makima daemon. //! //! This module provides functionality to check for required dependencies //! (Claude Code, git) and optional tools (gh) when the daemon starts, //! and verify git authentication capabilities. use std::process::Stdio; use tokio::process::Command; /// ANSI color codes for terminal output mod colors { pub const RED: &str = "\x1b[31m"; pub const GREEN: &str = "\x1b[32m"; pub const YELLOW: &str = "\x1b[33m"; pub const RESET: &str = "\x1b[0m"; pub const BOLD: &str = "\x1b[1m"; } /// Information about a dependency's installation status #[derive(Debug, Clone)] pub struct DependencyInfo { pub name: &'static str, pub version: Option, pub installed: bool, pub critical: bool, } impl DependencyInfo { fn new(name: &'static str, critical: bool) -> Self { Self { name, version: None, installed: false, critical, } } } /// Result of checking git authentication against remote origins #[derive(Debug, Clone)] pub struct GitAuthCheckResult { /// Whether basic git credential helper is configured pub credential_helper_configured: bool, /// Results of testing authentication against specific origins pub origin_results: Vec, } #[derive(Debug, Clone)] pub struct GitOriginAuthResult { pub origin_url: String, pub can_authenticate: bool, pub error: Option, } /// Result of checking all dependencies #[derive(Debug)] pub struct DependencyCheckResult { pub claude: DependencyInfo, pub git: DependencyInfo, pub git_auth: GitAuthCheckResult, pub gh: DependencyInfo, } impl DependencyCheckResult { /// Check if all critical dependencies are installed pub fn all_critical_installed(&self) -> bool { (!self.claude.critical || self.claude.installed) && (!self.git.critical || self.git.installed) } /// Check if gh (GitHub CLI) is available pub fn gh_available(&self) -> bool { self.gh.installed } } /// Check the version of a command by running it with --version async fn get_command_version(command: &str, args: &[&str]) -> Option { let result = Command::new(command) .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await; match result { Ok(output) if output.status.success() => { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); // Parse version from output (different tools output differently) let combined = format!("{}{}", stdout, stderr); extract_version(&combined) } _ => None, } } /// Extract a version number from command output fn extract_version(output: &str) -> Option { // Look for common version patterns // e.g., "git version 2.40.0", "v1.2.3", "npm 10.0.0" let output = output.trim(); // Try to find version number pattern (digits separated by dots) for word in output.split_whitespace() { let word = word.trim_start_matches('v'); if word.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) { // Check if it looks like a version number let parts: Vec<&str> = word.split('.').collect(); if parts.len() >= 1 && parts.iter().all(|p| { let p = p.split('-').next().unwrap_or(""); p.chars().all(|c| c.is_ascii_digit()) }) { // Clean up any trailing non-version chars let clean_version = word .chars() .take_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-') .collect::(); if !clean_version.is_empty() { return Some(clean_version); } } } } // If we couldn't parse a version but got output, just return "installed" if !output.is_empty() { Some("installed".to_string()) } else { None } } /// Check all dependencies and return their status pub async fn check_dependencies() -> DependencyCheckResult { // Run all checks in parallel for speed let (claude_version, git_version, gh_version) = tokio::join!( get_command_version("claude", &["--version"]), get_command_version("git", &["--version"]), get_command_version("gh", &["--version"]), ); let mut claude = DependencyInfo::new("Claude Code", true); claude.version = claude_version; claude.installed = claude.version.is_some(); let mut git = DependencyInfo::new("git", true); git.version = git_version; git.installed = git.version.is_some(); let mut gh = DependencyInfo::new("gh (GitHub CLI)", false); gh.version = gh_version; gh.installed = gh.version.is_some(); // Check git authentication (only if git is installed) let git_auth = if git.installed { check_git_auth().await } else { GitAuthCheckResult { credential_helper_configured: false, origin_results: vec![], } }; DependencyCheckResult { claude, git, git_auth, gh } } /// Display a nice summary of dependency status pub fn print_dependency_summary(result: &DependencyCheckResult) { fn format_dep(dep: &DependencyInfo) -> String { if dep.installed { let version = dep.version.as_deref().unwrap_or("installed"); format!( " {}: {} {}{}", dep.name, version, colors::GREEN, colors::RESET ) } else { let status = if dep.critical { "MISSING" } else { "not found" }; let color = if dep.critical { colors::RED } else { colors::YELLOW }; format!( " {}: {}{}{} {}", dep.name, color, status, colors::RESET, if dep.critical { "(required)" } else { "(optional)" } ) } } eprintln!("{}", format_dep(&result.claude)); eprintln!("{}", format_dep(&result.git)); // Print git auth status if result.git.installed { if result.git_auth.credential_helper_configured { eprintln!(" git credentials: {}configured{}", colors::GREEN, colors::RESET); } else { eprintln!(" git credentials: {}no credential helper configured{} (git may prompt for passwords)", colors::YELLOW, colors::RESET); } } // Print origin auth results if any for origin in &result.git_auth.origin_results { if origin.can_authenticate { eprintln!(" git origin {}: {}ok{}", origin.origin_url, colors::GREEN, colors::RESET); } else { let err_msg = origin.error.as_deref().unwrap_or("authentication failed"); let err_msg = if err_msg.len() > 80 { &err_msg[..80] } else { err_msg }; eprintln!(" git origin {}: {}failed{} ({})", origin.origin_url, colors::RED, colors::RESET, err_msg.trim()); } } eprintln!("{}", format_dep(&result.gh)); } /// Detect the current operating system #[derive(Debug, Clone, Copy, PartialEq)] pub enum OperatingSystem { MacOS, Linux, Windows, Unknown, } impl OperatingSystem { pub fn detect() -> Self { #[cfg(target_os = "macos")] return OperatingSystem::MacOS; #[cfg(target_os = "linux")] return OperatingSystem::Linux; #[cfg(target_os = "windows")] return OperatingSystem::Windows; #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] return OperatingSystem::Unknown; } } /// Print instructions for installing Claude Code pub fn print_claude_install_instructions(os: OperatingSystem) { eprintln!(); eprintln!("{}{}Claude Code is required to run the makima daemon.{}", colors::BOLD, colors::RED, colors::RESET); eprintln!(); eprintln!("Install Claude Code:"); match os { OperatingSystem::MacOS => { eprintln!(" {}brew install claude-code{}", colors::BOLD, colors::RESET); eprintln!(" Or visit: https://docs.anthropic.com/en/docs/claude-code"); } _ => { eprintln!(" Visit: https://docs.anthropic.com/en/docs/claude-code"); } } eprintln!(); eprintln!("For more information, visit: https://docs.anthropic.com/en/docs/claude-code"); } /// Print instructions for installing git pub fn print_git_install_instructions(os: OperatingSystem) { eprintln!(); eprintln!("{}{}git is required to run the makima daemon.{}", colors::BOLD, colors::RED, colors::RESET); eprintln!(); match os { OperatingSystem::MacOS => { eprintln!("Install git on macOS:"); eprintln!(" {}xcode-select --install{}", colors::BOLD, colors::RESET); eprintln!(" Or: {}brew install git{}", colors::BOLD, colors::RESET); } OperatingSystem::Linux => { eprintln!("Install git on Linux:"); eprintln!(" Debian/Ubuntu: {}sudo apt-get install git{}", colors::BOLD, colors::RESET); eprintln!(" Fedora: {}sudo dnf install git{}", colors::BOLD, colors::RESET); eprintln!(" Arch: {}sudo pacman -S git{}", colors::BOLD, colors::RESET); } OperatingSystem::Windows => { eprintln!("Install git on Windows:"); eprintln!(" Download from: https://git-scm.com/download/win"); } OperatingSystem::Unknown => { eprintln!("Install git from: https://git-scm.com/downloads"); } } } /// Check git authentication capabilities async fn check_git_auth() -> GitAuthCheckResult { // Check if a credential helper is configured let cred_helper = Command::new("git") .args(["config", "credential.helper"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await; let credential_helper_configured = match cred_helper { Ok(output) => { output.status.success() && !String::from_utf8_lossy(&output.stdout).trim().is_empty() } _ => false, }; GitAuthCheckResult { credential_helper_configured, origin_results: vec![], } } /// Test git authentication against a specific remote URL. /// Uses `git ls-remote --exit-code ` with a timeout to verify access. pub async fn check_git_origin_auth(url: &str) -> GitOriginAuthResult { let result = tokio::time::timeout( std::time::Duration::from_secs(15), Command::new("git") .args(["ls-remote", "--exit-code", url, "HEAD"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() ).await; match result { Ok(Ok(output)) if output.status.success() => { GitOriginAuthResult { origin_url: url.to_string(), can_authenticate: true, error: None, } } Ok(Ok(output)) => { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); GitOriginAuthResult { origin_url: url.to_string(), can_authenticate: false, error: Some(stderr), } } Ok(Err(e)) => { GitOriginAuthResult { origin_url: url.to_string(), can_authenticate: false, error: Some(format!("Failed to run git: {}", e)), } } Err(_) => { GitOriginAuthResult { origin_url: url.to_string(), can_authenticate: false, error: Some("Timed out after 15 seconds".to_string()), } } } } /// Print warnings about git authentication issues (non-fatal) pub fn print_git_auth_warnings(result: &DependencyCheckResult) { if !result.git_auth.credential_helper_configured && result.git.installed { eprintln!(); eprintln!(" {}WARNING:{} No git credential helper configured.", colors::YELLOW, colors::RESET); eprintln!(" Git operations may prompt for passwords interactively."); eprintln!(" Configure a credential helper: git config --global credential.helper store"); } for origin in &result.git_auth.origin_results { if !origin.can_authenticate { eprintln!(); eprintln!(" {}WARNING:{} Cannot authenticate with git origin: {}", colors::YELLOW, colors::RESET, origin.origin_url); if let Some(ref err) = origin.error { let err_trimmed = err.trim(); if !err_trimmed.is_empty() { eprintln!(" Error: {}", if err_trimmed.len() > 120 { &err_trimmed[..120] } else { err_trimmed }); } } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_version() { assert_eq!(extract_version("git version 2.40.0"), Some("2.40.0".to_string())); assert_eq!(extract_version("v1.2.3"), Some("1.2.3".to_string())); assert_eq!(extract_version("10.0.0"), Some("10.0.0".to_string())); assert_eq!(extract_version("npm 10.2.4"), Some("10.2.4".to_string())); } #[test] fn test_os_detection() { let os = OperatingSystem::detect(); // Just verify it doesn't panic assert!(matches!(os, OperatingSystem::MacOS | OperatingSystem::Linux | OperatingSystem::Windows | OperatingSystem::Unknown)); } }