Skip to main content

mars_agents/cli/
why.rs

1//! `mars why <name>` — explain why an item is installed.
2
3use std::path::Path;
4
5use serde::Serialize;
6
7use crate::error::MarsError;
8use crate::lock::ItemKind;
9
10use super::output;
11
12/// Arguments for `mars why`.
13#[derive(Debug, clap::Args)]
14pub struct WhyArgs {
15    /// Item name to explain (e.g., "frontend-design" or "coder").
16    pub name: String,
17}
18
19#[derive(Debug, Serialize)]
20struct WhyResult {
21    name: String,
22    kind: String,
23    source: String,
24    version: String,
25    dest_path: String,
26    required_by: Vec<String>,
27}
28
29/// Run `mars why`.
30pub fn run(args: &WhyArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
31    let lock = crate::lock::load(&ctx.project_root)?;
32
33    // Find the item by name (try matching dest_path, name stem, or skill dir name)
34    let mut found = None;
35    for (dest_path, item) in &lock.items {
36        let name_matches =
37            dest_path.item_name(item.kind) == args.name || dest_path.as_str() == args.name;
38
39        if name_matches {
40            found = Some((dest_path.clone(), item.clone()));
41            break;
42        }
43    }
44
45    let (dest_path, item) = match found {
46        Some(f) => f,
47        None => {
48            return Err(MarsError::Source {
49                source_name: "why".to_string(),
50                message: format!("item `{}` not found in lock file", args.name),
51            });
52        }
53    };
54
55    // Find which agents reference this item (if it's a skill)
56    let mars_dir = ctx.project_root.join(".mars");
57    let required_by = if item.kind == ItemKind::Skill {
58        let skill_name = dest_path.item_name(ItemKind::Skill);
59        find_referencing_agents(&mars_dir, &lock, &skill_name)
60    } else {
61        Vec::new()
62    };
63
64    let result = WhyResult {
65        name: args.name.clone(),
66        kind: item.kind.to_string(),
67        source: item.source.to_string(),
68        version: item.version.clone().unwrap_or_else(|| "-".to_string()),
69        dest_path: dest_path.to_string(),
70        required_by: required_by.clone(),
71    };
72
73    if json {
74        output::print_json(&result);
75    } else {
76        println!("{} ({})", args.name, item.kind);
77        println!(
78            "  provided by: {}@{}",
79            item.source,
80            item.version.as_deref().unwrap_or("-")
81        );
82        println!("  installed at: {dest_path}");
83        if required_by.is_empty() {
84            println!("  required by: (no dependents)");
85        } else {
86            println!("  required by:");
87            for agent in &required_by {
88                println!("    {agent}");
89            }
90        }
91    }
92
93    Ok(0)
94}
95
96/// Find agents that reference a skill name in their frontmatter.
97fn find_referencing_agents(
98    root: &Path,
99    lock: &crate::lock::LockFile,
100    skill_name: &str,
101) -> Vec<String> {
102    let mut refs = Vec::new();
103
104    for (dest_path, item) in &lock.items {
105        if item.kind != ItemKind::Agent {
106            continue;
107        }
108
109        let agent_path = dest_path.resolve(root);
110        if let Ok(skills) = crate::validate::parse_agent_skills(&agent_path)
111            && skills.iter().any(|s| s == skill_name)
112        {
113            refs.push(dest_path.to_string());
114        }
115    }
116
117    refs.sort();
118    refs
119}