metis_docs_cli/commands/
init.rs1use crate::workspace;
2use anyhow::Result;
3use clap::Args;
4use metis_core::{
5 application::services::workspace::WorkspaceInitializationService,
6 domain::configuration::FlightLevelConfig,
7 Database,
8};
9
10#[derive(Args)]
11pub struct InitCommand {
12 #[arg(short, long)]
14 pub name: Option<String>,
15 #[arg(short = 'P', long)]
17 pub prefix: Option<String>,
18 #[arg(short, long)]
20 pub preset: Option<String>,
21 #[arg(long)]
23 pub strategies: Option<bool>,
24 #[arg(long)]
26 pub initiatives: Option<bool>,
27}
28
29impl InitCommand {
30 pub async fn execute(&self) -> Result<()> {
31 let (workspace_exists, _) = workspace::has_metis_vault();
33 if workspace_exists {
34 println!("Metis workspace already exists in this directory");
35 return Ok(());
36 }
37
38 let current_dir = std::env::current_dir()?;
40
41 let project_name = self.name.as_deref().unwrap_or("Project Vision");
43 let project_prefix = self.determine_project_prefix(project_name);
44
45 let result = WorkspaceInitializationService::initialize_workspace_with_prefix(
47 ¤t_dir,
48 project_name,
49 Some(&project_prefix),
50 )
51 .await
52 .map_err(|e| anyhow::anyhow!("Failed to initialize workspace: {}", e))?;
53
54 let flight_config = self.determine_flight_config()?;
56 let db = Database::new(result.database_path.to_str().unwrap())
57 .map_err(|e| anyhow::anyhow!("Failed to open database: {}", e))?;
58 let mut config_repo = db
59 .configuration_repository()
60 .map_err(|e| anyhow::anyhow!("Failed to create configuration repository: {}", e))?;
61
62 let current_config = config_repo.get_flight_level_config()
64 .map_err(|e| anyhow::anyhow!("Failed to get flight level config: {}", e))?;
65 if flight_config != current_config {
66 config_repo
67 .set_flight_level_config(&flight_config)
68 .map_err(|e| anyhow::anyhow!("Failed to set flight level configuration: {}", e))?;
69
70 let config_file_path = result.metis_dir.join("config.toml");
72 let config_file = metis_core::domain::configuration::ConfigFile::new(
73 project_prefix.clone(),
74 flight_config.clone()
75 ).map_err(|e| anyhow::anyhow!("Failed to create config file: {}", e))?;
76 config_file.save(&config_file_path)
77 .map_err(|e| anyhow::anyhow!("Failed to save config.toml: {}", e))?;
78 }
79
80 let gitignore_path = result.metis_dir.join(".gitignore");
82 std::fs::write(&gitignore_path, "metis.db\nmetis-mcp-server.log\n")
83 .map_err(|e| anyhow::anyhow!("Failed to create .gitignore: {}", e))?;
84
85 println!("✓ Initialized Metis workspace in {}", current_dir.display());
86 println!("✓ Created vision.md with project template");
87 println!("✓ Created config.toml with project settings");
88 println!("✓ Set project prefix: {}", project_prefix);
89 println!(
90 "✓ Set flight level configuration: {}",
91 flight_config.preset_name()
92 );
93
94 Ok(())
95 }
96
97 fn determine_project_prefix(&self, project_name: &str) -> String {
99 if let Some(prefix) = &self.prefix {
100 let truncated = prefix.to_uppercase();
102 if truncated.len() > 6 {
103 truncated.chars().take(6).collect()
104 } else {
105 truncated
106 }
107 } else if cfg!(test) {
108 "TEST".to_string()
110 } else {
111 project_name
113 .chars()
114 .filter(|c| c.is_alphabetic())
115 .map(|c| c.to_uppercase().collect::<String>())
116 .collect::<String>()
117 .get(0..6.min(project_name.len()))
118 .unwrap_or("PROJ")
119 .to_string()
120 }
121 }
122
123 fn determine_flight_config(&self) -> Result<FlightLevelConfig> {
125 if let Some(preset_name) = &self.preset {
126 match preset_name.as_str() {
128 "full" => Ok(FlightLevelConfig::full()),
129 "streamlined" => Ok(FlightLevelConfig::streamlined()),
130 "direct" => Ok(FlightLevelConfig::direct()),
131 _ => {
132 anyhow::bail!(
133 "Invalid preset '{}'. Valid presets are: full, streamlined, direct",
134 preset_name
135 );
136 }
137 }
138 } else if self.strategies.is_some() || self.initiatives.is_some() {
139 let default_config = FlightLevelConfig::streamlined();
141 let strategies_enabled = self.strategies.unwrap_or(default_config.strategies_enabled);
142 let initiatives_enabled = self
143 .initiatives
144 .unwrap_or(default_config.initiatives_enabled);
145
146 FlightLevelConfig::new(strategies_enabled, initiatives_enabled)
147 .map_err(|e| anyhow::anyhow!("Invalid configuration: {}", e))
148 } else {
149 Ok(FlightLevelConfig::streamlined())
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use std::fs;
159 use tempfile::tempdir;
160
161 #[tokio::test]
162 async fn test_init_command_creates_workspace() {
163 let temp_dir = tempdir().unwrap();
164 let original_dir = std::env::current_dir().ok();
165
166 std::env::set_current_dir(temp_dir.path()).unwrap();
168
169 let cmd = InitCommand {
171 name: Some("Test Project".to_string()),
172 preset: None,
173 strategies: None,
174 initiatives: None,
175 prefix: None,
176 };
177
178 let result = cmd.execute().await;
179 assert!(result.is_ok());
180
181 let metis_dir = temp_dir.path().join(".metis");
183 assert!(metis_dir.exists());
184 assert!(metis_dir.is_dir());
185
186 let db_path = metis_dir.join("metis.db");
188 assert!(db_path.exists());
189 assert!(db_path.is_file());
190
191 let strategies_dir = metis_dir.join("strategies");
193 assert!(strategies_dir.exists());
194 assert!(strategies_dir.is_dir());
195
196 let vision_path = metis_dir.join("vision.md");
198 assert!(vision_path.exists());
199 assert!(vision_path.is_file());
200
201 let vision_content = fs::read_to_string(&vision_path).unwrap();
203 assert!(vision_content.contains("Test Project"));
204 assert!(vision_content.contains("#vision"));
205 assert!(vision_content.contains("#phase/draft"));
206 assert!(vision_content.contains("archived: false"));
207
208 assert!(vision_content.contains("# Test Project Vision"));
210 assert!(vision_content.contains("## Purpose"));
211 assert!(vision_content.contains("## Current State"));
212 assert!(vision_content.contains("## Future State"));
213 assert!(vision_content.contains("## Success Criteria"));
214 assert!(vision_content.contains("## Principles"));
215 assert!(vision_content.contains("## Constraints"));
216
217 let config_path = metis_dir.join("config.toml");
219 assert!(config_path.exists(), "config.toml should be created");
220 assert!(config_path.is_file());
221
222 let config_content = fs::read_to_string(&config_path).unwrap();
224 assert!(config_content.contains("[project]"));
225 assert!(config_content.contains("prefix = \"TEST\""));
226 assert!(config_content.contains("[flight_levels]"));
227
228 if let Some(original) = original_dir {
230 let _ = std::env::set_current_dir(&original);
231 }
232 }
233
234 #[tokio::test]
235 async fn test_init_command_workspace_already_exists() {
236 let temp_dir = tempdir().unwrap();
237 let original_dir = std::env::current_dir().ok();
238 let metis_dir = temp_dir.path().join(".metis");
239 let db_path = metis_dir.join("metis.db");
240
241 fs::create_dir_all(&metis_dir).unwrap();
243 fs::write(&db_path, "existing").unwrap();
244
245 std::env::set_current_dir(temp_dir.path()).unwrap();
247
248 let cmd = InitCommand {
250 name: Some("Test Project".to_string()),
251 preset: None,
252 strategies: None,
253 initiatives: None,
254 prefix: None,
255 };
256
257 let result = cmd.execute().await;
258 assert!(result.is_ok());
259
260 let db_content = fs::read_to_string(&db_path).unwrap();
262 assert_eq!(db_content, "existing");
263
264 if let Some(original) = original_dir {
266 let _ = std::env::set_current_dir(&original);
267 }
268 }
269
270 #[tokio::test]
271 async fn test_init_command_default_name() {
272 let temp_dir = tempdir().unwrap();
273 let original_dir = std::env::current_dir().ok();
274
275 std::env::set_current_dir(temp_dir.path()).unwrap();
277
278 let cmd = InitCommand {
280 name: None,
281 preset: None,
282 strategies: None,
283 initiatives: None,
284 prefix: None,
285 };
286
287 let result = cmd.execute().await;
288 assert!(result.is_ok());
289
290 let vision_path = temp_dir.path().join(".metis").join("vision.md");
292 let vision_content = fs::read_to_string(&vision_path).unwrap();
293 assert!(vision_content.contains("Project Vision"));
294
295 if let Some(original) = original_dir {
297 let _ = std::env::set_current_dir(&original);
298 }
299 }
300
301 #[tokio::test]
302 async fn test_init_command_with_preset() {
303 let temp_dir = tempdir().unwrap();
304 let original_dir = std::env::current_dir().ok();
305
306 std::env::set_current_dir(temp_dir.path()).unwrap();
308
309 let cmd = InitCommand {
311 name: Some("Test Project".to_string()),
312 preset: Some("full".to_string()),
313 strategies: None,
314 initiatives: None,
315 prefix: None,
316 };
317
318 let result = cmd.execute().await;
319 assert!(result.is_ok());
320
321 let metis_dir = temp_dir.path().join(".metis");
323 assert!(metis_dir.exists());
324
325 use metis_core::Database;
327 let db_path = metis_dir.join("metis.db");
328 let db = Database::new(db_path.to_str().unwrap()).unwrap();
329 let mut config_repo = db.configuration_repository().unwrap();
330 let config = config_repo.get_flight_level_config().unwrap();
331
332 assert_eq!(
333 config,
334 metis_core::domain::configuration::FlightLevelConfig::full()
335 );
336
337 if let Some(original) = original_dir {
339 let _ = std::env::set_current_dir(&original);
340 }
341 }
342
343 #[tokio::test]
344 async fn test_init_command_with_custom_flags() {
345 let temp_dir = tempdir().unwrap();
346 let original_dir = std::env::current_dir().ok();
347
348 std::env::set_current_dir(temp_dir.path()).unwrap();
350
351 let cmd = InitCommand {
353 name: Some("Test Project".to_string()),
354 preset: None,
355 strategies: Some(false),
356 initiatives: Some(true),
357 prefix: None,
358 };
359
360 let result = cmd.execute().await;
361 assert!(result.is_ok());
362
363 use metis_core::Database;
365 let metis_dir = temp_dir.path().join(".metis");
366 let db_path = metis_dir.join("metis.db");
367 let db = Database::new(db_path.to_str().unwrap()).unwrap();
368 let mut config_repo = db.configuration_repository().unwrap();
369 let config = config_repo.get_flight_level_config().unwrap();
370
371 assert!(!config.strategies_enabled);
372 assert!(config.initiatives_enabled);
373
374 if let Some(original) = original_dir {
376 let _ = std::env::set_current_dir(&original);
377 }
378 }
379
380 #[tokio::test]
381 async fn test_init_command_default_streamlined() {
382 let temp_dir = tempdir().unwrap();
383 let original_dir = std::env::current_dir().ok();
384
385 std::env::set_current_dir(temp_dir.path()).unwrap();
387
388 let cmd = InitCommand {
390 name: Some("Test Project".to_string()),
391 preset: None,
392 strategies: None,
393 initiatives: None,
394 prefix: None,
395 };
396
397 let result = cmd.execute().await;
398 assert!(result.is_ok());
399
400 use metis_core::Database;
402 let metis_dir = temp_dir.path().join(".metis");
403 let db_path = metis_dir.join("metis.db");
404 let db = Database::new(db_path.to_str().unwrap()).unwrap();
405 let mut config_repo = db.configuration_repository().unwrap();
406 let config = config_repo.get_flight_level_config().unwrap();
407
408 assert_eq!(
409 config,
410 metis_core::domain::configuration::FlightLevelConfig::streamlined()
411 );
412
413 if let Some(original) = original_dir {
415 let _ = std::env::set_current_dir(&original);
416 }
417 }
418
419 #[tokio::test]
420 async fn test_init_command_invalid_preset() {
421 let temp_dir = tempdir().unwrap();
422 let original_dir = std::env::current_dir().ok();
423
424 std::env::set_current_dir(temp_dir.path()).unwrap();
426
427 let cmd = InitCommand {
429 name: Some("Test Project".to_string()),
430 preset: Some("invalid".to_string()),
431 strategies: None,
432 initiatives: None,
433 prefix: None,
434 };
435
436 let result = cmd.execute().await;
437 assert!(result.is_err());
438 assert!(result.unwrap_err().to_string().contains("Invalid preset"));
439
440 if let Some(original) = original_dir {
442 let _ = std::env::set_current_dir(&original);
443 }
444 }
445}