bamboo_agent/claude/
mod.rs1mod command;
6mod discovery;
7mod runner;
8mod stream_json;
9mod version;
10
11use log::{info, warn};
12use serde::{Deserialize, Serialize};
13use std::cmp::Ordering;
14
15use discovery::discover_system_installations;
16use version::{compare_versions, source_preference};
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub enum InstallationType {
20 System,
21 Custom,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ClaudeInstallation {
26 pub path: String,
27 pub version: Option<String>,
28 pub source: String,
29 pub installation_type: InstallationType,
30}
31
32pub fn find_claude_binary() -> Result<String, String> {
37 info!("Searching for claude binary...");
38
39 let installations = discover_system_installations();
40
41 if installations.is_empty() {
42 warn!("Could not find claude binary in any location");
43 return Err("Claude Code not found. Please ensure it's installed in one of these locations: PATH, /usr/local/bin, /opt/homebrew/bin, ~/.nvm/versions/node/*/bin, ~/.claude/local, ~/.local/bin".to_string());
44 }
45
46 for installation in &installations {
47 info!("Found Claude installation: {:?}", installation);
48 }
49
50 if let Some(best) = select_best_installation(installations) {
51 info!(
52 "Selected Claude installation: path={}, version={:?}, source={}",
53 best.path, best.version, best.source
54 );
55 Ok(best.path)
56 } else {
57 Err("No valid Claude installation found".to_string())
58 }
59}
60
61pub fn try_find_claude_binary() -> Option<String> {
66 find_claude_binary().ok()
67}
68
69pub fn discover_claude_installations() -> Vec<ClaudeInstallation> {
71 info!("Discovering all Claude installations...");
72
73 let mut installations = discover_system_installations();
74
75 installations.sort_by(|a, b| match (&a.version, &b.version) {
76 (Some(v1), Some(v2)) => match compare_versions(v2, v1) {
77 Ordering::Equal => source_preference(a).cmp(&source_preference(b)),
78 other => other,
79 },
80 (Some(_), None) => Ordering::Less,
81 (None, Some(_)) => Ordering::Greater,
82 (None, None) => source_preference(a).cmp(&source_preference(b)),
83 });
84
85 installations
86}
87
88fn select_best_installation(installations: Vec<ClaudeInstallation>) -> Option<ClaudeInstallation> {
89 installations
90 .into_iter()
91 .max_by(|a, b| match (&a.version, &b.version) {
92 (Some(v1), Some(v2)) => compare_versions(v1, v2),
93 (Some(_), None) => Ordering::Greater,
94 (None, Some(_)) => Ordering::Less,
95 (None, None) => {
96 if a.path == "claude" && b.path != "claude" {
97 Ordering::Less
98 } else if a.path != "claude" && b.path == "claude" {
99 Ordering::Greater
100 } else {
101 Ordering::Equal
102 }
103 }
104 })
105}
106
107pub use command::create_command_with_env;
108pub use command::create_tokio_command_with_env;
109pub use runner::{spawn_claude_code_cli, ClaudeCodeCliConfig};
110pub(crate) use stream_json::ClaudeStreamJsonParser;