rngo-cli 0.22.0

Data simulation CLI using api.rngo.dev
use crate::ai::run_prompt;
use crate::model::LocalSystem;
use crate::sim::load::load_systems_from_project_directory;
use anyhow::Result;
use reqwest::StatusCode;
use std::fs;
use std::path::Path;
use std::process::Command;

pub async fn infer(
    agent: Option<crate::config::AiAgent>,
    verbose: bool,
) -> Result<()> {
    let _ = dotenvy::dotenv();

    let config = crate::config::get_config()?;
    let client = reqwest::Client::new();

    let response = client
        .get(format!(
            "{docs_url}/llm/skills/infer-effects.md",
            docs_url = config.docs_url
        ))
        .send()
        .await?;

    if response.status() != StatusCode::OK {
        anyhow::bail!("Failed to download latest prompt")
    }

    let prompt = response.text().await?;

    let systems = load_systems_from_project_directory()?;
    let mut system_prompts = vec![];

    for (key, system) in systems {
        let system: LocalSystem = serde_json::from_value(system)?;
        let context_parts = system
            .infer
            .and_then(|infer| infer.context)
            .map(|context| (context.description, context.command));

        if let Some(context_parts) = context_parts {
            let mut system_prompt = format!("### System {key}");

            if let (Some(description), _) = context_parts {
                system_prompt.push_str(&format!("\n\n{description}"));
            }

            if let (_, Some(command)) = context_parts {
                #[cfg(target_os = "windows")]
                let (shell, flag) = ("cmd", "/C");

                #[cfg(not(target_os = "windows"))]
                let (shell, flag) = ("sh", "-c");

                let output = Command::new(shell).arg(flag).arg(command).output()?;
                if output.status.success() {
                    let output = String::from_utf8_lossy(&output.stdout);
                    system_prompt.push_str(&format!("\n\n```\n{output}\n```"))
                }
            }

            system_prompts.push(system_prompt)
        }
    }

    let inference_instructions = if system_prompts.is_empty() {
        "No systems in this project provide context, so you should infer effect definitions from migrations files, schema defintions and data access code."
    } else {
        "The remainder of this section contains context about each system for this application. You should use it to infer effect definitions."
    };

    let system_prompts = system_prompts.join("\n\n");
    let content = format!("{prompt}\n{inference_instructions}\n\n{system_prompts}");

    match agent {
        None => {
            println!("{}", content);
        }
        Some(agent) => {
            run_prompt(agent, &content, verbose, "effect inference")?;
            summarize_effects()?;
        }
    }

    Ok(())
}

fn summarize_effects() -> Result<()> {
    let effects_dir = Path::new(".rngo/effects");

    if !effects_dir.exists() {
        println!("No effects directory found at .rngo/effects");
        return Ok(());
    }

    let entries = fs::read_dir(effects_dir)?;
    let mut effect_files: Vec<String> = Vec::new();

    for entry in entries.flatten() {
        let path = entry.path();

        if path.is_file() {
            let ext = path.extension().and_then(|s| s.to_str());

            if (ext == Some("yml") || ext == Some("yaml"))
                && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
            {
                effect_files.push(stem.to_string());
            }
        }
    }

    effect_files.sort();

    if effect_files.is_empty() {
        println!("No effect definition files found in .rngo/effects/");
    } else {
        println!("Success! Effect definitions created:");
        for effect in effect_files {
            println!("  - {}", effect);
        }
        println!("Learn how to further customize effects at https://rngo.dev/docs/concepts/effect");
    }

    Ok(())
}