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