mi6_cli/commands/
status.rs

1//! Status command - show mi6 setup and data information.
2
3use std::collections::HashMap;
4use std::path::Path;
5use std::process::Command;
6
7use anyhow::{Context, Result};
8use serde::Serialize;
9
10use mi6_core::{Config, all_adapters};
11
12use super::upgrade::InstallMethod;
13use crate::display::{bold_green, bold_red, bold_white, bold_yellow, dark_grey, format_bytes};
14
15/// Status output structure for JSON serialization
16#[derive(Serialize)]
17struct StatusOutput {
18    version: String,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    commit: Option<String>,
21    frameworks: HashMap<String, FrameworkStatus>,
22    data: DataStatus,
23}
24
25#[derive(Serialize)]
26struct FrameworkStatus {
27    activated: bool,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    enabled_via: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    config_path: Option<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    verified: Option<VerificationResult>,
34}
35
36/// Result of hook verification
37#[derive(Clone, Serialize)]
38struct VerificationResult {
39    /// Whether hooks are responding
40    responding: bool,
41    /// Human-readable status message
42    message: String,
43}
44
45#[derive(Serialize)]
46struct DataStatus {
47    storage_bytes: u64,
48    storage_human: String,
49    path: String,
50}
51
52/// Get the total size of a directory in bytes
53fn get_dir_size(path: &Path) -> std::io::Result<u64> {
54    let mut total = 0;
55
56    if path.is_file() {
57        return Ok(path.metadata()?.len());
58    }
59
60    if path.is_dir() {
61        for entry in std::fs::read_dir(path)? {
62            let entry = entry?;
63            let path = entry.path();
64            if path.is_file() {
65                total += path.metadata()?.len();
66            } else if path.is_dir() {
67                total += get_dir_size(&path)?;
68            }
69        }
70    }
71
72    Ok(total)
73}
74
75/// Get git commit information (short hash)
76fn get_git_commit() -> Option<String> {
77    // Compile-time env var (set by build.rs or CI)
78    option_env!("MI6_GIT_COMMIT").map(String::from)
79}
80
81/// Get the enablement method description for a framework.
82fn get_enablement_method(name: &str) -> &'static str {
83    if name == "claude" {
84        "plugin"
85    } else {
86        "config hooks"
87    }
88}
89
90/// Verify that hooks can invoke mi6 successfully.
91///
92/// This tests the hook path by running `mi6 ingest event --ping`,
93/// which verifies that:
94/// 1. mi6 is in PATH (what hooks actually use)
95/// 2. mi6 can be executed
96/// 3. The command returns successfully
97///
98/// We use PATH lookup (`Command::new("mi6")`) rather than `current_exe()`
99/// because hooks invoke `mi6` via PATH, not the currently-running binary.
100/// This accurately reflects what hooks will experience at runtime.
101fn verify_hooks() -> VerificationResult {
102    // Use PATH lookup since that's what hooks use to invoke mi6
103    let result = Command::new("mi6")
104        .args(["ingest", "event", "--ping"])
105        .stdin(std::process::Stdio::null())
106        .stdout(std::process::Stdio::piped())
107        .stderr(std::process::Stdio::null())
108        .output();
109
110    match result {
111        Ok(output) if output.status.success() => {
112            let stdout = String::from_utf8_lossy(&output.stdout);
113            if stdout.trim() == "pong" {
114                VerificationResult {
115                    responding: true,
116                    message: "hooks responding".to_string(),
117                }
118            } else {
119                VerificationResult {
120                    responding: false,
121                    message: "unexpected response".to_string(),
122                }
123            }
124        }
125        Ok(_) => VerificationResult {
126            responding: false,
127            message: "command failed".to_string(),
128        },
129        Err(e) => {
130            let message = if e.kind() == std::io::ErrorKind::NotFound {
131                "mi6 not in PATH".to_string()
132            } else {
133                format!("error: {}", e)
134            };
135            VerificationResult {
136                responding: false,
137                message,
138            }
139        }
140    }
141}
142
143/// Run the mi6 status command.
144pub fn run_status_command(json: bool, verbose: bool, check: bool) -> Result<()> {
145    // Gather data
146    let version = env!("CARGO_PKG_VERSION").to_string();
147    let commit = get_git_commit();
148
149    // Run verification once if --check is enabled (applies to all enabled frameworks)
150    let verification = if check { Some(verify_hooks()) } else { None };
151
152    let mut frameworks = HashMap::new();
153    for adapter in all_adapters() {
154        let activated = adapter.has_mi6_hooks(false, false);
155        let enabled_via = if activated {
156            Some(get_enablement_method(adapter.name()).to_string())
157        } else {
158            None
159        };
160        let config_path = if activated {
161            // For Claude, show the actual plugin directory path instead of marketplace URL
162            if adapter.name() == "claude" {
163                Config::mi6_dir()
164                    .ok()
165                    .map(|d| d.join("claude-plugin").display().to_string())
166            } else {
167                adapter
168                    .settings_path(false, false)
169                    .ok()
170                    .map(|p| p.display().to_string())
171            }
172        } else {
173            None
174        };
175
176        // Only include verification for enabled frameworks
177        let verified = if activated {
178            verification.clone()
179        } else {
180            None
181        };
182
183        frameworks.insert(
184            adapter.name().to_string(),
185            FrameworkStatus {
186                activated,
187                enabled_via,
188                config_path,
189                verified,
190            },
191        );
192    }
193
194    let db_path = mi6_core::Config::db_path().context("failed to determine database path")?;
195    let storage_bytes = if db_path.exists() {
196        get_dir_size(&db_path).unwrap_or(0)
197    } else {
198        0
199    };
200
201    if json {
202        // JSON output
203        let output = StatusOutput {
204            version,
205            commit,
206            frameworks,
207            data: DataStatus {
208                storage_bytes,
209                storage_human: format_bytes(storage_bytes),
210                path: db_path.display().to_string(),
211            },
212        };
213        println!("{}", serde_json::to_string_pretty(&output)?);
214    } else {
215        // Human-readable output
216        print!("mi6 {}", bold_white(&version));
217        if let Some(ref c) = commit {
218            print!(" ({})", bold_white(c));
219        }
220        println!();
221
222        // Show installation method in verbose mode
223        if verbose {
224            println!();
225            let method_str = match InstallMethod::detect() {
226                Ok(method) => method.name().to_string(),
227                Err(_) => "unknown".to_string(),
228            };
229            println!(
230                "{} {}",
231                dark_grey("mi6 installation method:"),
232                bold_white(&method_str)
233            );
234        }
235        println!();
236
237        // Show mi6 directory
238        let mi6_dir = Config::mi6_dir().context("failed to determine mi6 directory")?;
239        println!(
240            "{} {}",
241            dark_grey("mi6 directory:"),
242            bold_white(&mi6_dir.display().to_string())
243        );
244        println!();
245
246        // Framework status table
247        let adapters: Vec<_> = all_adapters();
248        let framework_width = adapters
249            .iter()
250            .map(|a| a.display_name().len())
251            .max()
252            .unwrap_or(10)
253            .max("Framework".len());
254        let status_width = 13; // "NOT INSTALLED" is the longest
255        let method_width = 12; // "config hooks" is the longest
256        let otel_width = 4; // "OTEL" header width
257        let verified_width = if check { 20 } else { 0 }; // "hooks not responding" is the longest
258        let sep = dark_grey("  │  ");
259
260        // Print header
261        if check {
262            println!(
263                "{:<fw$}{}{:<sw$}{}{:<mw$}{}OTEL{}Verified",
264                "Framework",
265                sep,
266                "Status",
267                sep,
268                "Method",
269                sep,
270                sep,
271                fw = framework_width,
272                sw = status_width,
273                mw = method_width
274            );
275        } else {
276            println!(
277                "{:<fw$}{}{:<sw$}{}{:<mw$}{}OTEL",
278                "Framework",
279                sep,
280                "Status",
281                sep,
282                "Method",
283                sep,
284                fw = framework_width,
285                sw = status_width,
286                mw = method_width
287            );
288        }
289
290        // Print separator line
291        if check {
292            println!(
293                "{}",
294                dark_grey(&format!(
295                    "{}──┼──{}──┼──{}──┼──{}──┼──{}",
296                    "─".repeat(framework_width),
297                    "─".repeat(status_width),
298                    "─".repeat(method_width),
299                    "─".repeat(otel_width),
300                    "─".repeat(verified_width)
301                ))
302            );
303        } else {
304            println!(
305                "{}",
306                dark_grey(&format!(
307                    "{}──┼──{}──┼──{}──┼──{}",
308                    "─".repeat(framework_width),
309                    "─".repeat(status_width),
310                    "─".repeat(method_width),
311                    "─".repeat(otel_width)
312                ))
313            );
314        }
315
316        // Track if any framework has failing verification
317        let mut has_verification_failure = false;
318
319        // Print rows
320        for adapter in &adapters {
321            let status_info = frameworks.get(adapter.name());
322            let enabled = status_info.is_some_and(|f| f.activated);
323            let installed = adapter.is_installed();
324
325            // Pad text before applying colors (ANSI codes break format width)
326            let (status, method, otel, verified_str) = if enabled {
327                let method = status_info
328                    .and_then(|f| f.enabled_via.as_ref())
329                    .cloned()
330                    .unwrap_or_default();
331                // Use trait method for OTEL detection
332                let otel_str = match adapter.otel_support() {
333                    mi6_core::framework::OtelSupport::Enabled => "yes",
334                    mi6_core::framework::OtelSupport::Disabled => "no",
335                    mi6_core::framework::OtelSupport::Unsupported => "n/a",
336                };
337
338                // Get verification status if check mode
339                let verified = if check {
340                    if let Some(v) = status_info.and_then(|f| f.verified.as_ref()) {
341                        if v.responding {
342                            bold_green(&v.message)
343                        } else {
344                            has_verification_failure = true;
345                            bold_yellow(&format!("{} (!)", v.message))
346                        }
347                    } else {
348                        "-".to_string()
349                    }
350                } else {
351                    String::new()
352                };
353
354                (
355                    bold_green(&format!("{:<sw$}", "ENABLED", sw = status_width)),
356                    method,
357                    otel_str,
358                    verified,
359                )
360            } else if !installed {
361                (
362                    dark_grey(&format!("{:<sw$}", "NOT INSTALLED", sw = status_width)),
363                    "-".to_string(),
364                    "n/a",
365                    if check {
366                        "-".to_string()
367                    } else {
368                        String::new()
369                    },
370                )
371            } else {
372                (
373                    bold_red(&format!("{:<sw$}", "NOT ENABLED", sw = status_width)),
374                    "-".to_string(),
375                    "n/a",
376                    if check {
377                        "-".to_string()
378                    } else {
379                        String::new()
380                    },
381                )
382            };
383
384            if check {
385                println!(
386                    "{:<fw$}{}{}{}{:<mw$}{}{}{}{}",
387                    adapter.display_name(),
388                    sep,
389                    status,
390                    sep,
391                    method,
392                    sep,
393                    otel,
394                    sep,
395                    verified_str,
396                    fw = framework_width,
397                    mw = method_width
398                );
399            } else {
400                println!(
401                    "{:<fw$}{}{}{}{:<mw$}{}{}",
402                    adapter.display_name(),
403                    sep,
404                    status,
405                    sep,
406                    method,
407                    sep,
408                    otel,
409                    fw = framework_width,
410                    mw = method_width
411                );
412            }
413        }
414        println!();
415
416        // Show warning if verification failed
417        if check && has_verification_failure {
418            println!(
419                "{}",
420                bold_yellow("Warning: Some hooks are configured but not responding.")
421            );
422            println!("  - Check that mi6 is in your PATH");
423            println!("  - Run: which mi6");
424            println!();
425        }
426
427        // Show verbose section: Relevant Framework Files
428        if verbose {
429            // Collect enabled frameworks with their config paths from the already-computed data
430            let enabled_with_paths: Vec<_> = adapters
431                .iter()
432                .filter_map(|a| {
433                    frameworks.get(a.name()).and_then(|f| {
434                        if f.activated {
435                            f.config_path.as_ref().map(|p| (a.name(), p.as_str()))
436                        } else {
437                            None
438                        }
439                    })
440                })
441                .collect();
442
443            if !enabled_with_paths.is_empty() {
444                println!("{}", bold_white("Relevant Framework Files"));
445                for (name, path) in enabled_with_paths {
446                    println!("- {}: {}", name, path);
447                }
448                println!();
449            }
450        }
451
452        println!(
453            "{}{}{}{}{}",
454            dark_grey("(modify using "),
455            bold_white("mi6 enable <framework>"),
456            dark_grey(" or "),
457            bold_white("mi6 disable <framework>"),
458            dark_grey(")")
459        );
460    }
461
462    Ok(())
463}