Detect Pi from PATH inventory candidates

Prefer user and PATH candidates before system defaults, fall back to package.json discovery, and accept version output from stderr for shims.

---

Build: pass — cargo build --release

Tests: pass — cargo test (0 tests)
This commit is contained in:
Sam & Claude 2026-05-26 11:26:39 +02:00
parent 247ffc76a1
commit 6269030424

View file

@ -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<String> {
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<String> {
] {
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<String> {
.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<Path>) -> Option<String> {
let raw = fs::read_to_string(path).ok()?;
let parsed = serde_json::from_str::<serde_json::Value>(&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<String> {
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<String> {
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::<serde_json::Value>(&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<String> {