The daemon's auto-install skipped on presence, so a hook installed before #131 never gained UserPromptSubmit and no session was ever named after its prompt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
4.3 KiB
Rust
108 lines
4.3 KiB
Rust
//! First-start hook auto-install (#106).
|
|
//!
|
|
//! The daemon runs [`auto_install`] once at startup: unless the user's
|
|
//! `~/.claude/settings.json` already carries exactly what this build ships
|
|
//! (and `[hooks].auto_install` hasn't opted out — checked by the caller), the
|
|
//! same installer `maestro hooks install` uses writes it. That covers
|
|
//! upgrades as well as first starts: an install from a maestro that had fewer
|
|
//! events — #131 added `UserPromptSubmit` to a hook #106 had already put in
|
|
//! everyone's settings — is rewritten rather than left half-wired. Best-effort:
|
|
//! a failure is warned about and never blocks startup. The same global hook
|
|
//! serves every Claude Code session on the machine, so nothing here targets
|
|
//! repositories or worktrees.
|
|
|
|
use std::path::Path;
|
|
|
|
/// Install the Claude Code activity hook for `home_dir` unless the current
|
|
/// one is already there or the home dir is unresolvable (`""`). Returns what
|
|
/// happened; an older install being brought up to date is
|
|
/// [`AutoInstallOutcome::Upgraded`], so the log can say so.
|
|
pub fn auto_install(home_dir: &str) -> AutoInstallOutcome {
|
|
if home_dir.is_empty() {
|
|
return AutoInstallOutcome::Skipped;
|
|
}
|
|
let settings = maestro_hooks::settings_path(Path::new(home_dir));
|
|
let script = maestro_hooks::script_path(Path::new(home_dir));
|
|
if maestro_hooks::is_current(&settings, &script) {
|
|
return AutoInstallOutcome::AlreadyPresent;
|
|
}
|
|
let stale = maestro_hooks::is_installed(&settings);
|
|
match maestro_hooks::install_for_home(Path::new(home_dir)) {
|
|
Ok(_) if stale => AutoInstallOutcome::Upgraded,
|
|
Ok(_) => AutoInstallOutcome::Installed,
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "hooks auto-install failed (non-fatal)");
|
|
AutoInstallOutcome::Failed
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What [`auto_install`] did, so callers can log and tests can assert.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum AutoInstallOutcome {
|
|
Installed,
|
|
/// An older maestro's hook was there and has been rewritten.
|
|
Upgraded,
|
|
AlreadyPresent,
|
|
Failed,
|
|
Skipped,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Everything happens under a temp `$HOME`; the developer's own
|
|
/// `~/.claude/settings.json` is never read or written.
|
|
#[test]
|
|
fn installs_when_settings_carry_no_maestro_hook() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let outcome = auto_install(dir.path().to_str().unwrap());
|
|
assert_eq!(outcome, AutoInstallOutcome::Installed);
|
|
let settings = dir.path().join(".claude/settings.json");
|
|
assert!(maestro_hooks::is_installed(&settings));
|
|
}
|
|
|
|
#[test]
|
|
fn second_start_is_a_noop() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let home = dir.path().to_str().unwrap();
|
|
auto_install(home);
|
|
// A current install is left exactly as it is.
|
|
let settings = dir.path().join(".claude/settings.json");
|
|
let before = std::fs::read_to_string(&settings).unwrap();
|
|
assert_eq!(auto_install(home), AutoInstallOutcome::AlreadyPresent);
|
|
assert_eq!(
|
|
before,
|
|
std::fs::read_to_string(&settings).unwrap(),
|
|
"already-installed settings must not be rewritten"
|
|
);
|
|
}
|
|
|
|
/// An install from an older maestro — present, but missing an event this
|
|
/// build ships — is upgraded rather than left alone (#131 shipped a new
|
|
/// event after #106 had already installed itself).
|
|
#[test]
|
|
fn a_stale_install_is_upgraded() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let home = dir.path().to_str().unwrap();
|
|
auto_install(home);
|
|
let settings = dir.path().join(".claude/settings.json");
|
|
let script = maestro_hooks::script_path(dir.path());
|
|
|
|
let mut root: serde_json::Value =
|
|
serde_json::from_str(&std::fs::read_to_string(&settings).unwrap()).unwrap();
|
|
root["hooks"].as_object_mut().unwrap().remove("Stop");
|
|
std::fs::write(&settings, serde_json::to_string_pretty(&root).unwrap()).unwrap();
|
|
std::fs::write(&script, "#!/bin/sh\nexit 0\n").unwrap();
|
|
|
|
assert_eq!(auto_install(home), AutoInstallOutcome::Upgraded);
|
|
assert!(maestro_hooks::is_current(&settings, &script));
|
|
}
|
|
|
|
#[test]
|
|
fn unresolvable_home_is_skipped() {
|
|
assert_eq!(auto_install(""), AutoInstallOutcome::Skipped);
|
|
}
|
|
}
|