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
44/// Adapter error type. `#[non_exhaustive]` so future variants are non-breaking.
45#[derive(Debug, Error)]
46#[non_exhaustive]
47pub enum AdapterError {
48    #[error("adapter id {0:?} is not supported in this build")]
49    Unsupported(AdapterId),
50
51    #[error("config root unreadable: {0}")]
52    ConfigRootUnreadable(std::path::PathBuf),
53
54    #[error("io: {0}")]
55    Io(#[from] std::io::Error),
56
57    #[error("serialization: {0}")]
58    Serde(#[from] serde_json::Error),
59}
60
61/// Adapter trait. Object-safe; pin async to I/O methods only.
62#[async_trait::async_trait]
63pub trait Adapter: Send + Sync {
64    fn id(&self) -> AdapterId;
65
66    fn supports(&self, _cap: Capability) -> bool {
67        false
68    }
69
70    async fn detect(&self) -> Result<Detection, AdapterError>;
71
72    async fn install_command(&self) -> Result<String, AdapterError>;
73}
74
75/// Default adapter set known to this build. Empty in this scaffold; follow-up
76/// changes add entries as concrete adapter implementations land.
77pub const DEFAULT_ADAPTER_IDS: &[AdapterId] = &[];
78
79/// Switch-arm factory. Closed match; adding a new `AdapterId` variant breaks
80/// the build until this function is updated.
81pub fn new_adapter(id: AdapterId) -> Result<Arc<dyn Adapter>, AdapterError> {
82    match id {
83        AdapterId::ClaudeCode
84        | AdapterId::CodexCli
85        | AdapterId::OpenCode
86        | AdapterId::Cursor
87        | AdapterId::Cline => Err(AdapterError::Unsupported(id)),
88    }
89}
90
91/// Registry of adapter handles, keyed by [`AdapterId`].
92#[derive(Default)]
93pub struct AdapterRegistry {
94    inner: std::collections::HashMap<AdapterId, Arc<dyn Adapter>>,
95}
96
97impl AdapterRegistry {
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Register an adapter handle keyed by its [`AdapterId`]. Returns the
103    /// previous handle for that id if one was already registered, mirroring
104    /// [`std::collections::HashMap::insert`]. Callers can detect duplicate
105    /// registrations by checking for `Some(_)`.
106    pub fn register(&mut self, adapter: Arc<dyn Adapter>) -> Option<Arc<dyn Adapter>> {
107        self.inner.insert(adapter.id(), adapter)
108    }
109
110    pub fn get(&self, id: AdapterId) -> Option<Arc<dyn Adapter>> {
111        self.inner.get(&id).cloned()
112    }
113}
114
115/// Build a registry pre-populated with [`DEFAULT_ADAPTER_IDS`]. Empty in this
116/// scaffold; follow-up changes populate it as adapter implementations land.
117pub fn default_registry() -> Result<AdapterRegistry, AdapterError> {
118    let mut registry = AdapterRegistry::new();
119    for id in DEFAULT_ADAPTER_IDS {
120        let _ = registry.register(new_adapter(*id)?);
121    }
122    Ok(registry)
123}