ggen_cli_lib/cmds/lifecycle/
mod.rs

1//! Lifecycle command implementation
2//!
3//! Provides CLI interface for universal lifecycle management through make.toml
4
5use 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    /// List all available lifecycle phases
20    #[command(name = "list")]
21    List {
22        /// Path to project root (defaults to current directory)
23        #[arg(long, default_value = ".")]
24        root: PathBuf,
25    },
26
27    /// Show details of a specific phase
28    #[command(name = "show")]
29    Show {
30        /// Phase name to show
31        phase: String,
32
33        /// Path to project root
34        #[arg(long, default_value = ".")]
35        root: PathBuf,
36    },
37
38    /// Run a single lifecycle phase
39    #[command(name = "run")]
40    Run {
41        /// Phase name to run
42        phase: String,
43
44        /// Path to project root
45        #[arg(long, default_value = ".")]
46        root: PathBuf,
47
48        /// Environment name (development, staging, production)
49        #[arg(long, short = 'e')]
50        env: Option<String>,
51    },
52
53    /// Run multiple phases in sequence (pipeline)
54    #[command(name = "pipeline")]
55    Pipeline {
56        /// Phase names to run in order
57        phases: Vec<String>,
58
59        /// Path to project root
60        #[arg(long, default_value = ".")]
61        root: PathBuf,
62
63        /// Environment name
64        #[arg(long, short = 'e')]
65        env: Option<String>,
66    },
67
68    /// Check production readiness status
69    #[command(name = "readiness")]
70    Readiness {
71        /// Path to project root
72        #[arg(long, default_value = ".")]
73        root: PathBuf,
74
75        /// Show detailed breakdown by category
76        #[arg(long)]
77        detailed: bool,
78
79        /// Only show critical requirements
80        #[arg(long)]
81        critical_only: bool,
82    },
83
84    /// Update production readiness status
85    #[command(name = "readiness-update")]
86    ReadinessUpdate {
87        /// Requirement ID to update
88        requirement_id: String,
89
90        /// New status (complete, placeholder, missing, needs-review)
91        status: String,
92
93        /// Path to project root
94        #[arg(long, default_value = ".")]
95        root: PathBuf,
96    },
97
98    /// Show placeholders that need implementation
99    #[command(name = "placeholders")]
100    Placeholders {
101        /// Category filter (critical, important, nice-to-have)
102        #[arg(long)]
103        category: Option<String>,
104
105        /// Path to project root
106        #[arg(long, default_value = ".")]
107        root: PathBuf,
108    },
109
110    /// Validate production readiness for deployment
111    #[command(name = "validate")]
112    Validate {
113        /// Path to project root
114        #[arg(long, default_value = ".")]
115        root: PathBuf,
116
117        /// Environment to validate for (development, staging, production)
118        #[arg(long, short = 'e', default_value = "production")]
119        env: String,
120
121        /// Strict mode - fail if any requirements not met
122        #[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/// List all available phases from make.toml
151#[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    // Show last executed phase
181    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
193/// Show details of a specific phase
194fn 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    // Commands
211    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    // Metadata
221    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    // Hooks
235    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/// Run a single phase
284#[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
311/// Run a pipeline of phases
312fn 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/// Check production readiness status
332#[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    // Analyze project for existing implementations
350    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    // Overall assessment
417    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
431/// Print detailed report for a category
432fn 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
464/// Update production readiness status for a requirement
465fn 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    // Parse status string
474    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            // Show updated report
496            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
507/// Show placeholders that need implementation
508fn 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            // Note: example field not available in current Placeholder struct
545        }
546    } else {
547        // Show all categories
548        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/// Validate production readiness for deployment
559#[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    // Create validator with appropriate settings
570    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    // Run validation
579    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    // Display results
592    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    // Final deployment decision
624    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
652/// Build environment variables
653fn build_env(env: Option<String>) -> Vec<(String, String)> {
654    env.map(|e| vec![("GGEN_ENV".to_string(), e)])
655        .unwrap_or_default()
656}