1use anyhow::Result;
2use clap::{Parser, Subcommand};
3use tracing_subscriber::filter::LevelFilter;
4
5use crate::commands::{
6 ArchiveCommand, ConfigCommand, CreateCommand, InitCommand, ListCommand, McpCommand,
7 SearchCommand, StatusCommand, SyncCommand, TransitionCommand, ValidateCommand,
8};
9
10#[derive(Parser)]
11#[command(name = "metis")]
12#[command(about = "A document management system for strategic planning")]
13#[command(version)]
14pub struct Cli {
15 #[arg(short, long, action = clap::ArgAction::Count)]
17 pub verbose: u8,
18
19 #[command(subcommand)]
20 pub command: Commands,
21}
22
23#[derive(Subcommand)]
24pub enum Commands {
25 Init(InitCommand),
27 Sync(SyncCommand),
29 Create(CreateCommand),
31 Search(SearchCommand),
33 Transition(TransitionCommand),
35 List(ListCommand),
37 Status(StatusCommand),
39 Archive(ArchiveCommand),
41 Validate(ValidateCommand),
43 Mcp(McpCommand),
45 Config(ConfigCommand),
47}
48
49impl Cli {
50 pub fn init_logging(&self) {
51 let level = match self.verbose {
52 0 => LevelFilter::WARN,
53 1 => LevelFilter::INFO,
54 2 => LevelFilter::DEBUG,
55 _ => LevelFilter::TRACE,
56 };
57
58 tracing_subscriber::fmt()
59 .with_max_level(level)
60 .with_target(false)
61 .init();
62 }
63
64 pub async fn execute(&self) -> Result<()> {
65 match &self.command {
66 Commands::Init(cmd) => cmd.execute().await,
67 Commands::Sync(cmd) => cmd.execute().await,
68 Commands::Create(cmd) => cmd.execute().await,
69 Commands::Search(cmd) => cmd.execute().await,
70 Commands::Transition(cmd) => cmd.execute().await,
71 Commands::List(cmd) => cmd.execute().await,
72 Commands::Status(cmd) => cmd.execute().await,
73 Commands::Archive(cmd) => cmd.execute().await,
74 Commands::Validate(cmd) => cmd.execute().await,
75 Commands::Mcp(cmd) => cmd.execute().await,
76 Commands::Config(cmd) => cmd.execute().await,
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::commands::create::CreateCommands;
85 use crate::commands::{
86 ArchiveCommand, CreateCommand, ListCommand, SearchCommand, StatusCommand, SyncCommand,
87 TransitionCommand, ValidateCommand,
88 };
89 use std::fs;
90 use tempfile::tempdir;
91
92 #[tokio::test]
93 async fn test_comprehensive_cli_workflow() {
94 let temp_dir = tempdir().unwrap();
95 let original_dir = std::env::current_dir().ok();
96
97 std::env::set_current_dir(temp_dir.path()).unwrap();
99
100 let init_cmd = InitCommand {
102 name: Some("Integration Test Project".to_string()),
103 prefix: None,
104 preset: None,
105 strategies: None,
106 initiatives: None,
107 };
108 init_cmd
109 .execute()
110 .await
111 .expect("Failed to initialize project");
112
113 let metis_dir = temp_dir.path().join(".metis");
114 assert!(
115 metis_dir.exists(),
116 "Metis directory should exist after init"
117 );
118 assert!(
119 metis_dir.join("vision.md").exists(),
120 "Vision document should be created"
121 );
122
123 let sync_cmd = SyncCommand {};
125 sync_cmd.execute().await.expect("Failed to sync workspace");
126
127 let create_strategy_cmd = CreateCommand {
129 document_type: CreateCommands::Strategy {
130 title: "Test Strategy for Integration".to_string(),
131 vision: Some("integration-test-project".to_string()),
132 },
133 };
134 create_strategy_cmd
135 .execute()
136 .await
137 .expect("Failed to create strategy");
138
139 let create_initiative_cmd = CreateCommand {
141 document_type: CreateCommands::Initiative {
142 title: "Test Initiative".to_string(),
143 strategy: "TEST-S-0001".to_string(),
144 },
145 };
146 create_initiative_cmd
147 .execute()
148 .await
149 .expect("Failed to create initiative");
150
151 let create_task_cmd = CreateCommand {
153 document_type: CreateCommands::Task {
154 title: "Test Task".to_string(),
155 initiative: "TEST-I-0001".to_string(),
156 },
157 };
158 create_task_cmd
159 .execute()
160 .await
161 .expect("Failed to create task");
162
163 let create_adr_cmd = CreateCommand {
165 document_type: CreateCommands::Adr {
166 title: "Test Architecture Decision".to_string(),
167 },
168 };
169 create_adr_cmd
170 .execute()
171 .await
172 .expect("Failed to create ADR");
173
174 let adrs_dir = metis_dir.join("adrs");
176 let adr_files: Vec<_> = fs::read_dir(&adrs_dir)
177 .unwrap()
178 .filter_map(|entry| entry.ok())
179 .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "md"))
180 .collect();
181 assert!(!adr_files.is_empty(), "ADR file should be created");
182
183 let sync_cmd2 = SyncCommand {};
185 sync_cmd2
186 .execute()
187 .await
188 .expect("Failed to sync after creating documents");
189
190 let transition_vision_cmd = TransitionCommand {
192 short_code: "TEST-V-0001".to_string(),
193 phase: Some("review".to_string()),
194 };
195 transition_vision_cmd
196 .execute()
197 .await
198 .expect("Failed to transition vision");
199
200 let transition_strategy_to_design_cmd = TransitionCommand {
202 short_code: "TEST-S-0001".to_string(),
203 phase: Some("design".to_string()),
204 };
205 transition_strategy_to_design_cmd
206 .execute()
207 .await
208 .expect("Failed to transition strategy to design");
209
210 let transition_strategy_to_ready_cmd = TransitionCommand {
211 short_code: "TEST-S-0001".to_string(),
212 phase: Some("ready".to_string()),
213 };
214 transition_strategy_to_ready_cmd
215 .execute()
216 .await
217 .expect("Failed to transition strategy to ready");
218
219 let transition_strategy_to_active_cmd = TransitionCommand {
220 short_code: "TEST-S-0001".to_string(),
221 phase: Some("active".to_string()),
222 };
223 transition_strategy_to_active_cmd
224 .execute()
225 .await
226 .expect("Failed to transition strategy to active");
227
228 let transition_task_to_active_cmd = TransitionCommand {
230 short_code: "TEST-T-0001".to_string(),
231 phase: Some("active".to_string()),
232 };
233 transition_task_to_active_cmd
234 .execute()
235 .await
236 .expect("Failed to transition task to active");
237
238 let transition_task_to_completed_cmd = TransitionCommand {
239 short_code: "TEST-T-0001".to_string(),
240 phase: Some("completed".to_string()),
241 };
242 transition_task_to_completed_cmd
243 .execute()
244 .await
245 .expect("Failed to transition task to completed");
246
247 let archive_task_cmd = ArchiveCommand {
249 short_code: "TEST-T-0001".to_string(),
250 document_type: Some("task".to_string()),
251 };
252 archive_task_cmd
253 .execute()
254 .await
255 .expect("Failed to archive task");
256
257 let list_cmd = ListCommand {
259 document_type: None,
260 phase: None,
261 all: true,
262 include_archived: true,
263 };
264 list_cmd.execute().await.expect("Failed to list documents");
265
266 let status_cmd = StatusCommand {
268 include_archived: false,
269 };
270 status_cmd.execute().await.expect("Failed to get status");
271
272 let search_cmd = SearchCommand {
274 query: "test".to_string(),
275 limit: 10,
276 };
277 search_cmd
278 .execute()
279 .await
280 .expect("Failed to search documents");
281
282 let validate_cmd = ValidateCommand {
284 file_path: metis_dir.join("vision.md"),
285 };
286 validate_cmd
287 .execute()
288 .await
289 .expect("Failed to validate document");
290
291 if let Some(original) = original_dir {
293 let _ = std::env::set_current_dir(&original);
294 }
295
296 println!("✓ Comprehensive CLI workflow test completed successfully");
297 }
298}