mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Validation utilities for simulation runner
//!
//! Functions for validating Godot installation, symlinks, and project structure.

use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;

/// Validate Godot version is 4.3 or higher
pub fn validate_godot_version(godot_exe: &str) -> Result<()> {
    // Try to get Godot version
    let output = Command::new(godot_exe).arg("--version").output().map_err(|e| {
        // If Godot executable not found, suggest common paths
        let suggestions = suggest_godot_paths();
        anyhow::anyhow!(
            "Failed to execute Godot at '{}': {}\n\
                 \n\
                 Godot does not appear to be installed or is not in PATH.\n\
                 \n\
                 Common Godot installation paths:\n\
                 {}\n\
                 \n\
                 Installation help:\n\
                 - macOS: brew install godot\n\
                 - Linux: Download from https://godotengine.org/download\n\
                 - Windows: Download from https://godotengine.org/download\n\
                 \n\
                 Or set the GODOT_BIN environment variable:\n\
                 export GODOT_BIN=/path/to/godot",
            godot_exe,
            e,
            suggestions
        )
    })?;

    if !output.status.success() {
        anyhow::bail!(
            "Failed to get Godot version from '{}'\n\
             Exit code: {:?}",
            godot_exe,
            output.status.code()
        );
    }

    // Parse version from output
    let version_output = String::from_utf8_lossy(&output.stdout);
    let version_str = version_output.trim();

    // Expected format: "4.3.0.stable.official" or similar
    // Extract major.minor version
    let version_parts: Vec<&str> = version_str.split('.').collect();

    if version_parts.len() < 2 {
        anyhow::bail!(
            "Could not parse Godot version from output: '{}'\n\
             Expected format: X.Y.Z...",
            version_str
        );
    }

    let major: u32 = version_parts[0]
        .parse()
        .context(format!("Invalid major version number in '{}'", version_str))?;

    let minor: u32 = version_parts[1]
        .parse()
        .context(format!("Invalid minor version number in '{}'", version_str))?;

    // Check minimum version (4.3)
    const MIN_MAJOR: u32 = 4;
    const MIN_MINOR: u32 = 3;

    if major < MIN_MAJOR || (major == MIN_MAJOR && minor < MIN_MINOR) {
        anyhow::bail!(
            "Godot version {}.{} is not supported. Minimum required version is {}.{}.\n\
             \n\
             Current version: {}\n\
             \n\
             Please upgrade Godot:\n\
             - macOS: brew upgrade godot\n\
             - Linux/Windows: Download from https://godotengine.org/download",
            major,
            minor,
            MIN_MAJOR,
            MIN_MINOR,
            version_str
        );
    }

    println!("  ✓ Godot version: {}", version_str);
    println!("    (Minimum {}.{} required)", MIN_MAJOR, MIN_MINOR);

    Ok(())
}

/// Validate that a symlink exists and points to a valid target
pub fn validate_symlink(project_path: &Path, link_name: &str, expected_target: &str, description: &str) -> Result<()> {
    let symlink_path = project_path.join(link_name);

    // Check if symlink exists
    if !symlink_path.exists() {
        anyhow::bail!(
            "Required symlink missing: {}\n\
             Expected: {} -> {}\n\
             Purpose: {}\n\
             \n\
             Fix this by creating the symlink:\n\
             cd {} && ln -s {} {}",
            link_name,
            link_name,
            expected_target,
            description,
            project_path.display(),
            expected_target,
            link_name
        );
    }

    // Check if it's actually a symlink
    if !symlink_path.is_symlink() {
        // If it's a directory, that's okay too (might be a direct copy instead of symlink)
        if symlink_path.is_dir() {
            println!("{}: {} (directory)", description, symlink_path.display());
            return Ok(());
        }

        anyhow::bail!(
            "Expected symlink but found file: {}\n\
             Expected: {} -> {}",
            symlink_path.display(),
            link_name,
            expected_target
        );
    }

    // Check if symlink target exists
    match std::fs::read_link(&symlink_path) {
        Ok(target) => {
            // Resolve relative path
            let target_path = if target.is_absolute() {
                target.clone()
            } else {
                symlink_path.parent().unwrap().join(&target)
            };

            if !target_path.exists() {
                anyhow::bail!(
                    "Symlink is broken: {}\n\
                     Points to: {} (does not exist)\n\
                     Expected: {}\n\
                     \n\
                     Fix this by recreating the symlink:\n\
                     cd {} && rm {} && ln -s {} {}",
                    link_name,
                    target.display(),
                    expected_target,
                    project_path.display(),
                    link_name,
                    expected_target,
                    link_name
                );
            }

            // Verify it points to the expected target (allow some flexibility)
            let canonical_target = std::fs::canonicalize(&target_path).ok();
            let expected_full_path = project_path.join(expected_target);
            let canonical_expected = std::fs::canonicalize(&expected_full_path).ok();

            if let (Some(actual), Some(expected)) = (canonical_target, canonical_expected) {
                if actual != expected {
                    println!(
                        "  ⚠️  {}: {} -> {} (expected {})",
                        description,
                        link_name,
                        target.display(),
                        expected_target
                    );
                } else {
                    println!("{}: {} -> {}", description, link_name, target.display());
                }
            } else {
                println!("{}: {} -> {}", description, link_name, target.display());
            }

            Ok(())
        }
        Err(e) => {
            anyhow::bail!(
                "Failed to read symlink: {}\n\
                 Error: {}\n\
                 \n\
                 Try recreating the symlink:\n\
                 cd {} && rm {} && ln -s {} {}",
                symlink_path.display(),
                e,
                project_path.display(),
                link_name,
                expected_target,
                link_name
            );
        }
    }
}

/// Suggest common Godot installation paths based on the operating system
fn suggest_godot_paths() -> String {
    use std::path::Path;

    let mut suggestions = Vec::new();

    // Check common paths and mark which ones exist
    #[cfg(target_os = "macos")]
    {
        let paths = vec![
            "/Applications/Godot.app/Contents/MacOS/Godot",
            "/Applications/Godot_mono.app/Contents/MacOS/Godot",
            "/usr/local/bin/godot",
            "/opt/homebrew/bin/godot",
            "~/Applications/Godot.app/Contents/MacOS/Godot",
        ];

        for path in paths {
            let expanded_path = if path.starts_with("~/") {
                if let Ok(home) = std::env::var("HOME") {
                    path.replace("~", &home)
                } else {
                    path.to_string()
                }
            } else {
                path.to_string()
            };

            let exists = Path::new(&expanded_path).exists();
            let marker = if exists { "✓ FOUND" } else { "  " };
            suggestions.push(format!(" {} {}", marker, path));
        }
    }

    #[cfg(target_os = "linux")]
    {
        let paths = vec![
            "/usr/bin/godot",
            "/usr/local/bin/godot",
            "/snap/bin/godot",
            "~/.local/bin/godot",
            "/opt/godot/godot",
        ];

        for path in paths {
            let expanded_path = if path.starts_with("~/") {
                if let Ok(home) = std::env::var("HOME") {
                    path.replace("~", &home)
                } else {
                    path.to_string()
                }
            } else {
                path.to_string()
            };

            let exists = Path::new(&expanded_path).exists();
            let marker = if exists { "✓ FOUND" } else { "  " };
            suggestions.push(format!(" {} {}", marker, path));
        }
    }

    #[cfg(target_os = "windows")]
    {
        let paths = vec![
            "C:\\Program Files\\Godot\\Godot.exe",
            "C:\\Program Files (x86)\\Godot\\Godot.exe",
            "C:\\Godot\\Godot.exe",
            "%LOCALAPPDATA%\\Godot\\Godot.exe",
        ];

        for path in paths {
            let expanded_path = if path.contains("%LOCALAPPDATA%") {
                if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
                    path.replace("%LOCALAPPDATA%", &local_app_data)
                } else {
                    path.to_string()
                }
            } else {
                path.to_string()
            };

            let exists = Path::new(&expanded_path).exists();
            let marker = if exists { "✓ FOUND" } else { "  " };
            suggestions.push(format!(" {} {}", marker, path));
        }
    }

    if suggestions.is_empty() {
        "   (No common paths found for this platform)".to_string()
    } else {
        suggestions.join("\n ")
    }
}