codex-agent-indicator 0.4.15

Low-overhead Codex task status indicator for Logitech G915 G-keys
use std::env;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{Context, Result, bail};

const BINARY_NAME: &str = "codex-agent-indicator";
const LAUNCH_AGENT_LABEL: &str = "com.codex-agent-indicator";

#[derive(Debug, Eq, PartialEq)]
struct UpdatePlan {
    executable: PathBuf,
    install_root: PathBuf,
}

impl UpdatePlan {
    fn from_executable(executable: &Path) -> Result<Self> {
        if executable.file_name() != Some(OsStr::new(BINARY_NAME)) {
            bail!(
                "cannot update executable {}; expected it to be named {BINARY_NAME}",
                executable.display()
            );
        }

        let bin_directory = executable
            .parent()
            .context("cannot determine the executable directory")?;
        if bin_directory.file_name() != Some(OsStr::new("bin")) {
            bail!(
                "cannot determine the install root for {}; expected the executable inside a bin directory",
                executable.display()
            );
        }
        let install_root = bin_directory
            .parent()
            .context("cannot determine the Cargo install root")?;

        Ok(Self {
            executable: executable.to_path_buf(),
            install_root: install_root.to_path_buf(),
        })
    }

    fn cargo_arguments(&self) -> Vec<OsString> {
        [
            OsString::from("install"),
            OsString::from(BINARY_NAME),
            OsString::from("--registry"),
            OsString::from("crates-io"),
            OsString::from("--locked"),
            OsString::from("--force"),
            OsString::from("--root"),
            self.install_root.clone().into_os_string(),
        ]
        .into()
    }
}

pub fn run() -> Result<()> {
    if let Some(argument) = env::args().nth(2) {
        bail!("update does not accept arguments, got {argument:?}");
    }

    let executable = env::current_exe().context("failed to locate the running executable")?;
    let plan = UpdatePlan::from_executable(&executable)?;

    println!(
        "Updating {BINARY_NAME} from crates.io in {}...",
        plan.install_root.display()
    );
    let status = Command::new("cargo")
        .args(plan.cargo_arguments())
        .status()
        .context("failed to run cargo; install Rust and ensure cargo is on PATH")?;
    if !status.success() {
        bail!("cargo install failed with {status}");
    }

    let output = Command::new(&plan.executable)
        .arg("--version")
        .output()
        .with_context(|| {
            format!(
                "updated the package but failed to verify {}",
                plan.executable.display()
            )
        })?;
    if !output.status.success() {
        bail!(
            "updated the package but {} --version failed with {}",
            plan.executable.display(),
            output.status
        );
    }
    println!("{}", String::from_utf8_lossy(&output.stdout).trim());

    restart_launch_agent_if_managed(&plan);
    Ok(())
}

fn restart_launch_agent_if_managed(plan: &UpdatePlan) {
    let Some(home) = env::var_os("HOME").map(PathBuf::from) else {
        return;
    };
    let managed_executable = home.join(".local/bin").join(BINARY_NAME);
    let launch_agent = home
        .join("Library/LaunchAgents")
        .join(format!("{LAUNCH_AGENT_LABEL}.plist"));
    if plan.executable != managed_executable || !launch_agent.exists() {
        return;
    }

    let uid = match Command::new("/usr/bin/id").arg("-u").output() {
        Ok(output) if output.status.success() => {
            String::from_utf8_lossy(&output.stdout).trim().to_string()
        }
        _ => {
            eprintln!(
                "Warning: updated the binary but could not determine the user ID; restart the indicator service or your Mac to load it."
            );
            return;
        }
    };
    let service = format!("gui/{uid}/{LAUNCH_AGENT_LABEL}");
    match Command::new("/bin/launchctl")
        .args(["kickstart", "-k", &service])
        .status()
    {
        Ok(status) if status.success() => println!("Restarted the keyboard indicator service."),
        _ => eprintln!(
            "Warning: updated the binary but could not restart the keyboard indicator service; run the complete installer or restart your Mac."
        ),
    }
}

#[cfg(test)]
mod tests {
    use std::ffi::OsString;
    use std::path::Path;

    use super::{BINARY_NAME, UpdatePlan};

    #[test]
    fn targets_the_root_containing_the_running_binary() {
        let plan = UpdatePlan::from_executable(Path::new(
            "/Users/example/.local/bin/codex-agent-indicator",
        ))
        .expect("valid install layout");

        assert_eq!(plan.install_root, Path::new("/Users/example/.local"));
        assert_eq!(
            plan.cargo_arguments(),
            [
                "install",
                BINARY_NAME,
                "--registry",
                "crates-io",
                "--locked",
                "--force",
                "--root",
                "/Users/example/.local",
            ]
            .map(OsString::from)
        );
    }

    #[test]
    fn supports_the_default_cargo_install_layout() {
        let plan = UpdatePlan::from_executable(Path::new(
            "/Users/example/.cargo/bin/codex-agent-indicator",
        ))
        .expect("valid Cargo install layout");

        assert_eq!(plan.install_root, Path::new("/Users/example/.cargo"));
    }

    #[test]
    fn rejects_a_binary_outside_an_install_root() {
        let error = UpdatePlan::from_executable(Path::new(
            "/Users/example/Projects/indicator/target/release/codex-agent-indicator",
        ))
        .expect_err("source build is not an installed binary");

        assert!(
            error
                .to_string()
                .contains("expected the executable inside a bin directory")
        );
    }

    #[test]
    fn rejects_a_renamed_binary() {
        let error =
            UpdatePlan::from_executable(Path::new("/Users/example/.local/bin/indicator"))
                .expect_err("renamed binaries cannot be updated in place");

        assert!(error.to_string().contains(BINARY_NAME));
    }
}