1use crate::workspace;
2use anyhow::Result;
3use clap::{Args, ValueEnum};
4use metis_core::{Application, Database, Result as MetisResult};
5use serde::Serialize;
6
7#[derive(Debug, Clone, Copy, ValueEnum, Default)]
9pub enum OutputFormat {
10 #[default]
12 Table,
13 Compact,
15 Json,
17}
18
19#[derive(Args)]
20pub struct ListCommand {
21 #[arg(short = 't', long)]
23 pub document_type: Option<String>,
24
25 #[arg(short = 'p', long)]
27 pub phase: Option<String>,
28
29 #[arg(short = 'a', long)]
31 pub all: bool,
32
33 #[arg(long)]
35 pub include_archived: bool,
36
37 #[arg(short = 'f', long, value_enum, default_value = "table")]
39 pub format: OutputFormat,
40}
41
42#[derive(Serialize)]
44struct DocumentOutput {
45 #[serde(rename = "type")]
46 doc_type: String,
47 code: String,
48 title: String,
49 phase: String,
50}
51
52impl ListCommand {
53 pub async fn execute(&self) -> Result<()> {
54 let (workspace_exists, metis_dir) = workspace::has_metis_vault();
56 if !workspace_exists {
57 anyhow::bail!("Not in a Metis workspace. Run 'metis init' to create one.");
58 }
59 let metis_dir = metis_dir.unwrap();
60
61 let db_path = metis_dir.join("metis.db");
63 let database = Database::new(db_path.to_str().unwrap())
64 .map_err(|e| anyhow::anyhow!("Failed to open database for sync: {}", e))?;
65 let app = Application::new(database);
66 app.sync_directory(&metis_dir)
67 .await
68 .map_err(|e| anyhow::anyhow!("Failed to sync workspace: {}", e))?;
69
70 let db = Database::new(db_path.to_str().unwrap())
72 .map_err(|e| anyhow::anyhow!("Database connection failed: {}", e))?;
73 let mut repo = db.into_repository();
74
75 let documents = if self.all {
77 self.list_all_documents(&mut repo).await?
79 } else if let Some(doc_type) = &self.document_type {
80 if let Some(phase) = &self.phase {
81 repo.find_by_type_and_phase(doc_type, phase)
83 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
84 } else {
85 repo.find_by_type(doc_type)
87 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
88 }
89 } else if let Some(phase) = &self.phase {
90 repo.find_by_phase(phase)
92 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
93 } else {
94 self.list_all_documents(&mut repo).await?
96 };
97
98 if documents.is_empty() {
100 match self.format {
101 OutputFormat::Json => println!("[]"),
102 _ => println!("No documents found matching the criteria."),
103 }
104 return Ok(());
105 }
106
107 match self.format {
108 OutputFormat::Table => self.display_table(&documents),
109 OutputFormat::Compact => self.display_compact(&documents),
110 OutputFormat::Json => self.display_json(&documents),
111 }
112
113 Ok(())
114 }
115
116 async fn list_all_documents(
117 &self,
118 repo: &mut metis_core::dal::database::repository::DocumentRepository,
119 ) -> MetisResult<Vec<metis_core::dal::database::models::Document>> {
120 let mut all_docs = Vec::new();
122
123 for doc_type in ["vision", "strategy", "initiative", "task", "adr"] {
125 let mut docs = repo.find_by_type(doc_type)?;
126 all_docs.append(&mut docs);
127 }
128
129 if !self.include_archived {
131 all_docs.retain(|doc| !doc.archived);
132 }
133
134 let type_order = |t: &str| match t {
136 "vision" => 0,
137 "strategy" => 1,
138 "initiative" => 2,
139 "task" => 3,
140 "adr" => 4,
141 _ => 5,
142 };
143
144 all_docs.sort_by(|a, b| {
145 let type_cmp = type_order(&a.document_type).cmp(&type_order(&b.document_type));
146 if type_cmp != std::cmp::Ordering::Equal {
147 type_cmp
148 } else {
149 a.short_code.cmp(&b.short_code)
150 }
151 });
152
153 Ok(all_docs)
154 }
155
156 fn display_table(&self, documents: &[metis_core::dal::database::models::Document]) {
159 println!(
160 "\n{:<12} {:<14} {:<50} {:<12}",
161 "Type", "Code", "Title", "Phase"
162 );
163 println!("{}", "-".repeat(90));
164
165 for doc in documents {
166 println!(
167 "{:<12} {:<14} {:<50} {:<12}",
168 doc.document_type,
169 doc.short_code,
170 self.truncate_string(&doc.title, 48),
171 doc.phase
172 );
173 }
174
175 println!("\nTotal: {} documents", documents.len());
176 }
177
178 fn display_compact(&self, documents: &[metis_core::dal::database::models::Document]) {
181 for doc in documents {
182 println!("{} {} {}", doc.short_code, doc.phase, doc.title);
183 }
184 }
185
186 fn display_json(&self, documents: &[metis_core::dal::database::models::Document]) {
188 let output: Vec<DocumentOutput> = documents
189 .iter()
190 .map(|doc| DocumentOutput {
191 doc_type: doc.document_type.clone(),
192 code: doc.short_code.clone(),
193 title: doc.title.clone(),
194 phase: doc.phase.clone(),
195 })
196 .collect();
197
198 match serde_json::to_string_pretty(&output) {
199 Ok(json) => println!("{}", json),
200 Err(e) => eprintln!("Error serializing to JSON: {}", e),
201 }
202 }
203
204 fn truncate_string(&self, s: &str, max_len: usize) -> String {
205 if s.len() <= max_len {
206 s.to_string()
207 } else {
208 format!("{}...", &s[..max_len.saturating_sub(3)])
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::commands::InitCommand;
217 use tempfile::tempdir;
218
219 #[tokio::test]
220 async fn test_list_command_no_workspace() {
221 let temp_dir = tempdir().unwrap();
222 let original_dir = std::env::current_dir().ok();
223
224 if std::env::set_current_dir(temp_dir.path()).is_err() {
226 return; }
228
229 let cmd = ListCommand {
230 document_type: None,
231 phase: None,
232 all: false,
233 include_archived: false,
234 format: OutputFormat::Table,
235 };
236
237 let result = cmd.execute().await;
238
239 if let Some(original) = original_dir {
241 let _ = std::env::set_current_dir(&original);
242 }
243
244 assert!(result.is_err());
245 assert!(result
246 .unwrap_err()
247 .to_string()
248 .contains("Not in a Metis workspace"));
249 }
250
251 #[tokio::test]
252 async fn test_list_command_empty_workspace() {
253 let temp_dir = tempdir().unwrap();
254 let original_dir = std::env::current_dir().ok();
255
256 std::env::set_current_dir(temp_dir.path()).unwrap();
258
259 let init_cmd = InitCommand {
261 name: Some("Test Project".to_string()),
262 preset: None,
263 strategies: None,
264 initiatives: None,
265 prefix: None,
266 };
267 init_cmd.execute().await.unwrap();
268
269 let cmd = ListCommand {
270 document_type: None,
271 phase: None,
272 all: true,
273 include_archived: false,
274 format: OutputFormat::Table,
275 };
276
277 let result = cmd.execute().await;
278
279 if let Some(original) = original_dir {
281 let _ = std::env::set_current_dir(&original);
282 }
283
284 assert!(result.is_ok());
286 }
287}