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