rok-cli 0.6.1

Developer CLI for rok-based Axum applications
//! `rok upgrade` — guided version migration + --patch auto-fix (M7.7).

pub fn run(patch: bool) -> anyhow::Result<()> {
    let current = env!("CARGO_PKG_VERSION");

    println!();
    println!("rok upgrade — guided version migration");
    println!("{}", "".repeat(50));
    println!("  current rok-cli:  v{current}");

    // Check for workspace rok-* dependency versions
    let versions = collect_rok_versions();
    if versions.is_empty() {
        println!("  No rok-* crates found in Cargo.toml.");
    } else {
        println!();
        println!("  Workspace rok-* dependencies:");
        for (name, ver) in &versions {
            println!("    {name:<35} {ver}");
        }
    }

    println!();
    if patch {
        println!("  --patch mode: updating rok-* crates to latest patch versions...");
        match std::process::Command::new("cargo")
            .args(["update", "--precise", current])
            .output()
        {
            Ok(o) if o.status.success() => println!("  ✓ Cargo.lock updated"),
            Ok(o) => {
                let err = String::from_utf8_lossy(&o.stderr);
                println!("  ✗ cargo update failed: {}", err.lines().next().unwrap_or("unknown"));
            }
            Err(e) => println!("  ✗ cargo not found: {e}"),
        }
    } else {
        println!("  Tips:");
        println!("    - Run `rok upgrade --patch` to auto-update Cargo.lock");
        println!("    - Run `rok self-update` to update rok-cli itself");
        println!("    - Review CHANGELOG.md for breaking changes before bumping major");
    }

    println!();
    Ok(())
}

fn collect_rok_versions() -> Vec<(String, String)> {
    let Ok(content) = std::fs::read_to_string("Cargo.toml") else { return Vec::new() };
    let mut results = Vec::new();
    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("rok-") || trimmed.contains("rok-") {
            if let Some(eq_pos) = trimmed.find('=') {
                let name = trimmed[..eq_pos].trim().trim_matches('"').to_string();
                let rest = &trimmed[eq_pos + 1..];
                if name.starts_with("rok-") {
                    let ver = extract_version(rest);
                    results.push((name, ver));
                }
            }
        }
    }
    results.dedup_by(|a, b| a.0 == b.0);
    results
}

fn extract_version(val: &str) -> String {
    let val = val.trim().trim_matches('"');
    if val.starts_with('{') {
        for part in val.split(',') {
            let p = part.trim().trim_matches('{').trim_matches('}').trim();
            if p.starts_with("version") {
                if let Some(v) = p.split('=').nth(1) {
                    return v.trim().trim_matches('"').to_string();
                }
            }
        }
        "workspace".into()
    } else {
        val.to_string()
    }
}