Files
agent-manager/src/version.rs
T
bruno 8ee3ddf3db agent-manager v0.2.0 : gestionnaire d'agents IA locaux
- CLI Rust (am) : list/install/uninstall/update/start/stop/restart/run/status/search/info/config/doctor/completion/export/import/self-update + shell interactif (Tab, historique)
- 9 methodes d'installation (npm, pip, uv, cargo, go, bun, curl, binary, git)
- catalogue de 72 agents, dependances, processus, securite (dry-run, checksums)
- sondes de version paralleles + cache (am list ~25 ms a chaud)
- installateurs one-liner : install.sh, install.ps1
2026-08-15 11:26:34 -04:00

159 lines
5.1 KiB
Rust

//! Version string utilities: extraction from arbitrary command output and
//! lenient version comparison.
use std::cmp::Ordering;
/// Extract the first plausible version string from arbitrary text.
///
/// Handles forms such as "v1.2.3", "1.2", "go1.21.0", "2026.1", as well as
/// suffixes like "-beta" or "+meta". Prefers candidates with the most
/// dot-separated components.
pub fn find_version(text: &str) -> Option<String> {
let chars: Vec<char> = text.chars().collect();
let n = chars.len();
let mut best: Option<(usize, usize, usize)> = None; // (dots, len, start)
let mut i = 0;
while i < n {
if chars[i].is_ascii_digit() {
let mut j = i;
while j < n && chars[j].is_ascii_digit() {
j += 1;
}
let mut dots = 0usize;
loop {
if j + 1 < n && chars[j] == '.' && chars[j + 1].is_ascii_digit() {
dots += 1;
j += 1;
while j < n && chars[j].is_ascii_digit() {
j += 1;
}
} else {
break;
}
}
let mut k = j;
if k < n && (chars[k] == '-' || chars[k] == '+') {
k += 1;
while k < n
&& (chars[k].is_ascii_alphanumeric() || chars[k] == '.' || chars[k] == '-')
{
k += 1;
}
}
let len = k - i;
let score = dots.saturating_mul(1000) + len.min(999);
if score > best.map(|(d, l, _)| d * 1000 + l.min(999)).unwrap_or(0) {
best = Some((dots, len, i));
}
i = k.max(j);
} else {
i += 1;
}
}
let (_, _, start) = best?;
// Reconstruct the chosen candidate.
let mut j = start;
while j < n && chars[j].is_ascii_digit() {
j += 1;
}
while j + 1 < n && chars[j] == '.' && chars[j + 1].is_ascii_digit() {
j += 1;
while j < n && chars[j].is_ascii_digit() {
j += 1;
}
}
let mut k = j;
if k < n && (chars[k] == '-' || chars[k] == '+') {
k += 1;
while k < n && (chars[k].is_ascii_alphanumeric() || chars[k] == '.' || chars[k] == '-')
{
k += 1;
}
}
while k > start && (chars[k - 1] == '.' || chars[k - 1] == '-') {
k -= 1;
}
if k <= start {
return None;
}
Some(chars[start..k].iter().collect())
}
/// Strip a leading "v"/"V" and surrounding whitespace.
pub fn normalize(s: &str) -> String {
let mut t = s.trim().to_string();
if t.len() > 1
&& (t.starts_with('v') || t.starts_with('V'))
&& t[1..].chars().next().map_or(false, |c| c.is_ascii_digit())
{
t = t[1..].to_string();
}
t
}
/// Compare two version strings leniently: semver when possible, otherwise a
/// numeric-tuple comparison. Returns None when the two cannot be compared at
/// all (never happens for two non-empty strings).
pub fn compare(a: &str, b: &str) -> Option<Ordering> {
let a = normalize(a);
let b = normalize(b);
if a == b {
return Some(Ordering::Equal);
}
if let (Ok(av), Ok(bv)) = (semver::Version::parse(&a), semver::Version::parse(&b)) {
return Some(av.cmp(&bv));
}
let pa = numeric_parts(&a);
let pb = numeric_parts(&b);
let mut idx = 0;
loop {
match (pa.get(idx), pb.get(idx)) {
(Some(x), Some(y)) => {
if x != y {
return Some(x.cmp(y));
}
idx += 1;
}
(Some(_), None) => return Some(Ordering::Greater),
(None, Some(_)) => return Some(Ordering::Less),
(None, None) => return None,
}
}
}
fn numeric_parts(s: &str) -> Vec<u64> {
s.split(|c: char| !c.is_ascii_digit())
.filter(|p| !p.is_empty())
.filter_map(|p| p.parse::<u64>().ok())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_common_forms() {
assert_eq!(find_version("Claude Code v1.2.3 (abc)").as_deref(), Some("1.2.3"));
assert_eq!(find_version("go version go1.21.0 windows/amd64").as_deref(), Some("1.21.0"));
assert_eq!(find_version("uv 0.7.12").as_deref(), Some("0.7.12"));
assert_eq!(find_version("node 18.20.4").as_deref(), Some("18.20.4"));
assert_eq!(find_version("1.2.3-beta.1").as_deref(), Some("1.2.3-beta.1"));
assert_eq!(find_version("no version here"), None);
}
#[test]
fn prefers_dotted_candidate() {
assert_eq!(find_version("cargo 1.85.0 (8a56 2026-01-01)").as_deref(), Some("1.85.0"));
}
#[test]
fn compares_semver_and_numeric() {
assert_eq!(compare("1.10.0", "1.9.0"), Some(Ordering::Greater));
assert_eq!(compare("v2.0.0", "2.0.0"), Some(Ordering::Equal));
assert_eq!(compare("2.0.0-rc1", "2.0.0"), Some(Ordering::Less));
assert_eq!(compare("2026", "2025"), Some(Ordering::Greater));
assert_eq!(compare("0.7", "0.7.12"), Some(Ordering::Less));
}
}