use std::path::PathBuf;
use enum_dispatch::enum_dispatch;
pub mod ccode;
pub mod codex;
mod json_hooks;
pub mod opencode;
pub mod pi;
use ccode::Ccode;
use codex::Codex;
use opencode::Opencode;
use pi::Pi;
#[enum_dispatch]
pub trait Harness: std::fmt::Debug {
fn name(&self) -> &'static str;
fn alias_names(&self) -> &'static [&'static str] {
&[]
}
#[allow(async_fn_in_trait)]
async fn install_hooks(&self) -> Result<PathBuf, InstallHookError>;
}
#[enum_dispatch(Harness)]
#[derive(Debug, Clone, Copy)]
pub enum AnyHarness {
ClaudeCode(Ccode),
Codex(Codex),
Opencode(Opencode),
Pi(Pi),
}
#[derive(Debug, thiserror::Error)]
pub enum HarnessLookupError {
#[error("unknown harness. known harnesses: {0:?}")]
Unknown(Vec<&'static str>),
}
#[derive(Debug, thiserror::Error)]
pub enum InstallHookError {
#[error("unexpected io error")]
Io(
#[from]
#[source]
std::io::Error,
),
#[error("could not parse the existing config as JSON")]
Json(
#[from]
#[source]
serde_json::Error,
),
#[error("the config has an unexpected shape: {0}")]
Malformed(&'static str),
#[error("hook already installed")]
AlreadyInstalled,
}
impl AnyHarness {
pub fn from_name(name: &str) -> Result<Self, HarnessLookupError> {
Self::all()
.iter()
.find(|harness| {
std::iter::once(harness.name())
.chain(harness.alias_names().iter().copied())
.any(|candidate| candidate == name)
})
.copied()
.ok_or_else(|| {
HarnessLookupError::Unknown(Self::all().iter().map(Harness::name).collect())
})
}
#[must_use]
pub fn all() -> &'static [Self] {
&[Self::ClaudeCode(Ccode), Self::Codex(Codex), Self::Opencode(Opencode), Self::Pi(Pi)]
}
}