diff --git a/crates/spaceshd/src/launchd.rs b/crates/spaceshd/src/launchd.rs index 46ae93d..994e212 100644 --- a/crates/spaceshd/src/launchd.rs +++ b/crates/spaceshd/src/launchd.rs @@ -1,4 +1,68 @@ -use anyhow::Result; -pub fn install_agent() -> Result<()> { - anyhow::bail!("install-agent implemented in Task 16") +use anyhow::{Context, Result}; +use std::path::PathBuf; + +const LABEL: &str = "xyz.spacesh.daemon"; + +fn plist_path() -> Result { + let home = dirs::home_dir().context("no home")?; + Ok(home.join("Library").join("LaunchAgents").join(format!("{LABEL}.plist"))) +} + +/// Render the launchd plist. `run_at_load` defaults to false in this slice. +pub fn render_plist(exe: &str, run_at_load: bool) -> String { + format!( + r#" + + + + Label + {LABEL} + ProgramArguments + + {exe} + + KeepAlive + + RunAtLoad + <{run_at_load}/> + + +"#, + run_at_load = if run_at_load { "true" } else { "false" } + ) +} + +pub fn install_agent() -> Result<()> { + let exe = std::env::current_exe()?; + let plist = render_plist(&exe.to_string_lossy(), false); + let path = plist_path()?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&path, plist)?; + // Load it (best-effort; ignore "already loaded"). + let _ = std::process::Command::new("launchctl") + .arg("load") + .arg(&path) + .status(); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn plist_has_label_keepalive_and_exe() { + let p = render_plist("/usr/local/bin/spaceshd", false); + assert!(p.contains("xyz.spacesh.daemon")); + assert!(p.contains("/usr/local/bin/spaceshd")); + assert!(p.contains("KeepAlive\n ")); + assert!(p.contains("RunAtLoad\n ")); + } + + #[test] + fn run_at_load_toggles() { + assert!(render_plist("x", true).contains("RunAtLoad\n ")); + } }