//! Setup and dependency checking for the makima daemon. //! //! This module provides functionality to check for required dependencies //! (Claude Code, git, npm) when the daemon starts, and optionally install //! missing dependencies. 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 all dependencies #[derive(Debug)] pub struct DependencyCheckResult { pub claude: DependencyInfo, pub git: DependencyInfo, pub npm: 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 npm is available (for installing claude) pub fn npm_available(&self) -> bool { self.npm.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, npm_version) = tokio::join!( get_command_version("claude", &["--version"]), get_command_version("git", &["--version"]), get_command_version("npm", &["--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 npm = DependencyInfo::new("npm", false); npm.version = npm_version; npm.installed = npm.version.is_some(); DependencyCheckResult { claude, git, npm } } /// 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)); eprintln!("{}", format_dep(&result.npm)); } /// 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, npm_available: bool) { eprintln!(); eprintln!("{}{}Claude Code is required to run the makima daemon.{}", colors::BOLD, colors::RED, colors::RESET); eprintln!(); if npm_available { eprintln!("Install with npm:"); eprintln!(" {}npm install -g @anthropic-ai/claude-code{}", colors::BOLD, colors::RESET); } else { eprintln!("To install Claude Code, you first need npm (Node.js package manager)."); eprintln!(); match os { OperatingSystem::MacOS => { eprintln!("Install Node.js on macOS:"); eprintln!(" {}brew install node{}", colors::BOLD, colors::RESET); eprintln!(" Or download from: https://nodejs.org/"); } OperatingSystem::Linux => { eprintln!("Install Node.js on Linux:"); eprintln!(" {}curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -{}", colors::BOLD, colors::RESET); eprintln!(" {}sudo apt-get install -y nodejs{}", colors::BOLD, colors::RESET); eprintln!(" Or use your distribution's package manager"); } OperatingSystem::Windows => { eprintln!("Install Node.js on Windows:"); eprintln!(" Download from: https://nodejs.org/"); } OperatingSystem::Unknown => { eprintln!("Install Node.js from: https://nodejs.org/"); } } eprintln!(); eprintln!("Then install Claude Code:"); eprintln!(" {}npm install -g @anthropic-ai/claude-code{}", colors::BOLD, colors::RESET); } 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"); } } } /// Try to install Claude Code using npm pub async fn install_claude_with_npm() -> Result<(), String> { eprintln!("Installing Claude Code via npm..."); let result = Command::new("npm") .args(["install", "-g", "@anthropic-ai/claude-code"]) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .status() .await; match result { Ok(status) if status.success() => { eprintln!("{}Claude Code installed successfully!{}", colors::GREEN, colors::RESET); Ok(()) } Ok(status) => { Err(format!("npm install failed with exit code: {:?}", status.code())) } Err(e) => { Err(format!("Failed to run npm: {}", e)) } } } /// Read a single character from stdin (for y/n prompts) pub fn read_yes_no(prompt: &str) -> bool { use std::io::{self, Write}; eprint!("{} [y/N]: ", prompt); io::stderr().flush().ok(); let mut input = String::new(); if io::stdin().read_line(&mut input).is_ok() { let input = input.trim().to_lowercase(); input == "y" || input == "yes" } else { false } } #[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)); } }