pub trait ProcCheck {
fn is_live_claude_or_node(pid: u32) -> bool;
}
pub fn is_claude_or_node_name(base: &str) -> bool {
let lower = base.to_ascii_lowercase();
lower.ends_with("claude") || lower.ends_with("node")
}
#[cfg_attr(unix, allow(dead_code))]
pub struct SysinfoProcCheck;
impl ProcCheck for SysinfoProcCheck {
fn is_live_claude_or_node(pid: u32) -> bool {
use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System};
let pid = Pid::from_u32(pid);
let mut sys = System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::new()),
);
sys.refresh_processes_specifics(ProcessRefreshKind::new());
if let Some(proc) = sys.process(pid) {
if let Some(exe) = proc.exe() {
let stem = exe
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.trim_end_matches(".exe");
return is_claude_or_node_name(stem);
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::is_claude_or_node_name;
#[test]
fn test_claude_variants() {
assert!(is_claude_or_node_name("claude"));
assert!(is_claude_or_node_name("Claude"));
assert!(is_claude_or_node_name("CLAUDE"));
assert!(is_claude_or_node_name("claude"));
assert!(!is_claude_or_node_name("claude-3"));
}
#[test]
fn test_node_variants() {
assert!(is_claude_or_node_name("node"));
assert!(is_claude_or_node_name("Node"));
assert!(is_claude_or_node_name("NODE"));
}
#[test]
fn test_non_matches() {
assert!(!is_claude_or_node_name("bash"));
assert!(!is_claude_or_node_name("zsh"));
assert!(!is_claude_or_node_name("python3"));
assert!(!is_claude_or_node_name(""));
assert!(!is_claude_or_node_name("not-claud"));
assert!(!is_claude_or_node_name("claude-3"));
assert!(!is_claude_or_node_name("nodemon"));
}
#[test]
fn test_wsl_interop_paths() {
assert!(is_claude_or_node_name("claude"));
assert!(is_claude_or_node_name("node"));
}
}