Skip to main content

kernex_adapter_core/
lib.rs

1//! Core trait and supporting types for kernex agent adapters.
2//!
3//! Workspace-internal. Concrete adapter implementations land in follow-up
4//! changes; this crate defines the shape they target.
5
6#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
7
8use std::sync::Arc;
9
10use thiserror::Error;
11
12/// Stable identifier for a supported agent.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14#[non_exhaustive]
15pub enum AdapterId {
16    ClaudeCode,
17    CodexCli,
18    OpenCode,
19    Cursor,
20    Cline,
21}
22
23/// Capability surface an adapter exposes. Sync default methods so adapter
24/// authors can override without dragging async machinery into capability
25/// reporting.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
27#[non_exhaustive]
28pub enum Capability {
29    Skills,
30    Memory,
31    Mcp,
32    OutputStyle,
33}
34
35/// Lightweight detection result surfaced by [`Adapter::detect`].
36#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
37#[non_exhaustive]
38pub struct Detection {
39    pub installed: bool,
40    pub config_root: Option<std::path::PathBuf>,
41    pub version: Option<String>,
42}
43
44impl Detection {
45    /// Construct a `Detection` value without routing through `serde_json`.
46    ///
47    /// The type is `#[non_exhaustive]`, so external crates cannot use a
48    /// struct literal. This constructor is the additive public surface that
49    /// lets downstream consumers build the value with a single call.
50    pub fn new(
51        installed: bool,
52        config_root: Option<std::path::PathBuf>,
53        version: Option<String>,
54    ) -> Self {
55        Self {
56            installed,
57            config_root,
58            version,
59        }
60    }
61}
62
63/// Adapter error type. `#[non_exhaustive]` so future variants are non-breaking.
64#[derive(Debug, Error)]
65#[non_exhaustive]
66pub enum AdapterError {
67    #[error("adapter id {0:?} is not supported in this build")]
68    Unsupported(AdapterId),
69
70    #[error("config root unreadable: {0}")]
71    ConfigRootUnreadable(std::path::PathBuf),
72
73    #[error("io: {0}")]
74    Io(#[from] std::io::Error),
75
76    #[error("serialization: {0}")]
77    Serde(#[from] serde_json::Error),
78}
79
80/// Adapter trait. Object-safe; pin async to I/O methods only.
81#[async_trait::async_trait]
82pub trait Adapter: Send + Sync {
83    fn id(&self) -> AdapterId;
84
85    fn supports(&self, _cap: Capability) -> bool {
86        false
87    }
88
89    async fn detect(&self) -> Result<Detection, AdapterError>;
90
91    async fn install_command(&self) -> Result<String, AdapterError>;
92}
93
94/// Default adapter set known to this build. Empty in this scaffold; follow-up
95/// changes add entries as concrete adapter implementations land.
96pub const DEFAULT_ADAPTER_IDS: &[AdapterId] = &[];
97
98/// Switch-arm factory. Closed match; adding a new `AdapterId` variant breaks
99/// the build until this function is updated.
100pub fn new_adapter(id: AdapterId) -> Result<Arc<dyn Adapter>, AdapterError> {
101    match id {
102        AdapterId::ClaudeCode
103        | AdapterId::CodexCli
104        | AdapterId::OpenCode
105        | AdapterId::Cursor
106        | AdapterId::Cline => Err(AdapterError::Unsupported(id)),
107    }
108}
109
110/// Registry of adapter handles, keyed by [`AdapterId`].
111#[derive(Default)]
112pub struct AdapterRegistry {
113    inner: std::collections::HashMap<AdapterId, Arc<dyn Adapter>>,
114}
115
116impl AdapterRegistry {
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    /// Register an adapter handle keyed by its [`AdapterId`]. Returns the
122    /// previous handle for that id if one was already registered, mirroring
123    /// [`std::collections::HashMap::insert`]. Callers can detect duplicate
124    /// registrations by checking for `Some(_)`.
125    pub fn register(&mut self, adapter: Arc<dyn Adapter>) -> Option<Arc<dyn Adapter>> {
126        self.inner.insert(adapter.id(), adapter)
127    }
128
129    pub fn get(&self, id: AdapterId) -> Option<Arc<dyn Adapter>> {
130        self.inner.get(&id).cloned()
131    }
132}
133
134/// Build a registry pre-populated with [`DEFAULT_ADAPTER_IDS`]. Empty in this
135/// scaffold; follow-up changes populate it as adapter implementations land.
136pub fn default_registry() -> Result<AdapterRegistry, AdapterError> {
137    let mut registry = AdapterRegistry::new();
138    for id in DEFAULT_ADAPTER_IDS {
139        let _ = registry.register(new_adapter(*id)?);
140    }
141    Ok(registry)
142}