mi6_core/
enable.rs

1//! High-level enable API for programmatic hook installation.
2//!
3//! This module provides a simple, high-level API for enabling mi6 hooks
4//! that can be used by other Rust programs (like mi6) without depending
5//! on the CLI crate.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use mi6_core::{InitOptions, enable};
11//!
12//! // Enable with auto-detection
13//! let result = enable(InitOptions::default())?;
14//!
15//! for framework in &result.frameworks {
16//!     println!("Enabled {} at {}", framework.name, framework.settings_path.display());
17//! }
18//!
19//! // Enable specific frameworks with OTel
20//! let result = enable(
21//!     InitOptions::for_frameworks(vec!["claude".into(), "gemini".into()])
22//!         .otel(true)
23//!         .otel_port(4318)
24//! )?;
25//! ```
26
27use std::path::PathBuf;
28
29use crate::config::Config;
30use crate::framework::{
31    ConfigFormat, FrameworkResolutionMode, InitOptions, all_adapters, generate_config,
32    initialize_all, resolve_frameworks,
33};
34use crate::model::error::{FrameworkResolutionError, InitError};
35
36/// Result of enabling a single framework.
37#[derive(Debug, Clone)]
38pub struct FrameworkEnablement {
39    /// The framework name (e.g., "claude", "gemini", "codex").
40    pub name: String,
41    /// The display name (e.g., "Claude Code", "Gemini CLI").
42    pub display_name: String,
43    /// Path where hooks were installed.
44    pub settings_path: PathBuf,
45    /// The hooks configuration that was installed.
46    pub hooks_config: serde_json::Value,
47}
48
49/// Result of the enable process.
50#[derive(Debug, Clone)]
51pub struct EnableResult {
52    /// Frameworks that were enabled.
53    pub frameworks: Vec<FrameworkEnablement>,
54    /// Path to the database (if database initialization was requested).
55    pub db_path: Option<PathBuf>,
56    /// Whether the database should be initialized.
57    pub should_init_db: bool,
58}
59
60/// Error during enable.
61#[derive(Debug)]
62pub enum EnableError {
63    /// No frameworks detected or specified.
64    NoFrameworks {
65        /// List of all supported frameworks.
66        supported: Vec<String>,
67    },
68    /// Unknown framework name.
69    UnknownFramework(String),
70    /// Configuration error.
71    Config(String),
72    /// Initialization error from the core library.
73    Init(InitError),
74}
75
76impl std::fmt::Display for EnableError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            Self::NoFrameworks { supported } => {
80                write!(
81                    f,
82                    "no supported AI coding frameworks detected.\n\
83                     Supported frameworks: {}\n\
84                     Install one first, or specify explicitly.",
85                    supported.join(", ")
86                )
87            }
88            Self::UnknownFramework(name) => write!(f, "unknown framework: {name}"),
89            Self::Config(msg) => write!(f, "configuration error: {msg}"),
90            Self::Init(e) => write!(f, "{e}"),
91        }
92    }
93}
94
95impl std::error::Error for EnableError {
96    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
97        match self {
98            Self::Init(e) => Some(e),
99            _ => None,
100        }
101    }
102}
103
104impl From<InitError> for EnableError {
105    fn from(e: InitError) -> Self {
106        Self::Init(e)
107    }
108}
109
110impl From<FrameworkResolutionError> for EnableError {
111    fn from(e: FrameworkResolutionError) -> Self {
112        match e {
113            FrameworkResolutionError::NoFrameworksFound(_) => Self::NoFrameworks {
114                supported: all_adapters()
115                    .iter()
116                    .map(|a| a.name().to_string())
117                    .collect(),
118            },
119            FrameworkResolutionError::UnknownFramework(name) => Self::UnknownFramework(name),
120        }
121    }
122}
123
124/// Result of previewing enable for a single framework.
125#[derive(Debug, Clone)]
126pub struct PreviewResult {
127    /// The framework name (e.g., "claude", "gemini", "codex").
128    pub name: String,
129    /// The hooks configuration that would be installed.
130    pub hooks_config: serde_json::Value,
131    /// The config format for this framework (JSON or TOML).
132    pub config_format: ConfigFormat,
133}
134
135/// Enable mi6 hooks for AI coding frameworks.
136///
137/// This is the main entry point for programmatic enabling. It:
138/// 1. Resolves which frameworks to enable (auto-detect or explicit)
139/// 2. Installs hooks for each framework
140///
141/// Note: Port conflict checking should be done by the caller using
142/// `mi6_otel_server::is_server_running()` and `mi6_otel_server::find_available_port()`
143/// before calling this function if OTel is enabled.
144///
145/// # Example
146/// ```ignore
147/// use mi6_core::{InitOptions, enable};
148///
149/// let result = enable(InitOptions::for_framework("claude").otel(true))?;
150/// ```
151pub fn enable(options: InitOptions) -> Result<EnableResult, EnableError> {
152    let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
153
154    let mut result = EnableResult {
155        frameworks: Vec::new(),
156        db_path: None,
157        should_init_db: !options.hooks_only,
158    };
159
160    if result.should_init_db {
161        result.db_path = config.db_path().ok();
162    }
163
164    if options.db_only {
165        return Ok(result);
166    }
167
168    let adapters = resolve_frameworks(
169        &options.frameworks,
170        Some(FrameworkResolutionMode::Installed),
171    )?;
172
173    // Update options with resolved framework names so initialize_all knows what to install
174    let framework_names: Vec<String> = adapters.iter().map(|a| a.name().to_string()).collect();
175    let resolved_options = InitOptions {
176        frameworks: framework_names,
177        ..options
178    };
179
180    let init_results = initialize_all(&config, resolved_options)?;
181
182    for (adapter, init_result) in adapters.iter().zip(init_results) {
183        result.frameworks.push(FrameworkEnablement {
184            name: adapter.name().to_string(),
185            display_name: adapter.display_name().to_string(),
186            settings_path: init_result.settings_path,
187            hooks_config: init_result.hooks_config,
188        });
189    }
190
191    Ok(result)
192}
193
194/// Generate hooks configuration without installing.
195///
196/// This is useful for previewing what would be installed (e.g., `--print` mode).
197pub fn preview_enable(options: &InitOptions) -> Result<Vec<PreviewResult>, EnableError> {
198    let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
199    let adapters = resolve_frameworks(
200        &options.frameworks,
201        Some(FrameworkResolutionMode::Installed),
202    )?;
203
204    Ok(adapters
205        .into_iter()
206        .map(|adapter| PreviewResult {
207            name: adapter.name().to_string(),
208            hooks_config: generate_config(adapter, &config, options),
209            config_format: adapter.config_format(),
210        })
211        .collect())
212}
213
214/// Result of disabling a single framework.
215#[derive(Debug, Clone)]
216pub struct FrameworkDisablement {
217    /// The framework name (e.g., "claude", "gemini", "codex").
218    pub name: String,
219    /// The display name (e.g., "Claude Code", "Gemini CLI").
220    pub display_name: String,
221    /// Path where hooks were removed from.
222    pub settings_path: PathBuf,
223    /// Whether hooks were actually removed (false if no mi6 hooks were present).
224    pub hooks_removed: bool,
225}
226
227/// Result of the disable process.
228#[derive(Debug, Clone)]
229pub struct DisableResult {
230    /// Frameworks that were disabled.
231    pub frameworks: Vec<FrameworkDisablement>,
232}
233
234/// Disable mi6 hooks for AI coding frameworks.
235///
236/// This removes mi6 hooks from the specified frameworks' configuration files.
237/// For plugin-based frameworks (like Claude Code), this removes the plugin directory.
238/// If no frameworks are specified, it will auto-detect installed frameworks
239/// that have mi6 hooks enabled.
240///
241/// # Example
242/// ```ignore
243/// use mi6_core::{InitOptions, disable};
244///
245/// // Disable all frameworks with mi6 hooks
246/// let result = disable(&[])?;
247///
248/// // Disable specific framework
249/// let result = disable(&["claude"])?;
250/// ```
251pub fn disable(frameworks: &[&str]) -> Result<DisableResult, EnableError> {
252    disable_with_options(frameworks, false, false)
253}
254
255/// Disable mi6 hooks with specific options.
256///
257/// # Arguments
258/// * `frameworks` - Framework names to disable, or empty to auto-detect
259/// * `local` - If true, disable from project-level config instead of user-level
260/// * `settings_local` - If true, disable from the `.local` variant
261pub fn disable_with_options(
262    frameworks: &[&str],
263    local: bool,
264    settings_local: bool,
265) -> Result<DisableResult, EnableError> {
266    use crate::framework::get_adapter;
267
268    let adapters = if frameworks.is_empty() {
269        // Auto-detect frameworks with mi6 hooks
270        all_adapters()
271            .into_iter()
272            .filter(|a| a.has_mi6_hooks(local, settings_local))
273            .collect::<Vec<_>>()
274    } else {
275        // Use specified frameworks
276        let mut adapters = Vec::new();
277        for name in frameworks {
278            let adapter = get_adapter(name)
279                .ok_or_else(|| EnableError::UnknownFramework((*name).to_string()))?;
280            adapters.push(adapter);
281        }
282        adapters
283    };
284
285    let mut result = DisableResult {
286        frameworks: Vec::new(),
287    };
288
289    for adapter in adapters {
290        let settings_path = match adapter.settings_path(local, settings_local) {
291            Ok(path) => path,
292            Err(_) => continue,
293        };
294
295        // Use the uninstall_hooks method which handles both config-based and plugin-based frameworks
296        let hooks_removed = adapter
297            .uninstall_hooks(local, settings_local)
298            .unwrap_or_default();
299
300        result.frameworks.push(FrameworkDisablement {
301            name: adapter.name().to_string(),
302            display_name: adapter.display_name().to_string(),
303            settings_path,
304            hooks_removed,
305        });
306    }
307
308    Ok(result)
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_enable_error_display() {
317        let err = EnableError::NoFrameworks {
318            supported: vec!["claude".into(), "gemini".into()],
319        };
320        assert!(
321            err.to_string()
322                .contains("no supported AI coding frameworks")
323        );
324
325        let err = EnableError::UnknownFramework("foo".into());
326        assert!(err.to_string().contains("unknown framework: foo"));
327    }
328
329    #[test]
330    fn test_enable_error_from_framework_resolution_error() {
331        let err: EnableError = FrameworkResolutionError::UnknownFramework("foo".to_string()).into();
332        assert!(matches!(err, EnableError::UnknownFramework(name) if name == "foo"));
333
334        let err: EnableError =
335            FrameworkResolutionError::NoFrameworksFound("no frameworks".to_string()).into();
336        assert!(matches!(err, EnableError::NoFrameworks { .. }));
337    }
338}