diff --git a/src/bin/runtime_inventory.rs b/src/bin/runtime_inventory.rs index 922397b..8a07096 100644 --- a/src/bin/runtime_inventory.rs +++ b/src/bin/runtime_inventory.rs @@ -3,7 +3,7 @@ //! Matches the TypeScript `clawdie.runtime-version-inventory.v1` schema in //! `clawdie-ai/src/colibri-runtime-inventory.ts`. -use std::{collections::BTreeMap, env, fs, process::Command}; +use std::{collections::BTreeMap, env, fs, path::Path, process::Command}; use serde::Serialize; @@ -38,6 +38,9 @@ fn command_candidates(program: &str) -> Vec { candidates.push(format!("{home}/.npm-global/bin/{program}")); candidates.push(format!("{home}/.local/bin/{program}")); } + if let Ok(path) = env::var("PATH") { + candidates.extend(path.split(':').map(|dir| format!("{dir}/{program}"))); + } for dir in [ "/usr/local/sbin", "/usr/local/bin", @@ -48,9 +51,6 @@ fn command_candidates(program: &str) -> Vec { ] { candidates.push(format!("{dir}/{program}")); } - if let Ok(path) = env::var("PATH") { - candidates.extend(path.split(':').map(|dir| format!("{dir}/{program}"))); - } candidates.push(program.to_string()); candidates } @@ -64,7 +64,13 @@ fn command_output(program: &str, args: &[&str]) -> Option { .output() .ok() .filter(|output| output.status.success()) - .and_then(|output| String::from_utf8(output.stdout).ok()) + .and_then(|output| { + if !output.stdout.is_empty() { + String::from_utf8(output.stdout).ok() + } else { + String::from_utf8(output.stderr).ok() + } + }) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) }) @@ -93,26 +99,61 @@ fn command_exists(program: &str) -> bool { .unwrap_or(false) } +fn package_json_version(path: impl AsRef) -> Option { + let raw = fs::read_to_string(path).ok()?; + let parsed = serde_json::from_str::(&raw).ok()?; + parsed + .get("version") + .and_then(|value| value.as_str()) + .map(|value| value.to_string()) +} + +fn pi_package_version_from_bin(candidate: &str) -> Option { + let canonical = fs::canonicalize(candidate).ok()?; + for dir in canonical.ancestors() { + let package_json = dir.join("package.json"); + if let Some(version) = package_json_version(package_json) { + let dir_text = dir.to_string_lossy(); + if dir_text.contains("pi-coding-agent") { + return Some(version); + } + } + } + None +} + fn detect_pi_version() -> Option { + if let Ok(pi_bin) = env::var("PI_BIN") { + if let Some(version) = + command_output(&pi_bin, &["--version"]).or_else(|| pi_package_version_from_bin(&pi_bin)) + { + return Some(version); + } + } + if let Ok(home) = env::var("HOME") { let pi_bin = format!("{home}/.npm-global/bin/pi"); - if let Some(version) = command_output(&pi_bin, &["--version"]) { + if let Some(version) = + command_output(&pi_bin, &["--version"]).or_else(|| pi_package_version_from_bin(&pi_bin)) + { return Some(version); } let package_json = format!( "{home}/.npm-global/lib/node_modules/@earendil-works/pi-coding-agent/package.json" ); - if let Ok(raw) = fs::read_to_string(package_json) { - if let Ok(parsed) = serde_json::from_str::(&raw) { - if let Some(version) = parsed.get("version").and_then(|value| value.as_str()) { - return Some(version.to_string()); - } - } + if let Some(version) = package_json_version(package_json) { + return Some(version); } } - command_output("pi", &["--version"]) + if let Some(version) = command_output("pi", &["--version"]) { + return Some(version); + } + + command_candidates("pi") + .into_iter() + .find_map(|candidate| pi_package_version_from_bin(&candidate)) } fn detect_package_manager() -> Option {