pub trait ProcCheck {
fn is_live_claude_or_node(pid: u32) -> bool;
}
pub fn is_claude_or_node_name(base: &str) -> bool {
is_name_for(base, &crate::config::resolve_launch_command())
}
fn is_name_for(base: &str, launch: &[std::ffi::OsString]) -> bool {
let lower = base.to_ascii_lowercase();
if lower.ends_with("claude") || lower.ends_with("node") {
return true;
}
let name = launch
.first()
.map(std::path::Path::new)
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("")
.trim_end_matches(".exe")
.to_ascii_lowercase();
!name.is_empty() && name != "claude" && name != "node" && lower == name
}
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, is_name_for};
use std::ffi::OsString;
fn launch(toks: &[&str]) -> Vec<OsString> {
toks.iter().map(OsString::from).collect()
}
#[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"));
}
#[test]
fn launch_binary_matches_exact_basename() {
assert!(is_name_for("happy", &launch(&["happy"])));
assert!(is_name_for("tp", &launch(&["tp"])));
assert!(is_name_for("Happy", &launch(&["happy"])));
assert!(is_name_for("npx", &launch(&["npx", "happy"])));
}
#[test]
fn launch_binary_path_and_exe_stripped() {
assert!(is_name_for("happy", &launch(&["/usr/local/bin/happy"])));
assert!(is_name_for("happy", &launch(&["happy.exe"])));
}
#[test]
fn launch_binary_no_false_match() {
assert!(!is_name_for("bash", &launch(&["happy"])));
assert!(!is_name_for("zsh", &launch(&["happy"])));
}
#[test]
fn default_claude_adds_no_extra_match() {
assert!(is_name_for("claude", &launch(&["claude"])));
assert!(is_name_for("node", &launch(&["claude"])));
assert!(!is_name_for("happy", &launch(&["claude"])));
assert!(!is_name_for("bash", &launch(&["claude"])));
assert!(!is_name_for("happy", &launch(&[])));
assert!(is_name_for("claude", &launch(&[])));
}
}