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!("Enabling mi6 for {}...", framework.name);
126
127                // Display each command that was run with done indicator
128                for cmd in &framework.commands_run {
129                    eprintln!(
130                        "  Running: {}{}{}... {}done{}",
131                        colors.bold_white, cmd, colors.reset, colors.bold_green, colors.reset
132                    );
133                }
134
135                // Claude uses plugin-based installation, other frameworks use hooks in config files
136                let install_label = if framework.name == "claude" {
137                    "Installing plugin to"
138                } else {
139                    "Installing hooks to"
140                };
141                eprintln!(
142                    "  {}: {}{}{}... {}done{}",
143                    install_label,
144                    colors.bold_white,
145                    framework.settings_path.display(),
146                    colors.reset,
147                    colors.bold_green,
148                    colors.reset
149                );
150            }
151
152            if cli.settings_local {
153                for framework in &enable_result.frameworks {
154                    eprintln!(
155                        "  {}Note:{} Add '{}' to your project's .gitignore",
156                        colors.yellow,
157                        colors.reset,
158                        framework.settings_path.display()
159                    );
160                }
161            }
162
163            if cli.otel {
164                eprintln!(
165                    "  {}OpenTelemetry configured{} on port {} for automatic token tracking.",
166                    colors.cyan, colors.reset, cli.otel_port
167                );
168            }
169        }
170
171        // If there were failures, return an error after displaying the summary
172        if has_failures {
173            let failed_names: Vec<&str> = enable_result
174                .failures
175                .iter()
176                .map(|f| f.name.as_str())
177                .collect();
178            let count = enable_result.failures.len();
179            let framework_word = if count == 1 {
180                "framework"
181            } else {
182                "frameworks"
183            };
184            bail!(
185                "{} {} failed. Fix the errors and retry with: mi6 enable {}",
186                count,
187                framework_word,
188                failed_names.join(" ")
189            );
190        }
191    }
192
193    Ok(result)
194}
195
196/// Display summary output for partial success scenario.
197fn display_partial_success_summary(
198    enable_result: &mi6_core::EnableResult,
199    colors: &StderrColors,
200    settings_local: bool,
201    otel: bool,
202    otel_port: u16,
203) {
204    // Display enabled frameworks
205    eprintln!();
206    eprintln!("{}Enabled:{}", colors.green, colors.reset);
207    for framework in &enable_result.frameworks {
208        let install_label = if framework.name == "claude" {
209            "plugin"
210        } else {
211            "hooks"
212        };
213        eprintln!("  {}", framework.name);
214        // Display commands that were run
215        for cmd in &framework.commands_run {
216            eprintln!(
217                "    Running: {}{}{}... {}done{}",
218                colors.bold_white, cmd, colors.reset, colors.bold_green, colors.reset
219            );
220        }
221        eprintln!(
222            "    Installing {} to: {}{}{}... {}done{}",
223            install_label,
224            colors.bold_white,
225            framework.settings_path.display(),
226            colors.reset,
227            colors.bold_green,
228            colors.reset
229        );
230    }
231
232    // Display failed frameworks
233    eprintln!();
234    eprintln!("{}Failed:{}", colors.red, colors.reset);
235    for failure in &enable_result.failures {
236        eprintln!("  {}  -> {}", failure.name, failure.error);
237    }
238    eprintln!();
239
240    // Display notes
241    if settings_local {
242        for framework in &enable_result.frameworks {
243            eprintln!(
244                "{}Note:{} Add '{}' to your project's .gitignore",
245                colors.yellow,
246                colors.reset,
247                framework.settings_path.display()
248            );
249        }
250    }
251
252    if otel && !enable_result.frameworks.is_empty() {
253        eprintln!(
254            "{}OpenTelemetry configured{} on port {} for automatic token tracking.",
255            colors.cyan, colors.reset, otel_port
256        );
257    }
258}
259
260fn map_enable_error(e: mi6_core::EnableError) -> anyhow::Error {
261    use mi6_core::EnableError;
262    match e {
263        EnableError::NoFrameworks { supported } => {
264            anyhow::anyhow!(
265                "No supported AI coding frameworks detected.\n\
266                 Supported frameworks: {}\n\
267                 Install one first, or specify explicitly: mi6 enable claude",
268                supported.join(", ")
269            )
270        }
271        EnableError::UnknownFramework(name) => {
272            anyhow::anyhow!("unknown framework: {}", name)
273        }
274        other => anyhow::anyhow!("{}", other),
275    }
276}