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 tiny_http::{Server, Response, Header};
use std::fs;
use serde_json::Value;

pub fn execute(port: u16, project_root: &Path) -> Result<()> {
    let addr = format!("127.0.0.1:{}", port);
    let server = Server::http(&addr).map_err(|e| anyhow::anyhow!("Failed to start server: {}", e))?;

    println!("Dashboard backend running at http://{}", addr);
    println!("Press Ctrl+C to stop.");

    for request in server.incoming_requests() {
        let response = match (request.method(), request.url()) {
            (&tiny_http::Method::Get, "/api/sessions") => {
                handle_list_dir(project_root.join(".morph-cli/sessions"))
            }
            (&tiny_http::Method::Get, "/api/reports") => {
                handle_list_dir(project_root.join(".morph-cli/reports"))
            }
            (&tiny_http::Method::Get, "/api/validation") => {
                handle_list_dir(project_root.join(".morph-cli/validation"))
            }
            (&tiny_http::Method::Get, url) if url.starts_with("/api/sessions/") => {
                let id = &url[14..];
                handle_read_file(project_root.join(".morph-cli/sessions").join(format!("{}.json", id)))
            }
            (&tiny_http::Method::Get, url) if url.starts_with("/api/reports/") => {
                let id = &url[13..];
                // Reports might have .json or .md, but API asks for JSON endpoints
                handle_read_file(project_root.join(".morph-cli/reports").join(format!("{}.json", id)))
            }
            _ => Response::from_string("Not Found").with_status_code(404),
        };

        let response = response.with_header(Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap())
                               .with_header(Header::from_bytes(&b"Access-Control-Allow-Origin"[..], &b"*"[..]).unwrap());
        
        let _ = request.respond(response);
    }

    Ok(())
}

fn handle_list_dir(path: std::path::PathBuf) -> Response<std::io::Cursor<Vec<u8>>> {
    if !path.exists() {
        return Response::from_string("[]").with_status_code(200);
    }

    let mut entries = Vec::new();
    if let Ok(dir) = fs::read_dir(path) {
        for entry in dir.filter_map(|e| e.ok()) {
            if entry.path().extension().and_then(|s| s.to_str()) == Some("json") {
                if let Ok(content) = fs::read_to_string(entry.path()) {
                    if let Ok(json) = serde_json::from_str::<Value>(&content) {
                        entries.push(json);
                    }
                }
            }
        }
    }

    let json = serde_json::to_string(&entries).unwrap_or_else(|_| "[]".to_string());
    Response::from_string(json)
}

fn handle_read_file(path: std::path::PathBuf) -> Response<std::io::Cursor<Vec<u8>>> {
    if !path.exists() {
        return Response::from_string("Not Found").with_status_code(404);
    }

    match fs::read_to_string(path) {
        Ok(content) => Response::from_string(content),
        Err(_) => Response::from_string("Error reading file").with_status_code(500),
    }
}