harn-cli 0.8.79

CLI for the Harn programming language — run, test, REPL, format, and lint
use std::fs;
use std::path::{Path, PathBuf};

use crate::cli::{ProvidersBuildCapabilitiesArgs, ProvidersBuildConfigArgs};

const PROVIDER_CONFIG_GENERATED_HEADER: &str = "\
# @generated by `harn providers build-config`; do not edit directly.
# Edit crates/harn-vm/src/llm/catalog_sources/**/*.toml instead.
";
const PROVIDER_CAPABILITIES_GENERATED_HEADER: &str = "\
# @generated by `harn providers build-capabilities`; do not edit directly.
# Edit crates/harn-vm/src/llm/capability_sources/**/*.toml instead.
";

pub(crate) fn run_build_config(args: &ProvidersBuildConfigArgs) -> Result<(), String> {
    let generated = generated_provider_config(&args.source_dir)?;

    if args.check {
        match fs::read_to_string(&args.output) {
            Ok(existing) if existing == generated.body => {
                println!("provider config snapshot is up to date");
                return Ok(());
            }
            Ok(_) | Err(_) => {
                eprintln!(
                    "error: generated provider config is stale or missing: {}",
                    args.output.display()
                );
                return Err("provider config snapshot drifted".to_string());
            }
        }
    }

    if let Some(parent) = args.output.parent() {
        fs::create_dir_all(parent).map_err(|error| {
            format!(
                "failed to create provider config directory {}: {error}",
                parent.display()
            )
        })?;
    }
    fs::write(&args.output, generated.body)
        .map_err(|error| format!("failed to write {}: {error}", args.output.display()))?;
    println!(
        "wrote {} from {} TOML fragments",
        args.output.display(),
        generated.fragment_count
    );
    Ok(())
}

pub(crate) fn run_build_capabilities(args: &ProvidersBuildCapabilitiesArgs) -> Result<(), String> {
    let generated = generated_provider_capabilities(&args.source_dir)?;

    if args.check {
        match fs::read_to_string(&args.output) {
            Ok(existing) if existing == generated.body => {
                println!("provider capability snapshot is up to date");
                return Ok(());
            }
            Ok(_) | Err(_) => {
                eprintln!(
                    "error: generated provider capability snapshot is stale or missing: {}",
                    args.output.display()
                );
                return Err("provider capability snapshot drifted".to_string());
            }
        }
    }

    if let Some(parent) = args.output.parent() {
        fs::create_dir_all(parent).map_err(|error| {
            format!(
                "failed to create provider capability directory {}: {error}",
                parent.display()
            )
        })?;
    }
    fs::write(&args.output, generated.body)
        .map_err(|error| format!("failed to write {}: {error}", args.output.display()))?;
    println!(
        "wrote {} from {} TOML fragments",
        args.output.display(),
        generated.fragment_count
    );
    Ok(())
}

struct GeneratedProviderConfig {
    body: String,
    fragment_count: usize,
}

fn generated_provider_config(source_dir: &Path) -> Result<GeneratedProviderConfig, String> {
    let mut fragments = Vec::new();
    collect_toml_fragments(source_dir, &mut fragments)?;
    fragments.sort();
    if fragments.is_empty() {
        return Err(format!(
            "provider catalog source directory has no TOML fragments: {}",
            source_dir.display()
        ));
    }

    let mut body = String::from(PROVIDER_CONFIG_GENERATED_HEADER);
    for path in &fragments {
        let fragment = fs::read_to_string(path)
            .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
        body.push_str("\n# --- source: ");
        body.push_str(&fragment_label(source_dir, path));
        body.push_str(" ---\n");
        body.push_str(fragment.trim_end());
        body.push('\n');
    }

    harn_vm::llm_config::parse_config_toml(&body)
        .map_err(|error| format!("generated provider config does not parse: {error}"))?;

    Ok(GeneratedProviderConfig {
        body,
        fragment_count: fragments.len(),
    })
}

fn generated_provider_capabilities(source_dir: &Path) -> Result<GeneratedProviderConfig, String> {
    let mut fragments = Vec::new();
    collect_toml_fragments(source_dir, &mut fragments)?;
    fragments.sort();
    if fragments.is_empty() {
        return Err(format!(
            "provider capability source directory has no TOML fragments: {}",
            source_dir.display()
        ));
    }

    let mut body = String::from(PROVIDER_CAPABILITIES_GENERATED_HEADER);
    for path in &fragments {
        let fragment = fs::read_to_string(path)
            .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
        body.push_str("\n# --- source: ");
        body.push_str(&fragment_label(source_dir, path));
        body.push_str(" ---\n");
        body.push_str(fragment.trim_end());
        body.push('\n');
    }

    toml::from_str::<harn_vm::llm::capabilities::CapabilitiesFile>(&body)
        .map_err(|error| format!("generated provider capabilities do not parse: {error}"))?;

    Ok(GeneratedProviderConfig {
        body,
        fragment_count: fragments.len(),
    })
}

fn collect_toml_fragments(dir: &Path, fragments: &mut Vec<PathBuf>) -> Result<(), String> {
    let mut entries = Vec::new();
    let read_dir = fs::read_dir(dir).map_err(|error| {
        format!(
            "failed to read provider catalog source dir {}: {error}",
            dir.display()
        )
    })?;
    for entry in read_dir {
        let entry = entry.map_err(|error| {
            format!(
                "failed to read provider catalog source dir entry in {}: {error}",
                dir.display()
            )
        })?;
        entries.push(entry.path());
    }
    entries.sort();

    for path in entries {
        if path.is_dir() {
            collect_toml_fragments(&path, fragments)?;
        } else if path.extension().and_then(|extension| extension.to_str()) == Some("toml") {
            fragments.push(path);
        }
    }
    Ok(())
}

fn fragment_label(source_dir: &Path, path: &Path) -> String {
    path.strip_prefix(source_dir)
        .unwrap_or(path)
        .to_string_lossy()
        .replace('\\', "/")
}