codex-helper-core 0.19.0

Core library for codex-helper.
Documentation
use serde::{Deserialize, Serialize};
use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};

use super::model::FleetProcessSummary;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct FleetProcessInfo {
    pub pid: u32,
    pub name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at_ms: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct FleetProcessScan {
    pub processes: Vec<FleetProcessInfo>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

impl FleetProcessScan {
    pub fn summary(&self) -> FleetProcessSummary {
        FleetProcessSummary {
            scan_available: self.error.is_none(),
            codex_like_processes: self.processes.len(),
            error: self.error.clone(),
        }
    }
}

pub fn scan_codex_processes() -> FleetProcessScan {
    let mut system = System::new();
    system.refresh_processes_specifics(
        ProcessesToUpdate::All,
        true,
        ProcessRefreshKind::everything(),
    );

    let mut processes = system
        .processes()
        .values()
        .filter(|process| is_codex_like_process(process))
        .map(process_info)
        .collect::<Vec<_>>();
    processes.sort_by_key(|process| process.pid);

    FleetProcessScan {
        processes,
        error: None,
    }
}

fn process_info(process: &sysinfo::Process) -> FleetProcessInfo {
    FleetProcessInfo {
        pid: pid_to_u32(process.pid()),
        name: process.name().to_string_lossy().into_owned(),
        command: command_line(process),
        cwd: process.cwd().map(|path| path.display().to_string()),
        started_at_ms: Some(process.start_time().saturating_mul(1_000)),
    }
}

fn is_codex_like_process(process: &sysinfo::Process) -> bool {
    let name = process.name().to_string_lossy().to_ascii_lowercase();
    let command = process
        .cmd()
        .iter()
        .map(|part| part.to_string_lossy().to_ascii_lowercase())
        .collect::<Vec<_>>()
        .join(" ");

    is_codex_like_text(&name) || is_codex_like_text(&command)
}

fn is_codex_like_text(value: &str) -> bool {
    value.contains("codex")
        || value.contains("codex-helper")
        || value.contains("openai-codex")
        || value.contains("codex.exe")
}

fn command_line(process: &sysinfo::Process) -> Option<String> {
    let parts = process
        .cmd()
        .iter()
        .map(|part| part.to_string_lossy())
        .filter(|part| !part.trim().is_empty())
        .collect::<Vec<_>>();
    if parts.is_empty() {
        None
    } else {
        Some(parts.join(" "))
    }
}

fn pid_to_u32(pid: Pid) -> u32 {
    pid.as_u32()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn codex_like_text_matches_expected_process_names() {
        assert!(is_codex_like_text("codex"));
        assert!(is_codex_like_text("codex-helper.exe"));
        assert!(is_codex_like_text("openai-codex"));
        assert!(!is_codex_like_text("cargo"));
    }
}