1mod detection;
8
9mod claude;
10mod codex;
11mod cursor;
12mod gemini;
13mod kiro;
14mod opencode;
15mod windsurf;
16
17pub use claude::ClaudeAdapter;
18pub use codex::CodexAdapter;
19pub use cursor::CursorAdapter;
20pub use gemini::GeminiAdapter;
21pub use kiro::KiroAdapter;
22pub use opencode::OpenCodeAdapter;
23pub use windsurf::WindsurfAdapter;
24
25use std::path::{Path, PathBuf};
26
27#[derive(Debug, thiserror::Error)]
29pub enum AdapterError {
30 #[error("IO error: {0}")]
31 Io(#[from] std::io::Error),
32 #[error("JSON error: {0}")]
33 Json(#[from] serde_json::Error),
34 #[error("TOML deserialize error: {0}")]
35 TomlDe(#[from] toml::de::Error),
36 #[error("TOML serialize error: {0}")]
37 TomlSer(#[from] toml::ser::Error),
38 #[error("Config error: {0}")]
39 Config(String),
40 #[error("Home directory not found")]
41 NoHomeDir,
42}
43
44pub type Result<T> = std::result::Result<T, AdapterError>;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ToolStatus {
49 NotInstalled,
51 Detected,
53 HooksRegistered,
55}
56
57impl ToolStatus {
58 pub fn symbol(&self) -> &'static str {
59 match self {
60 Self::NotInstalled => "\u{2717}", Self::Detected => "\u{25cf}", Self::HooksRegistered => "\u{2713}", }
64 }
65
66 pub fn label(&self) -> &'static str {
67 match self {
68 Self::NotInstalled => "not found",
69 Self::Detected => "detected",
70 Self::HooksRegistered => "registered",
71 }
72 }
73}
74
75pub trait ToolAdapter: Send + Sync {
80 fn name(&self) -> &str;
82
83 fn display_name(&self) -> &str;
85
86 fn is_installed(&self) -> bool;
88
89 fn hooks_registered(&self) -> bool;
91
92 fn register_hooks(&self, bridge_script: &Path) -> Result<()>;
96
97 fn unregister_hooks(&self) -> Result<()>;
99
100 fn config_path(&self) -> Option<PathBuf>;
102
103 fn supported_events(&self) -> &[&str];
105
106 fn status(&self) -> ToolStatus {
108 if !self.is_installed() {
109 ToolStatus::NotInstalled
110 } else if self.hooks_registered() {
111 ToolStatus::HooksRegistered
112 } else {
113 ToolStatus::Detected
114 }
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct ToolInfo {
121 pub name: String,
122 pub display_name: String,
123 pub status: ToolStatus,
124 pub config_path: Option<PathBuf>,
125}
126
127pub fn all_adapters() -> Vec<Box<dyn ToolAdapter>> {
129 vec![
130 Box::new(ClaudeAdapter::new()),
131 Box::new(CursorAdapter::new()),
132 Box::new(CodexAdapter::new()),
133 Box::new(WindsurfAdapter::new()),
134 Box::new(KiroAdapter::new()),
135 Box::new(OpenCodeAdapter::new()),
136 Box::new(GeminiAdapter::new()),
137 ]
138}
139
140pub fn detect_all() -> Vec<ToolInfo> {
142 all_adapters()
143 .iter()
144 .map(|a| ToolInfo {
145 name: a.name().to_string(),
146 display_name: a.display_name().to_string(),
147 status: a.status(),
148 config_path: a.config_path(),
149 })
150 .collect()
151}
152
153pub fn auto_register_all(bridge_script: &Path) -> Vec<String> {
157 let mut registered = Vec::new();
158 for adapter in all_adapters() {
159 if adapter.is_installed() && !adapter.hooks_registered() {
160 if adapter.register_hooks(bridge_script).is_ok() {
161 registered.push(adapter.display_name().to_string());
162 }
163 }
164 }
165 registered
166}
167
168pub fn get_adapter(name: &str) -> Option<Box<dyn ToolAdapter>> {
170 all_adapters().into_iter().find(|a| a.name() == name)
171}