summaryrefslogblamecommitdiff
path: root/makima/src/daemon/setup.rs
blob: 0706972182013573a8ca06331ddee0cf11083888 (plain) (tree)




































































































































































































































































































































                                                                                                                                     
//! 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<String>,
    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<String> {
    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<String> {
    // 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::<String>();
                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));
    }
}