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 {
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        for framework in &enable_result.frameworks {
110            eprintln!(
111                "Enabling mi6 for {}... {}done{}",
112                framework.name, colors.green, colors.reset
113            );
114            // Claude uses plugin-based installation, other frameworks use hooks in config files
115            let install_label = if framework.name == "claude" {
116                "Installed plugin to:"
117            } else {
118                "Installed hooks to:"
119            };
120            eprintln!(
121                "  {} {}{}{}",
122                install_label,
123                colors.bold,
124                framework.settings_path.display(),
125                colors.reset
126            );
127        }
128
129        if cli.settings_local {
130            for framework in &enable_result.frameworks {
131                eprintln!(
132                    "  {}Note:{} Add '{}' to your project's .gitignore",
133                    colors.yellow,
134                    colors.reset,
135                    framework.settings_path.display()
136                );
137            }
138        }
139
140        if cli.otel {
141            eprintln!(
142                "  {}OpenTelemetry configured{} on port {} for automatic token tracking.",
143                colors.cyan, colors.reset, cli.otel_port
144            );
145        }
146    }
147
148    Ok(result)
149}
150
151fn map_enable_error(e: mi6_core::EnableError) -> anyhow::Error {
152    use mi6_core::EnableError;
153    match e {
154        EnableError::NoFrameworks { supported } => {
155            anyhow::anyhow!(
156                "No supported AI coding frameworks detected.\n\
157                 Supported frameworks: {}\n\
158                 Install one first, or specify explicitly: mi6 enable claude",
159                supported.join(", ")
160            )
161        }
162        EnableError::UnknownFramework(name) => {
163            anyhow::anyhow!("unknown framework: {}", name)
164        }
165        other => anyhow::anyhow!("{}", other),
166    }
167}