use super::{Ctx, Harness, Mode, Reason, Scope, Step, StepReport, StepStatus};
use crate::setup_hooks::{
AgentsBlockReport, AgentsOutcome, GateReport, 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 => 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,
home: ctx.home.as_deref(),
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(gate_steps(ctx, harness, &claude, "PreToolUse gate handler"));
}
if let Some(codex) = report.codex {
steps.extend(gate_steps(
ctx,
harness,
&codex.gate,
"PreToolUse gate handler; Codex runs it after you trust it in /hooks",
));
if let Some(block) = &codex.agents_block {
steps.push(routing_block_step(ctx, harness, block));
}
}
steps
}
fn gate_steps(
ctx: &Ctx,
harness: Harness,
gate: &GateReport,
handler_detail: &str,
) -> Vec<StepReport> {
let settings_status = match (&gate.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
}
};
let handler = StepReport::new(Some(harness), Step::Hooks, settings_status, ctx.scope())
.path(ctx, &gate.settings_path)
.detail(handler_detail);
let (script_status, reason) = match gate.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, &gate.script_path)
.detail("gate script");
if matches!(gate.script_outcome, ScriptOutcome::UserEditedPreserved) {
script.detail = Some("no fallow marker; pass --force to replace it".to_string());
}
script.reason = reason;
vec![handler, script]
}
fn routing_block_step(ctx: &Ctx, harness: Harness, block: &AgentsBlockReport) -> StepReport {
let (status, reason) = match block.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 block.outcome {
AgentsOutcome::MalformedPreserved => {
"fallow markers are out of order; repair AGENTS.md by hand".to_string()
}
_ => "routing block".to_string(),
};
let mut step = StepReport::new(Some(harness), Step::Hooks, status, Scope::Shared)
.path(ctx, &block.path)
.detail(detail);
step.reason = reason;
step
}