//! Makima Daemon - Git worktree orchestration for Claude Code.
use std::sync::Arc;
use std::path::Path;
use clap::Parser;
use makima_daemon::cli::Cli;
use makima_daemon::config::{DaemonConfig, RepoEntry};
use makima_daemon::db::LocalDb;
use makima_daemon::error::DaemonError;
use makima_daemon::task::{TaskConfig, TaskManager};
use makima_daemon::ws::{DaemonCommand, WsClient};
use tokio::process::Command;
use tokio::sync::mpsc;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
eprintln!("=== Makima Daemon Starting ===");
// Parse command-line arguments
let cli = Cli::parse();
// Load configuration with CLI overrides
eprintln!("[1/5] Loading configuration...");
let config = match DaemonConfig::load_with_cli(&cli) {
Ok(cfg) => {
eprintln!(" Config loaded: server={}", cfg.server.url);
cfg
}
Err(e) => {
eprintln!("Failed to load configuration: {}", e);
eprintln!();
eprintln!("Use CLI flags:");
eprintln!(" makima-daemon --server-url ws://localhost:8080 --api-key your-api-key");
eprintln!();
eprintln!("Or set environment variables:");
eprintln!(" MAKIMA_DAEMON_SERVER_URL=ws://localhost:8080");
eprintln!(" MAKIMA_API_KEY=your-api-key");
eprintln!();
eprintln!("Or create a config file: makima-daemon.toml");
eprintln!();
eprintln!("Run 'makima-daemon --help' for all options.");
std::process::exit(1);
}
};
// Initialize logging
init_logging(&config.logging.level, &config.logging.format);
eprintln!("[2/5] Logging initialized");
// Initialize local database
eprintln!("[3/5] Opening local database: {}", config.local_db.path.display());
let _local_db = LocalDb::open(&config.local_db.path)?;
eprintln!(" Database opened");
// Initialize worktree directories
eprintln!("[4/5] Setting up directories...");
tokio::fs::create_dir_all(&config.worktree.base_dir).await?;
tokio::fs::create_dir_all(&config.worktree.repos_dir).await?;
tokio::fs::create_dir_all(&config.repos.home_dir).await?;
eprintln!(" Worktree base: {}", config.worktree.base_dir.display());
eprintln!(" Repos cache: {}", config.worktree.repos_dir.display());
eprintln!(" Home dir: {}", config.repos.home_dir.display());
// Auto-clone repositories if configured
if !config.repos.auto_clone.is_empty() {
eprintln!(" Auto-cloning {} repositories...", config.repos.auto_clone.len());
for repo_entry in &config.repos.auto_clone {
if let Err(e) = auto_clone_repo(repo_entry, &config.repos.home_dir).await {
eprintln!(" WARNING: Failed to clone {}: {}", repo_entry.url(), e);
}
}
}
// Create channels for communication
let (command_tx, mut command_rx) = mpsc::channel::<DaemonCommand>(64);
// Get machine info
let machine_id = get_machine_id();
let hostname = get_hostname();
eprintln!(" Machine ID: {}", machine_id);
eprintln!(" Hostname: {}", hostname);
// Create WebSocket client
eprintln!("[5/5] Connecting to server: {}", config.server.url);
let mut ws_client = WsClient::new(
config.server.clone(),
machine_id,
hostname,
config.process.max_concurrent_tasks as i32,
command_tx,
);
// Get sender for task manager
let ws_tx = ws_client.sender();
// Create task configuration
let task_config = TaskConfig {
max_concurrent_tasks: config.process.max_concurrent_tasks,
worktree_base_dir: config.worktree.base_dir.clone(),
env_vars: config.process.env_vars.clone(),
claude_command: config.process.claude_command.clone(),
claude_args: config.process.claude_args.clone(),
claude_pre_args: config.process.claude_pre_args.clone(),
enable_permissions: config.process.enable_permissions,
disable_verbose: config.process.disable_verbose,
};
// Create task manager
let task_manager = Arc::new(TaskManager::new(task_config, ws_tx.clone()));
// Spawn command handler
let task_manager_clone = task_manager.clone();
tokio::spawn(async move {
tracing::info!("Command handler started, waiting for commands...");
while let Some(command) = command_rx.recv().await {
tracing::info!("Received command from channel: {:?}", command);
if let Err(e) = task_manager_clone.handle_command(command).await {
tracing::error!("Failed to handle command: {}", e);
}
}
tracing::info!("Command handler stopped");
});
// Handle shutdown signals
let shutdown_signal = async {
tokio::signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
eprintln!("\nReceived shutdown signal");
};
eprintln!("=== Daemon running (Ctrl+C to stop) ===");
// Run WebSocket client with shutdown handling
tokio::select! {
result = ws_client.run() => {
match result {
Ok(()) => eprintln!("WebSocket client exited cleanly"),
Err(DaemonError::AuthFailed(msg)) => {
eprintln!("ERROR: Authentication failed: {}", msg);
std::process::exit(1);
}
Err(e) => {
eprintln!("ERROR: WebSocket client error: {}", e);
std::process::exit(1);
}
}
}
_ = shutdown_signal => {
eprintln!("Shutting down...");
}
}
// Cleanup
tracing::info!("Daemon stopped");
Ok(())
}
fn init_logging(level: &str, format: &str) {
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(level))
.unwrap_or_else(|_| EnvFilter::new("info"));
let subscriber = tracing_subscriber::registry().with(filter);
if format == "json" {
subscriber.with(fmt::layer().json()).init();
} else {
subscriber.with(fmt::layer()).init();
}
}
fn get_machine_id() -> String {
// Try to read machine-id from standard locations
#[cfg(target_os = "linux")]
{
if let Ok(id) = std::fs::read_to_string("/etc/machine-id") {
return id.trim().to_string();
}
if let Ok(id) = std::fs::read_to_string("/var/lib/dbus/machine-id") {
return id.trim().to_string();
}
}
#[cfg(target_os = "macos")]
{
// Use IOPlatformSerialNumber
if let Ok(output) = std::process::Command::new("ioreg")
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("IOPlatformUUID") {
if let Some(uuid) = line.split('"').nth(3) {
return uuid.to_string();
}
}
}
}
}
// Fallback: generate a random ID and persist it
let state_dir = dirs_next::data_local_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("makima-daemon");
let machine_id_file = state_dir.join("machine-id");
if let Ok(id) = std::fs::read_to_string(&machine_id_file) {
return id.trim().to_string();
}
// Generate new ID
let new_id = uuid::Uuid::new_v4().to_string();
std::fs::create_dir_all(&state_dir).ok();
std::fs::write(&machine_id_file, &new_id).ok();
new_id
}
fn get_hostname() -> String {
hostname::get()
.map(|h| h.to_string_lossy().to_string())
.unwrap_or_else(|_| "unknown".to_string())
}
/// Auto-clone a repository to the home directory if it doesn't exist.
async fn auto_clone_repo(
repo_entry: &RepoEntry,
home_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let dir_name = repo_entry
.dir_name()
.ok_or("Could not determine directory name from URL")?;
let target_dir = home_dir.join(&dir_name);
// Check if already cloned
if target_dir.exists() {
eprintln!(" [skip] {} (already exists)", dir_name);
return Ok(());
}
let url = repo_entry.expanded_url();
eprintln!(" [clone] {} -> {}", url, target_dir.display());
// Build git clone command
let mut args = vec!["clone".to_string()];
// Add shallow clone if requested
if repo_entry.shallow() {
args.push("--depth".to_string());
args.push("1".to_string());
}
// Add branch if specified
if let Some(branch) = repo_entry.branch() {
args.push("--branch".to_string());
args.push(branch.to_string());
}
args.push(url.clone());
args.push(target_dir.to_string_lossy().to_string());
// Run git clone
let output = Command::new("git")
.args(&args)
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("git clone failed: {}", stderr).into());
}
eprintln!(" [done] {}", dir_name);
Ok(())
}
/// dirs_next minimal replacement
mod dirs_next {
use std::path::PathBuf;
pub fn data_local_dir() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join("Library").join("Application Support"))
}
#[cfg(target_os = "linux")]
{
std::env::var("XDG_DATA_HOME")
.ok()
.map(PathBuf::from)
.or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".local").join("share"))
})
}
#[cfg(target_os = "windows")]
{
std::env::var("LOCALAPPDATA").ok().map(PathBuf::from)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
None
}
}
}