//! 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));
}
}