//! Makima CLI - unified CLI for server, daemon, and task management. use std::io::{self, Read}; use std::path::Path; use std::sync::Arc; use makima::daemon::api::{ApiClient, CreateContractRequest}; use makima::daemon::cli::{ Cli, CliConfig, Commands, ConfigCommand, ContractCommand, DirectiveCommand, SupervisorCommand, ViewArgs, }; use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion}; 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; use tokio::sync::mpsc; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; #[tokio::main] async fn main() -> Result<(), Box> { let cli = Cli::parse_args(); match cli.command { Commands::Server(args) => run_server(args).await, Commands::Daemon(args) => run_daemon(args).await, Commands::Supervisor(cmd) => run_supervisor(cmd).await, Commands::Contract(cmd) => run_contract(cmd).await, Commands::Directive(cmd) => run_directive(cmd).await, Commands::View(args) => run_view(args).await, Commands::Config(cmd) => run_config(cmd).await, } } /// Run the makima server. async fn run_server( args: makima::daemon::cli::ServerArgs, ) -> Result<(), Box> { // Initialize logging init_logging(&args.log_level, "text"); eprintln!("=== Makima Server Starting ==="); eprintln!("Port: {}", args.port); // Create app state let mut app_state = makima::server::state::AppState::new( &args.parakeet_model_dir, &args.parakeet_eou_dir, &args.sortformer_model_path, &args.chatterbox_model_dir, ); // Connect to database if URL provided if let Some(ref db_url) = args.database_url { eprintln!("Connecting to database..."); let pool = makima::db::create_pool(db_url).await?; app_state = app_state.with_db_pool(pool); eprintln!("Database connected"); } let state = Arc::new(app_state); let addr = format!("0.0.0.0:{}", args.port); eprintln!("Starting server on {}", addr); makima::server::run_server(state, &addr).await?; Ok(()) } /// Run the makima daemon. async fn run_daemon( args: makima::daemon::cli::DaemonArgs, ) -> Result<(), Box> { 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(); setup::print_claude_install_instructions(os); std::process::exit(1); } if !dep_result.git.installed { let os = setup::OperatingSystem::detect(); setup::print_git_install_instructions(os); std::process::exit(1); } // Print git authentication warnings (non-fatal) setup::print_git_auth_warnings(&dep_result); } // Install Claude Code skills for makima commands eprintln!("[0.5/5] Installing Claude Code skills..."); if let Err(e) = makima::daemon::skill_installer::install_skills().await { eprintln!(" WARNING: Failed to install skills: {}", e); // Non-fatal: continue even if skill installation fails } // Build a temporary CLI struct for config loading let cli = makima::daemon::cli::daemon::DaemonArgs { config: args.config, repos_dir: args.repos_dir, worktrees_dir: args.worktrees_dir, server_url: args.server_url, api_key: args.api_key, 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 eprintln!("[1/5] Loading configuration..."); let config = match DaemonConfig::load_with_daemon_args(&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"); 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 = Arc::new(std::sync::Mutex::new(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::(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 bubblewrap_config = if config.process.bubblewrap.enabled { Some(config.process.bubblewrap.clone()) } else { None }; // Derive HTTP API URL from WebSocket server URL (wss://... -> https://...) let api_url = config .server .url .replace("wss://", "https://") .replace("ws://", "http://"); let task_config = TaskConfig { max_concurrent_tasks: config.process.max_concurrent_tasks, max_tasks_per_contract: config.process.max_tasks_per_contract, 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, bubblewrap: bubblewrap_config, api_url, api_key: config.server.api_key.clone(), heartbeat_commit_interval_secs: config.process.heartbeat_commit_interval_secs, checkpoint_patches: config.process.checkpoint_patches.clone(), }; // Create task manager with local database for crash recovery let task_manager = Arc::new(TaskManager::new(task_config, ws_tx.clone(), local_db)); // Recover any orphaned tasks from previous daemon run let recovered = task_manager.recover_orphaned_tasks().await; if !recovered.is_empty() { eprintln!(" Recovered {} orphaned tasks with intact worktrees", recovered.len()); } // 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"); }); // Spawn periodic worktree health check (every 60 seconds) let health_check_manager = task_manager.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); loop { interval.tick().await; let affected = health_check_manager.check_worktree_health().await; if !affected.is_empty() { tracing::info!( count = affected.len(), "Worktree health check detected missing worktrees - tasks marked for retry" ); } } }); // 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..."); } } // Gracefully shutdown all running Claude processes eprintln!("Terminating Claude processes..."); task_manager .shutdown_all_processes(std::time::Duration::from_secs(5)) .await; // Cleanup tracing::info!("Daemon stopped"); Ok(()) } /// Run supervisor commands. async fn run_supervisor( cmd: SupervisorCommand, ) -> Result<(), Box> { use makima::daemon::api::supervisor::*; match cmd { SupervisorCommand::Tasks(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.supervisor_tasks(args.contract_id).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Tree(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.supervisor_tree(args.contract_id).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Spawn(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; eprintln!("Creating task: {}...", args.name); let req = SpawnTaskRequest { name: args.name, plan: args.plan, contract_id: args.common.contract_id, parent_task_id: args.parent, checkpoint_sha: args.checkpoint, }; let result = client.supervisor_spawn(req).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Wait(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; eprintln!( "Waiting for task {} (timeout: {}s, poll interval: {}s)...", args.task_id, args.timeout, args.poll_interval ); let start_time = std::time::Instant::now(); let timeout_duration = std::time::Duration::from_secs(args.timeout as u64); let poll_interval = std::time::Duration::from_secs(args.poll_interval); let server_wait_timeout = 30i32; // Short timeout for server-side wait loop { // Check if we've exceeded the total timeout let remaining = timeout_duration.saturating_sub(start_time.elapsed()); if remaining.is_zero() { eprintln!("Timeout reached after {}s", args.timeout); let result = client.supervisor_get_task(args.task_id).await?; println!("{}", serde_json::to_string(&result.0)?); break; } // Try server-side wait with short timeout let wait_timeout = std::cmp::min(server_wait_timeout, remaining.as_secs() as i32); match client.supervisor_wait(args.task_id, wait_timeout).await { Ok(result) => { if let Some(completed) = result.0.get("completed").and_then(|c| c.as_bool()) { if completed { println!("{}", serde_json::to_string(&result.0)?); break; } } // Not completed yet, continue loop eprintln!("Task still running (elapsed: {:?})", start_time.elapsed()); } Err(e) => { eprintln!("Warning: Server wait failed: {}. Falling back to polling...", e); // Fall back to simple status poll if let Ok(result) = client.supervisor_get_task(args.task_id).await { if let Some(status) = result.0.get("status").and_then(|s| s.as_str()) { if status == "done" || status == "failed" || status == "merged" { let wait_response = serde_json::json!({ "taskId": args.task_id, "status": status, "completed": true, "outputSummary": result.0.get("progressSummary") }); println!("{}", serde_json::to_string(&wait_response)?); break; } } } } } // Small delay before retrying tokio::time::sleep(poll_interval).await; } } SupervisorCommand::ReadFile(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client .supervisor_read_file(args.task_id, &args.file_path) .await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Branch(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; eprintln!("Creating branch: {}...", args.name); let result = client.supervisor_branch(&args.name, args.from).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Merge(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; eprintln!("Merging task {}...", args.task_id); let result = client .supervisor_merge(args.task_id, args.to, args.squash) .await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Pr(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; eprintln!("Creating PR for branch {}...", args.branch); let body = args.body.as_deref().unwrap_or(""); let result = client .supervisor_pr(&args.branch, &args.title, body) .await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Diff(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client.supervisor_diff(args.task_id).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Checkpoint(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let task_id = args .common .self_task_id .ok_or("MAKIMA_TASK_ID is required for checkpoint")?; let result = client .supervisor_checkpoint(task_id, &args.message) .await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Checkpoints(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let task_id = args.self_task_id.ok_or("MAKIMA_TASK_ID is required")?; let result = client.supervisor_checkpoints(task_id).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Status(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.supervisor_status(args.contract_id).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Ask(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; eprintln!("Asking user: {}...", args.question); let choices = args .choices .map(|c| c.split(',').map(|s| s.trim().to_string()).collect()) .unwrap_or_default(); let result = client .supervisor_ask(&args.question, choices, args.context, args.timeout, args.phaseguard, args.multi_select, args.non_blocking, args.question_type) .await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::AdvancePhase(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; if args.confirmed { eprintln!("Advancing contract to phase: {} (confirmed)...", args.phase); } else { eprintln!("Requesting phase advance to: {} (use --confirmed to proceed)...", args.phase); } let result = client .supervisor_advance_phase(args.common.contract_id, &args.phase, args.confirmed) .await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Task(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client.supervisor_get_task(args.target_task_id).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::Output(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client.supervisor_get_task_output(args.target_task_id).await?; println!("{}", serde_json::to_string(&result.0)?); } SupervisorCommand::TaskHistory(args) => { eprintln!( "Task history for {} (limit: {:?}, format: {})", args.task_id, args.limit, args.format ); eprintln!("CLI integration not yet implemented. Use the API directly:"); eprintln!(" GET /api/v1/mesh/tasks/{}/conversation", args.task_id); } SupervisorCommand::TaskCheckpoints(args) => { eprintln!( "Task checkpoints for {} (with_diff: {})", args.task_id, args.with_diff ); eprintln!("CLI integration not yet implemented. Use the API directly:"); eprintln!(" GET /api/v1/mesh/tasks/{}/checkpoints", args.task_id); } SupervisorCommand::Resume(args) => { eprintln!( "Resume supervisor for contract {} (mode: {}, checkpoint: {:?})", args.common.contract_id, args.mode, args.checkpoint ); eprintln!("CLI integration not yet implemented. Use the API directly:"); eprintln!( " POST /api/v1/contracts/{}/supervisor/resume", args.common.contract_id ); } SupervisorCommand::TaskResumeFrom(args) => { eprintln!( "Resume task {} from checkpoint {} with plan: {}", args.task_id, args.checkpoint, args.plan ); eprintln!("CLI integration not yet implemented. Use the API directly:"); eprintln!( " POST /api/v1/mesh/tasks/{}/checkpoints/{}/resume", args.task_id, args.checkpoint ); } SupervisorCommand::TaskRewind(args) => { eprintln!( "Rewind task {} to checkpoint {} (preserve: {}, branch: {:?})", args.task_id, args.checkpoint, args.preserve, args.branch_name ); eprintln!("CLI integration not yet implemented. Use the API directly:"); eprintln!(" POST /api/v1/mesh/tasks/{}/rewind", args.task_id); } SupervisorCommand::TaskFork(args) => { eprintln!( "Fork task {} from checkpoint {} as '{}' with plan: {}", args.task_id, args.checkpoint, args.name, args.plan ); eprintln!("CLI integration not yet implemented. Use the API directly:"); eprintln!(" POST /api/v1/mesh/tasks/{}/fork", args.task_id); } SupervisorCommand::RewindConversation(args) => { eprintln!( "Rewind conversation for contract {} (by: {:?}, to: {:?}, rewind_code: {})", args.common.contract_id, args.by_messages, args.to_message, args.rewind_code ); eprintln!("CLI integration not yet implemented. Use the API directly:"); eprintln!( " POST /api/v1/contracts/{}/supervisor/conversation/rewind", args.common.contract_id ); } SupervisorCommand::Complete(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; eprintln!("Marking contract {} as complete...", args.common.contract_id); match client.supervisor_complete(args.common.contract_id).await { Ok(_) => { println!(r#"{{"success": true, "message": "Contract marked as complete"}}"#); } Err(e) => { eprintln!("Error: {}", e); println!(r#"{{"success": false, "error": "{}"}}"#, e); std::process::exit(1); } } } SupervisorCommand::ResumeContract(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; eprintln!("Resuming contract {}...", args.contract_id); let result = client.supervisor_resume_contract(args.contract_id).await?; println!("{}", serde_json::to_string(&serde_json::json!({ "success": true, "message": "Contract resumed", "contract": result.0 }))?); } SupervisorCommand::MarkDeliverable(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; eprintln!( "Marking deliverable '{}' as complete for contract {}...", args.deliverable_id, args.common.contract_id ); let result = client .supervisor_mark_deliverable( args.common.contract_id, &args.deliverable_id, args.phase.as_deref(), ) .await?; println!("{}", serde_json::to_string(&result.0)?); } } Ok(()) } /// Run contract commands. async fn run_contract( cmd: ContractCommand, ) -> Result<(), Box> { match cmd { ContractCommand::Status(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.contract_status(args.contract_id).await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::Checklist(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.contract_checklist(args.contract_id).await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::Goals(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.contract_goals(args.contract_id).await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::Files(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.contract_files(args.contract_id).await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::File(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client .contract_file(args.common.contract_id, args.file_id) .await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::Report(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client .contract_report(args.common.contract_id, &args.message, args.common.task_id) .await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::SuggestAction(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.contract_suggest_action(args.contract_id).await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::CompletionAction(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let files = args.files.map(|f| { f.split(',') .map(|s| s.trim().to_string()) .collect::>() }); let result = client .contract_completion_action( args.common.contract_id, args.common.task_id, files, args.lines_added, args.lines_removed, args.code, ) .await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::UpdateFile(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; // Read content from stdin let mut content = String::new(); io::stdin().read_to_string(&mut content)?; let result = client .contract_update_file(args.common.contract_id, args.file_id, &content) .await?; println!("{}", serde_json::to_string(&result.0)?); } ContractCommand::CreateFile(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; // Read content from stdin let mut content = String::new(); io::stdin().read_to_string(&mut content)?; let result = client .contract_create_file(args.common.contract_id, &args.name, &content) .await?; println!("{}", serde_json::to_string(&result.0)?); } } Ok(()) } /// Run directive commands. async fn run_directive( cmd: DirectiveCommand, ) -> Result<(), Box> { use makima::daemon::api::directive::*; match cmd { DirectiveCommand::List(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.list_directives().await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::Get(args) | DirectiveCommand::Status(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.get_directive(args.directive_id).await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::AddStep(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let depends_on: Vec = args .depends_on .map(|d| { d.split(',') .filter_map(|s| uuid::Uuid::parse_str(s.trim()).ok()) .collect() }) .unwrap_or_default(); let req = CreateStepRequest { name: args.name, description: args.description, task_plan: args.task_plan, depends_on, order_index: args.order_index, }; let result = client.directive_add_step(args.common.directive_id, req).await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::RemoveStep(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; client.directive_remove_step(args.common.directive_id, args.step_id).await?; println!(r#"{{"success": true}}"#); } DirectiveCommand::SetDeps(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let depends_on: Vec = args .depends_on .split(',') .filter_map(|s| uuid::Uuid::parse_str(s.trim()).ok()) .collect(); let result = client .directive_set_deps(args.common.directive_id, args.step_id, depends_on) .await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::Start(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.directive_start(args.directive_id).await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::Pause(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.directive_pause(args.directive_id).await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::Advance(args) => { let client = ApiClient::new(args.api_url, args.api_key)?; let result = client.directive_advance(args.directive_id).await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::CompleteStep(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client .directive_complete_step(args.common.directive_id, args.step_id) .await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::FailStep(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client .directive_fail_step(args.common.directive_id, args.step_id) .await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::SkipStep(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client .directive_skip_step(args.common.directive_id, args.step_id) .await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::UpdateGoal(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client .directive_update_goal(args.common.directive_id, &args.goal) .await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::BatchAddSteps(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let steps: serde_json::Value = serde_json::from_str(&args.json) .map_err(|e| format!("Invalid JSON: {}", e))?; let result = client .directive_batch_add_steps(args.common.directive_id, steps) .await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::Update(args) => { let client = ApiClient::new(args.common.api_url, args.common.api_key)?; let result = client .directive_update(args.common.directive_id, args.pr_url, args.pr_branch, args.status) .await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::Ask(args) => { let client = ApiClient::new(args.common.api_url.clone(), args.common.api_key.clone())?; eprintln!("Asking user: {}...", args.question); let choices = args .choices .map(|c| c.split(',').map(|s| s.trim().to_string()).collect()) .unwrap_or_default(); let result = client .supervisor_ask(&args.question, choices, args.context, args.timeout, args.phaseguard, args.multi_select, args.non_blocking, args.question_type) .await?; let mut response_value = result.0; // If the server returned still_pending, poll until we get a real response while response_value.get("stillPending").and_then(|v| v.as_bool()).unwrap_or(false) { // Extract question_id for polling let question_id_str = response_value.get("questionId") .and_then(|v| v.as_str()) .ok_or_else(|| { Box::::from( "Missing questionId in still_pending response" ) })?; let question_id: uuid::Uuid = question_id_str.parse().map_err(|e| { Box::::from( format!("Invalid questionId: {}", e) ) })?; eprintln!("Waiting for user response (polling)..."); let poll_result = client.supervisor_poll_question(question_id).await?; response_value = poll_result.0; } println!("{}", serde_json::to_string(&response_value)?); } DirectiveCommand::CreateOrder(args) => { // Validate order_type is spike or chore if args.order_type != "spike" && args.order_type != "chore" { eprintln!("Error: Only 'spike' and 'chore' order types are allowed. Got: '{}'", args.order_type); std::process::exit(1); } let client = ApiClient::new(args.common.api_url.clone(), args.common.api_key.clone())?; eprintln!("Creating order: {}...", args.title); let labels = args .labels .map(|l| { serde_json::Value::Array( l.split(',') .map(|s| serde_json::Value::String(s.trim().to_string())) .collect(), ) }) .unwrap_or_else(|| serde_json::json!([])); let req = makima::daemon::api::supervisor::CreateOrderRequest { title: args.title, description: args.description, priority: args.priority, order_type: args.order_type, labels, repository_url: None, }; let result = client.create_order(&req).await?; println!("{}", serde_json::to_string(&result.0)?); } DirectiveCommand::Verify(args) => { run_directive_verify(args).await?; } } Ok(()) } /// Run `makima directive verify` — checks that the current HEAD merges cleanly /// into `/`. Prints a JSON result and exits non-zero on conflict. /// /// Implementation uses `git merge-tree --write-tree` (Git ≥ 2.38), which performs /// the merge in-memory and lists conflicting paths without touching the working /// tree or creating any commits. async fn run_directive_verify( args: makima::daemon::cli::directive::VerifyArgs, ) -> Result<(), Box> { use std::process::Command; fn git(args: &[&str]) -> std::io::Result { Command::new("git").args(args).output() } let head_ref = args.head.as_deref().unwrap_or("HEAD").to_string(); let base_ref = format!("{}/{}", args.remote, args.base); if !args.skip_fetch { eprintln!("Fetching {} {}...", args.remote, args.base); let fetch = git(&["fetch", &args.remote, &args.base])?; if !fetch.status.success() { return Err(format!( "git fetch {} {} failed: {}", args.remote, args.base, String::from_utf8_lossy(&fetch.stderr) ) .into()); } } let head_rev = { let out = git(&["rev-parse", &head_ref])?; if !out.status.success() { return Err(format!( "git rev-parse {} failed: {}", head_ref, String::from_utf8_lossy(&out.stderr) ) .into()); } String::from_utf8_lossy(&out.stdout).trim().to_string() }; let base_rev = { let out = git(&["rev-parse", &base_ref])?; if !out.status.success() { return Err(format!( "git rev-parse {} failed (did you fetch?): {}", base_ref, String::from_utf8_lossy(&out.stderr) ) .into()); } String::from_utf8_lossy(&out.stdout).trim().to_string() }; eprintln!("Verifying merge: {} ({}) <- {} ({})", base_ref, &base_rev[..7.min(base_rev.len())], head_ref, &head_rev[..7.min(head_rev.len())]); let merge = Command::new("git") .args(["merge-tree", "--write-tree", "--name-only", "--no-messages", &base_rev, &head_rev]) .output()?; let stdout = String::from_utf8_lossy(&merge.stdout).to_string(); let stderr = String::from_utf8_lossy(&merge.stderr).to_string(); let success = merge.status.success(); let conflicting_files: Vec = if success { Vec::new() } else { stdout .lines() .skip(1) .filter(|l| !l.is_empty()) .map(|l| l.to_string()) .collect() }; let result = serde_json::json!({ "ok": success, "base": base_ref, "head": head_ref, "baseSha": base_rev, "headSha": head_rev, "conflictingFiles": conflicting_files, "goal": args.goal, }); println!("{}", serde_json::to_string(&result)?); if !success { eprintln!("\n[FAIL] Merge would conflict in {} file(s):", conflicting_files.len()); for f in &conflicting_files { eprintln!(" - {}", f); } if !stderr.is_empty() { eprintln!("\ngit stderr:\n{}", stderr); } eprintln!( "\nFix the conflicts before pushing. Typical workflow:\n \ git fetch {remote} {base}\n \ git merge {remote}/{base}\n \ # resolve conflicts, commit, then re-run `makima directive verify`", remote = args.remote, base = args.base, ); std::process::exit(1); } if let Some(goal) = &args.goal { eprintln!("\n[OK] No merge conflicts."); eprintln!("Reminder — directive goal:\n {}\n", goal); eprintln!("Confirm the diff (`git diff {}...HEAD`) actually delivers this goal before creating the PR.", base_ref); } else { eprintln!("[OK] No merge conflicts with {}.", base_ref); } Ok(()) } /// Run the TUI view command. async fn run_view(args: ViewArgs) -> Result<(), Box> { // Load CLI config for defaults let config = CliConfig::load(); // Get API URL and key, preferring CLI args > env vars > config file // Filter out empty strings let api_url = args.api_url .filter(|s| !s.is_empty()) .unwrap_or_else(|| config.get_api_url()); let api_key = match args.api_key.filter(|s| !s.is_empty()) { Some(key) => key, None => config.get_api_key().ok_or_else(|| { eprintln!("Error: No API key provided."); eprintln!(); eprintln!("Set your API key using one of these methods:"); eprintln!(" 1. Run: makima config set-key YOUR_API_KEY"); eprintln!(" 2. Set environment variable: export MAKIMA_API_KEY=YOUR_API_KEY"); eprintln!(" 3. Pass via CLI: makima view --api-key YOUR_API_KEY"); "No API key configured" })?, }; // Create API client let client = ApiClient::new(api_url.clone(), api_key.clone())?; // Start WebSocket client for task output streaming let ws_client = TuiWsClient::start(api_url, api_key); // Start at contracts view let mut app = App::new(ViewType::Contracts); // Set initial search query if provided if let Some(ref query) = args.query { app.search_query = query.clone(); } // Load initial contracts let items = load_contracts(&client).await?; app.set_items(items); // Run TUI with navigation support let result = run_tui_with_navigation(app, client, ws_client).await; result } /// Run config commands. async fn run_config(cmd: ConfigCommand) -> Result<(), Box> { match cmd { ConfigCommand::SetKey(args) => { let mut config = CliConfig::load(); config.api_key = Some(args.api_key); config.save()?; println!("API key saved to {:?}", CliConfig::config_path().unwrap_or_default()); Ok(()) } ConfigCommand::SetUrl(args) => { let mut config = CliConfig::load(); config.api_url = args.api_url; config.save()?; println!("API URL saved to {:?}", CliConfig::config_path().unwrap_or_default()); Ok(()) } ConfigCommand::Show => { let config = CliConfig::load(); println!("Configuration:"); println!(" API URL: {}", config.api_url); println!(" API Key: {}", config.api_key.as_ref().map(|k| { if k.len() > 10 { format!("{}...{}", &k[..6], &k[k.len()-4..]) } else { "***".to_string() } }).unwrap_or_else(|| "(not set)".to_string())); println!(); println!("Config file: {:?}", CliConfig::config_path().unwrap_or_default()); Ok(()) } ConfigCommand::Path => { if let Some(path) = CliConfig::config_path() { println!("{}", path.display()); } else { eprintln!("Could not determine config path"); } Ok(()) } } } /// Load contracts from API async fn load_contracts(client: &ApiClient) -> Result, Box> { let result = client.list_contracts().await?; let items = result.0.get("contracts") .and_then(|v| v.as_array()) .map(|arr| arr.iter().filter_map(ListItem::from_contract).collect()) .unwrap_or_default(); Ok(items) } /// Load tasks for a contract from API async fn load_tasks(client: &ApiClient, contract_id: uuid::Uuid) -> Result, Box> { // Use get_contract which returns tasks as part of the response (works with regular API key auth) let result = client.get_contract(contract_id).await?; let mut items: Vec = result.0.get("tasks") .and_then(|v| v.as_array()) .map(|arr| arr.iter().filter_map(ListItem::from_task).collect()) .unwrap_or_default(); // Sort tasks: supervisor first, then by status (running first), then by name items.sort_by(|a, b| { // Check if task is supervisor (role field in extra data) let a_is_supervisor = a.extra.get("role") .and_then(|v| v.as_str()) .map(|s| s == "supervisor") .unwrap_or(false); let b_is_supervisor = b.extra.get("role") .and_then(|v| v.as_str()) .map(|s| s == "supervisor") .unwrap_or(false); // Supervisor first match (a_is_supervisor, b_is_supervisor) { (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => { // Then by status: running/working tasks first let status_order = |s: Option<&String>| -> i32 { match s.map(|x| x.as_str()) { Some("running") | Some("working") => 0, Some("pending") | Some("queued") => 1, Some("completed") | Some("done") => 2, Some("failed") | Some("error") => 3, _ => 4, } }; let a_order = status_order(a.status.as_ref()); let b_order = status_order(b.status.as_ref()); match a_order.cmp(&b_order) { std::cmp::Ordering::Equal => a.name.cmp(&b.name), other => other, } } } }); Ok(items) } /// Run the TUI with navigation support for drill-down views async fn run_tui_with_navigation( mut app: App, client: ApiClient, ws_client: TuiWsClient, ) -> Result<(), Box> { use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::backend::CrosstermBackend; use std::io; // Setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = ratatui::Terminal::new(backend)?; let result = run_tui_loop(&mut terminal, &mut app, &client, &ws_client).await; // Cleanup WebSocket ws_client.shutdown(); // Cleanup terminal disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; terminal.show_cursor()?; match result { Ok(Some(path)) => { // Output the path for shell integration tui::print_path(&path); } Ok(None) => { // Normal exit } Err(e) => { eprintln!("TUI error: {}", e); std::process::exit(1); } } Ok(()) } /// Main TUI event loop with async data loading async fn run_tui_loop( terminal: &mut ratatui::Terminal>, app: &mut App, client: &ApiClient, ws_client: &TuiWsClient, ) -> Result, Box> { use crossterm::event::{self, Event}; use std::time::Duration; // Track currently subscribed task for cleanup let mut subscribed_task_id: Option = None; loop { terminal.draw(|f| tui::ui::render(f, app))?; // Process WebSocket events (non-blocking) while let Some(ws_event) = ws_client.try_recv() { handle_ws_event(app, ws_event); } // Poll for keyboard events with short timeout (50ms for responsive WS handling) if event::poll(Duration::from_millis(50))? { if let Event::Key(key) = event::read()? { let action = tui::event::handle_key_event(app, key); match action { Action::Quit => break, Action::OutputPath(path) => return Ok(Some(path)), Action::None => {} _ => { let result = app.handle_action(action); match result { Action::OutputPath(path) => return Ok(Some(path)), Action::LoadTasks { contract_id, contract_name: _ } => { // Unsubscribe from any previous task if let Some(task_id) = subscribed_task_id.take() { ws_client.unsubscribe(task_id); } // Load tasks for the selected contract match load_tasks(client, contract_id).await { Ok(items) => { app.set_items(items); } Err(e) => { app.status_message = Some(format!("Failed to load tasks: {}", e)); } } } Action::LoadTaskOutput { task_id, task_name: _ } => { // Clear previous output app.output_buffer.clear(); app.ws_state = WsConnectionState::Connecting; // Unsubscribe from previous task if any if let Some(old_task_id) = subscribed_task_id.take() { ws_client.unsubscribe(old_task_id); } // Load task output history first app.status_message = Some("Loading output history...".to_string()); match client.get_task_output(task_id).await { Ok(result) => { // Parse the entries array from response if let Some(entries) = result.0.get("entries").and_then(|v| v.as_array()) { for entry in entries { if let Some(line) = parse_output_entry(entry) { app.output_buffer.add_line(line); } } let count = entries.len(); app.status_message = Some(format!("Loaded {} history entries, streaming live...", count)); } } Err(e) => { app.status_message = Some(format!("Failed to load history: {}", e)); } } // Subscribe to new task output for live updates ws_client.subscribe(task_id); subscribed_task_id = Some(task_id); } Action::PerformDelete { id, item_type } => { // Perform the delete API call let delete_result = match item_type { ViewType::Contracts => { client.delete_contract(id).await } ViewType::Tasks => { client.delete_task(id).await } ViewType::TaskOutput => { // Can't delete from output view Ok(()) } }; match delete_result { Ok(()) => { app.status_message = Some("Deleted successfully".to_string()); // Remove item from list app.items.retain(|item| item.id != id); app.update_filtered_items(); } Err(e) => { app.status_message = Some(format!("Delete failed: {}", e)); } } } Action::PerformUpdate { id, item_type, name, description } => { // Perform the update API call let update_result = match item_type { ViewType::Contracts => { client.update_contract(id, Some(name.clone()), Some(description.clone())).await.map(|_| ()) } ViewType::Tasks => { // For tasks, description is the plan client.update_task(id, Some(name.clone()), Some(description.clone())).await.map(|_| ()) } ViewType::TaskOutput => { // Can't edit from output view Ok(()) } }; match update_result { Ok(()) => { app.status_message = Some("Updated successfully".to_string()); // Update item in list for item in &mut app.items { if item.id == id { item.name = name.clone(); item.description = Some(description.clone()); break; } } app.update_filtered_items(); } Err(e) => { app.status_message = Some(format!("Update failed: {}", e)); } } } Action::Refresh => { // Reload data for current view match app.view_type { ViewType::Contracts => { // Unsubscribe from task when going back to contracts if let Some(task_id) = subscribed_task_id.take() { ws_client.unsubscribe(task_id); } match load_contracts(client).await { Ok(items) => app.set_items(items), Err(e) => app.status_message = Some(format!("Refresh failed: {}", e)), } } ViewType::Tasks => { // Unsubscribe from task when going back to tasks if let Some(task_id) = subscribed_task_id.take() { ws_client.unsubscribe(task_id); } if let Some(contract_id) = app.contract_id { match load_tasks(client, contract_id).await { Ok(items) => app.set_items(items), Err(e) => app.status_message = Some(format!("Refresh failed: {}", e)), } } } ViewType::TaskOutput => { // Re-subscribe to the task output if let Some(task_id) = app.task_id { app.output_buffer.clear(); app.ws_state = WsConnectionState::Connecting; ws_client.subscribe(task_id); subscribed_task_id = Some(task_id); app.status_message = Some("Reconnecting...".to_string()); } } } } Action::GoBack => { // Unsubscribe when going back from output view if let Some(task_id) = subscribed_task_id.take() { ws_client.unsubscribe(task_id); app.ws_state = WsConnectionState::Disconnected; } } Action::PerformCreateContract { name, description, contract_type, repository_url } => { // Create the contract via API let req = CreateContractRequest { name: name.clone(), description: if description.is_empty() { None } else { Some(description) }, contract_type: Some(contract_type), initial_phase: None, autonomous_loop: None, phase_guard: None, local_only: None, auto_merge_local: None, }; match client.create_contract(req).await { Ok(result) => { let contract_name = result.0.get("name") .and_then(|v| v.as_str()) .unwrap_or(&name) .to_string(); let contract_id = result.0.get("id") .and_then(|v| v.as_str()) .and_then(|s| uuid::Uuid::parse_str(s).ok()); // Add repository if provided if let (Some(repo_url), Some(cid)) = (repository_url.as_ref(), contract_id) { if !repo_url.is_empty() { // Extract repo name from URL (e.g., "owner/repo" from GitHub URL) let repo_name = extract_repo_name(repo_url); match client.add_remote_repository(cid, &repo_name, repo_url, true).await { Ok(_) => { app.status_message = Some(format!( "Created contract '{}' with repository", contract_name )); } Err(e) => { app.status_message = Some(format!( "Created contract but failed to add repository: {}", e )); } } } else { app.status_message = Some(format!("Created contract: {}", contract_name)); } } else { app.status_message = Some(format!("Created contract: {}", contract_name)); } // Refresh the contracts list match load_contracts(client).await { Ok(items) => app.set_items(items), Err(e) => { let msg = app.status_message.take().unwrap_or_default(); app.status_message = Some(format!("{} (refresh failed: {})", msg, e)); } } } Err(e) => { app.status_message = Some(format!("Create failed: {}", e)); } } } Action::LoadRepoSuggestions => { // Load repository suggestions for the create form app.status_message = Some("Loading recent repositories...".to_string()); // Force a redraw to show the status terminal.draw(|f| tui::ui::render(f, app)).ok(); // Fetch all repository types (remote and local) match client.get_repository_suggestions(None, Some(10)).await { Ok(result) => { // Parse suggestions from API response let suggestions: Vec = result.0 .get("entries") .and_then(|v| v.as_array()) .map(|arr| { arr.iter().filter_map(|entry| { let name = entry.get("name") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let repository_url = entry.get("repositoryUrl") .or_else(|| entry.get("repository_url")) .and_then(|v| v.as_str()) .map(|s| s.to_string()); let local_path = entry.get("localPath") .or_else(|| entry.get("local_path")) .and_then(|v| v.as_str()) .map(|s| s.to_string()); let source_type = entry.get("sourceType") .or_else(|| entry.get("source_type")) .and_then(|v| v.as_str()) .unwrap_or("remote") .to_string(); let use_count = entry.get("useCount") .or_else(|| entry.get("use_count")) .and_then(|v| v.as_i64()) .unwrap_or(0) as i32; // Only include if we have a URL or path if repository_url.is_some() || local_path.is_some() { Some(RepositorySuggestion { name, repository_url, local_path, source_type, use_count, }) } else { None } }).collect() }) .unwrap_or_default(); let count = suggestions.len(); app.create_state.set_suggestions(suggestions); if count > 0 { app.status_message = Some(format!("Found {} recent repositories", count)); } else { app.status_message = Some("No recent repositories found".to_string()); } } Err(e) => { app.status_message = Some(format!("Could not load suggestions: {}", e)); app.create_state.suggestions_loaded = true; } } } _ => {} } } } } } if app.should_quit { break; } } Ok(None) } /// Extract a repository name from a URL. /// E.g., "https://github.com/owner/repo.git" -> "owner/repo" fn extract_repo_name(url: &str) -> String { // Remove .git suffix if present let url = url.trim_end_matches(".git"); // Try to extract owner/repo from common Git hosting URLs if let Some(path) = url.strip_prefix("https://github.com/") .or_else(|| url.strip_prefix("https://gitlab.com/")) .or_else(|| url.strip_prefix("https://bitbucket.org/")) .or_else(|| url.strip_prefix("git@github.com:")) .or_else(|| url.strip_prefix("git@gitlab.com:")) .or_else(|| url.strip_prefix("git@bitbucket.org:")) { // Return owner/repo return path.to_string(); } // Fallback: try to get the last path segment if let Some(last_segment) = url.rsplit('/').next() { if !last_segment.is_empty() { return last_segment.to_string(); } } // Last resort: use the full URL as the name url.to_string() } /// Parse an output entry from the API response into an OutputLine fn parse_output_entry(entry: &serde_json::Value) -> Option { let message_type = entry.get("messageType") .and_then(|v| v.as_str()) .unwrap_or("raw"); let content = entry.get("content") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let tool_name = entry.get("toolName") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let is_error = entry.get("isError") .and_then(|v| v.as_bool()) .unwrap_or(false); let cost_usd = entry.get("costUsd") .and_then(|v| v.as_f64()); let duration_ms = entry.get("durationMs") .and_then(|v| v.as_u64()); Some(OutputLine { message_type: OutputMessageType::from_str(message_type), content, tool_name, is_error, cost_usd, duration_ms, }) } /// Handle a WebSocket event and update app state fn handle_ws_event(app: &mut App, event: WsEvent) { match event { WsEvent::Connected => { app.ws_state = WsConnectionState::Connected; app.status_message = Some("Connected".to_string()); } WsEvent::Disconnected => { app.ws_state = WsConnectionState::Disconnected; app.status_message = Some("Disconnected".to_string()); } WsEvent::Reconnecting { attempt } => { app.ws_state = WsConnectionState::Reconnecting; app.status_message = Some(format!("Reconnecting (attempt {})...", attempt)); } WsEvent::Subscribed { task_id: _ } => { app.ws_state = WsConnectionState::Connected; app.status_message = Some("Subscribed to task output".to_string()); } WsEvent::Unsubscribed { task_id: _ } => { // No status update needed } WsEvent::TaskOutput(output) => { // Convert WebSocket event to OutputLine let line = OutputLine { message_type: OutputMessageType::from_str(&output.message_type), content: output.content, tool_name: output.tool_name, is_error: output.is_error.unwrap_or(false), cost_usd: output.cost_usd, duration_ms: output.duration_ms, }; app.output_buffer.add_line(line); // Clear status message once we're receiving output if app.status_message.as_ref().map(|s| s.contains("Subscribed")).unwrap_or(false) { app.status_message = None; } } WsEvent::Error { message } => { app.status_message = Some(format!("WS Error: {}", message)); } } } 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"); 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> { 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 { #[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 } } }