1use anyhow::{Context, Result};
8
9pub fn detect_cli_version(bin: &str, cache_file: &str, fallback: &str) -> String {
14 if let Some(v) = read_cached_version(cache_file) {
15 return v;
16 }
17 if let Some(v) = run_cli_version(bin) {
18 let _ = write_cached_version(cache_file, &v);
19 return v;
20 }
21 fallback.to_string()
22}
23
24pub fn parse_version(raw: &str) -> Option<String> {
28 raw.split_whitespace()
29 .find(|t| t.starts_with(|c: char| c.is_ascii_digit()) && t.contains('.'))
30 .map(str::to_string)
31}
32
33fn read_cached_version(cache_file: &str) -> Option<String> {
36 let path = crate::utils::get_cache_dir().ok()?.join(cache_file);
37 let text = std::fs::read_to_string(path).ok()?;
38 let v: serde_json::Value = serde_json::from_str(&text).ok()?;
39 if !is_today_utc(v.get("last_checked_at")?.as_str()?) {
40 return None;
41 }
42 v.get("version")?.as_str().map(str::to_string)
43}
44
45fn write_cached_version(cache_file: &str, version: &str) -> Result<()> {
47 let path = crate::utils::get_cache_dir()?.join(cache_file);
48 crate::utils::write_json_atomic(
49 path,
50 &serde_json::json!({
51 "version": version,
52 "last_checked_at": crate::utils::now_rfc3339_utc_nanos(),
53 }),
54 )
55}
56
57fn run_cli_version(bin: &str) -> Option<String> {
59 let output = std::process::Command::new(bin)
60 .arg("--version")
61 .output()
62 .ok()?;
63 output.status.success().then_some(())?;
64 parse_version(&String::from_utf8_lossy(&output.stdout))
65}
66
67fn is_today_utc(ts: &str) -> bool {
73 chrono::DateTime::parse_from_rfc3339(ts)
74 .map(|dt| dt.with_timezone(&chrono::Utc).date_naive() == chrono::Utc::now().date_naive())
75 .unwrap_or(false)
76}
77
78pub fn build_client() -> Result<reqwest::blocking::Client> {
88 reqwest::blocking::Client::builder()
89 .timeout(std::time::Duration::from_secs(8))
90 .build()
91 .context("Failed to build HTTP client")
92}
93
94pub fn iso_to_unix_secs(s: &str) -> Option<i64> {
100 let ms = crate::utils::parse_iso_timestamp(s);
101 (ms > 0).then_some(ms / 1000)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn iso_to_unix_secs_handles_bad_input() {
110 assert_eq!(iso_to_unix_secs("not-a-date"), None);
111 assert!(iso_to_unix_secs("2026-07-03T17:09:59.651608+00:00").unwrap() > 0);
112 }
113
114 #[test]
115 fn is_today_utc_matches_now_but_not_a_past_day() {
116 let now = crate::utils::now_rfc3339_utc_nanos();
117 assert!(is_today_utc(&now));
118 assert!(!is_today_utc("2000-01-01T00:00:00Z"));
119 assert!(!is_today_utc("not-a-timestamp"));
120 }
121
122 #[test]
123 fn parse_version_handles_leading_or_trailing_labels() {
124 assert_eq!(
126 parse_version("2.1.201 (Claude Code)").as_deref(),
127 Some("2.1.201")
128 );
129 assert_eq!(
131 parse_version("codex-cli 0.142.5").as_deref(),
132 Some("0.142.5")
133 );
134 assert_eq!(parse_version(" 2.0.14\n").as_deref(), Some("2.0.14"));
135 assert_eq!(parse_version(""), None);
136 assert_eq!(parse_version("Claude Code"), None);
137 }
138}