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, FrameworkAdapter, FrameworkResolutionMode, InitOptions, all_adapters,
32    generate_config, initialize_framework, 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    /// Shell commands that were executed during installation.
48    pub commands_run: Vec<String>,
49}
50
51/// Result of a failed framework enable attempt.
52#[derive(Debug, Clone)]
53pub struct FrameworkFailure {
54    /// The framework name (e.g., "claude", "gemini", "codex").
55    pub name: String,
56    /// Error message describing what went wrong.
57    pub error: String,
58}
59
60/// Result of the enable process.
61#[derive(Debug, Clone)]
62pub struct EnableResult {
63    /// Frameworks that were enabled.
64    pub frameworks: Vec<FrameworkEnablement>,
65    /// Frameworks that failed to enable.
66    pub failures: Vec<FrameworkFailure>,
67    /// Path to the database (if database initialization was requested).
68    pub db_path: Option<PathBuf>,
69    /// Whether the database should be initialized.
70    pub should_init_db: bool,
71}
72
73/// Error during enable.
74#[derive(Debug)]
75pub enum EnableError {
76    /// No frameworks detected or specified.
77    NoFrameworks {
78        /// List of all supported frameworks.
79        supported: Vec<String>,
80    },
81    /// Unknown framework name.
82    UnknownFramework(String),
83    /// Configuration error.
84    Config(String),
85    /// Initialization error from the core library.
86    Init(InitError),
87}
88
89impl std::fmt::Display for EnableError {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            Self::NoFrameworks { supported } => {
93                write!(
94                    f,
95                    "no supported AI coding frameworks detected.\n\
96                     Supported frameworks: {}\n\
97                     Install one first, or specify explicitly.",
98                    supported.join(", ")
99                )
100            }
101            Self::UnknownFramework(name) => write!(f, "unknown framework: {name}"),
102            Self::Config(msg) => write!(f, "configuration error: {msg}"),
103            Self::Init(e) => write!(f, "{e}"),
104        }
105    }
106}
107
108impl std::error::Error for EnableError {
109    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
110        match self {
111            Self::Init(e) => Some(e),
112            _ => None,
113        }
114    }
115}
116
117impl From<InitError> for EnableError {
118    fn from(e: InitError) -> Self {
119        Self::Init(e)
120    }
121}
122
123impl From<FrameworkResolutionError> for EnableError {
124    fn from(e: FrameworkResolutionError) -> Self {
125        match e {
126            FrameworkResolutionError::NoFrameworksFound(_) => Self::NoFrameworks {
127                supported: all_adapters()
128                    .iter()
129                    .map(|a| a.name().to_string())
130                    .collect(),
131            },
132            FrameworkResolutionError::UnknownFramework(name) => Self::UnknownFramework(name),
133        }
134    }
135}
136
137/// Result of previewing enable for a single framework.
138#[derive(Debug, Clone)]
139pub struct PreviewResult {
140    /// The framework name (e.g., "claude", "gemini", "codex").
141    pub name: String,
142    /// The hooks configuration that would be installed.
143    pub hooks_config: serde_json::Value,
144    /// The config format for this framework (JSON or TOML).
145    pub config_format: ConfigFormat,
146}
147
148/// Enable mi6 hooks for AI coding frameworks.
149///
150/// This is the main entry point for programmatic enabling. It:
151/// 1. Resolves which frameworks to enable (auto-detect or explicit)
152/// 2. Installs hooks for each framework
153///
154/// Note: Port conflict checking should be done by the caller using
155/// `mi6_otel_server::is_server_running()` and `mi6_otel_server::find_available_port()`
156/// before calling this function if OTel is enabled.
157///
158/// # Example
159/// ```ignore
160/// use mi6_core::{InitOptions, enable};
161///
162/// let result = enable(InitOptions::for_framework("claude").otel(true))?;
163/// ```
164pub fn enable(options: InitOptions) -> Result<EnableResult, EnableError> {
165    let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
166
167    let mut result = EnableResult {
168        frameworks: Vec::new(),
169        failures: Vec::new(),
170        db_path: None,
171        should_init_db: !options.hooks_only,
172    };
173
174    if result.should_init_db {
175        result.db_path = Config::db_path().ok();
176    }
177
178    if options.db_only {
179        return Ok(result);
180    }
181
182    let adapters = resolve_frameworks(
183        &options.frameworks,
184        Some(FrameworkResolutionMode::Installed),
185    )?;
186
187    // Try to enable each framework individually, collecting successes and failures
188    for adapter in adapters {
189        match enable_single_framework(&config, adapter, &options) {
190            Ok(enablement) => {
191                result.frameworks.push(enablement);
192            }
193            Err(e) => {
194                result.failures.push(FrameworkFailure {
195                    name: adapter.name().to_string(),
196                    error: e.to_string(),
197                });
198            }
199        }
200    }
201
202    // If all frameworks failed, return an error
203    if result.frameworks.is_empty() && !result.failures.is_empty() {
204        // Return the first error for backwards compatibility when all fail
205        let first_failure = &result.failures[0];
206        return Err(EnableError::Config(first_failure.error.clone()));
207    }
208
209    Ok(result)
210}
211
212/// Enable mi6 for a single framework adapter.
213fn enable_single_framework(
214    config: &Config,
215    adapter: &dyn FrameworkAdapter,
216    options: &InitOptions,
217) -> Result<FrameworkEnablement, InitError> {
218    let init_result = initialize_framework(config, adapter.name(), options)?;
219
220    Ok(FrameworkEnablement {
221        name: adapter.name().to_string(),
222        display_name: adapter.display_name().to_string(),
223        settings_path: init_result.settings_path,
224        hooks_config: init_result.hooks_config,
225        commands_run: init_result.commands_run,
226    })
227}
228
229/// Generate hooks configuration without installing.
230///
231/// This is useful for previewing what would be installed (e.g., `--print` mode).
232pub fn preview_enable(options: &InitOptions) -> Result<Vec<PreviewResult>, EnableError> {
233    let config = Config::load().map_err(|e| EnableError::Config(e.to_string()))?;
234    let adapters = resolve_frameworks(
235        &options.frameworks,
236        Some(FrameworkResolutionMode::Installed),
237    )?;
238
239    Ok(adapters
240        .into_iter()
241        .map(|adapter| PreviewResult {
242            name: adapter.name().to_string(),
243            hooks_config: generate_config(adapter, &config, options),
244            config_format: adapter.config_format(),
245        })
246        .collect())
247}
248
249/// Result of disabling a single framework.
250#[derive(Debug, Clone)]
251pub struct FrameworkDisablement {
252    /// The framework name (e.g., "claude", "gemini", "codex").
253    pub name: String,
254    /// The display name (e.g., "Claude Code", "Gemini CLI").
255    pub display_name: String,
256    /// Path where hooks were removed from.
257    pub settings_path: PathBuf,
258    /// Whether hooks were actually removed (false if no mi6 hooks were present).
259    pub hooks_removed: bool,
260    /// Shell commands that were executed during uninstallation.
261    pub commands_run: Vec<String>,
262}
263
264/// Result of the disable process.
265#[derive(Debug, Clone)]
266pub struct DisableResult {
267    /// Frameworks that were disabled.
268    pub frameworks: Vec<FrameworkDisablement>,
269}
270
271/// Disable mi6 hooks for AI coding frameworks.
272///
273/// This removes mi6 hooks from the specified frameworks' configuration files.
274/// For plugin-based frameworks (like Claude Code), this removes the plugin directory.
275/// If no frameworks are specified, it will auto-detect installed frameworks
276/// that have mi6 hooks enabled.
277///
278/// # Example
279/// ```ignore
280/// use mi6_core::{InitOptions, disable};
281///
282/// // Disable all frameworks with mi6 hooks
283/// let result = disable(&[])?;
284///
285/// // Disable specific framework
286/// let result = disable(&["claude"])?;
287/// ```
288pub fn disable(frameworks: &[&str]) -> Result<DisableResult, EnableError> {
289    disable_with_options(frameworks, false, false)
290}
291
292/// Disable mi6 hooks with specific options.
293///
294/// # Arguments
295/// * `frameworks` - Framework names to disable, or empty to auto-detect
296/// * `local` - If true, disable from project-level config instead of user-level
297/// * `settings_local` - If true, disable from the `.local` variant
298pub fn disable_with_options(
299    frameworks: &[&str],
300    local: bool,
301    settings_local: bool,
302) -> Result<DisableResult, EnableError> {
303    use crate::framework::get_adapter;
304
305    let adapters = if frameworks.is_empty() {
306        // Auto-detect frameworks with mi6 hooks
307        all_adapters()
308            .into_iter()
309            .filter(|a| a.has_mi6_hooks(local, settings_local))
310            .collect::<Vec<_>>()
311    } else {
312        // Use specified frameworks
313        let mut adapters = Vec::new();
314        for name in frameworks {
315            let adapter = get_adapter(name)
316                .ok_or_else(|| EnableError::UnknownFramework((*name).to_string()))?;
317            adapters.push(adapter);
318        }
319        adapters
320    };
321
322    let mut result = DisableResult {
323        frameworks: Vec::new(),
324    };
325
326    for adapter in adapters {
327        let settings_path = match adapter.settings_path(local, settings_local) {
328            Ok(path) => path,
329            Err(_) => continue,
330        };
331
332        // Use the uninstall_hooks method which handles both config-based and plugin-based frameworks
333        let uninstall_result = adapter
334            .uninstall_hooks(local, settings_local)
335            .unwrap_or_default();
336
337        result.frameworks.push(FrameworkDisablement {
338            name: adapter.name().to_string(),
339            display_name: adapter.display_name().to_string(),
340            settings_path,
341            hooks_removed: uninstall_result.hooks_removed,
342            commands_run: uninstall_result.commands_run,
343        });
344    }
345
346    Ok(result)
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_enable_error_display() {
355        let err = EnableError::NoFrameworks {
356            supported: vec!["claude".into(), "gemini".into()],
357        };
358        assert!(
359            err.to_string()
360                .contains("no supported AI coding frameworks")
361        );
362
363        let err = EnableError::UnknownFramework("foo".into());
364        assert!(err.to_string().contains("unknown framework: foo"));
365    }
366
367    #[test]
368    fn test_enable_error_from_framework_resolution_error() {
369        let err: EnableError = FrameworkResolutionError::UnknownFramework("foo".to_string()).into();
370        assert!(matches!(err, EnableError::UnknownFramework(name) if name == "foo"));
371
372        let err: EnableError =
373            FrameworkResolutionError::NoFrameworksFound("no frameworks".to_string()).into();
374        assert!(matches!(err, EnableError::NoFrameworks { .. }));
375    }
376
377    #[test]
378    fn test_framework_failure_struct() {
379        let failure = FrameworkFailure {
380            name: "codex".to_string(),
381            error: "config.toml has invalid TOML syntax".to_string(),
382        };
383        assert_eq!(failure.name, "codex");
384        assert!(failure.error.contains("invalid TOML"));
385    }
386
387    #[test]
388    fn test_enable_result_has_failures_field() {
389        let result = EnableResult {
390            frameworks: vec![],
391            failures: vec![FrameworkFailure {
392                name: "codex".to_string(),
393                error: "some error".to_string(),
394            }],
395            db_path: None,
396            should_init_db: true,
397        };
398        assert!(result.frameworks.is_empty());
399        assert_eq!(result.failures.len(), 1);
400        assert_eq!(result.failures[0].name, "codex");
401    }
402
403    #[test]
404    fn test_enable_result_with_mixed_success_failure() {
405        let result = EnableResult {
406            frameworks: vec![FrameworkEnablement {
407                name: "claude".to_string(),
408                display_name: "Claude Code".to_string(),
409                settings_path: PathBuf::from("/home/user/.claude/settings.json"),
410                hooks_config: serde_json::json!({}),
411                commands_run: vec![],
412            }],
413            failures: vec![FrameworkFailure {
414                name: "codex".to_string(),
415                error: "config error".to_string(),
416            }],
417            db_path: None,
418            should_init_db: true,
419        };
420        assert_eq!(result.frameworks.len(), 1);
421        assert_eq!(result.frameworks[0].name, "claude");
422        assert_eq!(result.failures.len(), 1);
423        assert_eq!(result.failures[0].name, "codex");
424    }
425}