morph-cli 0.1.0

AST-based codebase migration and codemod tool for JavaScript and TypeScript projects.
Documentation
use std::path::Path;
use anyhow::Result;
use crate::core::detection::scanner::Scanner;
use crate::core::detection::ModuleSystem;
use crate::utils::terminal;

pub fn execute(path: &Path, format: &str) -> Result<()> {
    let mut scanner = Scanner::new(path.to_path_buf());
    let result = scanner.scan();

    let score_data = calculate_score(&result);

    if format == "json" {
        println!("{}", serde_json::to_string_pretty(&score_data)?);
    } else {
        print_score_report(&score_data);
    }

    Ok(())
}

#[derive(serde::Serialize)]
struct ScoreData {
    overall_score: u8,
    readiness: String,
    metrics: ScoreMetrics,
    suggestions: Vec<String>,
}

#[derive(serde::Serialize)]
struct ScoreMetrics {
    typescript_adoption: String,
    module_system: String,
    risky_patterns: usize,
    deprecated_syntax: usize,
    migration_opportunities: usize,
}

fn calculate_score(result: &crate::core::detection::scanner::ScanResult) -> ScoreData {
    let mut score: i16 = 70; // Start at 70 (Average)

    let mut ts_adoption = "None";
    let has_ts = result.detection.frameworks.iter().any(|f| f.name == "TypeScript");
    
    if has_ts {
        score += 20;
        ts_adoption = "Full";
    }

    let module_system = match result.detection.module_system {
        ModuleSystem::ESM => {
            score += 10;
            "ESM"
        }
        ModuleSystem::CommonJS => {
            score -= 20;
            "CommonJS"
        }
        ModuleSystem::Mixed => {
            score -= 10;
            "Mixed"
        }
    };

    let risky_penalty = (result.detection.risky_areas.len() as i16 * 5).min(20);
    score -= risky_penalty;

    let opp_penalty = (result.detection.migration_opportunities.len() as i16 * 5).min(20);
    score -= opp_penalty;

    let final_score = score.clamp(0, 100) as u8;

    let readiness = if final_score > 80 {
        "Modern"
    } else if final_score > 50 {
        "Modernizing"
    } else if final_score > 20 {
        "Transitioning"
    } else {
        "Legacy"
    };

    let mut suggestions = Vec::new();
    for opp in &result.detection.migration_opportunities {
        for recipe in &opp.recipes {
            if !suggestions.contains(recipe) {
                suggestions.push(recipe.clone());
            }
        }
    }

    ScoreData {
        overall_score: final_score,
        readiness: readiness.to_string(),
        metrics: ScoreMetrics {
            typescript_adoption: ts_adoption.to_string(),
            module_system: module_system.to_string(),
            risky_patterns: result.detection.risky_areas.len(),
            deprecated_syntax: result.detection.migration_opportunities.len(), // Use opportunities as a proxy for now
            migration_opportunities: result.detection.migration_opportunities.len(),
        },
        suggestions,
    }
}

fn print_score_report(data: &ScoreData) {
    println!();
    println!("{}", terminal::label("".repeat(50).as_str()));
    println!("  Modernization Score: {}", terminal::label(&format!("{} / 100", data.overall_score)));
    println!("  Readiness Level:     {}", terminal::label(&data.readiness));
    println!("{}", terminal::label("".repeat(50).as_str()));
    println!();
    
    println!("  Metrics:");
    println!("    TypeScript:   {}", data.metrics.typescript_adoption);
    println!("    Module System: {}", data.metrics.module_system);
    println!("    Risky Areas:   {}", data.metrics.risky_patterns);
    println!("    Legacy Syntax: {}", data.metrics.deprecated_syntax);
    println!("    Opportunities: {}", data.metrics.migration_opportunities);
    println!();

    if !data.suggestions.is_empty() {
        println!("  Suggested Recipes:");
        for recipe in &data.suggestions {
            println!("    - {}", recipe);
        }
    } else {
        println!("  Project is up to date!");
    }
    println!();
}