use super::{Ctx, Harness, Mode, Reason, Scope, Step, StepReport, StepStatus};
use crate::setup_hooks::{
AgentsOutcome, HookAgentArg, ScriptOutcome, SettingsOutcome, SetupHooksOptions,
execute_agent_hooks,
};
pub fn install(ctx: &Ctx, harnesses: &[Harness]) -> Vec<StepReport> {
harnesses.iter().flat_map(|h| run(ctx, *h)).collect()
}
pub fn uninstall(ctx: &Ctx, harnesses: &[Harness]) -> Vec<StepReport> {
harnesses.iter().flat_map(|h| run(ctx, *h)).collect()
}
fn run(ctx: &Ctx, harness: Harness) -> Vec<StepReport> {
let agent = match harness {
Harness::Claude => HookAgentArg::Claude,
Harness::Codex => {
if ctx.user {
return vec![
StepReport::new(
Some(harness),
Step::Hooks,
StepStatus::Skipped,
Scope::Local,
)
.reason(Reason::UserScopeUnsupported)
.detail("the Codex gate lives in the project AGENTS.md"),
];
}
HookAgentArg::Codex
}
Harness::Cursor => {
return vec![
StepReport::new(Some(harness), Step::Hooks, StepStatus::Skipped, ctx.scope())
.reason(Reason::UnsupportedHarness)
.detail("Cursor's beforeShellExecution hook uses a different contract; Cursor still reads AGENTS.md"),
];
}
};
let opts = SetupHooksOptions {
root: &ctx.root,
agent: Some(agent),
dry_run: ctx.dry_run,
force: ctx.force,
user: ctx.user,
gitignore_claude: ctx.gitignore_claude,
uninstall: ctx.mode == Mode::Uninstall,
};
let report = match execute_agent_hooks(&opts, ctx.mode) {
Ok(Some(report)) => report,
Ok(None) => return Vec::new(),
Err(message) => {
return vec![StepReport::failed(
Some(harness),
Step::Hooks,
ctx.scope(),
message,
)];
}
};
let mut steps: Vec<StepReport> = Vec::new();
if let Some(claude) = report.claude {
steps.extend(claude_steps(ctx, harness, &claude));
}
if let Some(codex) = report.codex {
steps.push(codex_step(ctx, harness, &codex));
}
steps
}
fn claude_steps(
ctx: &Ctx,
harness: Harness,
claude: &crate::setup_hooks::ClaudeReport,
) -> Vec<StepReport> {
let mut steps: Vec<StepReport> = Vec::new();
{
let settings_status = match (&claude.settings_outcome, ctx.mode) {
(SettingsOutcome::Created | SettingsOutcome::Updated { .. }, Mode::Install) => {
StepStatus::Written
}
(SettingsOutcome::Updated { .. }, Mode::Uninstall) => StepStatus::Removed,
(SettingsOutcome::Created, Mode::Uninstall) => StepStatus::Unchanged,
(SettingsOutcome::Unchanged { .. } | SettingsOutcome::NotPresent, _) => {
StepStatus::Unchanged
}
};
steps.push(
StepReport::new(Some(harness), Step::Hooks, settings_status, ctx.scope())
.path(ctx, &claude.settings_path)
.detail("PreToolUse gate handler"),
);
let (script_status, reason) = match claude.script_outcome {
ScriptOutcome::Created | ScriptOutcome::Updated => (StepStatus::Written, None),
ScriptOutcome::Removed => (StepStatus::Removed, None),
ScriptOutcome::Unchanged | ScriptOutcome::NotPresent => (StepStatus::Unchanged, None),
ScriptOutcome::UserEditedPreserved => (StepStatus::Refused, Some(Reason::UserEdited)),
};
let mut script = StepReport::new(Some(harness), Step::Hooks, script_status, ctx.scope())
.path(ctx, &claude.script_path)
.detail("gate script");
if matches!(claude.script_outcome, ScriptOutcome::UserEditedPreserved) {
script.detail = Some("no fallow marker; pass --force to replace it".to_string());
}
script.reason = reason;
steps.push(script);
}
steps
}
fn codex_step(ctx: &Ctx, harness: Harness, codex: &crate::setup_hooks::CodexReport) -> StepReport {
{
let (status, reason) = match codex.outcome {
AgentsOutcome::Inserted | AgentsOutcome::Replaced => (StepStatus::Written, None),
AgentsOutcome::Removed => (StepStatus::Removed, None),
AgentsOutcome::Unchanged | AgentsOutcome::NotPresent => (StepStatus::Unchanged, None),
AgentsOutcome::MalformedPreserved => {
(StepStatus::Refused, Some(Reason::ManagedBlockMalformed))
}
};
let detail = match codex.outcome {
AgentsOutcome::MalformedPreserved => {
"fallow markers are out of order; repair AGENTS.md by hand".to_string()
}
_ => "gate block".to_string(),
};
let mut step = StepReport::new(Some(harness), Step::Hooks, status, Scope::Shared)
.path(ctx, &codex.agents_path)
.detail(detail);
step.reason = reason;
step
}
}