summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-21 23:50:23 +0000
committersoryu <soryu@soryu.co>2026-01-21 23:50:23 +0000
commit7c5ff74616d23a4e35fb7f84d5292fd90e6cd7a8 (patch)
treed496267b1c18e15a1c8413f32abdbb5be2b9d618
parent9e286c146e29e714b3b209b4d948d75cce179b05 (diff)
downloadsoryu-makima/task-task-dc119baa-dc119baa.tar.gz
soryu-makima/task-task-dc119baa-dc119baa.zip
Add dependency checking on daemon startupmakima/task-task-dc119baa-dc119baa
- 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.rs50
-rw-r--r--makima/src/daemon/cli/daemon.rs5
-rw-r--r--makima/src/daemon/mod.rs1
-rw-r--r--makima/src/daemon/setup.rs325
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));
+ }
+}