metis_docs_cli/commands/
list.rs1use crate::workspace;
2use anyhow::Result;
3use clap::Args;
4use metis_core::{Application, 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 database = Database::new(db_path.to_str().unwrap())
37 .map_err(|e| anyhow::anyhow!("Failed to open database for sync: {}", e))?;
38 let app = Application::new(database);
39 app.sync_directory(&metis_dir)
40 .await
41 .map_err(|e| anyhow::anyhow!("Failed to sync workspace: {}", e))?;
42
43 let db = Database::new(db_path.to_str().unwrap())
45 .map_err(|e| anyhow::anyhow!("Database connection failed: {}", e))?;
46 let mut repo = db.into_repository();
47
48 let documents = if self.all {
50 self.list_all_documents(&mut repo).await?
52 } else if let Some(doc_type) = &self.document_type {
53 if let Some(phase) = &self.phase {
54 repo.find_by_type_and_phase(doc_type, phase)
56 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
57 } else {
58 repo.find_by_type(doc_type)
60 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
61 }
62 } else if let Some(phase) = &self.phase {
63 repo.find_by_phase(phase)
65 .map_err(|e| anyhow::anyhow!("Database query failed: {}", e))?
66 } else {
67 self.list_all_documents(&mut repo).await?
69 };
70
71 if documents.is_empty() {
73 println!("No documents found matching the criteria.");
74 return Ok(());
75 }
76
77 self.display_documents(&documents);
78
79 Ok(())
80 }
81
82 async fn list_all_documents(
83 &self,
84 repo: &mut metis_core::dal::database::repository::DocumentRepository,
85 ) -> MetisResult<Vec<metis_core::dal::database::models::Document>> {
86 let mut all_docs = Vec::new();
88
89 for doc_type in ["vision", "strategy", "initiative", "task", "adr"] {
91 let mut docs = repo.find_by_type(doc_type)?;
92 all_docs.append(&mut docs);
93 }
94
95 if !self.include_archived {
97 all_docs.retain(|doc| !doc.archived);
98 }
99
100 all_docs.sort_by(|a, b| {
102 b.updated_at
103 .partial_cmp(&a.updated_at)
104 .unwrap_or(std::cmp::Ordering::Equal)
105 });
106
107 Ok(all_docs)
108 }
109
110 fn display_documents(&self, documents: &[metis_core::dal::database::models::Document]) {
111 println!(
112 "\n{:<15} {:<30} {:<15} {:<15} {:<20}",
113 "TYPE", "TITLE", "PHASE", "ID", "UPDATED"
114 );
115 println!("{}", "-".repeat(95));
116
117 for doc in documents {
118 let updated = chrono::DateTime::from_timestamp(doc.updated_at as i64, 0)
119 .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
120 .unwrap_or_else(|| "Unknown".to_string());
121
122 println!(
123 "{:<15} {:<30} {:<15} {:<15} {:<20}",
124 doc.document_type,
125 self.truncate_string(&doc.title, 28),
126 doc.phase,
127 self.truncate_string(&doc.id, 13),
128 updated
129 );
130 }
131
132 println!("\nTotal: {} documents", documents.len());
133 }
134
135 fn truncate_string(&self, s: &str, max_len: usize) -> String {
136 if s.len() <= max_len {
137 s.to_string()
138 } else {
139 format!("{}...", &s[..max_len.saturating_sub(3)])
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::commands::InitCommand;
148 use tempfile::tempdir;
149
150 #[tokio::test]
151 async fn test_list_command_no_workspace() {
152 let temp_dir = tempdir().unwrap();
153 let original_dir = std::env::current_dir().ok();
154
155 if std::env::set_current_dir(temp_dir.path()).is_err() {
157 return; }
159
160 let cmd = ListCommand {
161 document_type: None,
162 phase: None,
163 all: false,
164 include_archived: false,
165 };
166
167 let result = cmd.execute().await;
168
169 if let Some(original) = original_dir {
171 let _ = std::env::set_current_dir(&original);
172 }
173
174 assert!(result.is_err());
175 assert!(result
176 .unwrap_err()
177 .to_string()
178 .contains("Not in a Metis workspace"));
179 }
180
181 #[tokio::test]
182 async fn test_list_command_empty_workspace() {
183 let temp_dir = tempdir().unwrap();
184 let original_dir = std::env::current_dir().ok();
185
186 std::env::set_current_dir(temp_dir.path()).unwrap();
188
189 let init_cmd = InitCommand {
191 name: Some("Test Project".to_string()),
192 preset: None,
193 strategies: None,
194 initiatives: None,
195 prefix: None,
196 };
197 init_cmd.execute().await.unwrap();
198
199 let cmd = ListCommand {
200 document_type: None,
201 phase: None,
202 all: true,
203 include_archived: false,
204 };
205
206 let result = cmd.execute().await;
207
208 if let Some(original) = original_dir {
210 let _ = std::env::set_current_dir(&original);
211 }
212
213 assert!(result.is_ok());
215 }
216}