args-cli 0.1.1

Command-line interface for args
Documentation
use anyhow::Context;

use super::{Service, ServiceManager};
use crate::{config::runner::SYSTEM_USER, util::run_command};

const LABEL: &str = "io.args.runner";
const PLIST_PATH: &str = "/Library/LaunchDaemons/io.args.runner.plist";
const LOG_PATH: &str = "/Library/Logs/args-runner.log";

impl Service for ServiceManager {
    fn install(&self) -> anyhow::Result<()> {
        let plist = format!(
            r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>{label}</string>
    <key>ProgramArguments</key>
    <array>
        <string>{binary}</string>
        <string>ci</string>
        <string>run</string>
    </array>
    <key>UserName</key><string>{user}</string>
    <key>KeepAlive</key><true/>
    <key>RunAtLoad</key><true/>
    <key>StandardOutPath</key><string>{log}</string>
    <key>StandardErrorPath</key><string>{log}</string>
</dict>
</plist>
"#,
            label = LABEL,
            binary = self.binary_path,
            user = SYSTEM_USER,
            log = LOG_PATH,
        );

        let tmp = std::env::temp_dir().join("io.args.runner.plist");
        std::fs::write(&tmp, plist).context("Failed to write plist to temp file")?;
        let tmp_str = tmp.to_str().context("Temp path is not valid UTF-8")?;

        run_command("sudo", &["cp", tmp_str, PLIST_PATH])
            .with_context(|| format!("Failed to copy plist to {}", PLIST_PATH))?;

        run_command("sudo", &["touch", LOG_PATH]).context("Failed to create log file")?;
        run_command("sudo", &["chown", SYSTEM_USER, LOG_PATH])
            .context("Failed to set log file ownership")?;

        run_command("sudo", &["launchctl", "bootstrap", "system", PLIST_PATH])
            .context("Failed to bootstrap launchd daemon")?;

        Ok(())
    }

    fn uninstall(&self) -> anyhow::Result<()> {
        // Best-effort: ignore errors and suppress output (service may not be loaded yet)
        let _ = std::process::Command::new("sudo")
            .args(["launchctl", "bootout", "system", PLIST_PATH])
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status();
        let _ = std::process::Command::new("sudo")
            .args(["rm", "-f", PLIST_PATH])
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())
            .status();
        Ok(())
    }

    fn start(&self) -> anyhow::Result<()> {
        run_command(
            "sudo",
            &["launchctl", "kickstart", &format!("system/{}", LABEL)],
        )
        .context("Failed to start launchd daemon")?;
        Ok(())
    }

    fn stop(&self) -> anyhow::Result<()> {
        run_command(
            "sudo",
            &["launchctl", "kill", "TERM", &format!("system/{}", LABEL)],
        )
        .context("Failed to stop launchd daemon")?;
        Ok(())
    }
}