use std::{fs, os::unix::fs::PermissionsExt};
use anyhow::Result;
use supercli::starbase_styles::color::owo::OwoColorize;
use crate::{cli::banner, cli::output::*, git::GitRepo};
fn detect_hook_type(content: &str) -> &'static str {
let content_lower = content.to_lowercase();
if content_lower.contains("lefthook") {
"lefthook"
} else if content_lower.contains("husky") {
"husky"
} else if content_lower.contains("pre-commit") && content_lower.contains("framework") {
"pre-commit framework"
} else if content_lower.contains("lint-staged") {
"lint-staged"
} else {
"unknown"
}
}
pub async fn install_hooks(force: bool, hooks: Option<Vec<String>>) -> Result<()> {
install_hooks_at(force, hooks, None).await
}
pub async fn install_hooks_at(
force: bool,
hooks: Option<Vec<String>>,
repo_path: Option<&std::path::Path>,
) -> Result<()> {
banner::print_banner(None);
info!("Installing guardy hooks...");
let repo = match repo_path {
Some(path) => match GitRepo::open(path) {
Ok(repo) => repo,
Err(_) => {
error!(&format!("Not a git repository: {}", path.display()));
return Ok(());
}
},
None => match GitRepo::discover() {
Ok(repo) => repo,
Err(_) => {
error!("Not in a git repository. Run 'git init' first.");
return Ok(());
}
},
};
let hooks_dir = repo.git_dir().join("hooks");
if !hooks_dir.exists() {
fs::create_dir_all(&hooks_dir)?;
info!("Created .git/hooks directory");
}
if force {
warning!("Force mode enabled - will overwrite existing hooks");
}
let hooks_to_install = hooks.unwrap_or_else(|| {
vec![
"pre-commit".to_string(),
"commit-msg".to_string(),
"post-checkout".to_string(),
"pre-push".to_string(),
]
});
let mut installed_count = 0;
let mut already_installed_count = 0;
let total_hooks = hooks_to_install.len();
for hook_name in hooks_to_install {
let hook_path = hooks_dir.join(&hook_name);
if hook_path.exists() && !force {
if let Ok(existing_content) = fs::read_to_string(&hook_path) {
if existing_content.contains("# Guardy hook:") {
success!(&format!(
"{} {} {}",
"Hook".green(),
hook_name.yellow(),
"already installed".green()
));
already_installed_count += 1;
continue;
} else {
let hook_type = detect_hook_type(&existing_content);
warning!(&format!(
"Hook '{hook_name}' already exists ({hook_type}). Use --force to overwrite."
));
continue;
}
} else {
warning!(&format!(
"Hook '{hook_name}' already exists. Use --force to overwrite."
));
continue;
}
}
let hook_script = format!(
"#!/bin/sh\n# Guardy hook: {hook_name}\nexec guardy hooks run {hook_name} \"$@\"\n"
);
fs::write(&hook_path, hook_script)?;
let mut permissions = fs::metadata(&hook_path)?.permissions();
permissions.set_mode(0o755);
fs::set_permissions(&hook_path, permissions)?;
success!(&format!(
"{} {} {}",
"Installed".green(),
hook_name.yellow(),
"hook".green()
));
installed_count += 1;
}
if installed_count > 0 {
success!("Hook installation completed!");
} else if already_installed_count > 0 && already_installed_count == total_hooks {
success!(&format!(
"{} {}",
"All hooks".yellow(),
"already installed!".green()
));
}
info!("Next steps:");
println!(" - {} - Verify installation", "guardy status".bold());
println!(
" - {} - Test hooks manually",
"guardy hooks run pre-commit".bold()
);
println!(" - Configure patterns in .guardy.yml if needed");
Ok(())
}