//! 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<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 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<GitOriginAuthResult>,
}
#[derive(Debug, Clone)]
pub struct GitOriginAuthResult {
pub origin_url: String,
pub can_authenticate: bool,
pub error: Option<String>,
}
/// 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<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, 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 <url>` 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));
}
}