nomad-cli 0.1.1

Nomad CLI - extract structured web content
Documentation
use std::process::{Command, Stdio};
use std::fs;
use std::path::PathBuf;

use crate::cli::{DaemonArgs, DaemonAction};

fn pid_path() -> PathBuf {
    std::env::temp_dir().join("nomad-daemon.pid")
}

fn daemon_binary() -> PathBuf {
    // Look for nomad-daemon in standard locations
    let candidates = [
        PathBuf::from("/usr/local/bin/nomad-daemon"),
        PathBuf::from("/usr/bin/nomad-daemon"),
        // Relative to the nom binary
        std::env::current_exe()
            .ok()
            .and_then(|p| p.parent().map(|d| d.join("nomad-daemon")))
            .unwrap_or_default(),
    ];
    for c in &candidates {
        if c.exists() {
            return c.clone();
        }
    }
    // Fallback: just try the name
    PathBuf::from("nomad-daemon")
}

pub fn run(args: DaemonArgs) -> Result<(), Box<dyn std::error::Error>> {
    match args.action {
        DaemonAction::Start { port, cache_cap, parse_concurrency } => cmd_start(port, cache_cap, parse_concurrency),
        DaemonAction::Stop => cmd_stop(),
        DaemonAction::Status => cmd_status(),
        DaemonAction::Restart { port } => {
            cmd_stop().ok();
            std::thread::sleep(std::time::Duration::from_secs(1));
            cmd_start(port, 1, 2)
        }
    }
}

fn cmd_start(port: u16, cache_cap: usize, parse_concurrency: usize) -> Result<(), Box<dyn std::error::Error>> {
    if is_running() {
        println!("nomad-daemon is already running");
        return Ok(());
    }

    let binary = daemon_binary();
    if !binary.exists() {
        // Try to build it
        println!("Building nomad-daemon...");
        let status = Command::new("cargo")
            .args(["build", "--release", "-p", "nomad-daemon"])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()?;
        if !status.success() {
            return Err("failed to build nomad-daemon".into());
        }
        let target = PathBuf::from("target/release/nomad-daemon");
        if target.exists() {
            fs::copy(&target, &binary).ok();
        }
    }

    let child = Command::new(&binary)
        .env("PORT", port.to_string())
        .env("MALLOC_ARENA_MAX", "1")
        .env("NOMAD_CACHE_CAP", cache_cap.to_string())
        .env("NOMAD_PARSE_CONCURRENCY", parse_concurrency.to_string())
        .env("NOMAD_API_AUTH", "disabled")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;

    let pid = child.id();
    fs::write(pid_path(), pid.to_string())?;

    // Wait for daemon to become ready
    for _ in 0..20 {
        if crate::client::daemon_alive(port) {
            println!("nomad-daemon started (PID {pid}, port {port})");
            return Ok(());
        }
        std::thread::sleep(std::time::Duration::from_millis(250));
    }
    println!("Started but waiting for readiness...");
    Ok(())
}

fn cmd_stop() -> Result<(), Box<dyn std::error::Error>> {
    let pid_path = pid_path();
    if !pid_path.exists() {
        // Try pkill
        #[cfg(unix)]
        {
            let _ = Command::new("pkill")
                .args(["-x", "nomad-daemon"])
                .stdout(Stdio::null())
                .status();
        }
        #[cfg(windows)]
        {
            let _ = Command::new("taskkill")
                .args(["/f", "/im", "nomad-daemon.exe"])
                .stdout(Stdio::null())
                .status();
        }
        println!("nomad-daemon stopped");
        return Ok(());
    }

    let pid_str = fs::read_to_string(&pid_path)?;
    let pid: u32 = pid_str.trim().parse()?;

    #[cfg(unix)]
    {
        use std::process;
        let rc = Command::new("kill")
            .args(["-TERM", &pid.to_string()])
            .stdout(Stdio::null())
            .status()?;
        if rc.success() {
            // Wait for process to die
            for _ in 0..10 {
                if process::Command::new("kill")
                    .args(["-0", &pid.to_string()])
                    .stdout(Stdio::null())
                    .status()
                    .is_err()
                {
                    break;
                }
                std::thread::sleep(std::time::Duration::from_millis(200));
            }
        }
    }
    #[cfg(windows)]
    {
        let _ = Command::new("taskkill")
            .args(["/pid", &pid.to_string()])
            .stdout(Stdio::null())
            .status();
    }

    fs::remove_file(&pid_path).ok();
    println!("nomad-daemon stopped");
    Ok(())
}

fn cmd_status() -> Result<(), Box<dyn std::error::Error>> {
    if is_running() {
        // Try health endpoint on common ports
        for port in &[8000, 8001, 8080] {
            if crate::client::daemon_alive(*port) {
                crate::client::health(*port, false)?;
                return Ok(());
            }
        }
        println!("nomad-daemon: PID EXISTS but not responding");
    } else {
        println!("nomad-daemon: STOPPED");
    }
    Ok(())
}

fn is_running() -> bool {
    let pid_path = pid_path();
    if !pid_path.exists() {
        return false;
    }
    let pid_str = match fs::read_to_string(&pid_path) {
        Ok(s) => s,
        Err(_) => return false,
    };
    let pid: u32 = match pid_str.trim().parse() {
        Ok(p) => p,
        Err(_) => return false,
    };

    #[cfg(unix)]
    {
        std::process::Command::new("kill")
            .args(["-0", &pid.to_string()])
            .stdout(Stdio::null())
            .status()
            .is_ok()
    }
    #[cfg(windows)]
    {
        std::process::Command::new("tasklist")
            .args(["/fi", &format!("PID eq {}", pid)])
            .stdout(Stdio::null())
            .status()
            .is_ok()
    }
    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
    {
        false
    }
}