metis_docs_cli/commands/
list.rs1use crate::workspace;
2use anyhow::Result;
3use clap::Args;
4use metis_core::{Database, Result as MetisResult};
5
6#[derive(Args)]
7pub struct ListCommand {
8 #[arg(short = 't', long)]
10 pub document_type: Option<String>,
11
12 #[arg(short = 'p', long)]
14 pub phase: Option<String>,
15
16 #[arg(short = 'a', long)]
18 pub all: bool,
19
20 #[arg(long)]
22 pub include_archived: bool,
23}
24
25impl ListCommand {
26 pub async fn execute(&self) -> Result<()> {
27 let (workspace_exists, metis_dir) = workspace::has_metis_vault();
29 if !workspace_exists {
30 anyhow::bail!("Not in a Metis workspace. Run 'metis init' to create one.");
31 }
32 let metis_dir = metis_dir.unwrap();
33
34 let db_path = metis_dir.join("metis.db");
36 let db = Database::new(db_path.to_str().unwrap())
37 .map_err(|e| anyhow::anyhow!("Database connection failed: {}", e))?;
38 let mut repo = db.into_repository();
39
40 let documents = if self.all {
42 self.list_all_documents(&mut repo).await?
44 } else if let Some(doc_type) = &self.document_type {
45 if let Some(phase) = &self.phase {
46 repo.find_by_type_and_phase(doc_type, phase)
48 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
49 } else {
50 repo.find_by_type(doc_type)
52 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
53 }
54 } else if let Some(phase) = &self.phase {
55 repo.find_by_phase(phase)
57 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
58 } else {
59 self.list_all_documents(&mut repo).await?
61 };
62
63 if documents.is_empty() {
65 println!("No documents found matching the criteria.");
66 return Ok(());
67 }
68
69 self.display_documents(&documents);
70
71 Ok(())
72 }
73
74 async fn list_all_documents(
75 &self,
76 repo: &mut metis_core::dal::database::repository::DocumentRepository,
77 ) -> MetisResult<Vec<metis_core::dal::database::models::Document>> {
78 let mut all_docs = Vec::new();
80
81 for doc_type in ["vision", "strategy", "initiative", "task", "adr"] {
83 let mut docs = repo.find_by_type(doc_type)?;
84 all_docs.append(&mut docs);
85 }
86
87 if !self.include_archived {
89 all_docs.retain(|doc| !doc.archived);
90 }
91
92 all_docs.sort_by(|a, b| {
94 b.updated_at
95 .partial_cmp(&a.updated_at)
96 .unwrap_or(std::cmp::Ordering::Equal)
97 });
98
99 Ok(all_docs)
100 }
101
102 fn display_documents(&self, documents: &[metis_core::dal::database::models::Document]) {
103 println!(
104 "\n{:<15} {:<30} {:<15} {:<15} {:<20}",
105 "TYPE", "TITLE", "PHASE", "ID", "UPDATED"
106 );
107 println!("{}", "-".repeat(95));
108
109 for doc in documents {
110 let updated = chrono::DateTime::from_timestamp(doc.updated_at as i64, 0)
111 .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
112 .unwrap_or_else(|| "Unknown".to_string());
113
114 println!(
115 "{:<15} {:<30} {:<15} {:<15} {:<20}",
116 doc.document_type,
117 self.truncate_string(&doc.title, 28),
118 doc.phase,
119 self.truncate_string(&doc.id, 13),
120 updated
121 );
122 }
123
124 println!("\nTotal: {} documents", documents.len());
125 }
126
127 fn truncate_string(&self, s: &str, max_len: usize) -> String {
128 if s.len() <= max_len {
129 s.to_string()
130 } else {
131 format!("{}...", &s[..max_len.saturating_sub(3)])
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::commands::InitCommand;
140 use tempfile::tempdir;
141
142 #[tokio::test]
143 async fn test_list_command_no_workspace() {
144 let temp_dir = tempdir().unwrap();
145 let original_dir = std::env::current_dir().ok();
146
147 if std::env::set_current_dir(temp_dir.path()).is_err() {
149 return; }
151
152 let cmd = ListCommand {
153 document_type: None,
154 phase: None,
155 all: false,
156 include_archived: false,
157 };
158
159 let result = cmd.execute().await;
160
161 if let Some(original) = original_dir {
163 let _ = std::env::set_current_dir(&original);
164 }
165
166 assert!(result.is_err());
167 assert!(result
168 .unwrap_err()
169 .to_string()
170 .contains("Not in a Metis workspace"));
171 }
172
173 #[tokio::test]
174 async fn test_list_command_empty_workspace() {
175 let temp_dir = tempdir().unwrap();
176 let original_dir = std::env::current_dir().ok();
177
178 std::env::set_current_dir(temp_dir.path()).unwrap();
180
181 let init_cmd = InitCommand {
183 name: Some("Test Project".to_string()),
184 preset: None,
185 strategies: None,
186 initiatives: None,
187 prefix: None,
188 };
189 init_cmd.execute().await.unwrap();
190
191 let cmd = ListCommand {
192 document_type: None,
193 phase: None,
194 all: true,
195 include_archived: false,
196 };
197
198 let result = cmd.execute().await;
199
200 if let Some(original) = original_dir {
202 let _ = std::env::set_current_dir(&original);
203 }
204
205 assert!(result.is_ok());
207 }
208}