1use std::path::Path;
4
5use crate::error::MarsError;
6use crate::frontmatter;
7use crate::hash;
8use crate::lock::ItemKind;
9
10use super::output::{self, CatalogEntry, ListEntry};
11
12#[derive(Debug, clap::Args)]
14pub struct ListArgs {
15 #[arg(long)]
17 pub source: Option<String>,
18
19 #[arg(long)]
21 pub kind: Option<String>,
22
23 #[arg(long)]
25 pub status: bool,
26}
27
28pub fn run(args: &ListArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
30 let lock = crate::lock::load(&ctx.managed_root)?;
31
32 if args.status {
33 return run_status(args, &ctx.managed_root, &lock, json);
34 }
35
36 let mut agents = Vec::new();
38 let mut skills = Vec::new();
39
40 for (dest_path, item) in &lock.items {
41 if let Some(ref filter_source) = args.source
43 && &item.source != filter_source
44 {
45 continue;
46 }
47
48 if let Some(ref filter_kind) = args.kind {
50 let kind_str = match item.kind {
51 ItemKind::Agent => "agents",
52 ItemKind::Skill => "skills",
53 };
54 if kind_str != filter_kind && &item.kind.to_string() != filter_kind {
55 continue;
56 }
57 }
58
59 let disk_path = ctx.managed_root.join(dest_path);
61 let content_path = match item.kind {
62 ItemKind::Agent => disk_path.clone(),
63 ItemKind::Skill => disk_path.join("SKILL.md"),
64 };
65
66 let (name, description) = read_name_description(&content_path);
67
68 let entry = CatalogEntry {
69 name,
70 description,
71 kind: item.kind.to_string(),
72 };
73
74 match item.kind {
75 ItemKind::Agent => agents.push(entry),
76 ItemKind::Skill => skills.push(entry),
77 }
78 }
79
80 agents.sort_by(|a, b| a.name.cmp(&b.name));
81 skills.sort_by(|a, b| a.name.cmp(&b.name));
82
83 if json {
84 output::print_json(&serde_json::json!({
85 "agents": agents,
86 "skills": skills,
87 }));
88 } else {
89 output::print_catalog(&agents, &skills, args.kind.as_deref());
90 }
91
92 Ok(0)
93}
94
95fn read_name_description(path: &Path) -> (String, String) {
97 let content = match std::fs::read_to_string(path) {
98 Ok(c) => c,
99 Err(_) => return (path_to_name(path), String::new()),
100 };
101 match frontmatter::parse(&content) {
102 Ok(fm) => {
103 let name = fm
104 .name()
105 .map(str::to_string)
106 .unwrap_or_else(|| path_to_name(path));
107 let description = fm
108 .get("description")
109 .and_then(|v| v.as_str())
110 .unwrap_or_default()
111 .to_string();
112 (name, description)
113 }
114 Err(_) => (path_to_name(path), String::new()),
115 }
116}
117
118fn path_to_name(path: &Path) -> String {
120 path.file_stem()
121 .or_else(|| path.parent().and_then(|p| p.file_name()))
122 .and_then(|n| n.to_str())
123 .unwrap_or("unknown")
124 .to_string()
125}
126
127fn run_status(
129 args: &ListArgs,
130 root: &Path,
131 lock: &crate::lock::LockFile,
132 json: bool,
133) -> Result<i32, MarsError> {
134 let mut entries = Vec::new();
135
136 for (dest_path, item) in &lock.items {
137 if let Some(ref filter_source) = args.source
138 && &item.source != filter_source
139 {
140 continue;
141 }
142
143 if let Some(ref filter_kind) = args.kind {
144 let kind_str = match item.kind {
145 ItemKind::Agent => "agents",
146 ItemKind::Skill => "skills",
147 };
148 if kind_str != filter_kind && &item.kind.to_string() != filter_kind {
149 continue;
150 }
151 }
152
153 let disk_path = root.join(dest_path);
154 let status = if !disk_path.exists() {
155 "missing".to_string()
156 } else if has_conflict_markers(&disk_path) {
157 "conflicted".to_string()
158 } else {
159 let disk_hash = hash::compute_hash(&disk_path, item.kind)?;
160 if disk_hash == item.installed_checksum {
161 "ok".to_string()
162 } else {
163 "modified".to_string()
164 }
165 };
166
167 entries.push(ListEntry {
168 source: item.source.to_string(),
169 item: dest_path.to_string(),
170 kind: item.kind.to_string(),
171 version: item.version.clone().unwrap_or_else(|| "-".to_string()),
172 status,
173 });
174 }
175
176 entries.sort_by(|a, b| (&a.source, &a.item).cmp(&(&b.source, &b.item)));
177 output::print_list(&entries, json);
178 Ok(0)
179}
180
181fn has_conflict_markers(path: &Path) -> bool {
183 std::fs::read_to_string(path)
184 .map(|content| content.contains("<<<<<<<") && content.contains(">>>>>>>"))
185 .unwrap_or(false)
186}