1use std::path::Path;
4
5use serde::Serialize;
6
7use crate::error::MarsError;
8use crate::lock::ItemKind;
9
10use super::output;
11
12#[derive(Debug, clap::Args)]
14pub struct WhyArgs {
15 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
29pub fn run(args: &WhyArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
31 let lock = crate::lock::load(&ctx.project_root)?;
32
33 let mut found = None;
35 for (dest_path, item) in &lock.items {
36 let name_matches = match item.kind {
37 ItemKind::Agent => {
38 let stem = dest_path
39 .as_path()
40 .file_stem()
41 .map(|s| s.to_string_lossy().to_string())
42 .unwrap_or_default();
43 stem == args.name || dest_path.to_string() == args.name
44 }
45 ItemKind::Skill => {
46 let dir_name = dest_path
47 .as_path()
48 .file_name()
49 .map(|s| s.to_string_lossy().to_string())
50 .unwrap_or_default();
51 dir_name == args.name || dest_path.to_string() == args.name
52 }
53 };
54
55 if name_matches {
56 found = Some((dest_path.clone(), item.clone()));
57 break;
58 }
59 }
60
61 let (dest_path, item) = match found {
62 Some(f) => f,
63 None => {
64 return Err(MarsError::Source {
65 source_name: "why".to_string(),
66 message: format!("item `{}` not found in lock file", args.name),
67 });
68 }
69 };
70
71 let mars_dir = ctx.project_root.join(".mars");
73 let required_by = if item.kind == ItemKind::Skill {
74 find_referencing_agents(&mars_dir, &lock, &args.name)
75 } else {
76 Vec::new()
77 };
78
79 let result = WhyResult {
80 name: args.name.clone(),
81 kind: item.kind.to_string(),
82 source: item.source.to_string(),
83 version: item.version.clone().unwrap_or_else(|| "-".to_string()),
84 dest_path: dest_path.to_string(),
85 required_by: required_by.clone(),
86 };
87
88 if json {
89 output::print_json(&result);
90 } else {
91 println!("{} ({})", args.name, item.kind);
92 println!(
93 " provided by: {}@{}",
94 item.source,
95 item.version.as_deref().unwrap_or("-")
96 );
97 println!(" installed at: {dest_path}");
98 if required_by.is_empty() {
99 println!(" required by: (no dependents)");
100 } else {
101 println!(" required by:");
102 for agent in &required_by {
103 println!(" {agent}");
104 }
105 }
106 }
107
108 Ok(0)
109}
110
111fn find_referencing_agents(
113 root: &Path,
114 lock: &crate::lock::LockFile,
115 skill_name: &str,
116) -> Vec<String> {
117 let mut refs = Vec::new();
118
119 for (dest_path, item) in &lock.items {
120 if item.kind != ItemKind::Agent {
121 continue;
122 }
123
124 let agent_path = root.join(dest_path);
125 if let Ok(skills) = crate::validate::parse_agent_skills(&agent_path)
126 && skills.iter().any(|s| s == skill_name)
127 {
128 refs.push(dest_path.to_string());
129 }
130 }
131
132 refs.sort();
133 refs
134}