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
This commit is contained in:
@@ -409,14 +409,29 @@ fn get_claude_version(path: &str) -> Result<Option<String>, String> {
|
|||||||
fn extract_version_from_output(stdout: &[u8]) -> Option<String> {
|
fn extract_version_from_output(stdout: &[u8]) -> Option<String> {
|
||||||
let output_str = String::from_utf8_lossy(stdout);
|
let output_str = String::from_utf8_lossy(stdout);
|
||||||
|
|
||||||
// Extract version: first token before whitespace that looks like a version
|
// Debug log the raw output
|
||||||
output_str
|
debug!("Raw version output: {:?}", output_str);
|
||||||
.split_whitespace()
|
|
||||||
.find(|token| {
|
// Use regex to directly extract version pattern (e.g., "1.0.41")
|
||||||
// Version usually contains dots and numbers
|
// This pattern matches:
|
||||||
token.chars().any(|c| c == '.') && token.chars().any(|c| c.is_numeric())
|
// - One or more digits, followed by
|
||||||
})
|
// - A dot, followed by
|
||||||
.map(|s| s.to_string())
|
// - 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
|
/// Select the best installation based on version
|
||||||
|
@@ -11,6 +11,7 @@ use tauri::{AppHandle, Emitter, Manager, State};
|
|||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
use regex;
|
||||||
|
|
||||||
/// Finds the full path to the claude binary
|
/// Finds the full path to the claude binary
|
||||||
/// This is necessary because macOS apps have a limited PATH environment
|
/// 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
|
/// List all available Claude installations on the system
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_claude_installations(
|
pub async fn list_claude_installations(
|
||||||
|
app: AppHandle,
|
||||||
) -> Result<Vec<crate::claude_binary::ClaudeInstallation>, String> {
|
) -> Result<Vec<crate::claude_binary::ClaudeInstallation>, String> {
|
||||||
let installations = crate::claude_binary::discover_claude_installations();
|
let mut installations = crate::claude_binary::discover_claude_installations();
|
||||||
|
|
||||||
if installations.is_empty() {
|
if installations.is_empty() {
|
||||||
return Err("No Claude Code installations found on the system".to_string());
|
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)
|
Ok(installations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@ use tokio::process::{Child, Command};
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
use tauri_plugin_shell::process::CommandEvent;
|
use tauri_plugin_shell::process::CommandEvent;
|
||||||
|
use regex;
|
||||||
|
|
||||||
/// Global state to track current Claude process
|
/// Global state to track current Claude process
|
||||||
pub struct ClaudeProcessState {
|
pub struct ClaudeProcessState {
|
||||||
@@ -577,18 +578,90 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the selected path is the special sidecar identifier, we cannot execute it directly.
|
// If the selected path is the special sidecar identifier, execute it to get version
|
||||||
// Instead, assume the bundled sidecar is available (find_claude_binary already verified
|
|
||||||
// this) and return a positive status without a version string. Attempting to spawn the
|
|
||||||
// sidecar here would require async streaming plumbing that is over-kill for a simple
|
|
||||||
// presence check and fails in debug builds (os error 2).
|
|
||||||
if claude_path == "claude-code" {
|
if claude_path == "claude-code" {
|
||||||
|
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::error!("Failed to create sidecar command: {}", e);
|
||||||
return Ok(ClaudeVersionStatus {
|
return Ok(ClaudeVersionStatus {
|
||||||
is_installed: true,
|
is_installed: true, // We know it exists, just couldn't create command
|
||||||
version: None,
|
version: None,
|
||||||
output: "Using bundled Claude Code sidecar".to_string(),
|
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);
|
use log::debug;debug!("Claude path: {}", claude_path);
|
||||||
|
|
||||||
@@ -622,6 +695,18 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
|
|||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).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() {
|
let full_output = if stderr.is_empty() {
|
||||||
stdout.clone()
|
stdout.clone()
|
||||||
} else {
|
} else {
|
||||||
@@ -632,14 +717,6 @@ pub async fn check_claude_version(app: AppHandle) -> Result<ClaudeVersionStatus,
|
|||||||
// Expected format: "1.0.17 (Claude Code)" or similar
|
// Expected format: "1.0.17 (Claude Code)" or similar
|
||||||
let is_valid = stdout.contains("(Claude Code)") || stdout.contains("Claude Code");
|
let is_valid = stdout.contains("(Claude Code)") || stdout.contains("Claude Code");
|
||||||
|
|
||||||
// Extract version number if valid
|
|
||||||
let version = if is_valid {
|
|
||||||
// Try to extract just the version number
|
|
||||||
stdout.split_whitespace().next().map(|s| s.to_string())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ClaudeVersionStatus {
|
Ok(ClaudeVersionStatus {
|
||||||
is_installed: is_valid && output.status.success(),
|
is_installed: is_valid && output.status.success(),
|
||||||
version,
|
version,
|
||||||
|
@@ -112,7 +112,7 @@ export const Topbar: React.FC<TopbarProps> = ({
|
|||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{versionStatus.is_installed && versionStatus.version
|
{versionStatus.is_installed && versionStatus.version
|
||||||
? `Claude Code ${versionStatus.version}`
|
? `Claude Code v${versionStatus.version}`
|
||||||
: "Claude Code"}
|
: "Claude Code"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user