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, bail};
use crate::core::registry::RecipeRegistry;
use crate::core::detection::workspace::detect_workspaces;
use crate::core::ast::parser::parse_file;
use crate::core::ast::semantic::SemanticModel;
use walkdir::WalkDir;

pub fn execute(graph_type: &str, format: &str, path: &Path) -> Result<()> {
    match graph_type {
        "pipeline" => generate_pipeline_graph(format),
        "workspace" => generate_workspace_graph(format, path),
        "deps" => generate_file_deps_graph(format, path),
        _ => bail!("Unknown graph type: {}", graph_type),
    }
}

fn generate_pipeline_graph(format: &str) -> Result<()> {
    let registry = RecipeRegistry::new();
    let recipes = registry.all();

    if format == "json" {
        let mut nodes = Vec::new();
        for recipe in recipes {
            let metadata = recipe.metadata();
            nodes.push(serde_json::json!({
                "name": metadata.name,
                "requires": metadata.required_recipes,
                "incompatible": metadata.incompatible_recipes,
                "should_run_before": metadata.should_run_before,
                "should_run_after": metadata.should_run_after,
            }));
        }
        println!("{}", serde_json::to_string_pretty(&nodes)?);
    } else {
        println!("flowchart TD");
        for recipe in recipes {
            let metadata = recipe.metadata();
            println!("    {}", metadata.name);
            for req in metadata.required_recipes {
                println!("    {} --> {}", req, metadata.name);
            }
            for inc in metadata.incompatible_recipes {
                println!("    {} -.->|incompatible| {}", metadata.name, inc);
            }
            for before in metadata.should_run_before {
                println!("    {} -.->|before| {}", metadata.name, before);
            }
            for after in metadata.should_run_after {
                println!("    {} -.->|after| {}", after, metadata.name);
            }
        }
    }
    Ok(())
}

fn generate_workspace_graph(format: &str, path: &Path) -> Result<()> {
    let workspace = detect_workspaces(path);
    
    if format == "json" {
        println!("{}", serde_json::to_string_pretty(&workspace)?);
    } else {
        println!("flowchart LR");
        println!("    subgraph Workspace");
        for pkg in &workspace.packages {
            println!("        {}", pkg.name);
        }
        println!("    end");
    }
    Ok(())
}

fn generate_file_deps_graph(format: &str, path: &Path) -> Result<()> {
    let mut edges: Vec<(String, String)> = Vec::new();
    
    for entry in WalkDir::new(path)
        .max_depth(3)
        .into_iter()
        .filter_map(|e| e.ok())
    {
        let file_path = entry.path();
        if file_path.is_file() {
            let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
            if matches!(ext, "js" | "jsx" | "ts" | "tsx") {
                if let Ok(parsed) = parse_file(file_path) {
                    let model = SemanticModel::new(&parsed.module);
                    let source_file = file_path.file_name().unwrap().to_string_lossy().to_string();
                    for import_source in model.imports.keys() {
                        edges.push((source_file.clone(), import_source.clone()));
                    }
                }
            }
        }
    }

    if format == "json" {
        println!("{}", serde_json::to_string_pretty(&edges)?);
    } else {
        println!("graph TD");
        for (src, target) in edges {
            // Clean names for Mermaid
            let safe_src = src.replace('.', "_").replace('-', "_");
            let safe_target = target.replace('.', "_").replace('-', "_").replace('/', "_").replace('@', "_");
            println!("    {} --> \"{}\"", safe_src, safe_target);
        }
    }
    Ok(())
}