use tauri::State; use crate::docker; use crate::models::{container_config, AuthMode, Project, ProjectStatus}; use crate::storage::secure; use crate::AppState; #[tauri::command] pub async fn list_projects(state: State<'_, AppState>) -> Result, String> { Ok(state.projects_store.list()) } #[tauri::command] pub async fn add_project( name: String, path: String, state: State<'_, AppState>, ) -> Result { let project = Project::new(name, path); state.projects_store.add(project) } #[tauri::command] pub async fn remove_project( project_id: String, state: State<'_, AppState>, ) -> Result<(), String> { // Stop and remove container if it exists if let Some(project) = state.projects_store.get(&project_id) { if let Some(ref container_id) = project.container_id { let _ = docker::stop_container(container_id).await; let _ = docker::remove_container(container_id).await; } } // Close any exec sessions state.exec_manager.close_all_sessions().await; state.projects_store.remove(&project_id) } #[tauri::command] pub async fn update_project( project: Project, state: State<'_, AppState>, ) -> Result { state.projects_store.update(project) } #[tauri::command] pub async fn start_project_container( project_id: String, state: State<'_, AppState>, ) -> Result { let mut project = state .projects_store .get(&project_id) .ok_or_else(|| format!("Project {} not found", project_id))?; // Load settings for image resolution and global AWS let settings = state.settings_store.get(); let image_name = container_config::resolve_image_name(&settings.image_source, &settings.custom_image_name); // Get API key only if auth mode requires it let api_key = match project.auth_mode { AuthMode::ApiKey => { let key = secure::get_api_key()? .ok_or_else(|| "No API key configured. Please set your Anthropic API key in Settings.".to_string())?; Some(key) } AuthMode::Login => { None } AuthMode::Bedrock => { let bedrock = project.bedrock_config.as_ref() .ok_or_else(|| "Bedrock auth mode selected but no Bedrock configuration found.".to_string())?; // Region can come from per-project or global if bedrock.aws_region.is_empty() && settings.global_aws.aws_region.is_none() { return Err("AWS region is required for Bedrock auth mode. Set it per-project or in global AWS settings.".to_string()); } None } }; // Update status to starting state.projects_store.update_status(&project_id, ProjectStatus::Starting)?; // Ensure image exists if !docker::image_exists(&image_name).await? { state.projects_store.update_status(&project_id, ProjectStatus::Stopped)?; return Err(format!("Docker image '{}' not found. Please pull or build the image first.", image_name)); } // Determine docker socket path let docker_socket = settings.docker_socket_path .as_deref() .map(|s| s.to_string()) .unwrap_or_else(|| default_docker_socket()); // AWS config path from global settings let aws_config_path = settings.global_aws.aws_config_path.clone(); // Check for existing container let container_id = if let Some(existing_id) = docker::find_existing_container(&project).await? { // Check if docker socket mount matches the current project setting. // If the user toggled "Allow container spawning" after the container was // created, we need to recreate the container for the mount change to take // effect. let has_socket = docker::container_has_docker_socket(&existing_id).await.unwrap_or(false); if has_socket != project.allow_docker_access { log::info!( "Docker socket mismatch (container has_socket={}, project wants={}), recreating container", has_socket, project.allow_docker_access ); // Safe to remove and recreate: the claude config named volume is // keyed by project ID (not container ID) so it persists across // container recreation. Bind mounts (workspace, SSH, AWS) are // host paths and are unaffected. let _ = docker::stop_container(&existing_id).await; docker::remove_container(&existing_id).await?; let new_id = docker::create_container( &project, api_key.as_deref(), &docker_socket, &image_name, aws_config_path.as_deref(), &settings.global_aws, ).await?; docker::start_container(&new_id).await?; new_id } else { // Start existing container as-is docker::start_container(&existing_id).await?; existing_id } } else { // Create new container let new_id = docker::create_container( &project, api_key.as_deref(), &docker_socket, &image_name, aws_config_path.as_deref(), &settings.global_aws, ).await?; docker::start_container(&new_id).await?; new_id }; // Update project with container info state.projects_store.set_container_id(&project_id, Some(container_id.clone()))?; state.projects_store.update_status(&project_id, ProjectStatus::Running)?; project.container_id = Some(container_id); project.status = ProjectStatus::Running; Ok(project) } #[tauri::command] pub async fn stop_project_container( project_id: String, state: State<'_, AppState>, ) -> Result<(), String> { let project = state .projects_store .get(&project_id) .ok_or_else(|| format!("Project {} not found", project_id))?; if let Some(ref container_id) = project.container_id { state.projects_store.update_status(&project_id, ProjectStatus::Stopping)?; // Close exec sessions for this project state.exec_manager.close_all_sessions().await; docker::stop_container(container_id).await?; state.projects_store.update_status(&project_id, ProjectStatus::Stopped)?; } Ok(()) } #[tauri::command] pub async fn rebuild_project_container( project_id: String, state: State<'_, AppState>, ) -> Result { let project = state .projects_store .get(&project_id) .ok_or_else(|| format!("Project {} not found", project_id))?; // Remove existing container if let Some(ref container_id) = project.container_id { state.exec_manager.close_all_sessions().await; let _ = docker::stop_container(container_id).await; docker::remove_container(container_id).await?; state.projects_store.set_container_id(&project_id, None)?; } // Start fresh start_project_container(project_id, state).await } fn default_docker_socket() -> String { if cfg!(target_os = "windows") { "//./pipe/docker_engine".to_string() } else { "/var/run/docker.sock".to_string() } }