2026-02-27 04:29:51 +00:00
|
|
|
use tauri::State;
|
|
|
|
|
|
|
|
|
|
use crate::docker;
|
2026-02-27 15:22:49 +00:00
|
|
|
use crate::models::{container_config, AuthMode, Project, ProjectStatus};
|
2026-02-27 04:29:51 +00:00
|
|
|
use crate::storage::secure;
|
|
|
|
|
use crate::AppState;
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub async fn list_projects(state: State<'_, AppState>) -> Result<Vec<Project>, String> {
|
|
|
|
|
Ok(state.projects_store.list())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub async fn add_project(
|
|
|
|
|
name: String,
|
|
|
|
|
path: String,
|
|
|
|
|
state: State<'_, AppState>,
|
|
|
|
|
) -> Result<Project, String> {
|
|
|
|
|
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 {
|
2026-02-27 19:54:44 +00:00
|
|
|
state.exec_manager.close_sessions_for_container(container_id).await;
|
2026-02-27 04:29:51 +00:00
|
|
|
let _ = docker::stop_container(container_id).await;
|
|
|
|
|
let _ = docker::remove_container(container_id).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.projects_store.remove(&project_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub async fn update_project(
|
|
|
|
|
project: Project,
|
|
|
|
|
state: State<'_, AppState>,
|
|
|
|
|
) -> Result<Project, String> {
|
|
|
|
|
state.projects_store.update(project)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub async fn start_project_container(
|
|
|
|
|
project_id: String,
|
|
|
|
|
state: State<'_, AppState>,
|
|
|
|
|
) -> Result<Project, String> {
|
|
|
|
|
let mut project = state
|
|
|
|
|
.projects_store
|
|
|
|
|
.get(&project_id)
|
|
|
|
|
.ok_or_else(|| format!("Project {} not found", project_id))?;
|
|
|
|
|
|
2026-02-27 15:22:49 +00:00
|
|
|
// 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);
|
|
|
|
|
|
2026-02-27 04:29:51 +00:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-02-27 14:29:40 +00:00
|
|
|
AuthMode::Bedrock => {
|
|
|
|
|
let bedrock = project.bedrock_config.as_ref()
|
|
|
|
|
.ok_or_else(|| "Bedrock auth mode selected but no Bedrock configuration found.".to_string())?;
|
2026-02-27 15:22:49 +00:00
|
|
|
// 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());
|
2026-02-27 14:29:40 +00:00
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
2026-02-27 04:29:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Update status to starting
|
|
|
|
|
state.projects_store.update_status(&project_id, ProjectStatus::Starting)?;
|
|
|
|
|
|
|
|
|
|
// Ensure image exists
|
2026-02-27 15:22:49 +00:00
|
|
|
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));
|
2026-02-27 04:29:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine docker socket path
|
2026-02-27 15:22:49 +00:00
|
|
|
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();
|
2026-02-27 04:29:51 +00:00
|
|
|
|
|
|
|
|
// Check for existing container
|
|
|
|
|
let container_id = if let Some(existing_id) = docker::find_existing_container(&project).await? {
|
2026-02-27 19:37:06 +00:00
|
|
|
// Compare the running container's configuration (mounts, env vars)
|
|
|
|
|
// against the current project settings. If anything changed (SSH key
|
|
|
|
|
// path, git config, docker socket, etc.) we recreate the container.
|
|
|
|
|
// Safe to recreate: the claude config named volume is keyed by
|
|
|
|
|
// project ID (not container ID) so it persists across recreation.
|
2026-02-27 18:39:20 -08:00
|
|
|
let needs_recreation = docker::container_needs_recreation(
|
|
|
|
|
&existing_id,
|
|
|
|
|
&project,
|
|
|
|
|
settings.global_claude_instructions.as_deref(),
|
|
|
|
|
)
|
2026-02-27 19:37:06 +00:00
|
|
|
.await
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
if needs_recreation {
|
|
|
|
|
log::info!("Container config changed, recreating container for project {}", project.id);
|
2026-02-27 09:56:39 -08:00
|
|
|
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,
|
2026-02-27 18:39:20 -08:00
|
|
|
settings.global_claude_instructions.as_deref(),
|
2026-02-27 09:56:39 -08:00
|
|
|
).await?;
|
|
|
|
|
docker::start_container(&new_id).await?;
|
|
|
|
|
new_id
|
|
|
|
|
} else {
|
|
|
|
|
// Start existing container as-is
|
|
|
|
|
docker::start_container(&existing_id).await?;
|
|
|
|
|
existing_id
|
|
|
|
|
}
|
2026-02-27 04:29:51 +00:00
|
|
|
} else {
|
|
|
|
|
// Create new container
|
2026-02-27 15:22:49 +00:00
|
|
|
let new_id = docker::create_container(
|
|
|
|
|
&project,
|
|
|
|
|
api_key.as_deref(),
|
|
|
|
|
&docker_socket,
|
|
|
|
|
&image_name,
|
|
|
|
|
aws_config_path.as_deref(),
|
|
|
|
|
&settings.global_aws,
|
2026-02-27 18:39:20 -08:00
|
|
|
settings.global_claude_instructions.as_deref(),
|
2026-02-27 15:22:49 +00:00
|
|
|
).await?;
|
2026-02-27 04:29:51 +00:00
|
|
|
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
|
2026-02-27 19:54:44 +00:00
|
|
|
state.exec_manager.close_sessions_for_container(container_id).await;
|
2026-02-27 04:29:51 +00:00
|
|
|
|
|
|
|
|
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<Project, String> {
|
|
|
|
|
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 {
|
2026-02-27 19:54:44 +00:00
|
|
|
state.exec_manager.close_sessions_for_container(container_id).await;
|
2026-02-27 04:29:51 +00:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|