oxidite-cli 2.2.2

CLI tool for the Oxidite web framework
Documentation
use std::fs;
use std::io::{self, BufRead, Write};
use std::path::Path;
use std::process::Command;
use colored::Colorize;

/// Run the interactive Oxidite console (tinker).
///
/// This works by generating a temporary Rust binary (`src/bin/_tinker.rs`)
/// that imports the project's crate, compiling and running it for each
/// snippet the developer enters.  Pressing Ctrl-D (or typing `exit`)
/// ends the session and cleans up the temporary file.
pub fn run_tinker() -> Result<(), Box<dyn std::error::Error>> {
    // Ensure we're inside a Cargo project
    if !Path::new("Cargo.toml").exists() {
        return Err("Not inside a Cargo project. Run this command from your project root.".into());
    }

    // Read the crate name from Cargo.toml
    let cargo_toml = fs::read_to_string("Cargo.toml")?;
    let crate_name = extract_crate_name(&cargo_toml)
        .ok_or("Could not determine crate name from Cargo.toml")?;
    let crate_ident = crate_name.replace('-', "_");

    // Ensure src/bin/ exists
    fs::create_dir_all("src/bin")?;

    let tinker_path = Path::new("src/bin/_tinker.rs");

    println!("๐Ÿงช Oxidite Tinker v2.2.1");
    println!("Type Rust expressions to evaluate them in your project's context.");
    println!("Type `help` or `?` for a guide, and `exit` or Ctrl-D to quit.\n");

    let stdin = io::stdin();
    let mut lines = stdin.lock().lines();

    loop {
        print!("oxidite> ");
        io::stdout().flush()?;

        let line = match lines.next() {
            Some(Ok(l)) => l,
            _ => break, // EOF / Ctrl-D
        };

        let trimmed = line.trim();

        if trimmed.is_empty() {
            continue;
        }
        if trimmed == "exit" || trimmed == "quit" {
            break;
        }
        if trimmed == "help" || trimmed == "?" || trimmed == "--help" {
            println!("\n๐Ÿ“– Oxidite Tinker Help Guide:");
            println!("-----------------------------");
            println!("Oxidite Tinker evaluates Rust expressions inside your project's context.");
            println!("Every line you type is compiled with all of your project's models and");
            println!("dependencies pre-imported.\n");
            println!("Commands:");
            println!("  help, ?         Show this guide");
            println!("  clear           Clear the terminal screen");
            println!("  exit, quit      Exit the tinker session\n");
            println!("Examples:");
            println!("  1 + 2");
            println!("  Config::load()");
            println!("  Project::all(&db).await\n");
            continue;
        }
        if trimmed == "clear" {
            print!("\x1B[2J\x1B[1H");
            io::stdout().flush()?;
            continue;
        }

        // Build a small main() that imports the project and runs the snippet
        let code = format!(
            r#"// Auto-generated by `oxidite tinker`. Do not commit.
use {crate_ident}::*;

#[tokio::main]
async fn main() {{
    let __result = {{ {snippet} }};
    println!("{{:?}}", __result);
}}
"#,
            crate_ident = crate_ident,
            snippet = trimmed,
        );

        fs::write(tinker_path, &code)?;

        // Compile and run
        print!("{}", "  โš™๏ธ  Evaluating...".green());
        io::stdout().flush()?;

        let output = Command::new("cargo")
            .args(["run", "--bin", "_tinker", "--quiet"])
            .output();

        // Clear the "Evaluating..." loader line
        print!("\r\x1B[K");
        io::stdout().flush()?;

        match output {
            Ok(out) => {
                if !out.status.success() {
                    println!("{}", "  โŒ Evaluation failed:".red().bold());
                    let stderr = String::from_utf8_lossy(&out.stderr);
                    for line in stderr.lines() {
                        if line.trim_start().starts_with("Compiling")
                            || line.trim_start().starts_with("Finished")
                            || line.trim_start().starts_with("Running")
                        {
                            continue;
                        }
                        eprintln!("{}", line.red());
                    }
                } else {
                    if !out.stdout.is_empty() {
                        io::stdout().write_all(&out.stdout)?;
                    }
                }
            }
            Err(e) => {
                eprintln!("{}", format!("Failed to run snippet: {}", e).red());
            }
        }
    }

    // Cleanup
    if tinker_path.exists() {
        let _ = fs::remove_file(tinker_path);
    }

    println!("\n๐Ÿ‘‹ Goodbye!");
    Ok(())
}

fn extract_crate_name(toml_content: &str) -> Option<String> {
    // Simple parser: find `name = "..."` in [package]
    let mut in_package = false;
    for line in toml_content.lines() {
        let trimmed = line.trim();
        if trimmed == "[package]" {
            in_package = true;
            continue;
        }
        if trimmed.starts_with('[') {
            in_package = false;
            continue;
        }
        if in_package {
            if let Some(rest) = trimmed.strip_prefix("name") {
                let rest = rest.trim_start();
                if let Some(rest) = rest.strip_prefix('=') {
                    let rest = rest.trim();
                    let rest = rest.trim_matches('"');
                    return Some(rest.to_string());
                }
            }
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::extract_crate_name;

    #[test]
    fn extracts_name_from_cargo_toml() {
        let toml = r#"
[package]
name = "my-cool-app"
version = "0.1.0"
"#;
        assert_eq!(extract_crate_name(toml), Some("my-cool-app".to_string()));
    }

    #[test]
    fn returns_none_for_missing_name() {
        assert_eq!(extract_crate_name("[dependencies]"), None);
    }
}