use std::borrow::Cow;
use std::path::PathBuf;
use crate::ai_hooks::config::{AgentTarget, HookScope};
use crate::ai_hooks::error::AiHookError;
pub mod claude_code;
pub mod cline;
pub mod codex;
pub mod continue_dev;
pub mod cursor;
pub(crate) mod io;
pub mod json_merge;
pub mod markers;
pub mod windsurf;
#[derive(Debug, Clone)]
pub struct ApplyOutcome {
pub agent: &'static str,
pub path: PathBuf,
pub applied: usize,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct RemoveOutcome {
pub agent: &'static str,
pub path: PathBuf,
pub removed: usize,
#[allow(dead_code)]
pub foreign_preserved: usize,
#[allow(dead_code)]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct CheckOutcome {
pub agent: &'static str,
pub path: PathBuf,
pub missing: Vec<String>,
pub extra_jarvy: Vec<String>,
}
impl CheckOutcome {
pub fn is_clean(&self) -> bool {
self.missing.is_empty() && self.extra_jarvy.is_empty()
}
}
pub trait AgentProvisioner: Sync {
#[allow(dead_code)]
fn slug(&self) -> &'static str;
fn settings_path(&self, scope: HookScope) -> Result<PathBuf, AiHookError>;
fn apply(
&self,
entries: &[ResolvedEntry<'_>],
scope: HookScope,
) -> Result<ApplyOutcome, AiHookError>;
fn check(
&self,
entries: &[ResolvedEntry<'_>],
scope: HookScope,
) -> Result<CheckOutcome, AiHookError>;
fn remove(&self, scope: HookScope) -> Result<RemoveOutcome, AiHookError>;
}
#[derive(Debug, Clone)]
pub struct ResolvedEntry<'a> {
pub name: String,
pub library_source: Option<String>,
pub event: crate::ai_hooks::event::HookEvent,
pub matcher: Option<String>,
pub bash_command: Cow<'a, str>,
pub windows_command: Cow<'a, str>,
pub windows_warned: bool,
pub timeout_ms: u64,
}
static CLAUDE_CODE: claude_code::ClaudeCodeProvisioner = claude_code::ClaudeCodeProvisioner;
static CURSOR: cursor::CursorProvisioner = cursor::CursorProvisioner;
static CODEX: codex::CodexProvisioner = codex::CodexProvisioner;
static WINDSURF: windsurf::WindsurfProvisioner = windsurf::WindsurfProvisioner;
static CLINE: cline::ClineProvisioner = cline::ClineProvisioner;
static CONTINUE: continue_dev::ContinueProvisioner = continue_dev::ContinueProvisioner;
pub fn provisioner_for(target: AgentTarget) -> &'static dyn AgentProvisioner {
match target {
AgentTarget::ClaudeCode => &CLAUDE_CODE,
AgentTarget::Cursor => &CURSOR,
AgentTarget::Codex => &CODEX,
AgentTarget::Windsurf => &WINDSURF,
AgentTarget::Cline => &CLINE,
AgentTarget::Continue => &CONTINUE,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provisioner_for_returns_matching_slug() {
for target in AgentTarget::ALL {
let p = provisioner_for(*target);
assert_eq!(p.slug(), target.slug());
}
}
}