use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
const ON_FAILURE_SINCE: Version = (2, 1, 295);
const PROBE_TIMEOUT: Duration = Duration::from_secs(5);
const POLL_INTERVAL: Duration = Duration::from_millis(20);
type Version = (u64, u64, u64);
#[cfg(debug_assertions)]
const VERSION_OVERRIDE: &str = "MOAT_CLAUDE_CODE_VERSION";
pub fn blocks_on_failure() -> bool {
static SUPPORTED: OnceLock<bool> = OnceLock::new();
*SUPPORTED.get_or_init(|| version().is_some_and(supports_on_failure))
}
fn supports_on_failure(version: Version) -> bool {
version >= ON_FAILURE_SINCE
}
fn version() -> Option<Version> {
#[cfg(debug_assertions)]
if let Some(value) = std::env::var_os(VERSION_OVERRIDE) {
return parse(&value.to_string_lossy());
}
let path: Vec<PathBuf> = std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).collect())
.unwrap_or_default();
let claude = crate::environment::find_in(&path, &[], "claude")?;
parse(&run_version(Command::new(claude))?)
}
fn run_version(mut command: Command) -> Option<String> {
let mut child = command
.arg("--version")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let deadline = Instant::now() + PROBE_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(status)) if status.success() => break,
Ok(Some(_)) | Err(_) => return None,
Ok(None) if Instant::now() >= deadline => {
let _ = child.kill();
let _ = child.wait();
return None;
}
Ok(None) => std::thread::sleep(POLL_INTERVAL),
}
}
let mut text = String::new();
std::io::Read::read_to_string(&mut child.stdout.take()?, &mut text).ok()?;
Some(text)
}
fn parse(text: &str) -> Option<Version> {
let first = text.split_whitespace().next()?;
let mut parts = first.trim_start_matches('v').splitn(3, '.');
let number = |part: Option<&str>| -> Option<u64> {
let digits: String = part?.chars().take_while(char::is_ascii_digit).collect();
digits.parse().ok()
};
Some((
number(parts.next())?,
number(parts.next())?,
number(parts.next())?,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_what_claude_version_prints() {
assert_eq!(parse("2.1.295 (Claude Code)\n"), Some((2, 1, 295)));
assert_eq!(parse("v2.2.0"), Some((2, 2, 0)));
assert_eq!(parse("2.1.300-beta.1 (Claude Code)"), Some((2, 1, 300)));
assert_eq!(parse(""), None);
assert_eq!(parse("Claude Code"), None);
assert_eq!(parse("2.1"), None);
}
#[test]
fn only_v2_1_295_and_later_get_on_failure() {
assert!(supports_on_failure((2, 1, 295)));
assert!(supports_on_failure((2, 2, 0)));
assert!(supports_on_failure((3, 0, 0)));
assert!(!supports_on_failure((2, 1, 294)));
assert!(!supports_on_failure((2, 0, 999)));
assert!(!supports_on_failure((1, 9, 400)));
}
}