1use clap::Parser;
6use ggen_core::lifecycle::{load_make, load_state, run_phase, run_pipeline, Context};
7use ggen_core::lifecycle::{ReadinessReport, ReadinessTracker, ReadinessValidator};
8use std::path::{Path, PathBuf};
9
10#[derive(Parser, Debug, Clone)]
11#[command(name = "lifecycle", about = "Universal lifecycle management")]
12pub struct LifecycleArgs {
13 #[command(subcommand)]
14 pub command: LifecycleCmd,
15}
16
17#[derive(Parser, Debug, Clone)]
18pub enum LifecycleCmd {
19 #[command(name = "list")]
21 List {
22 #[arg(long, default_value = ".")]
24 root: PathBuf,
25 },
26
27 #[command(name = "show")]
29 Show {
30 phase: String,
32
33 #[arg(long, default_value = ".")]
35 root: PathBuf,
36 },
37
38 #[command(name = "run")]
40 Run {
41 phase: String,
43
44 #[arg(long, default_value = ".")]
46 root: PathBuf,
47
48 #[arg(long, short = 'e')]
50 env: Option<String>,
51 },
52
53 #[command(name = "pipeline")]
55 Pipeline {
56 phases: Vec<String>,
58
59 #[arg(long, default_value = ".")]
61 root: PathBuf,
62
63 #[arg(long, short = 'e')]
65 env: Option<String>,
66 },
67
68 #[command(name = "readiness")]
70 Readiness {
71 #[arg(long, default_value = ".")]
73 root: PathBuf,
74
75 #[arg(long)]
77 detailed: bool,
78
79 #[arg(long)]
81 critical_only: bool,
82 },
83
84 #[command(name = "readiness-update")]
86 ReadinessUpdate {
87 requirement_id: String,
89
90 status: String,
92
93 #[arg(long, default_value = ".")]
95 root: PathBuf,
96 },
97
98 #[command(name = "placeholders")]
100 Placeholders {
101 #[arg(long)]
103 category: Option<String>,
104
105 #[arg(long, default_value = ".")]
107 root: PathBuf,
108 },
109
110 #[command(name = "validate")]
112 Validate {
113 #[arg(long, default_value = ".")]
115 root: PathBuf,
116
117 #[arg(long, short = 'e', default_value = "production")]
119 env: String,
120
121 #[arg(long)]
123 strict: bool,
124 },
125}
126
127pub async fn run(args: LifecycleArgs) -> ggen_utils::error::Result<()> {
128 use LifecycleCmd::*;
129
130 match args.command {
131 List { root } => list_phases(&root),
132 Show { phase, root } => show_phase(&root, &phase),
133 Run { phase, root, env } => run_single_phase(&root, &phase, env),
134 Pipeline { phases, root, env } => run_phase_pipeline(&root, &phases, env),
135 Readiness {
136 root,
137 detailed,
138 critical_only,
139 } => check_readiness(&root, detailed, critical_only),
140 ReadinessUpdate {
141 requirement_id,
142 status,
143 root,
144 } => update_readiness(&root, &requirement_id, &status),
145 Placeholders { category, root } => show_placeholders(&root, category.as_deref()),
146 Validate { root, env, strict } => validate_for_deployment(&root, &env, strict),
147 }
148}
149
150#[tracing::instrument(name = "ggen.lifecycle.list", skip(root), fields(root = %root.display()))]
152fn list_phases(root: &Path) -> ggen_utils::error::Result<()> {
153 let make_path = root.join("make.toml");
154
155 if !make_path.exists() {
156 tracing::warn!("make.toml not found");
157 println!("ā No make.toml found in {}", root.display());
158 println!(" Initialize a project with 'ggen lifecycle init'");
159 return Ok(());
160 }
161
162 tracing::info!("Loading make.toml");
163 let make = load_make(&make_path).map_err(|e| anyhow::anyhow!(e))?;
164
165 println!("š Available lifecycle phases in {}:", root.display());
166 println!();
167
168 let phase_names = make.phase_names();
169 if phase_names.is_empty() {
170 println!(" (no phases defined)");
171 } else {
172 for phase_name in phase_names {
173 if let Some(phase) = make.lifecycle.get(&phase_name) {
174 let desc = phase.description.as_deref().unwrap_or("(no description)");
175 println!(" ⢠{} - {}", phase_name, desc);
176 }
177 }
178 }
179
180 let state_path = root.join(".ggen/state.json");
182 if state_path.exists() {
183 let state = load_state(&state_path).map_err(|e| anyhow::anyhow!(e))?;
184 if let Some(last) = state.last_phase {
185 println!();
186 println!("š Last executed: {}", last);
187 }
188 }
189
190 Ok(())
191}
192
193fn show_phase(root: &Path, phase_name: &str) -> ggen_utils::error::Result<()> {
195 let make_path = root.join("make.toml");
196 let make = load_make(&make_path).map_err(|e| anyhow::anyhow!(e))?;
197
198 let phase = make
199 .lifecycle
200 .get(phase_name)
201 .ok_or_else(|| anyhow::anyhow!("Phase '{}' not found in make.toml", phase_name))?;
202
203 println!("š¦ Phase: {}", phase_name);
204 println!();
205
206 if let Some(desc) = &phase.description {
207 println!("Description: {}", desc);
208 }
209
210 let cmds = make.phase_commands(phase_name);
212 if !cmds.is_empty() {
213 println!();
214 println!("Commands:");
215 for cmd in cmds {
216 println!(" $ {}", cmd);
217 }
218 }
219
220 if let Some(watch) = phase.watch {
222 println!();
223 println!("Watch mode: {}", watch);
224 }
225
226 if let Some(port) = phase.port {
227 println!("Port: {}", port);
228 }
229
230 if let Some(cache) = phase.cache {
231 println!("Caching: {}", cache);
232 }
233
234 if let Some(hooks) = &make.hooks {
236 let mut has_hooks = false;
237
238 let before = match phase_name {
239 "init" => &hooks.before_init,
240 "setup" => &hooks.before_setup,
241 "build" => &hooks.before_build,
242 "test" => &hooks.before_test,
243 "deploy" => &hooks.before_deploy,
244 _ => &None,
245 };
246
247 let after = match phase_name {
248 "init" => &hooks.after_init,
249 "setup" => &hooks.after_setup,
250 "build" => &hooks.after_build,
251 "test" => &hooks.after_test,
252 "deploy" => &hooks.after_deploy,
253 _ => &None,
254 };
255
256 if let Some(before_hooks) = before {
257 if !before_hooks.is_empty() {
258 println!();
259 println!("Before hooks:");
260 for h in before_hooks {
261 println!(" ā {}", h);
262 }
263 has_hooks = true;
264 }
265 }
266
267 if let Some(after_hooks) = after {
268 if !after_hooks.is_empty() {
269 if !has_hooks {
270 println!();
271 }
272 println!("After hooks:");
273 for h in after_hooks {
274 println!(" ā {}", h);
275 }
276 }
277 }
278 }
279
280 Ok(())
281}
282
283#[tracing::instrument(name = "ggen.lifecycle.run_phase", skip(root), fields(
285 root = %root.display(),
286 phase = %phase_name,
287 env = ?env
288))]
289fn run_single_phase(
290 root: &Path, phase_name: &str, env: Option<String>,
291) -> ggen_utils::error::Result<()> {
292 tracing::info!(phase = %phase_name, "Starting lifecycle phase");
293
294 let make_path = root.join("make.toml");
295 let make = std::sync::Arc::new(load_make(&make_path).map_err(|e| anyhow::anyhow!(e))?);
296
297 let env_vars = build_env(env);
298 let state_path = root.join(".ggen/state.json");
299
300 let ctx = Context::new(root.to_path_buf(), make, state_path, env_vars);
301
302 let _exec_span = tracing::info_span!("execute_phase", phase = %phase_name).entered();
303 run_phase(&ctx, phase_name).map_err(|e| anyhow::anyhow!(e))?;
304 drop(_exec_span);
305
306 tracing::info!(phase = %phase_name, "Phase completed successfully");
307
308 Ok(())
309}
310
311fn run_phase_pipeline(
313 root: &Path, phases: &[String], env: Option<String>,
314) -> ggen_utils::error::Result<()> {
315 let make_path = root.join("make.toml");
316 let make = std::sync::Arc::new(load_make(&make_path).map_err(|e| anyhow::anyhow!(e))?);
317
318 let env_vars = build_env(env);
319 let state_path = root.join(".ggen/state.json");
320
321 let ctx = Context::new(root.to_path_buf(), make, state_path, env_vars);
322
323 run_pipeline(&ctx, phases).map_err(|e| anyhow::anyhow!(e))?;
324
325 println!();
326 println!("ā
Pipeline completed: {}", phases.join(" ā "));
327
328 Ok(())
329}
330
331#[tracing::instrument(name = "ggen.lifecycle.readiness", skip(root), fields(
333 root = %root.display(),
334 detailed = detailed,
335 critical_only = critical_only
336))]
337fn check_readiness(
338 root: &Path, detailed: bool, critical_only: bool,
339) -> ggen_utils::error::Result<()> {
340 tracing::info!("Checking production readiness");
341
342 let _load_span = tracing::info_span!("load_tracker").entered();
343 let mut tracker = ReadinessTracker::new(root);
344 tracker
345 .load()
346 .map_err(|e: ggen_core::lifecycle::production::ProductionError| anyhow::anyhow!(e))?;
347 drop(_load_span);
348
349 let _analyze_span = tracing::info_span!("analyze_project").entered();
351 tracker
352 .analyze_project()
353 .map_err(|e: ggen_core::lifecycle::production::ProductionError| anyhow::anyhow!(e))?;
354 drop(_analyze_span);
355
356 let _report_span = tracing::info_span!("generate_report").entered();
357 let report = tracker.generate_report();
358 tracing::info!(
359 overall_score = report.overall_score,
360 total_requirements = report.requirements.len(),
361 blocking = report.blocking_requirements.len(),
362 "Readiness report generated"
363 );
364 drop(_report_span);
365
366 println!("š Production Readiness Report");
367 println!("š Overall Score: {:.1}%", report.overall_score);
368 println!(
369 "š
Generated: {}",
370 report.generated_at.format("%Y-%m-%d %H:%M:%S")
371 );
372
373 if critical_only {
374 println!("\nšØ Critical Requirements Only:");
375 print_category_report(&report, &ggen_core::lifecycle::ReadinessCategory::Critical);
376 } else if detailed {
377 println!("\nš Detailed Breakdown:");
378 for category in report.by_category.keys() {
379 print_category_report(&report, category);
380 }
381
382 if !report.blocking_requirements.is_empty() {
383 println!("\nš« BLOCKING REQUIREMENTS (must fix before production):");
384 for req_id in &report.blocking_requirements {
385 if let Some(req) = report.requirements.iter().find(|r| r.id == *req_id) {
386 println!(" ⢠{}: {} ({:?})", req.id, req.name, req.status);
387 }
388 }
389 }
390
391 if !report.next_steps.is_empty() {
392 println!("\nšÆ NEXT STEPS:");
393 for step in &report.next_steps {
394 println!(" ⢠{}", step);
395 }
396 }
397 } else {
398 println!("\nš Category Summary:");
399 for (category, category_report) in &report.by_category {
400 let status_emoji = match category {
401 ggen_core::lifecycle::ReadinessCategory::Critical => "šØ",
402 ggen_core::lifecycle::ReadinessCategory::Important => "ā ļø",
403 ggen_core::lifecycle::ReadinessCategory::NiceToHave => "ā¹ļø",
404 };
405 println!(
406 " {} {:?}: {:.1}% ({}/{} complete)",
407 status_emoji,
408 category,
409 category_report.score,
410 category_report.completed,
411 category_report.total_requirements
412 );
413 }
414 }
415
416 println!("\nšÆ Production Readiness Assessment:");
418 if report.overall_score >= 90.0 {
419 println!(" ā
EXCELLENT - Ready for production!");
420 } else if report.overall_score >= 75.0 {
421 println!(" ā ļø GOOD - Ready for production with minor improvements");
422 } else if report.overall_score >= 60.0 {
423 println!(" š§ FAIR - Can deploy but needs work");
424 } else {
425 println!(" ā POOR - Not ready for production");
426 }
427
428 Ok(())
429}
430
431fn print_category_report(
433 report: &ReadinessReport, category: &ggen_core::lifecycle::ReadinessCategory,
434) {
435 if let Some(category_report) = report.by_category.get(category) {
436 let status_emoji = match category {
437 ggen_core::lifecycle::ReadinessCategory::Critical => "šØ",
438 ggen_core::lifecycle::ReadinessCategory::Important => "ā ļø",
439 ggen_core::lifecycle::ReadinessCategory::NiceToHave => "ā¹ļø",
440 };
441
442 println!(
443 "\n{} {:?} Requirements ({:.1}% complete):",
444 status_emoji, category, category_report.score
445 );
446
447 for req_id in &category_report.requirements {
448 if let Some(req) = report.requirements.iter().find(|r| r.id == *req_id) {
449 let status_icon = match req.status {
450 ggen_core::lifecycle::ReadinessStatus::Complete => "ā
",
451 ggen_core::lifecycle::ReadinessStatus::Placeholder => "š§",
452 ggen_core::lifecycle::ReadinessStatus::Missing => "ā",
453 ggen_core::lifecycle::ReadinessStatus::NeedsReview => "š",
454 };
455 println!(
456 " {} {} - {} ({:?})",
457 status_icon, req.name, req.description, req.status
458 );
459 }
460 }
461 }
462}
463
464fn update_readiness(
466 root: &Path, requirement_id: &str, status_str: &str,
467) -> ggen_utils::error::Result<()> {
468 let mut tracker = ReadinessTracker::new(root);
469 tracker
470 .load()
471 .map_err(|e: ggen_core::lifecycle::production::ProductionError| anyhow::anyhow!(e))?;
472
473 let status = match status_str.to_lowercase().as_str() {
475 "complete" | "done" | "finished" => ggen_core::lifecycle::ReadinessStatus::Complete,
476 "placeholder" | "todo" | "stub" => ggen_core::lifecycle::ReadinessStatus::Placeholder,
477 "missing" | "not-implemented" => ggen_core::lifecycle::ReadinessStatus::Missing,
478 "needs-review" | "review" => ggen_core::lifecycle::ReadinessStatus::NeedsReview,
479 _ => {
480 println!("ā Invalid status: {}", status_str);
481 println!("Valid options: complete, placeholder, missing, needs-review");
482 return Ok(());
483 }
484 };
485
486 match tracker.update_requirement(requirement_id, status.clone()) {
487 Ok(_) => {
488 tracker
489 .save()
490 .map_err(|e: ggen_core::lifecycle::production::ProductionError| {
491 anyhow::anyhow!(e)
492 })?;
493 println!("ā
Updated '{}' to {:?}", requirement_id, status);
494
495 let report = tracker.generate_report();
497 println!("š New overall score: {:.1}%", report.overall_score);
498 }
499 Err(e) => {
500 println!("ā Failed to update requirement: {}", e);
501 }
502 }
503
504 Ok(())
505}
506
507fn show_placeholders(_root: &Path, category_filter: Option<&str>) -> ggen_utils::error::Result<()> {
509 use ggen_core::lifecycle::{PlaceholderRegistry, ReadinessCategory};
510
511 let registry = PlaceholderRegistry::new();
512
513 let category = if let Some(cat_str) = category_filter {
514 match cat_str.to_lowercase().as_str() {
515 "critical" => Some(ReadinessCategory::Critical),
516 "important" => Some(ReadinessCategory::Important),
517 "nice-to-have" | "nice" => Some(ReadinessCategory::NiceToHave),
518 _ => {
519 println!("ā Invalid category: {}", cat_str);
520 println!("Valid options: critical, important, nice-to-have");
521 return Ok(());
522 }
523 }
524 } else {
525 None
526 };
527
528 println!("š§ Production Readiness Placeholders");
529
530 if let Some(cat) = category {
531 println!("š Category: {:?}", cat);
532 let placeholders = registry.get_by_category(&cat);
533 for placeholder in placeholders {
534 println!("\nš {}", placeholder.description);
535 println!(" ID: {}", placeholder.id);
536 println!(" Priority: {}/10", placeholder.priority);
537 println!(" Category: {:?}", placeholder.category);
538 println!(" Guidance: {}", placeholder.guidance);
539
540 if !placeholder.affects.is_empty() {
541 println!(" Affects: {}", placeholder.affects.join(", "));
542 }
543
544 }
546 } else {
547 let summary = registry.generate_summary();
549 println!("{}", summary);
550 }
551
552 println!("\nš” Use 'ggen lifecycle readiness' to check current status");
553 println!("š” Use 'ggen lifecycle readiness-update <id> <status>' to update status");
554
555 Ok(())
556}
557
558#[tracing::instrument(name = "ggen.lifecycle.validate", skip(root), fields(
560 root = %root.display(),
561 env = %env,
562 strict = strict
563))]
564fn validate_for_deployment(root: &Path, env: &str, strict: bool) -> ggen_utils::error::Result<()> {
565 println!("š Production Readiness Validation for {} environment", env);
566 println!();
567 tracing::info!(environment = %env, strict_mode = strict, "Starting production validation");
568
569 let _validator_span = tracing::info_span!("create_validator", strict = strict).entered();
571 let validator = if strict {
572 ReadinessValidator::new().strict_mode(true)
573 } else {
574 ReadinessValidator::new().strict_mode(false)
575 };
576 drop(_validator_span);
577
578 let _validate_span = tracing::info_span!("run_validation").entered();
580 let result = validator
581 .validate_for_deployment(root)
582 .map_err(|e| anyhow::anyhow!("Validation failed: {}", e))?;
583 tracing::info!(
584 score = result.score,
585 passed = result.passed,
586 issues_count = result.issues.len(),
587 "Validation completed"
588 );
589 drop(_validate_span);
590
591 println!("š Overall Score: {:.1}%", result.score);
593 println!();
594
595 if result.issues.is_empty() {
596 println!("ā
No issues found!");
597 } else {
598 println!("š Issues Found:");
599 for issue in &result.issues {
600 let severity_icon = match issue.severity {
601 ggen_core::lifecycle::ValidationSeverity::Critical => "šØ",
602 ggen_core::lifecycle::ValidationSeverity::Warning => "ā ļø",
603 ggen_core::lifecycle::ValidationSeverity::Info => "ā¹ļø",
604 };
605 println!(
606 "\n{} {:?} - {:?}",
607 severity_icon, issue.severity, issue.category
608 );
609 println!(" Problem: {}", issue.description);
610 println!(" Fix: {}", issue.fix);
611 }
612 }
613
614 if !result.recommendations.is_empty() {
615 println!("\nš” Recommendations:");
616 for rec in &result.recommendations {
617 println!(" ⢠{}", rec);
618 }
619 }
620
621 println!();
622
623 if result.passed {
625 println!("š DEPLOYMENT READY! š");
626 println!("ā
All {} requirements met for production deployment", env);
627 Ok(())
628 } else {
629 println!("ā DEPLOYMENT BLOCKED");
630 println!(
631 "š« Critical requirements not met - cannot deploy to {}",
632 env
633 );
634 println!();
635 println!("š” Run 'ggen lifecycle readiness --detailed' to see full report");
636 println!("š” Run 'ggen lifecycle readiness-update <id> <status>' to update requirements");
637
638 Err(ggen_utils::error::Error::new(&format!(
639 "Production validation failed: {} critical issues found",
640 result
641 .issues
642 .iter()
643 .filter(|i| matches!(
644 i.severity,
645 ggen_core::lifecycle::ValidationSeverity::Critical
646 ))
647 .count()
648 )))
649 }
650}
651
652fn build_env(env: Option<String>) -> Vec<(String, String)> {
654 env.map(|e| vec![("GGEN_ENV".to_string(), e)])
655 .unwrap_or_default()
656}