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.managed_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 required_by = if item.kind == ItemKind::Skill {
73 find_referencing_agents(&ctx.managed_root, &lock, &args.name)
74 } else {
75 Vec::new()
76 };
77
78 let result = WhyResult {
79 name: args.name.clone(),
80 kind: item.kind.to_string(),
81 source: item.source.to_string(),
82 version: item.version.clone().unwrap_or_else(|| "-".to_string()),
83 dest_path: dest_path.to_string(),
84 required_by: required_by.clone(),
85 };
86
87 if json {
88 output::print_json(&result);
89 } else {
90 println!("{} ({})", args.name, item.kind);
91 println!(
92 " provided by: {}@{}",
93 item.source,
94 item.version.as_deref().unwrap_or("-")
95 );
96 println!(" installed at: {dest_path}");
97 if required_by.is_empty() {
98 println!(" required by: (no dependents)");
99 } else {
100 println!(" required by:");
101 for agent in &required_by {
102 println!(" {agent}");
103 }
104 }
105 }
106
107 Ok(0)
108}
109
110fn find_referencing_agents(
112 root: &Path,
113 lock: &crate::lock::LockFile,
114 skill_name: &str,
115) -> Vec<String> {
116 let mut refs = Vec::new();
117
118 for (dest_path, item) in &lock.items {
119 if item.kind != ItemKind::Agent {
120 continue;
121 }
122
123 let agent_path = root.join(dest_path);
124 if let Ok(skills) = crate::validate::parse_agent_skills(&agent_path)
125 && skills.iter().any(|s| s == skill_name)
126 {
127 refs.push(dest_path.to_string());
128 }
129 }
130
131 refs.sort();
132 refs
133}