Skip to main content

xchecker_runner/claude/
detect.rs

1use crate::command_spec::CommandSpec;
2use crate::error::RunnerError;
3use crate::types::RunnerMode;
4use std::process::Stdio;
5
6use super::exec::Runner;
7
8impl Runner {
9    /// Detect the best runner mode automatically
10    ///
11    /// On Windows:
12    /// 1. Try `claude --version` on PATH -> Native if succeeds
13    /// 2. Else try `wsl -e claude --version` -> WSL if returns 0
14    /// 3. Else: friendly preflight error suggesting `wsl --install` if needed
15    ///
16    /// On Linux/macOS: always Native
17    pub fn detect_auto() -> Result<RunnerMode, RunnerError> {
18        // On non-Windows platforms, always use native
19        if !cfg!(target_os = "windows") {
20            return Ok(RunnerMode::Native);
21        }
22
23        // On Windows, try native first
24        if Self::test_native_claude().is_ok() {
25            return Ok(RunnerMode::Native);
26        }
27
28        // Try WSL as fallback on Windows
29        match Self::test_wsl_claude() {
30            Ok(()) => Ok(RunnerMode::Wsl),
31            Err(_) => {
32                // Neither native nor WSL worked
33                Err(RunnerError::DetectionFailed {
34                    reason: "Claude CLI not found in Windows PATH and WSL is not available or doesn't have Claude installed".to_string(),
35                })
36            }
37        }
38    }
39
40    /// Test if native Claude CLI is available
41    pub fn test_native_claude() -> Result<(), RunnerError> {
42        // Use CommandSpec for consistent argv-style execution
43        let output = CommandSpec::new("claude")
44            .arg("--version")
45            .to_command()
46            .stdout(Stdio::piped())
47            .stderr(Stdio::piped())
48            .output()
49            .map_err(|e| RunnerError::NativeExecutionFailed {
50                reason: format!("Failed to execute 'claude --version': {e}"),
51            })?;
52
53        if output.status.success() {
54            Ok(())
55        } else {
56            Err(RunnerError::NativeExecutionFailed {
57                reason: format!(
58                    "'claude --version' failed with exit code: {}",
59                    output.status.code().unwrap_or(-1)
60                ),
61            })
62        }
63    }
64
65    /// Test if WSL Claude CLI is available
66    pub fn test_wsl_claude() -> Result<(), RunnerError> {
67        // Use CommandSpec for consistent argv-style execution
68        let output = CommandSpec::new("wsl")
69            .args(["-e", "claude", "--version"])
70            .to_command()
71            .stdout(Stdio::piped())
72            .stderr(Stdio::piped())
73            .output()
74            .map_err(|e| RunnerError::WslNotAvailable {
75                reason: format!("Failed to execute 'wsl -e claude --version': {e}"),
76            })?;
77
78        if output.status.success() {
79            Ok(())
80        } else {
81            Err(RunnerError::WslExecutionFailed {
82                reason: format!(
83                    "'wsl -e claude --version' failed with exit code: {}",
84                    output.status.code().unwrap_or(-1)
85                ),
86            })
87        }
88    }
89
90    /// Validate the runner configuration
91    pub fn validate(&self) -> Result<(), RunnerError> {
92        match self.mode {
93            RunnerMode::Auto => {
94                // Auto mode validation happens during detection
95                Self::detect_auto().map(|_| ())
96            }
97            RunnerMode::Native => self.test_native_claude_with_path(),
98            RunnerMode::Wsl => {
99                // Validate WSL is available
100                if cfg!(target_os = "windows") {
101                    Self::test_wsl_claude()
102                } else {
103                    Err(RunnerError::ConfigurationInvalid {
104                        reason: "WSL runner mode is only supported on Windows".to_string(),
105                    })
106                }
107            }
108        }
109    }
110
111    /// Get a user-friendly description of the runner configuration
112    #[must_use]
113    #[allow(dead_code)] // Runner introspection utility
114    pub fn description(&self) -> String {
115        match self.mode {
116            RunnerMode::Auto => {
117                "Automatic detection (native first, then WSL on Windows)".to_string()
118            }
119            RunnerMode::Native => {
120                let mut desc = "Native execution (spawn claude directly)".to_string();
121                if let Some(claude_path) = &self.wsl_options.claude_path {
122                    desc.push_str(&format!(" (claude path: {claude_path})"));
123                }
124                desc
125            }
126            RunnerMode::Wsl => {
127                let mut desc = "WSL execution".to_string();
128                if let Some(distro) = &self.wsl_options.distro {
129                    desc.push_str(&format!(" (distro: {distro})"));
130                }
131                if let Some(claude_path) = &self.wsl_options.claude_path {
132                    desc.push_str(&format!(" (claude path: {claude_path})"));
133                }
134                desc
135            }
136        }
137    }
138
139    fn test_native_claude_with_path(&self) -> Result<(), RunnerError> {
140        let output = self
141            .native_command_spec(&["--version".to_string()])
142            .to_command()
143            .stdout(Stdio::piped())
144            .stderr(Stdio::piped())
145            .output()
146            .map_err(|e| RunnerError::NativeExecutionFailed {
147                reason: format!("Failed to execute 'claude --version': {e}"),
148            })?;
149
150        if output.status.success() {
151            Ok(())
152        } else {
153            Err(RunnerError::NativeExecutionFailed {
154                reason: format!(
155                    "'claude --version' failed with exit code: {}",
156                    output.status.code().unwrap_or(-1)
157                ),
158            })
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::Runner;
166    use crate::claude::WslOptions;
167    use crate::error::RunnerError;
168    use crate::types::RunnerMode;
169
170    #[test]
171    fn test_runner_description() {
172        let runner = Runner::new(RunnerMode::Native, WslOptions::default());
173        assert_eq!(
174            runner.description(),
175            "Native execution (spawn claude directly)"
176        );
177
178        let wsl_options = WslOptions {
179            distro: Some("Ubuntu-22.04".to_string()),
180            claude_path: Some("/usr/local/bin/claude".to_string()),
181        };
182        let runner = Runner::new(RunnerMode::Wsl, wsl_options);
183        assert!(runner.description().contains("WSL execution"));
184        assert!(runner.description().contains("Ubuntu-22.04"));
185        assert!(runner.description().contains("/usr/local/bin/claude"));
186    }
187
188    #[cfg(not(target_os = "windows"))]
189    #[test]
190    fn test_auto_detection_non_windows() {
191        // On non-Windows platforms, auto detection should always return Native
192        let result = Runner::detect_auto();
193        assert!(result.is_ok());
194        assert_eq!(result.unwrap(), RunnerMode::Native);
195    }
196
197    #[test]
198    fn test_wsl_validation_on_non_windows() {
199        if !cfg!(target_os = "windows") {
200            let runner = Runner::new(RunnerMode::Wsl, WslOptions::default());
201            let result = runner.validate();
202            assert!(result.is_err());
203            if let Err(RunnerError::ConfigurationInvalid { reason }) = result {
204                assert!(reason.contains("only supported on Windows"));
205            } else {
206                panic!("Expected ConfigurationInvalid error");
207            }
208        }
209    }
210}