From 9b4777978e566b5b77080179ec0e71fd03ebf7e1 Mon Sep 17 00:00:00 2001 From: Mufeed VH Date: Fri, 4 Jul 2025 18:25:06 +0530 Subject: [PATCH] feat(version): enhance version detection and display across sidecar and binary installations - Implement regex-based version extraction for more reliable parsing - Add version detection for bundled sidecar installations with proper execution - Improve version checking for sidecar binary with timeout and error handling - Update UI to display version with "v" prefix for consistency - Add comprehensive logging for version detection debugging - Handle edge cases in version extraction with fallback mechanisms --- src-tauri/src/claude_binary.rs | 33 ++++++--- src-tauri/src/commands/agents.rs | 75 +++++++++++++++++++- src-tauri/src/commands/claude.rs | 113 ++++++++++++++++++++++++++----- src/components/Topbar.tsx | 2 +- 4 files changed, 194 insertions(+), 29 deletions(-) diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 0b7691e..36e88dc 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -408,15 +408,30 @@ fn get_claude_version(path: &str) -> Result, String> { /// Extract version string from command output fn extract_version_from_output(stdout: &[u8]) -> Option { let output_str = String::from_utf8_lossy(stdout); - - // Extract version: first token before whitespace that looks like a version - output_str - .split_whitespace() - .find(|token| { - // Version usually contains dots and numbers - token.chars().any(|c| c == '.') && token.chars().any(|c| c.is_numeric()) - }) - .map(|s| s.to_string()) + + // Debug log the raw output + debug!("Raw version output: {:?}", output_str); + + // Use regex to directly extract version pattern (e.g., "1.0.41") + // This pattern matches: + // - One or more digits, followed by + // - A dot, followed by + // - One or more digits, followed by + // - A dot, followed by + // - One or more digits + // - Optionally followed by pre-release/build metadata + let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok()?; + + if let Some(captures) = version_regex.captures(&output_str) { + if let Some(version_match) = captures.get(1) { + let version = version_match.as_str().to_string(); + debug!("Extracted version: {:?}", version); + return Some(version); + } + } + + debug!("No version found in output"); + None } /// Select the best installation based on version diff --git a/src-tauri/src/commands/agents.rs b/src-tauri/src/commands/agents.rs index 9a49220..3ad4a2e 100644 --- a/src-tauri/src/commands/agents.rs +++ b/src-tauri/src/commands/agents.rs @@ -11,6 +11,7 @@ use tauri::{AppHandle, Emitter, Manager, State}; use tauri_plugin_shell::ShellExt; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; +use regex; /// Finds the full path to the claude binary /// This is necessary because macOS apps have a limited PATH environment @@ -1697,13 +1698,85 @@ pub async fn set_claude_binary_path(db: State<'_, AgentDb>, path: String) -> Res /// List all available Claude installations on the system #[tauri::command] pub async fn list_claude_installations( + app: AppHandle, ) -> Result, String> { - let installations = crate::claude_binary::discover_claude_installations(); + let mut installations = crate::claude_binary::discover_claude_installations(); if installations.is_empty() { return Err("No Claude Code installations found on the system".to_string()); } + // For bundled installations, execute the sidecar to get the actual version + for installation in &mut installations { + if installation.installation_type == crate::claude_binary::InstallationType::Bundled { + // Try to get the version by executing the sidecar + use tauri_plugin_shell::process::CommandEvent; + + // Create a temporary directory for the sidecar to run in + let temp_dir = std::env::temp_dir(); + + // Create sidecar command with --version flag + let sidecar_cmd = match app + .shell() + .sidecar("claude-code") { + Ok(cmd) => cmd.args(["--version"]).current_dir(&temp_dir), + Err(e) => { + log::warn!("Failed to create sidecar command for version check: {}", e); + continue; + } + }; + + // Spawn the sidecar and collect output + match sidecar_cmd.spawn() { + Ok((mut rx, _child)) => { + let mut stdout_output = String::new(); + let mut stderr_output = String::new(); + + // Set a timeout for version check + let timeout = tokio::time::Duration::from_secs(5); + let start_time = tokio::time::Instant::now(); + + while let Ok(Some(event)) = tokio::time::timeout_at( + start_time + timeout, + rx.recv() + ).await { + match event { + CommandEvent::Stdout(data) => { + stdout_output.push_str(&String::from_utf8_lossy(&data)); + } + CommandEvent::Stderr(data) => { + stderr_output.push_str(&String::from_utf8_lossy(&data)); + } + CommandEvent::Terminated { .. } => { + break; + } + CommandEvent::Error(e) => { + log::warn!("Error during sidecar version check: {}", e); + break; + } + _ => {} + } + } + + // Use regex to directly extract version pattern + let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok(); + + if let Some(regex) = version_regex { + if let Some(captures) = regex.captures(&stdout_output) { + if let Some(version_match) = captures.get(1) { + installation.version = Some(version_match.as_str().to_string()); + log::info!("Bundled sidecar version: {}", version_match.as_str()); + } + } + } + } + Err(e) => { + log::warn!("Failed to spawn sidecar for version check: {}", e); + } + } + } + } + Ok(installations) } diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index ce50ce4..7478246 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -11,6 +11,7 @@ use tokio::process::{Child, Command}; use tokio::sync::Mutex; use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::process::CommandEvent; +use regex; /// Global state to track current Claude process pub struct ClaudeProcessState { @@ -577,17 +578,89 @@ pub async fn check_claude_version(app: AppHandle) -> Result cmd.args(["--version"]).current_dir(&temp_dir), + Err(e) => { + log::error!("Failed to create sidecar command: {}", e); + return Ok(ClaudeVersionStatus { + is_installed: true, // We know it exists, just couldn't create command + version: None, + output: format!("Using bundled Claude Code sidecar (command creation failed: {})", e), + }); + } + }; + + // Spawn the sidecar and collect output + match sidecar_cmd.spawn() { + Ok((mut rx, _child)) => { + let mut stdout_output = String::new(); + let mut stderr_output = String::new(); + let mut exit_success = false; + + // Collect output from the sidecar + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(data) => { + let line = String::from_utf8_lossy(&data); + stdout_output.push_str(&line); + } + CommandEvent::Stderr(data) => { + let line = String::from_utf8_lossy(&data); + stderr_output.push_str(&line); + } + CommandEvent::Terminated(payload) => { + exit_success = payload.code.unwrap_or(-1) == 0; + break; + } + _ => {} + } + } + + // Use regex to directly extract version pattern (e.g., "1.0.41") + let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok(); + + let version = if let Some(regex) = version_regex { + regex.captures(&stdout_output) + .and_then(|captures| captures.get(1)) + .map(|m| m.as_str().to_string()) + } else { + None + }; + + let full_output = if stderr_output.is_empty() { + stdout_output.clone() + } else { + format!("{}\n{}", stdout_output, stderr_output) + }; + + // Check if the output matches the expected format + let is_valid = stdout_output.contains("(Claude Code)") || stdout_output.contains("Claude Code") || version.is_some(); + + return Ok(ClaudeVersionStatus { + is_installed: is_valid && exit_success, + version, + output: full_output.trim().to_string(), + }); + } + Err(e) => { + log::error!("Failed to execute sidecar: {}", e); + return Ok(ClaudeVersionStatus { + is_installed: true, // We know it exists, just couldn't get version + version: None, + output: format!("Using bundled Claude Code sidecar (version check failed: {})", e), + }); + } + } } use log::debug;debug!("Claude path: {}", claude_path); @@ -622,6 +695,18 @@ pub async fn check_claude_version(app: AppHandle) -> Result { let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + // Use regex to directly extract version pattern (e.g., "1.0.41") + let version_regex = regex::Regex::new(r"(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)").ok(); + + let version = if let Some(regex) = version_regex { + regex.captures(&stdout) + .and_then(|captures| captures.get(1)) + .map(|m| m.as_str().to_string()) + } else { + None + }; + let full_output = if stderr.is_empty() { stdout.clone() } else { @@ -632,14 +717,6 @@ pub async fn check_claude_version(app: AppHandle) -> Result = ({ /> {versionStatus.is_installed && versionStatus.version - ? `Claude Code ${versionStatus.version}` + ? `Claude Code v${versionStatus.version}` : "Claude Code"}