From 564c9d77f649da3bb0f3edb92bfc2ab7ad5ca54e Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Tue, 16 Sep 2025 17:14:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AF=BC=E8=88=AA=E6=A0=8Fcl?= =?UTF-8?q?aude=E7=89=88=E6=9C=AC=E4=B8=8D=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/claude_binary.rs | 110 ++++++++++++++++++++++++++-- src-tauri/src/commands/claude.rs | 120 ++++++++----------------------- src/App.tsx | 28 +++++--- 3 files changed, 151 insertions(+), 107 deletions(-) diff --git a/src-tauri/src/claude_binary.rs b/src-tauri/src/claude_binary.rs index 7ca336c..031457f 100644 --- a/src-tauri/src/claude_binary.rs +++ b/src-tauri/src/claude_binary.rs @@ -202,7 +202,16 @@ fn find_which_installations() -> Vec { let mut installations = Vec::new(); - match Command::new(command_name).arg("claude").output() { + // Create command with enhanced PATH for production environments + let mut cmd = Command::new(command_name); + cmd.arg("claude"); + + // In production (DMG), we need to ensure proper PATH is set + let enhanced_path = build_enhanced_path(); + debug!("Using enhanced PATH for {}: {}", command_name, enhanced_path); + cmd.env("PATH", enhanced_path); + + match cmd.output() { Ok(output) if output.status.success() => { let output_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); @@ -401,7 +410,11 @@ fn find_standard_installations() -> Vec { } // Also check if claude is available in PATH (without full path) - if let Ok(output) = Command::new("claude").arg("--version").output() { + let mut path_cmd = Command::new("claude"); + path_cmd.arg("--version"); + path_cmd.env("PATH", build_enhanced_path()); + + if let Ok(output) = path_cmd.output() { if output.status.success() { debug!("claude is available in PATH"); // Combine stdout and stderr for robust version extraction @@ -427,7 +440,11 @@ fn find_standard_installations() -> Vec { /// Get Claude version by running --version command fn get_claude_version(path: &str) -> Result, String> { - match Command::new(path).arg("--version").output() { + // Use the helper function to create command with proper environment + let mut cmd = create_command_with_env(path); + cmd.arg("--version"); + + match cmd.output() { Ok(output) => { if output.status.success() { // Combine stdout and stderr for robust version extraction @@ -556,11 +573,15 @@ pub fn create_command_with_env(program: &str) -> Command { info!("Creating command for: {}", program); + // Build enhanced PATH for production environments (DMG/App Bundle) + let enhanced_path = build_enhanced_path(); + debug!("Enhanced PATH: {}", enhanced_path); + cmd.env("PATH", enhanced_path.clone()); + // Inherit essential environment variables from parent process for (key, value) in std::env::vars() { - // Pass through PATH and other essential environment variables - if key == "PATH" - || key == "HOME" + // Pass through essential environment variables (excluding PATH which we set above) + if key == "HOME" || key == "USER" || key == "SHELL" || key == "LANG" @@ -595,7 +616,12 @@ pub fn create_command_with_env(program: &str) -> Command { if program.contains("/.nvm/versions/node/") { if let Some(node_bin_dir) = std::path::Path::new(program).parent() { // Ensure the Node.js bin directory is in PATH - let current_path = std::env::var("PATH").unwrap_or_default(); + let current_path = cmd.get_envs() + .find(|(k, _)| k.to_str() == Some("PATH")) + .and_then(|(_, v)| v) + .and_then(|v| v.to_str()) + .unwrap_or(&enhanced_path) + .to_string(); let node_bin_str = node_bin_dir.to_string_lossy(); if !current_path.contains(&node_bin_str.as_ref()) { let new_path = format!("{}:{}", node_bin_str, current_path); @@ -607,3 +633,73 @@ pub fn create_command_with_env(program: &str) -> Command { cmd } + +/// Build an enhanced PATH that includes all possible Claude installation locations +/// This is especially important for DMG/packaged applications where PATH may be limited +fn build_enhanced_path() -> String { + let mut paths = Vec::new(); + + // Start with current PATH + if let Ok(current_path) = std::env::var("PATH") { + paths.push(current_path); + } + + // Add standard system paths that might be missing in packaged apps + let system_paths = vec![ + "/usr/local/bin", + "/usr/bin", + "/bin", + "/opt/homebrew/bin", + "/opt/homebrew/sbin", + ]; + + for path in system_paths { + if PathBuf::from(path).exists() { + paths.push(path.to_string()); + } + } + + // Add user-specific paths + if let Ok(home) = std::env::var("HOME") { + let user_paths = vec![ + format!("{}/.local/bin", home), + format!("{}/.claude/local", home), + format!("{}/.npm-global/bin", home), + format!("{}/.yarn/bin", home), + format!("{}/.bun/bin", home), + format!("{}/bin", home), + format!("{}/.config/yarn/global/node_modules/.bin", home), + format!("{}/node_modules/.bin", home), + ]; + + for path in user_paths { + if PathBuf::from(&path).exists() { + paths.push(path); + } + } + + // Add all NVM node versions + let nvm_dir = PathBuf::from(&home).join(".nvm/versions/node"); + if nvm_dir.exists() { + if let Ok(entries) = std::fs::read_dir(&nvm_dir) { + for entry in entries.flatten() { + if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { + let bin_path = entry.path().join("bin"); + if bin_path.exists() { + paths.push(bin_path.to_string_lossy().to_string()); + } + } + } + } + } + } + + // Remove duplicates while preserving order + let mut seen = std::collections::HashSet::new(); + let unique_paths: Vec = paths + .into_iter() + .filter(|path| seen.insert(path.clone())) + .collect(); + + unique_paths.join(":") +} diff --git a/src-tauri/src/commands/claude.rs b/src-tauri/src/commands/claude.rs index b402c5c..1ccee31 100644 --- a/src-tauri/src/commands/claude.rs +++ b/src-tauri/src/commands/claude.rs @@ -623,102 +623,40 @@ pub async fn get_system_prompt() -> Result { /// Checks if Claude Code is installed and gets its version #[tauri::command] -pub async fn check_claude_version(app: AppHandle) -> Result { +pub async fn check_claude_version(_app: AppHandle) -> Result { log::info!("Checking Claude Code version"); - let claude_path = match find_claude_binary(&app) { - Ok(path) => path, - Err(e) => { - return Ok(ClaudeVersionStatus { - is_installed: false, - version: None, - output: e, - }); - } - }; - - use log::debug;debug!("Claude path: {}", claude_path); - - // In production builds, we can't check the version directly - #[cfg(not(debug_assertions))] - { - log::warn!("Cannot check claude version in production build"); - // If we found a path (either stored or in common locations), assume it's installed - if claude_path != "claude" && PathBuf::from(&claude_path).exists() { - return Ok(ClaudeVersionStatus { - is_installed: true, - version: None, - output: "Claude binary found at: ".to_string() + &claude_path, - }); - } else { - return Ok(ClaudeVersionStatus { - is_installed: false, - version: None, - output: "Cannot verify Claude installation in production build. Please ensure Claude Code is installed.".to_string(), - }); - } + // Try to find Claude installations with versions + let installations = crate::claude_binary::discover_claude_installations(); + + if installations.is_empty() { + return Ok(ClaudeVersionStatus { + is_installed: false, + version: None, + output: "Claude Code not found. Please ensure it's installed.".to_string(), + }); } - #[cfg(debug_assertions)] - { - let output = std::process::Command::new(claude_path) - .arg("--version") - .output(); - - match output { - Ok(output) => { - 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(); - - // Combine stdout and stderr for version extraction (some tools write version to stderr) - let mut version_src = stdout.clone(); - if !stderr.is_empty() { - version_src.push('\n'); - version_src.push_str(&stderr); - } - - let version = if let Some(regex) = version_regex { - regex - .captures(&version_src) - .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 { - format!("{}\n{}", stdout, stderr) - }; - - // Check if the output matches the expected format - // Expected format: "1.0.17 (Claude Code)" or similar - let is_valid = - stdout.contains("(Claude Code)") - || stdout.contains("Claude Code") - || stderr.contains("(Claude Code)") - || stderr.contains("Claude Code"); - - Ok(ClaudeVersionStatus { - is_installed: is_valid && output.status.success(), - version, - output: full_output.trim().to_string(), - }) + // Find the best installation (highest version or first found) + let best_installation = installations + .into_iter() + .max_by(|a, b| { + match (&a.version, &b.version) { + (Some(v1), Some(v2)) => v1.cmp(v2), + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + (None, None) => std::cmp::Ordering::Equal, } - Err(e) => { - log::error!("Failed to run claude command: {}", e); - Ok(ClaudeVersionStatus { - is_installed: false, - version: None, - output: format!("Command not found: {}", e), - }) - } - } - } + }) + .unwrap(); // Safe because we checked is_empty() above + + log::info!("Found Claude installation: {:?}", best_installation); + + Ok(ClaudeVersionStatus { + is_installed: true, + version: best_installation.version, + output: format!("Claude binary found at: {}", best_installation.path), + }) } /// Saves the CLAUDE.md system prompt file diff --git a/src/App.tsx b/src/App.tsx index 0796a15..98ebbda 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, lazy, Suspense } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Plus, Loader2, ArrowLeft } from "lucide-react"; import { api, type Project, type Session, type ClaudeMdFile } from "@/lib/api"; @@ -10,12 +10,8 @@ import { ProjectList } from "@/components/ProjectList"; import { SessionList } from "@/components/SessionList"; import { RunningClaudeSessions } from "@/components/RunningClaudeSessions"; import { Topbar } from "@/components/Topbar"; -import { MarkdownEditor } from "@/components/MarkdownEditor"; import { ClaudeFileEditor } from "@/components/ClaudeFileEditor"; -import { Settings } from "@/components/Settings"; import { CCAgents } from "@/components/CCAgents"; -import { UsageDashboard } from "@/components/UsageDashboard"; -import { MCPManager } from "@/components/MCPManager"; import { NFOCredits } from "@/components/NFOCredits"; import { ClaudeBinaryDialog } from "@/components/ClaudeBinaryDialog"; import { Toast, ToastContainer } from "@/components/ui/toast"; @@ -32,6 +28,12 @@ import RelayStationManager from "@/components/RelayStationManager"; import { CcrRouterManager } from "@/components/CcrRouterManager"; import i18n from "@/lib/i18n"; +// Lazy load these components to match TabContent's dynamic imports +const MarkdownEditor = lazy(() => import('@/components/MarkdownEditor').then(m => ({ default: m.MarkdownEditor }))); +const Settings = lazy(() => import('@/components/Settings').then(m => ({ default: m.Settings }))); +const UsageDashboard = lazy(() => import('@/components/UsageDashboard').then(m => ({ default: m.UsageDashboard }))); +const MCPManager = lazy(() => import('@/components/MCPManager').then(m => ({ default: m.MCPManager }))); + type View = | "welcome" | "projects" @@ -299,14 +301,18 @@ function AppContent() { case "editor": return (
- handleViewChange("welcome")} /> +
}> + handleViewChange("welcome")} /> + ); case "settings": return (
- handleViewChange("welcome")} /> +
}> + handleViewChange("welcome")} /> + ); @@ -459,12 +465,16 @@ function AppContent() { case "usage-dashboard": return ( - handleViewChange("welcome")} /> + }> + handleViewChange("welcome")} /> + ); case "mcp": return ( - handleViewChange("welcome")} /> + }> + handleViewChange("welcome")} /> + ); case "project-settings":