diff --git a/scripts/README.md b/scripts/README.md index 6dbc30a..1554c14 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -76,6 +76,7 @@ All executables are created in the `src-tauri/binaries/` directory with the foll - **Cross-platform**: Supports all major operating systems and architectures - **CPU Variants**: Modern variants for newer CPUs (2013+), baseline for compatibility - **Self-contained**: No external dependencies required at runtime +- **Tauri Integration**: Automatic sidecar binary naming for seamless Tauri integration ## Requirements diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index f51af53..9c3242d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -11,6 +11,26 @@ "shell:allow-execute", "shell:allow-spawn", "shell:allow-open", + { + "identifier": "shell:allow-execute", + "allow": [ + { + "name": "claude-code", + "sidecar": true, + "args": true + } + ] + }, + { + "identifier": "shell:allow-spawn", + "allow": [ + { + "name": "claude-code", + "sidecar": true, + "args": true + } + ] + }, "fs:default", "fs:allow-mkdir", "fs:allow-read", diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index f5b8a85..0b7691e 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -3,38 +3,59 @@ use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; /// Shared module for detecting Claude Code binary installations -/// Supports NVM installations, aliased paths, and version-based selection +/// Supports NVM installations, aliased paths, version-based selection, and bundled sidecars use std::path::PathBuf; use std::process::Command; use tauri::Manager; +/// Type of Claude installation +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum InstallationType { + /// Bundled sidecar binary (preferred) + Bundled, + /// System-installed binary + System, + /// Custom path specified by user + Custom, +} + /// Represents a Claude installation with metadata #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClaudeInstallation { - /// Full path to the Claude binary + /// Full path to the Claude binary (or "claude-code" for sidecar) pub path: String, /// Version string if available pub version: Option, - /// Source of discovery (e.g., "nvm", "system", "homebrew", "which") + /// Source of discovery (e.g., "nvm", "system", "homebrew", "which", "bundled") pub source: String, + /// Type of installation + pub installation_type: InstallationType, } /// Main function to find the Claude binary -/// Checks database first, then discovers all installations and selects the best one +/// Checks database first for stored path and preference, then prioritizes accordingly pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result { info!("Searching for claude binary..."); - // First check if we have a stored path in the database + // First check if we have a stored path and preference in the database if let Ok(app_data_dir) = app_handle.path().app_data_dir() { let db_path = app_data_dir.join("agents.db"); if db_path.exists() { if let Ok(conn) = rusqlite::Connection::open(&db_path) { + // Check for stored path first if let Ok(stored_path) = conn.query_row( "SELECT value FROM app_settings WHERE key = 'claude_binary_path'", [], |row| row.get::<_, String>(0), ) { info!("Found stored claude path in database: {}", stored_path); + + // If it's a sidecar reference, return it directly + if stored_path == "claude-code" { + return Ok(stored_path); + } + + // Otherwise check if the path still exists let path_buf = PathBuf::from(&stored_path); if path_buf.exists() && path_buf.is_file() { return Ok(stored_path); @@ -42,12 +63,33 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result(0), + ).unwrap_or_else(|_| "bundled".to_string()); + + info!("User preference for Claude installation: {}", preference); + + // If user prefers bundled and it's available, use it + if preference == "bundled" && is_sidecar_available(app_handle) { + info!("Using bundled Claude Code sidecar per user preference"); + return Ok("claude-code".to_string()); + } } } } - // Discover all available installations - let installations = discover_all_installations(); + // Check for bundled sidecar (if no preference or bundled preferred) + if is_sidecar_available(app_handle) { + info!("Found bundled Claude Code sidecar"); + return Ok("claude-code".to_string()); + } + + // Discover all available system installations + let installations = discover_system_installations(); if installations.is_empty() { error!("Could not find claude binary in any location"); @@ -71,39 +113,77 @@ pub fn find_claude_binary(app_handle: &tauri::AppHandle) -> Result bool { + // Try to create a sidecar command to test availability + use tauri_plugin_shell::ShellExt; + + match app_handle.shell().sidecar("claude-code") { + Ok(_) => { + debug!("Bundled Claude Code sidecar is available"); + true + } + Err(e) => { + debug!("Bundled Claude Code sidecar not available: {}", e); + false + } + } +} + /// Discovers all available Claude installations and returns them for selection /// This allows UI to show a version selector pub fn discover_claude_installations() -> Vec { info!("Discovering all Claude installations..."); - let installations = discover_all_installations(); + let mut installations = Vec::new(); - // Sort by version (highest first), then by source preference - let mut sorted = installations; - sorted.sort_by(|a, b| { - match (&a.version, &b.version) { - (Some(v1), Some(v2)) => { - // Compare versions in descending order (newest first) - match compare_versions(v2, v1) { - Ordering::Equal => { - // If versions are equal, prefer by source - source_preference(a).cmp(&source_preference(b)) + // Always add bundled sidecar as first option if available + // We can't easily check version for sidecar without spawning it, so we'll mark it as bundled + installations.push(ClaudeInstallation { + path: "claude-code".to_string(), + version: None, // Version will be determined at runtime + source: "bundled".to_string(), + installation_type: InstallationType::Bundled, + }); + + // Add system installations + installations.extend(discover_system_installations()); + + // Sort by installation type (Bundled first), then by version (highest first), then by source preference + installations.sort_by(|a, b| { + // First sort by installation type (Bundled comes first) + match (&a.installation_type, &b.installation_type) { + (InstallationType::Bundled, InstallationType::Bundled) => Ordering::Equal, + (InstallationType::Bundled, _) => Ordering::Less, + (_, InstallationType::Bundled) => Ordering::Greater, + _ => { + // For non-bundled installations, sort by version then source + match (&a.version, &b.version) { + (Some(v1), Some(v2)) => { + // Compare versions in descending order (newest first) + match compare_versions(v2, v1) { + Ordering::Equal => { + // If versions are equal, prefer by source + source_preference(a).cmp(&source_preference(b)) + } + other => other, + } } - other => other, + (Some(_), None) => Ordering::Less, // Version comes before no version + (None, Some(_)) => Ordering::Greater, + (None, None) => source_preference(a).cmp(&source_preference(b)), } } - (Some(_), None) => Ordering::Less, // Version comes before no version - (None, Some(_)) => Ordering::Greater, - (None, None) => source_preference(a).cmp(&source_preference(b)), } }); - sorted + installations } /// Returns a preference score for installation sources (lower is better) fn source_preference(installation: &ClaudeInstallation) -> u8 { match installation.source.as_str() { + "bundled" => 0, // Bundled sidecar has highest preference "which" => 1, "homebrew" => 2, "system" => 3, @@ -120,8 +200,8 @@ fn source_preference(installation: &ClaudeInstallation) -> u8 { } } -/// Discovers all Claude installations on the system -fn discover_all_installations() -> Vec { +/// Discovers all Claude system installations on the system (excludes bundled sidecar) +fn discover_system_installations() -> Vec { let mut installations = Vec::new(); // 1. Try 'which' command first (now works in production) @@ -179,6 +259,7 @@ fn try_which_command() -> Option { path, version, source: "which".to_string(), + installation_type: InstallationType::System, }) } _ => None, @@ -215,6 +296,7 @@ fn find_nvm_installations() -> Vec { path: path_str, version, source: format!("nvm ({})", node_version), + installation_type: InstallationType::System, }); } } @@ -283,6 +365,7 @@ fn find_standard_installations() -> Vec { path, version, source, + installation_type: InstallationType::System, }); } } @@ -297,6 +380,7 @@ fn find_standard_installations() -> Vec { path: "claude".to_string(), version, source: "PATH".to_string(), + installation_type: InstallationType::System, }); } } diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 39e9b93..32925d5 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -1392,7 +1392,20 @@ pub async fn get_claude_binary_path(db: State<'_, AgentDb>) -> Result, path: String) -> Result<(), String> { let conn = db.0.lock().map_err(|e| e.to_string())?; - // Validate that the path exists and is executable + // Special handling for bundled sidecar reference + if path == "claude-code" { + // For bundled sidecar, we don't need to validate file existence + // as it's handled by Tauri's sidecar system + conn.execute( + "INSERT INTO app_settings (key, value) VALUES ('claude_binary_path', ?1) + ON CONFLICT(key) DO UPDATE SET value = ?1", + params![path], + ) + .map_err(|e| format!("Failed to save Claude binary path: {}", e))?; + return Ok(()); + } + + // Validate that the path exists and is executable for system installations let path_buf = std::path::PathBuf::from(&path); if !path_buf.exists() { return Err(format!("File does not exist: {}", path)); @@ -1489,6 +1502,26 @@ fn create_command_with_env(program: &str) -> Command { tokio_cmd.env("PATH", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"); } + // BEGIN PATCH: Ensure bundled sidecar directory is in PATH when using the "claude-code" placeholder + if program == "claude-code" { + // Attempt to locate the sidecar binaries directory that Tauri uses during development + // At compile-time, CARGO_MANIFEST_DIR resolves to the absolute path of the src-tauri crate. + // The sidecar binaries live in /binaries. + #[allow(clippy::redundant_clone)] + let sidecar_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("binaries"); + if sidecar_dir.exists() { + if let Some(sidecar_dir_str) = sidecar_dir.to_str() { + let current_path = std::env::var("PATH").unwrap_or_default(); + let separator = if cfg!(target_os = "windows") { ";" } else { ":" }; + if !current_path.split(separator).any(|p| p == sidecar_dir_str) { + let new_path = format!("{}{}{}", sidecar_dir_str, separator, current_path); + tokio_cmd.env("PATH", new_path); + } + } + } + } + // END PATCH + tokio_cmd } diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index 74f2a20..ce50ce4 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -9,6 +9,8 @@ use std::time::SystemTime; use tauri::{AppHandle, Emitter, Manager}; use tokio::process::{Child, Command}; use tokio::sync::Mutex; +use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::process::CommandEvent; /// Global state to track current Claude process pub struct ClaudeProcessState { @@ -263,6 +265,51 @@ fn create_command_with_env(program: &str) -> Command { tokio_cmd } +/// Determines whether to use sidecar or system binary execution +fn should_use_sidecar(claude_path: &str) -> bool { + claude_path == "claude-code" +} + +/// Creates a sidecar command with the given arguments +fn create_sidecar_command( + app: &AppHandle, + args: Vec, + project_path: &str, +) -> Result { + let mut sidecar_cmd = app + .shell() + .sidecar("claude-code") + .map_err(|e| format!("Failed to create sidecar command: {}", e))?; + + // Add all arguments + sidecar_cmd = sidecar_cmd.args(args); + + // Set working directory + sidecar_cmd = sidecar_cmd.current_dir(project_path); + + Ok(sidecar_cmd) +} + +/// Creates a system binary command with the given arguments +fn create_system_command( + claude_path: &str, + args: Vec, + project_path: &str, +) -> Command { + let mut cmd = create_command_with_env(claude_path); + + // Add all arguments + for arg in args { + cmd.arg(arg); + } + + cmd.current_dir(project_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + cmd +} + /// Lists all projects in the ~/.claude/projects directory #[tauri::command] pub async fn list_projects() -> Result, String> { @@ -530,6 +577,21 @@ pub async fn check_claude_version(app: AppHandle) -> Result