metis_docs_cli/
cli.rs

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    /// Increase verbosity (-v, -vv, -vvv)
16    #[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    /// Initialize a new Metis workspace
26    Init(InitCommand),
27    /// Synchronize workspace with file system
28    Sync(SyncCommand),
29    /// Create new documents
30    Create(CreateCommand),
31    /// Search documents in the workspace
32    Search(SearchCommand),
33    /// Transition documents between phases
34    Transition(TransitionCommand),
35    /// List documents in the workspace
36    List(ListCommand),
37    /// Show workspace status and actionable items
38    Status(StatusCommand),
39    /// Archive completed documents and move them to archived folder
40    Archive(ArchiveCommand),
41    /// Validate a document file
42    Validate(ValidateCommand),
43    /// Launch the MCP server for external integrations
44    Mcp(McpCommand),
45    /// Manage flight level configuration
46    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        // Change to temp directory
98        std::env::set_current_dir(temp_dir.path()).unwrap();
99
100        // 1. Initialize a new project
101        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        // 2. Sync the workspace to populate database
124        let sync_cmd = SyncCommand {};
125        sync_cmd.execute().await.expect("Failed to sync workspace");
126
127        // 3. Create a strategy
128        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        // 4. Create an initiative under the strategy
140        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        // 5. Create a task under the initiative
152        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        // 6. Create an ADR
164        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        // Find the created ADR (it will have a number prefix)
175        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        // 7. Sync after creating documents
184        let sync_cmd2 = SyncCommand {};
185        sync_cmd2
186            .execute()
187            .await
188            .expect("Failed to sync after creating documents");
189
190        // 8. Transition the vision to review phase
191        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        // 9. Transition the strategy through its phases: Shaping → Design → Ready → Active
201        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        // 10. Transition the task through its phases: Todo → Active → Completed
229        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        // 11. Archive the completed task
248        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        // 12. List all documents to verify they exist
258        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        // 13. Test status command
267        let status_cmd = StatusCommand {
268            include_archived: false,
269        };
270        status_cmd.execute().await.expect("Failed to get status");
271
272        // 14. Search for content
273        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        // 15. Validate a document file
283        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        // Restore original directory
292        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}