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 println!("✓ Initialized Metis workspace in {}", current_dir.display());
81 println!("✓ Created vision.md with project template");
82 println!("✓ Created config.toml with project settings");
83 println!("✓ Set project prefix: {}", project_prefix);
84 println!(
85 "✓ Set flight level configuration: {}",
86 flight_config.preset_name()
87 );
88
89 Ok(())
90 }
91
92 fn determine_project_prefix(&self, project_name: &str) -> String {
94 if let Some(prefix) = &self.prefix {
95 let truncated = prefix.to_uppercase();
97 if truncated.len() > 6 {
98 truncated.chars().take(6).collect()
99 } else {
100 truncated
101 }
102 } else if cfg!(test) {
103 "TEST".to_string()
105 } else {
106 project_name
108 .chars()
109 .filter(|c| c.is_alphabetic())
110 .map(|c| c.to_uppercase().collect::<String>())
111 .collect::<String>()
112 .get(0..6.min(project_name.len()))
113 .unwrap_or("PROJ")
114 .to_string()
115 }
116 }
117
118 fn determine_flight_config(&self) -> Result<FlightLevelConfig> {
120 if let Some(preset_name) = &self.preset {
121 match preset_name.as_str() {
123 "full" => Ok(FlightLevelConfig::full()),
124 "streamlined" => Ok(FlightLevelConfig::streamlined()),
125 "direct" => Ok(FlightLevelConfig::direct()),
126 _ => {
127 anyhow::bail!(
128 "Invalid preset '{}'. Valid presets are: full, streamlined, direct",
129 preset_name
130 );
131 }
132 }
133 } else if self.strategies.is_some() || self.initiatives.is_some() {
134 let default_config = FlightLevelConfig::streamlined();
136 let strategies_enabled = self.strategies.unwrap_or(default_config.strategies_enabled);
137 let initiatives_enabled = self
138 .initiatives
139 .unwrap_or(default_config.initiatives_enabled);
140
141 FlightLevelConfig::new(strategies_enabled, initiatives_enabled)
142 .map_err(|e| anyhow::anyhow!("Invalid configuration: {}", e))
143 } else {
144 Ok(FlightLevelConfig::streamlined())
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use std::fs;
154 use tempfile::tempdir;
155
156 #[tokio::test]
157 async fn test_init_command_creates_workspace() {
158 let temp_dir = tempdir().unwrap();
159 let original_dir = std::env::current_dir().ok();
160
161 std::env::set_current_dir(temp_dir.path()).unwrap();
163
164 let cmd = InitCommand {
166 name: Some("Test Project".to_string()),
167 preset: None,
168 strategies: None,
169 initiatives: None,
170 prefix: None,
171 };
172
173 let result = cmd.execute().await;
174 assert!(result.is_ok());
175
176 let metis_dir = temp_dir.path().join(".metis");
178 assert!(metis_dir.exists());
179 assert!(metis_dir.is_dir());
180
181 let db_path = metis_dir.join("metis.db");
183 assert!(db_path.exists());
184 assert!(db_path.is_file());
185
186 let strategies_dir = metis_dir.join("strategies");
188 assert!(strategies_dir.exists());
189 assert!(strategies_dir.is_dir());
190
191 let vision_path = metis_dir.join("vision.md");
193 assert!(vision_path.exists());
194 assert!(vision_path.is_file());
195
196 let vision_content = fs::read_to_string(&vision_path).unwrap();
198 assert!(vision_content.contains("Test Project"));
199 assert!(vision_content.contains("#vision"));
200 assert!(vision_content.contains("#phase/draft"));
201 assert!(vision_content.contains("archived: false"));
202
203 assert!(vision_content.contains("# Test Project Vision"));
205 assert!(vision_content.contains("## Purpose"));
206 assert!(vision_content.contains("## Current State"));
207 assert!(vision_content.contains("## Future State"));
208 assert!(vision_content.contains("## Success Criteria"));
209 assert!(vision_content.contains("## Principles"));
210 assert!(vision_content.contains("## Constraints"));
211
212 let config_path = metis_dir.join("config.toml");
214 assert!(config_path.exists(), "config.toml should be created");
215 assert!(config_path.is_file());
216
217 let config_content = fs::read_to_string(&config_path).unwrap();
219 assert!(config_content.contains("[project]"));
220 assert!(config_content.contains("prefix = \"TEST\""));
221 assert!(config_content.contains("[flight_levels]"));
222
223 if let Some(original) = original_dir {
225 let _ = std::env::set_current_dir(&original);
226 }
227 }
228
229 #[tokio::test]
230 async fn test_init_command_workspace_already_exists() {
231 let temp_dir = tempdir().unwrap();
232 let original_dir = std::env::current_dir().ok();
233 let metis_dir = temp_dir.path().join(".metis");
234 let db_path = metis_dir.join("metis.db");
235
236 fs::create_dir_all(&metis_dir).unwrap();
238 fs::write(&db_path, "existing").unwrap();
239
240 std::env::set_current_dir(temp_dir.path()).unwrap();
242
243 let cmd = InitCommand {
245 name: Some("Test Project".to_string()),
246 preset: None,
247 strategies: None,
248 initiatives: None,
249 prefix: None,
250 };
251
252 let result = cmd.execute().await;
253 assert!(result.is_ok());
254
255 let db_content = fs::read_to_string(&db_path).unwrap();
257 assert_eq!(db_content, "existing");
258
259 if let Some(original) = original_dir {
261 let _ = std::env::set_current_dir(&original);
262 }
263 }
264
265 #[tokio::test]
266 async fn test_init_command_default_name() {
267 let temp_dir = tempdir().unwrap();
268 let original_dir = std::env::current_dir().ok();
269
270 std::env::set_current_dir(temp_dir.path()).unwrap();
272
273 let cmd = InitCommand {
275 name: None,
276 preset: None,
277 strategies: None,
278 initiatives: None,
279 prefix: None,
280 };
281
282 let result = cmd.execute().await;
283 assert!(result.is_ok());
284
285 let vision_path = temp_dir.path().join(".metis").join("vision.md");
287 let vision_content = fs::read_to_string(&vision_path).unwrap();
288 assert!(vision_content.contains("Project Vision"));
289
290 if let Some(original) = original_dir {
292 let _ = std::env::set_current_dir(&original);
293 }
294 }
295
296 #[tokio::test]
297 async fn test_init_command_with_preset() {
298 let temp_dir = tempdir().unwrap();
299 let original_dir = std::env::current_dir().ok();
300
301 std::env::set_current_dir(temp_dir.path()).unwrap();
303
304 let cmd = InitCommand {
306 name: Some("Test Project".to_string()),
307 preset: Some("full".to_string()),
308 strategies: None,
309 initiatives: None,
310 prefix: None,
311 };
312
313 let result = cmd.execute().await;
314 assert!(result.is_ok());
315
316 let metis_dir = temp_dir.path().join(".metis");
318 assert!(metis_dir.exists());
319
320 use metis_core::Database;
322 let db_path = metis_dir.join("metis.db");
323 let db = Database::new(db_path.to_str().unwrap()).unwrap();
324 let mut config_repo = db.configuration_repository().unwrap();
325 let config = config_repo.get_flight_level_config().unwrap();
326
327 assert_eq!(
328 config,
329 metis_core::domain::configuration::FlightLevelConfig::full()
330 );
331
332 if let Some(original) = original_dir {
334 let _ = std::env::set_current_dir(&original);
335 }
336 }
337
338 #[tokio::test]
339 async fn test_init_command_with_custom_flags() {
340 let temp_dir = tempdir().unwrap();
341 let original_dir = std::env::current_dir().ok();
342
343 std::env::set_current_dir(temp_dir.path()).unwrap();
345
346 let cmd = InitCommand {
348 name: Some("Test Project".to_string()),
349 preset: None,
350 strategies: Some(false),
351 initiatives: Some(true),
352 prefix: None,
353 };
354
355 let result = cmd.execute().await;
356 assert!(result.is_ok());
357
358 use metis_core::Database;
360 let metis_dir = temp_dir.path().join(".metis");
361 let db_path = metis_dir.join("metis.db");
362 let db = Database::new(db_path.to_str().unwrap()).unwrap();
363 let mut config_repo = db.configuration_repository().unwrap();
364 let config = config_repo.get_flight_level_config().unwrap();
365
366 assert!(!config.strategies_enabled);
367 assert!(config.initiatives_enabled);
368
369 if let Some(original) = original_dir {
371 let _ = std::env::set_current_dir(&original);
372 }
373 }
374
375 #[tokio::test]
376 async fn test_init_command_default_streamlined() {
377 let temp_dir = tempdir().unwrap();
378 let original_dir = std::env::current_dir().ok();
379
380 std::env::set_current_dir(temp_dir.path()).unwrap();
382
383 let cmd = InitCommand {
385 name: Some("Test Project".to_string()),
386 preset: None,
387 strategies: None,
388 initiatives: None,
389 prefix: None,
390 };
391
392 let result = cmd.execute().await;
393 assert!(result.is_ok());
394
395 use metis_core::Database;
397 let metis_dir = temp_dir.path().join(".metis");
398 let db_path = metis_dir.join("metis.db");
399 let db = Database::new(db_path.to_str().unwrap()).unwrap();
400 let mut config_repo = db.configuration_repository().unwrap();
401 let config = config_repo.get_flight_level_config().unwrap();
402
403 assert_eq!(
404 config,
405 metis_core::domain::configuration::FlightLevelConfig::streamlined()
406 );
407
408 if let Some(original) = original_dir {
410 let _ = std::env::set_current_dir(&original);
411 }
412 }
413
414 #[tokio::test]
415 async fn test_init_command_invalid_preset() {
416 let temp_dir = tempdir().unwrap();
417 let original_dir = std::env::current_dir().ok();
418
419 std::env::set_current_dir(temp_dir.path()).unwrap();
421
422 let cmd = InitCommand {
424 name: Some("Test Project".to_string()),
425 preset: Some("invalid".to_string()),
426 strategies: None,
427 initiatives: None,
428 prefix: None,
429 };
430
431 let result = cmd.execute().await;
432 assert!(result.is_err());
433 assert!(result.unwrap_err().to_string().contains("Invalid preset"));
434
435 if let Some(original) = original_dir {
437 let _ = std::env::set_current_dir(&original);
438 }
439 }
440}