diff options
| author | soryu <soryu@soryu.co> | 2026-01-22 01:32:19 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-22 01:32:19 +0000 |
| commit | f7c08980d2a161a3739c71fddc5ca20871892365 (patch) | |
| tree | d517c4399edf09c3b7fe957c27d852b052f6ec31 | |
| parent | b61a907bac09a7649ca3f6d850e771b3b75c7015 (diff) | |
| download | soryu-f7c08980d2a161a3739c71fddc5ca20871892365.tar.gz soryu-f7c08980d2a161a3739c71fddc5ca20871892365.zip | |
Add dependency checking on daemon startup (#19)
- Create setup module with check_dependencies() to verify Claude Code,
git, and npm are installed
- Add colored status output showing version info for each dependency
- If Claude Code is missing and npm is available, offer to install it
- Show OS-specific installation instructions for missing dependencies
- Add --skip-setup-check flag to DaemonArgs for CI/CD environments
- Check runs as step [0/5] before configuration loading
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | makima/src/bin/makima.rs | 50 | ||||
| -rw-r--r-- | makima/src/daemon/cli/daemon.rs | 5 | ||||
| -rw-r--r-- | makima/src/daemon/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/daemon/setup.rs | 325 |
4 files changed, 381 insertions, 0 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 67eefc6..3be6003 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -12,6 +12,7 @@ use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, Ws use makima::daemon::config::{DaemonConfig, RepoEntry}; use makima::daemon::db::LocalDb; use makima::daemon::error::DaemonError; +use makima::daemon::setup; use makima::daemon::task::{TaskConfig, TaskManager}; use makima::daemon::ws::{DaemonCommand, WsClient}; use tokio::process::Command; @@ -72,6 +73,54 @@ async fn run_daemon( ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { eprintln!("=== Makima Daemon Starting ==="); + // Check dependencies unless skipped + if !args.skip_setup_check { + eprintln!("[0/5] Checking dependencies..."); + let dep_result = setup::check_dependencies().await; + setup::print_dependency_summary(&dep_result); + + // Check for missing critical dependencies + if !dep_result.claude.installed { + let os = setup::OperatingSystem::detect(); + + // If npm is available, offer to install Claude Code + if dep_result.npm_available() { + eprintln!(); + if setup::read_yes_no("Would you like to install Claude Code now?") { + match setup::install_claude_with_npm().await { + Ok(()) => { + eprintln!(); + // Re-check to verify installation + let recheck = setup::check_dependencies().await; + if !recheck.claude.installed { + eprintln!("\x1b[31mClaude Code installation could not be verified.\x1b[0m"); + eprintln!("Please try installing manually and restart the daemon."); + std::process::exit(1); + } + } + Err(e) => { + eprintln!("\x1b[31mFailed to install Claude Code: {}\x1b[0m", e); + setup::print_claude_install_instructions(os, true); + std::process::exit(1); + } + } + } else { + setup::print_claude_install_instructions(os, true); + std::process::exit(1); + } + } else { + setup::print_claude_install_instructions(os, false); + std::process::exit(1); + } + } + + if !dep_result.git.installed { + let os = setup::OperatingSystem::detect(); + setup::print_git_install_instructions(os); + std::process::exit(1); + } + } + // Build a temporary CLI struct for config loading let cli = makima::daemon::cli::daemon::DaemonArgs { config: args.config, @@ -82,6 +131,7 @@ async fn run_daemon( max_tasks: args.max_tasks, log_level: args.log_level, bubblewrap: args.bubblewrap, + skip_setup_check: args.skip_setup_check, }; // Load configuration with CLI overrides diff --git a/makima/src/daemon/cli/daemon.rs b/makima/src/daemon/cli/daemon.rs index c779d64..a8c0b4a 100644 --- a/makima/src/daemon/cli/daemon.rs +++ b/makima/src/daemon/cli/daemon.rs @@ -38,4 +38,9 @@ pub struct DaemonArgs { /// Requires bwrap to be installed on the system. #[arg(long, env = "MAKIMA_DAEMON_BUBBLEWRAP")] pub bubblewrap: bool, + + /// Skip dependency checks on startup. + /// Useful for CI/CD or when you know dependencies are installed. + #[arg(long, env = "MAKIMA_DAEMON_SKIP_SETUP_CHECK")] + pub skip_setup_check: bool, } diff --git a/makima/src/daemon/mod.rs b/makima/src/daemon/mod.rs index 349a769..18b5e8a 100644 --- a/makima/src/daemon/mod.rs +++ b/makima/src/daemon/mod.rs @@ -13,6 +13,7 @@ pub mod config; pub mod db; pub mod error; pub mod process; +pub mod setup; pub mod task; pub mod temp; pub mod tui; diff --git a/makima/src/daemon/setup.rs b/makima/src/daemon/setup.rs new file mode 100644 index 0000000..0706972 --- /dev/null +++ b/makima/src/daemon/setup.rs @@ -0,0 +1,325 @@ +//! 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)); + } +} |
