mi6_cli/commands/
enable.rs

1//! Enable command - install mi6 hooks for AI coding frameworks.
2
3use std::path::PathBuf;
4
5use anyhow::{Context, Result, bail};
6
7use mi6_core::{ConfigFormat, InitOptions, json_to_toml_string, preview_enable};
8use mi6_otel_server::{find_available_port, is_mi6_server, is_server_running};
9
10use crate::display::StderrColors;
11
12/// Result of running enable - used by the binary to determine what storage to create.
13pub struct EnableResult {
14    pub should_init_db: bool,
15    pub db_path: Option<PathBuf>,
16}
17
18/// CLI options for the enable command.
19pub struct EnableCliOptions {
20    pub frameworks: Vec<String>,
21    pub local: bool,
22    pub settings_local: bool,
23    pub print: bool,
24    pub db_only: bool,
25    pub hooks_only: bool,
26    pub otel: bool,
27    pub otel_port: u16,
28    pub no_otel: bool,
29}
30
31/// Run the enable command.
32pub fn run_enable(cli: EnableCliOptions) -> Result<EnableResult> {
33    let colors = StderrColors::new();
34    let auto_detecting = cli.frameworks.is_empty();
35
36    let opts = InitOptions::for_frameworks(cli.frameworks)
37        .local(cli.local)
38        .settings_local(cli.settings_local)
39        .otel(cli.otel)
40        .otel_port(cli.otel_port)
41        .remove_otel(cli.no_otel)
42        .db_only(cli.db_only)
43        .hooks_only(cli.hooks_only);
44
45    let mut result = EnableResult {
46        should_init_db: !cli.hooks_only,
47        db_path: None,
48    };
49
50    if !cli.hooks_only {
51        result.db_path =
52            Some(mi6_core::Config::db_path().context("failed to determine database path")?);
53    }
54
55    // Check for port collision if otel is enabled
56    if cli.otel && is_server_running(cli.otel_port) && !is_mi6_server(cli.otel_port) {
57        let available_port = find_available_port(cli.otel_port.saturating_add(1));
58        bail!(
59            "port {} is in use by another service\n\n\
60             Try using a different port:\n  \
61             mi6 enable --otel --otel-port {}",
62            cli.otel_port,
63            available_port
64        );
65    }
66
67    // Handle print mode
68    if cli.print {
69        let previews = preview_enable(&opts).map_err(map_enable_error)?;
70        for preview in previews {
71            match preview.config_format {
72                ConfigFormat::Json => {
73                    println!(
74                        "{}",
75                        serde_json::to_string_pretty(&preview.hooks_config)
76                            .context("failed to serialize hooks")?
77                    );
78                }
79                ConfigFormat::Toml => {
80                    println!(
81                        "{}",
82                        json_to_toml_string(&preview.hooks_config)
83                            .context("failed to serialize hooks")?
84                    );
85                }
86            }
87        }
88        return Ok(result);
89    }
90
91    // Install hooks unless db_only
92    if !cli.db_only {
93        let enable_result = mi6_core::enable(opts).map_err(map_enable_error)?;
94
95        if auto_detecting && !enable_result.frameworks.is_empty() {
96            let names: Vec<&str> = enable_result
97                .frameworks
98                .iter()
99                .map(|f| f.name.as_str())
100                .collect();
101            eprintln!(
102                "{}Detected frameworks:{} {}",
103                colors.cyan,
104                colors.reset,
105                names.join(", ")
106            );
107        }
108
109        // Check if we have partial failures (some succeeded, some failed)
110        let has_failures = !enable_result.failures.is_empty();
111        let has_successes = !enable_result.frameworks.is_empty();
112
113        if has_failures && has_successes {
114            // Display summary format for partial success
115            display_partial_success_summary(
116                &enable_result,
117                &colors,
118                cli.settings_local,
119                cli.otel,
120                cli.otel_port,
121            );
122        } else if has_successes {
123            // All succeeded - display normal output
124            for framework in &enable_result.frameworks {
125                eprintln!(
126                    "Enabling mi6 for {}... {}done{}",
127                    framework.name, colors.green, colors.reset
128                );
129                // Claude uses plugin-based installation, other frameworks use hooks in config files
130                let install_label = if framework.name == "claude" {
131                    "Installed plugin to:"
132                } else {
133                    "Installed hooks to:"
134                };
135                eprintln!(
136                    "  {} {}{}{}",
137                    install_label,
138                    colors.bold,
139                    framework.settings_path.display(),
140                    colors.reset
141                );
142            }
143
144            if cli.settings_local {
145                for framework in &enable_result.frameworks {
146                    eprintln!(
147                        "  {}Note:{} Add '{}' to your project's .gitignore",
148                        colors.yellow,
149                        colors.reset,
150                        framework.settings_path.display()
151                    );
152                }
153            }
154
155            if cli.otel {
156                eprintln!(
157                    "  {}OpenTelemetry configured{} on port {} for automatic token tracking.",
158                    colors.cyan, colors.reset, cli.otel_port
159                );
160            }
161        }
162
163        // If there were failures, return an error after displaying the summary
164        if has_failures {
165            let failed_names: Vec<&str> = enable_result
166                .failures
167                .iter()
168                .map(|f| f.name.as_str())
169                .collect();
170            let count = enable_result.failures.len();
171            let framework_word = if count == 1 {
172                "framework"
173            } else {
174                "frameworks"
175            };
176            bail!(
177                "{} {} failed. Fix the errors and retry with: mi6 enable {}",
178                count,
179                framework_word,
180                failed_names.join(" ")
181            );
182        }
183    }
184
185    Ok(result)
186}
187
188/// Display summary output for partial success scenario.
189fn display_partial_success_summary(
190    enable_result: &mi6_core::EnableResult,
191    colors: &StderrColors,
192    settings_local: bool,
193    otel: bool,
194    otel_port: u16,
195) {
196    // Display enabled frameworks
197    eprintln!();
198    eprintln!("{}Enabled:{}", colors.green, colors.reset);
199    for framework in &enable_result.frameworks {
200        let install_label = if framework.name == "claude" {
201            "plugin"
202        } else {
203            "hooks"
204        };
205        eprintln!(
206            "  {}  -> {} ({})",
207            framework.name,
208            framework.settings_path.display(),
209            install_label
210        );
211    }
212
213    // Display failed frameworks
214    eprintln!();
215    eprintln!("{}Failed:{}", colors.red, colors.reset);
216    for failure in &enable_result.failures {
217        eprintln!("  {}  -> {}", failure.name, failure.error);
218    }
219    eprintln!();
220
221    // Display notes
222    if settings_local {
223        for framework in &enable_result.frameworks {
224            eprintln!(
225                "{}Note:{} Add '{}' to your project's .gitignore",
226                colors.yellow,
227                colors.reset,
228                framework.settings_path.display()
229            );
230        }
231    }
232
233    if otel && !enable_result.frameworks.is_empty() {
234        eprintln!(
235            "{}OpenTelemetry configured{} on port {} for automatic token tracking.",
236            colors.cyan, colors.reset, otel_port
237        );
238    }
239}
240
241fn map_enable_error(e: mi6_core::EnableError) -> anyhow::Error {
242    use mi6_core::EnableError;
243    match e {
244        EnableError::NoFrameworks { supported } => {
245            anyhow::anyhow!(
246                "No supported AI coding frameworks detected.\n\
247                 Supported frameworks: {}\n\
248                 Install one first, or specify explicitly: mi6 enable claude",
249                supported.join(", ")
250            )
251        }
252        EnableError::UnknownFramework(name) => {
253            anyhow::anyhow!("unknown framework: {}", name)
254        }
255        other => anyhow::anyhow!("{}", other),
256    }
257}