use anyhow::{anyhow, Context, Result};
use super::settings::{
atomic_write, backup_path, cleanup_empty_containers, filter_pretool_entries,
find_pretool_entry_by_command, insert_hook_entry, is_old_style_hook_command,
is_repotoire_hook_command, read_settings_or_empty, settings_path,
};
pub fn run(allow_dev_binary: bool) -> Result<()> {
let bin = std::env::current_exe().context("could not determine current executable path")?;
let bin_str = bin.to_string_lossy().to_string();
if !allow_dev_binary
&& (bin_str.contains("/target/debug/")
|| bin_str.contains("/target/release/")
|| bin_str.contains("\\target\\debug\\")
|| bin_str.contains("\\target\\release\\"))
{
anyhow::bail!(
"Error: refusing to install hook with a dev binary at {}. \
Use the installed binary, or pass --allow-dev-binary if you really mean it.",
bin.display()
);
}
if bin_str.contains(' ') {
anyhow::bail!(
"Error: binary path {} contains spaces; cannot install hook. \
Move the binary or symlink to a space-free path before installing.",
bin.display()
);
}
let our_command = format!("{bin_str} claude-hook run");
let settings = settings_path()?;
let mut root = read_settings_or_empty(&settings)
.with_context(|| format!("read settings.json at {}", settings.display()))?;
let home = home::home_dir().ok_or_else(|| anyhow!("home directory not resolvable"))?;
let old_script = home.join(".repotoire").join("hooks").join("pre-commit.sh");
let old_in_settings =
find_pretool_entry_by_command(&root, |c| is_old_style_hook_command(c, &home)).is_some();
if old_script.exists() || old_in_settings {
eprintln!(
"\u{26a0} Old-style Repotoire hook detected (slow `repotoire analyze` on every commit). \
Removing and replacing with the fast `repotoire claude-hook` subcommand."
);
let _ = std::fs::remove_file(&old_script);
filter_pretool_entries(&mut root, |c| is_old_style_hook_command(c, &home));
}
if find_pretool_entry_by_command(&root, |c| c == our_command).is_some() {
eprintln!(
"\u{2139} Repotoire hook already installed at {}",
settings.display()
);
return Ok(());
}
let stale_removed = filter_pretool_entries(&mut root, |c| {
is_repotoire_hook_command(c) && c != our_command
});
if stale_removed > 0 {
eprintln!(
"\u{2139} Removed {stale_removed} stale repotoire hook entry/entries from previous installs."
);
}
let backup = if settings.exists() {
let bp = backup_path(&settings);
std::fs::copy(&settings, &bp).with_context(|| format!("write backup {}", bp.display()))?;
Some(bp)
} else {
None
};
insert_hook_entry(&mut root, &our_command);
cleanup_empty_containers(&mut root);
let out = serde_json::to_string_pretty(&root)?;
atomic_write(&settings, out.as_bytes())
.with_context(|| format!("atomic write {}", settings.display()))?;
println!("\u{2713} Hook installed: {our_command}");
match backup {
Some(b) => println!(
"\u{2713} Updated: {} (backup: {})",
settings.display(),
b.display()
),
None => println!("\u{2713} Created: {}", settings.display()),
}
println!(
"\u{2139} Run `repotoire analyze` to seed the diff baseline; the hook stays silent until then."
);
println!("\u{2139} If you have Claude Code open, restart it to pick up the new hook.");
println!("\u{2139} Uninstall: repotoire claude-hook uninstall");
Ok(())
}