pub mod claude;
pub mod codex;
pub mod cursor;
pub mod opencode;
pub mod shared;
pub mod templates;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ClientId {
Claude,
Codex,
Cursor,
OpenCode,
}
impl ClientId {
pub fn as_str(self) -> &'static str {
match self {
ClientId::Claude => "claude",
ClientId::Codex => "codex",
ClientId::Cursor => "cursor",
ClientId::OpenCode => "opencode",
}
}
}
#[derive(Debug, Clone)]
pub struct InstallContext {
pub binary_path: Option<PathBuf>,
pub config_path: PathBuf,
pub dry_run: bool,
pub force: bool,
}
impl InstallContext {
pub fn new(config_path: PathBuf) -> Self {
Self {
binary_path: None,
config_path,
dry_run: false,
force: false,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct InstallReport {
pub client: String,
pub binary_path: PathBuf,
pub config_path: PathBuf,
pub status: InstallStatus,
pub planned_writes: Vec<PathBuf>,
pub backups: Vec<PathBuf>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InstallStatus {
Installed,
Unchanged,
Conflict,
DryRun,
}
#[derive(Debug, Clone, Serialize)]
pub struct UninstallReport {
pub client: String,
pub status: UninstallStatus,
pub removed_paths: Vec<PathBuf>,
pub backups: Vec<PathBuf>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum UninstallStatus {
Removed,
NotInstalled,
DryRun,
}
#[derive(Debug, Clone, Serialize)]
pub struct UpdateReport {
pub client: String,
pub status: UpdateStatus,
pub updated_paths: Vec<PathBuf>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum UpdateStatus {
Updated,
Unchanged,
NotInstalled,
DryRun,
}
#[derive(Debug, Clone, Serialize)]
pub struct DiagnosticReport {
pub client: String,
pub checks: Vec<DiagnosticCheck>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DiagnosticCheck {
pub name: String,
pub status: DiagnosticStatus,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticStatus {
Ok,
Warn,
Fail,
NotApplicable,
}
pub trait Installer {
fn id(&self) -> ClientId;
fn detect(&self) -> anyhow::Result<bool>;
fn install(&self, ctx: &InstallContext) -> anyhow::Result<InstallReport>;
fn update(&self, ctx: &InstallContext) -> anyhow::Result<UpdateReport>;
fn uninstall(&self, ctx: &InstallContext) -> anyhow::Result<UninstallReport>;
fn diagnose(&self, ctx: &InstallContext) -> anyhow::Result<DiagnosticReport>;
}
pub fn installer_for(id: ClientId) -> Box<dyn Installer> {
match id {
ClientId::Claude => Box::new(claude::ClaudeInstaller::new()),
ClientId::Codex => Box::new(codex::CodexInstaller::new()),
ClientId::Cursor => Box::new(cursor::CursorInstaller::new()),
ClientId::OpenCode => Box::new(opencode::OpenCodeInstaller::new()),
}
}