use anyhow::{Context, Result};
use std::fs;
use std::path::PathBuf;
use super::StatusCheck;
const HOOKS_JSON: &str = include_str!("../../.github/hooks/workmux-status/hooks.json");
fn copilot_dir() -> Option<PathBuf> {
if let Ok(dir) = std::env::var("COPILOT_CONFIG_DIR") {
return Some(PathBuf::from(dir));
}
home::home_dir().map(|h| h.join(".copilot"))
}
pub fn detect() -> Option<&'static str> {
if crate::git::get_repo_root().is_err() {
return None;
}
if copilot_dir().is_some_and(|d| d.is_dir()) {
return Some("found ~/.copilot/");
}
None
}
pub fn check() -> Result<StatusCheck> {
let root = match crate::git::get_repo_root() {
Ok(r) => r,
Err(e) => return Ok(StatusCheck::Error(e.to_string())),
};
let hooks_dir = root.join(".github/hooks");
if !hooks_dir.is_dir() {
return Ok(StatusCheck::NotInstalled);
}
if let Ok(entries) = fs::read_dir(&hooks_dir) {
for entry in entries.flatten() {
if !entry.path().is_dir() {
continue;
}
let hooks_file = entry.path().join("hooks.json");
if hooks_file.exists()
&& let Ok(content) = fs::read_to_string(&hooks_file)
&& content.contains("workmux set-window-status")
{
return Ok(StatusCheck::Installed);
}
}
}
Ok(StatusCheck::NotInstalled)
}
pub fn install() -> Result<String> {
let root = crate::git::get_repo_root()
.context("Must be in a git repository to install Copilot hooks")?;
let hooks_dir = root.join(".github/hooks/workmux-status");
fs::create_dir_all(&hooks_dir).context("Failed to create .github/hooks/workmux-status/")?;
let hooks_file = hooks_dir.join("hooks.json");
fs::write(&hooks_file, HOOKS_JSON).context("Failed to write hooks.json")?;
Ok(format!(
"Installed hooks to {}",
hooks_file
.strip_prefix(&root)
.unwrap_or(&hooks_file)
.display()
))
}
pub fn uninstall() -> Result<String> {
let root = match crate::git::get_repo_root() {
Ok(r) => r,
Err(_) => return Ok("Not in a git repository, nothing to uninstall".to_string()),
};
uninstall_at(root)
}
fn uninstall_at(root: PathBuf) -> Result<String> {
let hooks_dir = root.join(".github/hooks/workmux-status");
if hooks_dir.exists() {
fs::remove_dir_all(&hooks_dir)?;
let hooks_parent = root.join(".github/hooks");
if hooks_parent
.read_dir()
.is_ok_and(|mut it| it.next().is_none())
{
let _ = fs::remove_dir(&hooks_parent);
}
Ok("Removed .github/hooks/workmux-status/ from current repo".to_string())
} else {
Ok("No Copilot hooks found in current repo".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hooks_json_is_valid() {
let parsed: serde_json::Value =
serde_json::from_str(HOOKS_JSON).expect("embedded hooks.json is valid JSON");
assert_eq!(parsed.get("version").and_then(|v| v.as_u64()), Some(1));
let hooks = parsed.get("hooks").unwrap().as_object().unwrap();
assert!(hooks.contains_key("userPromptSubmitted"));
assert!(hooks.contains_key("postToolUse"));
assert!(hooks.contains_key("agentStop"));
}
#[test]
fn test_hooks_json_contains_workmux_command() {
assert!(HOOKS_JSON.contains("workmux set-window-status"));
}
#[test]
fn test_uninstall_no_hooks_dir() {
let tmp = tempfile::tempdir().unwrap();
let result = uninstall_at(tmp.path().to_path_buf()).unwrap();
assert!(result.contains("No Copilot hooks found"));
}
#[test]
fn test_uninstall_removes_hooks_dir() {
let tmp = tempfile::tempdir().unwrap();
let hooks_dir = tmp.path().join(".github/hooks/workmux-status");
std::fs::create_dir_all(&hooks_dir).unwrap();
std::fs::write(hooks_dir.join("hooks.json"), "{}").unwrap();
let result = uninstall_at(tmp.path().to_path_buf()).unwrap();
assert!(result.contains("Removed .github/hooks/workmux-status"));
assert!(!hooks_dir.exists());
}
#[test]
fn test_uninstall_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let hooks_dir = tmp.path().join(".github/hooks/workmux-status");
std::fs::create_dir_all(&hooks_dir).unwrap();
std::fs::write(hooks_dir.join("hooks.json"), "{}").unwrap();
let result1 = uninstall_at(tmp.path().to_path_buf()).unwrap();
assert!(result1.contains("Removed"));
let result2 = uninstall_at(tmp.path().to_path_buf()).unwrap();
assert!(result2.contains("No Copilot hooks found"));
}
}